aplance-ui 1.3.0 → 1.5.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/registry/components/primitives/icon/icon.tsx +4 -6
- package/registry/components/primitives/modal/modal.module.css +5 -0
- package/registry/components/primitives/modal/modal.tsx +32 -34
- package/registry/components/primitives/popover/popover-context.ts +33 -29
- package/registry/components/primitives/popover/popover.module.css +4 -2
- package/registry/components/primitives/popover/popover.tsx +150 -37
- package/registry/components/ui/accordion/accordion.module.css +52 -0
- package/registry/components/ui/accordion/accordion.tsx +97 -25
- package/registry/components/ui/avatar/avatar.tsx +2 -2
- package/registry/components/ui/breadcrumbs/breadcrumbs.module.css +9 -9
- package/registry/components/ui/button/button.module.css +43 -5
- package/registry/components/ui/button/button.tsx +2 -1
- package/registry/components/ui/copy-button/copy-button.module.css +2 -0
- package/registry/components/ui/copy-button/copy-button.tsx +5 -0
- package/registry/components/ui/data-table/data-table.module.css +786 -710
- package/registry/components/ui/data-table/data-table.tsx +1238 -1211
- package/registry/components/ui/data-table/filter-modal.tsx +86 -0
- package/registry/components/ui/data-table/filters.tsx +21 -0
- package/registry/components/ui/data-table/pagination-bar.tsx +31 -17
- package/registry/components/ui/data-table/types.ts +19 -1
- package/registry/components/ui/data-table/useDataTable.tsx +2 -2
- package/registry/components/ui/data-table/utils.ts +30 -4
- package/registry/components/ui/input-fields/common/input-field-common.module.css +8 -2
- package/registry/components/ui/input-fields/common/picker-popover-common.module.css +4 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.module.css +118 -76
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +552 -484
- package/registry/components/ui/input-fields/date-picker/date-picker.tsx +936 -933
- package/registry/components/ui/input-fields/date-picker/types.ts +2 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +8 -0
- package/registry/components/ui/input-fields/input/input.tsx +348 -348
- package/registry/components/ui/input-fields/media-input/media-input.tsx +3 -1
- package/registry/components/ui/input-fields/media-input/types.ts +8 -2
- package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +5 -5
- package/registry/components/ui/input-fields/media-input/utils.ts +18 -5
- package/registry/components/ui/scroll-rail/scroll-rail.module.css +104 -0
- package/registry/components/ui/scroll-rail/scroll-rail.tsx +159 -0
- package/registry/components/ui/tabs/tabs.module.css +8 -12
- package/registry/components/ui/tabs/tabs.tsx +94 -50
- package/registry/components/ui/tooltip/tooltip.module.css +8 -3
- package/registry/components/ui/tooltip/tooltip.tsx +16 -3
- package/registry/hooks/useMediaQuery.ts +38 -0
- package/registry/lib/string.ts +3 -3
- package/registry/registry.json +54 -4
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { IconFilter } from "@tabler/icons-react";
|
|
3
|
+
import {
|
|
4
|
+
Modal,
|
|
5
|
+
ModalBody,
|
|
6
|
+
ModalFooter,
|
|
7
|
+
ModalHeader,
|
|
8
|
+
ModalTitle,
|
|
9
|
+
} from "../../primitives/modal/modal";
|
|
10
|
+
import { Icon } from "../../primitives/icon/icon";
|
|
11
|
+
import { Button } from "../button/button";
|
|
12
|
+
import type { FilterBarProps } from "./types";
|
|
13
|
+
import s from "./data-table.module.css";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Small-screen counterpart to <FilterBar>. It keeps filtering live while moving
|
|
17
|
+
* the same field contract into a Modal bottom sheet.
|
|
18
|
+
*/
|
|
19
|
+
export const FilterModal = ({ fields, values, onChange }: FilterBarProps) => {
|
|
20
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
21
|
+
|
|
22
|
+
// Action slots (buttons/menus) render alongside the inputs; only input fields
|
|
23
|
+
// carry a value, so only they count toward the active badge + Clear-all reset.
|
|
24
|
+
const inputFields = fields.filter((field) => field.type === "field");
|
|
25
|
+
const activeCount = inputFields.filter(
|
|
26
|
+
(field) => (values[field.key] ?? "") !== "",
|
|
27
|
+
).length;
|
|
28
|
+
const clearAll = () => {
|
|
29
|
+
inputFields.forEach((field) => {
|
|
30
|
+
if ((values[field.key] ?? "") !== "") onChange(field.key, "");
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className={s.filter_modal_bar}>
|
|
36
|
+
<Button
|
|
37
|
+
type="button"
|
|
38
|
+
variant="bordered"
|
|
39
|
+
className={s.filter_trigger}
|
|
40
|
+
onClick={() => setIsOpen(true)}
|
|
41
|
+
>
|
|
42
|
+
<Icon>
|
|
43
|
+
<IconFilter />
|
|
44
|
+
</Icon>
|
|
45
|
+
Filters
|
|
46
|
+
{activeCount > 0 && <span className={s.filter_count}>{activeCount}</span>}
|
|
47
|
+
</Button>
|
|
48
|
+
|
|
49
|
+
<Modal isOpen={isOpen} onOpenChange={setIsOpen} placement="bottom" size="md">
|
|
50
|
+
<ModalHeader>
|
|
51
|
+
<ModalTitle>Filters</ModalTitle>
|
|
52
|
+
</ModalHeader>
|
|
53
|
+
<ModalBody>
|
|
54
|
+
<div className={s.filter_modal_fields}>
|
|
55
|
+
{fields.map((field, i) => (
|
|
56
|
+
<div
|
|
57
|
+
className={s.filter_modal_field}
|
|
58
|
+
key={field.type === "action" ? `act:${i}` : `k:${field.key}`}
|
|
59
|
+
>
|
|
60
|
+
{field.type === "action"
|
|
61
|
+
? field.render()
|
|
62
|
+
: field.render({
|
|
63
|
+
value: values[field.key] ?? "",
|
|
64
|
+
onChange: (v) => onChange(field.key, v),
|
|
65
|
+
})}
|
|
66
|
+
</div>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
</ModalBody>
|
|
70
|
+
<ModalFooter>
|
|
71
|
+
<Button
|
|
72
|
+
type="button"
|
|
73
|
+
variant="ghost"
|
|
74
|
+
onClick={clearAll}
|
|
75
|
+
disabled={activeCount === 0}
|
|
76
|
+
>
|
|
77
|
+
Clear all
|
|
78
|
+
</Button>
|
|
79
|
+
<Button type="button" variant="primary" onClick={() => setIsOpen(false)}>
|
|
80
|
+
Done
|
|
81
|
+
</Button>
|
|
82
|
+
</ModalFooter>
|
|
83
|
+
</Modal>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useMediaQuery } from "../../../hooks/useMediaQuery";
|
|
2
|
+
import { FilterBar } from "./filter-bar";
|
|
3
|
+
import { FilterModal } from "./filter-modal";
|
|
4
|
+
import type { FilterBarProps } from "./types";
|
|
5
|
+
|
|
6
|
+
// Below this width an inline filter row can't fit its fields (matches the
|
|
7
|
+
// `--md` custom-media token in styles/media.css): the filters collapse into a
|
|
8
|
+
// single "Filters" button and a modal instead of stacking full-height.
|
|
9
|
+
const FILTER_COMPACT_QUERY = "(max-width: 900px)";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A list's filters: the inline <FilterBar> while it fits, the <FilterModal>
|
|
13
|
+
* on a narrower screen. DataTable shows its filters through it; a list built
|
|
14
|
+
* on its own uses it the same way.
|
|
15
|
+
*/
|
|
16
|
+
export const Filters = (props: FilterBarProps) =>
|
|
17
|
+
useMediaQuery(FILTER_COMPACT_QUERY) ? (
|
|
18
|
+
<FilterModal {...props} />
|
|
19
|
+
) : (
|
|
20
|
+
<FilterBar {...props} />
|
|
21
|
+
);
|
|
@@ -8,6 +8,7 @@ import clsx from "clsx";
|
|
|
8
8
|
import { Button } from "../button/button";
|
|
9
9
|
import { CustomSelect } from "../input-fields/custom-select/custom-select";
|
|
10
10
|
import { Loader } from "../loader/loader";
|
|
11
|
+
import { ScrollRail } from "../scroll-rail/scroll-rail";
|
|
11
12
|
import type { PaginationBarProps } from "./types";
|
|
12
13
|
import s from "./data-table.module.css";
|
|
13
14
|
|
|
@@ -28,6 +29,7 @@ export const PaginationBar = <TData,>({
|
|
|
28
29
|
rowLabel,
|
|
29
30
|
rowsSelectable,
|
|
30
31
|
selectedCount,
|
|
32
|
+
label,
|
|
31
33
|
extra,
|
|
32
34
|
}: PaginationBarProps<TData>) => {
|
|
33
35
|
const pageCount = table.getPageCount();
|
|
@@ -42,6 +44,7 @@ export const PaginationBar = <TData,>({
|
|
|
42
44
|
const pageNavItems = [
|
|
43
45
|
{
|
|
44
46
|
key: "first",
|
|
47
|
+
edge: true,
|
|
45
48
|
label: "First page",
|
|
46
49
|
icon: <IconChevronsLeft size={14} stroke={2} />,
|
|
47
50
|
onClick: () => goTo(0),
|
|
@@ -63,6 +66,7 @@ export const PaginationBar = <TData,>({
|
|
|
63
66
|
},
|
|
64
67
|
{
|
|
65
68
|
key: "last",
|
|
69
|
+
edge: true,
|
|
66
70
|
label: "Last page",
|
|
67
71
|
icon: <IconChevronsRight size={14} stroke={2} />,
|
|
68
72
|
onClick: () => goTo(pageCount - 1),
|
|
@@ -111,11 +115,9 @@ export const PaginationBar = <TData,>({
|
|
|
111
115
|
);
|
|
112
116
|
|
|
113
117
|
return (
|
|
114
|
-
<div
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
>
|
|
118
|
-
{/* left - count + optional bulk-action extra slot */}
|
|
118
|
+
<div className={s.pagination_bar} aria-busy={isLoading || undefined}>
|
|
119
|
+
{/* left - the count (or the caller's label), then the extra slot,
|
|
120
|
+
which scrolls on its own while the count stays put */}
|
|
119
121
|
<div
|
|
120
122
|
className={clsx(
|
|
121
123
|
s.pagination_left,
|
|
@@ -131,18 +133,26 @@ export const PaginationBar = <TData,>({
|
|
|
131
133
|
) : (
|
|
132
134
|
<>
|
|
133
135
|
<p className={s.pagination_count}>
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
{label ??
|
|
137
|
+
(rowsSelectable && selectedCount > 0 ? (
|
|
138
|
+
<>
|
|
139
|
+
<span>{selectedCount}</span> of <span>{total}</span>{" "}
|
|
140
|
+
{rowLabel}(s) selected
|
|
141
|
+
</>
|
|
142
|
+
) : (
|
|
143
|
+
<>
|
|
144
|
+
<span>{total}</span> {rowLabel}(s)
|
|
145
|
+
</>
|
|
146
|
+
))}
|
|
144
147
|
</p>
|
|
145
|
-
{extra
|
|
148
|
+
{extra ? (
|
|
149
|
+
<ScrollRail
|
|
150
|
+
className={s.pagination_extra}
|
|
151
|
+
contentClassName={s.pagination_extra_items}
|
|
152
|
+
>
|
|
153
|
+
{extra}
|
|
154
|
+
</ScrollRail>
|
|
155
|
+
) : null}
|
|
146
156
|
</>
|
|
147
157
|
)}
|
|
148
158
|
</div>
|
|
@@ -205,7 +215,10 @@ export const PaginationBar = <TData,>({
|
|
|
205
215
|
<Loader
|
|
206
216
|
key={`${item.key}-skeleton`}
|
|
207
217
|
variant="skeleton"
|
|
208
|
-
className={
|
|
218
|
+
className={clsx(
|
|
219
|
+
s.pagination_button_skeleton,
|
|
220
|
+
item.edge && s.page_nav_edge,
|
|
221
|
+
)}
|
|
209
222
|
/>
|
|
210
223
|
))
|
|
211
224
|
: pageNavItems.map((item) => (
|
|
@@ -213,6 +226,7 @@ export const PaginationBar = <TData,>({
|
|
|
213
226
|
key={item.key}
|
|
214
227
|
variant="bordered"
|
|
215
228
|
layout="box"
|
|
229
|
+
className={item.edge ? s.page_nav_edge : undefined}
|
|
216
230
|
onClick={item.onClick}
|
|
217
231
|
disabled={item.disabled}
|
|
218
232
|
aria-label={item.label}
|
|
@@ -159,6 +159,7 @@ export interface PaginationBarProps<TData> {
|
|
|
159
159
|
rowLabel: string;
|
|
160
160
|
rowsSelectable: boolean;
|
|
161
161
|
selectedCount: number;
|
|
162
|
+
label: ReactNode;
|
|
162
163
|
extra: ReactNode;
|
|
163
164
|
}
|
|
164
165
|
|
|
@@ -298,10 +299,19 @@ export interface DataTableProps<TData extends { id: string }> {
|
|
|
298
299
|
|
|
299
300
|
/**
|
|
300
301
|
* Slot rendered inside the pagination bar next to the count text.
|
|
301
|
-
* Perfect for bulk-action buttons. Does not shift table layout
|
|
302
|
+
* Perfect for bulk-action buttons. Does not shift table layout: when it
|
|
303
|
+
* does not fit, it scrolls across on its own (a ScrollRail) while the count
|
|
304
|
+
* stays put.
|
|
302
305
|
*/
|
|
303
306
|
paginationExtra?: ReactNode;
|
|
304
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Replaces the pagination bar's count text, e.g. for a selection that
|
|
310
|
+
* reaches past the loaded rows ("All 120 files selected"). Numbers wrapped
|
|
311
|
+
* in a `<span>` are emphasised like the default count's.
|
|
312
|
+
*/
|
|
313
|
+
paginationLabel?: ReactNode;
|
|
314
|
+
|
|
305
315
|
className?: string;
|
|
306
316
|
}
|
|
307
317
|
|
|
@@ -556,6 +566,8 @@ export interface PersistArgs {
|
|
|
556
566
|
setGrouping: (next: GroupingState) => void;
|
|
557
567
|
expanded: ExpandedState;
|
|
558
568
|
setExpanded: (next: ExpandedState) => void;
|
|
569
|
+
/** Resolved default expansion - used to skip persisting it when unchanged. */
|
|
570
|
+
defaultExpanded: ExpandedState;
|
|
559
571
|
}
|
|
560
572
|
|
|
561
573
|
// ─── Snapshot / apply helpers shape ───────────────────────────────────────────
|
|
@@ -573,6 +585,12 @@ export interface SnapshotInput {
|
|
|
573
585
|
columnSizing: ColumnSizingState;
|
|
574
586
|
grouping: GroupingState;
|
|
575
587
|
expanded: ExpandedState;
|
|
588
|
+
/**
|
|
589
|
+
* Resolved default expansion (the hook's `defaultExpanded ?? true`). Used to
|
|
590
|
+
* skip persisting `expanded` when it still matches the default - otherwise the
|
|
591
|
+
* default writes a noisy `?expanded=true` even when no group was touched.
|
|
592
|
+
*/
|
|
593
|
+
defaultExpanded: ExpandedState;
|
|
576
594
|
}
|
|
577
595
|
|
|
578
596
|
/** Setters used by `applyPersistedState` to push hydrated values back into state. */
|
|
@@ -260,8 +260,7 @@ export function useDataTable<TData extends { id: string }>({
|
|
|
260
260
|
return;
|
|
261
261
|
}
|
|
262
262
|
const next = [...prev];
|
|
263
|
-
|
|
264
|
-
next.splice(targetIdx, 0, moved);
|
|
263
|
+
next.splice(targetIdx, 0, ...next.splice(draggedIdx, 1));
|
|
265
264
|
localDataRef.current = next;
|
|
266
265
|
setLocalData(next);
|
|
267
266
|
onDataChange?.(next);
|
|
@@ -334,6 +333,7 @@ export function useDataTable<TData extends { id: string }>({
|
|
|
334
333
|
setGrouping: setGroupingState,
|
|
335
334
|
expanded,
|
|
336
335
|
setExpanded: setExpandedState,
|
|
336
|
+
defaultExpanded: defaultExpanded ?? true,
|
|
337
337
|
});
|
|
338
338
|
|
|
339
339
|
// ── Derived data ────────────────────────────────────────────────────────────
|
|
@@ -147,6 +147,23 @@ export const RESERVED_PARAM_KEYS = new Set<string>(Object.values(PARAM_KEYS));
|
|
|
147
147
|
const isNonEmptyExpanded = (e: ExpandedState | undefined): boolean =>
|
|
148
148
|
e !== undefined && (e === true || Object.keys(e).length > 0);
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Structural equality for two `ExpandedState`s (each a boolean or an
|
|
152
|
+
* id -> boolean record). Lets the snapshot skip persisting `expanded` while it
|
|
153
|
+
* still matches the table's default.
|
|
154
|
+
*/
|
|
155
|
+
const expandedEquals = (
|
|
156
|
+
a: ExpandedState | undefined,
|
|
157
|
+
b: ExpandedState | undefined,
|
|
158
|
+
): boolean => {
|
|
159
|
+
if (a === b) return true;
|
|
160
|
+
if (typeof a === "boolean" || typeof b === "boolean") return false;
|
|
161
|
+
if (!a || !b) return false;
|
|
162
|
+
const aKeys = Object.keys(a);
|
|
163
|
+
if (aKeys.length !== Object.keys(b).length) return false;
|
|
164
|
+
return aKeys.every((k) => a[k] === b[k]);
|
|
165
|
+
};
|
|
166
|
+
|
|
150
167
|
/**
|
|
151
168
|
* Build a `PersistedState` snapshot from current state, omitting empty/default
|
|
152
169
|
* slices so the persisted blob (URL or storage) stays minimal.
|
|
@@ -183,7 +200,15 @@ export function snapshotPersistedState(input: SnapshotInput): PersistedState {
|
|
|
183
200
|
out.columnSizing = input.columnSizing;
|
|
184
201
|
}
|
|
185
202
|
if (input.grouping.length > 0) out.grouping = input.grouping;
|
|
186
|
-
|
|
203
|
+
// Persist expansion only when it deviates from the default - otherwise the
|
|
204
|
+
// default (`true` = all groups expanded) writes a noisy `?expanded=true` even
|
|
205
|
+
// when the user never touched a group.
|
|
206
|
+
if (
|
|
207
|
+
isNonEmptyExpanded(input.expanded) &&
|
|
208
|
+
!expandedEquals(input.expanded, input.defaultExpanded)
|
|
209
|
+
) {
|
|
210
|
+
out.expanded = input.expanded;
|
|
211
|
+
}
|
|
187
212
|
|
|
188
213
|
return out;
|
|
189
214
|
}
|
|
@@ -333,9 +358,10 @@ export function writeSearchParams(
|
|
|
333
358
|
params.delete(p(PARAM_KEYS.pageSize));
|
|
334
359
|
}
|
|
335
360
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
params.set(p(PARAM_KEYS.
|
|
361
|
+
const sort = state.sorting?.[0];
|
|
362
|
+
if (sort) {
|
|
363
|
+
params.set(p(PARAM_KEYS.sortBy), sort.id);
|
|
364
|
+
params.set(p(PARAM_KEYS.sortDir), sort.desc ? "desc" : "asc");
|
|
339
365
|
} else {
|
|
340
366
|
params.delete(p(PARAM_KEYS.sortBy));
|
|
341
367
|
params.delete(p(PARAM_KEYS.sortDir));
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
.container {
|
|
1
|
+
.container {
|
|
2
2
|
width: 100%;
|
|
3
|
-
|
|
3
|
+
/* A field takes the width its layout gives it, never its content's: a long
|
|
4
|
+
value truncates instead of pushing the field over its neighbour in a grid
|
|
5
|
+
column or a flex row. */
|
|
6
|
+
min-width: 0;
|
|
4
7
|
display: flex;
|
|
5
8
|
flex-direction: column;
|
|
6
9
|
gap: var(--spacing-2);
|
|
@@ -248,6 +251,9 @@
|
|
|
248
251
|
Keep these after .field_default so they reliably override base paddings. */
|
|
249
252
|
.field_full_width {
|
|
250
253
|
width: 100% !important;
|
|
254
|
+
/* Fills the field and no more: a button is otherwise never narrower than
|
|
255
|
+
its label (`--button-min-width`), which would defeat `.truncate_text`. */
|
|
256
|
+
--button-min-width: 0;
|
|
251
257
|
}
|
|
252
258
|
|
|
253
259
|
.field_wrapper_fit {
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
.picker_popover_trigger {
|
|
12
12
|
width: 100%;
|
|
13
|
+
/* A field's trigger takes the field's width, never its value's: Popover
|
|
14
|
+
keeps a trigger at least as wide as its content (right for a plain
|
|
15
|
+
button), which would push a long value over the next field. */
|
|
16
|
+
--popover-trigger-min-width: 0;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
.picker_popover_menu_base {
|
|
@@ -1,76 +1,118 @@
|
|
|
1
|
-
/* uses input-field-common (.container and all)
|
|
2
|
-
+ popover related stuff
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/* actual menu selector */
|
|
6
|
-
.popover_menu {
|
|
7
|
-
--picker-popover-menu-width: 100%;
|
|
8
|
-
width: 100%;
|
|
9
|
-
border-radius: calc(var(--radius-md) + var(--spacing-2));
|
|
10
|
-
|
|
11
|
-
/* Search container */
|
|
12
|
-
.search_container {
|
|
13
|
-
padding: var(--spacing-4);
|
|
14
|
-
border-bottom: 1px solid var(--color-border);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/* Options container */
|
|
18
|
-
.options_container {
|
|
19
|
-
width: 100%;
|
|
20
|
-
max-height: 16rem;
|
|
21
|
-
overflow-y: auto;
|
|
22
|
-
padding: var(--spacing-2);
|
|
23
|
-
display: flex;
|
|
24
|
-
flex-direction: column;
|
|
25
|
-
gap: var(--spacing-2);
|
|
26
|
-
|
|
27
|
-
.no_options {
|
|
28
|
-
padding: var(--spacing-4);
|
|
29
|
-
text-align: center;
|
|
30
|
-
color: var(--color-muted-foreground);
|
|
31
|
-
font-size: var(--font-size-sm);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/* Option button */
|
|
35
|
-
.option {
|
|
36
|
-
width: 100%;
|
|
37
|
-
/*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
1
|
+
/* uses input-field-common (.container and all)
|
|
2
|
+
+ popover related stuff
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* actual menu selector */
|
|
6
|
+
.popover_menu {
|
|
7
|
+
--picker-popover-menu-width: 100%;
|
|
8
|
+
width: 100%;
|
|
9
|
+
border-radius: calc(var(--radius-md) + var(--spacing-2));
|
|
10
|
+
|
|
11
|
+
/* Search container */
|
|
12
|
+
.search_container {
|
|
13
|
+
padding: var(--spacing-4);
|
|
14
|
+
border-bottom: 1px solid var(--color-border);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Options container */
|
|
18
|
+
.options_container {
|
|
19
|
+
width: 100%;
|
|
20
|
+
max-height: 16rem;
|
|
21
|
+
overflow-y: auto;
|
|
22
|
+
padding: var(--spacing-2);
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
gap: var(--spacing-2);
|
|
26
|
+
|
|
27
|
+
.no_options {
|
|
28
|
+
padding: var(--spacing-4);
|
|
29
|
+
text-align: center;
|
|
30
|
+
color: var(--color-muted-foreground);
|
|
31
|
+
font-size: var(--font-size-sm);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Option button */
|
|
35
|
+
.option {
|
|
36
|
+
width: 100%;
|
|
37
|
+
/* The list scrolls; an option keeps its height. The button base sets a
|
|
38
|
+
min-height, which would otherwise let a long list squeeze each option
|
|
39
|
+
down to it, its text spilling over the next. */
|
|
40
|
+
flex-shrink: 0;
|
|
41
|
+
/* button base is min-width: fit-content - a long label must truncate
|
|
42
|
+
inside the row, never widen it past the menu */
|
|
43
|
+
min-width: 0;
|
|
44
|
+
/* button has justify-content: space-between */
|
|
45
|
+
justify-content: flex-start;
|
|
46
|
+
/* modifying padding of actual button variant for consistency */
|
|
47
|
+
padding: var(--spacing-3) var(--spacing-4);
|
|
48
|
+
border-radius: var(--radius-md);
|
|
49
|
+
|
|
50
|
+
/* > icon */
|
|
51
|
+
|
|
52
|
+
/* The label owns the row's slack space so trailing affordances
|
|
53
|
+
(.option_delete) always sit at the row's end - layout-driven, no
|
|
54
|
+
auto-margins (those tie with the Icon primitive's own margin reset
|
|
55
|
+
and the winner would depend on CSS bundle order). */
|
|
56
|
+
.option_label {
|
|
57
|
+
flex: 1 1 auto;
|
|
58
|
+
min-width: 0;
|
|
59
|
+
text-align: start;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* With a description: two lines, so more room around them, and what
|
|
63
|
+
leads the row (a checkbox, an icon) sits at the label's line. */
|
|
64
|
+
&.option_rich {
|
|
65
|
+
padding-block: var(--spacing-5);
|
|
66
|
+
align-items: flex-start;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.option_label {
|
|
73
|
+
max-width: 100% !important;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* An option with a description: its label over a line saying what it does.
|
|
77
|
+
The pair takes the row's slack, as a label alone does, and the line wraps:
|
|
78
|
+
the button base keeps its text on one line. */
|
|
79
|
+
.option_text {
|
|
80
|
+
flex: 1 1 auto;
|
|
81
|
+
min-width: 0;
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-direction: column;
|
|
84
|
+
gap: var(--spacing-2);
|
|
85
|
+
text-align: start;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.option_description {
|
|
89
|
+
color: var(--color-muted-foreground);
|
|
90
|
+
font-size: var(--font-size-xs);
|
|
91
|
+
font-weight: var(--font-weight-normal);
|
|
92
|
+
line-height: var(--line-height-md);
|
|
93
|
+
white-space: normal;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Trailing per-option meta (option.meta). The growing .option_label pushes it
|
|
97
|
+
to the row's end; consumers own the slot's visual styling. */
|
|
98
|
+
.option_meta {
|
|
99
|
+
flex-shrink: 0;
|
|
100
|
+
display: inline-flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: var(--spacing-2);
|
|
103
|
+
margin-inline-start: var(--spacing-3);
|
|
104
|
+
white-space: nowrap;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Trailing per-option delete affordance (option.onDelete). The growing
|
|
108
|
+
.option_label pushes it to the row's end; muted until hovered, then
|
|
109
|
+
error-toned. Click is stopPropagation'd in the component so it never
|
|
110
|
+
selects the option. */
|
|
111
|
+
.option_delete {
|
|
112
|
+
flex-shrink: 0;
|
|
113
|
+
--icon-color: var(--color-muted-foreground);
|
|
114
|
+
|
|
115
|
+
&:hover {
|
|
116
|
+
--icon-color: var(--color-error-foreground);
|
|
117
|
+
}
|
|
118
|
+
}
|