@designa8/vibedesign-stripe-deepseek-2-176b 0.1.2 → 0.2.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/README.md +1 -1
- package/dist/styles.css +57 -4
- package/package.json +1 -1
- package/src/atoms/Button/Button.tsx +30 -0
- package/src/atoms/Modal/Modal.tsx +291 -94
- package/src/atoms/Modal/index.ts +6 -1
- package/src/atoms/Modal/preview.tsx +158 -76
- package/src/tokens.css +1 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @designa8/vibedesign-stripe-deepseek-2-176b
|
|
2
2
|
|
|
3
|
-
Complete design system generated by Atoms Design, version **0.
|
|
3
|
+
Complete design system generated by Atoms Design, version **0.2.0**.
|
|
4
4
|
Tokens, atoms, molecules, organisms, and templates — import from the package root;
|
|
5
5
|
never hardcode visual values in consumer code.
|
|
6
6
|
|
package/dist/styles.css
CHANGED
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
--container-3xl: 48rem;
|
|
20
20
|
--container-4xl: 56rem;
|
|
21
21
|
--container-5xl: 64rem;
|
|
22
|
-
--container-6xl: 72rem;
|
|
23
22
|
--font-weight-normal: 400;
|
|
24
23
|
--tracking-wide: 0.025em;
|
|
25
24
|
--radius-xs: 4px;
|
|
@@ -162,6 +161,8 @@
|
|
|
162
161
|
--container-dialog: 420px;
|
|
163
162
|
--container-sidebar: 240px;
|
|
164
163
|
--container-sidebar-rail: 72px;
|
|
164
|
+
--container-palette: 640px;
|
|
165
|
+
--container-dialog-md: 560px;
|
|
165
166
|
--grid-template-columns-system: repeat(12, minmax(0, 1fr));
|
|
166
167
|
}
|
|
167
168
|
}
|
|
@@ -818,6 +819,9 @@
|
|
|
818
819
|
.max-h-64 {
|
|
819
820
|
max-height: calc(var(--spacing) * 64);
|
|
820
821
|
}
|
|
822
|
+
.max-h-full {
|
|
823
|
+
max-height: 100%;
|
|
824
|
+
}
|
|
821
825
|
.min-h-0 {
|
|
822
826
|
min-height: 0;
|
|
823
827
|
}
|
|
@@ -926,21 +930,24 @@
|
|
|
926
930
|
.max-w-5xl {
|
|
927
931
|
max-width: var(--container-5xl);
|
|
928
932
|
}
|
|
929
|
-
.max-w-6xl {
|
|
930
|
-
max-width: var(--container-6xl);
|
|
931
|
-
}
|
|
932
933
|
.max-w-16 {
|
|
933
934
|
max-width: calc(var(--spacing) * 16);
|
|
934
935
|
}
|
|
935
936
|
.max-w-dialog {
|
|
936
937
|
max-width: var(--container-dialog);
|
|
937
938
|
}
|
|
939
|
+
.max-w-dialog-md {
|
|
940
|
+
max-width: var(--container-dialog-md);
|
|
941
|
+
}
|
|
938
942
|
.max-w-full {
|
|
939
943
|
max-width: 100%;
|
|
940
944
|
}
|
|
941
945
|
.max-w-md {
|
|
942
946
|
max-width: var(--spacing-md);
|
|
943
947
|
}
|
|
948
|
+
.max-w-palette {
|
|
949
|
+
max-width: var(--container-palette);
|
|
950
|
+
}
|
|
944
951
|
.max-w-prose {
|
|
945
952
|
max-width: 65ch;
|
|
946
953
|
}
|
|
@@ -1608,6 +1615,12 @@
|
|
|
1608
1615
|
.bg-semantic-error-soft {
|
|
1609
1616
|
background-color: var(--color-semantic-error-soft);
|
|
1610
1617
|
}
|
|
1618
|
+
.bg-semantic-error\/60 {
|
|
1619
|
+
background-color: color-mix(in srgb, #EA2261 60%, transparent);
|
|
1620
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1621
|
+
background-color: color-mix(in oklab, var(--color-semantic-error) 60%, transparent);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1611
1624
|
.bg-semantic-info-soft {
|
|
1612
1625
|
background-color: var(--color-semantic-info-soft);
|
|
1613
1626
|
}
|
|
@@ -2663,6 +2676,16 @@
|
|
|
2663
2676
|
}
|
|
2664
2677
|
}
|
|
2665
2678
|
}
|
|
2679
|
+
.hover\:bg-semantic-error\/60 {
|
|
2680
|
+
&:hover {
|
|
2681
|
+
@media (hover: hover) {
|
|
2682
|
+
background-color: color-mix(in srgb, #EA2261 60%, transparent);
|
|
2683
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
2684
|
+
background-color: color-mix(in oklab, var(--color-semantic-error) 60%, transparent);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2666
2689
|
.hover\:bg-surface-soft {
|
|
2667
2690
|
&:hover {
|
|
2668
2691
|
@media (hover: hover) {
|
|
@@ -2733,6 +2756,13 @@
|
|
|
2733
2756
|
}
|
|
2734
2757
|
}
|
|
2735
2758
|
}
|
|
2759
|
+
.hover\:text-semantic-error {
|
|
2760
|
+
&:hover {
|
|
2761
|
+
@media (hover: hover) {
|
|
2762
|
+
color: var(--color-semantic-error);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2736
2766
|
.hover\:underline {
|
|
2737
2767
|
&:hover {
|
|
2738
2768
|
@media (hover: hover) {
|
|
@@ -2787,6 +2817,11 @@
|
|
|
2787
2817
|
color: var(--color-primary);
|
|
2788
2818
|
}
|
|
2789
2819
|
}
|
|
2820
|
+
.focus-visible\:text-semantic-error {
|
|
2821
|
+
&:focus-visible {
|
|
2822
|
+
color: var(--color-semantic-error);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2790
2825
|
.focus-visible\:ring-0 {
|
|
2791
2826
|
&:focus-visible {
|
|
2792
2827
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
|
@@ -2913,6 +2948,11 @@
|
|
|
2913
2948
|
border-color: var(--color-primary-active);
|
|
2914
2949
|
}
|
|
2915
2950
|
}
|
|
2951
|
+
.active\:border-semantic-error {
|
|
2952
|
+
&:active {
|
|
2953
|
+
border-color: var(--color-semantic-error);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2916
2956
|
.active\:border-transparent {
|
|
2917
2957
|
&:active {
|
|
2918
2958
|
border-color: transparent;
|
|
@@ -2944,6 +2984,11 @@
|
|
|
2944
2984
|
}
|
|
2945
2985
|
}
|
|
2946
2986
|
}
|
|
2987
|
+
.active\:bg-semantic-error {
|
|
2988
|
+
&:active {
|
|
2989
|
+
background-color: var(--color-semantic-error);
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2947
2992
|
.active\:bg-surface-soft {
|
|
2948
2993
|
&:active {
|
|
2949
2994
|
background-color: var(--color-surface-soft);
|
|
@@ -3039,6 +3084,14 @@
|
|
|
3039
3084
|
}
|
|
3040
3085
|
}
|
|
3041
3086
|
}
|
|
3087
|
+
.disabled\:bg-semantic-error\/60 {
|
|
3088
|
+
&:disabled {
|
|
3089
|
+
background-color: color-mix(in srgb, #EA2261 60%, transparent);
|
|
3090
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
3091
|
+
background-color: color-mix(in oklab, var(--color-semantic-error) 60%, transparent);
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3042
3095
|
.disabled\:bg-surface-soft {
|
|
3043
3096
|
&:disabled {
|
|
3044
3097
|
background-color: var(--color-surface-soft);
|
package/package.json
CHANGED
|
@@ -3,6 +3,8 @@ import * as React from "react";
|
|
|
3
3
|
type ButtonVariant =
|
|
4
4
|
| "primary"
|
|
5
5
|
| "primary-soft"
|
|
6
|
+
| "danger"
|
|
7
|
+
| "danger-soft"
|
|
6
8
|
| "secondary"
|
|
7
9
|
| "outline"
|
|
8
10
|
| "outline-soft"
|
|
@@ -88,6 +90,21 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|
|
88
90
|
"focus-visible:bg-canvas focus-visible:text-primary focus-visible:border-primary focus-visible:ring-focus-blue/40",
|
|
89
91
|
"disabled:bg-primary-disabled disabled:text-muted disabled:border-transparent",
|
|
90
92
|
),
|
|
93
|
+
/**
|
|
94
|
+
* The destructive twin of `primary`: the same filled geometry on the error
|
|
95
|
+
* hue, for the action that removes money, deletes a record, or ends access.
|
|
96
|
+
* Hover inverts to the error outline exactly like the brand fill, so the
|
|
97
|
+
* paired footer keeps one press grammar whichever tone the commit wears.
|
|
98
|
+
*/
|
|
99
|
+
danger: cn(
|
|
100
|
+
focusRing,
|
|
101
|
+
"justify-center rounded-xl active:scale-[0.98]",
|
|
102
|
+
"border-2 bg-semantic-error text-on-primary border-transparent",
|
|
103
|
+
"hover:bg-canvas hover:text-semantic-error hover:border-semantic-error",
|
|
104
|
+
"active:bg-semantic-error active:text-on-primary active:border-semantic-error",
|
|
105
|
+
"focus-visible:bg-canvas focus-visible:text-semantic-error focus-visible:border-semantic-error focus-visible:ring-focus-blue/40",
|
|
106
|
+
"disabled:bg-primary-disabled disabled:text-muted disabled:border-transparent",
|
|
107
|
+
),
|
|
91
108
|
/**
|
|
92
109
|
* The same filled pill at reduced strength: the action is committed and the
|
|
93
110
|
* request is in flight, so the control keeps its size, its label, and the
|
|
@@ -102,6 +119,19 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|
|
102
119
|
"focus-visible:ring-focus-blue/40",
|
|
103
120
|
"disabled:pointer-events-none disabled:border-transparent disabled:bg-primary/60 disabled:text-on-primary",
|
|
104
121
|
),
|
|
122
|
+
/**
|
|
123
|
+
* The dimmed destructive twin, used while a destructive request is in
|
|
124
|
+
* flight: it holds the pill's size and label, keeps the error hue, and pins
|
|
125
|
+
* the fill so a working control can never be hovered brighter.
|
|
126
|
+
*/
|
|
127
|
+
"danger-soft": cn(
|
|
128
|
+
focusRing,
|
|
129
|
+
"justify-center rounded-xl active:scale-[0.98]",
|
|
130
|
+
"border-transparent bg-semantic-error/60 text-on-primary",
|
|
131
|
+
"hover:bg-semantic-error/60 hover:text-on-primary hover:border-transparent",
|
|
132
|
+
"focus-visible:ring-focus-blue/40",
|
|
133
|
+
"disabled:pointer-events-none disabled:border-transparent disabled:bg-semantic-error/60 disabled:text-on-primary",
|
|
134
|
+
),
|
|
105
135
|
// Persistent outline treatment.
|
|
106
136
|
secondary: cn(
|
|
107
137
|
focusRing,
|
|
@@ -14,16 +14,36 @@ const REASONS: DropdownOption[] = [
|
|
|
14
14
|
{ value: "other", label: "Other" },
|
|
15
15
|
];
|
|
16
16
|
|
|
17
|
+
/** Everything the focus trap may move to, in DOM order. */
|
|
18
|
+
const FOCUSABLE =
|
|
19
|
+
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
20
|
+
|
|
21
|
+
/** The money line of the default refund body, kept out of the props default so
|
|
22
|
+
* a caller that drops the reason control can also drop the amount. */
|
|
23
|
+
const DEFAULT_AMOUNT = "$89.00";
|
|
24
|
+
|
|
17
25
|
/**
|
|
18
26
|
* The dismiss layer is pinned inline rather than with a utility: the Button's
|
|
19
27
|
* own geometry pins `position: relative`, and a style declaration is the only
|
|
20
28
|
* thing that reliably wins over it without a second set of position classes.
|
|
29
|
+
* Inside a host panel it is pinned to that panel instead of the viewport, so the
|
|
30
|
+
* dimming stops at the panel's edge.
|
|
21
31
|
*/
|
|
22
|
-
const
|
|
23
|
-
position: "fixed",
|
|
32
|
+
const scrimStyle = (contained: boolean): React.CSSProperties => ({
|
|
33
|
+
position: contained ? "absolute" : "fixed",
|
|
24
34
|
inset: 0,
|
|
25
35
|
background: "color-mix(in srgb, var(--color-scrim) 50%, transparent)",
|
|
26
|
-
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const sizeStyles = {
|
|
39
|
+
sm: "max-w-dialog",
|
|
40
|
+
md: "max-w-dialog-md",
|
|
41
|
+
lg: "max-w-palette",
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
export type ModalSize = keyof typeof sizeStyles;
|
|
45
|
+
export type ModalConfirmTone = "primary" | "danger";
|
|
46
|
+
export type ModalPlacement = "center" | "top";
|
|
27
47
|
|
|
28
48
|
export interface ModalProps
|
|
29
49
|
extends Omit<
|
|
@@ -40,7 +60,34 @@ export interface ModalProps
|
|
|
40
60
|
title?: string;
|
|
41
61
|
/** Supporting line beneath the heading. */
|
|
42
62
|
description?: string;
|
|
43
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Replaces the whole default body (amount + reason). The body scrolls between
|
|
65
|
+
* the pinned header and the pinned footer, so a long form never pushes the
|
|
66
|
+
* actions out of the viewport.
|
|
67
|
+
*/
|
|
68
|
+
children?: React.ReactNode;
|
|
69
|
+
/**
|
|
70
|
+
* Replaces the default Cancel / Confirm pair. Pass `null` to drop the footer
|
|
71
|
+
* entirely — a command palette has nowhere to put actions.
|
|
72
|
+
*/
|
|
73
|
+
footer?: React.ReactNode | null;
|
|
74
|
+
/** Panel width: `sm` dialog (420), `md` form (560), `lg` palette (640). */
|
|
75
|
+
size?: ModalSize;
|
|
76
|
+
/**
|
|
77
|
+
* `center` is the transactional dialog. `top` anchors the panel near the top
|
|
78
|
+
* of the viewport, which is where a command palette belongs.
|
|
79
|
+
*/
|
|
80
|
+
placement?: ModalPlacement;
|
|
81
|
+
/**
|
|
82
|
+
* Drops the header row. A palette makes its search input the header, so the
|
|
83
|
+
* title stays the accessible name without a visible heading.
|
|
84
|
+
*/
|
|
85
|
+
hideHeader?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Money value under the description, rendered in tabular figures. It is part
|
|
88
|
+
* of the default refund body only: omitted, the default layout still shows
|
|
89
|
+
* the payment total unless the reason control is dropped as well.
|
|
90
|
+
*/
|
|
44
91
|
amount?: string;
|
|
45
92
|
/** Label above the reason control. */
|
|
46
93
|
reasonLabel?: string;
|
|
@@ -57,8 +104,14 @@ export interface ModalProps
|
|
|
57
104
|
* It thickens that one stroke and never adds a second box.
|
|
58
105
|
*/
|
|
59
106
|
reasonFocused?: boolean;
|
|
107
|
+
/** Drops the reason control from the default body, leaving the amount. */
|
|
108
|
+
hideReason?: boolean;
|
|
60
109
|
/** In-flight state: dims the reason field and swaps the confirm action. */
|
|
61
110
|
confirming?: boolean;
|
|
111
|
+
/** Tone of the committing action; `danger` paints it on the error hue. */
|
|
112
|
+
confirmTone?: ModalConfirmTone;
|
|
113
|
+
/** Holds the committing action closed until the form is valid. */
|
|
114
|
+
confirmDisabled?: boolean;
|
|
62
115
|
/** Label of the dismissive action. */
|
|
63
116
|
cancelLabel?: string;
|
|
64
117
|
/** Label of the committing action. */
|
|
@@ -69,6 +122,12 @@ export interface ModalProps
|
|
|
69
122
|
onCancel?: () => void;
|
|
70
123
|
/** Fires when the committing action is taken. */
|
|
71
124
|
onConfirm?: () => void;
|
|
125
|
+
/**
|
|
126
|
+
* Portals the dialog into this element and pins it there with `absolute`
|
|
127
|
+
* positioning instead of covering the viewport. Page scroll stays unlocked,
|
|
128
|
+
* which is what a demo of the dialog inside a panel needs.
|
|
129
|
+
*/
|
|
130
|
+
container?: HTMLElement | null;
|
|
72
131
|
/**
|
|
73
132
|
* Renders the panel in place as a static board tile instead of portalling it
|
|
74
133
|
* over a scrim. Documentation boards use it to show several states at once;
|
|
@@ -78,16 +137,19 @@ export interface ModalProps
|
|
|
78
137
|
}
|
|
79
138
|
|
|
80
139
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
140
|
+
* The system dialog. Called with no children it is still the refund
|
|
141
|
+
* confirmation: heading and dismiss glyph on one line, a muted instruction, the
|
|
83
142
|
* amount in tabular figures, one reason select, and a paired footer that reads
|
|
84
|
-
* dismiss-then-commit
|
|
143
|
+
* dismiss-then-commit. Called with children it is any dialog — a dispute form,
|
|
144
|
+
* a payment-link wizard, an invite, a ⌘K palette — keeping the same pinned
|
|
145
|
+
* header, scrolling body, and pinned footer.
|
|
85
146
|
*
|
|
86
|
-
* The overlay never scrolls. The scrim is its own
|
|
87
|
-
*
|
|
88
|
-
* its header and footer pinned, and scrolls only its body.
|
|
89
|
-
* portals its listbox to `document.body` at `--z-popover`, above this
|
|
90
|
-
* `--z-modal`, so opening it can never grow the panel or paint
|
|
147
|
+
* The overlay never scrolls. The scrim is its own layer so it covers the whole
|
|
148
|
+
* surface at every scroll position; the panel is capped to the viewport, keeps
|
|
149
|
+
* its header and footer pinned, and scrolls only its body. Any select inside
|
|
150
|
+
* the body portals its listbox to `document.body` at `--z-popover`, above this
|
|
151
|
+
* dialog's `--z-modal`, so opening it can never grow the panel or paint
|
|
152
|
+
* underneath it.
|
|
91
153
|
*/
|
|
92
154
|
export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
|
93
155
|
(
|
|
@@ -97,19 +159,28 @@ export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
|
|
97
159
|
onOpenChange,
|
|
98
160
|
title = "Refund payment",
|
|
99
161
|
description = "Refund this payment to the customer.",
|
|
100
|
-
|
|
162
|
+
children,
|
|
163
|
+
footer,
|
|
164
|
+
size = "sm",
|
|
165
|
+
placement = "center",
|
|
166
|
+
hideHeader = false,
|
|
167
|
+
amount,
|
|
101
168
|
reasonLabel = "Reason",
|
|
102
169
|
reasons = REASONS,
|
|
103
170
|
reason,
|
|
104
171
|
defaultReason = REASONS[0].value,
|
|
105
172
|
onReasonChange,
|
|
106
173
|
reasonFocused = false,
|
|
174
|
+
hideReason = false,
|
|
107
175
|
confirming,
|
|
176
|
+
confirmTone = "primary",
|
|
177
|
+
confirmDisabled = false,
|
|
108
178
|
cancelLabel = "Cancel",
|
|
109
179
|
confirmLabel = "Confirm refund",
|
|
110
180
|
confirmingLabel = "Confirming...",
|
|
111
181
|
onCancel,
|
|
112
182
|
onConfirm,
|
|
183
|
+
container,
|
|
113
184
|
inline = false,
|
|
114
185
|
className,
|
|
115
186
|
id,
|
|
@@ -122,6 +193,7 @@ export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
|
|
122
193
|
const titleId = `${rootId}-title`;
|
|
123
194
|
const descriptionId = `${rootId}-description`;
|
|
124
195
|
|
|
196
|
+
const panelRef = React.useRef<HTMLDivElement | null>(null);
|
|
125
197
|
const closeRef = React.useRef<HTMLButtonElement | null>(null);
|
|
126
198
|
const restoreFocusRef = React.useRef<HTMLElement | null>(null);
|
|
127
199
|
|
|
@@ -134,6 +206,30 @@ export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
|
|
134
206
|
const isConfirming =
|
|
135
207
|
confirming !== undefined ? confirming : internalConfirming;
|
|
136
208
|
const state = isOpen ? "open" : "closed";
|
|
209
|
+
const contained = Boolean(container);
|
|
210
|
+
/* The refund body owns the default money line; a caller that strips the
|
|
211
|
+
reason control has replaced that layout and owns the amount itself. */
|
|
212
|
+
const showDefaultBody = children === undefined;
|
|
213
|
+
const resolvedAmount =
|
|
214
|
+
amount !== undefined
|
|
215
|
+
? amount
|
|
216
|
+
: showDefaultBody && !hideReason
|
|
217
|
+
? DEFAULT_AMOUNT
|
|
218
|
+
: undefined;
|
|
219
|
+
const showFooter = footer !== null;
|
|
220
|
+
const hasCustomFooter = footer !== undefined;
|
|
221
|
+
|
|
222
|
+
const assignPanelRef = React.useCallback(
|
|
223
|
+
(node: HTMLDivElement | null) => {
|
|
224
|
+
panelRef.current = node;
|
|
225
|
+
if (typeof ref === "function") {
|
|
226
|
+
ref(node);
|
|
227
|
+
} else if (ref) {
|
|
228
|
+
(ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
[ref],
|
|
232
|
+
);
|
|
137
233
|
|
|
138
234
|
const setOpenState = React.useCallback(
|
|
139
235
|
(next: boolean) => {
|
|
@@ -157,14 +253,21 @@ export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
|
|
157
253
|
}
|
|
158
254
|
}, [isOpen]);
|
|
159
255
|
|
|
160
|
-
/* Remember where focus came from, then move it inside the panel.
|
|
256
|
+
/* Remember where focus came from, then move it inside the panel. The close
|
|
257
|
+
gutter is the usual first stop; a header-less palette focuses its input. */
|
|
161
258
|
React.useEffect(() => {
|
|
162
259
|
if (!isOpen || inline) {
|
|
163
260
|
return;
|
|
164
261
|
}
|
|
165
262
|
restoreFocusRef.current = document.activeElement as HTMLElement | null;
|
|
166
263
|
const frame = window.requestAnimationFrame(() => {
|
|
167
|
-
|
|
264
|
+
const panel = panelRef.current;
|
|
265
|
+
const target =
|
|
266
|
+
closeRef.current ??
|
|
267
|
+
panel?.querySelector<HTMLElement>(FOCUSABLE) ??
|
|
268
|
+
panel ??
|
|
269
|
+
null;
|
|
270
|
+
target?.focus({ preventScroll: true });
|
|
168
271
|
});
|
|
169
272
|
return () => window.cancelAnimationFrame(frame);
|
|
170
273
|
}, [isOpen, inline]);
|
|
@@ -183,6 +286,55 @@ export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
|
|
183
286
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
184
287
|
}, [isOpen, inline, close]);
|
|
185
288
|
|
|
289
|
+
/* A viewport dialog holds the page still. A dialog inside a panel does not. */
|
|
290
|
+
React.useEffect(() => {
|
|
291
|
+
if (!isOpen || inline || contained) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const previous = document.body.style.overflow;
|
|
295
|
+
document.body.style.overflow = "hidden";
|
|
296
|
+
return () => {
|
|
297
|
+
document.body.style.overflow = previous;
|
|
298
|
+
};
|
|
299
|
+
}, [isOpen, inline, contained]);
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Tab cycles inside the panel rather than escaping to the page behind it,
|
|
303
|
+
* and wraps at both ends. Anything the component portalled to `body` — a
|
|
304
|
+
* select's listbox — is deliberately outside this loop.
|
|
305
|
+
*/
|
|
306
|
+
const trapFocus = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
307
|
+
if (event.key !== "Tab" || inline) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const root = event.currentTarget;
|
|
311
|
+
const focusables = Array.from(
|
|
312
|
+
root.querySelectorAll<HTMLElement>(FOCUSABLE),
|
|
313
|
+
).filter((element) => element.getClientRects().length > 0);
|
|
314
|
+
if (focusables.length === 0) {
|
|
315
|
+
event.preventDefault();
|
|
316
|
+
root.focus({ preventScroll: true });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const first = focusables[0];
|
|
320
|
+
const last = focusables[focusables.length - 1];
|
|
321
|
+
const active = document.activeElement as HTMLElement | null;
|
|
322
|
+
const inside = active ? root.contains(active) : false;
|
|
323
|
+
|
|
324
|
+
if (!inside) {
|
|
325
|
+
event.preventDefault();
|
|
326
|
+
(event.shiftKey ? last : first).focus();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!event.shiftKey && active === last) {
|
|
330
|
+
event.preventDefault();
|
|
331
|
+
first.focus();
|
|
332
|
+
} else if (event.shiftKey && active === first) {
|
|
333
|
+
event.preventDefault();
|
|
334
|
+
last.focus();
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
186
338
|
const chooseReason = React.useCallback(
|
|
187
339
|
(value: string) => {
|
|
188
340
|
if (reason === undefined) {
|
|
@@ -203,17 +355,36 @@ export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
|
|
203
355
|
onConfirm?.();
|
|
204
356
|
};
|
|
205
357
|
|
|
358
|
+
const confirmVariant =
|
|
359
|
+
confirmTone === "danger"
|
|
360
|
+
? isConfirming
|
|
361
|
+
? "danger-soft"
|
|
362
|
+
: "danger"
|
|
363
|
+
: isConfirming
|
|
364
|
+
? "primary-soft"
|
|
365
|
+
: "primary";
|
|
366
|
+
|
|
206
367
|
const panel = (
|
|
207
368
|
<div
|
|
208
|
-
ref={
|
|
369
|
+
ref={assignPanelRef}
|
|
209
370
|
role="dialog"
|
|
210
|
-
aria-modal={inline ? undefined : true}
|
|
211
|
-
aria-labelledby={titleId}
|
|
212
|
-
aria-
|
|
371
|
+
aria-modal={inline || contained ? undefined : true}
|
|
372
|
+
aria-labelledby={hideHeader ? undefined : titleId}
|
|
373
|
+
aria-label={hideHeader ? title : undefined}
|
|
374
|
+
aria-describedby={
|
|
375
|
+
hideHeader || !showDefaultBody ? undefined : descriptionId
|
|
376
|
+
}
|
|
213
377
|
data-state={state}
|
|
378
|
+
tabIndex={-1}
|
|
379
|
+
onKeyDown={trapFocus}
|
|
214
380
|
className={cn(
|
|
215
|
-
"relative flex w-full
|
|
216
|
-
|
|
381
|
+
"relative flex w-full flex-col overflow-hidden",
|
|
382
|
+
sizeStyles[size],
|
|
383
|
+
contained
|
|
384
|
+
? "max-h-full"
|
|
385
|
+
: placement === "top"
|
|
386
|
+
? "max-h-[calc(88dvh-1rem)]"
|
|
387
|
+
: "max-h-[calc(100dvh-2rem)]",
|
|
217
388
|
"rounded-lg border border-hairline bg-surface shadow-overlay",
|
|
218
389
|
"transition duration-200 ease-out",
|
|
219
390
|
"data-[state=open]:translate-y-0 data-[state=open]:scale-100 data-[state=open]:opacity-100",
|
|
@@ -222,79 +393,101 @@ export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
|
|
222
393
|
)}
|
|
223
394
|
{...rest}
|
|
224
395
|
>
|
|
225
|
-
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
</div>
|
|
242
|
-
|
|
243
|
-
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-xl pt-sm">
|
|
244
|
-
<p id={descriptionId} className="text-body-md text-body">
|
|
245
|
-
{description}
|
|
246
|
-
</p>
|
|
247
|
-
|
|
248
|
-
<p
|
|
249
|
-
className="mt-lg text-display-lg text-ink [font-feature-settings:'tnum']"
|
|
250
|
-
>
|
|
251
|
-
{amount}
|
|
252
|
-
</p>
|
|
253
|
-
|
|
254
|
-
<div className="mt-lg">
|
|
255
|
-
<Dropdown
|
|
256
|
-
label={reasonLabel}
|
|
257
|
-
labelVariant="plain"
|
|
258
|
-
tone="soft"
|
|
259
|
-
options={reasons}
|
|
260
|
-
value={selectedReason}
|
|
261
|
-
onValueChange={chooseReason}
|
|
262
|
-
disabled={isConfirming}
|
|
263
|
-
forceFocus={reasonFocused}
|
|
264
|
-
ariaLabel={`${reasonLabel} for this refund`}
|
|
396
|
+
{hideHeader ? null : (
|
|
397
|
+
<div className="flex shrink-0 items-start justify-between gap-lg px-xl pt-xl">
|
|
398
|
+
<h2
|
|
399
|
+
id={titleId}
|
|
400
|
+
className="text-display-md font-weight-body-md text-ink"
|
|
401
|
+
>
|
|
402
|
+
{title}
|
|
403
|
+
</h2>
|
|
404
|
+
<Button
|
|
405
|
+
ref={closeRef}
|
|
406
|
+
variant="bare"
|
|
407
|
+
size="icon-sm"
|
|
408
|
+
aria-label="Close dialog"
|
|
409
|
+
onClick={close}
|
|
410
|
+
className="-mr-xs -mt-xs text-ink hover:text-muted"
|
|
411
|
+
leadingIcon={<Icon name="x" size={18} strokeWidth={1.75} />}
|
|
265
412
|
/>
|
|
266
413
|
</div>
|
|
267
|
-
|
|
414
|
+
)}
|
|
268
415
|
|
|
269
|
-
<div
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
416
|
+
<div
|
|
417
|
+
className={cn(
|
|
418
|
+
"min-h-0 flex-1 overflow-y-auto overscroll-contain px-xl",
|
|
419
|
+
hideHeader ? "pt-lg" : "pt-sm",
|
|
420
|
+
showFooter ? "" : "pb-xl",
|
|
421
|
+
)}
|
|
422
|
+
>
|
|
423
|
+
{showDefaultBody ? (
|
|
424
|
+
<>
|
|
425
|
+
<p id={descriptionId} className="text-body-md text-body">
|
|
426
|
+
{description}
|
|
427
|
+
</p>
|
|
428
|
+
|
|
429
|
+
{resolvedAmount === undefined ? null : (
|
|
430
|
+
<p className="mt-lg text-display-lg text-ink [font-feature-settings:'tnum']">
|
|
431
|
+
{resolvedAmount}
|
|
432
|
+
</p>
|
|
433
|
+
)}
|
|
434
|
+
|
|
435
|
+
{hideReason ? null : (
|
|
436
|
+
<div className="mt-lg">
|
|
437
|
+
<Dropdown
|
|
438
|
+
label={reasonLabel}
|
|
439
|
+
labelVariant="plain"
|
|
440
|
+
tone="soft"
|
|
441
|
+
options={reasons}
|
|
442
|
+
value={selectedReason}
|
|
443
|
+
onValueChange={chooseReason}
|
|
444
|
+
disabled={isConfirming}
|
|
445
|
+
forceFocus={reasonFocused}
|
|
446
|
+
ariaLabel={`${reasonLabel} for this refund`}
|
|
447
|
+
/>
|
|
448
|
+
</div>
|
|
449
|
+
)}
|
|
450
|
+
</>
|
|
451
|
+
) : (
|
|
452
|
+
children
|
|
453
|
+
)}
|
|
297
454
|
</div>
|
|
455
|
+
|
|
456
|
+
{showFooter ? (
|
|
457
|
+
hasCustomFooter ? (
|
|
458
|
+
<div className="shrink-0 px-xl pb-xl pt-lg">{footer}</div>
|
|
459
|
+
) : (
|
|
460
|
+
<div className="grid shrink-0 grid-cols-5 gap-md px-xl pb-xl pt-lg">
|
|
461
|
+
<Button
|
|
462
|
+
variant="outline-soft"
|
|
463
|
+
size="lg"
|
|
464
|
+
fullWidth
|
|
465
|
+
onClick={handleCancel}
|
|
466
|
+
className="col-span-2"
|
|
467
|
+
>
|
|
468
|
+
{cancelLabel}
|
|
469
|
+
</Button>
|
|
470
|
+
<Button
|
|
471
|
+
variant={confirmVariant}
|
|
472
|
+
size="lg"
|
|
473
|
+
fullWidth
|
|
474
|
+
disabled={isConfirming || confirmDisabled}
|
|
475
|
+
onClick={handleConfirm}
|
|
476
|
+
className="col-span-3"
|
|
477
|
+
leadingIcon={
|
|
478
|
+
isConfirming ? (
|
|
479
|
+
<span
|
|
480
|
+
aria-hidden="true"
|
|
481
|
+
className="size-4 shrink-0 animate-spin rounded-full border-2 border-on-primary/70 border-t-transparent"
|
|
482
|
+
/>
|
|
483
|
+
) : undefined
|
|
484
|
+
}
|
|
485
|
+
>
|
|
486
|
+
{isConfirming ? confirmingLabel : confirmLabel}
|
|
487
|
+
</Button>
|
|
488
|
+
</div>
|
|
489
|
+
)
|
|
490
|
+
) : null}
|
|
298
491
|
</div>
|
|
299
492
|
);
|
|
300
493
|
|
|
@@ -311,13 +504,17 @@ export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
|
|
311
504
|
data-state={state}
|
|
312
505
|
inert={isOpen ? undefined : true}
|
|
313
506
|
className={cn(
|
|
314
|
-
"
|
|
507
|
+
"flex justify-center",
|
|
508
|
+
contained ? "absolute inset-0" : "fixed inset-0",
|
|
509
|
+
placement === "top"
|
|
510
|
+
? "items-start px-base pt-[12dvh]"
|
|
511
|
+
: "items-center p-base",
|
|
315
512
|
"transition duration-200 ease-out",
|
|
316
513
|
"data-[state=closed]:pointer-events-none data-[state=closed]:invisible",
|
|
317
514
|
)}
|
|
318
515
|
style={{ zIndex: "var(--z-modal, 1200)" }}
|
|
319
516
|
>
|
|
320
|
-
{/* Its own
|
|
517
|
+
{/* Its own layer, so the scrim covers the whole surface. */}
|
|
321
518
|
<Button
|
|
322
519
|
type="button"
|
|
323
520
|
variant="bare"
|
|
@@ -326,7 +523,7 @@ export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
|
|
326
523
|
aria-label="Close dialog"
|
|
327
524
|
data-state={state}
|
|
328
525
|
onClick={close}
|
|
329
|
-
style={
|
|
526
|
+
style={scrimStyle(contained)}
|
|
330
527
|
className={cn(
|
|
331
528
|
"transition-opacity duration-200 ease-out",
|
|
332
529
|
"data-[state=open]:opacity-100 data-[state=closed]:opacity-0",
|
|
@@ -335,7 +532,7 @@ export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
|
|
335
532
|
|
|
336
533
|
{panel}
|
|
337
534
|
</div>,
|
|
338
|
-
document.body,
|
|
535
|
+
container ?? document.body,
|
|
339
536
|
);
|
|
340
537
|
},
|
|
341
538
|
);
|
package/src/atoms/Modal/index.ts
CHANGED
|
@@ -1,97 +1,179 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { Button } from "../Button";
|
|
3
2
|
import { Modal } from "./Modal";
|
|
3
|
+
import { Button } from "../Button";
|
|
4
|
+
import { Input } from "../Input";
|
|
5
|
+
import { Icon } from "../Icon";
|
|
4
6
|
|
|
5
|
-
const
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
id: "focus",
|
|
13
|
-
label: "FOCUS",
|
|
14
|
-
blurb: "Focused reason select control.",
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
id: "confirming",
|
|
18
|
-
label: "CONFIRMING",
|
|
19
|
-
blurb: "Confirming state after action.",
|
|
20
|
-
},
|
|
21
|
-
] as const;
|
|
7
|
+
const COMMANDS = [
|
|
8
|
+
{ icon: "search", label: "Open payment search", hint: "G then P" },
|
|
9
|
+
{ icon: "credit-card", label: "Refund a payment", hint: "R" },
|
|
10
|
+
{ icon: "user", label: "Jump to a customer", hint: "C" },
|
|
11
|
+
{ icon: "file", label: "Open dispute evidence", hint: "D" },
|
|
12
|
+
];
|
|
22
13
|
|
|
23
|
-
/**
|
|
24
|
-
* Every board renders the real dialog in place, so the amount, the reason
|
|
25
|
-
* select, and the paired footer are the same markup in each state. The tiles
|
|
26
|
-
* take the dialog's own width, so a panel is never squeezed below 420px —
|
|
27
|
-
* they simply wrap when the canvas is narrower than two of them.
|
|
28
|
-
*
|
|
29
|
-
* The last section triggers the portalled dialog: it opens over a scrim and
|
|
30
|
-
* closes on Escape, on the dismiss glyph, or on Cancel.
|
|
31
|
-
*/
|
|
32
14
|
export default function ModalPreview() {
|
|
33
|
-
const [
|
|
34
|
-
const [
|
|
35
|
-
const [
|
|
15
|
+
const [refundOpen, setRefundOpen] = React.useState(false);
|
|
16
|
+
const [dangerOpen, setDangerOpen] = React.useState(false);
|
|
17
|
+
const [formOpen, setFormOpen] = React.useState(false);
|
|
18
|
+
const [paletteOpen, setPaletteOpen] = React.useState(false);
|
|
19
|
+
const [containedOpen, setContainedOpen] = React.useState(false);
|
|
20
|
+
const [host, setHost] = React.useState<HTMLDivElement | null>(null);
|
|
21
|
+
const [amount, setAmount] = React.useState("24.00");
|
|
22
|
+
const [query, setQuery] = React.useState("");
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<h2 className="text-display-md text-ink">Modal / Dialog</h2>
|
|
41
|
-
<p className="text-body-md text-body">
|
|
42
|
-
420px dialog for refunding a payment, with amount, reason, and actions.
|
|
43
|
-
</p>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<div className="flex w-full flex-wrap items-start gap-xl">
|
|
47
|
-
{BOARDS.map((board) => (
|
|
48
|
-
<div
|
|
49
|
-
key={board.id}
|
|
50
|
-
className="flex w-full max-w-dialog flex-col gap-md"
|
|
51
|
-
>
|
|
52
|
-
<div className="flex w-full flex-col gap-xs">
|
|
53
|
-
<div className="flex w-full">
|
|
54
|
-
<span className="rounded-full bg-primary-subdued px-md py-xxs text-micro-cap text-primary-on-subdued">
|
|
55
|
-
{board.label}
|
|
56
|
-
</span>
|
|
57
|
-
</div>
|
|
58
|
-
<span className="text-body-md text-body">{board.blurb}</span>
|
|
59
|
-
</div>
|
|
24
|
+
const results = COMMANDS.filter((command) =>
|
|
25
|
+
command.label.toLowerCase().includes(query.trim().toLowerCase()),
|
|
26
|
+
);
|
|
60
27
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
28
|
+
return (
|
|
29
|
+
<div className="w-full max-w-3xl mx-auto space-y-lg p-lg">
|
|
30
|
+
<div className="flex flex-wrap items-center gap-sm">
|
|
31
|
+
<Button variant="primary" size="md" onClick={() => setRefundOpen(true)}>
|
|
32
|
+
Refund payment
|
|
33
|
+
</Button>
|
|
34
|
+
<Button
|
|
35
|
+
variant="outline-soft"
|
|
36
|
+
size="md"
|
|
37
|
+
onClick={() => setDangerOpen(true)}
|
|
38
|
+
>
|
|
39
|
+
Cancel a payout
|
|
40
|
+
</Button>
|
|
41
|
+
<Button
|
|
42
|
+
variant="outline-soft"
|
|
43
|
+
size="md"
|
|
44
|
+
onClick={() => setFormOpen(true)}
|
|
45
|
+
>
|
|
46
|
+
Edit refund amount
|
|
47
|
+
</Button>
|
|
48
|
+
<Button
|
|
49
|
+
variant="secondary"
|
|
50
|
+
size="md"
|
|
51
|
+
onClick={() => setPaletteOpen(true)}
|
|
52
|
+
>
|
|
53
|
+
Search commands
|
|
54
|
+
</Button>
|
|
68
55
|
</div>
|
|
69
56
|
|
|
70
|
-
<div
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
<div
|
|
58
|
+
ref={setHost}
|
|
59
|
+
className="relative overflow-hidden rounded-lg border border-hairline bg-canvas p-lg"
|
|
60
|
+
>
|
|
61
|
+
<p className="text-body-sm text-ink">Dialog hosted inside a panel</p>
|
|
62
|
+
<p className="mt-xs text-caption text-muted">
|
|
63
|
+
Portalled into this card and positioned absolutely, so page scroll
|
|
64
|
+
stays unlocked and the dimming stops at the panel edge.
|
|
73
65
|
</p>
|
|
74
66
|
<Button
|
|
67
|
+
variant="outline-soft"
|
|
75
68
|
size="sm"
|
|
76
|
-
|
|
77
|
-
onClick={() =>
|
|
78
|
-
setConfirming(false);
|
|
79
|
-
setOpen(true);
|
|
80
|
-
}}
|
|
69
|
+
className="mt-md"
|
|
70
|
+
onClick={() => setContainedOpen(true)}
|
|
81
71
|
>
|
|
82
|
-
|
|
72
|
+
Invite teammate
|
|
83
73
|
</Button>
|
|
74
|
+
|
|
75
|
+
<Modal
|
|
76
|
+
open={containedOpen}
|
|
77
|
+
onOpenChange={setContainedOpen}
|
|
78
|
+
container={host}
|
|
79
|
+
title="Invite teammate"
|
|
80
|
+
description="They get access to payments for this workspace."
|
|
81
|
+
hideReason
|
|
82
|
+
cancelLabel="Not now"
|
|
83
|
+
confirmLabel="Send invite"
|
|
84
|
+
/>
|
|
84
85
|
</div>
|
|
85
86
|
|
|
87
|
+
{/* The dialog with no children is still the refund confirmation. */}
|
|
86
88
|
<Modal
|
|
87
|
-
open={
|
|
88
|
-
onOpenChange={
|
|
89
|
-
|
|
90
|
-
onReasonChange={setReason}
|
|
91
|
-
confirming={confirming}
|
|
92
|
-
onConfirm={() => setConfirming(true)}
|
|
93
|
-
onCancel={() => setOpen(false)}
|
|
89
|
+
open={refundOpen}
|
|
90
|
+
onOpenChange={setRefundOpen}
|
|
91
|
+
defaultReason="customer-request"
|
|
94
92
|
/>
|
|
93
|
+
|
|
94
|
+
{/* Destructive commit on the error hue. */}
|
|
95
|
+
<Modal
|
|
96
|
+
open={dangerOpen}
|
|
97
|
+
onOpenChange={setDangerOpen}
|
|
98
|
+
title="Cancel this payout"
|
|
99
|
+
description="The payout to your bank is cancelled and the balance returns to your account."
|
|
100
|
+
amount="$1,240.00"
|
|
101
|
+
hideReason
|
|
102
|
+
confirmTone="danger"
|
|
103
|
+
confirmLabel="Cancel payout"
|
|
104
|
+
cancelLabel="Keep payout"
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
{/* Custom body, default footer, held closed until the field has a value. */}
|
|
108
|
+
<Modal
|
|
109
|
+
open={formOpen}
|
|
110
|
+
onOpenChange={setFormOpen}
|
|
111
|
+
size="md"
|
|
112
|
+
title="Edit refund amount"
|
|
113
|
+
description="The customer sees the new amount within one business day."
|
|
114
|
+
confirmLabel="Save amount"
|
|
115
|
+
confirmDisabled={amount.trim().length === 0}
|
|
116
|
+
>
|
|
117
|
+
<div className="space-y-md pt-sm">
|
|
118
|
+
<Input
|
|
119
|
+
label="Refund amount"
|
|
120
|
+
labelVariant="plain"
|
|
121
|
+
value={amount}
|
|
122
|
+
onChange={(event) => setAmount(event.target.value)}
|
|
123
|
+
helper="Up to $89.00 can be returned on this payment."
|
|
124
|
+
/>
|
|
125
|
+
<p className="text-caption text-muted">
|
|
126
|
+
Refunds are final and cannot be reversed from this panel.
|
|
127
|
+
</p>
|
|
128
|
+
</div>
|
|
129
|
+
</Modal>
|
|
130
|
+
|
|
131
|
+
{/* Command palette: top placement, wide panel, no header, no footer. */}
|
|
132
|
+
<Modal
|
|
133
|
+
open={paletteOpen}
|
|
134
|
+
onOpenChange={setPaletteOpen}
|
|
135
|
+
size="lg"
|
|
136
|
+
placement="top"
|
|
137
|
+
hideHeader
|
|
138
|
+
footer={null}
|
|
139
|
+
title="Search commands"
|
|
140
|
+
>
|
|
141
|
+
<div className="space-y-sm">
|
|
142
|
+
<Input
|
|
143
|
+
ariaLabel="Search commands"
|
|
144
|
+
placeholder="Search commands…"
|
|
145
|
+
value={query}
|
|
146
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
147
|
+
leading={<Icon name="search" size={16} />}
|
|
148
|
+
chrome="none"
|
|
149
|
+
/>
|
|
150
|
+
<ul>
|
|
151
|
+
{results.length === 0 ? (
|
|
152
|
+
<li className="px-md py-sm text-body-sm text-muted">
|
|
153
|
+
No commands match “{query}”.
|
|
154
|
+
</li>
|
|
155
|
+
) : (
|
|
156
|
+
results.map((command) => (
|
|
157
|
+
<li key={command.label}>
|
|
158
|
+
<Button
|
|
159
|
+
variant="row"
|
|
160
|
+
size="row"
|
|
161
|
+
onClick={() => setPaletteOpen(false)}
|
|
162
|
+
leadingIcon={<Icon name={command.icon} size={16} />}
|
|
163
|
+
trailingIcon={
|
|
164
|
+
<span className="text-caption text-muted-soft">
|
|
165
|
+
{command.hint}
|
|
166
|
+
</span>
|
|
167
|
+
}
|
|
168
|
+
>
|
|
169
|
+
{command.label}
|
|
170
|
+
</Button>
|
|
171
|
+
</li>
|
|
172
|
+
))
|
|
173
|
+
)}
|
|
174
|
+
</ul>
|
|
175
|
+
</div>
|
|
176
|
+
</Modal>
|
|
95
177
|
</div>
|
|
96
178
|
);
|
|
97
179
|
}
|
package/src/tokens.css
CHANGED