@adamosuiteservices/ui 2.19.2 → 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.
- package/dist/components/layout/sticky-section/index.d.ts +1 -0
- package/dist/components/layout/sticky-section/sticky-section.d.ts +28 -0
- package/dist/components/ui/badge/badge.d.ts +1 -1
- package/dist/hooks/use-element-rect.d.ts +11 -0
- package/dist/sticky-section.cjs +1 -0
- package/dist/sticky-section.js +65 -0
- package/dist/styles.css +1 -1
- package/dist/timeline.cjs +2 -2
- package/dist/timeline.js +20 -20
- package/docs/components/layout/sticky-section.md +248 -0
- package/docs/components/ui/timeline.md +6 -1
- package/llm.txt +3 -2
- package/package.json +5 -1
package/dist/timeline.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const i=require("./jsx-runtime-BB_1_6y_.cjs"),a=require("./index-DoxiiusW.cjs"),R=require("./index-DCsgSkBj.cjs"),E=require("react");function T(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t){for(const n in t)if(n!=="default"){const r=Object.getOwnPropertyDescriptor(t,n);Object.defineProperty(e,n,r.get?r:{enumerable:!0,get:()=>t[n]})}}return e.default=t,Object.freeze(e)}const s=T(E),d=s.createContext({orientation:"vertical",scrollable:!1}),l=s.createContext({status:"pending"}),v=s.createContext(!1);function C({className:t,orientation:e="vertical",responsive:n=!0,children:r,...m}){const o=R.useMediaQuery("(max-width: 639px)"),c=e==="horizontal"&&n&&o?"vertical":e,u=e==="horizontal"&&!n,p=s.Children.toArray(r),f=i.jsxRuntimeExports.jsx("ol",{"data-slot":"timeline","data-orientation":c,className:a.cn("adm:flex",c==="vertical"?"adm:flex-col":"adm:flex-row adm:items-start",t),...m,children:p.map((x,j)=>{if(!s.isValidElement(x))return x;const b=p[j-1],h=s.isValidElement(b)?b.props.status??"pending":"pending";return i.jsxRuntimeExports.jsx(v.Provider,{value:h==="complete",children:x},j)})});return i.jsxRuntimeExports.jsx(d.Provider,{value:{orientation:c,scrollable:u},children:u?i.jsxRuntimeExports.jsx("div",{className:"adm:w-full adm:overflow-x-auto",children:f}):f})}function y({className:t,status:e="pending",...n}){const{orientation:r,scrollable:m}=s.useContext(d);return i.jsxRuntimeExports.jsx(l.Provider,{value:{status:e},children:i.jsxRuntimeExports.jsx("li",{"data-slot":"timeline-item","data-status":e,className:a.cn("adm:group adm:relative",r==="vertical"?"adm:flex adm:gap-4":a.cn("adm:flex adm:flex-1 adm:flex-col adm:items-center",m&&"adm:min-w-28"),t),...n})})}function g(){const{status:t}=s.useContext(l),e=t==="complete",n=t==="active";return i.jsxRuntimeExports.jsxs("div",{"data-slot":"timeline-dot",className:a.cn("adm:relative adm:flex adm:size-5 adm:shrink-0 adm:items-center","adm:justify-center","adm:rounded-full adm:border","adm:transition-[color,box-shadow,background-color,border-color]",e&&"adm:border-primary adm:bg-primary",n&&"adm:border-primary adm:bg-background",!n&&!e&&"adm:border-border adm:bg-background"),children:[n&&i.jsxRuntimeExports.jsx("span",{className:"adm:size-2 adm:rounded-full adm:bg-primary"}),e&&i.jsxRuntimeExports.jsx("svg",{viewBox:"0 0 12 12",className:"adm:size-3",fill:"none","aria-hidden":!0,children:i.jsxRuntimeExports.jsx("path",{d:"M2 6l3 3 5-5",stroke:"white",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})]})}function N({className:t,...e}){const{orientation:n}=s.useContext(d),{status:r}=s.useContext(l),m=s.useContext(v),o=r==="complete";return n==="vertical"?i.jsxRuntimeExports.jsxs("div",{"data-slot":"timeline-indicator",className:a.cn("adm:flex adm:shrink-0 adm:flex-col adm:items-center",t),...e,children:[i.jsxRuntimeExports.jsx(g,{}),i.jsxRuntimeExports.jsx("div",{className:a.cn("adm:my-1 adm:min-h-4 adm:w-px adm:flex-1","adm:group-last:hidden",o?"adm:bg-primary":"adm:bg-border")})]}):i.jsxRuntimeExports.jsxs("div",{"data-slot":"timeline-indicator",className:a.cn("adm:flex adm:w-full adm:items-center",t),...e,children:[i.jsxRuntimeExports.jsx("div",{className:a.cn("adm:h-px adm:flex-1","adm:group-first:invisible",m?"adm:bg-primary":"adm:bg-border")}),i.jsxRuntimeExports.jsx("div",{className:"adm:mx-1",children:i.jsxRuntimeExports.jsx(g,{})}),i.jsxRuntimeExports.jsx("div",{className:a.cn("adm:h-px adm:flex-1","adm:group-last:
|
|
1
|
+
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const i=require("./jsx-runtime-BB_1_6y_.cjs"),a=require("./index-DoxiiusW.cjs"),R=require("./index-DCsgSkBj.cjs"),E=require("react");function T(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t){for(const n in t)if(n!=="default"){const r=Object.getOwnPropertyDescriptor(t,n);Object.defineProperty(e,n,r.get?r:{enumerable:!0,get:()=>t[n]})}}return e.default=t,Object.freeze(e)}const s=T(E),d=s.createContext({orientation:"vertical",scrollable:!1}),l=s.createContext({status:"pending"}),v=s.createContext(!1);function C({className:t,orientation:e="vertical",responsive:n=!0,children:r,...m}){const o=R.useMediaQuery("(max-width: 639px)"),c=e==="horizontal"&&n&&o?"vertical":e,u=e==="horizontal"&&!n,p=s.Children.toArray(r),f=i.jsxRuntimeExports.jsx("ol",{"data-slot":"timeline","data-orientation":c,className:a.cn("adm:flex",c==="vertical"?"adm:flex-col":"adm:flex-row adm:items-start",t),...m,children:p.map((x,j)=>{if(!s.isValidElement(x))return x;const b=p[j-1],h=s.isValidElement(b)?b.props.status??"pending":"pending";return i.jsxRuntimeExports.jsx(v.Provider,{value:h==="complete",children:x},j)})});return i.jsxRuntimeExports.jsx(d.Provider,{value:{orientation:c,scrollable:u},children:u?i.jsxRuntimeExports.jsx("div",{className:"adm:w-full adm:overflow-x-auto",children:f}):f})}function y({className:t,status:e="pending",...n}){const{orientation:r,scrollable:m}=s.useContext(d);return i.jsxRuntimeExports.jsx(l.Provider,{value:{status:e},children:i.jsxRuntimeExports.jsx("li",{"data-slot":"timeline-item","data-status":e,className:a.cn("adm:group adm:relative",r==="vertical"?"adm:flex adm:gap-4":a.cn("adm:flex adm:flex-1 adm:flex-col adm:items-center",m&&"adm:min-w-28"),t),...n})})}function g(){const{status:t}=s.useContext(l),e=t==="complete",n=t==="active";return i.jsxRuntimeExports.jsxs("div",{"data-slot":"timeline-dot",className:a.cn("adm:relative adm:flex adm:size-5 adm:shrink-0 adm:items-center","adm:justify-center","adm:rounded-full adm:border","adm:transition-[color,box-shadow,background-color,border-color]",e&&"adm:border-primary adm:bg-primary",n&&"adm:border-primary adm:bg-background",!n&&!e&&"adm:border-border adm:bg-background"),children:[n&&i.jsxRuntimeExports.jsx("span",{className:"adm:size-2 adm:rounded-full adm:bg-primary"}),e&&i.jsxRuntimeExports.jsx("svg",{viewBox:"0 0 12 12",className:"adm:size-3",fill:"none","aria-hidden":!0,children:i.jsxRuntimeExports.jsx("path",{d:"M2 6l3 3 5-5",stroke:"white",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})]})}function N({className:t,...e}){const{orientation:n}=s.useContext(d),{status:r}=s.useContext(l),m=s.useContext(v),o=r==="complete";return n==="vertical"?i.jsxRuntimeExports.jsxs("div",{"data-slot":"timeline-indicator",className:a.cn("adm:flex adm:shrink-0 adm:flex-col adm:items-center",t),...e,children:[i.jsxRuntimeExports.jsx(g,{}),i.jsxRuntimeExports.jsx("div",{className:a.cn("adm:my-1 adm:min-h-4 adm:w-px adm:flex-1","adm:group-last:hidden",o?"adm:bg-primary":"adm:bg-border")})]}):i.jsxRuntimeExports.jsxs("div",{"data-slot":"timeline-indicator",className:a.cn("adm:flex adm:w-full adm:items-center",t),...e,children:[i.jsxRuntimeExports.jsx("div",{className:a.cn("adm:h-px adm:flex-1","adm:group-first:invisible",m?"adm:bg-primary":"adm:bg-border")}),i.jsxRuntimeExports.jsx("div",{className:"adm:mx-1",children:i.jsxRuntimeExports.jsx(g,{})}),i.jsxRuntimeExports.jsx("div",{className:a.cn("adm:h-px adm:flex-1","adm:group-last:invisible",o?"adm:bg-primary":"adm:bg-border")})]})}function w({className:t,...e}){const{orientation:n}=s.useContext(d),{status:r}=s.useContext(l);return i.jsxRuntimeExports.jsx("div",{"data-slot":"timeline-content",className:a.cn("adm:flex adm:flex-col adm:gap-3",n==="vertical"?`
|
|
2
2
|
adm:min-w-0 adm:flex-1 adm:pb-4
|
|
3
3
|
adm:group-last:pb-0
|
|
4
|
-
`:"adm:items-center adm:pt-3",r==="pending"?"adm:text-disabled-foreground":"adm:text-foreground",t),...e})}function k({className:t,...e}){return i.jsxRuntimeExports.jsx("p",{"data-slot":"timeline-title",className:a.cn("adm:text-sm adm:leading-5 adm:font-semibold",t),...e})}function O({className:t,...e}){return i.jsxRuntimeExports.jsx("p",{"data-slot":"timeline-description",className:a.cn("adm:text-sm adm:leading-5 adm:font-normal",t),...e})}function z({className:t,...e}){return i.jsxRuntimeExports.jsx("time",{"data-slot":"timeline-time",className:a.cn("adm:text-xs adm:leading-4",t),...e})}exports.Timeline=C;exports.TimelineContent=w;exports.TimelineDescription=O;exports.TimelineIndicator=N;exports.TimelineItem=y;exports.TimelineTime=z;exports.TimelineTitle=k;
|
|
4
|
+
`:"adm:items-center adm:pt-3 adm:text-center",r==="pending"?"adm:text-disabled-foreground":"adm:text-foreground",t),...e})}function k({className:t,...e}){return i.jsxRuntimeExports.jsx("p",{"data-slot":"timeline-title",className:a.cn("adm:text-sm adm:leading-5 adm:font-semibold",t),...e})}function O({className:t,...e}){return i.jsxRuntimeExports.jsx("p",{"data-slot":"timeline-description",className:a.cn("adm:text-sm adm:leading-5 adm:font-normal",t),...e})}function z({className:t,...e}){return i.jsxRuntimeExports.jsx("time",{"data-slot":"timeline-time",className:a.cn("adm:text-xs adm:leading-4",t),...e})}exports.Timeline=C;exports.TimelineContent=w;exports.TimelineDescription=O;exports.TimelineIndicator=N;exports.TimelineItem=y;exports.TimelineTime=z;exports.TimelineTitle=k;
|
package/dist/timeline.js
CHANGED
|
@@ -3,9 +3,9 @@ import { j as e } from "./jsx-runtime-BzflLqGi.js";
|
|
|
3
3
|
import { c as i } from "./index-CRiPKpXj.js";
|
|
4
4
|
import { u as C } from "./index-BBT2EGq8.js";
|
|
5
5
|
import * as n from "react";
|
|
6
|
-
const o = n.createContext({ orientation: "vertical", scrollable: !1 }), l = n.createContext({ status: "pending" }),
|
|
7
|
-
function w({ className: a, orientation: t = "vertical", responsive:
|
|
8
|
-
const s = C("(max-width: 639px)"), c = t === "horizontal" &&
|
|
6
|
+
const o = n.createContext({ orientation: "vertical", scrollable: !1 }), l = n.createContext({ status: "pending" }), j = n.createContext(!1);
|
|
7
|
+
function w({ className: a, orientation: t = "vertical", responsive: m = !0, children: d, ...r }) {
|
|
8
|
+
const s = C("(max-width: 639px)"), c = t === "horizontal" && m && s ? "vertical" : t, u = t === "horizontal" && !m, p = n.Children.toArray(d), f = /* @__PURE__ */ e.jsx(
|
|
9
9
|
"ol",
|
|
10
10
|
{
|
|
11
11
|
"data-slot": "timeline",
|
|
@@ -18,15 +18,15 @@ function w({ className: a, orientation: t = "vertical", responsive: d = !0, chil
|
|
|
18
18
|
...r,
|
|
19
19
|
children: p.map((x, b) => {
|
|
20
20
|
if (!n.isValidElement(x)) return x;
|
|
21
|
-
const v = p[b - 1],
|
|
22
|
-
return /* @__PURE__ */ e.jsx(
|
|
21
|
+
const v = p[b - 1], h = n.isValidElement(v) ? v.props.status ?? "pending" : "pending";
|
|
22
|
+
return /* @__PURE__ */ e.jsx(j.Provider, { value: h === "complete", children: x }, b);
|
|
23
23
|
})
|
|
24
24
|
}
|
|
25
25
|
);
|
|
26
26
|
return /* @__PURE__ */ e.jsx(o.Provider, { value: { orientation: c, scrollable: u }, children: u ? /* @__PURE__ */ e.jsx("div", { className: "adm:w-full adm:overflow-x-auto", children: f }) : f });
|
|
27
27
|
}
|
|
28
|
-
function k({ className: a, status: t = "pending", ...
|
|
29
|
-
const { orientation:
|
|
28
|
+
function k({ className: a, status: t = "pending", ...m }) {
|
|
29
|
+
const { orientation: d, scrollable: r } = n.useContext(o);
|
|
30
30
|
return /* @__PURE__ */ e.jsx(l.Provider, { value: { status: t }, children: /* @__PURE__ */ e.jsx(
|
|
31
31
|
"li",
|
|
32
32
|
{
|
|
@@ -34,18 +34,18 @@ function k({ className: a, status: t = "pending", ...d }) {
|
|
|
34
34
|
"data-status": t,
|
|
35
35
|
className: i(
|
|
36
36
|
"adm:group adm:relative",
|
|
37
|
-
|
|
37
|
+
d === "vertical" ? "adm:flex adm:gap-4" : i(
|
|
38
38
|
"adm:flex adm:flex-1 adm:flex-col adm:items-center",
|
|
39
39
|
r && "adm:min-w-28"
|
|
40
40
|
),
|
|
41
41
|
a
|
|
42
42
|
),
|
|
43
|
-
...
|
|
43
|
+
...m
|
|
44
44
|
}
|
|
45
45
|
) });
|
|
46
46
|
}
|
|
47
47
|
function g() {
|
|
48
|
-
const { status: a } = n.useContext(l), t = a === "complete",
|
|
48
|
+
const { status: a } = n.useContext(l), t = a === "complete", m = a === "active";
|
|
49
49
|
return /* @__PURE__ */ e.jsxs(
|
|
50
50
|
"div",
|
|
51
51
|
{
|
|
@@ -56,11 +56,11 @@ function g() {
|
|
|
56
56
|
"adm:rounded-full adm:border",
|
|
57
57
|
"adm:transition-[color,box-shadow,background-color,border-color]",
|
|
58
58
|
t && "adm:border-primary adm:bg-primary",
|
|
59
|
-
|
|
60
|
-
!
|
|
59
|
+
m && "adm:border-primary adm:bg-background",
|
|
60
|
+
!m && !t && "adm:border-border adm:bg-background"
|
|
61
61
|
),
|
|
62
62
|
children: [
|
|
63
|
-
|
|
63
|
+
m && /* @__PURE__ */ e.jsx("span", { className: "adm:size-2 adm:rounded-full adm:bg-primary" }),
|
|
64
64
|
t && /* @__PURE__ */ e.jsx("svg", { viewBox: "0 0 12 12", className: "adm:size-3", fill: "none", "aria-hidden": !0, children: /* @__PURE__ */ e.jsx(
|
|
65
65
|
"path",
|
|
66
66
|
{
|
|
@@ -76,8 +76,8 @@ function g() {
|
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
78
|
function z({ className: a, ...t }) {
|
|
79
|
-
const { orientation:
|
|
80
|
-
return
|
|
79
|
+
const { orientation: m } = n.useContext(o), { status: d } = n.useContext(l), r = n.useContext(j), s = d === "complete";
|
|
80
|
+
return m === "vertical" ? /* @__PURE__ */ e.jsxs(
|
|
81
81
|
"div",
|
|
82
82
|
{
|
|
83
83
|
"data-slot": "timeline-indicator",
|
|
@@ -120,7 +120,7 @@ function z({ className: a, ...t }) {
|
|
|
120
120
|
{
|
|
121
121
|
className: i(
|
|
122
122
|
"adm:h-px adm:flex-1",
|
|
123
|
-
"adm:group-last:
|
|
123
|
+
"adm:group-last:invisible",
|
|
124
124
|
s ? "adm:bg-primary" : "adm:bg-border"
|
|
125
125
|
)
|
|
126
126
|
}
|
|
@@ -130,18 +130,18 @@ function z({ className: a, ...t }) {
|
|
|
130
130
|
);
|
|
131
131
|
}
|
|
132
132
|
function A({ className: a, ...t }) {
|
|
133
|
-
const { orientation:
|
|
133
|
+
const { orientation: m } = n.useContext(o), { status: d } = n.useContext(l);
|
|
134
134
|
return /* @__PURE__ */ e.jsx(
|
|
135
135
|
"div",
|
|
136
136
|
{
|
|
137
137
|
"data-slot": "timeline-content",
|
|
138
138
|
className: i(
|
|
139
139
|
"adm:flex adm:flex-col adm:gap-3",
|
|
140
|
-
|
|
140
|
+
m === "vertical" ? `
|
|
141
141
|
adm:min-w-0 adm:flex-1 adm:pb-4
|
|
142
142
|
adm:group-last:pb-0
|
|
143
|
-
` : "adm:items-center adm:pt-3",
|
|
144
|
-
|
|
143
|
+
` : "adm:items-center adm:pt-3 adm:text-center",
|
|
144
|
+
d === "pending" ? "adm:text-disabled-foreground" : "adm:text-foreground",
|
|
145
145
|
a
|
|
146
146
|
),
|
|
147
147
|
...t
|
|
@@ -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)
|
|
@@ -198,11 +198,16 @@ Renders as a `<time>` element with `text-xs`. Optional — omit when no timestam
|
|
|
198
198
|
|
|
199
199
|
Each connector segment is split in two halves. The connector below a step (vertical) or the right half of a step (horizontal) is colored by whether **that step itself** is `complete`. The left half of a horizontal step is colored by whether the **previous step** was `complete`. This is managed internally via React context — no extra props are needed.
|
|
200
200
|
|
|
201
|
-
### `group-last:hidden
|
|
201
|
+
### `group-last:invisible`, `group-last:hidden`, and `group-first:invisible`
|
|
202
202
|
|
|
203
203
|
- The vertical connector of the last `TimelineItem` is hidden via `group-last:hidden` so no trailing line appears below the final step.
|
|
204
|
+
- The horizontal right spacer of the last item uses `group-last:invisible` (takes space but is transparent) to keep the last dot center-aligned with the rest.
|
|
204
205
|
- The horizontal left spacer of the first item uses `group-first:invisible` (takes space but is transparent) to keep the first dot center-aligned with the rest.
|
|
205
206
|
|
|
207
|
+
### Horizontal text alignment
|
|
208
|
+
|
|
209
|
+
`TimelineContent` applies `text-center` when `orientation="horizontal"` so that titles, descriptions, and timestamps are centered below each dot.
|
|
210
|
+
|
|
206
211
|
## Accessibility
|
|
207
212
|
|
|
208
213
|
- The container renders as a semantic `<ol>` (ordered list), and each step is an `<li>`.
|
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
|
|
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 (
|
|
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.
|
|
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"
|