@apliteni/apliteni-ui 0.63.0 → 0.65.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/package.json +1 -1
- package/react/README.md +61 -1
- package/react/dist/index.d.ts +88 -17
- package/react/dist/index.js +153 -9
- package/src/components/index.js +7 -5
- package/src/components/success.js +30 -37
- package/src/index.d.ts +1 -1
- package/src/styles/callout.css +7 -1
- package/src/styles/success.css +33 -81
package/package.json
CHANGED
package/react/README.md
CHANGED
|
@@ -29,7 +29,7 @@ import '@apliteni/apliteni-ui/react/css'; // React components' shell styles (mo
|
|
|
29
29
|
import { DataTable, Modal, Button } from '@apliteni/apliteni-ui/react';
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Components: `Checkbox`, `Switch`, `EmptyState`, `DataTable`, `Pagination`, `StatBand`, `Modal`, `Confirm`, `Drawer`, `CommandPalette`, `Dropdown`, `BackLink`, `Tooltip`, `Snippet`, `Tabs`, `Button`, `Badge`, `Pill`, `StatusDot`, `Card`, `Callout`, `Icon`.
|
|
32
|
+
Components: `Success`, `SuccessPanel`, `SuccessCheck`, `Checkbox`, `Switch`, `EmptyState`, `DataTable`, `Pagination`, `StatBand`, `Modal`, `Confirm`, `Drawer`, `CommandPalette`, `Dropdown`, `BackLink`, `Tooltip`, `Snippet`, `Tabs`, `Button`, `Badge`, `Pill`, `StatusDot`, `Card`, `Callout`, `Icon`.
|
|
33
33
|
|
|
34
34
|
`Pill` renders metadata with the existing pill spacing. Omit `variant` for neutral
|
|
35
35
|
metadata or use `live` or `soon`. Its children accept React content.
|
|
@@ -69,6 +69,20 @@ for a small inline control: `<Button size="xs" variant="ghost" icon="copy" iconO
|
|
|
69
69
|
Its glyph is 13px and its icon-only target is 24×24px; labelled xs buttons use
|
|
70
70
|
`--text-xs`. The other sizes keep 16px glyphs.
|
|
71
71
|
|
|
72
|
+
`Button href="/reports"` renders a native link and forwards an anchor ref. Without
|
|
73
|
+
`href`, it remains a native button. `leading={<VendorMark />}` puts decorative
|
|
74
|
+
caller artwork before the label and takes precedence over `icon`; artwork must
|
|
75
|
+
contain no focusable elements. Use visible children or an explicit accessible name.
|
|
76
|
+
Disabled links leave the tab order; busy links keep focus. Both remove their href
|
|
77
|
+
and block activation until enabled. Anchor attributes such as `target`, `rel`,
|
|
78
|
+
and `download` pass through. The kit does not supply vendor artwork.
|
|
79
|
+
|
|
80
|
+
A `className` you pass is added to the kit's classes rather than swapped for them,
|
|
81
|
+
and the component's own `aria-busy`, `aria-disabled` and `data-btn-*` attributes win
|
|
82
|
+
over anything you spread in, so a busy control cannot be made to read as idle. An
|
|
83
|
+
icon-only control with no children, no `icon` and no label ships nameless on purpose:
|
|
84
|
+
the gap then shows up in an accessibility check instead of reading as satisfied.
|
|
85
|
+
|
|
72
86
|
`CommandPalette` renders the kit's `commandPalette()` markup, class for class, and imports the
|
|
73
87
|
kit's ranking rather than repeating it — so a palette a server rendered and the same palette
|
|
74
88
|
after a keystroke put the same row first. Two differences from the vanilla one follow from
|
|
@@ -539,6 +553,52 @@ Tooltip accepts text and inline children, without nested controls. Hovering or f
|
|
|
539
553
|
<Tooltip text="Updated daily">Balance</Tooltip>
|
|
540
554
|
```
|
|
541
555
|
|
|
556
|
+
## Success
|
|
557
|
+
|
|
558
|
+
`SuccessPanel` confirms an outcome inside the current page with `title` and optional
|
|
559
|
+
`sub`, takes the same `check` as `Success`, and forwards a ref to its root div. `SuccessCheck` is the bare mark both
|
|
560
|
+
confirmations draw, matching the kit's `successCheck()`, and forwards a ref to its
|
|
561
|
+
`svg`. It carries no size or box of its own: to compose it alone, put it in a wrapper
|
|
562
|
+
that sets both, such as the kit's `ui-success__check`, and name the outcome in text
|
|
563
|
+
next to it.
|
|
564
|
+
|
|
565
|
+
```tsx
|
|
566
|
+
<div className="ui-success__check"><SuccessCheck /></div>
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
The mark comes in two: `line` (the default) is the bare Lucide check in the success
|
|
570
|
+
colour, which strokes itself on and takes its size from the layout it lands in;
|
|
571
|
+
`circled` is Lucide `circle-check-big` at 20px, the kit's label size, drawn at rest and
|
|
572
|
+
the same size everywhere — in every `Success` layout and in `SuccessPanel`. Pick it with `check` on `Success` or `variant` on
|
|
573
|
+
`SuccessCheck`. Guidelines / Iconography reserves a circled glyph for a state and a
|
|
574
|
+
bare one for an action, and a confirmation reports a state — so `circled` is the mark
|
|
575
|
+
that rule asks for, and `line` is the default because it carries the moment at page
|
|
576
|
+
size. Neither has a filled disc behind it.
|
|
577
|
+
|
|
578
|
+
`Success` provides `hero`, `split`, and `compact` layouts on a plain elevated card;
|
|
579
|
+
there is no backdrop layer and no `backdrop` prop. Pass `title`, one short `body`
|
|
580
|
+
line, and React `actions`. There is no `eyebrow` prop: a confirmation carries one
|
|
581
|
+
title and at most one line under it, so put the outcome in the title rather than in a
|
|
582
|
+
label above it. A kit `Button` carries the focus ring already; a router link or
|
|
583
|
+
a plain `<a>` must also take `className="ui-focusable"`, or it falls back to the
|
|
584
|
+
browser's own focus outline. An omitted or empty `actions`
|
|
585
|
+
leaves out the actions row. Hero and split default to h1; compact defaults to h2.
|
|
586
|
+
`level` overrides the heading rank, and a value outside 1–6 falls back to that layout
|
|
587
|
+
default. `confetti` enables the existing decorative animation. All three components
|
|
588
|
+
use the kit CSS and its reduced-motion rules.
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
<Success title="Changes saved" check="circled"
|
|
592
|
+
countdown={waiting ? { seconds: 5, label: 'Continuing' } : null}
|
|
593
|
+
onCountdownEnd={continueToNextPage}
|
|
594
|
+
actions={<Button onClick={() => setWaiting(false)}>Stay here</Button>} />
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Removing `countdown` or unmounting cancels the timer. Changing its duration restarts
|
|
598
|
+
it; changing its label or callback does not. Durations are whole seconds rounded
|
|
599
|
+
down, with five seconds used for omitted, non-finite, or sub-one values. Completion
|
|
600
|
+
fires once per countdown, including in StrictMode. Navigation belongs to the caller.
|
|
601
|
+
|
|
542
602
|
## Checkbox and Switch
|
|
543
603
|
|
|
544
604
|
`Checkbox` wraps a native checkbox and its visible `label`. Set `type="radio"`
|
package/react/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { InputHTMLAttributes, ReactNode, ButtonHTMLAttributes, HTMLAttributes, RefObject, MouseEvent, ElementType, ComponentPropsWithoutRef, SelectHTMLAttributes, TextareaHTMLAttributes } from 'react';
|
|
2
|
+
import { InputHTMLAttributes, ReactNode, ButtonHTMLAttributes, RefAttributes, ReactElement, AnchorHTMLAttributes, HTMLAttributes, RefObject, MouseEvent, ElementType, ComponentPropsWithoutRef, SelectHTMLAttributes, TextareaHTMLAttributes, SVGAttributes } from 'react';
|
|
3
3
|
|
|
4
4
|
declare function Icon({ name }: {
|
|
5
5
|
name: string;
|
|
@@ -21,10 +21,12 @@ declare const Switch: react.ForwardRefExoticComponent<Omit<InputHTMLAttributes<H
|
|
|
21
21
|
label: string;
|
|
22
22
|
} & react.RefAttributes<HTMLInputElement>>;
|
|
23
23
|
|
|
24
|
-
type
|
|
24
|
+
type ButtonOptions = {
|
|
25
25
|
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
26
26
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
27
27
|
icon?: string;
|
|
28
|
+
/** Decorative caller artwork, before the label; takes precedence over icon. */
|
|
29
|
+
leading?: ReactNode;
|
|
28
30
|
iconRight?: string;
|
|
29
31
|
iconOnly?: boolean;
|
|
30
32
|
block?: boolean;
|
|
@@ -33,20 +35,22 @@ type ButtonProps = {
|
|
|
33
35
|
/** Message after busy ends; an empty string leaves outcome announcements to the caller. */
|
|
34
36
|
completionMessage?: string;
|
|
35
37
|
children?: ReactNode;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
};
|
|
39
|
+
type NativeButtonProps = ButtonOptions & ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
40
|
+
href?: never;
|
|
41
|
+
};
|
|
42
|
+
type ButtonLinkProps = ButtonOptions & AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
43
|
+
href: string;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
};
|
|
46
|
+
type ButtonProps = NativeButtonProps | ButtonLinkProps;
|
|
47
|
+
type ButtonElement = HTMLButtonElement | HTMLAnchorElement;
|
|
48
|
+
declare const Button: {
|
|
49
|
+
(props: NativeButtonProps & RefAttributes<HTMLButtonElement>): ReactElement;
|
|
50
|
+
(props: ButtonLinkProps & RefAttributes<HTMLAnchorElement>): ReactElement;
|
|
51
|
+
(props: ButtonProps & RefAttributes<ButtonElement>): ReactElement;
|
|
52
|
+
displayName?: string;
|
|
53
|
+
};
|
|
50
54
|
|
|
51
55
|
declare function Badge({ variant, children }: {
|
|
52
56
|
variant?: string;
|
|
@@ -675,6 +679,73 @@ type TooltipProps = {
|
|
|
675
679
|
};
|
|
676
680
|
declare function Tooltip({ text, children }: TooltipProps): react.JSX.Element;
|
|
677
681
|
|
|
682
|
+
type SuccessMark = 'line' | 'circled';
|
|
683
|
+
type SuccessCheckProps = Omit<SVGAttributes<SVGSVGElement>, 'children'> & {
|
|
684
|
+
/** `line` is the bare Lucide check and draws itself on; `circled` is Lucide
|
|
685
|
+
* circle-check-big at 20px, the kit's label size, drawn at rest. */
|
|
686
|
+
variant?: SuccessMark;
|
|
687
|
+
};
|
|
688
|
+
/** Decorative mark. Supply the outcome in adjacent text. */
|
|
689
|
+
declare const SuccessCheck: react.ForwardRefExoticComponent<Omit<SVGAttributes<SVGSVGElement>, "children"> & {
|
|
690
|
+
/** `line` is the bare Lucide check and draws itself on; `circled` is Lucide
|
|
691
|
+
* circle-check-big at 20px, the kit's label size, drawn at rest. */
|
|
692
|
+
variant?: SuccessMark;
|
|
693
|
+
} & react.RefAttributes<SVGSVGElement>>;
|
|
694
|
+
type SuccessPanelProps = Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'children'> & {
|
|
695
|
+
title?: string;
|
|
696
|
+
sub?: string;
|
|
697
|
+
/** Which check mark to draw. See SuccessCheck. */
|
|
698
|
+
check?: SuccessMark;
|
|
699
|
+
};
|
|
700
|
+
declare const SuccessPanel: react.ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLDivElement>, "children" | "title"> & {
|
|
701
|
+
title?: string;
|
|
702
|
+
sub?: string;
|
|
703
|
+
/** Which check mark to draw. See SuccessCheck. */
|
|
704
|
+
check?: SuccessMark;
|
|
705
|
+
} & react.RefAttributes<HTMLDivElement>>;
|
|
706
|
+
type SuccessCountdown = {
|
|
707
|
+
seconds?: number;
|
|
708
|
+
label?: string;
|
|
709
|
+
};
|
|
710
|
+
type SuccessProps = Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'children'> & {
|
|
711
|
+
layout?: 'hero' | 'split' | 'compact';
|
|
712
|
+
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
713
|
+
/** Which check mark to draw. See SuccessCheck. */
|
|
714
|
+
check?: SuccessMark;
|
|
715
|
+
title?: string;
|
|
716
|
+
/** One short line under the title, or nothing. There is no eyebrow tier.
|
|
717
|
+
* why: docs/specification.md#success-confirmations */
|
|
718
|
+
body?: string;
|
|
719
|
+
/** Removed: a confirmation carries one title and at most one line. Typed as
|
|
720
|
+
* `never` so a caller still passing it is told where it went rather than
|
|
721
|
+
* having it spread silently onto the root as an unknown DOM attribute. */
|
|
722
|
+
eyebrow?: never;
|
|
723
|
+
actions?: ReactNode;
|
|
724
|
+
confetti?: boolean;
|
|
725
|
+
/** Set to null to cancel; changing seconds starts a new countdown. */
|
|
726
|
+
countdown?: SuccessCountdown | null;
|
|
727
|
+
onCountdownEnd?: () => void;
|
|
728
|
+
};
|
|
729
|
+
declare const Success: react.ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLDivElement>, "children" | "title"> & {
|
|
730
|
+
layout?: "hero" | "split" | "compact";
|
|
731
|
+
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
732
|
+
/** Which check mark to draw. See SuccessCheck. */
|
|
733
|
+
check?: SuccessMark;
|
|
734
|
+
title?: string;
|
|
735
|
+
/** One short line under the title, or nothing. There is no eyebrow tier.
|
|
736
|
+
* why: docs/specification.md#success-confirmations */
|
|
737
|
+
body?: string;
|
|
738
|
+
/** Removed: a confirmation carries one title and at most one line. Typed as
|
|
739
|
+
* `never` so a caller still passing it is told where it went rather than
|
|
740
|
+
* having it spread silently onto the root as an unknown DOM attribute. */
|
|
741
|
+
eyebrow?: never;
|
|
742
|
+
actions?: ReactNode;
|
|
743
|
+
confetti?: boolean;
|
|
744
|
+
/** Set to null to cancel; changing seconds starts a new countdown. */
|
|
745
|
+
countdown?: SuccessCountdown | null;
|
|
746
|
+
onCountdownEnd?: () => void;
|
|
747
|
+
} & react.RefAttributes<HTMLDivElement>>;
|
|
748
|
+
|
|
678
749
|
type SidebarNavBadge = string | number | {
|
|
679
750
|
text: string | number;
|
|
680
751
|
tone?: 'neutral' | 'accent' | 'live' | 'danger';
|
|
@@ -728,4 +799,4 @@ declare const SidebarNav: react.ForwardRefExoticComponent<Omit<Omit<react.Detail
|
|
|
728
799
|
declare const PAGE_SIZES: readonly number[];
|
|
729
800
|
declare const DEFAULT_PAGE_SIZE: number;
|
|
730
801
|
|
|
731
|
-
export { AppShell, type AppShellProps, type AppShellSection, BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, Checkbox, type CheckboxProps, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DeltaValue, type DeltaValueProps, Denied, type DeniedProps, Drawer, type DrawerProps, DrawerSection, type DrawerSectionProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, EmptyState, type EmptyStateProps, type EmptyStateVariant, Field, type FieldControlProps, type FieldProps, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, Pill, type PillProps, RowIdentity, type RowIdentityProps, type SegmentOption, Segmented, type SegmentedProps, SelectField, type SelectFieldProps, SidebarNav, type SidebarNavBadge, type SidebarNavItem, type SidebarNavLeaf, type SidebarNavProps, type SidebarNavSection, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, StatusDot, type StatusDotProps, Switch, type SwitchProps, THEME_INIT_SCRIPT, type TabItem, type TableSort, Tabs, type TabsProps, TextArea, type TextAreaProps, TextField, type TextFieldProps, ThemeToggle, type ThemeToggleProps, Timeline, type TimelineEvent, type TimelineEventKind, type TimelineProps, Toast, type ToastNotice, type ToastProps, Tooltip, type TooltipProps, sortTableRows, useToast };
|
|
802
|
+
export { AppShell, type AppShellProps, type AppShellSection, BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonLinkProps, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, Checkbox, type CheckboxProps, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DeltaValue, type DeltaValueProps, Denied, type DeniedProps, Drawer, type DrawerProps, DrawerSection, type DrawerSectionProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, EmptyState, type EmptyStateProps, type EmptyStateVariant, Field, type FieldControlProps, type FieldProps, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, type NativeButtonProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, Pill, type PillProps, RowIdentity, type RowIdentityProps, type SegmentOption, Segmented, type SegmentedProps, SelectField, type SelectFieldProps, SidebarNav, type SidebarNavBadge, type SidebarNavItem, type SidebarNavLeaf, type SidebarNavProps, type SidebarNavSection, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, StatusDot, type StatusDotProps, Success, SuccessCheck, type SuccessCheckProps, type SuccessCountdown, SuccessPanel, type SuccessPanelProps, type SuccessProps, Switch, type SwitchProps, THEME_INIT_SCRIPT, type TabItem, type TableSort, Tabs, type TabsProps, TextArea, type TextAreaProps, TextField, type TextFieldProps, ThemeToggle, type ThemeToggleProps, Timeline, type TimelineEvent, type TimelineEventKind, type TimelineProps, Toast, type ToastNotice, type ToastProps, Tooltip, type TooltipProps, sortTableRows, useToast };
|
package/react/dist/index.js
CHANGED
|
@@ -80,6 +80,7 @@ var Button = forwardRef3(function Button2({
|
|
|
80
80
|
variant = "secondary",
|
|
81
81
|
size = "md",
|
|
82
82
|
icon: icon6,
|
|
83
|
+
leading,
|
|
83
84
|
iconRight,
|
|
84
85
|
iconOnly,
|
|
85
86
|
block,
|
|
@@ -87,7 +88,12 @@ var Button = forwardRef3(function Button2({
|
|
|
87
88
|
completionMessage,
|
|
88
89
|
children,
|
|
89
90
|
type = "button",
|
|
91
|
+
href,
|
|
90
92
|
disabled,
|
|
93
|
+
className,
|
|
94
|
+
tabIndex,
|
|
95
|
+
onAuxClick,
|
|
96
|
+
onAuxClickCapture,
|
|
91
97
|
onClick,
|
|
92
98
|
onClickCapture,
|
|
93
99
|
onKeyDown,
|
|
@@ -97,7 +103,7 @@ var Button = forwardRef3(function Button2({
|
|
|
97
103
|
...rest
|
|
98
104
|
}, forwardedRef) {
|
|
99
105
|
const buttonRef = useRef(null);
|
|
100
|
-
useImperativeHandle(forwardedRef, () => buttonRef.current, []);
|
|
106
|
+
useImperativeHandle(forwardedRef, () => buttonRef.current, [href !== void 0]);
|
|
101
107
|
const labelRef = useRef(null);
|
|
102
108
|
const announcer = useRef(null);
|
|
103
109
|
const hasBeenBusy = useRef(false);
|
|
@@ -122,35 +128,47 @@ var Button = forwardRef3(function Button2({
|
|
|
122
128
|
previous.current = null;
|
|
123
129
|
}, []);
|
|
124
130
|
const blockActivation = (event) => {
|
|
125
|
-
if (!busy) return false;
|
|
131
|
+
if (!busy && !disabled) return false;
|
|
126
132
|
event.preventDefault();
|
|
127
133
|
event.stopPropagation();
|
|
128
134
|
return true;
|
|
129
135
|
};
|
|
130
|
-
const blockKey = (event) => (event.key === "Enter" || event.key === " ") && blockActivation(event);
|
|
136
|
+
const blockKey = (event) => (event.key === "Enter" || event.key === " " && href === void 0) && blockActivation(event);
|
|
131
137
|
const cls = cx(
|
|
132
138
|
"ui-btn",
|
|
133
139
|
variant && `ui-btn--${variant}`,
|
|
134
140
|
size !== "md" && `ui-btn--${size}`,
|
|
135
141
|
block && "ui-btn--block",
|
|
136
|
-
iconOnly && "ui-btn--icon"
|
|
142
|
+
iconOnly && "ui-btn--icon",
|
|
143
|
+
className
|
|
137
144
|
);
|
|
138
145
|
const labelled = rest["aria-label"] != null || rest["aria-labelledby"] != null;
|
|
139
146
|
const fallback = typeof children === "string" && children.trim() ? children.trim() : icon6;
|
|
140
147
|
const named = iconOnly && !labelled && fallback ? { "aria-label": fallback, title: rest.title ?? fallback } : {};
|
|
148
|
+
const Root = href === void 0 ? "button" : "a";
|
|
141
149
|
return /* @__PURE__ */ jsxs3(
|
|
142
|
-
|
|
150
|
+
Root,
|
|
143
151
|
{
|
|
152
|
+
...rest,
|
|
144
153
|
ref: buttonRef,
|
|
145
154
|
"data-btn-wired": "",
|
|
146
155
|
"data-btn-ready": !busy && hasBeenBusy.current ? "" : void 0,
|
|
147
|
-
type,
|
|
156
|
+
type: href === void 0 ? type : void 0,
|
|
157
|
+
href: disabled || busy ? void 0 : href,
|
|
158
|
+
role: href !== void 0 ? "link" : rest.role,
|
|
159
|
+
tabIndex: href !== void 0 && disabled ? -1 : href !== void 0 ? tabIndex ?? 0 : tabIndex,
|
|
160
|
+
"data-btn-disabled": disabled ? "" : void 0,
|
|
148
161
|
className: cls,
|
|
149
|
-
disabled,
|
|
162
|
+
disabled: href === void 0 ? disabled : void 0,
|
|
150
163
|
"aria-disabled": disabled || busy ? true : void 0,
|
|
151
164
|
"aria-busy": busy ? true : void 0,
|
|
152
|
-
...rest,
|
|
153
165
|
...named,
|
|
166
|
+
onAuxClickCapture: (event) => {
|
|
167
|
+
if (!blockActivation(event)) onAuxClickCapture?.(event);
|
|
168
|
+
},
|
|
169
|
+
onAuxClick: (event) => {
|
|
170
|
+
if (!blockActivation(event)) onAuxClick?.(event);
|
|
171
|
+
},
|
|
154
172
|
onClickCapture: (event) => {
|
|
155
173
|
if (!blockActivation(event)) onClickCapture?.(event);
|
|
156
174
|
},
|
|
@@ -170,7 +188,7 @@ var Button = forwardRef3(function Button2({
|
|
|
170
188
|
if (!blockKey(event)) onKeyUp?.(event);
|
|
171
189
|
},
|
|
172
190
|
children: [
|
|
173
|
-
icon6 && /* @__PURE__ */ jsx4(Icon, { name: icon6 }),
|
|
191
|
+
leading != null ? /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", style: { display: "inline-flex" }, children: leading }) : icon6 && /* @__PURE__ */ jsx4(Icon, { name: icon6 }),
|
|
174
192
|
!iconOnly && renderedChildren != null && /* @__PURE__ */ jsx4("span", { className: "ui-btn__label-slot", children: /* @__PURE__ */ jsx4("span", { className: "ui-btn__label", ref: labelRef, children: renderedChildren }) }),
|
|
175
193
|
iconRight && /* @__PURE__ */ jsx4(Icon, { name: iconRight }),
|
|
176
194
|
busy && /* @__PURE__ */ jsxs3("span", { className: "ui-btn__dots", "aria-hidden": "true", children: [
|
|
@@ -2850,6 +2868,129 @@ function ThemeToggle({ labelled = false }) {
|
|
|
2850
2868
|
] }) : button;
|
|
2851
2869
|
}
|
|
2852
2870
|
|
|
2871
|
+
// src/Success.tsx
|
|
2872
|
+
import { forwardRef as forwardRef8, useEffect as useEffect14, useRef as useRef17, useState as useState13 } from "react";
|
|
2873
|
+
import { jsx as jsx34, jsxs as jsxs29 } from "react/jsx-runtime";
|
|
2874
|
+
var SuccessCheck = forwardRef8(function SuccessCheck2({ variant = "line", className, ...rest }, ref) {
|
|
2875
|
+
const circled = variant === "circled";
|
|
2876
|
+
return /* @__PURE__ */ jsxs29(
|
|
2877
|
+
"svg",
|
|
2878
|
+
{
|
|
2879
|
+
...rest,
|
|
2880
|
+
ref,
|
|
2881
|
+
className: ["ui-sx__check", `ui-sx__check--${circled ? "circled" : "line"}`, className].filter(Boolean).join(" "),
|
|
2882
|
+
width: "24",
|
|
2883
|
+
height: "24",
|
|
2884
|
+
viewBox: "0 0 24 24",
|
|
2885
|
+
"aria-hidden": "true",
|
|
2886
|
+
focusable: "false",
|
|
2887
|
+
children: [
|
|
2888
|
+
circled && /* @__PURE__ */ jsx34("path", { className: "ui-sx__circle", d: "M22 11.08V12a10 10 0 1 1-5.93-9.14" }),
|
|
2889
|
+
/* @__PURE__ */ jsx34("path", { className: "ui-sx__tick", d: circled ? "M22 4L12 14.01l-3-3" : "M20 6L9 17l-5-5" })
|
|
2890
|
+
]
|
|
2891
|
+
}
|
|
2892
|
+
);
|
|
2893
|
+
});
|
|
2894
|
+
var SuccessPanel = forwardRef8(function SuccessPanel2({ title = "Done", sub, check = "line", className, ...rest }, ref) {
|
|
2895
|
+
const circled = check === "circled";
|
|
2896
|
+
return /* @__PURE__ */ jsxs29("div", { ...rest, ref, className: ["ui-success", className].filter(Boolean).join(" "), children: [
|
|
2897
|
+
/* @__PURE__ */ jsx34("div", { className: ["ui-success__check", circled && "ui-success__check--circled"].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx34(SuccessCheck, { variant: circled ? "circled" : "line" }) }),
|
|
2898
|
+
/* @__PURE__ */ jsx34("div", { className: "ui-success__title", children: title }),
|
|
2899
|
+
sub && /* @__PURE__ */ jsx34("div", { className: "ui-success__sub", children: sub })
|
|
2900
|
+
] });
|
|
2901
|
+
});
|
|
2902
|
+
var pieces = [
|
|
2903
|
+
[8, 0, -24, "a", 1],
|
|
2904
|
+
[20, 0.14, 40, "g", 0.8],
|
|
2905
|
+
[31, 0.06, 12, "c", 1.1],
|
|
2906
|
+
[42, 0.2, -52, "p", 0.9],
|
|
2907
|
+
[50, 0.02, 28, "a", 1],
|
|
2908
|
+
[58, 0.18, -16, "k", 0.75],
|
|
2909
|
+
[67, 0.09, 60, "g", 1.05],
|
|
2910
|
+
[76, 0.24, -36, "c", 0.85],
|
|
2911
|
+
[85, 0.05, 20, "a", 1],
|
|
2912
|
+
[92, 0.16, -48, "p", 0.9],
|
|
2913
|
+
[14, 0.3, 44, "k", 0.8],
|
|
2914
|
+
[37, 0.34, -28, "g", 1],
|
|
2915
|
+
[62, 0.28, 52, "a", 0.9],
|
|
2916
|
+
[80, 0.36, -20, "c", 1.05]
|
|
2917
|
+
];
|
|
2918
|
+
function Countdown({ seconds, label = "Redirecting", onDone }) {
|
|
2919
|
+
const [remaining, setRemaining] = useState13(seconds);
|
|
2920
|
+
const callback = useRef17(onDone);
|
|
2921
|
+
useEffect14(() => {
|
|
2922
|
+
callback.current = onDone;
|
|
2923
|
+
}, [onDone]);
|
|
2924
|
+
useEffect14(() => {
|
|
2925
|
+
let count = seconds;
|
|
2926
|
+
const timer = setInterval(() => {
|
|
2927
|
+
count -= 1;
|
|
2928
|
+
setRemaining(count);
|
|
2929
|
+
if (count === 0) {
|
|
2930
|
+
clearInterval(timer);
|
|
2931
|
+
callback.current?.();
|
|
2932
|
+
}
|
|
2933
|
+
}, 1e3);
|
|
2934
|
+
return () => clearInterval(timer);
|
|
2935
|
+
}, [seconds]);
|
|
2936
|
+
return /* @__PURE__ */ jsxs29("div", { className: "ui-sx__count", "data-sx-count": "", style: { "--sx-secs": `${seconds}s` }, children: [
|
|
2937
|
+
/* @__PURE__ */ jsx34("span", { className: "ui-sx__count-ring", "aria-hidden": "true" }),
|
|
2938
|
+
/* @__PURE__ */ jsxs29("span", { className: "ui-sx__count-text", children: [
|
|
2939
|
+
label,
|
|
2940
|
+
" in ",
|
|
2941
|
+
/* @__PURE__ */ jsx34("b", { "data-sx-num": "", children: remaining }),
|
|
2942
|
+
"s"
|
|
2943
|
+
] })
|
|
2944
|
+
] });
|
|
2945
|
+
}
|
|
2946
|
+
var Success = forwardRef8(function Success2({
|
|
2947
|
+
layout = "hero",
|
|
2948
|
+
level,
|
|
2949
|
+
check = "line",
|
|
2950
|
+
title = "All done",
|
|
2951
|
+
body,
|
|
2952
|
+
actions,
|
|
2953
|
+
confetti = false,
|
|
2954
|
+
countdown,
|
|
2955
|
+
onCountdownEnd,
|
|
2956
|
+
className,
|
|
2957
|
+
// Destructured only to keep it out of `rest`; see SuccessProps.eyebrow.
|
|
2958
|
+
eyebrow: _removedEyebrow,
|
|
2959
|
+
...rest
|
|
2960
|
+
}, ref) {
|
|
2961
|
+
void _removedEyebrow;
|
|
2962
|
+
const rank = [1, 2, 3, 4, 5, 6].includes(Number(level)) ? Number(level) : layout === "compact" ? 2 : 1;
|
|
2963
|
+
const Heading = `h${rank}`;
|
|
2964
|
+
const hasActions = Array.isArray(actions) ? actions.length > 0 : Boolean(actions);
|
|
2965
|
+
const seconds = countdown?.seconds;
|
|
2966
|
+
const duration = seconds != null && Number.isFinite(seconds) && seconds >= 1 ? Math.floor(seconds) : 5;
|
|
2967
|
+
const mark = check === "circled" ? "circled" : "line";
|
|
2968
|
+
return /* @__PURE__ */ jsxs29(
|
|
2969
|
+
"div",
|
|
2970
|
+
{
|
|
2971
|
+
role: "status",
|
|
2972
|
+
"aria-live": "polite",
|
|
2973
|
+
...rest,
|
|
2974
|
+
ref,
|
|
2975
|
+
className: ["ui-sx", `ui-sx--${layout}`, `ui-sx--check-${mark}`, confetti && "ui-sx--confetti", className].filter(Boolean).join(" "),
|
|
2976
|
+
children: [
|
|
2977
|
+
confetti && /* @__PURE__ */ jsx34("div", { className: "ui-sx__confetti", "aria-hidden": "true", children: pieces.map(
|
|
2978
|
+
([x, d, r, t, s], index) => /* @__PURE__ */ jsx34("i", { className: `ui-sx__piece ui-sx__piece--${t}`, style: { left: `${x}%`, "--sx-d": `${d}s`, "--sx-r": `${r}deg`, "--sx-s": s } }, index)
|
|
2979
|
+
) }),
|
|
2980
|
+
/* @__PURE__ */ jsxs29("div", { className: "ui-sx__inner", children: [
|
|
2981
|
+
/* @__PURE__ */ jsx34("div", { className: "ui-sx__visual", children: /* @__PURE__ */ jsx34(SuccessCheck, { variant: mark }) }),
|
|
2982
|
+
/* @__PURE__ */ jsxs29("div", { className: "ui-sx__content", children: [
|
|
2983
|
+
/* @__PURE__ */ jsx34(Heading, { className: "ui-sx__title", children: title }),
|
|
2984
|
+
body && /* @__PURE__ */ jsx34("p", { className: "ui-sx__body", children: body }),
|
|
2985
|
+
hasActions && /* @__PURE__ */ jsx34("div", { className: "ui-sx__actions", children: actions }),
|
|
2986
|
+
countdown && /* @__PURE__ */ jsx34(Countdown, { seconds: duration, label: countdown.label, onDone: onCountdownEnd }, duration)
|
|
2987
|
+
] })
|
|
2988
|
+
] })
|
|
2989
|
+
]
|
|
2990
|
+
}
|
|
2991
|
+
);
|
|
2992
|
+
});
|
|
2993
|
+
|
|
2853
2994
|
// src/index.ts
|
|
2854
2995
|
var PAGE_SIZES = KIT_PAGE_SIZES;
|
|
2855
2996
|
var DEFAULT_PAGE_SIZE3 = KIT_DEFAULT_PAGE_SIZE;
|
|
@@ -2891,6 +3032,9 @@ export {
|
|
|
2891
3032
|
Snippet,
|
|
2892
3033
|
StatBand,
|
|
2893
3034
|
StatusDot,
|
|
3035
|
+
Success,
|
|
3036
|
+
SuccessCheck,
|
|
3037
|
+
SuccessPanel,
|
|
2894
3038
|
Switch,
|
|
2895
3039
|
THEME_INIT_SCRIPT,
|
|
2896
3040
|
Tabs,
|
package/src/components/index.js
CHANGED
|
@@ -264,11 +264,13 @@ export function toast({
|
|
|
264
264
|
// a card. Two strings, no options; styles are .ui-success in styles/callout.css.
|
|
265
265
|
//
|
|
266
266
|
// The kit also publishes success() from components/success.js, which is the same
|
|
267
|
-
// idea at page size: layouts,
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
export function successPanel({ title = 'Done', sub = '' } = {}) {
|
|
271
|
-
|
|
267
|
+
// idea at page size: layouts, follow-up buttons and an auto-redirect countdown.
|
|
268
|
+
// Pick by how much of the screen the confirmation owns. The two share the check;
|
|
269
|
+
// their layout and content remain independent.
|
|
270
|
+
export function successPanel({ title = 'Done', sub = '', check = 'line' } = {}) {
|
|
271
|
+
// The circled mark is one status size everywhere, so the box narrows for it.
|
|
272
|
+
const box = check === 'circled' ? 'ui-success__check ui-success__check--circled' : 'ui-success__check';
|
|
273
|
+
return `<div class="ui-success"><div class="${box}">${successCheck(check)}</div><div class="ui-success__title">${esc(title)}</div>${sub ? `<div class="ui-success__sub">${esc(sub)}</div>` : ''}</div>`;
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
// ---- Empty state ---------------------------------------------------------
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
// Success / confirmation surface. One factory, three layouts
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// Accent-aware, but the success mark stays on the --green family.
|
|
1
|
+
// Success / confirmation surface. One factory, three layouts, two check marks,
|
|
2
|
+
// optional confetti and an optional auto-redirect countdown, every motion path
|
|
3
|
+
// reduced-motion safe. Accent-aware, but the mark stays on the --green family.
|
|
5
4
|
//
|
|
6
5
|
// container.innerHTML = success({ title, body, actions: [{ label, variant }] });
|
|
7
6
|
//
|
|
8
|
-
// The check animation is pure CSS, so string-rendered markup animates
|
|
9
|
-
//
|
|
10
|
-
// shows the ring sweep and a static number.
|
|
7
|
+
// The check animation is pure CSS, so string-rendered markup animates once
|
|
8
|
+
// mounted. A live countdown is opt-in via wireSuccess().
|
|
11
9
|
import { esc, button } from './index.js';
|
|
12
10
|
|
|
13
|
-
//
|
|
14
|
-
// the
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
// 'line' is the bare Lucide `check`, 'circled' is Lucide circle-check-big. Which
|
|
12
|
+
// is the default, and why that departs from Guidelines / Iconography:
|
|
13
|
+
// why: docs/specification.md#success-confirmations
|
|
14
|
+
// Each is written whole because the icon-sizing and glyph-stroke gates find their
|
|
15
|
+
// subjects by scanning source for a class on an `<svg …>`: a tag split across
|
|
16
|
+
// string pieces leaves both silent rather than failing.
|
|
17
|
+
const MARKS = {
|
|
18
|
+
line: '<svg class="ui-sx__check ui-sx__check--line" width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="ui-sx__tick" d="M20 6L9 17l-5-5"/></svg>',
|
|
19
|
+
circled: '<svg class="ui-sx__check ui-sx__check--circled" width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="ui-sx__circle" d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path class="ui-sx__tick" d="M22 4L12 14.01l-3-3"/></svg>',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function successCheck(variant = 'line') {
|
|
23
|
+
return Object.hasOwn(MARKS, variant) ? MARKS[variant] : MARKS.line;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// A deterministic confetti field (opt-in). Decorative + aria-hidden; each piece
|
|
@@ -60,21 +60,26 @@ function countdownEl({ seconds = 5, label = 'Redirecting' } = {}) {
|
|
|
60
60
|
// For a confirmation that stays inside the page the user is already on, the kit
|
|
61
61
|
// publishes successPanel() from components/index.js — a check, a title and one
|
|
62
62
|
// line of sub, and nothing to configure. Pick by how much of the screen the
|
|
63
|
-
// confirmation owns. The two share the
|
|
64
|
-
//
|
|
63
|
+
// confirmation owns. The two share the check, while their layout and content
|
|
64
|
+
// remain independent.
|
|
65
|
+
//
|
|
66
|
+
// Both carry one title and at most one short line under it. There is no eyebrow
|
|
67
|
+
// tier: a confirmation stacking a label, a headline and a paragraph reads as
|
|
68
|
+
// three competing voices for one outcome.
|
|
69
|
+
// why: docs/specification.md#success-confirmations
|
|
65
70
|
export function success({
|
|
66
71
|
layout = 'hero', // 'hero' | 'split' | 'compact'
|
|
67
72
|
level, // heading level of the title; see the note below
|
|
68
|
-
|
|
69
|
-
eyebrow = '',
|
|
73
|
+
check = 'line', // 'line' | 'circled' — see successCheck() above
|
|
70
74
|
title = 'All done',
|
|
71
|
-
body = '',
|
|
75
|
+
body = '', // one short line, or nothing; see the note below
|
|
72
76
|
actions = [], // [{ label, variant, href, icon, iconRight, size }]
|
|
73
77
|
confetti = false, // opt-in particle burst (reduced-motion safe)
|
|
74
78
|
countdown = null, // { seconds, label } | null
|
|
75
79
|
className = '',
|
|
76
80
|
} = {}) {
|
|
77
|
-
const
|
|
81
|
+
const mark = check === 'circled' ? 'circled' : 'line';
|
|
82
|
+
const cls = ['ui-sx', `ui-sx--${layout}`, `ui-sx--check-${mark}`, confetti && 'ui-sx--confetti', className]
|
|
78
83
|
.filter(Boolean).join(' ');
|
|
79
84
|
|
|
80
85
|
// The title's rank follows the layout, because the layout is the question
|
|
@@ -87,16 +92,6 @@ export function success({
|
|
|
87
92
|
const rank = [1, 2, 3, 4, 5, 6].includes(Number(level)) ? Number(level) : (layout === 'compact' ? 2 : 1);
|
|
88
93
|
const h = `h${rank}`;
|
|
89
94
|
|
|
90
|
-
const bd = backdrop === 'aurora'
|
|
91
|
-
? `<div class="ui-sx__aurora" aria-hidden="true">
|
|
92
|
-
<span class="ui-sx__glow ui-sx__glow--a"></span>
|
|
93
|
-
<span class="ui-sx__glow ui-sx__glow--b"></span>
|
|
94
|
-
</div>`
|
|
95
|
-
: backdrop === 'glow'
|
|
96
|
-
? `<span class="ui-glow ui-glow--green ui-sx__bg-glow" aria-hidden="true"></span>`
|
|
97
|
-
: '';
|
|
98
|
-
|
|
99
|
-
const eyebrowEl = eyebrow ? `<div class="ui-sx__eyebrow">${esc(eyebrow)}</div>` : '';
|
|
100
95
|
const bodyEl = body ? `<p class="ui-sx__body">${esc(body)}</p>` : '';
|
|
101
96
|
const actionsEl = actions.length
|
|
102
97
|
? `<div class="ui-sx__actions">${actions.map((a) => button({ size: 'md', ...a })).join('')}</div>`
|
|
@@ -104,12 +99,10 @@ export function success({
|
|
|
104
99
|
const countEl = countdown ? countdownEl(countdown) : '';
|
|
105
100
|
|
|
106
101
|
return `<div class="${esc(cls)}" role="status" aria-live="polite">
|
|
107
|
-
${bd}
|
|
108
102
|
${confetti ? confettiField() : ''}
|
|
109
103
|
<div class="ui-sx__inner">
|
|
110
|
-
<div class="ui-sx__visual">${successCheck()}</div>
|
|
104
|
+
<div class="ui-sx__visual">${successCheck(mark)}</div>
|
|
111
105
|
<div class="ui-sx__content">
|
|
112
|
-
${eyebrowEl}
|
|
113
106
|
<${h} class="ui-sx__title">${esc(title)}</${h}>
|
|
114
107
|
${bodyEl}
|
|
115
108
|
${actionsEl}
|
package/src/index.d.ts
CHANGED
|
@@ -82,7 +82,7 @@ export declare function deckTextSwitch(active?: string): string;
|
|
|
82
82
|
export declare function hlShell(raw: string): string;
|
|
83
83
|
export declare function pill(label: string, variant?: string): string;
|
|
84
84
|
export declare function statusDot(live?: boolean): string;
|
|
85
|
-
export declare function successCheck(): string;
|
|
85
|
+
export declare function successCheck(variant?: 'line' | 'circled'): string;
|
|
86
86
|
export declare function themeIcon(theme: string): string;
|
|
87
87
|
export declare function themeName(theme: string): string;
|
|
88
88
|
export declare function themeToggle(theme?: string): string;
|
package/src/styles/callout.css
CHANGED
|
@@ -123,9 +123,15 @@
|
|
|
123
123
|
}
|
|
124
124
|
.ui-success__check {
|
|
125
125
|
--sx-green: var(--chip-success-ink);
|
|
126
|
-
width:
|
|
126
|
+
width: 28px; height: 28px;
|
|
127
127
|
margin: 0 auto 14px;
|
|
128
128
|
}
|
|
129
129
|
.ui-success__check .ui-sx__check { width: 100%; height: 100%; }
|
|
130
|
+
/* `circled` is one status size everywhere, this panel included: the box narrows to
|
|
131
|
+
the 20px success() draws it at, rather than stretching a status mark to fill the
|
|
132
|
+
line mark's box. The mark fills the box, so the box is what decides. The modifier
|
|
133
|
+
is written by successPanel(), as success() writes ui-sx--check-circled on its root.
|
|
134
|
+
why: docs/specification.md#success-confirmations */
|
|
135
|
+
.ui-success__check--circled { width: 20px; height: 20px; }
|
|
130
136
|
.ui-success__title { color: var(--strong); font-size: var(--text-lg); font-weight: var(--weight-semibold); margin-bottom: 5px; }
|
|
131
137
|
.ui-success__sub { color: var(--text); font-size: var(--text-base); }
|
package/src/styles/success.css
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
/* ============================================================================
|
|
2
2
|
* Success / confirmation surface — .ui-sx*
|
|
3
|
-
* The
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* layouts (hero / split / compact) and three backdrops (aurora / glow / flat).
|
|
3
|
+
* The end of a flow: a check mark, the outcome in words, somewhere to go
|
|
4
|
+
* next, an optional confetti field and an optional auto-redirect countdown.
|
|
5
|
+
* Three layouts (hero / split / compact) and two check marks.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* the foot. Markup comes from success() in components/index
|
|
7
|
+
* The card is a plain elevated surface — no blurred glows or drifting blobs
|
|
8
|
+
* behind it. Accent-aware via the kit tokens; the mark stays on the --green
|
|
9
|
+
* family (green = success). Every motion path is reduced-motion safe — see
|
|
10
|
+
* the block at the foot. Markup comes from success() in components/index
|
|
11
|
+
* (re-exported).
|
|
12
12
|
* ========================================================================== */
|
|
13
13
|
|
|
14
|
-
.ui-sx, .ui-success__check {
|
|
15
|
-
--sx-tint: color-mix(in srgb, var(--green) 18%, transparent);
|
|
16
|
-
--sx-track: color-mix(in srgb, var(--green) 32%, transparent);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
14
|
.ui-sx {
|
|
20
15
|
--sx-green: var(--green);
|
|
21
16
|
position: relative;
|
|
@@ -26,41 +21,9 @@
|
|
|
26
21
|
isolation: isolate;
|
|
27
22
|
}
|
|
28
23
|
|
|
29
|
-
/* ---- Backdrops ----------------------------------------------------------- */
|
|
30
|
-
/* Aurora: two soft blurred blobs — a green wash under the check + an accent
|
|
31
|
-
blob for identity — so the success matches the rest of the kit's warmth. */
|
|
32
|
-
.ui-sx__aurora { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
|
|
33
|
-
.ui-sx__glow {
|
|
34
|
-
position: absolute; border-radius: 50%; filter: blur(70px); opacity: 0.9;
|
|
35
|
-
aspect-ratio: 1; will-change: transform;
|
|
36
|
-
}
|
|
37
|
-
.ui-sx__glow--a {
|
|
38
|
-
width: 58%; top: -14%; left: 50%; translate: -50% 0;
|
|
39
|
-
background: radial-gradient(circle at center, var(--sx-tint), transparent 68%);
|
|
40
|
-
animation: ui-sx-float 14s ease-in-out infinite alternate; /* motion: ambient — a background glow that drifts on its own clock; `alternate` needs a symmetric curve, which var(--ease) is not */
|
|
41
|
-
}
|
|
42
|
-
.ui-sx__glow--b {
|
|
43
|
-
width: 52%; bottom: -20%; left: 12%;
|
|
44
|
-
background: radial-gradient(circle at center,
|
|
45
|
-
color-mix(in srgb, var(--accent) 30%, transparent), transparent 68%);
|
|
46
|
-
animation: ui-sx-float 18s ease-in-out -6s infinite alternate; /* motion: ambient — the second glow, offset in period and phase so the pair never lines up */
|
|
47
|
-
}
|
|
48
|
-
:root[data-theme="light"] .ui-sx__glow { opacity: 0.7; }
|
|
49
|
-
@keyframes ui-sx-float {
|
|
50
|
-
from { transform: translate(-2%, -2%) scale(1); }
|
|
51
|
-
to { transform: translate(3%, 3%) scale(1.14); }
|
|
52
|
-
}
|
|
53
|
-
/* Glow backdrop: reuse the shared .ui-glow--green, parked behind the check. */
|
|
54
|
-
.ui-sx__bg-glow { top: -40%; left: 50%; translate: -50% 0; width: 360px; height: 360px; }
|
|
55
|
-
|
|
56
24
|
/* ---- Layout shell -------------------------------------------------------- */
|
|
57
25
|
.ui-sx__inner { position: relative; z-index: 1; }
|
|
58
26
|
.ui-sx__content { min-width: 0; }
|
|
59
|
-
.ui-sx__eyebrow {
|
|
60
|
-
/* rank: label */
|
|
61
|
-
color: var(--green); font-size: var(--text-sm); font-weight: var(--weight-medium);
|
|
62
|
-
margin-bottom: 10px;
|
|
63
|
-
}
|
|
64
27
|
.ui-sx__title {
|
|
65
28
|
color: var(--strong); font-size: var(--text-xl); font-weight: var(--weight-semibold);
|
|
66
29
|
letter-spacing: -.02em; line-height: 1.2; margin: 0;
|
|
@@ -76,7 +39,7 @@
|
|
|
76
39
|
.ui-sx--hero .ui-sx__visual { margin-bottom: 22px; }
|
|
77
40
|
.ui-sx--hero .ui-sx__body { margin-left: auto; margin-right: auto; }
|
|
78
41
|
.ui-sx--hero .ui-sx__actions { justify-content: center; }
|
|
79
|
-
.ui-sx--hero .ui-sx__check { width:
|
|
42
|
+
.ui-sx--hero .ui-sx__check { width: 56px; height: 56px; }
|
|
80
43
|
|
|
81
44
|
/* split — visual panel beside the copy */
|
|
82
45
|
.ui-sx--split .ui-sx__inner {
|
|
@@ -87,14 +50,14 @@
|
|
|
87
50
|
background: color-mix(in srgb, var(--green) 8%, transparent); /* ring-gap: inherit — decorative illustration has no nested focus target. */
|
|
88
51
|
border-right: 1px solid var(--border);
|
|
89
52
|
}
|
|
90
|
-
.ui-sx--split .ui-sx__check { width:
|
|
53
|
+
.ui-sx--split .ui-sx__check { width: 72px; height: 72px; }
|
|
91
54
|
.ui-sx--split .ui-sx__content { padding: 40px 36px; align-self: center; }
|
|
92
55
|
|
|
93
56
|
/* compact — inline, panel/toast-adjacent */
|
|
94
57
|
.ui-sx--compact { padding: 18px 20px; }
|
|
95
58
|
.ui-sx--compact .ui-sx__inner { display: flex; align-items: center; gap: 16px; }
|
|
96
59
|
.ui-sx--compact .ui-sx__visual { flex: none; }
|
|
97
|
-
.ui-sx--compact .ui-sx__check { width:
|
|
60
|
+
.ui-sx--compact .ui-sx__check { width: 28px; height: 28px; }
|
|
98
61
|
.ui-sx--compact .ui-sx__title { font-size: calc(var(--text-md) + 0.5px); }
|
|
99
62
|
.ui-sx--compact .ui-sx__body { font-size: var(--text-sm); margin-top: 3px; }
|
|
100
63
|
.ui-sx--compact .ui-sx__actions { margin-top: 0; margin-left: auto; flex: none; }
|
|
@@ -102,38 +65,29 @@
|
|
|
102
65
|
.ui-sx--compact .ui-sx__content > div:first-child,
|
|
103
66
|
.ui-sx--compact .ui-sx__title { min-width: 0; }
|
|
104
67
|
|
|
105
|
-
/* ---- The
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.ui-sx__tick {
|
|
114
|
-
fill: none; stroke: var(--sx-green); stroke-width:
|
|
68
|
+
/* ---- The check mark ------------------------------------------------------ */
|
|
69
|
+
/* Two marks, both unmodified Lucide paths at Lucide's own stroke width, in the
|
|
70
|
+
success colour. `line` is the bare check (Lucide `check`), the default. It
|
|
71
|
+
strokes itself on via dash-offset; the path is 22.6 units long, which is what
|
|
72
|
+
the dasharray is tuned to. `circled` is Lucide `circle-check-big` at 20px —
|
|
73
|
+
the kit's label size — for a confirmation that should stay quiet. It is drawn
|
|
74
|
+
at rest, because a 20px mark has nothing to choreograph. */
|
|
75
|
+
.ui-sx__check { display: block; }
|
|
76
|
+
.ui-sx__tick, .ui-sx__circle {
|
|
77
|
+
fill: none; stroke: var(--sx-green, var(--green)); stroke-width: 2;
|
|
115
78
|
stroke-linecap: round; stroke-linejoin: round;
|
|
116
|
-
stroke-dasharray: 34; stroke-dashoffset: 34;
|
|
117
|
-
animation: ui-sx-draw .5s cubic-bezier(.6,0,.35,1) .28s forwards; /* motion: choreographed — the tick draws while the disc is still settling */
|
|
118
|
-
filter: drop-shadow(0 0 5px color-mix(in srgb, var(--green) 55%, transparent));
|
|
119
|
-
}
|
|
120
|
-
/* Burst ring that expands and fades once, framing the moment. */
|
|
121
|
-
.ui-sx__ring {
|
|
122
|
-
fill: none; stroke: var(--sx-green); stroke-width: 2;
|
|
123
|
-
transform-origin: center; opacity: 0;
|
|
124
|
-
animation: ui-sx-burst .7s cubic-bezier(.3,.7,.4,1) .2s forwards; /* motion: choreographed — the ring outlives the tick so the moment ends on the frame */
|
|
125
79
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
60% { transform: scale(1.06); opacity: 1; }
|
|
130
|
-
100% { transform: scale(1); }
|
|
80
|
+
.ui-sx__check--line .ui-sx__tick {
|
|
81
|
+
stroke-dasharray: 23; stroke-dashoffset: 23;
|
|
82
|
+
animation: ui-sx-draw var(--dur-slow) var(--ease-out) forwards;
|
|
131
83
|
}
|
|
132
84
|
@keyframes ui-sx-draw { to { stroke-dashoffset: 0; } }
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
85
|
+
|
|
86
|
+
/* circled — one small size in every layout, so it reads as a status mark
|
|
87
|
+
rather than an illustration. The split panel narrows to match it. */
|
|
88
|
+
.ui-sx--check-circled .ui-sx__check { width: 20px; height: 20px; }
|
|
89
|
+
.ui-sx--check-circled.ui-sx--hero .ui-sx__visual { margin-bottom: 12px; }
|
|
90
|
+
.ui-sx--check-circled.ui-sx--split .ui-sx__inner { grid-template-columns: minmax(120px, 160px) 1fr; }
|
|
137
91
|
|
|
138
92
|
/* ---- Confetti (opt-in) --------------------------------------------------- */
|
|
139
93
|
.ui-sx__confetti { position: absolute; inset: 0; z-index: 2; pointer-events: none; overflow: hidden; }
|
|
@@ -171,12 +125,9 @@
|
|
|
171
125
|
.ui-sx__count-text { color: var(--text); font-size: var(--text-sm); }
|
|
172
126
|
.ui-sx__count-text b { color: var(--text); font-weight: var(--weight-semibold); font-variant-numeric: tabular-nums; }
|
|
173
127
|
|
|
174
|
-
/* ---- Reduced motion — static check, no
|
|
128
|
+
/* ---- Reduced motion — static check, no confetti / sweep ------------------ */
|
|
175
129
|
@media (prefers-reduced-motion: reduce) {
|
|
176
|
-
.ui-
|
|
177
|
-
.ui-sx__disc { animation: none; }
|
|
178
|
-
.ui-sx__tick { animation: none; stroke-dashoffset: 0; }
|
|
179
|
-
.ui-sx__ring { display: none; }
|
|
130
|
+
.ui-sx__check--line .ui-sx__tick { animation: none; stroke-dashoffset: 0; }
|
|
180
131
|
.ui-sx__confetti { display: none; }
|
|
181
132
|
.ui-sx__count-ring { animation: none; background: var(--surface-2); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ }
|
|
182
133
|
}
|
|
@@ -184,6 +135,7 @@
|
|
|
184
135
|
/* ---- Narrow — split stacks -------------------------------------------- */
|
|
185
136
|
@media (max-width: 560px) {
|
|
186
137
|
.ui-sx--split .ui-sx__inner { grid-template-columns: 1fr; }
|
|
138
|
+
.ui-sx--check-circled.ui-sx--split .ui-sx__inner { grid-template-columns: 1fr; }
|
|
187
139
|
.ui-sx--split .ui-sx__visual { border-right: 0; border-bottom: 1px solid var(--border); padding: 32px 24px; }
|
|
188
140
|
.ui-sx--split .ui-sx__content { padding: 28px 26px; text-align: center; }
|
|
189
141
|
.ui-sx--split .ui-sx__actions { justify-content: center; }
|