@flamingo-stack/openframe-frontend-core 0.0.499 → 0.0.500
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/{chunk-XV22URNM.cjs → chunk-4NDGD2YH.cjs} +19 -19
- package/dist/{chunk-XV22URNM.cjs.map → chunk-4NDGD2YH.cjs.map} +1 -1
- package/dist/{chunk-YDNIQLAP.js → chunk-5CY6YKWY.js} +6 -6
- package/dist/{chunk-Q66TP2LY.js → chunk-6TJJFQ74.js} +6 -6
- package/dist/{chunk-47LHA4QI.cjs → chunk-C5R2OX7A.cjs} +7 -2
- package/dist/chunk-C5R2OX7A.cjs.map +1 -0
- package/dist/{chunk-NSDDV4F6.cjs → chunk-ETU6VFOL.cjs} +40 -40
- package/dist/{chunk-NSDDV4F6.cjs.map → chunk-ETU6VFOL.cjs.map} +1 -1
- package/dist/{chunk-WJ2S6I34.cjs → chunk-GDUWGCPP.cjs} +12 -12
- package/dist/{chunk-WJ2S6I34.cjs.map → chunk-GDUWGCPP.cjs.map} +1 -1
- package/dist/{chunk-44YXQBNM.cjs → chunk-I64ZGPA6.cjs} +3 -3
- package/dist/{chunk-44YXQBNM.cjs.map → chunk-I64ZGPA6.cjs.map} +1 -1
- package/dist/{chunk-7DUMNMED.cjs → chunk-IDRKW6MS.cjs} +12 -12
- package/dist/{chunk-7DUMNMED.cjs.map → chunk-IDRKW6MS.cjs.map} +1 -1
- package/dist/{chunk-SGL6URIJ.cjs → chunk-L5CI57RM.cjs} +9 -5
- package/dist/chunk-L5CI57RM.cjs.map +1 -0
- package/dist/{chunk-Q6B7VAHN.cjs → chunk-LN2X4XAB.cjs} +18 -18
- package/dist/{chunk-Q6B7VAHN.cjs.map → chunk-LN2X4XAB.cjs.map} +1 -1
- package/dist/{chunk-RAL274JM.js → chunk-P3B65FBX.js} +4 -4
- package/dist/{chunk-QWCW4Z75.js → chunk-P5UMIPKR.js} +5 -5
- package/dist/{chunk-QL5YB4F7.js → chunk-P7IOCYGD.js} +52 -47
- package/dist/chunk-P7IOCYGD.js.map +1 -0
- package/dist/{chunk-V2APV4E4.cjs → chunk-PAO45LBQ.cjs} +1113 -916
- package/dist/chunk-PAO45LBQ.cjs.map +1 -0
- package/dist/{chunk-3AXJESKA.js → chunk-QQAKDJPE.js} +3 -3
- package/dist/{chunk-O7JY7LRP.js → chunk-R2IGOYHU.js} +3 -3
- package/dist/{chunk-RBZOUGCL.cjs → chunk-S3COVC7H.cjs} +73 -73
- package/dist/{chunk-RBZOUGCL.cjs.map → chunk-S3COVC7H.cjs.map} +1 -1
- package/dist/{chunk-QUFB5SDZ.js → chunk-SOX7CGFX.js} +303 -106
- package/dist/{chunk-QUFB5SDZ.js.map → chunk-SOX7CGFX.js.map} +1 -1
- package/dist/{chunk-W42256FC.cjs → chunk-TJCUGL7S.cjs} +33 -33
- package/dist/{chunk-W42256FC.cjs.map → chunk-TJCUGL7S.cjs.map} +1 -1
- package/dist/{chunk-SSYE7BQC.js → chunk-UM2YFCPE.js} +3 -3
- package/dist/{chunk-2CEDCSYD.cjs → chunk-V5ALIJZ6.cjs} +32 -32
- package/dist/{chunk-2CEDCSYD.cjs.map → chunk-V5ALIJZ6.cjs.map} +1 -1
- package/dist/{chunk-EP7ELZTU.cjs → chunk-VLI6ESJN.cjs} +93 -93
- package/dist/{chunk-EP7ELZTU.cjs.map → chunk-VLI6ESJN.cjs.map} +1 -1
- package/dist/{chunk-BAMTV6GL.js → chunk-VMDQDD5Y.js} +2 -2
- package/dist/{chunk-IUP26OXG.js → chunk-VPQBQJ4S.js} +2 -2
- package/dist/{chunk-TTJSLIXA.js → chunk-W6Y66G4K.js} +3 -3
- package/dist/{chunk-QKWOBV55.js → chunk-WHJGRRK2.js} +9 -5
- package/dist/chunk-WHJGRRK2.js.map +1 -0
- package/dist/{chunk-AEZ5BXLJ.js → chunk-XBLRT3MX.js} +4 -4
- package/dist/{chunk-FLZTUE2T.cjs → chunk-YSN2AZJR.cjs} +5 -5
- package/dist/{chunk-FLZTUE2T.cjs.map → chunk-YSN2AZJR.cjs.map} +1 -1
- package/dist/{chunk-6QVV3OLL.cjs → chunk-ZLJCE75C.cjs} +40 -40
- package/dist/{chunk-6QVV3OLL.cjs.map → chunk-ZLJCE75C.cjs.map} +1 -1
- package/dist/{chunk-UD4IT3GY.js → chunk-ZU27I5NZ.js} +6 -6
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/index.js +3 -3
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/filters-dropdown.d.ts.map +1 -1
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +165 -161
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +16 -12
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/app-header.d.ts +48 -0
- package/dist/components/navigation/app-header.d.ts.map +1 -1
- package/dist/components/navigation/header-mingo-button.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +6 -4
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.d.ts +2 -2
- package/dist/components/navigation/index.d.ts.map +1 -1
- package/dist/components/navigation/index.js +5 -3
- package/dist/components/navigation/navigation-sidebar-item.d.ts +14 -0
- package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
- package/dist/components/navigation/navigation-sidebar-toggle.d.ts.map +1 -1
- package/dist/components/navigation/navigation-sidebar.d.ts +18 -0
- package/dist/components/navigation/navigation-sidebar.d.ts.map +1 -1
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/button/button-styles.d.ts +1 -0
- package/dist/components/ui/button/button-styles.d.ts.map +1 -1
- package/dist/components/ui/button/button.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-body.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-column-filter.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-header.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-row.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-skeleton.d.ts +12 -0
- package/dist/components/ui/data-table/data-table-skeleton.d.ts.map +1 -1
- package/dist/components/ui/data-table/index.d.ts +1 -1
- package/dist/components/ui/data-table/index.d.ts.map +1 -1
- package/dist/components/ui/data-table/utils.d.ts +18 -0
- package/dist/components/ui/data-table/utils.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +50 -50
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +6 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +5 -3
- package/dist/hooks/index.cjs +4 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +3 -1
- package/dist/hooks/ui/index.d.ts +1 -0
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/hooks/ui/use-isomorphic-layout-effect.d.ts +17 -0
- package/dist/hooks/ui/use-isomorphic-layout-effect.d.ts.map +1 -0
- package/dist/index.cjs +10 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -3
- package/dist/types/navigation.d.ts +24 -0
- package/dist/types/navigation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/features/filters-dropdown.tsx +17 -5
- package/src/components/navigation/app-header.tsx +178 -0
- package/src/components/navigation/header-mingo-button.tsx +5 -0
- package/src/components/navigation/index.ts +2 -2
- package/src/components/navigation/navigation-sidebar-item.tsx +22 -0
- package/src/components/navigation/navigation-sidebar-toggle.tsx +10 -6
- package/src/components/navigation/navigation-sidebar.tsx +187 -44
- package/src/components/ui/button/button-styles.ts +11 -0
- package/src/components/ui/button/button.tsx +6 -3
- package/src/components/ui/button/split-button.tsx +2 -2
- package/src/components/ui/data-table/data-table-body.tsx +5 -2
- package/src/components/ui/data-table/data-table-column-filter.tsx +54 -25
- package/src/components/ui/data-table/data-table-header.tsx +134 -45
- package/src/components/ui/data-table/data-table-row.tsx +3 -2
- package/src/components/ui/data-table/data-table-skeleton.tsx +17 -2
- package/src/components/ui/data-table/index.ts +1 -1
- package/src/components/ui/data-table/utils.ts +49 -3
- package/src/hooks/ui/index.ts +1 -0
- package/src/hooks/ui/use-isomorphic-layout-effect.ts +20 -0
- package/src/types/navigation.ts +21 -0
- package/dist/chunk-47LHA4QI.cjs.map +0 -1
- package/dist/chunk-QKWOBV55.js.map +0 -1
- package/dist/chunk-QL5YB4F7.js.map +0 -1
- package/dist/chunk-SGL6URIJ.cjs.map +0 -1
- package/dist/chunk-V2APV4E4.cjs.map +0 -1
- /package/dist/{chunk-YDNIQLAP.js.map → chunk-5CY6YKWY.js.map} +0 -0
- /package/dist/{chunk-Q66TP2LY.js.map → chunk-6TJJFQ74.js.map} +0 -0
- /package/dist/{chunk-RAL274JM.js.map → chunk-P3B65FBX.js.map} +0 -0
- /package/dist/{chunk-QWCW4Z75.js.map → chunk-P5UMIPKR.js.map} +0 -0
- /package/dist/{chunk-3AXJESKA.js.map → chunk-QQAKDJPE.js.map} +0 -0
- /package/dist/{chunk-O7JY7LRP.js.map → chunk-R2IGOYHU.js.map} +0 -0
- /package/dist/{chunk-SSYE7BQC.js.map → chunk-UM2YFCPE.js.map} +0 -0
- /package/dist/{chunk-BAMTV6GL.js.map → chunk-VMDQDD5Y.js.map} +0 -0
- /package/dist/{chunk-IUP26OXG.js.map → chunk-VPQBQJ4S.js.map} +0 -0
- /package/dist/{chunk-TTJSLIXA.js.map → chunk-W6Y66G4K.js.map} +0 -0
- /package/dist/{chunk-AEZ5BXLJ.js.map → chunk-XBLRT3MX.js.map} +0 -0
- /package/dist/{chunk-UD4IT3GY.js.map → chunk-ZU27I5NZ.js.map} +0 -0
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from 'react'
|
|
4
4
|
import { flexRender, type Header } from '@tanstack/react-table'
|
|
5
|
-
import { useLgUp } from '../../../hooks/ui/use-media-query'
|
|
6
5
|
import { cn } from '../../../utils/cn'
|
|
7
6
|
import {
|
|
8
7
|
Arrow01DownIcon,
|
|
@@ -11,7 +10,8 @@ import {
|
|
|
11
10
|
} from '../../icons-v2-generated'
|
|
12
11
|
import { DataTableColumnFilter } from './data-table-column-filter'
|
|
13
12
|
import { useDataTableContext } from './data-table'
|
|
14
|
-
import {
|
|
13
|
+
import type { TailwindBreakpoint } from './types'
|
|
14
|
+
import { alignJustify, BREAKPOINT_ORDER, getHideAtVisibility } from './utils'
|
|
15
15
|
|
|
16
16
|
/** Single-column sort descriptor consumed by the header. */
|
|
17
17
|
export interface DataTableSortState {
|
|
@@ -56,22 +56,21 @@ export function DataTableHeader({
|
|
|
56
56
|
onSortChange,
|
|
57
57
|
}: DataTableHeaderProps) {
|
|
58
58
|
const table = useDataTableContext()
|
|
59
|
-
const isLgUp = useLgUp() ?? false
|
|
60
59
|
|
|
61
60
|
// Flat header group (nested headers can be added later if needed).
|
|
62
61
|
const headerGroup = table.getHeaderGroups()[0]
|
|
63
62
|
if (!headerGroup) return null
|
|
64
63
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// rightSlot in
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
64
|
+
// Below lg only filterable columns (and explicit opt-ins) are visible — the same
|
|
65
|
+
// `keepsCellOnTablet` predicate the cells themselves use, so the two cannot drift.
|
|
66
|
+
// If a table has none, every cell is hidden there, the flex row has no height, and
|
|
67
|
+
// an absolutely-positioned rightSlot has nothing to sit in — so that slot goes
|
|
68
|
+
// in-flow instead. Derived from column META, never from the viewport: the same
|
|
69
|
+
// answer on the server, on the first client paint and after, which is the whole
|
|
70
|
+
// point of this file no longer reading a media query.
|
|
71
|
+
const hasTabletVisibleCell = headerGroup.headers.some(
|
|
72
|
+
header => !header.isPlaceholder && keepsCellOnTablet(header.column.columnDef.meta),
|
|
73
|
+
)
|
|
75
74
|
|
|
76
75
|
return (
|
|
77
76
|
<div
|
|
@@ -83,27 +82,26 @@ export function DataTableHeader({
|
|
|
83
82
|
>
|
|
84
83
|
<div className="flex items-stretch gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)] relative">
|
|
85
84
|
{headerGroup.headers.map(header => (
|
|
86
|
-
<HeaderCell
|
|
87
|
-
key={header.id}
|
|
88
|
-
header={header}
|
|
89
|
-
isLgUp={isLgUp}
|
|
90
|
-
sort={sort}
|
|
91
|
-
onSortChange={onSortChange}
|
|
92
|
-
/>
|
|
85
|
+
<HeaderCell key={header.id} header={header} sort={sort} onSortChange={onSortChange} />
|
|
93
86
|
))}
|
|
94
|
-
{rightSlot &&
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
87
|
+
{rightSlot && (
|
|
88
|
+
<div
|
|
89
|
+
className={cn(
|
|
90
|
+
'flex items-center',
|
|
91
|
+
hasTabletVisibleCell
|
|
92
|
+
? 'absolute right-[var(--spacing-system-mf)] inset-y-0'
|
|
93
|
+
: // No cell is visible below lg, so there this slot is the only thing
|
|
94
|
+
// giving the row height and carries the same fixed 48 rather than
|
|
95
|
+
// padding out to 44. From lg the cells are back and it returns to
|
|
96
|
+
// the absolute placement. Safe as a `max-lg:` override: nothing
|
|
97
|
+
// else in this string sets margin or height, so it competes only
|
|
98
|
+
// with unprefixed defaults, never with a `md:`/`lg:` class.
|
|
99
|
+
'max-lg:ml-auto max-lg:h-12 lg:absolute lg:right-[var(--spacing-system-mf)] lg:inset-y-0',
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
{rightSlot}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
107
105
|
</div>
|
|
108
106
|
</div>
|
|
109
107
|
)
|
|
@@ -115,19 +113,92 @@ type AnyHeader = Header<unknown, unknown>
|
|
|
115
113
|
|
|
116
114
|
interface HeaderCellProps {
|
|
117
115
|
header: AnyHeader
|
|
118
|
-
isLgUp: boolean
|
|
119
116
|
sort: DataTableSortState | null
|
|
120
117
|
onSortChange?: (columnId: string) => void
|
|
121
118
|
}
|
|
122
119
|
|
|
123
|
-
|
|
120
|
+
type ColumnMeta = AnyHeader['column']['columnDef']['meta']
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Whether a column's header stays visible below `lg`, where the row is narrow
|
|
124
|
+
* enough that only the controls a user can act on earn their space: the filter
|
|
125
|
+
* dropdowns, plus anything explicitly opted in via `meta.alwaysShowHeader`.
|
|
126
|
+
*/
|
|
127
|
+
function keepsCellOnTablet(meta: ColumnMeta): boolean {
|
|
128
|
+
return Boolean(meta?.filter) || meta?.alwaysShowHeader === true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Literal class maps — Tailwind's scanner needs the full class strings, which a
|
|
132
|
+
// `${bp}:flex` template would not give it.
|
|
133
|
+
const SHOW_FROM: Record<TailwindBreakpoint, string> = {
|
|
134
|
+
md: 'md:flex',
|
|
135
|
+
lg: 'lg:flex',
|
|
136
|
+
xl: 'xl:flex',
|
|
137
|
+
'2xl': '2xl:flex',
|
|
138
|
+
}
|
|
139
|
+
const HIDE_FROM: Record<TailwindBreakpoint, string> = {
|
|
140
|
+
md: 'md:hidden',
|
|
141
|
+
lg: 'lg:hidden',
|
|
142
|
+
xl: 'xl:hidden',
|
|
143
|
+
'2xl': '2xl:hidden',
|
|
144
|
+
}
|
|
145
|
+
/** Index of `lg` in the `[base, md, lg, xl, 2xl]` visibility array. */
|
|
146
|
+
const LG_STEP = BREAKPOINT_ORDER.indexOf('lg') + 1
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Visibility classes for one header cell, as a plain min-width ladder.
|
|
150
|
+
*
|
|
151
|
+
* Two rules stack here: the column's own `hideAt`, and "below `lg` only the
|
|
152
|
+
* cells a user can act on". Layering them as classes does not work — a
|
|
153
|
+
* `max-lg:` override cannot cancel a `md:hidden` from `hideAt`, because Tailwind
|
|
154
|
+
* emits the `max-lg` block BEFORE the `md` one and equal-specificity rules are
|
|
155
|
+
* decided by source order. So both rules are resolved on booleans first, and only
|
|
156
|
+
* the result is turned into classes: at most one utility per breakpoint, each a
|
|
157
|
+
* `min-width` variant, no ordering left to chance.
|
|
158
|
+
*/
|
|
159
|
+
function getCellVisibilityClasses(
|
|
160
|
+
keepOnTablet: boolean,
|
|
161
|
+
hideAt: TailwindBreakpoint | TailwindBreakpoint[] | undefined,
|
|
162
|
+
): string {
|
|
163
|
+
const fromHideAt = getHideAtVisibility(hideAt)
|
|
164
|
+
const visible = fromHideAt.map((shown, step) =>
|
|
165
|
+
keepOnTablet
|
|
166
|
+
? // Reachable below lg whatever `hideAt` says; from lg it governs again.
|
|
167
|
+
step < LG_STEP || shown
|
|
168
|
+
: // Otherwise lg is a floor `hideAt` can raise but never lower.
|
|
169
|
+
shown && step >= LG_STEP,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const classes = [visible[0] ? 'flex' : 'hidden']
|
|
173
|
+
for (let step = 1; step < visible.length; step++) {
|
|
174
|
+
if (visible[step] === visible[step - 1]) continue
|
|
175
|
+
const breakpoint = BREAKPOINT_ORDER[step - 1]
|
|
176
|
+
classes.push(visible[step] ? SHOW_FROM[breakpoint] : HIDE_FROM[breakpoint])
|
|
177
|
+
}
|
|
178
|
+
return classes.join(' ')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* One header cell.
|
|
183
|
+
*
|
|
184
|
+
* Everything responsive here is a CSS class, deliberately. This used to branch on
|
|
185
|
+
* `useLgUp() ?? false`, which answers `undefined` until an effect has run — so the
|
|
186
|
+
* FIRST render (server included) took the "not lg" path on every viewport: each
|
|
187
|
+
* non-filterable cell returned `null`, and the survivors rendered with no width and
|
|
188
|
+
* no alignment. One frame later the real values arrived, so the header visibly
|
|
189
|
+
* filled in and the columns snapped into place — while `DataTableRow` and
|
|
190
|
+
* `DataTableSkeleton`, which have always used plain `hideAt` classes, were correct
|
|
191
|
+
* from the start. Header and body therefore disagreed about which columns exist and
|
|
192
|
+
* how wide they are for exactly one frame, which is the flicker-and-jump on every
|
|
193
|
+
* table's load.
|
|
194
|
+
*/
|
|
195
|
+
function HeaderCell({ header, sort, onSortChange }: HeaderCellProps) {
|
|
124
196
|
if (header.isPlaceholder) return null
|
|
125
197
|
|
|
126
198
|
const column = header.column
|
|
127
199
|
const meta = column.columnDef.meta
|
|
128
200
|
const hasFilter = Boolean(meta?.filter)
|
|
129
|
-
|
|
130
|
-
const keepOnTablet = hasFilter || meta?.alwaysShowHeader === true
|
|
201
|
+
const keepOnTablet = keepsCellOnTablet(meta)
|
|
131
202
|
const align = meta?.align ?? 'left'
|
|
132
203
|
// Sort is opt-in via `meta.sortable`. Direction is fully consumer-driven via
|
|
133
204
|
// the `sort` prop; we do not consult TanStack's sort APIs here.
|
|
@@ -135,17 +206,33 @@ function HeaderCell({ header, isLgUp, sort, onSortChange }: HeaderCellProps) {
|
|
|
135
206
|
const sortDir: false | 'asc' | 'desc' =
|
|
136
207
|
sort?.id === column.id ? (sort.desc ? 'desc' : 'asc') : false
|
|
137
208
|
|
|
138
|
-
// Mobile (md, below lg): hide non-filter columns so only filters are accessible.
|
|
139
|
-
if (!isLgUp && !keepOnTablet) return null
|
|
140
|
-
|
|
141
209
|
return (
|
|
142
210
|
<div
|
|
143
211
|
className={cn(
|
|
144
|
-
'
|
|
145
|
-
|
|
212
|
+
'items-stretch',
|
|
213
|
+
// Cells are hidden rather than dropped from the tree, so which columns
|
|
214
|
+
// exist never depends on when an effect ran.
|
|
215
|
+
getCellVisibilityClasses(keepOnTablet, meta?.hideAt),
|
|
216
|
+
// Width applies from lg only. Stated as "always, neutralized below lg"
|
|
217
|
+
// because `meta.width` is an opaque consumer string that cannot be
|
|
218
|
+
// prefixed. Only `keepOnTablet` cells need the override — the rest are
|
|
219
|
+
// hidden below lg anyway.
|
|
220
|
+
//
|
|
221
|
+
// `[&&]` doubles the class in the selector (`.cls.cls`, specificity 0-2-0),
|
|
222
|
+
// and it is load-bearing: consumers routinely write a RESPONSIVE width
|
|
223
|
+
// (`w-[80px] md:w-1/5` is the common shape), and between md and lg that
|
|
224
|
+
// `md:` rule and this `max-lg:` one both match. At equal specificity source
|
|
225
|
+
// order decides, and Tailwind emits the `max-lg` block BEFORE the `md` one
|
|
226
|
+
// — the same ordering documented for visibility above — so the tablet would
|
|
227
|
+
// keep a width the design drops there, leaving the cells spread across the
|
|
228
|
+
// row instead of packed beside their filters. An opaque consumer string
|
|
229
|
+
// cannot be outranked by ordering, so this wins on the cascade's other axis.
|
|
230
|
+
// Specificity rather than `!important`: it stays a normal declaration, so a
|
|
231
|
+
// consumer that genuinely needs a tablet width can still take it back with a
|
|
232
|
+
// more specific rule — `!important` could only be answered with another one.
|
|
233
|
+
meta?.width || 'flex-1 min-w-0',
|
|
234
|
+
keepOnTablet && 'max-lg:[&&]:w-auto max-lg:[&&]:flex-none max-lg:[&&]:basis-auto',
|
|
146
235
|
meta?.headerClassName,
|
|
147
|
-
// Don't apply hide classes if column is filterable on tablet (keep filter accessible)
|
|
148
|
-
!(keepOnTablet && !isLgUp) && getHideClasses(meta?.hideAt),
|
|
149
236
|
)}
|
|
150
237
|
>
|
|
151
238
|
{hasFilter ? (
|
|
@@ -162,7 +249,9 @@ function HeaderCell({ header, isLgUp, sort, onSortChange }: HeaderCellProps) {
|
|
|
162
249
|
// Fixed 48px header height per design (20px label centered inside)
|
|
163
250
|
// instead of the padding-driven 44px.
|
|
164
251
|
'flex w-full items-center gap-[var(--spacing-system-xsf)] h-12 rounded-sm select-none transition-colors duration-200',
|
|
165
|
-
|
|
252
|
+
// Same "always, neutralized below lg" shape as the width above.
|
|
253
|
+
alignJustify(align),
|
|
254
|
+
keepOnTablet && 'max-lg:justify-start',
|
|
166
255
|
canSort && 'group cursor-pointer',
|
|
167
256
|
)}
|
|
168
257
|
onClick={canSort ? () => onSortChange?.(column.id) : undefined}
|
|
@@ -4,7 +4,7 @@ import Link from '../../../embed-shims/next-link'
|
|
|
4
4
|
import React, { memo, useCallback, useRef, type ReactNode } from 'react'
|
|
5
5
|
import { flexRender, type Row } from '@tanstack/react-table'
|
|
6
6
|
import { cn } from '../../../utils/cn'
|
|
7
|
-
import { ROW_HEIGHT_DESKTOP } from './data-table-skeleton'
|
|
7
|
+
import { ROW_HEIGHT_DESKTOP, ROW_SHELL_CLASSES } from './data-table-skeleton'
|
|
8
8
|
import { alignJustify, getHideClasses } from './utils'
|
|
9
9
|
|
|
10
10
|
export interface DataTableRowProps<T> {
|
|
@@ -106,7 +106,8 @@ function DataTableRowImpl<T>({
|
|
|
106
106
|
const cells = (
|
|
107
107
|
<div
|
|
108
108
|
className={cn(
|
|
109
|
-
'flex
|
|
109
|
+
'flex',
|
|
110
|
+
ROW_SHELL_CLASSES,
|
|
110
111
|
compact
|
|
111
112
|
? 'py-[var(--spacing-system-xsf)]'
|
|
112
113
|
: autoHeight
|
|
@@ -12,6 +12,19 @@ import { getHideClasses } from './utils'
|
|
|
12
12
|
export const ROW_HEIGHT_DESKTOP = 'h-[66px] md:h-[78px]'
|
|
13
13
|
export const ROW_HEIGHT_MOBILE = 'h-[66px]'
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The row shell's inset and column gap.
|
|
17
|
+
*
|
|
18
|
+
* Shared by the real row (`data-table-row`), the skeleton rows below and the
|
|
19
|
+
* pad rows in `data-table-body`, because a difference between them is a visible
|
|
20
|
+
* shift on load — and there WAS one: all three placeholders used
|
|
21
|
+
* `--spacing-system-sf` (12px) in their mobile variant while the real row uses
|
|
22
|
+
* `--spacing-system-mf` (16px) at every width, so a loading table sat 4px closer
|
|
23
|
+
* to the card edge than the loaded one and its content nudged over when data
|
|
24
|
+
* arrived. Kept as a constant rather than three copies so that cannot drift again.
|
|
25
|
+
*/
|
|
26
|
+
export const ROW_SHELL_CLASSES = 'items-center gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)]'
|
|
27
|
+
|
|
15
28
|
export interface DataTableSkeletonProps {
|
|
16
29
|
rows?: number
|
|
17
30
|
className?: string
|
|
@@ -39,7 +52,8 @@ export function DataTableSkeleton({
|
|
|
39
52
|
>
|
|
40
53
|
<div
|
|
41
54
|
className={cn(
|
|
42
|
-
'hidden md:flex
|
|
55
|
+
'hidden md:flex py-0',
|
|
56
|
+
ROW_SHELL_CLASSES,
|
|
43
57
|
ROW_HEIGHT_DESKTOP,
|
|
44
58
|
rowClassName,
|
|
45
59
|
)}
|
|
@@ -70,7 +84,8 @@ export function DataTableSkeleton({
|
|
|
70
84
|
</div>
|
|
71
85
|
<div
|
|
72
86
|
className={cn(
|
|
73
|
-
'flex md:hidden
|
|
87
|
+
'flex md:hidden justify-start py-0',
|
|
88
|
+
ROW_SHELL_CLASSES,
|
|
74
89
|
ROW_HEIGHT_MOBILE,
|
|
75
90
|
rowClassName,
|
|
76
91
|
)}
|
|
@@ -50,7 +50,7 @@ export {
|
|
|
50
50
|
|
|
51
51
|
export { useDataTableContext } from './data-table'
|
|
52
52
|
export { useDataTable } from './use-data-table'
|
|
53
|
-
export { ROW_HEIGHT_DESKTOP, ROW_HEIGHT_MOBILE } from './data-table-skeleton'
|
|
53
|
+
export { ROW_HEIGHT_DESKTOP, ROW_HEIGHT_MOBILE, ROW_SHELL_CLASSES } from './data-table-skeleton'
|
|
54
54
|
export { alignJustify, getHideClasses, multiSelectFilterFn } from './utils'
|
|
55
55
|
|
|
56
56
|
export type { DataTableFilterOption, TailwindBreakpoint } from './types'
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { FilterFn } from '@tanstack/react-table'
|
|
2
2
|
import type { TailwindBreakpoint } from './types'
|
|
3
3
|
|
|
4
|
+
/** Ascending breakpoint scale `hideAt` is expressed in. */
|
|
5
|
+
export const BREAKPOINT_ORDER: TailwindBreakpoint[] = ['md', 'lg', 'xl', '2xl']
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* Generates Tailwind hide classes based on breakpoint configuration.
|
|
6
9
|
*
|
|
@@ -14,18 +17,61 @@ export function getHideClasses(
|
|
|
14
17
|
if (!hideAt) return ''
|
|
15
18
|
|
|
16
19
|
const breakpoints = Array.isArray(hideAt) ? hideAt : [hideAt]
|
|
17
|
-
const order: TailwindBreakpoint[] = ['md', 'lg', 'xl', '2xl']
|
|
18
20
|
|
|
19
21
|
if (breakpoints.length === 1) {
|
|
20
22
|
return `hidden ${breakpoints[0]}:flex`
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
const maxIdx = Math.max(...breakpoints.map(bp =>
|
|
25
|
+
const maxIdx = Math.max(...breakpoints.map(bp => BREAKPOINT_ORDER.indexOf(bp)))
|
|
24
26
|
const hideClasses = breakpoints.map(bp => `${bp}:hidden`).join(' ')
|
|
25
|
-
const showBreakpoint =
|
|
27
|
+
const showBreakpoint = BREAKPOINT_ORDER[maxIdx + 1]
|
|
26
28
|
return showBreakpoint ? `${hideClasses} ${showBreakpoint}:flex` : hideClasses
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* The visibility `getHideClasses(hideAt)` actually produces, evaluated per
|
|
33
|
+
* breakpoint: `[base, md, lg, xl, 2xl]`.
|
|
34
|
+
*
|
|
35
|
+
* The header needs to *know* this rather than emit it, because its cells carry a
|
|
36
|
+
* second rule on top (only filterable columns show below `lg`) and the two cannot
|
|
37
|
+
* be layered as classes — Tailwind orders `max-*` media queries BEFORE `min-*`
|
|
38
|
+
* ones, so a `max-lg:` override loses to any `md:`/`lg:` class it is meant to
|
|
39
|
+
* cancel. Combining the two rules here, on plain booleans, sidesteps that
|
|
40
|
+
* entirely: the header emits one unambiguous min-width ladder.
|
|
41
|
+
*
|
|
42
|
+
* Must stay in step with `getHideClasses` — the body rows and the skeleton apply
|
|
43
|
+
* those classes directly, and a disagreement is a header that shows a different
|
|
44
|
+
* set of columns than the rows beneath it.
|
|
45
|
+
*/
|
|
46
|
+
export function getHideAtVisibility(
|
|
47
|
+
hideAt?: TailwindBreakpoint | TailwindBreakpoint[],
|
|
48
|
+
): boolean[] {
|
|
49
|
+
const steps = BREAKPOINT_ORDER.length + 1
|
|
50
|
+
if (!hideAt) return Array.from({ length: steps }, () => true)
|
|
51
|
+
|
|
52
|
+
const breakpoints = Array.isArray(hideAt) ? hideAt : [hideAt]
|
|
53
|
+
|
|
54
|
+
// Single breakpoint → `hidden <bp>:flex`: hidden below it, visible from it.
|
|
55
|
+
if (breakpoints.length === 1) {
|
|
56
|
+
const showFrom = BREAKPOINT_ORDER.indexOf(breakpoints[0]) + 1
|
|
57
|
+
return Array.from({ length: steps }, (_, step) => step >= showFrom)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Array → one `<bp>:hidden` per entry plus a single `<next>:flex`, and no bare
|
|
61
|
+
// `hidden`, so the base step stays visible. Walk the scale upwards: later
|
|
62
|
+
// classes win, and the show breakpoint always sits past every hide one.
|
|
63
|
+
const maxIdx = Math.max(...breakpoints.map(bp => BREAKPOINT_ORDER.indexOf(bp)))
|
|
64
|
+
const showBreakpoint = BREAKPOINT_ORDER[maxIdx + 1]
|
|
65
|
+
const visible = [true]
|
|
66
|
+
for (const bp of BREAKPOINT_ORDER) {
|
|
67
|
+
let state = visible[visible.length - 1]
|
|
68
|
+
if (breakpoints.includes(bp)) state = false
|
|
69
|
+
if (bp === showBreakpoint) state = true
|
|
70
|
+
visible.push(state)
|
|
71
|
+
}
|
|
72
|
+
return visible
|
|
73
|
+
}
|
|
74
|
+
|
|
29
75
|
/** Maps `meta.align` to flex justify classes. */
|
|
30
76
|
export function alignJustify(align?: 'left' | 'center' | 'right'): string {
|
|
31
77
|
switch (align) {
|
package/src/hooks/ui/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './use-header-height'
|
|
|
7
7
|
export * from './use-horizontal-scrollbar'
|
|
8
8
|
export * from './use-image-edge-color'
|
|
9
9
|
export * from './use-is-truncated'
|
|
10
|
+
export * from './use-isomorphic-layout-effect'
|
|
10
11
|
export * from './use-local-storage'
|
|
11
12
|
export * from './use-marquee-engine'
|
|
12
13
|
export * from './use-media-query'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useLayoutEffect } from 'react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `useLayoutEffect` in the browser, `useEffect` on the server.
|
|
7
|
+
*
|
|
8
|
+
* `useLayoutEffect` never runs during server rendering — React cannot measure or
|
|
9
|
+
* mutate a layout that has not been laid out — so calling it in a component that
|
|
10
|
+
* SSRs logs a warning on every render pass. Client components still render on the
|
|
11
|
+
* server in Next.js, so `'use client'` alone does not avoid it.
|
|
12
|
+
*
|
|
13
|
+
* Use this wherever the effect writes something the FIRST paint depends on (a CSS
|
|
14
|
+
* custom property, a scroll position, a measured size). A plain `useEffect` there
|
|
15
|
+
* runs after paint, which shows one frame of the pre-effect value; a plain
|
|
16
|
+
* `useLayoutEffect` gets the timing right and pays for it with the warning. This
|
|
17
|
+
* gets both: paint-blocking where there is a paint, a no-op where there is not.
|
|
18
|
+
*/
|
|
19
|
+
export const useIsomorphicLayoutEffect =
|
|
20
|
+
typeof window !== 'undefined' ? useLayoutEffect : useEffect
|
package/src/types/navigation.ts
CHANGED
|
@@ -92,6 +92,27 @@ export interface SlidingSidebarConfig {
|
|
|
92
92
|
*/
|
|
93
93
|
export interface NavigationSidebarConfig {
|
|
94
94
|
items: NavigationSidebarItem[]
|
|
95
|
+
/**
|
|
96
|
+
* Draw placeholder rows instead of `items`.
|
|
97
|
+
*
|
|
98
|
+
* For hosts whose navigation is not knowable at first paint — entries gated by
|
|
99
|
+
* server-loaded feature flags, permissions, or tenant config. Such a host has no
|
|
100
|
+
* good option without this: rendering the items it has yet means a nav that grows
|
|
101
|
+
* and shifts as answers arrive, and guessing the gated ones wrong shows entries
|
|
102
|
+
* that don't belong to the user (a flag that HIDES a legacy entry when enabled
|
|
103
|
+
* makes "not answered yet" indistinguishable from "off").
|
|
104
|
+
*
|
|
105
|
+
* The rows reuse the real entry's geometry and the sidebar's own minimized state,
|
|
106
|
+
* so the placeholder is correct in both the expanded and the minimized rail with
|
|
107
|
+
* nothing to pass in — and the handoff to the real nav moves nothing.
|
|
108
|
+
*/
|
|
109
|
+
loading?: boolean
|
|
110
|
+
/**
|
|
111
|
+
* How many placeholder rows `loading` draws, per section. Defaults to 7 primary
|
|
112
|
+
* and 2 secondary — the shape of a typical console nav. Pass the counts the host
|
|
113
|
+
* expects so the placeholder and the loaded nav are the same height.
|
|
114
|
+
*/
|
|
115
|
+
loadingRows?: { primary?: number; secondary?: number }
|
|
95
116
|
minimized?: boolean
|
|
96
117
|
onNavigate?: (path: string) => void
|
|
97
118
|
onToggleMinimized?: () => void
|