@gogitcms/design-system 0.16.0-next.1 → 0.16.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gogitcms/design-system",
|
|
3
|
-
"version": "0.16.0-next.
|
|
3
|
+
"version": "0.16.0-next.3",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"types": "src/index.ts",
|
|
6
6
|
"// exports": "The root entry is the react-native source the SPAs, desktop app and mobile app consume. ./web is the plain-DOM build for server-rendered surfaces (the Astro docs site) that don't run react-native-web, and ./css ships the tokens as custom properties. The trailing wildcard keeps deep paths resolvable — plugin bundling and the Vite aliases reach into src/ directly.",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { render, screen, fireEvent } from "@testing-library/react";
|
|
2
|
+
import { render, screen, fireEvent, within } from "@testing-library/react";
|
|
3
3
|
import { ThemeProvider } from "../ThemeProvider";
|
|
4
4
|
import { ContentBrowser, type CmsEntry, type CmsNavSection } from "../components/ContentBrowser";
|
|
5
5
|
import { FORMS_NAV_KEY, formsNavKey, type FormInfo, type FormsApi } from "../forms";
|
|
@@ -109,3 +109,49 @@ test("an open form takes the whole content area", async () => {
|
|
|
109
109
|
expect(screen.queryByTestId("forms-empty")).not.toBeInTheDocument();
|
|
110
110
|
expect(screen.queryByTestId("resize-forms")).not.toBeInTheDocument();
|
|
111
111
|
});
|
|
112
|
+
|
|
113
|
+
// Media, Forms and Changes head their content pane with PaneTitle; a collection
|
|
114
|
+
// heads the same pane with its own header. They have to be the same heading, or
|
|
115
|
+
// the title moves and changes size as you switch between them.
|
|
116
|
+
test("the forms heading matches a collection's, not a smaller indented one", () => {
|
|
117
|
+
const { unmount } = render(
|
|
118
|
+
wrap(
|
|
119
|
+
<ContentBrowser
|
|
120
|
+
workspace={{ name: "acme/site", initials: "AC", branch: "main", changed: 0 }}
|
|
121
|
+
sections={sections}
|
|
122
|
+
activeNavKey={FORMS_NAV_KEY}
|
|
123
|
+
onSelectNav={() => {}}
|
|
124
|
+
entries={entries}
|
|
125
|
+
userInitials="ED"
|
|
126
|
+
forms={makeApi()}
|
|
127
|
+
/>,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
// Scoped to the pane: "Forms" is also the nav item that got us here.
|
|
131
|
+
const formsHeading = within(screen.getByTestId("pane-forms")).getByText("Forms");
|
|
132
|
+
// The heading sits directly in Pane's header row — the row with the pane's
|
|
133
|
+
// height and the rule under it. A padded wrapper of its own in between is
|
|
134
|
+
// what used to push this title a step right of every collection's.
|
|
135
|
+
expect(formsHeading.parentElement).toHaveStyle({ height: "48px", paddingLeft: "16px" });
|
|
136
|
+
const formsStyle = window.getComputedStyle(formsHeading);
|
|
137
|
+
unmount();
|
|
138
|
+
|
|
139
|
+
render(
|
|
140
|
+
wrap(
|
|
141
|
+
<ContentBrowser
|
|
142
|
+
workspace={{ name: "acme/site", initials: "AC", branch: "main", changed: 0 }}
|
|
143
|
+
sections={sections}
|
|
144
|
+
activeNavKey="posts"
|
|
145
|
+
onSelectNav={() => {}}
|
|
146
|
+
entries={entries}
|
|
147
|
+
userInitials="ED"
|
|
148
|
+
/>,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
const collectionHeading = within(screen.getByTestId("pane-entries")).getByText("Posts");
|
|
152
|
+
const collectionStyle = window.getComputedStyle(collectionHeading);
|
|
153
|
+
|
|
154
|
+
expect(formsStyle.fontSize).toBe(collectionStyle.fontSize);
|
|
155
|
+
expect(formsStyle.fontWeight).toBe(collectionStyle.fontWeight);
|
|
156
|
+
expect(formsStyle.flexGrow).toBe(collectionStyle.flexGrow);
|
|
157
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { ThemeProvider } from "../ThemeProvider";
|
|
4
|
+
import { ProtectedBranchModal } from "../components/ProtectedBranchModal";
|
|
5
|
+
|
|
6
|
+
const wrap = (ui: React.ReactElement) => <ThemeProvider>{ui}</ThemeProvider>;
|
|
7
|
+
|
|
8
|
+
const base = { branch: "main", onCreate: () => {}, onCancel: () => {} };
|
|
9
|
+
|
|
10
|
+
test("it names the branch, the document, and offers the one remedy", () => {
|
|
11
|
+
render(wrap(<ProtectedBranchModal {...base} documentLabel="Hello world" />));
|
|
12
|
+
|
|
13
|
+
const modal = screen.getByTestId("protected-branch-modal");
|
|
14
|
+
expect(modal).toHaveTextContent("This branch is protected");
|
|
15
|
+
expect(modal).toHaveTextContent("main");
|
|
16
|
+
expect(modal).toHaveTextContent("Hello world");
|
|
17
|
+
expect(modal).toHaveTextContent("Branched from main");
|
|
18
|
+
expect(screen.getByTestId("protected-branch-submit")).toHaveTextContent("Create branch and save");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("the suggested name prefills the field and submits", () => {
|
|
22
|
+
const onCreate = jest.fn();
|
|
23
|
+
render(wrap(<ProtectedBranchModal {...base} suggestedName="edit/hello" onCreate={onCreate} />));
|
|
24
|
+
|
|
25
|
+
expect(screen.getByTestId("protected-branch-name")).toHaveValue("edit/hello");
|
|
26
|
+
fireEvent.click(screen.getByTestId("protected-branch-submit"));
|
|
27
|
+
expect(onCreate).toHaveBeenCalledWith("edit/hello");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// The author's own name wins: a suggestion arriving late (the host resolves the
|
|
31
|
+
// document a render after the modal opens) must not overwrite what they typed.
|
|
32
|
+
test("a late suggestion does not overwrite a typed name", () => {
|
|
33
|
+
const { rerender } = render(wrap(<ProtectedBranchModal {...base} />));
|
|
34
|
+
fireEvent.change(screen.getByTestId("protected-branch-name"), { target: { value: "mine" } });
|
|
35
|
+
rerender(wrap(<ProtectedBranchModal {...base} suggestedName="edit/hello" />));
|
|
36
|
+
expect(screen.getByTestId("protected-branch-name")).toHaveValue("mine");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("an empty name cannot be submitted", () => {
|
|
40
|
+
const onCreate = jest.fn();
|
|
41
|
+
render(wrap(<ProtectedBranchModal {...base} onCreate={onCreate} />));
|
|
42
|
+
fireEvent.click(screen.getByTestId("protected-branch-submit"));
|
|
43
|
+
expect(onCreate).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// While the branch is being created there is no way out but forward: cancelling
|
|
47
|
+
// mid-flight would abandon a save whose branch may already exist.
|
|
48
|
+
test("busy disables both actions and hides the close control", () => {
|
|
49
|
+
const onCreate = jest.fn();
|
|
50
|
+
render(wrap(<ProtectedBranchModal {...base} suggestedName="edit/hello" busy onCreate={onCreate} />));
|
|
51
|
+
|
|
52
|
+
expect(screen.getByTestId("protected-branch-submit")).toHaveTextContent("Creating…");
|
|
53
|
+
expect(screen.queryByTestId("protected-branch-close")).not.toBeInTheDocument();
|
|
54
|
+
fireEvent.click(screen.getByTestId("protected-branch-submit"));
|
|
55
|
+
expect(onCreate).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("the server's message is shown so a rejected name can be corrected", () => {
|
|
59
|
+
render(wrap(<ProtectedBranchModal {...base} error={`branch "edit/hello" already exists`} />));
|
|
60
|
+
expect(screen.getByTestId("protected-branch-error")).toHaveTextContent("already exists");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("cancel dismisses", () => {
|
|
64
|
+
const onCancel = jest.fn();
|
|
65
|
+
render(wrap(<ProtectedBranchModal {...base} onCancel={onCancel} />));
|
|
66
|
+
fireEvent.click(screen.getByTestId("protected-branch-cancel"));
|
|
67
|
+
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
68
|
+
});
|
|
@@ -15,6 +15,7 @@ import { BranchMenu, type BranchRef } from "./BranchMenu";
|
|
|
15
15
|
import { ProjectMenu, type ProjectRef } from "./ProjectMenu";
|
|
16
16
|
import { ChangeDetail, type DocumentChange, type FieldConflict, type ConflictChoice } from "./ChangeDetail";
|
|
17
17
|
import { ApplyChangesModal, type MergeProgress } from "./ApplyChangesModal";
|
|
18
|
+
import { ProtectedBranchModal } from "./ProtectedBranchModal";
|
|
18
19
|
import { NotificationBell, type NotificationItem } from "./Notifications";
|
|
19
20
|
import {
|
|
20
21
|
CollabInput,
|
|
@@ -478,6 +479,19 @@ export type ContentBrowserProps = {
|
|
|
478
479
|
changeRequestOpen?: boolean;
|
|
479
480
|
changeRequestUrl?: string;
|
|
480
481
|
creatingChangeRequest?: boolean;
|
|
482
|
+
|
|
483
|
+
// Protected-branch save prompt. The server refuses a save to a branch the
|
|
484
|
+
// provider protects; the host catches that, sets protectedBranch to the branch
|
|
485
|
+
// name, and this modal offers the one thing that resolves it — a branch to put
|
|
486
|
+
// the edit on. onCreateBranchAndSave receives the name; the host creates the
|
|
487
|
+
// branch and applies the pending edit to it in one server call.
|
|
488
|
+
protectedBranch?: string;
|
|
489
|
+
protectedBranchDocument?: string;
|
|
490
|
+
protectedBranchSuggestedName?: string;
|
|
491
|
+
creatingProtectedBranch?: boolean;
|
|
492
|
+
protectedBranchError?: string | null;
|
|
493
|
+
onCreateBranchAndSave?: (name: string) => void;
|
|
494
|
+
onCancelProtectedSave?: () => void;
|
|
481
495
|
onCreateChangeRequest?: (explanation: string) => void;
|
|
482
496
|
onViewChangeRequest?: () => void;
|
|
483
497
|
|
|
@@ -3567,9 +3581,28 @@ function ReadOnlyBanner({ notice, actionLabel, onAction }: { notice?: string; ac
|
|
|
3567
3581
|
);
|
|
3568
3582
|
}
|
|
3569
3583
|
|
|
3570
|
-
//
|
|
3571
|
-
//
|
|
3572
|
-
//
|
|
3584
|
+
// ModalOverlays renders the browser's full-screen modals — apply changes /
|
|
3585
|
+
// change request, and the protected-branch save prompt. Shared by both layouts
|
|
3586
|
+
// so every modal is reachable on mobile too.
|
|
3587
|
+
function ModalOverlays(props: ContentBrowserProps) {
|
|
3588
|
+
return (
|
|
3589
|
+
<>
|
|
3590
|
+
<ApplyModalOverlay {...props} />
|
|
3591
|
+
{props.protectedBranch ? (
|
|
3592
|
+
<ProtectedBranchModal
|
|
3593
|
+
branch={props.protectedBranch}
|
|
3594
|
+
documentLabel={props.protectedBranchDocument}
|
|
3595
|
+
suggestedName={props.protectedBranchSuggestedName}
|
|
3596
|
+
busy={props.creatingProtectedBranch}
|
|
3597
|
+
error={props.protectedBranchError}
|
|
3598
|
+
onCreate={props.onCreateBranchAndSave ?? (() => {})}
|
|
3599
|
+
onCancel={props.onCancelProtectedSave ?? (() => {})}
|
|
3600
|
+
/>
|
|
3601
|
+
) : null}
|
|
3602
|
+
</>
|
|
3603
|
+
);
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3573
3606
|
function ApplyModalOverlay(props: ContentBrowserProps) {
|
|
3574
3607
|
if (!props.applyOpen) return null;
|
|
3575
3608
|
return (
|
|
@@ -4066,7 +4099,7 @@ function DesktopBrowser(props: ContentBrowserProps) {
|
|
|
4066
4099
|
const readOnlyBanner = (
|
|
4067
4100
|
<ReadOnlyBanner notice={props.readOnlyNotice} actionLabel={props.readOnlyNoticeActionLabel} onAction={props.onReadOnlyNoticeAction} />
|
|
4068
4101
|
);
|
|
4069
|
-
const
|
|
4102
|
+
const modalOverlays = <ModalOverlays {...props} />;
|
|
4070
4103
|
|
|
4071
4104
|
// The changes surface reuses the same three-pane model as Edit, so switching
|
|
4072
4105
|
// between them doesn't relayout the screen — only what each pane contains
|
|
@@ -4119,7 +4152,7 @@ function DesktopBrowser(props: ContentBrowserProps) {
|
|
|
4119
4152
|
conflicts={props.selectedConflicts}
|
|
4120
4153
|
onResolveConflict={props.onResolveConflict}
|
|
4121
4154
|
/>
|
|
4122
|
-
{
|
|
4155
|
+
{modalOverlays}
|
|
4123
4156
|
</AppShell>
|
|
4124
4157
|
);
|
|
4125
4158
|
}
|
|
@@ -4134,7 +4167,7 @@ function DesktopBrowser(props: ContentBrowserProps) {
|
|
|
4134
4167
|
<Pane flex={1} testID="pane-plugin" scroll={false}>
|
|
4135
4168
|
{props.contentSlot}
|
|
4136
4169
|
</Pane>
|
|
4137
|
-
{
|
|
4170
|
+
{modalOverlays}
|
|
4138
4171
|
</AppShell>
|
|
4139
4172
|
);
|
|
4140
4173
|
}
|
|
@@ -4174,7 +4207,7 @@ function DesktopBrowser(props: ContentBrowserProps) {
|
|
|
4174
4207
|
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", padding: t.space(6) }}>
|
|
4175
4208
|
<Text variant="body" color="tertiary" testID="forms-empty">Select a form</Text>
|
|
4176
4209
|
</View>
|
|
4177
|
-
{
|
|
4210
|
+
{modalOverlays}
|
|
4178
4211
|
</AppShell>
|
|
4179
4212
|
);
|
|
4180
4213
|
}
|
|
@@ -4192,7 +4225,7 @@ function DesktopBrowser(props: ContentBrowserProps) {
|
|
|
4192
4225
|
variant="desktop"
|
|
4193
4226
|
/>
|
|
4194
4227
|
</Pane>
|
|
4195
|
-
{
|
|
4228
|
+
{modalOverlays}
|
|
4196
4229
|
</AppShell>
|
|
4197
4230
|
);
|
|
4198
4231
|
}
|
|
@@ -4219,7 +4252,7 @@ function DesktopBrowser(props: ContentBrowserProps) {
|
|
|
4219
4252
|
<Text variant="body" color="tertiary" testID="media-sets-empty">Select a media collection</Text>
|
|
4220
4253
|
</View>
|
|
4221
4254
|
)}
|
|
4222
|
-
{
|
|
4255
|
+
{modalOverlays}
|
|
4223
4256
|
</AppShell>
|
|
4224
4257
|
);
|
|
4225
4258
|
}
|
|
@@ -4420,7 +4453,7 @@ function DesktopBrowser(props: ContentBrowserProps) {
|
|
|
4420
4453
|
onClose={() => setMovePrompt(false)}
|
|
4421
4454
|
/>
|
|
4422
4455
|
) : null}
|
|
4423
|
-
{
|
|
4456
|
+
{modalOverlays}
|
|
4424
4457
|
</AppShell>
|
|
4425
4458
|
);
|
|
4426
4459
|
}
|
|
@@ -4496,7 +4529,7 @@ function MobileBrowser(props: ContentBrowserProps) {
|
|
|
4496
4529
|
const readOnlyBanner = (
|
|
4497
4530
|
<ReadOnlyBanner notice={props.readOnlyNotice} actionLabel={props.readOnlyNoticeActionLabel} onAction={props.onReadOnlyNoticeAction} />
|
|
4498
4531
|
);
|
|
4499
|
-
const
|
|
4532
|
+
const modalOverlays = <ModalOverlays {...props} />;
|
|
4500
4533
|
|
|
4501
4534
|
// Media drills the same way a collection does — nav → list → detail — so the
|
|
4502
4535
|
// back arrow means the same thing at every level. Pressing Media lists the
|
|
@@ -4622,7 +4655,7 @@ function MobileBrowser(props: ContentBrowserProps) {
|
|
|
4622
4655
|
testID="mobile-shell"
|
|
4623
4656
|
header={header}
|
|
4624
4657
|
title={props.surface === "changes" ? "Changes" : "Content"}
|
|
4625
|
-
overlay={
|
|
4658
|
+
overlay={modalOverlays}
|
|
4626
4659
|
>
|
|
4627
4660
|
{/* Edit / Changes surface toggle (fits its content) with the Apply button
|
|
4628
4661
|
to its right on the changes surface. The read-only notice sits below. */}
|
|
@@ -4703,7 +4736,7 @@ function MobileBrowser(props: ContentBrowserProps) {
|
|
|
4703
4736
|
testID="mobile-entries"
|
|
4704
4737
|
scroll={false}
|
|
4705
4738
|
banner={readOnlyBanner}
|
|
4706
|
-
overlay={
|
|
4739
|
+
overlay={modalOverlays}
|
|
4707
4740
|
header={
|
|
4708
4741
|
search.open ? (
|
|
4709
4742
|
<SearchHeaderBar search={search} showFilter={facetFields.length > 0} />
|
|
@@ -4784,7 +4817,7 @@ function MobileBrowser(props: ContentBrowserProps) {
|
|
|
4784
4817
|
<MobileScreen
|
|
4785
4818
|
testID="mobile-entry"
|
|
4786
4819
|
banner={readOnlyBanner}
|
|
4787
|
-
overlay={
|
|
4820
|
+
overlay={modalOverlays}
|
|
4788
4821
|
header={
|
|
4789
4822
|
<>
|
|
4790
4823
|
<IconButton name="chevronLeft" onPress={backToEntries} size="md" label="Back" />
|
|
@@ -5082,14 +5115,20 @@ function mediaSetLabel(sets: MediaSetInfo[], name: string): string {
|
|
|
5082
5115
|
return set ? titleCaseWord(set.name) : titleCaseWord(name);
|
|
5083
5116
|
}
|
|
5084
5117
|
|
|
5085
|
-
// PaneTitle is the content pane's heading
|
|
5086
|
-
//
|
|
5118
|
+
// PaneTitle is the content pane's heading for the surfaces with no controls
|
|
5119
|
+
// beside it — Changes, Media, Forms.
|
|
5120
|
+
//
|
|
5121
|
+
// Deliberately the same Text the collection header uses (`listHeader`), and
|
|
5122
|
+
// nothing else. Pane's header row already supplies the height, the horizontal
|
|
5123
|
+
// padding and the rule beneath, so the padded wrapper this used to add sat
|
|
5124
|
+
// inside that padding and pushed the title a step right of every collection's —
|
|
5125
|
+
// at `body` rather than `h3`, so it read a size smaller too. Switching between
|
|
5126
|
+
// Posts and Media moved the heading twice over.
|
|
5087
5127
|
function PaneTitle({ title }: { title: string }) {
|
|
5088
|
-
const t = useTheme();
|
|
5089
5128
|
return (
|
|
5090
|
-
<
|
|
5091
|
-
|
|
5092
|
-
</
|
|
5129
|
+
<Text variant="h3" weight="semibold" style={{ flex: 1 }}>
|
|
5130
|
+
{title}
|
|
5131
|
+
</Text>
|
|
5093
5132
|
);
|
|
5094
5133
|
}
|
|
5095
5134
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React, { useEffect, 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 ProtectedBranchModalProps = {
|
|
10
|
+
/** The protected branch the save was refused on. */
|
|
11
|
+
branch: string;
|
|
12
|
+
/** The document that could not be saved, for naming what is at stake. */
|
|
13
|
+
documentLabel?: string;
|
|
14
|
+
/** Prefills the field — the host derives it from the document. */
|
|
15
|
+
suggestedName?: string;
|
|
16
|
+
/** True while the branch is being created and the save applied. */
|
|
17
|
+
busy?: boolean;
|
|
18
|
+
/** The server's message, when the attempt failed. */
|
|
19
|
+
error?: string | null;
|
|
20
|
+
/** Create `name` from `branch` and save the pending edit onto it. */
|
|
21
|
+
onCreate: (name: string) => void;
|
|
22
|
+
/** Dismiss. The edit stays as an unsaved draft on the protected branch. */
|
|
23
|
+
onCancel: () => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Shown when a save is refused because the branch is protected.
|
|
28
|
+
*
|
|
29
|
+
* It is a prompt rather than an error banner because the refusal has exactly one
|
|
30
|
+
* remedy and the editor knows what it is: the work is intact, it just needs
|
|
31
|
+
* somewhere it can land. Presenting that as "here is a message, now go find the
|
|
32
|
+
* branch menu" would make the author reconstruct a plan the product already has.
|
|
33
|
+
*
|
|
34
|
+
* The draft is deliberately NOT discarded on cancel — a protected branch is
|
|
35
|
+
* often noticed mid-thought, and the author may want to keep writing and pick a
|
|
36
|
+
* branch name later. It stays in storage exactly as any other unsaved work does.
|
|
37
|
+
*/
|
|
38
|
+
export function ProtectedBranchModal({
|
|
39
|
+
branch, documentLabel, suggestedName, busy, error, onCreate, onCancel,
|
|
40
|
+
}: ProtectedBranchModalProps) {
|
|
41
|
+
const t = useTheme();
|
|
42
|
+
const [name, setName] = useState(suggestedName ?? "");
|
|
43
|
+
// The suggestion is derived from the document, which the host may resolve a
|
|
44
|
+
// render after the modal opens. Adopted only while the field is untouched.
|
|
45
|
+
const [touched, setTouched] = useState(false);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!touched && suggestedName) setName(suggestedName);
|
|
48
|
+
}, [suggestedName, touched]);
|
|
49
|
+
|
|
50
|
+
const trimmed = name.trim();
|
|
51
|
+
const submit = () => {
|
|
52
|
+
if (!trimmed || busy) return;
|
|
53
|
+
onCreate(trimmed);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View
|
|
58
|
+
testID="protected-branch-modal"
|
|
59
|
+
style={{
|
|
60
|
+
position: "absolute", top: 0, left: 0, right: 0, bottom: 0, zIndex: 50,
|
|
61
|
+
alignItems: "center", justifyContent: "center", padding: t.space(4),
|
|
62
|
+
backgroundColor: "rgba(0,0,0,0.45)",
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<View
|
|
66
|
+
style={{
|
|
67
|
+
width: 460, maxWidth: "100%", gap: t.space(4), padding: t.space(5),
|
|
68
|
+
borderRadius: t.radius.lg, borderWidth: 1, borderColor: t.color.borderDefault,
|
|
69
|
+
backgroundColor: t.color.surfaceRaised,
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
|
|
73
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: t.space(2) }}>
|
|
74
|
+
<Icon name="lock" size={16} color={t.color.textSecondary} />
|
|
75
|
+
<Text variant="h3" weight="semibold">This branch is protected</Text>
|
|
76
|
+
</View>
|
|
77
|
+
{busy ? null : (
|
|
78
|
+
<Pressable
|
|
79
|
+
testID="protected-branch-close"
|
|
80
|
+
accessibilityRole="button"
|
|
81
|
+
accessibilityLabel="Cancel"
|
|
82
|
+
onPress={onCancel}
|
|
83
|
+
style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}
|
|
84
|
+
>
|
|
85
|
+
<Icon name="x" size={18} color={t.color.textSecondary} />
|
|
86
|
+
</Pressable>
|
|
87
|
+
)}
|
|
88
|
+
</View>
|
|
89
|
+
|
|
90
|
+
<Text variant="body" color="secondary">
|
|
91
|
+
{documentLabel
|
|
92
|
+
? `${branch} doesn’t accept changes directly, so “${documentLabel}” can’t be saved to it. Name a branch to save it to instead — your edit lands there.`
|
|
93
|
+
: `${branch} doesn’t accept changes directly. Name a branch to save your edit to instead.`}
|
|
94
|
+
</Text>
|
|
95
|
+
|
|
96
|
+
<Input
|
|
97
|
+
label="New branch"
|
|
98
|
+
value={name}
|
|
99
|
+
onChangeText={(v) => { setTouched(true); setName(v); }}
|
|
100
|
+
onSubmitEditing={submit}
|
|
101
|
+
placeholder="feat/my-change"
|
|
102
|
+
autoCapitalize="none"
|
|
103
|
+
autoFocus
|
|
104
|
+
mono
|
|
105
|
+
editable={!busy}
|
|
106
|
+
error={!!error}
|
|
107
|
+
testID="protected-branch-name"
|
|
108
|
+
/>
|
|
109
|
+
<Text variant="monoSm" color="tertiary">{`Branched from ${branch}`}</Text>
|
|
110
|
+
|
|
111
|
+
{error ? (
|
|
112
|
+
<Text testID="protected-branch-error" variant="monoSm" color={t.color.diffDelFg}>{error}</Text>
|
|
113
|
+
) : null}
|
|
114
|
+
|
|
115
|
+
<View style={{ flexDirection: "row", justifyContent: "flex-end", gap: t.space(2) }}>
|
|
116
|
+
<Button
|
|
117
|
+
title="Cancel"
|
|
118
|
+
variant="ghost"
|
|
119
|
+
size="md"
|
|
120
|
+
disabled={busy}
|
|
121
|
+
onPress={onCancel}
|
|
122
|
+
testID="protected-branch-cancel"
|
|
123
|
+
/>
|
|
124
|
+
<Button
|
|
125
|
+
title={busy ? "Creating…" : "Create branch and save"}
|
|
126
|
+
variant="primary"
|
|
127
|
+
size="md"
|
|
128
|
+
disabled={busy || trimmed === ""}
|
|
129
|
+
onPress={submit}
|
|
130
|
+
testID="protected-branch-submit"
|
|
131
|
+
/>
|
|
132
|
+
</View>
|
|
133
|
+
</View>
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -55,6 +55,8 @@ export type {
|
|
|
55
55
|
} from "./components/ApplyChangesModal";
|
|
56
56
|
|
|
57
57
|
// Change-request summary surface (developer escalation)
|
|
58
|
+
export { ProtectedBranchModal } from "./components/ProtectedBranchModal";
|
|
59
|
+
export type { ProtectedBranchModalProps } from "./components/ProtectedBranchModal";
|
|
58
60
|
export { ChangeRequestSummary } from "./components/ChangeRequestSummary";
|
|
59
61
|
export type {
|
|
60
62
|
ChangeRequestSummaryProps,
|