@multiplatform.one/rich-text 7.7.0 → 7.7.2
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/cjs/RichEditor.native.js +20 -0
- package/dist/cjs/RichEditor.native.js.map +1 -1
- package/dist/cjs/editor/TentapEditor.native.js +18 -0
- package/dist/cjs/editor/TentapEditor.native.js.map +1 -1
- package/dist/esm/RichEditor.native.js +21 -1
- package/dist/esm/RichEditor.native.js.map +1 -1
- package/dist/esm/editor/TentapEditor.native.js +18 -0
- package/dist/esm/editor/TentapEditor.native.js.map +1 -1
- package/dist/jsx/RichEditor.native.js +20 -0
- package/dist/jsx/RichEditor.native.js.map +1 -1
- package/dist/jsx/editor/TentapEditor.native.js +18 -0
- package/dist/jsx/editor/TentapEditor.native.js.map +1 -1
- package/package.json +8 -8
- package/src/RichEditor.native.tsx +19 -1
- package/src/RichEditor.parts.stories.tsx +139 -0
- package/src/editor/TentapEditor.native.tsx +35 -0
- package/src/richEditorPartsStories.spec.tsx +150 -0
- package/src/toolbar/FixedToolbar.stories.tsx +224 -0
- package/src/toolbar/StatusBar.stories.tsx +146 -0
- package/src/toolbar/ToolbarButton.stories.tsx +193 -0
- package/src/toolbar/ToolbarGroup.stories.tsx +148 -0
- package/src/toolbar/fixedToolbarStories.spec.tsx +200 -0
- package/src/toolbar/statusBarStories.spec.tsx +86 -0
- package/src/toolbar/toolbarButtonStories.spec.tsx +122 -0
- package/src/toolbar/toolbarGroupStories.spec.tsx +108 -0
- package/types/RichEditor.native.d.ts.map +1 -1
- package/types/editor/TentapEditor.native.d.ts +4 -0
- package/types/editor/TentapEditor.native.d.ts.map +1 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storybook stories for FixedToolbar — the bar across the top of the editor,
|
|
3
|
+
* the thing the whole toolbar/ folder assembles into.
|
|
4
|
+
*
|
|
5
|
+
* Stories: Main, Sections, MarkdownCapabilities, ErrorState, FullscreenToggle,
|
|
6
|
+
* LiveEditor
|
|
7
|
+
*
|
|
8
|
+
* The toolbar owns no editor state: it renders `state` and calls commands
|
|
9
|
+
* through `editorRef`. So every story but the last drives it from a plain
|
|
10
|
+
* state object and reports the commands into the actions panel, which is the
|
|
11
|
+
* only way to see the command wiring without a document to run it against.
|
|
12
|
+
* LiveEditor is the exception — a real editor behind a standalone bar, which
|
|
13
|
+
* is what the compound `RichEditor.Editor` export exists for.
|
|
14
|
+
*/
|
|
15
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
16
|
+
import { action } from "@multiplatform.one/storybook";
|
|
17
|
+
import { useRef, useState } from "react";
|
|
18
|
+
import { Paragraph, YStack } from "tamagui";
|
|
19
|
+
import { RichEditorContext } from "../context";
|
|
20
|
+
import { EditorFrame } from "../styled";
|
|
21
|
+
import { RichEditor } from "../RichEditor";
|
|
22
|
+
import { defaultEditorState } from "../types";
|
|
23
|
+
import type { EditorCapabilities, EditorState, RichEditorRef, ToolbarSection } from "../types";
|
|
24
|
+
import { FixedToolbar } from "./FixedToolbar";
|
|
25
|
+
|
|
26
|
+
export interface FixedToolbarStoryArgs {
|
|
27
|
+
/** Bold is on in the current selection */
|
|
28
|
+
isBold: boolean;
|
|
29
|
+
/** Italic is on in the current selection */
|
|
30
|
+
isItalic: boolean;
|
|
31
|
+
/** There is history to undo (an empty history washes the Undo button) */
|
|
32
|
+
canUndo: boolean;
|
|
33
|
+
/** There is history to redo */
|
|
34
|
+
canRedo: boolean;
|
|
35
|
+
/** Which sections show; undefined shows every one */
|
|
36
|
+
sections?: ToolbarSection[];
|
|
37
|
+
/** markdown drops the controls GFM cannot express */
|
|
38
|
+
capabilities: EditorCapabilities;
|
|
39
|
+
/** Paints the bar's bottom rule red, matching the frame */
|
|
40
|
+
hasError: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stateFor(args: FixedToolbarStoryArgs): EditorState {
|
|
44
|
+
return {
|
|
45
|
+
...defaultEditorState,
|
|
46
|
+
isBold: args.isBold,
|
|
47
|
+
isItalic: args.isItalic,
|
|
48
|
+
canUndo: args.canUndo,
|
|
49
|
+
canRedo: args.canRedo,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A toolbar with no editor under it. `editorRef` stays null, so commands are
|
|
55
|
+
* reported to the actions panel instead of running — which is the point:
|
|
56
|
+
* the bar's job is to name the command, not to perform it.
|
|
57
|
+
*/
|
|
58
|
+
function ToolbarSpecimen(args: FixedToolbarStoryArgs) {
|
|
59
|
+
const editorRef = useRef<RichEditorRef>({
|
|
60
|
+
executeCommand: (command, payload) => action("executeCommand")(command, payload),
|
|
61
|
+
getState: () => stateFor(args),
|
|
62
|
+
getHTML: () => "",
|
|
63
|
+
getJSON: () => null,
|
|
64
|
+
getMarkdown: () => "",
|
|
65
|
+
setHTML: () => {},
|
|
66
|
+
setJSON: () => {},
|
|
67
|
+
setMarkdown: () => {},
|
|
68
|
+
focus: () => {},
|
|
69
|
+
blur: () => {},
|
|
70
|
+
});
|
|
71
|
+
return (
|
|
72
|
+
<EditorFrame maxWidth={720} borderRadius="$3" hasError={args.hasError}>
|
|
73
|
+
<FixedToolbar
|
|
74
|
+
editorRef={editorRef}
|
|
75
|
+
state={stateFor(args)}
|
|
76
|
+
sections={args.sections}
|
|
77
|
+
capabilities={args.capabilities}
|
|
78
|
+
hasError={args.hasError}
|
|
79
|
+
/>
|
|
80
|
+
<YStack minHeight={100} padding="$3">
|
|
81
|
+
<Paragraph size="$3" color="$color11">
|
|
82
|
+
The bar above drives this space. Commands land in the Actions panel.
|
|
83
|
+
</Paragraph>
|
|
84
|
+
</YStack>
|
|
85
|
+
</EditorFrame>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const meta: Meta<FixedToolbarStoryArgs> = {
|
|
90
|
+
title: "Forms/RichTextEditor/FixedToolbar",
|
|
91
|
+
component: FixedToolbar as never,
|
|
92
|
+
parameters: {
|
|
93
|
+
layout: "padded",
|
|
94
|
+
},
|
|
95
|
+
argTypes: {
|
|
96
|
+
isBold: { control: "boolean" },
|
|
97
|
+
isItalic: { control: "boolean" },
|
|
98
|
+
canUndo: { control: "boolean" },
|
|
99
|
+
canRedo: { control: "boolean" },
|
|
100
|
+
capabilities: { control: "radio", options: ["full", "markdown"] },
|
|
101
|
+
hasError: { control: "boolean" },
|
|
102
|
+
sections: { control: false },
|
|
103
|
+
},
|
|
104
|
+
args: {
|
|
105
|
+
isBold: false,
|
|
106
|
+
isItalic: false,
|
|
107
|
+
canUndo: true,
|
|
108
|
+
canRedo: false,
|
|
109
|
+
capabilities: "full",
|
|
110
|
+
hasError: false,
|
|
111
|
+
},
|
|
112
|
+
decorators: [
|
|
113
|
+
(Story) => (
|
|
114
|
+
<RichEditorContext.Provider size="$true" compact={false} disabled={false}>
|
|
115
|
+
<Story />
|
|
116
|
+
</RichEditorContext.Provider>
|
|
117
|
+
),
|
|
118
|
+
],
|
|
119
|
+
render: (args) => <ToolbarSpecimen {...args} />,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export default meta;
|
|
123
|
+
type Story = StoryObj<FixedToolbarStoryArgs>;
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Main (with controls) — every section, bold off, redo unavailable
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
export const Main: Story = {};
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Sections — the `sections` prop trims the bar to the listed groups
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
export const Sections: Story = {
|
|
136
|
+
args: {
|
|
137
|
+
sections: ["formatting", "link"],
|
|
138
|
+
isBold: true,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// MarkdownCapabilities — GFM cannot express colour or underline, so those
|
|
144
|
+
// controls are absent, not merely disabled
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
export const MarkdownCapabilities: Story = {
|
|
148
|
+
name: "Markdown capabilities",
|
|
149
|
+
args: {
|
|
150
|
+
capabilities: "markdown",
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// ErrorState — the bar's bottom rule turns red with the frame
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
export const ErrorState: Story = {
|
|
159
|
+
name: "Error state",
|
|
160
|
+
args: {
|
|
161
|
+
hasError: true,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// FullscreenToggle — onFullscreenToggle adds the expand control at the right
|
|
167
|
+
// end, outside the scrolling strip so it never scrolls away
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
export const FullscreenToggle: Story = {
|
|
171
|
+
name: "Fullscreen toggle",
|
|
172
|
+
render: (args) => {
|
|
173
|
+
const FullscreenSpecimen = () => {
|
|
174
|
+
const [fullscreen, setFullscreen] = useState(false);
|
|
175
|
+
const editorRef = useRef<RichEditorRef>(null);
|
|
176
|
+
return (
|
|
177
|
+
<EditorFrame maxWidth={720} borderRadius="$3">
|
|
178
|
+
<FixedToolbar
|
|
179
|
+
editorRef={editorRef}
|
|
180
|
+
state={stateFor(args)}
|
|
181
|
+
fullscreen={fullscreen}
|
|
182
|
+
onFullscreenToggle={() => {
|
|
183
|
+
action("onFullscreenToggle")(!fullscreen);
|
|
184
|
+
setFullscreen(!fullscreen);
|
|
185
|
+
}}
|
|
186
|
+
/>
|
|
187
|
+
<YStack minHeight={100} padding="$3">
|
|
188
|
+
<Paragraph size="$3" color="$color11">
|
|
189
|
+
{fullscreen ? "Fullscreen: on" : "Fullscreen: off"}
|
|
190
|
+
</Paragraph>
|
|
191
|
+
</YStack>
|
|
192
|
+
</EditorFrame>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
return <FullscreenSpecimen />;
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// LiveEditor — a standalone bar wired to a real editor through the compound
|
|
201
|
+
// `RichEditor.Editor` export; typing updates the marks
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
export const LiveEditor: Story = {
|
|
205
|
+
name: "Live editor",
|
|
206
|
+
render: () => {
|
|
207
|
+
const LiveSpecimen = () => {
|
|
208
|
+
const editorRef = useRef<RichEditorRef>(null);
|
|
209
|
+
const [state, setState] = useState<EditorState>(defaultEditorState);
|
|
210
|
+
return (
|
|
211
|
+
<EditorFrame maxWidth={720} borderRadius="$3">
|
|
212
|
+
<FixedToolbar editorRef={editorRef} state={state} />
|
|
213
|
+
<RichEditor.Editor
|
|
214
|
+
ref={editorRef}
|
|
215
|
+
initialContent="<p>Select this text and press <strong>B</strong> in the bar above — the button marks itself from the editor's own state.</p>"
|
|
216
|
+
onStateChange={setState}
|
|
217
|
+
minHeight={160}
|
|
218
|
+
/>
|
|
219
|
+
</EditorFrame>
|
|
220
|
+
);
|
|
221
|
+
};
|
|
222
|
+
return <LiveSpecimen />;
|
|
223
|
+
},
|
|
224
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storybook stories for StatusBar — the editor footer: the rich-text /
|
|
3
|
+
* markdown mode toggle on the left, word and character counts on the right.
|
|
4
|
+
*
|
|
5
|
+
* Stories: Main, SingularCounts, MarkdownMode, Empty, InFrame
|
|
6
|
+
*
|
|
7
|
+
* The footer is a pure function of `state` and `mode`, so the controls drive
|
|
8
|
+
* the two counts and the active mode directly; pressing a mode label reports
|
|
9
|
+
* through `onModeChange` (actions panel) and flips the local mode so the
|
|
10
|
+
* marking swaps the way it does inside RichEditor.
|
|
11
|
+
*/
|
|
12
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
13
|
+
import { action } from "@multiplatform.one/storybook";
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
|
+
import { Paragraph, View, YStack } from "tamagui";
|
|
16
|
+
import { RichEditorContext } from "../context";
|
|
17
|
+
import { EditorFrame } from "../styled";
|
|
18
|
+
import { defaultEditorState } from "../types";
|
|
19
|
+
import type { EditorMode, EditorState } from "../types";
|
|
20
|
+
import { StatusBar } from "./StatusBar";
|
|
21
|
+
|
|
22
|
+
export interface StatusBarStoryArgs {
|
|
23
|
+
/** Word count shown on the right */
|
|
24
|
+
wordCount: number;
|
|
25
|
+
/** Character count shown on the right */
|
|
26
|
+
characterCount: number;
|
|
27
|
+
/** Which mode label is marked active */
|
|
28
|
+
mode: EditorMode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function stateFor(args: StatusBarStoryArgs): EditorState {
|
|
32
|
+
return {
|
|
33
|
+
...defaultEditorState,
|
|
34
|
+
wordCount: args.wordCount,
|
|
35
|
+
characterCount: args.characterCount,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Mode is local so the toggle is live; the `mode` control seeds and resets it. */
|
|
40
|
+
function StatusBarSpecimen(args: StatusBarStoryArgs) {
|
|
41
|
+
const [mode, setMode] = useState<EditorMode>(args.mode);
|
|
42
|
+
useEffect(() => setMode(args.mode), [args.mode]);
|
|
43
|
+
return (
|
|
44
|
+
<StatusBar
|
|
45
|
+
state={stateFor(args)}
|
|
46
|
+
mode={mode}
|
|
47
|
+
onModeChange={(next) => {
|
|
48
|
+
action("onModeChange")(next);
|
|
49
|
+
setMode(next);
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const meta: Meta<StatusBarStoryArgs> = {
|
|
56
|
+
title: "Forms/RichTextEditor/StatusBar",
|
|
57
|
+
component: StatusBar as never,
|
|
58
|
+
parameters: {
|
|
59
|
+
layout: "padded",
|
|
60
|
+
},
|
|
61
|
+
argTypes: {
|
|
62
|
+
wordCount: { control: { type: "number", min: 0 } },
|
|
63
|
+
characterCount: { control: { type: "number", min: 0 } },
|
|
64
|
+
mode: { control: "radio", options: ["richtext", "markdown"] },
|
|
65
|
+
},
|
|
66
|
+
args: {
|
|
67
|
+
wordCount: 42,
|
|
68
|
+
characterCount: 240,
|
|
69
|
+
mode: "richtext",
|
|
70
|
+
},
|
|
71
|
+
decorators: [
|
|
72
|
+
(Story) => (
|
|
73
|
+
<RichEditorContext.Provider size="$true" compact={false} disabled={false}>
|
|
74
|
+
<Story />
|
|
75
|
+
</RichEditorContext.Provider>
|
|
76
|
+
),
|
|
77
|
+
],
|
|
78
|
+
render: (args) => (
|
|
79
|
+
<View maxWidth={520} borderWidth={1} borderColor="$color6" borderRadius="$3">
|
|
80
|
+
<StatusBarSpecimen {...args} />
|
|
81
|
+
</View>
|
|
82
|
+
),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default meta;
|
|
86
|
+
type Story = StoryObj<StatusBarStoryArgs>;
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Main (with controls)
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
export const Main: Story = {};
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// SingularCounts — the plural forms flip exactly at one
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export const SingularCounts: Story = {
|
|
99
|
+
args: {
|
|
100
|
+
wordCount: 1,
|
|
101
|
+
characterCount: 1,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// MarkdownMode — the raw-source toggle is the marked one
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
export const MarkdownMode: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
mode: "markdown",
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Empty — a fresh document pluralises at zero
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
export const Empty: Story = {
|
|
120
|
+
args: {
|
|
121
|
+
wordCount: 0,
|
|
122
|
+
characterCount: 0,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// InFrame — where it sits: the foot of an EditorFrame, under the content
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
export const InFrame: Story = {
|
|
131
|
+
name: "In frame",
|
|
132
|
+
args: {
|
|
133
|
+
wordCount: 12,
|
|
134
|
+
characterCount: 71,
|
|
135
|
+
},
|
|
136
|
+
render: (args) => (
|
|
137
|
+
<EditorFrame maxWidth={520} borderRadius="$3">
|
|
138
|
+
<YStack minHeight={120} padding="$3">
|
|
139
|
+
<Paragraph size="$3" color="$color11">
|
|
140
|
+
Twelve words of body copy stand in for the editor content here.
|
|
141
|
+
</Paragraph>
|
|
142
|
+
</YStack>
|
|
143
|
+
<StatusBarSpecimen {...args} />
|
|
144
|
+
</EditorFrame>
|
|
145
|
+
),
|
|
146
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storybook stories for ToolbarButton and ToolbarDropdownButton — the two
|
|
3
|
+
* pressables every toolbar cluster is built from.
|
|
4
|
+
*
|
|
5
|
+
* Stories: Main, Active, Unavailable, WithTooltip, DropdownButton, Row
|
|
6
|
+
*
|
|
7
|
+
* Two things are worth seeing here and nowhere else. The pressable is a 44x44
|
|
8
|
+
* target (Axiom 12) while the visible chrome is a 30x30 box inside it, so the
|
|
9
|
+
* strip stays dense without shrinking the hit area. And an unavailable control
|
|
10
|
+
* is never bare-`disabled` (DG-ST-03): it keeps focus, reports `aria-disabled`,
|
|
11
|
+
* dims, drops its hover surface, and loses its tooltip (DG-OVL-05).
|
|
12
|
+
*/
|
|
13
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
14
|
+
import { action } from "@multiplatform.one/storybook";
|
|
15
|
+
import { XStack } from "tamagui";
|
|
16
|
+
import { RichEditorContext } from "../context";
|
|
17
|
+
import { ToolbarFrame } from "../styled";
|
|
18
|
+
import { BoldIcon, ItalicIcon, RedoIcon, StrikeIcon, UndoIcon } from "./icons";
|
|
19
|
+
import { ToolbarButton, ToolbarDropdownButton } from "./ToolbarButton";
|
|
20
|
+
import { ToolbarGroup } from "./ToolbarGroup";
|
|
21
|
+
|
|
22
|
+
export interface ToolbarButtonStoryArgs {
|
|
23
|
+
/** Format is on: the chrome box takes the active surface */
|
|
24
|
+
active: boolean;
|
|
25
|
+
/** Unavailable: aria-disabled, dimmed, no hover surface, no tooltip */
|
|
26
|
+
disabled: boolean;
|
|
27
|
+
/** Shortcut hint on hover; suppressed while unavailable */
|
|
28
|
+
tooltip: string;
|
|
29
|
+
/** Accessible name — required, since the glyph carries no text */
|
|
30
|
+
"aria-label": string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const meta: Meta<ToolbarButtonStoryArgs> = {
|
|
34
|
+
title: "Forms/RichTextEditor/ToolbarButton",
|
|
35
|
+
component: ToolbarButton as never,
|
|
36
|
+
parameters: {
|
|
37
|
+
layout: "padded",
|
|
38
|
+
},
|
|
39
|
+
argTypes: {
|
|
40
|
+
active: { control: "boolean" },
|
|
41
|
+
disabled: { control: "boolean" },
|
|
42
|
+
tooltip: { control: "text" },
|
|
43
|
+
"aria-label": { control: "text" },
|
|
44
|
+
},
|
|
45
|
+
args: {
|
|
46
|
+
active: false,
|
|
47
|
+
disabled: false,
|
|
48
|
+
tooltip: "Bold (⌘B)",
|
|
49
|
+
"aria-label": "Bold",
|
|
50
|
+
},
|
|
51
|
+
decorators: [
|
|
52
|
+
(Story) => (
|
|
53
|
+
<RichEditorContext.Provider size="$true" compact={false} disabled={false}>
|
|
54
|
+
<Story />
|
|
55
|
+
</RichEditorContext.Provider>
|
|
56
|
+
),
|
|
57
|
+
],
|
|
58
|
+
render: (args) => (
|
|
59
|
+
<ToolbarFrame width={80} justifyContent="center">
|
|
60
|
+
<ToolbarButton
|
|
61
|
+
active={args.active}
|
|
62
|
+
disabled={args.disabled}
|
|
63
|
+
tooltip={args.tooltip}
|
|
64
|
+
aria-label={args["aria-label"]}
|
|
65
|
+
onPress={() => action("onPress")(args["aria-label"])}
|
|
66
|
+
>
|
|
67
|
+
<BoldIcon>B</BoldIcon>
|
|
68
|
+
</ToolbarButton>
|
|
69
|
+
</ToolbarFrame>
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default meta;
|
|
74
|
+
type Story = StoryObj<ToolbarButtonStoryArgs>;
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Main (with controls)
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
export const Main: Story = {};
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Active — the format is on, so the chrome box takes the active surface
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export const Active: Story = {
|
|
87
|
+
args: {
|
|
88
|
+
active: true,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Unavailable — DG-ST-03: aria-disabled and washed, never bare `disabled`
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
export const Unavailable: Story = {
|
|
97
|
+
args: {
|
|
98
|
+
disabled: true,
|
|
99
|
+
"aria-label": "Undo",
|
|
100
|
+
tooltip: "Undo (⌘Z)",
|
|
101
|
+
},
|
|
102
|
+
render: (args) => (
|
|
103
|
+
<ToolbarFrame width={80} justifyContent="center">
|
|
104
|
+
<ToolbarButton
|
|
105
|
+
disabled={args.disabled}
|
|
106
|
+
tooltip={args.tooltip}
|
|
107
|
+
aria-label={args["aria-label"]}
|
|
108
|
+
onPress={() => action("onPress")(args["aria-label"])}
|
|
109
|
+
>
|
|
110
|
+
<UndoIcon />
|
|
111
|
+
</ToolbarButton>
|
|
112
|
+
</ToolbarFrame>
|
|
113
|
+
),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// WithTooltip — the shortcut hint that returns with availability
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
export const WithTooltip: Story = {
|
|
121
|
+
name: "With tooltip",
|
|
122
|
+
args: {
|
|
123
|
+
tooltip: "Strikethrough (⌘⇧X)",
|
|
124
|
+
"aria-label": "Strikethrough",
|
|
125
|
+
},
|
|
126
|
+
render: (args) => (
|
|
127
|
+
<ToolbarFrame width={80} justifyContent="center">
|
|
128
|
+
<ToolbarButton
|
|
129
|
+
tooltip={args.tooltip}
|
|
130
|
+
aria-label={args["aria-label"]}
|
|
131
|
+
onPress={() => action("onPress")(args["aria-label"])}
|
|
132
|
+
>
|
|
133
|
+
<StrikeIcon>S</StrikeIcon>
|
|
134
|
+
</ToolbarButton>
|
|
135
|
+
</ToolbarFrame>
|
|
136
|
+
),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// DropdownButton — the caret variant: fixed height, width follows its label
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
export const DropdownButton: Story = {
|
|
144
|
+
name: "Dropdown button",
|
|
145
|
+
render: () => (
|
|
146
|
+
<ToolbarFrame width={220} justifyContent="center">
|
|
147
|
+
<XStack alignItems="center" gap="$1">
|
|
148
|
+
<ToolbarDropdownButton aria-label="Heading" onPress={() => action("onPress")("Heading")}>
|
|
149
|
+
H1
|
|
150
|
+
</ToolbarDropdownButton>
|
|
151
|
+
<ToolbarDropdownButton active aria-label="Lists" onPress={() => action("onPress")("Lists")}>
|
|
152
|
+
Bullets
|
|
153
|
+
</ToolbarDropdownButton>
|
|
154
|
+
<ToolbarDropdownButton
|
|
155
|
+
disabled
|
|
156
|
+
aria-label="Table"
|
|
157
|
+
onPress={() => action("onPress")("Table")}
|
|
158
|
+
>
|
|
159
|
+
Table
|
|
160
|
+
</ToolbarDropdownButton>
|
|
161
|
+
</XStack>
|
|
162
|
+
</ToolbarFrame>
|
|
163
|
+
),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Row — the four states side by side, in the bar they live in
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
export const Row: Story = {
|
|
171
|
+
render: () => (
|
|
172
|
+
<ToolbarFrame width={320}>
|
|
173
|
+
<XStack alignItems="center" gap="$1" paddingHorizontal="$2">
|
|
174
|
+
<ToolbarGroup>
|
|
175
|
+
<ToolbarButton disabled onPress={() => {}} aria-label="Undo" tooltip="Undo (⌘Z)">
|
|
176
|
+
<UndoIcon />
|
|
177
|
+
</ToolbarButton>
|
|
178
|
+
<ToolbarButton onPress={() => action("onPress")("Redo")} aria-label="Redo">
|
|
179
|
+
<RedoIcon />
|
|
180
|
+
</ToolbarButton>
|
|
181
|
+
</ToolbarGroup>
|
|
182
|
+
<ToolbarGroup showSeparator={false}>
|
|
183
|
+
<ToolbarButton active onPress={() => action("onPress")("Bold")} aria-label="Bold">
|
|
184
|
+
<BoldIcon>B</BoldIcon>
|
|
185
|
+
</ToolbarButton>
|
|
186
|
+
<ToolbarButton onPress={() => action("onPress")("Italic")} aria-label="Italic">
|
|
187
|
+
<ItalicIcon>I</ItalicIcon>
|
|
188
|
+
</ToolbarButton>
|
|
189
|
+
</ToolbarGroup>
|
|
190
|
+
</XStack>
|
|
191
|
+
</ToolbarFrame>
|
|
192
|
+
),
|
|
193
|
+
};
|