@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/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:hidden",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"?`
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" }), h = n.createContext(!1);
7
- function w({ className: a, orientation: t = "vertical", responsive: d = !0, children: m, ...r }) {
8
- const s = C("(max-width: 639px)"), c = t === "horizontal" && d && s ? "vertical" : t, u = t === "horizontal" && !d, p = n.Children.toArray(m), f = /* @__PURE__ */ e.jsx(
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], j = n.isValidElement(v) ? v.props.status ?? "pending" : "pending";
22
- return /* @__PURE__ */ e.jsx(h.Provider, { value: j === "complete", children: x }, b);
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", ...d }) {
29
- const { orientation: m, scrollable: r } = n.useContext(o);
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
- m === "vertical" ? "adm:flex adm:gap-4" : i(
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
- ...d
43
+ ...m
44
44
  }
45
45
  ) });
46
46
  }
47
47
  function g() {
48
- const { status: a } = n.useContext(l), t = a === "complete", d = a === "active";
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
- d && "adm:border-primary adm:bg-background",
60
- !d && !t && "adm:border-border adm:bg-background"
59
+ m && "adm:border-primary adm:bg-background",
60
+ !m && !t && "adm:border-border adm:bg-background"
61
61
  ),
62
62
  children: [
63
- d && /* @__PURE__ */ e.jsx("span", { className: "adm:size-2 adm:rounded-full adm:bg-primary" }),
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: d } = n.useContext(o), { status: m } = n.useContext(l), r = n.useContext(h), s = m === "complete";
80
- return d === "vertical" ? /* @__PURE__ */ e.jsxs(
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:hidden",
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: d } = n.useContext(o), { status: m } = n.useContext(l);
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
- d === "vertical" ? `
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
- m === "pending" ? "adm:text-disabled-foreground" : "adm:text-foreground",
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` and `group-first:invisible`
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 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.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"