@drakkar.software/octospaces-ui 0.4.2 → 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/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,35 +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;
749
793
  /**
750
- * Footer rendered below the space list + action rows — use for account-switcher
751
- * sections (with separator if needed). Fully app-owned.
794
+ * Render an unread-count badge on a space row.
795
+ * Receives the count (always > 0 when called). Omit to hide badges.
752
796
  */
753
- footerSlot?: React.ReactNode;
797
+ renderBadge?: (count: number) => React.ReactNode;
754
798
  /**
755
- * When provided, replaces the default "open dropdown" behaviour on trigger press.
756
- * The chevron is also hidden. Use this to navigate directly to a space-details
757
- * page instead of opening a picker — e.g. on desktop where a rail handles
758
- * switching and a dropdown is redundant.
759
- *
760
- * @example
761
- * ```tsx
762
- * // Navigate to space details instead of opening the picker (desktop sidebar)
763
- * <SpaceSwitcher
764
- * onTriggerPress={() => router.push(`/space/${activeId}`)}
765
- * // renderContainer still required but never called when onTriggerPress is set
766
- * renderContainer={() => null}
767
- * ...
768
- * />
769
- * ```
799
+ * Footer rendered below the space list + action rows use for account-switcher
800
+ * sections. Receives `close` so the account section can dismiss the dropdown after
801
+ * an action (e.g. `<AccountSwitcher onRequestClose={close} />`). Fully app-owned.
770
802
  */
771
- onTriggerPress?: () => void;
803
+ footerSlot?: (close: () => void) => React.ReactNode;
772
804
  }
773
- declare function SpaceSwitcher({ spaces, activeId, onSelect, onAdd, addLabel, onSettings, settingsLabel, variant, renderContainer, renderTriggerAvatar, renderSpaceAvatar, renderIcon, footerSlot, onTriggerPress, }: 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;
774
806
 
775
807
  /**
776
808
  * Full-screen scrim overlay that centers its content. Tapping the backdrop, the
package/dist/index.js CHANGED
@@ -1059,15 +1059,21 @@ 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,
1069
- footerSlot,
1070
- onTriggerPress
1075
+ renderBadge,
1076
+ footerSlot
1071
1077
  }) {
1072
1078
  const theme = useOctoSpacesTheme();
1073
1079
  const { colors, type: typeScale, fonts, spacing: sp, radii } = theme;
@@ -1084,10 +1090,18 @@ function SpaceSwitcher({
1084
1090
  close();
1085
1091
  onAdd?.();
1086
1092
  };
1093
+ const handleBrowse = () => {
1094
+ close();
1095
+ onBrowse?.();
1096
+ };
1087
1097
  const handleSettings = () => {
1088
1098
  close();
1089
1099
  onSettings?.();
1090
1100
  };
1101
+ const handleSeeAll = () => {
1102
+ close();
1103
+ onSeeAll?.();
1104
+ };
1091
1105
  const sp1 = sp["1"] ?? 4;
1092
1106
  const sp2 = sp["2"] ?? 8;
1093
1107
  const sp3 = sp["3"] ?? 12;
@@ -1098,7 +1112,9 @@ function SpaceSwitcher({
1098
1112
  const bodyLine = typeScale["callout"]?.lineHeight ?? 18;
1099
1113
  const labelSize = typeScale["caption"]?.size ?? 11;
1100
1114
  const labelLine = typeScale["caption"]?.lineHeight ?? 16;
1101
- 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" ? {
1102
1118
  flex: 1,
1103
1119
  flexDirection: "row",
1104
1120
  alignItems: "center",
@@ -1106,8 +1122,7 @@ function SpaceSwitcher({
1106
1122
  paddingHorizontal: sp2,
1107
1123
  paddingVertical: sp1 + 2,
1108
1124
  borderRadius: radMd,
1109
- minWidth: 0,
1110
- backgroundColor: triggerHovered ? colors.primarySubtle ?? "rgba(0,0,0,0.05)" : "transparent"
1125
+ minWidth: 0
1111
1126
  } : {
1112
1127
  flexDirection: "row",
1113
1128
  alignItems: "center",
@@ -1115,8 +1130,7 @@ function SpaceSwitcher({
1115
1130
  gap: sp2,
1116
1131
  paddingHorizontal: sp2,
1117
1132
  paddingVertical: sp1,
1118
- borderRadius: radMd,
1119
- backgroundColor: triggerHovered ? colors.primarySubtle ?? "rgba(0,0,0,0.05)" : "transparent"
1133
+ borderRadius: radMd
1120
1134
  };
1121
1135
  const dropdownContent = /* @__PURE__ */ React10.createElement(View8, { style: { paddingVertical: sp1 } }, spaces.length > 0 ? /* @__PURE__ */ React10.createElement(
1122
1136
  SectionLabel,
@@ -1129,7 +1143,7 @@ function SpaceSwitcher({
1129
1143
  paddingH: sp4,
1130
1144
  paddingV: sp1
1131
1145
  }
1132
- ) : null, spaces.map((s) => /* @__PURE__ */ React10.createElement(
1146
+ ) : null, visibleSpaces.map((s) => /* @__PURE__ */ React10.createElement(
1133
1147
  SpaceRow,
1134
1148
  {
1135
1149
  key: s.id,
@@ -1138,6 +1152,23 @@ function SpaceSwitcher({
1138
1152
  onPress: () => handleSelect(s.id),
1139
1153
  renderAvatar: renderSpaceAvatar,
1140
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,
1141
1172
  colors,
1142
1173
  bodyFont,
1143
1174
  bodySize,
@@ -1147,7 +1178,7 @@ function SpaceSwitcher({
1147
1178
  sp4,
1148
1179
  radMd
1149
1180
  }
1150
- )), onAdd ? /* @__PURE__ */ React10.createElement(
1181
+ ) : null, onAdd ? /* @__PURE__ */ React10.createElement(
1151
1182
  ActionRow,
1152
1183
  {
1153
1184
  label: spaces.length > 0 ? addLabel : "Create your first space",
@@ -1163,6 +1194,22 @@ function SpaceSwitcher({
1163
1194
  sp4,
1164
1195
  radMd
1165
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
+ }
1166
1213
  ) : null, onSettings && active ? /* @__PURE__ */ React10.createElement(
1167
1214
  ActionRow,
1168
1215
  {
@@ -1189,7 +1236,7 @@ function SpaceSwitcher({
1189
1236
  marginHorizontal: sp2
1190
1237
  }
1191
1238
  }
1192
- ), footerSlot) : null);
1239
+ ), footerSlot(close)) : null);
1193
1240
  return /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement(
1194
1241
  Pressable6,
1195
1242
  {
@@ -1198,12 +1245,17 @@ function SpaceSwitcher({
1198
1245
  accessibilityLabel: active ? `${active.name} \u2014 switch space` : "Switch space",
1199
1246
  accessibilityState: { expanded: open },
1200
1247
  hitSlop: 6,
1201
- onPress: onTriggerPress ?? (() => setOpen(true)),
1248
+ onPress: () => setOpen(true),
1202
1249
  onMouseEnter: () => setTriggerHovered(true),
1203
1250
  onMouseLeave: () => setTriggerHovered(false),
1204
- 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
+ ]
1205
1257
  },
1206
- 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,
1207
1259
  /* @__PURE__ */ React10.createElement(
1208
1260
  Text6,
1209
1261
  {
@@ -1221,9 +1273,13 @@ function SpaceSwitcher({
1221
1273
  },
1222
1274
  active?.name ?? "Spaces"
1223
1275
  ),
1224
- !onTriggerPress && renderIcon ? renderIcon("chevron-down", 14, colors.textTertiary) : null
1225
- ), onTriggerPress ? null : renderContainer({ isOpen: open, onClose: close, anchorRef, children: dropdownContent }));
1276
+ renderIcon ? renderIcon("chevron-down", 14, colors.textTertiary) : null
1277
+ ), renderContainer({ isOpen: open, onClose: close, anchorRef, children: dropdownContent }));
1226
1278
  }
1279
+ var styles3 = StyleSheet3.create({
1280
+ avatarWrap: { position: "relative" },
1281
+ triggerBadge: { position: "absolute", top: -2, right: -2 }
1282
+ });
1227
1283
  function SectionLabel({ label, color, size, lineHeight, font, paddingH, paddingV }) {
1228
1284
  return /* @__PURE__ */ React10.createElement(
1229
1285
  Text6,
@@ -1249,6 +1305,7 @@ function SpaceRow({
1249
1305
  onPress,
1250
1306
  renderAvatar,
1251
1307
  renderIcon,
1308
+ renderBadge,
1252
1309
  colors,
1253
1310
  bodyFont,
1254
1311
  bodySize,
@@ -1259,7 +1316,7 @@ function SpaceRow({
1259
1316
  radMd
1260
1317
  }) {
1261
1318
  const [hovered, setHovered] = useState5(false);
1262
- const bg = hovered ? colors.primarySubtle ?? "rgba(0,0,0,0.04)" : "transparent";
1319
+ const unread = space.unread ?? 0;
1263
1320
  return /* @__PURE__ */ React10.createElement(
1264
1321
  Pressable6,
1265
1322
  {
@@ -1269,15 +1326,15 @@ function SpaceRow({
1269
1326
  onPress,
1270
1327
  onMouseEnter: () => setHovered(true),
1271
1328
  onMouseLeave: () => setHovered(false),
1272
- style: {
1329
+ style: ({ pressed }) => ({
1273
1330
  flexDirection: "row",
1274
1331
  alignItems: "center",
1275
1332
  gap: sp3,
1276
1333
  paddingHorizontal: sp4,
1277
1334
  paddingVertical: sp2,
1278
1335
  borderRadius: radMd,
1279
- backgroundColor: bg
1280
- }
1336
+ backgroundColor: pressed ? colors.primarySubtle ?? "rgba(0,0,0,0.08)" : hovered ? colors.primarySubtle ?? "rgba(0,0,0,0.04)" : "transparent"
1337
+ })
1281
1338
  },
1282
1339
  renderAvatar ? renderAvatar(space, 24) : null,
1283
1340
  /* @__PURE__ */ React10.createElement(
@@ -1296,6 +1353,7 @@ function SpaceRow({
1296
1353
  },
1297
1354
  space.name
1298
1355
  ),
1356
+ unread > 0 && renderBadge ? renderBadge(unread) : null,
1299
1357
  active && renderIcon ? renderIcon("check", 15, colors.primary) : null
1300
1358
  );
1301
1359
  }
@@ -1314,7 +1372,6 @@ function ActionRow({
1314
1372
  radMd
1315
1373
  }) {
1316
1374
  const [hovered, setHovered] = useState5(false);
1317
- const bg = hovered ? colors.primarySubtle ?? "rgba(0,0,0,0.04)" : "transparent";
1318
1375
  return /* @__PURE__ */ React10.createElement(
1319
1376
  Pressable6,
1320
1377
  {
@@ -1323,15 +1380,15 @@ function ActionRow({
1323
1380
  onPress,
1324
1381
  onMouseEnter: () => setHovered(true),
1325
1382
  onMouseLeave: () => setHovered(false),
1326
- style: {
1383
+ style: ({ pressed }) => ({
1327
1384
  flexDirection: "row",
1328
1385
  alignItems: "center",
1329
1386
  gap: sp3,
1330
1387
  paddingHorizontal: sp4,
1331
1388
  paddingVertical: sp2,
1332
1389
  borderRadius: radMd,
1333
- backgroundColor: bg
1334
- }
1390
+ backgroundColor: pressed ? colors.primarySubtle ?? "rgba(0,0,0,0.08)" : hovered ? colors.primarySubtle ?? "rgba(0,0,0,0.04)" : "transparent"
1391
+ })
1335
1392
  },
1336
1393
  renderIcon ? /* @__PURE__ */ React10.createElement(View8, { style: { width: 24, alignItems: "center", justifyContent: "center" } }, renderIcon(iconName, 15, colors.textSecondary)) : null,
1337
1394
  /* @__PURE__ */ React10.createElement(