@drakkar.software/octospaces-ui 0.2.1 → 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`.
@@ -349,4 +350,112 @@ interface DiscoverScreenProps {
349
350
  }
350
351
  declare function DiscoverScreen({ loadEntries, renderIcon, onOpen, title, emptyMessage, emptySearchMessage, searchEnabled, reloadRef, }: DiscoverScreenProps): React.JSX.Element;
351
352
 
352
- 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 };
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;
460
+
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
@@ -398,11 +398,442 @@ function DiscoverScreen({
398
398
  }
399
399
  ));
400
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
+ }
401
831
  export {
402
832
  DiscoverList,
403
833
  DiscoverRow,
404
834
  DiscoverScreen,
405
835
  OctoSpacesThemeProvider,
836
+ SpacesRail,
406
837
  avatarTint,
407
838
  filterDiscoverEntries,
408
839
  focusRingStyle,