@lateralus-ai/shipping-ui 2.0.0-dev.22 → 2.0.0-dev.24

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.
@@ -1,61 +1,74 @@
1
- import { forwardRef, type ButtonHTMLAttributes } from "react";
2
- import { cn } from "../../utils/cn";
3
- import { FiltersIcon } from "../../icons/generated";
4
- import { Badge } from "../../primitives/Badge";
5
-
6
- export type FilterPillProps = Omit<
7
- ButtonHTMLAttributes<HTMLButtonElement>,
8
- "children"
9
- > & {
10
- /** When > 0, trigger uses active (blue) fill and shows a count badge. */
11
- activeFilterCount?: number;
12
- };
13
-
14
- /**
15
- * Filter trigger Figma Filter Pill (6154:149608 / 6154:149599).
16
- * Idle: grey-100. Active: accent blue-100 + orange count badge.
17
- */
18
- export const FilterPill = forwardRef<HTMLButtonElement, FilterPillProps>(
19
- (
20
- {
21
- activeFilterCount = 0,
22
- className,
23
- type = "button",
24
- "aria-label": ariaLabel = "Filters",
25
- ...props
26
- },
27
- ref,
28
- ) => {
29
- const active = activeFilterCount > 0;
30
-
31
- return (
32
- <button
33
- ref={ref}
34
- type={type}
35
- aria-label={ariaLabel}
36
- aria-pressed={active}
37
- className={cn(
38
- "relative flex size-9 shrink-0 items-center justify-center rounded-full transition-colors",
39
- active
40
- ? "bg-accent-bg-light text-display-on-light-secondary hover:text-display-on-light-primary"
41
- : "bg-background-secondary text-display-on-light-secondary hover:text-display-on-light-primary",
42
- className,
43
- )}
44
- {...props}
45
- >
46
- <FiltersIcon size="small" />
47
- {active && (
48
- <Badge
49
- color="orange"
50
- type="icon"
51
- className="absolute -right-1 -top-2 border-2 border-background-primary text-display-on-light-primary"
52
- >
53
- {activeFilterCount > 99 ? "99+" : activeFilterCount}
54
- </Badge>
55
- )}
56
- </button>
57
- );
58
- },
59
- );
60
-
61
- FilterPill.displayName = "FilterPill";
1
+ import { forwardRef, type ButtonHTMLAttributes } from "react";
2
+ import { cn } from "../../utils/cn";
3
+ import { FiltersIcon } from "../../icons/generated";
4
+ import { Badge } from "../../primitives/Badge";
5
+
6
+ export type FilterPillAppearance = "filled" | "ghost";
7
+
8
+ export type FilterPillProps = Omit<
9
+ ButtonHTMLAttributes<HTMLButtonElement>,
10
+ "children"
11
+ > & {
12
+ /** When > 0, shows the orange count badge. */
13
+ activeFilterCount?: number;
14
+ /**
15
+ * `filled`grey idle / blue when active (Figma default).
16
+ * `ghost` white idle and active; only hover changes surface color.
17
+ */
18
+ appearance?: FilterPillAppearance;
19
+ };
20
+
21
+ /**
22
+ * Filter trigger — Figma Filter Pill (6154:149608 / 6154:149599).
23
+ */
24
+ export const FilterPill = forwardRef<HTMLButtonElement, FilterPillProps>(
25
+ (
26
+ {
27
+ activeFilterCount = 0,
28
+ appearance = "filled",
29
+ className,
30
+ type = "button",
31
+ "aria-label": ariaLabel = "Filters",
32
+ ...props
33
+ },
34
+ ref,
35
+ ) => {
36
+ const active = activeFilterCount > 0;
37
+ const ghost = appearance === "ghost";
38
+
39
+ return (
40
+ <button
41
+ ref={ref}
42
+ type={type}
43
+ aria-label={ariaLabel}
44
+ aria-pressed={active}
45
+ className={cn(
46
+ "relative flex size-9 shrink-0 items-center justify-center rounded-full transition-colors",
47
+ ghost
48
+ ? "bg-white text-display-on-light-secondary hover:bg-grey-50 hover:text-display-on-light-primary"
49
+ : active
50
+ ? "bg-accent-bg-light text-display-on-light-secondary hover:text-display-on-light-primary"
51
+ : "bg-background-secondary text-display-on-light-secondary hover:text-display-on-light-primary",
52
+ className,
53
+ )}
54
+ {...props}
55
+ >
56
+ <FiltersIcon size="small" />
57
+ {active && (
58
+ <Badge
59
+ color="orange"
60
+ type="icon"
61
+ className={cn(
62
+ "absolute -right-1 -top-2 border-2 text-display-on-light-primary",
63
+ ghost ? "border-white" : "border-background-primary",
64
+ )}
65
+ >
66
+ {activeFilterCount > 99 ? "99+" : activeFilterCount}
67
+ </Badge>
68
+ )}
69
+ </button>
70
+ );
71
+ },
72
+ );
73
+
74
+ FilterPill.displayName = "FilterPill";
@@ -1,34 +1,34 @@
1
- import { cn } from "../../utils/cn";
2
- import { FilteredPill, type FilteredPillProps } from "./FilteredPill";
3
-
4
- export type FilterPillsItem = {
5
- key: string;
6
- label: string;
7
- onRemove: () => void;
8
- removeAriaLabel?: string;
9
- classNames?: FilteredPillProps["classNames"];
10
- };
11
-
12
- export type FilterPillsProps = {
13
- chips: FilterPillsItem[];
14
- className?: string;
15
- };
16
-
17
- /**
18
- * Partner row for FilterDropdown — active filter pills.
19
- * Always renders the row wrapper (even when empty) so sibling filter
20
- * triggers can stay pinned with `ml-auto` / flex layouts.
21
- */
22
- export const FilterPills = ({ chips, className }: FilterPillsProps) => (
23
- <div className={cn("flex min-w-0 flex-wrap items-start gap-2", className)}>
24
- {chips.map((chip) => (
25
- <FilteredPill
26
- key={chip.key}
27
- label={chip.label}
28
- onRemove={chip.onRemove}
29
- removeAriaLabel={chip.removeAriaLabel}
30
- classNames={chip.classNames}
31
- />
32
- ))}
33
- </div>
34
- );
1
+ import { cn } from "../../utils/cn";
2
+ import { FilteredPill, type FilteredPillProps } from "./FilteredPill";
3
+
4
+ export type FilterPillsItem = {
5
+ key: string;
6
+ label: string;
7
+ onRemove: () => void;
8
+ removeAriaLabel?: string;
9
+ classNames?: FilteredPillProps["classNames"];
10
+ };
11
+
12
+ export type FilterPillsProps = {
13
+ chips: FilterPillsItem[];
14
+ className?: string;
15
+ };
16
+
17
+ /**
18
+ * Partner row for FilterDropdown — active filter pills.
19
+ * Always renders the row wrapper (even when empty) so sibling filter
20
+ * triggers can stay pinned with `ml-auto` / flex layouts.
21
+ */
22
+ export const FilterPills = ({ chips, className }: FilterPillsProps) => (
23
+ <div className={cn("flex min-w-0 flex-wrap items-start gap-2", className)}>
24
+ {chips.map((chip) => (
25
+ <FilteredPill
26
+ key={chip.key}
27
+ label={chip.label}
28
+ onRemove={chip.onRemove}
29
+ removeAriaLabel={chip.removeAriaLabel}
30
+ classNames={chip.classNames}
31
+ />
32
+ ))}
33
+ </div>
34
+ );
@@ -1,59 +1,59 @@
1
- import { cn } from "../../utils/cn";
2
- import { ClearIcon } from "../../icons";
3
- import { Tooltip } from "../../primitives/Tooltip";
4
-
5
- export type FilteredPillProps = {
6
- label: string;
7
- onRemove: () => void;
8
- removeAriaLabel?: string;
9
- className?: string;
10
- /** Optional tone overrides. Prefer default beige unless a product needs a custom tone. */
11
- classNames?: {
12
- root?: string;
13
- label?: string;
14
- remove?: string;
15
- };
16
- };
17
-
18
- /**
19
- * Active filter value pill — Figma Filtered (6154:149787).
20
- * Clear control on the left, label on the right. Max 300px with ellipsis;
21
- * full label in a tooltip on hover.
22
- */
23
- export const FilteredPill = ({
24
- label,
25
- onRemove,
26
- removeAriaLabel,
27
- className,
28
- classNames,
29
- }: FilteredPillProps) => (
30
- <Tooltip content={label} hint side="top">
31
- <div
32
- className={cn(
33
- "inline-flex max-w-[300px] min-h-9 items-center justify-center gap-2.5 rounded-full bg-background-secondary px-3 py-1",
34
- classNames?.root,
35
- className,
36
- )}
37
- >
38
- <button
39
- type="button"
40
- onClick={onRemove}
41
- aria-label={removeAriaLabel ?? `Remove ${label} filter`}
42
- className={cn(
43
- "inline-flex size-4 shrink-0 items-center justify-center rounded-control text-display-on-light-secondary transition-colors hover:text-display-on-light-primary",
44
- classNames?.remove,
45
- )}
46
- >
47
- <ClearIcon size="xs" />
48
- </button>
49
- <span
50
- className={cn(
51
- "min-w-0 truncate text-caption-2-em text-display-on-light-primary",
52
- classNames?.label,
53
- )}
54
- >
55
- {label}
56
- </span>
57
- </div>
58
- </Tooltip>
59
- );
1
+ import { cn } from "../../utils/cn";
2
+ import { ClearIcon } from "../../icons";
3
+ import { Tooltip } from "../../primitives/Tooltip";
4
+
5
+ export type FilteredPillProps = {
6
+ label: string;
7
+ onRemove: () => void;
8
+ removeAriaLabel?: string;
9
+ className?: string;
10
+ /** Optional tone overrides. Prefer default beige unless a product needs a custom tone. */
11
+ classNames?: {
12
+ root?: string;
13
+ label?: string;
14
+ remove?: string;
15
+ };
16
+ };
17
+
18
+ /**
19
+ * Active filter value pill — Figma Filtered (6154:149787).
20
+ * Clear control on the left, label on the right. Max 300px with ellipsis;
21
+ * full label in a tooltip on hover.
22
+ */
23
+ export const FilteredPill = ({
24
+ label,
25
+ onRemove,
26
+ removeAriaLabel,
27
+ className,
28
+ classNames,
29
+ }: FilteredPillProps) => (
30
+ <Tooltip content={label} hint side="top">
31
+ <div
32
+ className={cn(
33
+ "inline-flex max-w-[300px] min-h-9 items-center justify-center gap-2.5 rounded-full bg-background-secondary px-3 py-1",
34
+ classNames?.root,
35
+ className,
36
+ )}
37
+ >
38
+ <button
39
+ type="button"
40
+ onClick={onRemove}
41
+ aria-label={removeAriaLabel ?? `Remove ${label} filter`}
42
+ className={cn(
43
+ "inline-flex size-4 shrink-0 items-center justify-center rounded-control text-display-on-light-secondary transition-colors hover:text-display-on-light-primary",
44
+ classNames?.remove,
45
+ )}
46
+ >
47
+ <ClearIcon size="xs" />
48
+ </button>
49
+ <span
50
+ className={cn(
51
+ "min-w-0 truncate text-caption-2-em text-display-on-light-primary",
52
+ classNames?.label,
53
+ )}
54
+ >
55
+ {label}
56
+ </span>
57
+ </div>
58
+ </Tooltip>
59
+ );
@@ -1,72 +1,72 @@
1
- import { cn } from "../../utils/cn";
2
- import { SearchIcon } from "../../icons/generated";
3
- import { FilterPill } from "./FilterPill";
4
- import { FilterPills } from "./FilterPills";
5
-
6
- export type FiltersBarTabs = "on" | "off";
7
- export type FiltersBarFilters = "on" | "off";
8
-
9
- export type FiltersBarProps = {
10
- tabs?: FiltersBarTabs;
11
- filters?: FiltersBarFilters;
12
- tabLabels?: string[];
13
- activeTab?: string;
14
- searchPlaceholder?: string;
15
- filterChips?: string[];
16
- className?: string;
17
- };
18
-
19
- const defaultTabs = ["All", "Reports", "Chats", "Issues", "Forms", "Workflows"];
20
-
21
- export const FiltersBar = ({
22
- tabs = "on",
23
- filters = "off",
24
- tabLabels = defaultTabs,
25
- activeTab = "All",
26
- searchPlaceholder = "Search",
27
- filterChips = ["Status", "Date", "Chief"],
28
- className,
29
- }: FiltersBarProps) => (
30
- <div className={cn("flex w-full flex-col gap-3", className)}>
31
- <div className="flex items-center justify-between gap-4">
32
- {tabs === "on" && (
33
- <div className="flex flex-wrap gap-2">
34
- {tabLabels.map((tab) => (
35
- <button
36
- key={tab}
37
- type="button"
38
- className={cn(
39
- "rounded-full px-3 py-1.5 text-caption-2-em transition-colors",
40
- tab === activeTab
41
- ? "bg-background-selected text-display-on-dark-primary"
42
- : "bg-background-secondary text-display-on-light-tertiary hover:text-display-on-light-primary",
43
- )}
44
- >
45
- {tab}
46
- </button>
47
- ))}
48
- </div>
49
- )}
50
- <div className="flex shrink-0 items-center gap-4">
51
- <FilterPill
52
- activeFilterCount={filters === "on" ? filterChips.length : 0}
53
- />
54
- <div className="flex w-[250px] items-center gap-2 rounded-md border border-divider-primary px-3 py-2">
55
- <span className="flex-1 text-body text-display-on-light-secondary">
56
- {searchPlaceholder}
57
- </span>
58
- <SearchIcon size="small" className="text-display-on-light-tertiary" />
59
- </div>
60
- </div>
61
- </div>
62
- {filters === "on" && (
63
- <FilterPills
64
- chips={filterChips.map((chip) => ({
65
- key: chip,
66
- label: chip,
67
- onRemove: () => undefined,
68
- }))}
69
- />
70
- )}
71
- </div>
72
- );
1
+ import { cn } from "../../utils/cn";
2
+ import { SearchIcon } from "../../icons/generated";
3
+ import { FilterPill } from "./FilterPill";
4
+ import { FilterPills } from "./FilterPills";
5
+
6
+ export type FiltersBarTabs = "on" | "off";
7
+ export type FiltersBarFilters = "on" | "off";
8
+
9
+ export type FiltersBarProps = {
10
+ tabs?: FiltersBarTabs;
11
+ filters?: FiltersBarFilters;
12
+ tabLabels?: string[];
13
+ activeTab?: string;
14
+ searchPlaceholder?: string;
15
+ filterChips?: string[];
16
+ className?: string;
17
+ };
18
+
19
+ const defaultTabs = ["All", "Reports", "Chats", "Issues", "Forms", "Workflows"];
20
+
21
+ export const FiltersBar = ({
22
+ tabs = "on",
23
+ filters = "off",
24
+ tabLabels = defaultTabs,
25
+ activeTab = "All",
26
+ searchPlaceholder = "Search",
27
+ filterChips = ["Status", "Date", "Chief"],
28
+ className,
29
+ }: FiltersBarProps) => (
30
+ <div className={cn("flex w-full flex-col gap-3", className)}>
31
+ <div className="flex items-center justify-between gap-4">
32
+ {tabs === "on" && (
33
+ <div className="flex flex-wrap gap-2">
34
+ {tabLabels.map((tab) => (
35
+ <button
36
+ key={tab}
37
+ type="button"
38
+ className={cn(
39
+ "rounded-full px-3 py-1.5 text-caption-2-em transition-colors",
40
+ tab === activeTab
41
+ ? "bg-background-selected text-display-on-dark-primary"
42
+ : "bg-background-secondary text-display-on-light-tertiary hover:text-display-on-light-primary",
43
+ )}
44
+ >
45
+ {tab}
46
+ </button>
47
+ ))}
48
+ </div>
49
+ )}
50
+ <div className="flex shrink-0 items-center gap-4">
51
+ <FilterPill
52
+ activeFilterCount={filters === "on" ? filterChips.length : 0}
53
+ />
54
+ <div className="flex w-[250px] items-center gap-2 rounded-md border border-divider-primary px-3 py-2">
55
+ <span className="flex-1 text-body text-display-on-light-secondary">
56
+ {searchPlaceholder}
57
+ </span>
58
+ <SearchIcon size="small" className="text-display-on-light-tertiary" />
59
+ </div>
60
+ </div>
61
+ </div>
62
+ {filters === "on" && (
63
+ <FilterPills
64
+ chips={filterChips.map((chip) => ({
65
+ key: chip,
66
+ label: chip,
67
+ onRemove: () => undefined,
68
+ }))}
69
+ />
70
+ )}
71
+ </div>
72
+ );
@@ -1,23 +1,23 @@
1
- /**
2
- * Format the user-visible label for an active-filter chip (audit-prep rules).
3
- *
4
- * - 0 selected → "" (caller should not render)
5
- * - 1 selected → `<filterName> (<value>)`
6
- * - 2 selected → `<filterName> (<v1>, <v2>)`
7
- * - 3+ selected → `<filterName> (<v1>, <v2>...)` (literal `...`)
8
- *
9
- * `selectedLabels` are already human-readable, in selection order.
10
- */
11
- export function formatActiveFilterChipLabel(
12
- filterName: string,
13
- selectedLabels: string[],
14
- ): string {
15
- if (selectedLabels.length === 0) return "";
16
- if (selectedLabels.length === 1) {
17
- return `${filterName} (${selectedLabels[0]})`;
18
- }
19
- if (selectedLabels.length === 2) {
20
- return `${filterName} (${selectedLabels[0]}, ${selectedLabels[1]})`;
21
- }
22
- return `${filterName} (${selectedLabels[0]}, ${selectedLabels[1]}...)`;
23
- }
1
+ /**
2
+ * Format the user-visible label for an active-filter chip (audit-prep rules).
3
+ *
4
+ * - 0 selected → "" (caller should not render)
5
+ * - 1 selected → `<filterName> (<value>)`
6
+ * - 2 selected → `<filterName> (<v1>, <v2>)`
7
+ * - 3+ selected → `<filterName> (<v1>, <v2>...)` (literal `...`)
8
+ *
9
+ * `selectedLabels` are already human-readable, in selection order.
10
+ */
11
+ export function formatActiveFilterChipLabel(
12
+ filterName: string,
13
+ selectedLabels: string[],
14
+ ): string {
15
+ if (selectedLabels.length === 0) return "";
16
+ if (selectedLabels.length === 1) {
17
+ return `${filterName} (${selectedLabels[0]})`;
18
+ }
19
+ if (selectedLabels.length === 2) {
20
+ return `${filterName} (${selectedLabels[0]}, ${selectedLabels[1]})`;
21
+ }
22
+ return `${filterName} (${selectedLabels[0]}, ${selectedLabels[1]}...)`;
23
+ }
@@ -1,35 +1,37 @@
1
- export {
2
- FiltersBar,
3
- type FiltersBarFilters,
4
- type FiltersBarProps,
5
- type FiltersBarTabs,
6
- } from "./FiltersBar";
7
- export {
8
- FilteredPill,
9
- type FilteredPillProps,
10
- } from "./FilteredPill";
11
- export {
12
- FilterPill,
13
- type FilterPillProps,
14
- } from "./FilterPill";
15
- export {
16
- FilterPills,
17
- type FilterPillsItem,
18
- type FilterPillsProps,
19
- } from "./FilterPills";
20
- export {
21
- FilterDropdown,
22
- resolveSubmenuView,
23
- type FilterCategoryRow,
24
- type FilterCategorySubmenuRow,
25
- type FilterCategoryToggleRow,
26
- type FilterDropdownProps,
27
- type FilterNestedItem,
28
- type FilterOption,
29
- type FilterSelectionMode,
30
- type FilterSubmenuContent,
31
- } from "./FilterDropdown";
32
- export { formatActiveFilterChipLabel } from "./formatActiveFilterChipLabel";
33
-
34
- /** @deprecated Use `activeFilterCount` on FilterPill instead. */
35
- export type FilterPillIndicator = "on" | "off";
1
+ export {
2
+ FiltersBar,
3
+ type FiltersBarFilters,
4
+ type FiltersBarProps,
5
+ type FiltersBarTabs,
6
+ } from "./FiltersBar";
7
+ export {
8
+ FilteredPill,
9
+ type FilteredPillProps,
10
+ } from "./FilteredPill";
11
+ export {
12
+ FilterPill,
13
+ type FilterPillAppearance,
14
+ type FilterPillProps,
15
+ } from "./FilterPill";
16
+ export {
17
+ FilterPills,
18
+ type FilterPillsItem,
19
+ type FilterPillsProps,
20
+ } from "./FilterPills";
21
+ export {
22
+ FilterDropdown,
23
+ resolveSubmenuView,
24
+ type FilterCategoryInlineOptionsRow,
25
+ type FilterCategoryRow,
26
+ type FilterCategorySubmenuRow,
27
+ type FilterCategoryToggleRow,
28
+ type FilterDropdownProps,
29
+ type FilterNestedItem,
30
+ type FilterOption,
31
+ type FilterSelectionMode,
32
+ type FilterSubmenuContent,
33
+ } from "./FilterDropdown";
34
+ export { formatActiveFilterChipLabel } from "./formatActiveFilterChipLabel";
35
+
36
+ /** @deprecated Use `activeFilterCount` on FilterPill instead. */
37
+ export type FilterPillIndicator = "on" | "off";
@@ -1,12 +1,19 @@
1
1
  import { type ButtonHTMLAttributes } from "react";
2
2
  import { cn } from "../utils/cn";
3
3
 
4
- export type SwitchProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> & {
4
+ export type SwitchProps = Omit<
5
+ ButtonHTMLAttributes<HTMLButtonElement>,
6
+ "onChange"
7
+ > & {
5
8
  checked: boolean;
6
9
  onChange: (checked: boolean) => void;
7
10
  disabled?: boolean;
8
11
  };
9
12
 
13
+ /**
14
+ * On/off control — Figma Switch (3068:65681).
15
+ * Off: grey track, thumb left. On: grey-900 track, thumb right. Raise-2 on thumb.
16
+ */
10
17
  export const Switch = ({
11
18
  checked,
12
19
  onChange,
@@ -19,18 +26,21 @@ export const Switch = ({
19
26
  role="switch"
20
27
  aria-checked={checked}
21
28
  disabled={disabled}
22
- onClick={() => onChange(!checked)}
29
+ onClick={(event) => {
30
+ event.stopPropagation();
31
+ onChange(!checked);
32
+ }}
23
33
  className={cn(
24
- "relative inline-flex h-6 w-10 shrink-0 items-center rounded-full transition-colors disabled:cursor-not-allowed",
25
- checked ? "bg-blue-600" : "bg-grey-200",
26
- disabled && "opacity-50",
34
+ "relative inline-flex h-4 w-9 shrink-0 items-center rounded-full transition-colors",
35
+ "disabled:cursor-not-allowed disabled:opacity-50",
36
+ checked ? "bg-grey-900" : "bg-grey-200",
27
37
  className,
28
38
  )}
29
39
  {...props}
30
40
  >
31
41
  <span
32
42
  className={cn(
33
- "inline-block size-5 rounded-full bg-white shadow-raise1 transition-transform",
43
+ "inline-block size-3.5 rounded-full bg-white shadow-raise2 transition-transform",
34
44
  checked ? "translate-x-[18px]" : "translate-x-0.5",
35
45
  )}
36
46
  />