@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.
Files changed (156) hide show
  1. package/dist/{chunk-XV22URNM.cjs → chunk-4NDGD2YH.cjs} +19 -19
  2. package/dist/{chunk-XV22URNM.cjs.map → chunk-4NDGD2YH.cjs.map} +1 -1
  3. package/dist/{chunk-YDNIQLAP.js → chunk-5CY6YKWY.js} +6 -6
  4. package/dist/{chunk-Q66TP2LY.js → chunk-6TJJFQ74.js} +6 -6
  5. package/dist/{chunk-47LHA4QI.cjs → chunk-C5R2OX7A.cjs} +7 -2
  6. package/dist/chunk-C5R2OX7A.cjs.map +1 -0
  7. package/dist/{chunk-NSDDV4F6.cjs → chunk-ETU6VFOL.cjs} +40 -40
  8. package/dist/{chunk-NSDDV4F6.cjs.map → chunk-ETU6VFOL.cjs.map} +1 -1
  9. package/dist/{chunk-WJ2S6I34.cjs → chunk-GDUWGCPP.cjs} +12 -12
  10. package/dist/{chunk-WJ2S6I34.cjs.map → chunk-GDUWGCPP.cjs.map} +1 -1
  11. package/dist/{chunk-44YXQBNM.cjs → chunk-I64ZGPA6.cjs} +3 -3
  12. package/dist/{chunk-44YXQBNM.cjs.map → chunk-I64ZGPA6.cjs.map} +1 -1
  13. package/dist/{chunk-7DUMNMED.cjs → chunk-IDRKW6MS.cjs} +12 -12
  14. package/dist/{chunk-7DUMNMED.cjs.map → chunk-IDRKW6MS.cjs.map} +1 -1
  15. package/dist/{chunk-SGL6URIJ.cjs → chunk-L5CI57RM.cjs} +9 -5
  16. package/dist/chunk-L5CI57RM.cjs.map +1 -0
  17. package/dist/{chunk-Q6B7VAHN.cjs → chunk-LN2X4XAB.cjs} +18 -18
  18. package/dist/{chunk-Q6B7VAHN.cjs.map → chunk-LN2X4XAB.cjs.map} +1 -1
  19. package/dist/{chunk-RAL274JM.js → chunk-P3B65FBX.js} +4 -4
  20. package/dist/{chunk-QWCW4Z75.js → chunk-P5UMIPKR.js} +5 -5
  21. package/dist/{chunk-QL5YB4F7.js → chunk-P7IOCYGD.js} +52 -47
  22. package/dist/chunk-P7IOCYGD.js.map +1 -0
  23. package/dist/{chunk-V2APV4E4.cjs → chunk-PAO45LBQ.cjs} +1113 -916
  24. package/dist/chunk-PAO45LBQ.cjs.map +1 -0
  25. package/dist/{chunk-3AXJESKA.js → chunk-QQAKDJPE.js} +3 -3
  26. package/dist/{chunk-O7JY7LRP.js → chunk-R2IGOYHU.js} +3 -3
  27. package/dist/{chunk-RBZOUGCL.cjs → chunk-S3COVC7H.cjs} +73 -73
  28. package/dist/{chunk-RBZOUGCL.cjs.map → chunk-S3COVC7H.cjs.map} +1 -1
  29. package/dist/{chunk-QUFB5SDZ.js → chunk-SOX7CGFX.js} +303 -106
  30. package/dist/{chunk-QUFB5SDZ.js.map → chunk-SOX7CGFX.js.map} +1 -1
  31. package/dist/{chunk-W42256FC.cjs → chunk-TJCUGL7S.cjs} +33 -33
  32. package/dist/{chunk-W42256FC.cjs.map → chunk-TJCUGL7S.cjs.map} +1 -1
  33. package/dist/{chunk-SSYE7BQC.js → chunk-UM2YFCPE.js} +3 -3
  34. package/dist/{chunk-2CEDCSYD.cjs → chunk-V5ALIJZ6.cjs} +32 -32
  35. package/dist/{chunk-2CEDCSYD.cjs.map → chunk-V5ALIJZ6.cjs.map} +1 -1
  36. package/dist/{chunk-EP7ELZTU.cjs → chunk-VLI6ESJN.cjs} +93 -93
  37. package/dist/{chunk-EP7ELZTU.cjs.map → chunk-VLI6ESJN.cjs.map} +1 -1
  38. package/dist/{chunk-BAMTV6GL.js → chunk-VMDQDD5Y.js} +2 -2
  39. package/dist/{chunk-IUP26OXG.js → chunk-VPQBQJ4S.js} +2 -2
  40. package/dist/{chunk-TTJSLIXA.js → chunk-W6Y66G4K.js} +3 -3
  41. package/dist/{chunk-QKWOBV55.js → chunk-WHJGRRK2.js} +9 -5
  42. package/dist/chunk-WHJGRRK2.js.map +1 -0
  43. package/dist/{chunk-AEZ5BXLJ.js → chunk-XBLRT3MX.js} +4 -4
  44. package/dist/{chunk-FLZTUE2T.cjs → chunk-YSN2AZJR.cjs} +5 -5
  45. package/dist/{chunk-FLZTUE2T.cjs.map → chunk-YSN2AZJR.cjs.map} +1 -1
  46. package/dist/{chunk-6QVV3OLL.cjs → chunk-ZLJCE75C.cjs} +40 -40
  47. package/dist/{chunk-6QVV3OLL.cjs.map → chunk-ZLJCE75C.cjs.map} +1 -1
  48. package/dist/{chunk-UD4IT3GY.js → chunk-ZU27I5NZ.js} +6 -6
  49. package/dist/components/case-studies/index.cjs +10 -10
  50. package/dist/components/case-studies/index.js +4 -4
  51. package/dist/components/chat/index.cjs +4 -4
  52. package/dist/components/chat/index.js +3 -3
  53. package/dist/components/contact/index.cjs +5 -5
  54. package/dist/components/contact/index.js +4 -4
  55. package/dist/components/docs/index.cjs +7 -7
  56. package/dist/components/docs/index.js +6 -6
  57. package/dist/components/embeds/index.cjs +5 -5
  58. package/dist/components/embeds/index.js +4 -4
  59. package/dist/components/faq/index.cjs +6 -6
  60. package/dist/components/faq/index.js +5 -5
  61. package/dist/components/features/filters-dropdown.d.ts.map +1 -1
  62. package/dist/components/features/index.cjs +4 -4
  63. package/dist/components/features/index.js +3 -3
  64. package/dist/components/help-center-pages/index.cjs +23 -23
  65. package/dist/components/help-center-pages/index.js +14 -14
  66. package/dist/components/index.cjs +165 -161
  67. package/dist/components/index.cjs.map +1 -1
  68. package/dist/components/index.js +16 -12
  69. package/dist/components/index.js.map +1 -1
  70. package/dist/components/navigation/app-header.d.ts +48 -0
  71. package/dist/components/navigation/app-header.d.ts.map +1 -1
  72. package/dist/components/navigation/header-mingo-button.d.ts.map +1 -1
  73. package/dist/components/navigation/index.cjs +6 -4
  74. package/dist/components/navigation/index.cjs.map +1 -1
  75. package/dist/components/navigation/index.d.ts +2 -2
  76. package/dist/components/navigation/index.d.ts.map +1 -1
  77. package/dist/components/navigation/index.js +5 -3
  78. package/dist/components/navigation/navigation-sidebar-item.d.ts +14 -0
  79. package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
  80. package/dist/components/navigation/navigation-sidebar-toggle.d.ts.map +1 -1
  81. package/dist/components/navigation/navigation-sidebar.d.ts +18 -0
  82. package/dist/components/navigation/navigation-sidebar.d.ts.map +1 -1
  83. package/dist/components/onboarding-guides/index.cjs +8 -8
  84. package/dist/components/onboarding-guides/index.js +7 -7
  85. package/dist/components/related-content/index.cjs +6 -6
  86. package/dist/components/related-content/index.js +5 -5
  87. package/dist/components/tickets/index.cjs +7 -7
  88. package/dist/components/tickets/index.js +6 -6
  89. package/dist/components/ui/button/button-styles.d.ts +1 -0
  90. package/dist/components/ui/button/button-styles.d.ts.map +1 -1
  91. package/dist/components/ui/button/button.d.ts.map +1 -1
  92. package/dist/components/ui/data-table/data-table-body.d.ts.map +1 -1
  93. package/dist/components/ui/data-table/data-table-column-filter.d.ts.map +1 -1
  94. package/dist/components/ui/data-table/data-table-header.d.ts.map +1 -1
  95. package/dist/components/ui/data-table/data-table-row.d.ts.map +1 -1
  96. package/dist/components/ui/data-table/data-table-skeleton.d.ts +12 -0
  97. package/dist/components/ui/data-table/data-table-skeleton.d.ts.map +1 -1
  98. package/dist/components/ui/data-table/index.d.ts +1 -1
  99. package/dist/components/ui/data-table/index.d.ts.map +1 -1
  100. package/dist/components/ui/data-table/utils.d.ts +18 -0
  101. package/dist/components/ui/data-table/utils.d.ts.map +1 -1
  102. package/dist/components/ui/file-manager/index.cjs +50 -50
  103. package/dist/components/ui/file-manager/index.js +1 -1
  104. package/dist/components/ui/index.cjs +6 -4
  105. package/dist/components/ui/index.cjs.map +1 -1
  106. package/dist/components/ui/index.js +5 -3
  107. package/dist/hooks/index.cjs +4 -2
  108. package/dist/hooks/index.cjs.map +1 -1
  109. package/dist/hooks/index.js +3 -1
  110. package/dist/hooks/ui/index.d.ts +1 -0
  111. package/dist/hooks/ui/index.d.ts.map +1 -1
  112. package/dist/hooks/ui/use-isomorphic-layout-effect.d.ts +17 -0
  113. package/dist/hooks/ui/use-isomorphic-layout-effect.d.ts.map +1 -0
  114. package/dist/index.cjs +10 -4
  115. package/dist/index.cjs.map +1 -1
  116. package/dist/index.js +9 -3
  117. package/dist/types/navigation.d.ts +24 -0
  118. package/dist/types/navigation.d.ts.map +1 -1
  119. package/package.json +1 -1
  120. package/src/components/features/filters-dropdown.tsx +17 -5
  121. package/src/components/navigation/app-header.tsx +178 -0
  122. package/src/components/navigation/header-mingo-button.tsx +5 -0
  123. package/src/components/navigation/index.ts +2 -2
  124. package/src/components/navigation/navigation-sidebar-item.tsx +22 -0
  125. package/src/components/navigation/navigation-sidebar-toggle.tsx +10 -6
  126. package/src/components/navigation/navigation-sidebar.tsx +187 -44
  127. package/src/components/ui/button/button-styles.ts +11 -0
  128. package/src/components/ui/button/button.tsx +6 -3
  129. package/src/components/ui/button/split-button.tsx +2 -2
  130. package/src/components/ui/data-table/data-table-body.tsx +5 -2
  131. package/src/components/ui/data-table/data-table-column-filter.tsx +54 -25
  132. package/src/components/ui/data-table/data-table-header.tsx +134 -45
  133. package/src/components/ui/data-table/data-table-row.tsx +3 -2
  134. package/src/components/ui/data-table/data-table-skeleton.tsx +17 -2
  135. package/src/components/ui/data-table/index.ts +1 -1
  136. package/src/components/ui/data-table/utils.ts +49 -3
  137. package/src/hooks/ui/index.ts +1 -0
  138. package/src/hooks/ui/use-isomorphic-layout-effect.ts +20 -0
  139. package/src/types/navigation.ts +21 -0
  140. package/dist/chunk-47LHA4QI.cjs.map +0 -1
  141. package/dist/chunk-QKWOBV55.js.map +0 -1
  142. package/dist/chunk-QL5YB4F7.js.map +0 -1
  143. package/dist/chunk-SGL6URIJ.cjs.map +0 -1
  144. package/dist/chunk-V2APV4E4.cjs.map +0 -1
  145. /package/dist/{chunk-YDNIQLAP.js.map → chunk-5CY6YKWY.js.map} +0 -0
  146. /package/dist/{chunk-Q66TP2LY.js.map → chunk-6TJJFQ74.js.map} +0 -0
  147. /package/dist/{chunk-RAL274JM.js.map → chunk-P3B65FBX.js.map} +0 -0
  148. /package/dist/{chunk-QWCW4Z75.js.map → chunk-P5UMIPKR.js.map} +0 -0
  149. /package/dist/{chunk-3AXJESKA.js.map → chunk-QQAKDJPE.js.map} +0 -0
  150. /package/dist/{chunk-O7JY7LRP.js.map → chunk-R2IGOYHU.js.map} +0 -0
  151. /package/dist/{chunk-SSYE7BQC.js.map → chunk-UM2YFCPE.js.map} +0 -0
  152. /package/dist/{chunk-BAMTV6GL.js.map → chunk-VMDQDD5Y.js.map} +0 -0
  153. /package/dist/{chunk-IUP26OXG.js.map → chunk-VPQBQJ4S.js.map} +0 -0
  154. /package/dist/{chunk-TTJSLIXA.js.map → chunk-W6Y66G4K.js.map} +0 -0
  155. /package/dist/{chunk-AEZ5BXLJ.js.map → chunk-XBLRT3MX.js.map} +0 -0
  156. /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 { alignJustify, getHideClasses } from './utils'
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
- // On tablet (md, below lg), only filterable columns render. If none exist,
66
- // every HeaderCell returns null and the flex row collapses which would
67
- // break the absolutely-positioned rightSlot. Detect that case and render
68
- // rightSlot in-flow instead.
69
- const hasVisibleHeaderCell = headerGroup.headers.some(header => {
70
- if (header.isPlaceholder) return false
71
- if (isLgUp) return true
72
- const meta = header.column.columnDef.meta
73
- return Boolean(meta?.filter) || meta?.alwaysShowHeader === true
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
- (hasVisibleHeaderCell ? (
96
- <div className="absolute right-[var(--spacing-system-mf)] inset-y-0 flex items-center">
97
- {rightSlot}
98
- </div>
99
- ) : (
100
- // In-flow fallback: with no visible header cell this slot is the
101
- // only thing giving the row height, so it carries the same fixed 48
102
- // rather than padding out to 44.
103
- <div className="ml-auto flex items-center h-12">
104
- {rightSlot}
105
- </div>
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
- function HeaderCell({ header, isLgUp, sort, onSortChange }: HeaderCellProps) {
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
- // Tablet visibility: filterable columns and explicit opt-ins stay rendered.
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
- 'flex items-stretch',
145
- isLgUp && (meta?.width || 'flex-1 min-w-0'),
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
- isLgUp && alignJustify(align),
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 items-center gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)]',
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 items-center gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)] py-0',
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 gap-[var(--spacing-system-sf)] items-center justify-start px-[var(--spacing-system-sf)] py-0',
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 => order.indexOf(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 = order[maxIdx + 1]
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) {
@@ -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
@@ -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