@astra-kit/ui 0.0.0-stage → 0.0.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/CHANGELOG.md +15 -0
- package/LICENSE.md +21 -0
- package/README.md +384 -2
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/assets/fonts/geist-LICENSE.txt +93 -0
- package/assets/fonts/geist-latin-variable.woff2 +0 -0
- package/assets/fonts/ibm-plex-mono-latin-400.woff2 +0 -0
- package/assets/fonts/ibm-plex-mono-latin-500.woff2 +0 -0
- package/assets/fonts/ibmplexmono-LICENSE.txt +93 -0
- package/assets/fonts/inter-LICENSE.txt +93 -0
- package/assets/fonts/inter-latin-variable.woff2 +0 -0
- package/dist/adapters/unpic-image.d.ts +3 -0
- package/dist/adapters/unpic-image.js +98 -0
- package/dist/components/account-menu.d.ts +67 -0
- package/dist/components/account-menu.js +440 -0
- package/dist/components/action-button.d.ts +25 -0
- package/dist/components/action-button.js +264 -0
- package/dist/components/alert.d.ts +31 -0
- package/dist/components/alert.js +503 -0
- package/dist/components/animated-collapse.d.ts +19 -0
- package/dist/components/animated-collapse.js +170 -0
- package/dist/components/animated-count.d.ts +13 -0
- package/dist/components/animated-count.js +105 -0
- package/dist/components/animated-list-item.d.ts +25 -0
- package/dist/components/animated-list-item.js +134 -0
- package/dist/components/animated-theme-toggler.d.ts +12 -0
- package/dist/components/animated-theme-toggler.js +200 -0
- package/dist/components/avatar.d.ts +65 -0
- package/dist/components/avatar.js +463 -0
- package/dist/components/badge.d.ts +21 -0
- package/dist/components/badge.js +297 -0
- package/dist/components/button-interaction.d.ts +23 -0
- package/dist/components/button-interaction.js +22 -0
- package/dist/components/button-label.d.ts +8 -0
- package/dist/components/button-label.js +203 -0
- package/dist/components/button.d.ts +23 -0
- package/dist/components/button.js +145 -0
- package/dist/components/calendar.d.ts +47 -0
- package/dist/components/calendar.js +155 -0
- package/dist/components/card.d.ts +38 -0
- package/dist/components/card.js +904 -0
- package/dist/components/carousel.d.ts +71 -0
- package/dist/components/carousel.js +585 -0
- package/dist/components/checkbox.d.ts +15 -0
- package/dist/components/checkbox.js +324 -0
- package/dist/components/control-button.d.ts +16 -0
- package/dist/components/control-button.js +215 -0
- package/dist/components/data-table.d.ts +118 -0
- package/dist/components/data-table.js +895 -0
- package/dist/components/dialog-surface.d.ts +49 -0
- package/dist/components/dialog-surface.js +220 -0
- package/dist/components/dialog.d.ts +92 -0
- package/dist/components/dialog.js +320 -0
- package/dist/components/dropdown-menu.d.ts +57 -0
- package/dist/components/dropdown-menu.js +107 -0
- package/dist/components/field.d.ts +53 -0
- package/dist/components/field.js +710 -0
- package/dist/components/file-upload.d.ts +88 -0
- package/dist/components/file-upload.js +776 -0
- package/dist/components/filter-presets-bar.d.ts +73 -0
- package/dist/components/filter-presets-bar.js +510 -0
- package/dist/components/filter-query-bar.d.ts +21 -0
- package/dist/components/filter-query-bar.js +881 -0
- package/dist/components/filter-query-grammar.d.ts +188 -0
- package/dist/components/filter-query-grammar.js +875 -0
- package/dist/components/form.d.ts +29 -0
- package/dist/components/form.js +330 -0
- package/dist/components/highlighter.d.ts +15 -0
- package/dist/components/highlighter.js +152 -0
- package/dist/components/hover-card.d.ts +50 -0
- package/dist/components/hover-card.js +80 -0
- package/dist/components/input-group.d.ts +16 -0
- package/dist/components/input-group.js +277 -0
- package/dist/components/input.d.ts +17 -0
- package/dist/components/input.js +120 -0
- package/dist/components/label.d.ts +4 -0
- package/dist/components/label.js +43 -0
- package/dist/components/link.d.ts +9 -0
- package/dist/components/link.js +43 -0
- package/dist/components/morph-text.d.ts +14 -0
- package/dist/components/morph-text.js +210 -0
- package/dist/components/pagination.d.ts +59 -0
- package/dist/components/pagination.js +190 -0
- package/dist/components/popover.d.ts +17 -0
- package/dist/components/popover.js +40 -0
- package/dist/components/presence-swap.d.ts +22 -0
- package/dist/components/presence-swap.js +209 -0
- package/dist/components/primitives/alert-dialog.d.ts +68 -0
- package/dist/components/primitives/alert-dialog.js +629 -0
- package/dist/components/primitives/avatar.d.ts +46 -0
- package/dist/components/primitives/avatar.js +265 -0
- package/dist/components/primitives/calendar.d.ts +37 -0
- package/dist/components/primitives/calendar.js +1250 -0
- package/dist/components/primitives/carousel.d.ts +106 -0
- package/dist/components/primitives/carousel.js +928 -0
- package/dist/components/primitives/dialog.d.ts +62 -0
- package/dist/components/primitives/dialog.js +490 -0
- package/dist/components/primitives/dropdown-menu-items.d.ts +59 -0
- package/dist/components/primitives/dropdown-menu-items.js +265 -0
- package/dist/components/primitives/dropdown-menu.d.ts +64 -0
- package/dist/components/primitives/dropdown-menu.js +560 -0
- package/dist/components/primitives/hover-card.d.ts +50 -0
- package/dist/components/primitives/hover-card.js +507 -0
- package/dist/components/primitives/pagination.d.ts +65 -0
- package/dist/components/primitives/pagination.js +373 -0
- package/dist/components/primitives/popover.d.ts +22 -0
- package/dist/components/primitives/popover.js +213 -0
- package/dist/components/primitives/select.d.ts +37 -0
- package/dist/components/primitives/select.js +428 -0
- package/dist/components/primitives/tooltip.d.ts +30 -0
- package/dist/components/primitives/tooltip.js +277 -0
- package/dist/components/progress-ring.d.ts +20 -0
- package/dist/components/progress-ring.js +53 -0
- package/dist/components/radio-group.d.ts +34 -0
- package/dist/components/radio-group.js +392 -0
- package/dist/components/reaction-button.d.ts +30 -0
- package/dist/components/reaction-button.js +189 -0
- package/dist/components/rich-link-editor.d.ts +25 -0
- package/dist/components/rich-link-editor.js +320 -0
- package/dist/components/rich-markdown-content.d.ts +10 -0
- package/dist/components/rich-markdown-content.js +56 -0
- package/dist/components/rich-text-content.d.ts +19 -0
- package/dist/components/rich-text-content.js +26 -0
- package/dist/components/select.d.ts +55 -0
- package/dist/components/select.js +188 -0
- package/dist/components/separator.d.ts +3 -0
- package/dist/components/separator.js +48 -0
- package/dist/components/sheet.d.ts +19 -0
- package/dist/components/sheet.js +420 -0
- package/dist/components/sidebar-panel-context.d.ts +17 -0
- package/dist/components/sidebar-panel-context.js +14 -0
- package/dist/components/sidebar.d.ts +195 -0
- package/dist/components/sidebar.js +2041 -0
- package/dist/components/skeleton.d.ts +23 -0
- package/dist/components/skeleton.js +334 -0
- package/dist/components/split-button.d.ts +36 -0
- package/dist/components/split-button.js +306 -0
- package/dist/components/svgs/whatsappIcon.d.ts +3 -0
- package/dist/components/svgs/whatsappIcon.js +14 -0
- package/dist/components/switch.d.ts +13 -0
- package/dist/components/switch.js +444 -0
- package/dist/components/table.d.ts +10 -0
- package/dist/components/table.js +298 -0
- package/dist/components/tabs.d.ts +36 -0
- package/dist/components/tabs.js +636 -0
- package/dist/components/textarea.d.ts +16 -0
- package/dist/components/textarea.js +111 -0
- package/dist/components/themed-toaster.d.ts +7 -0
- package/dist/components/themed-toaster.js +86 -0
- package/dist/components/toaster.d.ts +12 -0
- package/dist/components/toaster.js +62 -0
- package/dist/components/tooltip.d.ts +43 -0
- package/dist/components/tooltip.js +65 -0
- package/dist/components/typing-animation.d.ts +34 -0
- package/dist/components/typing-animation.js +389 -0
- package/dist/constants/action-button.d.ts +20 -0
- package/dist/constants/action-button.js +17 -0
- package/dist/constants/alert.d.ts +16 -0
- package/dist/constants/alert.js +13 -0
- package/dist/constants/avatar.d.ts +38 -0
- package/dist/constants/avatar.js +32 -0
- package/dist/constants/badge.d.ts +6 -0
- package/dist/constants/badge.js +6 -0
- package/dist/constants/button.d.ts +16 -0
- package/dist/constants/button.js +16 -0
- package/dist/constants/calendar.d.ts +36 -0
- package/dist/constants/calendar.js +34 -0
- package/dist/constants/card.d.ts +23 -0
- package/dist/constants/card.js +20 -0
- package/dist/constants/carousel.d.ts +55 -0
- package/dist/constants/carousel.js +74 -0
- package/dist/constants/checkbox.d.ts +17 -0
- package/dist/constants/checkbox.js +17 -0
- package/dist/constants/dialog.d.ts +11 -0
- package/dist/constants/dialog.js +10 -0
- package/dist/constants/dropdown-menu.d.ts +10 -0
- package/dist/constants/dropdown-menu.js +37 -0
- package/dist/constants/field.d.ts +24 -0
- package/dist/constants/field.js +24 -0
- package/dist/constants/hover-card.d.ts +37 -0
- package/dist/constants/hover-card.js +85 -0
- package/dist/constants/morph-text.d.ts +13 -0
- package/dist/constants/morph-text.js +13 -0
- package/dist/constants/motion.d.ts +12 -0
- package/dist/constants/motion.js +12 -0
- package/dist/constants/pagination.d.ts +20 -0
- package/dist/constants/pagination.js +20 -0
- package/dist/constants/popover.d.ts +7 -0
- package/dist/constants/popover.js +33 -0
- package/dist/constants/radio-group.d.ts +10 -0
- package/dist/constants/radio-group.js +10 -0
- package/dist/constants/select.d.ts +7 -0
- package/dist/constants/select.js +34 -0
- package/dist/constants/sidebar.d.ts +9 -0
- package/dist/constants/sidebar.js +17 -0
- package/dist/constants/skeleton.d.ts +17 -0
- package/dist/constants/skeleton.js +17 -0
- package/dist/constants/split-button.d.ts +7 -0
- package/dist/constants/split-button.js +5 -0
- package/dist/constants/switch.d.ts +33 -0
- package/dist/constants/switch.js +41 -0
- package/dist/constants/tabs.d.ts +17 -0
- package/dist/constants/tabs.js +17 -0
- package/dist/constants/toast.d.ts +3 -0
- package/dist/constants/toast.js +3 -0
- package/dist/constants/tooltip.d.ts +13 -0
- package/dist/constants/tooltip.js +52 -0
- package/dist/hooks/use-horizontal-swipe.d.ts +23 -0
- package/dist/hooks/use-horizontal-swipe.js +80 -0
- package/dist/hooks/use-is-hydrated.d.ts +6 -0
- package/dist/hooks/use-is-hydrated.js +20 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-mobile.js +19 -0
- package/dist/hooks/use-prefers-reduced-motion.d.ts +2 -0
- package/dist/hooks/use-prefers-reduced-motion.js +27 -0
- package/dist/hooks/use-rich-link-editor.d.ts +55 -0
- package/dist/hooks/use-rich-link-editor.js +515 -0
- package/dist/hooks/use-sidebar-persistence.d.ts +6 -0
- package/dist/hooks/use-sidebar-persistence.js +52 -0
- package/dist/hooks/use-today.d.ts +7 -0
- package/dist/hooks/use-today.js +50 -0
- package/dist/hooks/use-viewer-time-zone.d.ts +6 -0
- package/dist/hooks/use-viewer-time-zone.js +20 -0
- package/dist/hooks.d.ts +11 -0
- package/dist/hooks.js +11 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +69 -0
- package/dist/lib/browser-clipboard.d.ts +13 -0
- package/dist/lib/browser-clipboard.js +48 -0
- package/dist/lib/column-filter-value.d.ts +12 -0
- package/dist/lib/column-filter-value.js +52 -0
- package/dist/lib/file-acceptance.d.ts +43 -0
- package/dist/lib/file-acceptance.js +107 -0
- package/dist/lib/format-file-size.d.ts +16 -0
- package/dist/lib/format-file-size.js +34 -0
- package/dist/lib/fuzzy-search.d.ts +17 -0
- package/dist/lib/fuzzy-search.js +183 -0
- package/dist/lib/horizontal-swipe.d.ts +23 -0
- package/dist/lib/horizontal-swipe.js +29 -0
- package/dist/lib/image-dimensions.d.ts +2 -0
- package/dist/lib/image-dimensions.js +7 -0
- package/dist/lib/name-initials.d.ts +7 -0
- package/dist/lib/name-initials.js +11 -0
- package/dist/lib/rich-text/link-markdown-constants.d.ts +147 -0
- package/dist/lib/rich-text/link-markdown-constants.js +458 -0
- package/dist/lib/rich-text/link-markdown-types.d.ts +53 -0
- package/dist/lib/rich-text/link-markdown-types.js +1 -0
- package/dist/lib/rich-text/link-markdown.d.ts +193 -0
- package/dist/lib/rich-text/link-markdown.js +592 -0
- package/dist/lib/rich-text/rich-markdown.d.ts +41 -0
- package/dist/lib/rich-text/rich-markdown.js +105 -0
- package/dist/lib/rich-text/rich-text-editor-dom.d.ts +26 -0
- package/dist/lib/rich-text/rich-text-editor-dom.js +79 -0
- package/dist/lib/toast-layer.d.ts +24 -0
- package/dist/lib/toast-layer.js +76 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +2 -0
- package/dist/lib/year-month-filter.d.ts +16 -0
- package/dist/lib/year-month-filter.js +46 -0
- package/dist/motion/arc-floating-slot.d.ts +40 -0
- package/dist/motion/arc-floating-slot.js +177 -0
- package/dist/motion/arc-press.d.ts +6 -0
- package/dist/motion/arc-press.js +59 -0
- package/dist/motion/arc-swap.d.ts +22 -0
- package/dist/motion/arc-swap.js +49 -0
- package/dist/motion/arc-tokens.d.ts +53 -0
- package/dist/motion/arc-tokens.js +73 -0
- package/dist/motion/glide-indicator.d.ts +69 -0
- package/dist/motion/glide-indicator.js +309 -0
- package/dist/motion/glide-slot.d.ts +9 -0
- package/dist/motion/glide-slot.js +71 -0
- package/dist/motion/motion-slot.d.ts +12 -0
- package/dist/motion/motion-slot.js +376 -0
- package/dist/motion/overlay-warmth.d.ts +24 -0
- package/dist/motion/overlay-warmth.js +70 -0
- package/dist/motion/presence.d.ts +40 -0
- package/dist/motion/presence.js +101 -0
- package/dist/motion/surface-keyframes.d.ts +24 -0
- package/dist/motion/surface-keyframes.js +141 -0
- package/dist/motion/tokens.d.ts +81 -0
- package/dist/motion/tokens.js +67 -0
- package/dist/motion/use-attached-element.d.ts +7 -0
- package/dist/motion/use-attached-element.js +22 -0
- package/dist/motion/use-morph-width.d.ts +13 -0
- package/dist/motion/use-morph-width.js +59 -0
- package/dist/next.d.ts +14 -0
- package/dist/next.js +79 -0
- package/dist/providers/astra-ui-provider.d.ts +27 -0
- package/dist/providers/astra-ui-provider.js +100 -0
- package/dist/providers/native-provider.d.ts +4 -0
- package/dist/providers/native-provider.js +45 -0
- package/dist/sonner.d.ts +7 -0
- package/dist/sonner.js +8 -0
- package/dist/styles.css +2 -0
- package/dist/tanstack.d.ts +11 -0
- package/dist/tanstack.js +50 -0
- package/dist/theme.d.ts +3 -0
- package/dist/theme.js +1 -0
- package/dist/types/data-table.d.ts +27 -0
- package/dist/types/data-table.js +1 -0
- package/fonts.css +5 -0
- package/package.json +450 -4
- package/styles.css +4 -0
- package/theme.css +248 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { parseRichTextSegments } from "./link-markdown.js";
|
|
2
|
+
import { RICH_TEXT_SEGMENT_TYPE } from "./link-markdown-constants.js";
|
|
3
|
+
/**
|
|
4
|
+
* Block-level markdown subset for long-form content such as descriptions and stories.
|
|
5
|
+
*
|
|
6
|
+
* It builds on top of the shared link parser (`parseRichTextSegments`) without
|
|
7
|
+
* touching it, adding paragraphs, unordered lists (`- item` / `* item`) and
|
|
8
|
+
* inline bold (`**text**`). Everything else stays plain text, so the renderer
|
|
9
|
+
* never interprets raw HTML.
|
|
10
|
+
*/
|
|
11
|
+
export const RICH_MARKDOWN_BLOCK_TYPE = {
|
|
12
|
+
list: "list",
|
|
13
|
+
paragraph: "paragraph"
|
|
14
|
+
};
|
|
15
|
+
export const RICH_MARKDOWN_INLINE_TYPE = {
|
|
16
|
+
bold: "bold",
|
|
17
|
+
link: "link",
|
|
18
|
+
text: "text"
|
|
19
|
+
};
|
|
20
|
+
const LINE_BREAK_PATTERN = /\r?\n/;
|
|
21
|
+
const LIST_ITEM_PATTERN = /^\s*[-*]\s+(.*)$/;
|
|
22
|
+
const BOLD_PATTERN = /\*\*([^*]+)\*\*/g;
|
|
23
|
+
const PARAGRAPH_LINE_SEPARATOR = "\n";
|
|
24
|
+
function parseInlineLinks(text) {
|
|
25
|
+
return parseRichTextSegments(text).map((segment) => segment.type === RICH_TEXT_SEGMENT_TYPE.link ? {
|
|
26
|
+
text: segment.text,
|
|
27
|
+
type: RICH_MARKDOWN_INLINE_TYPE.link,
|
|
28
|
+
url: segment.url
|
|
29
|
+
} : {
|
|
30
|
+
text: segment.text,
|
|
31
|
+
type: RICH_MARKDOWN_INLINE_TYPE.text
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function parseInlineSegments(text) {
|
|
35
|
+
const segments = [];
|
|
36
|
+
let lastIndex = 0;
|
|
37
|
+
for (const boldMatch of text.matchAll(BOLD_PATTERN)) {
|
|
38
|
+
const matchIndex = boldMatch.index ?? 0;
|
|
39
|
+
if (matchIndex > lastIndex) {
|
|
40
|
+
segments.push(...parseInlineLinks(text.slice(lastIndex, matchIndex)));
|
|
41
|
+
}
|
|
42
|
+
segments.push({
|
|
43
|
+
text: boldMatch[1],
|
|
44
|
+
type: RICH_MARKDOWN_INLINE_TYPE.bold
|
|
45
|
+
});
|
|
46
|
+
lastIndex = matchIndex + boldMatch[0].length;
|
|
47
|
+
}
|
|
48
|
+
if (lastIndex < text.length) {
|
|
49
|
+
segments.push(...parseInlineLinks(text.slice(lastIndex)));
|
|
50
|
+
}
|
|
51
|
+
return segments;
|
|
52
|
+
}
|
|
53
|
+
const EXCERPT_SEGMENT_SEPARATOR = " ";
|
|
54
|
+
const EXCERPT_ELLIPSIS = "…";
|
|
55
|
+
/**
|
|
56
|
+
* Flattens the parsed content into plain text (formatting stripped) and trims it
|
|
57
|
+
* to `maxLength`, for SEO descriptions and previews.
|
|
58
|
+
*/
|
|
59
|
+
export function buildRichMarkdownExcerpt(content, maxLength) {
|
|
60
|
+
const plainText = parseRichMarkdownBlocks(content).flatMap((block) => block.type === RICH_MARKDOWN_BLOCK_TYPE.paragraph ? block.segments.map((segment) => segment.text) : block.items.flatMap((itemSegments) => itemSegments.map((segment) => segment.text))).join(EXCERPT_SEGMENT_SEPARATOR).replace(/\s+/g, EXCERPT_SEGMENT_SEPARATOR).trim();
|
|
61
|
+
if (plainText.length <= maxLength) {
|
|
62
|
+
return plainText;
|
|
63
|
+
}
|
|
64
|
+
return plainText.slice(0, maxLength - EXCERPT_ELLIPSIS.length).trimEnd() + EXCERPT_ELLIPSIS;
|
|
65
|
+
}
|
|
66
|
+
export function parseRichMarkdownBlocks(content) {
|
|
67
|
+
const blocks = [];
|
|
68
|
+
let paragraphLines = [];
|
|
69
|
+
let listItems = [];
|
|
70
|
+
const flushParagraph = () => {
|
|
71
|
+
if (paragraphLines.length > 0) {
|
|
72
|
+
blocks.push({
|
|
73
|
+
segments: parseInlineSegments(paragraphLines.join(PARAGRAPH_LINE_SEPARATOR)),
|
|
74
|
+
type: RICH_MARKDOWN_BLOCK_TYPE.paragraph
|
|
75
|
+
});
|
|
76
|
+
paragraphLines = [];
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const flushList = () => {
|
|
80
|
+
if (listItems.length > 0) {
|
|
81
|
+
blocks.push({
|
|
82
|
+
items: listItems,
|
|
83
|
+
type: RICH_MARKDOWN_BLOCK_TYPE.list
|
|
84
|
+
});
|
|
85
|
+
listItems = [];
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
for (const line of content.split(LINE_BREAK_PATTERN)) {
|
|
89
|
+
const listItemMatch = LIST_ITEM_PATTERN.exec(line);
|
|
90
|
+
if (listItemMatch) {
|
|
91
|
+
flushParagraph();
|
|
92
|
+
listItems.push(parseInlineSegments(listItemMatch[1]));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
flushList();
|
|
96
|
+
if (line.trim().length === 0) {
|
|
97
|
+
flushParagraph();
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
paragraphLines.push(line);
|
|
101
|
+
}
|
|
102
|
+
flushParagraph();
|
|
103
|
+
flushList();
|
|
104
|
+
return blocks;
|
|
105
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM helpers that translate between the rich link editor's plain-text offsets
|
|
3
|
+
* and the browser `Selection`/`Range` API. Kept apart from the pure helpers so
|
|
4
|
+
* the latter can be unit-tested without a DOM.
|
|
5
|
+
*/
|
|
6
|
+
import type { RichTextSelectionRange } from "./link-markdown-types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Returns the plain-text character offset within `editor` for a DOM
|
|
9
|
+
* `(container, offset)` boundary, or `null` when it cannot be resolved.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getEditorBoundaryOffset(editor: HTMLDivElement, container: Node, offset: number): number | null;
|
|
12
|
+
/**
|
|
13
|
+
* Reads the current selection as a plain-text offset range within `editor`, or
|
|
14
|
+
* `null` when there is no selection inside the editor.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getEditorSelectionRange(editor: HTMLDivElement): RichTextSelectionRange | null;
|
|
17
|
+
/**
|
|
18
|
+
* Resolves a plain-text offset within `editor` to a DOM `(node, offset)`
|
|
19
|
+
* boundary, walking its text nodes.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getEditorTextBoundary(editor: HTMLDivElement, targetOffset: number): {
|
|
22
|
+
node: Node;
|
|
23
|
+
offset: number;
|
|
24
|
+
};
|
|
25
|
+
/** Restores a plain-text offset range as the live selection inside `editor`. */
|
|
26
|
+
export declare function setEditorSelectionRange(editor: HTMLDivElement, selectionRange: RichTextSelectionRange): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the plain-text character offset within `editor` for a DOM
|
|
3
|
+
* `(container, offset)` boundary, or `null` when it cannot be resolved.
|
|
4
|
+
*/
|
|
5
|
+
export function getEditorBoundaryOffset(editor, container, offset) {
|
|
6
|
+
const boundaryRange = document.createRange();
|
|
7
|
+
boundaryRange.selectNodeContents(editor);
|
|
8
|
+
try {
|
|
9
|
+
boundaryRange.setEnd(container, offset);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return boundaryRange.toString().length;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reads the current selection as a plain-text offset range within `editor`, or
|
|
17
|
+
* `null` when there is no selection inside the editor.
|
|
18
|
+
*/
|
|
19
|
+
export function getEditorSelectionRange(editor) {
|
|
20
|
+
const selection = window.getSelection();
|
|
21
|
+
if (!selection || selection.rangeCount === 0) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const anchorNode = selection.anchorNode;
|
|
25
|
+
const focusNode = selection.focusNode;
|
|
26
|
+
if (!anchorNode || !focusNode) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (!editor.contains(anchorNode) || !editor.contains(focusNode)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const anchorOffset = getEditorBoundaryOffset(editor, anchorNode, selection.anchorOffset);
|
|
33
|
+
const focusOffset = getEditorBoundaryOffset(editor, focusNode, selection.focusOffset);
|
|
34
|
+
if (anchorOffset === null || focusOffset === null) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
end: Math.max(anchorOffset, focusOffset),
|
|
39
|
+
start: Math.min(anchorOffset, focusOffset)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolves a plain-text offset within `editor` to a DOM `(node, offset)`
|
|
44
|
+
* boundary, walking its text nodes.
|
|
45
|
+
*/
|
|
46
|
+
export function getEditorTextBoundary(editor, targetOffset) {
|
|
47
|
+
const textNodeWalker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
|
|
48
|
+
let remainingOffset = targetOffset;
|
|
49
|
+
let currentNode = textNodeWalker.nextNode();
|
|
50
|
+
while (currentNode) {
|
|
51
|
+
const currentTextLength = currentNode.textContent?.length ?? 0;
|
|
52
|
+
if (remainingOffset <= currentTextLength) {
|
|
53
|
+
return {
|
|
54
|
+
node: currentNode,
|
|
55
|
+
offset: remainingOffset
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
remainingOffset -= currentTextLength;
|
|
59
|
+
currentNode = textNodeWalker.nextNode();
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
node: editor,
|
|
63
|
+
offset: editor.childNodes.length
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Restores a plain-text offset range as the live selection inside `editor`. */
|
|
67
|
+
export function setEditorSelectionRange(editor, selectionRange) {
|
|
68
|
+
const selection = window.getSelection();
|
|
69
|
+
if (!selection) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const editorRange = document.createRange();
|
|
73
|
+
const startBoundary = getEditorTextBoundary(editor, selectionRange.start);
|
|
74
|
+
const endBoundary = getEditorTextBoundary(editor, selectionRange.end);
|
|
75
|
+
editorRange.setStart(startBoundary.node, startBoundary.offset);
|
|
76
|
+
editorRange.setEnd(endBoundary.node, endBoundary.offset);
|
|
77
|
+
selection.removeAllRanges();
|
|
78
|
+
selection.addRange(editorRange);
|
|
79
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tells whether an event target belongs to the toast layer.
|
|
3
|
+
* @param target - Target of the outside interaction.
|
|
4
|
+
* @returns `true` when the target is a toast or one of its controls.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isToastLayerTarget(target: EventTarget | null): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Lets keyboard focus enter the toast layer while a modal traps focus. Radix's focus trap listens for `focusin` and
|
|
9
|
+
* `focusout` on the document and pulls focus back into the modal whenever it lands outside, so Sonner's hotkey could
|
|
10
|
+
* never move focus to a toast's controls. Focus changes that land on a toast stop at the body, after React has run
|
|
11
|
+
* its own handlers and before the document-level trap sees them; every other focus change still reaches the trap.
|
|
12
|
+
* Because the trap never sees focus leave, it cannot pull it back either: when the focused toast control is removed
|
|
13
|
+
* (an action or close button dismisses its toast), focus returns to the element it came from instead of the body.
|
|
14
|
+
* @param ownerDocument - Document whose body hosts the app and the modal portals.
|
|
15
|
+
* @returns Removes the listeners.
|
|
16
|
+
*/
|
|
17
|
+
export declare function letFocusReachToastLayer(ownerDocument: Document): () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Wraps a modal's `onInteractOutside` so presses on toasts never dismiss it. The consumer's handler still runs first
|
|
20
|
+
* and can keep preventing dismissal for its own reasons.
|
|
21
|
+
* @param handler - The consumer's `onInteractOutside`, if any.
|
|
22
|
+
* @returns The handler to give the Radix content.
|
|
23
|
+
*/
|
|
24
|
+
export declare function keepOpenOnToastInteraction<OutsideEvent extends Event>(handler?: (event: OutsideEvent) => void): (event: OutsideEvent) => void;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps toasts usable while a modal overlay is open. Toasts sit above every overlay, but a modal `Dialog` or `Sheet`
|
|
3
|
+
* treats a press on them as a press outside and closes, so a toast's action or close button would dismiss the form
|
|
4
|
+
* that raised it instead of running.
|
|
5
|
+
*/
|
|
6
|
+
import { TOAST_LAYER_SELECTOR } from "../constants/toast.js";
|
|
7
|
+
/**
|
|
8
|
+
* Tells whether an event target belongs to the toast layer.
|
|
9
|
+
* @param target - Target of the outside interaction.
|
|
10
|
+
* @returns `true` when the target is a toast or one of its controls.
|
|
11
|
+
*/
|
|
12
|
+
export function isToastLayerTarget(target) {
|
|
13
|
+
return target instanceof Element && target.closest(TOAST_LAYER_SELECTOR) !== null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Lets keyboard focus enter the toast layer while a modal traps focus. Radix's focus trap listens for `focusin` and
|
|
17
|
+
* `focusout` on the document and pulls focus back into the modal whenever it lands outside, so Sonner's hotkey could
|
|
18
|
+
* never move focus to a toast's controls. Focus changes that land on a toast stop at the body, after React has run
|
|
19
|
+
* its own handlers and before the document-level trap sees them; every other focus change still reaches the trap.
|
|
20
|
+
* Because the trap never sees focus leave, it cannot pull it back either: when the focused toast control is removed
|
|
21
|
+
* (an action or close button dismisses its toast), focus returns to the element it came from instead of the body.
|
|
22
|
+
* @param ownerDocument - Document whose body hosts the app and the modal portals.
|
|
23
|
+
* @returns Removes the listeners.
|
|
24
|
+
*/
|
|
25
|
+
export function letFocusReachToastLayer(ownerDocument) {
|
|
26
|
+
const { body } = ownerDocument;
|
|
27
|
+
let focusOrigin = null;
|
|
28
|
+
let focusedToastElement = null;
|
|
29
|
+
const restoreFocusAfterRemoval = new MutationObserver(() => {
|
|
30
|
+
if (focusedToastElement?.isConnected !== false) return;
|
|
31
|
+
const activeElement = ownerDocument.activeElement;
|
|
32
|
+
if (activeElement === null || activeElement === body) focusOrigin?.focus({ preventScroll: true });
|
|
33
|
+
stopTrackingToastFocus();
|
|
34
|
+
});
|
|
35
|
+
const stopTrackingToastFocus = () => {
|
|
36
|
+
restoreFocusAfterRemoval.disconnect();
|
|
37
|
+
focusOrigin = null;
|
|
38
|
+
focusedToastElement = null;
|
|
39
|
+
};
|
|
40
|
+
const stopFocusIntoToast = (event) => {
|
|
41
|
+
const focusedElement = event.type === "focusin" ? event.target : event.relatedTarget;
|
|
42
|
+
if (!isToastLayerTarget(focusedElement)) {
|
|
43
|
+
if (event.type === "focusin") stopTrackingToastFocus();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
event.stopPropagation();
|
|
47
|
+
if (event.type !== "focusin") return;
|
|
48
|
+
if (focusedToastElement === null) {
|
|
49
|
+
focusOrigin = event.relatedTarget instanceof HTMLElement ? event.relatedTarget : null;
|
|
50
|
+
restoreFocusAfterRemoval.observe(body, {
|
|
51
|
+
childList: true,
|
|
52
|
+
subtree: true
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
focusedToastElement = focusedElement;
|
|
56
|
+
};
|
|
57
|
+
body.addEventListener("focusin", stopFocusIntoToast);
|
|
58
|
+
body.addEventListener("focusout", stopFocusIntoToast);
|
|
59
|
+
return () => {
|
|
60
|
+
stopTrackingToastFocus();
|
|
61
|
+
body.removeEventListener("focusin", stopFocusIntoToast);
|
|
62
|
+
body.removeEventListener("focusout", stopFocusIntoToast);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Wraps a modal's `onInteractOutside` so presses on toasts never dismiss it. The consumer's handler still runs first
|
|
67
|
+
* and can keep preventing dismissal for its own reasons.
|
|
68
|
+
* @param handler - The consumer's `onInteractOutside`, if any.
|
|
69
|
+
* @returns The handler to give the Radix content.
|
|
70
|
+
*/
|
|
71
|
+
export function keepOpenOnToastInteraction(handler) {
|
|
72
|
+
return (event) => {
|
|
73
|
+
handler?.(event);
|
|
74
|
+
if (isToastLayerTarget(event.target)) event.preventDefault();
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsea un texto `MM/AAAA` a su valor numérico comparable `AAAAMM`
|
|
3
|
+
* (por ejemplo `06/2026` → `202606`). Devuelve `null` cuando el texto no es
|
|
4
|
+
* un mes-año válido.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseYearMonthFilterInput(value: string): number | null;
|
|
7
|
+
/**
|
|
8
|
+
* Formatea un valor numérico `AAAAMM` de vuelta a `MM/AAAA` para rehidratar
|
|
9
|
+
* los inputs del filtro al reabrir el diálogo.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Evalúa un filtro de rango de año-mes contra el valor numérico `AAAAMM` de una
|
|
13
|
+
* fila (`null` cuando la fila no tiene fecha). Devuelve `true` cuando el filtro
|
|
14
|
+
* no aplica, replicando el contrato de los demás matchers de la tabla.
|
|
15
|
+
*/
|
|
16
|
+
export declare function matchesAdvancedYearMonthRangeFilter(columnFilterValue: unknown, value: number | null): boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const YEAR_MONTH_INPUT_PATTERN = /^(0[1-9]|1[0-2])\/(\d{4})$/;
|
|
2
|
+
/**
|
|
3
|
+
* Parsea un texto `MM/AAAA` a su valor numérico comparable `AAAAMM`
|
|
4
|
+
* (por ejemplo `06/2026` → `202606`). Devuelve `null` cuando el texto no es
|
|
5
|
+
* un mes-año válido.
|
|
6
|
+
*/
|
|
7
|
+
export function parseYearMonthFilterInput(value) {
|
|
8
|
+
const match = YEAR_MONTH_INPUT_PATTERN.exec(value.trim());
|
|
9
|
+
if (!match) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const [, month, year] = match;
|
|
13
|
+
return Number(`${year}${month}`);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Formatea un valor numérico `AAAAMM` de vuelta a `MM/AAAA` para rehidratar
|
|
17
|
+
* los inputs del filtro al reabrir el diálogo.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Evalúa un filtro de rango de año-mes contra el valor numérico `AAAAMM` de una
|
|
21
|
+
* fila (`null` cuando la fila no tiene fecha). Devuelve `true` cuando el filtro
|
|
22
|
+
* no aplica, replicando el contrato de los demás matchers de la tabla.
|
|
23
|
+
*/
|
|
24
|
+
export function matchesAdvancedYearMonthRangeFilter(columnFilterValue, value) {
|
|
25
|
+
if (!columnFilterValue || typeof columnFilterValue !== "object" || columnFilterValue.kind !== "yearMonthRange") {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const filterValue = columnFilterValue;
|
|
29
|
+
const hasValue = value != null && Number.isFinite(value);
|
|
30
|
+
if (filterValue.mode === "hasValue") {
|
|
31
|
+
return hasValue;
|
|
32
|
+
}
|
|
33
|
+
if (filterValue.mode === "noValue") {
|
|
34
|
+
return !hasValue;
|
|
35
|
+
}
|
|
36
|
+
if (!hasValue) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (filterValue.min != null && value < filterValue.min) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (filterValue.max != null && value > filterValue.max) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry and exit playback of Arc's floating layers (https://uiarc.dev, MIT License, see THIRD_PARTY_NOTICES.md):
|
|
3
|
+
* tooltips, popovers and hover cards start a few pixels toward their trigger, slightly scaled down, and settle in
|
|
4
|
+
* place. Arc drives them with CSS transitions so a close that interrupts an open, or a reopen during a close, turns
|
|
5
|
+
* around from where the layer is; this slot keeps that property with Motion: an interrupted playback continues from
|
|
6
|
+
* the values it had reached instead of jumping to rest first. It animates the primitive's own node through a Slot,
|
|
7
|
+
* without adding DOM wrappers, and holds the node for `AnimatePresence` until its exit finishes.
|
|
8
|
+
*/
|
|
9
|
+
import { type ComponentProps, type ReactElement } from "react";
|
|
10
|
+
import { Slot } from "radix-ui";
|
|
11
|
+
import { type AnimationOptions } from "motion/react";
|
|
12
|
+
/** A visual state of the floating layer, relative to its resting place. */
|
|
13
|
+
export interface ArcFloatingPose {
|
|
14
|
+
/** Pixels the layer sits toward its trigger, along the side it was placed on. */
|
|
15
|
+
travel: number;
|
|
16
|
+
scale: number;
|
|
17
|
+
opacity: number;
|
|
18
|
+
}
|
|
19
|
+
/** The two transitions of one playback: the transform may spring while the fade stays a short tween. */
|
|
20
|
+
export interface ArcFloatingTransitions {
|
|
21
|
+
transform: AnimationOptions;
|
|
22
|
+
opacity: AnimationOptions;
|
|
23
|
+
}
|
|
24
|
+
/** Entry and exit of a floating layer. The entry always ends at rest: no travel, full scale and full opacity. */
|
|
25
|
+
export interface ArcFloatingMotion {
|
|
26
|
+
enterFrom: ArcFloatingPose;
|
|
27
|
+
enter: ArcFloatingTransitions;
|
|
28
|
+
exitTo: ArcFloatingPose;
|
|
29
|
+
exit: ArcFloatingTransitions;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Animates the floating layer rendered as its only child.
|
|
33
|
+
* @param props.motion - Returns the entry and exit of the layer. It is read once, when the layer mounts, so a layer
|
|
34
|
+
* revived mid-exit keeps the path it came in on.
|
|
35
|
+
* @returns The child with the slot's ref composed into it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function ArcFloatingSlot({ children, motion, ref, ...props }: Omit<ComponentProps<typeof Slot.Root>, "children"> & {
|
|
38
|
+
children: ReactElement;
|
|
39
|
+
motion: () => ArcFloatingMotion;
|
|
40
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* Entry and exit playback of Arc's floating layers (https://uiarc.dev, MIT License, see THIRD_PARTY_NOTICES.md):
|
|
4
|
+
* tooltips, popovers and hover cards start a few pixels toward their trigger, slightly scaled down, and settle in
|
|
5
|
+
* place. Arc drives them with CSS transitions so a close that interrupts an open, or a reopen during a close, turns
|
|
6
|
+
* around from where the layer is; this slot keeps that property with Motion: an interrupted playback continues from
|
|
7
|
+
* the values it had reached instead of jumping to rest first. It animates the primitive's own node through a Slot,
|
|
8
|
+
* without adding DOM wrappers, and holds the node for `AnimatePresence` until its exit finishes.
|
|
9
|
+
*/
|
|
10
|
+
import { useLayoutEffect, useRef } from "react";
|
|
11
|
+
import { Slot } from "radix-ui";
|
|
12
|
+
import { animate, frame, usePresence } from "motion/react";
|
|
13
|
+
import { usePrefersReducedMotion } from "../hooks/use-prefers-reduced-motion.js";
|
|
14
|
+
import { MILLISECONDS_PER_SECOND, MOTION_ENTERING_ATTRIBUTE } from "../constants/motion.js";
|
|
15
|
+
import { ARC_DURATION, ARC_EASE } from "./arc-tokens.js";
|
|
16
|
+
import { useAttachedElement } from "./use-attached-element.js";
|
|
17
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
+
const ANIMATED_PROPERTIES = ["opacity", "transform"];
|
|
19
|
+
const REST_POSE = {
|
|
20
|
+
travel: 0,
|
|
21
|
+
scale: 1,
|
|
22
|
+
opacity: 1
|
|
23
|
+
};
|
|
24
|
+
/** Reduced motion keeps an entering layer in place and only fades it in, on Arc's instant timing. */
|
|
25
|
+
const REDUCED_TWEEN = {
|
|
26
|
+
duration: ARC_DURATION.instant,
|
|
27
|
+
ease: [...ARC_EASE.standard]
|
|
28
|
+
};
|
|
29
|
+
/** Grace, in ms, past the planned exit before the layer is removed even if its playback never reports completion. */
|
|
30
|
+
const EXIT_DEADLINE_GRACE_MS = 100;
|
|
31
|
+
/**
|
|
32
|
+
* Reads how long a transition plays, in ms: a spring's `visualDuration` (which overrides `duration`) or a tween's `duration`.
|
|
33
|
+
* @param transition - Motion transition options.
|
|
34
|
+
* @returns The planned duration, or Arc's standard duration when the transition does not state one.
|
|
35
|
+
*/
|
|
36
|
+
function plannedDurationMs({ duration, visualDuration }) {
|
|
37
|
+
return (visualDuration ?? duration ?? ARC_DURATION.standard) * MILLISECONDS_PER_SECOND;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Builds the transform of a pose. Radix writes the placed side on `data-side`; the offset points toward the trigger,
|
|
41
|
+
* so a layer flipped above its trigger still grows from the trigger edge.
|
|
42
|
+
*/
|
|
43
|
+
function poseTransform(element, { travel, scale }) {
|
|
44
|
+
const side = element.dataset.side;
|
|
45
|
+
const horizontalOffset = side === "left" ? travel : side === "right" ? -travel : 0;
|
|
46
|
+
const verticalOffset = side === "top" ? travel : side === "bottom" ? -travel : 0;
|
|
47
|
+
return `translate(${horizontalOffset}px, ${verticalOffset}px) scale(${scale})`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Animates the floating layer rendered as its only child.
|
|
51
|
+
* @param props.motion - Returns the entry and exit of the layer. It is read once, when the layer mounts, so a layer
|
|
52
|
+
* revived mid-exit keeps the path it came in on.
|
|
53
|
+
* @returns The child with the slot's ref composed into it.
|
|
54
|
+
*/
|
|
55
|
+
export function ArcFloatingSlot({ children, motion, ref, ...props }) {
|
|
56
|
+
const [element, attach] = useAttachedElement(ref);
|
|
57
|
+
const reduceMotion = usePrefersReducedMotion();
|
|
58
|
+
const [isPresent, safeToRemove] = usePresence();
|
|
59
|
+
// AnimatePresence hands out a new `safeToRemove` on every render; a ref keeps a re-render from restarting an exit.
|
|
60
|
+
const safeToRemoveRef = useRef(safeToRemove);
|
|
61
|
+
const resolveMotionRef = useRef(motion);
|
|
62
|
+
useLayoutEffect(() => {
|
|
63
|
+
safeToRemoveRef.current = safeToRemove;
|
|
64
|
+
resolveMotionRef.current = motion;
|
|
65
|
+
});
|
|
66
|
+
/** The motion chosen when this node mounted. */
|
|
67
|
+
const plannedRef = useRef(null);
|
|
68
|
+
/** Values an interrupted playback had reached, so the next one continues from them. */
|
|
69
|
+
const interruptedRef = useRef(null);
|
|
70
|
+
useLayoutEffect(() => {
|
|
71
|
+
let active = true;
|
|
72
|
+
// Some primitives keep children mounted in a detached portal while closed; they must not hold an exit open.
|
|
73
|
+
if (!element || !element.isConnected) {
|
|
74
|
+
if (!isPresent) queueMicrotask(() => {
|
|
75
|
+
if (active) safeToRemoveRef.current?.();
|
|
76
|
+
});
|
|
77
|
+
return () => {
|
|
78
|
+
active = false;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Reduced motion removes a leaving layer at once, like `MotionSlot`: a fade would keep the modal layer hiding the
|
|
82
|
+
// rest of the page from assistive technology until it ends.
|
|
83
|
+
if (reduceMotion && !isPresent) {
|
|
84
|
+
queueMicrotask(() => {
|
|
85
|
+
if (active) safeToRemoveRef.current?.();
|
|
86
|
+
});
|
|
87
|
+
return () => {
|
|
88
|
+
active = false;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const isFirstEntry = plannedRef.current === null;
|
|
92
|
+
plannedRef.current ??= resolveMotionRef.current();
|
|
93
|
+
const planned = plannedRef.current;
|
|
94
|
+
const original = {
|
|
95
|
+
opacity: element.style.opacity,
|
|
96
|
+
transform: element.style.transform
|
|
97
|
+
};
|
|
98
|
+
const restore = () => {
|
|
99
|
+
ANIMATED_PROPERTIES.forEach((property) => {
|
|
100
|
+
element.style[property] = original[property];
|
|
101
|
+
});
|
|
102
|
+
element.removeAttribute(MOTION_ENTERING_ATTRIBUTE);
|
|
103
|
+
};
|
|
104
|
+
const interrupted = interruptedRef.current;
|
|
105
|
+
interruptedRef.current = null;
|
|
106
|
+
const startsFromEntryPose = isPresent && isFirstEntry && !interrupted;
|
|
107
|
+
const from = {
|
|
108
|
+
opacity: startsFromEntryPose ? planned.enterFrom.opacity : interrupted?.opacity ? Number.parseFloat(interrupted.opacity) : REST_POSE.opacity,
|
|
109
|
+
transform: startsFromEntryPose ? reduceMotion ? "none" : poseTransform(element, planned.enterFrom) : interrupted?.transform ?? "none"
|
|
110
|
+
};
|
|
111
|
+
const targetPose = isPresent ? REST_POSE : planned.exitTo;
|
|
112
|
+
const to = {
|
|
113
|
+
opacity: targetPose.opacity,
|
|
114
|
+
transform: reduceMotion ? "none" : poseTransform(element, targetPose)
|
|
115
|
+
};
|
|
116
|
+
const transitions = reduceMotion ? {
|
|
117
|
+
transform: REDUCED_TWEEN,
|
|
118
|
+
opacity: REDUCED_TWEEN
|
|
119
|
+
} : isPresent ? planned.enter : planned.exit;
|
|
120
|
+
// Motion starts on its next frame; painting the first keyframe now keeps that frame from flashing the rest state.
|
|
121
|
+
element.style.opacity = String(from.opacity);
|
|
122
|
+
element.style.transform = from.transform;
|
|
123
|
+
// A transform makes the layer the containing block of fixed descendants, so they stay hidden until it settles.
|
|
124
|
+
if (isPresent) element.setAttribute(MOTION_ENTERING_ATTRIBUTE, "");
|
|
125
|
+
const animations = [animate(element, { opacity: [from.opacity, to.opacity] }, transitions.opacity), ...from.transform === to.transform ? [] : [animate(element, { transform: [from.transform, to.transform] }, transitions.transform)]];
|
|
126
|
+
/*
|
|
127
|
+
* Playback reports completion from Motion's frame loop, which runs on animation frames. When frames stop (a hidden
|
|
128
|
+
* tab, or a test whose fake clock swallowed the pending frame), a leaving layer would stay mounted forever, so
|
|
129
|
+
* its removal also has a deadline on a plain timer, shortly after the planned exit.
|
|
130
|
+
*/
|
|
131
|
+
let removed = false;
|
|
132
|
+
const removeLeavingLayer = () => {
|
|
133
|
+
if (removed || !active) return;
|
|
134
|
+
removed = true;
|
|
135
|
+
safeToRemoveRef.current?.();
|
|
136
|
+
};
|
|
137
|
+
const exitDeadline = isPresent ? undefined : setTimeout(removeLeavingLayer, Math.max(plannedDurationMs(transitions.opacity), plannedDurationMs(transitions.transform)) + EXIT_DEADLINE_GRACE_MS);
|
|
138
|
+
void Promise.all(animations).then(() => {
|
|
139
|
+
if (!active) return;
|
|
140
|
+
if (!isPresent) {
|
|
141
|
+
removeLeavingLayer();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Motion commits the final keyframe inline; giving the caller's styles back leaves no compositing layer.
|
|
145
|
+
frame.postRender(() => {
|
|
146
|
+
if (active) restore();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
return () => {
|
|
150
|
+
active = false;
|
|
151
|
+
clearTimeout(exitDeadline);
|
|
152
|
+
const computed = getComputedStyle(element);
|
|
153
|
+
const isMidPlayback = element.hasAttribute(MOTION_ENTERING_ATTRIBUTE) || !isPresent;
|
|
154
|
+
// A settled entrance needs no snapshot: the next playback starts from rest.
|
|
155
|
+
interruptedRef.current = isMidPlayback ? {
|
|
156
|
+
opacity: computed.opacity,
|
|
157
|
+
transform: computed.transform
|
|
158
|
+
} : null;
|
|
159
|
+
animations.forEach((animation) => animation.cancel());
|
|
160
|
+
restore();
|
|
161
|
+
};
|
|
162
|
+
}, [
|
|
163
|
+
element,
|
|
164
|
+
isPresent,
|
|
165
|
+
reduceMotion
|
|
166
|
+
]);
|
|
167
|
+
return /* @__PURE__ */ _jsx(Slot.Root, {
|
|
168
|
+
ref: attach,
|
|
169
|
+
...props,
|
|
170
|
+
// A leaving layer can be neither clicked, focused nor announced while it fades out.
|
|
171
|
+
...isPresent ? {} : {
|
|
172
|
+
inert: true,
|
|
173
|
+
"aria-hidden": true
|
|
174
|
+
},
|
|
175
|
+
children
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scales the element down while pressed and springs it back on release.
|
|
3
|
+
* @param element - Rendered button, link or Slot child; nothing happens until it is attached.
|
|
4
|
+
* @param isEnabled - False skips the feedback, for reduced motion or variants that should not press.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useArcPress(element: HTMLElement | null, isEnabled: boolean): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { c as _c } from "react/compiler-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Press feedback adapted from Arc (https://uiarc.dev), Copyright (c) 2026 Elia Kuratli, MIT License.
|
|
5
|
+
* The full license text ships in THIRD_PARTY_NOTICES.md. Works on whatever node a Slot or Base UI
|
|
6
|
+
* renders, so `asChild` links press exactly like native buttons.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect } from "react";
|
|
9
|
+
import { animate, press } from "motion/react";
|
|
10
|
+
import { BUTTON_DEFAULT_PRESS_SCALE, BUTTON_ICON_PRESS_SCALE, BUTTON_ICON_THRESHOLD_PX, BUTTON_WIDE_PRESS_SCALE, BUTTON_WIDE_THRESHOLD_PX } from "../constants/button.js";
|
|
11
|
+
import { ARC_DURATION, ARC_EASE, ARC_SPRING_SNAPPY } from "./arc-tokens.js";
|
|
12
|
+
/** Matches triggers of menus, popovers and dialogs, which never scale: Radix measures them on pointerdown. */
|
|
13
|
+
const LAYER_ANCHOR_SELECTOR = "[aria-haspopup]:not([aria-haspopup=\"false\"]), [data-state], [role=\"combobox\"]";
|
|
14
|
+
/** Matches elements that ignore presses: disabled, busy or marked unavailable. */
|
|
15
|
+
const INERT_SELECTOR = ":disabled, [data-disabled], [aria-disabled=\"true\"], [aria-busy=\"true\"]";
|
|
16
|
+
/** Icon buttons press a little deeper and wide buttons a little less, so every size reads as the same push. */
|
|
17
|
+
function getPressScale(width) {
|
|
18
|
+
if (width > BUTTON_WIDE_THRESHOLD_PX) return BUTTON_WIDE_PRESS_SCALE;
|
|
19
|
+
if (width && width <= BUTTON_ICON_THRESHOLD_PX) return BUTTON_ICON_PRESS_SCALE;
|
|
20
|
+
return BUTTON_DEFAULT_PRESS_SCALE;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Scales the element down while pressed and springs it back on release.
|
|
24
|
+
* @param element - Rendered button, link or Slot child; nothing happens until it is attached.
|
|
25
|
+
* @param isEnabled - False skips the feedback, for reduced motion or variants that should not press.
|
|
26
|
+
*/
|
|
27
|
+
export function useArcPress(element, isEnabled) {
|
|
28
|
+
const $ = _c(4);
|
|
29
|
+
let t0;
|
|
30
|
+
let t1;
|
|
31
|
+
if ($[0] !== element || $[1] !== isEnabled) {
|
|
32
|
+
t0 = () => {
|
|
33
|
+
if (!element || !isEnabled) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
return press(element, () => {
|
|
37
|
+
if (element.matches(INERT_SELECTOR) || element.matches(LAYER_ANCHOR_SELECTOR)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
animate(element, { scale: getPressScale(element.offsetWidth) }, {
|
|
41
|
+
duration: ARC_DURATION.instant,
|
|
42
|
+
ease: [...ARC_EASE.standard]
|
|
43
|
+
});
|
|
44
|
+
return () => {
|
|
45
|
+
animate(element, { scale: 1 }, ARC_SPRING_SNAPPY);
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
t1 = [element, isEnabled];
|
|
50
|
+
$[0] = element;
|
|
51
|
+
$[1] = isEnabled;
|
|
52
|
+
$[2] = t0;
|
|
53
|
+
$[3] = t1;
|
|
54
|
+
} else {
|
|
55
|
+
t0 = $[2];
|
|
56
|
+
t1 = $[3];
|
|
57
|
+
}
|
|
58
|
+
useEffect(t0, t1);
|
|
59
|
+
}
|