@drakkar.software/octospaces-ui 0.2.1 → 0.4.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 +353 -2
- package/dist/index.js +715 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +22 -0
- package/src/lightbox/Lightbox.tsx +132 -0
- package/src/sidebar/Sidebar.tsx +101 -0
- package/src/sidebar/SidebarActionButton.tsx +86 -0
- package/src/sidebar/SidebarHeader.tsx +111 -0
- package/src/sidebar/SidebarItem.tsx +148 -0
- package/src/sidebar/SpacesRail.tsx +590 -0
- package/src/sidebar/index.ts +11 -0
- package/src/sidebar/tile-state.test.ts +126 -0
- package/src/sidebar/tile-state.ts +100 -0
- package/src/sidebar/types.ts +25 -0
- package/src/theme/helpers.test.ts +1 -0
- package/src/theme/types.ts +1 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { railTileState } from './tile-state.js';
|
|
3
|
+
import type { RailTileTokens } from './tile-state.js';
|
|
4
|
+
|
|
5
|
+
// ── Shared fixtures ────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const tokens: RailTileTokens = {
|
|
8
|
+
primary: '#0e7090',
|
|
9
|
+
primaryMuted: '#e0f2f8',
|
|
10
|
+
primarySubtle: '#cce9f3',
|
|
11
|
+
surfaceInput: '#f0f4f6',
|
|
12
|
+
borderSubtle: '#d1dde3',
|
|
13
|
+
textOnPrimary: '#ffffff',
|
|
14
|
+
textSecondary: '#5c7080',
|
|
15
|
+
textTertiary: '#8a9daa',
|
|
16
|
+
railTile: '#e8edf0',
|
|
17
|
+
railTileHoverBorder: '#90c8da',
|
|
18
|
+
railGlow: '#5bc8e2',
|
|
19
|
+
railTileHoverInk: '#0e7090',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const RADIUS_ACTIVE = 12;
|
|
23
|
+
const RADIUS_DEFAULT = 16;
|
|
24
|
+
|
|
25
|
+
// ── railTileState ──────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
describe('railTileState — active tile', () => {
|
|
28
|
+
it('uses primary bg with no border', () => {
|
|
29
|
+
const style = railTileState({ active: true, hovered: false, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
30
|
+
expect(style.bg).toBe(tokens.primary);
|
|
31
|
+
expect(style.borderWidth).toBe(0);
|
|
32
|
+
expect(style.borderColor).toBe('transparent');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('uses textOnPrimary label color', () => {
|
|
36
|
+
const style = railTileState({ active: true, hovered: false, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
37
|
+
expect(style.labelColor).toBe(tokens.textOnPrimary);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('applies a glow shadow', () => {
|
|
41
|
+
const style = railTileState({ active: true, hovered: false, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
42
|
+
expect(style.shadow).not.toBeNull();
|
|
43
|
+
expect(style.shadow!.shadowColor).toBe(tokens.railGlow);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('uses the active (smaller) radius', () => {
|
|
47
|
+
const style = railTileState({ active: true, hovered: false, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
48
|
+
expect(style.radius).toBe(RADIUS_ACTIVE);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('railTileState — hovered tile (not active)', () => {
|
|
53
|
+
it('uses primaryMuted bg', () => {
|
|
54
|
+
const style = railTileState({ active: false, hovered: true, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
55
|
+
expect(style.bg).toBe(tokens.primaryMuted);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('uses railTileHoverBorder as border color', () => {
|
|
59
|
+
const style = railTileState({ active: false, hovered: true, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
60
|
+
expect(style.borderColor).toBe(tokens.railTileHoverBorder);
|
|
61
|
+
expect(style.borderWidth).toBe(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('uses railTileHoverInk as label color', () => {
|
|
65
|
+
const style = railTileState({ active: false, hovered: true, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
66
|
+
expect(style.labelColor).toBe(tokens.railTileHoverInk);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('uses the active (smaller) radius when hovered', () => {
|
|
70
|
+
const style = railTileState({ active: false, hovered: true, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
71
|
+
expect(style.radius).toBe(RADIUS_ACTIVE);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('has no glow shadow', () => {
|
|
75
|
+
const style = railTileState({ active: false, hovered: true, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
76
|
+
expect(style.shadow).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('railTileState — resting tile (not active, not hovered)', () => {
|
|
81
|
+
it('uses railTile bg token', () => {
|
|
82
|
+
const style = railTileState({ active: false, hovered: false, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
83
|
+
expect(style.bg).toBe(tokens.railTile);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('uses borderSubtle and borderWidth 1', () => {
|
|
87
|
+
const style = railTileState({ active: false, hovered: false, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
88
|
+
expect(style.borderColor).toBe(tokens.borderSubtle);
|
|
89
|
+
expect(style.borderWidth).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('uses textSecondary label color', () => {
|
|
93
|
+
const style = railTileState({ active: false, hovered: false, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
94
|
+
expect(style.labelColor).toBe(tokens.textSecondary);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('uses the default (larger) radius', () => {
|
|
98
|
+
const style = railTileState({ active: false, hovered: false, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
99
|
+
expect(style.radius).toBe(RADIUS_DEFAULT);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('has no glow shadow', () => {
|
|
103
|
+
const style = railTileState({ active: false, hovered: false, over: false }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
104
|
+
expect(style.shadow).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('railTileState — drop-over state (DnD target)', () => {
|
|
109
|
+
it('shows primary border when another tile is dragged over (not active)', () => {
|
|
110
|
+
const style = railTileState({ active: false, hovered: false, over: true }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
111
|
+
expect(style.borderColor).toBe(tokens.primary);
|
|
112
|
+
expect(style.borderWidth).toBe(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('uses the active radius when over', () => {
|
|
116
|
+
const style = railTileState({ active: false, hovered: false, over: true }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
117
|
+
expect(style.radius).toBe(RADIUS_ACTIVE);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('does NOT override border when tile is active (active wins)', () => {
|
|
121
|
+
const style = railTileState({ active: true, hovered: false, over: true }, tokens, RADIUS_ACTIVE, RADIUS_DEFAULT);
|
|
122
|
+
// Active tile keeps its no-border style even when over
|
|
123
|
+
expect(style.borderWidth).toBe(0);
|
|
124
|
+
expect(style.borderColor).toBe('transparent');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper: maps tile interaction state + resolved theme tokens → visual style.
|
|
3
|
+
* No React or React Native imports — fully testable in isolation.
|
|
4
|
+
*/
|
|
5
|
+
import type { ShadowToken } from '../theme/types.js';
|
|
6
|
+
import { glowShadow } from '../theme/helpers.js';
|
|
7
|
+
|
|
8
|
+
// ── Token contract ─────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolved color tokens consumed by {@link railTileState}. Built from a `Theme`
|
|
12
|
+
* object in the component layer (see `resolveRailTokens` in `SpacesRail.tsx`).
|
|
13
|
+
* Swatch entries fall back to palette tokens when the host app hasn't set them.
|
|
14
|
+
*/
|
|
15
|
+
export interface RailTileTokens {
|
|
16
|
+
// Core palette tokens
|
|
17
|
+
primary: string;
|
|
18
|
+
primaryMuted: string;
|
|
19
|
+
primarySubtle: string;
|
|
20
|
+
surfaceInput: string;
|
|
21
|
+
borderSubtle: string;
|
|
22
|
+
textOnPrimary: string;
|
|
23
|
+
textSecondary: string;
|
|
24
|
+
textTertiary: string;
|
|
25
|
+
// Optional swatch overrides (host app can tune rail-specific colors)
|
|
26
|
+
railTile: string; // swatch 'railTile' ?? surfaceInput
|
|
27
|
+
railTileHoverBorder: string; // swatch 'railTileHoverBorder' ?? primarySubtle
|
|
28
|
+
railGlow: string; // swatch 'railGlow' ?? primary
|
|
29
|
+
railTileHoverInk: string; // swatch 'railTileHoverInk' ?? primary
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Output ─────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/** Resolved visual properties for a single rail tile. */
|
|
35
|
+
export interface RailTileStyle {
|
|
36
|
+
/** Tile background color. */
|
|
37
|
+
bg: string;
|
|
38
|
+
/** Tile border color. */
|
|
39
|
+
borderColor: string;
|
|
40
|
+
/** Tile border width (0 when active, 1 otherwise). */
|
|
41
|
+
borderWidth: number;
|
|
42
|
+
/** Tile border-radius in pixels (squared-off when active/hovered, rounded otherwise). */
|
|
43
|
+
radius: number;
|
|
44
|
+
/** Monogram label color. */
|
|
45
|
+
labelColor: string;
|
|
46
|
+
/** Active glow shadow, or `null` when not active. */
|
|
47
|
+
shadow: ShadowToken | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Mapping ────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Map tile interaction state to visual style tokens.
|
|
54
|
+
*
|
|
55
|
+
* @param state Current interaction state.
|
|
56
|
+
* @param tokens Resolved color tokens (see {@link RailTileTokens}).
|
|
57
|
+
* @param radiusActive Border-radius when the tile is active or hovered (squarer look).
|
|
58
|
+
* @param radiusDefault Border-radius for the resting state (rounder look).
|
|
59
|
+
*/
|
|
60
|
+
export function railTileState(
|
|
61
|
+
state: { active: boolean; hovered: boolean; over: boolean },
|
|
62
|
+
tokens: RailTileTokens,
|
|
63
|
+
radiusActive: number,
|
|
64
|
+
radiusDefault: number,
|
|
65
|
+
): RailTileStyle {
|
|
66
|
+
const { active, hovered, over } = state;
|
|
67
|
+
|
|
68
|
+
let bg: string;
|
|
69
|
+
let borderColor: string;
|
|
70
|
+
let borderWidth: number;
|
|
71
|
+
let labelColor: string;
|
|
72
|
+
|
|
73
|
+
if (active) {
|
|
74
|
+
bg = tokens.primary;
|
|
75
|
+
borderColor = 'transparent';
|
|
76
|
+
borderWidth = 0;
|
|
77
|
+
labelColor = tokens.textOnPrimary;
|
|
78
|
+
} else if (hovered) {
|
|
79
|
+
bg = tokens.primaryMuted;
|
|
80
|
+
borderColor = tokens.railTileHoverBorder;
|
|
81
|
+
borderWidth = 1;
|
|
82
|
+
labelColor = tokens.railTileHoverInk;
|
|
83
|
+
} else {
|
|
84
|
+
bg = tokens.railTile;
|
|
85
|
+
borderColor = tokens.borderSubtle;
|
|
86
|
+
borderWidth = 1;
|
|
87
|
+
labelColor = tokens.textSecondary;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Drop-target overlay: accent border when a dragged tile is over this one.
|
|
91
|
+
if (over && !active) {
|
|
92
|
+
borderColor = tokens.primary;
|
|
93
|
+
borderWidth = 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const radius = active || hovered || over ? radiusActive : radiusDefault;
|
|
97
|
+
const shadow: ShadowToken | null = active ? glowShadow(tokens.railGlow, 8, 0.3) : null;
|
|
98
|
+
|
|
99
|
+
return { bg, borderColor, borderWidth, radius, labelColor, shadow };
|
|
100
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the SpacesRail component.
|
|
3
|
+
*
|
|
4
|
+
* Structurally compatible with `Space` from `@drakkar.software/octochat-sdk` /
|
|
5
|
+
* `@drakkar.software/octospaces-sdk` — apps can pass their domain objects directly
|
|
6
|
+
* without a runtime conversion step.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A minimal space descriptor for the rail: id + display info + badge state. */
|
|
10
|
+
export interface RailSpace {
|
|
11
|
+
/** Unique space identifier. */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Short display name or initials shown as a monogram when no image is available. */
|
|
14
|
+
short: string;
|
|
15
|
+
/** Optional image URI (data URI or URL) rendered as the tile background via
|
|
16
|
+
* `SpacesRailProps.renderTileImage`. */
|
|
17
|
+
image?: string;
|
|
18
|
+
/** Unread-message count shown as a badge overlay. */
|
|
19
|
+
unread?: number;
|
|
20
|
+
/** Whether the space is muted (shows a mute-corner icon when `renderIcon` is provided). */
|
|
21
|
+
muted?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Named icon slots injected into the rail via `SpacesRailProps.renderIcon`. */
|
|
25
|
+
export type RailIconName = 'dm' | 'lock' | 'mute' | 'add';
|
package/src/theme/types.ts
CHANGED