@lotics/ui 47.6.1 → 47.7.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/docs/catalog.md CHANGED
@@ -1045,8 +1045,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1045
1045
  - **`card_select_item`** — `CardSelectItem`: a bordered, card-shaped button; `selected` =
1046
1046
  persistent ring (reads as `aria-pressed`) — the org-picker / entity-switcher item.
1047
1047
  - **`switcher`** — `Switcher`: current-item trigger opening a popover of `MenuButton` items
1048
- (`SwitcherItem { id, label }`, `currentId`, `onSelect`) — the compact entity/workspace
1049
- switcher.
1048
+ (`SwitcherItem { id, label, imageUri? }`, `currentId`, `onSelect`) — the compact
1049
+ entity/workspace switcher. A square `Avatar` appears on the trigger and every row as soon as
1050
+ ONE item carries `imageUri`, the rest falling back to initials, so an unpictured entry still
1051
+ lines up. `minItemsToRender` defaults to 2 (a single choice is clutter); lower it to 1 where
1052
+ the switcher states a scope the reader has no other way to see.
1050
1053
  - **`count`** — `Count`: a 20px count disc that grows into a pill past one digit (`color` highlight|muted|red).
1051
1054
  - **`shortcut_badge`** — `ShortcutBadge`: the keycap hint pill — a zinc-50 badge rendering a
1052
1055
  shortcut from a raw string or `ShortcutDescriptor` (⌘B on Mac, Ctrl+B elsewhere); null on
@@ -1772,7 +1775,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1772
1775
  opened the screen for. Pass `activeCount` so the collapsed button still says the list is
1773
1776
  filtered, and `onClearAll` to put one clear in the sheet. Compose the band with this rather
1774
1777
  than a hand-rolled `<View flexDirection="row">`: a hand-rolled band is the one that does not
1775
- collapse.
1778
+ collapse. **A facet with nothing to offer should render `null`, and the band then draws no
1779
+ trigger at all** — narrow as well as wide. Gate each chip on its own options (a status filter
1780
+ whose values are all already in view, a round picker before any round is recorded) rather than
1781
+ shipping an empty one; search and the action stay on the band either way.
1776
1782
  - **`filter_chip`** — `FilterChip` + `selectSummary`: the toolbar filter pill hosting a
1777
1783
  facet (options, a `Slider range`, a `Counter`). It draws NO bottom band of its own — the ×
1778
1784
  on the pill is the clear, and the editor inside brings whatever actions it has, so a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "47.6.1",
3
+ "version": "47.7.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./accordion": "./src/accordion.tsx",
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useState, type ReactNode } from "react";
1
+ import { Children, createContext, useContext, useState, type ReactNode } from "react";
2
2
  import { View } from "react-native";
3
3
  import { Button } from "./button";
4
4
  import { Dialog, DialogFooter, DialogHeader, DialogHeaderTitle, DialogScrollArea } from "./dialog";
@@ -58,6 +58,14 @@ export function FilterBand(props: FilterBandProps) {
58
58
  const t = useLoticsLocale().filterBand;
59
59
  const { small } = useScreenSize();
60
60
  const [open, setOpen] = useState(false);
61
+ // WHETHER THERE IS ANYTHING TO COLLAPSE. `Children.toArray` drops the nulls a
62
+ // caller renders for a facet that has nothing to offer — a status filter whose
63
+ // options are all in view, a round picker before any round is recorded — so a
64
+ // band that draws no chips wide must not draw a button narrow. It did: the wide
65
+ // branch renders `children` and gets nothing, the narrow one rendered the
66
+ // trigger unconditionally, and the two presentations stopped agreeing at exactly
67
+ // the width where a control that opens an empty sheet costs the most.
68
+ const hasFilters = Children.toArray(children).length > 0;
61
69
 
62
70
  if (!small) {
63
71
  return (
@@ -77,14 +85,16 @@ export function FilterBand(props: FilterBandProps) {
77
85
  return (
78
86
  <View testID={testID} style={{ flexDirection: "row", alignItems: "center", gap: SPACE.sm }}>
79
87
  {search != null && <View style={{ flexGrow: 1, flexShrink: 1, minWidth: 0 }}>{search}</View>}
80
- <Button
81
- color="secondary"
82
- title={activeCount > 0 ? `${t.filters} (${activeCount})` : t.filters}
83
- onPress={() => setOpen(true)}
84
- testID={testID ? `${testID}-filters` : undefined}
85
- />
88
+ {hasFilters && (
89
+ <Button
90
+ color="secondary"
91
+ title={activeCount > 0 ? `${t.filters} (${activeCount})` : t.filters}
92
+ onPress={() => setOpen(true)}
93
+ testID={testID ? `${testID}-filters` : undefined}
94
+ />
95
+ )}
86
96
  {action}
87
- <Dialog open={open} onOpenChange={setOpen} width="92%" maxWidth={520}>
97
+ <Dialog open={open && hasFilters} onOpenChange={setOpen} width="92%" maxWidth={520}>
88
98
  <DialogHeader>
89
99
  <DialogHeaderTitle>{t.filters}</DialogHeaderTitle>
90
100
  </DialogHeader>
package/src/switcher.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import React, { useCallback, useState } from "react";
2
2
  import { StyleSheet } from "react-native";
3
+ import { Avatar } from "./avatar";
3
4
  import { Icon } from "./icon";
4
5
  import { MenuButton } from "./menu_button";
5
6
  import { PressableHighlight } from "./pressable_highlight";
@@ -12,6 +13,15 @@ import { CONTROL_HEIGHT, CONTROL_RADIUS } from "./control_surface";
12
13
  export interface SwitcherItem {
13
14
  id: string;
14
15
  label: string;
16
+ /**
17
+ * An identity image for this item. Optional, and optional PER ITEM rather than
18
+ * per switcher: the marks appear as soon as ONE item carries an image, and an
19
+ * item without one falls back to `Avatar`'s initials. Letting the mark appear
20
+ * only on the items that have a picture is the alternative, and it shifts every
21
+ * unmarked label sideways — so the one entry nobody has photographed is also the
22
+ * one that stops lining up with the rest of the list.
23
+ */
24
+ imageUri?: string;
15
25
  }
16
26
 
17
27
  interface SwitcherProps {
@@ -61,17 +71,29 @@ export function Switcher(props: SwitcherProps) {
61
71
  [currentId, onSelect],
62
72
  );
63
73
 
64
- if (items.length < minItemsToRender) return null;
74
+ // `minItemsToRender` can be lowered to 1 — a switcher that always shows is how a
75
+ // hard scope tells the reader which one they are in — so the empty set is the
76
+ // only one left to answer for, and there is nothing to render for it.
77
+ if (items.length === 0 || items.length < minItemsToRender) return null;
65
78
 
66
- const current = items.find((item) => item.id === currentId);
67
- const label = current?.label ?? items[0].label;
79
+ const current = items.find((item) => item.id === currentId) ?? items[0];
80
+ const withMarks = items.some((item) => item.imageUri);
81
+ // The `md` rung is `CONTROL_CONTENT_HEIGHT` — what a 40px band seats, which
82
+ // both the trigger and a `MenuButton` row are. `square`, because the things a
83
+ // switcher switches BETWEEN are organizations, workspaces and projects; a disc
84
+ // would file them as people.
85
+ const mark = (item: SwitcherItem) =>
86
+ withMarks ? (
87
+ <Avatar shape="square" size="md" name={item.label} source={item.imageUri ? { uri: item.imageUri } : undefined} />
88
+ ) : undefined;
68
89
 
69
90
  return (
70
91
  <Popover open={open} onOpenChange={setOpen} side={side} align={align}>
71
92
  <PopoverTrigger>
72
93
  <PressableHighlight focusRing style={[styles.trigger, { maxWidth: maxTriggerWidth }]}>
94
+ {mark(current)}
73
95
  <Text size="sm" weight="medium" color="muted" numberOfLines={1}>
74
- {label}
96
+ {current.label}
75
97
  </Text>
76
98
  <Icon name="chevrons-up-down" size={14} color={colors.zinc["500"]} />
77
99
  </PressableHighlight>
@@ -81,6 +103,7 @@ export function Switcher(props: SwitcherProps) {
81
103
  {items.map((item) => (
82
104
  <MenuButton
83
105
  key={item.id}
106
+ icon={mark(item)}
84
107
  title={item.label}
85
108
  onPress={() => handleSelect(item.id)}
86
109
  right={