diff --git a/assets/scss/common/_variables-dart.scss b/assets/scss/common/_variables-dart.scss index 2ee5e5848..39dbe77dc 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 42869b108..6f40d953a 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 400866ead..2a1a1eae7 100644 --- a/config/postcss.config.js +++ b/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/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 000000000..0bca3c138 --- /dev/null +++ b/docs/superpowers/plans/2026-07-14-table-wrap-datatables.md @@ -0,0 +1,1097 @@ +# 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** + +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 exampleSite/config/_default/hugo.toml +git diff --stat exampleSite/hinode.work exampleSite/config/_default/hugo.toml +``` + +Expected: no output — both files are clean. + +- [ ] **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. 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 000000000..f3aa7a241 --- /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 `` elements: one holding the first N−1 cells (bottom border suppressed +with `.table-border-bottom-wrap`) and one holding the last cell as `` inserted immediately after, + as a single `
` and splits every +body row into two `
`. + +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 `
`, 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. diff --git a/exampleSite/config/postcss.config.js b/exampleSite/config/postcss.config.js index 80edf4c74..f8ca700d6 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/exampleSite/content/en/table-demo.md b/exampleSite/content/en/table-demo.md new file mode 100644 index 000000000..1c55b9a0d --- /dev/null +++ b/exampleSite/content/en/table-demo.md @@ -0,0 +1,38 @@ +--- +title: Table Demo +modules: ["simple-datatables"] +--- + +## 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 >}} diff --git a/exampleSite/hugo_stats.json b/exampleSite/hugo_stats.json index a00f0d486..0ae00765f 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-321d41a307c26e3edcc8c34a729fd7e6", + "dropdown-panel-57c19d939338c2007375ebd1e379fcae", + "dropdown-panel-cc1ffa9fcdfbec0a7e09a9255c9fd137", + "dropdown-panel-ef8a8c4738c433641633ecd2933822d1", "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-d772a553fd4d0d3afbf082dfa513b616", + "faq-d772a553fd4d0d3afbf082dfa513b616-heading-faq-d772a553fd4d0d3afbf082dfa513b616", + "faq-d772a553fd4d0d3afbf082dfa513b616-item-0", + "faq-d772a553fd4d0d3afbf082dfa513b616-item-1", + "faq-d772a553fd4d0d3afbf082dfa513b616-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-321d41a307c26e3edcc8c34a729fd7e6", + "nav-panel-57c19d939338c2007375ebd1e379fcae", + "nav-panel-cc1ffa9fcdfbec0a7e09a9255c9fd137", + "nav-panel-ef8a8c4738c433641633ecd2933822d1", "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-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", @@ -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-0cd2dbdb09233984dc9e75865f87b8e8-desktop", + "preview-0cd2dbdb09233984dc9e75865f87b8e8-desktop-tab", + "preview-0cd2dbdb09233984dc9e75865f87b8e8-mobile", + "preview-0cd2dbdb09233984dc9e75865f87b8e8-mobile-tab", + "preview-0cd2dbdb09233984dc9e75865f87b8e8-tablet", + "preview-0cd2dbdb09233984dc9e75865f87b8e8-tablet-tab", "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-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", @@ -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-c10eee4901103f585e0296bf9d94605a", "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", diff --git a/go.mod b/go.mod index 84919211d..95f1ae80a 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 522b40c61..ea19b7e30 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= diff --git a/layouts/_markup/render-table.html b/layouts/_markup/render-table.html index 7e4958b5a..08a77b15c 100644 --- a/layouts/_markup/render-table.html +++ b/layouts/_markup/render-table.html @@ -1,16 +1,36 @@ {{/* 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), 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 }} -{{ $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") (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. */}} +{{ $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 6ba30ea39..64c8020ee 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 }} diff --git a/layouts/_partials/head/stylesheet.html b/layouts/_partials/head/stylesheet.html index 8012889bd..24d2dfd36 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")))