@valbuild/ui 0.12.0 → 0.13.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/dist/valbuild-ui.cjs.js +66 -10
- package/dist/valbuild-ui.esm.js +66 -10
- package/package.json +1 -1
- package/src/assets/icons/Bold.tsx +23 -0
- package/src/assets/icons/Chevron.tsx +28 -0
- package/src/assets/icons/FontColor.tsx +30 -0
- package/src/assets/icons/ImageIcon.tsx +21 -0
- package/src/assets/icons/Italic.tsx +24 -0
- package/src/assets/icons/Strikethrough.tsx +22 -0
- package/src/assets/icons/Underline.tsx +22 -0
- package/src/assets/icons/Undo.tsx +20 -0
- package/src/components/Button.tsx +58 -0
- package/src/components/Checkbox.tsx +51 -0
- package/src/components/Dropdown.tsx +92 -0
- package/src/components/EditButton.tsx +10 -0
- package/src/components/ErrorText.tsx +3 -0
- package/src/components/RichTextEditor/ContentEditable.tsx +9 -0
- package/src/components/RichTextEditor/Nodes/ImageNode.tsx +117 -0
- package/src/components/RichTextEditor/Plugins/AutoFocus.tsx +12 -0
- package/src/components/RichTextEditor/Plugins/ImagePlugin.tsx +46 -0
- package/src/components/RichTextEditor/Plugins/Toolbar.tsx +381 -0
- package/src/components/RichTextEditor/RichTextEditor.tsx +176 -0
- package/src/components/UploadModal.tsx +109 -0
- package/src/components/ValOverlay.tsx +41 -0
- package/src/components/ValWindow.stories.tsx +182 -0
- package/src/components/ValWindow.tsx +192 -0
- package/src/components/forms/Form.tsx +122 -0
- package/src/components/forms/FormContainer.tsx +24 -0
- package/src/components/forms/ImageForm.tsx +195 -0
- package/src/components/forms/TextForm.tsx +22 -0
- package/src/exports.ts +3 -0
- package/src/index.css +79 -0
- package/src/index.tsx +14 -0
- package/src/server.ts +41 -0
- package/src/stories/Button.stories.tsx +20 -0
- package/src/stories/Checkbox.stories.tsx +14 -0
- package/src/stories/Dropdown.stories.tsx +23 -0
- package/src/stories/Introduction.mdx +221 -0
- package/src/stories/RichTextEditor.stories.tsx +314 -0
- package/src/stories/assets/code-brackets.svg +1 -0
- package/src/stories/assets/colors.svg +1 -0
- package/src/stories/assets/comments.svg +1 -0
- package/src/stories/assets/direction.svg +1 -0
- package/src/stories/assets/flow.svg +1 -0
- package/src/stories/assets/plugin.svg +1 -0
- package/src/stories/assets/repo.svg +1 -0
- package/src/stories/assets/stackalt.svg +1 -0
- package/src/vite-env.d.ts +1 -0
- package/src/vite-index.tsx +7 -0
- package/src/vite-server.ts +8 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { UploadCloud } from "react-feather";
|
|
3
|
+
import { ErrorText } from "../ErrorText";
|
|
4
|
+
|
|
5
|
+
type Error = "invalid-file" | "file-too-large";
|
|
6
|
+
export type ImageData =
|
|
7
|
+
| {
|
|
8
|
+
src: string;
|
|
9
|
+
addMetadata: boolean;
|
|
10
|
+
metadata?: { width: number; height: number; sha256: string };
|
|
11
|
+
}
|
|
12
|
+
| {
|
|
13
|
+
url: string;
|
|
14
|
+
metadata?: { width: number; height: number; sha256: string };
|
|
15
|
+
}
|
|
16
|
+
| null;
|
|
17
|
+
export type ImageInputProps = {
|
|
18
|
+
name: string;
|
|
19
|
+
data: ImageData;
|
|
20
|
+
maxSize?: number;
|
|
21
|
+
error: Error | null;
|
|
22
|
+
onChange: (
|
|
23
|
+
result: { error: null; value: ImageData } | { error: Error; value: null }
|
|
24
|
+
) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const textEncoder = new TextEncoder();
|
|
28
|
+
// TODO: handle hashes some other way
|
|
29
|
+
const getSHA256Hash = async (bits: Uint8Array) => {
|
|
30
|
+
const hashBuffer = await window.crypto.subtle.digest("SHA-256", bits);
|
|
31
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
32
|
+
const hash = hashArray
|
|
33
|
+
.map((item) => item.toString(16).padStart(2, "0"))
|
|
34
|
+
.join("");
|
|
35
|
+
return hash;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function ImageForm({
|
|
39
|
+
name,
|
|
40
|
+
data: data,
|
|
41
|
+
maxSize,
|
|
42
|
+
error,
|
|
43
|
+
onChange,
|
|
44
|
+
}: ImageInputProps) {
|
|
45
|
+
const [currentData, setCurrentData] = useState<ImageData>(null);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
setCurrentData(data);
|
|
48
|
+
}, [data]);
|
|
49
|
+
const [currentError, setCurrentError] = useState<Error | null>(null);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
setCurrentError(error);
|
|
52
|
+
}, [data]);
|
|
53
|
+
|
|
54
|
+
// TODO: we should update the Input type - we should never have a url here?
|
|
55
|
+
const src =
|
|
56
|
+
(currentData &&
|
|
57
|
+
(("src" in currentData && currentData.src) ||
|
|
58
|
+
("url" in currentData && currentData.url))) ||
|
|
59
|
+
undefined;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="w-full py-2 max-w-[90vw] object-contain">
|
|
63
|
+
<label htmlFor={name}>
|
|
64
|
+
<div className="flex items-center justify-center w-full h-full min-h-[200px] cursor-pointer">
|
|
65
|
+
{currentData !== null && (
|
|
66
|
+
<img className="object-contain max-h-[300px]" src={src} />
|
|
67
|
+
)}
|
|
68
|
+
{!src && <UploadCloud size={24} className="text-primary" />}
|
|
69
|
+
</div>
|
|
70
|
+
<input
|
|
71
|
+
hidden
|
|
72
|
+
type="file"
|
|
73
|
+
id={name}
|
|
74
|
+
name={name}
|
|
75
|
+
onChange={(e) => {
|
|
76
|
+
const file = e.target.files?.[0];
|
|
77
|
+
if (file) {
|
|
78
|
+
if (maxSize && file.size > maxSize) {
|
|
79
|
+
onChange({ error: "file-too-large", value: null });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const reader = new FileReader();
|
|
83
|
+
reader.addEventListener("load", () => {
|
|
84
|
+
const result = reader.result;
|
|
85
|
+
if (typeof result === "string") {
|
|
86
|
+
const image = new Image();
|
|
87
|
+
image.onload = async () => {
|
|
88
|
+
const nextSource = {
|
|
89
|
+
src: result,
|
|
90
|
+
metadata: {
|
|
91
|
+
width: image.naturalWidth,
|
|
92
|
+
height: image.naturalHeight,
|
|
93
|
+
sha256: await getSHA256Hash(textEncoder.encode(result)),
|
|
94
|
+
},
|
|
95
|
+
addMetadata: !currentData?.metadata,
|
|
96
|
+
};
|
|
97
|
+
setCurrentData(nextSource);
|
|
98
|
+
setCurrentError(null);
|
|
99
|
+
onChange({
|
|
100
|
+
error: null,
|
|
101
|
+
value: nextSource,
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
image.src = result;
|
|
105
|
+
} else {
|
|
106
|
+
onChange({ error: "invalid-file", value: null });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
reader.readAsDataURL(file);
|
|
110
|
+
}
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
</label>
|
|
114
|
+
{currentData &&
|
|
115
|
+
(!currentData.metadata ||
|
|
116
|
+
typeof currentData.metadata.height === "undefined" ||
|
|
117
|
+
typeof currentData.metadata.width === "undefined" ||
|
|
118
|
+
typeof currentData.metadata.sha256 === "undefined") && (
|
|
119
|
+
<ErrorText>
|
|
120
|
+
<div className="">
|
|
121
|
+
<div>Validation failed: missing metadata</div>
|
|
122
|
+
<button
|
|
123
|
+
className="px-4 py-2 border border-dark-gray"
|
|
124
|
+
onClick={async (ev) => {
|
|
125
|
+
ev.preventDefault();
|
|
126
|
+
|
|
127
|
+
const image = new Image();
|
|
128
|
+
image.onload = async () => {
|
|
129
|
+
const nextSource = {
|
|
130
|
+
src: image.src,
|
|
131
|
+
metadata: {
|
|
132
|
+
width: image.naturalWidth,
|
|
133
|
+
height: image.naturalHeight,
|
|
134
|
+
sha256: await getSHA256Hash(
|
|
135
|
+
textEncoder.encode(image.src)
|
|
136
|
+
),
|
|
137
|
+
},
|
|
138
|
+
addMetadata: !currentData?.metadata,
|
|
139
|
+
};
|
|
140
|
+
setCurrentData(nextSource);
|
|
141
|
+
setCurrentError(null);
|
|
142
|
+
onChange({
|
|
143
|
+
error: null,
|
|
144
|
+
value: nextSource,
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if ("url" in currentData) {
|
|
149
|
+
const src = await fetch(currentData.url)
|
|
150
|
+
.then((response) => response.blob())
|
|
151
|
+
.then((blob) => {
|
|
152
|
+
return new Promise<string>((resolve, reject) => {
|
|
153
|
+
const reader = new FileReader();
|
|
154
|
+
reader.onload = function () {
|
|
155
|
+
if (typeof this.result !== "string") {
|
|
156
|
+
return reject(
|
|
157
|
+
Error(
|
|
158
|
+
`Could not read file from url: ${currentData.url}`
|
|
159
|
+
)
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
resolve(this.result);
|
|
163
|
+
}; // <--- `this.result` contains a base64 data URI
|
|
164
|
+
reader.readAsDataURL(blob);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
image.src = src;
|
|
168
|
+
} else {
|
|
169
|
+
image.src = currentData.src;
|
|
170
|
+
}
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
Fix
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
</ErrorText>
|
|
177
|
+
)}
|
|
178
|
+
{currentData?.metadata && (
|
|
179
|
+
<div className="ml-auto text-primary">
|
|
180
|
+
Dimensions: {currentData.metadata.width}x{currentData.metadata.height}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
{currentError && <ImageError error={currentError} />}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function ImageError({ error }: { error: Error }): React.ReactElement | null {
|
|
189
|
+
if (error === "invalid-file") {
|
|
190
|
+
return <ErrorText>Invalid file</ErrorText>;
|
|
191
|
+
} else if (error === "file-too-large") {
|
|
192
|
+
return <ErrorText>File is too large</ErrorText>;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type TextData = string;
|
|
2
|
+
export type TextInputProps = {
|
|
3
|
+
name: string;
|
|
4
|
+
text: TextData;
|
|
5
|
+
onChange: (value: string) => void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function TextForm({ name, text, onChange }: TextInputProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className="w-full py-2 grow-wrap"
|
|
12
|
+
data-replicated-value={text} /* see grow-wrap */
|
|
13
|
+
>
|
|
14
|
+
<textarea
|
|
15
|
+
name={name}
|
|
16
|
+
className="w-full p-2 border outline-none bg-fill text-primary border-border focus-visible:border-highlight"
|
|
17
|
+
defaultValue={text}
|
|
18
|
+
onChange={(e) => onChange(e.target.value)}
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
package/src/exports.ts
ADDED
package/src/index.css
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Need to explicitly set config path, otherwise it may fail to resolve when
|
|
3
|
+
built from outside packages/ui.
|
|
4
|
+
*/
|
|
5
|
+
@config "../tailwind.config.js";
|
|
6
|
+
|
|
7
|
+
@layer base {
|
|
8
|
+
/* For use with Shadow DOM, copied from the TailwindCSS prelude */
|
|
9
|
+
:host {
|
|
10
|
+
line-height: 1.5;
|
|
11
|
+
-webkit-text-size-adjust: 100%;
|
|
12
|
+
tab-size: 4;
|
|
13
|
+
font-family: Roboto, system-ui, -apple-system, BlinkMacSystemFont,
|
|
14
|
+
"Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
|
|
15
|
+
"Helvetica Neue", sans-serif;
|
|
16
|
+
font-feature-settings: normal;
|
|
17
|
+
margin: 0;
|
|
18
|
+
|
|
19
|
+
/* theme properties - if updated remember to update the .storybook/theme.css file as well */
|
|
20
|
+
--val-theme-white: #fcfcfc;
|
|
21
|
+
--val-theme-light-gray: #d6d6d6;
|
|
22
|
+
--val-theme-dark-gray: #575757;
|
|
23
|
+
--val-theme-medium-black: #303030;
|
|
24
|
+
--val-theme-warm-black: #1a1a1a;
|
|
25
|
+
--val-theme-yellow: #ffff00;
|
|
26
|
+
--val-theme-red: #f02929;
|
|
27
|
+
--val-theme-green: #1ced1c;
|
|
28
|
+
|
|
29
|
+
/* light theme */
|
|
30
|
+
--val-theme-base: var(--val-theme-white);
|
|
31
|
+
--val-theme-highlight: var(--val-theme-yellow);
|
|
32
|
+
--val-theme-border: var(--val-theme-light-gray);
|
|
33
|
+
--val-theme-fill: var(--val-theme-light-gray);
|
|
34
|
+
--val-theme-primary: var(--val-theme-warm-black);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* dark theme */
|
|
38
|
+
*[data-mode="dark"] {
|
|
39
|
+
--val-theme-base: var(--val-theme-warm-black);
|
|
40
|
+
--val-theme-highlight: var(--val-theme-yellow);
|
|
41
|
+
--val-theme-border: var(--val-theme-dark-gray);
|
|
42
|
+
--val-theme-fill: var(--val-theme-medium-black);
|
|
43
|
+
--val-theme-primary: var(--val-theme-white);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* text area auto-grow */
|
|
47
|
+
.grow-wrap {
|
|
48
|
+
/* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */
|
|
49
|
+
display: grid;
|
|
50
|
+
}
|
|
51
|
+
.grow-wrap::after {
|
|
52
|
+
/* Note the weird space! Needed to preventy jumpy behavior */
|
|
53
|
+
content: attr(data-replicated-value) " ";
|
|
54
|
+
|
|
55
|
+
/* This is how textarea text behaves */
|
|
56
|
+
white-space: pre-wrap;
|
|
57
|
+
|
|
58
|
+
/* Hidden from view, clicks, and screen readers */
|
|
59
|
+
visibility: hidden;
|
|
60
|
+
}
|
|
61
|
+
.grow-wrap > textarea {
|
|
62
|
+
/* Firefox shows scrollbar on growth, you can hide like this. */
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
}
|
|
65
|
+
.grow-wrap > textarea,
|
|
66
|
+
.grow-wrap::after {
|
|
67
|
+
/* Identical styling required!! */
|
|
68
|
+
border: 1px solid black;
|
|
69
|
+
padding: 0.5rem;
|
|
70
|
+
font: inherit;
|
|
71
|
+
|
|
72
|
+
/* Place on top of each other */
|
|
73
|
+
grid-area: 1 / 1 / 2 / 2;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@tailwind base;
|
|
78
|
+
@tailwind components;
|
|
79
|
+
@tailwind utilities;
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This module is the entrypoint of @valbuild/ui until the package is built with
|
|
3
|
+
* Vite. It is used only as a shim during local development, and is actually not
|
|
4
|
+
* part of the build output meant for consumers.
|
|
5
|
+
*
|
|
6
|
+
* After building with Vite, this entrypoint is replaced by ./vite-index.tsx,
|
|
7
|
+
* which is optimized for consumers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export * from "./exports";
|
|
11
|
+
|
|
12
|
+
export function Style(): JSX.Element | null {
|
|
13
|
+
return <link rel="stylesheet" href="/api/val/static/style.css" />;
|
|
14
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This module is the entrypoint of @valbuild/ui/server until the package is
|
|
3
|
+
* built with Vite. It is used only as a shim during local development, and is
|
|
4
|
+
* actually not part of the build output meant for consumers.
|
|
5
|
+
*
|
|
6
|
+
* After building with Vite, this entrypoint is replaced by ./vite-server.tsx,
|
|
7
|
+
* which is optimized for consumers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { RequestHandler } from "express";
|
|
11
|
+
|
|
12
|
+
export function createRequestHandler(): RequestHandler {
|
|
13
|
+
if (typeof window === "undefined") {
|
|
14
|
+
const vite = (async () => {
|
|
15
|
+
const { fileURLToPath, URL: URL_noresolve } = await import("node:url");
|
|
16
|
+
const { createServer } = await import(/* @vite-ignore */ "v" + "ite");
|
|
17
|
+
const vite = await createServer({
|
|
18
|
+
root: fileURLToPath(new URL_noresolve("..", import.meta.url)),
|
|
19
|
+
configFile: fileURLToPath(
|
|
20
|
+
new URL_noresolve("../vite.config.ts", import.meta.url)
|
|
21
|
+
),
|
|
22
|
+
server: { middlewareMode: true },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return vite;
|
|
26
|
+
})();
|
|
27
|
+
return async (req, res, next) => {
|
|
28
|
+
if (req.url === "/style.css") {
|
|
29
|
+
const styleModule = await (await vite).ssrLoadModule("./src/index.css");
|
|
30
|
+
const style = styleModule.default as string;
|
|
31
|
+
res.statusCode = 200;
|
|
32
|
+
res.setHeader("Content-Type", "text/css");
|
|
33
|
+
return res.end(style);
|
|
34
|
+
} else {
|
|
35
|
+
return next();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
} else {
|
|
39
|
+
throw Error("Cannot get middleware in browser");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Meta, Story } from "@storybook/react";
|
|
2
|
+
import Underline from "../assets/icons/Underline";
|
|
3
|
+
import Button, { ButtonProps } from "../components/Button";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: "Button",
|
|
7
|
+
component: Button,
|
|
8
|
+
} as Meta;
|
|
9
|
+
|
|
10
|
+
const Template: Story<ButtonProps> = (args) => <Button {...args} />;
|
|
11
|
+
|
|
12
|
+
export const ButtonStory = Template.bind({});
|
|
13
|
+
ButtonStory.args = {
|
|
14
|
+
children: <div>Button</div>,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const ButtonWithIcon = Template.bind({});
|
|
18
|
+
ButtonWithIcon.args = {
|
|
19
|
+
icon: <Underline />,
|
|
20
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Meta, Story } from "@storybook/react";
|
|
2
|
+
import Checkbox, { CheckboxProps } from "../components/Checkbox";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: "Checkbox",
|
|
6
|
+
component: Checkbox,
|
|
7
|
+
} as Meta;
|
|
8
|
+
|
|
9
|
+
const Template: Story<CheckboxProps> = (args) => <Checkbox {...args} />;
|
|
10
|
+
|
|
11
|
+
export const CheckboxStory = Template.bind({});
|
|
12
|
+
CheckboxStory.args = {
|
|
13
|
+
label: "Check me",
|
|
14
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Meta, Story } from "@storybook/react";
|
|
2
|
+
import Italic from "../assets/icons/Italic";
|
|
3
|
+
import Dropdown, { DropdownProps } from "../components/Dropdown";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: "Dropdown",
|
|
7
|
+
component: Dropdown,
|
|
8
|
+
} as Meta;
|
|
9
|
+
|
|
10
|
+
const Template: Story<DropdownProps> = (args) => <Dropdown {...args} />;
|
|
11
|
+
|
|
12
|
+
export const DropdownStory = Template.bind({});
|
|
13
|
+
DropdownStory.args = {
|
|
14
|
+
label: "Font",
|
|
15
|
+
options: ["Arial", "Times New Roman", "Courier New"],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const DropdownWithIcon = Template.bind({});
|
|
19
|
+
DropdownWithIcon.args = {
|
|
20
|
+
label: "Font",
|
|
21
|
+
options: ["Arial", "Times New Roman", "Courier New"],
|
|
22
|
+
icon: <Italic />,
|
|
23
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { Meta } from "@storybook/blocks";
|
|
2
|
+
import Code from "./assets/code-brackets.svg";
|
|
3
|
+
import Colors from "./assets/colors.svg";
|
|
4
|
+
import Comments from "./assets/comments.svg";
|
|
5
|
+
import Direction from "./assets/direction.svg";
|
|
6
|
+
import Flow from "./assets/flow.svg";
|
|
7
|
+
import Plugin from "./assets/plugin.svg";
|
|
8
|
+
import Repo from "./assets/repo.svg";
|
|
9
|
+
import StackAlt from "./assets/stackalt.svg";
|
|
10
|
+
|
|
11
|
+
<Meta title="Example/Introduction" />
|
|
12
|
+
|
|
13
|
+
<style>
|
|
14
|
+
{`
|
|
15
|
+
.subheading {
|
|
16
|
+
--mediumdark: '#999999';
|
|
17
|
+
font-weight: 700;
|
|
18
|
+
font-size: 13px;
|
|
19
|
+
color: #999;
|
|
20
|
+
letter-spacing: 6px;
|
|
21
|
+
line-height: 24px;
|
|
22
|
+
text-transform: uppercase;
|
|
23
|
+
margin-bottom: 12px;
|
|
24
|
+
margin-top: 40px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.link-list {
|
|
28
|
+
display: grid;
|
|
29
|
+
grid-template-columns: 1fr;
|
|
30
|
+
grid-template-rows: 1fr 1fr;
|
|
31
|
+
row-gap: 10px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@media (min-width: 620px) {
|
|
35
|
+
.link-list {
|
|
36
|
+
row-gap: 20px;
|
|
37
|
+
column-gap: 20px;
|
|
38
|
+
grid-template-columns: 1fr 1fr;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@media all and (-ms-high-contrast:none) {
|
|
43
|
+
.link-list {
|
|
44
|
+
display: -ms-grid;
|
|
45
|
+
-ms-grid-columns: 1fr 1fr;
|
|
46
|
+
-ms-grid-rows: 1fr 1fr;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.link-item {
|
|
51
|
+
display: block;
|
|
52
|
+
padding: 20px;
|
|
53
|
+
border: 1px solid #00000010;
|
|
54
|
+
border-radius: 5px;
|
|
55
|
+
transition: background 150ms ease-out, border 150ms ease-out, transform 150ms ease-out;
|
|
56
|
+
color: #333333;
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: flex-start;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.link-item:hover {
|
|
62
|
+
border-color: #1EA7FD50;
|
|
63
|
+
transform: translate3d(0, -3px, 0);
|
|
64
|
+
box-shadow: rgba(0, 0, 0, 0.08) 0 3px 10px 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.link-item:active {
|
|
68
|
+
border-color: #1EA7FD;
|
|
69
|
+
transform: translate3d(0, 0, 0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.link-item strong {
|
|
73
|
+
font-weight: 700;
|
|
74
|
+
display: block;
|
|
75
|
+
margin-bottom: 2px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.link-item img {
|
|
79
|
+
height: 40px;
|
|
80
|
+
width: 40px;
|
|
81
|
+
margin-right: 15px;
|
|
82
|
+
flex: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.link-item span,
|
|
86
|
+
.link-item p {
|
|
87
|
+
margin: 0;
|
|
88
|
+
font-size: 14px;
|
|
89
|
+
line-height: 20px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.tip {
|
|
93
|
+
display: inline-block;
|
|
94
|
+
border-radius: 1em;
|
|
95
|
+
font-size: 11px;
|
|
96
|
+
line-height: 12px;
|
|
97
|
+
font-weight: 700;
|
|
98
|
+
background: #E7FDD8;
|
|
99
|
+
color: #66BF3C;
|
|
100
|
+
padding: 4px 12px;
|
|
101
|
+
margin-right: 10px;
|
|
102
|
+
vertical-align: top;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.tip-wrapper {
|
|
106
|
+
font-size: 13px;
|
|
107
|
+
line-height: 20px;
|
|
108
|
+
margin-top: 40px;
|
|
109
|
+
margin-bottom: 40px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.tip-wrapper code {
|
|
113
|
+
font-size: 12px;
|
|
114
|
+
display: inline-block;
|
|
115
|
+
}
|
|
116
|
+
`}
|
|
117
|
+
</style>
|
|
118
|
+
|
|
119
|
+
# Welcome to Storybook
|
|
120
|
+
|
|
121
|
+
Storybook helps you build UI components in isolation from your app's business logic, data, and context.
|
|
122
|
+
That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA.
|
|
123
|
+
|
|
124
|
+
Browse example stories now by navigating to them in the sidebar.
|
|
125
|
+
View their code in the `stories` directory to learn how they work.
|
|
126
|
+
We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages.
|
|
127
|
+
|
|
128
|
+
<div className="subheading">Configure</div>
|
|
129
|
+
|
|
130
|
+
<div className="link-list">
|
|
131
|
+
<a
|
|
132
|
+
className="link-item"
|
|
133
|
+
href="https://storybook.js.org/docs/react/addons/addon-types"
|
|
134
|
+
target="_blank"
|
|
135
|
+
>
|
|
136
|
+
<img src={Plugin} alt="plugin" />
|
|
137
|
+
<span>
|
|
138
|
+
<strong>Presets for popular tools</strong>
|
|
139
|
+
Easy setup for TypeScript, SCSS and more.
|
|
140
|
+
</span>
|
|
141
|
+
</a>
|
|
142
|
+
<a
|
|
143
|
+
className="link-item"
|
|
144
|
+
href="https://storybook.js.org/docs/react/configure/webpack"
|
|
145
|
+
target="_blank"
|
|
146
|
+
>
|
|
147
|
+
<img src={StackAlt} alt="Build" />
|
|
148
|
+
<span>
|
|
149
|
+
<strong>Build configuration</strong>
|
|
150
|
+
How to customize webpack and Babel
|
|
151
|
+
</span>
|
|
152
|
+
</a>
|
|
153
|
+
<a
|
|
154
|
+
className="link-item"
|
|
155
|
+
href="https://storybook.js.org/docs/react/configure/styling-and-css"
|
|
156
|
+
target="_blank"
|
|
157
|
+
>
|
|
158
|
+
<img src={Colors} alt="colors" />
|
|
159
|
+
<span>
|
|
160
|
+
<strong>Styling</strong>
|
|
161
|
+
How to load and configure CSS libraries
|
|
162
|
+
</span>
|
|
163
|
+
</a>
|
|
164
|
+
<a
|
|
165
|
+
className="link-item"
|
|
166
|
+
href="https://storybook.js.org/docs/react/get-started/setup#configure-storybook-for-your-stack"
|
|
167
|
+
target="_blank"
|
|
168
|
+
>
|
|
169
|
+
<img src={Flow} alt="flow" />
|
|
170
|
+
<span>
|
|
171
|
+
<strong>Data</strong>
|
|
172
|
+
Providers and mocking for data libraries
|
|
173
|
+
</span>
|
|
174
|
+
</a>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div className="subheading">Learn</div>
|
|
178
|
+
|
|
179
|
+
<div className="link-list">
|
|
180
|
+
<a className="link-item" href="https://storybook.js.org/docs" target="_blank">
|
|
181
|
+
<img src={Repo} alt="repo" />
|
|
182
|
+
<span>
|
|
183
|
+
<strong>Storybook documentation</strong>
|
|
184
|
+
Configure, customize, and extend
|
|
185
|
+
</span>
|
|
186
|
+
</a>
|
|
187
|
+
<a
|
|
188
|
+
className="link-item"
|
|
189
|
+
href="https://storybook.js.org/tutorials/"
|
|
190
|
+
target="_blank"
|
|
191
|
+
>
|
|
192
|
+
<img src={Direction} alt="direction" />
|
|
193
|
+
<span>
|
|
194
|
+
<strong>In-depth guides</strong>
|
|
195
|
+
Best practices from leading teams
|
|
196
|
+
</span>
|
|
197
|
+
</a>
|
|
198
|
+
<a
|
|
199
|
+
className="link-item"
|
|
200
|
+
href="https://github.com/storybookjs/storybook"
|
|
201
|
+
target="_blank"
|
|
202
|
+
>
|
|
203
|
+
<img src={Code} alt="code" />
|
|
204
|
+
<span>
|
|
205
|
+
<strong>GitHub project</strong>
|
|
206
|
+
View the source and add issues
|
|
207
|
+
</span>
|
|
208
|
+
</a>
|
|
209
|
+
<a className="link-item" href="https://discord.gg/storybook" target="_blank">
|
|
210
|
+
<img src={Comments} alt="comments" />
|
|
211
|
+
<span>
|
|
212
|
+
<strong>Discord chat</strong>
|
|
213
|
+
Chat with maintainers and the community
|
|
214
|
+
</span>
|
|
215
|
+
</a>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<div className="tip-wrapper">
|
|
219
|
+
<span className="tip">Tip</span>Edit the Markdown in{" "}
|
|
220
|
+
<code>stories/Introduction.stories.mdx</code>
|
|
221
|
+
</div>
|