@forwardreach/saas-ui 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -4
- package/dist/components/badge.d.ts +0 -1
- package/dist/components/badge.js +0 -1
- package/dist/components/card.d.ts +15 -0
- package/dist/components/card.js +17 -0
- package/dist/components/combobox.d.ts +7 -33
- package/dist/components/combobox.js +20 -70
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.js +5 -0
- package/dist/components/option-list.d.ts +140 -0
- package/dist/components/option-list.js +142 -0
- package/dist/components/page-container.d.ts +7 -0
- package/dist/components/page-container.js +20 -0
- package/dist/components/page-header.d.ts +21 -1
- package/dist/components/page-header.js +14 -2
- package/dist/components/pill.d.ts +20 -0
- package/dist/components/pill.js +23 -0
- package/dist/components/popover.d.ts +23 -0
- package/dist/components/popover.js +32 -0
- package/dist/components/role-menu.d.ts +7 -5
- package/dist/components/role-menu.js +7 -5
- package/dist/components/select-menu.d.ts +128 -0
- package/dist/components/select-menu.js +219 -0
- package/dist/components/select.d.ts +6 -0
- package/dist/components/select.js +6 -0
- package/dist/styles/index.css +14 -0
- package/package.json +3 -2
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronDown } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../utils/cn.js";
|
|
6
|
+
import { useFormFieldProps } from "./form-field.js";
|
|
7
|
+
import { OptionListbox, defaultOptionFilter, nextActiveIndex, optionElementId, scrollOptionIntoView, } from "./option-list.js";
|
|
8
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
|
|
9
|
+
/**
|
|
10
|
+
* The option count at which `SelectMenu` shows a filter when the consumer has
|
|
11
|
+
* not said either way. Below it the whole list is on screen at once, and a
|
|
12
|
+
* filter is a row to skip past on the way to an answer already visible; above
|
|
13
|
+
* it the reader is scanning. Exported so a consumer can compare against it
|
|
14
|
+
* rather than duplicate the number.
|
|
15
|
+
*/
|
|
16
|
+
export const SELECT_MENU_SEARCH_THRESHOLD = 8;
|
|
17
|
+
// Height, text and padding are held together per size rather than split across
|
|
18
|
+
// rules, matching `Select`, so the two controls line up when swapped.
|
|
19
|
+
const CONTROL_SIZE_CLASSES = {
|
|
20
|
+
md: "h-10 py-2 pl-3 pr-3 text-sm",
|
|
21
|
+
sm: "h-8 py-1 pl-2.5 pr-2.5 text-xs",
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Single choice from a list, in a popup this package draws.
|
|
25
|
+
*
|
|
26
|
+
* The fourth answer to "choose one" here, and the one to reach for when the
|
|
27
|
+
* popup's appearance is the point: `Select` is the native control, whose popup
|
|
28
|
+
* belongs to the platform and cannot be themed past its background colour;
|
|
29
|
+
* `Combobox` is a search field for lists too long to read; `RoleMenu` is for
|
|
30
|
+
* two or three options that each need a sentence. `SelectMenu` is the `Select`
|
|
31
|
+
* slot drawn as a menu — a trigger that can read as bounded field or as plain
|
|
32
|
+
* text, an option list with headings, and a filter once the list is long
|
|
33
|
+
* enough to want one. The table in `docs/packages/saas-ui.md` says which of the
|
|
34
|
+
* four answers which case.
|
|
35
|
+
*
|
|
36
|
+
* Built on `Popover` rather than `DropdownMenu` because a menu's roving focus
|
|
37
|
+
* and typeahead both fight a filter field. Inside the popup the list runs the
|
|
38
|
+
* listbox pattern with `aria-activedescendant`: focus stays in the filter field
|
|
39
|
+
* when there is one, or on the listbox itself when there is not, and the
|
|
40
|
+
* highlight moves independently — the model `Combobox` already implements, and
|
|
41
|
+
* the same option list rendering, extracted so the two cannot drift.
|
|
42
|
+
*
|
|
43
|
+
* What it gives up, stated plainly: on a phone a native `<select>` opens the
|
|
44
|
+
* platform's own picker, and this cannot. `Select` stays for the bounded form
|
|
45
|
+
* field where that is worth more than a themed popup.
|
|
46
|
+
*/
|
|
47
|
+
export function SelectMenu({ options, value, onValueChange, variant = "control", size = "md", searchable, searchPlaceholder = "Search", emptyMessage, emptyLabel, placeholder, name, disabled, align = "start", open: openProp, defaultOpen = false, onOpenChange, className, ...ariaProps }) {
|
|
48
|
+
// An enclosing `FormField` owns the trigger's id and error wiring, as it does
|
|
49
|
+
// for `Input`, `Combobox` and `RoleMenu`.
|
|
50
|
+
const { "aria-label": ariaLabel, ...fieldProps } = useFormFieldProps(ariaProps);
|
|
51
|
+
const generatedId = React.useId();
|
|
52
|
+
const id = fieldProps.id ?? generatedId;
|
|
53
|
+
const listboxId = `${id}-listbox`;
|
|
54
|
+
const filterId = `${id}-filter`;
|
|
55
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
|
|
56
|
+
const open = openProp ?? uncontrolledOpen;
|
|
57
|
+
const [query, setQuery] = React.useState("");
|
|
58
|
+
const [activeIndex, setActiveIndex] = React.useState(-1);
|
|
59
|
+
const filterRef = React.useRef(null);
|
|
60
|
+
const listRef = React.useRef(null);
|
|
61
|
+
const triggerRef = React.useRef(null);
|
|
62
|
+
// The empty choice is a real option at the head of the list — it shares the
|
|
63
|
+
// index space, the highlight and `aria-activedescendant` — so no second
|
|
64
|
+
// keyboard path exists for it. `""` is what it reports. An empty `value` is
|
|
65
|
+
// "nothing chosen" everywhere below rather than "the empty row is chosen":
|
|
66
|
+
// the trigger shows its placeholder, no row is marked current, and the list
|
|
67
|
+
// opens with nothing highlighted.
|
|
68
|
+
const chosen = value === "" ? undefined : value;
|
|
69
|
+
const selected = options.find((option) => option.value === chosen);
|
|
70
|
+
const selectedLabel = selected?.inputLabel ?? selected?.label;
|
|
71
|
+
const offered = emptyLabel !== undefined ? [{ value: "", label: emptyLabel }, ...options] : [...options];
|
|
72
|
+
const showFilter = searchable ?? options.length >= SELECT_MENU_SEARCH_THRESHOLD;
|
|
73
|
+
// A freshly opened list starts with nothing typed and the current value
|
|
74
|
+
// highlighted, so Enter with no other key confirms what is already set and an
|
|
75
|
+
// arrow moves from it rather than from the top. The reset keys on the open
|
|
76
|
+
// state itself rather than on the trigger, because the trigger is only one
|
|
77
|
+
// way in: `defaultOpen` and a consumer flipping `open` from its own gesture —
|
|
78
|
+
// the click-to-edit row this component exists for — never touch it. Done
|
|
79
|
+
// during render, in the pattern React documents for reacting to a prop
|
|
80
|
+
// change, so the popup's first frame is already reset rather than one frame
|
|
81
|
+
// stale.
|
|
82
|
+
const [wasOpen, setWasOpen] = React.useState(false);
|
|
83
|
+
if (open !== wasOpen) {
|
|
84
|
+
setWasOpen(open);
|
|
85
|
+
if (open) {
|
|
86
|
+
setQuery("");
|
|
87
|
+
setActiveIndex(chosen === undefined ? -1 : offered.findIndex((option) => option.value === chosen));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const navigable = showFilter ? offered.filter((option) => defaultOptionFilter(option, query)) : offered;
|
|
91
|
+
const activeOption = activeIndex >= 0 ? navigable[activeIndex] : undefined;
|
|
92
|
+
function setOpen(next) {
|
|
93
|
+
if (next === open)
|
|
94
|
+
return;
|
|
95
|
+
if (openProp === undefined)
|
|
96
|
+
setUncontrolledOpen(next);
|
|
97
|
+
onOpenChange?.(next);
|
|
98
|
+
}
|
|
99
|
+
function commit(option) {
|
|
100
|
+
if (option.value !== (value ?? ""))
|
|
101
|
+
void onValueChange?.(option.value);
|
|
102
|
+
setOpen(false);
|
|
103
|
+
}
|
|
104
|
+
function moveActive(delta) {
|
|
105
|
+
if (navigable.length === 0)
|
|
106
|
+
return;
|
|
107
|
+
const next = nextActiveIndex(activeIndex, delta, navigable.length);
|
|
108
|
+
setActiveIndex(next);
|
|
109
|
+
scrollOptionIntoView(listRef.current, next);
|
|
110
|
+
}
|
|
111
|
+
function handleKeyDown(event) {
|
|
112
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
moveActive(event.key === "ArrowDown" ? 1 : -1);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (event.key === "Home" || event.key === "End") {
|
|
118
|
+
if (navigable.length === 0)
|
|
119
|
+
return;
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
const next = event.key === "Home" ? 0 : navigable.length - 1;
|
|
122
|
+
setActiveIndex(next);
|
|
123
|
+
scrollOptionIntoView(listRef.current, next);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (event.key === "Enter") {
|
|
127
|
+
// The open popup owns Enter whether or not anything is highlighted, so a
|
|
128
|
+
// stray press cannot reach the form behind the control.
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
if (activeOption)
|
|
131
|
+
commit(activeOption);
|
|
132
|
+
}
|
|
133
|
+
// Escape is left to the popover, which closes on it and returns focus to
|
|
134
|
+
// the trigger; stopping it here would keep the popup open.
|
|
135
|
+
}
|
|
136
|
+
function handleTriggerKeyDown(event) {
|
|
137
|
+
// A native `<select>` opens on either arrow, and so does `RoleMenu`; a user
|
|
138
|
+
// swapping `Select` for this keeps the key. Space and Enter are the button's
|
|
139
|
+
// own, and the open state's reset above does the rest.
|
|
140
|
+
if (open || (event.key !== "ArrowDown" && event.key !== "ArrowUp"))
|
|
141
|
+
return;
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
setOpen(true);
|
|
144
|
+
}
|
|
145
|
+
// The name carries purpose and current value, as `RoleMenu`'s does. A
|
|
146
|
+
// `FormField`'s visible label does that job better, so a consumer inside one
|
|
147
|
+
// passes no `aria-label`; one that does gets what it asked for, as it would
|
|
148
|
+
// from `RoleMenu`. With neither, the button's own text — the current value —
|
|
149
|
+
// is its name.
|
|
150
|
+
const currentText = selectedLabel ?? (typeof placeholder === "string" ? placeholder : "none");
|
|
151
|
+
const triggerLabel = ariaLabel ? `${ariaLabel} (current: ${currentText})` : undefined;
|
|
152
|
+
const activeDescendant = open && activeOption ? optionElementId(id, activeOption.value) : undefined;
|
|
153
|
+
return (_jsxs(_Fragment, { children: [name ? _jsx("input", { disabled: disabled, name: name, type: "hidden", value: value ?? "" }) : null, _jsxs(Popover, { onOpenChange: setOpen, open: open, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { ...fieldProps, "aria-controls": open ? listboxId : undefined, "aria-haspopup": "listbox", "aria-label": triggerLabel, className: cn("group/select-menu inline-flex items-center gap-1.5 text-left transition-colors focus-visible:outline-none", variant === "control"
|
|
154
|
+
? cn(
|
|
155
|
+
// `Select`'s field, so the two line up when swapped: same
|
|
156
|
+
// radius, border, shadow, sizes, and ring with an offset.
|
|
157
|
+
"w-full justify-between rounded-[var(--ssui-radius)] border border-[color:var(--ssui-border-control,var(--ssui-border))] bg-[color:var(--ssui-surface)] text-[color:var(--ssui-text)] shadow-sm focus-visible:ring-2 focus-visible:ring-[color:var(--ssui-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--ssui-bg)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-[color:var(--ssui-surface-muted)] disabled:opacity-70", CONTROL_SIZE_CLASSES[size])
|
|
158
|
+
: // No box at rest: the row's own text on the row's own background,
|
|
159
|
+
// and only as wide as that text — `w-fit`, because a grid or flex
|
|
160
|
+
// cell would otherwise stretch it, and a hover surface the width
|
|
161
|
+
// of the row reads as a bar rather than as the value lighting up.
|
|
162
|
+
// The hover surface and the chevron arrive on hover, focus and
|
|
163
|
+
// while open, and the focus ring carries no offset — an inline
|
|
164
|
+
// trigger sits in a line with a couple of pixels to give, and a
|
|
165
|
+
// ring pushed outside it lands on the lines above and below
|
|
166
|
+
// rather than on this one. The ring is 2px, so with no offset
|
|
167
|
+
// it fits exactly the gutter a dense list keeps. Disabled, the
|
|
168
|
+
// pointer gets nothing: hover is the whole signal this variant
|
|
169
|
+
// uses to say "operable", so it must not light up and grow a
|
|
170
|
+
// chevron for a trigger that will then do nothing.
|
|
171
|
+
"-mx-1 w-fit max-w-full rounded-[var(--ssui-radius-sm)] px-1 text-[color:var(--ssui-text)] hover:bg-[color:var(--ssui-overlay-hover)] focus-visible:ring-2 focus-visible:ring-[color:var(--ssui-focus-ring)] data-[state=open]:bg-[color:var(--ssui-overlay-hover)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-70", className), disabled: disabled, onKeyDown: handleTriggerKeyDown, ref: triggerRef, type: "button", children: [_jsx("span", { className: cn("min-w-0 truncate", selectedLabel === undefined && "text-[color:var(--ssui-text-subtle)]"), children: selectedLabel ?? placeholder }), _jsx(ChevronDown, { "aria-hidden": "true", className: cn("shrink-0 text-[color:var(--ssui-text-subtle)]", variant === "control"
|
|
172
|
+
? size === "sm"
|
|
173
|
+
? "size-3.5"
|
|
174
|
+
: "size-4"
|
|
175
|
+
: "size-3.5 opacity-0 transition-opacity group-hover/select-menu:opacity-100 group-focus-visible/select-menu:opacity-100 group-data-[state=open]/select-menu:opacity-100") })] }) }), _jsxs(PopoverContent, { align: align,
|
|
176
|
+
// The panel's own padding plus a row's puts an option's text 12px
|
|
177
|
+
// inside the panel, while an inline trigger's text sits 4px inside the
|
|
178
|
+
// box the panel aligns to. Pulling the panel back by the difference
|
|
179
|
+
// lands the options directly under the value they replace, so the
|
|
180
|
+
// popup reads as that value opening rather than as a panel arriving
|
|
181
|
+
// beside it. A bounded trigger's own padding already matches, so it
|
|
182
|
+
// needs no correction.
|
|
183
|
+
alignOffset: variant === "inline" ? -8 : 0, className: cn("p-1",
|
|
184
|
+
// A bounded trigger's popup starts at the field's own width, so the two
|
|
185
|
+
// read as one control rather than as a panel that happens to be near it,
|
|
186
|
+
// and grows only if an option needs more than the field has. A fixed
|
|
187
|
+
// width would truncate those options; a fixed minimum — which this used
|
|
188
|
+
// to carry for both variants — made every narrow field's popup overhang
|
|
189
|
+
// it, which is what the field-type control showed. An inline trigger is
|
|
190
|
+
// as wide as its text, which is no width for a list, so it keeps a floor
|
|
191
|
+
// of its own.
|
|
192
|
+
variant === "control" ? "min-w-[var(--radix-popover-trigger-width)]" : "min-w-56"), onCloseAutoFocus: (event) => {
|
|
193
|
+
// Focus returns to the trigger on close — unless the trigger has
|
|
194
|
+
// left the document with the popup, as it does when a consumer
|
|
195
|
+
// unmounts the control on a choice or a cancel. Returning focus to
|
|
196
|
+
// a detached element is a no-op that still pre-empts wherever the
|
|
197
|
+
// consumer then puts it, so the return is skipped and left to them.
|
|
198
|
+
if (!triggerRef.current?.isConnected)
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
}, onOpenAutoFocus: (event) => {
|
|
201
|
+
// Focus goes to whichever element runs the keyboard model — the
|
|
202
|
+
// filter field, or the listbox itself when there is none — not to
|
|
203
|
+
// the first tabbable thing Radix would pick.
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
(filterRef.current ?? listRef.current)?.focus();
|
|
206
|
+
},
|
|
207
|
+
// The panel is a listbox popup, not a dialog: the element that holds
|
|
208
|
+
// focus inside it announces itself, and a "dialog" wrapper around a
|
|
209
|
+
// select's options would be one more thing to hear on the way to them.
|
|
210
|
+
role: "presentation", children: [showFilter ? (_jsx("input", { "aria-activedescendant": activeDescendant, "aria-autocomplete": "list", "aria-controls": listboxId, "aria-expanded": "true", "aria-label": searchPlaceholder, autoComplete: "off", className: "mb-1 flex h-8 w-full rounded-[var(--ssui-radius-sm)] border border-[color:var(--ssui-border-control,var(--ssui-border))] bg-[color:var(--ssui-surface)] px-2 text-sm text-[color:var(--ssui-text)] placeholder:text-[color:var(--ssui-text-subtle)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--ssui-focus-ring)]", id: filterId, onChange: (event) => {
|
|
211
|
+
setQuery(event.target.value);
|
|
212
|
+
setActiveIndex(-1);
|
|
213
|
+
}, onKeyDown: handleKeyDown, placeholder: searchPlaceholder, ref: filterRef, role: "combobox", type: "text", value: query })) : null, _jsx(OptionListbox, { activeIndex: activeIndex, "aria-activedescendant": showFilter ? undefined : activeDescendant, "aria-label": showFilter ? undefined : ariaLabel, className: "max-h-64 overflow-y-auto outline-none",
|
|
214
|
+
// `undefined` is "the consumer said nothing"; `null` is "show
|
|
215
|
+
// nothing", which the list documents and `Combobox` honours. `??`
|
|
216
|
+
// would swallow the second, so the test is on `undefined` alone.
|
|
217
|
+
emptyMessage: emptyMessage !== undefined ? emptyMessage : showFilter ? "No matches" : "No options", id: listboxId, idPrefix: id, onActivate: setActiveIndex, onCommit: commit, onKeyDown: showFilter ? undefined : handleKeyDown, options: navigable, ref: listRef, tabIndex: showFilter ? undefined : -1, value: chosen })] })] })] }));
|
|
218
|
+
}
|
|
219
|
+
SelectMenu.displayName = "SelectMenu";
|
|
@@ -12,5 +12,11 @@ export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectE
|
|
|
12
12
|
* Styled native `<select>` matching the shared `Input` look, with a trailing
|
|
13
13
|
* chevron. Native on purpose: correct mobile behavior and form participation
|
|
14
14
|
* with zero positioning code. Participates in `FormField` wiring.
|
|
15
|
+
*
|
|
16
|
+
* The default for a bounded field in an ordinary form. Its popup belongs to the
|
|
17
|
+
* platform and cannot be themed past its background colour; where that popup is
|
|
18
|
+
* the whole of what the reader sees — a choice presented as inline text — reach
|
|
19
|
+
* for `SelectMenu` instead. The four single-choice controls and which case each
|
|
20
|
+
* answers are tabled once, in `docs/packages/saas-ui.md`.
|
|
15
21
|
*/
|
|
16
22
|
export declare const Select: React.ForwardRefExoticComponent<SelectProps & React.RefAttributes<HTMLSelectElement>>;
|
|
@@ -14,6 +14,12 @@ const SIZE_CLASSES = {
|
|
|
14
14
|
* Styled native `<select>` matching the shared `Input` look, with a trailing
|
|
15
15
|
* chevron. Native on purpose: correct mobile behavior and form participation
|
|
16
16
|
* with zero positioning code. Participates in `FormField` wiring.
|
|
17
|
+
*
|
|
18
|
+
* The default for a bounded field in an ordinary form. Its popup belongs to the
|
|
19
|
+
* platform and cannot be themed past its background colour; where that popup is
|
|
20
|
+
* the whole of what the reader sees — a choice presented as inline text — reach
|
|
21
|
+
* for `SelectMenu` instead. The four single-choice controls and which case each
|
|
22
|
+
* answers are tabled once, in `docs/packages/saas-ui.md`.
|
|
17
23
|
*/
|
|
18
24
|
export const Select = React.forwardRef(({ className, children, size = "md", ...props }, ref) => (_jsxs("div", { className: cn("relative", className), children: [_jsx("select", { ref: ref, className: cn("flex w-full cursor-pointer appearance-none rounded-[var(--ssui-radius)] border border-[color:var(--ssui-border-control,var(--ssui-border))] bg-[color:var(--ssui-surface)] text-[color:var(--ssui-text)] shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--ssui-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--ssui-bg)] disabled:cursor-not-allowed disabled:bg-[color:var(--ssui-surface-muted)] disabled:opacity-70", SIZE_CLASSES[size].select), ...useFormFieldProps(props), children: children }), _jsx(ChevronDown, { "aria-hidden": "true", className: cn("pointer-events-none absolute top-1/2 -translate-y-1/2 text-[color:var(--ssui-text-subtle)]", SIZE_CLASSES[size].chevron) })] })));
|
|
19
25
|
Select.displayName = "Select";
|
package/dist/styles/index.css
CHANGED
|
@@ -63,6 +63,20 @@
|
|
|
63
63
|
--ssui-avatar-4-text: #9d174d;
|
|
64
64
|
--ssui-avatar-5-bg: #f0f9ff;
|
|
65
65
|
--ssui-avatar-5-text: #075985;
|
|
66
|
+
/* Pill tones: a reference (a link to a record), a tag, an attribute (a field), a date, and
|
|
67
|
+
a category (a grouping tag). Each defaults to a DERIVATION of the base contract rather
|
|
68
|
+
than a literal, so a consumer that has mapped the status and accent ramps sees a
|
|
69
|
+
coherent pair in both of its themes before it maps a single pill token. */
|
|
70
|
+
--ssui-pill-reference-bg: var(--ssui-accent-subtle);
|
|
71
|
+
--ssui-pill-reference-text: var(--ssui-accent-subtle-foreground);
|
|
72
|
+
--ssui-pill-tag-bg: var(--ssui-status-info-bg);
|
|
73
|
+
--ssui-pill-tag-text: var(--ssui-status-info-text);
|
|
74
|
+
--ssui-pill-attribute-bg: var(--ssui-status-neutral-bg);
|
|
75
|
+
--ssui-pill-attribute-text: var(--ssui-status-neutral-text);
|
|
76
|
+
--ssui-pill-date-bg: var(--ssui-status-warning-bg);
|
|
77
|
+
--ssui-pill-date-text: var(--ssui-status-warning-text);
|
|
78
|
+
--ssui-pill-category-bg: var(--ssui-status-success-bg);
|
|
79
|
+
--ssui-pill-category-text: var(--ssui-status-success-text);
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
@media (prefers-reduced-motion: reduce) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardreach/saas-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Brand-neutral React UI primitives and SaaS app patterns for ForwardReach-owned business applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"@radix-ui/react-avatar": "^1.1.10",
|
|
52
52
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
53
53
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
54
|
+
"@radix-ui/react-popover": "^1.1.23",
|
|
54
55
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
55
56
|
"@radix-ui/react-separator": "^1.1.7",
|
|
56
57
|
"@radix-ui/react-slot": "^1.2.4",
|
|
@@ -72,7 +73,7 @@
|
|
|
72
73
|
"typescript": "^5.8.3",
|
|
73
74
|
"vitest": "^3.2.4"
|
|
74
75
|
},
|
|
75
|
-
"gitHead": "
|
|
76
|
+
"gitHead": "519fa3eab663bd95bba8a6dae8ac056538d25431",
|
|
76
77
|
"scripts": {
|
|
77
78
|
"build": "pnpm clean && tsc -p tsconfig.build.json && node scripts/copy-styles.mjs",
|
|
78
79
|
"dev": "node scripts/copy-styles.mjs && tsc -p tsconfig.build.json --watch",
|