@seekalfred/primitives 0.6.0 → 0.7.1
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/README.md +19 -11
- package/dist/activity-heatmap/activity-heatmap.d.ts +92 -0
- package/dist/activity-heatmap/activity-heatmap.js +699 -0
- package/dist/activity-heatmap/activity-heatmap.js.map +1 -0
- package/dist/activity-heatmap/index.d.ts +2 -0
- package/dist/activity-heatmap/index.js +2 -0
- package/dist/alert/alert.d.ts +5 -1
- package/dist/alert/alert.js +68 -7
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert-dialog/alert-dialog.d.ts +45 -18
- package/dist/alert-dialog/alert-dialog.js +128 -9
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/alert-dialog/index.d.ts +2 -2
- package/dist/alert-dialog/index.js +2 -2
- package/dist/animated-counter/animated-counter.d.ts +38 -0
- package/dist/animated-counter/animated-counter.js +230 -0
- package/dist/animated-counter/animated-counter.js.map +1 -0
- package/dist/animated-counter/index.d.ts +2 -0
- package/dist/animated-counter/index.js +2 -0
- package/dist/announcement-bar/announcement-bar.d.ts +67 -0
- package/dist/announcement-bar/announcement-bar.js +364 -0
- package/dist/announcement-bar/announcement-bar.js.map +1 -0
- package/dist/announcement-bar/index.d.ts +2 -0
- package/dist/announcement-bar/index.js +2 -0
- package/dist/badge/badge.d.ts +1 -1
- package/dist/bar-chart/bar-chart.d.ts +51 -0
- package/dist/bar-chart/bar-chart.js +565 -0
- package/dist/bar-chart/bar-chart.js.map +1 -0
- package/dist/bar-chart/index.d.ts +2 -0
- package/dist/bar-chart/index.js +2 -0
- package/dist/breadcrumb/breadcrumb.d.ts +2 -2
- package/dist/breadcrumb/breadcrumb.js +12 -3
- package/dist/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/brush-chart/brush-chart.d.ts +64 -0
- package/dist/brush-chart/brush-chart.js +1056 -0
- package/dist/brush-chart/brush-chart.js.map +1 -0
- package/dist/brush-chart/index.d.ts +2 -0
- package/dist/brush-chart/index.js +2 -0
- package/dist/button/button.d.ts +2 -2
- package/dist/button/button.js +31 -21
- package/dist/button/button.js.map +1 -1
- package/dist/button-group/button-group.js +3 -4
- package/dist/button-group/button-group.js.map +1 -1
- package/dist/card/card.js +1 -1
- package/dist/card/card.js.map +1 -1
- package/dist/card-stack/card-stack.d.ts +54 -0
- package/dist/card-stack/card-stack.js +492 -0
- package/dist/card-stack/card-stack.js.map +1 -0
- package/dist/card-stack/index.d.ts +2 -0
- package/dist/card-stack/index.js +2 -0
- package/dist/carousel/carousel.d.ts +56 -0
- package/dist/carousel/carousel.js +389 -0
- package/dist/carousel/carousel.js.map +1 -0
- package/dist/carousel/index.d.ts +2 -0
- package/dist/carousel/index.js +2 -0
- package/dist/checkbox/checkbox.js +4 -4
- package/dist/checkbox/checkbox.js.map +1 -1
- package/dist/chip-group/chip-group.d.ts +37 -0
- package/dist/chip-group/chip-group.js +148 -0
- package/dist/chip-group/chip-group.js.map +1 -0
- package/dist/chip-group/index.d.ts +2 -0
- package/dist/chip-group/index.js +2 -0
- package/dist/countdown/countdown.d.ts +45 -0
- package/dist/countdown/countdown.js +298 -0
- package/dist/countdown/countdown.js.map +1 -0
- package/dist/countdown/index.d.ts +2 -0
- package/dist/countdown/index.js +2 -0
- package/dist/date-picker/picker-parts.js +1 -1
- package/dist/date-picker/picker-parts.js.map +1 -1
- package/dist/dialog/dialog.js +1 -1
- package/dist/dialog/dialog.js.map +1 -1
- package/dist/donut-chart/donut-chart.d.ts +64 -0
- package/dist/donut-chart/donut-chart.js +576 -0
- package/dist/donut-chart/donut-chart.js.map +1 -0
- package/dist/donut-chart/index.d.ts +2 -0
- package/dist/donut-chart/index.js +2 -0
- package/dist/empty-state/empty-state.d.ts +21 -10
- package/dist/empty-state/empty-state.js +32 -26
- package/dist/empty-state/empty-state.js.map +1 -1
- package/dist/empty-state/index.d.ts +2 -2
- package/dist/empty-state/index.js +2 -2
- package/dist/file-drop-zone/file-drop-zone.js +1 -1
- package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
- package/dist/filter-bar/filter-bar.d.ts +47 -26
- package/dist/filter-bar/filter-bar.js +272 -156
- package/dist/filter-bar/filter-bar.js.map +1 -1
- package/dist/gauge/gauge.d.ts +45 -0
- package/dist/gauge/gauge.js +219 -0
- package/dist/gauge/gauge.js.map +1 -0
- package/dist/gauge/index.d.ts +2 -0
- package/dist/gauge/index.js +2 -0
- package/dist/in-view-title/in-view-title.d.ts +33 -0
- package/dist/in-view-title/in-view-title.js +119 -0
- package/dist/in-view-title/in-view-title.js.map +1 -0
- package/dist/in-view-title/index.d.ts +2 -0
- package/dist/in-view-title/index.js +2 -0
- package/dist/index.d.ts +49 -6
- package/dist/index.js +48 -8
- package/dist/lib/cn.js +11 -1
- package/dist/lib/cn.js.map +1 -1
- package/dist/lib/field-message.js +28 -10
- package/dist/lib/field-message.js.map +1 -1
- package/dist/lib/field-surface.js +9 -8
- package/dist/lib/field-surface.js.map +1 -1
- package/dist/lib/flag-emoji.js +50 -0
- package/dist/lib/flag-emoji.js.map +1 -0
- package/dist/lib/flip.js +11 -3
- package/dist/lib/flip.js.map +1 -1
- package/dist/lib/menu-highlight.js +64 -0
- package/dist/lib/menu-highlight.js.map +1 -0
- package/dist/lib/motion.js +9 -2
- package/dist/lib/motion.js.map +1 -1
- package/dist/lib/rolling-value.js +1 -1
- package/dist/lib/rolling-value.js.map +1 -1
- package/dist/lib/spring-tween.js +500 -0
- package/dist/lib/spring-tween.js.map +1 -0
- package/dist/lib/swap-text.js +3 -1
- package/dist/lib/swap-text.js.map +1 -1
- package/dist/line-chart/index.d.ts +2 -0
- package/dist/line-chart/index.js +2 -0
- package/dist/line-chart/line-chart.d.ts +76 -0
- package/dist/line-chart/line-chart.js +611 -0
- package/dist/line-chart/line-chart.js.map +1 -0
- package/dist/mention-input/index.d.ts +2 -0
- package/dist/mention-input/index.js +2 -0
- package/dist/mention-input/mention-input.d.ts +98 -0
- package/dist/mention-input/mention-input.js +576 -0
- package/dist/mention-input/mention-input.js.map +1 -0
- package/dist/menu/menu.d.ts +2 -0
- package/dist/menu/menu.js +23 -12
- package/dist/menu/menu.js.map +1 -1
- package/dist/money-input/index.d.ts +2 -0
- package/dist/money-input/index.js +2 -0
- package/dist/money-input/money-input.d.ts +65 -0
- package/dist/money-input/money-input.js +607 -0
- package/dist/money-input/money-input.js.map +1 -0
- package/dist/morph-select/index.d.ts +2 -0
- package/dist/morph-select/index.js +2 -0
- package/dist/morph-select/morph-select.d.ts +71 -0
- package/dist/morph-select/morph-select.js +693 -0
- package/dist/morph-select/morph-select.js.map +1 -0
- package/dist/multi-select/index.d.ts +2 -0
- package/dist/multi-select/index.js +2 -0
- package/dist/multi-select/multi-select.d.ts +36 -0
- package/dist/multi-select/multi-select.js +152 -0
- package/dist/multi-select/multi-select.js.map +1 -0
- package/dist/number-field/index.d.ts +2 -0
- package/dist/number-field/index.js +2 -0
- package/dist/number-field/number-field.d.ts +42 -0
- package/dist/number-field/number-field.js +199 -0
- package/dist/number-field/number-field.js.map +1 -0
- package/dist/onboarding-checklist/index.d.ts +2 -0
- package/dist/onboarding-checklist/index.js +2 -0
- package/dist/onboarding-checklist/onboarding-checklist.d.ts +55 -0
- package/dist/onboarding-checklist/onboarding-checklist.js +577 -0
- package/dist/onboarding-checklist/onboarding-checklist.js.map +1 -0
- package/dist/password-field/password-field.js +2 -2
- package/dist/password-field/password-field.js.map +1 -1
- package/dist/phone-input/index.d.ts +3 -0
- package/dist/phone-input/index.js +3 -0
- package/dist/phone-input/phone-countries.d.ts +29 -0
- package/dist/phone-input/phone-countries.js +170 -0
- package/dist/phone-input/phone-countries.js.map +1 -0
- package/dist/phone-input/phone-input.d.ts +66 -0
- package/dist/phone-input/phone-input.js +440 -0
- package/dist/phone-input/phone-input.js.map +1 -0
- package/dist/popover/popover.d.ts +12 -0
- package/dist/popover/popover.js +31 -7
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/progress.js +1 -1
- package/dist/radio-group/radio-group.js +1 -1
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/reactions/index.d.ts +2 -0
- package/dist/reactions/index.js +2 -0
- package/dist/reactions/reactions.d.ts +50 -0
- package/dist/reactions/reactions.js +315 -0
- package/dist/reactions/reactions.js.map +1 -0
- package/dist/rich-text-editor/index.d.ts +3 -0
- package/dist/rich-text-editor/index.js +2 -0
- package/dist/rich-text-editor/rich-text-editor.d.ts +70 -0
- package/dist/rich-text-editor/rich-text-editor.js +909 -0
- package/dist/rich-text-editor/rich-text-editor.js.map +1 -0
- package/dist/rich-text-editor/rich-text-engine.d.ts +16 -0
- package/dist/rich-text-editor/rich-text-engine.js +366 -0
- package/dist/rich-text-editor/rich-text-engine.js.map +1 -0
- package/dist/rich-text-editor/rich-text-html.js +174 -0
- package/dist/rich-text-editor/rich-text-html.js.map +1 -0
- package/dist/rich-text-editor/rich-text-icons.js +43 -0
- package/dist/rich-text-editor/rich-text-icons.js.map +1 -0
- package/dist/ridgeline/index.d.ts +2 -0
- package/dist/ridgeline/index.js +2 -0
- package/dist/ridgeline/ridgeline.d.ts +55 -0
- package/dist/ridgeline/ridgeline.js +701 -0
- package/dist/ridgeline/ridgeline.js.map +1 -0
- package/dist/scroll-area/index.d.ts +2 -2
- package/dist/scroll-area/index.js +2 -2
- package/dist/scroll-area/scroll-area.d.ts +68 -31
- package/dist/scroll-area/scroll-area.js +208 -59
- package/dist/scroll-area/scroll-area.js.map +1 -1
- package/dist/scrub-input/evaluate.d.ts +14 -0
- package/dist/scrub-input/evaluate.js +72 -0
- package/dist/scrub-input/evaluate.js.map +1 -0
- package/dist/scrub-input/index.d.ts +3 -0
- package/dist/scrub-input/index.js +3 -0
- package/dist/scrub-input/scrub-input.d.ts +74 -0
- package/dist/scrub-input/scrub-input.js +486 -0
- package/dist/scrub-input/scrub-input.js.map +1 -0
- package/dist/search-field/search-field.d.ts +2 -1
- package/dist/search-field/search-field.js +4 -4
- package/dist/search-field/search-field.js.map +1 -1
- package/dist/select/select.d.ts +3 -1
- package/dist/select/select.js +20 -8
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +1 -1
- package/dist/sheet/sheet.js +2 -1
- package/dist/sheet/sheet.js.map +1 -1
- package/dist/shortcut-recorder/index.d.ts +3 -0
- package/dist/shortcut-recorder/index.js +3 -0
- package/dist/shortcut-recorder/shortcut-model.d.ts +27 -0
- package/dist/shortcut-recorder/shortcut-model.js +206 -0
- package/dist/shortcut-recorder/shortcut-model.js.map +1 -0
- package/dist/shortcut-recorder/shortcut-recorder.d.ts +93 -0
- package/dist/shortcut-recorder/shortcut-recorder.js +489 -0
- package/dist/shortcut-recorder/shortcut-recorder.js.map +1 -0
- package/dist/slider/index.d.ts +2 -2
- package/dist/slider/slider.d.ts +46 -14
- package/dist/slider/slider.js +151 -67
- package/dist/slider/slider.js.map +1 -1
- package/dist/slope-chart/index.d.ts +2 -0
- package/dist/slope-chart/index.js +2 -0
- package/dist/slope-chart/slope-chart.d.ts +54 -0
- package/dist/slope-chart/slope-chart.js +568 -0
- package/dist/slope-chart/slope-chart.js.map +1 -0
- package/dist/sparkline/index.d.ts +2 -0
- package/dist/sparkline/index.js +2 -0
- package/dist/sparkline/sparkline.d.ts +46 -0
- package/dist/sparkline/sparkline.js +390 -0
- package/dist/sparkline/sparkline.js.map +1 -0
- package/dist/split-bar/index.d.ts +2 -0
- package/dist/split-bar/index.js +2 -0
- package/dist/split-bar/split-bar.d.ts +62 -0
- package/dist/split-bar/split-bar.js +513 -0
- package/dist/split-bar/split-bar.js.map +1 -0
- package/dist/split-button/split-button.d.ts +1 -1
- package/dist/split-button/split-button.js +12 -11
- package/dist/split-button/split-button.js.map +1 -1
- package/dist/stat-card/index.d.ts +2 -2
- package/dist/stat-card/index.js +2 -2
- package/dist/stat-card/stat-card.d.ts +24 -4
- package/dist/stat-card/stat-card.js +114 -17
- package/dist/stat-card/stat-card.js.map +1 -1
- package/dist/stepper/index.d.ts +2 -0
- package/dist/stepper/index.js +2 -0
- package/dist/stepper/stepper.d.ts +51 -0
- package/dist/stepper/stepper.js +215 -0
- package/dist/stepper/stepper.js.map +1 -0
- package/dist/streamgraph/index.d.ts +2 -0
- package/dist/streamgraph/index.js +2 -0
- package/dist/streamgraph/streamgraph.d.ts +70 -0
- package/dist/streamgraph/streamgraph.js +613 -0
- package/dist/streamgraph/streamgraph.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/swipe-actions/index.d.ts +2 -0
- package/dist/swipe-actions/index.js +2 -0
- package/dist/swipe-actions/swipe-actions.d.ts +67 -0
- package/dist/swipe-actions/swipe-actions.js +552 -0
- package/dist/swipe-actions/swipe-actions.js.map +1 -0
- package/dist/switch/switch.js +2 -2
- package/dist/switch/switch.js.map +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +2 -2
- package/dist/table/table.d.ts +56 -4
- package/dist/table/table.js +340 -165
- package/dist/table/table.js.map +1 -1
- package/dist/tabs/tabs.js +2 -2
- package/dist/tabs/tabs.js.map +1 -1
- package/dist/tag-input/index.d.ts +2 -0
- package/dist/tag-input/index.js +2 -0
- package/dist/tag-input/tag-input.d.ts +40 -0
- package/dist/tag-input/tag-input.js +269 -0
- package/dist/tag-input/tag-input.js.map +1 -0
- package/dist/text-morph/index.d.ts +2 -0
- package/dist/text-morph/index.js +2 -0
- package/dist/text-morph/text-morph.d.ts +23 -0
- package/dist/text-morph/text-morph.js +21 -0
- package/dist/text-morph/text-morph.js.map +1 -0
- package/dist/text-reveal/index.d.ts +2 -0
- package/dist/text-reveal/index.js +2 -0
- package/dist/text-reveal/text-reveal.d.ts +27 -0
- package/dist/text-reveal/text-reveal.js +51 -0
- package/dist/text-reveal/text-reveal.js.map +1 -0
- package/dist/text-shimmer/index.d.ts +2 -0
- package/dist/text-shimmer/index.js +2 -0
- package/dist/text-shimmer/text-shimmer.d.ts +29 -0
- package/dist/text-shimmer/text-shimmer.js +29 -0
- package/dist/text-shimmer/text-shimmer.js.map +1 -0
- package/dist/time-picker/time-picker.js +2 -2
- package/dist/time-picker/time-picker.js.map +1 -1
- package/dist/toast/index.d.ts +2 -2
- package/dist/toast/index.js +2 -2
- package/dist/toast/toast.d.ts +6 -0
- package/dist/toast/toast.js +82 -40
- package/dist/toast/toast.js.map +1 -1
- package/dist/tooltip/tooltip.d.ts +9 -2
- package/dist/tooltip/tooltip.js +7 -7
- package/dist/tooltip/tooltip.js.map +1 -1
- package/dist/treemap/index.d.ts +2 -0
- package/dist/treemap/index.js +2 -0
- package/dist/treemap/treemap.d.ts +53 -0
- package/dist/treemap/treemap.js +699 -0
- package/dist/treemap/treemap.js.map +1 -0
- package/dist/usage-meter/index.d.ts +2 -0
- package/dist/usage-meter/index.js +2 -0
- package/dist/usage-meter/usage-meter.d.ts +50 -0
- package/dist/usage-meter/usage-meter.js +440 -0
- package/dist/usage-meter/usage-meter.js.map +1 -0
- package/dist/waffle-chart/index.d.ts +2 -0
- package/dist/waffle-chart/index.js +2 -0
- package/dist/waffle-chart/waffle-chart.d.ts +57 -0
- package/dist/waffle-chart/waffle-chart.js +426 -0
- package/dist/waffle-chart/waffle-chart.js.map +1 -0
- package/package.json +165 -3
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn, cnAll } from "../lib/cn.js";
|
|
3
|
+
import { prefersReducedMotion, useExitBackstop, useIsomorphicLayoutEffect } from "../lib/motion.js";
|
|
4
|
+
import { useHeightFollow } from "../lib/height-follow.js";
|
|
5
|
+
import { fieldSurfaceVariants } from "../lib/field-surface.js";
|
|
6
|
+
import { useLeavingList } from "../lib/leaving-list.js";
|
|
7
|
+
import { FieldShell, useFieldWiring } from "../lib/field-shell.js";
|
|
8
|
+
import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from "react";
|
|
9
|
+
import { Button, Group, Input, Tag, TagGroup, TagList } from "react-aria-components";
|
|
10
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
//#region src/tag-input/tag-input.tsx
|
|
12
|
+
/** A no-break space, appended to a repeated announcement so a screen reader says it again. */
|
|
13
|
+
const NBSP = String.fromCharCode(160);
|
|
14
|
+
/** The longest a leaving tag's collapse takes, plus a margin, for the backstop that removes a tag whose animation never ends. */
|
|
15
|
+
const COLLAPSE_BACKSTOP = 900;
|
|
16
|
+
const shellClass = cnAll(fieldSurfaceVariants(), "flex min-h-(--density-control-h-md) cursor-text flex-wrap items-center gap-1.5 p-1.75", "has-[input:focus]:not-data-[invalid]:border-foreground", "forced-colors:has-[input:focus]:outline-solid forced-colors:has-[input:focus]:outline-(length:--focus-ring-width) forced-colors:has-[input:focus]:outline-offset-(--focus-ring-offset)");
|
|
17
|
+
const tagClass = "relative inline-flex min-h-7 flex-none items-center gap-0.5 rounded-pill border-(length:--border-width) border-border bg-muted py-0 ps-3 pe-0.5 font-sans text-sm leading-(--leading-normal) text-foreground outline-none [interpolate-size:allow-keywords] data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width) data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring) data-[pulse]:motion-safe:animate-tag-pulse";
|
|
18
|
+
const removeClass = "grid size-6 flex-none cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-text-secondary outline-none motion-safe:transition-[background-color,color,scale] motion-safe:duration-(--duration-quick) motion-safe:ease-settle data-[hovered]:bg-surface data-[hovered]:text-foreground data-[pressed]:scale-95 motion-safe:data-[pressed]:duration-(--duration-instant) data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width) data-[focus-visible]:outline-(--color-ring) data-[disabled]:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:size-3.5";
|
|
19
|
+
const TagInput = forwardRef(function TagInput({ label, value, defaultValue = [], onValueChange, placeholder = "Add a tag", description, error, disabled = false, name, id, className }, forwardedRef) {
|
|
20
|
+
const [internal, setInternal] = useState(defaultValue);
|
|
21
|
+
const tags = value ?? internal;
|
|
22
|
+
const [draft, setDraft] = useState("");
|
|
23
|
+
const [notice, setNotice] = useState("");
|
|
24
|
+
const [initial] = useState(() => new Set(tags));
|
|
25
|
+
const [ready, setReady] = useState(false);
|
|
26
|
+
useIsomorphicLayoutEffect(() => {
|
|
27
|
+
const frame = requestAnimationFrame(() => setReady(true));
|
|
28
|
+
return () => cancelAnimationFrame(frame);
|
|
29
|
+
}, []);
|
|
30
|
+
const inputRef = useRef(null);
|
|
31
|
+
useImperativeHandle(forwardedRef, () => inputRef.current);
|
|
32
|
+
const shellRef = useRef(null);
|
|
33
|
+
const frame = useRef(null);
|
|
34
|
+
const field = useFieldWiring({
|
|
35
|
+
enabled: true,
|
|
36
|
+
id,
|
|
37
|
+
describedBy: void 0,
|
|
38
|
+
invalid: void 0,
|
|
39
|
+
description,
|
|
40
|
+
error
|
|
41
|
+
});
|
|
42
|
+
const { list, done } = useLeavingList(tags.map((tag) => ({ id: tag })));
|
|
43
|
+
useHeightFollow(frame, shellRef, tags.join("\0"));
|
|
44
|
+
function say(message) {
|
|
45
|
+
setNotice((previous) => previous === message ? `${message}${NBSP}` : message);
|
|
46
|
+
}
|
|
47
|
+
function update(next) {
|
|
48
|
+
if (value === void 0) setInternal(next);
|
|
49
|
+
onValueChange?.(next);
|
|
50
|
+
}
|
|
51
|
+
/** The tag element for a value, to swell it. */
|
|
52
|
+
function pulse(tag) {
|
|
53
|
+
const node = shellRef.current?.querySelector(`[data-tag="${CSS.escape(tag)}"]`);
|
|
54
|
+
if (!node) return;
|
|
55
|
+
node.removeAttribute("data-pulse");
|
|
56
|
+
node.offsetWidth;
|
|
57
|
+
node.setAttribute("data-pulse", "");
|
|
58
|
+
}
|
|
59
|
+
/** Adds each non-empty candidate that is not there yet. A duplicate swells the tag that is already there. */
|
|
60
|
+
function add(candidates) {
|
|
61
|
+
const next = [...tags];
|
|
62
|
+
const added = [];
|
|
63
|
+
for (const candidate of candidates) {
|
|
64
|
+
const tag = candidate.trim();
|
|
65
|
+
if (!tag) continue;
|
|
66
|
+
const existing = next.find((item) => item.toLowerCase() === tag.toLowerCase());
|
|
67
|
+
if (existing) {
|
|
68
|
+
pulse(existing);
|
|
69
|
+
say(`${existing} is already added`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
next.push(tag);
|
|
73
|
+
added.push(tag);
|
|
74
|
+
}
|
|
75
|
+
if (!added.length) return false;
|
|
76
|
+
update(next);
|
|
77
|
+
say(`Added ${added.join(", ")}`);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
function remove(keys) {
|
|
81
|
+
const gone = tags.filter((tag) => keys.has(tag));
|
|
82
|
+
if (!gone.length) return;
|
|
83
|
+
for (const tag of gone) initial.delete(tag);
|
|
84
|
+
update(tags.filter((tag) => !keys.has(tag)));
|
|
85
|
+
say(`Removed ${gone.join(", ")}`);
|
|
86
|
+
inputRef.current?.focus();
|
|
87
|
+
}
|
|
88
|
+
function onChange(event) {
|
|
89
|
+
const parts = event.currentTarget.value.split(",");
|
|
90
|
+
if (parts.length > 1) {
|
|
91
|
+
add(parts.slice(0, -1));
|
|
92
|
+
setDraft(parts[parts.length - 1] ?? "");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
setDraft(parts[0] ?? "");
|
|
96
|
+
}
|
|
97
|
+
function onPaste(event) {
|
|
98
|
+
const text = event.clipboardData.getData("text");
|
|
99
|
+
if (!/[,\n]/.test(text)) return;
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
const parts = (draft + text).split(/[,\n]/);
|
|
102
|
+
add(parts.slice(0, -1));
|
|
103
|
+
setDraft(parts[parts.length - 1] ?? "");
|
|
104
|
+
}
|
|
105
|
+
function onKeyDown(event) {
|
|
106
|
+
const input = event.currentTarget;
|
|
107
|
+
if (event.key === "Enter") {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
if (add([draft])) setDraft("");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const atStart = input.selectionStart === 0 && input.selectionEnd === 0;
|
|
113
|
+
if ((event.key === "Backspace" || event.key === "ArrowLeft") && atStart && tags.length) {
|
|
114
|
+
const rows = shellRef.current?.querySelectorAll("[data-slot=\"tag-input-tag\"]:not([inert])");
|
|
115
|
+
const last = rows?.[rows.length - 1];
|
|
116
|
+
if (last) {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
last.focus();
|
|
119
|
+
say(`${last.dataset.tag ?? ""} selected. Press Backspace to remove it.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** From the last tag, the right arrow returns to the field. */
|
|
124
|
+
function onShellKeyDown(event) {
|
|
125
|
+
if (event.key !== "ArrowRight" || event.target === inputRef.current) return;
|
|
126
|
+
const row = event.target.closest("[data-slot=\"tag-input-tag\"]");
|
|
127
|
+
if (!row) return;
|
|
128
|
+
const rows = shellRef.current?.querySelectorAll("[data-slot=\"tag-input-tag\"]:not([inert])");
|
|
129
|
+
if (rows && rows[rows.length - 1] === row) {
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
event.stopPropagation();
|
|
132
|
+
inputRef.current?.focus();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const control = /* @__PURE__ */ jsx("div", {
|
|
136
|
+
ref: frame,
|
|
137
|
+
"data-slot": "tag-input-frame",
|
|
138
|
+
className: "height-follow min-w-0",
|
|
139
|
+
children: /* @__PURE__ */ jsxs(Group, {
|
|
140
|
+
ref: shellRef,
|
|
141
|
+
role: "presentation",
|
|
142
|
+
isDisabled: disabled,
|
|
143
|
+
isInvalid: Boolean(error),
|
|
144
|
+
"data-slot": "tag-input-shell",
|
|
145
|
+
onKeyDownCapture: onShellKeyDown,
|
|
146
|
+
onClick: (event) => {
|
|
147
|
+
if (event.target === event.currentTarget) inputRef.current?.focus();
|
|
148
|
+
},
|
|
149
|
+
className: shellClass,
|
|
150
|
+
children: [list.length > 0 ? /* @__PURE__ */ jsx(TagGroup, {
|
|
151
|
+
"aria-label": label,
|
|
152
|
+
disabledKeys: disabled ? tags : void 0,
|
|
153
|
+
onRemove: disabled ? void 0 : remove,
|
|
154
|
+
className: "contents",
|
|
155
|
+
children: /* @__PURE__ */ jsx(TagList, {
|
|
156
|
+
items: list.map(({ item, leaving }) => ({
|
|
157
|
+
id: item.id,
|
|
158
|
+
leaving
|
|
159
|
+
})),
|
|
160
|
+
className: "contents",
|
|
161
|
+
children: (item) => /* @__PURE__ */ jsx(TagChip, {
|
|
162
|
+
tag: item.id,
|
|
163
|
+
leaving: item.leaving,
|
|
164
|
+
fresh: ready && !initial.has(item.id),
|
|
165
|
+
onGone: done
|
|
166
|
+
}, item.id)
|
|
167
|
+
})
|
|
168
|
+
}) : null, /* @__PURE__ */ jsx(Input, {
|
|
169
|
+
ref: inputRef,
|
|
170
|
+
...field.controlProps,
|
|
171
|
+
"data-slot": "tag-input-input",
|
|
172
|
+
type: "text",
|
|
173
|
+
value: draft,
|
|
174
|
+
disabled,
|
|
175
|
+
autoComplete: "off",
|
|
176
|
+
spellCheck: false,
|
|
177
|
+
placeholder: tags.length ? "" : placeholder,
|
|
178
|
+
onChange,
|
|
179
|
+
onKeyDown,
|
|
180
|
+
onPaste,
|
|
181
|
+
onBlur: () => {
|
|
182
|
+
if (add([draft])) setDraft("");
|
|
183
|
+
},
|
|
184
|
+
className: "h-7 min-w-24 flex-1 basis-24 border-0 bg-transparent px-1.5 font-sans text-base text-foreground outline-none placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground data-[disabled]:cursor-not-allowed"
|
|
185
|
+
})]
|
|
186
|
+
})
|
|
187
|
+
});
|
|
188
|
+
return /* @__PURE__ */ jsx("div", {
|
|
189
|
+
className: cn("w-full min-w-0 font-sans", className),
|
|
190
|
+
"data-slot": "tag-input",
|
|
191
|
+
children: /* @__PURE__ */ jsxs(FieldShell, {
|
|
192
|
+
label,
|
|
193
|
+
controlId: field.controlId,
|
|
194
|
+
description,
|
|
195
|
+
hintId: field.hintId,
|
|
196
|
+
error,
|
|
197
|
+
errorId: field.errorId,
|
|
198
|
+
children: [
|
|
199
|
+
control,
|
|
200
|
+
name ? tags.map((tag) => /* @__PURE__ */ jsx("input", {
|
|
201
|
+
type: "hidden",
|
|
202
|
+
name,
|
|
203
|
+
value: tag
|
|
204
|
+
}, tag)) : null,
|
|
205
|
+
/* @__PURE__ */ jsx("span", {
|
|
206
|
+
className: "sr-only",
|
|
207
|
+
role: "status",
|
|
208
|
+
"aria-live": "polite",
|
|
209
|
+
children: notice
|
|
210
|
+
})
|
|
211
|
+
]
|
|
212
|
+
})
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
/**
|
|
216
|
+
* One tag. A tag that appears after the first render fades up from a blur and scales
|
|
217
|
+
* in while its slot opens; one that is removed stays, shrinking and collapsing, until its
|
|
218
|
+
* exit ends. A leaving tag is inert, hidden from assistive technology, and skipped by the arrows.
|
|
219
|
+
*/
|
|
220
|
+
function TagChip({ tag, leaving, fresh, onGone }) {
|
|
221
|
+
const [arrives] = useState(fresh);
|
|
222
|
+
const gone = useCallback(() => onGone(tag), [onGone, tag]);
|
|
223
|
+
useExitBackstop(leaving, gone, COLLAPSE_BACKSTOP);
|
|
224
|
+
useIsomorphicLayoutEffect(() => {
|
|
225
|
+
if (leaving && prefersReducedMotion()) gone();
|
|
226
|
+
}, [leaving, gone]);
|
|
227
|
+
return /* @__PURE__ */ jsx(Tag, {
|
|
228
|
+
id: tag,
|
|
229
|
+
textValue: tag,
|
|
230
|
+
isDisabled: leaving,
|
|
231
|
+
"data-slot": "tag-input-tag",
|
|
232
|
+
"data-tag": tag,
|
|
233
|
+
ref: (node) => {
|
|
234
|
+
if (!node) return;
|
|
235
|
+
if (leaving) {
|
|
236
|
+
node.setAttribute("inert", "");
|
|
237
|
+
node.setAttribute("aria-hidden", "true");
|
|
238
|
+
} else {
|
|
239
|
+
node.removeAttribute("inert");
|
|
240
|
+
node.removeAttribute("aria-hidden");
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
onAnimationEnd: (event) => {
|
|
244
|
+
if (leaving && event.target === event.currentTarget && event.animationName === "chip-slot-out") gone();
|
|
245
|
+
},
|
|
246
|
+
className: cnAll(tagClass, leaving ? "pointer-events-none motion-safe:animate-chip-out" : arrives && "motion-safe:animate-chip-in motion-reduce:animate-fade-in"),
|
|
247
|
+
children: ({ allowsRemoving }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
248
|
+
className: "truncate",
|
|
249
|
+
children: tag
|
|
250
|
+
}), allowsRemoving ? /* @__PURE__ */ jsx(Button, {
|
|
251
|
+
slot: "remove",
|
|
252
|
+
excludeFromTabOrder: true,
|
|
253
|
+
className: removeClass,
|
|
254
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
255
|
+
"aria-hidden": "true",
|
|
256
|
+
viewBox: "0 0 16 16",
|
|
257
|
+
fill: "none",
|
|
258
|
+
stroke: "currentColor",
|
|
259
|
+
strokeWidth: "1.75",
|
|
260
|
+
strokeLinecap: "round",
|
|
261
|
+
children: /* @__PURE__ */ jsx("path", { d: "m4 4 8 8M12 4l-8 8" })
|
|
262
|
+
})
|
|
263
|
+
}) : null] })
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
export { TagInput };
|
|
268
|
+
|
|
269
|
+
//# sourceMappingURL=tag-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tag-input.js","names":["AriaGroup","AriaTagGroup","AriaTagList","AriaInput","AriaTag","AriaButton"],"sources":["../../src/tag-input/tag-input.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n forwardRef,\n useCallback,\n useImperativeHandle,\n useRef,\n useState,\n type ChangeEvent,\n type ClipboardEvent,\n type KeyboardEvent,\n type MouseEvent,\n} from \"react\";\nimport {\n Button as AriaButton,\n Group as AriaGroup,\n Input as AriaInput,\n Tag as AriaTag,\n TagGroup as AriaTagGroup,\n TagList as AriaTagList,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { FieldShell, useFieldWiring } from \"../lib/field-shell.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\nimport { useHeightFollow } from \"../lib/height-follow.js\";\nimport { useLeavingList } from \"../lib/leaving-list.js\";\nimport { prefersReducedMotion, useExitBackstop, useIsomorphicLayoutEffect } from \"../lib/motion.js\";\n\n/**\n * TagInput: type a word and press Enter or comma to turn it into a tag. A free-text\n * field on React Aria's `TagGroup`, so the tags are a grid: the arrow keys move\n * between them, Backspace or Delete removes the focused one, and each has its own\n * remove button.\n *\n * Tags keep the field still: a new tag grows in where its text was typed (it fades\n * up from a blur and scales in while its slot opens, so the caret glides aside),\n * a removed tag shrinks away and the rest glide over, and the field follows its\n * wrapped rows on a spring. Backspace at the start of an empty field moves to the\n * last tag, and the next Backspace removes it. Adding a tag that is already there\n * swells the existing one. Nothing animates on first render.\n *\n * Pasting \"a, b, c\" adds three tags. A tag you are typing is added when the field\n * loses focus.\n */\nexport interface TagInputProps {\n /** What the tags are, and the field's accessible name. */\n label: string;\n /** The tags. Controlled when given. */\n value?: string[];\n defaultValue?: string[];\n onValueChange?: (value: string[]) => void;\n /** Shown while there are no tags and nothing is typed. @default \"Add a tag\" */\n placeholder?: string;\n /** Helper text under the field. A string, because the copy is animated word by word. */\n description?: string;\n /** Error text under the field. Sets `aria-invalid` and is announced as an alert. */\n error?: string;\n disabled?: boolean;\n /** Adds one hidden input per tag, so a native form submits them all under this name. */\n name?: string;\n id?: string;\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\n/** A no-break space, appended to a repeated announcement so a screen reader says it again. */\nconst NBSP = String.fromCharCode(160);\n\n/** The longest a leaving tag's collapse takes, plus a margin, for the backstop that removes a tag whose animation never ends. */\nconst COLLAPSE_BACKSTOP = 900;\n\nconst shellClass = cnAll(\n fieldSurfaceVariants(),\n \"flex min-h-(--density-control-h-md) cursor-text flex-wrap items-center gap-1.5 p-1.75\",\n // Focus turns the box's own border dark whenever the input holds focus; no separate ring.\n \"has-[input:focus]:not-data-[invalid]:border-foreground\",\n \"forced-colors:has-[input:focus]:outline-solid forced-colors:has-[input:focus]:outline-(length:--focus-ring-width) forced-colors:has-[input:focus]:outline-offset-(--focus-ring-offset)\",\n);\n\nconst tagClass =\n \"relative inline-flex min-h-7 flex-none items-center gap-0.5 rounded-pill border-(length:--border-width) border-border bg-muted py-0 ps-3 pe-0.5 font-sans text-sm leading-(--leading-normal) text-foreground outline-none [interpolate-size:allow-keywords] data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width) data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring) data-[pulse]:motion-safe:animate-tag-pulse\";\n\nconst removeClass =\n \"grid size-6 flex-none cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-text-secondary outline-none motion-safe:transition-[background-color,color,scale] motion-safe:duration-(--duration-quick) motion-safe:ease-settle data-[hovered]:bg-surface data-[hovered]:text-foreground data-[pressed]:scale-95 motion-safe:data-[pressed]:duration-(--duration-instant) data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width) data-[focus-visible]:outline-(--color-ring) data-[disabled]:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:size-3.5\";\n\nexport const TagInput = forwardRef<HTMLInputElement, TagInputProps>(function TagInput(\n {\n label,\n value,\n defaultValue = [],\n onValueChange,\n placeholder = \"Add a tag\",\n description,\n error,\n disabled = false,\n name,\n id,\n className,\n },\n forwardedRef,\n) {\n const [internal, setInternal] = useState(defaultValue);\n const tags = value ?? internal;\n const [draft, setDraft] = useState(\"\");\n const [notice, setNotice] = useState(\"\");\n // The tags there at first render never animate; one added later grows in, even a tag that was there once and left.\n const [initial] = useState(() => new Set(tags));\n // Chips added from the next frame on arrive animated.\n const [ready, setReady] = useState(false);\n useIsomorphicLayoutEffect(() => {\n const frame = requestAnimationFrame(() => setReady(true));\n return () => cancelAnimationFrame(frame);\n }, []);\n\n const inputRef = useRef<HTMLInputElement>(null);\n useImperativeHandle(forwardedRef, () => inputRef.current as HTMLInputElement);\n const shellRef = useRef<HTMLDivElement>(null);\n const frame = useRef<HTMLDivElement>(null);\n\n const field = useFieldWiring({\n enabled: true,\n id,\n describedBy: undefined,\n invalid: undefined,\n description,\n error,\n });\n\n // A removed tag stays on screen, collapsing, until its exit ends.\n const { list, done } = useLeavingList(tags.map((tag) => ({ id: tag })));\n // The field follows its height when a tag makes a line wrap or unwrap.\n useHeightFollow(frame, shellRef, tags.join(\"\\u0000\"));\n\n function say(message: string) {\n setNotice((previous) => (previous === message ? `${message}${NBSP}` : message));\n }\n function update(next: string[]) {\n if (value === undefined) setInternal(next);\n onValueChange?.(next);\n }\n\n /** The tag element for a value, to swell it. */\n function pulse(tag: string) {\n const node = shellRef.current?.querySelector<HTMLElement>(`[data-tag=\"${CSS.escape(tag)}\"]`);\n if (!node) return;\n // Restart the animation: the attribute has to leave and come back across a style recalculation.\n node.removeAttribute(\"data-pulse\");\n void node.offsetWidth;\n node.setAttribute(\"data-pulse\", \"\");\n }\n\n /** Adds each non-empty candidate that is not there yet. A duplicate swells the tag that is already there. */\n function add(candidates: string[]) {\n const next = [...tags];\n const added: string[] = [];\n for (const candidate of candidates) {\n const tag = candidate.trim();\n if (!tag) continue;\n const existing = next.find((item) => item.toLowerCase() === tag.toLowerCase());\n if (existing) {\n pulse(existing);\n say(`${existing} is already added`);\n continue;\n }\n next.push(tag);\n added.push(tag);\n }\n if (!added.length) return false;\n update(next);\n say(`Added ${added.join(\", \")}`);\n return true;\n }\n\n function remove(keys: Set<unknown>) {\n const gone = tags.filter((tag) => keys.has(tag));\n if (!gone.length) return;\n for (const tag of gone) initial.delete(tag);\n update(tags.filter((tag) => !keys.has(tag)));\n say(`Removed ${gone.join(\", \")}`);\n inputRef.current?.focus();\n }\n\n function onChange(event: ChangeEvent<HTMLInputElement>) {\n const parts = event.currentTarget.value.split(\",\");\n // A comma, typed or pasted, ends a tag.\n if (parts.length > 1) {\n add(parts.slice(0, -1));\n setDraft(parts[parts.length - 1] ?? \"\");\n return;\n }\n setDraft(parts[0] ?? \"\");\n }\n\n function onPaste(event: ClipboardEvent<HTMLInputElement>) {\n const text = event.clipboardData.getData(\"text\");\n if (!/[,\\n]/.test(text)) return;\n event.preventDefault();\n const parts = (draft + text).split(/[,\\n]/);\n add(parts.slice(0, -1));\n setDraft(parts[parts.length - 1] ?? \"\");\n }\n\n function onKeyDown(event: KeyboardEvent<HTMLInputElement>) {\n const input = event.currentTarget;\n if (event.key === \"Enter\") {\n // Enter submits nothing from here: it only adds.\n event.preventDefault();\n if (add([draft])) setDraft(\"\");\n return;\n }\n const atStart = input.selectionStart === 0 && input.selectionEnd === 0;\n if ((event.key === \"Backspace\" || event.key === \"ArrowLeft\") && atStart && tags.length) {\n // The last tag takes focus; Backspace on it removes it.\n const rows = shellRef.current?.querySelectorAll<HTMLElement>('[data-slot=\"tag-input-tag\"]:not([inert])');\n const last = rows?.[rows.length - 1];\n if (last) {\n event.preventDefault();\n last.focus();\n say(`${last.dataset.tag ?? \"\"} selected. Press Backspace to remove it.`);\n }\n }\n }\n\n /** From the last tag, the right arrow returns to the field. */\n function onShellKeyDown(event: KeyboardEvent<HTMLElement>) {\n if (event.key !== \"ArrowRight\" || event.target === inputRef.current) return;\n const row = (event.target as HTMLElement).closest<HTMLElement>('[data-slot=\"tag-input-tag\"]');\n if (!row) return;\n const rows = shellRef.current?.querySelectorAll('[data-slot=\"tag-input-tag\"]:not([inert])');\n if (rows && rows[rows.length - 1] === row) {\n // Capture phase: React Aria would otherwise wrap to the first tag.\n event.preventDefault();\n event.stopPropagation();\n inputRef.current?.focus();\n }\n }\n\n const control = (\n <div ref={frame} data-slot=\"tag-input-frame\" className=\"height-follow min-w-0\">\n <AriaGroup\n ref={shellRef}\n role=\"presentation\"\n isDisabled={disabled}\n isInvalid={Boolean(error)}\n data-slot=\"tag-input-shell\"\n onKeyDownCapture={onShellKeyDown}\n onClick={(event: MouseEvent) => {\n if (event.target === event.currentTarget) inputRef.current?.focus();\n }}\n className={shellClass}\n >\n {/* The grid's own boxes dissolve, so the tags and the field share one wrapping row. An empty grid is\n a focusable element with nothing in it, so there is none until there is a tag. */}\n {list.length > 0 ? (\n <AriaTagGroup\n aria-label={label}\n disabledKeys={disabled ? tags : undefined}\n onRemove={disabled ? undefined : remove}\n className=\"contents\"\n >\n <AriaTagList\n items={list.map(({ item, leaving }) => ({ id: item.id, leaving }))}\n className=\"contents\"\n >\n {(item) => (\n <TagChip\n key={item.id}\n tag={item.id}\n leaving={item.leaving}\n fresh={ready && !initial.has(item.id)}\n onGone={done}\n />\n )}\n </AriaTagList>\n </AriaTagGroup>\n ) : null}\n <AriaInput\n ref={inputRef}\n {...field.controlProps}\n data-slot=\"tag-input-input\"\n type=\"text\"\n value={draft}\n disabled={disabled}\n autoComplete=\"off\"\n spellCheck={false}\n placeholder={tags.length ? \"\" : placeholder}\n onChange={onChange}\n onKeyDown={onKeyDown}\n onPaste={onPaste}\n onBlur={() => {\n if (add([draft])) setDraft(\"\");\n }}\n className=\"h-7 min-w-24 flex-1 basis-24 border-0 bg-transparent px-1.5 font-sans text-base text-foreground outline-none placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground data-[disabled]:cursor-not-allowed\"\n />\n </AriaGroup>\n </div>\n );\n\n return (\n <div className={cn(\"w-full min-w-0 font-sans\", className)} data-slot=\"tag-input\">\n <FieldShell\n label={label}\n controlId={field.controlId}\n description={description}\n hintId={field.hintId}\n error={error}\n errorId={field.errorId}\n >\n {control}\n {name\n ? tags.map((tag) => <input key={tag} type=\"hidden\" name={name} value={tag} />)\n : null}\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {notice}\n </span>\n </FieldShell>\n </div>\n );\n});\n\n/**\n * One tag. A tag that appears after the first render fades up from a blur and scales\n * in while its slot opens; one that is removed stays, shrinking and collapsing, until its\n * exit ends. A leaving tag is inert, hidden from assistive technology, and skipped by the arrows.\n */\nfunction TagChip({\n tag,\n leaving,\n fresh,\n onGone,\n}: {\n tag: string;\n leaving: boolean;\n fresh: boolean;\n onGone: (tag: string) => void;\n}) {\n // Decided once, at this tag's own mount.\n const [arrives] = useState(fresh);\n const gone = useCallback(() => onGone(tag), [onGone, tag]);\n useExitBackstop(leaving, gone, COLLAPSE_BACKSTOP);\n // Reduced motion removes it at once: an animation that is switched off never ends.\n useIsomorphicLayoutEffect(() => {\n if (leaving && prefersReducedMotion()) gone();\n }, [leaving, gone]);\n return (\n <AriaTag\n id={tag}\n textValue={tag}\n isDisabled={leaving}\n data-slot=\"tag-input-tag\"\n data-tag={tag}\n ref={(node) => {\n if (!node) return;\n // React Aria drops aria-hidden from the props it forwards, so the leaving state is written here.\n if (leaving) {\n node.setAttribute(\"inert\", \"\");\n node.setAttribute(\"aria-hidden\", \"true\");\n } else {\n node.removeAttribute(\"inert\");\n node.removeAttribute(\"aria-hidden\");\n }\n }}\n onAnimationEnd={(event) => {\n if (leaving && event.target === event.currentTarget && event.animationName === \"chip-slot-out\")\n gone();\n }}\n className={cnAll(\n tagClass,\n leaving\n ? \"pointer-events-none motion-safe:animate-chip-out\"\n : arrives && \"motion-safe:animate-chip-in motion-reduce:animate-fade-in\",\n )}\n >\n {({ allowsRemoving }) => (\n <>\n <span className=\"truncate\">{tag}</span>\n {allowsRemoving ? (\n <AriaButton slot=\"remove\" excludeFromTabOrder className={removeClass}>\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n ) : null}\n </>\n )}\n </AriaTag>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/text-morph/text-morph.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* TextMorph: one short label morphing into the next, in place. Letters both
|
|
4
|
+
* labels share glide to their new positions, new letters rise in from a soft
|
|
5
|
+
* blur, removed letters lift away, and the width follows on a spring so the
|
|
6
|
+
* layout around it glides instead of jumping. Use it for status words that
|
|
7
|
+
* change a few letters at a time (Publish, Publishing, Published; Follow,
|
|
8
|
+
* Following). It stays on one line. The letters are a picture: assistive
|
|
9
|
+
* technology reads the plain text. Nothing animates on first render, and reduced
|
|
10
|
+
* motion fades instead of moving.
|
|
11
|
+
*/
|
|
12
|
+
export interface TextMorphProps {
|
|
13
|
+
/** The label. Changing it morphs from the old one. */
|
|
14
|
+
children: string;
|
|
15
|
+
/** The element to render. @default "span" */
|
|
16
|
+
as?: "span" | "div" | "p" | "strong" | "h1" | "h2" | "h3";
|
|
17
|
+
id?: string;
|
|
18
|
+
/** Extra classes. Merged last. */
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare const TextMorph: import("react").ForwardRefExoticComponent<TextMorphProps & import("react").RefAttributes<HTMLElement>>;
|
|
22
|
+
//#endregion
|
|
23
|
+
//# sourceMappingURL=text-morph.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { MorphText } from "../lib/morph-text.js";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/text-morph/text-morph.tsx
|
|
6
|
+
const TextMorph = forwardRef(function TextMorph({ children, as = "span", id, className }, ref) {
|
|
7
|
+
return /* @__PURE__ */ jsxs(as, {
|
|
8
|
+
ref,
|
|
9
|
+
id,
|
|
10
|
+
"data-slot": "text-morph",
|
|
11
|
+
className,
|
|
12
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
13
|
+
className: "sr-only",
|
|
14
|
+
children
|
|
15
|
+
}), /* @__PURE__ */ jsx(MorphText, { text: children })]
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
//#endregion
|
|
19
|
+
export { TextMorph };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=text-morph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-morph.js","names":["Tag"],"sources":["../../src/text-morph/text-morph.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, type ElementType } from \"react\";\n\nimport { MorphText } from \"../lib/morph-text.js\";\n\n/**\n * TextMorph: one short label morphing into the next, in place. Letters both\n * labels share glide to their new positions, new letters rise in from a soft\n * blur, removed letters lift away, and the width follows on a spring so the\n * layout around it glides instead of jumping. Use it for status words that\n * change a few letters at a time (Publish, Publishing, Published; Follow,\n * Following). It stays on one line. The letters are a picture: assistive\n * technology reads the plain text. Nothing animates on first render, and reduced\n * motion fades instead of moving.\n */\nexport interface TextMorphProps {\n /** The label. Changing it morphs from the old one. */\n children: string;\n /** The element to render. @default \"span\" */\n as?: \"span\" | \"div\" | \"p\" | \"strong\" | \"h1\" | \"h2\" | \"h3\";\n id?: string;\n /** Extra classes. Merged last. */\n className?: string;\n}\n\nexport const TextMorph = forwardRef<HTMLElement, TextMorphProps>(function TextMorph(\n { children, as = \"span\", id, className },\n ref,\n) {\n const Tag = as as ElementType;\n return (\n <Tag ref={ref} id={id} data-slot=\"text-morph\" className={className}>\n <span className=\"sr-only\">{children}</span>\n <MorphText text={children} />\n </Tag>\n );\n});\n"],"mappings":";;;;;AA0BA,MAAa,YAAY,WAAwC,SAAS,UACxE,EAAE,UAAU,KAAK,QAAQ,IAAI,aAC7B,KACA;CAEA,OACE,qBAACA,IAAD;EAAU;EAAS;EAAI,aAAU;EAAwB;EAAzD,UAAA,CACE,oBAAC,QAAD;GAAM,WAAU;GAAW;EAAe,CAAA,GAC1C,oBAAC,WAAD,EAAW,MAAM,SAAW,CAAA,CACzB;;AAET,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/text-reveal/text-reveal.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* TextReveal: a short headline or sentence that reveals itself once, as it
|
|
4
|
+
* mounts. Each word rises out of its own clip while it sharpens from a soft
|
|
5
|
+
* blur, one word a beat after the last (the whole stagger stays under 480ms
|
|
6
|
+
* however long the text is).
|
|
7
|
+
*
|
|
8
|
+
* The entrance is CSS, so it starts on first paint and never leaves text
|
|
9
|
+
* hidden while scripts are slow. Use `\n` in `text` for a deliberate line break,
|
|
10
|
+
* and change the element's `key` to play it again. A heading takes its
|
|
11
|
+
* accessible name from the sentence itself, so it is read once; a paragraph
|
|
12
|
+
* cannot be named, so it keeps a visually hidden copy. Reduced motion fades.
|
|
13
|
+
*/
|
|
14
|
+
export interface TextRevealProps {
|
|
15
|
+
/** The sentence. `\n` breaks the line. */
|
|
16
|
+
text: string;
|
|
17
|
+
/** The element to render. @default "h2" */
|
|
18
|
+
as?: "h1" | "h2" | "h3" | "p";
|
|
19
|
+
/** Milliseconds to wait before the first word rises. @default 0 */
|
|
20
|
+
delay?: number;
|
|
21
|
+
id?: string;
|
|
22
|
+
/** Extra classes. Merged last. */
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare const TextReveal: import("react").ForwardRefExoticComponent<TextRevealProps & import("react").RefAttributes<HTMLElement>>;
|
|
26
|
+
//#endregion
|
|
27
|
+
//# sourceMappingURL=text-reveal.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { cnAll } from "../lib/cn.js";
|
|
2
|
+
import { Fragment, forwardRef } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/text-reveal/text-reveal.tsx
|
|
5
|
+
/** The longest the stagger may run, in ms (the considered duration), however long the text is. */
|
|
6
|
+
const MAX_STAGGER_MS = 480;
|
|
7
|
+
const WORD_STEP_MS = 40;
|
|
8
|
+
const TYPE = {
|
|
9
|
+
h1: "font-display text-h1 leading-(--leading-snug) font-semibold",
|
|
10
|
+
h2: "font-display text-h2 leading-(--leading-snug) font-semibold",
|
|
11
|
+
h3: "font-display text-h3 leading-(--leading-snug) font-semibold",
|
|
12
|
+
p: "text-lg leading-(--leading-normal)"
|
|
13
|
+
};
|
|
14
|
+
const TextReveal = forwardRef(function TextReveal({ text, as = "h2", delay = 0, id, className }, ref) {
|
|
15
|
+
const Tag = as;
|
|
16
|
+
const lines = text.split("\n").map((line) => line.split(" ").filter(Boolean));
|
|
17
|
+
const count = lines.reduce((total, words) => total + words.length, 0);
|
|
18
|
+
const step = Math.min(WORD_STEP_MS, MAX_STAGGER_MS / Math.max(count, 1));
|
|
19
|
+
const spoken = text.replace(/\n/g, " ");
|
|
20
|
+
const named = as !== "p";
|
|
21
|
+
let index = 0;
|
|
22
|
+
return /* @__PURE__ */ jsxs(Tag, {
|
|
23
|
+
ref,
|
|
24
|
+
id,
|
|
25
|
+
"data-slot": "text-reveal",
|
|
26
|
+
"aria-label": named ? spoken : void 0,
|
|
27
|
+
className: cnAll("text-reveal", TYPE[as], className),
|
|
28
|
+
children: [named ? null : /* @__PURE__ */ jsx("span", {
|
|
29
|
+
className: "sr-only",
|
|
30
|
+
children: spoken
|
|
31
|
+
}), lines.map((words, lineIndex) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
32
|
+
className: "text-reveal-line",
|
|
33
|
+
"aria-hidden": "true",
|
|
34
|
+
children: words.map((word, wordIndex) => {
|
|
35
|
+
const position = index++;
|
|
36
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
37
|
+
className: "text-reveal-clip",
|
|
38
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
39
|
+
className: "text-reveal-word",
|
|
40
|
+
style: { "--reveal-delay": `${delay + (position + lineIndex) * step}ms` },
|
|
41
|
+
children: word
|
|
42
|
+
})
|
|
43
|
+
}), wordIndex < words.length - 1 ? " " : null] }, `${word}-${position}`);
|
|
44
|
+
})
|
|
45
|
+
}), lineIndex < lines.length - 1 ? " " : null] }, lineIndex))]
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
//#endregion
|
|
49
|
+
export { TextReveal };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=text-reveal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-reveal.js","names":[],"sources":["../../src/text-reveal/text-reveal.tsx"],"sourcesContent":["import { Fragment, forwardRef, type CSSProperties, type ElementType } from \"react\";\n\nimport { cnAll } from \"../lib/cn.js\";\n\n/**\n * TextReveal: a short headline or sentence that reveals itself once, as it\n * mounts. Each word rises out of its own clip while it sharpens from a soft\n * blur, one word a beat after the last (the whole stagger stays under 480ms\n * however long the text is).\n *\n * The entrance is CSS, so it starts on first paint and never leaves text\n * hidden while scripts are slow. Use `\\n` in `text` for a deliberate line break,\n * and change the element's `key` to play it again. A heading takes its\n * accessible name from the sentence itself, so it is read once; a paragraph\n * cannot be named, so it keeps a visually hidden copy. Reduced motion fades.\n */\nexport interface TextRevealProps {\n /** The sentence. `\\n` breaks the line. */\n text: string;\n /** The element to render. @default \"h2\" */\n as?: \"h1\" | \"h2\" | \"h3\" | \"p\";\n /** Milliseconds to wait before the first word rises. @default 0 */\n delay?: number;\n id?: string;\n /** Extra classes. Merged last. */\n className?: string;\n}\n\n/** The longest the stagger may run, in ms (the considered duration), however long the text is. */\nconst MAX_STAGGER_MS = 480;\nconst WORD_STEP_MS = 40;\n\n// Static strings: Tailwind scans source text, so a class built at runtime would never be emitted.\nconst TYPE: Record<NonNullable<TextRevealProps[\"as\"]>, string> = {\n h1: \"font-display text-h1 leading-(--leading-snug) font-semibold\",\n h2: \"font-display text-h2 leading-(--leading-snug) font-semibold\",\n h3: \"font-display text-h3 leading-(--leading-snug) font-semibold\",\n p: \"text-lg leading-(--leading-normal)\",\n};\n\nexport const TextReveal = forwardRef<HTMLElement, TextRevealProps>(function TextReveal(\n { text, as = \"h2\", delay = 0, id, className },\n ref,\n) {\n const Tag = as as ElementType;\n const lines = text.split(\"\\n\").map((line) => line.split(\" \").filter(Boolean));\n const count = lines.reduce((total, words) => total + words.length, 0);\n const step = Math.min(WORD_STEP_MS, MAX_STAGGER_MS / Math.max(count, 1));\n const spoken = text.replace(/\\n/g, \" \");\n // Headings take their accessible name from aria-label, so the sentence appears once in the DOM.\n const named = as !== \"p\";\n let index = 0;\n return (\n <Tag\n ref={ref}\n id={id}\n data-slot=\"text-reveal\"\n aria-label={named ? spoken : undefined}\n className={cnAll(\"text-reveal\", TYPE[as], className)}\n >\n {named ? null : <span className=\"sr-only\">{spoken}</span>}\n {lines.map((words, lineIndex) => (\n <Fragment key={lineIndex}>\n <span className=\"text-reveal-line\" aria-hidden=\"true\">\n {words.map((word, wordIndex) => {\n const position = index++;\n return (\n <Fragment key={`${word}-${position}`}>\n <span className=\"text-reveal-clip\">\n <span\n className=\"text-reveal-word\"\n style={\n {\n \"--reveal-delay\": `${delay + (position + lineIndex) * step}ms`,\n } as CSSProperties\n }\n >\n {word}\n </span>\n </span>\n {wordIndex < words.length - 1 ? \" \" : null}\n </Fragment>\n );\n })}\n </span>\n {lineIndex < lines.length - 1 ? \" \" : null}\n </Fragment>\n ))}\n </Tag>\n );\n});\n"],"mappings":";;;;;AA6BA,MAAM,iBAAiB;AACvB,MAAM,eAAe;AAGrB,MAAM,OAA2D;CAC/D,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,GAAG;AACL;AAEA,MAAa,aAAa,WAAyC,SAAS,WAC1E,EAAE,MAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,aAClC,KACA;CACA,MAAM,MAAM;CACZ,MAAM,QAAQ,KAAK,MAAM,IAAI,CAAC,CAAC,KAAK,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO,CAAC;CAC5E,MAAM,QAAQ,MAAM,QAAQ,OAAO,UAAU,QAAQ,MAAM,QAAQ,CAAC;CACpE,MAAM,OAAO,KAAK,IAAI,cAAc,iBAAiB,KAAK,IAAI,OAAO,CAAC,CAAC;CACvE,MAAM,SAAS,KAAK,QAAQ,OAAO,GAAG;CAEtC,MAAM,QAAQ,OAAO;CACrB,IAAI,QAAQ;CACZ,OACE,qBAAC,KAAD;EACO;EACD;EACJ,aAAU;EACV,cAAY,QAAQ,SAAS,KAAA;EAC7B,WAAW,MAAM,eAAe,KAAK,KAAK,SAAS;EALrD,UAAA,CAOG,QAAQ,OAAO,oBAAC,QAAD;GAAM,WAAU;GAAW,UAAA;EAAa,CAAA,GACvD,MAAM,KAAK,OAAO,cACjB,qBAAC,UAAD,EAAA,UAAA,CACE,oBAAC,QAAD;GAAM,WAAU;GAAmB,eAAY;GAC5C,UAAA,MAAM,KAAK,MAAM,cAAc;IAC9B,MAAM,WAAW;IACjB,OACE,qBAAC,UAAD,EAAA,UAAA,CACE,oBAAC,QAAD;KAAM,WAAU;KACd,UAAA,oBAAC,QAAD;MACE,WAAU;MACV,OACE,EACE,kBAAkB,GAAG,SAAS,WAAW,aAAa,KAAK,IAC7D;MAGD,UAAA;KACG,CAAA;IACF,CAAA,GACL,YAAY,MAAM,SAAS,IAAI,MAAM,IAC9B,EAAA,GAdK,GAAG,KAAK,GAAG,UAchB;GAEd,CAAC;EACG,CAAA,GACL,YAAY,MAAM,SAAS,IAAI,MAAM,IAC9B,EAAA,GAxBK,SAwBL,CACX,CACE;;AAET,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/text-shimmer/text-shimmer.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* TextShimmer: a calm light sweep across a short status line, such as
|
|
4
|
+
* "Thinking" or "Generating summary", to show that work is ongoing.
|
|
5
|
+
*
|
|
6
|
+
* The band is the current text colour over a muted base, painted by CSS alone
|
|
7
|
+
* (a gradient clipped to the text, its position driven by a registered custom
|
|
8
|
+
* property), so nothing re-renders per frame. A pause of about a quarter of the
|
|
9
|
+
* cycle follows each pass. When `active` turns false the text settles to solid
|
|
10
|
+
* over a short transition. A changed label rises in place. Under reduced motion
|
|
11
|
+
* nothing moves and the text stays in its muted base colour. The component sets
|
|
12
|
+
* `aria-busy`; announce completion from your own live region.
|
|
13
|
+
*/
|
|
14
|
+
export interface TextShimmerProps {
|
|
15
|
+
/** The status text. Keep it to one short line. */
|
|
16
|
+
children: string;
|
|
17
|
+
/** Sweep while work is ongoing. @default true */
|
|
18
|
+
active?: boolean;
|
|
19
|
+
/** Seconds for one pass of the band across the text (the pause between passes comes on top). @default 1.8 */
|
|
20
|
+
duration?: number;
|
|
21
|
+
/** The element to render. @default "span" */
|
|
22
|
+
as?: "span" | "p" | "div" | "h2" | "h3" | "h4";
|
|
23
|
+
/** Extra classes for the element. Merged last. */
|
|
24
|
+
className?: string;
|
|
25
|
+
id?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare const TextShimmer: import("react").ForwardRefExoticComponent<TextShimmerProps & import("react").RefAttributes<HTMLElement>>;
|
|
28
|
+
//#endregion
|
|
29
|
+
//# sourceMappingURL=text-shimmer.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { SwapText } from "../lib/swap-text.js";
|
|
4
|
+
import { createElement, forwardRef } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
//#region src/text-shimmer/text-shimmer.tsx
|
|
7
|
+
/** The sweep takes this share of one cycle; the rest is the rest between passes. */
|
|
8
|
+
const SWEEP_SHARE = .76;
|
|
9
|
+
const TextShimmer = forwardRef(function TextShimmer({ children, active = true, duration = 1.8, as = "span", className, id }, ref) {
|
|
10
|
+
return createElement(as, {
|
|
11
|
+
ref,
|
|
12
|
+
id,
|
|
13
|
+
"data-slot": "text-shimmer",
|
|
14
|
+
"data-state": active ? "active" : "idle",
|
|
15
|
+
"aria-busy": active || void 0,
|
|
16
|
+
style: { "--text-shimmer-cycle": `${duration / SWEEP_SHARE}s` },
|
|
17
|
+
className: cn("shimmer-text", className)
|
|
18
|
+
}, /* @__PURE__ */ jsx(SwapText, {
|
|
19
|
+
swapKey: children,
|
|
20
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
21
|
+
className: "shimmer-text-label",
|
|
22
|
+
children
|
|
23
|
+
})
|
|
24
|
+
}));
|
|
25
|
+
});
|
|
26
|
+
//#endregion
|
|
27
|
+
export { TextShimmer };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=text-shimmer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-shimmer.js","names":[],"sources":["../../src/text-shimmer/text-shimmer.tsx"],"sourcesContent":["\"use client\";\n\nimport { createElement, forwardRef, type CSSProperties } from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * TextShimmer: a calm light sweep across a short status line, such as\n * \"Thinking\" or \"Generating summary\", to show that work is ongoing.\n *\n * The band is the current text colour over a muted base, painted by CSS alone\n * (a gradient clipped to the text, its position driven by a registered custom\n * property), so nothing re-renders per frame. A pause of about a quarter of the\n * cycle follows each pass. When `active` turns false the text settles to solid\n * over a short transition. A changed label rises in place. Under reduced motion\n * nothing moves and the text stays in its muted base colour. The component sets\n * `aria-busy`; announce completion from your own live region.\n */\nexport interface TextShimmerProps {\n /** The status text. Keep it to one short line. */\n children: string;\n /** Sweep while work is ongoing. @default true */\n active?: boolean;\n /** Seconds for one pass of the band across the text (the pause between passes comes on top). @default 1.8 */\n duration?: number;\n /** The element to render. @default \"span\" */\n as?: \"span\" | \"p\" | \"div\" | \"h2\" | \"h3\" | \"h4\";\n /** Extra classes for the element. Merged last. */\n className?: string;\n id?: string;\n}\n\n/** The sweep takes this share of one cycle; the rest is the rest between passes. */\nconst SWEEP_SHARE = 0.76;\n\nexport const TextShimmer = forwardRef<HTMLElement, TextShimmerProps>(function TextShimmer(\n { children, active = true, duration = 1.8, as = \"span\", className, id },\n ref,\n) {\n return createElement(\n as,\n {\n ref,\n id,\n \"data-slot\": \"text-shimmer\",\n \"data-state\": active ? \"active\" : \"idle\",\n \"aria-busy\": active || undefined,\n style: { \"--text-shimmer-cycle\": `${duration / SWEEP_SHARE}s` } as CSSProperties,\n className: cn(\"shimmer-text\", className),\n },\n <SwapText swapKey={children}>\n <span className=\"shimmer-text-label\">{children}</span>\n </SwapText>,\n );\n});\n"],"mappings":";;;;;;;AAkCA,MAAM,cAAc;AAEpB,MAAa,cAAc,WAA0C,SAAS,YAC5E,EAAE,UAAU,SAAS,MAAM,WAAW,KAAK,KAAK,QAAQ,WAAW,MACnE,KACA;CACA,OAAO,cACL,IACA;EACE;EACA;EACA,aAAa;EACb,cAAc,SAAS,WAAW;EAClC,aAAa,UAAU,KAAA;EACvB,OAAO,EAAE,wBAAwB,GAAG,WAAW,YAAY,GAAG;EAC9D,WAAW,GAAG,gBAAgB,SAAS;CACzC,GACA,oBAAC,UAAD;EAAU,SAAS;EACjB,UAAA,oBAAC,QAAD;GAAM,WAAU;GAAsB;EAAe,CAAA;CAC7C,CAAA,CACZ;AACF,CAAC"}
|
|
@@ -27,14 +27,14 @@ import { Time } from "@internationalized/date";
|
|
|
27
27
|
const timePickerListVariants = cva([
|
|
28
28
|
"picker-pop w-(--trigger-width) origin-top overflow-hidden",
|
|
29
29
|
"[--pop-in-y:-6px] [--pop-in-scale:0.97] [--pop-out-y:-4px] [--pop-out-scale:0.98] [--pop-out-duration:120ms]",
|
|
30
|
-
"rounded-
|
|
30
|
+
"rounded-panel border-(length:--border-width) border-border p-1",
|
|
31
31
|
"bg-card text-card-foreground shadow-floating",
|
|
32
32
|
"box-border font-sans text-sm leading-(--leading-normal)"
|
|
33
33
|
]);
|
|
34
34
|
/** A time. The highlight changes almost at once (the arrows move it often); a dot marks the chosen one. */
|
|
35
35
|
const timePickerItemVariants = cva([
|
|
36
36
|
"relative flex min-h-9 cursor-pointer select-none items-center justify-between gap-2",
|
|
37
|
-
"rounded-
|
|
37
|
+
"rounded-control px-3 py-1.5 tabular-nums outline-none",
|
|
38
38
|
"text-sm leading-(--leading-normal) text-card-foreground",
|
|
39
39
|
"data-[focused]:bg-muted",
|
|
40
40
|
"motion-safe:transition-colors motion-safe:duration-80 motion-safe:ease-settle",
|