@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
|
@@ -1,18 +1,115 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect,
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { useIsomorphicLayoutEffect } from '../../hooks/ui/use-isomorphic-layout-effect'
|
|
4
5
|
import { useLocalStorage } from '../../hooks/ui/use-local-storage'
|
|
5
6
|
import { useLgUp, useMdUp } from '../../hooks/ui/use-media-query'
|
|
6
7
|
import { NavigationSidebarConfig, NavigationSidebarItem } from '../../types/navigation'
|
|
7
8
|
import { cn } from '../../utils'
|
|
8
9
|
import { NavigationSidebarHeader } from './navigation-sidebar-header'
|
|
9
|
-
import { NavigationSidebarItemButton } from './navigation-sidebar-item'
|
|
10
|
+
import { NavigationSidebarItemButton, NavigationSidebarItemSkeleton } from './navigation-sidebar-item'
|
|
10
11
|
import { NavigationSidebarToggle } from './navigation-sidebar-toggle'
|
|
11
12
|
|
|
12
13
|
const MINIMIZED_WIDTH = 56 // 3.5rem = 56px
|
|
13
14
|
const EXPANDED_WIDTH = 224 // 14rem = 224px
|
|
14
15
|
const STORAGE_KEY = 'of.navigationSidebar.minimized'
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* The desktop width, as a custom property on `:root`. Read by the `lg:` rule in
|
|
19
|
+
* {@link SIDEBAR_GEOMETRY_CLASSES} and written by this component whenever the
|
|
20
|
+
* user toggles — but NEVER during render.
|
|
21
|
+
*
|
|
22
|
+
* It has to be a global variable rather than an inline style because of WHEN the
|
|
23
|
+
* value is knowable. The width follows a preference in `localStorage`, which the
|
|
24
|
+
* server cannot read: any markup derived from it differs between the server's
|
|
25
|
+
* HTML and the client's hydration render, and React tears the tree down and
|
|
26
|
+
* regenerates it. Seeding this property from a tiny script in `<head>` moves the
|
|
27
|
+
* preference OUT of React's rendered output — the markup becomes identical on
|
|
28
|
+
* both sides, while the width is already correct on the very first paint.
|
|
29
|
+
*
|
|
30
|
+
* Consumers are expected to seed it before first paint; unseeded, the fallback
|
|
31
|
+
* in the class below paints the expanded width until this component's effect
|
|
32
|
+
* catches up, which is the flash the seed exists to remove.
|
|
33
|
+
*/
|
|
34
|
+
export const NAVIGATION_SIDEBAR_WIDTH_VAR = '--of-navigation-sidebar-width'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The rail as a query container, so the parts of the sidebar that follow its
|
|
38
|
+
* collapsed state can read that state off the rail's own width — currently the
|
|
39
|
+
* collapse chevron's direction, in `navigation-sidebar-toggle.tsx`.
|
|
40
|
+
*
|
|
41
|
+
* There is no second source of truth for "is it collapsed", and that is the
|
|
42
|
+
* point: the width already has to be correct on the first paint and at every
|
|
43
|
+
* breakpoint, so anything derived from it is too — the seeded desktop
|
|
44
|
+
* preference, the tablet rail and an overlay opened on tablet all come along for
|
|
45
|
+
* free. A parallel copy would have to be kept in step with each of the three
|
|
46
|
+
* separately, and the custom property that used to live here fell out of step on
|
|
47
|
+
* the last one: it could not see the tablet rail, so that case got its own media
|
|
48
|
+
* query, which in turn could not see an open overlay and pointed the chevron
|
|
49
|
+
* backwards under it.
|
|
50
|
+
*
|
|
51
|
+
* Queried as `@[140px]/of-nav-sidebar:` — 140px is the midpoint between the 56px
|
|
52
|
+
* rail and the 224px expanded sidebar, so the width only crosses it mid-toggle.
|
|
53
|
+
* Container queries are min-width, so a follower styles its COLLAPSED look as the
|
|
54
|
+
* default and lets the query take it back. Written out literally at each use
|
|
55
|
+
* rather than interpolated from a constant: Tailwind scans source text, and a
|
|
56
|
+
* class it cannot see spelled out is a class it never generates.
|
|
57
|
+
*/
|
|
58
|
+
const SIDEBAR_CONTAINER = '@container/of-nav-sidebar'
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Where the sidebar sits and how wide it is, per breakpoint — as literal classes,
|
|
62
|
+
* because this cannot be decided in JavaScript in time.
|
|
63
|
+
*
|
|
64
|
+
* `useMdUp`/`useLgUp` answer `undefined` until an effect has run, so `isTablet`
|
|
65
|
+
* is false on the first render — the SERVER's render included. A tablet
|
|
66
|
+
* therefore received the desktop branch in its HTML (`relative`, `width:224px`,
|
|
67
|
+
* an inline style no class could outrank) and only snapped to the 56px rail once
|
|
68
|
+
* hydration and an effect had both completed. That is the wide sidebar that
|
|
69
|
+
* flashed on every load, and no amount of JS could fix it: the server has no
|
|
70
|
+
* viewport to consult, so the answer has to be deferred to the one consumer that
|
|
71
|
+
* always knows — the browser's own media evaluation.
|
|
72
|
+
*
|
|
73
|
+
* The state that a viewport CANNOT determine still lives outside the render: the
|
|
74
|
+
* persisted desktop preference reaches the `lg:` rule through
|
|
75
|
+
* {@link NAVIGATION_SIDEBAR_WIDTH_VAR}. An inline width would be simpler than a
|
|
76
|
+
* custom property, but it would also override the tablet rail on the very first
|
|
77
|
+
* paint, which is the bug being fixed.
|
|
78
|
+
*
|
|
79
|
+
* The tablet WIDTH is deliberately not here — see {@link SIDEBAR_TABLET_WIDTH}.
|
|
80
|
+
*
|
|
81
|
+
* Widths are `w-14`/`w-56` — the class equivalents of `MINIMIZED_WIDTH` and
|
|
82
|
+
* `EXPANDED_WIDTH`, as is the `14rem` fallback below. Keep them in step; a class
|
|
83
|
+
* string cannot interpolate a constant.
|
|
84
|
+
*/
|
|
85
|
+
const SIDEBAR_GEOMETRY_CLASSES = [
|
|
86
|
+
// Tablet: float over the content, anchored to the layout row (`absolute`
|
|
87
|
+
// within AppLayout's `relative` row) — NOT the viewport — so an optional
|
|
88
|
+
// `topBar` above the row is not overlapped. With no topBar the row spans the
|
|
89
|
+
// full viewport, so this is visually identical to a viewport-fixed sidebar.
|
|
90
|
+
'md:absolute md:inset-y-0 md:left-0 md:z-[45]',
|
|
91
|
+
// Desktop: back in the flex flow, width from the persisted preference. The
|
|
92
|
+
// fallback IS the default, so the property needs no global declaration and an
|
|
93
|
+
// unseeded consumer gets the full sidebar rather than a rail with no labels.
|
|
94
|
+
'lg:relative lg:inset-auto lg:z-auto lg:h-full lg:w-[var(--of-navigation-sidebar-width,14rem)]',
|
|
95
|
+
].join(' ')
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The tablet width, as ONE `md:` class rather than a rail plus an override.
|
|
99
|
+
*
|
|
100
|
+
* Tablet is a 56px rail by design and only widens for an overlay the user has
|
|
101
|
+
* opened — state no viewport can imply, so it has to come from JS. Written as
|
|
102
|
+
* `md:w-14` + `max-lg:w-56` the two rules both match between md and lg at equal
|
|
103
|
+
* specificity, and Tailwind emits the `max-lg` block BEFORE the `md` one, so
|
|
104
|
+
* source order hands the tablet straight back to the rail. Picking one class
|
|
105
|
+
* removes the contest instead of trying to win it — and keeps the `!important`
|
|
106
|
+
* that winning it would need out of the tree.
|
|
107
|
+
*
|
|
108
|
+
* `lg:` still governs desktop either way: it is emitted after `md:`, so an
|
|
109
|
+
* overlay left open while the window widens does not leak into the desktop width.
|
|
110
|
+
*/
|
|
111
|
+
const SIDEBAR_TABLET_WIDTH = { rail: 'md:w-14', overlay: 'md:w-56' } as const
|
|
112
|
+
|
|
16
113
|
/** A click the browser will resolve itself — new tab, new window, middle button. */
|
|
17
114
|
const isModifiedClick = (event?: React.MouseEvent): boolean =>
|
|
18
115
|
!!event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)
|
|
@@ -47,7 +144,22 @@ export function NavigationSidebar({ config, disabled = false }: NavigationSideba
|
|
|
47
144
|
if (isTablet) setTabletMinimized(true)
|
|
48
145
|
}, [isTablet])
|
|
49
146
|
|
|
50
|
-
|
|
147
|
+
// `useLocalStorage` reads the store SYNCHRONOUSLY in its initializer, so on the
|
|
148
|
+
// hydration render it already knows a preference the server could not. Anything
|
|
149
|
+
// derived from it — labels, `aria-hidden`, the chevron, the placeholder rows —
|
|
150
|
+
// then differs from the server's HTML, and React throws out the whole tree and
|
|
151
|
+
// regenerates it. So until hydration is done, this renders the server's answer
|
|
152
|
+
// and nothing else.
|
|
153
|
+
//
|
|
154
|
+
// That costs nothing visually. The width is already correct at that point (it
|
|
155
|
+
// comes from the seeded CSS var, not from here), and at the minimized width
|
|
156
|
+
// every label is a `flex-1` in a rail with no room to give — zero-wide whatever
|
|
157
|
+
// this says. What flips on the next commit is opacity and margin on boxes that
|
|
158
|
+
// were never visible.
|
|
159
|
+
const [hydrated, setHydrated] = useState(false)
|
|
160
|
+
useEffect(() => setHydrated(true), [])
|
|
161
|
+
|
|
162
|
+
const minimized = hydrated ? (isTablet ? tabletMinimized : desktopMinimized) : (config.minimized ?? false)
|
|
51
163
|
|
|
52
164
|
// Enable transitions only after the correct width is painted
|
|
53
165
|
const [transitionsEnabled, setTransitionsEnabled] = useState(false)
|
|
@@ -147,11 +259,35 @@ export function NavigationSidebar({ config, disabled = false }: NavigationSideba
|
|
|
147
259
|
secondaryItems: config.items.filter(item => item.section === 'secondary'),
|
|
148
260
|
}), [config.items])
|
|
149
261
|
|
|
262
|
+
// Placeholder rows while the host cannot know its entries yet — see
|
|
263
|
+
// `NavigationSidebarConfig.loading`. Only the ROWS are stood in for: the header,
|
|
264
|
+
// the collapse toggle, the widths and the tablet overlay all behave exactly as
|
|
265
|
+
// when loaded, so this is the loaded rail with unknown contents rather than a
|
|
266
|
+
// separate skeleton sidebar to keep in sync.
|
|
267
|
+
const loadingRows = useMemo(() => {
|
|
268
|
+
if (!config.loading) return null
|
|
269
|
+
const primary = config.loadingRows?.primary ?? 7
|
|
270
|
+
const secondary = config.loadingRows?.secondary ?? 2
|
|
271
|
+
return {
|
|
272
|
+
primary: Array.from({ length: Math.max(0, primary) }, (_, i) => `primary-${i}`),
|
|
273
|
+
secondary: Array.from({ length: Math.max(0, secondary) }, (_, i) => `secondary-${i}`),
|
|
274
|
+
}
|
|
275
|
+
}, [config.loading, config.loadingRows?.primary, config.loadingRows?.secondary])
|
|
276
|
+
|
|
150
277
|
const sidebarWidth = useMemo(
|
|
151
278
|
() => (minimized ? `${MINIMIZED_WIDTH}px` : `${EXPANDED_WIDTH}px`),
|
|
152
279
|
[minimized],
|
|
153
280
|
)
|
|
154
281
|
|
|
282
|
+
// Published to CSS from an EFFECT, never from render. Before hydration the
|
|
283
|
+
// width belongs to whatever seeded `NAVIGATION_SIDEBAR_WIDTH_VAR` in `<head>`
|
|
284
|
+
// — put it in the markup instead and it becomes part of what React hydrates,
|
|
285
|
+
// which is precisely what cannot agree across the server boundary. Afterwards
|
|
286
|
+
// this is what a toggle moves.
|
|
287
|
+
useIsomorphicLayoutEffect(() => {
|
|
288
|
+
document.documentElement.style.setProperty(NAVIGATION_SIDEBAR_WIDTH_VAR, sidebarWidth)
|
|
289
|
+
}, [sidebarWidth])
|
|
290
|
+
|
|
155
291
|
// There used to be an `isHydrated` gate here — `isMdUp !== undefined && ...`
|
|
156
292
|
// — meant to hold the sidebar's contents back until the media queries
|
|
157
293
|
// resolved. It could never be false: the `?? false` above had already
|
|
@@ -159,7 +295,7 @@ export function NavigationSidebar({ config, disabled = false }: NavigationSideba
|
|
|
159
295
|
// because repairing it would be the regression: the contents are server-
|
|
160
296
|
// rendered today, and gating them behind a client-only media query would
|
|
161
297
|
// trade a first paint of the real navigation for an empty rail.
|
|
162
|
-
|
|
298
|
+
useIsomorphicLayoutEffect(() => {
|
|
163
299
|
if (!transitionsEnabled) {
|
|
164
300
|
const id = requestAnimationFrame(() => {
|
|
165
301
|
setTransitionsEnabled(true)
|
|
@@ -183,61 +319,68 @@ export function NavigationSidebar({ config, disabled = false }: NavigationSideba
|
|
|
183
319
|
/>
|
|
184
320
|
|
|
185
321
|
{/* Flex-flow placeholder — reserves the collapsed 56px slot on tablet so
|
|
186
|
-
the main content keeps its position while the sidebar floats above it
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
/>
|
|
193
|
-
)}
|
|
322
|
+
the main content keeps its position while the sidebar floats above it.
|
|
323
|
+
Rendered unconditionally and scoped by classes for the same reason the
|
|
324
|
+
geometry is: gated on `isTablet`, it was missing from the server HTML
|
|
325
|
+
and from the first client paint — exactly when the sidebar it
|
|
326
|
+
compensates for has already gone `absolute`. */}
|
|
327
|
+
<div className="h-full hidden md:block lg:hidden w-14 flex-shrink-0" aria-hidden="true" />
|
|
194
328
|
|
|
195
329
|
<aside
|
|
196
330
|
className={cn(
|
|
331
|
+
SIDEBAR_CONTAINER,
|
|
197
332
|
"flex-col hidden md:flex flex-shrink-0",
|
|
198
333
|
"bg-ods-card border-r border-ods-border",
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
isTablet ? "absolute inset-y-0 left-0 z-[45]" : "relative h-full",
|
|
334
|
+
SIDEBAR_GEOMETRY_CLASSES,
|
|
335
|
+
// The one width a viewport cannot imply: an overlay the user opened on
|
|
336
|
+
// tablet. Only ever true after a click, so the first paint always gets
|
|
337
|
+
// the rail.
|
|
338
|
+
isOverlayOpen ? SIDEBAR_TABLET_WIDTH.overlay : SIDEBAR_TABLET_WIDTH.rail,
|
|
205
339
|
transitionsEnabled && "transition-[width] duration-300",
|
|
206
340
|
config.className,
|
|
207
341
|
)}
|
|
208
|
-
style={{ width: sidebarWidth }}
|
|
209
342
|
aria-label="Main navigation sidebar"
|
|
210
343
|
>
|
|
211
344
|
<NavigationSidebarHeader minimized={minimized} />
|
|
212
345
|
|
|
213
346
|
<div className="flex-1 flex flex-col justify-between py-4 overflow-y-auto">
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
347
|
+
{/* `aria-busy` on the nav, not the rows: the region is what is loading,
|
|
348
|
+
and each placeholder row is already `aria-hidden`. */}
|
|
349
|
+
<nav className="flex flex-col" aria-label="Primary navigation" aria-busy={!!loadingRows}>
|
|
350
|
+
{loadingRows
|
|
351
|
+
? loadingRows.primary.map(key => (
|
|
352
|
+
<NavigationSidebarItemSkeleton key={key} showLabel={showLabel} />
|
|
353
|
+
))
|
|
354
|
+
: primaryItems.map(item => (
|
|
355
|
+
<NavigationSidebarItemButton
|
|
356
|
+
key={item.id}
|
|
357
|
+
item={item}
|
|
358
|
+
isActive={item.id === committedActiveId}
|
|
359
|
+
isPending={item.id === pendingItemId}
|
|
360
|
+
showLabel={showLabel}
|
|
361
|
+
disabled={disabled}
|
|
362
|
+
onClick={handleItemClick}
|
|
363
|
+
/>
|
|
364
|
+
))}
|
|
226
365
|
</nav>
|
|
227
366
|
|
|
228
|
-
{secondaryItems.length > 0 && (
|
|
229
|
-
<nav className="flex flex-col" aria-label="Secondary navigation">
|
|
230
|
-
{
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
367
|
+
{(loadingRows ? loadingRows.secondary.length > 0 : secondaryItems.length > 0) && (
|
|
368
|
+
<nav className="flex flex-col" aria-label="Secondary navigation" aria-busy={!!loadingRows}>
|
|
369
|
+
{loadingRows
|
|
370
|
+
? loadingRows.secondary.map(key => (
|
|
371
|
+
<NavigationSidebarItemSkeleton key={key} showLabel={showLabel} />
|
|
372
|
+
))
|
|
373
|
+
: secondaryItems.map(item => (
|
|
374
|
+
<NavigationSidebarItemButton
|
|
375
|
+
key={item.id}
|
|
376
|
+
item={item}
|
|
377
|
+
isActive={item.id === committedActiveId}
|
|
378
|
+
isPending={item.id === pendingItemId}
|
|
379
|
+
showLabel={showLabel}
|
|
380
|
+
disabled={disabled}
|
|
381
|
+
onClick={handleItemClick}
|
|
382
|
+
/>
|
|
383
|
+
))}
|
|
241
384
|
</nav>
|
|
242
385
|
)}
|
|
243
386
|
</div>
|
|
@@ -32,6 +32,17 @@ export const buttonSurfaceClasses = {
|
|
|
32
32
|
export const outlineBorderClasses =
|
|
33
33
|
"border border-ods-border hover:border-ods-border-hover active:border-ods-border-active disabled:border-ods-border aria-disabled:border-ods-border"
|
|
34
34
|
|
|
35
|
+
// Glyph scale for the split-layout button family: 16px below md, 24px from md.
|
|
36
|
+
//
|
|
37
|
+
// Shared because the same page-header action renders through EITHER of the two
|
|
38
|
+
// split layouts depending on its shape — `SplitButton` for two click targets, and
|
|
39
|
+
// `Button` + `splitIcon` for one with a decorative trailing glyph — and the two sit
|
|
40
|
+
// side by side in the same row. They used to disagree: `SplitButton` carried this
|
|
41
|
+
// responsive scale while the `Button` split slots re-declared a flat 20px, so a
|
|
42
|
+
// "submenu" action's icons rendered 4px smaller than its neighbour's on desktop.
|
|
43
|
+
// `Button`'s own flat `[&_svg]:h-5` stays as it is for ordinary (non-split) buttons.
|
|
44
|
+
export const splitGlyphSizeClasses = "[&_svg]:h-4 [&_svg]:w-4 md:[&_svg]:h-6 md:[&_svg]:w-6"
|
|
45
|
+
|
|
35
46
|
// Color of the vertical seam between the main and icon halves (split layouts).
|
|
36
47
|
export const splitDividerColorClasses = {
|
|
37
48
|
accent: "border-ods-accent-active",
|
|
@@ -6,7 +6,7 @@ import Link from "../../../embed-shims/next-link"
|
|
|
6
6
|
import React from "react"
|
|
7
7
|
|
|
8
8
|
import { cn } from "../../../utils/cn"
|
|
9
|
-
import { buttonSurfaceClasses, outlineBorderClasses, splitDividerColorClasses } from "./button-styles"
|
|
9
|
+
import { buttonSurfaceClasses, outlineBorderClasses, splitDividerColorClasses, splitGlyphSizeClasses } from "./button-styles"
|
|
10
10
|
|
|
11
11
|
// Default layout: centered single content area, padding/gap on the button itself.
|
|
12
12
|
const buttonVariants = cva(
|
|
@@ -104,14 +104,17 @@ const splitShellVariants = cva(
|
|
|
104
104
|
)
|
|
105
105
|
|
|
106
106
|
const splitSlotVariants = cva(
|
|
107
|
-
|
|
107
|
+
// Glyph size lives per-size, not in the base: cva concatenates base + variant
|
|
108
|
+
// without tailwind-merge, so a base `[&_svg]:h-5` and a variant `[&_svg]:h-4`
|
|
109
|
+
// would both ship and the winner would come down to stylesheet order.
|
|
110
|
+
["inline-flex items-center justify-center", "[&_svg]:shrink-0"],
|
|
108
111
|
{
|
|
109
112
|
variants: {
|
|
110
113
|
slot: {
|
|
111
114
|
main: "gap-[var(--spacing-system-xsf)]",
|
|
112
115
|
icon: "border-l",
|
|
113
116
|
},
|
|
114
|
-
size: { default:
|
|
117
|
+
size: { default: splitGlyphSizeClasses, small: "[&_svg]:h-4 [&_svg]:w-4" },
|
|
115
118
|
variant: { accent: "", outline: "", transparent: "", destructive: "", warning: "" },
|
|
116
119
|
},
|
|
117
120
|
compoundVariants: [
|
|
@@ -5,7 +5,7 @@ import Link from "../../../embed-shims/next-link"
|
|
|
5
5
|
import React from "react"
|
|
6
6
|
|
|
7
7
|
import { cn } from "../../../utils/cn"
|
|
8
|
-
import { buttonSurfaceClasses, splitDividerColorClasses } from "./button-styles"
|
|
8
|
+
import { buttonSurfaceClasses, splitDividerColorClasses, splitGlyphSizeClasses } from "./button-styles"
|
|
9
9
|
|
|
10
10
|
// Two independent interactive halves: each a `<button>` or `<a>`. The seam is
|
|
11
11
|
// a 1px border on the left edge of the icon half, colored per variant. For a
|
|
@@ -29,7 +29,7 @@ const splitHalfVariants = cva(splitHalfBase, {
|
|
|
29
29
|
warning: buttonSurfaceClasses.warning,
|
|
30
30
|
},
|
|
31
31
|
size: {
|
|
32
|
-
default:
|
|
32
|
+
default: `h-10 md:h-12 px-[var(--spacing-system-m)] py-[var(--spacing-system-sf)] text-h3 ${splitGlyphSizeClasses}`,
|
|
33
33
|
small: "h-6 md:h-8 px-[var(--spacing-system-xs)] text-h5 [&_svg]:h-3 [&_svg]:w-3 md:[&_svg]:h-4 md:[&_svg]:w-4",
|
|
34
34
|
},
|
|
35
35
|
side: { main: "", icon: "" },
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
DataTableSkeleton,
|
|
11
11
|
ROW_HEIGHT_DESKTOP,
|
|
12
12
|
ROW_HEIGHT_MOBILE,
|
|
13
|
+
ROW_SHELL_CLASSES,
|
|
13
14
|
} from './data-table-skeleton'
|
|
14
15
|
|
|
15
16
|
export interface DataTableBodyProps<T = any> {
|
|
@@ -133,13 +134,15 @@ export function DataTableBody<T = any>({
|
|
|
133
134
|
>
|
|
134
135
|
<div
|
|
135
136
|
className={cn(
|
|
136
|
-
'hidden md:flex
|
|
137
|
+
'hidden md:flex py-0',
|
|
138
|
+
ROW_SHELL_CLASSES,
|
|
137
139
|
ROW_HEIGHT_DESKTOP,
|
|
138
140
|
)}
|
|
139
141
|
/>
|
|
140
142
|
<div
|
|
141
143
|
className={cn(
|
|
142
|
-
'flex md:hidden
|
|
144
|
+
'flex md:hidden justify-start py-0',
|
|
145
|
+
ROW_SHELL_CLASSES,
|
|
143
146
|
ROW_HEIGHT_MOBILE,
|
|
144
147
|
)}
|
|
145
148
|
/>
|
|
@@ -62,34 +62,63 @@ export function DataTableColumnFilter({
|
|
|
62
62
|
column.setFilterValue(undefined)
|
|
63
63
|
}, [column])
|
|
64
64
|
|
|
65
|
+
// A filterable column whose choices have not arrived yet. Declaring the filter
|
|
66
|
+
// with no options is how a caller says "this column filters, ask me later" —
|
|
67
|
+
// the alternative, dropping `meta.filter` until the query resolves, takes the
|
|
68
|
+
// header cell with it: below `lg` only filterable cells are shown at all, so
|
|
69
|
+
// every header on a tablet vanished on reload and came back when the options
|
|
70
|
+
// landed. It also moved the labels, since the funnel is only drawn for a
|
|
71
|
+
// filterable column.
|
|
72
|
+
const isPending = options.length === 0
|
|
73
|
+
|
|
74
|
+
const trigger = (
|
|
75
|
+
<div
|
|
76
|
+
className={cn(
|
|
77
|
+
// Fixed 48px per design, same as the plain header cell next to it —
|
|
78
|
+
// NOT padding around the label. The two were drifting: 12px+20px+12px
|
|
79
|
+
// came out 44px here while the sibling was pinned to 48, so in a
|
|
80
|
+
// header mixing filterable and plain columns the labels sat on
|
|
81
|
+
// different centre lines and the filter's hit area was 4px short of
|
|
82
|
+
// the row it belongs to.
|
|
83
|
+
'group flex w-full items-center gap-[var(--spacing-system-xsf)] h-12 rounded-sm transition-colors duration-200 select-none',
|
|
84
|
+
alignJustify(align),
|
|
85
|
+
isPending ? 'cursor-default' : 'cursor-pointer',
|
|
86
|
+
)}
|
|
87
|
+
aria-label={`Filter by ${label}`}
|
|
88
|
+
aria-disabled={isPending || undefined}
|
|
89
|
+
>
|
|
90
|
+
<span
|
|
91
|
+
className={cn(
|
|
92
|
+
'text-h5 text-ods-text-secondary whitespace-nowrap transition-colors duration-200',
|
|
93
|
+
!isPending && 'group-hover:text-ods-text-primary',
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
{label}
|
|
97
|
+
</span>
|
|
98
|
+
{/* Rendered while pending too, and that is the point: it holds its own
|
|
99
|
+
width, so the label does not shift sideways the moment the options
|
|
100
|
+
resolve. */}
|
|
101
|
+
<Filter02Icon
|
|
102
|
+
className={cn(
|
|
103
|
+
'w-4 h-4 transition-colors',
|
|
104
|
+
activeCount > 0 && 'text-ods-accent',
|
|
105
|
+
activeCount === 0 && 'text-ods-text-secondary',
|
|
106
|
+
activeCount === 0 && !isPending && 'group-hover:text-ods-text-primary',
|
|
107
|
+
)}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// Same box, no dropdown: opening onto an empty list would be a worse answer
|
|
113
|
+
// than not opening at all.
|
|
114
|
+
if (isPending) {
|
|
115
|
+
return <div className="block w-full">{trigger}</div>
|
|
116
|
+
}
|
|
117
|
+
|
|
65
118
|
return (
|
|
66
119
|
<FiltersDropdown
|
|
67
120
|
className="!block w-full"
|
|
68
|
-
triggerElement={
|
|
69
|
-
<div
|
|
70
|
-
className={cn(
|
|
71
|
-
// Fixed 48px per design, same as the plain header cell next to it —
|
|
72
|
-
// NOT padding around the label. The two were drifting: 12px+20px+12px
|
|
73
|
-
// came out 44px here while the sibling was pinned to 48, so in a
|
|
74
|
-
// header mixing filterable and plain columns the labels sat on
|
|
75
|
-
// different centre lines and the filter's hit area was 4px short of
|
|
76
|
-
// the row it belongs to.
|
|
77
|
-
'group flex w-full items-center gap-[var(--spacing-system-xsf)] h-12 rounded-sm cursor-pointer transition-colors duration-200 select-none',
|
|
78
|
-
alignJustify(align),
|
|
79
|
-
)}
|
|
80
|
-
aria-label={`Filter by ${label}`}
|
|
81
|
-
>
|
|
82
|
-
<span className="text-h5 text-ods-text-secondary whitespace-nowrap transition-colors duration-200 group-hover:text-ods-text-primary">
|
|
83
|
-
{label}
|
|
84
|
-
</span>
|
|
85
|
-
<Filter02Icon
|
|
86
|
-
className={cn(
|
|
87
|
-
'w-4 h-4 transition-colors',
|
|
88
|
-
activeCount > 0 ? 'text-ods-accent' : 'text-ods-text-secondary group-hover:text-ods-text-primary',
|
|
89
|
-
)}
|
|
90
|
-
/>
|
|
91
|
-
</div>
|
|
92
|
-
}
|
|
121
|
+
triggerElement={trigger}
|
|
93
122
|
sections={sections}
|
|
94
123
|
currentFilters={currentFilters}
|
|
95
124
|
onApply={handleApply}
|