laif-ds 1.0.0 → 1.0.2-app-data-grid.76

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.
Files changed (76) hide show
  1. package/dist/CHANGELOG.md +282 -0
  2. package/dist/_virtual/index7.js +5 -2
  3. package/dist/_virtual/index8.js +2 -5
  4. package/dist/agent-docs/adoption-report.json +164 -83
  5. package/dist/agent-docs/components/AppBottomNav.md +180 -49
  6. package/dist/agent-docs/components/AppCard.md +25 -21
  7. package/dist/agent-docs/components/AppDataGrid.md +2602 -0
  8. package/dist/agent-docs/components/AppEditor.md +18 -13
  9. package/dist/agent-docs/components/AppSelect.md +50 -0
  10. package/dist/agent-docs/components/AppSidebar.md +504 -11
  11. package/dist/agent-docs/components/DataTable.md +34 -3
  12. package/dist/agent-docs/components/DatePicker.md +7 -7
  13. package/dist/agent-docs/components/FileUploader.md +12 -12
  14. package/dist/agent-docs/components/GridFilterPanel.md +112 -0
  15. package/dist/agent-docs/components/Sidebar.md +72 -1
  16. package/dist/agent-docs/components/SidebarMenuBadge.md +87 -0
  17. package/dist/agent-docs/components-list.md +47 -2
  18. package/dist/agent-docs/manifest.json +310 -36
  19. package/dist/components/design-tokens.js +138 -66
  20. package/dist/components/nav-tokens.js +41 -0
  21. package/dist/components/ui/app-bottom-nav.js +125 -130
  22. package/dist/components/ui/app-data-grid/app-data-grid.js +874 -0
  23. package/dist/components/ui/app-data-grid/engine/use-grid-column-resize.js +110 -0
  24. package/dist/components/ui/app-data-grid/engine/use-grid-engine.js +598 -0
  25. package/dist/components/ui/app-data-grid/engine/use-grid-infinite-loader.js +50 -0
  26. package/dist/components/ui/app-data-grid/engine/use-grid-virtualizer.js +90 -0
  27. package/dist/components/ui/app-data-grid/grid-aggregate.js +5 -0
  28. package/dist/components/ui/app-data-grid/grid-column-registry.js +28 -0
  29. package/dist/components/ui/app-data-grid/grid-columns.js +204 -0
  30. package/dist/components/ui/app-data-grid/grid-filter-compile.js +33 -0
  31. package/dist/components/ui/app-data-grid/grid-filter-operators.js +139 -0
  32. package/dist/components/ui/app-data-grid/grid-grouping.js +104 -0
  33. package/dist/components/ui/app-data-grid/grid-header-matrix.js +112 -0
  34. package/dist/components/ui/app-data-grid/grid-i18n.js +207 -0
  35. package/dist/components/ui/app-data-grid/grid-mount-count.js +8 -0
  36. package/dist/components/ui/app-data-grid/grid-reducer.js +200 -0
  37. package/dist/components/ui/app-data-grid/grid-result-scope.js +35 -0
  38. package/dist/components/ui/app-data-grid/grid-tokens.js +169 -0
  39. package/dist/components/ui/app-data-grid/grid-view-preset.js +97 -0
  40. package/dist/components/ui/app-data-grid/parts/grid-actions-row.js +86 -0
  41. package/dist/components/ui/app-data-grid/parts/grid-cell-text.js +67 -0
  42. package/dist/components/ui/app-data-grid/parts/grid-column-registry-panel.js +320 -0
  43. package/dist/components/ui/app-data-grid/parts/grid-density-control.js +88 -0
  44. package/dist/components/ui/app-data-grid/parts/grid-filter-chips.js +224 -0
  45. package/dist/components/ui/app-data-grid/parts/grid-filter-editor.js +197 -0
  46. package/dist/components/ui/app-data-grid/parts/grid-filter-panel.js +386 -0
  47. package/dist/components/ui/app-data-grid/parts/grid-filter-row.js +273 -0
  48. package/dist/components/ui/app-data-grid/parts/grid-fullscreen-toggle.js +29 -0
  49. package/dist/components/ui/app-data-grid/parts/grid-group-row.js +135 -0
  50. package/dist/components/ui/app-data-grid/parts/grid-header.js +338 -0
  51. package/dist/components/ui/app-data-grid/parts/grid-pagination-control.js +218 -0
  52. package/dist/components/ui/app-data-grid/parts/grid-row.js +181 -0
  53. package/dist/components/ui/app-data-grid/parts/grid-scroll-area.js +186 -0
  54. package/dist/components/ui/app-data-grid/parts/grid-selection-bar.js +195 -0
  55. package/dist/components/ui/app-data-grid/parts/grid-status-surface.js +66 -0
  56. package/dist/components/ui/app-data-grid/parts/grid-table.js +482 -0
  57. package/dist/components/ui/app-data-grid/parts/grid-toolbar-slots.js +192 -0
  58. package/dist/components/ui/app-data-grid/use-grid-filters.js +173 -0
  59. package/dist/components/ui/app-select.js +332 -222
  60. package/dist/components/ui/app-sidebar-mobile.js +119 -104
  61. package/dist/components/ui/app-sidebar.js +516 -313
  62. package/dist/components/ui/button.js +2 -1
  63. package/dist/components/ui/gantt/components/Chart/Chart.js +1 -1
  64. package/dist/components/ui/nav-badge.js +66 -0
  65. package/dist/components/ui/sidebar.js +272 -207
  66. package/dist/components/ui/tables/data-cross-table/data-cross-table.js +6 -6
  67. package/dist/components/ui/tables/data-table/data-table-constants.js +36 -16
  68. package/dist/components/ui/tables/data-table/data-table-i18n.js +135 -1
  69. package/dist/css-for-template.css +27 -0
  70. package/dist/index.d.ts +5872 -144
  71. package/dist/index.js +473 -406
  72. package/dist/node_modules/eventemitter3/index2.js +1 -1
  73. package/dist/styles.css +2 -1
  74. package/dist/styles.v3.css +1 -1
  75. package/package.json +25 -7
  76. /package/dist/agent-docs/components/{truncated-cell.md → TruncatedCell.md} +0 -0
@@ -9,6 +9,8 @@ const e = {
9
9
  default: "rounded-md",
10
10
  sm: "rounded-sm",
11
11
  lg: "rounded-lg",
12
+ xl: "rounded-xl",
13
+ "2xl": "rounded-2xl",
12
14
  full: "rounded-full"
13
15
  },
14
16
  // Input & Button heights/paddings
@@ -21,9 +23,136 @@ const e = {
21
23
  // out at h-10 = 40px, which is fine for a mouse and short for a thumb.
22
24
  touch: "min-h-11 px-4 py-2.5 text-sm",
23
25
  icon: {
24
- default: "size-9"
26
+ default: "size-9",
27
+ sm: "size-8",
28
+ lg: "size-10",
29
+ // 24px. Below the 32px of `sm`, because a 36px row with `py-1` leaves 28px
30
+ // of usable space and `sm` does not fit in it. Not a default anywhere:
31
+ // it exists so a dense row can have a real target instead of a hand-written
32
+ // `className="size-6"` override.
33
+ xs: "size-6"
25
34
  }
26
35
  },
36
+ // Table geometry, measured on the AppDataGrid mockup rather than declared:
37
+ // a 36px data row and a 32px header row are what the two-level sticky header
38
+ // offsets (`top-0` then `top-8`) already assume.
39
+ table: {
40
+ rowHeight: 36,
41
+ rowHeightClass: "h-9",
42
+ headerHeight: 32,
43
+ headerHeightClass: "h-8",
44
+ cellPaddingX: "px-3"
45
+ },
46
+ // Stacking order inside a table. A pinned column and a sticky header both win
47
+ // over body cells, and where they intersect the pinned header cell must win
48
+ // over both — that intersection is why three levels are needed and not two.
49
+ tableZ: {
50
+ pinnedCell: 10,
51
+ stickyHeader: 20,
52
+ pinnedHeader: 30,
53
+ focusLift: 40,
54
+ overlay: 50,
55
+ // APPENDED IN R21, and it is the rung the `<thead>` borrows WHILE a resize
56
+ // handle inside it has the focus — never at rest. It exists because
57
+ // `focusLift` cannot do the job it was named for once the scrollbar lane
58
+ // has a z of its own.
59
+ //
60
+ // THE MECHANISM, and it is not the one it looks like. The `<thead>` is
61
+ // `sticky` and carries `stickyHeader`, so it is a positioned element with a
62
+ // z-index other than auto: it CREATES A STACKING CONTEXT. The resize handle
63
+ // lives in a leaf `<th>` that is `relative` with NO z of its own, so its
64
+ // own focus lift is sealed inside the header's context — from outside, the
65
+ // whole `<thead>` is worth 20. Inside that context the lift still does its
66
+ // job (it beats the pinned cells at 30, which share the context); against
67
+ // the scrollbar lane, which lives in the ScrollArea root's context at 40,
68
+ // no value on the handle can win. RAISING THE HANDLE'S LIFT IS INEFFECTIVE
69
+ // BY CONSTRUCTION — verified, not assumed. So the CONTEXT is what moves.
70
+ //
71
+ // ⚠️ THE VALUE IS MEASURED, not one step above something. Three nodes are
72
+ // involved and all three were probed in the same run, in the scene that has
73
+ // all of them: the filter panel body reads 50, the `<thead>` reads 20, the
74
+ // vertical lane reads 40 — and the stacking-context root of all three is the
75
+ // DOCUMENT ROOT (walked the ancestors of each: no positioned+z, transform,
76
+ // filter, will-change, contain:paint, isolation or opacity<1 in between), so
77
+ // the three numbers really do compare with one another. The order that has
78
+ // to hold is lane < lifted header < filter panel: an overlay covering the
79
+ // header is correct, a scrollbar covering a focused control is WCAG 2.2
80
+ // SC 2.4.11. That leaves 41..49, and 45 is taken because it sits midway —
81
+ // equidistant headroom if either neighbour is later nudged by a step.
82
+ // 50 was rejected on the measurement: it TIES with the filter panel, and a
83
+ // tie is decided by document order, which is not a thing this token can
84
+ // control.
85
+ //
86
+ // ⚠️ Deliberately NOT a Tailwind class, for the same reason as `fullscreen`
87
+ // right below: neither config emits a step at this number — the v3 bundle
88
+ // emits only the multiples of ten from 0 to 50 — and the lane's own gate
89
+ // rejects any class token containing a bracket, variants included. It is
90
+ // applied INLINE from this token in `parts/grid-table.tsx`, one source.
91
+ //
92
+ // ⚠️ AND IT IS TEMPORARY. Applied only while a handle holds the focus, and
93
+ // removed on blur so the header falls back to its class. A header parked up
94
+ // here would re-stack every scene in the grid, and the specs that read the
95
+ // header's z at rest are many.
96
+ stickyHeaderFocusLift: 45,
97
+ // ADDED IN P6, and it is the only rung that has to beat things this object
98
+ // does not describe. A grid promoted to full screen must cover the overlays
99
+ // the OLD DataTable portals to the body (PIANO §6, row "Fullscreen"), and
100
+ // those do not read `tableZ` — they carry the z-index of the shared
101
+ // primitive they are built from. So the number comes from a MEASUREMENT of
102
+ // what is actually on the page, not from "one step above `overlay`":
103
+ //
104
+ // grep -rhoE 'z-(\[[0-9]+\]|[0-9]+)' src/components | sort -rn | uniq -c
105
+ // 100 ← ONE site, and it is the frozen table's own horizontal ScrollBar
106
+ // (`data-table-body.tsx:386`)
107
+ // 60 ← `popover.tsx` — the primitive the old table's column-visibility
108
+ // and filter popovers are made of
109
+ // 50 ← `dropdown-menu.tsx`, and `tableZ.overlay`
110
+ //
111
+ // 60 was the first choice and it was WRONG: it TIES with `popover.tsx`, and
112
+ // a tie is decided by document order — a Radix popover portals to the end of
113
+ // <body>, i.e. always after the grid, so the popover would have won and the
114
+ // criterion would have failed. 110 clears the measured ceiling (100) with
115
+ // the smallest clean jump, so the fullscreen grid also covers that
116
+ // ScrollBar, which belongs to the old table too.
117
+ //
118
+ // ⚠️ Deliberately NOT a Tailwind class. Neither config has a step at that number,
119
+ // and adding one would put the number in two configs PLUS here — three
120
+ // sources for one scale, which is the defect P4's review found on the
121
+ // checkbox column width. It is applied INLINE from this token, one source.
122
+ //
123
+ // ⚠️ MEASURED LIMIT, not a guess. A `position: fixed` element only competes
124
+ // with a body-level portal when NO ancestor creates a stacking context.
125
+ // Probed in Chrome with `document.elementFromPoint`: with a plain ancestor
126
+ // the fullscreen node wins; give that same ancestor `transform:
127
+ // translateZ(0)` or `filter: blur(0px)` and the PORTAL wins, because the
128
+ // fixed node's z-index is then scoped inside the ancestor's context and is
129
+ // compared against its siblings, not against the body. No number fixes
130
+ // that — it is a containing-block fact. Documented in AppDataGrid.md.
131
+ fullscreen: 110,
132
+ // ADDED IN P6, and it exists because `fullscreen` alone created the MIRROR
133
+ // of the problem it solved. Raising the grid above the old table's
134
+ // body-portaled overlays also raised it above ITS OWN: the grid's column
135
+ // menu is a Radix dropdown portaled to <body> at z-50, so at full screen it
136
+ // was painted UNDER the grid and became unreachable — taking with it P4's
137
+ // non-dragging pointer path (WCAG 2.5.7 "widen/narrow") and sorting from the
138
+ // menu. Measured with `document.elementFromPoint` at the open menu's centre:
139
+ // "GRID on top — MENU BURIED".
140
+ //
141
+ // WHY A NUMBER AND NOT A PORTAL CONTAINER. The right fix architecturally is
142
+ // to portal the grid's own overlays INTO the grid, but `DropdownMenuContent`
143
+ // hard-codes its Portal with no `container` passthrough, and that file is
144
+ // SHARED SURFACE the frozen DataTable renders — this lane does not change it.
145
+ // So the grid raises its own overlay from its own side instead.
146
+ //
147
+ // HOW IT REACHES THE RIGHT ELEMENT (measured, and not obvious): the z-index
148
+ // that decides is the one on Radix's popper WRAPPER, not on the menu content
149
+ // — the wrapper is the stacking context. Probed in Chrome: setting the
150
+ // content's z to 120 after mount left the menu buried, setting the WRAPPER's
151
+ // to 120 lifted it. Radix copies the content's computed z-index onto the
152
+ // wrapper at mount, so an inline z on the content does reach the wrapper —
153
+ // which is why it is applied inline and only while full screen is active.
154
+ fullscreenOverlay: 120
155
+ },
27
156
  // Text styles for inputs/buttons
28
157
  text: {
29
158
  base: "text-sm font-medium whitespace-nowrap",
@@ -68,74 +197,17 @@ const e = {
68
197
  },
69
198
  // Badge specific
70
199
  badge: {
71
- base: "inline-flex items-center justify-center border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 gap-1"
72
- },
73
- // Navigation rows (sidebar / menu lists) specific.
74
- //
75
- // Not `dropdownItem.selected`: that one paints the accent colour *on a 10%
76
- // dilution of itself* — measured 1.85:1 in light, 3.28 tangerine, 4.23
77
- // claymorphism, 6.62 dark. Fine as a chip inside a dropdown, unreadable as a
78
- // row label. Here the tint stays on the surface and the label keeps
79
- // full-contrast `text-d-foreground` (12.19 / 10.35 / 9.35 / 10.22).
80
- //
81
- // The bar is `d-foreground`, not `d-primary`, for the same reason: in the
82
- // light theme `--d-primary` is a pale cyan that measures 1.85:1 against the
83
- // tinted row, so a primary bar would fail WCAG 1.4.11 (>= 3:1 for a non-text
84
- // indicator) in the default theme. Shape and position carry the state; the
85
- // tint adds the brand colour on top where the theme can afford it.
86
- //
87
- // `base` goes on every row so the selected row's 2px does not shift content.
88
- navItem: {
89
- base: "border-l-2 border-l-transparent",
90
- selected: "bg-d-primary/10 border-l-d-foreground text-d-foreground font-semibold",
91
- // A bottom bar's cells are columns, not rows, so the left rule has no edge
92
- // to run along and the state has to be carried by the cell's own surface: a
93
- // pill, no border on any side.
94
- //
95
- // The pill is the sidebar row's own state, ported: `bg-d-sidebar-accent` +
96
- // `text-d-sidebar-accent-foreground` + a bold label, which is exactly what
97
- // `sidebarMenuButtonVariants` paints under `data-[active=true]`. One
98
- // vocabulary for "you are here" across the two navigations, which is worth
99
- // more than either surface being optimal on its own.
100
- //
101
- // What comes with it is the sidebar's contrast, and it is very low.
102
- // `--d-sidebar-accent` is `--colors--surface--tertiary` at 40% alpha, so on
103
- // the bar (`d-card`, #fafafa light / #1f1f1f dark) it composites to #efefef
104
- // and #2f2f2f — **1.10:1 in light and 1.23:1 in dark**, far under the 3:1 of
105
- // WCAG 1.4.11 for a non-text state indicator. That is not a regression
106
- // introduced here: the same fill on the sidebar's own surface (#f5f5f5)
107
- // measures 1.08:1, so the desktop sidebar has always marked the current row
108
- // by *weight*, with the tint as a hint. The label is never at risk either
109
- // way — `--d-sidebar-accent-foreground` is `--colors--text--body-primary`,
110
- // the colour the inactive cells already use, 11.9:1 on the composited fill.
111
- //
112
- // `font-bold` and not `semibold`: with a fill this faint the weight is the
113
- // state, and it is the sidebar's own value. It costs label width on a cell
114
- // that already truncates at 375px — if that has to be bought back,
115
- // `font-semibold` is the place to spend it, not the fill.
116
- //
117
- // Measured alternatives, for whenever the 3:1 floor has to be met.
118
- // `bg-d-primary` + `text-d-primary-foreground`: 1.98:1 light / 7.99:1 dark —
119
- // the brand fill this replaced, under the floor in light but separating by
120
- // hue, which 1.4.11 does not measure. `bg-d-foreground` + `text-d-background`:
121
- // 13.01:1 / 12.49:1 — conforming, but it reads as a selected chip rather
122
- // than the current tab and leaves the bar with no brand. Not a tint of
123
- // primary: /10 is 1.07:1 light, /40 still 1.30:1, solid `bg-d-accent` 1.26:1.
124
- //
125
- // `rounded-md` (6px, = `radius.default`) and not `radius.sm`: `sm` is 4px in
126
- // the v4 build and 2px in the v3 one, and this would be the single place
127
- // where the two CSS bundles disagree. Not `rounded-d-md` either — that maps
128
- // onto `--d-radius` and becomes 18px under claymorphism, while the dock it
129
- // sits inside is a fixed `rounded-t-lg`.
130
- //
131
- // `basePill` reserves nothing (there is no border to reserve), so activating
132
- // a cell cannot shift its content; it is there so every cell shares the
133
- // radius and only the fill changes.
134
- basePill: "rounded-md",
135
- selectedPill: "bg-d-sidebar-accent text-d-sidebar-accent-foreground font-bold"
200
+ base: "inline-flex items-center justify-center border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 gap-1",
201
+ sizes: {
202
+ default: "h-5 px-2.5",
203
+ // Badges are usually smaller
204
+ sm: "h-4 px-1.5 text-[10px]",
205
+ lg: "h-6 px-3"
206
+ }
136
207
  },
137
208
  // Dropdown item specific
138
209
  dropdownItem: {
210
+ base: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm text-d-foreground outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
139
211
  selected: "bg-d-primary/10 text-d-primary font-medium data-[selected=true]:text-d-primary data-[selected=true]:bg-d-primary/30 hover:text-d-primary hover:bg-d-primary/30",
140
212
  hover: "data-[selected=true]:bg-d-accent/50 data-[selected=true]:text-d-accent-foreground hover:bg-d-accent/50 hover:text-d-accent-foreground"
141
213
  }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+ const e = {
3
+ /**
4
+ * The state of a navigation item, in the two shapes the surfaces need: a left
5
+ * rule for a vertical list, a pill for a horizontal bar.
6
+ */
7
+ item: {
8
+ // The bar is `d-foreground`, not `d-primary`: in the light theme
9
+ // `--d-primary` measures 1.85:1 against the tinted row and a primary bar
10
+ // would fail WCAG 1.4.11 (>= 3:1 for a non-text indicator).
11
+ // `base` goes on every row so the selected row's 2px does not shift content.
12
+ base: "border-l-2 border-l-transparent",
13
+ selected: "bg-d-primary/10 border-l-d-foreground text-d-foreground font-semibold",
14
+ // Duplicates what `sidebarMenuButtonVariants` paints under
15
+ // `data-[active=true]` in `sidebar.tsx` — two copies of "you are here", by
16
+ // decision. The fill is ~1.1:1, under WCAG 1.4.11's 3:1, so `font-bold`
17
+ // carries the state and the tint is only a hint. `rounded-md` and not
18
+ // `radius.sm` (4px in v4, 2px in v3) nor `rounded-d-md` (18px in clay).
19
+ basePill: "rounded-md",
20
+ selectedPill: "bg-d-sidebar-accent text-d-sidebar-accent-foreground font-bold"
21
+ },
22
+ /**
23
+ * The icon rail at 3rem, where the glyph carries the whole row. One home for
24
+ * the geometry because `sidebarMenuButtonVariants`, the two initial-letter
25
+ * fallbacks in `app-sidebar.tsx` and the group selector must agree on it.
26
+ */
27
+ rail: {
28
+ // The content box is exactly the glyph (`32 - 2*6 = 20`), and that equality
29
+ // is the centring mechanism: the label stays mounted as a `flex-1` sibling,
30
+ // so any slack in the box is claimed by the label and pushes every glyph
31
+ // off-centre. Change one of the three numbers and the other two follow.
32
+ button: "group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-1.5 group-data-[collapsible=icon]:mx-auto group-data-[collapsible=icon]:[&>svg]:size-5",
33
+ // Sized to the rail's content box in both states, for the same reason: a
34
+ // 16px box in the rail's 20px content box leaves 4px for the label to claim
35
+ // and that row's letter sits left of every glyph above it.
36
+ fallbackGlyph: "size-4 group-data-[collapsible=icon]:size-5 group-data-[collapsible=icon]:text-sm"
37
+ }
38
+ };
39
+ export {
40
+ e as navTokens
41
+ };
@@ -1,159 +1,154 @@
1
1
  "use client";
2
- import { jsx as o, jsxs as y, Fragment as T } from "react/jsx-runtime";
3
- import * as F from "react";
4
- import { designTokens as u } from "../design-tokens.js";
5
- import { Icon as K } from "./icon.js";
6
- import { useSidebar as R } from "./sidebar.js";
7
- import { cn as i } from "../../lib/utils.js";
8
- const I = 6, w = /* @__PURE__ */ new Set(), c = (t) => {
9
- w.has(t) || (w.add(t), console.warn(t));
10
- }, $ = (t) => `${t.url} ${t.title}`, k = (t) => !!t.isActive || !!t.subItems?.some((n) => n.isActive), z = "md:hidden shrink-0 overflow-hidden bg-d-card border-d-border rounded-t-lg border-t pb-[env(safe-area-inset-bottom)]", D = "flex items-stretch p-1", m = i(
2
+ import { jsxs as d, jsx as n, Fragment as J } from "react/jsx-runtime";
3
+ import * as Q from "react";
4
+ import { designTokens as U } from "../design-tokens.js";
5
+ import { navTokens as T } from "../nav-tokens.js";
6
+ import { Icon as V } from "./icon.js";
7
+ import { warnOnce as i, someRowBadged as _, normalizeNavBadge as W, NavBadgeOverlay as Y, NavBadgeDot as w, NavBadgeSrLabel as Z } from "./nav-badge.js";
8
+ import { useSidebar as ee } from "./sidebar.js";
9
+ import { cn as r } from "../../lib/utils.js";
10
+ const O = 6, $ = (a) => `${a.url} ${a.title}`, A = (a) => !!a.isActive || !!a.subItems?.some((o) => o.isActive), te = "md:hidden shrink-0 overflow-hidden bg-d-card border-d-border rounded-t-lg border-t pb-[env(safe-area-inset-bottom)]", ae = "flex items-stretch p-1", g = r(
11
11
  // `min-h-11` (44px) written out rather than `sizes.touch`: that token also
12
- // carries `px-4`, and 16px of horizontal padding inside a ~61px cell would
13
- // leave 29px for the label. `min-h-`, never `h-`: a cell must be able to grow.
14
- // `py-1.5` and not `py-2.5`: the bar's breathing room is the `<nav>`'s 4px
15
- // inset, and paying for it twice would put the cell at 54px and the bar at
16
- // 63px — past the column budget for no legibility gained. It stayed at
17
- // `py-1.5` when the icon dropped to 16px, deliberately: `py-2` would have held
18
- // the bar at the 59px it had with a 20px glyph, and a lower bar was the point
19
- // of the smaller glyph, not a side effect of it. 12px of vertical padding
20
- // around 34px of content (16 + 2 + 16) puts the pill at 46px.
12
+ // carries `px-4` and `text-sm`, and 16px of horizontal padding inside a ~61px
13
+ // cell would leave 29px for the label. `min-h-`, never `h-`: a cell must be
14
+ // able to grow. `cursor-pointer` because v4's preflight sets
15
+ // `cursor: default` on `<button>`, so without it the Menu cell would feel
16
+ // inert next to the `<a>` cells.
21
17
  //
22
- // Which leaves this value boxed in from both directions, and tighter than it
23
- // was: upwards by the column budget, downwards by the 44px touch floor, now
24
- // 2px away instead of 6. Trim `py-1.5`, the icon or the label's line-height
25
- // any further and `min-h-11` takes the cell's height over from the padding —
26
- // at which point the class list stops describing what renders, silently.
27
- // Measure the rendered cell, not the padding.
28
- // `cursor-pointer` because the two cell elements must not diverge: v4's
29
- // preflight sets `cursor: default` on `<button>`, so without it the Menu cell
30
- // would feel inert next to the `<a>` cells on a desktop-narrow window.
31
- //
32
- // `flex-1 min-w-0` is the equal-width guarantee, and both halves are load
33
- // bearing. `flex-1` alone is `1 1 0%`, which distributes the row evenly only
34
- // as long as no cell's content forces it wider: a flex item's automatic
35
- // minimum size is its min-content width, so a cell whose contents cannot
36
- // shrink below the fair share silently takes more than it and every other
37
- // cell takes less. Here that floor is 24px — a `shrink-0` 16px icon plus
38
- // `px-1` — and the narrowest cell the budget produces is 52px (six cells at
39
- // 320px), so today the row is even by arithmetic. `min-w-0` makes it even by
40
- // construction instead, at any width and whatever a cell holds, which is what
41
- // the row's rhythm actually depends on. The label shrinks in exchange
42
- // (`w-full truncate`, whose `overflow-hidden` already floors its own
43
- // min-content size at 0) — the label is the right thing to spend.
44
- "flex min-h-11 min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 px-1 py-1.5 text-xs",
45
- // The radius goes on every cell, not only the marked one: it costs nothing
46
- // while the fill is transparent, and it keeps the hover and press surfaces of
47
- // an inactive cell the same shape as the pill they can become.
48
- u.navItem.basePill,
49
- u.focusRing,
50
- // `ring-inset` is still right, for a new reason: the cells now touch each
51
- // other, not the dock's clipped corners, so a 3px outset ring would not be
52
- // shaved — it would spill over the neighbouring cell and read as if two cells
53
- // were focused.
18
+ // `min-w-0` beside `flex-1` is the equal-width guarantee: a flex item's
19
+ // automatic minimum size is its min-content width, so without it a cell whose
20
+ // contents cannot shrink below the fair share takes more than it and every
21
+ // other cell takes less. The label is what gives way instead.
22
+ "flex min-h-11 min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 px-1 py-2.5 text-xs",
23
+ // On every cell, not only the marked one: it keeps the hover and press
24
+ // surfaces of an inactive cell the same shape as the pill they can become.
25
+ T.item.basePill,
26
+ U.focusRing,
27
+ // `ring-inset` because the cells touch each other: an outset ring would spill
28
+ // over the neighbour and read as if two cells were focused.
54
29
  "focus-visible:ring-inset"
55
- ), p = i(
30
+ ), h = r(
56
31
  "text-d-foreground font-medium",
57
32
  "[@media(hover:hover)_and_(pointer:fine)]:hover:bg-d-sidebar-accent [@media(hover:hover)_and_(pointer:fine)]:hover:text-d-sidebar-accent-foreground",
58
33
  "active:bg-d-sidebar-accent active:text-d-sidebar-accent-foreground"
59
- ), v = u.navItem.selectedPill, d = (t, n) => /* @__PURE__ */ y(T, { children: [
60
- /* @__PURE__ */ o(K, { name: t, size: "xs", className: "shrink-0" }),
61
- /* @__PURE__ */ o("span", { className: "w-full truncate text-center", children: n })
34
+ ), M = T.item.selectedPill, P = "relative flex size-5 shrink-0 items-center justify-center", c = (a, o) => /* @__PURE__ */ d("span", { className: P, children: [
35
+ /* @__PURE__ */ n(V, { name: a, size: "sm", className: "shrink-0" }),
36
+ o
37
+ ] }), ne = (a, o) => /* @__PURE__ */ d("span", { className: r(P, "[&>svg]:size-full"), children: [
38
+ a,
39
+ o
40
+ ] }), oe = "-top-1 -right-1.5", S = "absolute -top-1 -right-1", N = (a, o, m) => /* @__PURE__ */ d(J, { children: [
41
+ a,
42
+ /* @__PURE__ */ n("span", { className: "w-full truncate text-center", children: o }),
43
+ m
62
44
  ] });
63
- function J({
64
- items: t,
65
- showMenu: n = !0,
66
- menuLabel: A = "Menu",
67
- ariaLabel: B = "Main navigation",
68
- menuIconName: M = "Menu",
69
- linkComponent: b,
70
- linkProps: S = {},
71
- className: _,
72
- ...j
45
+ function pe({
46
+ items: a,
47
+ showMenu: o = !0,
48
+ menuLabel: m = "Menu",
49
+ ariaLabel: j = "Main navigation",
50
+ menuIcon: p = "Menu",
51
+ menuBadge: D = !1,
52
+ linkComponent: x,
53
+ linkProps: z = {},
54
+ className: E,
55
+ ...R
73
56
  }) {
74
- const { openMobile: f, setOpenMobile: h, mobileNavTarget: L, setMobileNavTarget: g } = R(), s = n ? I - 1 : I, r = t.slice(0, s);
75
- if (t.length > s) {
76
- const e = t.slice(s).map((a) => `"${a.title}"`).join(", ");
77
- c(
78
- `AppBottomNav: ${t.length} items passed, ${s} slots available. Dropped: ${e}.`
57
+ const { openMobile: I, setOpenMobile: B, mobileNavTarget: F, setMobileNavTarget: C } = ee(), u = o ? O - 1 : O, l = a.slice(0, u), v = a.slice(u);
58
+ if (v.length > 0) {
59
+ const e = v.map((t) => `"${t.title}"`).join(", ");
60
+ i(
61
+ `AppBottomNav: ${a.length} items passed, ${u} slots available. Dropped: ${e}.`
79
62
  );
80
63
  }
81
- const N = /* @__PURE__ */ new Set();
82
- for (const e of r) {
83
- const a = $(e);
84
- N.has(a) && c(
85
- `AppBottomNav: duplicate item "${a}" — one of them is ignored.`
86
- ), N.add(a);
64
+ const L = /* @__PURE__ */ new Set();
65
+ for (const e of l) {
66
+ const t = $(e);
67
+ L.has(t) && i(
68
+ `AppBottomNav: duplicate item "${t}" — one of them is ignored.`
69
+ ), L.add(t);
87
70
  }
88
- const l = r.findIndex(k);
89
- r.filter(k).length > 1 && c(
90
- `AppBottomNav: more than one active item — "${r[l].title}" wins.`
71
+ const b = l.findIndex(A);
72
+ l.filter(A).length > 1 && i(
73
+ `AppBottomNav: more than one active item — "${l[b].title}" wins.`
91
74
  );
92
- const x = n && l === -1, P = (e, a) => {
93
- const C = {
94
- className: i(
95
- m,
96
- a ? v : p
75
+ const K = D || _(v), G = (() => {
76
+ const e = K ? /* @__PURE__ */ n(w, { className: S }) : null;
77
+ if (typeof p == "string") return c(p, e);
78
+ const t = p();
79
+ return t == null ? c("Menu", e) : ne(t, e);
80
+ })(), k = (e) => {
81
+ const t = W(e.badge, "compact", e.title), s = /* @__PURE__ */ n(Z, { row: e, badge: t });
82
+ return t?.kind === "text" ? {
83
+ visual: /* @__PURE__ */ n(Y, { text: t.text, className: oe }),
84
+ srLabel: s
85
+ } : t?.kind === "dot" || _(e.subItems) ? { visual: /* @__PURE__ */ n(w, { className: S }), srLabel: s } : { visual: null, srLabel: s };
86
+ }, X = (e, t) => {
87
+ const s = {
88
+ className: r(
89
+ g,
90
+ t ? M : h
97
91
  ),
98
92
  "data-slot": "app-bottom-nav-item",
99
- "data-active": a ? "true" : void 0,
93
+ "data-active": t ? "true" : void 0,
100
94
  // Not set unconditionally: `truncate` does not alter the text node, so
101
- // the full title is already in the accessibility tree and a duplicating
102
- // `aria-label` is dead weight.
95
+ // the full title is already in the accessibility tree.
103
96
  "aria-label": e.ariaLabel,
104
- "aria-current": a ? "page" : void 0
105
- };
106
- return b ? /* @__PURE__ */ o(b, { href: e.url, ...C, ...S, children: d(e.iconName, e.title) }) : /* @__PURE__ */ o("a", { href: e.url, ...C, children: d(e.iconName, e.title) });
107
- }, O = (e, a) => (
108
- // A button is not a page, so no `aria-current` even when the indicator is here.
109
- // No `aria-controls` either — the drill panel's id is Radix-generated and
110
- // unreachable from the bar, and a dangling reference is worse than none —
111
- // and no `aria-haspopup`, which would promise arrow-key menu semantics the
112
- // drill-down does not implement.
113
- /* @__PURE__ */ o(
114
- "button",
115
- {
116
- type: "button",
117
- "data-slot": "app-bottom-nav-item",
118
- "data-active": a ? "true" : void 0,
119
- "aria-label": e.ariaLabel,
120
- "aria-expanded": f && L === e.title,
121
- className: i(
122
- m,
123
- a ? v : p
124
- ),
125
- onClick: () => {
126
- g(e.title), h(!0);
127
- },
128
- children: d(e.iconName, e.title)
129
- }
130
- )
131
- );
132
- return r.length === 0 && !n ? (c(
97
+ "aria-current": t ? "page" : void 0
98
+ }, { visual: f, srLabel: H } = k(e), y = N(
99
+ c(e.iconName, f),
100
+ e.title,
101
+ H
102
+ );
103
+ return x ? /* @__PURE__ */ n(x, { href: e.url, ...s, ...z, children: y }) : /* @__PURE__ */ n("a", { href: e.url, ...s, children: y });
104
+ }, q = (e, t) => {
105
+ const { visual: s, srLabel: f } = k(e);
106
+ return (
107
+ // A button is not a page, so no `aria-current` even when the indicator is
108
+ // here. No `aria-controls` — the drill panel's id is Radix-generated and
109
+ // unreachable from the bar — and no `aria-haspopup`, which would promise
110
+ // arrow-key menu semantics the drill-down does not implement.
111
+ /* @__PURE__ */ n(
112
+ "button",
113
+ {
114
+ type: "button",
115
+ "data-slot": "app-bottom-nav-item",
116
+ "data-active": t ? "true" : void 0,
117
+ "aria-label": e.ariaLabel,
118
+ "aria-expanded": I && F === e.title,
119
+ className: r(
120
+ g,
121
+ t ? M : h
122
+ ),
123
+ onClick: () => {
124
+ C(e.title), B(!0);
125
+ },
126
+ children: N(c(e.iconName, s), e.title, f)
127
+ }
128
+ )
129
+ );
130
+ };
131
+ return l.length === 0 && !o ? (i(
133
132
  "AppBottomNav: no items and showMenu={false} — nothing to render, the bar is omitted."
134
- ), null) : /* @__PURE__ */ o(
133
+ ), null) : /* @__PURE__ */ n(
135
134
  "div",
136
135
  {
137
136
  "data-slot": "app-bottom-nav",
138
- className: i(z, _),
139
- ...j,
140
- children: /* @__PURE__ */ y("nav", { "aria-label": B, className: D, children: [
141
- r.map((e, a) => /* @__PURE__ */ o(F.Fragment, { children: e.subItems?.length ? O(e, a === l) : P(e, a === l) }, $(e))),
142
- n && /* @__PURE__ */ o(
137
+ className: r(te, E),
138
+ ...R,
139
+ children: /* @__PURE__ */ d("nav", { "aria-label": j, className: ae, children: [
140
+ l.map((e, t) => /* @__PURE__ */ n(Q.Fragment, { children: e.subItems?.length ? q(e, t === b) : X(e, t === b) }, $(e))),
141
+ o && /* @__PURE__ */ n(
143
142
  "button",
144
143
  {
145
144
  type: "button",
146
145
  "data-slot": "app-bottom-nav-item",
147
- "data-active": x ? "true" : void 0,
148
- "aria-expanded": f,
149
- className: i(
150
- m,
151
- x ? v : p
152
- ),
146
+ "aria-expanded": I,
147
+ className: r(g, h),
153
148
  onClick: () => {
154
- g(null), h(!0);
149
+ C(null), B(!0);
155
150
  },
156
- children: d(M, A)
151
+ children: N(G, m)
157
152
  }
158
153
  )
159
154
  ] })
@@ -161,5 +156,5 @@ function J({
161
156
  );
162
157
  }
163
158
  export {
164
- J as AppBottomNav
159
+ pe as AppBottomNav
165
160
  };