@remit/ui 0.0.67 → 0.0.69

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 (28) hide show
  1. package/package.json +1 -1
  2. package/src/components/folder-manage-actions.render.test.ts +51 -0
  3. package/src/components/folder-manage-actions.stories.tsx +94 -0
  4. package/src/components/folder-manage-actions.tsx +54 -0
  5. package/src/components/folder-manager.render.test.ts +80 -0
  6. package/src/components/folder-manager.stories.tsx +184 -0
  7. package/src/components/folder-manager.tsx +77 -0
  8. package/src/components/folder-rename-dialog.render.test.ts +51 -0
  9. package/src/components/folder-rename-dialog.stories.tsx +66 -0
  10. package/src/components/folder-rename-dialog.tsx +95 -0
  11. package/src/components/folder-row.render.test.ts +88 -0
  12. package/src/components/folder-row.stories.tsx +118 -0
  13. package/src/components/folder-row.tsx +156 -0
  14. package/src/components/folder-tree-picker.create.test.ts +155 -8
  15. package/src/components/folder-tree-picker.render.test.ts +12 -284
  16. package/src/components/folder-tree-picker.stories.tsx +20 -7
  17. package/src/components/folder-tree-picker.tsx +130 -534
  18. package/src/components/new-folder-action.render.test.ts +44 -0
  19. package/src/components/new-folder-action.stories.tsx +92 -0
  20. package/src/components/new-folder-action.tsx +59 -0
  21. package/src/components/new-folder-form.render.test.ts +64 -0
  22. package/src/components/new-folder-form.stories.tsx +74 -0
  23. package/src/components/new-folder-form.tsx +126 -0
  24. package/src/index.ts +47 -0
  25. package/src/lib/folder-tree-focus.test.ts +98 -0
  26. package/src/lib/folder-tree-focus.ts +52 -0
  27. package/src/lib/folder-tree.test.ts +281 -0
  28. package/src/lib/folder-tree.ts +201 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/ui",
3
- "version": "0.0.67",
3
+ "version": "0.0.69",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -0,0 +1,51 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { createElement } from "react";
4
+ import { renderToString } from "react-dom/server";
5
+ import {
6
+ FolderManageActions,
7
+ type FolderManageActionsProps,
8
+ } from "./folder-manage-actions.js";
9
+
10
+ const render = (props: Partial<FolderManageActionsProps>) =>
11
+ renderToString(
12
+ createElement(FolderManageActions, {
13
+ label: "Travel",
14
+ onRename: () => undefined,
15
+ onDelete: () => undefined,
16
+ ...props,
17
+ }),
18
+ );
19
+
20
+ describe("FolderManageActions", () => {
21
+ it("names both controls after the folder they act on", () => {
22
+ const html = render({});
23
+ assert.match(html, /aria-label="Rename Travel"/);
24
+ assert.match(html, /aria-label="Delete Travel"/);
25
+ });
26
+
27
+ it("leaves a deletable folder's delete pressable", () => {
28
+ assert.doesNotMatch(render({}), /disabled=""/);
29
+ });
30
+
31
+ it("states why a folder stays, in the control's own name", () => {
32
+ const html = render({
33
+ deleteBlockedReason: "The inbox can't be deleted.",
34
+ });
35
+ assert.match(
36
+ html,
37
+ /aria-label="Delete Travel — The inbox can&#x27;t be deleted."/,
38
+ );
39
+ assert.match(html, /disabled=""/);
40
+ assert.match(html, /title="The inbox can&#x27;t be deleted."/);
41
+ });
42
+
43
+ it("takes the surface's own wording for each control", () => {
44
+ const html = render({
45
+ renameLabel: (label) => `Hernoem ${label}`,
46
+ deleteLabel: (label) => `Verwijder ${label}`,
47
+ });
48
+ assert.match(html, /aria-label="Hernoem Travel"/);
49
+ assert.match(html, /aria-label="Verwijder Travel"/);
50
+ });
51
+ });
@@ -0,0 +1,94 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import type { ReactNode } from "react";
3
+ import { FolderManageActions } from "./folder-manage-actions.js";
4
+ import { FolderRow } from "./folder-row.js";
5
+
6
+ const meta: Meta<typeof FolderManageActions> = {
7
+ title: "Mail/FolderManageActions",
8
+ component: FolderManageActions,
9
+ parameters: { layout: "centered" },
10
+ };
11
+ export default meta;
12
+
13
+ type Story = StoryObj<typeof FolderManageActions>;
14
+
15
+ function List({ children }: { children: ReactNode }) {
16
+ return (
17
+ <div className="w-[360px] overflow-hidden rounded-lg border border-line bg-surface font-sans text-fg">
18
+ {children}
19
+ </div>
20
+ );
21
+ }
22
+
23
+ export const OnARow: Story = {
24
+ name: "Beside the folder it acts on",
25
+ render: () => (
26
+ <List>
27
+ <FolderRow
28
+ label="Travel"
29
+ depth={0}
30
+ expanded={false}
31
+ ariaLabel="Travel"
32
+ separated
33
+ actions={
34
+ <FolderManageActions
35
+ label="Travel"
36
+ onRename={() => {}}
37
+ onDelete={() => {}}
38
+ />
39
+ }
40
+ />
41
+ <FolderRow
42
+ label="Hotels"
43
+ depth={1}
44
+ expanded={false}
45
+ ariaLabel="Hotels"
46
+ actions={
47
+ <FolderManageActions
48
+ label="Hotels"
49
+ onRename={() => {}}
50
+ onDelete={() => {}}
51
+ />
52
+ }
53
+ />
54
+ </List>
55
+ ),
56
+ };
57
+
58
+ /** A folder the account depends on keeps rename and states why it stays. */
59
+ export const Blocked: Story = {
60
+ name: "A folder that can't be deleted",
61
+ render: () => (
62
+ <List>
63
+ <FolderRow
64
+ label="Inbox"
65
+ depth={0}
66
+ expanded={false}
67
+ ariaLabel="Inbox"
68
+ separated
69
+ actions={
70
+ <FolderManageActions
71
+ label="Inbox"
72
+ deleteBlockedReason="The inbox can't be deleted."
73
+ onRename={() => {}}
74
+ onDelete={() => {}}
75
+ />
76
+ }
77
+ />
78
+ <FolderRow
79
+ label="Trash"
80
+ depth={0}
81
+ expanded={false}
82
+ ariaLabel="Trash"
83
+ actions={
84
+ <FolderManageActions
85
+ label="Trash"
86
+ deleteBlockedReason="This folder is your Trash folder. Reassign that role before deleting it."
87
+ onRename={() => {}}
88
+ onDelete={() => {}}
89
+ />
90
+ }
91
+ />
92
+ </List>
93
+ ),
94
+ };
@@ -0,0 +1,54 @@
1
+ import { Pencil, Trash2 } from "lucide-react";
2
+ import { Button } from "./button.js";
3
+
4
+ export interface FolderManageActionsProps {
5
+ /** The folder as it reads on the row, so each control names its own target. */
6
+ label: string;
7
+ onRename: () => void;
8
+ onDelete: () => void;
9
+ /** Why this folder can't be deleted; absent means it can. */
10
+ deleteBlockedReason?: string;
11
+ renameLabel?: (label: string) => string;
12
+ deleteLabel?: (label: string) => string;
13
+ }
14
+
15
+ /**
16
+ * What a folder row offers beyond opening: rename it, or delete it. A folder
17
+ * the account depends on states why it stays — as the control's own name, so
18
+ * the reason is announced rather than only hovered.
19
+ */
20
+ export const FolderManageActions = ({
21
+ label,
22
+ onRename,
23
+ onDelete,
24
+ deleteBlockedReason,
25
+ renameLabel = (name) => `Rename ${name}`,
26
+ deleteLabel = (name) => `Delete ${name}`,
27
+ }: FolderManageActionsProps) => {
28
+ const deleteName = deleteBlockedReason
29
+ ? `${deleteLabel(label)} — ${deleteBlockedReason}`
30
+ : deleteLabel(label);
31
+ return (
32
+ <span className="flex shrink-0 items-center gap-0.5 pr-2">
33
+ <Button
34
+ variant="ghost"
35
+ size="sm"
36
+ icon={<Pencil className="size-3.5" />}
37
+ aria-label={renameLabel(label)}
38
+ onClick={onRename}
39
+ />
40
+ {/* The title sits on the wrapper: a disabled button takes no pointer
41
+ events, so a tooltip on it never shows. */}
42
+ <span title={deleteBlockedReason}>
43
+ <Button
44
+ variant="ghost"
45
+ size="sm"
46
+ icon={<Trash2 className="size-3.5" />}
47
+ aria-label={deleteName}
48
+ disabled={deleteBlockedReason !== undefined}
49
+ onClick={onDelete}
50
+ />
51
+ </span>
52
+ </span>
53
+ );
54
+ };
@@ -0,0 +1,80 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { createElement } from "react";
4
+ import { renderToString } from "react-dom/server";
5
+ import { FolderManager, type ManagedFolder } from "./folder-manager.js";
6
+
7
+ const folders: ManagedFolder[] = [
8
+ {
9
+ id: "inbox",
10
+ label: "Inbox",
11
+ path: "INBOX",
12
+ deleteBlockedReason: "The inbox can't be deleted.",
13
+ },
14
+ { id: "travel", label: "Travel", path: "Travel" },
15
+ { id: "hotels", label: "Hotels", path: "Travel/Hotels" },
16
+ { id: "trash", label: "Trash", path: "Deleted Messages" },
17
+ ];
18
+
19
+ const render = (props: Partial<Parameters<typeof FolderManager>[0]> = {}) =>
20
+ renderToString(
21
+ createElement(FolderManager, {
22
+ folders,
23
+ onRename: () => undefined,
24
+ onDelete: () => undefined,
25
+ ...props,
26
+ }),
27
+ );
28
+
29
+ describe("FolderManager", () => {
30
+ it("browses the folders as a tree that starts at its top level", () => {
31
+ const html = render();
32
+ assert.match(html, /role="tree"/);
33
+ assert.equal(html.match(/role="treeitem"/g)?.length, 3);
34
+ assert.doesNotMatch(html, /aria-label="Hotels"/);
35
+ });
36
+
37
+ it("reads a row as the folder itself, not as a destination", () => {
38
+ const html = render();
39
+ assert.match(html, /aria-label="Travel"/);
40
+ assert.doesNotMatch(html, /Move to/);
41
+ });
42
+
43
+ it("labels a folder by its role while nesting it by its real path", () => {
44
+ const html = render();
45
+ assert.match(html, /aria-label="Trash"/);
46
+ assert.doesNotMatch(html, />Deleted Messages</);
47
+ });
48
+
49
+ it("offers rename and delete on every row", () => {
50
+ const html = render();
51
+ assert.match(html, /aria-label="Rename Travel"/);
52
+ assert.match(html, /aria-label="Delete Travel"/);
53
+ });
54
+
55
+ it("carries a folder's reason for staying onto its delete control", () => {
56
+ assert.match(
57
+ render(),
58
+ /aria-label="Delete Inbox — The inbox can&#x27;t be deleted."/,
59
+ );
60
+ });
61
+
62
+ it("offers no create affordance without onCreateFolder", () => {
63
+ assert.doesNotMatch(render(), /New folder/);
64
+ });
65
+
66
+ it("makes a folder where the user is looking once creating is wired", () => {
67
+ const html = render({
68
+ onCreateFolder: () =>
69
+ Promise.resolve({ id: "made", label: "Made", path: "Made" }),
70
+ });
71
+ assert.match(html, /aria-label="New folder"/);
72
+ });
73
+
74
+ it("takes the surface's own tree name", () => {
75
+ assert.match(
76
+ render({ labels: { treeAriaLabel: "All folders for a@b.example" } }),
77
+ /aria-label="All folders for a@b.example"/,
78
+ );
79
+ });
80
+ });
@@ -0,0 +1,184 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { type ReactNode, useState } from "react";
3
+ import type { FolderTreeNode } from "../lib/folder-tree.js";
4
+ import { FolderManager, type ManagedFolder } from "./folder-manager.js";
5
+ import { FolderRenameDialog } from "./folder-rename-dialog.js";
6
+
7
+ const folders: ManagedFolder[] = [
8
+ {
9
+ id: "mbx-inbox",
10
+ label: "Inbox",
11
+ path: "INBOX",
12
+ deleteBlockedReason: "The inbox can't be deleted.",
13
+ },
14
+ {
15
+ id: "mbx-archive",
16
+ label: "Archive",
17
+ path: "Archive",
18
+ deleteBlockedReason:
19
+ "This folder is your Archive folder. Reassign that role before deleting it.",
20
+ },
21
+ {
22
+ id: "mbx-trash",
23
+ label: "Trash",
24
+ path: "Deleted Messages",
25
+ deleteBlockedReason:
26
+ "This folder is your Trash folder. Reassign that role before deleting it.",
27
+ },
28
+ {
29
+ id: "mbx-travel",
30
+ label: "Travel",
31
+ path: "Travel",
32
+ deleteBlockedReason: "This folder has subfolders. Delete them first.",
33
+ },
34
+ { id: "mbx-travel-flights", label: "Flights", path: "Travel/Flights" },
35
+ { id: "mbx-travel-hotels", label: "Hotels", path: "Travel/Hotels" },
36
+ {
37
+ id: "mbx-travel-hotels-receipts",
38
+ label: "Receipts",
39
+ path: "Travel/Hotels/Receipts",
40
+ },
41
+ {
42
+ id: "mbx-finance",
43
+ label: "Finance",
44
+ path: "Finance",
45
+ deleteBlockedReason: "This folder has subfolders. Delete them first.",
46
+ },
47
+ { id: "mbx-finance-invoices", label: "Invoices", path: "Finance/Invoices" },
48
+ { id: "mbx-finance-tax", label: "Tax", path: "Finance/Tax" },
49
+ { id: "mbx-family", label: "Family", path: "Family" },
50
+ { id: "mbx-news", label: "Newsletters", path: "Newsletters" },
51
+ { id: "mbx-news-tech", label: "Tech", path: "Newsletters/Tech" },
52
+ ];
53
+
54
+ const meta: Meta<typeof FolderManager> = {
55
+ title: "Mail/FolderManager",
56
+ component: FolderManager,
57
+ parameters: { layout: "centered" },
58
+ };
59
+ export default meta;
60
+
61
+ type Story = StoryObj<typeof FolderManager>;
62
+
63
+ function Frame({
64
+ children,
65
+ className = "h-[520px] w-[420px] rounded-lg border border-line shadow-lg",
66
+ }: {
67
+ children: ReactNode;
68
+ className?: string;
69
+ }) {
70
+ return (
71
+ <div
72
+ className={`flex flex-col overflow-hidden bg-surface font-sans text-fg ${className}`}
73
+ >
74
+ {children}
75
+ </div>
76
+ );
77
+ }
78
+
79
+ let createdSeq = 0;
80
+ const createFolder = (
81
+ name: string,
82
+ parentPath: string,
83
+ ): Promise<FolderTreeNode> =>
84
+ new Promise((resolve) => {
85
+ createdSeq += 1;
86
+ setTimeout(
87
+ () =>
88
+ resolve({
89
+ id: `mbx-created-${createdSeq}`,
90
+ label: name,
91
+ path: parentPath ? `${parentPath}/${name}` : name,
92
+ }),
93
+ 400,
94
+ );
95
+ });
96
+
97
+ function Manager({
98
+ renaming = false,
99
+ className,
100
+ }: {
101
+ renaming?: boolean;
102
+ className?: string;
103
+ }) {
104
+ const [known, setKnown] = useState<ManagedFolder[]>(folders);
105
+ const [renamed, setRenamed] = useState<ManagedFolder | null>(
106
+ renaming ? (folders[2] as ManagedFolder) : null,
107
+ );
108
+ const [draft, setDraft] = useState("Trash");
109
+ return (
110
+ <>
111
+ <Frame className={className}>
112
+ <FolderManager
113
+ folders={known}
114
+ onCreateFolder={(name, parentPath) =>
115
+ createFolder(name, parentPath).then((created) => {
116
+ setKnown((current) => [...current, created]);
117
+ return created;
118
+ })
119
+ }
120
+ onRename={(folder) => {
121
+ setRenamed(folder);
122
+ setDraft(folder.label);
123
+ }}
124
+ onDelete={(folder) =>
125
+ setKnown((current) =>
126
+ current.filter((entry) => entry.id !== folder.id),
127
+ )
128
+ }
129
+ labels={{ treeAriaLabel: "All folders for alice@northwind.example" }}
130
+ />
131
+ </Frame>
132
+ {renamed && (
133
+ <FolderRenameDialog
134
+ open
135
+ folderLabel={renamed.label}
136
+ defaultLabel="Deleted Messages"
137
+ name={draft}
138
+ onNameChange={setDraft}
139
+ onSubmit={() => setRenamed(null)}
140
+ onClose={() => setRenamed(null)}
141
+ />
142
+ )}
143
+ </>
144
+ );
145
+ }
146
+
147
+ /**
148
+ * The account's folders as they really nest, closed to the top level. Every row
149
+ * opens where you tap it and carries rename and delete; a folder the account
150
+ * depends on keeps rename and states why it stays.
151
+ */
152
+ export const Default: Story = {
153
+ name: "Default (collapsed to top level)",
154
+ render: () => <Manager />,
155
+ };
156
+
157
+ /** Renaming a folder: the name is Remit's own, and clearing it restores the server's. */
158
+ export const Renaming: Story = {
159
+ name: "Renaming a folder",
160
+ render: () => <Manager renaming />,
161
+ };
162
+
163
+ /** A first-run account with nothing but its inbox. */
164
+ export const JustTheInbox: Story = {
165
+ name: "A single folder",
166
+ render: () => (
167
+ <Frame>
168
+ <FolderManager
169
+ folders={[folders[0] as ManagedFolder]}
170
+ onCreateFolder={createFolder}
171
+ onRename={() => {}}
172
+ onDelete={() => {}}
173
+ />
174
+ </Frame>
175
+ ),
176
+ };
177
+
178
+ /** The surface as it sits on a phone: full width, one branch open at a time. */
179
+ export const Phone: Story = {
180
+ name: "Phone",
181
+ globals: { viewport: { value: "mobile" } },
182
+ parameters: { layout: "fullscreen" },
183
+ render: () => <Manager className="h-screen w-full" />,
184
+ };
@@ -0,0 +1,77 @@
1
+ import type { FolderTreeNode } from "../lib/folder-tree.js";
2
+ import { FolderManageActions } from "./folder-manage-actions.js";
3
+ import {
4
+ FolderTreePicker,
5
+ type FolderTreePickerLabels,
6
+ } from "./folder-tree-picker.js";
7
+
8
+ export interface ManagedFolder extends FolderTreeNode {
9
+ /** Why this folder can't be deleted; absent means it can. */
10
+ deleteBlockedReason?: string;
11
+ }
12
+
13
+ export interface FolderManagerLabels extends FolderTreePickerLabels {
14
+ rename?: (label: string) => string;
15
+ remove?: (label: string) => string;
16
+ }
17
+
18
+ export interface FolderManagerProps {
19
+ folders: readonly ManagedFolder[];
20
+ onRename: (folder: ManagedFolder) => void;
21
+ onDelete: (folder: ManagedFolder) => void;
22
+ /**
23
+ * Creating a folder is an IMAP mutation, so this resolves only once the mail
24
+ * server confirms it. Absent means no create affordance renders.
25
+ */
26
+ onCreateFolder?: (
27
+ name: string,
28
+ parentPath: string,
29
+ signal?: AbortSignal,
30
+ ) => Promise<FolderTreeNode>;
31
+ /** The provider's hierarchy separator. */
32
+ delimiter?: string;
33
+ labels?: FolderManagerLabels;
34
+ }
35
+
36
+ /**
37
+ * The folders as the account holds them, browsed the same way a destination is
38
+ * picked — open a folder to see what is inside it, filter to narrow, make a new
39
+ * one where you are looking — with rename and delete on each row. Nothing is
40
+ * chosen here, so a row opens and closes and nothing else.
41
+ */
42
+ export const FolderManager = ({
43
+ folders,
44
+ onRename,
45
+ onDelete,
46
+ onCreateFolder,
47
+ delimiter,
48
+ labels,
49
+ }: FolderManagerProps) => {
50
+ const byId = new Map(folders.map((folder) => [folder.id, folder]));
51
+ return (
52
+ <FolderTreePicker
53
+ folders={folders}
54
+ onCreateFolder={onCreateFolder}
55
+ delimiter={delimiter}
56
+ labels={{
57
+ treeAriaLabel: "Your folders",
58
+ optionLabel: (label) => label,
59
+ ...labels,
60
+ }}
61
+ rowActions={(folder) => {
62
+ const managed = byId.get(folder.id);
63
+ if (!managed) return null;
64
+ return (
65
+ <FolderManageActions
66
+ label={managed.label}
67
+ deleteBlockedReason={managed.deleteBlockedReason}
68
+ renameLabel={labels?.rename}
69
+ deleteLabel={labels?.remove}
70
+ onRename={() => onRename(managed)}
71
+ onDelete={() => onDelete(managed)}
72
+ />
73
+ );
74
+ }}
75
+ />
76
+ );
77
+ };
@@ -0,0 +1,51 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { createElement } from "react";
4
+ import { renderToString } from "react-dom/server";
5
+ import {
6
+ FolderRenameDialog,
7
+ type FolderRenameDialogProps,
8
+ } from "./folder-rename-dialog.js";
9
+
10
+ const render = (props: Partial<FolderRenameDialogProps>) =>
11
+ renderToString(
12
+ createElement(FolderRenameDialog, {
13
+ open: true,
14
+ folderLabel: "Trash",
15
+ defaultLabel: "Deleted Messages",
16
+ name: "Trash",
17
+ onNameChange: () => undefined,
18
+ onSubmit: () => undefined,
19
+ onClose: () => undefined,
20
+ ...props,
21
+ }),
22
+ );
23
+
24
+ describe("FolderRenameDialog", () => {
25
+ it("renders nothing when closed", () => {
26
+ assert.equal(render({ open: false }), "");
27
+ });
28
+
29
+ it("names the folder it is renaming", () => {
30
+ assert.match(render({}), /Rename Trash/);
31
+ });
32
+
33
+ it("states what clearing the name falls back to", () => {
34
+ const html = render({});
35
+ assert.match(html, /Leave it blank to use/);
36
+ assert.match(html, />Deleted Messages</);
37
+ });
38
+
39
+ it("holds the wait while the name is saved", () => {
40
+ const html = render({ pending: true });
41
+ assert.match(html, /Saving…/);
42
+ assert.match(html, /disabled/);
43
+ });
44
+
45
+ it("states a failure where it happened", () => {
46
+ assert.match(
47
+ render({ error: "Couldn't save that name." }),
48
+ /role="alert"[^>]*>Couldn&#x27;t save that name./,
49
+ );
50
+ });
51
+ });
@@ -0,0 +1,66 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { FolderRenameDialog } from "./folder-rename-dialog.js";
4
+
5
+ const meta: Meta<typeof FolderRenameDialog> = {
6
+ title: "Mail/FolderRenameDialog",
7
+ component: FolderRenameDialog,
8
+ parameters: { layout: "fullscreen" },
9
+ };
10
+ export default meta;
11
+
12
+ type Story = StoryObj<typeof FolderRenameDialog>;
13
+
14
+ function Live({
15
+ initialName = "Trash",
16
+ pending,
17
+ error,
18
+ }: {
19
+ initialName?: string;
20
+ pending?: boolean;
21
+ error?: string;
22
+ }) {
23
+ const [name, setName] = useState(initialName);
24
+ return (
25
+ <div className="h-screen bg-canvas font-sans">
26
+ <FolderRenameDialog
27
+ open
28
+ folderLabel="Trash"
29
+ defaultLabel="Deleted Messages"
30
+ name={name}
31
+ onNameChange={setName}
32
+ onSubmit={() => {}}
33
+ onClose={() => {}}
34
+ pending={pending}
35
+ error={error}
36
+ />
37
+ </div>
38
+ );
39
+ }
40
+
41
+ export const Default: Story = {
42
+ name: "Renaming an appointed folder",
43
+ render: () => <Live />,
44
+ };
45
+
46
+ /** Cleared: the folder goes back to what the mail server calls it. */
47
+ export const Cleared: Story = {
48
+ name: "Name cleared",
49
+ render: () => <Live initialName="" />,
50
+ };
51
+
52
+ export const Saving: Story = {
53
+ name: "Saving",
54
+ render: () => <Live pending />,
55
+ };
56
+
57
+ export const Failed: Story = {
58
+ name: "The save failed",
59
+ render: () => <Live error="Couldn't save that name. Please try again." />,
60
+ };
61
+
62
+ export const Phone: Story = {
63
+ name: "Phone",
64
+ globals: { viewport: { value: "mobile" } },
65
+ render: () => <Live />,
66
+ };