@drakkar.software/octospaces-ui 0.2.0 → 0.3.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/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { View } from 'react-native';
2
3
 
3
4
  /**
4
5
  * Theme contract for `@drakkar.software/octospaces-ui`.
@@ -334,7 +335,127 @@ interface DiscoverScreenProps {
334
335
  * @default true
335
336
  */
336
337
  searchEnabled?: boolean;
338
+ /**
339
+ * Optional ref whose `.current` is set to a `reload()` function once mounted.
340
+ * Lets a host (e.g. a tab screen) trigger a soft-refresh on focus without
341
+ * blanking the existing list — identical to pull-to-refresh behaviour.
342
+ *
343
+ * ```tsx
344
+ * const reloadRef = useRef<() => void>(null);
345
+ * useFocusEffect(useCallback(() => { reloadRef.current?.(); }, []));
346
+ * <DiscoverScreen reloadRef={reloadRef} ... />
347
+ * ```
348
+ */
349
+ reloadRef?: React.RefObject<(() => void) | null>;
337
350
  }
338
- declare function DiscoverScreen({ loadEntries, renderIcon, onOpen, title, emptyMessage, emptySearchMessage, searchEnabled, }: DiscoverScreenProps): React.JSX.Element;
351
+ declare function DiscoverScreen({ loadEntries, renderIcon, onOpen, title, emptyMessage, emptySearchMessage, searchEnabled, reloadRef, }: DiscoverScreenProps): React.JSX.Element;
352
+
353
+ /**
354
+ * Shared types for the SpacesRail component.
355
+ *
356
+ * Structurally compatible with `Space` from `@drakkar.software/octochat-sdk` /
357
+ * `@drakkar.software/octospaces-sdk` — apps can pass their domain objects directly
358
+ * without a runtime conversion step.
359
+ */
360
+ /** A minimal space descriptor for the rail: id + display info + badge state. */
361
+ interface RailSpace {
362
+ /** Unique space identifier. */
363
+ id: string;
364
+ /** Short display name or initials shown as a monogram when no image is available. */
365
+ short: string;
366
+ /** Optional image URI (data URI or URL) rendered as the tile background via
367
+ * `SpacesRailProps.renderTileImage`. */
368
+ image?: string;
369
+ /** Unread-message count shown as a badge overlay. */
370
+ unread?: number;
371
+ /** Whether the space is muted (shows a mute-corner icon when `renderIcon` is provided). */
372
+ muted?: boolean;
373
+ }
374
+ /** Named icon slots injected into the rail via `SpacesRailProps.renderIcon`. */
375
+ type RailIconName = 'dm' | 'lock' | 'mute' | 'add';
376
+
377
+ /**
378
+ * Headless, abstractly-themed vertical spaces rail.
379
+ *
380
+ * The component reads the injected {@link Theme} via `useOctoSpacesTheme()` and
381
+ * delegates all app-specific concerns to props:
382
+ *
383
+ * - Icons are rendered by `renderIcon` (keeps `@expo/vector-icons` out of this package).
384
+ * - Space tile images are rendered by `renderTileImage` (keeps `expo-image` out too).
385
+ * - Unread badges are rendered by `renderBadge`.
386
+ * - The rail foot (account avatar + menu) is rendered by `renderFoot`.
387
+ * - Web drag-reorder is wired via the `useTileDnd` hook prop (see below).
388
+ *
389
+ * All React Native primitives used here ship with the `react-native` peer dep.
390
+ */
391
+
392
+ interface SpacesRailProps {
393
+ /** The spaces to show in the scrollable column. */
394
+ spaces: RailSpace[];
395
+ /** The currently-active space id (or null / undefined for none). */
396
+ activeId?: string | null;
397
+ /** Called when the user selects a space tile. */
398
+ onSelect?: (id: string) => void;
399
+ /** Called when the user taps the "add" tile. */
400
+ onAdd?: () => void;
401
+ /** When provided, renders a leading DM-home tile. */
402
+ onSelectDms?: () => void;
403
+ /** Whether the DM-home tile is the active selection. */
404
+ dmsActive?: boolean;
405
+ /** Unread count for the DM-home tile badge. */
406
+ dmUnread?: number;
407
+ /** Accessibility label for the DM-home tile (default: "Direct messages"). */
408
+ dmLabel?: string;
409
+ /** Accessibility label for the add-space tile (default: "Create or join a space"). */
410
+ addLabel?: string;
411
+ /**
412
+ * Render a named icon at the given size and color. Used for the DM tile icon
413
+ * (`'dm'`), the E2EE lock corner (`'lock'`), the mute corner (`'mute'`),
414
+ * and the add tile icon (`'add'`). Return `null` to suppress the icon slot.
415
+ * If omitted, all icon slots render nothing.
416
+ */
417
+ renderIcon?: (name: RailIconName, size: number, color: string) => React.ReactNode;
418
+ /**
419
+ * Render an image filling the tile background. Only called when `space.image`
420
+ * is set. The component must fill its parent (`StyleSheet.absoluteFill` or
421
+ * equivalent). If omitted, the short-name monogram is shown instead.
422
+ */
423
+ renderTileImage?: (space: RailSpace) => React.ReactNode;
424
+ /**
425
+ * Render an unread badge. Only called when `space.unread > 0`.
426
+ * If omitted, badges are not shown.
427
+ */
428
+ renderBadge?: (count: number) => React.ReactNode;
429
+ /**
430
+ * When `true`, each tile shows a small E2EE-lock corner badge (bottom-right).
431
+ * Requires `renderIcon` to be provided (otherwise the corner renders nothing).
432
+ * Default: `false`.
433
+ */
434
+ showLockCorner?: boolean;
435
+ /**
436
+ * Render the pinned rail foot (e.g. the account avatar and popover).
437
+ * The host app owns this entirely — identity state stays out of the package.
438
+ */
439
+ renderFoot?: () => React.ReactNode;
440
+ /**
441
+ * **Hook injection for web drag-reorder.** When provided, each space tile is
442
+ * wrapped in a `DndTile` that calls `useTileDnd(spaceId)` unconditionally at
443
+ * the top of its render — treat this prop as a React hook and keep it stable
444
+ * for the lifetime of a `SpacesRail` mount (always provided or always absent).
445
+ * Omit on native / in apps that don't need DnD.
446
+ */
447
+ useTileDnd?: (spaceId: string) => {
448
+ ref?: React.Ref<View>;
449
+ over?: boolean;
450
+ };
451
+ }
452
+ /**
453
+ * Vertical spaces rail — a 64px-wide column of square space tiles, a DM-home tile,
454
+ * an add-space tile, and a pinned foot for the account widget.
455
+ *
456
+ * Styled entirely from the injected {@link Theme} via `useOctoSpacesTheme()`.
457
+ * All icons, images, badges, and the account foot are provided by the host app.
458
+ */
459
+ declare function SpacesRail({ spaces, activeId, onSelect, onAdd, onSelectDms, dmsActive, dmUnread, dmLabel, addLabel, renderIcon, renderTileImage, renderBadge, showLockCorner, renderFoot, useTileDnd, }: SpacesRailProps): React.JSX.Element;
339
460
 
340
- export { type ColorScheme, type DiscoverEntry, DiscoverList, type DiscoverListProps, DiscoverRow, type DiscoverRowProps, DiscoverScreen, type DiscoverScreenProps, type Easing, type Fonts, type LabelTracking, type Layers, type Layout, type Motion, type MotionToken, OctoSpacesThemeProvider, type OctoSpacesThemeProviderProps, type Opacity, type Palette, type Radii, type ShadowToken, type Shadows, type Spacing, type Swatches, type Theme, type TypeScale, type Typography, avatarTint, filterDiscoverEntries, focusRingStyle, glowShadow, paperBorder, presenceColor, sortDiscoverEntries, statusColor, swatch, useOctoSpacesTheme, verificationColor };
461
+ export { type ColorScheme, type DiscoverEntry, DiscoverList, type DiscoverListProps, DiscoverRow, type DiscoverRowProps, DiscoverScreen, type DiscoverScreenProps, type Easing, type Fonts, type LabelTracking, type Layers, type Layout, type Motion, type MotionToken, OctoSpacesThemeProvider, type OctoSpacesThemeProviderProps, type Opacity, type Palette, type Radii, type RailIconName, type RailSpace, type ShadowToken, type Shadows, SpacesRail, type SpacesRailProps, type Spacing, type Swatches, type Theme, type TypeScale, type Typography, avatarTint, filterDiscoverEntries, focusRingStyle, glowShadow, paperBorder, presenceColor, sortDiscoverEntries, statusColor, swatch, useOctoSpacesTheme, verificationColor };
package/dist/index.js CHANGED
@@ -237,7 +237,7 @@ function DiscoverList({
237
237
  }
238
238
 
239
239
  // src/discover/DiscoverScreen.tsx
240
- import React4, { useCallback as useCallback3, useEffect, useRef, useState } from "react";
240
+ import React4, { useCallback as useCallback3, useEffect, useImperativeHandle, useRef, useState } from "react";
241
241
  import { ActivityIndicator, Pressable as Pressable2, Text as Text3, TextInput, View as View3 } from "react-native";
242
242
  function DiscoverScreen({
243
243
  loadEntries,
@@ -246,7 +246,8 @@ function DiscoverScreen({
246
246
  title = "Discover",
247
247
  emptyMessage = "No public objects yet",
248
248
  emptySearchMessage,
249
- searchEnabled = true
249
+ searchEnabled = true,
250
+ reloadRef
250
251
  }) {
251
252
  const theme = useOctoSpacesTheme();
252
253
  const [state, setState] = useState({ status: "idle" });
@@ -278,6 +279,7 @@ function DiscoverScreen({
278
279
  if (!cancelledRef.current) setRefreshing(false);
279
280
  }
280
281
  }, [loadEntries]);
282
+ useImperativeHandle(reloadRef, () => handleRefresh, [handleRefresh]);
281
283
  useEffect(() => {
282
284
  cancelledRef.current = false;
283
285
  void load();
@@ -396,11 +398,442 @@ function DiscoverScreen({
396
398
  }
397
399
  ));
398
400
  }
401
+
402
+ // src/sidebar/SpacesRail.tsx
403
+ import React5, { useState as useState2 } from "react";
404
+ import {
405
+ Pressable as RNPressable,
406
+ ScrollView,
407
+ Text as Text4,
408
+ View as View4
409
+ } from "react-native";
410
+
411
+ // src/sidebar/tile-state.ts
412
+ function railTileState(state, tokens, radiusActive, radiusDefault) {
413
+ const { active, hovered, over } = state;
414
+ let bg;
415
+ let borderColor;
416
+ let borderWidth;
417
+ let labelColor;
418
+ if (active) {
419
+ bg = tokens.primary;
420
+ borderColor = "transparent";
421
+ borderWidth = 0;
422
+ labelColor = tokens.textOnPrimary;
423
+ } else if (hovered) {
424
+ bg = tokens.primaryMuted;
425
+ borderColor = tokens.railTileHoverBorder;
426
+ borderWidth = 1;
427
+ labelColor = tokens.railTileHoverInk;
428
+ } else {
429
+ bg = tokens.railTile;
430
+ borderColor = tokens.borderSubtle;
431
+ borderWidth = 1;
432
+ labelColor = tokens.textSecondary;
433
+ }
434
+ if (over && !active) {
435
+ borderColor = tokens.primary;
436
+ borderWidth = 1;
437
+ }
438
+ const radius = active || hovered || over ? radiusActive : radiusDefault;
439
+ const shadow = active ? glowShadow(tokens.railGlow, 8, 0.3) : null;
440
+ return { bg, borderColor, borderWidth, radius, labelColor, shadow };
441
+ }
442
+
443
+ // src/sidebar/SpacesRail.tsx
444
+ var Pressable3 = RNPressable;
445
+ function resolveRailTokens(theme) {
446
+ const { colors, swatches } = theme;
447
+ return {
448
+ primary: colors.primary,
449
+ primaryMuted: colors.primaryMuted,
450
+ primarySubtle: colors.primarySubtle,
451
+ surfaceInput: colors.surfaceInput,
452
+ borderSubtle: colors.borderSubtle,
453
+ textOnPrimary: colors.textOnPrimary,
454
+ textSecondary: colors.textSecondary,
455
+ textTertiary: colors.textTertiary,
456
+ railTile: swatches["railTile"] ?? colors.surfaceInput,
457
+ railTileHoverBorder: swatches["railTileHoverBorder"] ?? colors.primarySubtle,
458
+ railGlow: swatches["railGlow"] ?? colors.primary,
459
+ railTileHoverInk: swatches["railTileHoverInk"] ?? colors.primary
460
+ };
461
+ }
462
+ var TILE_SIZE = 40;
463
+ var CORNER_SIZE = 16;
464
+ var BADGE_OFFSET = -5;
465
+ var CORNER_OFFSET = -3;
466
+ function TileContent({
467
+ space,
468
+ labelColor,
469
+ fontFamily,
470
+ fontSize,
471
+ lineHeight,
472
+ cornerBg,
473
+ cornerBorder,
474
+ cornerIconColor,
475
+ renderIcon,
476
+ renderTileImage,
477
+ renderBadge,
478
+ showLockCorner
479
+ }) {
480
+ return /* @__PURE__ */ React5.createElement(React5.Fragment, null, space.image && renderTileImage ? renderTileImage(space) : /* @__PURE__ */ React5.createElement(
481
+ Text4,
482
+ {
483
+ style: {
484
+ fontSize,
485
+ lineHeight,
486
+ fontWeight: "700",
487
+ fontFamily: fontFamily ?? void 0,
488
+ color: labelColor
489
+ },
490
+ numberOfLines: 1
491
+ },
492
+ space.short
493
+ ), showLockCorner && renderIcon ? /* @__PURE__ */ React5.createElement(
494
+ View4,
495
+ {
496
+ style: {
497
+ position: "absolute",
498
+ bottom: CORNER_OFFSET,
499
+ right: CORNER_OFFSET,
500
+ width: CORNER_SIZE,
501
+ height: CORNER_SIZE,
502
+ borderRadius: CORNER_SIZE / 2,
503
+ borderWidth: 1,
504
+ alignItems: "center",
505
+ justifyContent: "center",
506
+ backgroundColor: cornerBg,
507
+ borderColor: cornerBorder
508
+ }
509
+ },
510
+ renderIcon("lock", 9, cornerIconColor)
511
+ ) : null, space.muted && renderIcon ? /* @__PURE__ */ React5.createElement(
512
+ View4,
513
+ {
514
+ style: {
515
+ position: "absolute",
516
+ bottom: CORNER_OFFSET,
517
+ left: CORNER_OFFSET,
518
+ width: CORNER_SIZE,
519
+ height: CORNER_SIZE,
520
+ borderRadius: CORNER_SIZE / 2,
521
+ borderWidth: 1,
522
+ alignItems: "center",
523
+ justifyContent: "center",
524
+ backgroundColor: cornerBg,
525
+ borderColor: cornerBorder
526
+ }
527
+ },
528
+ renderIcon("mute", 9, cornerIconColor)
529
+ ) : null, space.unread ? /* @__PURE__ */ React5.createElement(
530
+ View4,
531
+ {
532
+ style: {
533
+ position: "absolute",
534
+ top: BADGE_OFFSET,
535
+ right: BADGE_OFFSET
536
+ }
537
+ },
538
+ renderBadge ? renderBadge(space.unread) : null
539
+ ) : null);
540
+ }
541
+ function PlainTile({
542
+ space,
543
+ active,
544
+ onPress,
545
+ tokens,
546
+ radiusActive,
547
+ radiusDefault,
548
+ renderIcon,
549
+ renderTileImage,
550
+ renderBadge,
551
+ showLockCorner,
552
+ cornerBg,
553
+ cornerBorder,
554
+ fontFamily,
555
+ fontSize,
556
+ lineHeight
557
+ }) {
558
+ const [hovered, setHovered] = useState2(false);
559
+ const s = railTileState({ active, hovered, over: false }, tokens, radiusActive, radiusDefault);
560
+ return /* @__PURE__ */ React5.createElement(
561
+ Pressable3,
562
+ {
563
+ onPress,
564
+ onMouseEnter: () => setHovered(true),
565
+ onMouseLeave: () => setHovered(false),
566
+ accessibilityRole: "button",
567
+ accessibilityLabel: space.short,
568
+ style: {
569
+ position: "relative",
570
+ width: TILE_SIZE,
571
+ height: TILE_SIZE,
572
+ alignItems: "center",
573
+ justifyContent: "center",
574
+ overflow: "hidden",
575
+ borderRadius: s.radius,
576
+ backgroundColor: s.bg,
577
+ borderWidth: s.borderWidth,
578
+ borderColor: s.borderColor,
579
+ ...s.shadow ?? {}
580
+ }
581
+ },
582
+ /* @__PURE__ */ React5.createElement(
583
+ TileContent,
584
+ {
585
+ space,
586
+ labelColor: s.labelColor,
587
+ fontFamily,
588
+ fontSize,
589
+ lineHeight,
590
+ cornerBg,
591
+ cornerBorder,
592
+ cornerIconColor: tokens.textTertiary,
593
+ renderIcon,
594
+ renderTileImage,
595
+ renderBadge,
596
+ showLockCorner
597
+ }
598
+ )
599
+ );
600
+ }
601
+ function DndTile({
602
+ space,
603
+ active,
604
+ onPress,
605
+ tokens,
606
+ radiusActive,
607
+ radiusDefault,
608
+ renderIcon,
609
+ renderTileImage,
610
+ renderBadge,
611
+ showLockCorner,
612
+ cornerBg,
613
+ cornerBorder,
614
+ fontFamily,
615
+ fontSize,
616
+ lineHeight,
617
+ dnd
618
+ }) {
619
+ const [hovered, setHovered] = useState2(false);
620
+ const { ref, over = false } = dnd(space.id);
621
+ const s = railTileState({ active, hovered, over }, tokens, radiusActive, radiusDefault);
622
+ return /* @__PURE__ */ React5.createElement(
623
+ Pressable3,
624
+ {
625
+ ref,
626
+ onPress,
627
+ onMouseEnter: () => setHovered(true),
628
+ onMouseLeave: () => setHovered(false),
629
+ accessibilityRole: "button",
630
+ accessibilityLabel: space.short,
631
+ style: {
632
+ position: "relative",
633
+ width: TILE_SIZE,
634
+ height: TILE_SIZE,
635
+ alignItems: "center",
636
+ justifyContent: "center",
637
+ overflow: "hidden",
638
+ borderRadius: s.radius,
639
+ backgroundColor: s.bg,
640
+ borderWidth: s.borderWidth,
641
+ borderColor: s.borderColor,
642
+ ...s.shadow ?? {}
643
+ }
644
+ },
645
+ /* @__PURE__ */ React5.createElement(
646
+ TileContent,
647
+ {
648
+ space,
649
+ labelColor: s.labelColor,
650
+ fontFamily,
651
+ fontSize,
652
+ lineHeight,
653
+ cornerBg,
654
+ cornerBorder,
655
+ cornerIconColor: tokens.textTertiary,
656
+ renderIcon,
657
+ renderTileImage,
658
+ renderBadge,
659
+ showLockCorner
660
+ }
661
+ )
662
+ );
663
+ }
664
+ function SpacesRail({
665
+ spaces,
666
+ activeId,
667
+ onSelect,
668
+ onAdd,
669
+ onSelectDms,
670
+ dmsActive = false,
671
+ dmUnread,
672
+ dmLabel = "Direct messages",
673
+ addLabel = "Create or join a space",
674
+ renderIcon,
675
+ renderTileImage,
676
+ renderBadge,
677
+ showLockCorner = false,
678
+ renderFoot,
679
+ useTileDnd
680
+ }) {
681
+ const theme = useOctoSpacesTheme();
682
+ const { colors, spacing, radii, type: typeScale, fonts, layout } = theme;
683
+ const tokens = resolveRailTokens(theme);
684
+ const railWidth = layout["railWidth"] ?? 64;
685
+ const spaceV = spacing["2"] ?? 8;
686
+ const spaceXs = spacing["1"] ?? 4;
687
+ const spaceS = spacing["2"] ?? 8;
688
+ const spaceMd = spacing["3"] ?? 12;
689
+ const radiusActive = radii["lg"] ?? 12;
690
+ const radiusDefault = radii["xl"] ?? 16;
691
+ const footnoteSize = typeScale["footnote"]?.size ?? 12;
692
+ const footnoteLineH = typeScale["footnote"]?.lineHeight ?? 18;
693
+ const monoFont = fonts["mono"] ?? void 0;
694
+ const cornerBg = colors.sidebar;
695
+ const cornerBorder = colors.border;
696
+ const tileShared = {
697
+ tokens,
698
+ radiusActive,
699
+ radiusDefault,
700
+ renderIcon,
701
+ renderTileImage,
702
+ renderBadge,
703
+ showLockCorner,
704
+ cornerBg,
705
+ cornerBorder,
706
+ fontFamily: monoFont,
707
+ fontSize: footnoteSize,
708
+ lineHeight: footnoteLineH
709
+ };
710
+ const [dmHovered, setDmHovered] = useState2(false);
711
+ const dmTileStyle = railTileState(
712
+ { active: dmsActive, hovered: dmHovered, over: false },
713
+ tokens,
714
+ radiusActive,
715
+ radiusDefault
716
+ );
717
+ const dmIconColor = dmsActive ? colors.textOnPrimary : dmHovered ? tokens.railTileHoverInk : colors.textSecondary;
718
+ const [addHovered, setAddHovered] = useState2(false);
719
+ const hasDnd = !!useTileDnd;
720
+ return /* @__PURE__ */ React5.createElement(
721
+ View4,
722
+ {
723
+ style: {
724
+ width: railWidth,
725
+ paddingVertical: spaceMd,
726
+ borderRightWidth: 1,
727
+ borderRightColor: colors.border,
728
+ backgroundColor: colors.sidebar,
729
+ alignItems: "center",
730
+ gap: spaceS
731
+ }
732
+ },
733
+ /* @__PURE__ */ React5.createElement(
734
+ ScrollView,
735
+ {
736
+ style: { alignSelf: "stretch", flex: 1 },
737
+ contentContainerStyle: {
738
+ alignItems: "center",
739
+ gap: spaceV,
740
+ paddingVertical: spaceXs
741
+ },
742
+ showsVerticalScrollIndicator: false
743
+ },
744
+ onSelectDms ? /* @__PURE__ */ React5.createElement(View4, { style: { position: "relative" } }, /* @__PURE__ */ React5.createElement(
745
+ Pressable3,
746
+ {
747
+ onPress: onSelectDms,
748
+ onMouseEnter: () => setDmHovered(true),
749
+ onMouseLeave: () => setDmHovered(false),
750
+ accessibilityRole: "button",
751
+ accessibilityLabel: dmLabel,
752
+ style: {
753
+ width: TILE_SIZE,
754
+ height: TILE_SIZE,
755
+ alignItems: "center",
756
+ justifyContent: "center",
757
+ borderRadius: dmTileStyle.radius,
758
+ backgroundColor: dmTileStyle.bg,
759
+ borderWidth: dmTileStyle.borderWidth,
760
+ borderColor: dmTileStyle.borderColor,
761
+ ...dmTileStyle.shadow ?? {}
762
+ }
763
+ },
764
+ renderIcon ? renderIcon("dm", 20, dmIconColor) : null
765
+ ), showLockCorner && renderIcon ? /* @__PURE__ */ React5.createElement(
766
+ View4,
767
+ {
768
+ style: {
769
+ position: "absolute",
770
+ bottom: CORNER_OFFSET,
771
+ right: CORNER_OFFSET,
772
+ width: CORNER_SIZE,
773
+ height: CORNER_SIZE,
774
+ borderRadius: CORNER_SIZE / 2,
775
+ borderWidth: 1,
776
+ alignItems: "center",
777
+ justifyContent: "center",
778
+ backgroundColor: cornerBg,
779
+ borderColor: cornerBorder
780
+ }
781
+ },
782
+ renderIcon("lock", 9, tokens.textTertiary)
783
+ ) : null, dmUnread ? /* @__PURE__ */ React5.createElement(View4, { style: { position: "absolute", top: BADGE_OFFSET, right: BADGE_OFFSET } }, renderBadge ? renderBadge(dmUnread) : null) : null) : null,
784
+ spaces.map(
785
+ (s) => hasDnd ? /* @__PURE__ */ React5.createElement(
786
+ DndTile,
787
+ {
788
+ key: s.id,
789
+ space: s,
790
+ active: s.id === activeId,
791
+ onPress: () => onSelect?.(s.id),
792
+ dnd: useTileDnd,
793
+ ...tileShared
794
+ }
795
+ ) : /* @__PURE__ */ React5.createElement(
796
+ PlainTile,
797
+ {
798
+ key: s.id,
799
+ space: s,
800
+ active: s.id === activeId,
801
+ onPress: () => onSelect?.(s.id),
802
+ ...tileShared
803
+ }
804
+ )
805
+ ),
806
+ /* @__PURE__ */ React5.createElement(
807
+ Pressable3,
808
+ {
809
+ onPress: onAdd,
810
+ onMouseEnter: () => setAddHovered(true),
811
+ onMouseLeave: () => setAddHovered(false),
812
+ accessibilityRole: "button",
813
+ accessibilityLabel: addLabel,
814
+ style: {
815
+ width: TILE_SIZE,
816
+ height: TILE_SIZE,
817
+ alignItems: "center",
818
+ justifyContent: "center",
819
+ borderRadius: radiusDefault,
820
+ borderWidth: 1,
821
+ borderStyle: "dashed",
822
+ borderColor: addHovered ? colors.border : colors.borderSubtle
823
+ }
824
+ },
825
+ renderIcon ? renderIcon("add", 16, addHovered ? tokens.railTileHoverInk : colors.textTertiary) : null
826
+ )
827
+ ),
828
+ renderFoot ? renderFoot() : null
829
+ );
830
+ }
399
831
  export {
400
832
  DiscoverList,
401
833
  DiscoverRow,
402
834
  DiscoverScreen,
403
835
  OctoSpacesThemeProvider,
836
+ SpacesRail,
404
837
  avatarTint,
405
838
  filterDiscoverEntries,
406
839
  focusRingStyle,