Skip to content

Lowdefy

Developer Productivity

A config‑first web framework that lets AI generate concise, reviewable application definitions while humans maintain and extend them.

JavaScript Latest v5.3.0 · 23d ago Security brief →

Features

  • Config‑first architecture: define apps with YAML instead of thousands of code lines
  • Schema‑validated properties prevent arbitrary code paths and injection vulnerabilities
  • Built on Next.js & Auth.js for production‑ready deployment anywhere
  • 70+ UI blocks, 50+ logic operators, and 10+ data connectors out of the box
  • Extensible via npm plugins with tree‑shaken bundles

Recent releases

View all 9 releases →
v5.3.0 New feature
⚠ Upgrade required
  • `@lowdefy/blocks-markdown` now declares `antd` (>=6) as a peer dependency; ensure it is installed in consuming projects.
  • PageHeaderMenu gains a default `borderBottom` for visual consistency—override via `styles.header` if needed.
Notable features
  • AI agents with `@lowdefy/ai-utils` runtime, multi‑provider connections (`anthropic`, `openai`, `google`, `ai-gateway`) and `AgentChat` composite block for streaming chat UI.
  • `blocks-aggrid` button cell renderer enabling per‑row action buttons with full antd Button configuration.
  • Ag‑Grid upgraded to v32.3.9, tag cells support arrays, auto‑colour by default, and suppressed cell focus/overflow clipping.
Full changelog

Highlights

  • AI agents are now first-class in Lowdefy. New @lowdefy/ai-utils runtime, a config-driven AgentChat composite block built on Ant Design X, and four new provider connections (@lowdefy/connection-anthropic, @lowdefy/connection-openai, @lowdefy/connection-google, @lowdefy/connection-ai-gateway) let you wire a streaming, tool-using AI agent into any page from YAML. Agents support multi-turn tool calling, server-side hooks (instructions, onStart, onStepStart, onToolCallStart, onToolCallFinish, onStepFinish, onFinish) callable as Lowdefy endpoints, sub-agents exposed as tools, file attachments with S3 upload, reasoning/thinking display, source citation streaming, and context compaction via pruneMessages. Provider-agnostic by design through the Vercel AI SDK.

  • AgentChat block: real-time streaming chat UI. Sequential message part rendering with configurable reasoning display, tool approval UI for endpoint and MCP tools marked confirm: true, file attachments (configurable accept types and max size, S3 upload integration), drawer display mode with a FloatButton trigger, source citation rendering, Mermaid diagrams, LaTeX, and syntax-highlighted code blocks (toggled via renderMermaid and codeHighlighter). Includes copy, feedback, regenerate, and delete message actions, plus suggestions and Sender.Header / Sender.Switch affordances. The new AgentConversations block provides a standalone conversations sidebar for independent placement.

  • MCP integration: agents can use external tools via Model Context Protocol. New @lowdefy/connection-mcp connection type supports HTTP, SSE, and stdio transports. Agents can reference MCP connections via connectionId or inline config with build-time validation. Runtime MCP client creation does automatic tool discovery, merging, and cleanup, and tool approval support via confirm: true works for both endpoint tools and MCP sources.

  • sharedState two-way binding: agents can read and write page state. The built-in update-page-state tool lets an agent write to the page state of the AgentChat block, so the agent can drive UI changes, fill forms, or read context the user has set elsewhere on the page.

  • File system agent tools. Sandboxed listFiles, readFile, searchFiles, statFile, and resolvePath tools give agents scoped access to agent-specific file directories. copyAgentFileSystems emits an agentFileSystems.json manifest so the production server includes each agent's fileSystem.basePath in Next.js file tracing, so agents that read files work on Vercel and standalone (output: 'standalone') deployments without manual next.config.js configuration.

  • blocks-aggrid: new cell.type: buttons renderer. Render a list of action buttons in a column with each button firing its own block-level event with the row data on the payload. Per-button properties mirror the antd Button block (title, icon, type, variant, color, size, shape, danger, ghost, hideTitle, disabled) plus row-data-path variants (titleField, iconField, disabledField, hiddenField) for per-row state. Use this for inline Edit/Delete/Approve actions without _if dispatching.

  • blocks-aggrid: ag-grid upgraded to v32.3.9. Pulls in two majors of upstream fixes. The column header UX (hamburger column menu with filter popup) is preserved by default; opt into the new ag-grid v32 column menu via columnMenu: 'new' on the block. Cell focus is suppressed by default (suppressCellFocus: true) so the keyboard focus outline doesn't visually compete with built-in cell renderers; override with suppressCellFocus: false. Cell overflow is clipped so flex-rendered content stays inside its column.

  • blocks-aggrid: tag cells render one tag per item for array-valued fields. The cell.type: tag renderer now accepts an array of strings in addition to a single string. Each item is rendered as its own styled tag and resolves its colour through the existing colorMap / colorFrom / default configuration. Empty arrays and arrays containing only null/empty entries render the em-dash placeholder. Single-string values are unchanged.

  • blocks-aggrid: tag cells auto-colour by default. When a cell.type: tag column is used with no colorMap, colorFrom, or default, tag values are coloured from a stable hash so the same value always gets the same colour across rows, columns, and tables. The palette uses 12 antd named hues (red, volcano, orange, gold, yellow, lime, green, cyan, blue, geekblue, purple, magenta) and respects the active theme. Opt out with cell: { type: tag, default: default }.

  • blocks-antd: PageHeaderMenu now has a default borderBottom, matching the existing default borders on PageSiderMenu and PageSidebarLayout, for visual consistency across the page menu blocks. The default can still be overridden via styles.header.

  • blocks-markdown: antd declared as a peer dependency. MarkdownWithCode imports antd for theme-aware syntax highlighting but the package did not declare it in peerDependencies, causing module resolution failures when bundling apps that include @lowdefy/blocks-markdown without already pulling in antd. Now declares antd (>=6) as a peer dependency.

Changes

feat: Add AI agent support with multi-provider chat and tool use

Packages: @lowdefy/api, @lowdefy/build, @lowdefy/blocks-antd-x, @lowdefy/connection-ai-gateway, @lowdefy/connection-anthropic, @lowdefy/connection-google, @lowdefy/connection-mcp, @lowdefy/connection-openai, @lowdefy/server, @lowdefy/server-dev, @lowdefy/ai-utils

Agent Runtime (@lowdefy/ai-utils)

  • handleAgentChat orchestrates the full agent lifecycle: tool merging, MCP client lifecycle, hook callbacks, and stream composition
  • ToolLoopAgent handles multi-turn tool calling, streaming responses, and artifact cleaning
  • createAgentUIStreamResponse converts agent output to a streaming HTTP response for the client
  • buildAgentTools merges endpoint tools, MCP tools, and sub-agent tools into AI SDK tool objects
  • buildPrepareStep enables dynamic tool phasing per step
  • buildUpdatePageStateTool provides a built-in tool for the agent to write to page state via the AgentChat block
  • File system agent tools: listFiles, readFile, searchFiles, statFile, resolvePath for sandboxed access to agent-scoped file directories
  • pruneMessages for context compaction
  • experimental_repairToolCall integration
  • Sub-agent support — agents can be exposed as tools to other agents
  • Reserved tool name collision detection (e.g. update-page-state)
  • Server-side hooks (instructions, onStart, onStepStart, onToolCallStart, onToolCallFinish, onStepFinish, onFinish) callable as Lowdefy endpoints
  • Provider-agnostic design using the Vercel AI SDK — supports reasoning/thinking display, providerOptions passthrough, and source citation streaming via sendSources
  • Strip data: URL prefix from file attachments before AI SDK processing

AgentChat Block (@lowdefy/blocks-antd-x)

  • New AgentChat composite block built on Ant Design X with real-time streaming display
  • Sequential message part rendering with configurable reasoning/thinking display
  • Tool approval UI for endpoint and MCP tools marked confirm: true
  • File attachment support (configurable accept types and max size) with S3 upload integration
  • Drawer display mode with a FloatButton trigger for embedding chat on any page
  • Source citation rendering for source-url and source-document parts
  • Mermaid diagrams, LaTeX, and syntax-highlighted code blocks (with copy + language label) — toggled via renderMermaid and codeHighlighter
  • Copy, feedback, regenerate, and delete message actions
  • Suggestions and Sender.Header / Sender.Switch UI affordances
  • Configurable roles, avatars, and names per message role
  • Event bridging for agent lifecycle events (onSuccess, onError, onFinish, onFeedback)
  • sharedState two-way binding lets the agent read and write page state via the update-page-state tool

AgentConversations Block (@lowdefy/blocks-antd-x)

  • New standalone conversations sidebar block, extracted from AgentChat for independent placement

Connection Plugins

  • @lowdefy/connection-anthropic: Anthropic connection with AnthropicAgent resolver supporting Claude models
  • @lowdefy/connection-openai: OpenAI connection with OpenAIAgent resolver supporting GPT models
  • @lowdefy/connection-google: Google AI connection with GeminiAgent resolver, including thinkingConfig and safetySettings sugar props
  • @lowdefy/connection-ai-gateway: Vercel AI Gateway connection with AIGatewayAgent resolver for routing to multiple providers through a single endpoint

MCP Integration (@lowdefy/connection-mcp, @lowdefy/ai-utils, @lowdefy/build)

  • New Mcp connection type for HTTP, SSE, and stdio transport config
  • Agents can reference MCP connections via connectionId or inline config with build-time validation
  • Runtime MCP client creation with automatic tool discovery, merging, and cleanup
  • Tool approval support via confirm: true on both endpoint tools and MCP sources

Build Pipeline (@lowdefy/build)

  • buildAgents validates agent config (model, tools, sub-agents, MCP) and normalizes tool definitions
  • writeAgents writes agent artifacts for server consumption
  • Sub-agent circular reference detection
  • Tool object format with confirm support
  • MCP connectionId normalization (inline config vs reference)
  • Lazy module variable resolution for agent properties referenced from modules
  • Agent schema validation integrated into the build pipeline
  • copyAgentFileSystems emits an agentFileSystems.json manifest so the production server can include each agent's fileSystem.basePath directory in Next.js file tracing — agents that read files now work on Vercel and standalone (output: 'standalone') deployments without manual next.config.js configuration

API (@lowdefy/api)

  • Agent route handler (callAgent) for streaming agent responses
  • Endpoint tool execution context with operator evaluation
  • Sub-agent resolver methods for agents-as-tools
  • MCP connectionId resolution at request time
  • getAgentConfig and getAgentResolver helpers for runtime agent resolution

Servers (@lowdefy/server, @lowdefy/server-dev)

  • Agent API route (/api/agent/[...path]) added to both production and development servers
  • urlQuery validation
  • 10 MB request body limit for file attachments
  • Server-side hooks for agent lifecycle callbacks (instructions, onFinish)

feat(blocks-aggrid): Buttons cell renderer and ag-grid v32 update.

Packages: @lowdefy/blocks-aggrid

New cell.type: buttons renderer — render a list of action buttons in a column with each button firing its own block-level event with the row data on the payload. Per-button properties mirror the antd Button block (title, icon, type, variant, color, size, shape, danger, ghost, hideTitle, disabled) plus row-data-path variants (titleField, iconField, disabledField, hiddenField) for per-row state. Use this for inline Edit/Delete/Approve actions without _if dispatching.

ag-grid updated to v32.3.9 — pulls in two majors of upstream fixes. The column header UX (hamburger column menu with filter popup) is preserved by default; opt into the new ag-grid v32 column menu via columnMenu: 'new' on the block.

Cell focus suppressed by defaultsuppressCellFocus now defaults to true so the keyboard focus outline doesn't visually compete with built-in cell renderers (tags, buttons, links). Override with suppressCellFocus: false if needed. Cell overflow is also clipped so flex-rendered content stays inside its column.

feat(blocks-aggrid): Tag cell renders one tag per item for array-valued fields.

Packages: @lowdefy/blocks-aggrid

The cell.type: tag renderer now accepts an array of strings in addition to a single string. Each item is rendered as its own styled tag and resolves its colour through the existing colorMap / colorFrom / default configuration. Empty arrays and arrays containing only null/empty entries render the em-dash placeholder, matching the existing null-value behaviour. Single-string values are unchanged.

feat(blocks-aggrid): Auto-colour tag cells by default for consistent per-value colouring.

Packages: @lowdefy/blocks-aggrid

When a cell.type: tag column is used with no colorMap, no colorFrom, and no default, tag values are now coloured from a stable hash so the same value always gets the same colour across rows, columns, and tables. The palette uses 12 antd named hues (red, volcano, orange, gold, yellow, lime, green, cyan, blue, geekblue, purple, magenta) and respects the active theme.

The grey fallback is still available — set cell: { type: tag, default: default } on any column to opt out. When colorMap, colorFrom, or default is set, behaviour is unchanged.

Fixes & Improvements

  • fix(blocks-antd): Add default header border to PageHeaderMenu. (@lowdefy/blocks-antd)

    PageHeaderMenu now has a default borderBottom matching the existing default borders on PageSiderMenu and PageSidebarLayout, for visual consistency across the page menu blocks. The default can still be overridden via styles.header.

  • fix(blocks-markdown): Declare antd as a peer dependency. (@lowdefy/blocks-markdown)

    MarkdownWithCode imports antd for theme-aware syntax highlighting but the package did not declare antd in its peerDependencies, causing module resolution failures (e.g., when bundling apps that include @lowdefy/blocks-markdown without already pulling in antd). Added antd (>=6) as a peer dependency to match the version range used by @lowdefy/blocks-antd.

v5.2.0 Breaking risk

First‑class reusable modules, new rich‑text editor blocks (Tiptap), improved S3 upload handling and metadata encoding, holdValue request flag for stale data prevention, and breaking SQLite/MySQL driver upgrades.

Full changelog

Highlights

  • Modules: a first-class system for reusable config packages. Apps can now declare modules in lowdefy.yaml from GitHub repos (github:owner/repo[/path]@ref) or local paths (file:./path), with namespaced page/connection/endpoint IDs, validated vars, declared exports, plugin compatibility checks, secret allowlists, connection remapping, and cross-module dependencies — replacing the copy-paste-between-projects pattern with a declarative dependency. New _module.var / _module.pageId / _module.connectionId / _module.endpointId / _module.id operators, _ref: { module, component, vars } for reusing config fragments, picomatch glob patterns in auth page rules, and server route support for slashed page IDs (e.g. /team-users/users-list).

  • Rich-text editing out of the box: new @lowdefy/blocks-tiptap package. Two new default blocks — TiptapInput (bold/italic/strike, multi-color highlight, headings, lists, tables, links, bubble menu) and TiptapMentionInput (adds @-mention dropdown from static options or a request). Both emit { html, text, markdown, fileList, mentions? } and expose clear / setContent / focus methods. Configurable per-extension via properties.starterKit / image / table / link / highlight / mentions. Image drag/drop and paste are supported by wiring properties.s3PostPolicyRequestId to an S3 presigned POST request. Uses the open-source @tiptap/extension-file-handler — no TIPTAP_PRO_TOKEN or scoped-registry config needed.

  • Server-to-server API calls via CallApi / InternalApi. API endpoint routines can now call other endpoints in-process (no HTTP) with isolated steps / payload namespaces and a recursion cap of 10. New InternalApi endpoint type is blocked from HTTP access; client CallAPI actions targeting InternalApi get a build warning (error in production). CallApi routine steps are validated at build time (require properties.endpointId, reject connectionId).

  • _js operator now accepts pre-computed args. Use the { fn, args } form and resolve values with any Lowdefy operator (_state, _request, _user, nested _js) before the JS runs — values are injected as the args object inside the function body. Keeps JS focused on computation while operator lookups stay in YAML. The string form still works, and identical fn bodies still share a compiled function at build time.

  • holdValue flag on Request and CallAPI actions. UI bound to _request: <id> or _api: <endpointId> retains the previous response while a refetch is loading (and on error), instead of flashing to null. The error is still observable via _request_details / _api.

  • ControlledList gains onAdd / onRemove events. Both fire after the list mutation with { index, item }onRemove captures the row value before deletion, so handlers can reference deleted data (e.g. _event: item._id to delete from a backend). The remove icon now defaults to var(--ant-color-error) at var(--ant-font-size-lg) with hover/active states, and list-type blocks now receive a value prop with the current list data.

  • S3 upload blocks: new onBeforeUpload event and antd v6 fixes. S3UploadButton, S3UploadDragger, and S3UploadPhoto fire onBeforeUpload before the upload starts — throw to cancel (validation, confirmation prompts, etc.). S3UploadDragger properties.height works again on antd v6 and accepts CSS lengths ("50vh", "300px"); all three blocks expose antd v6 semantic slots (trigger, list, item) through Lowdefy's classNames / styles API and are wrapped in withTheme('Upload', …) for per-instance Upload token overrides. S3Download adds showRemoveIcon and an onRemove event. Migration: styles previously targeting style: { .element: { … } } on the inner drag div should move to style: { .trigger: { … } }.

  • AwsS3PresignedPostPolicy auto URL-encodes x-amz-meta-* fields. S3 user-metadata values containing names, URLs, or non-ASCII characters no longer need to be wrapped with _uri.encode per request — the connection encodes them before signing. Note: readers of the metadata (e.g. Lambda S3-event triggers) should decodeURIComponent x-amz-meta-* values before use.

  • SQL drivers consolidated onto actively-maintained packages. @lowdefy/connection-knex upgraded knex 2.5.1 → 3.2.9, replaced sqlite3 with better-sqlite3 12.9.0, replaced mysql with mysql2 3.22.3, and replaced mssql with the tedious driver knex actually loads. Migration: apps using client: sqlite3 must switch to client: better-sqlite3 (or the client: sqlite alias); apps using client: mysql must switch to client: mysql2 — both throw a ConfigError with a migration message. client: mssql is unchanged. better-sqlite3 is allowlisted in pnpm.onlyBuiltDependencies so its native binding builds correctly under pnpm 10.

  • Build validation: schema errors are now warnings, with focused per-step validations. AJV schema mismatches no longer block the build — they emit warnings, and dedicated validations in validateBlock, buildConnections, buildEvents, etc. produce errors with full context (page, block, event names) instead of generic schema messages. New focused validation for connections and menu items.

  • Client rendering fixes. Container, InputContainer, and List no longer create empty content[slotKey] functions — blocks using the content.X && content.X() pattern correctly render nothing (no wrapping Area) when a slot is empty. Link variants (backLink, newOriginLink, sameOriginLink, noLink) now forward the style prop, fixing inline color: 'inherit' on header notification/profile/dark-mode rows, the disabled-anchor color, and per-link style: config in menu items.

  • Logger crash fix on literal null lines. JSON.parse can return null for "null" input — non-object parsed values are now treated as plain text instead of crashing the CLI log handler.

Changes

  • @lowdefy/api: ### Minor Changes

  • 73fa2b9: feat: Internal API endpoint calls

    Endpoint-to-Endpoint Calls (@lowdefy/api)

    • API endpoint routines can call other endpoints server-side via CallApi steps, without HTTP
    • Each called endpoint runs in an isolated context with its own steps and payload namespaces
    • Recursive endpoint call depth is capped at 10 to prevent infinite loops
    • InternalApi endpoints are blocked from HTTP access — they return the same response as a missing endpoint

    Build Support (@lowdefy/build)

    • CallApi routine steps validated at build time: require properties.endpointId, reject connectionId
    • InternalApi endpoint type accepted alongside Api
    • Client-side CallAPI actions targeting InternalApi endpoints produce a build warning (error in production)

    Operator Parser (@lowdefy/operators)

    • ServerParser.parse() accepts steps and payload per call for routine context isolation

Patch Changes

  • @lowdefy/build: ### Minor Changes

  • 73fa2b9: feat: Internal API endpoint calls

    Endpoint-to-Endpoint Calls (@lowdefy/api)

    • API endpoint routines can call other endpoints server-side via CallApi steps, without HTTP
    • Each called endpoint runs in an isolated context with its own steps and payload namespaces
    • Recursive endpoint call depth is capped at 10 to prevent infinite loops
    • InternalApi endpoints are blocked from HTTP access — they return the same response as a missing endpoint

    Build Support (@lowdefy/build)

    • CallApi routine steps validated at build time: require properties.endpointId, reject connectionId
    • InternalApi endpoint type accepted alongside Api
    • Client-side CallAPI actions targeting InternalApi endpoints produce a build warning (error in production)

    Operator Parser (@lowdefy/operators)

    • ServerParser.parse() accepts steps and payload per call for routine context isolation
  • 69a59c0: feat(_js): Pass pre-computed values into _js via an args object.

    The _js operator now accepts an object form { fn, args } alongside the existing string form. Values in args are resolved by the parser — using any Lowdefy operator (_state, _request, _user, nested _js, etc.) — before the JavaScript function runs, and are injected as the args object inside the function body.

    _js:
      fn: |
        const { products, target } = args;
        return products
          .filter((p) => p.category === target)
          .reduce((a, p) => a + p.price, 0);
      args:
        products:
          _request: get_products.data.products
        target: smartphones
    

    This lets you precompute or normalize values in YAML and keep the JavaScript body focused on computation, rather than mixing operator lookups into the function. The string form continues to work unchanged, and identical fn bodies still share a single compiled function at build time — only args varies per call.

  • 0f38c9f: feat: First-class module system for reusable config packages

    Modules are reusable bundles of Lowdefy config — pages, connections, API endpoints, menus, and exposed components — hosted in GitHub repositories or local directories. Apps install modules in lowdefy.yaml and configure them through vars, replacing the copy-paste-between-projects pattern with a declarative dependency.

    Module entries (@lowdefy/build)

    • Apps declare entries in the modules array of lowdefy.yaml with id, source, and optional vars, connections, and dependencies.
    • The entry id namespaces the module's content and forms the URL prefix for its pages (e.g. /team-users/users-list).
    • Multi-instance: the same module source can be installed multiple times under different entry IDs, each with its own vars and namespace.
    • GitHub sources (github:owner/repo[/path]@ref) are fetched as tarballs and locally cached. Private repos use GITHUB_TOKEN, the gh CLI, or git credential helpers.
    • Local sources (file:./relative/path) resolve relative to the project root.

    Module manifest (module.lowdefy.yaml)

    • Declares the module's interface: name, description, vars, connections, pages, api, components, menus, dependencies, exports, plugins, and secrets.
    • vars declarations validate consumer values with type, required, default, and description. Consumer values override manifest defaults; omitted values fall back to the declared default.
    • exports declares the module's public interface — the IDs other modules and apps may reference. The build validates cross-module references against exports.
    • plugins declarations are validated against the app's installed plugins with semver compatibility checks.
    • secrets is an allowlist of secrets the module may access; undeclared _secret references fail the build. Remapped connections skip the module's secret references for that connection.

    Module operators

    • _module.var — read manifest-validated vars, including consumer overrides and declared defaults.
    • _module.pageId, _module.connectionId, _module.endpointId — produce scoped IDs from a module-author's unscoped ID.
    • _module.id — the entry ID of the current module.

    Auto-scoped IDs

    Page, connection, API endpoint, and menu item IDs are auto-prefixed with the entry ID. Block and request IDs inherit page scope and are not rewritten.

    Consuming module resources

    • Pages and APIs are auto-included and auto-scoped — they appear in the app under the entry-ID prefix.
    • Components are reusable config fragments included with _ref: { module, component, vars }. They can export any config — UI blocks, enum maps, config templates, schema fragments — and accept vars at the call site.
    • Menus are included with _ref: { module, menu }, typically wrapped in a MenuGroup.

    Connection remapping

    Apps can redirect a module connection to an existing app connection via the entry's connections map. The module's connection definition and its declared secrets are skipped — the app connection handles them.

    Cross-module dependencies

    Modules can reference each other's pages, components, menus, connections, and APIs via abstract dependencies declared in module.lowdefy.yaml.

    • Auto-wiring: when a module entry's id matches a declared dependency name, the build wires it automatically.
    • Explicit wiring: the entry's dependencies map overrides auto-wiring and supports multi-instance topologies where each instance points at a different partner.
    • The build validates every wiring, detects dependency cycles, and reports unmapped or undeclared dependencies with remediation hints.

    Auth page rules

    Picomatch glob patterns in auth page rules (e.g. team-users/*) for wildcard module page matching.

    Slashed page IDs (@lowdefy/server, @lowdefy/server-dev)

    Server routes support module page IDs containing / (e.g. /team-users/users-list).

Patch Changes

  • 762755c: feat(blocks-tiptap): Add new default block package with TiptapInput and TiptapMentionInput rich-text editors.

    @lowdefy/blocks-tiptap ships two rich-text editor blocks built on TipTap:

    • TiptapInput — standard rich-text editor with bold/italic/strike-through, multi-color highlight, headings, lists, tables, links, and a bubble menu.
    • TiptapMentionInput — everything TiptapInput does, plus an @-mention dropdown populated from a static options list or a Lowdefy request. Resolved mentions are returned on the block value as mentions: [...].

    Both blocks emit an object value shaped { html, text, markdown, fileList, mentions? } and register clear, setContent, and focus methods.

    Configurable extensions — defaults preserve the bundled editor; override any of these to trim the editor down or tune it:

    • properties.starterKit — object forwarded to TipTap StarterKit, e.g. { heading: false, codeBlock: false }.
    • properties.image{ enabled, maxWidth, zoom }
    • properties.table{ enabled, resizable }
    • properties.link{ enabled, autolink, linkOnPaste, openOnClick, defaultProtocol }
    • properties.highlight{ enabled, multicolor }
    • properties.mentions.char / properties.mentions.allowSpaces — change the trigger char (e.g. # for hashtags) or disable spaces inside a mention query (TiptapMentionInput only).

    Image drag/drop and paste are supported by pointing properties.s3PostPolicyRequestId at a request that returns an S3 presigned POST policy (e.g. AwsS3PresignedPostPolicy). The file handler is optional — omit the request id to disable uploads entirely.

    The blocks are registered in the default types map and are available out of the box on @lowdefy/server-dev. No private-registry tokens are required: the blocks use the open-source @tiptap/extension-file-handler instead of @tiptap-pro/extension-file-handler, so projects that migrated from a custom TipTap plugin can drop their TIPTAP_PRO_TOKEN environment variable and .npmrc scoped-registry config.

  • 72b6159: fix(build): Replace schema validation errors with warnings and add focused validations.

    AJV schema validation now emits warnings instead of blocking the build. Focused validations in each build step (validateBlock, buildConnections, buildEvents, etc.) provide better error messages with full context — page, block, and event names — instead of generic schema messages. Added focused validation for connections and menu items that previously relied on schema checks alone.

  • @lowdefy/client: ### Patch Changes

  • 01e249b: feat(blocks-antd): ControlledList now fires onAdd / onRemove events and defaults the remove icon to the antd error color at a standard size.

    Events. Both events fire after the list mutation completes. The event payload is { index, item }:

    • onAddindex is where the new row was inserted (0 for addToFront: true, else list.length). item is the newly added value (typically undefined for an empty row).
    • onRemoveindex is the removed row's position. item is the row value captured before removal, so handlers can reference the deleted data (e.g., _event: item._id to delete from a backend).
    - id: tags
      type: ControlledList
      events:
        onRemove:
          - id: notify
            type: DisplayMessage
            params:
              content:
                _string.concat: ['Removed at index ', { _event: index }]
      blocks:
        - id: tags.$.label
          type: TextInput
    

    Remove icon styling. The remove icon now defaults to var(--ant-color-error) at var(--ant-font-size-lg), with --ant-color-error-hover / --ant-color-error-active on hover/press — no more hardcoded hex colors, and the size no longer swings with properties.size. Override via class.removeIcon / style.removeIcon (both slots target the icon wrapper). Existing configs that hardcoded color: '#ff4d4f' on removeItemIcon can drop it — the default is already danger.

    @lowdefy/client also now passes the list's current state value to list-type block components via a value prop, so any list block can read its own array data.

  • a4ecee5: fix(client): Skip rendering content slots that have no blocks.

    Container, InputContainer, and List no longer create a content[slotKey] function when the slot's blocks array is empty. Blocks that use the content.X && content.X() pattern (for optional header, footer, extra, etc.) now correctly render nothing — including no wrapping Area element — when the user leaves the slot empty.

  • 6ec0dd4: fix(client): Forward style prop to all Link variants.

    createLinkComponent previously destructured every prop except style, so any <Link style={...}> passed by a block was silently dropped. Inline style overrides only worked via className + CSS. All four link variants (backLink, newOriginLink, sameOriginLink — both newTab and same-origin branches — and noLink) now thread style through to the rendered <a> (or <span> for noLink).

    Surfaces fixes in three places that were already passing style and silently broken: headerActions.js notifications/profile/dark-mode rows had color: 'inherit' that didn't reach the <a> (label rendered as antd link blue); Anchor.js disabled state set color: '#BEBEBE' that never applied; buildMenuItems.js per-link style: config was discarded.

  • @lowdefy/engine: ### Minor Changes

  • 1d18a13: feat(actions): holdValue flag on Request and CallAPI actions.

    Request and CallAPI actions now accept a holdValue: true flag that retains the previous response value while a new call is loading. UI bound to _request: <id> or _api: <endpointId> keeps showing the previous response instead of flashing to null during a refetch. The previous response is also retained if the new call errors — the error is still observable via _request_details / _api.

    - id: refresh_table
      type: Request
      params:
        requestId: my_table_request
        holdValue: true
    

    The Request action's object-form params now also support { requestId, holdValue } and { requestIds, holdValue } shapes alongside the existing { all } shape.

Patch Changes

  • d105b81: fix(engine): Preserve input values across visible toggles when set via SetState.

    When an input was inside a hidden container and a SetState action set its value (e.g. on onInit before the container becomes visible), the value was silently reset to the input's enforceType default (typically null/"") on the next SetState. The next SetState triggered RootSlots.reset(), which found the field missing from context.state (because Slots.updateState correctly deletes invisible blocks' state fields) and overwrote this.value with the type default.

    Block.reset() now skips the type-default fallback when the block was hidden in the previous eval cycle and has in-memory state to preserve — this.value for inputs, or existing subSlots for lists (which would otherwise be truncated by the rebuild loop reading an empty enforceType('array', null)). The next updateState republishes the value to context.state if the block becomes visible, or leaves the field absent if it stays hidden.

    This brings SetState-driven visibility toggles into parity with setValue-driven toggles, which already preserved the value via Block.evaluate. Lists with nested inputs also retain per-item values across hide/reveal cycles.

    Behaviour change: apps that relied on a hidden input being silently reset to its default by an unrelated SetState will now see the previously-set value preserved. To explicitly clear a value, use SetState({ myInput: null }). Invisible blocks continue to have no representation in context.state, the user-facing Reset action still produces enforceType defaults, and List sub-slot rebuilding on SetState({ list: [...] }) is unchanged.

  • @lowdefy/operators: ### Patch Changes

  • 73fa2b9: feat: Internal API endpoint calls

    Endpoint-to-Endpoint Calls (@lowdefy/api)

    • API endpoint routines can call other endpoints server-side via CallApi steps, without HTTP
    • Each called endpoint runs in an isolated context with its own steps and payload namespaces
    • Recursive endpoint call depth is capped at 10 to prevent infinite loops
    • InternalApi endpoints are blocked from HTTP access — they return the same response as a missing endpoint

    Build Support (@lowdefy/build)

    • CallApi routine steps validated at build time: require properties.endpointId, reject connectionId
    • InternalApi endpoint type accepted alongside Api
    • Client-side CallAPI actions targeting InternalApi endpoints produce a build warning (error in production)

    Operator Parser (@lowdefy/operators)

    • ServerParser.parse() accepts steps and payload per call for routine context isolation
  • 1e964c4: fix(operators): Preserve source location on build operator results.

    Build operator results (e.g. from _build.array.concat) now retain the source file and line number of the expression that produced them. Previously, operator evaluation replaced the expression object with a fresh result, losing the source location markers. This caused build errors inside operator-produced arrays (such as null blocks) to show the file path but no line number.

  • @lowdefy/actions-core: ### Minor Changes

  • 1d18a13: feat(actions): holdValue flag on Request and CallAPI actions.

    Request and CallAPI actions now accept a holdValue: true flag that retains the previous response value while a new call is loading. UI bound to _request: <id> or _api: <endpointId> keeps showing the previous response instead of flashing to null during a refetch. The previous response is also retained if the new call errors — the error is still observable via _request_details / _api.

    - id: refresh_table
      type: Request
      params:
        requestId: my_table_request
        holdValue: true
    

    The Request action's object-form params now also support { requestId, holdValue } and { requestIds, holdValue } shapes alongside the existing { all } shape.

Patch Changes

  • @lowdefy/[email protected]

  • @lowdefy/[email protected]

  • @lowdefy/blocks-aggrid: ### Patch Changes

  • 186a57d: fix(blocks-aggrid): Suppress cell focus by default and clip overflowing cell content.

    The ag-grid cell focus outline visually competed with built-in cell renderers (buttons, links, tags), so suppressCellFocus now defaults to true and can still be overridden per grid. The antd cell wrapper also clips overflowing flex children so long text and inline cell components no longer blow out the cell width.

  • @lowdefy/blocks-antd: ### Minor Changes

  • 01e249b: feat(blocks-antd): ControlledList now fires onAdd / onRemove events and defaults the remove icon to the antd error color at a standard size.

    Events. Both events fire after the list mutation completes. The event payload is { index, item }:

    • onAddindex is where the new row was inserted (0 for addToFront: true, else list.length). item is the newly added value (typically undefined for an empty row).
    • onRemoveindex is the removed row's position. item is the row value captured before removal, so handlers can reference the deleted data (e.g., _event: item._id to delete from a backend).
    - id: tags
      type: ControlledList
      events:
        onRemove:
          - id: notify
            type: DisplayMessage
            params:
              content:
                _string.concat: ['Removed at index ', { _event: index }]
      blocks:
        - id: tags.$.label
          type: TextInput
    

    Remove icon styling. The remove icon now defaults to var(--ant-color-error) at var(--ant-font-size-lg), with --ant-color-error-hover / --ant-color-error-active on hover/press — no more hardcoded hex colors, and the size no longer swings with properties.size. Override via class.removeIcon / style.removeIcon (both slots target the icon wrapper). Existing configs that hardcoded color: '#ff4d4f' on removeItemIcon can drop it — the default is already danger.

    @lowdefy/client also now passes the list's current state value to list-type block components via a value prop, so any list block can read its own array data.

Patch Changes

  • 6ec2cd9: fix(PageSidebarLayout): Pin the sider to the viewport so the bottom actions stay visible.

    The sider is now position: sticky with height: 100vh, so the menu, notifications, profile avatar, dark-mode toggle, and logo remain on screen as the page content scrolls. The sticky footer container fades from transparent to the container background so content passing behind it doesn't cut off abruptly.

  • fd1604f: feat(blocks-antd): DropdownButton now supports the standard Lowdefy event-shortcut schema (events.<eventName>.shortcut) for item shortcuts, alongside the existing item-level shortcut property.

    When a shortcut is configured via the event, the framework-level shortcut manager binds and fires it — consistent with Button. The shortcut badge renders next to the item label in both cases. If both are set on the same item, the event-level shortcut wins. The split-button's main action now also renders a badge when events.onClick.shortcut is configured.

    Event-level (preferred, matches Button):

    - id: actions
      type: DropdownButton
      properties:
        items:
          - title: Undo
            eventName: onUndo
      events:
        onUndo:
          shortcut: mod+z
          try:
            - id: undo
              type: ...
    

    Item-level (still supported):

    - id: actions
      type: DropdownButton
      properties:
        items:
          - title: Undo
            eventName: onUndo
            shortcut: mod+z
      events:
        onUndo:
          - id: undo
            type: ...
    
  • cea34ac: fix(PageSidebarLayout): Render notifications, profile, and dark-mode toggle as labeled, left-aligned rows when the sider is expanded.

    When the sider is open, the bottom actions now render as [icon] [label] rows that match the visual style of the menu items above (e.g. "Notifications", "Profile", "Light mode"). When the sider is collapsed, the actions remain as a centered icon stack. Two new optional schema fields — notifications.title (default Notifications) and profile.title (default Profile) — let consumers customise the expanded labels; consumers can also bind _user: name to profile.title to show the authenticated user's name.

    The profile dropdown's default trigger now depends on whether the sider is expanded: click when expanded (the labeled row invites click), hover when collapsed (original small-avatar behavior). Consumers passing profile.trigger explicitly are unaffected.

    No change to PageSiderMenu or PageHeaderMenu — their header-bar rendering still uses the icon-only layout and hover trigger.

  • @lowdefy/blocks-loaders: ### Patch Changes

  • c91003d: refactor(blocks-loaders): Modernize skeleton shimmer animation.

    The skeleton placeholder now uses a compositor-accelerated transform: translateX shimmer on a ::after pseudo-element, replacing the older left-animated ::before gradient. The shimmer is softer (three-stop gradient over secondary/quaternary fills), the base element uses isolation: isolate to scope its stacking context, and prefers-reduced-motion disables the animation for accessibility.

    SkeletonInput and SkeletonParagraph also switch to a flex-column layout with gap spacing: tighter rhythm between rows, distinct border radii for the label vs input, and a slightly wider trailing line (60%) in paragraphs for a more readable look. Purely visual — no API changes.

  • @lowdefy/blocks-tiptap: ### Minor Changes

  • 762755c: feat(blocks-tiptap): Add new default block package with TiptapInput and TiptapMentionInput rich-text editors.

    @lowdefy/blocks-tiptap ships two rich-text editor blocks built on TipTap:

    • TiptapInput — standard rich-text editor with bold/italic/strike-through, multi-color highlight, headings, lists, tables, links, and a bubble menu.
    • TiptapMentionInput — everything TiptapInput does, plus an @-mention dropdown populated from a static options list or a Lowdefy request. Resolved mentions are returned on the block value as mentions: [...].

    Both blocks emit an object value shaped { html, text, markdown, fileList, mentions? } and register clear, setContent, and focus methods.

    Configurable extensions — defaults preserve the bundled editor; override any of these to trim the editor down or tune it:

    • properties.starterKit — object forwarded to TipTap StarterKit, e.g. { heading: false, codeBlock: false }.
    • properties.image{ enabled, maxWidth, zoom }
    • properties.table{ enabled, resizable }
    • properties.link{ enabled, autolink, linkOnPaste, openOnClick, defaultProtocol }
    • properties.highlight{ enabled, multicolor }
    • properties.mentions.char / properties.mentions.allowSpaces — change the trigger char (e.g. # for hashtags) or disable spaces inside a mention query (TiptapMentionInput only).

    Image drag/drop and paste are supported by pointing properties.s3PostPolicyRequestId at a request that returns an S3 presigned POST policy (e.g. AwsS3PresignedPostPolicy). The file handler is optional — omit the request id to disable uploads entirely.

    The blocks are registered in the default types map and are available out of the box on @lowdefy/server-dev. No private-registry tokens are required: the blocks use the open-source @tiptap/extension-file-handler instead of @tiptap-pro/extension-file-handler, so projects that migrated from a custom TipTap plugin can drop their TIPTAP_PRO_TOKEN environment variable and .npmrc scoped-registry config.

Patch Changes

  • @lowdefy/connection-knex: ### Minor Changes

  • 596fddc: chore(connection-knex): update knex and SQL drivers; replace sqlite3 with better-sqlite3; replace mysql with mysql2.

    Bumped knex and its dialect drivers, and consolidated onto the actively-maintained drivers — replaced sqlite3 with better-sqlite3 and mysql with mysql2. Subsumes the prior [email protected] darwin-arm64 fix.

    @lowdefy/connection-knex dependency changes:

    • knex 2.5.13.2.9. Knex 3.x drops Node < 16; Lowdefy already requires Node 18+. The knex(config), .raw(), and dynamic query-builder API surface used by KnexRaw / KnexBuilder is unchanged.
    • pg 8.11.38.20.0.
    • Removed mssql. Knex's mssql dialect actually requires tedious (not the mssql package), and Lowdefy never imported mssql directly — it was only ever a vehicle for pulling tedious into the install tree. client: mssql in user YAML is unchanged: the knex client name stays the same, only the underlying npm package shipped with connection-knex changes.
    • Added tedious 19.2.1 as the SQL Server driver — the package knex actually loads when client: mssql is used.
    • Removed sqlite3. The driver is in maintenance-only mode upstream (the v6 release marked the repo unmaintained).
    • Added better-sqlite3 12.9.0 as the SQLite driver. Selectable as client: better-sqlite3 (or client: sqlite, which is now an alias of better-sqlite3 — see runtime client handling below).
    • Removed mysql. Unmaintained upstream since 2020.
    • Added mysql2 3.22.3 as the MySQL / MariaDB driver. Selectable as client: mysql2 in connection YAML.

    Runtime client handling (in createKnex):

    • client: sqlite is silently remapped to client: better-sqlite3. sqlite was historically a knex-level alias of sqlite3; this preserves the YAML alias while the underlying driver changes.
    • client: sqlite3 now throws a ConfigError with a migration message: Knex connection "client: sqlite3" is no longer supported. Use "client: better-sqlite3" or "client: sqlite" instead. Existing apps using client: sqlite3 need to update their connection YAML.
    • client: mysql now throws a ConfigError with a migration message: Knex connection "client: mysql" is no longer supported. Use "client: mysql2" instead. Existing apps using client: mysql need to update their connection YAML. mysql is not silently remapped because knex treats mysql and mysql2 as separate dialects with subtly different SQL formatters, not aliases — the migration is a deliberate user choice.

    pnpm.onlyBuiltDependencies allowlist for better-sqlite3:

    better-sqlite3 runs a native-binding install script (prebuild-install with a node-gyp rebuild fallback). pnpm 10 silently suppresses postinstall scripts for unapproved packages, which leaves the binding unbuilt and crashes KnexRaw / KnexBuilder at runtime.

    • Added better-sqlite3 to the allowlist on @lowdefy/server, @lowdefy/server-dev, and @lowdefy/server-e2e. These are the install roots in the CLI fetch flow under .lowdefy/{dev,build}/, where pnpm honors the per-package pnpm.onlyBuiltDependencies field.
    • Also added the same allowlist to the monorepo root package.json. The per-package field is ignored at workspace-root install (pnpm 10 only honors it on the install root), so contributors running pnpm install at the repo root would otherwise have to pnpm rebuild better-sqlite3 manually.

Patch Changes

  • @lowdefy/[email protected]

  • @lowdefy/[email protected]

  • @lowdefy/operators-js: ### Minor Changes

  • 69a59c0: feat(_js): Pass pre-computed values into _js via an args object.

    The _js operator now accepts an object form { fn, args } alongside the existing string form. Values in args are resolved by the parser — using any Lowdefy operator (_state, _request, _user, nested _js, etc.) — before the JavaScript function runs, and are injected as the args object inside the function body.

    _js:
      fn: |
        const { products, target } = args;
        return products
          .filter((p) => p.category === target)
          .reduce((a, p) => a + p.price, 0);
      args:
        products:
          _request: get_products.data.products
        target: smartphones
    

    This lets you precompute or normalize values in YAML and keep the JavaScript body focused on computation, rather than mixing operator lookups into the function. The string form continues to work unchanged, and identical fn bodies still share a single compiled function at build time — only args varies per call.

  • 0d44433: feat(_string): Add _string.format for template-style string interpolation.

    _string.format substitutes placeholders in a template string with values, accepting either a positional array form or a named object form. null/undefined values render as empty strings, which often makes _if_none unnecessary.

    # Positional placeholders
    _string.format:
      - 'Updates ({0})'
      - _request: get_counts.0.update
    
    # Named placeholders
    _string.format:
      template: 'Updates ({count}) since {date}'
      on:
        count:
          _request: get_counts.0.update
        date:
          _date.format:
            - YYYY-MM-DD
            - _state: lastSync
    

    Use {{ / }} to include literal braces. Prefer _string.format over _string.concat for label-style interpolation, and use _nunjucks when you need conditionals, loops, or filters.

Patch Changes

  • 1d18a13: feat(actions): holdValue flag on Request and CallAPI actions.

    Request and CallAPI actions now accept a holdValue: true flag that retains the previous response value while a new call is loading. UI bound to _request: <id> or _api: <endpointId> keeps showing the previous response instead of flashing to null during a refetch. The previous response is also retained if the new call errors — the error is still observable via _request_details / _api.

    - id: refresh_table
      type: Request
      params:
        requestId: my_table_request
        holdValue: true
    

    The Request action's object-form params now also support { requestId, holdValue } and { requestIds, holdValue } shapes alongside the existing { all } shape.

  • @lowdefy/plugin-aws: ### Minor Changes

  • 235e219: feat(plugin-aws): Add onBeforeUpload event to S3 upload blocks.

    S3UploadButton, S3UploadDragger, and S3UploadPhoto now fire an onBeforeUpload event before the upload starts. If any action throws, the upload is cancelled. This allows validation, confirmation prompts, or other logic to run before files are sent to S3.

Patch Changes

  • a52db1c: feat(plugin-aws): Auto URL-encode x-amz-meta-* fields on AwsS3PresignedPostPolicy.

    AwsS3PresignedPostPolicy now URL-encodes any field whose name starts with x-amz-meta- (case-insensitive) before signing the policy. S3 user metadata must be ASCII, so values containing names, URLs, or other non-ASCII characters previously had to be wrapped with _uri.encode in every request config.

    With this change, upload policies can pass values through directly:

    fields:
      x-amz-meta-uploaded-by-name:
        _user: profile.name
      x-amz-meta-uploaded-by-url:
        _payload: url
    

    Other protocol fields (acl, Content-Type, success_action_redirect, etc.) continue to pass through literally. Readers of the metadata (e.g., Lambda triggers that ingest S3 events) should URL-decode x-amz-meta-* values via decodeURIComponent before use.

  • 825f86c: fix(plugin-aws): S3 upload blocks — working height on S3UploadDragger in antd v6, antd semantic-slot passthrough across all three upload blocks, cleaner classNames/styles wiring.

    After the antd v6 upgrade the Upload wrapper changed from a <div> to an inline <span>, which caused the antd Dragger height prop to resize only the inner drag box — the wrapping element and the clickable ant-upload-btn (which uses height: 100%) collapsed, so the drag area looked unchanged.

    This release:

    • Makes properties.height actually resize the full drag surface. Height is applied to the block's outer wrapper, and the antd Upload wrapper is forced to display: block; height: 100% so nested height: 100% rules resolve correctly.
    • Defaults height to the antd controlHeight theme token (matches the default button height and follows the compact algorithm).
    • Accepts height as a number or a string so CSS lengths like "50vh" / "300px" are supported.
    • Exposes the antd v6 Upload semantic slots (trigger, list, item) through Lowdefy's classNames / styles API, so file-list rows and the drop trigger can be styled from YAML (style: { .trigger: { … }, .list: { … }, .item: { … } }).
    • Moves classNames.element / styles.element onto the block's outer wrapper (matching the convention used in blocks-antd/Search), merges marker classes via cn(), and adds a lf-s3-upload-dragger-hint marker to the hint node for future scoped CSS.
    • Fixes precedence: style.element.height now overrides properties.height again, matching the original schema docs.

    S3UploadButton and S3UploadPhoto receive the same treatment: classNames.element / styles.element move to an outer block wrapper carrying a marker class (lf-s3-upload-button, lf-s3-upload-photo), and the antd v6 semantic slots trigger / list / item are piped through as Lowdefy slots. S3UploadPhoto also keeps .icon and .title slots for styling the content of the upload trigger card. The avatar-uploader hardcoded class on S3UploadPhoto (which had no CSS attached) was removed in favour of the marker-class pattern.

    S3UploadDragger, S3UploadButton, and S3UploadPhoto are now wrapped in withTheme('Upload', …) (previously only S3Download was). Each block's meta.js exposes a theme property whose object is forwarded into a scoped <ConfigProvider theme={{ components: { Upload: theme } }}>, giving per-instance overrides of antd Upload design tokens (actionsColor, pictureCardSize, controlItemBgHover, colorIcon, fontSize, borderRadiusSM) — the tokens that semantic classNames / styles slots cannot reach. The shared schema lives at packages/plugins/plugins/plugin-aws/src/schemas/uploadTheme.js and is imported by all four upload/download block metas.

    S3Download gains a showRemoveIcon property (default false) and an onRemove event. When the remove icon is clicked, the block fires onRemove with the clicked file and returns false to antd — the controlled fileList stays authoritative, and the action handler decides whether to update state (e.g. via SetState).

    Migration note: Apps that relied on style: { .element: { … } } styling the inner .ant-upload-drag / .ant-upload-select div (e.g. custom hover interactions coupled to that selector) should move those declarations to style: { .trigger: { … } }. All visual styling cases in the gallery (background, border, shadow, padding) render identically on the outer wrapper, so typical apps need no changes.

  • @lowdefy/server: ### Minor Changes

  • 0f38c9f: feat: First-class module system for reusable config packages

    Modules are reusable bundles of Lowdefy config — pages, connections, API endpoints, menus, and exposed components — hosted in GitHub repositories or local directories. Apps install modules in lowdefy.yaml and configure them through vars, replacing the copy-paste-between-projects pattern with a declarative dependency.

    Module entries (@lowdefy/build)

    • Apps declare entries in the modules array of lowdefy.yaml with id, source, and optional vars, connections, and dependencies.
    • The entry id namespaces the module's content and forms the URL prefix for its pages (e.g. /team-users/users-list).
    • Multi-instance: the same module source can be installed multiple times under different entry IDs, each with its own vars and namespace.
    • GitHub sources (github:owner/repo[/path]@ref) are fetched as tarballs and locally cached. Private repos use GITHUB_TOKEN, the gh CLI, or git credential helpers.
    • Local sources (file:./relative/path) resolve relative to the project root.

    Module manifest (module.lowdefy.yaml)

    • Declares the module's interface: name, description, vars, connections, pages, api, components, menus, dependencies, exports, plugins, and secrets.
    • vars declarations validate consumer values with type, required, default, and description. Consumer values override manifest defaults; omitted values fall back to the declared default.
    • exports declares the module's public interface — the IDs other modules and apps may reference. The build validates cross-module references against exports.
    • plugins declarations are validated against the app's installed plugins with semver compatibility checks.
    • secrets is an allowlist of secrets the module may access; undeclared _secret references fail the build. Remapped connections skip the module's secret references for that connection.

    Module operators

    • _module.var — read manifest-validated vars, including consumer overrides and declared defaults.
    • _module.pageId, _module.connectionId, _module.endpointId — produce scoped IDs from a module-author's unscoped ID.
    • _module.id — the entry ID of the current module.

    Auto-scoped IDs

    Page, connection, API endpoint, and menu item IDs are auto-prefixed with the entry ID. Block and request IDs inherit page scope and are not rewritten.

    Consuming module resources

    • Pages and APIs are auto-included and auto-scoped — they appear in the app under the entry-ID prefix.
    • Components are reusable config fragments included with _ref: { module, component, vars }. They can export any config — UI blocks, enum maps, config templates, schema fragments — and accept vars at the call site.
    • Menus are included with _ref: { module, menu }, typically wrapped in a MenuGroup.

    Connection remapping

    Apps can redirect a module connection to an existing app connection via the entry's connections map. The module's connection definition and its declared secrets are skipped — the app connection handles them.

    Cross-module dependencies

    Modules can reference each other's pages, components, menus, connections, and APIs via abstract dependencies declared in module.lowdefy.yaml.

    • Auto-wiring: when a module entry's id matches a declared dependency name, the build wires it automatically.
    • Explicit wiring: the entry's dependencies map overrides auto-wiring and supports multi-instance topologies where each instance points at a different partner.
    • The build validates every wiring, detects dependency cycles, and reports unmapped or undeclared dependencies with remediation hints.

    Auth page rules

    Picomatch glob patterns in auth page rules (e.g. team-users/*) for wildcard module page matching.

    Slashed page IDs (@lowdefy/server, @lowdefy/server-dev)

    Server routes support module page IDs containing / (e.g. /team-users/users-list).

Patch Changes

  • 596fddc: chore(connection-knex): update knex and SQL drivers; replace sqlite3 with better-sqlite3; replace mysql with mysql2.

    Bumped knex and its dialect drivers, and consolidated onto the actively-maintained drivers — replaced sqlite3 with better-sqlite3 and mysql with mysql2. Subsumes the prior [email protected] darwin-arm64 fix.

    @lowdefy/connection-knex dependency changes:

    • knex 2.5.13.2.9. Knex 3.x drops Node < 16; Lowdefy already requires Node 18+. The knex(config), .raw(), and dynamic query-builder API surface used by KnexRaw / KnexBuilder is unchanged.
    • pg 8.11.38.20.0.
    • Removed mssql. Knex's mssql dialect actually requires tedious (not the mssql package), and Lowdefy never imported mssql directly — it was only ever a vehicle for pulling tedious into the install tree. client: mssql in user YAML is unchanged: the knex client name stays the same, only the underlying npm package shipped with connection-knex changes.
    • Added tedious 19.2.1 as the SQL Server driver — the package knex actually loads when client: mssql is used.
    • Removed sqlite3. The driver is in maintenance-only mode upstream (the v6 release marked the repo unmaintained).
    • Added better-sqlite3 12.9.0 as the SQLite driver. Selectable as client: better-sqlite3 (or client: sqlite, which is now an alias of better-sqlite3 — see runtime client handling below).
    • Removed mysql. Unmaintained upstream since 2020.
    • Added mysql2 3.22.3 as the MySQL / MariaDB driver. Selectable as client: mysql2 in connection YAML.

    Runtime client handling (in createKnex):

    • client: sqlite is silently remapped to client: better-sqlite3. sqlite was historically a knex-level alias of sqlite3; this preserves the YAML alias while the underlying driver changes.
    • client: sqlite3 now throws a ConfigError with a migration message: Knex connection "client: sqlite3" is no longer supported. Use "client: better-sqlite3" or "client: sqlite" instead. Existing apps using client: sqlite3 need to update their connection YAML.
    • client: mysql now throws a ConfigError with a migration message: Knex connection "client: mysql" is no longer supported. Use "client: mysql2" instead. Existing apps using client: mysql need to update their connection YAML. mysql is not silently remapped because knex treats mysql and mysql2 as separate dialects with subtly different SQL formatters, not aliases — the migration is a deliberate user choice.

    pnpm.onlyBuiltDependencies allowlist for better-sqlite3:

    better-sqlite3 runs a native-binding install script (prebuild-install with a node-gyp rebuild fallback). pnpm 10 silently suppresses postinstall scripts for unapproved packages, which leaves the binding unbuilt and crashes KnexRaw / KnexBuilder at runtime.

    • Added better-sqlite3 to the allowlist on @lowdefy/server, @lowdefy/server-dev, and @lowdefy/server-e2e. These are the install roots in the CLI fetch flow under .lowdefy/{dev,build}/, where pnpm honors the per-package pnpm.onlyBuiltDependencies field.
    • Also added the same allowlist to the monorepo root package.json. The per-package field is ignored at workspace-root install (pnpm 10 only honors it on the install root), so contributors running pnpm install at the repo root would otherwise have to pnpm rebuild better-sqlite3 manually.
  • @lowdefy/server-dev: ### Minor Changes

  • 0f38c9f: feat: First-class module system for reusable config packages

    Modules are reusable bundles of Lowdefy config — pages, connections, API endpoints, menus, and exposed components — hosted in GitHub repositories or local directories. Apps install modules in lowdefy.yaml and configure them through vars, replacing the copy-paste-between-projects pattern with a declarative dependency.

    Module entries (@lowdefy/build)

    • Apps declare entries in the modules array of lowdefy.yaml with id, source, and optional vars, connections, and dependencies.
    • The entry id namespaces the module's content and forms the URL prefix for its pages (e.g. /team-users/users-list).
    • Multi-instance: the same module source can be installed multiple times under different entry IDs, each with its own vars and namespace.
    • GitHub sources (github:owner/repo[/path]@ref) are fetched as tarballs and locally cached. Private repos use GITHUB_TOKEN, the gh CLI, or git credential helpers.
    • Local sources (file:./relative/path) resolve relative to the project root.

    Module manifest (module.lowdefy.yaml)

    • Declares the module's interface: name, description, vars, connections, pages, api, components, menus, dependencies, exports, plugins, and secrets.
    • vars declarations validate consumer values with type, required, default, and description. Consumer values override manifest defaults; omitted values fall back to the declared default.
    • exports declares the module's public interface — the IDs other modules and apps may reference. The build validates cross-module references against exports.
    • plugins declarations are validated against the app's installed plugins with semver compatibility checks.
    • secrets is an allowlist of secrets the module may access; undeclared _secret references fail the build. Remapped connections skip the module's secret references for that connection.

    Module operators

    • _module.var — read manifest-validated vars, including consumer overrides and declared defaults.
    • _module.pageId, _module.connectionId, _module.endpointId — produce scoped IDs from a module-author's unscoped ID.
    • _module.id — the entry ID of the current module.

    Auto-scoped IDs

    Page, connection, API endpoint, and menu item IDs are auto-prefixed with the entry ID. Block and request IDs inherit page scope and are not rewritten.

    Consuming module resources

    • Pages and APIs are auto-included and auto-scoped — they appear in the app under the entry-ID prefix.
    • Components are reusable config fragments included with _ref: { module, component, vars }. They can export any config — UI blocks, enum maps, config templates, schema fragments — and accept vars at the call site.
    • Menus are included with _ref: { module, menu }, typically wrapped in a MenuGroup.

    Connection remapping

    Apps can redirect a module connection to an existing app connection via the entry's connections map. The module's connection definition and its declared secrets are skipped — the app connection handles them.

    Cross-module dependencies

    Modules can reference each other's pages, components, menus, connections, and APIs via abstract dependencies declared in module.lowdefy.yaml.

    • Auto-wiring: when a module entry's id matches a declared dependency name, the build wires it automatically.
    • Explicit wiring: the entry's dependencies map overrides auto-wiring and supports multi-instance topologies where each instance points at a different partner.
    • The build validates every wiring, detects dependency cycles, and reports unmapped or undeclared dependencies with remediation hints.

    Auth page rules

    Picomatch glob patterns in auth page rules (e.g. team-users/*) for wildcard module page matching.

    Slashed page IDs (@lowdefy/server, @lowdefy/server-dev)

    Server routes support module page IDs containing / (e.g. /team-users/users-list).

Patch Changes

  • 596fddc: chore(connection-knex): update knex and SQL drivers; replace sqlite3 with better-sqlite3; replace mysql with mysql2.

    Bumped knex and its dialect drivers, and consolidated onto the actively-maintained drivers — replaced sqlite3 with better-sqlite3 and mysql with mysql2. Subsumes the prior [email protected] darwin-arm64 fix.

    @lowdefy/connection-knex dependency changes:

    • knex 2.5.13.2.9. Knex 3.x drops Node < 16; Lowdefy already requires Node 18+. The knex(config), .raw(), and dynamic query-builder API surface used by KnexRaw / KnexBuilder is unchanged.
    • pg 8.11.38.20.0.
    • Removed mssql. Knex's mssql dialect actually requires tedious (not the mssql package), and Lowdefy never imported mssql directly — it was only ever a vehicle for pulling tedious into the install tree. client: mssql in user YAML is unchanged: the knex client name stays the same, only the underlying npm package shipped with connection-knex changes.
    • Added tedious 19.2.1 as the SQL Server driver — the package knex actually loads when client: mssql is used.
    • Removed sqlite3. The driver is in maintenance-only mode upstream (the v6 release marked the repo unmaintained).
    • Added better-sqlite3 12.9.0 as the SQLite driver. Selectable as client: better-sqlite3 (or client: sqlite, which is now an alias of better-sqlite3 — see runtime client handling below).
    • Removed mysql. Unmaintained upstream since 2020.
    • Added mysql2 3.22.3 as the MySQL / MariaDB driver. Selectable as client: mysql2 in connection YAML.

    Runtime client handling (in createKnex):

    • client: sqlite is silently remapped to client: better-sqlite3. sqlite was historically a knex-level alias of sqlite3; this preserves the YAML alias while the underlying driver changes.
    • client: sqlite3 now throws a ConfigError with a migration message: Knex connection "client: sqlite3" is no longer supported. Use "client: better-sqlite3" or "client: sqlite" instead. Existing apps using client: sqlite3 need to update their connection YAML.
    • client: mysql now throws a ConfigError with a migration message: Knex connection "client: mysql" is no longer supported. Use "client: mysql2" instead. Existing apps using client: mysql need to update their connection YAML. mysql is not silently remapped because knex treats mysql and mysql2 as separate dialects with subtly different SQL formatters, not aliases — the migration is a deliberate user choice.

    pnpm.onlyBuiltDependencies allowlist for better-sqlite3:

    better-sqlite3 runs a native-binding install script (prebuild-install with a node-gyp rebuild fallback). pnpm 10 silently suppresses postinstall scripts for unapproved packages, which leaves the binding unbuilt and crashes KnexRaw / KnexBuilder at runtime.

    • Added better-sqlite3 to the allowlist on @lowdefy/server, @lowdefy/server-dev, and @lowdefy/server-e2e. These are the install roots in the CLI fetch flow under .lowdefy/{dev,build}/, where pnpm honors the per-package pnpm.onlyBuiltDependencies field.
    • Also added the same allowlist to the monorepo root package.json. The per-package field is ignored at workspace-root install (pnpm 10 only honors it on the install root), so contributors running pnpm install at the repo root would otherwise have to pnpm rebuild better-sqlite3 manually.
  • 762755c: feat(blocks-tiptap): Add new default block package with TiptapInput and TiptapMentionInput rich-text editors.

    @lowdefy/blocks-tiptap ships two rich-text editor blocks built on TipTap:

    • TiptapInput — standard rich-text editor with bold/italic/strike-through, multi-color highlight, headings, lists, tables, links, and a bubble menu.
    • TiptapMentionInput — everything TiptapInput does, plus an @-mention dropdown populated from a static options list or a Lowdefy request. Resolved mentions are returned on the block value as mentions: [...].

    Both blocks emit an object value shaped { html, text, markdown, fileList, mentions? } and register clear, setContent, and focus methods.

    Configurable extensions — defaults preserve the bundled editor; override any of these to trim the editor down or tune it:

    • properties.starterKit — object forwarded to TipTap StarterKit, e.g. { heading: false, codeBlock: false }.
    • properties.image{ enabled, maxWidth, zoom }
    • properties.table{ enabled, resizable }
    • properties.link{ enabled, autolink, linkOnPaste, openOnClick, defaultProtocol }
    • properties.highlight{ enabled, multicolor }
    • properties.mentions.char / properties.mentions.allowSpaces — change the trigger char (e.g. # for hashtags) or disable spaces inside a mention query (TiptapMentionInput only).

    Image drag/drop and paste are supported by pointing properties.s3PostPolicyRequestId at a request that returns an S3 presigned POST policy (e.g. AwsS3PresignedPostPolicy). The file handler is optional — omit the request id to disable uploads entirely.

    The blocks are registered in the default types map and are available out of the box on @lowdefy/server-dev. No private-registry tokens are required: the blocks use the open-source @tiptap/extension-file-handler instead of @tiptap-pro/extension-file-handler, so projects that migrated from a custom TipTap plugin can drop their TIPTAP_PRO_TOKEN environment variable and .npmrc scoped-registry config.

  • @lowdefy/server-e2e: ### Patch Changes

  • 596fddc: chore(connection-knex): update knex and SQL drivers; replace sqlite3 with better-sqlite3; replace mysql with mysql2.

    Bumped knex and its dialect drivers, and consolidated onto the actively-maintained drivers — replaced sqlite3 with better-sqlite3 and mysql with mysql2. Subsumes the prior [email protected] darwin-arm64 fix.

    @lowdefy/connection-knex dependency changes:

    • knex 2.5.13.2.9. Knex 3.x drops Node < 16; Lowdefy already requires Node 18+. The knex(config), .raw(), and dynamic query-builder API surface used by KnexRaw / KnexBuilder is unchanged.
    • pg 8.11.38.20.0.
    • Removed mssql. Knex's mssql dialect actually requires tedious (not the mssql package), and Lowdefy never imported mssql directly — it was only ever a vehicle for pulling tedious into the install tree. client: mssql in user YAML is unchanged: the knex client name stays the same, only the underlying npm package shipped with connection-knex changes.
    • Added tedious 19.2.1 as the SQL Server driver — the package knex actually loads when client: mssql is used.
    • Removed sqlite3. The driver is in maintenance-only mode upstream (the v6 release marked the repo unmaintained).
    • Added better-sqlite3 12.9.0 as the SQLite driver. Selectable as client: better-sqlite3 (or client: sqlite, which is now an alias of better-sqlite3 — see runtime client handling below).
    • Removed mysql. Unmaintained upstream since 2020.
    • Added mysql2 3.22.3 as the MySQL / MariaDB driver. Selectable as client: mysql2 in connection YAML.

    Runtime client handling (in createKnex):

    • client: sqlite is silently remapped to client: better-sqlite3. sqlite was historically a knex-level alias of sqlite3; this preserves the YAML alias while the underlying driver changes.
    • client: sqlite3 now throws a ConfigError with a migration message: Knex connection "client: sqlite3" is no longer supported. Use "client: better-sqlite3" or "client: sqlite" instead. Existing apps using client: sqlite3 need to update their connection YAML.
    • client: mysql now throws a ConfigError with a migration message: Knex connection "client: mysql" is no longer supported. Use "client: mysql2" instead. Existing apps using client: mysql need to update their connection YAML. mysql is not silently remapped because knex treats mysql and mysql2 as separate dialects with subtly different SQL formatters, not aliases — the migration is a deliberate user choice.

    pnpm.onlyBuiltDependencies allowlist for better-sqlite3:

    better-sqlite3 runs a native-binding install script (prebuild-install with a node-gyp rebuild fallback). pnpm 10 silently suppresses postinstall scripts for unapproved packages, which leaves the binding unbuilt and crashes KnexRaw / KnexBuilder at runtime.

    • Added better-sqlite3 to the allowlist on @lowdefy/server, @lowdefy/server-dev, and @lowdefy/server-e2e. These are the install roots in the CLI fetch flow under .lowdefy/{dev,build}/, where pnpm honors the per-package pnpm.onlyBuiltDependencies field.
    • Also added the same allowlist to the monorepo root package.json. The per-package field is ignored at workspace-root install (pnpm 10 only honors it on the install root), so contributors running pnpm install at the repo root would otherwise have to pnpm rebuild better-sqlite3 manually.
  • 762755c: feat(blocks-tiptap): Add new default block package with TiptapInput and TiptapMentionInput rich-text editors.

    @lowdefy/blocks-tiptap ships two rich-text editor blocks built on TipTap:

    • TiptapInput — standard rich-text editor with bold/italic/strike-through, multi-color highlight, headings, lists, tables, links, and a bubble menu.
    • TiptapMentionInput — everything TiptapInput does, plus an @-mention dropdown populated from a static options list or a Lowdefy request. Resolved mentions are returned on the block value as mentions: [...].

    Both blocks emit an object value shaped { html, text, markdown, fileList, mentions? } and register clear, setContent, and focus methods.

    Configurable extensions — defaults preserve the bundled editor; override any of these to trim the editor down or tune it:

    • properties.starterKit — object forwarded to TipTap StarterKit, e.g. { heading: false, codeBlock: false }.
    • properties.image{ enabled, maxWidth, zoom }
    • properties.table{ enabled, resizable }
    • properties.link{ enabled, autolink, linkOnPaste, openOnClick, defaultProtocol }
    • properties.highlight{ enabled, multicolor }
    • properties.mentions.char / properties.mentions.allowSpaces — change the trigger char (e.g. # for hashtags) or disable spaces inside a mention query (TiptapMentionInput only).

    Image drag/drop and paste are supported by pointing properties.s3PostPolicyRequestId at a request that returns an S3 presigned POST policy (e.g. AwsS3PresignedPostPolicy). The file handler is optional — omit the request id to disable uploads entirely.

    The blocks are registered in the default types map and are available out of the box on @lowdefy/server-dev. No private-registry tokens are required: the blocks use the open-source @tiptap/extension-file-handler instead of @tiptap-pro/extension-file-handler, so projects that migrated from a custom TipTap plugin can drop their TIPTAP_PRO_TOKEN environment variable and .npmrc scoped-registry config.

  • @lowdefy/logger: ### Patch Changes

  • e3fc007: fix(logger): Handle non-object JSON values in stdout line handler.

    JSON.parse can return null for literal "null" input, crashing the CLI log handler. Non-object parsed values are now treated as plain text lines.

v5.1.0 Breaking risk
⚠ Upgrade required
  • Migrate existing `DataDiff` usages to the new blocks in `@lowdefy/blocks-diff` using the provided mapping.
  • Update AgGrid columns to use the new `cell` object for built‑in renderers if leveraging number formatting, links, etc.
Breaking changes
  • Removal of `DataDiff` block from `@lowdefy/blocks-antd`; migrate to corresponding blocks in `@lowdefy/blocks-diff` (DiffList, DiffSideBySide, DiffTimeline, DiffGit).
Security fixes
  • Theme values interpolated into pre-hydration inline script are now fully escaped via `safeScriptJson`, closing six CodeQL `js/bad-code-sanitization` alerts.
Notable features
  • Dark mode polish: per-mode antd tokens (`lightToken`, `darkToken`, `lightComponents`, `darkComponents`) and pre-hydration script to eliminate white flash.
  • New package `@lowdefy/blocks-diff` with four diff blocks (DiffList, DiffSideBySide, DiffTimeline, DiffGit).
  • AgGrid built‑in cell renderers (`tag`, `avatar`, `link`, `date`, `boolean`, `progress`, `number`) and tooltip schema additions.
Full changelog

Highlights

  • Dark mode polish across the framework — new lightToken / darkToken and lightComponents / darkComponents keys on theme.antd let you override antd tokens per mode, a pre-hydration inline script paints the correct background before first paint (no more white flash on navigation), and the generated globals.css now ships themed scrollbars that auto-swap with the active theme.
  • New @lowdefy/blocks-diff package — Four dedicated blocks: DiffList, DiffSideBySide, DiffTimeline, and DiffGit.
  • New PageSidebarLayout block — full-page layout with a full-height collapsible sider, localStorage-persisted open state, mobile drawer, 8 content slots, and dark-mode-aware logo switching. PageSiderMenu also now persists its sider state (shared siderStorageKey).
  • AgGrid built-in cell renderers — every column now accepts a cell object with tag, avatar, link, date, boolean, progress, or number types, Excel-style number formatting via Intl.NumberFormat, row-data field paths (nameField, srcField, etc.), an ellipsis column helper, and a new onCellLink event. Tooltip props (tooltipField, tooltipValueGetter, enableBrowserTooltips) are now declared in the schemas, and loading overlays have been rewritten to fix a long-standing Safari stuck-overlay bug.
  • User-role operators_user.hasRole, _user.hasSomeRoles, and _user.hasAllRoles check the user.roles array and return a boolean, making role-gated UI and request auth easier to express.
  • selector cssKey on Select blocksSelector, MultipleSelector, and AutoComplete now expose a selector cssKey so you can cap the tag container height and scroll internally without reaching for Tailwind arbitrary variants or global CSS.
  • Security hardening — theme values interpolated into the pre-hydration <script> are now fully escaped (<, >, U+2028, U+2029) via a new safeScriptJson helper, closing six CodeQL js/bad-code-sanitization alerts.

What's New

feat(build): Themed default scrollbars in generated globals.css.

Packages: @lowdefy/build

Every Lowdefy app now ships with themed scrollbars out of the box. Native Windows/Linux scrollbars were rendering as light grey on dark surfaces (Modal, Drawer, overflowing containers), clashing with dark themes — macOS overlay scrollbars hid the problem. The generated globals.css now emits a @layer base block that:

  • Sets scrollbar-width: thin and scrollbar-color for Firefox and modern browsers.
  • Styles ::-webkit-scrollbar (10px, transparent track, subtle thumb with inset border, hover darkens) for Chromium / WebKit.
  • Drives all colors from antd CSS custom properties (--ant-color-border-secondary, --ant-color-text-tertiary) so they auto-swap on dark / light mode toggle.

User-provided CSS remains in @layer components, so any app-level ::-webkit-scrollbar overrides in public/styles.css still win.

feat(client): Per-mode theme tokens for dark/light customization.

Packages: @lowdefy/client, @lowdefy/server, @lowdefy/server-dev

theme.antd now accepts four new sibling keys so apps can soften base surfaces without juggling two theme files. Each is merged on top of the shared equivalent only when the matching mode is active:

  • lightToken / darkToken — override antd design tokens (e.g. colorBgLayout, colorBgContainer, colorBgElevated) per mode.
  • lightComponents / darkComponents — override component-level tokens per mode (e.g. Layout.siderBg, Layout.headerBg, Menu.darkItemBg) that aren't reachable via seed tokens.

The <html> pre-hydration inline script now reads darkToken.colorBgLayout / lightToken.colorBgLayout from the built theme, so the first paint matches your configured surface color with no flash of #000 or #fff.

theme:
  antd:
    token:
      colorPrimary: '#6366f1'
    darkToken:
      colorBgLayout: '#131419'
      colorBgContainer: '#1a1b22'
    darkComponents:
      Layout:
        headerBg: '#0e0f13'
        siderBg: '#0e0f13'
      Menu:
        darkItemBg: '#0e0f13'
        darkItemSelectedBg: '#252731'
  darkMode: system

Backwards compatible — apps that only use theme.antd.token keep antd's default base colors (dark #000, light browser-default).

feat(blocks-aggrid): Add built-in cell renderer types.

Packages: @lowdefy/blocks-aggrid

Every AgGrid column now accepts a cell object on columnDefs entries that selects a first-class renderer — tag, avatar, link, date, boolean, progress, number — plus an ellipsis: N column-level helper that auto-enables wrapText + autoHeight with an N-line clamp.

The number renderer wraps Intl.NumberFormat with Excel-style config: format (number / currency / percent / compact), locale, currency, decimals, accounting-style negative: parentheses, signColor (green/red by sign), and optional prefix / suffix. Number columns auto-right-align (cellStyle.justifyContent: flex-end + ag-right-aligned-header) and every cell.type supports an align: left | center | right override. Renderer output is React, vertically centred, and styled entirely through antd CSS tokens (--ant-control-height, --ant-margin-xs, --ant-color-*, --ant-border-radius, --ant-font-size, etc.) so the grid adapts to Material vs Balham row heights and to dark / compact antd theme.algorithm without per-theme overrides.

Field-valued keys (nameField, srcField, idField, colorFrom, and every value inside link.urlQuery) are plain row-data path strings — no _function wrapping required. Null values render a muted em-dash across every built-in type.

Link navigation: cell.type: link and avatar.link render anchors and emit a new onCellLink block event with the resolved link config; wire it to a Link action (params: { _event: link }) to navigate — matches the existing Lowdefy event → action pattern.

antd, @ant-design/icons, and dayjs are now declared as peer dependencies on @lowdefy/blocks-aggrid (de-facto required by the existing ag-grid-antd.module.css token mapping).

Also fixes the long-standing cell vertical-centering drift: .ag-cell is now a flex container via the antd theme CSS module, which also benefits users' existing renderHtml cells.

feat(blocks-aggrid): Declare tooltip properties in block schemas.

Packages: @lowdefy/blocks-aggrid

All six AgGrid variants (Alpine/Balham/Material for display and input) now declare enableBrowserTooltips, tooltipShowDelay, and tooltipHideDelay at the grid level and tooltipField, tooltipValueGetter, and tooltipComponent at the column level. These AG Grid props already worked — they were passed through via property spreading — but were not documented in the block schemas. Users can now discover and configure tooltips directly from the schema.

feat(blocks-antd): DataDiff gains sideBySide, timeline, and gitDiff modes, plus depth-aware nested rendering.

Packages: @lowdefy/blocks-antd

  • mode: sideBySide — two aligned antd Descriptions panels (Before / After) that respond to breakpoints.
  • mode: timeline — antd Timeline with one item per change, colour-coded and breadcrumb-labelled for context.
  • mode: gitDiff — unified-diff YAML patch rendering with +/− line markers; for technical users who want to see every line of change. Ignores the structured-rendering props (labels, format, maxDepth) but still honours hide / show.
  • list mode now sub-groups array-of-objects changes into per-item sections with their own summary chips, and breadcrumbs deeply-nested row labels (Order 1 › Customer › Name instead of just Name).
  • New maxDepth property (default 4) collapses changes deeper than the cap into a single JSON-rendered row at the cap, keeping deep payloads legible.

feat(blocks-antd): Add DataDiff block for rendering user-friendly diffs.

Packages: @lowdefy/blocks-antd

DataDiff compares two objects (before / after) and renders the differences using antd primitives (Descriptions, Collapse, Tag, Empty). v1 ships a polished list mode: grouped by top-level key, with +N / −N / ~N summary chips per group, icon- and color-coded change rows, and a collapsed-JSON fallback for entirely-new nested objects. All colors come from antd semantic tokens (colorSuccess, colorError, colorWarning) so the block respects dark mode and theme overrides automatically.

Per-path value formatters — date, datetime, boolean (Yes/No tag), currency (Intl.NumberFormat), json (pretty inside a subtle collapse), code, and enum (value → { label, color } map) — turn raw field values into something end-users can read. labels maps dotted paths to display names; hide / show accept exact paths, prefix.*, or *.leaf patterns. Built on microdiff (~1 kB, zero deps).

- id: order_audit
  type: DataDiff
  properties:
    before: _state.original
    after: _state.current
    labels:
      status: Order status
      total: Total
    format:
      total: { type: currency, currency: USD }
      status:
        type: enum
        map:
          pending: { label: Pending, color: warning }
          paid: { label: Paid, color: success }
    hide:
      - 'internal.*'

feat(blocks-antd): Expose selector cssKey on Select-based blocks.

Packages: @lowdefy/blocks-antd

Selector, MultipleSelector, and AutoComplete now expose a selector cssKey that targets the inner tag/value container (antd v6's content semantic slot, rendered as .ant-select-content in the DOM). Use it to cap the tag container height and enable internal scroll on multi-select blocks:

style:
  .selector:
    maxHeight: 96px
    overflowY: auto

Before this change, users had to reach for Tailwind arbitrary variants or global CSS (e.g. [&_.ant-select-selector]:max-h-24) to style the inner container, which leaked antd internals into app YAML and was brittle across antd upgrades.

feat(blocks-diff): New package. DataDiff extracted from blocks-antd and split

Packages: @lowdefy/blocks-antd, @lowdefy/blocks-diff

into DiffList, DiffSideBySide, DiffTimeline, and DiffGit blocks.

BREAKING: The DataDiff block has been removed from @lowdefy/blocks-antd.
Migrate to the per-mode blocks in @lowdefy/blocks-diff:

  • mode: listDiff.DiffList
  • mode: sideBySideDiff.DiffSideBySide
  • mode: timelineDiff.DiffTimeline
  • mode: gitDiffDiff.DiffGit

The diff, yaml, pluralize, and microdiff dependencies have been moved
from @lowdefy/blocks-antd to @lowdefy/blocks-diff along with the block.

feat: Add PageSidebarLayout block

Packages: @lowdefy/blocks-antd

New full-page layout block with a full-height sidebar, no top-level header, and mobile drawer navigation. The sider spans the entire viewport height with the logo pinned at the bottom.

PageSidebarLayout

  • Full-height collapsible sider with inline menu
  • Sider collapse state persists in localStorage (configurable key via siderStorageKey)
  • Dark mode via app-level ConfigProvider — all components adapt automatically via CSS variables
  • darkModeToggle, notifications, and profile properties shown in the sider on desktop and the mobile header on small screens, matching PageHeaderMenu and PageSiderMenu
  • theme property accepts an Ant Design design token object for fine-grained color customization via ConfigProvider
  • Responsive logo: full logo when sider is expanded, square logo when collapsed, auto-swaps between light and dark variants based on dark mode
  • 8 content slots: content, footer, header, siderOpen, siderClosed, mobileExtra, mobileDrawerContent, mobileDrawerFooter

Drawer

  • Added footer content slot and styles.footer passthrough

MobileMenu

  • Added logo property for drawer header branding with dark mode-aware logo switching
  • Added drawerContent and drawerFooter content slots
  • Changed category from display to container to support slot resolution

feat: Persist PageSiderMenu sider open state to localStorage

Packages: @lowdefy/blocks-antd

PageSiderMenu now persists its sider collapsed/expanded state across page navigations and reloads, matching PageSidebarLayout behavior.

  • Sider open state reads from and writes to lf-{siderStorageKey}-open in localStorage
  • New siderStorageKey property (default 'sider') — shares the same key as PageSidebarLayout by default, so the user's preference survives swapping between layouts
  • New sider.initialCollapsed property used as the fallback when no persisted value exists
  • Gracefully handles SSR and privacy-mode localStorage unavailability

Fixes & Improvements

  • fix(server): Prevent white flash on page navigation in dark mode. (@lowdefy/build, @lowdefy/client, @lowdefy/server, @lowdefy/server-dev)

    Pages no longer flash white when navigating between pages in dark mode. A synchronous inline script now sets the correct background color before the page paints, matching the user's dark mode preference from config, localStorage, or system settings.

  • feat(operators-js): Add _user.hasRole, _user.hasSomeRoles, and _user.hasAllRoles methods to check user roles against the user.roles array. hasRole takes a single role string; hasSomeRoles and hasAllRoles take an array of role strings. All return a boolean. (@lowdefy/docs, @lowdefy/operators-js)

  • fix(blocks-aggrid): React to loading prop changes on AgGrid. (@lowdefy/blocks-aggrid)

    The loading block flag now toggles AG Grid's native showLoadingOverlay / hideOverlay at runtime. Previously the overlay calls were inside a useEffect with an empty dependency array, so they only ran once on mount and never reacted to subsequent loading changes. The effect has been split in two — method registration still runs once, overlay toggling now runs whenever loading changes — and an onGridReady callback applies the initial overlay state safely after the grid api is attached.

  • fix(blocks-aggrid): Safari loading overlay stuck. (@lowdefy/blocks-aggrid)

    AgGrid and AgGridInput no longer use ag-grid's internal showLoadingOverlay / hideOverlay API to reflect the block's loading prop. On Safari / WebKit, a microtask race between our hideOverlay() call and ag-grid's own late showOverlay tick left the "Loading…" box stuck on screen even after data had rendered (ag-grid issues #4421, #1665, #8358). Chromium happened to win the race the other way, which hid the symptom.

    Both blocks now wrap AgGridReact in a position: relative div, set suppressLoadingOverlay on the grid, and render a small themed overlay component (LoadingOverlay.js) when the Lowdefy loading prop is true. The overlay is styled via antd CSS custom properties, so it follows the active theme.

  • fix(blocks-antd): PageSiderMenu state sync, setSiderOpen bug, and menu auto-popup. (@lowdefy/blocks-antd)

    Three related fixes so the sider persists correctly and the inline menu doesn't pop open flyouts on page load.

    • Persistence on hard-refresh / new tab: PageSiderMenu now feeds its computed openSiderState (read from localStorage['lf-{siderStorageKey}-open']) into the inner Sider block as initialCollapsed. Previously the Sider re-read the media-query-computed initialCollapsed independently and ignored the persisted value, so the sider always started collapsed on desktop regardless of the user's preference.
    • setSiderOpen({ open }) bug: the action called the Sider's _toggleSiderOpen (a no-arg toggle) with an { open } argument that was silently ignored. Fixed to call _setSiderOpen({ open }) so the sider lands in the requested state. toggleSiderOpen now also uses the explicit setter with the computed next value for symmetry.
    • Menu auto-popup when sider is collapsed: the Menu block's defaultOpenKeys guard checked properties.collapsed !== true, but PageSiderMenu never passes collapsed as a prop — antd derives the collapsed state from SiderContext. As a result the current page's parent group was added to defaultOpenKeys, and antd's Menu auto-popped the flyout for that group on mount. Menu now reads siderCollapsed from Layout._InternalSiderContext (the same channel antd's own Menu uses) so the group auto-expansion only applies when the sider is expanded.
  • fix(server): Escape theme values embedded in the pre-hydration inline script. (@lowdefy/server, @lowdefy/server-dev)

    _document.js interpolates configColorMode, darkToken.colorBgLayout, and lightToken.colorBgLayout from theme.json into a synchronous <script> block to set the <html> background before hydration. Previously the values went through JSON.stringify only — enough to escape JS-string-context characters, but not enough to prevent a value containing </script> (or U+2028 / U+2029 line separators) from breaking out of the enclosing <script> tag.

    Added a safeScriptJson helper that additionally escapes <, >, control chars, and U+2028 / U+2029 to \uXXXX sequences after JSON.stringify. For every valid color value (#1e293b, rgb(...), slategray, oklch(...), etc.) the output is byte-identical to the previous behavior; only payloads that would have tripped <script> breakout or JS-line-terminator injection are now neutralized.

    Closes the six js/bad-code-sanitization CodeQL alerts (89 – 94) opened against the per-mode-theme PR.

v5.0.0 Breaking risk
⚠ Upgrade required
  • Migrate all _moment usages to _dayjs; update format strings if necessary
  • Review date picker configurations for day.js compatibility
  • Check Google Sheets connections and AG Grid cell renderers for date handling changes
Breaking changes
  • _moment operator removed; use _dayjs instead
  • @lowdefy/operators-moment package removed
  • Nunjucks `date` filter now uses day.js (format strings must be day.js‑compatible)
Notable features
  • Theme tokens at runtime via _theme operator and custom token config
  • Keyboard shortcuts on block events with platform‑aware bindings
  • System dark mode support through theme.darkMode config
Full changelog

Highlights

  • Theme tokens at runtime: Access Ant Design v6 design tokens (colors, spacing, typography) with the new _theme operator. Configure custom tokens via theme.antd.token and theme.antd.algorithm in lowdefy.yaml for theme-aware component styling.
  • Keyboard shortcuts on events: Add shortcut property to block events for platform-aware keyboard bindings (e.g., mod+K → Cmd+K on Mac, Ctrl+K on Windows). Supports key sequences and multiple bindings. Built-in validation warns about duplicates and browser conflicts. Button, Anchor, Tag, and Search blocks display shortcut badges.
  • System dark mode support: New theme.darkMode: 'system' | 'light' | 'dark' config auto-follows OS preference. SetDarkMode action accepts string params to control mode. _media: darkModePreference returns user preference; _media: darkMode continues returning the boolean state.
  • Search command palette block: New Search display block provides full-text search with MiniSearch. Supports static JSON index via indexUrl or runtime indexing with documents. Features grouped results, keyboard navigation, term highlighting, and search history. Replaces Algolia DocSearch with self-hosted alternative.
  • lowdefy upgrade command: New CLI tool guides version migrations with prompt-based codemods. Use --to, --plan, or --resume to manage upgrades. Each prompt can be copied to clipboard, viewed as a manual guide, or skipped. Upgrade state persists for interrupted migrations. v5.0 codemods cover antd v6, layout grid, and dayjs migrations.
  • Better build error messages: Schema validation errors now include property names. CSS errors suggest dot-prefixed slot keys. YAML parse errors surface immediately instead of crashing on null entries.
  • AG Grid automatic theming: All six grid blocks now follow Ant Design theme automatically, responding to light/dark mode and custom colors without configuration. Removed separate dark variant blocks. Override individual --ag-* variables via the block's style property.
  • Dark mode toggle in headers: New darkModeToggle: true property on PageHeaderMenu and PageSiderMenu renders a built-in sun/moon toggle. Preference persists to localStorage and respects OS default. toggleDarkMode method available for programmatic control.
  • Header customization: Add color property to set header background (defaults to --ant-color-bg-container). New iconsColor property controls notification, profile, and dark mode toggle icon colors — useful with dark backgrounds.
  • PhoneNumberInput formatting: Phone number values now strip leading zeros and non-digits. 0821234567 with +27 selected produces +27821234567 instead of +270821234567.
  • S3 upload improvements: New onBeforeUpload event fires before each file upload; throw in the handler to cancel. XHR uploads are now Promise-based with proper error propagation. CORS/network failures throw ServiceError with diagnostic message. File metadata serialized to plain objects to preserve _event references.
  • Modulo operator: Added _math.mod for remainder calculations. Use _math.mod: [10, 3] or _math.mod: { dividend: 10, divisor: 3 }.
  • _date operator flexibility: Now accepts Date objects in addition to numbers and strings.
  • Anchor icon spacing: Fixed icon margin issue so icons no longer sit flush against title text.
  • Error serialization robustness: Fixed crashes on circular references in error objects (e.g., Axios responses with Node.js request cycles). Errors now log properly instead of crashing the logger.
  • ⚠️ BREAKING: Block metadata restructure — Move block definitions from schema.js to meta.js. Block packages export ./metas instead of ./schemas. Components no longer have .meta static property. Metadata loaded from blockMetas.json build artifact at runtime. Use buildBlockSchema(meta) from @lowdefy/block-utils to generate JSON Schema.
  • ⚠️ BREAKING: Moment.js removed — Replace _moment operator with _dayjs. Apps using _moment must migrate. @lowdefy/operators-moment package removed. Date picker blocks use day.js internally. Nunjucks date filter uses day.js. humanizeDuration thresholds parameter is now ignored.
  • ⚠️ BREAKING: Component theme properties removed — Remove header.theme, sider.theme, menu.theme properties from PageSiderMenu, PageHeaderMenu, Header, and Sider blocks. Dark mode now automatic via CSS variables. Also removed header.style, sider.style, etc. — use style: { .header }, style: { .sider } instead. Removed onProfileClick and onNotificationClick events; use notifications.link property instead. collapsible and initialCollapsed properties removed from PageSiderMenu.
  • ⚠️ BREAKING: Algolia blocks removed — Use the Search block in @lowdefy/blocks-antd instead of @lowdefy/blocks-algolia.

What's New

Add theme token system. Use _theme operator to access Ant Design v6 design tokens (colors, spacing, typography) at runtime. Theme is configured via theme.antd.token and theme.antd.algorithm in lowdefy.yaml. The _theme operator resolves the full computed token set including antd defaults.

Packages: @lowdefy/api, @lowdefy/build, @lowdefy/build, @lowdefy/build, @lowdefy/build, @lowdefy/build, @lowdefy/build, @lowdefy/client, @lowdefy/client, @lowdefy/client, @lowdefy/client, @lowdefy/client, @lowdefy/docs, @lowdefy/engine, @lowdefy/engine, @lowdefy/layout, @lowdefy/layout, @lowdefy/layout, @lowdefy/actions-core, @lowdefy/actions-pdf-make, @lowdefy/blocks-aggrid, @lowdefy/blocks-aggrid, @lowdefy/blocks-aggrid, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-basic, @lowdefy/blocks-basic, @lowdefy/blocks-basic, @lowdefy/blocks-echarts, @lowdefy/blocks-echarts, @lowdefy/blocks-echarts, @lowdefy/blocks-echarts, @lowdefy/blocks-google-maps, @lowdefy/blocks-google-maps, @lowdefy/blocks-google-maps, @lowdefy/blocks-loaders, @lowdefy/blocks-loaders, @lowdefy/blocks-loaders, @lowdefy/blocks-markdown, @lowdefy/blocks-markdown, @lowdefy/blocks-markdown, @lowdefy/blocks-qr, @lowdefy/blocks-qr, @lowdefy/blocks-qr, @lowdefy/connection-axios-http, @lowdefy/connection-elasticsearch, @lowdefy/connection-google-sheets, @lowdefy/connection-knex, @lowdefy/connection-redis, @lowdefy/connection-sendgrid, @lowdefy/connection-stripe, @lowdefy/connection-test, @lowdefy/operators-change-case, @lowdefy/operators-diff, @lowdefy/operators-js, @lowdefy/operators-js, @lowdefy/operators-jsonata, @lowdefy/operators-mql, @lowdefy/operators-nunjucks, @lowdefy/operators-uuid, @lowdefy/operators-yaml, @lowdefy/plugin-auth0, @lowdefy/plugin-aws, @lowdefy/plugin-aws, @lowdefy/plugin-aws, @lowdefy/plugin-csv, @lowdefy/plugin-next-auth, @lowdefy/server, @lowdefy/server, @lowdefy/server-dev, @lowdefy/server-dev, @lowdefy/server-dev, @lowdefy/server-dev, @lowdefy/block-dev, @lowdefy/block-dev, @lowdefy/block-utils, @lowdefy/block-utils, @lowdefy/e2e-utils, @lowdefy/node-utils

Restructure block metadata from component static properties to dedicated meta.js files.

Packages: @lowdefy/build, @lowdefy/client, @lowdefy/engine, @lowdefy/blocks-aggrid, @lowdefy/blocks-antd, @lowdefy/blocks-basic, @lowdefy/blocks-echarts, @lowdefy/blocks-google-maps, @lowdefy/blocks-loaders, @lowdefy/blocks-markdown, @lowdefy/blocks-qr, @lowdefy/plugin-aws, @lowdefy/server-dev, @lowdefy/block-utils

Breaking Changes

  • schema.js renamed to meta.js: Block definitions moved from schema.js to meta.js. The meta.js files export category, icons, valueType, cssKeys, events, and properties (JSON Schema).
  • schemas.js barrel renamed to metas.js: Block packages export ./metas instead of ./schemas.
  • .meta removed from components: Block components no longer have a .meta static property. Metadata is loaded from the blockMetas.json build artifact at runtime.
  • blockMetas.json build artifact: The build pipeline writes plugins/blockMetas.json containing category, valueType, and initValue for each block type.
  • buildBlockSchema(meta): New function in @lowdefy/block-utils generates complete JSON Schema from meta objects with operator support and CSS slot key validation.

Replace moment.js with day.js across the monorepo.

Packages: @lowdefy/build, @lowdefy/blocks-antd, @lowdefy/connection-google-sheets, @lowdefy/operators-dayjs, @lowdefy/nunjucks

Breaking Changes

  • _moment operator removed: Use _dayjs instead. The new @lowdefy/operators-dayjs package provides the _dayjs operator with the same API patterns.
  • @lowdefy/operators-moment package removed: Apps using _moment must migrate to _dayjs.
  • Nunjucks date filter: Now uses day.js internally. Format strings are day.js compatible (mostly identical to moment).
  • Date picker blocks: All date/time picker blocks use day.js instead of moment for value parsing and formatting.
  • Google Sheets connection: Date serialization uses day.js internally.
  • humanizeDuration thresholds: The thresholds parameter on _dayjs.humanizeDuration is silently ignored (day.js does not support it).
  • AgGrid cell renderers: Update __moment to __dayjs in custom AG Grid cell renderer references.
  • Date selector UTC handling: Antd v6 bundles its own dayjs without the UTC plugin. Date selector blocks wrap antd's dayjs instances with the extended dayjs before calling .utc() — this is handled internally and requires no user action.

Add keyboard shortcut support for block events.

Packages: @lowdefy/build, @lowdefy/client, @lowdefy/engine, @lowdefy/blocks-antd, @lowdefy/blocks-basic, @lowdefy/block-utils

Blocks can now define keyboard shortcuts on events using the shortcut property in the event long-form object. Shortcuts are platform-aware (mod+K maps to Cmd+K on Mac, Ctrl+K on Windows), support sequences (g i), and can be arrays for multiple bindings.

  • Build validation warns on duplicate shortcuts within a page and conflicts with browser defaults (e.g. mod+N)
  • ShortcutManager registers a single global keydown listener via tinykeys with visibility gating and input field suppression
  • ShortcutBadge component renders platform-appropriate key symbols (e.g. ⌘ K) and is available to all blocks via components.ShortcutBadge
  • ShortcutBadge in blocks: Button, Anchor, Tag, and Search blocks display a platform-aware keyboard shortcut badge (e.g. ⌘S / Ctrl+S) next to the title when the event has a shortcut defined

Add theme.darkMode config with system preference support.

Packages: @lowdefy/build, @lowdefy/client, @lowdefy/actions-core, @lowdefy/blocks-antd, @lowdefy/operators-js, @lowdefy/server, @lowdefy/server-dev

System Dark Mode (theme.darkMode)

  • New theme.darkMode config key accepts 'system' (default), 'light', or 'dark'
  • When set to 'system', the app follows the OS dark mode preference and updates live when it changes
  • When set to 'light' or 'dark', the developer locks the mode — user preferences are stored but not applied

SetDarkMode Action

  • Now accepts string params: darkMode: 'system' | 'light' | 'dark'
  • Without params, cycles through light, dark, and system preferences

_media Operator

  • New _media: darkModePreference returns the user's preference ('system', 'light', or 'dark')
  • _media: darkMode continues to return the effective boolean state

Dark Mode Rendering

  • Notification, Message, and ConfirmModal render with correct dark mode colors via App.useApp() hooks
  • Loader blocks (Skeleton, Spinner) use antd design tokens instead of hardcoded colors
  • 404 page and loading states use theme-aware backgrounds
  • Mobile menu drawer background matches the active theme

feat: Add lowdefy upgrade command with prompt-based codemod system

Packages: lowdefy, @lowdefy/codemods

New CLI command that guides version migrations using markdown prompts. Resolves a chain of upgrade phases from current to target version, presents migration prompts in order, and tracks progress for --resume support.

CLI (lowdefy)

  • lowdefy upgrade command with --to, --plan, --resume options
  • Version chain resolver computes ordered upgrade phases from semver ranges
  • Fetches @lowdefy/codemods package from npm, presents migration prompts
  • Each prompt can be copied to clipboard for AI tools, viewed as a manual guide, or skipped
  • Upgrade state persistence in .lowdefy/upgrade-state.json for interrupted upgrades
  • Build-time warning when skipped codemods are detected

Codemods (@lowdefy/codemods)

  • v5.0 entry with 20 migration prompts
  • Covers antd v6 upgrade (14 prompts), layout grid migration (4 prompts), dayjs migration (2 prompts)
  • Self-contained markdown prompts with context, examples, edge cases, and verification steps

AG Grid blocks now follow the Ant Design theme automatically. All six grid blocks (AgGridAlpine, AgGridBalham, AgGridMaterial, and their Input variants) map ag-grid CSS variables to antd design tokens, so they respond to light/dark mode and custom theme colors without any configuration. Override individual --ag-* variables via the block's style property for per-instance customization. The explicit dark variant blocks (AgGridAlpineDark, AgGridBalhamDark, AgGridInputAlpineDark, AgGridInputBalhamDark) have been removed.

Packages: @lowdefy/blocks-aggrid

Remove per-component header.theme, sider.theme, and menu.theme string properties from PageSiderMenu, PageHeaderMenu, Header, and Sider blocks. Dark mode now works automatically via CSS variables from the root ConfigProvider — no manual theme switching needed.

Packages: @lowdefy/blocks-antd

Removed properties:

| Block | Removed Property |
| -------------- | ------------------------- |
| PageSiderMenu | properties.header.theme |
| PageSiderMenu | properties.sider.theme |
| PageSiderMenu | properties.menu.theme |
| PageHeaderMenu | properties.header.theme |
| PageHeaderMenu | properties.menu.theme |
| Header | properties.theme |
| Sider | properties.theme |

Migration: Simply remove these properties. Dark mode is handled automatically by the global ConfigProvider. Use darkModeToggle: true on page blocks or SetDarkMode action for user-facing toggle. Use properties.theme (design token object) for fine-grained color customization.

Also removed:

| Block | Removed Property | Replacement |
| -------------- | -------------------------------- | --------------------------- |
| PageSiderMenu | properties.header.style | style: { .header } |
| PageSiderMenu | properties.header.contentStyle | style: { .headerContent } |
| PageSiderMenu | properties.sider.style | style: { .sider } |
| PageSiderMenu | properties.footer.style | style: { .footer } |
| PageSiderMenu | properties.content.style | style: { .content } |
| PageSiderMenu | properties.logo.style | style: { .logo } |
| PageHeaderMenu | properties.header.style | style: { .header } |
| PageHeaderMenu | properties.header.contentStyle | style: { .headerContent } |
| PageHeaderMenu | properties.footer.style | style: { .footer } |
| PageHeaderMenu | properties.content.style | style: { .content } |
| PageHeaderMenu | properties.logo.style | style: { .logo } |

Events removed:

| Block | Removed Event | Replacement |
| -------------- | --------------------- | ----------------------------------------- |
| PageSiderMenu | onNotificationClick | Use notifications.link property instead |
| PageSiderMenu | onProfileClick | Removed |
| PageHeaderMenu | onNotificationClick | Use notifications.link property instead |
| PageHeaderMenu | onProfileClick | Removed |

Other removals:

  • collapsible and initialCollapsed properties removed from PageSiderMenu sider
  • Horizontal menu border removed from PageHeaderMenu header

Added:

  • notifications.link property for notification item navigation

Add darkModeToggle property to PageHeaderMenu and PageSiderMenu. Set darkModeToggle: true to render a built-in sun/moon toggle button in the header that switches between light and dark Ant Design themes. The preference is persisted to localStorage and respects the OS dark mode setting as default. A toggleDarkMode method is also registered for programmatic control.

Packages: @lowdefy/blocks-antd

feat(plugin-aws): Add onBeforeUpload event and improve S3 upload error handling.

Packages: @lowdefy/plugin-aws

S3UploadButton and S3UploadDragger now fire an onBeforeUpload event before each file upload begins. If any action in the event handler throws, the upload is cancelled — useful for file validation, size checks, or confirmation prompts.

Upload error handling has been rewritten: XHR uploads are now Promise-based with proper error propagation, CORS/network failures throw a ServiceError with a diagnostic message, and file metadata is serialized into a plain object so _event resolution no longer destroys File/Blob references.

Fixes & Improvements

  • Improve build error messages: schema validation errors include the property name, style/class errors suggest dot-prefixed CSS slot keys, and YAML parse errors surface immediately instead of crashing on null entries. (@lowdefy/build, @lowdefy/operators-js)

  • feat(blocks-antd): Add Search command palette block with MiniSearch. (@lowdefy/build, @lowdefy/blocks-antd)

    New Search display block provides a full-text search command palette (Cmd+K / Ctrl+K) using MiniSearch (~6KB) and antd Modal.

    • Pre-built index support: Load a static JSON index via indexUrl for zero-config search on static sites
    • Runtime indexing: Pass documents array with fields and storeFields for client-side indexing
    • Grouped results: Results auto-grouped by configurable field with section headers
    • Keyboard navigation: Arrow keys, Enter to select, Escape to close
    • Term highlighting: Matched search terms highlighted in results
    • Recent searches: localStorage-backed search history with configurable count
    • 14 CSS slots: Full style customization via styles/classNames (trigger, modal, input, results, groups, highlights)
    • Analytics-friendly events: onSelect passes the result item, search query, and resultCount for click-through tracking; onSearch passes the search term and result count on each query change

    Docs app integration

    • New search index transformer (generateSiteAssets.js) builds a MiniSearch index at build time from page content
    • Replaces Algolia DocSearch with the self-hosted Search block — removes external CDN dependency

    Removed

    • @lowdefy/blocks-algolia package has been removed. Use the Search block in @lowdefy/blocks-antd instead.
  • fix(blocks-antd): Format PhoneNumberInput phone_number value. (@lowdefy/blocks-antd)

    PhoneNumberInput now strips leading zeros and non-digit characters from user input when building the phone_number value. Typing 0821234567 with +27 selected now produces +27821234567 instead of +270821234567. Empty input produces an empty string instead of just the dial code.

  • Add color and iconsColor properties to Header block. (@lowdefy/blocks-antd)

    Set color to change the header background color (defaults to --ant-color-bg-container). Set iconsColor to control the color of notification, profile, and dark mode toggle icons — useful when using a dark background color. The iconsColor property is also available on PageHeaderMenu and PageSiderMenu.

  • Fix Anchor block icon spacing by adding marginRight: 4 to the icon style so the icon doesn't sit flush against the title text. (@lowdefy/blocks-basic)

  • feat(operators-js): Add _math.mod modulo operator. (@lowdefy/operators-js)

    Added _math.mod operator for modulo (remainder) calculations. Supports both array and named argument forms: _math.mod: [10, 3] or _math.mod: { dividend: 10, divisor: 3 }.

  • fix(operators-js): The _date operator now accepts Date objects as input, in addition to numbers and strings. (@lowdefy/operators-js)

  • refactor(plugin-aws): Migrate S3 presigned URL operations from deprecated aws-sdk v2 to modular @aws-sdk v3. (@lowdefy/plugin-aws)

    Replaced the monolithic aws-sdk package with the modular v3 packages (@aws-sdk/client-s3, @aws-sdk/s3-request-presigner, @aws-sdk/s3-presigned-post). No changes to the request/connection API — existing configs work without modification.

  • fix(helpers): Fix error serialization crash on circular structures. (@lowdefy/helpers)

    Errors with circular references in nested objects (e.g., Axios HTTP error responses containing Node.js request/response cycles) crashed the logger with "Converting circular structure to JSON" instead of logging the actual error. extractErrorProps now deep-cleans plain objects, arrays, and non-Error causes — stripping class instances, detecting circular references, and capping object depth.

v4.7.3 Security relevant
Security fixes
  • Validate session.user.roles as an array of strings to prevent silent authorization bypasses caused by misconfigured auth.userFields mappings
Notable features
  • Dev server skeleton rebuild detection uses build's ref map for accuracy
  • Runtime operators _date, _intl, and _number.toLocaleString evaluate at runtime
Full changelog

Highlights

  • Session role validation: Session roles are now validated as arrays during session assembly, catching misconfigured auth.userFields mappings early with a clear error message. Prevents silent authorization bypasses from incorrect role configuration.

  • Dev server skeleton rebuild detection: Fixed dev server to more accurately detect which file changes require skeleton rebuilds, using the build's ref map instead of file paths. Prevents unnecessary rebuilds when modifying page templates and catches changes to API endpoints.

  • Runtime operators no longer frozen at build time: Fixed _date, _intl, and _number.toLocaleString operators from being evaluated during build, which was freezing their values instead of evaluating them at runtime with the correct context.

Fixes & Improvements

  • fix(api): Validate session.user.roles is an array of strings. (@lowdefy/api)

    Misconfigured auth.userFields mapping roles to a non-array provider field (e.g., a string) caused silent authorization bypasses via String.prototype.includes substring matching. Session roles are now validated after session assembly, throwing a clear ConfigError pointing to the auth configuration. Added a defense-in-depth guard in createAuthorize for the same check.

  • fix(build,server-dev): Improved accuracy of dev server skeleton rebuild detection. (@lowdefy/build, @lowdefy/server-dev)

    The dev server previously used a path-based heuristic to decide which file changes required a skeleton rebuild. This could miss changes to API endpoints referenced from page directories, and unnecessarily rebuild for non-skeleton page templates. Skeleton rebuild classification now uses the build's ref map as the source of truth, ensuring only the correct file changes trigger skeleton rebuilds.

  • fix: Prevent _date, _intl, and _number.toLocaleString operators from being evaluated at build time. (@lowdefy/operators-js)

    These operators depend on runtime context (current date/time, locale) and were incorrectly marked as static, causing them to be evaluated during the build and freezing their values.

Weekly OSS security release digest.

The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.

No spam, unsubscribe anytime.

About

Stars
2,971
Forks
188
Languages
JavaScript CSS TypeScript
Downloads/week
959 ↓49%
NPM Maintainers
3
Contributors
30

Install & Platforms

Install via
npm

Community & Support

Beta — feedback welcome: [email protected]