@godxjp/ui 33.25.1 → 33.26.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/agent/tokens.json CHANGED
@@ -13337,6 +13337,120 @@
13337
13337
  "tier": "component",
13338
13338
  "value": "initial"
13339
13339
  },
13340
+ {
13341
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13342
+ "name": "--flex-card-size-ratio",
13343
+ "tier": "component",
13344
+ "value": "1.4"
13345
+ },
13346
+ {
13347
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13348
+ "name": "--flex-card-width",
13349
+ "tier": "component",
13350
+ "value": "clamp(3.25rem, 6.6vw, 5.625rem)"
13351
+ },
13352
+ {
13353
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13354
+ "name": "--flex-compact-card-width",
13355
+ "tier": "component",
13356
+ "value": "clamp(1.875rem, 3.3vw, 2.8125rem)"
13357
+ },
13358
+ {
13359
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13360
+ "name": "--flex-orientation-duration",
13361
+ "tier": "component",
13362
+ "value": "180ms"
13363
+ },
13364
+ {
13365
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13366
+ "name": "--flex-felt-background",
13367
+ "tier": "component",
13368
+ "value": "initial"
13369
+ },
13370
+ {
13371
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13372
+ "name": "--flex-felt-foreground",
13373
+ "tier": "component",
13374
+ "value": "initial"
13375
+ },
13376
+ {
13377
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13378
+ "name": "--flex-felt-muted-foreground",
13379
+ "tier": "component",
13380
+ "value": "initial"
13381
+ },
13382
+ {
13383
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13384
+ "name": "--flex-felt-focus-color",
13385
+ "tier": "component",
13386
+ "value": "initial"
13387
+ },
13388
+ {
13389
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13390
+ "name": "--flex-felt-control-border-alpha",
13391
+ "tier": "component",
13392
+ "value": "0.65"
13393
+ },
13394
+ {
13395
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13396
+ "name": "--flex-felt-track-alpha",
13397
+ "tier": "component",
13398
+ "value": "0.8"
13399
+ },
13400
+ {
13401
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13402
+ "name": "--flex-felt-grain-width",
13403
+ "tier": "component",
13404
+ "value": "1px"
13405
+ },
13406
+ {
13407
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13408
+ "name": "--flex-felt-highlight-size",
13409
+ "tier": "component",
13410
+ "value": "72%"
13411
+ },
13412
+ {
13413
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13414
+ "name": "--flex-felt-grain-size",
13415
+ "tier": "component",
13416
+ "value": "3px"
13417
+ },
13418
+ {
13419
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13420
+ "name": "--flex-felt-grain-alpha",
13421
+ "tier": "component",
13422
+ "value": "0.025"
13423
+ },
13424
+ {
13425
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13426
+ "name": "--flex-felt-highlight-alpha",
13427
+ "tier": "component",
13428
+ "value": "0.045"
13429
+ },
13430
+ {
13431
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13432
+ "name": "--flex-etched-border-color",
13433
+ "tier": "component",
13434
+ "value": "initial"
13435
+ },
13436
+ {
13437
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13438
+ "name": "--flex-etched-border-alpha",
13439
+ "tier": "component",
13440
+ "value": "0.5"
13441
+ },
13442
+ {
13443
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13444
+ "name": "--flex-etched-border-size",
13445
+ "tier": "component",
13446
+ "value": "var(--stroke-hairline)"
13447
+ },
13448
+ {
13449
+ "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
13450
+ "name": "--flex-etched-radius",
13451
+ "tier": "component",
13452
+ "value": "initial"
13453
+ },
13340
13454
  {
13341
13455
  "description": "THE TWO CORNER INSETS gh#558 ASKED FOR BY NAME. Ant Design's are `marginXXL` (48px) from the bottom and `marginLG` (24px) from the inline end. Spelled here in this library's own stack/inline steps, which land on the same two numbers: --space-stack-xl = --space-10 = 40px, --space-6 = 24px. The block inset is the one a service actually has to move — a sticky action bar, a cookie strip, a mobile tab bar all live in that corner — and moving it is now retuning ONE custom property, not writing a media query at the call site (which is `no-arbitrary-spacing` and has no legal spelling).",
13342
13456
  "name": "--float-button-offset-block-end",
@@ -15701,6 +15815,90 @@
15701
15815
  "tier": "component",
15702
15816
  "value": "initial"
15703
15817
  },
15818
+ {
15819
+ "description": "RadarChart tokens. Radii and font sizes use intrinsic SVG coordinates; colors resolve in the local theme scope.",
15820
+ "name": "--radar-chart-label-inset",
15821
+ "tier": "component",
15822
+ "value": "8px"
15823
+ },
15824
+ {
15825
+ "description": "Unitless scale of the normalized polar geometry.",
15826
+ "name": "--radar-chart-plot-radius",
15827
+ "tier": "component",
15828
+ "value": "96"
15829
+ },
15830
+ {
15831
+ "description": "scale-exempt: polar distance in viewBox user units, not a corner radius",
15832
+ "name": "--radar-chart-label-radius",
15833
+ "tier": "component",
15834
+ "value": "124px"
15835
+ },
15836
+ {
15837
+ "description": "RadarChart tokens. Radii and font sizes use intrinsic SVG coordinates; colors resolve in the local theme scope.",
15838
+ "name": "--radar-chart-grid-alpha",
15839
+ "tier": "component",
15840
+ "value": "0.16"
15841
+ },
15842
+ {
15843
+ "description": "RadarChart tokens. Radii and font sizes use intrinsic SVG coordinates; colors resolve in the local theme scope.",
15844
+ "name": "--radar-chart-boundary-alpha",
15845
+ "tier": "component",
15846
+ "value": "0.32"
15847
+ },
15848
+ {
15849
+ "description": "RadarChart tokens. Radii and font sizes use intrinsic SVG coordinates; colors resolve in the local theme scope.",
15850
+ "name": "--radar-chart-axis-alpha",
15851
+ "tier": "component",
15852
+ "value": "0.2"
15853
+ },
15854
+ {
15855
+ "description": "scale-exempt: viewBox user units, scaled with the SVG, not CSS type",
15856
+ "name": "--radar-chart-label-font-size",
15857
+ "tier": "component",
15858
+ "value": "16px"
15859
+ },
15860
+ {
15861
+ "description": "Unitless second-line leading, multiplied by the resolved label font size.",
15862
+ "name": "--radar-chart-label-line-height",
15863
+ "tier": "component",
15864
+ "value": "1.25"
15865
+ },
15866
+ {
15867
+ "description": "RadarChart tokens. Radii and font sizes use intrinsic SVG coordinates; colors resolve in the local theme scope.",
15868
+ "name": "--radar-chart-tick-offset",
15869
+ "tier": "component",
15870
+ "value": "6px"
15871
+ },
15872
+ {
15873
+ "description": "scale-exempt: viewBox user units, scaled with the SVG, not CSS type",
15874
+ "name": "--radar-chart-tick-font-size",
15875
+ "tier": "component",
15876
+ "value": "10px"
15877
+ },
15878
+ {
15879
+ "description": "scale-exempt: SVG circle r in viewBox user units, not a corner radius",
15880
+ "name": "--radar-chart-point-radius",
15881
+ "tier": "component",
15882
+ "value": "3px"
15883
+ },
15884
+ {
15885
+ "description": "HSL channels; default muted-foreground resolves at the chart.",
15886
+ "name": "--radar-chart-grid-color",
15887
+ "tier": "component",
15888
+ "value": "initial"
15889
+ },
15890
+ {
15891
+ "description": "HSL channels; default foreground resolves at the chart.",
15892
+ "name": "--radar-chart-label-color",
15893
+ "tier": "component",
15894
+ "value": "initial"
15895
+ },
15896
+ {
15897
+ "description": "RadarChart tokens. Radii and font sizes use intrinsic SVG coordinates; colors resolve in the local theme scope.",
15898
+ "name": "--radar-chart-grid-stroke-width",
15899
+ "tier": "component",
15900
+ "value": "1px"
15901
+ },
15704
15902
  {
15705
15903
  "description": "Separator layout thickness; focus paint must not reserve extra space.",
15706
15904
  "name": "--resizable-handle-size",
@@ -18,3 +18,5 @@ export { PieChart } from "./pie-chart.js";
18
18
  export type { PieChartProp, PieChartProps } from "./pie-chart.js";
19
19
  export type { ChartSeriesProp, ChartDatum, ChartDatumSelect } from "../../props/components/charts.prop.js";
20
20
  export { CHART_COLORS } from "./chart-frame.js";
21
+ export { RadarChart } from "./radar-chart.js";
22
+ export type { RadarChartProp, RadarChartProps } from "./radar-chart.js";
@@ -5,6 +5,7 @@ import { HeatmapChart } from "./heatmap-chart.js";
5
5
  import { AreaChart } from "./area-chart.js";
6
6
  import { PieChart } from "./pie-chart.js";
7
7
  import { CHART_COLORS } from "./chart-frame.js";
8
+ import { RadarChart } from "./radar-chart.js";
8
9
  export {
9
10
  AreaChart,
10
11
  BarChart,
@@ -12,5 +13,6 @@ export {
12
13
  CompactBarTrend,
13
14
  HeatmapChart,
14
15
  LineChart,
15
- PieChart
16
+ PieChart,
17
+ RadarChart
16
18
  };
@@ -0,0 +1,5 @@
1
+ import * as React from "react";
2
+ import type { RadarChartProp } from "../../props/components/charts.prop.js";
3
+ export type { RadarChartProp, RadarChartProp as RadarChartProps, } from "../../props/components/charts.prop.js";
4
+ /** Shared-scale profile. SVG geometry is intrinsic, never consumer CSS or inferred scores. */
5
+ export declare function RadarChart({ data, categoryKey, valueKey, valueMax, valueTicks, label, showCaption, description, size, numberFormat, emptyMessage, footer, className, id, ref, }: RadarChartProp): React.JSX.Element;
@@ -0,0 +1,273 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { EmptyState } from "../data-display/empty-state.js";
5
+ import { useTranslation } from "../../i18n/use-translation.js";
6
+ import { ChartFrame, chartColor, chartHeight, useChartNumberFormat } from "./chart-frame.js";
7
+ function wrapLabel(text, locale, width, measure) {
8
+ const words = new Intl.Segmenter(locale, { granularity: "word" });
9
+ const graphemes = new Intl.Segmenter(locale, { granularity: "grapheme" });
10
+ const lines = [];
11
+ let line = "";
12
+ for (const { segment } of words.segment(text)) {
13
+ if (measure(line + segment) <= width) {
14
+ line += segment;
15
+ continue;
16
+ }
17
+ if (line.trim()) {
18
+ lines.push(line.trim());
19
+ line = "";
20
+ }
21
+ for (const { segment: char } of graphemes.segment(segment.trimStart())) {
22
+ if (line && measure(line + char) > width) {
23
+ lines.push(line);
24
+ line = "";
25
+ }
26
+ line += char;
27
+ }
28
+ }
29
+ if (line.trim()) lines.push(line.trim());
30
+ if (lines.length <= 2) return lines;
31
+ const last = Array.from(graphemes.segment(lines[1]), (part) => part.segment);
32
+ while (last.length && measure(last.join("") + "\u2026") > width) last.pop();
33
+ return [lines[0], last.join("") + "\u2026"];
34
+ }
35
+ function RadarChart({
36
+ data,
37
+ categoryKey,
38
+ valueKey,
39
+ valueMax,
40
+ valueTicks,
41
+ label,
42
+ showCaption = true,
43
+ description,
44
+ size = "md",
45
+ numberFormat,
46
+ emptyMessage,
47
+ footer,
48
+ className,
49
+ id,
50
+ ref
51
+ }) {
52
+ const { t, locale } = useTranslation();
53
+ const fmt = useChartNumberFormat(numberFormat ?? { maximumSignificantDigits: 21 });
54
+ const uid = React.useId();
55
+ const svgRef = React.useRef(null);
56
+ const [measuredLabels, setMeasuredLabels] = React.useState([]);
57
+ React.useEffect(() => {
58
+ const svg = svgRef.current;
59
+ if (!svg || typeof svg.getBBox !== "function") return;
60
+ let disposed = false;
61
+ const context = document.createElement("canvas").getContext("2d");
62
+ if (!context) return;
63
+ const update = () => {
64
+ if (disposed) return;
65
+ const lines = Array.from(
66
+ svg.querySelectorAll('[data-slot="radar-label"]'),
67
+ (node, index) => {
68
+ const style = getComputedStyle(node);
69
+ context.font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
70
+ const transform = new DOMMatrix(style.transform);
71
+ const inset = parseFloat(style.getPropertyValue("--radar-chart-label-inset")) || 0;
72
+ const width = Math.abs(transform.e) > 1 ? 240 - Math.abs(transform.e) - inset : 480 - inset * 2;
73
+ return {
74
+ lines: wrapLabel(
75
+ String(data[index][categoryKey] ?? ""),
76
+ locale,
77
+ Math.max(1, width),
78
+ (text) => context.measureText(text).width
79
+ ),
80
+ lineHeight: parseFloat(style.getPropertyValue("--radar-chart-label-line-height")) || 1.25
81
+ };
82
+ }
83
+ );
84
+ setMeasuredLabels(
85
+ (previous) => JSON.stringify(previous) === JSON.stringify(lines) ? previous : lines
86
+ );
87
+ };
88
+ update();
89
+ const resize = new ResizeObserver(update);
90
+ resize.observe(svg);
91
+ const theme = new MutationObserver(update);
92
+ for (let node = svg; node; node = node.parentElement) {
93
+ theme.observe(node, {
94
+ attributes: true,
95
+ attributeFilter: ["class", "style", "data-theme", "data-tenant"]
96
+ });
97
+ }
98
+ void document.fonts?.ready.then(update);
99
+ return () => {
100
+ disposed = true;
101
+ resize.disconnect();
102
+ theme.disconnect();
103
+ };
104
+ }, [data, categoryKey, locale, size, valueMax]);
105
+ const valid = data.length >= 3 && data.length <= 6 && Number.isFinite(valueMax) && valueMax > 0 && data.every(
106
+ (d) => typeof d[valueKey] === "number" && Number.isFinite(d[valueKey]) && Number(d[valueKey]) >= 0 && Number(d[valueKey]) <= valueMax
107
+ );
108
+ const point = (index, fraction, radius = 1) => {
109
+ const angle = -Math.PI / 2 + index * Math.PI * 2 / data.length;
110
+ return [Math.cos(angle) * radius * fraction, Math.sin(angle) * radius * fraction];
111
+ };
112
+ const polygon = (fraction) => data.map((_, i) => point(i, fraction).join(",")).join(" ");
113
+ const rows = data.map(
114
+ (d) => `${String(d[categoryKey] ?? "")}: ${fmt.format(Number(d[valueKey]))}`
115
+ );
116
+ const summary = t("chart.summaryRadar", {
117
+ maximum: fmt.format(valueMax),
118
+ points: fmt.format(data.length)
119
+ });
120
+ const ticks = valueTicks?.filter((v) => Number.isFinite(v) && v > 0 && v <= valueMax).map((v) => v / valueMax) ?? [0.25, 0.5, 0.75, 1];
121
+ const stroke = "hsl(var(--radar-chart-grid-color, var(--muted-foreground)))";
122
+ return /* @__PURE__ */ jsx(
123
+ ChartFrame,
124
+ {
125
+ ref,
126
+ label,
127
+ showCaption,
128
+ description,
129
+ summaryRows: rows,
130
+ imgSummary: summary,
131
+ hasData: valid,
132
+ size,
133
+ footer,
134
+ className,
135
+ id,
136
+ children: valid ? /* @__PURE__ */ jsxs(
137
+ "svg",
138
+ {
139
+ ref: svgRef,
140
+ width: "100%",
141
+ style: { maxHeight: chartHeight(size), display: "block" },
142
+ viewBox: "0 0 480 320",
143
+ "aria-hidden": "true",
144
+ "data-slot": "radar-plot",
145
+ children: [
146
+ /* @__PURE__ */ jsx("title", { id: `${uid}-radar-title`, children: typeof label === "string" ? label : summary }),
147
+ /* @__PURE__ */ jsx("desc", { children: rows.join("; ") }),
148
+ /* @__PURE__ */ jsxs("g", { transform: "translate(240 160)", children: [
149
+ ticks.map((f) => /* @__PURE__ */ jsx(
150
+ "polygon",
151
+ {
152
+ points: polygon(f),
153
+ fill: "none",
154
+ style: { transform: "scale(var(--radar-chart-plot-radius))" },
155
+ vectorEffect: "non-scaling-stroke",
156
+ strokeWidth: "var(--radar-chart-grid-stroke-width)",
157
+ stroke,
158
+ opacity: f === 1 ? "var(--radar-chart-boundary-alpha)" : "var(--radar-chart-grid-alpha)"
159
+ },
160
+ f
161
+ )),
162
+ data.map((_, i) => {
163
+ const [x, y] = point(i, 1);
164
+ return /* @__PURE__ */ jsx(
165
+ "line",
166
+ {
167
+ strokeWidth: "var(--radar-chart-grid-stroke-width)",
168
+ x1: 0,
169
+ y1: 0,
170
+ x2: x,
171
+ y2: y,
172
+ stroke,
173
+ opacity: "var(--radar-chart-axis-alpha)",
174
+ vectorEffect: "non-scaling-stroke",
175
+ style: { transform: "scale(var(--radar-chart-plot-radius))" }
176
+ },
177
+ i
178
+ );
179
+ }),
180
+ ticks.map((f) => /* @__PURE__ */ jsx(
181
+ "text",
182
+ {
183
+ "data-slot": "radar-tick",
184
+ style: {
185
+ transform: `translate(calc(-1 * var(--radar-chart-tick-offset)), calc(-${f} * var(--radar-chart-plot-radius) * 1px))`
186
+ },
187
+ textAnchor: "end",
188
+ fill: stroke,
189
+ fontSize: "var(--radar-chart-tick-font-size)",
190
+ children: fmt.format(valueMax * f)
191
+ },
192
+ f
193
+ )),
194
+ /* @__PURE__ */ jsx(
195
+ "polygon",
196
+ {
197
+ "data-slot": "radar-area",
198
+ style: { transform: "scale(var(--radar-chart-plot-radius))" },
199
+ vectorEffect: "non-scaling-stroke",
200
+ points: data.map((d, i) => point(i, Number(d[valueKey]) / valueMax).join(",")).join(" "),
201
+ fill: chartColor(0),
202
+ fillOpacity: "var(--chart-area-fill-alpha)",
203
+ stroke: chartColor(0),
204
+ strokeWidth: "var(--chart-series-stroke-width)",
205
+ strokeLinejoin: "round"
206
+ }
207
+ ),
208
+ data.map((d, i) => {
209
+ const [x, y] = point(i, Number(d[valueKey]) / valueMax);
210
+ const [lx, ly] = point(i, 1);
211
+ const fullLabel = String(d[categoryKey] ?? "");
212
+ const shown = measuredLabels[i]?.lines ?? wrapLabel(
213
+ fullLabel,
214
+ locale,
215
+ Math.abs(lx) > 0.1 ? 104 : 400,
216
+ (text) => Array.from(text).reduce(
217
+ (width, char) => width + (char.codePointAt(0) > 255 ? 16 : 9),
218
+ 0
219
+ )
220
+ );
221
+ return /* @__PURE__ */ jsxs("g", { children: [
222
+ /* @__PURE__ */ jsx(
223
+ "circle",
224
+ {
225
+ r: "var(--radar-chart-point-radius)",
226
+ style: {
227
+ transform: `translate(calc(${x} * var(--radar-chart-plot-radius) * 1px), calc(${y} * var(--radar-chart-plot-radius) * 1px))`
228
+ },
229
+ fill: chartColor(0)
230
+ }
231
+ ),
232
+ /* @__PURE__ */ jsxs(
233
+ "text",
234
+ {
235
+ "data-slot": "radar-label",
236
+ style: {
237
+ transform: `translate(calc(${lx} * var(--radar-chart-label-radius)), calc(${ly} * var(--radar-chart-label-radius)))`
238
+ },
239
+ textAnchor: lx > 0.1 ? "start" : lx < -0.1 ? "end" : "middle",
240
+ dominantBaseline: "middle",
241
+ fill: "hsl(var(--radar-chart-label-color, var(--foreground)))",
242
+ fontSize: "var(--radar-chart-label-font-size)",
243
+ fontFamily: "inherit",
244
+ children: [
245
+ /* @__PURE__ */ jsxs("title", { children: [
246
+ fullLabel,
247
+ ": ",
248
+ fmt.format(Number(d[valueKey]))
249
+ ] }),
250
+ shown.map((line, index) => /* @__PURE__ */ jsx(
251
+ "tspan",
252
+ {
253
+ x: 0,
254
+ dy: index ? `${measuredLabels[i]?.lineHeight ?? 1.25}em` : 0,
255
+ children: line
256
+ },
257
+ index
258
+ ))
259
+ ]
260
+ }
261
+ )
262
+ ] }, i);
263
+ })
264
+ ] })
265
+ ]
266
+ }
267
+ ) : /* @__PURE__ */ jsx(EmptyState, { title: emptyMessage ?? t("chart.empty") })
268
+ }
269
+ );
270
+ }
271
+ export {
272
+ RadarChart
273
+ };
@@ -67,7 +67,9 @@ const Affix = React.forwardRef(function Affix2({
67
67
  assumeInView: true
68
68
  });
69
69
  const [rendered, setRendered] = React.useState(true);
70
- const affixed = !inView && rendered;
70
+ const [sticky, setSticky] = React.useState(false);
71
+ const [stuck, setStuck] = React.useState(false);
72
+ const affixed = (sticky ? stuck : !inView) && rendered;
71
73
  const [box, setBox] = React.useState(null);
72
74
  const [targetInset, setTargetInset] = React.useState(0);
73
75
  const [room, setRoom] = React.useState(null);
@@ -84,21 +86,31 @@ const Affix = React.forwardRef(function Affix2({
84
86
  );
85
87
  const rect = targetElement?.getBoundingClientRect();
86
88
  const blockEl = fixedContainingBlock(content);
89
+ const sentinelRect = sentinelRef.current?.getBoundingClientRect();
90
+ const insetStart = sentinelRect ? rootRect.top - sentinelRect.top : 0;
91
+ const insetEnd = sentinelRect ? sentinelRect.bottom - rootRect.bottom : 0;
92
+ const visibleTop = Math.max(rect ? rect.top : 0, 0);
93
+ const visibleBottom = Math.min(rect ? rect.bottom : window.innerHeight, window.innerHeight);
94
+ const line = pinToEnd ? visibleBottom - insetEnd : (rect ? rect.top : 0) + insetStart;
95
+ const parent = root.parentElement;
96
+ const nextSticky = blockEl !== null && blockEl === targetElement && parent !== null && parent.getBoundingClientRect().height > rootRect.height + 1;
97
+ setSticky((previous) => previous === nextSticky ? previous : nextSticky);
98
+ if (nextSticky) {
99
+ const atLine = Math.abs((pinToEnd ? rootRect.bottom : rootRect.top) - line) < 1;
100
+ const nextStuck = atLine && (targetElement?.scrollTop ?? 0) > 0;
101
+ setStuck((previous) => previous === nextStuck ? previous : nextStuck);
102
+ setTargetInset((previous) => previous === 0 ? previous : 0);
103
+ }
87
104
  const block = blockEl?.getBoundingClientRect();
88
105
  const viewportInset = rect ? pinToEnd ? window.innerHeight - rect.bottom : rect.top : 0;
89
106
  const blockOffset = block ? pinToEnd ? window.innerHeight - (block.bottom + ((blockEl?.scrollHeight ?? 0) - (blockEl?.clientHeight ?? 0) - (blockEl?.scrollTop ?? 0))) : block.top - (blockEl?.scrollTop ?? 0) : 0;
90
107
  const next = viewportInset - blockOffset;
91
- setTargetInset((previous) => previous === next ? previous : next);
92
- if (!affixed) {
108
+ if (!nextSticky) setTargetInset((previous) => previous === next ? previous : next);
109
+ if (!affixed && !nextSticky) {
93
110
  setRoom((previous) => previous === null ? previous : null);
94
111
  return;
95
112
  }
96
- const nextRoom = Math.max(
97
- 0,
98
- Math.floor(
99
- pinToEnd ? contentRect.bottom - Math.max(rect ? rect.top : 0, 0) : Math.min(rect ? rect.bottom : window.innerHeight, window.innerHeight) - contentRect.top
100
- )
101
- );
113
+ const nextRoom = Math.max(0, Math.floor(pinToEnd ? line - visibleTop : visibleBottom - line));
102
114
  const clamp = blockSize > nextRoom ? nextRoom : null;
103
115
  setRoom((previous) => previous === clamp ? previous : clamp);
104
116
  }, [affixed, pinToEnd, targetElement]);
@@ -120,7 +132,7 @@ const Affix = React.forwardRef(function Affix2({
120
132
  };
121
133
  }, [measure, targetElement]);
122
134
  React.useEffect(() => {
123
- if (!affixed || typeof window === "undefined") return void 0;
135
+ if (!affixed && !sticky || typeof window === "undefined") return void 0;
124
136
  let frame = null;
125
137
  const onScroll = () => {
126
138
  if (frame !== null) return;
@@ -136,7 +148,7 @@ const Affix = React.forwardRef(function Affix2({
136
148
  window.removeEventListener("resize", onScroll);
137
149
  if (frame !== null) window.cancelAnimationFrame(frame);
138
150
  };
139
- }, [affixed, targetElement, measure]);
151
+ }, [affixed, sticky, targetElement, measure]);
140
152
  React.useEffect(() => {
141
153
  const content = contentRef.current;
142
154
  if (pinToEnd || !affixed || !content || typeof document === "undefined") return void 0;
@@ -165,9 +177,9 @@ const Affix = React.forwardRef(function Affix2({
165
177
  const style = {
166
178
  ...offsetBlockStart === void 0 ? void 0 : { "--affix-inset-block-start": `${offsetBlockStart}px` },
167
179
  ...offsetBlockEnd === void 0 ? void 0 : { "--affix-inset-block-end": `${offsetBlockEnd}px` },
168
- ...targetInset === 0 ? void 0 : { "--affix-target-inset": `${targetInset}px` },
169
- ...affixed && box ? { "--affix-content-inline-size": `${box.inline}px` } : void 0,
170
- ...affixed && room !== null ? { "--affix-content-max-block-size": `${room}px` } : void 0
180
+ ...sticky || targetInset === 0 ? void 0 : { "--affix-target-inset": `${targetInset}px` },
181
+ ...!sticky && affixed && box ? { "--affix-content-inline-size": `${box.inline}px` } : void 0,
182
+ ...(affixed || sticky) && room !== null ? { "--affix-content-max-block-size": `${room}px` } : void 0
171
183
  };
172
184
  return /* @__PURE__ */ jsxs(
173
185
  "div",
@@ -180,6 +192,7 @@ const Affix = React.forwardRef(function Affix2({
180
192
  id,
181
193
  "data-slot": "affix",
182
194
  "data-affixed": affixed ? "" : void 0,
195
+ "data-pin": sticky ? pinToEnd ? "sticky-end" : "sticky-start" : void 0,
183
196
  className: cn("ui-affix", className),
184
197
  style,
185
198
  ...rest,
@@ -194,7 +207,7 @@ const Affix = React.forwardRef(function Affix2({
194
207
  className: "ui-affix-sentinel"
195
208
  }
196
209
  ),
197
- affixed && box ? /* @__PURE__ */ jsx(
210
+ affixed && box && !sticky ? /* @__PURE__ */ jsx(
198
211
  "div",
199
212
  {
200
213
  "data-slot": "affix-placeholder",
@@ -209,7 +222,7 @@ const Affix = React.forwardRef(function Affix2({
209
222
  ref: contentRef,
210
223
  "data-slot": "affix-content",
211
224
  "data-edge": pinToEnd ? "block-end" : "block-start",
212
- "data-clamped": affixed && room !== null ? "" : void 0,
225
+ "data-clamped": (affixed || sticky) && room !== null ? "" : void 0,
213
226
  "data-affixed": affixed ? "" : void 0,
214
227
  className: "ui-affix-content",
215
228
  children