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.
- package/dist/CHANGELOG.md +282 -0
- package/dist/_virtual/index7.js +5 -2
- package/dist/_virtual/index8.js +2 -5
- package/dist/agent-docs/adoption-report.json +164 -83
- package/dist/agent-docs/components/AppBottomNav.md +180 -49
- package/dist/agent-docs/components/AppCard.md +25 -21
- package/dist/agent-docs/components/AppDataGrid.md +2602 -0
- package/dist/agent-docs/components/AppEditor.md +18 -13
- package/dist/agent-docs/components/AppSelect.md +50 -0
- package/dist/agent-docs/components/AppSidebar.md +504 -11
- package/dist/agent-docs/components/DataTable.md +34 -3
- package/dist/agent-docs/components/DatePicker.md +7 -7
- package/dist/agent-docs/components/FileUploader.md +12 -12
- package/dist/agent-docs/components/GridFilterPanel.md +112 -0
- package/dist/agent-docs/components/Sidebar.md +72 -1
- package/dist/agent-docs/components/SidebarMenuBadge.md +87 -0
- package/dist/agent-docs/components-list.md +47 -2
- package/dist/agent-docs/manifest.json +310 -36
- package/dist/components/design-tokens.js +138 -66
- package/dist/components/nav-tokens.js +41 -0
- package/dist/components/ui/app-bottom-nav.js +125 -130
- package/dist/components/ui/app-data-grid/app-data-grid.js +874 -0
- package/dist/components/ui/app-data-grid/engine/use-grid-column-resize.js +110 -0
- package/dist/components/ui/app-data-grid/engine/use-grid-engine.js +598 -0
- package/dist/components/ui/app-data-grid/engine/use-grid-infinite-loader.js +50 -0
- package/dist/components/ui/app-data-grid/engine/use-grid-virtualizer.js +90 -0
- package/dist/components/ui/app-data-grid/grid-aggregate.js +5 -0
- package/dist/components/ui/app-data-grid/grid-column-registry.js +28 -0
- package/dist/components/ui/app-data-grid/grid-columns.js +204 -0
- package/dist/components/ui/app-data-grid/grid-filter-compile.js +33 -0
- package/dist/components/ui/app-data-grid/grid-filter-operators.js +139 -0
- package/dist/components/ui/app-data-grid/grid-grouping.js +104 -0
- package/dist/components/ui/app-data-grid/grid-header-matrix.js +112 -0
- package/dist/components/ui/app-data-grid/grid-i18n.js +207 -0
- package/dist/components/ui/app-data-grid/grid-mount-count.js +8 -0
- package/dist/components/ui/app-data-grid/grid-reducer.js +200 -0
- package/dist/components/ui/app-data-grid/grid-result-scope.js +35 -0
- package/dist/components/ui/app-data-grid/grid-tokens.js +169 -0
- package/dist/components/ui/app-data-grid/grid-view-preset.js +97 -0
- package/dist/components/ui/app-data-grid/parts/grid-actions-row.js +86 -0
- package/dist/components/ui/app-data-grid/parts/grid-cell-text.js +67 -0
- package/dist/components/ui/app-data-grid/parts/grid-column-registry-panel.js +320 -0
- package/dist/components/ui/app-data-grid/parts/grid-density-control.js +88 -0
- package/dist/components/ui/app-data-grid/parts/grid-filter-chips.js +224 -0
- package/dist/components/ui/app-data-grid/parts/grid-filter-editor.js +197 -0
- package/dist/components/ui/app-data-grid/parts/grid-filter-panel.js +386 -0
- package/dist/components/ui/app-data-grid/parts/grid-filter-row.js +273 -0
- package/dist/components/ui/app-data-grid/parts/grid-fullscreen-toggle.js +29 -0
- package/dist/components/ui/app-data-grid/parts/grid-group-row.js +135 -0
- package/dist/components/ui/app-data-grid/parts/grid-header.js +338 -0
- package/dist/components/ui/app-data-grid/parts/grid-pagination-control.js +218 -0
- package/dist/components/ui/app-data-grid/parts/grid-row.js +181 -0
- package/dist/components/ui/app-data-grid/parts/grid-scroll-area.js +186 -0
- package/dist/components/ui/app-data-grid/parts/grid-selection-bar.js +195 -0
- package/dist/components/ui/app-data-grid/parts/grid-status-surface.js +66 -0
- package/dist/components/ui/app-data-grid/parts/grid-table.js +482 -0
- package/dist/components/ui/app-data-grid/parts/grid-toolbar-slots.js +192 -0
- package/dist/components/ui/app-data-grid/use-grid-filters.js +173 -0
- package/dist/components/ui/app-select.js +332 -222
- package/dist/components/ui/app-sidebar-mobile.js +119 -104
- package/dist/components/ui/app-sidebar.js +516 -313
- package/dist/components/ui/button.js +2 -1
- package/dist/components/ui/gantt/components/Chart/Chart.js +1 -1
- package/dist/components/ui/nav-badge.js +66 -0
- package/dist/components/ui/sidebar.js +272 -207
- package/dist/components/ui/tables/data-cross-table/data-cross-table.js +6 -6
- package/dist/components/ui/tables/data-table/data-table-constants.js +36 -16
- package/dist/components/ui/tables/data-table/data-table-i18n.js +135 -1
- package/dist/css-for-template.css +27 -0
- package/dist/index.d.ts +5872 -144
- package/dist/index.js +473 -406
- package/dist/node_modules/eventemitter3/index2.js +1 -1
- package/dist/styles.css +2 -1
- package/dist/styles.v3.css +1 -1
- package/package.json +25 -7
- /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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 {
|
|
3
|
-
import * as
|
|
4
|
-
import { designTokens as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
13
|
-
// leave 29px for the label. `min-h-`, never `h-`: a cell must be
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
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
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
//
|
|
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
|
-
),
|
|
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
|
-
),
|
|
60
|
-
/* @__PURE__ */
|
|
61
|
-
|
|
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
|
|
64
|
-
items:
|
|
65
|
-
showMenu:
|
|
66
|
-
menuLabel:
|
|
67
|
-
ariaLabel:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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:
|
|
75
|
-
if (
|
|
76
|
-
const e =
|
|
77
|
-
|
|
78
|
-
`AppBottomNav: ${
|
|
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
|
|
82
|
-
for (const e of
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
`AppBottomNav: duplicate item "${
|
|
86
|
-
),
|
|
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
|
|
89
|
-
|
|
90
|
-
`AppBottomNav: more than one active item — "${
|
|
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
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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":
|
|
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
|
|
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":
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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__ */
|
|
133
|
+
), null) : /* @__PURE__ */ n(
|
|
135
134
|
"div",
|
|
136
135
|
{
|
|
137
136
|
"data-slot": "app-bottom-nav",
|
|
138
|
-
className:
|
|
139
|
-
...
|
|
140
|
-
children: /* @__PURE__ */
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
"
|
|
148
|
-
|
|
149
|
-
className: i(
|
|
150
|
-
m,
|
|
151
|
-
x ? v : p
|
|
152
|
-
),
|
|
146
|
+
"aria-expanded": I,
|
|
147
|
+
className: r(g, h),
|
|
153
148
|
onClick: () => {
|
|
154
|
-
|
|
149
|
+
C(null), B(!0);
|
|
155
150
|
},
|
|
156
|
-
children:
|
|
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
|
-
|
|
159
|
+
pe as AppBottomNav
|
|
165
160
|
};
|