@drakkar.software/octospaces-ui 0.4.0 → 0.4.1

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
@@ -645,6 +645,115 @@ interface SidebarItemProps {
645
645
  }
646
646
  declare function SidebarItem({ label, icon, active, badge, onPress, onLongPress, trailing, indent, }: SidebarItemProps): React.JSX.Element;
647
647
 
648
+ /**
649
+ * Headless themed space-switcher component.
650
+ *
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,
653
+ * optional space settings, and an app-provided footer slot (account section, etc.).
654
+ *
655
+ * The popup container (Popover on desktop, Sheet on mobile) is fully delegated to
656
+ * 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
+ *
659
+ * @example
660
+ * ```tsx
661
+ * // OctoVault — sidebar variant with Popover container
662
+ * <SpaceSwitcher
663
+ * spaces={spaces}
664
+ * activeId={activeId}
665
+ * onSelect={switchSpace}
666
+ * onAdd={() => router.push('/join')}
667
+ * onSettings={() => router.push(`/space/${activeId}`)}
668
+ * variant="sidebar"
669
+ * renderTriggerAvatar={(space, size) => (
670
+ * <Avatar label={space?.short ?? ''} image={space?.image} size={size} />
671
+ * )}
672
+ * renderSpaceAvatar={(space, size) => (
673
+ * <Avatar label={space.short ?? ''} image={space.image} size={size} />
674
+ * )}
675
+ * renderIcon={(name, size, color) => <Icon name={SWITCHER_ICON[name]} size={size} color={color} />}
676
+ * renderContainer={({ isOpen, onClose, anchorRef, children }) => (
677
+ * <>
678
+ * <Popover visible={isOpen} onClose={onClose} anchorRef={anchorRef} placement="bottom-start" width={240}>
679
+ * {children}
680
+ * </Popover>
681
+ * </>
682
+ * )}
683
+ * footerSlot={<AccountSwitcher onRequestClose={...} onViewProfile={...} />}
684
+ * />
685
+ * ```
686
+ */
687
+
688
+ /** Structural space item — no SDK dependency. */
689
+ interface SwitcherSpace {
690
+ id: string;
691
+ name: string;
692
+ /** 2-letter monogram used as avatar fallback. */
693
+ short?: string;
694
+ /** Uploaded space image URI; absent → host renders monogram. */
695
+ image?: string;
696
+ }
697
+ /** Icon name union for the switcher's built-in glyphs. */
698
+ type SwitcherIconName = 'chevron-down' | 'check' | 'plus' | 'gear';
699
+ interface SpaceSwitcherProps {
700
+ spaces: SwitcherSpace[];
701
+ activeId?: string | null;
702
+ /** Called when the user taps a space row. */
703
+ onSelect: (id: string) => void;
704
+ /** "Join or create a space" action. Omit to hide the row. */
705
+ onAdd?: () => void;
706
+ /** Override the add-row label. @default "Join or create a space" */
707
+ addLabel?: string;
708
+ /**
709
+ * "Space settings" action. Only shown when both `onSettings` and `activeId`
710
+ * are provided. Omit to hide.
711
+ */
712
+ onSettings?: () => void;
713
+ /** Override the settings-row label. @default "Space settings" */
714
+ settingsLabel?: string;
715
+ /**
716
+ * Visual variant:
717
+ * - `'sidebar'` — compact left-aligned trigger for the desktop sidebar header.
718
+ * - `'appbar'` — centered trigger for a phone app-bar title area.
719
+ */
720
+ variant: 'sidebar' | 'appbar';
721
+ /**
722
+ * Wraps the dropdown content in the host app's container (Popover / Sheet).
723
+ * Called with `{ isOpen, onClose, anchorRef, children }` — must render
724
+ * children inside an appropriate modal surface.
725
+ */
726
+ renderContainer: (props: {
727
+ isOpen: boolean;
728
+ onClose: () => void;
729
+ anchorRef: React.RefObject<View>;
730
+ children: React.ReactNode;
731
+ }) => React.ReactNode;
732
+ /**
733
+ * Render the active-space avatar inside the trigger button.
734
+ * Receives the active `SwitcherSpace` (or `null` when none) and a pixel size.
735
+ * Omit to render nothing in the avatar slot.
736
+ */
737
+ renderTriggerAvatar?: (space: SwitcherSpace | null, size: number) => React.ReactNode;
738
+ /**
739
+ * Render a space row's leading avatar.
740
+ * Receives the `SwitcherSpace` and a pixel size.
741
+ * Omit to render nothing in the leading slot.
742
+ */
743
+ renderSpaceAvatar?: (space: SwitcherSpace, size: number) => React.ReactNode;
744
+ /**
745
+ * Render an icon glyph. Name is one of `'chevron-down' | 'check' | 'plus' | 'gear'`.
746
+ * Omit to hide chevron, check, and action icons (spaces remain selectable).
747
+ */
748
+ renderIcon?: (name: SwitcherIconName, size: number, color: string) => React.ReactNode;
749
+ /**
750
+ * Footer rendered below the space list + action rows — use for account-switcher
751
+ * sections (with separator if needed). Fully app-owned.
752
+ */
753
+ footerSlot?: React.ReactNode;
754
+ }
755
+ declare function SpaceSwitcher({ spaces, activeId, onSelect, onAdd, addLabel, onSettings, settingsLabel, variant, renderContainer, renderTriggerAvatar, renderSpaceAvatar, renderIcon, footerSlot, }: SpaceSwitcherProps): React.JSX.Element;
756
+
648
757
  /**
649
758
  * Full-screen scrim overlay that centers its content. Tapping the backdrop, the
650
759
  * close button, the Escape key (web) or the hardware back (Android) dismisses it.
@@ -700,4 +809,4 @@ interface LightboxProps {
700
809
  */
701
810
  declare function Lightbox({ visible, onClose, children, closeLabel, renderCloseButton, renderActions, }: LightboxProps): React.JSX.Element;
702
811
 
703
- export { type ColorScheme, type DiscoverEntry, DiscoverList, type DiscoverListProps, DiscoverRow, type DiscoverRowProps, DiscoverScreen, type DiscoverScreenProps, type Easing, type Fonts, type LabelTracking, type Layers, type Layout, Lightbox, type LightboxProps, type Motion, type MotionToken, OctoSpacesThemeProvider, type OctoSpacesThemeProviderProps, type Opacity, type Palette, type Radii, type RailIconName, type RailSpace, type ShadowToken, type Shadows, Sidebar, SidebarActionButton, type SidebarActionButtonProps, SidebarHeader, type SidebarHeaderProps, SidebarItem, type SidebarItemProps, type SidebarProps, SpacesRail, type SpacesRailProps, type Spacing, type Swatches, type Theme, type TypeScale, type Typography, avatarTint, filterDiscoverEntries, focusRingStyle, glowShadow, paperBorder, presenceColor, sortDiscoverEntries, statusColor, swatch, useOctoSpacesTheme, verificationColor };
812
+ export { type ColorScheme, type DiscoverEntry, DiscoverList, type DiscoverListProps, DiscoverRow, type DiscoverRowProps, DiscoverScreen, type DiscoverScreenProps, type Easing, type Fonts, type LabelTracking, type Layers, type Layout, Lightbox, type LightboxProps, type Motion, type MotionToken, OctoSpacesThemeProvider, type OctoSpacesThemeProviderProps, type Opacity, type Palette, type Radii, type RailIconName, type RailSpace, type ShadowToken, type Shadows, Sidebar, SidebarActionButton, type SidebarActionButtonProps, SidebarHeader, type SidebarHeaderProps, SidebarItem, type SidebarItemProps, type SidebarProps, SpaceSwitcher, type SpaceSwitcherProps, SpacesRail, type SpacesRailProps, type Spacing, type Swatches, type SwitcherIconName, type SwitcherSpace, type Theme, type TypeScale, type Typography, avatarTint, filterDiscoverEntries, focusRingStyle, glowShadow, paperBorder, presenceColor, sortDiscoverEntries, statusColor, swatch, useOctoSpacesTheme, verificationColor };
package/dist/index.js CHANGED
@@ -1049,9 +1049,312 @@ var styles2 = StyleSheet2.create({
1049
1049
  iconSlot: { width: 18, alignItems: "center", justifyContent: "center" }
1050
1050
  });
1051
1051
 
1052
+ // src/sidebar/SpaceSwitcher.tsx
1053
+ import React10, { useRef as useRef2, useState as useState5 } from "react";
1054
+ import { Pressable as RNPressable4, StyleSheet as StyleSheet3, Text as Text6, View as View8 } from "react-native";
1055
+ var Pressable6 = RNPressable4;
1056
+ function SpaceSwitcher({
1057
+ spaces,
1058
+ activeId,
1059
+ onSelect,
1060
+ onAdd,
1061
+ addLabel = "Join or create a space",
1062
+ onSettings,
1063
+ settingsLabel = "Space settings",
1064
+ variant,
1065
+ renderContainer,
1066
+ renderTriggerAvatar,
1067
+ renderSpaceAvatar,
1068
+ renderIcon,
1069
+ footerSlot
1070
+ }) {
1071
+ const theme = useOctoSpacesTheme();
1072
+ const { colors, type: typeScale, fonts, spacing: sp, radii } = theme;
1073
+ const [open, setOpen] = useState5(false);
1074
+ const [triggerHovered, setTriggerHovered] = useState5(false);
1075
+ const anchorRef = useRef2(null);
1076
+ const active = spaces.find((s) => s.id === activeId) ?? spaces[0] ?? null;
1077
+ const close = () => setOpen(false);
1078
+ const handleSelect = (id) => {
1079
+ close();
1080
+ onSelect(id);
1081
+ };
1082
+ const handleAdd = () => {
1083
+ close();
1084
+ onAdd?.();
1085
+ };
1086
+ const handleSettings = () => {
1087
+ close();
1088
+ onSettings?.();
1089
+ };
1090
+ const sp1 = sp["1"] ?? 4;
1091
+ const sp2 = sp["2"] ?? 8;
1092
+ const sp3 = sp["3"] ?? 12;
1093
+ const sp4 = sp["4"] ?? 16;
1094
+ const radMd = radii["md"] ?? 6;
1095
+ const bodyFont = fonts["body"] ?? void 0;
1096
+ const bodySize = typeScale["callout"]?.size ?? 13;
1097
+ const bodyLine = typeScale["callout"]?.lineHeight ?? 18;
1098
+ const labelSize = typeScale["caption"]?.size ?? 11;
1099
+ const labelLine = typeScale["caption"]?.lineHeight ?? 16;
1100
+ const triggerStyle = variant === "sidebar" ? {
1101
+ flex: 1,
1102
+ flexDirection: "row",
1103
+ alignItems: "center",
1104
+ gap: sp2,
1105
+ paddingHorizontal: sp2,
1106
+ paddingVertical: sp1 + 2,
1107
+ borderRadius: radMd,
1108
+ minWidth: 0,
1109
+ backgroundColor: triggerHovered ? colors.primarySubtle ?? "rgba(0,0,0,0.05)" : "transparent"
1110
+ } : {
1111
+ flexDirection: "row",
1112
+ alignItems: "center",
1113
+ justifyContent: "center",
1114
+ gap: sp2,
1115
+ paddingHorizontal: sp2,
1116
+ paddingVertical: sp1,
1117
+ borderRadius: radMd,
1118
+ backgroundColor: triggerHovered ? colors.primarySubtle ?? "rgba(0,0,0,0.05)" : "transparent"
1119
+ };
1120
+ const dropdownContent = /* @__PURE__ */ React10.createElement(View8, { style: { paddingVertical: sp1 } }, spaces.length > 0 ? /* @__PURE__ */ React10.createElement(
1121
+ SectionLabel,
1122
+ {
1123
+ label: "Spaces",
1124
+ color: colors.textTertiary,
1125
+ size: labelSize,
1126
+ lineHeight: labelLine,
1127
+ font: bodyFont,
1128
+ paddingH: sp4,
1129
+ paddingV: sp1
1130
+ }
1131
+ ) : null, spaces.map((s) => /* @__PURE__ */ React10.createElement(
1132
+ SpaceRow,
1133
+ {
1134
+ key: s.id,
1135
+ space: s,
1136
+ active: s.id === (active?.id ?? null),
1137
+ onPress: () => handleSelect(s.id),
1138
+ renderAvatar: renderSpaceAvatar,
1139
+ renderIcon,
1140
+ colors,
1141
+ bodyFont,
1142
+ bodySize,
1143
+ bodyLine,
1144
+ sp2,
1145
+ sp3,
1146
+ sp4,
1147
+ radMd
1148
+ }
1149
+ )), onAdd ? /* @__PURE__ */ React10.createElement(
1150
+ ActionRow,
1151
+ {
1152
+ label: spaces.length > 0 ? addLabel : "Create your first space",
1153
+ iconName: "plus",
1154
+ onPress: handleAdd,
1155
+ renderIcon,
1156
+ colors,
1157
+ bodyFont,
1158
+ bodySize,
1159
+ bodyLine,
1160
+ sp2,
1161
+ sp3,
1162
+ sp4,
1163
+ radMd
1164
+ }
1165
+ ) : null, onSettings && active ? /* @__PURE__ */ React10.createElement(
1166
+ ActionRow,
1167
+ {
1168
+ label: settingsLabel,
1169
+ iconName: "gear",
1170
+ onPress: handleSettings,
1171
+ renderIcon,
1172
+ colors,
1173
+ bodyFont,
1174
+ bodySize,
1175
+ bodyLine,
1176
+ sp2,
1177
+ sp3,
1178
+ sp4,
1179
+ radMd
1180
+ }
1181
+ ) : null, footerSlot != null ? /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement(
1182
+ View8,
1183
+ {
1184
+ style: {
1185
+ height: StyleSheet3.hairlineWidth,
1186
+ backgroundColor: colors.borderSubtle,
1187
+ marginVertical: sp1,
1188
+ marginHorizontal: sp2
1189
+ }
1190
+ }
1191
+ ), footerSlot) : null);
1192
+ return /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement(
1193
+ Pressable6,
1194
+ {
1195
+ ref: anchorRef,
1196
+ accessibilityRole: "button",
1197
+ accessibilityLabel: active ? `${active.name} \u2014 switch space` : "Switch space",
1198
+ accessibilityState: { expanded: open },
1199
+ hitSlop: 6,
1200
+ onPress: () => setOpen(true),
1201
+ onMouseEnter: () => setTriggerHovered(true),
1202
+ onMouseLeave: () => setTriggerHovered(false),
1203
+ style: triggerStyle
1204
+ },
1205
+ renderTriggerAvatar ? renderTriggerAvatar(active, 22) : null,
1206
+ /* @__PURE__ */ React10.createElement(
1207
+ Text6,
1208
+ {
1209
+ numberOfLines: 1,
1210
+ style: {
1211
+ flex: variant === "sidebar" ? 1 : void 0,
1212
+ minWidth: 0,
1213
+ flexShrink: 1,
1214
+ fontSize: typeScale["heading"]?.size ?? 15,
1215
+ lineHeight: typeScale["heading"]?.lineHeight ?? 20,
1216
+ fontWeight: "600",
1217
+ color: colors.text,
1218
+ fontFamily: bodyFont
1219
+ }
1220
+ },
1221
+ active?.name ?? "Spaces"
1222
+ ),
1223
+ renderIcon ? renderIcon("chevron-down", 14, colors.textTertiary) : null
1224
+ ), renderContainer({ isOpen: open, onClose: close, anchorRef, children: dropdownContent }));
1225
+ }
1226
+ function SectionLabel({ label, color, size, lineHeight, font, paddingH, paddingV }) {
1227
+ return /* @__PURE__ */ React10.createElement(
1228
+ Text6,
1229
+ {
1230
+ style: {
1231
+ fontSize: size,
1232
+ lineHeight,
1233
+ fontWeight: "600",
1234
+ color,
1235
+ fontFamily: font,
1236
+ textTransform: "uppercase",
1237
+ letterSpacing: 0.5,
1238
+ paddingHorizontal: paddingH,
1239
+ paddingVertical: paddingV
1240
+ }
1241
+ },
1242
+ label
1243
+ );
1244
+ }
1245
+ function SpaceRow({
1246
+ space,
1247
+ active,
1248
+ onPress,
1249
+ renderAvatar,
1250
+ renderIcon,
1251
+ colors,
1252
+ bodyFont,
1253
+ bodySize,
1254
+ bodyLine,
1255
+ sp2,
1256
+ sp3,
1257
+ sp4,
1258
+ radMd
1259
+ }) {
1260
+ const [hovered, setHovered] = useState5(false);
1261
+ const bg = hovered ? colors.primarySubtle ?? "rgba(0,0,0,0.04)" : "transparent";
1262
+ return /* @__PURE__ */ React10.createElement(
1263
+ Pressable6,
1264
+ {
1265
+ accessibilityRole: "menuitem",
1266
+ accessibilityLabel: active ? `${space.name} (current)` : `Switch to ${space.name}`,
1267
+ accessibilityState: { selected: active },
1268
+ onPress,
1269
+ onMouseEnter: () => setHovered(true),
1270
+ onMouseLeave: () => setHovered(false),
1271
+ style: {
1272
+ flexDirection: "row",
1273
+ alignItems: "center",
1274
+ gap: sp3,
1275
+ paddingHorizontal: sp4,
1276
+ paddingVertical: sp2,
1277
+ borderRadius: radMd,
1278
+ backgroundColor: bg
1279
+ }
1280
+ },
1281
+ renderAvatar ? renderAvatar(space, 24) : null,
1282
+ /* @__PURE__ */ React10.createElement(
1283
+ Text6,
1284
+ {
1285
+ numberOfLines: 1,
1286
+ style: {
1287
+ flex: 1,
1288
+ minWidth: 0,
1289
+ fontSize: bodySize,
1290
+ lineHeight: bodyLine,
1291
+ fontWeight: active ? "600" : "400",
1292
+ color: active ? colors.primary : colors.text,
1293
+ fontFamily: bodyFont
1294
+ }
1295
+ },
1296
+ space.name
1297
+ ),
1298
+ active && renderIcon ? renderIcon("check", 15, colors.primary) : null
1299
+ );
1300
+ }
1301
+ function ActionRow({
1302
+ label,
1303
+ iconName,
1304
+ onPress,
1305
+ renderIcon,
1306
+ colors,
1307
+ bodyFont,
1308
+ bodySize,
1309
+ bodyLine,
1310
+ sp2,
1311
+ sp3,
1312
+ sp4,
1313
+ radMd
1314
+ }) {
1315
+ const [hovered, setHovered] = useState5(false);
1316
+ const bg = hovered ? colors.primarySubtle ?? "rgba(0,0,0,0.04)" : "transparent";
1317
+ return /* @__PURE__ */ React10.createElement(
1318
+ Pressable6,
1319
+ {
1320
+ accessibilityRole: "menuitem",
1321
+ accessibilityLabel: label,
1322
+ onPress,
1323
+ onMouseEnter: () => setHovered(true),
1324
+ onMouseLeave: () => setHovered(false),
1325
+ style: {
1326
+ flexDirection: "row",
1327
+ alignItems: "center",
1328
+ gap: sp3,
1329
+ paddingHorizontal: sp4,
1330
+ paddingVertical: sp2,
1331
+ borderRadius: radMd,
1332
+ backgroundColor: bg
1333
+ }
1334
+ },
1335
+ renderIcon ? /* @__PURE__ */ React10.createElement(View8, { style: { width: 24, alignItems: "center", justifyContent: "center" } }, renderIcon(iconName, 15, colors.textSecondary)) : null,
1336
+ /* @__PURE__ */ React10.createElement(
1337
+ Text6,
1338
+ {
1339
+ numberOfLines: 1,
1340
+ style: {
1341
+ flex: 1,
1342
+ minWidth: 0,
1343
+ fontSize: bodySize,
1344
+ lineHeight: bodyLine,
1345
+ fontWeight: "400",
1346
+ color: colors.text,
1347
+ fontFamily: bodyFont
1348
+ }
1349
+ },
1350
+ label
1351
+ )
1352
+ );
1353
+ }
1354
+
1052
1355
  // src/lightbox/Lightbox.tsx
1053
- import React10, { useEffect as useEffect2 } from "react";
1054
- import { Modal, Platform, Pressable as Pressable6, View as View8 } from "react-native";
1356
+ import React11, { useEffect as useEffect2 } from "react";
1357
+ import { Modal, Platform, Pressable as Pressable7, View as View9 } from "react-native";
1055
1358
  function Lightbox({
1056
1359
  visible,
1057
1360
  onClose,
@@ -1072,7 +1375,7 @@ function Lightbox({
1072
1375
  const pad = theme.spacing["6"] ?? 24;
1073
1376
  const insetV = theme.spacing["8"] ?? 32;
1074
1377
  const insetH = theme.spacing["4"] ?? 16;
1075
- return /* @__PURE__ */ React10.createElement(
1378
+ return /* @__PURE__ */ React11.createElement(
1076
1379
  Modal,
1077
1380
  {
1078
1381
  visible,
@@ -1081,8 +1384,8 @@ function Lightbox({
1081
1384
  onRequestClose: onClose,
1082
1385
  statusBarTranslucent: true
1083
1386
  },
1084
- /* @__PURE__ */ React10.createElement(
1085
- Pressable6,
1387
+ /* @__PURE__ */ React11.createElement(
1388
+ Pressable7,
1086
1389
  {
1087
1390
  style: {
1088
1391
  flex: 1,
@@ -1094,16 +1397,16 @@ function Lightbox({
1094
1397
  onPress: onClose,
1095
1398
  accessibilityLabel: closeLabel
1096
1399
  },
1097
- /* @__PURE__ */ React10.createElement(
1098
- View8,
1400
+ /* @__PURE__ */ React11.createElement(
1401
+ View9,
1099
1402
  {
1100
1403
  style: { alignItems: "center", justifyContent: "center" },
1101
1404
  pointerEvents: "box-none"
1102
1405
  },
1103
1406
  children
1104
1407
  ),
1105
- renderCloseButton ? /* @__PURE__ */ React10.createElement(View8, { style: { position: "absolute", top: insetV, right: insetH } }, renderCloseButton(onClose)) : null,
1106
- renderActions ? /* @__PURE__ */ React10.createElement(View8, { style: { position: "absolute", bottom: insetV, right: insetH } }, renderActions()) : null
1408
+ renderCloseButton ? /* @__PURE__ */ React11.createElement(View9, { style: { position: "absolute", top: insetV, right: insetH } }, renderCloseButton(onClose)) : null,
1409
+ renderActions ? /* @__PURE__ */ React11.createElement(View9, { style: { position: "absolute", bottom: insetV, right: insetH } }, renderActions()) : null
1107
1410
  )
1108
1411
  );
1109
1412
  }
@@ -1117,6 +1420,7 @@ export {
1117
1420
  SidebarActionButton,
1118
1421
  SidebarHeader,
1119
1422
  SidebarItem,
1423
+ SpaceSwitcher,
1120
1424
  SpacesRail,
1121
1425
  avatarTint,
1122
1426
  filterDiscoverEntries,