@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.
Files changed (70) hide show
  1. package/README.md +77 -0
  2. package/css/components.css +312 -0
  3. package/css/tokens.css +212 -0
  4. package/package.json +65 -0
  5. package/src/ThemeProvider.tsx +86 -0
  6. package/src/__tests__/ApplyChangesModal.test.tsx +148 -0
  7. package/src/__tests__/BranchImport.test.tsx +46 -0
  8. package/src/__tests__/Button.test.tsx +45 -0
  9. package/src/__tests__/ChangeRequestSummary.test.tsx +57 -0
  10. package/src/__tests__/ContentBrowser.changes.test.tsx +611 -0
  11. package/src/__tests__/ContentBrowser.collab.test.tsx +322 -0
  12. package/src/__tests__/ContentBrowser.collabsync.test.tsx +264 -0
  13. package/src/__tests__/ContentBrowser.contentslot.test.tsx +53 -0
  14. package/src/__tests__/ContentBrowser.discriminator.test.tsx +142 -0
  15. package/src/__tests__/ContentBrowser.drafts.test.tsx +271 -0
  16. package/src/__tests__/ContentBrowser.fields.test.tsx +117 -0
  17. package/src/__tests__/ContentBrowser.media.test.tsx +140 -0
  18. package/src/__tests__/ContentBrowser.mixedcollab.test.tsx +63 -0
  19. package/src/__tests__/ContentBrowser.mixedvalues.test.tsx +38 -0
  20. package/src/__tests__/ContentBrowser.pagination.test.tsx +62 -0
  21. package/src/__tests__/ContentBrowser.previewtab.test.tsx +212 -0
  22. package/src/__tests__/ContentBrowser.reorder.test.tsx +45 -0
  23. package/src/__tests__/ContentBrowser.search.test.tsx +135 -0
  24. package/src/__tests__/ContentBrowser.selectvalue.test.tsx +185 -0
  25. package/src/__tests__/ContentBrowser.staged.test.tsx +132 -0
  26. package/src/__tests__/ContentBrowser.usermenu.test.tsx +56 -0
  27. package/src/__tests__/MediaBrowser.test.tsx +353 -0
  28. package/src/__tests__/MediaField.test.tsx +185 -0
  29. package/src/__tests__/Notifications.test.tsx +69 -0
  30. package/src/__tests__/Onboarding.test.tsx +287 -0
  31. package/src/__tests__/cssTokens.test.ts +201 -0
  32. package/src/__tests__/fieldComponents.test.ts +43 -0
  33. package/src/__tests__/reorder.test.ts +58 -0
  34. package/src/components/ApplyChangesModal.tsx +348 -0
  35. package/src/components/BranchImport.tsx +157 -0
  36. package/src/components/BranchMenu.tsx +192 -0
  37. package/src/components/Button.tsx +131 -0
  38. package/src/components/ChangeDetail.tsx +472 -0
  39. package/src/components/ChangeRequestSummary.tsx +173 -0
  40. package/src/components/CollabField.tsx +388 -0
  41. package/src/components/ContentBrowser.tsx +5073 -0
  42. package/src/components/Icon.tsx +28 -0
  43. package/src/components/Icon.web.tsx +31 -0
  44. package/src/components/Input.tsx +106 -0
  45. package/src/components/MediaBrowser.tsx +766 -0
  46. package/src/components/MediaField.tsx +670 -0
  47. package/src/components/MediaPreview.tsx +91 -0
  48. package/src/components/MediaPreview.web.tsx +169 -0
  49. package/src/components/NavRow.tsx +105 -0
  50. package/src/components/Notifications.tsx +301 -0
  51. package/src/components/Onboarding.tsx +751 -0
  52. package/src/components/ProjectMenu.tsx +124 -0
  53. package/src/components/Segment.tsx +87 -0
  54. package/src/components/Skeleton.tsx +216 -0
  55. package/src/components/Spinner.tsx +44 -0
  56. package/src/components/Text.tsx +85 -0
  57. package/src/components/documentDrafts.ts +213 -0
  58. package/src/components/layout.tsx +284 -0
  59. package/src/components/primitives.tsx +143 -0
  60. package/src/components/reorder.ts +40 -0
  61. package/src/fieldComponents.ts +63 -0
  62. package/src/icons.ts +102 -0
  63. package/src/index.ts +198 -0
  64. package/src/media.ts +229 -0
  65. package/src/theme.ts +116 -0
  66. package/src/web/Button.tsx +110 -0
  67. package/src/web/Icon.tsx +52 -0
  68. package/src/web/Input.tsx +39 -0
  69. package/src/web/index.ts +43 -0
  70. package/src/web/primitives.tsx +119 -0
@@ -0,0 +1,86 @@
1
+ import React, { createContext, useCallback, useContext, useMemo, useState } from "react";
2
+ import { useColorScheme, useWindowDimensions } from "react-native";
3
+ import { makeTheme, lightTheme, Theme, ThemeMode, DESKTOP_BREAKPOINT } from "./theme";
4
+
5
+ type ThemeContextValue = {
6
+ theme: Theme;
7
+ mode: ThemeMode;
8
+ setMode: (mode: ThemeMode) => void;
9
+ toggleMode: () => void;
10
+ };
11
+
12
+ const ThemeContext = createContext<ThemeContextValue | null>(null);
13
+
14
+ export type ThemeProviderProps = {
15
+ children: React.ReactNode;
16
+ /** Force a mode; omit to follow the system color scheme. */
17
+ initialMode?: ThemeMode;
18
+ };
19
+
20
+ export function ThemeProvider({ children, initialMode }: ThemeProviderProps) {
21
+ const system = useColorScheme();
22
+ const [override, setOverride] = useState<ThemeMode | undefined>(initialMode);
23
+ const mode: ThemeMode = override ?? (system === "dark" ? "dark" : "light");
24
+
25
+ const theme = useMemo(() => makeTheme(mode), [mode]);
26
+ const setMode = useCallback((m: ThemeMode) => setOverride(m), []);
27
+ const toggleMode = useCallback(
28
+ () => setOverride((m) => ((m ?? mode) === "dark" ? "light" : "dark")),
29
+ [mode]
30
+ );
31
+
32
+ const value = useMemo(
33
+ () => ({ theme, mode, setMode, toggleMode }),
34
+ [theme, mode, setMode, toggleMode]
35
+ );
36
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
37
+ }
38
+
39
+ /**
40
+ * Overrides the theme palette for a subtree while keeping the app's real
41
+ * mode/setMode/toggleMode intact — so a nested control (e.g. the top bar's
42
+ * theme toggle) still drives the whole app, not just the scoped subtree.
43
+ * Used to render the dark-chrome top bar's inline contents against a dark
44
+ * palette regardless of the active app theme.
45
+ */
46
+ export function ThemeScope({ theme, children }: { theme: Theme; children: React.ReactNode }) {
47
+ const ctx = useContext(ThemeContext);
48
+ const value = useMemo<ThemeContextValue>(
49
+ () => ({
50
+ theme,
51
+ mode: ctx?.mode ?? "light",
52
+ setMode: ctx?.setMode ?? (() => {}),
53
+ toggleMode: ctx?.toggleMode ?? (() => {}),
54
+ }),
55
+ [theme, ctx]
56
+ );
57
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
58
+ }
59
+
60
+ // Both hooks fall back to the light theme when no provider is mounted, so
61
+ // components never crash in isolation (tests, Storybook-style previews).
62
+ export function useTheme(): Theme {
63
+ const ctx = useContext(ThemeContext);
64
+ return ctx ? ctx.theme : lightTheme;
65
+ }
66
+
67
+ export function useThemeMode() {
68
+ const ctx = useContext(ThemeContext);
69
+ if (ctx) return { mode: ctx.mode, setMode: ctx.setMode, toggleMode: ctx.toggleMode };
70
+ return { mode: "light" as ThemeMode, setMode: () => {}, toggleMode: () => {} };
71
+ }
72
+
73
+ /**
74
+ * Responsive layout switch. Works on native and web (react-native-web) via
75
+ * useWindowDimensions, so the same components pick desktop vs mobile layout
76
+ * from the viewport width — no CSS media queries needed.
77
+ */
78
+ export function useResponsive() {
79
+ const { width, height } = useWindowDimensions();
80
+ return {
81
+ width,
82
+ height,
83
+ isDesktop: width >= DESKTOP_BREAKPOINT,
84
+ isMobile: width < DESKTOP_BREAKPOINT,
85
+ };
86
+ }
@@ -0,0 +1,148 @@
1
+ import React from "react";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import { ThemeProvider } from "../ThemeProvider";
4
+ import { ApplyChangesModal, type MergeProgress } from "../components/ApplyChangesModal";
5
+
6
+ const wrap = (ui: React.ReactElement) => <ThemeProvider>{ui}</ThemeProvider>;
7
+
8
+ const base: MergeProgress = {
9
+ status: "running",
10
+ step: "comparing",
11
+ sourceBranch: "draft/spring",
12
+ targetBranch: "main",
13
+ };
14
+
15
+ const branches = { sourceBranch: "draft/spring", targetBranch: "main" };
16
+
17
+ test("without a run it shows a confirmation step explaining the effects", () => {
18
+ const onConfirm = jest.fn();
19
+ render(wrap(<ApplyChangesModal {...branches} summary="3 collections changed" onConfirm={onConfirm} onClose={() => {}} />));
20
+
21
+ const confirm = screen.getByTestId("apply-confirm");
22
+ // The irreversible, outward-facing effects are spelled out.
23
+ expect(confirm).toHaveTextContent("merges draft/spring into main");
24
+ expect(confirm).toHaveTextContent("pushes a merge commit to origin/main");
25
+ expect(confirm).toHaveTextContent("draft/spring is then deleted");
26
+ expect(confirm).toHaveTextContent("can’t be undone");
27
+ expect(confirm).toHaveTextContent("3 collections changed");
28
+
29
+ // The checklist is not shown until the merge actually starts.
30
+ expect(screen.queryByTestId("merge-step-active")).not.toBeInTheDocument();
31
+
32
+ fireEvent.click(screen.getByTestId("apply-confirm-submit"));
33
+ expect(onConfirm).toHaveBeenCalledTimes(1);
34
+ });
35
+
36
+ test("the checklist advances: earlier steps done, current active, later pending", () => {
37
+ render(wrap(<ApplyChangesModal {...branches} progress={{ ...base, step: "merging" }} onClose={() => {}} />));
38
+
39
+ // comparing is behind us → done; merging is current → active; pushing → pending.
40
+ expect(screen.getAllByTestId("merge-step-done")).toHaveLength(1);
41
+ expect(screen.getAllByTestId("merge-step-active")).toHaveLength(1);
42
+ expect(screen.getAllByTestId("merge-step-pending")).toHaveLength(1);
43
+ });
44
+
45
+ test("the header shows source → target", () => {
46
+ render(wrap(<ApplyChangesModal {...branches} progress={base} onClose={() => {}} />));
47
+ expect(screen.getByText("draft/spring")).toBeInTheDocument();
48
+ expect(screen.getByText("main")).toBeInTheDocument();
49
+ });
50
+
51
+ test("success shows the merge panel with commit and stats, and a Done button", () => {
52
+ const onDone = jest.fn();
53
+ render(
54
+ wrap(
55
+ <ApplyChangesModal
56
+ {...branches}
57
+ progress={{
58
+ ...base,
59
+ status: "succeeded",
60
+ step: "pushing",
61
+ commit: "b7e1a2c9deadbeef",
62
+ filesChanged: 3,
63
+ added: 64,
64
+ removed: 6,
65
+ }}
66
+ onClose={() => {}}
67
+ onDone={onDone}
68
+ />,
69
+ ),
70
+ );
71
+
72
+ const panel = screen.getByTestId("merge-success");
73
+ expect(panel).toHaveTextContent("Merged draft/spring into main");
74
+ expect(panel).toHaveTextContent("b7e1a2c"); // short sha
75
+ expect(panel).toHaveTextContent("3 files");
76
+ expect(panel).toHaveTextContent("+64 −6");
77
+
78
+ // Every step reads as done on success.
79
+ expect(screen.getAllByTestId("merge-step-done")).toHaveLength(3);
80
+ expect(screen.getByTestId("apply-done")).toBeInTheDocument();
81
+ });
82
+
83
+ test("needs_review lists the conflicting files and escalates with an explanation", () => {
84
+ const onCreateChangeRequest = jest.fn();
85
+ render(
86
+ wrap(
87
+ <ApplyChangesModal
88
+ {...branches}
89
+ progress={{ ...base, status: "needs_review", step: "merging", escalationFiles: ["src/app.go", "package.json"] }}
90
+ onCreateChangeRequest={onCreateChangeRequest}
91
+ onClose={() => {}}
92
+ />,
93
+ ),
94
+ );
95
+
96
+ const panel = screen.getByTestId("merge-needs-review");
97
+ expect(panel).toHaveTextContent("Code conflicts need a developer");
98
+ expect(screen.getByTestId("escalation-file-src/app.go")).toBeInTheDocument();
99
+ expect(screen.getByTestId("escalation-file-package.json")).toBeInTheDocument();
100
+
101
+ // The submit is disabled until an explanation is entered.
102
+ const submit = screen.getByTestId("create-change-request-submit");
103
+ fireEvent.click(submit);
104
+ expect(onCreateChangeRequest).not.toHaveBeenCalled();
105
+
106
+ fireEvent.change(screen.getByTestId("change-request-explanation"), { target: { value: "Updated pricing copy" } });
107
+ fireEvent.click(submit);
108
+ expect(onCreateChangeRequest).toHaveBeenCalledWith("Updated pricing copy");
109
+ });
110
+
111
+ test("an open change request replaces the confirm step with a shareable link and view button", () => {
112
+ const onViewChangeRequest = jest.fn();
113
+ render(
114
+ wrap(
115
+ <ApplyChangesModal
116
+ {...branches}
117
+ changeRequestOpen
118
+ changeRequestUrl="https://github.com/acme/site/pull/42"
119
+ onViewChangeRequest={onViewChangeRequest}
120
+ onClose={() => {}}
121
+ />,
122
+ ),
123
+ );
124
+ const panel = screen.getByTestId("apply-cr-open");
125
+ expect(panel).toHaveTextContent("A change request is already open");
126
+ expect(panel).toHaveTextContent("Send them this link");
127
+ // The normal apply confirmation is not offered.
128
+ expect(screen.queryByTestId("apply-confirm-submit")).not.toBeInTheDocument();
129
+ // The PR link is shown read-only with a copy button.
130
+ expect(screen.getByTestId("change-request-link")).toHaveValue("https://github.com/acme/site/pull/42");
131
+ expect(screen.getByTestId("copy-change-request-link")).toBeInTheDocument();
132
+ fireEvent.click(screen.getByTestId("apply-view-change-request"));
133
+ expect(onViewChangeRequest).toHaveBeenCalledTimes(1);
134
+ });
135
+
136
+ test("failure surfaces the error and a dismiss button", () => {
137
+ render(
138
+ wrap(
139
+ <ApplyChangesModal
140
+ {...branches}
141
+ progress={{ ...base, status: "failed", step: "pushing", error: "main moved while merging" }}
142
+ onClose={() => {}}
143
+ />,
144
+ ),
145
+ );
146
+ expect(screen.getByTestId("merge-error")).toHaveTextContent("main moved while merging");
147
+ expect(screen.getByTestId("apply-dismiss")).toBeInTheDocument();
148
+ });
@@ -0,0 +1,46 @@
1
+ import React from "react";
2
+ import { render, fireEvent, screen } from "@testing-library/react";
3
+ import { ThemeProvider } from "../ThemeProvider";
4
+ import { BranchImportingScreen, BranchImportFailedScreen } from "../components/BranchImport";
5
+
6
+ const wrap = (ui: React.ReactElement) => <ThemeProvider>{ui}</ThemeProvider>;
7
+
8
+ test("the importing screen names the branch it is waiting on", () => {
9
+ render(wrap(<BranchImportingScreen branch="release/2.0" repository="acme/site" />));
10
+ expect(screen.getByTestId("branch-importing")).toBeInTheDocument();
11
+ expect(screen.getByText("Importing release/2.0")).toBeInTheDocument();
12
+ expect(screen.getByText(/acme\/site/)).toBeInTheDocument();
13
+ });
14
+
15
+ test("a missing config reads as a setup step, not an error", () => {
16
+ render(
17
+ wrap(
18
+ <BranchImportFailedScreen
19
+ branch="main"
20
+ repository="acme/site"
21
+ error="no go-git-cms config found"
22
+ errorCode="config_not_found"
23
+ />,
24
+ ),
25
+ );
26
+ expect(screen.getByText("main has no CMS config yet")).toBeInTheDocument();
27
+ expect(screen.getByText(/go-git-cms\.yml/)).toBeInTheDocument();
28
+ // The raw message is redundant once the screen has said what to commit.
29
+ expect(screen.queryByTestId("branch-import-error")).not.toBeInTheDocument();
30
+ });
31
+
32
+ test("any other failure shows the run's own message", () => {
33
+ render(wrap(<BranchImportFailedScreen branch="main" error="clone: permission denied" />));
34
+ expect(screen.getByText("Couldn’t import main")).toBeInTheDocument();
35
+ expect(screen.getByTestId("branch-import-error")).toHaveTextContent("clone: permission denied");
36
+ });
37
+
38
+ test("check again is offered only when the host can re-read the state", () => {
39
+ const onRecheck = jest.fn();
40
+ const { rerender } = render(wrap(<BranchImportFailedScreen branch="main" errorCode="config_not_found" />));
41
+ expect(screen.queryByTestId("branch-import-recheck")).not.toBeInTheDocument();
42
+
43
+ rerender(wrap(<BranchImportFailedScreen branch="main" errorCode="config_not_found" onRecheck={onRecheck} />));
44
+ fireEvent.click(screen.getByTestId("branch-import-recheck"));
45
+ expect(onRecheck).toHaveBeenCalled();
46
+ });
@@ -0,0 +1,45 @@
1
+ import React from "react";
2
+ import { render, fireEvent, screen } from "@testing-library/react";
3
+ import { ThemeProvider } from "../ThemeProvider";
4
+ import { Button } from "../components/Button";
5
+ import { Badge } from "../components/primitives";
6
+ import { NavRow } from "../components/NavRow";
7
+ import { Icon } from "../components/Icon";
8
+
9
+ const wrap = (ui: React.ReactElement) => <ThemeProvider>{ui}</ThemeProvider>;
10
+
11
+ test("Button fires onPress", () => {
12
+ const onPress = jest.fn();
13
+ render(wrap(<Button title="Commit" onPress={onPress} testID="commit" />));
14
+ fireEvent.click(screen.getByTestId("commit"));
15
+ expect(onPress).toHaveBeenCalledTimes(1);
16
+ });
17
+
18
+ test("Button does not fire when disabled", () => {
19
+ const onPress = jest.fn();
20
+ render(wrap(<Button title="Commit" onPress={onPress} disabled testID="commit" />));
21
+ fireEvent.click(screen.getByTestId("commit"));
22
+ expect(onPress).not.toHaveBeenCalled();
23
+ });
24
+
25
+ test("Badge renders its label", () => {
26
+ render(wrap(<Badge label="M" tone="strong" testID="status" />));
27
+ expect(screen.getByTestId("status")).toHaveTextContent("M");
28
+ });
29
+
30
+ test("NavRow shows label + count and fires onPress", () => {
31
+ const onPress = jest.fn();
32
+ render(wrap(<NavRow label="Blog posts" icon="newspaper" count={24} onPress={onPress} testID="row" />));
33
+ const row = screen.getByTestId("row");
34
+ expect(row).toHaveTextContent("Blog posts");
35
+ expect(row).toHaveTextContent("24");
36
+ fireEvent.click(row);
37
+ expect(onPress).toHaveBeenCalledTimes(1);
38
+ });
39
+
40
+ test("Icon (web) renders an svg with paths", () => {
41
+ const { container } = render(wrap(<Icon name="gitBranch" />));
42
+ const svg = container.querySelector("svg");
43
+ expect(svg).toBeTruthy();
44
+ expect(svg!.querySelectorAll("path").length).toBeGreaterThan(0);
45
+ });
@@ -0,0 +1,57 @@
1
+ import React from "react";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import { ThemeProvider } from "../ThemeProvider";
4
+ import { ChangeRequestSummary, type ChangeRequestSummaryData } from "../components/ChangeRequestSummary";
5
+
6
+ const wrap = (ui: React.ReactElement) => <ThemeProvider>{ui}</ThemeProvider>;
7
+
8
+ const data: ChangeRequestSummaryData = {
9
+ id: "cr-1",
10
+ status: "open",
11
+ sourceBranch: "draft/spring",
12
+ targetBranch: "main",
13
+ prNumber: 42,
14
+ prUrl: "https://github.com/acme/site/pull/42",
15
+ explanation: "Updated the pricing table and checkout copy",
16
+ conflictFiles: ["src/app.go", "package.json"],
17
+ mergeBranch: "cms-merge/abc123",
18
+ description: "Opened by the CMS because a content merge conflicts in code files.",
19
+ comments: [{ author: "dev", body: "On it — resolving now.", createdAt: "2026-07-23T10:00:00Z" }],
20
+ };
21
+
22
+ test("renders the description, conflicting files, dev instructions and comments", () => {
23
+ render(wrap(<ChangeRequestSummary data={data} cloneUrl="https://github.com/acme/site.git" />));
24
+
25
+ // Status + PR number.
26
+ expect(screen.getByTestId("cr-status")).toHaveTextContent("open");
27
+ expect(screen.getByText("#42")).toBeInTheDocument();
28
+
29
+ // Explanation + live description.
30
+ expect(screen.getByText("Updated the pricing table and checkout copy")).toBeInTheDocument();
31
+ expect(screen.getByText(/content merge conflicts in code files/)).toBeInTheDocument();
32
+
33
+ // Conflicting files.
34
+ expect(screen.getByTestId("cr-file-src/app.go")).toBeInTheDocument();
35
+ expect(screen.getByTestId("cr-file-package.json")).toBeInTheDocument();
36
+
37
+ // Developer instructions reference the merge branch to check out.
38
+ expect(screen.getByText("git fetch origin main cms-merge/abc123")).toBeInTheDocument();
39
+ expect(screen.getByText("git checkout cms-merge/abc123")).toBeInTheDocument();
40
+
41
+ // Comments.
42
+ expect(screen.getByText("dev")).toBeInTheDocument();
43
+ expect(screen.getByText("On it — resolving now.")).toBeInTheDocument();
44
+ });
45
+
46
+ test("back button fires onBack", () => {
47
+ const onBack = jest.fn();
48
+ render(wrap(<ChangeRequestSummary data={data} onBack={onBack} />));
49
+ fireEvent.click(screen.getByTestId("cr-back"));
50
+ expect(onBack).toHaveBeenCalledTimes(1);
51
+ });
52
+
53
+ test("a completed request hides the resolve-it instructions", () => {
54
+ render(wrap(<ChangeRequestSummary data={{ ...data, status: "completed" }} />));
55
+ expect(screen.getByTestId("cr-status")).toHaveTextContent("completed");
56
+ expect(screen.queryByText(/git fetch origin/)).not.toBeInTheDocument();
57
+ });