@drakkar.software/octospaces-ui 0.3.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 +354 -3
- package/dist/index.js +588 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +18 -1
- 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/SpaceSwitcher.tsx +521 -0
- package/src/sidebar/index.ts +13 -0
- package/src/theme/helpers.test.ts +1 -0
- package/src/theme/types.ts +1 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless themed space-switcher component.
|
|
3
|
+
*
|
|
4
|
+
* Renders a trigger button (active-space avatar + name + chevron) that opens a
|
|
5
|
+
* dropdown listing all spaces with per-row selection, a "join or create" action,
|
|
6
|
+
* optional space settings, and an app-provided footer slot (account section, etc.).
|
|
7
|
+
*
|
|
8
|
+
* The popup container (Popover on desktop, Sheet on mobile) is fully delegated to
|
|
9
|
+
* the host via `renderContainer` so this package stays free of modal dependencies.
|
|
10
|
+
* Avatars and icons are delegated via render-props for the same reason.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* // OctoVault — sidebar variant with Popover container
|
|
15
|
+
* <SpaceSwitcher
|
|
16
|
+
* spaces={spaces}
|
|
17
|
+
* activeId={activeId}
|
|
18
|
+
* onSelect={switchSpace}
|
|
19
|
+
* onAdd={() => router.push('/join')}
|
|
20
|
+
* onSettings={() => router.push(`/space/${activeId}`)}
|
|
21
|
+
* variant="sidebar"
|
|
22
|
+
* renderTriggerAvatar={(space, size) => (
|
|
23
|
+
* <Avatar label={space?.short ?? ''} image={space?.image} size={size} />
|
|
24
|
+
* )}
|
|
25
|
+
* renderSpaceAvatar={(space, size) => (
|
|
26
|
+
* <Avatar label={space.short ?? ''} image={space.image} size={size} />
|
|
27
|
+
* )}
|
|
28
|
+
* renderIcon={(name, size, color) => <Icon name={SWITCHER_ICON[name]} size={size} color={color} />}
|
|
29
|
+
* renderContainer={({ isOpen, onClose, anchorRef, children }) => (
|
|
30
|
+
* <>
|
|
31
|
+
* <Popover visible={isOpen} onClose={onClose} anchorRef={anchorRef} placement="bottom-start" width={240}>
|
|
32
|
+
* {children}
|
|
33
|
+
* </Popover>
|
|
34
|
+
* </>
|
|
35
|
+
* )}
|
|
36
|
+
* footerSlot={<AccountSwitcher onRequestClose={...} onViewProfile={...} />}
|
|
37
|
+
* />
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
import React, { useRef, useState } from 'react';
|
|
41
|
+
import { Pressable as RNPressable, StyleSheet, Text, View } from 'react-native';
|
|
42
|
+
import type { PressableProps, TextStyle, View as RNView } from 'react-native';
|
|
43
|
+
|
|
44
|
+
import { useOctoSpacesTheme } from '../theme/provider.js';
|
|
45
|
+
|
|
46
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/** Structural space item — no SDK dependency. */
|
|
49
|
+
export interface SwitcherSpace {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
/** 2-letter monogram used as avatar fallback. */
|
|
53
|
+
short?: string;
|
|
54
|
+
/** Uploaded space image URI; absent → host renders monogram. */
|
|
55
|
+
image?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Icon name union for the switcher's built-in glyphs. */
|
|
59
|
+
export type SwitcherIconName = 'chevron-down' | 'check' | 'plus' | 'gear';
|
|
60
|
+
|
|
61
|
+
export interface SpaceSwitcherProps {
|
|
62
|
+
spaces: SwitcherSpace[];
|
|
63
|
+
activeId?: string | null;
|
|
64
|
+
/** Called when the user taps a space row. */
|
|
65
|
+
onSelect: (id: string) => void;
|
|
66
|
+
/** "Join or create a space" action. Omit to hide the row. */
|
|
67
|
+
onAdd?: () => void;
|
|
68
|
+
/** Override the add-row label. @default "Join or create a space" */
|
|
69
|
+
addLabel?: string;
|
|
70
|
+
/**
|
|
71
|
+
* "Space settings" action. Only shown when both `onSettings` and `activeId`
|
|
72
|
+
* are provided. Omit to hide.
|
|
73
|
+
*/
|
|
74
|
+
onSettings?: () => void;
|
|
75
|
+
/** Override the settings-row label. @default "Space settings" */
|
|
76
|
+
settingsLabel?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Visual variant:
|
|
79
|
+
* - `'sidebar'` — compact left-aligned trigger for the desktop sidebar header.
|
|
80
|
+
* - `'appbar'` — centered trigger for a phone app-bar title area.
|
|
81
|
+
*/
|
|
82
|
+
variant: 'sidebar' | 'appbar';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Wraps the dropdown content in the host app's container (Popover / Sheet).
|
|
86
|
+
* Called with `{ isOpen, onClose, anchorRef, children }` — must render
|
|
87
|
+
* children inside an appropriate modal surface.
|
|
88
|
+
*/
|
|
89
|
+
renderContainer: (props: {
|
|
90
|
+
isOpen: boolean;
|
|
91
|
+
onClose: () => void;
|
|
92
|
+
anchorRef: React.RefObject<RNView>;
|
|
93
|
+
children: React.ReactNode;
|
|
94
|
+
}) => React.ReactNode;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Render the active-space avatar inside the trigger button.
|
|
98
|
+
* Receives the active `SwitcherSpace` (or `null` when none) and a pixel size.
|
|
99
|
+
* Omit to render nothing in the avatar slot.
|
|
100
|
+
*/
|
|
101
|
+
renderTriggerAvatar?: (space: SwitcherSpace | null, size: number) => React.ReactNode;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Render a space row's leading avatar.
|
|
105
|
+
* Receives the `SwitcherSpace` and a pixel size.
|
|
106
|
+
* Omit to render nothing in the leading slot.
|
|
107
|
+
*/
|
|
108
|
+
renderSpaceAvatar?: (space: SwitcherSpace, size: number) => React.ReactNode;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Render an icon glyph. Name is one of `'chevron-down' | 'check' | 'plus' | 'gear'`.
|
|
112
|
+
* Omit to hide chevron, check, and action icons (spaces remain selectable).
|
|
113
|
+
*/
|
|
114
|
+
renderIcon?: (name: SwitcherIconName, size: number, color: string) => React.ReactNode;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Footer rendered below the space list + action rows — use for account-switcher
|
|
118
|
+
* sections (with separator if needed). Fully app-owned.
|
|
119
|
+
*/
|
|
120
|
+
footerSlot?: React.ReactNode;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Hover-aware Pressable (RN-Web) ────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
type HoverProps = { onMouseEnter?: () => void; onMouseLeave?: () => void };
|
|
126
|
+
const Pressable = RNPressable as React.ForwardRefExoticComponent<
|
|
127
|
+
PressableProps & HoverProps & React.RefAttributes<RNView>
|
|
128
|
+
>;
|
|
129
|
+
|
|
130
|
+
// ── Component ─────────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
export function SpaceSwitcher({
|
|
133
|
+
spaces,
|
|
134
|
+
activeId,
|
|
135
|
+
onSelect,
|
|
136
|
+
onAdd,
|
|
137
|
+
addLabel = 'Join or create a space',
|
|
138
|
+
onSettings,
|
|
139
|
+
settingsLabel = 'Space settings',
|
|
140
|
+
variant,
|
|
141
|
+
renderContainer,
|
|
142
|
+
renderTriggerAvatar,
|
|
143
|
+
renderSpaceAvatar,
|
|
144
|
+
renderIcon,
|
|
145
|
+
footerSlot,
|
|
146
|
+
}: SpaceSwitcherProps) {
|
|
147
|
+
const theme = useOctoSpacesTheme();
|
|
148
|
+
const { colors, type: typeScale, fonts, spacing: sp, radii } = theme;
|
|
149
|
+
|
|
150
|
+
const [open, setOpen] = useState(false);
|
|
151
|
+
const [triggerHovered, setTriggerHovered] = useState(false);
|
|
152
|
+
const anchorRef = useRef<RNView>(null);
|
|
153
|
+
|
|
154
|
+
const active = spaces.find((s) => s.id === activeId) ?? spaces[0] ?? null;
|
|
155
|
+
|
|
156
|
+
const close = () => setOpen(false);
|
|
157
|
+
const handleSelect = (id: string) => {
|
|
158
|
+
close();
|
|
159
|
+
onSelect(id);
|
|
160
|
+
};
|
|
161
|
+
const handleAdd = () => {
|
|
162
|
+
close();
|
|
163
|
+
onAdd?.();
|
|
164
|
+
};
|
|
165
|
+
const handleSettings = () => {
|
|
166
|
+
close();
|
|
167
|
+
onSettings?.();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// ── spacing lookups ──────────────────────────────────────────────────────
|
|
171
|
+
const sp1 = (sp['1'] as number | undefined) ?? 4;
|
|
172
|
+
const sp2 = (sp['2'] as number | undefined) ?? 8;
|
|
173
|
+
const sp3 = (sp['3'] as number | undefined) ?? 12;
|
|
174
|
+
const sp4 = (sp['4'] as number | undefined) ?? 16;
|
|
175
|
+
const radMd = (radii['md'] as number | undefined) ?? 6;
|
|
176
|
+
|
|
177
|
+
const bodyFont = fonts['body'] ?? undefined;
|
|
178
|
+
const bodySize = typeScale['callout']?.size ?? 13;
|
|
179
|
+
const bodyLine = typeScale['callout']?.lineHeight ?? 18;
|
|
180
|
+
const labelSize = typeScale['caption']?.size ?? 11;
|
|
181
|
+
const labelLine = typeScale['caption']?.lineHeight ?? 16;
|
|
182
|
+
|
|
183
|
+
// ── trigger style ────────────────────────────────────────────────────────
|
|
184
|
+
const triggerStyle =
|
|
185
|
+
variant === 'sidebar'
|
|
186
|
+
? {
|
|
187
|
+
flex: 1 as const,
|
|
188
|
+
flexDirection: 'row' as const,
|
|
189
|
+
alignItems: 'center' as const,
|
|
190
|
+
gap: sp2,
|
|
191
|
+
paddingHorizontal: sp2,
|
|
192
|
+
paddingVertical: sp1 + 2,
|
|
193
|
+
borderRadius: radMd,
|
|
194
|
+
minWidth: 0,
|
|
195
|
+
backgroundColor: triggerHovered
|
|
196
|
+
? (colors.primarySubtle ?? 'rgba(0,0,0,0.05)')
|
|
197
|
+
: 'transparent',
|
|
198
|
+
}
|
|
199
|
+
: {
|
|
200
|
+
flexDirection: 'row' as const,
|
|
201
|
+
alignItems: 'center' as const,
|
|
202
|
+
justifyContent: 'center' as const,
|
|
203
|
+
gap: sp2,
|
|
204
|
+
paddingHorizontal: sp2,
|
|
205
|
+
paddingVertical: sp1,
|
|
206
|
+
borderRadius: radMd,
|
|
207
|
+
backgroundColor: triggerHovered
|
|
208
|
+
? (colors.primarySubtle ?? 'rgba(0,0,0,0.05)')
|
|
209
|
+
: 'transparent',
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// ── dropdown content ─────────────────────────────────────────────────────
|
|
213
|
+
const dropdownContent = (
|
|
214
|
+
<View style={{ paddingVertical: sp1 }}>
|
|
215
|
+
{spaces.length > 0 ? (
|
|
216
|
+
<SectionLabel
|
|
217
|
+
label="Spaces"
|
|
218
|
+
color={colors.textTertiary}
|
|
219
|
+
size={labelSize}
|
|
220
|
+
lineHeight={labelLine}
|
|
221
|
+
font={bodyFont}
|
|
222
|
+
paddingH={sp4}
|
|
223
|
+
paddingV={sp1}
|
|
224
|
+
/>
|
|
225
|
+
) : null}
|
|
226
|
+
|
|
227
|
+
{spaces.map((s) => (
|
|
228
|
+
<SpaceRow
|
|
229
|
+
key={s.id}
|
|
230
|
+
space={s}
|
|
231
|
+
active={s.id === (active?.id ?? null)}
|
|
232
|
+
onPress={() => handleSelect(s.id)}
|
|
233
|
+
renderAvatar={renderSpaceAvatar}
|
|
234
|
+
renderIcon={renderIcon}
|
|
235
|
+
colors={colors}
|
|
236
|
+
bodyFont={bodyFont}
|
|
237
|
+
bodySize={bodySize}
|
|
238
|
+
bodyLine={bodyLine}
|
|
239
|
+
sp2={sp2}
|
|
240
|
+
sp3={sp3}
|
|
241
|
+
sp4={sp4}
|
|
242
|
+
radMd={radMd}
|
|
243
|
+
/>
|
|
244
|
+
))}
|
|
245
|
+
|
|
246
|
+
{onAdd ? (
|
|
247
|
+
<ActionRow
|
|
248
|
+
label={spaces.length > 0 ? addLabel : 'Create your first space'}
|
|
249
|
+
iconName="plus"
|
|
250
|
+
onPress={handleAdd}
|
|
251
|
+
renderIcon={renderIcon}
|
|
252
|
+
colors={colors}
|
|
253
|
+
bodyFont={bodyFont}
|
|
254
|
+
bodySize={bodySize}
|
|
255
|
+
bodyLine={bodyLine}
|
|
256
|
+
sp2={sp2}
|
|
257
|
+
sp3={sp3}
|
|
258
|
+
sp4={sp4}
|
|
259
|
+
radMd={radMd}
|
|
260
|
+
/>
|
|
261
|
+
) : null}
|
|
262
|
+
|
|
263
|
+
{onSettings && active ? (
|
|
264
|
+
<ActionRow
|
|
265
|
+
label={settingsLabel}
|
|
266
|
+
iconName="gear"
|
|
267
|
+
onPress={handleSettings}
|
|
268
|
+
renderIcon={renderIcon}
|
|
269
|
+
colors={colors}
|
|
270
|
+
bodyFont={bodyFont}
|
|
271
|
+
bodySize={bodySize}
|
|
272
|
+
bodyLine={bodyLine}
|
|
273
|
+
sp2={sp2}
|
|
274
|
+
sp3={sp3}
|
|
275
|
+
sp4={sp4}
|
|
276
|
+
radMd={radMd}
|
|
277
|
+
/>
|
|
278
|
+
) : null}
|
|
279
|
+
|
|
280
|
+
{footerSlot != null ? (
|
|
281
|
+
<>
|
|
282
|
+
<View
|
|
283
|
+
style={{
|
|
284
|
+
height: StyleSheet.hairlineWidth,
|
|
285
|
+
backgroundColor: colors.borderSubtle,
|
|
286
|
+
marginVertical: sp1,
|
|
287
|
+
marginHorizontal: sp2,
|
|
288
|
+
}}
|
|
289
|
+
/>
|
|
290
|
+
{footerSlot}
|
|
291
|
+
</>
|
|
292
|
+
) : null}
|
|
293
|
+
</View>
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<>
|
|
298
|
+
<Pressable
|
|
299
|
+
ref={anchorRef}
|
|
300
|
+
accessibilityRole="button"
|
|
301
|
+
accessibilityLabel={active ? `${active.name} — switch space` : 'Switch space'}
|
|
302
|
+
accessibilityState={{ expanded: open }}
|
|
303
|
+
hitSlop={6}
|
|
304
|
+
onPress={() => setOpen(true)}
|
|
305
|
+
onMouseEnter={() => setTriggerHovered(true)}
|
|
306
|
+
onMouseLeave={() => setTriggerHovered(false)}
|
|
307
|
+
style={triggerStyle}
|
|
308
|
+
>
|
|
309
|
+
{renderTriggerAvatar ? renderTriggerAvatar(active, 22) : null}
|
|
310
|
+
<Text
|
|
311
|
+
numberOfLines={1}
|
|
312
|
+
style={
|
|
313
|
+
{
|
|
314
|
+
flex: variant === 'sidebar' ? 1 : undefined,
|
|
315
|
+
minWidth: 0,
|
|
316
|
+
flexShrink: 1,
|
|
317
|
+
fontSize: typeScale['heading']?.size ?? 15,
|
|
318
|
+
lineHeight: typeScale['heading']?.lineHeight ?? 20,
|
|
319
|
+
fontWeight: '600',
|
|
320
|
+
color: colors.text,
|
|
321
|
+
fontFamily: bodyFont,
|
|
322
|
+
} as TextStyle
|
|
323
|
+
}
|
|
324
|
+
>
|
|
325
|
+
{active?.name ?? 'Spaces'}
|
|
326
|
+
</Text>
|
|
327
|
+
{renderIcon ? renderIcon('chevron-down', 14, colors.textTertiary) : null}
|
|
328
|
+
</Pressable>
|
|
329
|
+
|
|
330
|
+
{renderContainer({ isOpen: open, onClose: close, anchorRef, children: dropdownContent })}
|
|
331
|
+
</>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
interface SectionLabelProps {
|
|
338
|
+
label: string;
|
|
339
|
+
color: string;
|
|
340
|
+
size: number;
|
|
341
|
+
lineHeight: number;
|
|
342
|
+
font: string | undefined;
|
|
343
|
+
paddingH: number;
|
|
344
|
+
paddingV: number;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function SectionLabel({ label, color, size, lineHeight, font, paddingH, paddingV }: SectionLabelProps) {
|
|
348
|
+
return (
|
|
349
|
+
<Text
|
|
350
|
+
style={
|
|
351
|
+
{
|
|
352
|
+
fontSize: size,
|
|
353
|
+
lineHeight,
|
|
354
|
+
fontWeight: '600',
|
|
355
|
+
color,
|
|
356
|
+
fontFamily: font,
|
|
357
|
+
textTransform: 'uppercase',
|
|
358
|
+
letterSpacing: 0.5,
|
|
359
|
+
paddingHorizontal: paddingH,
|
|
360
|
+
paddingVertical: paddingV,
|
|
361
|
+
} as TextStyle
|
|
362
|
+
}
|
|
363
|
+
>
|
|
364
|
+
{label}
|
|
365
|
+
</Text>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
interface SpaceRowProps {
|
|
370
|
+
space: SwitcherSpace;
|
|
371
|
+
active: boolean;
|
|
372
|
+
onPress: () => void;
|
|
373
|
+
renderAvatar?: (space: SwitcherSpace, size: number) => React.ReactNode;
|
|
374
|
+
renderIcon?: (name: SwitcherIconName, size: number, color: string) => React.ReactNode;
|
|
375
|
+
colors: ReturnType<typeof useOctoSpacesTheme>['colors'];
|
|
376
|
+
bodyFont: string | undefined;
|
|
377
|
+
bodySize: number;
|
|
378
|
+
bodyLine: number;
|
|
379
|
+
sp2: number;
|
|
380
|
+
sp3: number;
|
|
381
|
+
sp4: number;
|
|
382
|
+
radMd: number;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function SpaceRow({
|
|
386
|
+
space,
|
|
387
|
+
active,
|
|
388
|
+
onPress,
|
|
389
|
+
renderAvatar,
|
|
390
|
+
renderIcon,
|
|
391
|
+
colors,
|
|
392
|
+
bodyFont,
|
|
393
|
+
bodySize,
|
|
394
|
+
bodyLine,
|
|
395
|
+
sp2,
|
|
396
|
+
sp3,
|
|
397
|
+
sp4,
|
|
398
|
+
radMd,
|
|
399
|
+
}: SpaceRowProps) {
|
|
400
|
+
const [hovered, setHovered] = useState(false);
|
|
401
|
+
|
|
402
|
+
const bg = hovered
|
|
403
|
+
? (colors.primarySubtle ?? 'rgba(0,0,0,0.04)')
|
|
404
|
+
: 'transparent';
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<Pressable
|
|
408
|
+
accessibilityRole="menuitem"
|
|
409
|
+
accessibilityLabel={active ? `${space.name} (current)` : `Switch to ${space.name}`}
|
|
410
|
+
accessibilityState={{ selected: active }}
|
|
411
|
+
onPress={onPress}
|
|
412
|
+
onMouseEnter={() => setHovered(true)}
|
|
413
|
+
onMouseLeave={() => setHovered(false)}
|
|
414
|
+
style={{
|
|
415
|
+
flexDirection: 'row',
|
|
416
|
+
alignItems: 'center',
|
|
417
|
+
gap: sp3,
|
|
418
|
+
paddingHorizontal: sp4,
|
|
419
|
+
paddingVertical: sp2,
|
|
420
|
+
borderRadius: radMd,
|
|
421
|
+
backgroundColor: bg,
|
|
422
|
+
}}
|
|
423
|
+
>
|
|
424
|
+
{renderAvatar ? renderAvatar(space, 24) : null}
|
|
425
|
+
<Text
|
|
426
|
+
numberOfLines={1}
|
|
427
|
+
style={
|
|
428
|
+
{
|
|
429
|
+
flex: 1,
|
|
430
|
+
minWidth: 0,
|
|
431
|
+
fontSize: bodySize,
|
|
432
|
+
lineHeight: bodyLine,
|
|
433
|
+
fontWeight: active ? '600' : '400',
|
|
434
|
+
color: active ? colors.primary : colors.text,
|
|
435
|
+
fontFamily: bodyFont,
|
|
436
|
+
} as TextStyle
|
|
437
|
+
}
|
|
438
|
+
>
|
|
439
|
+
{space.name}
|
|
440
|
+
</Text>
|
|
441
|
+
{active && renderIcon ? renderIcon('check', 15, colors.primary) : null}
|
|
442
|
+
</Pressable>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
interface ActionRowProps {
|
|
447
|
+
label: string;
|
|
448
|
+
iconName: SwitcherIconName;
|
|
449
|
+
onPress: () => void;
|
|
450
|
+
renderIcon?: (name: SwitcherIconName, size: number, color: string) => React.ReactNode;
|
|
451
|
+
colors: ReturnType<typeof useOctoSpacesTheme>['colors'];
|
|
452
|
+
bodyFont: string | undefined;
|
|
453
|
+
bodySize: number;
|
|
454
|
+
bodyLine: number;
|
|
455
|
+
sp2: number;
|
|
456
|
+
sp3: number;
|
|
457
|
+
sp4: number;
|
|
458
|
+
radMd: number;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function ActionRow({
|
|
462
|
+
label,
|
|
463
|
+
iconName,
|
|
464
|
+
onPress,
|
|
465
|
+
renderIcon,
|
|
466
|
+
colors,
|
|
467
|
+
bodyFont,
|
|
468
|
+
bodySize,
|
|
469
|
+
bodyLine,
|
|
470
|
+
sp2,
|
|
471
|
+
sp3,
|
|
472
|
+
sp4,
|
|
473
|
+
radMd,
|
|
474
|
+
}: ActionRowProps) {
|
|
475
|
+
const [hovered, setHovered] = useState(false);
|
|
476
|
+
|
|
477
|
+
const bg = hovered
|
|
478
|
+
? (colors.primarySubtle ?? 'rgba(0,0,0,0.04)')
|
|
479
|
+
: 'transparent';
|
|
480
|
+
|
|
481
|
+
return (
|
|
482
|
+
<Pressable
|
|
483
|
+
accessibilityRole="menuitem"
|
|
484
|
+
accessibilityLabel={label}
|
|
485
|
+
onPress={onPress}
|
|
486
|
+
onMouseEnter={() => setHovered(true)}
|
|
487
|
+
onMouseLeave={() => setHovered(false)}
|
|
488
|
+
style={{
|
|
489
|
+
flexDirection: 'row',
|
|
490
|
+
alignItems: 'center',
|
|
491
|
+
gap: sp3,
|
|
492
|
+
paddingHorizontal: sp4,
|
|
493
|
+
paddingVertical: sp2,
|
|
494
|
+
borderRadius: radMd,
|
|
495
|
+
backgroundColor: bg,
|
|
496
|
+
}}
|
|
497
|
+
>
|
|
498
|
+
{renderIcon ? (
|
|
499
|
+
<View style={{ width: 24, alignItems: 'center', justifyContent: 'center' }}>
|
|
500
|
+
{renderIcon(iconName, 15, colors.textSecondary)}
|
|
501
|
+
</View>
|
|
502
|
+
) : null}
|
|
503
|
+
<Text
|
|
504
|
+
numberOfLines={1}
|
|
505
|
+
style={
|
|
506
|
+
{
|
|
507
|
+
flex: 1,
|
|
508
|
+
minWidth: 0,
|
|
509
|
+
fontSize: bodySize,
|
|
510
|
+
lineHeight: bodyLine,
|
|
511
|
+
fontWeight: '400',
|
|
512
|
+
color: colors.text,
|
|
513
|
+
fontFamily: bodyFont,
|
|
514
|
+
} as TextStyle
|
|
515
|
+
}
|
|
516
|
+
>
|
|
517
|
+
{label}
|
|
518
|
+
</Text>
|
|
519
|
+
</Pressable>
|
|
520
|
+
);
|
|
521
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type { RailSpace, RailIconName } from './types.js';
|
|
2
|
+
export type { SpacesRailProps } from './SpacesRail.js';
|
|
3
|
+
export { SpacesRail } from './SpacesRail.js';
|
|
4
|
+
export type { SidebarProps } from './Sidebar.js';
|
|
5
|
+
export { Sidebar } from './Sidebar.js';
|
|
6
|
+
export type { SidebarHeaderProps } from './SidebarHeader.js';
|
|
7
|
+
export { SidebarHeader } from './SidebarHeader.js';
|
|
8
|
+
export type { SidebarActionButtonProps } from './SidebarActionButton.js';
|
|
9
|
+
export { SidebarActionButton } from './SidebarActionButton.js';
|
|
10
|
+
export type { SidebarItemProps } from './SidebarItem.js';
|
|
11
|
+
export { SidebarItem } from './SidebarItem.js';
|
|
12
|
+
export type { SwitcherSpace, SwitcherIconName, SpaceSwitcherProps } from './SpaceSwitcher.js';
|
|
13
|
+
export { SpaceSwitcher } from './SpaceSwitcher.js';
|
package/src/theme/types.ts
CHANGED