@drakkar.software/octospaces-ui 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog — @drakkar.software/octospaces-ui
2
+
3
+ ## 0.4.2
4
+
5
+ ### `SpaceSwitcher` — `onTriggerPress` override
6
+
7
+ Added an optional `onTriggerPress?: () => void` prop to `SpaceSwitcher`.
8
+
9
+ When provided:
10
+ - The trigger button's `onPress` calls `onTriggerPress` instead of opening the
11
+ built-in dropdown.
12
+ - The chevron icon is hidden (the button no longer implies a picker).
13
+ - The `renderContainer` callback is never invoked.
14
+
15
+ Use this on surfaces where a dropdown is redundant (e.g. a desktop sidebar that
16
+ already has a rail for space switching) and a single tap to navigate to the
17
+ space-details page is the right UX.
18
+
19
+ Fully backward-compatible — existing consumers that do not pass `onTriggerPress`
20
+ retain identical behaviour.
21
+
22
+ ---
23
+
24
+ ## 0.4.1
25
+
26
+ - `Lightbox` component added (headless fullscreen media viewer + zoom).
27
+
28
+ ## 0.4.0
29
+
30
+ - `Sidebar`, `SidebarHeader`, `SidebarActionButton`, `SidebarItem` — shared
31
+ desktop sidebar panel shell and primitives.
32
+ - `SpaceSwitcher` — headless space picker trigger with `renderContainer` delegation.
33
+ - `sidebarPanel` added to `Palette` type (panel background distinct from rail
34
+ background `sidebar`).
package/dist/index.d.ts CHANGED
@@ -649,12 +649,13 @@ declare function SidebarItem({ label, icon, active, badge, onPress, onLongPress,
649
649
  * Headless themed space-switcher component.
650
650
  *
651
651
  * Renders a trigger button (active-space avatar + name + chevron) that opens a
652
- * dropdown listing all spaces with per-row selection, a "join or create" action,
652
+ * dropdown listing all spaces with per-row selection (+ optional unread badges),
653
+ * a "see all" overflow row, a "join or create" action, a "browse spaces" action,
653
654
  * optional space settings, and an app-provided footer slot (account section, etc.).
654
655
  *
655
656
  * The popup container (Popover on desktop, Sheet on mobile) is fully delegated to
656
657
  * the host via `renderContainer` so this package stays free of modal dependencies.
657
- * Avatars and icons are delegated via render-props for the same reason.
658
+ * Avatars, icons and badges are delegated via render-props for the same reason.
658
659
  *
659
660
  * @example
660
661
  * ```tsx
@@ -674,13 +675,34 @@ declare function SidebarItem({ label, icon, active, badge, onPress, onLongPress,
674
675
  * )}
675
676
  * renderIcon={(name, size, color) => <Icon name={SWITCHER_ICON[name]} size={size} color={color} />}
676
677
  * renderContainer={({ isOpen, onClose, anchorRef, children }) => (
677
- * <>
678
- * <Popover visible={isOpen} onClose={onClose} anchorRef={anchorRef} placement="bottom-start" width={240}>
679
- * {children}
680
- * </Popover>
681
- * </>
678
+ * <Popover visible={isOpen} onClose={onClose} anchorRef={anchorRef} placement="bottom-start" width={240}>
679
+ * {children}
680
+ * </Popover>
682
681
  * )}
683
- * footerSlot={<AccountSwitcher onRequestClose={...} onViewProfile={...} />}
682
+ * footerSlot={(close) => <AccountSwitcher onRequestClose={close} onViewProfile={...} />}
683
+ * />
684
+ *
685
+ * // OctoChat — appbar variant with bottom Sheet, overflow + badges
686
+ * <SpaceSwitcher
687
+ * spaces={spaces}
688
+ * activeId={activeId}
689
+ * onSelect={(id) => { tapFeedback(); setActiveId(id); }}
690
+ * onAdd={() => router.push('/join')}
691
+ * onBrowse={() => router.push('/spaces/explore')}
692
+ * onSettings={() => router.push(`/space/${activeId}`)}
693
+ * maxVisible={5}
694
+ * onSeeAll={() => router.push('/spaces')}
695
+ * seeAllLabel="See all spaces"
696
+ * variant="appbar"
697
+ * renderTriggerAvatar={(space, size) => <Avatar label={space?.short ?? ''} image={space?.image} size={size} />}
698
+ * renderSpaceAvatar={(space, size) => <Avatar label={space.short ?? ''} image={space.image} size={size} />}
699
+ * renderIcon={(name, size, color) => <Icon name={SWITCHER_ICON[name]} size={size} color={color} />}
700
+ * renderBadge={(count) => <Badge count={count} />}
701
+ * renderTriggerBadge={() => <UnreadDot />}
702
+ * renderContainer={({ isOpen, onClose, children }) => (
703
+ * <BottomSheet visible={isOpen} onClose={onClose}>{children}</BottomSheet>
704
+ * )}
705
+ * footerSlot={(close) => <AccountSwitcher onRequestClose={close} onViewProfile={...} />}
684
706
  * />
685
707
  * ```
686
708
  */
@@ -693,9 +715,11 @@ interface SwitcherSpace {
693
715
  short?: string;
694
716
  /** Uploaded space image URI; absent → host renders monogram. */
695
717
  image?: string;
718
+ /** Unread message count displayed as a badge on the row. */
719
+ unread?: number;
696
720
  }
697
721
  /** Icon name union for the switcher's built-in glyphs. */
698
- type SwitcherIconName = 'chevron-down' | 'check' | 'plus' | 'gear';
722
+ type SwitcherIconName = 'chevron-down' | 'chevron-right' | 'check' | 'plus' | 'gear' | 'globe';
699
723
  interface SpaceSwitcherProps {
700
724
  spaces: SwitcherSpace[];
701
725
  activeId?: string | null;
@@ -705,6 +729,10 @@ interface SpaceSwitcherProps {
705
729
  onAdd?: () => void;
706
730
  /** Override the add-row label. @default "Join or create a space" */
707
731
  addLabel?: string;
732
+ /** "Browse spaces" action (e.g. a public directory). Omit to hide the row. */
733
+ onBrowse?: () => void;
734
+ /** Override the browse-row label. @default "Browse spaces" */
735
+ browseLabel?: string;
708
736
  /**
709
737
  * "Space settings" action. Only shown when both `onSettings` and `activeId`
710
738
  * are provided. Omit to hide.
@@ -712,6 +740,17 @@ interface SpaceSwitcherProps {
712
740
  onSettings?: () => void;
713
741
  /** Override the settings-row label. @default "Space settings" */
714
742
  settingsLabel?: string;
743
+ /**
744
+ * When set, limits how many space rows are rendered inline.
745
+ * If `spaces.length > maxVisible` AND `onSeeAll` is also set, a "See all"
746
+ * row is appended after the visible rows. Without `onSeeAll`, overflow rows
747
+ * are simply hidden.
748
+ */
749
+ maxVisible?: number;
750
+ /** Called when the user taps the "See all" overflow row. */
751
+ onSeeAll?: () => void;
752
+ /** Override the see-all-row label. @default "See all spaces" */
753
+ seeAllLabel?: string;
715
754
  /**
716
755
  * Visual variant:
717
756
  * - `'sidebar'` — compact left-aligned trigger for the desktop sidebar header.
@@ -735,6 +774,11 @@ interface SpaceSwitcherProps {
735
774
  * Omit to render nothing in the avatar slot.
736
775
  */
737
776
  renderTriggerAvatar?: (space: SwitcherSpace | null, size: number) => React.ReactNode;
777
+ /**
778
+ * Render an overlay node anchored top-right of the trigger avatar — used for
779
+ * an "other spaces have unread" aggregate indicator. Omit to hide the overlay.
780
+ */
781
+ renderTriggerBadge?: () => React.ReactNode;
738
782
  /**
739
783
  * Render a space row's leading avatar.
740
784
  * Receives the `SwitcherSpace` and a pixel size.
@@ -742,17 +786,23 @@ interface SpaceSwitcherProps {
742
786
  */
743
787
  renderSpaceAvatar?: (space: SwitcherSpace, size: number) => React.ReactNode;
744
788
  /**
745
- * Render an icon glyph. Name is one of `'chevron-down' | 'check' | 'plus' | 'gear'`.
789
+ * Render an icon glyph. Name is one of the `SwitcherIconName` union values.
746
790
  * Omit to hide chevron, check, and action icons (spaces remain selectable).
747
791
  */
748
792
  renderIcon?: (name: SwitcherIconName, size: number, color: string) => React.ReactNode;
793
+ /**
794
+ * Render an unread-count badge on a space row.
795
+ * Receives the count (always > 0 when called). Omit to hide badges.
796
+ */
797
+ renderBadge?: (count: number) => React.ReactNode;
749
798
  /**
750
799
  * Footer rendered below the space list + action rows — use for account-switcher
751
- * sections (with separator if needed). Fully app-owned.
800
+ * sections. Receives `close` so the account section can dismiss the dropdown after
801
+ * an action (e.g. `<AccountSwitcher onRequestClose={close} />`). Fully app-owned.
752
802
  */
753
- footerSlot?: React.ReactNode;
803
+ footerSlot?: (close: () => void) => React.ReactNode;
754
804
  }
755
- declare function SpaceSwitcher({ spaces, activeId, onSelect, onAdd, addLabel, onSettings, settingsLabel, variant, renderContainer, renderTriggerAvatar, renderSpaceAvatar, renderIcon, footerSlot, }: SpaceSwitcherProps): React.JSX.Element;
805
+ declare function SpaceSwitcher({ spaces, activeId, onSelect, onAdd, addLabel, onBrowse, browseLabel, onSettings, settingsLabel, maxVisible, onSeeAll, seeAllLabel, variant, renderContainer, renderTriggerAvatar, renderTriggerBadge, renderSpaceAvatar, renderIcon, renderBadge, footerSlot, }: SpaceSwitcherProps): React.JSX.Element;
756
806
 
757
807
  /**
758
808
  * Full-screen scrim overlay that centers its content. Tapping the backdrop, the
package/dist/index.js CHANGED
@@ -1059,13 +1059,20 @@ function SpaceSwitcher({
1059
1059
  onSelect,
1060
1060
  onAdd,
1061
1061
  addLabel = "Join or create a space",
1062
+ onBrowse,
1063
+ browseLabel = "Browse spaces",
1062
1064
  onSettings,
1063
1065
  settingsLabel = "Space settings",
1066
+ maxVisible,
1067
+ onSeeAll,
1068
+ seeAllLabel = "See all spaces",
1064
1069
  variant,
1065
1070
  renderContainer,
1066
1071
  renderTriggerAvatar,
1072
+ renderTriggerBadge,
1067
1073
  renderSpaceAvatar,
1068
1074
  renderIcon,
1075
+ renderBadge,
1069
1076
  footerSlot
1070
1077
  }) {
1071
1078
  const theme = useOctoSpacesTheme();
@@ -1083,10 +1090,18 @@ function SpaceSwitcher({
1083
1090
  close();
1084
1091
  onAdd?.();
1085
1092
  };
1093
+ const handleBrowse = () => {
1094
+ close();
1095
+ onBrowse?.();
1096
+ };
1086
1097
  const handleSettings = () => {
1087
1098
  close();
1088
1099
  onSettings?.();
1089
1100
  };
1101
+ const handleSeeAll = () => {
1102
+ close();
1103
+ onSeeAll?.();
1104
+ };
1090
1105
  const sp1 = sp["1"] ?? 4;
1091
1106
  const sp2 = sp["2"] ?? 8;
1092
1107
  const sp3 = sp["3"] ?? 12;
@@ -1097,7 +1112,9 @@ function SpaceSwitcher({
1097
1112
  const bodyLine = typeScale["callout"]?.lineHeight ?? 18;
1098
1113
  const labelSize = typeScale["caption"]?.size ?? 11;
1099
1114
  const labelLine = typeScale["caption"]?.lineHeight ?? 16;
1100
- const triggerStyle = variant === "sidebar" ? {
1115
+ const overflow = maxVisible != null && onSeeAll != null && spaces.length > maxVisible;
1116
+ const visibleSpaces = overflow ? spaces.slice(0, maxVisible) : spaces;
1117
+ const baseStyle = variant === "sidebar" ? {
1101
1118
  flex: 1,
1102
1119
  flexDirection: "row",
1103
1120
  alignItems: "center",
@@ -1105,8 +1122,7 @@ function SpaceSwitcher({
1105
1122
  paddingHorizontal: sp2,
1106
1123
  paddingVertical: sp1 + 2,
1107
1124
  borderRadius: radMd,
1108
- minWidth: 0,
1109
- backgroundColor: triggerHovered ? colors.primarySubtle ?? "rgba(0,0,0,0.05)" : "transparent"
1125
+ minWidth: 0
1110
1126
  } : {
1111
1127
  flexDirection: "row",
1112
1128
  alignItems: "center",
@@ -1114,8 +1130,7 @@ function SpaceSwitcher({
1114
1130
  gap: sp2,
1115
1131
  paddingHorizontal: sp2,
1116
1132
  paddingVertical: sp1,
1117
- borderRadius: radMd,
1118
- backgroundColor: triggerHovered ? colors.primarySubtle ?? "rgba(0,0,0,0.05)" : "transparent"
1133
+ borderRadius: radMd
1119
1134
  };
1120
1135
  const dropdownContent = /* @__PURE__ */ React10.createElement(View8, { style: { paddingVertical: sp1 } }, spaces.length > 0 ? /* @__PURE__ */ React10.createElement(
1121
1136
  SectionLabel,
@@ -1128,7 +1143,7 @@ function SpaceSwitcher({
1128
1143
  paddingH: sp4,
1129
1144
  paddingV: sp1
1130
1145
  }
1131
- ) : null, spaces.map((s) => /* @__PURE__ */ React10.createElement(
1146
+ ) : null, visibleSpaces.map((s) => /* @__PURE__ */ React10.createElement(
1132
1147
  SpaceRow,
1133
1148
  {
1134
1149
  key: s.id,
@@ -1137,6 +1152,23 @@ function SpaceSwitcher({
1137
1152
  onPress: () => handleSelect(s.id),
1138
1153
  renderAvatar: renderSpaceAvatar,
1139
1154
  renderIcon,
1155
+ renderBadge,
1156
+ colors,
1157
+ bodyFont,
1158
+ bodySize,
1159
+ bodyLine,
1160
+ sp2,
1161
+ sp3,
1162
+ sp4,
1163
+ radMd
1164
+ }
1165
+ )), overflow ? /* @__PURE__ */ React10.createElement(
1166
+ ActionRow,
1167
+ {
1168
+ label: seeAllLabel,
1169
+ iconName: "chevron-right",
1170
+ onPress: handleSeeAll,
1171
+ renderIcon,
1140
1172
  colors,
1141
1173
  bodyFont,
1142
1174
  bodySize,
@@ -1146,7 +1178,7 @@ function SpaceSwitcher({
1146
1178
  sp4,
1147
1179
  radMd
1148
1180
  }
1149
- )), onAdd ? /* @__PURE__ */ React10.createElement(
1181
+ ) : null, onAdd ? /* @__PURE__ */ React10.createElement(
1150
1182
  ActionRow,
1151
1183
  {
1152
1184
  label: spaces.length > 0 ? addLabel : "Create your first space",
@@ -1162,6 +1194,22 @@ function SpaceSwitcher({
1162
1194
  sp4,
1163
1195
  radMd
1164
1196
  }
1197
+ ) : null, onBrowse ? /* @__PURE__ */ React10.createElement(
1198
+ ActionRow,
1199
+ {
1200
+ label: browseLabel,
1201
+ iconName: "globe",
1202
+ onPress: handleBrowse,
1203
+ renderIcon,
1204
+ colors,
1205
+ bodyFont,
1206
+ bodySize,
1207
+ bodyLine,
1208
+ sp2,
1209
+ sp3,
1210
+ sp4,
1211
+ radMd
1212
+ }
1165
1213
  ) : null, onSettings && active ? /* @__PURE__ */ React10.createElement(
1166
1214
  ActionRow,
1167
1215
  {
@@ -1188,7 +1236,7 @@ function SpaceSwitcher({
1188
1236
  marginHorizontal: sp2
1189
1237
  }
1190
1238
  }
1191
- ), footerSlot) : null);
1239
+ ), footerSlot(close)) : null);
1192
1240
  return /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement(
1193
1241
  Pressable6,
1194
1242
  {
@@ -1200,9 +1248,14 @@ function SpaceSwitcher({
1200
1248
  onPress: () => setOpen(true),
1201
1249
  onMouseEnter: () => setTriggerHovered(true),
1202
1250
  onMouseLeave: () => setTriggerHovered(false),
1203
- style: triggerStyle
1251
+ style: ({ pressed }) => [
1252
+ baseStyle,
1253
+ {
1254
+ backgroundColor: pressed ? colors.primarySubtle ?? "rgba(0,0,0,0.08)" : triggerHovered ? colors.primarySubtle ?? "rgba(0,0,0,0.05)" : "transparent"
1255
+ }
1256
+ ]
1204
1257
  },
1205
- renderTriggerAvatar ? renderTriggerAvatar(active, 22) : null,
1258
+ renderTriggerAvatar != null || renderTriggerBadge != null ? /* @__PURE__ */ React10.createElement(View8, { style: styles3.avatarWrap }, renderTriggerAvatar ? renderTriggerAvatar(active, 22) : null, renderTriggerBadge ? /* @__PURE__ */ React10.createElement(View8, { style: styles3.triggerBadge }, renderTriggerBadge()) : null) : null,
1206
1259
  /* @__PURE__ */ React10.createElement(
1207
1260
  Text6,
1208
1261
  {
@@ -1223,6 +1276,10 @@ function SpaceSwitcher({
1223
1276
  renderIcon ? renderIcon("chevron-down", 14, colors.textTertiary) : null
1224
1277
  ), renderContainer({ isOpen: open, onClose: close, anchorRef, children: dropdownContent }));
1225
1278
  }
1279
+ var styles3 = StyleSheet3.create({
1280
+ avatarWrap: { position: "relative" },
1281
+ triggerBadge: { position: "absolute", top: -2, right: -2 }
1282
+ });
1226
1283
  function SectionLabel({ label, color, size, lineHeight, font, paddingH, paddingV }) {
1227
1284
  return /* @__PURE__ */ React10.createElement(
1228
1285
  Text6,
@@ -1248,6 +1305,7 @@ function SpaceRow({
1248
1305
  onPress,
1249
1306
  renderAvatar,
1250
1307
  renderIcon,
1308
+ renderBadge,
1251
1309
  colors,
1252
1310
  bodyFont,
1253
1311
  bodySize,
@@ -1258,7 +1316,7 @@ function SpaceRow({
1258
1316
  radMd
1259
1317
  }) {
1260
1318
  const [hovered, setHovered] = useState5(false);
1261
- const bg = hovered ? colors.primarySubtle ?? "rgba(0,0,0,0.04)" : "transparent";
1319
+ const unread = space.unread ?? 0;
1262
1320
  return /* @__PURE__ */ React10.createElement(
1263
1321
  Pressable6,
1264
1322
  {
@@ -1268,15 +1326,15 @@ function SpaceRow({
1268
1326
  onPress,
1269
1327
  onMouseEnter: () => setHovered(true),
1270
1328
  onMouseLeave: () => setHovered(false),
1271
- style: {
1329
+ style: ({ pressed }) => ({
1272
1330
  flexDirection: "row",
1273
1331
  alignItems: "center",
1274
1332
  gap: sp3,
1275
1333
  paddingHorizontal: sp4,
1276
1334
  paddingVertical: sp2,
1277
1335
  borderRadius: radMd,
1278
- backgroundColor: bg
1279
- }
1336
+ backgroundColor: pressed ? colors.primarySubtle ?? "rgba(0,0,0,0.08)" : hovered ? colors.primarySubtle ?? "rgba(0,0,0,0.04)" : "transparent"
1337
+ })
1280
1338
  },
1281
1339
  renderAvatar ? renderAvatar(space, 24) : null,
1282
1340
  /* @__PURE__ */ React10.createElement(
@@ -1295,6 +1353,7 @@ function SpaceRow({
1295
1353
  },
1296
1354
  space.name
1297
1355
  ),
1356
+ unread > 0 && renderBadge ? renderBadge(unread) : null,
1298
1357
  active && renderIcon ? renderIcon("check", 15, colors.primary) : null
1299
1358
  );
1300
1359
  }
@@ -1313,7 +1372,6 @@ function ActionRow({
1313
1372
  radMd
1314
1373
  }) {
1315
1374
  const [hovered, setHovered] = useState5(false);
1316
- const bg = hovered ? colors.primarySubtle ?? "rgba(0,0,0,0.04)" : "transparent";
1317
1375
  return /* @__PURE__ */ React10.createElement(
1318
1376
  Pressable6,
1319
1377
  {
@@ -1322,15 +1380,15 @@ function ActionRow({
1322
1380
  onPress,
1323
1381
  onMouseEnter: () => setHovered(true),
1324
1382
  onMouseLeave: () => setHovered(false),
1325
- style: {
1383
+ style: ({ pressed }) => ({
1326
1384
  flexDirection: "row",
1327
1385
  alignItems: "center",
1328
1386
  gap: sp3,
1329
1387
  paddingHorizontal: sp4,
1330
1388
  paddingVertical: sp2,
1331
1389
  borderRadius: radMd,
1332
- backgroundColor: bg
1333
- }
1390
+ backgroundColor: pressed ? colors.primarySubtle ?? "rgba(0,0,0,0.08)" : hovered ? colors.primarySubtle ?? "rgba(0,0,0,0.04)" : "transparent"
1391
+ })
1334
1392
  },
1335
1393
  renderIcon ? /* @__PURE__ */ React10.createElement(View8, { style: { width: 24, alignItems: "center", justifyContent: "center" } }, renderIcon(iconName, 15, colors.textSecondary)) : null,
1336
1394
  /* @__PURE__ */ React10.createElement(