From c7fc9ab079904b09b09318dd7ea6b2aa7db66142 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 08:11:05 +0200 Subject: [PATCH 01/11] docs: add design for wrap support in data tables Design to make the table `wrap` argument compatible with data tables: - Hinode renders a single table (one RenderString) for both wrap paths - Plain tables carry both layouts via responsive display utilities - Data tables get the wrapped layout from a simple-datatables tableRender hook, gated on matchMedia and re-rendered with dt.update(true) Co-Authored-By: Claude Opus 4.8 (1M context) --- ...2026-07-14-table-wrap-datatables-design.md | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-14-table-wrap-datatables-design.md diff --git a/docs/superpowers/specs/2026-07-14-table-wrap-datatables-design.md b/docs/superpowers/specs/2026-07-14-table-wrap-datatables-design.md new file mode 100644 index 00000000..f3aa7a24 --- /dev/null +++ b/docs/superpowers/specs/2026-07-14-table-wrap-datatables-design.md @@ -0,0 +1,201 @@ +# Design: `wrap` support for data tables + +Date: 2026-07-14 +Status: Approved +Repositories: `gethinode/hinode`, `gethinode/mod-simple-datatables`, `gethinode/mod-utils` + +## Problem + +Hinode's table shortcode accepts a `wrap` argument that improves the readability of wide +tables on small devices: below the site's main breakpoint, the last column moves out of the +row and onto a full-width row of its own beneath it. + +Separately, the `sortable`, `paginate` and `searchable` arguments turn the table into a data +table, driven by the optional `mod-simple-datatables` module. + +The two cannot be combined. `mod-utils` documents this explicitly — the `wrap` argument's +comment in `data/structures/_arguments.yml` ends with *"This setting is not compatible with +data tables."* This design removes that restriction. + +### Why they collide today + +`layouts/_partials/assets/table.html` implements `wrap` by rendering the table **twice**: + +- a *regular* variant, shown at or above the main breakpoint (`d-none d-{bp}-block`); +- a *wrapped* variant, shown below it (`d-{bp}-none`). + +The wrapped variant is produced by appending a `{.table-wrap}` class to the Markdown, which +`layouts/_markup/render-table.html` detects. It then drops the last `` and splits every +body row into two `` elements: one holding the first N−1 cells (bottom border suppressed +with `.table-border-bottom-wrap`) and one holding the last cell as ``. + +When any data table flag is also set, both variants receive the `.data-table` class and the +`data-table-*` attributes, so `mod-simple-datatables` initialises simple-datatables on both. +The wrapped variant's DOM is ragged — N−1 header cells, then alternating rows of N−1 and 1 +cells — which simple-datatables' uniform row model cannot represent. Sorting, paging and +searching all operate on that model, so the result is incoherent. + +## Constraints + +- **`mod-simple-datatables` is optional.** Hinode core must not depend on its JavaScript. + The wrap for plain tables therefore has to be produced by Hinode, and the wrap for data + tables has to be produced inside `mod-simple-datatables`. Two implementations of the same + visual output are structurally unavoidable; they must be kept in sync by convention. +- **Plain tables must render correctly without JavaScript** and must remain real tables for + assistive technology. This rules out a CSS-grid reflow (`display: grid` on a `` + drops its table semantics from the accessibility tree) and rules out moving the plain-table + wrap into JavaScript. +- Data tables already require JavaScript, so producing their wrapped layout in JavaScript + costs nothing. + +## Key library facts + +Verified against the vendored `simple-datatables` v10.2.0 bundle: + +- `tableRender(data, vdom, type)` is invoked with `type` of `"main"`, `"header"`, `"print"` + or `"message"`. Returning a node replaces the virtual DOM for that render. +- The `
` element's original attributes are captured once into `_tableAttributes` and + re-applied on every render, so a `data-table-wrap` marker attribute survives re-renders. +- `THEAD` is unshifted to `childNodes[0]`, ahead of `TBODY`. (`mod-simple-datatables`' + existing header-styling hook already relies on this.) +- `dt.update(true)` forces a full re-render while preserving the current sort, page and + search term. `dt.refresh()` also re-renders but clears the search, so it is not suitable + here. +- Sorting, searching and paging all run against the internal data model, never against the + rendered DOM. A `tableRender` transform is therefore purely cosmetic. + +## Design + +### 1. Hinode — `layouts/_partials/assets/table.html` + +Collapse to a **single** `RenderString` call producing a **single** `
`. Two wrap +strategies, chosen by whether the table is a data table: + +**Plain table (`wrap` set, no `sortable`/`paginate`/`searchable`):** +append `{.table-wrap}` to the Markdown as today. The render hook emits the hybrid markup +described in section 2. + +**Data table (`wrap` set together with any data table flag):** +do *not* append `{.table-wrap}`. Emit a uniform table and add `data-table-wrap="true"` plus +`data-table-wrap-breakpoint=""` alongside the existing +`data-table-*` attributes. + +The plain path needs no breakpoint attribute: it encodes the breakpoint in the `d-{bp}-*` +utility classes it emits. Both paths resolve the breakpoint from the same source +(`site.Params.main.breakpoint`), as does the SCSS in section 4, so all three switch at the +same width. + +The `$regular` / `$wrapped` duality, the second `RenderString` call, and the second +`.table-responsive` wrapper are all removed. + +**Guard:** if the resolved main breakpoint is `xs`, `wrap` is a no-op. Bootstrap has no `xs` +display-utility infix, so `d-xs-none` does not exist. (This is a latent bug in the current +implementation, not a new one.) + +### 2. Hinode — `layouts/_markup/render-table.html` + +When `.table-wrap` is present, emit one table carrying both layouts rather than a +mobile-only structure: + +```html +
+ + + + + + + + + + + + + + + + + +
NameTypeDescription
alphawidgetA long description…
A long description…
+``` + +The breakpoint infix (`md` above) comes from `site.Params.main.breakpoint`. + +Only the last column's content is duplicated, instead of the entire table. The rendered +appearance is unchanged at every width: below the breakpoint the last header and last cell +are hidden and the `colspan` row shows; at or above it, the `colspan` row is hidden and the +table reads normally. + +Consequences beyond the size reduction: the Markdown is parsed once instead of twice, and +Flexsearch no longer indexes the full text of every wrapped table twice. + +### 3. mod-simple-datatables — `assets/js/modules/simple-datatables/simple-datatables.load.js` + +For each `.data-table`, read `data-table-wrap`. When it is `"true"`: + +1. Build `window.matchMedia("(max-width: )")` from + `data-table-wrap-breakpoint`, following Bootstrap's `media-breakpoint-down` convention. +2. Install a `tableRender` hook, composed with the existing header-styling hook, that + transforms the virtual DOM **only when `type === "main"` and the media query matches**: + - remove the last `` from `THEAD`; + - for each `TBODY` row, move the last cell into a new `` inserted immediately after, + as a single ``, and add `table-border-bottom-wrap` to the N−1 cells + that remain in the original row. + Bail out unchanged when fewer than two cells are present. +3. Register a `change` listener on the media query that calls `dt.update(true)`, so crossing + the breakpoint re-renders in the other mode without losing sort, page or search state. + +`type === "message"` (the "no rows" / "no results" placeholder) and `type === "header"` (the +sticky-header clone) are left untouched. `type === "print"` is left unwrapped. + +Because the data model stays uniform, pager row counts, sort order and search results are all +correct. The existing category-filter integration continues to work unchanged; it now +registers one DataTable instance per table instead of two. + +### 4. Hinode — styling and build configuration + +- Add `main-breakpoint` (from `site.Params.main.breakpoint`, default `md`) to the `$vars` + dict in `layouts/_partials/head/stylesheet.html`, mirroring the existing `navbar-size` + entry. +- In `assets/scss/components/_table.scss`, scope the border-suppression rule under + `media-breakpoint-down(#{$main-breakpoint})` instead of applying it unconditionally. This + is now required, because a single table serves both widths. +- Add `table-border-bottom-wrap` to the PurgeCSS `standard` safelist in + `config/postcss.config.js`. In the data table path that class is added only by JavaScript, + so it never reaches `hugo_stats.json` and would otherwise be purged on any site that uses + `wrap` exclusively with data tables. + +### 5. Documentation + +- `mod-utils`: remove *"This setting is not compatible with data tables."* from the `wrap` + comment in `data/structures/_arguments.yml`. +- Hinode: add a `release` marker to `wrap` in `data/structures/table.yml`. + +## Behaviour decisions + +**The last column's header is dropped below the breakpoint**, for data tables exactly as for +plain tables. That column therefore cannot be sorted on small screens; sorting by it still +works at or above the breakpoint. This keeps wrapped data tables visually identical to +wrapped plain tables, and the wrapped column is in practice a free-text description that +nobody sorts on. + +**One DataTable instance per table.** The alternative — keeping two rendered tables and making +the wrapped one data-table-safe — would leave two live instances with independent state, so +resizing across the breakpoint would silently reset the reader's sort, page and search term, +and would double the DOM and the controls. + +## Testing + +- A wrapped data table with `sortable`, `paginate`, `searchable` and `filter` all enabled: + verify below the breakpoint that the layout wraps, the pager counts data rows (not DOM + rows), sorting keeps each description attached to its record, search matches against the + wrapped column, and the category filter still applies. +- Resize across the breakpoint with a sort, a page and a search term active: verify the + layout switches and all three survive. +- A wrapped plain table: verify the rendered appearance is unchanged from the current + implementation at both widths, and that it still renders correctly with JavaScript + disabled. +- A wrapped table with two columns (N−1 = 1) and with a single column: verify no breakage. +- A wrapped data table whose search yields no results: verify the "no results" message + renders normally. +- `npm run lint` and `npm run build:example` clean. From 2e5e3b159efebdadb44ba181f7badb77a825bf4f Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 08:28:03 +0200 Subject: [PATCH 02/11] docs: add implementation plan for wrap support in data tables Six tasks across hinode, mod-simple-datatables and mod-utils, each ending in an independently verifiable deliverable. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-07-14-table-wrap-datatables.md | 1092 +++++++++++++++++ 1 file changed, 1092 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-14-table-wrap-datatables.md diff --git a/docs/superpowers/plans/2026-07-14-table-wrap-datatables.md b/docs/superpowers/plans/2026-07-14-table-wrap-datatables.md new file mode 100644 index 00000000..ad846609 --- /dev/null +++ b/docs/superpowers/plans/2026-07-14-table-wrap-datatables.md @@ -0,0 +1,1092 @@ +# Table `wrap` Support for Data Tables — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make the table shortcode's `wrap` argument work together with `sortable`, `paginate` and `searchable`, and render plain wrapped tables from a single Hugo render pass instead of two. + +**Architecture:** Hinode renders exactly one `` per shortcode invocation. A *plain* wrapped table carries both layouts in that one table, switching with Bootstrap display utilities, so it needs no JavaScript. A *data table* is rendered uniformly and marked with `data-table-wrap`; `mod-simple-datatables` then produces the wrapped layout below the breakpoint through simple-datatables' `tableRender` hook, which rewrites the virtual DOM and never the row model — so sorting, searching and paging stay correct. + +**Tech Stack:** Hugo templates and render hooks, Bootstrap 5.3.8 SCSS, simple-datatables v10.2.0, PostCSS/PurgeCSS, Playwright for browser verification. + +## Global Constraints + +- Spec: `docs/superpowers/specs/2026-07-14-table-wrap-datatables-design.md`. Read it first. +- Three repositories, all siblings under `/Users/mark/Development/GitHub/gethinode/`: + `hinode` (branch `feat/table-wrap-datatables`, already created), `mod-simple-datatables`, + `mod-utils`. +- The site's main breakpoint is `site.Params.main.breakpoint`, default `md`. It is the single + source of truth for the wrap switch — the render hook, the SCSS and the JavaScript must all + derive from it. It is **not** the table shortcode's own `breakpoint` argument, which controls + `.table-responsive-{bp}` and is unrelated. +- Bootstrap has no `xs` infix for display utilities. When the main breakpoint resolves to `xs`, + `wrap` is a no-op. +- `layouts/_partials/assets/args.html:83` calls the table partial with `"wrap" true` — the + documentation argument tables are a production consumer of the plain wrap path. Its output must + stay visually identical. +- Commits follow Angular Conventional Commits (commitlint pre-commit hook). Body lines must not + exceed 100 characters. Types: `feat`, `fix`, `refactor`, `style`, `docs`, `chore`. +- Per `CLAUDE.md`: never use the `or` operator as fallback logic for boolean arguments — read + `$args.wrap` directly, because `or false ` returns the fallback. +- **Never run `npm run build:example` or `npm run start:example` while local module overrides are + active.** Their `prestart`/`prebuild` hooks re-vendor modules from the remote and silently wipe + the local `mod-simple-datatables` changes. Use the project-pinned binary `node_modules/.bin/hugo` + directly. The system `hugo` is older and fails with `"modulequeries" is not a valid cache name`. +- Do not start a second `hugo server` against `exampleSite` while the user's dev server is + running — both write to the shared `hinode/resources/_gen`, and a bad purge gets cached and + served to the other server. Check with `lsof -i :1313` first. +- Every task references `$SCRATCH`. Export it once per shell: + + ```bash + export SCRATCH=/private/tmp/claude-501/-Users-mark-Development-GitHub-gethinode-hinode/491eb2c0-b0cd-401d-9256-134afea0e318/scratchpad + ``` + +## File Structure + +| File | Repo | Responsibility | +| --- | --- | --- | +| `exampleSite/content/en/table-demo.md` | hinode | Fixture page exercising every wrap case. Committed; it is the regression target. | +| `layouts/_markup/render-table.html` | hinode | Emits the single-table hybrid markup for plain wrapped tables. | +| `layouts/_partials/assets/table.html` | hinode | One `RenderString`, one `
`; picks the plain or the data table wrap strategy. | +| `assets/scss/components/_table.scss` | hinode | Breakpoint-scoped border suppression; striping recomputed per record pair. | +| `assets/scss/common/_variables-dart.scss` | hinode | Re-exports `$main-breakpoint` for the dart-sass entry point. | +| `layouts/_partials/head/stylesheet.html` | hinode | Injects `main-breakpoint` into the SCSS vars. | +| `config/postcss.config.js` | hinode | Safelists the two classes that only JavaScript ever emits. | +| `assets/js/modules/simple-datatables/simple-datatables.load.js` | mod-simple-datatables | `tableRender` wrap hook + `matchMedia` re-render. | +| `data/structures/_arguments.yml` | mod-utils | Drops the "not compatible with data tables" note. | + +## Known gap, deliberately not addressed + +The table shortcode does not forward the `filter` / `filter-col` arguments to the partial — +`layouts/_shortcodes/table.html` simply omits them — so the category filter is unreachable from +Markdown and cannot be exercised from the exampleSite. This is pre-existing and out of scope. Task 4 +therefore leaves the filter code path in `simple-datatables.load.js` functionally untouched and only +corrects its now-stale comment. Flag it to the user as a follow-up. + +--- + +### Task 1: Fixture page and local module workspace + +Sets up everything later tasks verify against, and pins down exactly how the current code fails. + +**Files:** + +- Create: `exampleSite/content/en/table-demo.md` +- Modify: `exampleSite/hinode.work` (local only — **must be reverted in Task 6, never committed**) + +**Interfaces:** + +- Produces: the page `exampleSite/public/en/table-demo/index.html`, holding three tables tagged with + the marker classes `fixture-plain`, `fixture-data` and `fixture-two-col`. Every later task selects + them by those classes. (`id` is *not* a valid table argument — `InitArgs` validates against + `data/structures/table.yml`, which does not list it — so the `class` argument carries the marker.) + +- [ ] **Step 1: Confirm no Hugo server is running** + +```bash +lsof -i :1313 || echo "clear" +``` + +Expected: `clear`. If a server is listed, stop and ask the user to shut it down — building alongside +it can poison the shared CSS cache. + +- [ ] **Step 2: Point the exampleSite at the local modules** + +`exampleSite/config/_default/hugo.toml:112` already sets `workspace = "hinode.work"`, so Hugo reads +the workspace file. It currently contains: + +```text +go 1.19 + +use . +use ../ +``` + +Rewrite it as: + +```text +go 1.19 + +use . +use ../ +use ../../mod-simple-datatables +use ../../mod-utils +``` + +Workspace `use` directives take precedence over the vendored copies, so Hugo reads both modules +straight from the working tree. + +- [ ] **Step 3: Put the local mod-simple-datatables checkout on a feature branch** + +The local clone currently sits on `develop`, which is stale: its `go.mod` still declares +`github.com/gethinode/mod-simple-datatables/v3`, while Hinode requires `/v4`. The workspace `use` +directive will not resolve until this is fixed. + +```bash +git -C ../mod-simple-datatables checkout -b feat/table-wrap origin/main +head -1 ../mod-simple-datatables/go.mod +``` + +Expected: `module github.com/gethinode/mod-simple-datatables/v4` + +- [ ] **Step 4: Put the local mod-utils checkout on a feature branch** + +```bash +git -C ../mod-utils checkout -b docs/table-wrap-datatables origin/main +``` + +- [ ] **Step 5: Write the fixture page** + +Create `exampleSite/content/en/table-demo.md`. The marker classes ride on the `class` argument; +`fixture-plain` also carries `table-striped`, because striping is the case most likely to regress. + +````markdown +--- +title: Table Demo +--- + +## Plain wrapped table + +{{< table wrap="true" class="table-striped fixture-plain" >}} +| Name | Type | Description | +|---------|--------|--------------------------------------------------------------------| +| alpha | widget | The first record, with a description long enough to need wrapping. | +| bravo | gadget | The second record, also with a fairly long trailing description. | +| charlie | widget | The third record. Short. | +| delta | gadget | The fourth record, whose description runs on for a little while. | +{{< /table >}} + +## Wrapped data table + +{{< table wrap="true" sortable="true" searchable="true" paginate="true" pagination="2" class="fixture-data" >}} +| Name | Type | Description | +|---------|--------|--------------------------------------------------------------------| +| alpha | widget | The first record, with a description long enough to need wrapping. | +| bravo | gadget | The second record, also with a fairly long trailing description. | +| charlie | widget | The third record. Short. | +| delta | gadget | The fourth record, whose description runs on for a little while. | +{{< /table >}} + +## Two-column wrapped table + +{{< table wrap="true" class="fixture-two-col" >}} +| Name | Description | +|-------|--------------------------------------------------------------------| +| alpha | The first record, with a description long enough to need wrapping. | +| bravo | The second record, also with a fairly long trailing description. | +{{< /table >}} +```` + +- [ ] **Step 6: Write the verification script** + +Create `$SCRATCH/verify-table-wrap.mjs`. It is a scratch file — do **not** commit it. + +```js +import { readFileSync } from 'node:fs' + +const html = readFileSync('exampleSite/public/en/table-demo/index.html', 'utf8') + +const checks = [] +const check = (name, pass, detail = '') => checks.push({ name, pass, detail }) + +// Each shortcode must render exactly one table. The duplicate-render approach produced two +// per wrapped shortcode, so the current code yields six. +const tables = html.match(/]/g) ?? [] +check('one table per shortcode', tables.length === 3, `found ${tables.length}`) + +check( + 'no duplicate-render wrappers', + !/d-none d-md-block/.test(html) +) + +// --- Plain wrapped tables carry both layouts in one table --- + +check( + 'plain wrap hides the last header below the breakpoint', + //g) ?? [] +check( + 'plain wrap emits one colspan row per record', + colspanRows.length === 6, + `found ${colspanRows.length}, expected 6 (4 plain + 2 two-column)` +) +check('plain wrap spans the remaining columns', /` holding one `
\s*Description/.test(html) +) +check( + 'plain wrap suppresses the data row border', + /class="table-border-bottom-wrap"/.test(html) +) +// Four records in the plain table plus two in the two-column table. +const colspanRows = html.match(/
/)?.[0] ?? '' +check('data table is rendered', dataTable.length > 0) +check('data table declares the wrap', /data-table-wrap=true/.test(dataTable)) +check( + 'data table declares the wrap breakpoint', + /data-table-wrap-breakpoint=md/.test(dataTable) +) +check( + 'data table DOM stays uniform', + !/table-border-bottom-wrap/.test(dataTable) && !/d-md-none/.test(dataTable), + 'the split rows must come from tableRender, not from Hugo' +) +check( + 'data table is not marked table-wrap server-side', + !/ tbody > tr:nth-of-type(n) > * { + --bs-table-color-type: initial; + --bs-table-bg-type: initial; + } + + > tbody > tr:nth-of-type(4n + 1) > * { + --bs-table-color-type: var(--bs-table-striped-color); + --bs-table-bg-type: var(--bs-table-striped-bg); + } +} + +@include media-breakpoint-down(#{$main-breakpoint}) { + // Below the breakpoint the data row and the row holding the wrapped column must read as a + // single record, so the data row drops its bottom border. + .table-border-bottom-wrap { + border-bottom-style: none !important; + } + + // Here the second row of each pair is visible too, so it joins its record's stripe. + .table-wrap.table-striped > tbody > tr:nth-of-type(4n + 2) > * { + --bs-table-color-type: var(--bs-table-striped-color); + --bs-table-bg-type: var(--bs-table-striped-bg); + } +} +``` + +Two things matter here. The `:nth-of-type(n)` reset and the `4n + 1` rule have identical +specificity, so the reset **must** come first; likewise the media-query block must follow the base +block. And the `--bs-` custom properties are written out literally rather than interpolated from +`$prefix`, matching the dark-mode block further down the same file. + +Why hidden rows still matter: `:nth-of-type` counts elements regardless of `display: none`, so at +desktop width the hidden `d-md-none` rows shift Bootstrap's `odd` selector onto *every* data row. + +- [ ] **Step 4: Safelist the JavaScript-only classes** + +In `config/postcss.config.js`, add to the `standard` safelist array, after the +`// SimpleDatatables table rendering classes (added by JS)` group: + +```js + // Hinode wrapped tables. On a data table both classes are added by SimpleDatatables' + // tableRender hook, so they never reach hugo_stats.json and would otherwise be purged. + 'table-wrap', + 'table-border-bottom-wrap', +``` + +- [ ] **Step 5: Build and confirm the CSS survives purging** + +The stylesheet is fingerprinted in a production build, so glob the directory rather than naming the +file. + +```bash +node_modules/.bin/hugo --gc -s exampleSite --logLevel warn \ + && grep -c "table-border-bottom-wrap" exampleSite/public/css/*.css \ + && grep -c "767.98" exampleSite/public/css/*.css +``` + +Expected: non-zero counts for both. `767.98` confirms `media-breakpoint-down(md)` resolved into a +`max-width` query. A count of `0` for `table-border-bottom-wrap` means PurgeCSS stripped it — +recheck the safelist edit. + +- [ ] **Step 6: Lint** + +```bash +npm run lint:styles +``` + +Expected: no errors. + +- [ ] **Step 7: Commit** + +```bash +git add layouts/_partials/head/stylesheet.html assets/scss/common/_variables-dart.scss \ + assets/scss/components/_table.scss config/postcss.config.js +git commit -m "style(table): scope wrap styling to the main breakpoint + +A wrapped table will render as a single table serving both widths, so the +border suppression has to be scoped to the breakpoint rather than applied +unconditionally, and Bootstrap's odd-row striping has to be recomputed per +record pair. Safelist both wrap classes: on a data table they are only ever +added by JavaScript." +``` + +--- + +### Task 3: One table per shortcode + +Rewrites the render hook and the table partial so `wrap` produces a single table, and marks data +tables for the JavaScript hook added in Task 4. This is the task that makes Task 1's verification +script pass. + +**Files:** + +- Modify: `layouts/_markup/render-table.html` (full rewrite) +- Modify: `layouts/_partials/assets/table.html` (full rewrite) + +**Interfaces:** + +- Consumes: `table-wrap`, `table-border-bottom-wrap` (Task 2). +- Produces: on a wrapped data table, the attributes `data-table-wrap="true"` and + `data-table-wrap-breakpoint=""`, where `` is a Bootstrap breakpoint key + (`sm`/`md`/`lg`/`xl`/`xxl`). Task 4 reads both. + +- [ ] **Step 1: Rewrite the render hook** + +Replace the whole of `layouts/_markup/render-table.html` with: + +```hugo +{{/* Adapted from https://gohugo.io/render-hooks/tables/ */}} + +{{/* Wrapping moves the last column onto a row of its own below the site's main breakpoint. It is a + no-op at `xs` (Bootstrap has no `xs` display-utility infix) and on a single-column table + (nothing to wrap). The marker class is dropped when it does not apply, so the wrap-specific + striping rules cannot misfire on a table that renders normally. */}} +{{ $breakpoint := site.Params.main.breakpoint | default "md" }} +{{ $cols := 0 }} +{{ range .THead }}{{ $cols = len . }}{{ end }} + +{{ $attr := .Attributes }} +{{ $given := split (or $attr.class "") " " }} +{{ $wrap := and (in $given "table-wrap") (ne $breakpoint "xs") (gt $cols 1) }} + +{{/* Rebuild the class list: drop empties, drop `table-wrap` when it does not apply, and ensure + `table` is present so Bootstrap styles the element. */}} +{{ $classes := slice }} +{{ range $given }} + {{ if and (ne . "") (or $wrap (ne . "table-wrap")) }} + {{ $classes = $classes | append . }} + {{ end }} +{{ end }} +{{ if not (in $classes "table") }}{{ $classes = $classes | append "table" }}{{ end }} +{{ $attr = merge $attr (dict "class" (delimit ($classes | uniq) " ")) }} + +{{ $align := dict "left" "start" "center" "center" "right" "end" }} + +
+ + {{- range .THead }} + {{- $length := len . }} + + {{- range $i, $cell := . }} + {{- $cellClasses := slice }} + {{- with $cell.Alignment }} + {{- $cellClasses = $cellClasses | append (printf "text-%s" (index $align .)) }} + {{- end }} + {{- if and $wrap (eq $i (sub $length 1)) }} + {{- $cellClasses = $cellClasses | append "d-none" (printf "d-%s-table-cell" $breakpoint) }} + {{- end }} + + {{- $cell.Text -}} + + {{- end }} + + {{- end }} + + + {{- range .TBody }} + {{- $length := len . }} + + {{- range $i, $cell := . }} + {{- $cellClasses := slice }} + {{- with $cell.Alignment }} + {{- $cellClasses = $cellClasses | append (printf "text-%s" (index $align .)) }} + {{- end }} + {{- if $wrap }} + {{- if eq $i (sub $length 1) }} + {{- $cellClasses = $cellClasses | append "d-none" (printf "d-%s-table-cell" $breakpoint) }} + {{- else }} + {{- $cellClasses = $cellClasses | append "table-border-bottom-wrap" }} + {{- end }} + {{- end }} + + {{- $cell.Text -}} + + {{- end }} + + {{- if $wrap }} + {{- $last := index . (sub $length 1) }} + + + + {{- end }} + {{- end }} + +
+ {{- $last.Text -}} +
+``` + +(`slice` with no arguments returns an empty slice — `mod-utils`' own `InitArgs.html` uses the same +idiom, so it is safe.) + +- [ ] **Step 2: Rewrite the table partial** + +Replace the whole of `layouts/_partials/assets/table.html` with: + +```hugo +{{/* + Copyright © 2022 - 2026 The Hinode Team / Mark Dumay. All rights reserved. + Use of this source code is governed by The MIT License (MIT) that can be found in the LICENSE file. + Visit gethinode.com/license for more details. +*/}} + +{{/* Initialize arguments */}} +{{ $args := partial "utilities/InitArgs.html" (dict "structure" "table" "args" . "group" "partial")}} +{{ if or $args.err $args.warnmsg }} + {{ partial (cond $args.err "utilities/LogErr.html" "utilities/LogWarn.html") (dict + "partial" "assets/table.html" + "warnid" "warn-invalid-arguments" + "msg" "Invalid arguments" + "details" ($args.errmsg | append $args.warnmsg) + "file" page.File + )}} +{{ end }} + +{{/* Initialize local variables */}} +{{ $breakpoint := site.Params.main.breakpoint | default "md" }} +{{ $dataTable := or $args.sortable $args.paginate $args.searchable }} +{{ $wrap := and $args.wrap (ne $breakpoint "xs") }} + +{{ $class := or $args.class "" }} +{{ if $dataTable }}{{ $class = trim (printf "%s data-table" $class) " " }}{{ end }} + +{{ $attributes := "" }} +{{ if $args.sortable }}{{ $attributes = printf "%s data-table-sortable=true" $attributes }}{{ end }} +{{ if $args.paginate }} + {{ $pagination := or $args.pagination $args.pagingOptionPerPage | default 10 }} + {{ $select := or $args.paginationSelect $args.pagingOptionPageSelect }} + {{ $attributes = printf "%s data-table-paging=true" $attributes }} + {{ $attributes = printf "%s data-table-paging-option-perPage=%d" $attributes $pagination }} + {{ with $select }} + {{ $attributes = printf "%s data-table-paging-option-perPageSelect=%s" $attributes . }} + {{ end }} +{{ end }} +{{ if $args.searchable }}{{ $attributes = printf "%s data-table-searchable=true" $attributes }}{{ end }} + +{{/* A wrapped data table is rendered uniformly: simple-datatables builds its row model from this + DOM, and applies the wrapped layout below the breakpoint through its tableRender hook. A plain + wrapped table carries both layouts in a single table, switched with display utilities, so it + needs no JavaScript. */}} +{{ if and $wrap $dataTable }} + {{ $attributes = printf "%s data-table-wrap=true data-table-wrap-breakpoint=%s" $attributes $breakpoint }} +{{ end }} + +{{/* Filter */}} +{{ $filter := $args.filter }} +{{ $filterCol := $args.filterCol | default 1 }} +{{ $filterId := "" }} +{{ if $filter }} + {{ $filterId = printf "table-filter-%s" (md5 (delimit (slice . now) "-")) }} + {{ $attributes = printf "%s data-filter-col=%d data-filter-id=%s" $attributes $filterCol $filterId }} +{{ end }} + +{{/* Main code */}} +{{ if not $args.err }} + {{ $input := $args.input }} + {{ if and $wrap (not $dataTable) }}{{ $input = printf "%s\n{.table-wrap}" (chomp $input) }}{{ end }} + {{ $input = $input | $args.page.RenderString }} + + {{ $regex := ` +
+ + {{ range $filter }} + + {{ end }} +
+ + {{ end }} + + {{ if eq $args.breakpoint "none" }} + {{ if $wrapper }} +
{{ $input | safeHTML }}
+ {{ else }} + {{ $input | safeHTML }} + {{ end }} + {{ else }} +
+ {{ $input | safeHTML }} +
+ {{ end }} +{{ end }} +``` + +The `inline/table.html` define block is gone, along with the `$regular` / `$wrapped` pair, the second +`RenderString` call and the second `.table-responsive` wrapper. `$args.wrap` is read directly rather +than through `or`, because `or false ` would discard an explicit `false`. + +- [ ] **Step 3: Build and run the verification script** + +```bash +node_modules/.bin/hugo --gc -s exampleSite --logLevel warn \ + && node "$SCRATCH/verify-table-wrap.mjs" +``` + +Expected: **all checks PASS**. + +- [ ] **Step 4: Sanity-check the documentation argument tables** + +`layouts/_partials/assets/args.html:83` calls this partial with `"wrap" true` and no data table +flags, so the argument tables on gethinode.com take the plain path. Confirm the fixture's plain +table has, for each record, a data row of three `
` cells followed by a +`
`, and three `` cells of which the last +carries `d-none d-md-table-cell`. + +- [ ] **Step 5: Lint and commit** + +```bash +npm run lint +git add layouts/_markup/render-table.html layouts/_partials/assets/table.html +git commit -m "refactor(table): render one table per shortcode + +A wrapped table was rendered twice - once regular, once wrapped - and both +copies were emitted into the page. Render it once instead: a plain table now +carries both layouts and switches with display utilities, and a data table is +rendered uniformly and marked with data-table-wrap so simple-datatables can +apply the wrapped layout itself. + +This halves the markup of a wrapped table, parses the Markdown once instead of +twice, and stops Flexsearch indexing every wrapped table twice." +``` + +--- + +### Task 4: Wrapped data tables + +The `tableRender` hook in `mod-simple-datatables`. Committed in that repository. + +**Files:** + +- Modify: `../mod-simple-datatables/assets/js/modules/simple-datatables/simple-datatables.load.js` + +**Interfaces:** + +- Consumes: `data-table-wrap` and `data-table-wrap-breakpoint` on the `` (Task 3); the CSS + classes `table-wrap` and `table-border-bottom-wrap` (Task 2). + +- [ ] **Step 1: Extract the existing header styling into a named function** + +`tableOptions` currently holds an inline `tableRender`. Pull it out so it can be composed with the +wrap hook: delete the `tableRender: (_data, table, _type) => { … }` property from `tableOptions`, +and add this function above `tableOptions`: + +```js +// Applies Bootstrap's header markup to simple-datatables' virtual DOM. +const styleHeader = (table) => { + const thead = table.childNodes[0] + thead.childNodes[0].childNodes.forEach(th => { + if (!th.attributes) { + th.attributes = {} + } + th.attributes.scope = "col" + const innerHeader = th.childNodes[0] + if (!innerHeader.attributes) { + innerHeader.attributes = {} + } + let innerHeaderClass = innerHeader.attributes.class ? `${innerHeader.attributes.class} th-inner` : "th-inner" + + if (innerHeader.nodeName === "a") { + innerHeaderClass += " sortable sortable-center both" + if (th.attributes.class?.includes("desc")) { + innerHeaderClass += " desc" + } else if (th.attributes.class?.includes("asc")) { + innerHeaderClass += " asc" + } + } + innerHeader.attributes.class = innerHeaderClass + }) + return table +} +``` + +- [ ] **Step 2: Add the wrap transform** + +Below `styleHeader`, add: + +```js +// Mirrors Bootstrap's $grid-breakpoints. A wrapped table switches layout below the site's main +// breakpoint, following Bootstrap's `media-breakpoint-down` convention. +const breakpoints = { xs: 0, sm: 576, md: 768, lg: 992, xl: 1200, xxl: 1400 } + +// Moves the last column of every record onto a row of its own, so wide tables stay readable on +// small devices. This rewrites the virtual DOM only - never the row model - so sorting, searching +// and paging keep operating on the unmodified data, and the pager keeps counting records rather +// than rendered rows. +const wrapLastColumn = (table) => { + const thead = table.childNodes[0] + const tbody = table.childNodes[1] + const headerRow = thead?.childNodes[0] + if (!headerRow || headerRow.childNodes.length < 2 || !tbody) { + return table + } + + headerRow.childNodes = headerRow.childNodes.slice(0, -1) + + // Marks the table for the wrap-specific striping rules in Hinode's SCSS. + table.attributes = { + ...table.attributes, + class: `${table.attributes?.class ?? ""} table-wrap`.trim() + } + + tbody.childNodes = tbody.childNodes.flatMap(row => { + const cells = row.childNodes + if (!cells || cells.length < 2) { + return [row] + } + const span = cells.length - 1 + const last = cells[span] + + return [ + { + ...row, + childNodes: cells.slice(0, span).map(cell => ({ + ...cell, + attributes: { + ...cell.attributes, + class: `${cell.attributes?.class ?? ""} table-border-bottom-wrap`.trim() + } + })) + }, + { + nodeName: "TR", + childNodes: [{ + ...last, + attributes: { ...last.attributes, colspan: String(span) } + }] + } + ] + }) + + return table +} +``` + +- [ ] **Step 3: Give each table its own options and wire up the hook** + +Replace the body of the `document.querySelectorAll('.data-table').forEach(...)` loop. `tableOptions` +becomes a template that each table copies, because each table now needs its own `tableRender` +closure: + +```js +document.querySelectorAll('.data-table').forEach(tbl => { + let perPageSelectAttr = tbl.getAttribute('data-table-paging-option-perPageSelect'); + let perPageSelect; + if (perPageSelectAttr) { + try { + perPageSelect = JSON.parse(perPageSelectAttr); + } catch (e) { + console.error('Error parsing perPageSelect, use default value:', e); + perPageSelect = [5, 10, 20, 50, ["{{ T "tablePerPageSelectAll" }}", -1]]; + } + } else { + perPageSelect = [5, 10, 20, 50, ["{{ T "tablePerPageSelectAll" }}", -1]]; + } + + const options = { + ...tableOptions, + sortable: (tbl.getAttribute('data-table-sortable') === 'true'), + paging: (tbl.getAttribute('data-table-paging') === 'true'), + searchable: (tbl.getAttribute('data-table-searchable') === 'true'), + perPage: parseInt(tbl.getAttribute('data-table-paging-option-perPage')) || 10, + perPageSelect: perPageSelect + } + + // A wrapped table renders its last column on a row of its own below the main breakpoint. At + // `xs` the max-width evaluates below zero, so the query never matches and wrapping is off - + // matching Hinode, which does not emit the attribute at `xs` either. + let media = null + if (tbl.getAttribute('data-table-wrap') === 'true') { + const name = tbl.getAttribute('data-table-wrap-breakpoint') || 'md' + const width = breakpoints[name] ?? breakpoints.md + media = window.matchMedia(`(max-width: ${width - 0.02}px)`) + } + + options.tableRender = (_data, table, type) => { + const rendered = styleHeader(table) + // 'header' is the sticky-header clone, 'message' the no-rows placeholder and 'print' the + // print view - none of them wrap. + if (type !== 'main' || !media?.matches) { + return rendered + } + return wrapLastColumn(rendered) + } + + const dt = new window.simpleDatatables.DataTable(tbl, options) + + // Redraw on the other side of the breakpoint. `update(true)` keeps the active sort, page and + // search term; `refresh()` would clear the search. + if (media) { + media.addEventListener('change', () => dt.update(true)) + } + + // Register instance for category filter integration + const filterId = tbl.getAttribute('data-filter-id') + if (filterId) { + const filterCol = parseInt(tbl.getAttribute('data-filter-col') ?? '1') + if (!tableFilterInstances[filterId]) tableFilterInstances[filterId] = [] + tableFilterInstances[filterId].push({ dt, filterCol }) + } +}) +``` + +- [ ] **Step 4: Correct the stale filter comment** + +The comment above `tableFilterInstances` reads *"An array is used per ID because both the regular and +responsive-wrapped table variants need to be filtered together."* That is no longer true — a wrapped +table is now a single table with a single instance. Replace it with: + +```js +// Track DataTable instances keyed by filter container ID. An array is used per ID so several +// tables can share one filter button group. +const tableFilterInstances = {} +``` + +Leave the filter logic itself untouched. + +- [ ] **Step 5: Lint the module** + +```bash +(cd ../mod-simple-datatables && npx eslint assets/js --no-error-on-unmatched-pattern) +``` + +Expected: no errors. The file is a Hugo template (it contains `{{ T "…" }}`), so if that repository +has no ESLint configuration covering it, skip this step rather than fighting it. + +- [ ] **Step 6: Rebuild Hinode's exampleSite and confirm the HTML is unchanged** + +The transform runs in the browser, so the generated HTML must still show a uniform data table. + +```bash +node_modules/.bin/hugo --gc -s exampleSite --logLevel warn \ + && node "$SCRATCH/verify-table-wrap.mjs" +``` + +Expected: all checks still PASS. + +- [ ] **Step 7: Commit in mod-simple-datatables** + +```bash +git -C ../mod-simple-datatables add assets/js/modules/simple-datatables/simple-datatables.load.js +git -C ../mod-simple-datatables commit -m "feat(table): support Hinode's wrap argument + +Wrap the last column onto a row of its own below the main breakpoint when the +table declares data-table-wrap. The transform runs in simple-datatables' +tableRender hook, so it rewrites the virtual DOM and never the row model: +sorting, searching and paging keep operating on the unmodified data, and the +pager keeps counting records rather than rendered rows. + +Crossing the breakpoint redraws through update(true), which preserves the +active sort, page and search term." +``` + +--- + +### Task 5: Browser verification + +Nothing so far proves the wrapped data table actually behaves. This task drives it. + +**Files:** none — verification only. + +- [ ] **Step 1: Start a Hugo server on an isolated cache** + +```bash +HUGO_RESOURCEDIR="$SCRATCH/resources" HUGO_CACHEDIR="$SCRATCH/cache" \ + node_modules/.bin/hugo server -s exampleSite --port 1314 --disableFastRender +``` + +Run it in the background. The isolated resource directory keeps this build from touching the shared +`hinode/resources/_gen`. + +- [ ] **Step 2: Check the data table at desktop width** + +With the Playwright MCP tools, navigate to `http://localhost:1314/en/table-demo/` and resize to +1280×900. On `.fixture-data`: + +- Three column headers, all sortable. +- No `table-border-bottom-wrap` cells and no `colspan` rows — it renders as an ordinary table. +- The pager shows **2** pages: 4 records at 2 per page. + +- [ ] **Step 3: Establish state, then cross the breakpoint** + +- Sort by Name descending; confirm `delta` is first. +- Type `bravo` into the search box; confirm one record matches. Clear it. +- Go to page 2. +- Resize to 400×900. + +Verify: the layout wraps, **and the sort is still descending and the page is still 2**. This is what +`update(true)` buys over `refresh()` — if either resets, the wrong method is being called. + +- [ ] **Step 4: Exercise the data table at mobile width** + +Still at 400×900, on `.fixture-data`: + +- Two column headers (`Name`, `Type`); the `Description` header is gone. +- Each record renders as two rows: a data row of two cells, then a row with `colspan="2"`. +- The pager still shows **2** pages. It counts records, not the four rendered rows per page — **a + pager reading 4 pages means the transform leaked into the row model.** +- Sorting by Name keeps each description attached to its own record. +- Searching for a word that appears **only in a description** still matches, proving the wrapped + column is still in the searchable data. + +- [ ] **Step 5: Check the empty state** + +On `.fixture-data`, search for `zzzz`. The "no results" message must render as a normal single row, +not a mangled one — this exercises the `type === 'message'` guard. + +- [ ] **Step 6: Check the plain tables** + +- `.fixture-plain` at 400×900: wrapped layout, and each record's two rows share one stripe — stripes + alternate per record, not per row. +- `.fixture-plain` at 1280×900: three columns, no visible description rows, stripes alternating per + record. **Every row striped means the `4n + 1` override from Task 2 is not applying.** +- `.fixture-two-col` at 400×900: one column plus a full-width description row, with `colspan="1"`. + +- [ ] **Step 7: Confirm the plain table needs no JavaScript** + +Reload with JavaScript disabled and check `.fixture-plain` at both widths. The layout must still +switch — it is pure CSS. The data table will render unwrapped and unsorted; that is expected, it +already required JavaScript. + +- [ ] **Step 8: Stop the server and report** + +Report any failure rather than papering over it. + +--- + +### Task 6: Documentation and clean-up + +**Files:** + +- Modify: `../mod-utils/data/structures/_arguments.yml:1687-1692` +- Modify: `exampleSite/hinode.work` (revert) +- Modify: `hugo_stats.json`, `exampleSite/hugo_stats.json` + +- [ ] **Step 1: Drop the incompatibility note in mod-utils** + +The `wrap` entry currently reads: + +```yaml + wrap: + type: bool + optional: true + comment: >- + Toggle the last column to wrap to a new row on smaller devices. This + setting is not compatible with data tables. +``` + +Replace it with: + +```yaml + wrap: + type: bool + optional: true + comment: >- + Toggle the last column to wrap to a new row on smaller devices. +``` + +```bash +git -C ../mod-utils add data/structures/_arguments.yml +git -C ../mod-utils commit -m "docs(arguments): drop the wrap/data-table incompatibility note + +Hinode and mod-simple-datatables now render a wrapped data table through +simple-datatables' tableRender hook, so the two arguments combine." +``` + +The spec also called for a `release` marker on `wrap` in Hinode's `data/structures/table.yml`. That +field records the version in which an **argument** was introduced, and `wrap` long predates this +change, so a marker would misdate it. Deliberately omitted — raise it with the user if they disagree. + +- [ ] **Step 2: Revert the workspace overrides** + +```bash +git checkout exampleSite/hinode.work +git diff --stat exampleSite/hinode.work +``` + +Expected: no output — the file is clean and back to `use .` / `use ../`. + +- [ ] **Step 3: Rebuild against the vendored modules** + +```bash +node_modules/.bin/hugo --gc -s exampleSite --logLevel warn +git status --short +``` + +This build uses the **vendored** mod-simple-datatables, which does not yet carry Task 4, so the +wrapped data table renders unwrapped here. That is expected until the module is released and +re-vendored. The plain wrapped tables must still be correct — re-run the verification script; every +check except the browser behaviour still passes, because they all assert on Hugo's output. + +- [ ] **Step 4: Commit the build stats** + +`hugo_stats.json` is tracked and feeds PurgeCSS. The fixture page adds classes to it. + +```bash +git add hugo_stats.json exampleSite/hugo_stats.json +git commit -m "chore: update build stats" +``` + +- [ ] **Step 5: Run the full lint suite** + +```bash +npm test +``` + +Expected: no errors from ESLint, Stylelint or markdownlint. + +- [ ] **Step 6: Confirm nothing stray is committed** + +```bash +git log --oneline main..HEAD +git diff --stat main..HEAD +``` + +Expected: the fixture, the SCSS and build plumbing, the render rework, and the build stats. No +`hinode.work`, no scratchpad script. + +- [ ] **Step 7: Report the release ordering** + +Summarise for the user: three branches (`hinode:feat/table-wrap-datatables`, +`mod-simple-datatables:feat/table-wrap`, `mod-utils:docs/table-wrap-datatables`). +`mod-simple-datatables` must be released and re-vendored into Hinode before wrapped data tables work +on a real site, so Hinode's `go.mod` needs a bump to the new minor once it ships. + +--- + +## Notes for the reviewer + +- **Striping.** The spec did not mention it, but adding a second `` per record breaks Bootstrap's + `.table-striped`, which selects `tr:nth-of-type(odd)`. Hidden rows still count for `:nth-of-type`, + so without the override in Task 2 a wrapped table would stripe *every* record at desktop width. The + `4n + 1` / `4n + 2` rules restore per-record striping, and as a side effect fix today's behaviour + below the breakpoint, where a description row is left unstriped while its data row is striped. +- **`table-wrap` is now load-bearing**, not merely a render-hook marker: the striping rules key off + it. The render hook therefore strips it when wrapping does not apply (`xs`, single column), and the + JavaScript adds it when it does. +- **The `xs` guard** fixes a latent bug — the current code emits `d-xs-none`, which Bootstrap does + not generate. +- **`filter` is unreachable from the shortcode** (see "Known gap" above), so the filter-plus-wrap + combination could not be exercised end to end. The code path is preserved unchanged. From 49f2b94cc1bb3e0027c552031e7308072a24bec0 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:32:20 +0200 Subject: [PATCH 03/11] test(table): add exampleSite fixture for wrapped tables Exercises a plain wrapped table with striping, a wrapped data table, and a two-column wrapped table. Regression target for the wrap rework. --- exampleSite/content/en/table-demo.md | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 exampleSite/content/en/table-demo.md diff --git a/exampleSite/content/en/table-demo.md b/exampleSite/content/en/table-demo.md new file mode 100644 index 00000000..9accf9d2 --- /dev/null +++ b/exampleSite/content/en/table-demo.md @@ -0,0 +1,37 @@ +--- +title: Table Demo +--- + +## Plain wrapped table + +{{< table wrap="true" class="table-striped fixture-plain" >}} + +| Name | Type | Description | +|---------|--------|--------------------------------------------------------------------| +| alpha | widget | The first record, with a description long enough to need wrapping. | +| bravo | gadget | The second record, also with a fairly long trailing description. | +| charlie | widget | The third record. Short. | +| delta | gadget | The fourth record, whose description runs on for a little while. | +{{< /table >}} + +## Wrapped data table + +{{< table wrap="true" sortable="true" searchable="true" paginate="true" pagination="2" class="fixture-data" >}} + +| Name | Type | Description | +|---------|--------|--------------------------------------------------------------------| +| alpha | widget | The first record, with a description long enough to need wrapping. | +| bravo | gadget | The second record, also with a fairly long trailing description. | +| charlie | widget | The third record. Short. | +| delta | gadget | The fourth record, whose description runs on for a little while. | +{{< /table >}} + +## Two-column wrapped table + +{{< table wrap="true" class="fixture-two-col" >}} + +| Name | Description | +|-------|--------------------------------------------------------------------| +| alpha | The first record, with a description long enough to need wrapping. | +| bravo | The second record, also with a fairly long trailing description. | +{{< /table >}} From ef9d0d971f39f93498aa09790ed36115a55cb45c Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:38:36 +0200 Subject: [PATCH 04/11] docs: note the hugo.toml override in the plan's clean-up task A workspace use directive alone does not override mod-simple-datatables, so a replacements line is needed too. Both files must be reverted before merge. --- .../plans/2026-07-14-table-wrap-datatables.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/plans/2026-07-14-table-wrap-datatables.md b/docs/superpowers/plans/2026-07-14-table-wrap-datatables.md index ad846609..0bca3c13 100644 --- a/docs/superpowers/plans/2026-07-14-table-wrap-datatables.md +++ b/docs/superpowers/plans/2026-07-14-table-wrap-datatables.md @@ -1021,12 +1021,17 @@ change, so a marker would misdate it. Deliberately omitted — raise it with the - [ ] **Step 2: Revert the workspace overrides** +Task 1 found that a workspace `use` directive alone does **not** override `mod-simple-datatables` — +Hugo keeps resolving it from the committed `_vendor/` copy. It therefore also added a local +`replacements` line to `exampleSite/config/_default/hugo.toml` (following the pattern already +commented out in that file). **Both** files are local-only and must be reverted: + ```bash -git checkout exampleSite/hinode.work -git diff --stat exampleSite/hinode.work +git checkout exampleSite/hinode.work exampleSite/config/_default/hugo.toml +git diff --stat exampleSite/hinode.work exampleSite/config/_default/hugo.toml ``` -Expected: no output — the file is clean and back to `use .` / `use ../`. +Expected: no output — both files are clean. - [ ] **Step 3: Rebuild against the vendored modules** From 8bbfe410f7fee5006ad0bea4065604b04dd31b15 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:43:23 +0200 Subject: [PATCH 05/11] style(table): scope wrap styling to the main breakpoint A wrapped table will render as a single table serving both widths, so the border suppression has to be scoped to the breakpoint rather than applied unconditionally, and Bootstrap's odd-row striping has to be recomputed per record pair. Safelist both wrap classes: on a data table they are only ever added by JavaScript. --- assets/scss/common/_variables-dart.scss | 1 + assets/scss/components/_table.scss | 29 +++++++++++++++++++++++-- config/postcss.config.js | 4 ++++ layouts/_partials/head/stylesheet.html | 1 + 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/assets/scss/common/_variables-dart.scss b/assets/scss/common/_variables-dart.scss index 2ee5e584..39dbe77d 100644 --- a/assets/scss/common/_variables-dart.scss +++ b/assets/scss/common/_variables-dart.scss @@ -26,6 +26,7 @@ $navbar-height: h.$navbar-height; $navbar-offset: h.$navbar-offset; $navbar-offset-xs: h.$navbar-offset-xs; $navbar-size: h.$navbar-size; +$main-breakpoint: h.$main-breakpoint; $overlay-offset: h.$overlay-offset; $primary: h.$primary; $secondary: h.$secondary; diff --git a/assets/scss/components/_table.scss b/assets/scss/components/_table.scss index 42869b10..6f40d953 100644 --- a/assets/scss/components/_table.scss +++ b/assets/scss/components/_table.scss @@ -11,8 +11,33 @@ padding-right: 1rem; } -.table-border-bottom-wrap { - border-bottom-style: none !important +// Bootstrap stripes every odd row. A wrapped table renders two rows per record - the data row +// and the row holding the wrapped last column - so `odd` would stripe every record instead of +// alternating. Recompute the stripes per record pair. +.table-wrap.table-striped { + > tbody > tr:nth-of-type(n) > * { + --bs-table-color-type: initial; + --bs-table-bg-type: initial; + } + + > tbody > tr:nth-of-type(4n + 1) > * { + --bs-table-color-type: var(--bs-table-striped-color); + --bs-table-bg-type: var(--bs-table-striped-bg); + } +} + +@include media-breakpoint-down(#{$main-breakpoint}) { + // Below the breakpoint the data row and the row holding the wrapped column must read as a + // single record, so the data row drops its bottom border. + .table-border-bottom-wrap { + border-bottom-style: none !important; + } + + // Here the second row of each pair is visible too, so it joins its record's stripe. + .table-wrap.table-striped > tbody > tr:nth-of-type(4n + 2) > * { + --bs-table-color-type: var(--bs-table-striped-color); + --bs-table-bg-type: var(--bs-table-striped-bg); + } } @if $enable-dark-mode { diff --git a/config/postcss.config.js b/config/postcss.config.js index 400866ea..c534ee06 100644 --- a/config/postcss.config.js +++ b/config/postcss.config.js @@ -38,6 +38,10 @@ const purgecss = purgeCSSPlugin({ 'both', 'desc', 'asc', + // Hinode wrapped tables. On a data table both classes are added by SimpleDatatables' + // tableRender hook, so they never reach hugo_stats.json and would otherwise be purged. + 'table-wrap', + 'table-border-bottom-wrap', // SimpleDatatables search component 'search-data-table', 'search-input', diff --git a/layouts/_partials/head/stylesheet.html b/layouts/_partials/head/stylesheet.html index 8012889b..24d2dfd3 100644 --- a/layouts/_partials/head/stylesheet.html +++ b/layouts/_partials/head/stylesheet.html @@ -63,6 +63,7 @@ "navbar-offset" $navbarOffset "navbar-offset-xs" $navbarOffsetXS "navbar-size" (site.Params.navigation.size | default "md") + "main-breakpoint" (site.Params.main.breakpoint | default "md") "overlay-offset" $overlayOffset "enable-dark-mode" (printf "%t" ((default true (or site.Params.main.enableDarkMode site.Params.main.colorMode.enabled)))) "import-fonts" (printf "%t" (not (hasPrefix (lower site.Params.style.themeFontPath) "http"))) From ffbca8bf0cb79a8b14e9139ecd5307bff7a3ec4b Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 10:27:44 +0200 Subject: [PATCH 06/11] refactor(table): render one table per shortcode A wrapped table was rendered twice - once regular, once wrapped - and both copies were emitted into the page. Render it once instead: a plain table now carries both layouts and switches with display utilities, and a data table is rendered uniformly and marked with data-table-wrap so simple-datatables can apply the wrapped layout itself. This halves the markup of a wrapped table, parses the Markdown once instead of twice, and stops Flexsearch indexing every wrapped table twice. --- layouts/_markup/render-table.html | 96 ++++++++++++++++------------- layouts/_partials/assets/table.html | 73 ++++++++++------------ 2 files changed, 86 insertions(+), 83 deletions(-) diff --git a/layouts/_markup/render-table.html b/layouts/_markup/render-table.html index 7e4958b5..58a1d7e4 100644 --- a/layouts/_markup/render-table.html +++ b/layouts/_markup/render-table.html @@ -1,16 +1,30 @@ {{/* Adapted from https://gohugo.io/render-hooks/tables/ */}} -{{/* Ensure presence of `.table` class attribute to trigger Bootstrap styling */}} +{{/* Wrapping moves the last column onto a row of its own below the site's main breakpoint. It is a + no-op at `xs` (Bootstrap has no `xs` display-utility infix) and on a single-column table + (nothing to wrap). The marker class is dropped when it does not apply, so the wrap-specific + striping rules cannot misfire on a table that renders normally. */}} +{{ $breakpoint := site.Params.main.breakpoint | default "md" }} +{{ $cols := 0 }} +{{ range .THead }}{{ $cols = len . }}{{ end }} + {{ $attr := .Attributes }} -{{ $class := split $attr.class " " }} -{{ if not (in $class "table") }} - {{ $attr = merge $attr (dict "class" (trim (delimit ($class | append "table") " ") " ")) }} +{{ $given := split (or $attr.class "") " " }} +{{ $wrap := and (in $given "table-wrap") (ne $breakpoint "xs") (gt $cols 1) }} + +{{/* Rebuild the class list: drop empties, drop `table-wrap` when it does not apply, and ensure + `table` is present so Bootstrap styles the element. */}} +{{ $classes := slice }} +{{ range $given }} + {{ if and (ne . "") (or $wrap (ne . "table-wrap")) }} + {{ $classes = $classes | append . }} + {{ end }} {{ end }} +{{ if not (in $classes "table") }}{{ $classes = $classes | append "table" }}{{ end }} +{{ $attr = merge $attr (dict "class" (delimit ($classes | uniq) " ")) }} -{{ $wrap := in $class "table-wrap" }} {{ $align := dict "left" "start" "center" "center" "right" "end" }} -{{ $header := "" }}
{{- range .THead }} - {{ $length := len . }} - {{ if $wrap }} - - {{- range . | first (sub $length 1) }} - - {{- end }} - {{- range . | last 1 }} - {{ $header = .Text }} - {{ end }} - - {{ else }} + {{- $length := len . }} - {{- range . }} - {{- end }} - {{ end }} {{- end }} {{- range .TBody }} - {{ $length := len . }} - {{ if $wrap }} + {{- $length := len . }} - {{- range . | first (sub $length 1) }} - {{- end }} - - {{- range . | last 1 }} - + - {{ end }} - - {{ else }} - - {{- range . }} - - {{- end }} - {{ end }} + {{- end }} {{- end }} -
- {{- .Text -}} -
- {{- .Text -}} + {{- range $i, $cell := . }} + {{- $cellClasses := slice }} + {{- with $cell.Alignment }} + {{- $cellClasses = $cellClasses | append (printf "text-%s" (index $align .)) }} + {{- end }} + {{- if and $wrap (eq $i (sub $length 1)) }} + {{- $cellClasses = $cellClasses | append "d-none" (printf "d-%s-table-cell" $breakpoint) }} + {{- end }} + + {{- $cell.Text -}}
- {{- .Text -}} + {{- range $i, $cell := . }} + {{- $cellClasses := slice }} + {{- with $cell.Alignment }} + {{- $cellClasses = $cellClasses | append (printf "text-%s" (index $align .)) }} + {{- end }} + {{- if $wrap }} + {{- if eq $i (sub $length 1) }} + {{- $cellClasses = $cellClasses | append "d-none" (printf "d-%s-table-cell" $breakpoint) }} + {{- else }} + {{- $cellClasses = $cellClasses | append "table-border-bottom-wrap" }} + {{- end }} + {{- end }} + + {{- $cell.Text -}}
- {{- .Text -}} + {{- if $wrap }} + {{- $last := index . (sub $length 1) }} +
+ {{- $last.Text -}}
- {{- .Text -}} -
\ No newline at end of file +
diff --git a/layouts/_partials/assets/table.html b/layouts/_partials/assets/table.html index 6ba30ea3..9cf68a82 100644 --- a/layouts/_partials/assets/table.html +++ b/layouts/_partials/assets/table.html @@ -1,33 +1,14 @@ -{{/* +{{/* Copyright © 2022 - 2026 The Hinode Team / Mark Dumay. All rights reserved. Use of this source code is governed by The MIT License (MIT) that can be found in the LICENSE file. Visit gethinode.com/license for more details. */}} -{{ define "_partials/inline/table.html" }} - {{ $page := .page }} - {{ $input := .input }} - {{ $attributes := .attributes }} - {{ $class := .class }} - {{ $wrap := .wrap }} - - {{ if $wrap }}{{ $input = printf "%s\n{.table-wrap}" (chomp $input) }}{{ end }} - {{- $input = $input | $page.RenderString }} - {{ $regex := `{{ . | safeHTML }}{{ else }}{{ . | safeHTML }}{{ end }} + {{ if $wrapper }} +
{{ $input | safeHTML }}
+ {{ else }} + {{ $input | safeHTML }} {{ end }} {{ else }} -
- {{ $regular | safeHTML }} +
+ {{ $input | safeHTML }}
- - {{ with $wrapped }} -
- {{ . | safeHTML }} -
- {{ end }} {{ end }} -{{ end }} \ No newline at end of file +{{ end }} From 0a9a5ff0b89a0d90359fdbf309fd6ab86d5adc69 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:03:05 +0200 Subject: [PATCH 07/11] fix(build): safelist d-none for wrapped data tables A wrapped data table gets its layout built in the browser, so the `d-none` that hides the wrapped column's heading is emitted by JavaScript alone. On a site whose only wrapped tables are data tables the class never reaches hugo_stats.json and PurgeCSS would strip it, leaving the heading visible below the breakpoint. It survives today only incidentally, because the navbar chrome happens to emit it. Safelist it explicitly alongside table-wrap and table-border-bottom-wrap. Co-Authored-By: Claude Opus 4.8 (1M context) --- config/postcss.config.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/postcss.config.js b/config/postcss.config.js index c534ee06..c2eab55d 100644 --- a/config/postcss.config.js +++ b/config/postcss.config.js @@ -38,10 +38,13 @@ const purgecss = purgeCSSPlugin({ 'both', 'desc', 'asc', - // Hinode wrapped tables. On a data table both classes are added by SimpleDatatables' + // Hinode wrapped tables. On a data table these classes are added by SimpleDatatables' // tableRender hook, so they never reach hugo_stats.json and would otherwise be purged. + // `d-none` hides the heading of the wrapped column; on a site whose only wrapped tables are + // data tables it is emitted by JavaScript alone. 'table-wrap', 'table-border-bottom-wrap', + 'd-none', // SimpleDatatables search component 'search-data-table', 'search-input', From 7660650869f8a40f1e4114a8a29d4ee823e3dd13 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:04:55 +0200 Subject: [PATCH 08/11] test(table): load the datatables module on the fixture page mod-simple-datatables declares integration = "optional", so Hinode only loads its JavaScript for pages that list it in frontmatter. Without this the fixture's data table was inert - no DataTable instance, no pager - so the wrapped data-table path was never actually exercised. --- exampleSite/content/en/table-demo.md | 1 + 1 file changed, 1 insertion(+) diff --git a/exampleSite/content/en/table-demo.md b/exampleSite/content/en/table-demo.md index 9accf9d2..1c55b9a0 100644 --- a/exampleSite/content/en/table-demo.md +++ b/exampleSite/content/en/table-demo.md @@ -1,5 +1,6 @@ --- title: Table Demo +modules: ["simple-datatables"] --- ## Plain wrapped table From 70aac7b6463297bdf8d012d9b989b6da551a775b Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:24:09 +0200 Subject: [PATCH 09/11] chore: update build stats --- exampleSite/hugo_stats.json | 180 +++++++++++++++++++----------------- 1 file changed, 94 insertions(+), 86 deletions(-) diff --git a/exampleSite/hugo_stats.json b/exampleSite/hugo_stats.json index a00f0d48..a317235d 100644 --- a/exampleSite/hugo_stats.json +++ b/exampleSite/hugo_stats.json @@ -286,6 +286,7 @@ "d-md-block", "d-md-flex", "d-md-none", + "d-md-table-cell", "d-none", "d-none-dark", "d-none-light", @@ -417,6 +418,9 @@ "file-controls", "file-panel", "fixed-top", + "fixture-data", + "fixture-plain", + "fixture-two-col", "flex-column", "flex-fill", "flex-grow-1", @@ -1051,10 +1055,10 @@ "dropdown-align-end-1", "dropdown-callout-1", "dropdown-nav-0", - "dropdown-panel-11e787ef202b8688cb27dbe17c128734", - "dropdown-panel-4a001f76d71caecb3d2fce077fd46393", - "dropdown-panel-5eaa5132a20f55224eb11d3c088ee771", - "dropdown-panel-eec9623a9a84a0e76bc1b02e5a1c846f", + "dropdown-panel-27f33355a2e8662dc39d7afea4afee18", + "dropdown-panel-297b86aa8655b304ea88fcfddb8e4185", + "dropdown-panel-4fecac3f53af5d0e26b4108a1f0384b6", + "dropdown-panel-7ebb2b1258f2e74b239647017e70c011", "dropdown-pills-1", "dropdown-tabs-1", "dropdown-underline-1", @@ -1078,11 +1082,11 @@ "fab-whatsapp", "fab-x-twitter", "faq", - "faq-408fa686b569b4187a457e39f3d18d95", - "faq-408fa686b569b4187a457e39f3d18d95-heading-faq-408fa686b569b4187a457e39f3d18d95", - "faq-408fa686b569b4187a457e39f3d18d95-item-0", - "faq-408fa686b569b4187a457e39f3d18d95-item-1", - "faq-408fa686b569b4187a457e39f3d18d95-item-2", + "faq-29344cbdfb3fb9d79728e663557b5436", + "faq-29344cbdfb3fb9d79728e663557b5436-heading-faq-29344cbdfb3fb9d79728e663557b5436", + "faq-29344cbdfb3fb9d79728e663557b5436-item-0", + "faq-29344cbdfb3fb9d79728e663557b5436-item-1", + "faq-29344cbdfb3fb9d79728e663557b5436-item-2", "far-square", "fas-1", "fas-2", @@ -1256,10 +1260,10 @@ "nav-align-end-1", "nav-callout-1", "nav-nav-0", - "nav-panel-11e787ef202b8688cb27dbe17c128734", - "nav-panel-4a001f76d71caecb3d2fce077fd46393", - "nav-panel-5eaa5132a20f55224eb11d3c088ee771", - "nav-panel-eec9623a9a84a0e76bc1b02e5a1c846f", + "nav-panel-27f33355a2e8662dc39d7afea4afee18", + "nav-panel-297b86aa8655b304ea88fcfddb8e4185", + "nav-panel-4fecac3f53af5d0e26b4108a1f0384b6", + "nav-panel-7ebb2b1258f2e74b239647017e70c011", "nav-pills-1", "nav-tabs-1", "nav-underline-1", @@ -1297,36 +1301,36 @@ "over-mij", "overview", "page-link", - "panel-11e787ef202b8688cb27dbe17c128734-0", - "panel-11e787ef202b8688cb27dbe17c128734-1", - "panel-11e787ef202b8688cb27dbe17c128734-2", - "panel-11e787ef202b8688cb27dbe17c128734-btn-0", - "panel-11e787ef202b8688cb27dbe17c128734-btn-1", - "panel-11e787ef202b8688cb27dbe17c128734-btn-2", - "panel-4a001f76d71caecb3d2fce077fd46393-0", - "panel-4a001f76d71caecb3d2fce077fd46393-1", - "panel-4a001f76d71caecb3d2fce077fd46393-2", - "panel-4a001f76d71caecb3d2fce077fd46393-btn-0", - "panel-4a001f76d71caecb3d2fce077fd46393-btn-1", - "panel-4a001f76d71caecb3d2fce077fd46393-btn-2", - "panel-526059b90c63f6b89fdfcf18d82c58a2-0", - "panel-526059b90c63f6b89fdfcf18d82c58a2-1", - "panel-526059b90c63f6b89fdfcf18d82c58a2-2", - "panel-526059b90c63f6b89fdfcf18d82c58a2-btn-0", - "panel-526059b90c63f6b89fdfcf18d82c58a2-btn-1", - "panel-526059b90c63f6b89fdfcf18d82c58a2-btn-2", - "panel-5eaa5132a20f55224eb11d3c088ee771-0", - "panel-5eaa5132a20f55224eb11d3c088ee771-1", - "panel-5eaa5132a20f55224eb11d3c088ee771-2", - "panel-5eaa5132a20f55224eb11d3c088ee771-btn-0", - "panel-5eaa5132a20f55224eb11d3c088ee771-btn-1", - "panel-5eaa5132a20f55224eb11d3c088ee771-btn-2", - "panel-eec9623a9a84a0e76bc1b02e5a1c846f-0", - "panel-eec9623a9a84a0e76bc1b02e5a1c846f-1", - "panel-eec9623a9a84a0e76bc1b02e5a1c846f-2", - "panel-eec9623a9a84a0e76bc1b02e5a1c846f-btn-0", - "panel-eec9623a9a84a0e76bc1b02e5a1c846f-btn-1", - "panel-eec9623a9a84a0e76bc1b02e5a1c846f-btn-2", + "panel-27f33355a2e8662dc39d7afea4afee18-0", + "panel-27f33355a2e8662dc39d7afea4afee18-1", + "panel-27f33355a2e8662dc39d7afea4afee18-2", + "panel-27f33355a2e8662dc39d7afea4afee18-btn-0", + "panel-27f33355a2e8662dc39d7afea4afee18-btn-1", + "panel-27f33355a2e8662dc39d7afea4afee18-btn-2", + "panel-297b86aa8655b304ea88fcfddb8e4185-0", + "panel-297b86aa8655b304ea88fcfddb8e4185-1", + "panel-297b86aa8655b304ea88fcfddb8e4185-2", + "panel-297b86aa8655b304ea88fcfddb8e4185-btn-0", + "panel-297b86aa8655b304ea88fcfddb8e4185-btn-1", + "panel-297b86aa8655b304ea88fcfddb8e4185-btn-2", + "panel-2a8682caf401d524f22ca39480c9e30f-0", + "panel-2a8682caf401d524f22ca39480c9e30f-1", + "panel-2a8682caf401d524f22ca39480c9e30f-2", + "panel-2a8682caf401d524f22ca39480c9e30f-btn-0", + "panel-2a8682caf401d524f22ca39480c9e30f-btn-1", + "panel-2a8682caf401d524f22ca39480c9e30f-btn-2", + "panel-4fecac3f53af5d0e26b4108a1f0384b6-0", + "panel-4fecac3f53af5d0e26b4108a1f0384b6-1", + "panel-4fecac3f53af5d0e26b4108a1f0384b6-2", + "panel-4fecac3f53af5d0e26b4108a1f0384b6-btn-0", + "panel-4fecac3f53af5d0e26b4108a1f0384b6-btn-1", + "panel-4fecac3f53af5d0e26b4108a1f0384b6-btn-2", + "panel-7ebb2b1258f2e74b239647017e70c011-0", + "panel-7ebb2b1258f2e74b239647017e70c011-1", + "panel-7ebb2b1258f2e74b239647017e70c011-2", + "panel-7ebb2b1258f2e74b239647017e70c011-btn-0", + "panel-7ebb2b1258f2e74b239647017e70c011-btn-1", + "panel-7ebb2b1258f2e74b239647017e70c011-btn-2", "panels", "persona", "pie-chart", @@ -1343,55 +1347,56 @@ "placement-1-1", "placement-1-btn-0", "placement-1-btn-1", + "plain-wrapped-table", "politique-de-confidentialité", "politique-de-cookies", "powershell", "premier-article", "preview", "preview-1", - "preview-1129bdca440fff920cf370b325b6017c-desktop", - "preview-1129bdca440fff920cf370b325b6017c-desktop-tab", - "preview-1129bdca440fff920cf370b325b6017c-mobile", - "preview-1129bdca440fff920cf370b325b6017c-mobile-tab", - "preview-1129bdca440fff920cf370b325b6017c-tablet", - "preview-1129bdca440fff920cf370b325b6017c-tablet-tab", - "preview-148c120579885f24d7c69f95066e2dc0-desktop", - "preview-148c120579885f24d7c69f95066e2dc0-desktop-tab", - "preview-148c120579885f24d7c69f95066e2dc0-mobile", - "preview-148c120579885f24d7c69f95066e2dc0-mobile-tab", - "preview-148c120579885f24d7c69f95066e2dc0-tablet", - "preview-148c120579885f24d7c69f95066e2dc0-tablet-tab", - "preview-18d8f5232f650ceb2173c5761aca7d52-desktop", - "preview-18d8f5232f650ceb2173c5761aca7d52-desktop-tab", - "preview-18d8f5232f650ceb2173c5761aca7d52-mobile", - "preview-18d8f5232f650ceb2173c5761aca7d52-mobile-tab", - "preview-18d8f5232f650ceb2173c5761aca7d52-tablet", - "preview-18d8f5232f650ceb2173c5761aca7d52-tablet-tab", "preview-2", - "preview-85013e32a49b5d1ae6ee6fde028491a4-desktop", - "preview-85013e32a49b5d1ae6ee6fde028491a4-mobile", - "preview-85013e32a49b5d1ae6ee6fde028491a4-tablet", - "preview-940f9b590622dca23b7b8f964d72b968-desktop", - "preview-940f9b590622dca23b7b8f964d72b968-desktop-tab", - "preview-940f9b590622dca23b7b8f964d72b968-mobile", - "preview-940f9b590622dca23b7b8f964d72b968-mobile-tab", - "preview-940f9b590622dca23b7b8f964d72b968-tablet", - "preview-940f9b590622dca23b7b8f964d72b968-tablet-tab", - "preview-985b07e2e66af523972f6cddfa198cc0-desktop", - "preview-985b07e2e66af523972f6cddfa198cc0-desktop-tab", - "preview-985b07e2e66af523972f6cddfa198cc0-mobile", - "preview-985b07e2e66af523972f6cddfa198cc0-mobile-tab", - "preview-985b07e2e66af523972f6cddfa198cc0-tablet", - "preview-985b07e2e66af523972f6cddfa198cc0-tablet-tab", - "preview-aadd2c1e9433baacfd2c1d08e4d85dda-desktop", - "preview-aadd2c1e9433baacfd2c1d08e4d85dda-mobile", - "preview-aadd2c1e9433baacfd2c1d08e4d85dda-tablet", - "preview-d2aa27f97fea368d81a985a691a7f13b-desktop", - "preview-d2aa27f97fea368d81a985a691a7f13b-desktop-tab", - "preview-d2aa27f97fea368d81a985a691a7f13b-mobile", - "preview-d2aa27f97fea368d81a985a691a7f13b-mobile-tab", - "preview-d2aa27f97fea368d81a985a691a7f13b-tablet", - "preview-d2aa27f97fea368d81a985a691a7f13b-tablet-tab", + "preview-3b8bc34da08de43d9c3d205f856765a9-desktop", + "preview-3b8bc34da08de43d9c3d205f856765a9-desktop-tab", + "preview-3b8bc34da08de43d9c3d205f856765a9-mobile", + "preview-3b8bc34da08de43d9c3d205f856765a9-mobile-tab", + "preview-3b8bc34da08de43d9c3d205f856765a9-tablet", + "preview-3b8bc34da08de43d9c3d205f856765a9-tablet-tab", + "preview-731baf1c61b2c63423ab486ec89ba890-desktop", + "preview-731baf1c61b2c63423ab486ec89ba890-mobile", + "preview-731baf1c61b2c63423ab486ec89ba890-tablet", + "preview-831e10d8a1d79dd7e784a962b8e4e703-desktop", + "preview-831e10d8a1d79dd7e784a962b8e4e703-desktop-tab", + "preview-831e10d8a1d79dd7e784a962b8e4e703-mobile", + "preview-831e10d8a1d79dd7e784a962b8e4e703-mobile-tab", + "preview-831e10d8a1d79dd7e784a962b8e4e703-tablet", + "preview-831e10d8a1d79dd7e784a962b8e4e703-tablet-tab", + "preview-851f9799e70f2016593ed770f4c7a194-desktop", + "preview-851f9799e70f2016593ed770f4c7a194-desktop-tab", + "preview-851f9799e70f2016593ed770f4c7a194-mobile", + "preview-851f9799e70f2016593ed770f4c7a194-mobile-tab", + "preview-851f9799e70f2016593ed770f4c7a194-tablet", + "preview-851f9799e70f2016593ed770f4c7a194-tablet-tab", + "preview-9de8a6ddc0a3a4cb4f3adc28b1401c8c-desktop", + "preview-9de8a6ddc0a3a4cb4f3adc28b1401c8c-desktop-tab", + "preview-9de8a6ddc0a3a4cb4f3adc28b1401c8c-mobile", + "preview-9de8a6ddc0a3a4cb4f3adc28b1401c8c-mobile-tab", + "preview-9de8a6ddc0a3a4cb4f3adc28b1401c8c-tablet", + "preview-9de8a6ddc0a3a4cb4f3adc28b1401c8c-tablet-tab", + "preview-e95c899bbedadd659b4f3195301586f4-desktop", + "preview-e95c899bbedadd659b4f3195301586f4-mobile", + "preview-e95c899bbedadd659b4f3195301586f4-tablet", + "preview-ee74a96c39d0cf87a0718a7c83fcf0bb-desktop", + "preview-ee74a96c39d0cf87a0718a7c83fcf0bb-desktop-tab", + "preview-ee74a96c39d0cf87a0718a7c83fcf0bb-mobile", + "preview-ee74a96c39d0cf87a0718a7c83fcf0bb-mobile-tab", + "preview-ee74a96c39d0cf87a0718a7c83fcf0bb-tablet", + "preview-ee74a96c39d0cf87a0718a7c83fcf0bb-tablet-tab", + "preview-fc50a16d5143672ec1e789b183bb3749-desktop", + "preview-fc50a16d5143672ec1e789b183bb3749-desktop-tab", + "preview-fc50a16d5143672ec1e789b183bb3749-mobile", + "preview-fc50a16d5143672ec1e789b183bb3749-mobile-tab", + "preview-fc50a16d5143672ec1e789b183bb3749-tablet", + "preview-fc50a16d5143672ec1e789b183bb3749-tablet-tab", "preview-unavailable", "preview-unavailable-alert-only", "preview-with-specific-device", @@ -1451,6 +1456,7 @@ "sécurité", "tab-list", "table", + "table-demo", "table-wrapping", "tablet-preview", "tabs", @@ -1462,7 +1468,7 @@ "tabs-1-btn-2", "team", "testimonial", - "testimonial-carousel-9df0682b7628747cc5a1b858fce0a750", + "testimonial-carousel-6f4a0110e59e11bcf16b431468fb4ead", "testimonial-with-avatar", "testimonial-with-case-study", "testimonial-with-icon", @@ -1485,6 +1491,7 @@ "tooltip", "troisième-article", "tweede-artikel", + "two-column-wrapped-table", "types-de-cookies-que-nous-utilisons", "types-of-cookies-we-use", "underline", @@ -1513,6 +1520,7 @@ "welcome-to-hinode", "welkom-bij-hinode", "what-are-cookies", + "wrapped-data-table", "xy-chart", "your-rights", "youtube", From a953a2a710fd6c0c67f960e60bbbafd614813648 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:10:57 +0200 Subject: [PATCH 10/11] fix(table): safelist the wrap display utilities and own the table-wrap marker `d-{breakpoint}-table-cell` is emitted only by render-table.html, for a plain wrapped table. PurgeCSS is fed the previous build's hugo_stats.json whenever PostCSS is not deferred to Hugo's post-process phase (`hugo server`, or any non-production build with style.purge enabled). A site whose committed stats predate its first wrapped table therefore lost the rule: the wrapped column's cells kept only `d-none` and stayed hidden at every width, while the colspan rows showed at every width. Safelist the utility for every breakpoint infix. Correct the group's comment while here. `d-none` is emitted by a dozen core layouts and can never actually be purged, so it is listed only to record the dependency, not because JavaScript adds it. The exampleSite carries its own PostCSS config and had never received the wrap safelist at all, so apply the group there too. `table-wrap` is an internal marker that keys the wrap-specific striping rules, but the partial handed ownership back to the caller. `class="table-wrap"` without `wrap` striped a uniform table by record pairs, and a `{.table-wrap}` attribute block on a sortable table made the render hook split its rows server-side - the very layout simple-datatables must never be given. Filter the marker out of the caller's class list, and flag a data table through the page store so the render hook ignores the attribute there. --- config/postcss.config.js | 19 +++++++++++++++---- exampleSite/config/postcss.config.js | 18 ++++++++++++++++++ layouts/_markup/render-table.html | 14 ++++++++++---- layouts/_partials/assets/table.html | 17 ++++++++++++++++- 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/config/postcss.config.js b/config/postcss.config.js index c2eab55d..2a1a1eae 100644 --- a/config/postcss.config.js +++ b/config/postcss.config.js @@ -38,13 +38,24 @@ const purgecss = purgeCSSPlugin({ 'both', 'desc', 'asc', - // Hinode wrapped tables. On a data table these classes are added by SimpleDatatables' - // tableRender hook, so they never reach hugo_stats.json and would otherwise be purged. - // `d-none` hides the heading of the wrapped column; on a site whose only wrapped tables are - // data tables it is emitted by JavaScript alone. + // Hinode wrapped tables. + // + // On a data table `table-wrap` and `table-border-bottom-wrap` are applied in the browser by + // SimpleDatatables' tableRender hook, so on a site whose only wrapped tables are data tables + // they never reach hugo_stats.json at all and would otherwise be purged. 'table-wrap', 'table-border-bottom-wrap', + // `d-none` is also applied by that hook, but a dozen core layouts emit it as well, so it can + // never actually be purged. It is listed to record that the wrap depends on it. 'd-none', + // `d-{breakpoint}-table-cell` reveals the wrapped column above the breakpoint, and + // render-table.html is its only emitter in the whole theme. Unlike the classes above it does + // reach hugo_stats.json - but PurgeCSS is fed the *previous* build's stats whenever PostCSS + // is not deferred to Hugo's post-process phase (`hugo server`, or any non-production build + // with style.purge enabled). A site whose committed stats predate its first wrapped table + // would therefore lose the rule, leaving the wrapped column's cells on `d-none` alone and so + // hidden at every width. + /^d-(sm|md|lg|xl|xxl)-table-cell$/, // SimpleDatatables search component 'search-data-table', 'search-input', diff --git a/exampleSite/config/postcss.config.js b/exampleSite/config/postcss.config.js index 80edf4c7..f8ca700d 100644 --- a/exampleSite/config/postcss.config.js +++ b/exampleSite/config/postcss.config.js @@ -38,6 +38,24 @@ const purgecss = purgeCSSPlugin({ 'both', 'desc', 'asc', + // Hinode wrapped tables. + // + // On a data table `table-wrap` and `table-border-bottom-wrap` are applied in the browser by + // SimpleDatatables' tableRender hook, so on a site whose only wrapped tables are data tables + // they never reach hugo_stats.json at all and would otherwise be purged. + 'table-wrap', + 'table-border-bottom-wrap', + // `d-none` is also applied by that hook, but a dozen core layouts emit it as well, so it can + // never actually be purged. It is listed to record that the wrap depends on it. + 'd-none', + // `d-{breakpoint}-table-cell` reveals the wrapped column above the breakpoint, and + // render-table.html is its only emitter in the whole theme. Unlike the classes above it does + // reach hugo_stats.json - but PurgeCSS is fed the *previous* build's stats whenever PostCSS + // is not deferred to Hugo's post-process phase (`hugo server`, or any non-production build + // with style.purge enabled). A site whose committed stats predate its first wrapped table + // would therefore lose the rule, leaving the wrapped column's cells on `d-none` alone and so + // hidden at every width. + /^d-(sm|md|lg|xl|xxl)-table-cell$/, // SimpleDatatables search component 'search-data-table', 'search-input', diff --git a/layouts/_markup/render-table.html b/layouts/_markup/render-table.html index 58a1d7e4..08a77b15 100644 --- a/layouts/_markup/render-table.html +++ b/layouts/_markup/render-table.html @@ -1,16 +1,22 @@ {{/* Adapted from https://gohugo.io/render-hooks/tables/ */}} {{/* Wrapping moves the last column onto a row of its own below the site's main breakpoint. It is a - no-op at `xs` (Bootstrap has no `xs` display-utility infix) and on a single-column table - (nothing to wrap). The marker class is dropped when it does not apply, so the wrap-specific - striping rules cannot misfire on a table that renders normally. */}} + no-op at `xs` (Bootstrap has no `xs` display-utility infix), on a single-column table (nothing + to wrap), and on a data table (simple-datatables builds its row model from this DOM and applies + the wrap in the browser instead, so the rows must stay uniform here). The marker class is + dropped when it does not apply, so the wrap-specific striping rules cannot misfire on a table + that renders normally. */}} {{ $breakpoint := site.Params.main.breakpoint | default "md" }} {{ $cols := 0 }} {{ range .THead }}{{ $cols = len . }}{{ end }} +{{/* Set by assets/table.html around its RenderString call. `table-wrap` is an internal marker, so + an author-supplied `{.table-wrap}` attribute on a data table is ignored rather than honoured. */}} +{{ $dataTable := .Page.Store.Get "hinodeTableIsDataTable" }} + {{ $attr := .Attributes }} {{ $given := split (or $attr.class "") " " }} -{{ $wrap := and (in $given "table-wrap") (ne $breakpoint "xs") (gt $cols 1) }} +{{ $wrap := and (in $given "table-wrap") (not $dataTable) (ne $breakpoint "xs") (gt $cols 1) }} {{/* Rebuild the class list: drop empties, drop `table-wrap` when it does not apply, and ensure `table` is present so Bootstrap styles the element. */}} diff --git a/layouts/_partials/assets/table.html b/layouts/_partials/assets/table.html index 9cf68a82..64c8020e 100644 --- a/layouts/_partials/assets/table.html +++ b/layouts/_partials/assets/table.html @@ -21,7 +21,11 @@ {{ $dataTable := or $args.sortable $args.paginate $args.searchable }} {{ $wrap := and $args.wrap (ne $breakpoint "xs") }} -{{ $class := or $args.class "" }} +{{/* `table-wrap` is an internal marker owned by the templates, not a class a caller may set: it + keys the wrap-specific striping rules, which recompute Bootstrap's `:nth-of-type(odd)` per pair + of rows. Injecting it by hand would stripe a uniform table by record pairs, so drop it from the + caller's class list. The template adds it back below when the wrap genuinely applies. */}} +{{ $class := delimit (complement (slice "table-wrap") (split (or $args.class "") " ")) " " }} {{ if $dataTable }}{{ $class = trim (printf "%s data-table" $class) " " }}{{ end }} {{ $attributes := "" }} @@ -58,7 +62,18 @@ {{ if not $args.err }} {{ $input := $args.input }} {{ if and $wrap (not $dataTable) }}{{ $input = printf "%s\n{.table-wrap}" (chomp $input) }}{{ end }} + + {{/* The table render hook cannot tell a data table from a plain one by looking at the Markdown + alone, yet it must never build the wrapped layout for one: simple-datatables derives its + row model from this DOM, and applies the wrap itself in the browser. Flag the table for the + hook while it renders, so an author-supplied `{.table-wrap}` attribute cannot split a data + table's rows server-side. RenderString is synchronous, so the flag is set only while this + table is being rendered; the previous value is restored to keep nesting safe. */}} + {{ $store := $args.page.Store }} + {{ $outer := $store.Get "hinodeTableIsDataTable" }} + {{ $store.Set "hinodeTableIsDataTable" $dataTable }} {{ $input = $input | $args.page.RenderString }} + {{ $store.Set "hinodeTableIsDataTable" $outer }} {{ $regex := ` Date: Tue, 14 Jul 2026 12:52:48 +0200 Subject: [PATCH 11/11] build(deps): bump mod-utils to v6.4.2 Picks up the corrected wrap argument comment, which no longer claims that wrap is incompatible with data tables. The exampleSite resolves the same version through the committed workspace, so its own pin needs no change. --- exampleSite/hugo_stats.json | 172 ++++++++++++++++++------------------ go.mod | 2 +- go.sum | 2 + 3 files changed, 89 insertions(+), 87 deletions(-) diff --git a/exampleSite/hugo_stats.json b/exampleSite/hugo_stats.json index a317235d..0ae00765 100644 --- a/exampleSite/hugo_stats.json +++ b/exampleSite/hugo_stats.json @@ -1055,10 +1055,10 @@ "dropdown-align-end-1", "dropdown-callout-1", "dropdown-nav-0", - "dropdown-panel-27f33355a2e8662dc39d7afea4afee18", - "dropdown-panel-297b86aa8655b304ea88fcfddb8e4185", - "dropdown-panel-4fecac3f53af5d0e26b4108a1f0384b6", - "dropdown-panel-7ebb2b1258f2e74b239647017e70c011", + "dropdown-panel-321d41a307c26e3edcc8c34a729fd7e6", + "dropdown-panel-57c19d939338c2007375ebd1e379fcae", + "dropdown-panel-cc1ffa9fcdfbec0a7e09a9255c9fd137", + "dropdown-panel-ef8a8c4738c433641633ecd2933822d1", "dropdown-pills-1", "dropdown-tabs-1", "dropdown-underline-1", @@ -1082,11 +1082,11 @@ "fab-whatsapp", "fab-x-twitter", "faq", - "faq-29344cbdfb3fb9d79728e663557b5436", - "faq-29344cbdfb3fb9d79728e663557b5436-heading-faq-29344cbdfb3fb9d79728e663557b5436", - "faq-29344cbdfb3fb9d79728e663557b5436-item-0", - "faq-29344cbdfb3fb9d79728e663557b5436-item-1", - "faq-29344cbdfb3fb9d79728e663557b5436-item-2", + "faq-d772a553fd4d0d3afbf082dfa513b616", + "faq-d772a553fd4d0d3afbf082dfa513b616-heading-faq-d772a553fd4d0d3afbf082dfa513b616", + "faq-d772a553fd4d0d3afbf082dfa513b616-item-0", + "faq-d772a553fd4d0d3afbf082dfa513b616-item-1", + "faq-d772a553fd4d0d3afbf082dfa513b616-item-2", "far-square", "fas-1", "fas-2", @@ -1260,10 +1260,10 @@ "nav-align-end-1", "nav-callout-1", "nav-nav-0", - "nav-panel-27f33355a2e8662dc39d7afea4afee18", - "nav-panel-297b86aa8655b304ea88fcfddb8e4185", - "nav-panel-4fecac3f53af5d0e26b4108a1f0384b6", - "nav-panel-7ebb2b1258f2e74b239647017e70c011", + "nav-panel-321d41a307c26e3edcc8c34a729fd7e6", + "nav-panel-57c19d939338c2007375ebd1e379fcae", + "nav-panel-cc1ffa9fcdfbec0a7e09a9255c9fd137", + "nav-panel-ef8a8c4738c433641633ecd2933822d1", "nav-pills-1", "nav-tabs-1", "nav-underline-1", @@ -1301,36 +1301,36 @@ "over-mij", "overview", "page-link", - "panel-27f33355a2e8662dc39d7afea4afee18-0", - "panel-27f33355a2e8662dc39d7afea4afee18-1", - "panel-27f33355a2e8662dc39d7afea4afee18-2", - "panel-27f33355a2e8662dc39d7afea4afee18-btn-0", - "panel-27f33355a2e8662dc39d7afea4afee18-btn-1", - "panel-27f33355a2e8662dc39d7afea4afee18-btn-2", - "panel-297b86aa8655b304ea88fcfddb8e4185-0", - "panel-297b86aa8655b304ea88fcfddb8e4185-1", - "panel-297b86aa8655b304ea88fcfddb8e4185-2", - "panel-297b86aa8655b304ea88fcfddb8e4185-btn-0", - "panel-297b86aa8655b304ea88fcfddb8e4185-btn-1", - "panel-297b86aa8655b304ea88fcfddb8e4185-btn-2", - "panel-2a8682caf401d524f22ca39480c9e30f-0", - "panel-2a8682caf401d524f22ca39480c9e30f-1", - "panel-2a8682caf401d524f22ca39480c9e30f-2", - "panel-2a8682caf401d524f22ca39480c9e30f-btn-0", - "panel-2a8682caf401d524f22ca39480c9e30f-btn-1", - "panel-2a8682caf401d524f22ca39480c9e30f-btn-2", - "panel-4fecac3f53af5d0e26b4108a1f0384b6-0", - "panel-4fecac3f53af5d0e26b4108a1f0384b6-1", - "panel-4fecac3f53af5d0e26b4108a1f0384b6-2", - "panel-4fecac3f53af5d0e26b4108a1f0384b6-btn-0", - "panel-4fecac3f53af5d0e26b4108a1f0384b6-btn-1", - "panel-4fecac3f53af5d0e26b4108a1f0384b6-btn-2", - "panel-7ebb2b1258f2e74b239647017e70c011-0", - "panel-7ebb2b1258f2e74b239647017e70c011-1", - "panel-7ebb2b1258f2e74b239647017e70c011-2", - "panel-7ebb2b1258f2e74b239647017e70c011-btn-0", - "panel-7ebb2b1258f2e74b239647017e70c011-btn-1", - "panel-7ebb2b1258f2e74b239647017e70c011-btn-2", + "panel-321d41a307c26e3edcc8c34a729fd7e6-0", + "panel-321d41a307c26e3edcc8c34a729fd7e6-1", + "panel-321d41a307c26e3edcc8c34a729fd7e6-2", + "panel-321d41a307c26e3edcc8c34a729fd7e6-btn-0", + "panel-321d41a307c26e3edcc8c34a729fd7e6-btn-1", + "panel-321d41a307c26e3edcc8c34a729fd7e6-btn-2", + "panel-456533f3b96d0f229ca5306a17ca0efb-0", + "panel-456533f3b96d0f229ca5306a17ca0efb-1", + "panel-456533f3b96d0f229ca5306a17ca0efb-2", + "panel-456533f3b96d0f229ca5306a17ca0efb-btn-0", + "panel-456533f3b96d0f229ca5306a17ca0efb-btn-1", + "panel-456533f3b96d0f229ca5306a17ca0efb-btn-2", + "panel-57c19d939338c2007375ebd1e379fcae-0", + "panel-57c19d939338c2007375ebd1e379fcae-1", + "panel-57c19d939338c2007375ebd1e379fcae-2", + "panel-57c19d939338c2007375ebd1e379fcae-btn-0", + "panel-57c19d939338c2007375ebd1e379fcae-btn-1", + "panel-57c19d939338c2007375ebd1e379fcae-btn-2", + "panel-cc1ffa9fcdfbec0a7e09a9255c9fd137-0", + "panel-cc1ffa9fcdfbec0a7e09a9255c9fd137-1", + "panel-cc1ffa9fcdfbec0a7e09a9255c9fd137-2", + "panel-cc1ffa9fcdfbec0a7e09a9255c9fd137-btn-0", + "panel-cc1ffa9fcdfbec0a7e09a9255c9fd137-btn-1", + "panel-cc1ffa9fcdfbec0a7e09a9255c9fd137-btn-2", + "panel-ef8a8c4738c433641633ecd2933822d1-0", + "panel-ef8a8c4738c433641633ecd2933822d1-1", + "panel-ef8a8c4738c433641633ecd2933822d1-2", + "panel-ef8a8c4738c433641633ecd2933822d1-btn-0", + "panel-ef8a8c4738c433641633ecd2933822d1-btn-1", + "panel-ef8a8c4738c433641633ecd2933822d1-btn-2", "panels", "persona", "pie-chart", @@ -1353,50 +1353,50 @@ "powershell", "premier-article", "preview", + "preview-0cd2dbdb09233984dc9e75865f87b8e8-desktop", + "preview-0cd2dbdb09233984dc9e75865f87b8e8-desktop-tab", + "preview-0cd2dbdb09233984dc9e75865f87b8e8-mobile", + "preview-0cd2dbdb09233984dc9e75865f87b8e8-mobile-tab", + "preview-0cd2dbdb09233984dc9e75865f87b8e8-tablet", + "preview-0cd2dbdb09233984dc9e75865f87b8e8-tablet-tab", "preview-1", "preview-2", - "preview-3b8bc34da08de43d9c3d205f856765a9-desktop", - "preview-3b8bc34da08de43d9c3d205f856765a9-desktop-tab", - "preview-3b8bc34da08de43d9c3d205f856765a9-mobile", - "preview-3b8bc34da08de43d9c3d205f856765a9-mobile-tab", - "preview-3b8bc34da08de43d9c3d205f856765a9-tablet", - "preview-3b8bc34da08de43d9c3d205f856765a9-tablet-tab", - "preview-731baf1c61b2c63423ab486ec89ba890-desktop", - "preview-731baf1c61b2c63423ab486ec89ba890-mobile", - "preview-731baf1c61b2c63423ab486ec89ba890-tablet", - "preview-831e10d8a1d79dd7e784a962b8e4e703-desktop", - "preview-831e10d8a1d79dd7e784a962b8e4e703-desktop-tab", - "preview-831e10d8a1d79dd7e784a962b8e4e703-mobile", - "preview-831e10d8a1d79dd7e784a962b8e4e703-mobile-tab", - "preview-831e10d8a1d79dd7e784a962b8e4e703-tablet", - "preview-831e10d8a1d79dd7e784a962b8e4e703-tablet-tab", - "preview-851f9799e70f2016593ed770f4c7a194-desktop", - "preview-851f9799e70f2016593ed770f4c7a194-desktop-tab", - "preview-851f9799e70f2016593ed770f4c7a194-mobile", - "preview-851f9799e70f2016593ed770f4c7a194-mobile-tab", - "preview-851f9799e70f2016593ed770f4c7a194-tablet", - "preview-851f9799e70f2016593ed770f4c7a194-tablet-tab", - "preview-9de8a6ddc0a3a4cb4f3adc28b1401c8c-desktop", - "preview-9de8a6ddc0a3a4cb4f3adc28b1401c8c-desktop-tab", - "preview-9de8a6ddc0a3a4cb4f3adc28b1401c8c-mobile", - "preview-9de8a6ddc0a3a4cb4f3adc28b1401c8c-mobile-tab", - "preview-9de8a6ddc0a3a4cb4f3adc28b1401c8c-tablet", - "preview-9de8a6ddc0a3a4cb4f3adc28b1401c8c-tablet-tab", - "preview-e95c899bbedadd659b4f3195301586f4-desktop", - "preview-e95c899bbedadd659b4f3195301586f4-mobile", - "preview-e95c899bbedadd659b4f3195301586f4-tablet", - "preview-ee74a96c39d0cf87a0718a7c83fcf0bb-desktop", - "preview-ee74a96c39d0cf87a0718a7c83fcf0bb-desktop-tab", - "preview-ee74a96c39d0cf87a0718a7c83fcf0bb-mobile", - "preview-ee74a96c39d0cf87a0718a7c83fcf0bb-mobile-tab", - "preview-ee74a96c39d0cf87a0718a7c83fcf0bb-tablet", - "preview-ee74a96c39d0cf87a0718a7c83fcf0bb-tablet-tab", - "preview-fc50a16d5143672ec1e789b183bb3749-desktop", - "preview-fc50a16d5143672ec1e789b183bb3749-desktop-tab", - "preview-fc50a16d5143672ec1e789b183bb3749-mobile", - "preview-fc50a16d5143672ec1e789b183bb3749-mobile-tab", - "preview-fc50a16d5143672ec1e789b183bb3749-tablet", - "preview-fc50a16d5143672ec1e789b183bb3749-tablet-tab", + "preview-72e62a08c2f5b01a64c4e575c6c5f0cc-desktop", + "preview-72e62a08c2f5b01a64c4e575c6c5f0cc-mobile", + "preview-72e62a08c2f5b01a64c4e575c6c5f0cc-tablet", + "preview-7384bcaae2af2bd2e01aa17190b2997e-desktop", + "preview-7384bcaae2af2bd2e01aa17190b2997e-desktop-tab", + "preview-7384bcaae2af2bd2e01aa17190b2997e-mobile", + "preview-7384bcaae2af2bd2e01aa17190b2997e-mobile-tab", + "preview-7384bcaae2af2bd2e01aa17190b2997e-tablet", + "preview-7384bcaae2af2bd2e01aa17190b2997e-tablet-tab", + "preview-7c8d62c343ec1f9fcf5678fc5135fae7-desktop", + "preview-7c8d62c343ec1f9fcf5678fc5135fae7-desktop-tab", + "preview-7c8d62c343ec1f9fcf5678fc5135fae7-mobile", + "preview-7c8d62c343ec1f9fcf5678fc5135fae7-mobile-tab", + "preview-7c8d62c343ec1f9fcf5678fc5135fae7-tablet", + "preview-7c8d62c343ec1f9fcf5678fc5135fae7-tablet-tab", + "preview-a16ba1e527585df82b087c81883827fe-desktop", + "preview-a16ba1e527585df82b087c81883827fe-desktop-tab", + "preview-a16ba1e527585df82b087c81883827fe-mobile", + "preview-a16ba1e527585df82b087c81883827fe-mobile-tab", + "preview-a16ba1e527585df82b087c81883827fe-tablet", + "preview-a16ba1e527585df82b087c81883827fe-tablet-tab", + "preview-ceba12a843ab5cfdd0c5836f9020bca4-desktop", + "preview-ceba12a843ab5cfdd0c5836f9020bca4-desktop-tab", + "preview-ceba12a843ab5cfdd0c5836f9020bca4-mobile", + "preview-ceba12a843ab5cfdd0c5836f9020bca4-mobile-tab", + "preview-ceba12a843ab5cfdd0c5836f9020bca4-tablet", + "preview-ceba12a843ab5cfdd0c5836f9020bca4-tablet-tab", + "preview-eba8cd3c938fcca1990ad6661b2e6097-desktop", + "preview-eba8cd3c938fcca1990ad6661b2e6097-desktop-tab", + "preview-eba8cd3c938fcca1990ad6661b2e6097-mobile", + "preview-eba8cd3c938fcca1990ad6661b2e6097-mobile-tab", + "preview-eba8cd3c938fcca1990ad6661b2e6097-tablet", + "preview-eba8cd3c938fcca1990ad6661b2e6097-tablet-tab", + "preview-f1fc6120b3fe4d441ef6318970e35bfc-desktop", + "preview-f1fc6120b3fe4d441ef6318970e35bfc-mobile", + "preview-f1fc6120b3fe4d441ef6318970e35bfc-tablet", "preview-unavailable", "preview-unavailable-alert-only", "preview-with-specific-device", @@ -1468,7 +1468,7 @@ "tabs-1-btn-2", "team", "testimonial", - "testimonial-carousel-6f4a0110e59e11bcf16b431468fb4ead", + "testimonial-carousel-c10eee4901103f585e0296bf9d94605a", "testimonial-with-avatar", "testimonial-with-case-study", "testimonial-with-icon", diff --git a/go.mod b/go.mod index 84919211..95f1ae80 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/gethinode/mod-lottie/v3 v3.0.1 // indirect github.com/gethinode/mod-mermaid/v5 v5.0.1 // indirect github.com/gethinode/mod-simple-datatables/v4 v4.0.1 // indirect - github.com/gethinode/mod-utils/v6 v6.4.1 // indirect + github.com/gethinode/mod-utils/v6 v6.4.2 // indirect github.com/nextapps-de/flexsearch v0.0.0-20260529083235-f7ed963096a0 // indirect github.com/twbs/bootstrap v5.3.8+incompatible // indirect ) diff --git a/go.sum b/go.sum index 522b40c6..ea19b7e3 100644 --- a/go.sum +++ b/go.sum @@ -200,6 +200,8 @@ github.com/gethinode/mod-utils/v6 v6.4.0 h1:l49KrawKE/7c6XE00wXQ9T5/kuTfiETerBz6 github.com/gethinode/mod-utils/v6 v6.4.0/go.mod h1:E5tO9w3VKaidJpu1nI8zAKmh0bddFHOIIQnudAaXQTs= github.com/gethinode/mod-utils/v6 v6.4.1 h1:jHiJEwSjLp6tqIXayxUQTxILwnM7hWMgYyTYhxFE3ZA= github.com/gethinode/mod-utils/v6 v6.4.1/go.mod h1:E5tO9w3VKaidJpu1nI8zAKmh0bddFHOIIQnudAaXQTs= +github.com/gethinode/mod-utils/v6 v6.4.2 h1:xh3XBpuvD+AisycxqEX0Ao90wKlu7cuKM0QOZcdVtP4= +github.com/gethinode/mod-utils/v6 v6.4.2/go.mod h1:E5tO9w3VKaidJpu1nI8zAKmh0bddFHOIIQnudAaXQTs= github.com/nextapps-de/flexsearch v0.0.0-20250907103239-defb38b083f0 h1:55phPhe6fDjfjG0jX4+br3nLORKgjgx8abZUdI0YJRA= github.com/nextapps-de/flexsearch v0.0.0-20250907103239-defb38b083f0/go.mod h1:5GdMfPAXzbA2gXBqTjC6l27kioSYzHlqDMh0+wyx7sU= github.com/nextapps-de/flexsearch v0.0.0-20260529083235-f7ed963096a0 h1:QDKcU3q39lFGzdVwM6kgCGnW3ibbMfYIi0Rwl62iJpo=