@bitforgehq/ui 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/ai/MANIFEST.json +23 -11
- package/ai/REFERENCE.json +20 -4
- package/ai/components/app-frame.md +14 -7
- package/ai/components/header.md +9 -0
- package/ai/components/table.md +21 -3
- package/ai/reference/bf-labels.md +1 -0
- package/ai/reference/bf-select-harness.md +4 -0
- package/ai/reference/bf-table-harness.md +6 -0
- package/ai/reference/provide-bf-labels.md +1 -1
- package/fesm2022/bitforgehq-ui-mobile.mjs +11 -2
- package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-testing.mjs +4 -0
- package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui.mjs +34 -16
- package/fesm2022/bitforgehq-ui.mjs.map +1 -1
- package/llms-full.txt +56 -11
- package/package.json +1 -1
- package/theme.css +13 -0
- package/types/bitforgehq-ui-testing.d.ts +14 -0
- package/types/bitforgehq-ui.d.ts +18 -10
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ All notable changes to `@bitforgehq/ui`. One `##` section per version; one `###`
|
|
|
4
4
|
|
|
5
5
|
The heading for the version being worked on carries `(unreleased)` in place of a date. At release time, that placeholder is replaced with the release date (`## 0.3.1 (unreleased)` becomes `## 0.3.1 (YYYY-MM-DD)`) in the same commit that bumps `projects/ui/package.json`. See "Releasing" in the root `README.md`. 0.3.0 is the first version published to npm; the 0.2.0 and 0.1.0 sections below are the history of versions that were never published.
|
|
6
6
|
|
|
7
|
+
## 0.3.3 (2026-10-29)
|
|
8
|
+
|
|
9
|
+
### header
|
|
10
|
+
|
|
11
|
+
Fix: `bf-header` measures its title and slots in the next animation frame after a resize, not
|
|
12
|
+
inside the `ResizeObserver` callback. Its measurement writes the title inset and height, which
|
|
13
|
+
resize the observed elements, and WebKit then reported "ResizeObserver loop completed with
|
|
14
|
+
undelivered notifications": an error event that an app's global error listener
|
|
15
|
+
(`provideBrowserGlobalErrorListeners()`) logs as an error. Several resizes in one frame now
|
|
16
|
+
measure once.
|
|
17
|
+
|
|
18
|
+
### app-frame
|
|
19
|
+
|
|
20
|
+
Fix: the narrow header bar puts the `menu` slot at the inline start, before the `brand` (0.3.2 put
|
|
21
|
+
it after the brand), with the actions at the end as before: the top app bar convention. The menu
|
|
22
|
+
also comes first in the tab order. No API change; an app needs no change unless it relied on the
|
|
23
|
+
brand coming first.
|
|
24
|
+
|
|
25
|
+
### table
|
|
26
|
+
|
|
27
|
+
Fix: stacked rows (`stack="narrow"` and `stack="coarse"`) show their column labels again when the
|
|
28
|
+
package is installed from npm. 0.3.2 wrote the label as `content-[attr(data-label)_/_""]`; the
|
|
29
|
+
library build escaped its quotes in the FESM (`\"\"`), so Tailwind built the rule for a class no
|
|
30
|
+
cell carries and every label `::before` computed to `none`. The cells now carry
|
|
31
|
+
`before:content-data-label`, a utility `theme.css` defines (below). Apps that added the
|
|
32
|
+
workaround `@source inline('@max-xl:before:content-[attr(data-label)_/_""] pointer-coarse:before:content-[attr(data-label)_/_""]')`
|
|
33
|
+
can remove it. consumer-smoke now opens a stacked table built from the packed package in
|
|
34
|
+
Chromium, under a fine and a coarse pointer, and fails if any FESM class string holds a
|
|
35
|
+
backslash escape.
|
|
36
|
+
|
|
37
|
+
The stacked Sort by select's first option is Not sorted (new label key `tableSortNone`, "Not
|
|
38
|
+
sorted"): it shows while `sort` is null, so the select is no longer an empty box, and choosing it
|
|
39
|
+
sets `sort` back to null. New `BfTableHarness.getSortSelect()` returns that select's
|
|
40
|
+
`BfSelectHarness` (`null` when the table cannot stack or has no sortable column).
|
|
41
|
+
|
|
42
|
+
### theme
|
|
43
|
+
|
|
44
|
+
New utility `content-data-label` in `theme.css`: `content: attr(data-label) / ''` (the element's
|
|
45
|
+
`data-label`, with empty alternative text). The stacked `bf-table` uses it behind its stack
|
|
46
|
+
variants; apps can use it with `before:` the same way.
|
|
47
|
+
|
|
7
48
|
## 0.3.2 (2026-10-29)
|
|
8
49
|
|
|
9
50
|
### docs site
|
package/ai/MANIFEST.json
CHANGED
|
@@ -521,16 +521,16 @@
|
|
|
521
521
|
"file": "src/components/app-frame/app-frame.component.ts",
|
|
522
522
|
"category": "Navigation",
|
|
523
523
|
"summary": "Desktop app frame with skip link, sticky sidebar, narrow header and main content",
|
|
524
|
-
"description": "The frame of a desktop web app: a skip link, a sticky full-height sidebar column from the\nbreakpoint up, a header bar with the
|
|
524
|
+
"description": "The frame of a desktop web app: a skip link, a sticky full-height sidebar column from the\nbreakpoint up, a header bar with the narrow-screen menu, the brand and actions below it, and\nthe `main` landmark with a content column of a set maximum width.",
|
|
525
525
|
"overview": "The frame around a desktop web app's pages: a skip link, a [`bf-sidebar`](https://bitforge-ui.web.app/components/sidebar) column that sticks to the viewport at its full height from a breakpoint up, a header bar below the breakpoint with the brand, the narrow-screen [`bf-menu-button`](https://bitforge-ui.web.app/components/menu-button) and end actions, and the `main` landmark with a content column of a set maximum width. It replaces the `hidden md:flex` sidebar, the `md:hidden` menu button and the padded `main` an app would otherwise build by hand. The frame imports no overlay and never reads the router: the menu is your own `bf-menu-button`, projected into a slot.",
|
|
526
|
-
"example": "<bf-app-frame>\n <bf-sidebar bfSlot=\"sidebar\" label=\"Main\">…</bf-sidebar>\n <
|
|
526
|
+
"example": "<bf-app-frame>\n <bf-sidebar bfSlot=\"sidebar\" label=\"Main\">…</bf-sidebar>\n <bf-menu-button bfSlot=\"menu\" [menu]=\"navMenu\">Sections</bf-menu-button>\n <span bfSlot=\"brand\" class=\"text-heading\">Atlas</span>\n <router-outlet />\n</bf-app-frame>",
|
|
527
527
|
"primaryExample": "app-frame/overview",
|
|
528
528
|
"examples": [
|
|
529
529
|
{
|
|
530
530
|
"id": "app-frame/overview",
|
|
531
531
|
"title": "The bf-app-frame overview",
|
|
532
532
|
"file": "projects/docs/src/app/examples/app-frame/overview-example.component.ts",
|
|
533
|
-
"source": "import { Component, signal } from '@angular/core';\nimport { RouterLink, RouterLinkActive } from '@angular/router';\nimport {\n BfAppFrameComponent,\n BfIconButtonComponent,\n BfNavItemComponent,\n BfSidebarComponent,\n} from '@bitforgehq/ui';\nimport { BfIconComponent } from '@bitforgehq/ui/icons';\nimport {\n BfMenuButtonComponent,\n BfMenuComponent,\n BfMenuItemDirective,\n} from '@bitforgehq/ui/overlay';\nimport { Bell, type IconNode, Layers, Menu, PanelLeft, PanelTop } from 'lucide';\n\n/** One top-level destination of the app, shared by the sidebar and the narrow-screen menu. */\ninterface Destination {\n readonly path: string;\n readonly label: string;\n readonly icon: IconNode;\n}\n\n/**\n * The bf-app-frame overview.\n *\n * Live demo of `bf-app-frame` in a box of fixed height: one destinations array drives the\n * `bf-sidebar` (from `md` up) and the narrow header's `bf-menu-button
|
|
533
|
+
"source": "import { Component, signal } from '@angular/core';\nimport { RouterLink, RouterLinkActive } from '@angular/router';\nimport {\n BfAppFrameComponent,\n BfIconButtonComponent,\n BfNavItemComponent,\n BfSidebarComponent,\n} from '@bitforgehq/ui';\nimport { BfIconComponent } from '@bitforgehq/ui/icons';\nimport {\n BfMenuButtonComponent,\n BfMenuComponent,\n BfMenuItemDirective,\n} from '@bitforgehq/ui/overlay';\nimport { Bell, type IconNode, Layers, Menu, PanelLeft, PanelTop } from 'lucide';\n\n/** One top-level destination of the app, shared by the sidebar and the narrow-screen menu. */\ninterface Destination {\n readonly path: string;\n readonly label: string;\n readonly icon: IconNode;\n}\n\n/**\n * The bf-app-frame overview.\n *\n * Live demo of `bf-app-frame` in a box of fixed height: one destinations array drives the\n * `bf-sidebar` (from `md` up) and the narrow header's `bf-menu-button`, at the header's inline\n * start; the brand follows it, and a notifications action sits at the end. Tab into the frame to reveal its skip link; Enter moves focus\n * to the content. The result line reports it.\n */\n@Component({\n selector: 'app-example-app-frame-overview',\n imports: [\n RouterLink,\n RouterLinkActive,\n BfAppFrameComponent,\n BfIconButtonComponent,\n BfIconComponent,\n BfMenuButtonComponent,\n BfMenuComponent,\n BfMenuItemDirective,\n BfNavItemComponent,\n BfSidebarComponent,\n ],\n template: `\n <!-- In an app the frame is the root of the page; here a fixed height stands in for the\n viewport, so the sidebar and the header bar stick inside the box. -->\n <div (focusin)=\"onFocus($event)\">\n <bf-app-frame\n mainId=\"app-frame-demo-content\"\n class=\"border-border rounded-card h-96 overflow-auto border\"\n >\n <bf-sidebar bfSlot=\"sidebar\" label=\"App frame demo\">\n <span bfSlot=\"header\" class=\"text-heading ps-3\">Atlas</span>\n @for (destination of destinations; track destination.path) {\n <a\n bfNavItem\n [routerLink]=\"destination.path\"\n routerLinkActive\n ariaCurrentWhenActive=\"page\"\n >\n <bf-icon bfSlot=\"icon\" [icon]=\"destination.icon\" />\n {{ destination.label }}\n </a>\n }\n </bf-sidebar>\n\n <bf-menu-button bfSlot=\"menu\" variant=\"text\" [menu]=\"navMenu\">\n <bf-icon bfSlot=\"icon-start\" [icon]=\"menu\" />\n Sections\n </bf-menu-button>\n <span bfSlot=\"brand\" class=\"text-heading\">Atlas</span>\n <bf-icon-button\n bfSlot=\"actions\"\n variant=\"text\"\n label=\"Notifications\"\n (clicked)=\"status.set('Notifications opened.')\"\n >\n <bf-icon [icon]=\"bell\" />\n </bf-icon-button>\n\n <div class=\"flex flex-col gap-3\">\n <h2 class=\"text-title\">Inbox</h2>\n <p class=\"text-body text-text-muted max-w-prose\">\n The page sits in the content column, beside the sidebar from the md breakpoint up and\n under the header bar below it.\n </p>\n <p data-status aria-live=\"polite\" class=\"text-caption text-text-muted\">{{ status() }}</p>\n @for (row of rows; track row) {\n <div class=\"bg-surface rounded-tile text-body p-4\">{{ row }}</div>\n }\n </div>\n </bf-app-frame>\n </div>\n\n <ng-template #navMenu>\n <bf-menu>\n @for (destination of destinations; track destination.path) {\n <a\n bfMenuItem\n [routerLink]=\"destination.path\"\n routerLinkActive\n ariaCurrentWhenActive=\"page\"\n >{{ destination.label }}</a\n >\n }\n </bf-menu>\n </ng-template>\n `,\n})\nexport class AppFrameOverviewExampleComponent {\n /** In an app, derive this from the route config, so a new route appears on every surface. */\n protected readonly destinations: readonly Destination[] = [\n { path: '/components/app-frame', label: 'App frame', icon: PanelLeft },\n { path: '/components/sidebar', label: 'Sidebar', icon: PanelTop },\n { path: '/components/menu-button', label: 'Menu button', icon: Layers },\n ];\n protected readonly rows = [\n 'Review the brand brief',\n 'Plan the launch',\n 'Write the release notes',\n ];\n protected readonly bell = Bell;\n protected readonly menu = Menu;\n readonly status = signal('Tab into the frame to reveal the skip link.');\n\n /** Reports when focus lands on the content, as the skip link moves it there. */\n protected onFocus(event: FocusEvent): void {\n if (event.target instanceof HTMLElement && event.target.id === 'app-frame-demo-content') {\n this.status.set('Skipped to the content.');\n }\n }\n}\n",
|
|
534
534
|
"preview": "inline"
|
|
535
535
|
},
|
|
536
536
|
{
|
|
@@ -551,7 +551,7 @@
|
|
|
551
551
|
"usage": [
|
|
552
552
|
{
|
|
553
553
|
"heading": "Slots and layout",
|
|
554
|
-
"body": "Put the `bf-sidebar` in `bfSlot=\"sidebar\"`, the app's name or logo in `bfSlot=\"brand\"`, the narrow-screen `bf-menu-button` of `a[bfMenuItem]` router links in `bfSlot=\"menu\"` and end actions (the theme menu, the account) in `bfSlot=\"actions\"`; everything else is the page. From the breakpoint up the sidebar column sits at the inline start, `sticky top-0` and `h-dvh` tall, and the `bf-sidebar` inside fills it and scrolls its own destinations; the header bar is hidden. Below it the sidebar column is hidden and the header bar (`surface`, a bottom border, sticky at the top) shows the
|
|
554
|
+
"body": "Put the `bf-sidebar` in `bfSlot=\"sidebar\"`, the app's name or logo in `bfSlot=\"brand\"`, the narrow-screen `bf-menu-button` of `a[bfMenuItem]` router links in `bfSlot=\"menu\"` and end actions (the theme menu, the account) in `bfSlot=\"actions\"`; everything else is the page. From the breakpoint up the sidebar column sits at the inline start, `sticky top-0` and `h-dvh` tall, and the `bf-sidebar` inside fills it and scrolls its own destinations; the header bar is hidden. Below it the sidebar column is hidden and the header bar (`surface`, a bottom border, sticky at the top) shows the menu at the inline start, then the brand, and the actions at the end: the top app bar convention, and the menu comes first in the tab order too. The order is fixed, whatever order the slots have in your template. Keep the brand in the sidebar's own `header` slot too, for the wide layout."
|
|
555
555
|
},
|
|
556
556
|
{
|
|
557
557
|
"heading": "Breakpoint and content width",
|
|
@@ -602,12 +602,12 @@
|
|
|
602
602
|
"doc": "The `bf-sidebar`, shown from the breakpoint up; it fills the column's height."
|
|
603
603
|
},
|
|
604
604
|
{
|
|
605
|
-
"name": "
|
|
606
|
-
"doc": "The
|
|
605
|
+
"name": "menu",
|
|
606
|
+
"doc": "The narrow-screen navigation: a `bf-menu-button` of `a[bfMenuItem]` links, at the header bar's inline start."
|
|
607
607
|
},
|
|
608
608
|
{
|
|
609
|
-
"name": "
|
|
610
|
-
"doc": "The narrow
|
|
609
|
+
"name": "brand",
|
|
610
|
+
"doc": "The app's name or logo in the narrow header bar, after the menu."
|
|
611
611
|
},
|
|
612
612
|
{
|
|
613
613
|
"name": "actions",
|
|
@@ -620,6 +620,10 @@
|
|
|
620
620
|
],
|
|
621
621
|
"methods": [],
|
|
622
622
|
"changelog": [
|
|
623
|
+
{
|
|
624
|
+
"version": "0.3.3",
|
|
625
|
+
"text": "Fix: the narrow header bar puts the `menu` slot at the inline start, before the `brand` (0.3.2 put\nit after the brand), with the actions at the end as before: the top app bar convention. The menu\nalso comes first in the tab order. No API change; an app needs no change unless it relied on the\nbrand coming first."
|
|
626
|
+
},
|
|
623
627
|
{
|
|
624
628
|
"version": "0.3.2",
|
|
625
629
|
"text": "New `bf-app-frame` in `@bitforgehq/ui`: the frame of a desktop web app. Slots `sidebar` (a\n`bf-sidebar`, in a column that sticks to the viewport at full height from the breakpoint up),\n`brand`, `menu` (the narrow-screen `bf-menu-button`) and `actions` (the header bar shown below\nthe breakpoint) and the default slot (the page, in the `main` landmark). `breakpoint`\n(`BfAppFrameBreakpoint`: `'md'` default, `'lg'`), `contentWidth` (`BfAppFrameContentWidth`:\n`'md'` 48rem, `'lg'` 64rem, `'xl'` 80rem default, `'full'`) and `mainId`. A skip link, first in\nthe tab order and hidden until focused, moves focus to the main landmark by script, so it works\nunder `<base href=\"/\">`; new label key `skipToContent` (\"Skip to main content\"). Inside another\n`main` landmark the content drops its `main` role. Desktop only: the Capacitor shell keeps\n`bf-tabs`."
|
|
@@ -4825,6 +4829,10 @@
|
|
|
4825
4829
|
],
|
|
4826
4830
|
"methods": [],
|
|
4827
4831
|
"changelog": [
|
|
4832
|
+
{
|
|
4833
|
+
"version": "0.3.3",
|
|
4834
|
+
"text": "Fix: `bf-header` measures its title and slots in the next animation frame after a resize, not\ninside the `ResizeObserver` callback. Its measurement writes the title inset and height, which\nresize the observed elements, and WebKit then reported \"ResizeObserver loop completed with\nundelivered notifications\": an error event that an app's global error listener\n(`provideBrowserGlobalErrorListeners()`) logs as an error. Several resizes in one frame now\nmeasure once."
|
|
4835
|
+
},
|
|
4828
4836
|
{
|
|
4829
4837
|
"version": "0.3.2",
|
|
4830
4838
|
"text": "The `## Desktop` docs say `bf-app-frame` supplies the app-level header bar below its breakpoint."
|
|
@@ -10689,7 +10697,7 @@
|
|
|
10689
10697
|
"id": "table/stacked",
|
|
10690
10698
|
"title": "Stacked rows on a narrow table",
|
|
10691
10699
|
"file": "projects/docs/src/app/examples/table/stacked-example.component.ts",
|
|
10692
|
-
"source": "import { Component, computed, signal } from '@angular/core';\nimport {\n BfBadgeComponent,\n BfSegmentedComponent,\n BfTableCellDirective,\n BfTableComponent,\n type BfSegmentOption,\n type BfTableColumn,\n type BfTableSort,\n} from '@bitforgehq/ui';\n\ninterface Task {\n id: string;\n title: string;\n project: string;\n status: 'Open' | 'Done';\n hours: number;\n}\n\nconst TASKS: readonly Task[] = [\n { id: 'brief', title: 'Write the brief', project: 'Atlas', status: 'Done', hours: 3 },\n { id: 'moodboard', title: 'Bloom moodboard', project: 'Bloom', status: 'Open', hours: 6 },\n { id: 'deck', title: 'Pitch deck', project: 'Atlas', status: 'Open', hours: 9 },\n];\n\ntype Width = 'phone' | 'full';\n\n/**\n * Stacked rows on a narrow table.\n *\n * `stack=\"narrow\"` turns each row into a tile of label/value pairs while the table is narrower\n * than 36rem: the selection checkbox and the first cell head the tile, and a bar above the tiles\n * holds Select all and a Sort by select. Switch the box between phone width and full width.\n */\n@Component({\n selector: 'app-example-table-stacked',\n imports: [BfTableComponent, BfTableCellDirective, BfBadgeComponent, BfSegmentedComponent],\n template: `\n <!-- A stacking table is a size container: its width comes from its parent, never its columns. -->\n <div class=\"flex w-152 max-w-full flex-col gap-3\">\n <bf-segmented label=\"Box width\" [options]=\"widths\" [(value)]=\"width\" />\n <div [class]=\"width() === 'phone' ? 'w-full max-w-sm' : 'w-full'\">\n <bf-table\n label=\"Stacked tasks\"\n stack=\"narrow\"\n selectable\n [rows]=\"rows()\"\n [columns]=\"columns\"\n [(sort)]=\"sort\"\n [(selection)]=\"selection\"\n >\n <ng-template bfTableCell=\"status\" [bfTableCellOf]=\"rows()\" let-task>\n <bf-badge [variant]=\"task.status === 'Done' ? 'success' : 'neutral'\">\n {{ task.status }}\n </bf-badge>\n </ng-template>\n </bf-table>\n </div>\n <p data-status aria-live=\"polite\" class=\"text-caption text-text-muted\">{{ status() }}</p>\n </div>\n `,\n})\nexport class TableStackedExampleComponent {\n readonly widths: BfSegmentOption<Width>[] = [\n { value: 'phone', label: 'Phone' },\n { value: 'full', label: 'Full width' },\n ];\n readonly width = signal<Width | null>('phone');\n readonly columns: readonly BfTableColumn[] = [\n { key: 'title', label: 'Task', sortable: true },\n { key: 'project', label: 'Project' },\n { key: 'status', label: 'Status' },\n { key: 'hours', label: 'Hours', sortable: true, align: 'end' },\n ];\n readonly sort = signal<BfTableSort | null>(null);\n readonly selection = signal<readonly string[]>([]);\n\n /** The table never sorts: the page sorts its rows from `sort`. */\n readonly rows = computed(() => {\n const sort = this.sort();\n if (!sort) {\n return TASKS;\n }\n const factor = sort.direction === 'ascending' ? 1 : -1;\n return [...TASKS].sort((a, b) =>\n sort.column === 'hours'\n ? (a.hours - b.hours) * factor\n : a.title.localeCompare(b.title) * factor,\n );\n });\n\n readonly status = computed(() => {\n const sort = this.sort();\n const sorted = sort ? `Sorted by ${sort.column}, ${sort.direction}` : 'Unsorted';\n return `${sorted} · ${this.selection().length} selected.`;\n });\n}\n",
|
|
10700
|
+
"source": "import { Component, computed, signal } from '@angular/core';\nimport {\n BfBadgeComponent,\n BfSegmentedComponent,\n BfTableCellDirective,\n BfTableComponent,\n type BfSegmentOption,\n type BfTableColumn,\n type BfTableSort,\n} from '@bitforgehq/ui';\n\ninterface Task {\n id: string;\n title: string;\n project: string;\n status: 'Open' | 'Done';\n hours: number;\n}\n\nconst TASKS: readonly Task[] = [\n { id: 'brief', title: 'Write the brief', project: 'Atlas', status: 'Done', hours: 3 },\n { id: 'moodboard', title: 'Bloom moodboard', project: 'Bloom', status: 'Open', hours: 6 },\n { id: 'deck', title: 'Pitch deck', project: 'Atlas', status: 'Open', hours: 9 },\n];\n\ntype Width = 'phone' | 'full';\n\n/**\n * Stacked rows on a narrow table.\n *\n * `stack=\"narrow\"` turns each row into a tile of label/value pairs while the table is narrower\n * than 36rem: the selection checkbox and the first cell head the tile, and a bar above the tiles\n * holds Select all and a Sort by select, whose first option, Not sorted, shows while `sort` is\n * null and clears it. Switch the box between phone width and full width.\n */\n@Component({\n selector: 'app-example-table-stacked',\n imports: [BfTableComponent, BfTableCellDirective, BfBadgeComponent, BfSegmentedComponent],\n template: `\n <!-- A stacking table is a size container: its width comes from its parent, never its columns. -->\n <div class=\"flex w-152 max-w-full flex-col gap-3\">\n <bf-segmented label=\"Box width\" [options]=\"widths\" [(value)]=\"width\" />\n <div [class]=\"width() === 'phone' ? 'w-full max-w-sm' : 'w-full'\">\n <bf-table\n label=\"Stacked tasks\"\n stack=\"narrow\"\n selectable\n [rows]=\"rows()\"\n [columns]=\"columns\"\n [(sort)]=\"sort\"\n [(selection)]=\"selection\"\n >\n <ng-template bfTableCell=\"status\" [bfTableCellOf]=\"rows()\" let-task>\n <bf-badge [variant]=\"task.status === 'Done' ? 'success' : 'neutral'\">\n {{ task.status }}\n </bf-badge>\n </ng-template>\n </bf-table>\n </div>\n <p data-status aria-live=\"polite\" class=\"text-caption text-text-muted\">{{ status() }}</p>\n </div>\n `,\n})\nexport class TableStackedExampleComponent {\n readonly widths: BfSegmentOption<Width>[] = [\n { value: 'phone', label: 'Phone' },\n { value: 'full', label: 'Full width' },\n ];\n readonly width = signal<Width | null>('phone');\n readonly columns: readonly BfTableColumn[] = [\n { key: 'title', label: 'Task', sortable: true },\n { key: 'project', label: 'Project' },\n { key: 'status', label: 'Status' },\n { key: 'hours', label: 'Hours', sortable: true, align: 'end' },\n ];\n readonly sort = signal<BfTableSort | null>(null);\n readonly selection = signal<readonly string[]>([]);\n\n /** The table never sorts: the page sorts its rows from `sort`. */\n readonly rows = computed(() => {\n const sort = this.sort();\n if (!sort) {\n return TASKS;\n }\n const factor = sort.direction === 'ascending' ? 1 : -1;\n return [...TASKS].sort((a, b) =>\n sort.column === 'hours'\n ? (a.hours - b.hours) * factor\n : a.title.localeCompare(b.title) * factor,\n );\n });\n\n readonly status = computed(() => {\n const sort = this.sort();\n const sorted = sort ? `Sorted by ${sort.column}, ${sort.direction}` : 'Unsorted';\n return `${sorted} · ${this.selection().length} selected.`;\n });\n}\n",
|
|
10693
10701
|
"preview": "inline"
|
|
10694
10702
|
},
|
|
10695
10703
|
{
|
|
@@ -10731,7 +10739,7 @@
|
|
|
10731
10739
|
},
|
|
10732
10740
|
{
|
|
10733
10741
|
"heading": "Stacked rows",
|
|
10734
|
-
"body": "`stack` lets the table itself fit a narrow space. With `stack=\"narrow\"` it stacks while the table is narrower than 36rem (576 px at the default root size, Tailwind's `xl` container size): a container query on the table's own width, so a table in a narrow column or a side panel stacks even on a wide screen. The host is then a size container (`container-type: inline-size`), so give the table its width from its parent (a block or `w-full` / `flex-1`), not from its columns. With `stack=\"coarse\"` it stacks under a coarse pointer at any width (phones, tablets, the Capacitor shell). The default `never` keeps the table as it is.\n\nStacked, each row is a sunken tile (`surface-sunken`, `rounded-tile`, the tile a raised card holds; a selected row keeps `primary-soft`). Its checkbox and first cell head the tile in bold; every other cell shows its column label at the start and its value at the end. The header row is visually hidden, and a bar above the tiles holds a labelled Select all checkbox (with `selectable`) and a Sort by select
|
|
10742
|
+
"body": "`stack` lets the table itself fit a narrow space. With `stack=\"narrow\"` it stacks while the table is narrower than 36rem (576 px at the default root size, Tailwind's `xl` container size): a container query on the table's own width, so a table in a narrow column or a side panel stacks even on a wide screen. The host is then a size container (`container-type: inline-size`), so give the table its width from its parent (a block or `w-full` / `flex-1`), not from its columns. With `stack=\"coarse\"` it stacks under a coarse pointer at any width (phones, tablets, the Capacitor shell). The default `never` keeps the table as it is.\n\nStacked, each row is a sunken tile (`surface-sunken`, `rounded-tile`, the tile a raised card holds; a selected row keeps `primary-soft`). Its checkbox and first cell head the tile in bold; every other cell shows its column label at the start and its value at the end. The header row is visually hidden, and a bar above the tiles holds a labelled Select all checkbox (with `selectable`) and a Sort by select, since the hidden header's buttons are not shown. Its first option, Not sorted, shows while `sort` is null and sets it back to null; then every sortable column, ascending and descending (`tableSortBy`, `tableSortNone`, `tableSortAscending`, `tableSortDescending` labels). Choosing a column option sets `sort`, as a header click does. In a test, `BfTableHarness.getSortSelect()` returns that select's `BfSelectHarness`.\n\n<!-- example: table/stacked -->"
|
|
10735
10743
|
},
|
|
10736
10744
|
{
|
|
10737
10745
|
"heading": "Narrow screens and touch",
|
|
@@ -10882,6 +10890,10 @@
|
|
|
10882
10890
|
}
|
|
10883
10891
|
],
|
|
10884
10892
|
"changelog": [
|
|
10893
|
+
{
|
|
10894
|
+
"version": "0.3.3",
|
|
10895
|
+
"text": "Fix: stacked rows (`stack=\"narrow\"` and `stack=\"coarse\"`) show their column labels again when the\npackage is installed from npm. 0.3.2 wrote the label as `content-[attr(data-label)_/_\"\"]`; the\nlibrary build escaped its quotes in the FESM (`\\\"\\\"`), so Tailwind built the rule for a class no\ncell carries and every label `::before` computed to `none`. The cells now carry\n`before:content-data-label`, a utility `theme.css` defines (below). Apps that added the\nworkaround `@source inline('@max-xl:before:content-[attr(data-label)_/_\"\"] pointer-coarse:before:content-[attr(data-label)_/_\"\"]')`\ncan remove it. consumer-smoke now opens a stacked table built from the packed package in\nChromium, under a fine and a coarse pointer, and fails if any FESM class string holds a\nbackslash escape.\n\nThe stacked Sort by select's first option is Not sorted (new label key `tableSortNone`, \"Not\nsorted\"): it shows while `sort` is null, so the select is no longer an empty box, and choosing it\nsets `sort` back to null. New `BfTableHarness.getSortSelect()` returns that select's\n`BfSelectHarness` (`null` when the table cannot stack or has no sortable column)."
|
|
10896
|
+
},
|
|
10885
10897
|
{
|
|
10886
10898
|
"version": "0.3.2",
|
|
10887
10899
|
"text": "New opt-in `stack` input (`BfTableStack`: `'never'` by default, so existing tables do not\nchange, `'narrow'` or `'coarse'`): while the table is narrower than 36rem (a container query on\nthe host, which then gets `@container`) or, for `'coarse'`, under a coarse pointer at any width,\neach body row renders as a sunken tile of label/value pairs. The markup stays a `table` with\nexplicit `table`, `rowgroup`, `row`, `columnheader` and `cell` roles (some browsers drop a\ntable's semantics once its display changes), the header row is visually hidden but still read,\nthe selection checkbox and the first cell head each tile, and every other cell shows its column\nlabel (`data-label`, a `::before` with empty alternative text, so it is not read twice). A bar\nabove the tiles holds a labelled Select all checkbox and a Sort by `bf-select`, since the hidden\nheader's controls are not shown. New label keys `tableSortBy` (\"Sort by\"), `tableSortAscending`\nand `tableSortDescending` (`\"Hours, ascending\"`). `BfTableHarness` reads and drives a stacked\ntable the same way. New `table/stacked` example and a Stacked rows docs section."
|
|
@@ -10895,7 +10907,7 @@
|
|
|
10895
10907
|
"whenToUse": "Use `bf-table` on a desktop page for records the user scans and compares column by column: tasks, invoices, members. On a phone or in the Capacitor shell use [bf-list](https://bitforge-ui.web.app/components/list) rows instead, or let the table stack its rows into tiles with `stack` (see Stacked rows). Use a [bf-list](https://bitforge-ui.web.app/components/list) on desktop too when each record is one line of text with a status, and a description list for the fields of a single record.",
|
|
10896
10908
|
"desktop": "A selectable table tints a row `surface-muted` while hovered (a selected row `primary-soft-hover`); a table without selection does not, because its rows are not targets. Sortable headers darken from `text-muted` to `text` on hover and show the pointer. Hover styles apply only on devices that can hover. The focus ring shows on keyboard focus only, on the header button, the checkbox or the control in a custom cell. Desktop density is the default `md` step; `sm` suits a dense admin page ([density guide](https://bitforge-ui.web.app/density)).",
|
|
10897
10909
|
"theming": "- Header: `text-label-xs`, bold, `text-muted`; a sortable header's hover `text`. A `border` line under the header row.\n- Rows: `text` on the page's surface, divided by `border` lines; hover (selectable only) `surface-muted`; selected `primary-soft`, hover `primary-soft-hover`.\n- Sort arrow: a 1.5 px CSS chevron in the header's text colour.\n- Focus ring: `ring`. Header buttons: `rounded-field`.\n- Stacked rows: tiles on `surface-sunken` with `rounded-tile`, labels `text-label-xs` bold in `text-muted`.\n- Text: `text-body` (`text-label-sm` at `sm`), `tabular-nums` in `end` columns.\n\nPut the table on a `bf-card` (or another surface) yourself; it has no background or border of its own.",
|
|
10898
|
-
"accessibility": "A native `table` with a visually hidden `caption` from `label`, `th scope=\"col\"` headers and one `tr` per row. A sorted column's `th` carries `aria-sort=\"ascending\"` or `\"descending\"`; the others carry none. A sortable header is a native `button` inside the `th`, so its name is the column label; the arrow is `aria-hidden`. The header checkbox is named by `selectAllLabel`, else `provideBfLabels()`'s `tableSelectAll` (\"Select all rows\"), and is mixed (`indeterminate`) while some rows are selected. Each row checkbox is named by the row's first data cell through `aria-labelledby`, so it reads as the row's title. The empty row's cell spans every column.\n\nA table that can stack (`stack` other than `never`) sets explicit `table`, `rowgroup`, `row`, `columnheader` and `cell` roles and names the table from its caption with `aria-labelledby`, because Safari and some screen readers drop a table's semantics once CSS changes its display; so a stacked table still reads as a table with its column headers. Stacked, the header row is visually hidden but stays in the accessibility tree; its sort buttons and checkbox are hidden (a plain label takes a sortable header's place), so no control is focusable where nobody can see it, and the bar's Select all checkbox and Sort by select replace them. A cell's visible label is CSS generated content with empty alternative text (`content: attr(data-label) / \"\"`), so a screen reader reads the column header once, not the label as well.\n\n### Keyboard\n\n| Key | Action |\n| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves through the header buttons, checkboxes and the controls in custom cells in document order |\n| <kbd>ArrowDown</kbd> | On a button, link or checkbox in a row (or the header): moves focus to the control at the same place in the nearest row below that has one; nothing happens at the last row |\n| <kbd>ArrowUp</kbd> | The same, upwards, up to the header row |\n| <kbd>Space</kbd> | Toggles the focused checkbox, or sorts by the focused header |\n| <kbd>Enter</kbd> | Sorts by the focused header |\n\nA stacked table's Sort by select is a native select with its own keys.\n\nText fields, selects and composite widgets (radios, comboboxes, sliders) in a cell keep their own arrow keys.",
|
|
10910
|
+
"accessibility": "A native `table` with a visually hidden `caption` from `label`, `th scope=\"col\"` headers and one `tr` per row. A sorted column's `th` carries `aria-sort=\"ascending\"` or `\"descending\"`; the others carry none. A sortable header is a native `button` inside the `th`, so its name is the column label; the arrow is `aria-hidden`. The header checkbox is named by `selectAllLabel`, else `provideBfLabels()`'s `tableSelectAll` (\"Select all rows\"), and is mixed (`indeterminate`) while some rows are selected. Each row checkbox is named by the row's first data cell through `aria-labelledby`, so it reads as the row's title. The empty row's cell spans every column.\n\nA table that can stack (`stack` other than `never`) sets explicit `table`, `rowgroup`, `row`, `columnheader` and `cell` roles and names the table from its caption with `aria-labelledby`, because Safari and some screen readers drop a table's semantics once CSS changes its display; so a stacked table still reads as a table with its column headers. Stacked, the header row is visually hidden but stays in the accessibility tree; its sort buttons and checkbox are hidden (a plain label takes a sortable header's place), so no control is focusable where nobody can see it, and the bar's Select all checkbox and Sort by select replace them. A cell's visible label is CSS generated content with empty alternative text (`content: attr(data-label) / \"\"`, the `content-data-label` utility of `theme.css`), so a screen reader reads the column header once, not the label as well.\n\n### Keyboard\n\n| Key | Action |\n| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves through the header buttons, checkboxes and the controls in custom cells in document order |\n| <kbd>ArrowDown</kbd> | On a button, link or checkbox in a row (or the header): moves focus to the control at the same place in the nearest row below that has one; nothing happens at the last row |\n| <kbd>ArrowUp</kbd> | The same, upwards, up to the header row |\n| <kbd>Space</kbd> | Toggles the focused checkbox, or sorts by the focused header |\n| <kbd>Enter</kbd> | Sorts by the focused header |\n\nA stacked table's Sort by select is a native select with its own keys.\n\nText fields, selects and composite widgets (radios, comboboxes, sliders) in a cell keep their own arrow keys.",
|
|
10899
10911
|
"notes": "## Do\n\n- Give every table a `label`, and every sortable column a short header.\n- Sort and filter the rows in a `computed()` from `sort` and your filters.\n- Use `stack=\"narrow\"` for a table that may sit in a narrow column, or `stack=\"coarse\"` in the Capacitor shell; otherwise render the rows as `bf-list-item`s below `md`.\n\n## Don't\n\n- Don't put a table on a phone screen that scrolls sideways; stack it or use the list.\n- Don't put a stacking table in a parent that sizes it from its content: the container query needs a width from outside.\n- Don't make a whole row clickable; put a link or button in a custom cell."
|
|
10900
10912
|
},
|
|
10901
10913
|
{
|
package/ai/REFERENCE.json
CHANGED
|
@@ -1075,6 +1075,15 @@
|
|
|
1075
1075
|
"readonly": true,
|
|
1076
1076
|
"default": "'Sort by'"
|
|
1077
1077
|
},
|
|
1078
|
+
{
|
|
1079
|
+
"name": "tableSortNone",
|
|
1080
|
+
"kind": "property",
|
|
1081
|
+
"type": "string",
|
|
1082
|
+
"required": true,
|
|
1083
|
+
"doc": "A stacked bf-table's first sort option: rows in the order given, no column sorted.",
|
|
1084
|
+
"readonly": true,
|
|
1085
|
+
"default": "'Not sorted'"
|
|
1086
|
+
},
|
|
1078
1087
|
{
|
|
1079
1088
|
"name": "tableSortAscending",
|
|
1080
1089
|
"kind": "property",
|
|
@@ -2599,7 +2608,7 @@
|
|
|
2599
2608
|
"entryPoint": "@bitforgehq/ui",
|
|
2600
2609
|
"file": "src/config/labels.ts",
|
|
2601
2610
|
"summary": "Replaces built-in strings for an app or a sub-tree, such as a translation",
|
|
2602
|
-
"description": "Replaces built-in strings, merged over the parent's (validation merged key by key); a key left\nout or set to `undefined` keeps the parent's string. In `ApplicationConfig.providers` it covers\nevery string. In a component's `providers` it covers that sub-tree's widgets and form controls\n(`loading`, `dismiss`, `tabBar`, `tabBadge`, `back`, `refreshing`, `tableSelectAll`, `tableSortBy`,\n`tableSortAscending`, `tableSortDescending`, `theme`, `themeSystem`, `themeLight`, `themeDark`,\n`breadcrumb`, `breadcrumbExpand`, `navigation`, `navBadge`, `sidebarCollapse`, `sidebarExpand`,\n`skipToContent`, `pagination`, `previousPage`, `nextPage`, `pageOf`, `goToPage`, `tabGroup`,\n`calendarPreviousMonth`, `calendarNextMonth`, `calendarChooseMonth`, `calendarChooseYear`,\n`statTrendUp`, `statTrendDown`, `statTrendFlat`, `comboboxEmpty`, `comboboxRemove`,\n`dateInputDay`, `dateInputMonth`, `dateInputYear`, `dateInputOpenCalendar`, `dateInputInvalid`,\n`dateInputUnavailable`, `timeInputHour`, `timeInputMinute`, `timeInputSecond`,\n`timeInputDayPeriod`, `timeInputOpenList`, `timeInputInvalid`, `timeInputMin`, `timeInputMax`,\n`chart`, `chartLegend`, `chartValues`, `chartShowData`, `chartHideData`, `chartCategory`,\n`chartValue`, `chartEmpty`, `chartError`, `validation`); the overlay services (`ok`, `cancel`,\n`actions`, `sheet`, `modal`, `close`, `expand`, `collapse`, `sheetPosition`, `dialog`,\n`notifications`) are root singletons and pick it up only when the caller passes its injector:\n`open(…, { injector: inject(Injector) })`.",
|
|
2611
|
+
"description": "Replaces built-in strings, merged over the parent's (validation merged key by key); a key left\nout or set to `undefined` keeps the parent's string. In `ApplicationConfig.providers` it covers\nevery string. In a component's `providers` it covers that sub-tree's widgets and form controls\n(`loading`, `dismiss`, `tabBar`, `tabBadge`, `back`, `refreshing`, `tableSelectAll`, `tableSortBy`,\n`tableSortNone`, `tableSortAscending`, `tableSortDescending`, `theme`, `themeSystem`, `themeLight`, `themeDark`,\n`breadcrumb`, `breadcrumbExpand`, `navigation`, `navBadge`, `sidebarCollapse`, `sidebarExpand`,\n`skipToContent`, `pagination`, `previousPage`, `nextPage`, `pageOf`, `goToPage`, `tabGroup`,\n`calendarPreviousMonth`, `calendarNextMonth`, `calendarChooseMonth`, `calendarChooseYear`,\n`statTrendUp`, `statTrendDown`, `statTrendFlat`, `comboboxEmpty`, `comboboxRemove`,\n`dateInputDay`, `dateInputMonth`, `dateInputYear`, `dateInputOpenCalendar`, `dateInputInvalid`,\n`dateInputUnavailable`, `timeInputHour`, `timeInputMinute`, `timeInputSecond`,\n`timeInputDayPeriod`, `timeInputOpenList`, `timeInputInvalid`, `timeInputMin`, `timeInputMax`,\n`chart`, `chartLegend`, `chartValues`, `chartShowData`, `chartHideData`, `chartCategory`,\n`chartValue`, `chartEmpty`, `chartError`, `validation`); the overlay services (`ok`, `cancel`,\n`actions`, `sheet`, `modal`, `close`, `expand`, `collapse`, `sheetPosition`, `dialog`,\n`notifications`) are root singletons and pick it up only when the caller passes its injector:\n`open(…, { injector: inject(Injector) })`.",
|
|
2603
2612
|
"members": [],
|
|
2604
2613
|
"methods": [
|
|
2605
2614
|
{
|
|
@@ -2614,7 +2623,7 @@
|
|
|
2614
2623
|
}
|
|
2615
2624
|
],
|
|
2616
2625
|
"returns": "Provider",
|
|
2617
|
-
"doc": "Replaces built-in strings, merged over the parent's (validation merged key by key); a key left\nout or set to `undefined` keeps the parent's string. In `ApplicationConfig.providers` it covers\nevery string. In a component's `providers` it covers that sub-tree's widgets and form controls\n(`loading`, `dismiss`, `tabBar`, `tabBadge`, `back`, `refreshing`, `tableSelectAll`, `tableSortBy`,\n`tableSortAscending`, `tableSortDescending`, `theme`, `themeSystem`, `themeLight`, `themeDark`,\n`breadcrumb`, `breadcrumbExpand`, `navigation`, `navBadge`, `sidebarCollapse`, `sidebarExpand`,\n`skipToContent`, `pagination`, `previousPage`, `nextPage`, `pageOf`, `goToPage`, `tabGroup`,\n`calendarPreviousMonth`, `calendarNextMonth`, `calendarChooseMonth`, `calendarChooseYear`,\n`statTrendUp`, `statTrendDown`, `statTrendFlat`, `comboboxEmpty`, `comboboxRemove`,\n`dateInputDay`, `dateInputMonth`, `dateInputYear`, `dateInputOpenCalendar`, `dateInputInvalid`,\n`dateInputUnavailable`, `timeInputHour`, `timeInputMinute`, `timeInputSecond`,\n`timeInputDayPeriod`, `timeInputOpenList`, `timeInputInvalid`, `timeInputMin`, `timeInputMax`,\n`chart`, `chartLegend`, `chartValues`, `chartShowData`, `chartHideData`, `chartCategory`,\n`chartValue`, `chartEmpty`, `chartError`, `validation`); the overlay services (`ok`, `cancel`,\n`actions`, `sheet`, `modal`, `close`, `expand`, `collapse`, `sheetPosition`, `dialog`,\n`notifications`) are root singletons and pick it up only when the caller passes its injector:\n`open(…, { injector: inject(Injector) })`."
|
|
2626
|
+
"doc": "Replaces built-in strings, merged over the parent's (validation merged key by key); a key left\nout or set to `undefined` keeps the parent's string. In `ApplicationConfig.providers` it covers\nevery string. In a component's `providers` it covers that sub-tree's widgets and form controls\n(`loading`, `dismiss`, `tabBar`, `tabBadge`, `back`, `refreshing`, `tableSelectAll`, `tableSortBy`,\n`tableSortNone`, `tableSortAscending`, `tableSortDescending`, `theme`, `themeSystem`, `themeLight`, `themeDark`,\n`breadcrumb`, `breadcrumbExpand`, `navigation`, `navBadge`, `sidebarCollapse`, `sidebarExpand`,\n`skipToContent`, `pagination`, `previousPage`, `nextPage`, `pageOf`, `goToPage`, `tabGroup`,\n`calendarPreviousMonth`, `calendarNextMonth`, `calendarChooseMonth`, `calendarChooseYear`,\n`statTrendUp`, `statTrendDown`, `statTrendFlat`, `comboboxEmpty`, `comboboxRemove`,\n`dateInputDay`, `dateInputMonth`, `dateInputYear`, `dateInputOpenCalendar`, `dateInputInvalid`,\n`dateInputUnavailable`, `timeInputHour`, `timeInputMinute`, `timeInputSecond`,\n`timeInputDayPeriod`, `timeInputOpenList`, `timeInputInvalid`, `timeInputMin`, `timeInputMax`,\n`chart`, `chartLegend`, `chartValues`, `chartShowData`, `chartHideData`, `chartCategory`,\n`chartValue`, `chartEmpty`, `chartError`, `validation`); the overlay services (`ok`, `cancel`,\n`actions`, `sheet`, `modal`, `close`, `expand`, `collapse`, `sheetPosition`, `dialog`,\n`notifications`) are root singletons and pick it up only when the caller passes its injector:\n`open(…, { injector: inject(Injector) })`."
|
|
2618
2627
|
}
|
|
2619
2628
|
],
|
|
2620
2629
|
"slots": [],
|
|
@@ -9628,7 +9637,7 @@
|
|
|
9628
9637
|
],
|
|
9629
9638
|
"slots": [],
|
|
9630
9639
|
"references": [],
|
|
9631
|
-
"usedBy": [],
|
|
9640
|
+
"usedBy": ["bf-table-harness"],
|
|
9632
9641
|
"since": "0.3.0",
|
|
9633
9642
|
"signature": "class BfSelectHarness extends ComponentHarness"
|
|
9634
9643
|
},
|
|
@@ -9839,6 +9848,13 @@
|
|
|
9839
9848
|
"returns": "Promise<BfTableSort | null>",
|
|
9840
9849
|
"doc": "The sorted column (its `key`) and direction, from `aria-sort`."
|
|
9841
9850
|
},
|
|
9851
|
+
{
|
|
9852
|
+
"name": "getSortSelect",
|
|
9853
|
+
"signature": "getSortSelect(): Promise<BfSelectHarness | null>",
|
|
9854
|
+
"params": [],
|
|
9855
|
+
"returns": "Promise<BfSelectHarness | null>",
|
|
9856
|
+
"doc": "The Sort by select in a stacked table's bar: its first option, Not sorted (`tableSortNone`),\nsets `sort` to null and shows while it is; then each sortable column ascending and descending."
|
|
9857
|
+
},
|
|
9842
9858
|
{
|
|
9843
9859
|
"name": "selectRow",
|
|
9844
9860
|
"signature": "selectRow(index: number): Promise<void>",
|
|
@@ -9869,7 +9885,7 @@
|
|
|
9869
9885
|
}
|
|
9870
9886
|
],
|
|
9871
9887
|
"slots": [],
|
|
9872
|
-
"references": ["bf-table-sort"],
|
|
9888
|
+
"references": ["bf-select-harness", "bf-table-sort"],
|
|
9873
9889
|
"usedBy": [],
|
|
9874
9890
|
"since": "0.3.1",
|
|
9875
9891
|
"signature": "class BfTableHarness extends ComponentHarness"
|
|
@@ -34,8 +34,8 @@ interface Destination {
|
|
|
34
34
|
* The bf-app-frame overview.
|
|
35
35
|
*
|
|
36
36
|
* Live demo of `bf-app-frame` in a box of fixed height: one destinations array drives the
|
|
37
|
-
* `bf-sidebar` (from `md` up) and the narrow header's `bf-menu-button
|
|
38
|
-
* brand and a notifications action. Tab into the frame to reveal its skip link; Enter moves focus
|
|
37
|
+
* `bf-sidebar` (from `md` up) and the narrow header's `bf-menu-button`, at the header's inline
|
|
38
|
+
* start; the brand follows it, and a notifications action sits at the end. Tab into the frame to reveal its skip link; Enter moves focus
|
|
39
39
|
* to the content. The result line reports it.
|
|
40
40
|
*/
|
|
41
41
|
@Component({
|
|
@@ -75,11 +75,11 @@ interface Destination {
|
|
|
75
75
|
}
|
|
76
76
|
</bf-sidebar>
|
|
77
77
|
|
|
78
|
-
<span bfSlot="brand" class="text-heading">Atlas</span>
|
|
79
78
|
<bf-menu-button bfSlot="menu" variant="text" [menu]="navMenu">
|
|
80
79
|
<bf-icon bfSlot="icon-start" [icon]="menu" />
|
|
81
80
|
Sections
|
|
82
81
|
</bf-menu-button>
|
|
82
|
+
<span bfSlot="brand" class="text-heading">Atlas</span>
|
|
83
83
|
<bf-icon-button
|
|
84
84
|
bfSlot="actions"
|
|
85
85
|
variant="text"
|
|
@@ -146,7 +146,7 @@ export class AppFrameOverviewExampleComponent {
|
|
|
146
146
|
## Overview
|
|
147
147
|
|
|
148
148
|
The frame of a desktop web app: a skip link, a sticky full-height sidebar column from the
|
|
149
|
-
breakpoint up, a header bar with the
|
|
149
|
+
breakpoint up, a header bar with the narrow-screen menu, the brand and actions below it, and
|
|
150
150
|
the `main` landmark with a content column of a set maximum width.
|
|
151
151
|
|
|
152
152
|
The frame around a desktop web app's pages: a skip link, a [`bf-sidebar`](https://bitforge-ui.web.app/components/sidebar) column that sticks to the viewport at its full height from a breakpoint up, a header bar below the breakpoint with the brand, the narrow-screen [`bf-menu-button`](https://bitforge-ui.web.app/components/menu-button) and end actions, and the `main` landmark with a content column of a set maximum width. It replaces the `hidden md:flex` sidebar, the `md:hidden` menu button and the padded `main` an app would otherwise build by hand. The frame imports no overlay and never reads the router: the menu is your own `bf-menu-button`, projected into a slot.
|
|
@@ -157,7 +157,7 @@ Use `bf-app-frame` once, at the root of a desktop web app whose top-level destin
|
|
|
157
157
|
|
|
158
158
|
## Slots and layout
|
|
159
159
|
|
|
160
|
-
Put the `bf-sidebar` in `bfSlot="sidebar"`, the app's name or logo in `bfSlot="brand"`, the narrow-screen `bf-menu-button` of `a[bfMenuItem]` router links in `bfSlot="menu"` and end actions (the theme menu, the account) in `bfSlot="actions"`; everything else is the page. From the breakpoint up the sidebar column sits at the inline start, `sticky top-0` and `h-dvh` tall, and the `bf-sidebar` inside fills it and scrolls its own destinations; the header bar is hidden. Below it the sidebar column is hidden and the header bar (`surface`, a bottom border, sticky at the top) shows the
|
|
160
|
+
Put the `bf-sidebar` in `bfSlot="sidebar"`, the app's name or logo in `bfSlot="brand"`, the narrow-screen `bf-menu-button` of `a[bfMenuItem]` router links in `bfSlot="menu"` and end actions (the theme menu, the account) in `bfSlot="actions"`; everything else is the page. From the breakpoint up the sidebar column sits at the inline start, `sticky top-0` and `h-dvh` tall, and the `bf-sidebar` inside fills it and scrolls its own destinations; the header bar is hidden. Below it the sidebar column is hidden and the header bar (`surface`, a bottom border, sticky at the top) shows the menu at the inline start, then the brand, and the actions at the end: the top app bar convention, and the menu comes first in the tab order too. The order is fixed, whatever order the slots have in your template. Keep the brand in the sidebar's own `header` slot too, for the wide layout.
|
|
161
161
|
|
|
162
162
|
## Breakpoint and content width
|
|
163
163
|
|
|
@@ -294,8 +294,8 @@ The skip link is a native `<a>` with an href to the main landmark and is the fir
|
|
|
294
294
|
## Slots
|
|
295
295
|
|
|
296
296
|
- `sidebar`: The `bf-sidebar`, shown from the breakpoint up; it fills the column's height.
|
|
297
|
-
- `
|
|
298
|
-
- `
|
|
297
|
+
- `menu`: The narrow-screen navigation: a `bf-menu-button` of `a[bfMenuItem]` links, at the header bar's inline start.
|
|
298
|
+
- `brand`: The app's name or logo in the narrow header bar, after the menu.
|
|
299
299
|
- `actions`: End actions of the narrow header bar, such as the theme menu.
|
|
300
300
|
- `default`: The page: what the router shows, inside the `main` landmark.
|
|
301
301
|
|
|
@@ -314,6 +314,13 @@ The skip link is a native `<a>` with an href to the main landmark and is the fir
|
|
|
314
314
|
|
|
315
315
|
## Migration notes
|
|
316
316
|
|
|
317
|
+
### 0.3.3
|
|
318
|
+
|
|
319
|
+
Fix: the narrow header bar puts the `menu` slot at the inline start, before the `brand` (0.3.2 put
|
|
320
|
+
it after the brand), with the actions at the end as before: the top app bar convention. The menu
|
|
321
|
+
also comes first in the tab order. No API change; an app needs no change unless it relied on the
|
|
322
|
+
brand coming first.
|
|
323
|
+
|
|
317
324
|
### 0.3.2
|
|
318
325
|
|
|
319
326
|
New `bf-app-frame` in `@bitforgehq/ui`: the frame of a desktop web app. Slots `sidebar` (a
|
package/ai/components/header.md
CHANGED
|
@@ -748,6 +748,15 @@ The title is not in the tab order; after a push, `bf-stack-outlet` focuses it wi
|
|
|
748
748
|
|
|
749
749
|
## Migration notes
|
|
750
750
|
|
|
751
|
+
### 0.3.3
|
|
752
|
+
|
|
753
|
+
Fix: `bf-header` measures its title and slots in the next animation frame after a resize, not
|
|
754
|
+
inside the `ResizeObserver` callback. Its measurement writes the title inset and height, which
|
|
755
|
+
resize the observed elements, and WebKit then reported "ResizeObserver loop completed with
|
|
756
|
+
undelivered notifications": an error event that an app's global error listener
|
|
757
|
+
(`provideBrowserGlobalErrorListeners()`) logs as an error. Several resizes in one frame now
|
|
758
|
+
measure once.
|
|
759
|
+
|
|
751
760
|
### 0.3.2
|
|
752
761
|
|
|
753
762
|
The `## Desktop` docs say `bf-app-frame` supplies the app-level header bar below its breakpoint.
|
package/ai/components/table.md
CHANGED
|
@@ -405,7 +405,7 @@ export class TablePagedExampleComponent {
|
|
|
405
405
|
|
|
406
406
|
`stack` lets the table itself fit a narrow space. With `stack="narrow"` it stacks while the table is narrower than 36rem (576 px at the default root size, Tailwind's `xl` container size): a container query on the table's own width, so a table in a narrow column or a side panel stacks even on a wide screen. The host is then a size container (`container-type: inline-size`), so give the table its width from its parent (a block or `w-full` / `flex-1`), not from its columns. With `stack="coarse"` it stacks under a coarse pointer at any width (phones, tablets, the Capacitor shell). The default `never` keeps the table as it is.
|
|
407
407
|
|
|
408
|
-
Stacked, each row is a sunken tile (`surface-sunken`, `rounded-tile`, the tile a raised card holds; a selected row keeps `primary-soft`). Its checkbox and first cell head the tile in bold; every other cell shows its column label at the start and its value at the end. The header row is visually hidden, and a bar above the tiles holds a labelled Select all checkbox (with `selectable`) and a Sort by select
|
|
408
|
+
Stacked, each row is a sunken tile (`surface-sunken`, `rounded-tile`, the tile a raised card holds; a selected row keeps `primary-soft`). Its checkbox and first cell head the tile in bold; every other cell shows its column label at the start and its value at the end. The header row is visually hidden, and a bar above the tiles holds a labelled Select all checkbox (with `selectable`) and a Sort by select, since the hidden header's buttons are not shown. Its first option, Not sorted, shows while `sort` is null and sets it back to null; then every sortable column, ascending and descending (`tableSortBy`, `tableSortNone`, `tableSortAscending`, `tableSortDescending` labels). Choosing a column option sets `sort`, as a header click does. In a test, `BfTableHarness.getSortSelect()` returns that select's `BfSelectHarness`.
|
|
409
409
|
|
|
410
410
|
```ts
|
|
411
411
|
import { Component, computed, signal } from '@angular/core';
|
|
@@ -440,7 +440,8 @@ type Width = 'phone' | 'full';
|
|
|
440
440
|
*
|
|
441
441
|
* `stack="narrow"` turns each row into a tile of label/value pairs while the table is narrower
|
|
442
442
|
* than 36rem: the selection checkbox and the first cell head the tile, and a bar above the tiles
|
|
443
|
-
* holds Select all and a Sort by select
|
|
443
|
+
* holds Select all and a Sort by select, whose first option, Not sorted, shows while `sort` is
|
|
444
|
+
* null and clears it. Switch the box between phone width and full width.
|
|
444
445
|
*/
|
|
445
446
|
@Component({
|
|
446
447
|
selector: 'app-example-table-stacked',
|
|
@@ -587,7 +588,7 @@ Put the table on a `bf-card` (or another surface) yourself; it has no background
|
|
|
587
588
|
|
|
588
589
|
A native `table` with a visually hidden `caption` from `label`, `th scope="col"` headers and one `tr` per row. A sorted column's `th` carries `aria-sort="ascending"` or `"descending"`; the others carry none. A sortable header is a native `button` inside the `th`, so its name is the column label; the arrow is `aria-hidden`. The header checkbox is named by `selectAllLabel`, else `provideBfLabels()`'s `tableSelectAll` ("Select all rows"), and is mixed (`indeterminate`) while some rows are selected. Each row checkbox is named by the row's first data cell through `aria-labelledby`, so it reads as the row's title. The empty row's cell spans every column.
|
|
589
590
|
|
|
590
|
-
A table that can stack (`stack` other than `never`) sets explicit `table`, `rowgroup`, `row`, `columnheader` and `cell` roles and names the table from its caption with `aria-labelledby`, because Safari and some screen readers drop a table's semantics once CSS changes its display; so a stacked table still reads as a table with its column headers. Stacked, the header row is visually hidden but stays in the accessibility tree; its sort buttons and checkbox are hidden (a plain label takes a sortable header's place), so no control is focusable where nobody can see it, and the bar's Select all checkbox and Sort by select replace them. A cell's visible label is CSS generated content with empty alternative text (`content: attr(data-label) / ""`), so a screen reader reads the column header once, not the label as well.
|
|
591
|
+
A table that can stack (`stack` other than `never`) sets explicit `table`, `rowgroup`, `row`, `columnheader` and `cell` roles and names the table from its caption with `aria-labelledby`, because Safari and some screen readers drop a table's semantics once CSS changes its display; so a stacked table still reads as a table with its column headers. Stacked, the header row is visually hidden but stays in the accessibility tree; its sort buttons and checkbox are hidden (a plain label takes a sortable header's place), so no control is focusable where nobody can see it, and the bar's Select all checkbox and Sort by select replace them. A cell's visible label is CSS generated content with empty alternative text (`content: attr(data-label) / ""`, the `content-data-label` utility of `theme.css`), so a screen reader reads the column header once, not the label as well.
|
|
591
592
|
|
|
592
593
|
### Keyboard
|
|
593
594
|
|
|
@@ -665,6 +666,23 @@ Selects or clears one row.
|
|
|
665
666
|
|
|
666
667
|
## Migration notes
|
|
667
668
|
|
|
669
|
+
### 0.3.3
|
|
670
|
+
|
|
671
|
+
Fix: stacked rows (`stack="narrow"` and `stack="coarse"`) show their column labels again when the
|
|
672
|
+
package is installed from npm. 0.3.2 wrote the label as `content-[attr(data-label)_/_""]`; the
|
|
673
|
+
library build escaped its quotes in the FESM (`\"\"`), so Tailwind built the rule for a class no
|
|
674
|
+
cell carries and every label `::before` computed to `none`. The cells now carry
|
|
675
|
+
`before:content-data-label`, a utility `theme.css` defines (below). Apps that added the
|
|
676
|
+
workaround `@source inline('@max-xl:before:content-[attr(data-label)_/_""] pointer-coarse:before:content-[attr(data-label)_/_""]')`
|
|
677
|
+
can remove it. consumer-smoke now opens a stacked table built from the packed package in
|
|
678
|
+
Chromium, under a fine and a coarse pointer, and fails if any FESM class string holds a
|
|
679
|
+
backslash escape.
|
|
680
|
+
|
|
681
|
+
The stacked Sort by select's first option is Not sorted (new label key `tableSortNone`, "Not
|
|
682
|
+
sorted"): it shows while `sort` is null, so the select is no longer an empty box, and choosing it
|
|
683
|
+
sets `sort` back to null. New `BfTableHarness.getSortSelect()` returns that select's
|
|
684
|
+
`BfSelectHarness` (`null` when the table cannot stack or has no sortable column).
|
|
685
|
+
|
|
668
686
|
### 0.3.2
|
|
669
687
|
|
|
670
688
|
New opt-in `stack` input (`BfTableStack`: `'never'` by default, so existing tables do not
|
|
@@ -33,6 +33,7 @@ import { type BfLabels } from '@bitforgehq/ui';
|
|
|
33
33
|
| `notifications` | `string` | `'Notifications'` | The desktop toast stack's region name. |
|
|
34
34
|
| `tableSelectAll` | `string` | `'Select all rows'` | bf-table's header checkbox, which selects or clears every row. |
|
|
35
35
|
| `tableSortBy` | `string` | `'Sort by'` | The label of a stacked bf-table's sort select. |
|
|
36
|
+
| `tableSortNone` | `string` | `'Not sorted'` | A stacked bf-table's first sort option: rows in the order given, no column sorted. |
|
|
36
37
|
| `tableSortAscending` | `(column: string) => string` | `` (column) => `${column}, ascending` `` | A stacked bf-table's sort option for a column in ascending order. |
|
|
37
38
|
| `tableSortDescending` | `(column: string) => string` | `` (column) => `${column}, descending` `` | A stacked bf-table's sort option for a column in descending order. |
|
|
38
39
|
| `theme` | `string` | `'Theme'` | bf-theme-switch's and bf-theme-menu-button's name without a label. |
|
|
@@ -54,6 +54,11 @@ Throws for an unknown or unsortable column.
|
|
|
54
54
|
|
|
55
55
|
The sorted column (its `key`) and direction, from `aria-sort`.
|
|
56
56
|
|
|
57
|
+
### `getSortSelect(): Promise<BfSelectHarness | null>`
|
|
58
|
+
|
|
59
|
+
The Sort by select in a stacked table's bar: its first option, Not sorted (`tableSortNone`),
|
|
60
|
+
sets `sort` to null and shows while it is; then each sortable column ascending and descending.
|
|
61
|
+
|
|
57
62
|
### `selectRow(index: number): Promise<void>`
|
|
58
63
|
|
|
59
64
|
Checks one row's checkbox, as a user would; a selected row stays selected. Throws when the
|
|
@@ -74,4 +79,5 @@ The number of selected rows (checked row checkboxes).
|
|
|
74
79
|
|
|
75
80
|
## Uses
|
|
76
81
|
|
|
82
|
+
- [BfSelectHarness](bf-select-harness.md)
|
|
77
83
|
- [BfTableSort](bf-table-sort.md)
|
|
@@ -8,7 +8,7 @@ Replaces built-in strings, merged over the parent's (validation merged key by ke
|
|
|
8
8
|
out or set to `undefined` keeps the parent's string. In `ApplicationConfig.providers` it covers
|
|
9
9
|
every string. In a component's `providers` it covers that sub-tree's widgets and form controls
|
|
10
10
|
(`loading`, `dismiss`, `tabBar`, `tabBadge`, `back`, `refreshing`, `tableSelectAll`, `tableSortBy`,
|
|
11
|
-
`tableSortAscending`, `tableSortDescending`, `theme`, `themeSystem`, `themeLight`, `themeDark`,
|
|
11
|
+
`tableSortNone`, `tableSortAscending`, `tableSortDescending`, `theme`, `themeSystem`, `themeLight`, `themeDark`,
|
|
12
12
|
`breadcrumb`, `breadcrumbExpand`, `navigation`, `navBadge`, `sidebarCollapse`, `sidebarExpand`,
|
|
13
13
|
`skipToContent`, `pagination`, `previousPage`, `nextPage`, `pageOf`, `goToPage`, `tabGroup`,
|
|
14
14
|
`calendarPreviousMonth`, `calendarNextMonth`, `calendarChooseMonth`, `calendarChooseYear`,
|
|
@@ -2959,13 +2959,22 @@ var BfHeaderComponent = class BfHeaderComponent {
|
|
|
2959
2959
|
};
|
|
2960
2960
|
measure();
|
|
2961
2961
|
if (typeof ResizeObserver !== "undefined") {
|
|
2962
|
-
|
|
2962
|
+
let frame = null;
|
|
2963
|
+
const observer = new ResizeObserver(() => {
|
|
2964
|
+
frame ??= requestAnimationFrame(() => {
|
|
2965
|
+
frame = null;
|
|
2966
|
+
measure();
|
|
2967
|
+
});
|
|
2968
|
+
});
|
|
2963
2969
|
observer.observe(toolbar);
|
|
2964
2970
|
observer.observe(start);
|
|
2965
2971
|
observer.observe(end);
|
|
2966
2972
|
for (const element of [title, sizer]) if (element) observer.observe(element);
|
|
2967
2973
|
if (block) observer.observe(block);
|
|
2968
|
-
onCleanup(() =>
|
|
2974
|
+
onCleanup(() => {
|
|
2975
|
+
observer.disconnect();
|
|
2976
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
2977
|
+
});
|
|
2969
2978
|
}
|
|
2970
2979
|
});
|
|
2971
2980
|
let destroyed = false;
|