@gogitcms/design-system 0.15.0-next.3
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/README.md +77 -0
- package/css/components.css +312 -0
- package/css/tokens.css +212 -0
- package/package.json +65 -0
- package/src/ThemeProvider.tsx +86 -0
- package/src/__tests__/ApplyChangesModal.test.tsx +148 -0
- package/src/__tests__/BranchImport.test.tsx +46 -0
- package/src/__tests__/Button.test.tsx +45 -0
- package/src/__tests__/ChangeRequestSummary.test.tsx +57 -0
- package/src/__tests__/ContentBrowser.changes.test.tsx +611 -0
- package/src/__tests__/ContentBrowser.collab.test.tsx +322 -0
- package/src/__tests__/ContentBrowser.collabsync.test.tsx +264 -0
- package/src/__tests__/ContentBrowser.contentslot.test.tsx +53 -0
- package/src/__tests__/ContentBrowser.discriminator.test.tsx +142 -0
- package/src/__tests__/ContentBrowser.drafts.test.tsx +271 -0
- package/src/__tests__/ContentBrowser.fields.test.tsx +117 -0
- package/src/__tests__/ContentBrowser.media.test.tsx +140 -0
- package/src/__tests__/ContentBrowser.mixedcollab.test.tsx +63 -0
- package/src/__tests__/ContentBrowser.mixedvalues.test.tsx +38 -0
- package/src/__tests__/ContentBrowser.pagination.test.tsx +62 -0
- package/src/__tests__/ContentBrowser.previewtab.test.tsx +212 -0
- package/src/__tests__/ContentBrowser.reorder.test.tsx +45 -0
- package/src/__tests__/ContentBrowser.search.test.tsx +135 -0
- package/src/__tests__/ContentBrowser.selectvalue.test.tsx +185 -0
- package/src/__tests__/ContentBrowser.staged.test.tsx +132 -0
- package/src/__tests__/ContentBrowser.usermenu.test.tsx +56 -0
- package/src/__tests__/MediaBrowser.test.tsx +353 -0
- package/src/__tests__/MediaField.test.tsx +185 -0
- package/src/__tests__/Notifications.test.tsx +69 -0
- package/src/__tests__/Onboarding.test.tsx +287 -0
- package/src/__tests__/cssTokens.test.ts +201 -0
- package/src/__tests__/fieldComponents.test.ts +43 -0
- package/src/__tests__/reorder.test.ts +58 -0
- package/src/components/ApplyChangesModal.tsx +348 -0
- package/src/components/BranchImport.tsx +157 -0
- package/src/components/BranchMenu.tsx +192 -0
- package/src/components/Button.tsx +131 -0
- package/src/components/ChangeDetail.tsx +472 -0
- package/src/components/ChangeRequestSummary.tsx +173 -0
- package/src/components/CollabField.tsx +388 -0
- package/src/components/ContentBrowser.tsx +5073 -0
- package/src/components/Icon.tsx +28 -0
- package/src/components/Icon.web.tsx +31 -0
- package/src/components/Input.tsx +106 -0
- package/src/components/MediaBrowser.tsx +766 -0
- package/src/components/MediaField.tsx +670 -0
- package/src/components/MediaPreview.tsx +91 -0
- package/src/components/MediaPreview.web.tsx +169 -0
- package/src/components/NavRow.tsx +105 -0
- package/src/components/Notifications.tsx +301 -0
- package/src/components/Onboarding.tsx +751 -0
- package/src/components/ProjectMenu.tsx +124 -0
- package/src/components/Segment.tsx +87 -0
- package/src/components/Skeleton.tsx +216 -0
- package/src/components/Spinner.tsx +44 -0
- package/src/components/Text.tsx +85 -0
- package/src/components/documentDrafts.ts +213 -0
- package/src/components/layout.tsx +284 -0
- package/src/components/primitives.tsx +143 -0
- package/src/components/reorder.ts +40 -0
- package/src/fieldComponents.ts +63 -0
- package/src/icons.ts +102 -0
- package/src/index.ts +198 -0
- package/src/media.ts +229 -0
- package/src/theme.ts +116 -0
- package/src/web/Button.tsx +110 -0
- package/src/web/Icon.tsx +52 -0
- package/src/web/Input.tsx +39 -0
- package/src/web/index.ts +43 -0
- package/src/web/primitives.tsx +119 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { View, Pressable } from "react-native";
|
|
3
|
+
import { useTheme } from "../ThemeProvider";
|
|
4
|
+
import { Text } from "./Text";
|
|
5
|
+
import { Icon } from "./Icon";
|
|
6
|
+
import { Input } from "./Input";
|
|
7
|
+
import { Button } from "./Button";
|
|
8
|
+
|
|
9
|
+
export type BranchRef = { id: string; name: string; protected?: boolean };
|
|
10
|
+
|
|
11
|
+
export type BranchMenuProps = {
|
|
12
|
+
/** The branch currently being edited. */
|
|
13
|
+
current: string;
|
|
14
|
+
/**
|
|
15
|
+
* Every imported branch. Omitted (or a single entry) renders the inert badge —
|
|
16
|
+
* there is nothing to switch to, so no affordance is offered.
|
|
17
|
+
*/
|
|
18
|
+
branches?: BranchRef[];
|
|
19
|
+
/** Marked in the list; it is also the base every branch's changes compare to. */
|
|
20
|
+
defaultBranch?: string;
|
|
21
|
+
onSelect?: (branch: BranchRef) => void;
|
|
22
|
+
/** Omitted → no "New branch" row. Rejecting the name should throw. */
|
|
23
|
+
onCreate?: (name: string) => Promise<void> | void;
|
|
24
|
+
/**
|
|
25
|
+
* testID prefix for the trigger and the badge, so two menus in the same bar
|
|
26
|
+
* (source and target) are addressable apart. Defaults to "branch"; a caller
|
|
27
|
+
* that renders two passes a distinct value for the second.
|
|
28
|
+
*/
|
|
29
|
+
testID?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The top bar's branch control. It is a plain badge until the host supplies
|
|
34
|
+
* branches to switch between, at which point it becomes a menu — so a
|
|
35
|
+
* deployment that only ever imports one branch shows no dead affordance.
|
|
36
|
+
*
|
|
37
|
+
* Branch names are metadata and always render mono, per the system's rule that
|
|
38
|
+
* paths, hashes and refs are set in Geist Mono.
|
|
39
|
+
*/
|
|
40
|
+
export function BranchMenu({ current, branches, defaultBranch, onSelect, onCreate, testID = "branch" }: BranchMenuProps) {
|
|
41
|
+
const t = useTheme();
|
|
42
|
+
const [open, setOpen] = useState(false);
|
|
43
|
+
const [creating, setCreating] = useState(false);
|
|
44
|
+
const [name, setName] = useState("");
|
|
45
|
+
const [busy, setBusy] = useState(false);
|
|
46
|
+
const [error, setError] = useState<string | null>(null);
|
|
47
|
+
|
|
48
|
+
const switchable = (branches?.length ?? 0) > 1 || !!onCreate;
|
|
49
|
+
const currentProtected = !!branches?.find((b) => b.name === current)?.protected;
|
|
50
|
+
|
|
51
|
+
const badge = (
|
|
52
|
+
<View
|
|
53
|
+
style={{
|
|
54
|
+
flexDirection: "row", alignItems: "center", gap: 6,
|
|
55
|
+
paddingHorizontal: t.space(2), height: t.control.sm,
|
|
56
|
+
borderRadius: t.radius.md, borderWidth: 1, borderColor: t.color.borderDefault,
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<Icon name="gitBranch" size={13} color={t.color.textSecondary} />
|
|
60
|
+
<Text variant="monoSm" color="secondary">{current}</Text>
|
|
61
|
+
{currentProtected ? (
|
|
62
|
+
<Icon name="lock" size={12} color={t.color.textTertiary} />
|
|
63
|
+
) : null}
|
|
64
|
+
{switchable ? <Icon name="chevronDown" size={13} color={t.color.textTertiary} /> : null}
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (!switchable) return <View testID={`${testID}-badge`}>{badge}</View>;
|
|
69
|
+
|
|
70
|
+
const close = () => {
|
|
71
|
+
setOpen(false);
|
|
72
|
+
setCreating(false);
|
|
73
|
+
setName("");
|
|
74
|
+
setError(null);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const submit = async () => {
|
|
78
|
+
const trimmed = name.trim();
|
|
79
|
+
if (!trimmed || !onCreate) return;
|
|
80
|
+
setBusy(true);
|
|
81
|
+
setError(null);
|
|
82
|
+
try {
|
|
83
|
+
await onCreate(trimmed);
|
|
84
|
+
close();
|
|
85
|
+
} catch (e: unknown) {
|
|
86
|
+
setError((e as { message?: string })?.message || "Couldn’t create the branch.");
|
|
87
|
+
setBusy(false);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const rowStyle = ({ pressed, hovered }: { pressed: boolean; hovered?: boolean }) => ({
|
|
92
|
+
flexDirection: "row" as const, alignItems: "center" as const, gap: t.space(2),
|
|
93
|
+
paddingHorizontal: t.space(3), paddingVertical: t.space(2),
|
|
94
|
+
backgroundColor: hovered || pressed ? t.color.surfaceHover : "transparent",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<View style={{ position: "relative" }}>
|
|
99
|
+
<Pressable
|
|
100
|
+
testID={`${testID}-menu`}
|
|
101
|
+
accessibilityRole="button"
|
|
102
|
+
accessibilityLabel={`Branch ${current}. Switch branch`}
|
|
103
|
+
onPress={() => (open ? close() : setOpen(true))}
|
|
104
|
+
style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}
|
|
105
|
+
>
|
|
106
|
+
{badge}
|
|
107
|
+
</Pressable>
|
|
108
|
+
{open ? (
|
|
109
|
+
<View
|
|
110
|
+
testID={`${testID}-menu-list`}
|
|
111
|
+
style={{
|
|
112
|
+
position: "absolute", top: t.control.sm + 4, left: 0, minWidth: 240, zIndex: 20,
|
|
113
|
+
backgroundColor: t.color.surfaceRaised, borderWidth: 1, borderColor: t.color.borderDefault,
|
|
114
|
+
borderRadius: t.radius.md, paddingVertical: t.space(1),
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
{creating ? (
|
|
118
|
+
<View style={{ padding: t.space(3), gap: t.space(2) }}>
|
|
119
|
+
<Input
|
|
120
|
+
label="New branch"
|
|
121
|
+
value={name}
|
|
122
|
+
onChangeText={setName}
|
|
123
|
+
placeholder="feat/my-change"
|
|
124
|
+
mono
|
|
125
|
+
error={!!error}
|
|
126
|
+
testID={`${testID}-create-input`}
|
|
127
|
+
/>
|
|
128
|
+
<Text variant="monoSm" color="tertiary">
|
|
129
|
+
{`Branched from ${current}`}
|
|
130
|
+
</Text>
|
|
131
|
+
{error ? <Text variant="monoSm" color={t.color.diffDelFg}>{error}</Text> : null}
|
|
132
|
+
<View style={{ flexDirection: "row", justifyContent: "flex-end", gap: t.space(2) }}>
|
|
133
|
+
<Button title="Cancel" variant="ghost" size="sm" onPress={close} testID={`${testID}-create-cancel`} />
|
|
134
|
+
<Button
|
|
135
|
+
title={busy ? "Creating…" : "Create"}
|
|
136
|
+
variant="primary"
|
|
137
|
+
size="sm"
|
|
138
|
+
disabled={busy || name.trim() === ""}
|
|
139
|
+
onPress={submit}
|
|
140
|
+
testID={`${testID}-create-submit`}
|
|
141
|
+
/>
|
|
142
|
+
</View>
|
|
143
|
+
</View>
|
|
144
|
+
) : (
|
|
145
|
+
<>
|
|
146
|
+
{(branches ?? []).map((b) => {
|
|
147
|
+
const active = b.name === current;
|
|
148
|
+
return (
|
|
149
|
+
<Pressable
|
|
150
|
+
key={b.id}
|
|
151
|
+
testID={`${testID}-option-${b.id}`}
|
|
152
|
+
onPress={() => { close(); if (!active) onSelect?.(b); }}
|
|
153
|
+
style={rowStyle}
|
|
154
|
+
>
|
|
155
|
+
<Icon
|
|
156
|
+
name={active ? "check" : "gitBranch"}
|
|
157
|
+
size={14}
|
|
158
|
+
color={active ? t.color.textPrimary : t.color.textTertiary}
|
|
159
|
+
/>
|
|
160
|
+
<Text variant="monoSm" weight={active ? "semibold" : "regular"} style={{ flex: 1 }}>
|
|
161
|
+
{b.name}
|
|
162
|
+
</Text>
|
|
163
|
+
{b.protected ? (
|
|
164
|
+
<Icon name="lock" size={12} color={t.color.textTertiary} />
|
|
165
|
+
) : null}
|
|
166
|
+
{b.name === defaultBranch ? (
|
|
167
|
+
<Text variant="monoSm" color="tertiary">default</Text>
|
|
168
|
+
) : null}
|
|
169
|
+
</Pressable>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
{onCreate ? (
|
|
173
|
+
<Pressable
|
|
174
|
+
testID={`${testID}-create`}
|
|
175
|
+
onPress={() => { setCreating(true); setError(null); }}
|
|
176
|
+
style={(state: { pressed: boolean; hovered?: boolean }) => ({
|
|
177
|
+
...rowStyle(state),
|
|
178
|
+
borderTopWidth: 1,
|
|
179
|
+
borderTopColor: t.color.borderSubtle,
|
|
180
|
+
})}
|
|
181
|
+
>
|
|
182
|
+
<Icon name="plus" size={14} color={t.color.textSecondary} />
|
|
183
|
+
<Text variant="sm">New branch</Text>
|
|
184
|
+
</Pressable>
|
|
185
|
+
) : null}
|
|
186
|
+
</>
|
|
187
|
+
)}
|
|
188
|
+
</View>
|
|
189
|
+
) : null}
|
|
190
|
+
</View>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Pressable, StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import { useTheme } from "../ThemeProvider";
|
|
4
|
+
import { Text } from "./Text";
|
|
5
|
+
import { Icon } from "./Icon";
|
|
6
|
+
import type { IconName } from "../icons";
|
|
7
|
+
|
|
8
|
+
export type ButtonVariant = "primary" | "default" | "ghost" | "danger";
|
|
9
|
+
export type ButtonSize = "sm" | "md" | "lg";
|
|
10
|
+
|
|
11
|
+
export type ButtonProps = {
|
|
12
|
+
title: string;
|
|
13
|
+
onPress: () => void;
|
|
14
|
+
variant?: ButtonVariant;
|
|
15
|
+
size?: ButtonSize;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
iconLeft?: IconName;
|
|
18
|
+
fullWidth?: boolean;
|
|
19
|
+
style?: StyleProp<ViewStyle>;
|
|
20
|
+
testID?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function Button({
|
|
24
|
+
title,
|
|
25
|
+
onPress,
|
|
26
|
+
variant = "default",
|
|
27
|
+
size = "md",
|
|
28
|
+
disabled,
|
|
29
|
+
iconLeft,
|
|
30
|
+
fullWidth,
|
|
31
|
+
style,
|
|
32
|
+
testID,
|
|
33
|
+
}: ButtonProps) {
|
|
34
|
+
const t = useTheme();
|
|
35
|
+
const height = t.control[size];
|
|
36
|
+
const padH = size === "sm" ? t.space(3) : t.space(4);
|
|
37
|
+
|
|
38
|
+
function bg(pressed: boolean) {
|
|
39
|
+
switch (variant) {
|
|
40
|
+
case "primary": return pressed ? t.ink[800] : t.color.surfaceInverted;
|
|
41
|
+
case "default": return pressed ? t.color.surfaceActive : t.color.surfaceRaised;
|
|
42
|
+
case "ghost": return pressed ? t.color.surfaceHover : "transparent";
|
|
43
|
+
case "danger": return pressed ? t.color.diffDelBg : "transparent";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const fg =
|
|
47
|
+
variant === "primary" ? t.color.textInverted
|
|
48
|
+
: variant === "danger" ? t.color.diffDelFg
|
|
49
|
+
: t.color.textPrimary;
|
|
50
|
+
const borderColor =
|
|
51
|
+
variant === "default" ? t.color.borderDefault
|
|
52
|
+
: variant === "danger" ? t.color.borderDefault
|
|
53
|
+
: "transparent";
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Pressable
|
|
57
|
+
testID={testID}
|
|
58
|
+
accessibilityRole="button"
|
|
59
|
+
accessibilityLabel={title}
|
|
60
|
+
onPress={onPress}
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
style={({ pressed }) => [
|
|
63
|
+
{
|
|
64
|
+
height,
|
|
65
|
+
paddingHorizontal: padH,
|
|
66
|
+
flexDirection: "row",
|
|
67
|
+
alignItems: "center",
|
|
68
|
+
justifyContent: "center",
|
|
69
|
+
gap: t.space(1.5),
|
|
70
|
+
borderRadius: t.radius.md,
|
|
71
|
+
borderWidth: 1,
|
|
72
|
+
borderColor,
|
|
73
|
+
backgroundColor: bg(pressed),
|
|
74
|
+
opacity: disabled ? 0.4 : 1,
|
|
75
|
+
alignSelf: fullWidth ? "stretch" : "flex-start",
|
|
76
|
+
},
|
|
77
|
+
style,
|
|
78
|
+
]}
|
|
79
|
+
>
|
|
80
|
+
{iconLeft ? <Icon name={iconLeft} size={size === "sm" ? 14 : 16} color={fg} /> : null}
|
|
81
|
+
<Text variant={size === "sm" ? "sm" : "body"} weight="medium" color={fg}>
|
|
82
|
+
{title}
|
|
83
|
+
</Text>
|
|
84
|
+
</Pressable>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type IconButtonProps = {
|
|
89
|
+
name: IconName;
|
|
90
|
+
onPress: () => void;
|
|
91
|
+
size?: ButtonSize;
|
|
92
|
+
active?: boolean;
|
|
93
|
+
label: string; // accessibility
|
|
94
|
+
disabled?: boolean;
|
|
95
|
+
testID?: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export function IconButton({
|
|
99
|
+
name,
|
|
100
|
+
onPress,
|
|
101
|
+
size = "md",
|
|
102
|
+
active,
|
|
103
|
+
label,
|
|
104
|
+
disabled,
|
|
105
|
+
testID,
|
|
106
|
+
}: IconButtonProps) {
|
|
107
|
+
const t = useTheme();
|
|
108
|
+
const box = t.control[size];
|
|
109
|
+
return (
|
|
110
|
+
<Pressable
|
|
111
|
+
testID={testID}
|
|
112
|
+
accessibilityRole="button"
|
|
113
|
+
accessibilityLabel={label}
|
|
114
|
+
onPress={onPress}
|
|
115
|
+
disabled={disabled}
|
|
116
|
+
style={({ pressed }) => ({
|
|
117
|
+
width: box,
|
|
118
|
+
height: box,
|
|
119
|
+
alignItems: "center",
|
|
120
|
+
justifyContent: "center",
|
|
121
|
+
borderRadius: t.radius.md,
|
|
122
|
+
borderWidth: 1,
|
|
123
|
+
borderColor: active ? t.color.borderDefault : "transparent",
|
|
124
|
+
backgroundColor: active || pressed ? t.color.surfaceHover : "transparent",
|
|
125
|
+
opacity: disabled ? 0.4 : 1,
|
|
126
|
+
})}
|
|
127
|
+
>
|
|
128
|
+
<Icon name={name} size={size === "sm" ? 16 : 18} color={t.color.textSecondary} />
|
|
129
|
+
</Pressable>
|
|
130
|
+
);
|
|
131
|
+
}
|