@adamosuiteservices/ui 2.19.3 → 2.20.0

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.
@@ -0,0 +1,248 @@
1
+ # Sticky section
2
+
3
+ ## Description
4
+
5
+ A **layout utility component** that uses the `IntersectionObserver` API to detect when a section has scrolled past the top of the visible viewport area. It exposes `isSticky` and `topOffset` to children via a React context and a render-prop pattern, allowing any descendant to adapt its appearance once the section "sticks".
6
+
7
+ ## Features
8
+
9
+ - ✅ Automatic sticky detection via `IntersectionObserver` (no scroll listeners on the sticky element itself)
10
+ - ✅ Render-prop pattern: `children(isSticky, topOffset)`
11
+ - ✅ `useStickySection()` hook for deep descendants
12
+ - ✅ Configurable top offset via `topOffset` (fixed value) or `offsetSelector` (measured from a DOM element)
13
+ - ✅ Controlled mode: bypass internal detection by providing the `isSticky` prop
14
+ - ✅ `onIsStickyChange` callback for side effects without owning the state
15
+ - ✅ Defaults to measuring the library's `[data-slot='sidebar-top-bar']` so it works out of the box with `Sidebar`
16
+
17
+ ## Import
18
+
19
+ ```typescript
20
+ import {
21
+ StickySection,
22
+ useStickySection,
23
+ type StickySectionProps,
24
+ } from "@adamosuiteservices/ui/sticky-section";
25
+ ```
26
+
27
+ ## Basic usage
28
+
29
+ ### Render-prop pattern
30
+
31
+ ```tsx
32
+ import { StickySection } from "@adamosuiteservices/ui/sticky-section";
33
+
34
+ function PageHeader() {
35
+ return (
36
+ <StickySection>
37
+ {(isSticky, topOffset) => (
38
+ <header
39
+ style={{ top: isSticky ? topOffset : undefined }}
40
+ className={
41
+ isSticky ? "fixed left-0 right-0 shadow-md z-50" : "relative"
42
+ }
43
+ >
44
+ <h1>Page title</h1>
45
+ </header>
46
+ )}
47
+ </StickySection>
48
+ );
49
+ }
50
+ ```
51
+
52
+ ### Hook pattern (deep consumers)
53
+
54
+ ```tsx
55
+ import {
56
+ StickySection,
57
+ useStickySection,
58
+ } from "@adamosuiteservices/ui/sticky-section";
59
+
60
+ function Toolbar() {
61
+ const { isSticky, topOffset } = useStickySection();
62
+
63
+ return (
64
+ <div
65
+ style={{ top: isSticky ? topOffset : undefined }}
66
+ className={isSticky ? "fixed ..." : ""}
67
+ >
68
+ Actions
69
+ </div>
70
+ );
71
+ }
72
+
73
+ function Section() {
74
+ return (
75
+ <StickySection>
76
+ <Toolbar />
77
+ {/* other children */}
78
+ </StickySection>
79
+ );
80
+ }
81
+ ```
82
+
83
+ ## Props
84
+
85
+ | Prop | Type | Default | Description |
86
+ | ------------------ | -------------------------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------- |
87
+ | `topOffset` | `number` | — | Fixed top offset in pixels. Skips `offsetSelector` measurement when provided. |
88
+ | `offsetSelector` | `string` | `"[data-slot='sidebar-top-bar']"` | CSS selector of the element whose height is used as the top offset. Ignored when `topOffset` is set. |
89
+ | `isSticky` | `boolean` | — | Controlled sticky state. When provided, internal `IntersectionObserver` detection is bypassed. |
90
+ | `onIsStickyChange` | `(isSticky: boolean, topOffset: number) => void` | — | Called whenever the sticky state changes. |
91
+ | `children` | `ReactNode \| ((isSticky: boolean, topOffset: number) => ReactNode)` | required | ReactNode or a render-prop function. |
92
+
93
+ ## `useStickySection()` hook
94
+
95
+ Returns the value of the nearest `StickySection` context.
96
+
97
+ ```typescript
98
+ const { isSticky, topOffset } = useStickySection();
99
+ ```
100
+
101
+ | Value | Type | Description |
102
+ | ----------- | --------- | ------------------------------------------------------- |
103
+ | `isSticky` | `boolean` | Whether the section is currently stuck to the top. |
104
+ | `topOffset` | `number` | The computed top offset in pixels used by the observer. |
105
+
106
+ Throws if called outside a `StickySection`.
107
+
108
+ ## Usage patterns
109
+
110
+ ### With the Sidebar component (default behaviour)
111
+
112
+ No configuration needed. `StickySection` automatically measures the height of `[data-slot='sidebar-top-bar']` and applies it as the offset.
113
+
114
+ ```tsx
115
+ import { Sidebar, SidebarTopBar } from "@adamosuiteservices/ui/sidebar";
116
+ import { StickySection } from "@adamosuiteservices/ui/sticky-section";
117
+
118
+ function Page() {
119
+ return (
120
+ <Sidebar>
121
+ <SidebarTopBar>…</SidebarTopBar>
122
+ <main>
123
+ <StickySection>
124
+ {(isSticky) => (
125
+ <div className={isSticky ? "sticky top-16 shadow" : ""}>
126
+ Page actions
127
+ </div>
128
+ )}
129
+ </StickySection>
130
+ </main>
131
+ </Sidebar>
132
+ );
133
+ }
134
+ ```
135
+
136
+ ### With a custom top bar
137
+
138
+ ```tsx
139
+ <div data-slot="main-header" className="sticky top-0 h-20 …">My app header</div>
140
+
141
+ <StickySection offsetSelector="[data-slot='main-header']">
142
+ {(isSticky, topOffset) => (
143
+ <nav style={{ top: isSticky ? topOffset : undefined }} className={isSticky ? "fixed …" : ""}>
144
+ Sub-navigation
145
+ </nav>
146
+ )}
147
+ </StickySection>
148
+ ```
149
+
150
+ ### With a fixed top offset
151
+
152
+ ```tsx
153
+ <StickySection topOffset={80}>
154
+ {(isSticky, topOffset) => (
155
+ <div
156
+ style={{ top: isSticky ? topOffset : undefined }}
157
+ className={isSticky ? "fixed …" : ""}
158
+ >
159
+ Actions bar
160
+ </div>
161
+ )}
162
+ </StickySection>
163
+ ```
164
+
165
+ ### Controlled mode
166
+
167
+ ```tsx
168
+ const [pinned, setPinned] = useState(false);
169
+
170
+ <StickySection isSticky={pinned} topOffset={64}>
171
+ <ActionBar />
172
+ </StickySection>;
173
+ ```
174
+
175
+ ### Reacting to changes
176
+
177
+ ```tsx
178
+ <StickySection
179
+ topOffset={64}
180
+ onIsStickyChange={(isSticky, topOffset) => {
181
+ analytics.track("sticky_change", { isSticky, topOffset });
182
+ }}
183
+ >
184
+ <ActionBar />
185
+ </StickySection>
186
+ ```
187
+
188
+ ## Behavior
189
+
190
+ ### Sentinel element
191
+
192
+ `StickySection` places an invisible 1 px `<div>` (`position: absolute; top: 0`) at the very start of its render output. The `IntersectionObserver` watches this sentinel — when it leaves the viewport (factoring in `topOffset` via `rootMargin`), `isSticky` becomes `true`.
193
+
194
+ ### Top offset resolution order
195
+
196
+ 1. `topOffset` prop (explicit, takes priority)
197
+ 2. Height of the element matched by `offsetSelector` (measured via `useElementRect`)
198
+ 3. Fallback: `64` px
199
+
200
+ ### Controlled vs uncontrolled
201
+
202
+ | Mode | How to activate | Behavior |
203
+ | ------------ | ----------------------- | --------------------------------------------------------------------------------------- |
204
+ | Uncontrolled | Omit `isSticky` prop | `IntersectionObserver` manages state internally. |
205
+ | Controlled | Provide `isSticky` prop | Observer still fires `onIsStickyChange`, but internal `setInternalIsSticky` is skipped. |
206
+
207
+ ## Best practices
208
+
209
+ ### ✅ DO: Apply `position: fixed` inside the section
210
+
211
+ ```tsx
212
+ <StickySection topOffset={64}>
213
+ {(isSticky, topOffset) => (
214
+ <div
215
+ style={{ top: isSticky ? topOffset : undefined }}
216
+ className={isSticky ? "fixed left-0 right-0 shadow z-50" : "relative"}
217
+ >
218
+ Toolbar
219
+ </div>
220
+ )}
221
+ </StickySection>
222
+ ```
223
+
224
+ ### ❌ DON'T: Nest multiple `StickySection` components with conflicting selectors
225
+
226
+ Each `StickySection` creates its own sentinel and observer. Nesting without explicit `topOffset`
227
+ values can produce unexpected offsets. Prefer sibling sections or set explicit `topOffset` on nested ones.
228
+
229
+ ### ✅ DO: Wrap the layout section that owns the sticky header
230
+
231
+ The sentinel must be the first rendered element inside the container that scrolls.
232
+
233
+ ### ❌ DON'T: Use `StickySection` for static layouts
234
+
235
+ `IntersectionObserver` is set up on mount and torn down on unmount. Avoid mounting and unmounting
236
+ the component rapidly in tight render loops.
237
+
238
+ ## Accessibility
239
+
240
+ - `StickySection` renders one invisible `<div>` sentinel. It carries no semantic role and is not focusable.
241
+ - The sticky visual state is purely presentational; ensure that no information is conveyed exclusively through the sticky appearance.
242
+ - If the sticky element covers page content, verify that keyboard focus order is still logical and that overlapped content is reachable.
243
+
244
+ ## References
245
+
246
+ - [MDN — IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
247
+ - [MDN — rootMargin](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin)
248
+ - [`useElementRect` hook](../../src/hooks/use-element-rect.ts)
package/llm.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Adamo UI Component Library - LLM Context
2
2
 
3
- > **CRITICAL**: This is a COMPLETE React component library with 49+ components. DO NOT create components that already exist. ALWAYS check this file first and use existing components.
3
+ > **CRITICAL**: This is a COMPLETE React component library with 50+ components. DO NOT create components that already exist. ALWAYS check this file first and use existing components.
4
4
 
5
5
  ## 🚨 MOST IMPORTANT RULES
6
6
 
@@ -75,7 +75,7 @@ Before creating ANY component, verify it doesn't exist here. For implementation
75
75
  - **Separator** [`docs/components/ui/separator.md`] - `@adamosuiteservices/ui/separator`
76
76
  - **Scroll Area** [`docs/components/ui/scroll-area.md`] - `@adamosuiteservices/ui/scroll-area`
77
77
 
78
- ### Other Components (7)
78
+ ### Other Components (8)
79
79
  - **Icon** [`docs/components/ui/icon.md`] - `@adamosuiteservices/ui/icon` (Material Symbols)
80
80
  - **Calendar** [`docs/components/ui/calendar.md`] - `@adamosuiteservices/ui/calendar`
81
81
  - **Date Picker Selector** [`docs/components/ui/date-picker-selector.md`] - `@adamosuiteservices/ui/date-picker-selector`
@@ -83,6 +83,7 @@ Before creating ANY component, verify it doesn't exist here. For implementation
83
83
  - **Kbd** [`docs/components/ui/kbd.md`] - `@adamosuiteservices/ui/kbd`
84
84
  - **Input OTP** [`docs/components/ui/input-otp.md`] - `@adamosuiteservices/ui/input-otp`
85
85
  - **Full Screen Loader** [`docs/components/layout/full-screen-loader.md`] - `@adamosuiteservices/ui/full-screen-loader`
86
+ - **Sticky Section** [`docs/components/layout/sticky-section.md`] - `@adamosuiteservices/ui/sticky-section` (scroll-aware sticky detection with context + render-prop)
86
87
 
87
88
  ### Utilities
88
89
  - **cn()** - `@adamosuiteservices/ui/lib` - Merge Tailwind classes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adamosuiteservices/ui",
3
- "version": "2.19.3",
3
+ "version": "2.20.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -42,6 +42,10 @@
42
42
  "types": "./dist/components/layout/full-screen-loader/index.d.ts",
43
43
  "import": "./dist/full-screen-loader.js"
44
44
  },
45
+ "./sticky-section": {
46
+ "types": "./dist/components/layout/sticky-section/index.d.ts",
47
+ "import": "./dist/sticky-section.js"
48
+ },
45
49
  "./accordion": {
46
50
  "types": "./dist/components/ui/accordion/accordion.d.ts",
47
51
  "import": "./dist/accordion.js"