@apliteni/apliteni-ui 0.114.0 → 0.116.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/guidelines/colour-and-theming.md +2 -0
- package/guidelines/labels-and-titles.md +12 -0
- package/package.json +1 -1
- package/react/dist/index.d.ts +11 -2
- package/react/dist/index.js +70 -21
- package/src/components/dropdown.js +53 -19
- package/src/components/filter-bar.js +6 -3
- package/src/index.d.ts +12 -6
- package/src/logic/callout.js +9 -1
- package/src/logic/filter-bar.js +34 -16
- package/src/styles/badge.css +17 -12
- package/src/styles/dropdown.css +7 -7
- package/src/styles/filter-bar.css +55 -16
- package/src/styles/topbar.css +6 -3
- package/src/tokens/tokens.css +30 -2
|
@@ -68,3 +68,5 @@
|
|
|
68
68
|
**Don't:** Put text on grey fields, chips, headers or footers.
|
|
69
69
|
|
|
70
70
|
**Except:** The sidebar rail's current row. Its mark may be neither an accent nor an edge — an outline there means focus — and in the page shell the rail is the card surface already. It takes the quiet fill a rung off its rail, label and all. Artur, r34 of [#475](https://github.com/apliteni/apliteni-ui/issues/475).
|
|
71
|
+
|
|
72
|
+
**Except:** A status chip carries a soft fill. Where its tone reports no verdict it takes a light tint of muted ink that leaves the panel in one direction per theme, so the neutral tones read as the same family as the coloured ones.
|
|
@@ -40,6 +40,18 @@
|
|
|
40
40
|
|
|
41
41
|
**Don't:** Display “TOTAL REVENUE” above the figure.
|
|
42
42
|
|
|
43
|
+
## Say the noun once
|
|
44
|
+
|
|
45
|
+
<!-- rule: no-repeated-noun -->
|
|
46
|
+
|
|
47
|
+
**Rule:** Leave a noun out of a label its frame already names.
|
|
48
|
+
|
|
49
|
+
**Why:** Clutter hides the available action.
|
|
50
|
+
|
|
51
|
+
**Do:** The chips name the filters; the button says what it does.
|
|
52
|
+
|
|
53
|
+
**Don't:** The noun arrives twice.
|
|
54
|
+
|
|
43
55
|
## Rank card titles
|
|
44
56
|
|
|
45
57
|
<!-- rule: title-rank -->
|
package/package.json
CHANGED
package/react/dist/index.d.ts
CHANGED
|
@@ -571,17 +571,26 @@ type Filter = {
|
|
|
571
571
|
disabled?: boolean;
|
|
572
572
|
open?: boolean;
|
|
573
573
|
};
|
|
574
|
+
/** A filter the bar can put on itself: the chip's name, and the values its menu offers. */
|
|
575
|
+
type AddFilter = {
|
|
576
|
+
id: string;
|
|
577
|
+
label: string;
|
|
578
|
+
items: DropdownEntry[];
|
|
579
|
+
};
|
|
574
580
|
type FilterBarProps = {
|
|
575
581
|
filters: Filter[];
|
|
576
582
|
label?: string;
|
|
577
583
|
clearLabel?: string;
|
|
578
584
|
disabled?: boolean;
|
|
579
585
|
busy?: boolean;
|
|
586
|
+
add?: AddFilter[];
|
|
587
|
+
addLabel?: string;
|
|
580
588
|
onRemove: (id: string) => void;
|
|
581
589
|
onClear: () => void;
|
|
582
590
|
onChange: (id: string, value: string | undefined) => void;
|
|
591
|
+
onAdd?: (id: string, value: string | undefined) => void;
|
|
583
592
|
};
|
|
584
|
-
declare function FilterBar({ filters, label, clearLabel, disabled, busy, onRemove, onClear, onChange }: FilterBarProps): react.JSX.Element;
|
|
593
|
+
declare function FilterBar({ filters, label, clearLabel, add, addLabel, disabled, busy, onRemove, onClear, onChange, onAdd }: FilterBarProps): react.JSX.Element;
|
|
585
594
|
|
|
586
595
|
type KeyValueRow = {
|
|
587
596
|
label: string;
|
|
@@ -1046,4 +1055,4 @@ declare const AccentPicker: react.ForwardRefExoticComponent<Omit<HTMLAttributes<
|
|
|
1046
1055
|
declare const PAGE_SIZES: readonly number[];
|
|
1047
1056
|
declare const DEFAULT_PAGE_SIZE: number;
|
|
1048
1057
|
|
|
1049
|
-
export { type Accent, AccentPicker, type AccentPickerProps, 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, Chart, type ChartBridgeStep, type ChartBridgeTones, type ChartPeriod, type ChartProps, type ChartSeries, type ChartTone, Checkbox, type CheckboxProps, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DatePicker, type DatePickerMode, type DatePickerPreset, type DatePickerProps, type DatePickerRange, 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, FileDrop, type FileDropFile, type FileDropProps, type FileDropStatus, 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, SearchField, type SearchFieldProps, 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 };
|
|
1058
|
+
export { type Accent, AccentPicker, type AccentPickerProps, type AddFilter, 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, Chart, type ChartBridgeStep, type ChartBridgeTones, type ChartPeriod, type ChartProps, type ChartSeries, type ChartTone, Checkbox, type CheckboxProps, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DatePicker, type DatePickerMode, type DatePickerPreset, type DatePickerProps, type DatePickerRange, 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, FileDrop, type FileDropFile, type FileDropProps, type FileDropStatus, 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, SearchField, type SearchFieldProps, 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
|
@@ -1908,7 +1908,8 @@ import {
|
|
|
1908
1908
|
filterPanelFit,
|
|
1909
1909
|
transitionMs as transitionMs2,
|
|
1910
1910
|
dropdownViewportFit,
|
|
1911
|
-
dropdownHeightFit
|
|
1911
|
+
dropdownHeightFit,
|
|
1912
|
+
filterPanelRow
|
|
1912
1913
|
} from "@apliteni/apliteni-ui";
|
|
1913
1914
|
import { Fragment as Fragment5, jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1914
1915
|
var EXIT_SLACK_MS2 = 50;
|
|
@@ -2011,7 +2012,7 @@ function Dropdown({
|
|
|
2011
2012
|
};
|
|
2012
2013
|
fit();
|
|
2013
2014
|
const view = root.current?.ownerDocument?.defaultView;
|
|
2014
|
-
const row2 = root.current
|
|
2015
|
+
const row2 = filterPanelRow(root.current);
|
|
2015
2016
|
if (!row2 || !view) return;
|
|
2016
2017
|
const stop = [];
|
|
2017
2018
|
let ro = null;
|
|
@@ -2046,7 +2047,7 @@ function Dropdown({
|
|
|
2046
2047
|
opened.current = true;
|
|
2047
2048
|
return;
|
|
2048
2049
|
}
|
|
2049
|
-
if (!opened.current || !el || !root.current
|
|
2050
|
+
if (!opened.current || !el || !filterPanelRow(root.current)) return;
|
|
2050
2051
|
el.classList.add("is-closing");
|
|
2051
2052
|
let done = false;
|
|
2052
2053
|
const finish = () => {
|
|
@@ -2144,7 +2145,7 @@ function Dropdown({
|
|
|
2144
2145
|
const want = landOn.current;
|
|
2145
2146
|
landOn.current = null;
|
|
2146
2147
|
if (!open) {
|
|
2147
|
-
if (panel.current?.style && root.current?.closest(".ui-filter-bar") && !root.current
|
|
2148
|
+
if (panel.current?.style && root.current?.closest(".ui-filter-bar") && !filterPanelRow(root.current)) {
|
|
2148
2149
|
panel.current.style.minWidth = "";
|
|
2149
2150
|
}
|
|
2150
2151
|
return;
|
|
@@ -2568,18 +2569,43 @@ function Segmented({ options, value, onChange, label, appearance = "pill", size,
|
|
|
2568
2569
|
// src/FilterBar.tsx
|
|
2569
2570
|
import { useRef as useRef11, useState as useState8 } from "react";
|
|
2570
2571
|
import { filterChipText, filterChipName, filterChipUnset, filterChipItems, focusNextStop } from "@apliteni/apliteni-ui";
|
|
2571
|
-
import { jsx as jsx24, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
2572
|
-
|
|
2572
|
+
import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
2573
|
+
var SEARCH_AT = 10;
|
|
2574
|
+
var isRow2 = (entry) => entry !== "---" && !entry.separator;
|
|
2575
|
+
function FilterBar({ filters, label = "Filters", clearLabel = "Clear all", add, addLabel = "Filter", disabled = false, busy = false, onRemove, onClear, onChange, onAdd }) {
|
|
2573
2576
|
const bar = useRef11(null);
|
|
2574
2577
|
const focused = useRef11(null);
|
|
2578
|
+
const landing = useRef11(null);
|
|
2575
2579
|
const [opened, setOpened] = useState8({});
|
|
2580
|
+
const [adding, setAdding] = useState8(false);
|
|
2576
2581
|
const blocked = disabled || busy;
|
|
2582
|
+
const offer = (add || []).filter((entry) => !filters.some((filter) => filter.id === entry.id));
|
|
2583
|
+
const source = /* @__PURE__ */ new Map();
|
|
2584
|
+
const sections = offer.map((entry) => ({ label: entry.label, items: entry.items.map((item) => {
|
|
2585
|
+
if (!isRow2(item)) return item;
|
|
2586
|
+
const copy = { ...item };
|
|
2587
|
+
source.set(copy, entry.id);
|
|
2588
|
+
return copy;
|
|
2589
|
+
}) }));
|
|
2590
|
+
const options = sections.reduce((count, section) => count + section.items.filter(isRow2).length, 0);
|
|
2591
|
+
const offering = Boolean(onAdd && offer.length);
|
|
2577
2592
|
useIsoLayoutEffect(() => {
|
|
2578
2593
|
const stale = Object.keys(opened).filter((id) => !filters.some((filter) => filter.id === id && filter.open === opened[id].against));
|
|
2579
2594
|
if (stale.length) setOpened((previous) => Object.fromEntries(Object.entries(previous).filter(([id]) => !stale.includes(id))));
|
|
2595
|
+
const chips = Array.from(bar.current?.querySelectorAll("[data-filter-id]") ?? []);
|
|
2596
|
+
const asked = landing.current;
|
|
2597
|
+
landing.current = null;
|
|
2598
|
+
if (asked !== null) {
|
|
2599
|
+
const chip = chips.find((c) => c.dataset.filterId === asked && !c.disabled);
|
|
2600
|
+
const trigger = !blocked && chip?.querySelector("[data-dropdown-trigger]");
|
|
2601
|
+
if (trigger) {
|
|
2602
|
+
focused.current = null;
|
|
2603
|
+
trigger.focus();
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2580
2607
|
const prior = focused.current;
|
|
2581
2608
|
if (!prior || prior.id === null && filters.length > 0 || filters.some((f) => f.id === prior.id)) return;
|
|
2582
|
-
const chips = Array.from(bar.current?.querySelectorAll("[data-filter-id]") ?? []);
|
|
2583
2609
|
const next = chips.slice(prior.index).find((chip) => !chip.disabled) || chips.slice(0, prior.index).reverse().find((chip) => !chip.disabled);
|
|
2584
2610
|
const target = !blocked && next?.querySelector("[data-dropdown-trigger]");
|
|
2585
2611
|
focused.current = null;
|
|
@@ -2642,9 +2668,32 @@ function FilterBar({ filters, label = "Filters", clearLabel = "Clear all filters
|
|
|
2642
2668
|
},
|
|
2643
2669
|
filter.id
|
|
2644
2670
|
)),
|
|
2671
|
+
offering && /* @__PURE__ */ jsx24("div", { "data-filter-add": "", onFocus: () => {
|
|
2672
|
+
focused.current = null;
|
|
2673
|
+
}, children: /* @__PURE__ */ jsx24(
|
|
2674
|
+
Dropdown,
|
|
2675
|
+
{
|
|
2676
|
+
variant: "menu",
|
|
2677
|
+
sections,
|
|
2678
|
+
ariaLabel: addLabel,
|
|
2679
|
+
triggerContent: /* @__PURE__ */ jsxs19(Fragment6, { children: [
|
|
2680
|
+
/* @__PURE__ */ jsx24(Icon, { name: "plus" }),
|
|
2681
|
+
addLabel
|
|
2682
|
+
] }),
|
|
2683
|
+
search: options >= SEARCH_AT && { label: `Search ${label.toLowerCase()}` },
|
|
2684
|
+
open: adding && !blocked,
|
|
2685
|
+
onOpenChange: setAdding,
|
|
2686
|
+
onSelect: (value, item) => {
|
|
2687
|
+
const id = source.get(item);
|
|
2688
|
+
if (blocked || !id) return;
|
|
2689
|
+
landing.current = id;
|
|
2690
|
+
onAdd?.(id, value == null ? void 0 : String(value));
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
) }),
|
|
2645
2694
|
filters.length > 0 && /* @__PURE__ */ jsx24("span", { "data-filter-clear": "", onFocus: () => {
|
|
2646
2695
|
focused.current = { id: null, index: 0 };
|
|
2647
|
-
}, children: /* @__PURE__ */ jsx24(Button, { size: "sm", onClick: () => {
|
|
2696
|
+
}, children: /* @__PURE__ */ jsx24(Button, { size: "sm", variant: "ghost", onClick: () => {
|
|
2648
2697
|
if (!blocked) onClear();
|
|
2649
2698
|
}, children: clearLabel }) })
|
|
2650
2699
|
]
|
|
@@ -2678,7 +2727,7 @@ function DrawerSection({ title, headingLevel = 3, className, children, ...rest }
|
|
|
2678
2727
|
|
|
2679
2728
|
// src/Snippet.tsx
|
|
2680
2729
|
import { useEffect as useEffect7, useRef as useRef12, useState as useState9 } from "react";
|
|
2681
|
-
import { Fragment as
|
|
2730
|
+
import { Fragment as Fragment7, jsx as jsx26, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
2682
2731
|
function Snippet({ label = "shell", code = "", children, copy = true, reveal = false, copyLabel = "Copy code" }) {
|
|
2683
2732
|
const [status, setStatus] = useState9("idle");
|
|
2684
2733
|
const request = useRef12(0);
|
|
@@ -2706,7 +2755,7 @@ function Snippet({ label = "shell", code = "", children, copy = true, reveal = f
|
|
|
2706
2755
|
return /* @__PURE__ */ jsxs21("div", { className: reveal ? "ui-snippet ui-snippet--reveal" : "ui-snippet", children: [
|
|
2707
2756
|
/* @__PURE__ */ jsxs21("div", { className: "ui-snippet__bar", children: [
|
|
2708
2757
|
/* @__PURE__ */ jsx26("span", { children: label }),
|
|
2709
|
-
copy && /* @__PURE__ */ jsxs21(
|
|
2758
|
+
copy && /* @__PURE__ */ jsxs21(Fragment7, { children: [
|
|
2710
2759
|
/* @__PURE__ */ jsx26(
|
|
2711
2760
|
"button",
|
|
2712
2761
|
{
|
|
@@ -2728,7 +2777,7 @@ function Snippet({ label = "shell", code = "", children, copy = true, reveal = f
|
|
|
2728
2777
|
// src/Tabs.tsx
|
|
2729
2778
|
import { useEffect as useEffect8, useId as useId8, useRef as useRef13 } from "react";
|
|
2730
2779
|
import { playEntrance } from "@apliteni/apliteni-ui/motion";
|
|
2731
|
-
import { Fragment as
|
|
2780
|
+
import { Fragment as Fragment8, jsx as jsx27, jsxs as jsxs22 } from "react/jsx-runtime";
|
|
2732
2781
|
function Tabs({ items, value, onChange, label }) {
|
|
2733
2782
|
const id = useId8();
|
|
2734
2783
|
const previous = useRef13(value);
|
|
@@ -2777,7 +2826,7 @@ function Tabs({ items, value, onChange, label }) {
|
|
|
2777
2826
|
},
|
|
2778
2827
|
children: [
|
|
2779
2828
|
item.label,
|
|
2780
|
-
item.count != null && /* @__PURE__ */ jsxs22(
|
|
2829
|
+
item.count != null && /* @__PURE__ */ jsxs22(Fragment8, { children: [
|
|
2781
2830
|
" ",
|
|
2782
2831
|
item.count
|
|
2783
2832
|
] })
|
|
@@ -3004,7 +3053,7 @@ function FileField({ label, hint, error, required, disabled, onFileChange, class
|
|
|
3004
3053
|
|
|
3005
3054
|
// src/FileDrop.tsx
|
|
3006
3055
|
import { useRef as useRef16, useState as useState11 } from "react";
|
|
3007
|
-
import { Fragment as
|
|
3056
|
+
import { Fragment as Fragment9, jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
|
|
3008
3057
|
var MARKS = {
|
|
3009
3058
|
uploading: "clock",
|
|
3010
3059
|
done: "circleCheck",
|
|
@@ -3116,7 +3165,7 @@ function FileDrop({
|
|
|
3116
3165
|
children: /* @__PURE__ */ jsx30("span", { style: { width: `${file.progress}%` } })
|
|
3117
3166
|
}
|
|
3118
3167
|
)
|
|
3119
|
-
] }) : /* @__PURE__ */ jsxs25(
|
|
3168
|
+
] }) : /* @__PURE__ */ jsxs25(Fragment9, { children: [
|
|
3120
3169
|
/* @__PURE__ */ jsx30(
|
|
3121
3170
|
Button,
|
|
3122
3171
|
{
|
|
@@ -3313,7 +3362,7 @@ import { applyTheme, paletteHotkey, prism, railCollapsed, themeIcon, themeName }
|
|
|
3313
3362
|
// src/SidebarNav.tsx
|
|
3314
3363
|
import { forwardRef as forwardRef8, useEffect as useEffect12, useId as useId10, useState as useState13 } from "react";
|
|
3315
3364
|
import { ENTRANCE_FALLBACK_MS } from "@apliteni/apliteni-ui/motion";
|
|
3316
|
-
import { Fragment as
|
|
3365
|
+
import { Fragment as Fragment10, jsx as jsx33, jsxs as jsxs28 } from "react/jsx-runtime";
|
|
3317
3366
|
var badgeText = (badge) => typeof badge === "object" ? badge.text : badge;
|
|
3318
3367
|
function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
|
|
3319
3368
|
const uid = useId10();
|
|
@@ -3339,7 +3388,7 @@ function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
|
|
|
3339
3388
|
item.disabled && "is-disabled",
|
|
3340
3389
|
item.danger && "is-danger"
|
|
3341
3390
|
].filter(Boolean).join(" ");
|
|
3342
|
-
const children = /* @__PURE__ */ jsxs28(
|
|
3391
|
+
const children = /* @__PURE__ */ jsxs28(Fragment10, { children: [
|
|
3343
3392
|
(item.leading ?? item.icon) && /* @__PURE__ */ jsx33("span", { className: "ui-nav__ic", children: item.leading ?? /* @__PURE__ */ jsx33(Icon, { name: item.icon }) }),
|
|
3344
3393
|
/* @__PURE__ */ jsx33("span", { className: "ui-nav__label", children: item.label }),
|
|
3345
3394
|
count != null && count !== "" && /* @__PURE__ */ jsx33("span", { className: `ui-nav__badge is-${typeof item.badge === "object" ? item.badge.tone ?? "neutral" : "neutral"}`, children: count }),
|
|
@@ -3408,7 +3457,7 @@ var SidebarNav = forwardRef8(function SidebarNav2({
|
|
|
3408
3457
|
});
|
|
3409
3458
|
|
|
3410
3459
|
// src/AppShell.tsx
|
|
3411
|
-
import { Fragment as
|
|
3460
|
+
import { Fragment as Fragment11, jsx as jsx34, jsxs as jsxs29 } from "react/jsx-runtime";
|
|
3412
3461
|
var path = (value) => value.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
|
|
3413
3462
|
function AppShell({
|
|
3414
3463
|
sections,
|
|
@@ -3483,7 +3532,7 @@ function AppShell({
|
|
|
3483
3532
|
onClick: (event) => {
|
|
3484
3533
|
if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) setMore(false);
|
|
3485
3534
|
},
|
|
3486
|
-
children: /* @__PURE__ */ jsxs29(
|
|
3535
|
+
children: /* @__PURE__ */ jsxs29(Fragment11, { children: [
|
|
3487
3536
|
/* @__PURE__ */ jsx34("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx34(Icon, { name: section.icon }) }),
|
|
3488
3537
|
/* @__PURE__ */ jsx34("span", { className: "ui-nav__label", children: section.label }),
|
|
3489
3538
|
section.count != null && /* @__PURE__ */ jsx34("span", { className: "ui-nav__badge is-neutral", children: section.count })
|
|
@@ -3817,7 +3866,7 @@ import {
|
|
|
3817
3866
|
useState as useState16
|
|
3818
3867
|
} from "react";
|
|
3819
3868
|
import { CHART_FLOOR, bridgeWalk, chartScale, pointTarget } from "@apliteni/apliteni-ui";
|
|
3820
|
-
import { Fragment as
|
|
3869
|
+
import { Fragment as Fragment12, jsx as jsx38, jsxs as jsxs33 } from "react/jsx-runtime";
|
|
3821
3870
|
var HEIGHT = { months: 216, bridge: 216, spark: 32 };
|
|
3822
3871
|
var PAD = 2;
|
|
3823
3872
|
var ZERO_INSET = 1.5;
|
|
@@ -4447,7 +4496,7 @@ function Chart(props) {
|
|
|
4447
4496
|
`${column.label}-${i}`
|
|
4448
4497
|
)) })
|
|
4449
4498
|
] }) }),
|
|
4450
|
-
/* @__PURE__ */ jsx38("span", { ref: tipEl, className: cx8("ui-tip", openId && "is-open"), role: open ? "tooltip" : void 0, children: open && /* @__PURE__ */ jsxs33(
|
|
4499
|
+
/* @__PURE__ */ jsx38("span", { ref: tipEl, className: cx8("ui-tip", openId && "is-open"), role: open ? "tooltip" : void 0, children: open && /* @__PURE__ */ jsxs33(Fragment12, { children: [
|
|
4451
4500
|
/* @__PURE__ */ jsx38("span", { className: "ui-tip__label", children: spark ? columns[open.column]?.label : `${open.name}, ${columns[open.column]?.label}` }),
|
|
4452
4501
|
/* @__PURE__ */ jsx38("span", { className: "ui-tip__value", children: format(open.value) }),
|
|
4453
4502
|
(stateOf(columns[open.column]) || open.detail) && /* @__PURE__ */ jsx38("span", { className: "ui-tip__detail", children: stateOf(columns[open.column]) || open.detail })
|
|
@@ -4470,7 +4519,7 @@ function Chart(props) {
|
|
|
4470
4519
|
/* @__PURE__ */ jsx38("tbody", { children: tableRows.map((row, i) => /* @__PURE__ */ jsxs33("tr", { "aria-current": pick === i ? true : void 0, children: [
|
|
4471
4520
|
/* @__PURE__ */ jsxs33("th", { scope: "row", children: [
|
|
4472
4521
|
row.column.label,
|
|
4473
|
-
row.column.estimated && /* @__PURE__ */ jsxs33(
|
|
4522
|
+
row.column.estimated && /* @__PURE__ */ jsxs33(Fragment12, { children: [
|
|
4474
4523
|
" ",
|
|
4475
4524
|
/* @__PURE__ */ jsx38("span", { className: "ui-chart__state", children: stateOf(row.column) })
|
|
4476
4525
|
] })
|
|
@@ -384,6 +384,45 @@ function ddResetSearch(dd, panel, search) {
|
|
|
384
384
|
* fails when one drifts. why: src/styles/dropdown.css */
|
|
385
385
|
export const DD_MENU_FLOOR = 240;
|
|
386
386
|
|
|
387
|
+
/** The floor a panel asks for in the sheet, or null. A separate name from the
|
|
388
|
+
* floor this fit writes back: one that read its own output would pin a menu at
|
|
389
|
+
* whatever a narrow row once allowed. why: src/styles/filter-bar.css */
|
|
390
|
+
function ddPanelAsk(panel) {
|
|
391
|
+
const view = panel?.ownerDocument?.defaultView;
|
|
392
|
+
if (!panel || typeof view?.getComputedStyle !== 'function') return null;
|
|
393
|
+
const asked = Number.parseFloat(view.getComputedStyle(panel).getPropertyValue('--ui-filter-panel-ask'));
|
|
394
|
+
return Number.isFinite(asked) && asked > 0 ? asked : null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** The `.ui-dropdown__panel` inside a dropdown, by class rather than by data
|
|
398
|
+
* attribute: the fit's subjects are the panels the stylesheet's rules match, and
|
|
399
|
+
* filter-panel-fit.test.js hands this function hand-made elements. */
|
|
400
|
+
const ddFitPanelOf = (dd) => (typeof dd?.querySelector === 'function'
|
|
401
|
+
? dd.querySelector('.ui-dropdown__panel') : null);
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* The filter row an anchored menu is measured inside, or null outside one.
|
|
405
|
+
*
|
|
406
|
+
* Two panels in a row are anchored at a trigger of their own: a chip's values, and
|
|
407
|
+
* the add control's catalogue, which says so by asking for a width. The slide
|
|
408
|
+
* filterPanelFit() computes is measured from that trigger's offset along the row,
|
|
409
|
+
* which is arithmetic for either. A panel anchored to the row itself is neither,
|
|
410
|
+
* and sizes itself.
|
|
411
|
+
*
|
|
412
|
+
* Published because `<Dropdown>` asks the same question — the same row to observe,
|
|
413
|
+
* the same panels to hold through a close — and a second copy of this rule would
|
|
414
|
+
* drift from the stylesheet. why: src/styles/filter-bar.css
|
|
415
|
+
*
|
|
416
|
+
* @param {Element} dd a `.ui-dropdown` that may be inside a filter row
|
|
417
|
+
* @returns {Element|null} the `.ui-filter-bar` this menu's fit is measured in
|
|
418
|
+
*/
|
|
419
|
+
export function filterPanelRow(dd) {
|
|
420
|
+
if (typeof dd?.closest !== 'function') return null;
|
|
421
|
+
const chip = dd.closest('.ui-filter-bar__chip');
|
|
422
|
+
if (chip) return chip.closest('.ui-filter-bar');
|
|
423
|
+
return ddPanelAsk(ddFitPanelOf(dd)) ? dd.closest('.ui-filter-bar') : null;
|
|
424
|
+
}
|
|
425
|
+
|
|
387
426
|
/**
|
|
388
427
|
* Where a filter chip's menu may sit. #484 bounds the panel to its trigger, so a
|
|
389
428
|
* chip printing its value alone (#536) left a 48px menu breaking words
|
|
@@ -396,23 +435,23 @@ export const DD_MENU_FLOOR = 240;
|
|
|
396
435
|
* other way. Measuring an end-anchored panel forwards put one off the page.
|
|
397
436
|
* why: docs/components.md#a-filter-row-holds-its-panels
|
|
398
437
|
*
|
|
438
|
+
* A menu that is not a chip's takes this arithmetic by asking, on its own panel,
|
|
439
|
+
* for a width: `--ui-filter-panel-ask`. why: src/styles/filter-bar.css
|
|
440
|
+
*
|
|
399
441
|
* @param {Element} dd a `.ui-dropdown` that may be inside a filter row
|
|
400
|
-
* @param {number} [floor] the width to reach for
|
|
442
|
+
* @param {number} [floor] the width to reach for; the panel's own ask, else the kit's
|
|
401
443
|
* @returns {{room: number, shift: number, floor: number, end: boolean}|null}
|
|
402
444
|
*/
|
|
403
|
-
export function filterPanelFit(dd, floor
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const chip = typeof dd?.closest === 'function' ? dd.closest('.ui-filter-bar__chip') : null;
|
|
408
|
-
const bar = chip ? chip.closest('.ui-filter-bar') : null;
|
|
445
|
+
export function filterPanelFit(dd, floor) {
|
|
446
|
+
const panel = ddFitPanelOf(dd);
|
|
447
|
+
// Which menus are this function's subjects is filterPanelRow()'s question.
|
|
448
|
+
const bar = filterPanelRow(dd);
|
|
409
449
|
if (!bar || typeof bar.getBoundingClientRect !== 'function') return null;
|
|
410
450
|
const row = bar.getBoundingClientRect();
|
|
411
451
|
const box = dd.getBoundingClientRect();
|
|
412
|
-
const panel = typeof dd.querySelector === 'function' ? dd.querySelector('.ui-dropdown__panel') : null;
|
|
413
452
|
const end = !!panel?.classList?.contains('is-end');
|
|
414
453
|
// A row narrower than the floor decides the width; nothing may leave the row.
|
|
415
|
-
const want = Math.min(floor, row.width);
|
|
454
|
+
const want = Math.min(floor ?? ddPanelAsk(panel) ?? DD_MENU_FLOOR, row.width);
|
|
416
455
|
const ahead = end
|
|
417
456
|
? Math.max(0, box.right - row.left) // an end-anchored panel grows backwards
|
|
418
457
|
: Math.max(0, row.right - box.left);
|
|
@@ -466,11 +505,6 @@ function ddRefitFilterPanel(dd, panel) {
|
|
|
466
505
|
if (panel.offsetWidth) panel.style.minWidth = `${panel.offsetWidth}px`;
|
|
467
506
|
}
|
|
468
507
|
|
|
469
|
-
/** The row this chip's menu is fitted to, or null outside one. */
|
|
470
|
-
function ddRowOf(dd) {
|
|
471
|
-
return dd?.closest?.('.ui-filter-bar__chip')?.closest('.ui-filter-bar') || null;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
508
|
/** Is this node inside some dropdown panel? Both fits write to a panel and to
|
|
475
509
|
* nothing else, so a change inside one is never news — and a panel is
|
|
476
510
|
* `position: absolute`, so nothing in it can move a chip along its row. Any
|
|
@@ -501,7 +535,7 @@ function ddOurs(node) {
|
|
|
501
535
|
*/
|
|
502
536
|
function ddWatchRow(dd, panel) {
|
|
503
537
|
ddUnwatchRow(dd);
|
|
504
|
-
const row =
|
|
538
|
+
const row = filterPanelRow(dd);
|
|
505
539
|
const view = dd.ownerDocument?.defaultView;
|
|
506
540
|
if (!row) return;
|
|
507
541
|
const refit = () => { if (dd.classList.contains('open')) ddRefitFilterPanel(dd, panel); };
|
|
@@ -619,7 +653,7 @@ const DD_EXIT_SLACK_MS = 50;
|
|
|
619
653
|
const DD_SETTLE_MS = 450;
|
|
620
654
|
|
|
621
655
|
/**
|
|
622
|
-
* Hold a closing
|
|
656
|
+
* Hold a closing anchored menu's open geometry until its fade has finished.
|
|
623
657
|
*
|
|
624
658
|
* `.open` stops matching in the frame the menu closes; the fade does not — the
|
|
625
659
|
* panel transitions opacity over --dur-med. Dropping the width there left a 240px
|
|
@@ -823,10 +857,10 @@ function closeDropdown(dd) {
|
|
|
823
857
|
/* A search panel carries the width it was opened at as an inline `min-width`,
|
|
824
858
|
* and inline beats any sheet. Inside a filter row that leaves a SHUT panel wider
|
|
825
859
|
* than its trigger, which is #467. Cleared here, and ddResetSearch() writes it
|
|
826
|
-
* again on the next open — except for
|
|
827
|
-
* fit back at the END of its fade, both being its open geometry. A portalled
|
|
860
|
+
* again on the next open — except for an anchored menu, which gives the pin and
|
|
861
|
+
* the fit back at the END of its fade, both being its open geometry. A portalled
|
|
828
862
|
* panel is never fitted, so it has none to hold. why: ddHoldClose() */
|
|
829
|
-
if (panel?.style && !dd.__ddPanel &&
|
|
863
|
+
if (panel?.style && !dd.__ddPanel && filterPanelRow(dd)) ddHoldClose(dd, panel);
|
|
830
864
|
else if (panel?.style && dd.closest?.('.ui-filter-bar')) panel.style.minWidth = '';
|
|
831
865
|
dd.querySelector('[data-dropdown-trigger]')?.setAttribute('aria-expanded', 'false');
|
|
832
866
|
/* The anchor may have moved while the panel was open, and a shut panel is laid
|
|
@@ -2,7 +2,7 @@ import { button, esc } from './index.js';
|
|
|
2
2
|
import { dropdown, wireDropdown } from './dropdown.js';
|
|
3
3
|
import { filterChipText, filterChipName, filterChipUnset, filterChipItems, focusNextStop } from '../logic/filter-bar.js';
|
|
4
4
|
|
|
5
|
-
export function filterBar({ filters = [], label = 'Filters', clearLabel = 'Clear all
|
|
5
|
+
export function filterBar({ filters = [], label = 'Filters', clearLabel = 'Clear all', disabled = false, busy = false } = {}) {
|
|
6
6
|
return `<fieldset class="ui-filter-bar" data-filter-bar${disabled || busy ? ' disabled' : ''}${busy ? ' aria-busy="true"' : ''}>`
|
|
7
7
|
+ `<legend class="ui-filter-bar__legend">${esc(label)}</legend>`
|
|
8
8
|
+ filters.map(filter => `<fieldset class="ui-filter-bar__chip" data-filter-id="${esc(filter.id)}"${filter.disabled ? ' disabled' : ''}>`
|
|
@@ -21,7 +21,7 @@ export function filterBar({ filters = [], label = 'Filters', clearLabel = 'Clear
|
|
|
21
21
|
// live action beside two chips has to read as live.
|
|
22
22
|
// why: docs/components.md#a-filter-row-holds-its-panels
|
|
23
23
|
+ (filters.length
|
|
24
|
-
? `<span data-filter-clear>${button({ label: clearLabel, size: 'sm' })}</span>`
|
|
24
|
+
? `<span data-filter-clear>${button({ label: clearLabel, size: 'sm', variant: 'ghost' })}</span>`
|
|
25
25
|
: '')
|
|
26
26
|
+ '</fieldset>';
|
|
27
27
|
}
|
|
@@ -58,7 +58,10 @@ export function initFilterBar(host, options = {}) {
|
|
|
58
58
|
const chip = e.target.closest('[data-filter-id]');
|
|
59
59
|
if (e.target.closest('[data-filter-remove]')) emit('ui-filter-remove', { id: chip.dataset.filterId });
|
|
60
60
|
else if (e.target.closest('[data-filter-clear] button')) emit('ui-filter-clear', {});
|
|
61
|
-
|
|
61
|
+
// A chip's menu only. The catalogue a vanilla page draws under `data-filter-add` is the
|
|
62
|
+
// page's own, and a pick in it names no chip: reported as a change, it had a consumer
|
|
63
|
+
// mapping over its filters and writing the catalogue's value into every one of them. #518
|
|
64
|
+
else if (e.target.closest('[data-dd-item]') && !e.target.closest('[data-filter-add]')) {
|
|
62
65
|
const item = e.target.closest('[data-dd-item]');
|
|
63
66
|
if (item.getAttribute('aria-disabled') !== 'true') {
|
|
64
67
|
const id = chip?.dataset.filterId, value = item.dataset.value;
|
package/src/index.d.ts
CHANGED
|
@@ -43,12 +43,18 @@ export declare function dropdownAvail(opts: {
|
|
|
43
43
|
}): number;
|
|
44
44
|
/** The kit's menu floor, the width `.ui-dropdown__panel` writes. */
|
|
45
45
|
export declare const DD_MENU_FLOOR: number;
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
|
|
46
|
+
/** The `.ui-filter-bar` an anchored menu's fit is measured inside, or `null`.
|
|
47
|
+
* A chip's menu, and a menu that asks for a width with `--ui-filter-panel-ask` on
|
|
48
|
+
* its own panel: the slide is that trigger's offset along the row, so those two
|
|
49
|
+
* are the subjects and a panel anchored to the row itself sizes itself.
|
|
50
|
+
* why: docs/components.md */
|
|
51
|
+
export declare function filterPanelRow(dd: Element | null | undefined): Element | null;
|
|
52
|
+
/** Where an anchored filter menu can sit: the room from the edge it is anchored
|
|
53
|
+
* at to the far side of its row once slid, how far it had to slide, the width it
|
|
54
|
+
* reached for, and whether it is anchored at its inline end. `null` for a
|
|
55
|
+
* dropdown `filterPanelRow()` does not answer for. The width reached for is
|
|
56
|
+
* `floor`, else the panel's own `--ui-filter-panel-ask`, else `DD_MENU_FLOOR`.
|
|
57
|
+
* why: docs/components.md */
|
|
52
58
|
export declare function filterPanelFit(
|
|
53
59
|
dd: Element | null | undefined,
|
|
54
60
|
floor?: number,
|
package/src/logic/callout.js
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
// Default glyph per callout tone. A callout REPORTS a state, so its status
|
|
2
|
+
// glyphs are the circled ones — the same family toast() uses (TOAST_ICON in
|
|
3
|
+
// src/components/index.js) and the same split iconMeanings writes down. The
|
|
4
|
+
// bare `check` and `alert` this used to ship read as actions, which is why
|
|
5
|
+
// every showcase overrode them per instance. #453
|
|
6
|
+
//
|
|
7
|
+
// Neutral keeps `info` rather than the toast's `bolt`: a neutral callout is a
|
|
8
|
+
// standing note on the page, not something that just happened.
|
|
9
|
+
export const calloutIcons = { info: 'info', success: 'circleCheck', warn: 'circleAlert', danger: 'circleX', neutral: 'info' };
|
package/src/logic/filter-bar.js
CHANGED
|
@@ -67,37 +67,55 @@ function stopsAround(host) {
|
|
|
67
67
|
...order.filter(el => host.compareDocumentPosition(el) & PRECEDING).reverse()];
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/** The
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
/** The bar `host` is, or the one inside it. */
|
|
71
|
+
function barIn(host) {
|
|
72
|
+
if (!host || typeof host.matches !== 'function') return null;
|
|
73
|
+
return host.matches('[data-filter-bar]') ? host : host.querySelector('[data-filter-bar]');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Where an emptied bar hands the focus, in the order it tries: the way to add one
|
|
77
|
+
* still drawn on the row, which outlives the chips and so is nearer than anything
|
|
78
|
+
* outside, then the controls around the bar. The answer a consumer reads and the
|
|
79
|
+
* move the bar makes both come from here, so the two cannot disagree. A control
|
|
80
|
+
* the reader cannot reach is left out of the list. #518 */
|
|
81
|
+
function focusStops(host) {
|
|
82
|
+
const bar = barIn(host);
|
|
83
|
+
const add = bar && !bar.querySelector('[data-filter-id]')
|
|
84
|
+
&& bar.querySelector('[data-filter-add] [data-dropdown-trigger]');
|
|
85
|
+
const around = stopsAround(host);
|
|
86
|
+
return add && !unreachable(add) ? [add, ...around] : around;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Where an emptied bar sends the focus, without moving it: the way to add one
|
|
90
|
+
* still on its row, else the control a reader's next Tab would reach — forward in
|
|
91
|
+
* document order, else the nearest one behind. A bar that has just dropped its
|
|
92
|
+
* last chip keeps no box the focus ring can sit on, so with no control of its own
|
|
93
|
+
* left the focus has to leave rather than land on a line with no height.
|
|
94
|
+
* `focusNextStop()` acts on this same list.
|
|
74
95
|
* why: docs/components.md#a-filter-row-holds-its-panels */
|
|
75
96
|
export function nextFocusStop(host) {
|
|
76
|
-
return
|
|
97
|
+
return focusStops(host)[0] || null;
|
|
77
98
|
}
|
|
78
99
|
|
|
79
100
|
/** Puts the focus where an emptied bar hands it on, and answers with the element
|
|
80
101
|
* it ended on, or null if nothing took it.
|
|
81
102
|
*
|
|
82
|
-
* A bar that still holds chips draws a box around them
|
|
83
|
-
* itself
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* Being reachable is not the same as taking the focus: a control can be visible
|
|
89
|
-
* and enabled and still refuse, so each candidate is asked and then checked, and
|
|
90
|
-
* the next one tried.
|
|
103
|
+
* A bar that still holds chips draws a box around them and takes the focus
|
|
104
|
+
* itself. Emptied of them it has only an out-of-flow legend and measures 0 high,
|
|
105
|
+
* so the focus goes where `nextFocusStop()` names — the way to add one still on
|
|
106
|
+
* the row, which is what the Stock screener draws, else outside the bar.
|
|
107
|
+
* Being reachable is not taking the focus: each candidate is asked and then
|
|
108
|
+
* checked. #518
|
|
91
109
|
* why: docs/components.md#a-filter-row-holds-its-panels */
|
|
92
110
|
export function focusNextStop(host) {
|
|
93
111
|
const doc = host && host.ownerDocument;
|
|
94
112
|
if (!doc) return null;
|
|
95
|
-
const bar =
|
|
113
|
+
const bar = barIn(host);
|
|
96
114
|
if (bar && bar.querySelector('[data-filter-id]')) {
|
|
97
115
|
bar.tabIndex = -1; bar.focus();
|
|
98
116
|
return doc.activeElement === bar ? bar : null;
|
|
99
117
|
}
|
|
100
|
-
for (const stop of
|
|
118
|
+
for (const stop of focusStops(host)) {
|
|
101
119
|
stop.focus();
|
|
102
120
|
if (doc.activeElement === stop) return stop;
|
|
103
121
|
}
|
package/src/styles/badge.css
CHANGED
|
@@ -15,24 +15,29 @@
|
|
|
15
15
|
padding: 3px 9px;
|
|
16
16
|
border-radius: var(--radius-pill);
|
|
17
17
|
color: var(--text);
|
|
18
|
-
background: var(--
|
|
18
|
+
background: var(--chip-neutral-fill);
|
|
19
19
|
white-space: nowrap;
|
|
20
20
|
}
|
|
21
21
|
/* Status chips take the --chip-* pairs, so the light app's solid fills live in
|
|
22
22
|
tokens.css rather than in a theme override down here: on white the glow-tints
|
|
23
|
-
wash out and the ink has to deepen to clear AA, and that is a token decision.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
wash out and the ink has to deepen to clear AA, and that is a token decision.
|
|
24
|
+
The tones with no status take --chip-neutral-fill for the same reason: before #453
|
|
25
|
+
they painted the opaque --surface, the one soft badge fill that sat DARKER than the
|
|
26
|
+
panel in dark. why: docs/foundations.md#elevation
|
|
27
|
+
Soon takes body ink here, not --accent: accent ink measured 4.04:1 on a dark floating
|
|
28
|
+
panel under three of the four accents, body ink 9.74:1. See docs/foundations.md. */
|
|
29
|
+
.ui-badge--live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
|
|
30
|
+
.ui-badge--soon { color: var(--text); background: var(--chip-neutral-fill); }
|
|
31
|
+
.ui-badge--info { color: var(--chip-info-ink); background: var(--chip-info-fill); }
|
|
27
32
|
.ui-badge--warn { color: var(--chip-warn-ink); background: var(--chip-warn-fill); }
|
|
28
|
-
.ui-badge--danger { color: var(--chip-danger-ink); background: var(--chip-danger-fill);
|
|
29
|
-
.ui-badge--archive { /* muted-ink: state — archived record */ color: var(--muted); background: var(--
|
|
33
|
+
.ui-badge--danger { color: var(--chip-danger-ink); background: var(--chip-danger-fill); }
|
|
34
|
+
.ui-badge--archive { /* muted-ink: state — archived record */ color: var(--muted); background: var(--chip-neutral-fill); }
|
|
30
35
|
|
|
31
36
|
/* Semantic status variants — for record/row status in data tables
|
|
32
37
|
(paid / verified / pending / failed …). Same chip, mapped to meaning. */
|
|
33
|
-
.ui-badge--success { color: var(--chip-success-ink); background: var(--chip-success-fill);
|
|
38
|
+
.ui-badge--success { color: var(--chip-success-ink); background: var(--chip-success-fill); }
|
|
34
39
|
.ui-badge--pending { color: var(--chip-warn-ink); background: var(--chip-warn-fill); }
|
|
35
|
-
.ui-badge--neutral { color: var(--text); background: var(--
|
|
40
|
+
.ui-badge--neutral { color: var(--text); background: var(--chip-neutral-fill); }
|
|
36
41
|
|
|
37
42
|
/* Pill: same shape, sentence case, a touch larger — a metadata chip */
|
|
38
43
|
.ui-pill {
|
|
@@ -42,7 +47,7 @@
|
|
|
42
47
|
/* rank: chip */
|
|
43
48
|
font-size: var(--text-xs);
|
|
44
49
|
color: var(--text);
|
|
45
|
-
background: var(--
|
|
50
|
+
background: var(--chip-neutral-fill);
|
|
46
51
|
border-radius: var(--radius-pill);
|
|
47
52
|
padding: 4px 11px;
|
|
48
53
|
font-weight: var(--weight-semibold);
|
|
@@ -50,8 +55,8 @@
|
|
|
50
55
|
/* The live pill is the same status paint as the live badge, so it takes the
|
|
51
56
|
same pair. On its own it was --green on --glow-green, which is 3.97:1 over
|
|
52
57
|
white — under AA, while the badge beside it cleared at 4.64:1. See issue #131. */
|
|
53
|
-
.ui-pill--live { color: var(--chip-success-ink); background: var(--chip-success-fill);
|
|
54
|
-
.ui-pill--soon { color: var(--
|
|
58
|
+
.ui-pill--live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
|
|
59
|
+
.ui-pill--soon { color: var(--text); background: var(--chip-neutral-fill); }
|
|
55
60
|
|
|
56
61
|
/* Status dot */
|
|
57
62
|
.ui-dot {
|
package/src/styles/dropdown.css
CHANGED
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
accent band is the second band #578 round r34 took off the focus rules, and a
|
|
32
32
|
pointer resting on a focused control would put it back. One indicator, which is
|
|
33
33
|
what the kit's rows already chose. why: docs/foundations.md#the-focus-ring */
|
|
34
|
-
.ui-dropdown__trigger:hover:not(:focus-visible) { border-color: var(--accent); }
|
|
34
|
+
.ui-dropdown__trigger:enabled:hover:not(:focus-visible) { border-color: var(--accent); }
|
|
35
35
|
.ui-dropdown__trigger:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); }
|
|
36
|
-
/* Off. The off hover-not-focus selectors are (0,4,0),
|
|
36
|
+
/* Off. The off hover-not-focus selectors are (0,4,0), tied with live hover and later.
|
|
37
37
|
A pointer over a dead control changes nothing while an aria-disabled one keeps its stop and the ring.
|
|
38
38
|
No fill: --disabled-surface IS this control's --surface, in both themes. The
|
|
39
39
|
activation is refused in ddTriggerOff(), not with pointer-events, which would
|
|
@@ -295,12 +295,12 @@
|
|
|
295
295
|
margin-left: auto;
|
|
296
296
|
flex: none;
|
|
297
297
|
color: var(--text);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
:
|
|
302
|
-
background: color-mix(in srgb, var(--muted) 15%, transparent); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */
|
|
298
|
+
/* The kit's one neutral chip fill, which #453 moved into a token so the badge, the
|
|
299
|
+
pill, this chip and the version badge are one family. The per-theme alphas that
|
|
300
|
+
used to be written here twice live with the token. */
|
|
301
|
+
background: var(--chip-neutral-fill); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */
|
|
303
302
|
}
|
|
303
|
+
.ui-dropdown__badge.is-live { color: var(--chip-success-ink); background: var(--chip-success-fill); /* code-bg: inherit — decorative or text-only surface has no nested chip. */ }
|
|
304
304
|
|
|
305
305
|
/* Disabled + danger rows */
|
|
306
306
|
.ui-dropdown__item.is-disabled { /* muted-ink: state — unavailable control */ color: var(--disabled-ink); cursor: not-allowed; pointer-events: none; }
|
|
@@ -13,15 +13,19 @@
|
|
|
13
13
|
background: var(--surface);
|
|
14
14
|
}
|
|
15
15
|
.ui-filter-bar__chip .ui-dropdown__trigger { border: 0; background: transparent; padding: var(--space-1) var(--space-2); }
|
|
16
|
-
|
|
16
|
+
/* The chip is the edge the row's other boxes are measured against, so it fades with them:
|
|
17
|
+
off, the chips stayed at full strength and only their ink said so. Two carriers, because
|
|
18
|
+
a bar turned off disables the controls in a nested fieldset and not the fieldset. #518
|
|
19
|
+
why: guidelines/accessibility-floor.md#disabled-control-legibility */
|
|
20
|
+
.ui-filter-bar__chip:disabled,
|
|
21
|
+
.ui-filter-bar:disabled .ui-filter-bar__chip { border-color: var(--disabled-border); }
|
|
22
|
+
.ui-filter-bar__remove { border: 0; background: transparent; /* muted-ink: glyph — filter remove mark */ color: var(--muted); font: inherit; text-align: center; font-size: var(--text-lg); padding: 0; min-width: 24px; min-height: 24px; border-radius: var(--radius-xs); cursor: pointer; }
|
|
17
23
|
.ui-filter-bar__remove:focus-visible,
|
|
18
24
|
.ui-filter-bar:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); }
|
|
19
25
|
.ui-filter-bar button:disabled { /* muted-ink: state — disabled filters */ color: var(--disabled-ink); cursor: not-allowed; }
|
|
26
|
+
.ui-filter-bar .ui-filter-bar__remove:disabled { /* muted-ink: state — unavailable remove mark on the page */ color: var(--disabled-ink-bare); }
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
disabled ink it inherits instead of the value ink the enabled rules set. */
|
|
23
|
-
.ui-filter-bar .ui-dropdown__value { color: inherit; }
|
|
24
|
-
.ui-filter-bar button:enabled .ui-dropdown__value { color: var(--strong); }
|
|
28
|
+
.ui-filter-bar .ui-dropdown__value { color: inherit; font-weight: inherit; letter-spacing: normal; }
|
|
25
29
|
/* A chip with nothing chosen prints the field's name in the slot the value will
|
|
26
30
|
take — in the page's reading ink, not --muted. --disabled-ink IS --muted, so a
|
|
27
31
|
muted name on an offered filter said "switched off" in the one ink the kit
|
|
@@ -34,11 +38,8 @@
|
|
|
34
38
|
dropdown gets: it is laid out while hidden, so a wider one widened the page
|
|
35
39
|
(#467). why: docs/components.md#a-filter-row-holds-its-panels */
|
|
36
40
|
.ui-filter-bar .ui-dropdown__panel { min-width: 100%; max-width: 100%; overflow-wrap: anywhere; }
|
|
37
|
-
/*
|
|
38
|
-
|
|
39
|
-
(0,1,1) so `.ui-btn:hover`'s accent wash still outranks it; the rules under it
|
|
40
|
-
answer each state that paints a NEUTRAL fill, one step above the kit rule
|
|
41
|
-
writing it. Held by filter-bar.test.js. why: docs/components.md#a-filter-row-holds-its-panels */
|
|
41
|
+
/* Clear is a quiet text button: no edge in any state, and no fill but the pointer's wash below.
|
|
42
|
+
why: docs/components.md#a-filter-row-holds-its-panels */
|
|
42
43
|
[data-filter-clear] button { background: transparent; }
|
|
43
44
|
.ui-filter-bar [data-filter-clear] button:disabled,
|
|
44
45
|
.ui-filter-bar [data-filter-clear] button[aria-disabled="true"],
|
|
@@ -46,23 +47,40 @@
|
|
|
46
47
|
.ui-filter-bar [data-filter-clear] button[aria-busy="true"]:hover,
|
|
47
48
|
.ui-filter-bar [data-filter-clear] button[aria-busy="true"]:active,
|
|
48
49
|
.ui-filter-bar [data-filter-clear] button[aria-busy="true"][data-btn-disabled] { background: transparent; }
|
|
49
|
-
/*
|
|
50
|
-
measures on every ground. The edge stays the kit's --disabled-border. */
|
|
50
|
+
/* The ink button-disabled.test.js measures on every ground, since there is no fill. */
|
|
51
51
|
.ui-filter-bar [data-filter-clear] button:disabled,
|
|
52
52
|
.ui-filter-bar [data-filter-clear] button[aria-disabled="true"] { /* muted-ink: state — the clear action is off and its words sit on the page */ color: var(--disabled-ink-bare); }
|
|
53
|
+
.ui-filter-bar [data-filter-clear] button { border-color: transparent; }
|
|
54
|
+
.ui-filter-bar [data-filter-clear] button[aria-busy="true"],
|
|
55
|
+
.ui-filter-bar [data-filter-clear] button[aria-busy="true"]:hover,
|
|
56
|
+
.ui-filter-bar [data-filter-clear] button[aria-busy="true"]:active { border-color: transparent; }
|
|
57
|
+
/* The kit's own pointer answer, `.ui-btn:hover`: --accent at 14% and --strong ink, over the page
|
|
58
|
+
ground rather than --surface, which would be a grey block under words. Busy restated after. */
|
|
59
|
+
.ui-filter-bar [data-filter-clear] button:enabled:hover { border-color: transparent; background: color-mix(in srgb, var(--accent) 14%, transparent); /* code-bg: inherit — a translucent wash moves the ground, not the chip on it. */ }
|
|
60
|
+
.ui-filter-bar [data-filter-clear] button[aria-busy="true"]:hover { background: transparent; }
|
|
61
|
+
.ui-filter-bar [data-filter-clear] button:disabled,
|
|
62
|
+
.ui-filter-bar [data-filter-clear] button[aria-disabled="true"],
|
|
63
|
+
.ui-filter-bar [data-filter-clear] button[aria-busy="true"][data-btn-disabled] { border-color: transparent; }
|
|
53
64
|
|
|
54
|
-
/* Open, a
|
|
55
|
-
filterPanelFit(); `is-closing` holds that geometry through the fade.
|
|
65
|
+
/* Open, a filter menu anchored at its own trigger takes a floor measured and slid by
|
|
66
|
+
filterPanelFit(); `is-closing` holds that geometry through the fade. A chip's menu
|
|
67
|
+
reaches for the kit's menu floor; the add control's catalogue asks for a panel's
|
|
68
|
+
width instead, because a search field over it reads "Searc" at 240. #549, #496
|
|
56
69
|
why: docs/components.md#a-filter-row-holds-its-panels */
|
|
70
|
+
.ui-filter-bar [data-filter-add] .ui-dropdown__panel { --ui-filter-panel-ask: var(--panel-sm); }
|
|
57
71
|
.ui-filter-bar__chip .ui-dropdown.open .ui-dropdown__panel,
|
|
58
|
-
.ui-filter-bar__chip .ui-dropdown__panel.is-closing
|
|
72
|
+
.ui-filter-bar__chip .ui-dropdown__panel.is-closing,
|
|
73
|
+
.ui-filter-bar [data-filter-add] .ui-dropdown.open .ui-dropdown__panel,
|
|
74
|
+
.ui-filter-bar [data-filter-add] .ui-dropdown__panel.is-closing {
|
|
59
75
|
min-width: min(var(--ui-filter-panel-floor, 240px), var(--ui-filter-panel-room, 100%));
|
|
60
76
|
max-width: var(--ui-filter-panel-room, 100%);
|
|
61
77
|
margin-inline-start: calc(-1 * var(--ui-filter-panel-shift, 0px));
|
|
62
78
|
}
|
|
63
79
|
/* An end-aligned panel is pinned by its right edge, so the slide pushes it. */
|
|
64
80
|
.ui-filter-bar__chip .ui-dropdown.open .ui-dropdown__panel.is-end,
|
|
65
|
-
.ui-filter-bar__chip .ui-dropdown__panel.is-end.is-closing
|
|
81
|
+
.ui-filter-bar__chip .ui-dropdown__panel.is-end.is-closing,
|
|
82
|
+
.ui-filter-bar [data-filter-add] .ui-dropdown.open .ui-dropdown__panel.is-end,
|
|
83
|
+
.ui-filter-bar [data-filter-add] .ui-dropdown__panel.is-end.is-closing {
|
|
66
84
|
margin-inline-start: 0;
|
|
67
85
|
margin-inline-end: calc(-1 * var(--ui-filter-panel-shift, 0px));
|
|
68
86
|
}
|
|
@@ -104,3 +122,24 @@
|
|
|
104
122
|
field and filterBar() builds none, so an arrow key moves real focus instead. #578
|
|
105
123
|
why: docs/components.md#a-filter-row-holds-its-panels */
|
|
106
124
|
.ui-filter-bar .ui-dropdown__item.is-selected:hover { background: color-mix(in srgb, var(--glow-purple), var(--accent) 9%); /* code-bg: inherit — a translucent wash moves the ground, not the chip on it. */ }
|
|
125
|
+
|
|
126
|
+
/* A chip's height — its remove mark's line box, padding and border — named so the control
|
|
127
|
+
stands as tall on a line of its own, where there is no chip to stretch to. */
|
|
128
|
+
.ui-filter-bar { --ui-filter-row-h: calc(2px + 2 * var(--space-1) + var(--leading-normal) * var(--text-lg)); }
|
|
129
|
+
|
|
130
|
+
/* A chip's neighbour takes the chip's corner, that height and edge, but not its painted fill:
|
|
131
|
+
the trigger's own edge can differ from the row's, while its --surface made the
|
|
132
|
+
row's least consequential action its loudest box — and off, with its edge down, the one thing
|
|
133
|
+
still looking pressable. The states are
|
|
134
|
+
spelled out because this depth outranks the base rules; the fill needs one rule, because the
|
|
135
|
+
kit paints it at rest only. Under a pointer it takes the kit's trigger edge, --accent, which
|
|
136
|
+
steps aside under the band; held at its resting edge, hover and rest were one paint. #496, #518
|
|
137
|
+
why: guidelines/colour-and-theming.md#keep-text-off-grey-fills */
|
|
138
|
+
.ui-filter-bar [data-filter-add] { display: flex; align-self: stretch; }
|
|
139
|
+
.ui-filter-bar [data-filter-add] .ui-dropdown { display: flex; }
|
|
140
|
+
.ui-filter-bar [data-filter-add] .ui-dropdown__trigger { border-radius: var(--radius-sm); min-height: var(--ui-filter-row-h); border-color: var(--control-edge); background: transparent; }
|
|
141
|
+
.ui-filter-bar [data-filter-add] .ui-dropdown__trigger svg { width: 16px; height: 16px; flex: none; stroke-width: 2.4; }
|
|
142
|
+
.ui-filter-bar [data-filter-add] .ui-dropdown__trigger:enabled:hover:not(:focus-visible) { border-color: var(--accent); }
|
|
143
|
+
.ui-filter-bar [data-filter-add] .ui-dropdown__trigger:disabled { border-color: var(--disabled-border); }
|
|
144
|
+
.ui-filter-bar [data-filter-add] .ui-dropdown__trigger[aria-disabled="true"],
|
|
145
|
+
.ui-filter-bar [data-filter-add] .ui-dropdown__trigger[aria-disabled="true"]:hover:not(:focus-visible) { border-color: var(--disabled-border); }
|
package/src/styles/topbar.css
CHANGED
|
@@ -209,7 +209,7 @@
|
|
|
209
209
|
src/styles/dropdown.css:271 `.ui-dropdown__item:hover`.
|
|
210
210
|
why: docs/foundations.md#elevation */
|
|
211
211
|
.vopt:hover { outline: 1px solid var(--control-edge); outline-offset: -1px; background: var(--surface); /* code-bg: inherit — control paint; the containing surface is the ground, for a chip and for a band alike. */ }
|
|
212
|
-
/* The row is a keyboard stop: src/components/dropdown.js:
|
|
212
|
+
/* The row is a keyboard stop: src/components/dropdown.js:1141
|
|
213
213
|
`items[next < 0 ? 0 : next]?.focus()` moves focus onto a `[data-dd-item]` with
|
|
214
214
|
the arrow keys, and this is one. It takes the ring on its own 10px radius, the
|
|
215
215
|
same way `.amenu a` above does. #482 */
|
|
@@ -226,8 +226,11 @@
|
|
|
226
226
|
flex: none;
|
|
227
227
|
margin-top: 1px;
|
|
228
228
|
}
|
|
229
|
-
.vbadge.live { color: var(--chip-success-ink); background: var(--chip-success-fill); /* code-bg: inherit — decorative or text-only
|
|
230
|
-
|
|
229
|
+
.vbadge.live { color: var(--chip-success-ink); background: var(--chip-success-fill); /* code-bg: inherit — decorative or text-only surface has no nested chip. */ }
|
|
230
|
+
/* The archived version badge is the archive badge tone in a floating menu, so it takes
|
|
231
|
+
the same neutral chip fill rather than the card surface — which, inside a panel that
|
|
232
|
+
is itself lighter than a card, was darker than the ground it sat on. #453 */
|
|
233
|
+
.vbadge.arch { /* muted-ink: state — archived version */ color: var(--muted); background: var(--chip-neutral-fill); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
|
|
231
234
|
/* The phone step — the same one the rail folds at: docs/foundations.md#breakpoints.
|
|
232
235
|
The switcher keeps its glyph and drops its label.
|
|
233
236
|
The menu hangs the other way here. Anchored by its left edge it opened
|
package/src/tokens/tokens.css
CHANGED
|
@@ -233,10 +233,18 @@
|
|
|
233
233
|
--chip-success-fill: var(--glow-green);
|
|
234
234
|
--chip-warn-ink: var(--amber);
|
|
235
235
|
--chip-warn-fill: color-mix(in srgb, var(--amber) 15%, transparent);
|
|
236
|
-
--
|
|
236
|
+
/* Not --pink: that reads 4.23:1 on this fill over a floating panel, under the 4.5:1 badge
|
|
237
|
+
floor. Same hue lifted toward white — 8.64 page / 7.02 card / 6.30 panel. The signal
|
|
238
|
+
and the solid-toast pair keep --pink. why: docs/foundations.md#colour-and-contrast, #453 */
|
|
239
|
+
--chip-danger-ink: #f5aec6;
|
|
237
240
|
--chip-danger-fill: var(--glow-pink);
|
|
238
241
|
--chip-info-ink: var(--cyan);
|
|
239
242
|
--chip-info-fill: var(--glow-cyan);
|
|
243
|
+
/* The fill of a chip with no status to carry. The themes match on SEPARATION, not on
|
|
244
|
+
alpha — 12% here is 1.22:1 off the card, 15% in light is 1.23:1 — because --muted is
|
|
245
|
+
a light violet-grey in dark and a mid slate in light.
|
|
246
|
+
why: docs/foundations.md#colour-and-contrast, #453 */
|
|
247
|
+
--chip-neutral-fill: color-mix(in srgb, var(--muted) 12%, transparent);
|
|
240
248
|
|
|
241
249
|
/* Solid signal surfaces — a status that has stopped being ink and become the FILL (a
|
|
242
250
|
solid toast). Fill and ink are ONE pair, never two independent picks: the fill is the
|
|
@@ -412,7 +420,10 @@
|
|
|
412
420
|
chip text clears WCAG AA on its own pale fill, and on the soft toast's wash, which
|
|
413
421
|
#131 gave the same ink. Three came down again at #295 with the page; danger did not
|
|
414
422
|
need to. */
|
|
415
|
-
|
|
423
|
+
/* Success is held at the 7:1 body aim rather than the 4.5:1 floor: 7.17:1 on its own
|
|
424
|
+
fill, 8.33:1 on the card. Was #1c7034 at 5.29:1. The fill did not move.
|
|
425
|
+
why: docs/foundations.md#colour-and-contrast, #453 */
|
|
426
|
+
--chip-success-ink: #145a26;
|
|
416
427
|
--chip-success-fill: #dff3e4;
|
|
417
428
|
--chip-warn-ink: #825900;
|
|
418
429
|
--chip-warn-fill: #fbedd2;
|
|
@@ -423,6 +434,8 @@
|
|
|
423
434
|
already occupy. See issue #131. */
|
|
424
435
|
--chip-info-ink: #096a7c;
|
|
425
436
|
--chip-info-fill: #ddeff3;
|
|
437
|
+
/* See the dark block for why the two alphas differ. */
|
|
438
|
+
--chip-neutral-fill: color-mix(in srgb, var(--muted) 13%, transparent);
|
|
426
439
|
|
|
427
440
|
/* Light runs the same rule the other way. The signals are mid-tone here, so NO ink
|
|
428
441
|
rescues them and the fill is what has to move: it takes the deepened form the chip
|
|
@@ -579,3 +592,18 @@
|
|
|
579
592
|
.ui-callout--warn,
|
|
580
593
|
.ui-callout--danger,
|
|
581
594
|
.ui-success { --code-bg: var(--wash-code-bg); }
|
|
595
|
+
|
|
596
|
+
/* React badges and pills accept inline code on opaque or translucent status fills. */
|
|
597
|
+
.ui-badge,
|
|
598
|
+
.ui-badge--live,
|
|
599
|
+
.ui-badge--soon,
|
|
600
|
+
.ui-badge--info,
|
|
601
|
+
.ui-badge--warn,
|
|
602
|
+
.ui-badge--danger,
|
|
603
|
+
.ui-badge--archive,
|
|
604
|
+
.ui-badge--success,
|
|
605
|
+
.ui-badge--pending,
|
|
606
|
+
.ui-badge--neutral,
|
|
607
|
+
.ui-pill,
|
|
608
|
+
.ui-pill--live,
|
|
609
|
+
.ui-pill--soon { --code-bg: var(--wash-code-bg); }
|