@astryxdesign/core 0.1.0-canary.e2d38fb → 0.1.0-canary.e457dac
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/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.js +1 -5
- package/dist/Outline/Outline.d.ts +2 -3
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +4 -23
- package/dist/Outline/useScrollSpy.d.ts +1 -14
- package/dist/Outline/useScrollSpy.d.ts.map +1 -1
- package/dist/Outline/useScrollSpy.js +50 -161
- package/dist/ToggleButton/ToggleButton.d.ts +3 -10
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +18 -64
- package/dist/utils/dateParser.d.ts.map +1 -1
- package/dist/utils/dateParser.js +2 -15
- package/package.json +2 -2
- package/src/Chat/ChatLayoutScrollButton.tsx +1 -7
- package/src/DateInput/DateInput.test.tsx +20 -68
- package/src/Outline/Outline.doc.mjs +1 -1
- package/src/Outline/Outline.test.tsx +38 -76
- package/src/Outline/Outline.tsx +4 -23
- package/src/Outline/useScrollSpy.ts +63 -196
- package/src/ToggleButton/ToggleButton.doc.mjs +2 -2
- package/src/ToggleButton/ToggleButton.test.tsx +6 -148
- package/src/ToggleButton/ToggleButton.tsx +20 -83
- package/src/utils/dateParser.test.ts +0 -26
- package/src/utils/dateParser.ts +2 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatLayoutScrollButton.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatLayoutScrollButton.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAY1B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAO5C,MAAM,WAAW,2BAA4B,SAAQ,IAAI,CACvD,SAAS,CAAC,cAAc,CAAC,EACzB,SAAS,CACV;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,qCAAqC;IACrC,SAAS,EAAE,OAAO,CAAC;IACnB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;
|
|
1
|
+
{"version":3,"file":"ChatLayoutScrollButton.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatLayoutScrollButton.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAY1B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAO5C,MAAM,WAAW,2BAA4B,SAAQ,IAAI,CACvD,SAAS,CAAC,cAAc,CAAC,EACzB,SAAS,CACV;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,qCAAqC;IACrC,SAAS,EAAE,OAAO,CAAC;IACnB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAkDD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,GAAG,EACH,SAAS,EACT,KAAK,EACL,OAAO,EACP,MAAM,EACN,SAAS,EACT,KAAK,GACN,EAAE,2BAA2B,qBAwB7B;yBAhCe,sBAAsB"}
|
|
@@ -43,10 +43,6 @@ const styles = {
|
|
|
43
43
|
khDVqt: "xuxw1ft",
|
|
44
44
|
kg3NbH: "xf314gf",
|
|
45
45
|
$$css: true
|
|
46
|
-
},
|
|
47
|
-
buttonWithLabel: {
|
|
48
|
-
kwRFfy: "x1t818jl",
|
|
49
|
-
$$css: true
|
|
50
46
|
}
|
|
51
47
|
};
|
|
52
48
|
|
|
@@ -99,7 +95,7 @@ export function ChatLayoutScrollButton({
|
|
|
99
95
|
variant: "ghost",
|
|
100
96
|
size: "md",
|
|
101
97
|
onClick: onClick,
|
|
102
|
-
xstyle:
|
|
98
|
+
xstyle: styles.button,
|
|
103
99
|
children: label ?? undefined
|
|
104
100
|
})
|
|
105
101
|
})
|
|
@@ -29,9 +29,8 @@ export interface OutlineProps extends BaseProps<HTMLElement> {
|
|
|
29
29
|
* indentation based on each heading level. Features a sliding indicator
|
|
30
30
|
* track that animates to the active item.
|
|
31
31
|
*
|
|
32
|
-
* When `activeId` is omitted, it
|
|
33
|
-
*
|
|
34
|
-
* active — defaulting to the first item at the top and the last at the bottom.
|
|
32
|
+
* When `activeId` is omitted, it observes heading elements by id and marks
|
|
33
|
+
* the topmost visible heading active.
|
|
35
34
|
*
|
|
36
35
|
* @example
|
|
37
36
|
* ```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Outline.d.ts","sourceRoot":"","sources":["../../src/Outline/Outline.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,SAAS,CAAC;AAGzC,YAAY,EAAC,WAAW,EAAC,MAAM,SAAS,CAAC;AAEzC,MAAM,WAAW,YAAa,SAAQ,SAAS,CAAC,WAAW,CAAC;IAC1D,6CAA6C;IAC7C,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAE7B,+CAA+C;IAC/C,KAAK,EAAE,WAAW,EAAE,CAAC;IAErB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;OAKG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAEhC,sCAAsC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAgJD
|
|
1
|
+
{"version":3,"file":"Outline.d.ts","sourceRoot":"","sources":["../../src/Outline/Outline.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,SAAS,CAAC;AAGzC,YAAY,EAAC,WAAW,EAAC,MAAM,SAAS,CAAC;AAEzC,MAAM,WAAW,YAAa,SAAQ,SAAS,CAAC,WAAW,CAAC;IAC1D,6CAA6C;IAC7C,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAE7B,+CAA+C;IAC/C,KAAK,EAAE,WAAW,EAAE,CAAC;IAErB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;OAKG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAEhC,sCAAsC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAgJD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,KAA2B,EAC3B,OAAmB,EACnB,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,aAAa,EAAE,MAAM,EACrB,GAAG,KAAK,EACT,EAAE,YAAY,+BAuFd;yBAnGe,OAAO"}
|
package/dist/Outline/Outline.js
CHANGED
|
@@ -133,9 +133,8 @@ function getIndentStyle(level) {
|
|
|
133
133
|
* indentation based on each heading level. Features a sliding indicator
|
|
134
134
|
* track that animates to the active item.
|
|
135
135
|
*
|
|
136
|
-
* When `activeId` is omitted, it
|
|
137
|
-
*
|
|
138
|
-
* active — defaulting to the first item at the top and the last at the bottom.
|
|
136
|
+
* When `activeId` is omitted, it observes heading elements by id and marks
|
|
137
|
+
* the topmost visible heading active.
|
|
139
138
|
*
|
|
140
139
|
* @example
|
|
141
140
|
* ```
|
|
@@ -163,12 +162,7 @@ export function Outline({
|
|
|
163
162
|
}) {
|
|
164
163
|
const rootRef = useRef(null);
|
|
165
164
|
const LinkComponent = useLinkComponent();
|
|
166
|
-
const
|
|
167
|
-
const {
|
|
168
|
-
activeId: resolvedActiveId,
|
|
169
|
-
setActiveId,
|
|
170
|
-
lockActiveId
|
|
171
|
-
} = useScrollSpy({
|
|
165
|
+
const [resolvedActiveId, setActiveId] = useScrollSpy({
|
|
172
166
|
activeId,
|
|
173
167
|
items,
|
|
174
168
|
onActiveIdChange,
|
|
@@ -176,25 +170,12 @@ export function Outline({
|
|
|
176
170
|
});
|
|
177
171
|
const handleClick = id => event => {
|
|
178
172
|
const target = document.getElementById(id);
|
|
179
|
-
|
|
180
|
-
// Let the browser handle modified clicks (open in new tab, etc.) and
|
|
181
|
-
// missing targets without touching the active state.
|
|
173
|
+
setActiveId(id);
|
|
182
174
|
if (target == null || event.defaultPrevented || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
|
|
183
175
|
return;
|
|
184
176
|
}
|
|
185
177
|
event.preventDefault();
|
|
186
178
|
window.history.pushState(null, '', `#${id}`);
|
|
187
|
-
|
|
188
|
-
// Move the indicator to the clicked item in a single step. Controlled
|
|
189
|
-
// consumers own the active state (notify only); uncontrolled mode pins
|
|
190
|
-
// the active id and suppresses scroll-spy until the next manual scroll,
|
|
191
|
-
// so the click is honored — even for short/last sections — and the
|
|
192
|
-
// indicator doesn't chase the smooth scroll through other sections.
|
|
193
|
-
if (isControlled) {
|
|
194
|
-
setActiveId(id);
|
|
195
|
-
} else {
|
|
196
|
-
lockActiveId(id);
|
|
197
|
-
}
|
|
198
179
|
target.scrollIntoView({
|
|
199
180
|
behavior: 'smooth',
|
|
200
181
|
block: 'start'
|
|
@@ -5,19 +5,6 @@ interface UseScrollSpyOptions {
|
|
|
5
5
|
onActiveIdChange?: (id: string) => void;
|
|
6
6
|
rootRef: React.RefObject<HTMLElement | null>;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
activeId: string | undefined;
|
|
10
|
-
/** Set the active id (notifies onActiveIdChange). For controlled consumers. */
|
|
11
|
-
setActiveId: (id: string) => void;
|
|
12
|
-
/**
|
|
13
|
-
* Handle a click on the outline item with id `id`. Delays moving the
|
|
14
|
-
* indicator: scroll-spy is suppressed during the programmatic smooth scroll
|
|
15
|
-
* so the indicator doesn't chase it, then the indicator moves once to the
|
|
16
|
-
* clicked item when the scroll settles. If the user scrolls manually mid-way,
|
|
17
|
-
* scroll-position tracking resumes immediately instead.
|
|
18
|
-
*/
|
|
19
|
-
lockActiveId: (id: string) => void;
|
|
20
|
-
}
|
|
21
|
-
export declare function useScrollSpy({ activeId, items, onActiveIdChange, rootRef, }: UseScrollSpyOptions): UseScrollSpyResult;
|
|
8
|
+
export declare function useScrollSpy({ activeId, items, onActiveIdChange, rootRef, }: UseScrollSpyOptions): [string | undefined, (id: string) => void];
|
|
22
9
|
export {};
|
|
23
10
|
//# sourceMappingURL=useScrollSpy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useScrollSpy.d.ts","sourceRoot":"","sources":["../../src/Outline/useScrollSpy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useScrollSpy.d.ts","sourceRoot":"","sources":["../../src/Outline/useScrollSpy.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,SAAS,CAAC;AAwBzC,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CAC9C;AAED,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,KAAK,EACL,gBAAgB,EAChB,OAAO,GACR,EAAE,mBAAmB,GAAG,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC,CA2FlE"}
|
|
@@ -4,23 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file useScrollSpy.ts
|
|
7
|
-
* @input Uses React,
|
|
7
|
+
* @input Uses React, IntersectionObserver, OutlineItem type
|
|
8
8
|
* @output Exports internal useScrollSpy hook
|
|
9
9
|
* @position Internal behavior hook; consumed by Outline.tsx
|
|
10
10
|
*
|
|
11
|
-
* Drives the active outline item from scroll position. On each scroll
|
|
12
|
-
* (rAF-throttled) it reads live heading positions and marks the last heading
|
|
13
|
-
* whose top has passed its activation line (its own scroll-margin-top, i.e.
|
|
14
|
-
* where it lands when navigated to). This is stable — it never compares stale
|
|
15
|
-
* cached positions — so the indicator moves monotonically instead of jumping.
|
|
16
|
-
* Defaults to the first item at the top and the last item at the bottom so
|
|
17
|
-
* short final sections still activate.
|
|
18
|
-
*
|
|
19
11
|
* SYNC: When modified, update /packages/core/src/Outline/Outline.tsx
|
|
20
12
|
*/
|
|
21
|
-
import {
|
|
22
|
-
/** Keys that scroll the viewport — used to detect a manual scroll intent. */
|
|
23
|
-
const SCROLL_KEYS = new Set(['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' ', 'Spacebar']);
|
|
13
|
+
import { useEffect, useRef, useState } from 'react';
|
|
24
14
|
function getScrollableAncestor(element) {
|
|
25
15
|
let current = element?.parentElement ?? null;
|
|
26
16
|
while (current != null) {
|
|
@@ -34,41 +24,6 @@ function getScrollableAncestor(element) {
|
|
|
34
24
|
}
|
|
35
25
|
return null;
|
|
36
26
|
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Resolve the active heading id from current scroll position.
|
|
40
|
-
*
|
|
41
|
-
* A heading is "passed" once its top reaches its activation line — the scroll
|
|
42
|
-
* root's top plus the heading's own scroll-margin-top. The active heading is
|
|
43
|
-
* the last passed one (headings are in document order). When none have passed
|
|
44
|
-
* (scrolled above the first), the first item is active; at the bottom, the
|
|
45
|
-
* last item is active.
|
|
46
|
-
*/
|
|
47
|
-
function resolveActiveId(items, scrollRoot) {
|
|
48
|
-
if (items.length === 0) {
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
const rootTop = scrollRoot != null ? scrollRoot.getBoundingClientRect().top : 0;
|
|
52
|
-
const atBottom = scrollRoot != null ? scrollRoot.scrollTop + scrollRoot.clientHeight >= scrollRoot.scrollHeight - 2 : window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2;
|
|
53
|
-
if (atBottom) {
|
|
54
|
-
return items[items.length - 1].id;
|
|
55
|
-
}
|
|
56
|
-
let activeId = items[0].id;
|
|
57
|
-
for (const item of items) {
|
|
58
|
-
const element = document.getElementById(item.id);
|
|
59
|
-
if (element == null) {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
const top = element.getBoundingClientRect().top;
|
|
63
|
-
const marginTop = Number.parseFloat(window.getComputedStyle(element).scrollMarginTop) || 0;
|
|
64
|
-
if (top <= rootTop + marginTop + 1) {
|
|
65
|
-
activeId = item.id;
|
|
66
|
-
} else {
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return activeId;
|
|
71
|
-
}
|
|
72
27
|
export function useScrollSpy({
|
|
73
28
|
activeId,
|
|
74
29
|
items,
|
|
@@ -77,138 +32,72 @@ export function useScrollSpy({
|
|
|
77
32
|
}) {
|
|
78
33
|
const isControlled = activeId !== undefined;
|
|
79
34
|
const [uncontrolledActiveId, setUncontrolledActiveId] = useState(items[0]?.id);
|
|
35
|
+
const visibleHeadingIdsRef = useRef(new Set());
|
|
36
|
+
const headingTopRef = useRef(new Map());
|
|
80
37
|
const activeIdRef = useRef(activeId);
|
|
81
|
-
// While true, scroll-spy ignores scroll updates because a click is driving a
|
|
82
|
-
// programmatic scroll. Released when that scroll settles or the user scrolls.
|
|
83
|
-
const suppressRef = useRef(false);
|
|
84
|
-
const releaseSuppressionRef = useRef(null);
|
|
85
|
-
// Latest scroll-position resolver, so the click handler can resume tracking
|
|
86
|
-
// when the user scrolls during a programmatic scroll.
|
|
87
|
-
const syncRef = useRef(null);
|
|
88
|
-
// Keep latest items/callback in refs so the scroll listener effect doesn't
|
|
89
|
-
// re-subscribe on every render (items is a fresh array each render).
|
|
90
|
-
const itemsRef = useRef(items);
|
|
91
|
-
itemsRef.current = items;
|
|
92
|
-
const onActiveIdChangeRef = useRef(onActiveIdChange);
|
|
93
|
-
onActiveIdChangeRef.current = onActiveIdChange;
|
|
94
38
|
const itemIds = items.map(item => item.id).join('\n');
|
|
95
39
|
activeIdRef.current = isControlled ? activeId : uncontrolledActiveId;
|
|
96
40
|
useEffect(() => {
|
|
97
|
-
if (isControlled || typeof
|
|
41
|
+
if (isControlled || typeof IntersectionObserver === 'undefined') {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const headingElements = items.map(item => document.getElementById(item.id)).filter(element => element != null);
|
|
45
|
+
if (headingElements.length === 0) {
|
|
98
46
|
return;
|
|
99
47
|
}
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
frame = 0;
|
|
105
|
-
if (suppressRef.current) {
|
|
48
|
+
const visibleHeadingIds = visibleHeadingIdsRef.current;
|
|
49
|
+
const headingTop = headingTopRef.current;
|
|
50
|
+
const setNextActiveId = nextActiveId => {
|
|
51
|
+
if (activeIdRef.current === nextActiveId) {
|
|
106
52
|
return;
|
|
107
53
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
setUncontrolledActiveId(nextActiveId);
|
|
112
|
-
onActiveIdChangeRef.current?.(nextActiveId);
|
|
113
|
-
}
|
|
54
|
+
activeIdRef.current = nextActiveId;
|
|
55
|
+
setUncontrolledActiveId(nextActiveId);
|
|
56
|
+
onActiveIdChange?.(nextActiveId);
|
|
114
57
|
};
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
58
|
+
const chooseActiveHeading = () => {
|
|
59
|
+
let nextActiveId;
|
|
60
|
+
let nextTop = Number.POSITIVE_INFINITY;
|
|
61
|
+
for (const id of visibleHeadingIds) {
|
|
62
|
+
const top = headingTop.get(id) ?? Number.POSITIVE_INFINITY;
|
|
63
|
+
if (top < nextTop) {
|
|
64
|
+
nextTop = top;
|
|
65
|
+
nextActiveId = id;
|
|
66
|
+
}
|
|
118
67
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
update();
|
|
122
|
-
scrollTarget.addEventListener('scroll', onScroll, {
|
|
123
|
-
passive: true
|
|
124
|
-
});
|
|
125
|
-
window.addEventListener('resize', onScroll, {
|
|
126
|
-
passive: true
|
|
127
|
-
});
|
|
128
|
-
return () => {
|
|
129
|
-
syncRef.current = null;
|
|
130
|
-
scrollTarget.removeEventListener('scroll', onScroll);
|
|
131
|
-
window.removeEventListener('resize', onScroll);
|
|
132
|
-
if (frame !== 0) {
|
|
133
|
-
cancelAnimationFrame(frame);
|
|
68
|
+
if (nextActiveId != null) {
|
|
69
|
+
setNextActiveId(nextActiveId);
|
|
134
70
|
}
|
|
135
71
|
};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
72
|
+
const observer = new IntersectionObserver(entries => {
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
const id = entry.target.id;
|
|
75
|
+
headingTop.set(id, entry.boundingClientRect.top);
|
|
76
|
+
if (entry.isIntersecting) {
|
|
77
|
+
visibleHeadingIds.add(id);
|
|
78
|
+
} else {
|
|
79
|
+
visibleHeadingIds.delete(id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
chooseActiveHeading();
|
|
83
|
+
}, {
|
|
84
|
+
root: getScrollableAncestor(rootRef.current),
|
|
85
|
+
threshold: 0
|
|
86
|
+
});
|
|
87
|
+
for (const headingElement of headingElements) {
|
|
88
|
+
observer.observe(headingElement);
|
|
89
|
+
}
|
|
140
90
|
return () => {
|
|
141
|
-
|
|
91
|
+
observer.disconnect();
|
|
92
|
+
visibleHeadingIds.clear();
|
|
93
|
+
headingTop.clear();
|
|
142
94
|
};
|
|
143
|
-
}, []);
|
|
95
|
+
}, [isControlled, itemIds, items, onActiveIdChange, rootRef]);
|
|
144
96
|
const setActiveId = nextActiveId => {
|
|
145
97
|
if (!isControlled) {
|
|
146
98
|
setUncontrolledActiveId(nextActiveId);
|
|
147
99
|
}
|
|
148
100
|
onActiveIdChange?.(nextActiveId);
|
|
149
101
|
};
|
|
150
|
-
|
|
151
|
-
if (typeof window === 'undefined') {
|
|
152
|
-
setUncontrolledActiveId(clickedId);
|
|
153
|
-
activeIdRef.current = clickedId;
|
|
154
|
-
onActiveIdChangeRef.current?.(clickedId);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Freeze the indicator during the programmatic smooth scroll instead of
|
|
159
|
-
// moving it immediately — it lands on the clicked item once the scroll
|
|
160
|
-
// settles, so it doesn't chase the scroll through intervening sections.
|
|
161
|
-
suppressRef.current = true;
|
|
162
|
-
// Replace any in-flight handlers from a previous click.
|
|
163
|
-
releaseSuppressionRef.current?.();
|
|
164
|
-
let settleTimer = 0;
|
|
165
|
-
const cleanup = () => {
|
|
166
|
-
window.removeEventListener('scrollend', onSettle);
|
|
167
|
-
window.removeEventListener('wheel', onManual);
|
|
168
|
-
window.removeEventListener('touchmove', onManual);
|
|
169
|
-
window.removeEventListener('keydown', onKeyDown);
|
|
170
|
-
if (settleTimer !== 0) {
|
|
171
|
-
clearTimeout(settleTimer);
|
|
172
|
-
settleTimer = 0;
|
|
173
|
-
}
|
|
174
|
-
releaseSuppressionRef.current = null;
|
|
175
|
-
};
|
|
176
|
-
// Programmatic scroll finished: move the indicator to the clicked item.
|
|
177
|
-
const onSettle = () => {
|
|
178
|
-
cleanup();
|
|
179
|
-
suppressRef.current = false;
|
|
180
|
-
setUncontrolledActiveId(clickedId);
|
|
181
|
-
activeIdRef.current = clickedId;
|
|
182
|
-
onActiveIdChangeRef.current?.(clickedId);
|
|
183
|
-
};
|
|
184
|
-
// User scrolled mid-flight: hand control back to scroll-position tracking.
|
|
185
|
-
const onManual = () => {
|
|
186
|
-
cleanup();
|
|
187
|
-
suppressRef.current = false;
|
|
188
|
-
syncRef.current?.();
|
|
189
|
-
};
|
|
190
|
-
const onKeyDown = event => {
|
|
191
|
-
if (SCROLL_KEYS.has(event.key)) {
|
|
192
|
-
onManual();
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
window.addEventListener('scrollend', onSettle, {
|
|
196
|
-
once: true
|
|
197
|
-
});
|
|
198
|
-
window.addEventListener('wheel', onManual, {
|
|
199
|
-
passive: true
|
|
200
|
-
});
|
|
201
|
-
window.addEventListener('touchmove', onManual, {
|
|
202
|
-
passive: true
|
|
203
|
-
});
|
|
204
|
-
window.addEventListener('keydown', onKeyDown);
|
|
205
|
-
// Fallback when scrollend is unsupported or no scroll is needed.
|
|
206
|
-
settleTimer = window.setTimeout(onSettle, 1200);
|
|
207
|
-
releaseSuppressionRef.current = cleanup;
|
|
208
|
-
}, []);
|
|
209
|
-
return {
|
|
210
|
-
activeId: isControlled ? activeId : uncontrolledActiveId,
|
|
211
|
-
setActiveId,
|
|
212
|
-
lockActiveId
|
|
213
|
-
};
|
|
102
|
+
return [isControlled ? activeId : uncontrolledActiveId, setActiveId];
|
|
214
103
|
}
|
|
@@ -38,15 +38,8 @@ export interface ToggleButtonProps extends BaseProps<HTMLButtonElement> {
|
|
|
38
38
|
*/
|
|
39
39
|
onPressedChange?: (isPressed: boolean) => void;
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* pending — whether it returns a promise or synchronously triggers a
|
|
44
|
-
* suspending update (e.g. a router navigation that suspends on data).
|
|
45
|
-
*
|
|
46
|
-
* Because it runs in a transition, the toggle is *interruptible*: clicking
|
|
47
|
-
* again while an action is pending starts a new transition with the next
|
|
48
|
-
* optimistic state, so the action reflects the latest intent rather than
|
|
49
|
-
* being dropped.
|
|
41
|
+
* Async action handler for API-backed toggles.
|
|
42
|
+
* The button shows a loading spinner while the promise is pending.
|
|
50
43
|
*
|
|
51
44
|
* @example
|
|
52
45
|
* ```
|
|
@@ -60,7 +53,7 @@ export interface ToggleButtonProps extends BaseProps<HTMLButtonElement> {
|
|
|
60
53
|
* />
|
|
61
54
|
* ```
|
|
62
55
|
*/
|
|
63
|
-
pressedChangeAction?: (isPressed: boolean) =>
|
|
56
|
+
pressedChangeAction?: (isPressed: boolean) => Promise<void>;
|
|
64
57
|
/**
|
|
65
58
|
* The size of the toggle button.
|
|
66
59
|
* When used inside ToggleButtonGroup, defaults to the group's size.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToggleButton.d.ts","sourceRoot":"","sources":["../../src/ToggleButton/ToggleButton.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"ToggleButton.d.ts","sourceRoot":"","sources":["../../src/ToggleButton/ToggleButton.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,EAAc,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAIzD,OAAO,EAAS,KAAK,UAAU,EAAC,MAAM,WAAW,CAAC;AAElD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA0C5C,MAAM,WAAW,iBAAkB,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IACrE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IAE/C;;;;;;;;;;;;;;;OAeG;IACH,mBAAmB,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5D;;;;OAIG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAElB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IAEjB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,SAAS,CAAC;IAExB;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,GAAG,EACH,KAAK,EACL,SAAS,EAAE,aAAa,EACxB,eAAe,EAAE,mBAAmB,EACpC,mBAAmB,EACnB,IAAI,EAAE,QAAQ,EACd,UAAU,EAAE,cAAsB,EAClC,SAAiB,EACjB,IAAI,EACJ,UAAkB,EAClB,WAAW,EACX,QAAQ,EACR,OAAO,EACP,KAAK,EACL,MAAM,EACN,SAAS,EAAE,UAAU,EACrB,KAAK,EACL,GAAG,KAAK,EACT,EAAE,iBAAiB,GAAG,SAAS,CAuF/B;yBA1Ge,YAAY"}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - /apps/storybook/stories/ToggleButton.stories.tsx
|
|
20
20
|
* - /packages/cli/templates/blocks/components/ToggleButton/ (showcase blocks)
|
|
21
21
|
*/
|
|
22
|
-
import React, { useCallback
|
|
22
|
+
import React, { useCallback } from 'react';
|
|
23
23
|
import * as stylex from '@stylexjs/stylex';
|
|
24
24
|
import "../theme/tokens.stylex.js";
|
|
25
25
|
import { colorVars, fontWeightVars } from "../theme/tokens.stylex.js";
|
|
@@ -27,38 +27,6 @@ import { Button } from "../Button/index.js";
|
|
|
27
27
|
import { useToggleButtonGroup } from "./ToggleButtonGroup.js";
|
|
28
28
|
import { themeProps } from "../utils/themeProps.js";
|
|
29
29
|
|
|
30
|
-
// =============================================================================
|
|
31
|
-
// Constants & helpers
|
|
32
|
-
// =============================================================================
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* The spinner only appears once the action has been pending for this long.
|
|
36
|
-
* A fast action shows the optimistic pressed state immediately with no spinner
|
|
37
|
-
* flash, and rapid re-clicks can interrupt the in-flight action before the
|
|
38
|
-
* button locks behind the spinner.
|
|
39
|
-
*/
|
|
40
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
41
|
-
const PENDING_SPINNER_DELAY_MS = 150;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Returns `true` only once `active` has stayed `true` for `delayMs`.
|
|
45
|
-
* Used to debounce the loading spinner so the optimistic state shows first.
|
|
46
|
-
*/
|
|
47
|
-
function useDelayed(active, delayMs) {
|
|
48
|
-
const [delayed, setDelayed] = useState(false);
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
if (!active) {
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
const timer = setTimeout(() => setDelayed(true), delayMs);
|
|
54
|
-
return () => {
|
|
55
|
-
clearTimeout(timer);
|
|
56
|
-
setDelayed(false);
|
|
57
|
-
};
|
|
58
|
-
}, [active, delayMs]);
|
|
59
|
-
return active && delayed;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
30
|
// =============================================================================
|
|
63
31
|
// Styles
|
|
64
32
|
// =============================================================================
|
|
@@ -68,6 +36,7 @@ function useDelayed(active, delayMs) {
|
|
|
68
36
|
* A hidden span renders the same text at semibold weight to reserve
|
|
69
37
|
* the wider width, preventing layout shift when toggling.
|
|
70
38
|
*/
|
|
39
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
71
40
|
const pressedStyles = {
|
|
72
41
|
background: {
|
|
73
42
|
kWkggS: "xi89dp7",
|
|
@@ -124,47 +93,32 @@ export function ToggleButton({
|
|
|
124
93
|
style,
|
|
125
94
|
...props
|
|
126
95
|
}) {
|
|
96
|
+
// Read group context if inside a group
|
|
127
97
|
const group = useToggleButtonGroup();
|
|
128
|
-
|
|
98
|
+
|
|
99
|
+
// Resolve state from group or props
|
|
100
|
+
const isPressed = group && value != null ? group.selectedValues.has(value) : isPressedProp ?? false;
|
|
129
101
|
const size = sizeProp ?? group?.size ?? 'md';
|
|
130
102
|
const isDisabled = group?.isDisabled ?? isDisabledProp;
|
|
131
|
-
|
|
132
|
-
// Track the pressed state optimistically. While an action is pending, the
|
|
133
|
-
// button reflects the intended (optimistic) state immediately, and a click
|
|
134
|
-
// mid-flight derives its next state from this value — so rapid toggles read
|
|
135
|
-
// true -> false -> true rather than stalling on the last committed value.
|
|
136
|
-
const [optimisticPressed, setOptimisticPressed] = useOptimistic(committedPressed);
|
|
137
|
-
const isPressed = optimisticPressed;
|
|
138
103
|
const resolvedIcon = isPressed && pressedIcon ? pressedIcon : icon;
|
|
139
|
-
|
|
140
|
-
// Run the toggle inside a transition. The action is interruptible: clicking
|
|
141
|
-
// again while it is pending starts a fresh transition with the next
|
|
142
|
-
// optimistic state instead of being dropped, so there is no re-entry guard.
|
|
143
|
-
// Both onPressedChange and pressedChangeAction run inside the transition,
|
|
144
|
-
// which means a synchronous-but-suspending handler (e.g. a router navigation
|
|
145
|
-
// that suspends on data) also drives the pending state — not just promises.
|
|
146
|
-
const [isPending, startTransition] = useTransition();
|
|
147
|
-
// Debounce the spinner so a fast action shows the optimistic state without a
|
|
148
|
-
// spinner flash, and rapid re-clicks can interrupt before the button locks.
|
|
149
|
-
const showSpinner = useDelayed(isPending, PENDING_SPINNER_DELAY_MS);
|
|
150
|
-
const isLoadingState = isLoading || showSpinner;
|
|
151
104
|
const handleClick = useCallback(() => {
|
|
152
|
-
if (isDisabled) {
|
|
105
|
+
if (isDisabled || isLoading) {
|
|
153
106
|
return;
|
|
154
107
|
}
|
|
155
108
|
if (group && value != null) {
|
|
156
|
-
//
|
|
109
|
+
// Delegate to group context
|
|
157
110
|
group.toggle(value);
|
|
158
|
-
|
|
111
|
+
} else if (onPressedChangeProp) {
|
|
112
|
+
// Standalone toggle
|
|
113
|
+
const newState = !isPressed;
|
|
114
|
+
onPressedChangeProp(newState);
|
|
115
|
+
if (pressedChangeAction) {
|
|
116
|
+
void pressedChangeAction(newState);
|
|
117
|
+
}
|
|
159
118
|
}
|
|
160
|
-
|
|
161
|
-
startTransition(async () => {
|
|
162
|
-
setOptimisticPressed(newState);
|
|
163
|
-
onPressedChangeProp?.(newState);
|
|
164
|
-
await pressedChangeAction?.(newState);
|
|
165
|
-
});
|
|
166
|
-
}, [isDisabled, group, value, optimisticPressed, onPressedChangeProp, pressedChangeAction, setOptimisticPressed]);
|
|
119
|
+
}, [isDisabled, isLoading, group, value, onPressedChangeProp, pressedChangeAction, isPressed]);
|
|
167
120
|
|
|
121
|
+
// Label with font weight shift and width reservation
|
|
168
122
|
// isIconOnly prop is the source of truth for icon-only rendering.
|
|
169
123
|
const labelContent = children != null ? /*#__PURE__*/_jsxs("span", {
|
|
170
124
|
...{
|
|
@@ -211,7 +165,7 @@ export function ToggleButton({
|
|
|
211
165
|
variant: "ghost",
|
|
212
166
|
size: size,
|
|
213
167
|
isDisabled: isDisabled,
|
|
214
|
-
isLoading:
|
|
168
|
+
isLoading: isLoading,
|
|
215
169
|
isIconOnly: isIconOnly,
|
|
216
170
|
"aria-pressed": isPressed,
|
|
217
171
|
icon: resolvedIcon,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dateParser.d.ts","sourceRoot":"","sources":["../../src/utils/dateParser.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,OAAO,EAAC,KAAK,SAAS,EAAqC,MAAM,aAAa,CAAC;AAE/E,OAAO,EACL,gBAAgB,IAAI,QAAQ,EAC5B,cAAc,IAAI,SAAS,GAC5B,MAAM,aAAa,CAAC;AAErB;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAK1C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"dateParser.d.ts","sourceRoot":"","sources":["../../src/utils/dateParser.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,OAAO,EAAC,KAAK,SAAS,EAAqC,MAAM,aAAa,CAAC;AAE/E,OAAO,EACL,gBAAgB,IAAI,QAAQ,EAC5B,cAAc,IAAI,SAAS,GAC5B,MAAM,aAAa,CAAC;AAErB;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAK1C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAwF9D"}
|
package/dist/utils/dateParser.js
CHANGED
|
@@ -112,23 +112,10 @@ export function parseDateInput(input) {
|
|
|
112
112
|
return parseNumericDate(+first, +second, currentYear);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
// 6. Fall back to native Date parsing for other formats
|
|
116
|
-
//
|
|
117
|
-
// Skip bare numeric input (e.g. "0", "1", "01", "2026"). These are
|
|
118
|
-
// in-progress values a user is still typing, not complete dates. Native
|
|
119
|
-
// `Date` parsing coerces them into arbitrary dates ("0" -> year 2000 in V8,
|
|
120
|
-
// year 0 in some engines), which is both surprising and — when the year
|
|
121
|
-
// resolves to 0 — produces an out-of-range date that throws downstream.
|
|
122
|
-
// Treat them as not-yet-a-valid-date instead.
|
|
123
|
-
if (/^\d+$/.test(trimmed)) {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
115
|
+
// 6. Fall back to native Date parsing for other formats
|
|
126
116
|
const parsed = new Date(trimmed);
|
|
127
117
|
if (!isNaN(parsed.getTime())) {
|
|
128
|
-
|
|
129
|
-
// Validate the result so we never return an out-of-range date (e.g. a
|
|
130
|
-
// year of 0), which would throw when later re-parsed.
|
|
131
|
-
return tryCreatePlainDate(fromDate.year, fromDate.month, fromDate.day);
|
|
118
|
+
return plainDateFromDate(parsed);
|
|
132
119
|
}
|
|
133
120
|
return null;
|
|
134
121
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.1.0-canary.
|
|
3
|
+
"version": "0.1.0-canary.e457dac",
|
|
4
4
|
"displayName": "XDS Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"url": "https://github.com/facebook/astryx/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
|
-
"
|
|
18
|
+
"xds",
|
|
19
19
|
"design-system",
|
|
20
20
|
"react",
|
|
21
21
|
"components",
|
|
@@ -90,12 +90,6 @@ const styles = stylex.create({
|
|
|
90
90
|
whiteSpace: 'nowrap',
|
|
91
91
|
paddingInline: spacingVars['--spacing-2'],
|
|
92
92
|
},
|
|
93
|
-
// When a label is shown, the icon sits on the leading edge and the text on
|
|
94
|
-
// the trailing edge. Symmetric padding leaves the text cramped against the
|
|
95
|
-
// pill's rounded edge, so give the trailing side extra breathing room.
|
|
96
|
-
buttonWithLabel: {
|
|
97
|
-
paddingInlineEnd: spacingVars['--spacing-3'],
|
|
98
|
-
},
|
|
99
93
|
});
|
|
100
94
|
|
|
101
95
|
// =============================================================================
|
|
@@ -136,7 +130,7 @@ export function ChatLayoutScrollButton({
|
|
|
136
130
|
variant="ghost"
|
|
137
131
|
size="md"
|
|
138
132
|
onClick={onClick}
|
|
139
|
-
xstyle={
|
|
133
|
+
xstyle={styles.button}>
|
|
140
134
|
{label ?? undefined}
|
|
141
135
|
</Button>
|
|
142
136
|
</div>
|