@ryuzaki13/react-foundation-ui 1.0.11 → 1.0.12
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/README.md +457 -10
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/styles/config/_index.scss +77 -77
- package/src/styles/mixins/_mixin.scss +4 -4
- package/storybook-static/project.json +1 -1
package/README.md
CHANGED
|
@@ -1,26 +1,58 @@
|
|
|
1
1
|
# @ryuzaki13/react-foundation-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React UI primitives, выделенные из бывшего `src/shared/ui`.
|
|
4
4
|
|
|
5
|
-
Пакет не
|
|
5
|
+
Пакет публикуется в npm как обычная публичная библиотека, но его основная цель практическая: разделить повторно используемый UI-слой между собственными проектами автора. Это не попытка сделать универсальную дизайн-систему для всех сценариев. API, стили и Storybook в первую очередь оптимизируются под семейство проектов, где рядом используются `@ryuzaki13/react-foundation-lib` и `@ryuzaki13/react-foundation-api`.
|
|
6
|
+
|
|
7
|
+
## Импорты
|
|
8
|
+
|
|
9
|
+
Корневой импорт намеренно не открыт. Используйте точечные entrypoints:
|
|
6
10
|
|
|
7
11
|
```tsx
|
|
8
|
-
import "@ryuzaki13/react-foundation-ui/styles.css";
|
|
9
12
|
import { Button } from "@ryuzaki13/react-foundation-ui/button";
|
|
13
|
+
import { ContextMenu } from "@ryuzaki13/react-foundation-ui/context-menu";
|
|
14
|
+
import type { UiControlProps } from "@ryuzaki13/react-foundation-ui/types";
|
|
10
15
|
```
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
Стили подключаются отдельно и один раз на уровне host-приложения.
|
|
18
|
+
|
|
19
|
+
## Подключение стилей
|
|
20
|
+
|
|
21
|
+
### Готовый CSS
|
|
22
|
+
|
|
23
|
+
Самый простой вариант подходит для песочниц или приложений, которым достаточно дефолтных токенов пакета:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import "@ryuzaki13/react-foundation-ui/styles.css";
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
В этом режиме значения CSS-переменных уже скомпилированы в `dist/styles.css`. Host-проект может переопределять их обычным CSS поверх, но это runtime override, а не build-time настройка.
|
|
30
|
+
|
|
31
|
+
### Sass с настройкой host-проекта
|
|
32
|
+
|
|
33
|
+
Для рабочих проектов предпочтительнее Sass API. Он позволяет задать значения переменных на этапе сборки:
|
|
13
34
|
|
|
14
35
|
```scss
|
|
15
36
|
@use "@ryuzaki13/react-foundation-ui/styles/config" with (
|
|
16
37
|
$root-token-overrides: (
|
|
17
|
-
"--font-family": (
|
|
38
|
+
"--font-family": (
|
|
39
|
+
"Inter",
|
|
40
|
+
sans-serif
|
|
41
|
+
),
|
|
42
|
+
"--font-family-mono": (
|
|
43
|
+
"JetBrains Mono",
|
|
44
|
+
monospace
|
|
45
|
+
),
|
|
18
46
|
"--radius-md": 8px
|
|
19
47
|
),
|
|
20
48
|
$light-theme-overrides: (
|
|
21
49
|
tokens: (
|
|
22
50
|
"--surface-0": #ffffff,
|
|
23
|
-
"--surface-1": #
|
|
51
|
+
"--surface-1": #f6f7f9,
|
|
52
|
+
"--surface-2": #eceff3,
|
|
53
|
+
"--content-0": #111827,
|
|
54
|
+
"--content-1": #374151,
|
|
55
|
+
"--content-2": #6b7280
|
|
24
56
|
)
|
|
25
57
|
)
|
|
26
58
|
);
|
|
@@ -28,10 +60,425 @@ import { Button } from "@ryuzaki13/react-foundation-ui/button";
|
|
|
28
60
|
@use "@ryuzaki13/react-foundation-ui/styles.scss";
|
|
29
61
|
```
|
|
30
62
|
|
|
31
|
-
|
|
63
|
+
Важно: `styles/config` должен быть настроен до первого подключения любых foundation styles в Sass module graph. Иначе Sass уже загрузит конфиг с дефолтами и повторная настройка через `with (...)` не сработает.
|
|
64
|
+
|
|
65
|
+
### Host со своими темами
|
|
66
|
+
|
|
67
|
+
Если проект сам управляет темами, например через `data-theme="light:default"`, `data-scheme="light"` и `data-theme-mode="default"`, лучше подключать базовый слой отдельно от selectors:
|
|
68
|
+
|
|
69
|
+
```scss
|
|
70
|
+
@use "@ryuzaki13/react-foundation-ui/styles/config" with (
|
|
71
|
+
$root-token-overrides: (
|
|
72
|
+
"--font-family": (
|
|
73
|
+
"Inter",
|
|
74
|
+
sans-serif
|
|
75
|
+
)
|
|
76
|
+
),
|
|
77
|
+
$light-theme-overrides: (
|
|
78
|
+
tokens: (
|
|
79
|
+
"--surface-0": #ffffff,
|
|
80
|
+
"--surface-1": #f6f7f9,
|
|
81
|
+
"--surface-2": #eceff3
|
|
82
|
+
)
|
|
83
|
+
),
|
|
84
|
+
$dark-theme-overrides: (
|
|
85
|
+
tokens: (
|
|
86
|
+
"--surface-0": #0b0b0b,
|
|
87
|
+
"--surface-1": #151515,
|
|
88
|
+
"--surface-2": #202020
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
@use "@ryuzaki13/react-foundation-ui/styles/foundation";
|
|
94
|
+
@use "@ryuzaki13/react-foundation-ui/styles/themes" as foundationThemes;
|
|
95
|
+
|
|
96
|
+
:root[data-theme="light:default"] {
|
|
97
|
+
@include foundationThemes.light-theme;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
:root[data-theme="dark:default"] {
|
|
101
|
+
@include foundationThemes.dark-theme;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`styles/foundation` подключает normalize, root tokens, base styles и utility classes, но не навязывает дефолтные selectors `data-theme="light"` / `data-theme="dark"`. Selectors остаются ответственностью host-приложения.
|
|
106
|
+
|
|
107
|
+
Обычно в host-проекте это оформляется отдельным entrypoint, например `src/app/styles/shared.scss`, который сначала настраивает foundation config, затем подключает foundation styles, темы и доменные CSS-переменные приложения.
|
|
108
|
+
|
|
109
|
+
## Storybook
|
|
110
|
+
|
|
111
|
+
Stories и MDX не экспортируются как public API пакета. Вместо этого пакет собирает готовую статическую документацию:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm run build-storybook
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Результат лежит в `storybook-static` и включается в npm package через `files`. После установки пакета host-проект может отдать эту директорию:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx http-server node_modules/@ryuzaki13/react-foundation-ui/storybook-static -p 6007
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Такой запуск покажет готовые stories пакета, но не подмешает theme tokens конкретного host-проекта. Если нужно видеть компоненты в проектной теме, host должен запускать небольшую обертку:
|
|
124
|
+
|
|
125
|
+
1. отдать файлы из `node_modules/@ryuzaki13/react-foundation-ui/storybook-static`;
|
|
126
|
+
2. скомпилировать свой SCSS entrypoint, например `src/app/styles/shared.scss`;
|
|
127
|
+
3. внедрить ссылку на host CSS в `iframe.html`;
|
|
128
|
+
4. выставить нужные атрибуты на `document.documentElement`: `data-theme`, `data-scheme`, `data-theme-mode` и связанные accessibility presets;
|
|
129
|
+
5. отдавать host public assets, например шрифты, не перекрывая `storybook-static/assets`.
|
|
130
|
+
|
|
131
|
+
Именно host отвечает за конкретные значения CSS-переменных. Это позволяет одному и тому же Storybook пакета отображаться по-разному в разных проектах без копирования stories и MDX.
|
|
132
|
+
|
|
133
|
+
Пример обертки для запуска storybook в host проекте:
|
|
134
|
+
|
|
135
|
+
```mjs
|
|
136
|
+
#!/usr/bin/env node
|
|
137
|
+
|
|
138
|
+
// tools/serveFoundationUiStorybook.mjs
|
|
139
|
+
|
|
140
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
141
|
+
import { readFile, stat } from "node:fs/promises";
|
|
142
|
+
import { createServer } from "node:http";
|
|
143
|
+
import { createRequire } from "node:module";
|
|
144
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
145
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
146
|
+
|
|
147
|
+
const projectRoot = process.cwd();
|
|
148
|
+
const projectRequire = createRequire(join(projectRoot, "package.json"));
|
|
149
|
+
const options = parseArgs(process.argv.slice(2));
|
|
150
|
+
const styleEntry = resolve(projectRoot, options.styles);
|
|
151
|
+
const publicRoot = resolve(projectRoot, options.public);
|
|
152
|
+
const storybookRoot = resolveFoundationStorybookRoot();
|
|
153
|
+
|
|
154
|
+
await assertFile(styleEntry, "Style entry");
|
|
155
|
+
await assertDirectory(storybookRoot, "Foundation UI Storybook");
|
|
156
|
+
await compileHostStyles();
|
|
157
|
+
|
|
158
|
+
const server = createServer(async (request, response) => {
|
|
159
|
+
try {
|
|
160
|
+
const requestUrl = new URL(request.url ?? "/", `http://${options.host}:${options.port}`);
|
|
161
|
+
|
|
162
|
+
if (requestUrl.pathname === "/__foundation-host-styles.css") {
|
|
163
|
+
const css = await compileHostStyles();
|
|
164
|
+
send(response, 200, css, "text/css; charset=utf-8");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (requestUrl.pathname.startsWith("/assets/")) {
|
|
169
|
+
const storybookAssetPath = resolve(storybookRoot, `.${requestUrl.pathname}`);
|
|
170
|
+
|
|
171
|
+
if (isInside(storybookRoot, storybookAssetPath) && isFile(storybookAssetPath)) {
|
|
172
|
+
await serveFile(response, storybookRoot, requestUrl.pathname);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await serveFile(response, publicRoot, requestUrl.pathname);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const pathname = requestUrl.pathname === "/" ? "/index.html" : requestUrl.pathname;
|
|
181
|
+
|
|
182
|
+
if (pathname === "/iframe.html") {
|
|
183
|
+
const html = await readFile(join(storybookRoot, "iframe.html"), "utf8");
|
|
184
|
+
send(response, 200, injectHostPreview(html), "text/html; charset=utf-8");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await serveFile(response, storybookRoot, pathname);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const status = error?.code === "ENOENT" ? 404 : 500;
|
|
191
|
+
send(response, status, status === 404 ? "Not found" : String(error?.stack ?? error), "text/plain; charset=utf-8");
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
server.listen(options.port, options.host, () => {
|
|
196
|
+
console.log(`Foundation UI Storybook: http://${options.host}:${options.port}`);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
function parseArgs(args) {
|
|
200
|
+
const result = {
|
|
201
|
+
host: "127.0.0.1",
|
|
202
|
+
port: 6007,
|
|
203
|
+
public: "public",
|
|
204
|
+
styles: "src/app/styles/shared.scss",
|
|
205
|
+
theme: "light:default",
|
|
206
|
+
scheme: undefined,
|
|
207
|
+
themeMode: undefined,
|
|
208
|
+
attrs: []
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
212
|
+
const arg = args[index];
|
|
213
|
+
|
|
214
|
+
if (arg === "--host") result.host = readArgValue(args, ++index, arg);
|
|
215
|
+
else if (arg === "--port") result.port = Number(readArgValue(args, ++index, arg));
|
|
216
|
+
else if (arg === "--public") result.public = readArgValue(args, ++index, arg);
|
|
217
|
+
else if (arg === "--styles") result.styles = readArgValue(args, ++index, arg);
|
|
218
|
+
else if (arg === "--theme") result.theme = readArgValue(args, ++index, arg);
|
|
219
|
+
else if (arg === "--scheme") result.scheme = readArgValue(args, ++index, arg);
|
|
220
|
+
else if (arg === "--theme-mode") result.themeMode = readArgValue(args, ++index, arg);
|
|
221
|
+
else if (arg === "--attr") result.attrs.push(readArgValue(args, ++index, arg));
|
|
222
|
+
else if (arg === "--help") printHelpAndExit();
|
|
223
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!Number.isInteger(result.port) || result.port <= 0) {
|
|
227
|
+
throw new Error(`Invalid --port value: ${result.port}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function readArgValue(args, index, name) {
|
|
234
|
+
const value = args[index];
|
|
235
|
+
|
|
236
|
+
if (!value || value.startsWith("--")) {
|
|
237
|
+
throw new Error(`Missing value for ${name}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return value;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function printHelpAndExit() {
|
|
244
|
+
console.log(
|
|
245
|
+
[
|
|
246
|
+
"Usage: node tools/serveFoundationUiStorybook.mjs [options]",
|
|
247
|
+
"",
|
|
248
|
+
"Options:",
|
|
249
|
+
" --styles <path> Host SCSS/CSS entry. Default: src/app/styles/shared.scss",
|
|
250
|
+
" --public <path> Host public directory. Default: public",
|
|
251
|
+
" --theme <value> data-theme value. Default: light:default",
|
|
252
|
+
" --scheme <value> data-scheme value. Default: first part of --theme",
|
|
253
|
+
" --theme-mode <value> data-theme-mode value. Default: second part of --theme",
|
|
254
|
+
" --attr <name=value> Additional html attribute. Can be repeated.",
|
|
255
|
+
" --host <host> Server host. Default: 127.0.0.1",
|
|
256
|
+
" --port <port> Server port. Default: 6007"
|
|
257
|
+
].join("\n")
|
|
258
|
+
);
|
|
259
|
+
process.exit(0);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function resolveFoundationStorybookRoot() {
|
|
263
|
+
const packageJsonPath = projectRequire.resolve("@ryuzaki13/react-foundation-ui/package.json");
|
|
264
|
+
return join(dirname(packageJsonPath), "storybook-static");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function assertFile(filePath, label) {
|
|
268
|
+
const fileStat = await stat(filePath);
|
|
269
|
+
|
|
270
|
+
if (!fileStat.isFile()) {
|
|
271
|
+
throw new Error(`${label} is not a file: ${filePath}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function assertDirectory(directoryPath, label) {
|
|
276
|
+
const directoryStat = await stat(directoryPath);
|
|
277
|
+
|
|
278
|
+
if (!directoryStat.isDirectory()) {
|
|
279
|
+
throw new Error(`${label} is not a directory: ${directoryPath}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function compileHostStyles() {
|
|
284
|
+
const extension = extname(styleEntry);
|
|
285
|
+
|
|
286
|
+
if (extension === ".css") {
|
|
287
|
+
return readFile(styleEntry, "utf8");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const sass = await loadSass();
|
|
291
|
+
const result = sass.compile(styleEntry, {
|
|
292
|
+
style: "compressed",
|
|
293
|
+
loadPaths: [projectRoot, join(projectRoot, "src"), join(projectRoot, "node_modules")],
|
|
294
|
+
importers: [createSourceAliasImporter()]
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return result.css;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function loadSass() {
|
|
301
|
+
for (const packageName of ["sass-embedded", "sass"]) {
|
|
302
|
+
try {
|
|
303
|
+
const module = await import(projectRequire.resolve(packageName));
|
|
304
|
+
return module.default ?? module;
|
|
305
|
+
} catch {
|
|
306
|
+
// Try the next Sass implementation from the host project.
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
throw new Error("Install `sass` or `sass-embedded` in the host project to compile Storybook styles.");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function createSourceAliasImporter() {
|
|
314
|
+
return {
|
|
315
|
+
canonicalize(url, context) {
|
|
316
|
+
if (url.startsWith("@/")) {
|
|
317
|
+
const filePath = resolveSassFile(resolve(projectRoot, "src", url.slice(2)));
|
|
318
|
+
return filePath ? pathToFileURL(filePath) : null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (context.containingUrl?.protocol === "file:") {
|
|
322
|
+
const containingPath = fileURLToPath(context.containingUrl);
|
|
323
|
+
const filePath = resolveSassFile(resolve(dirname(containingPath), url));
|
|
324
|
+
|
|
325
|
+
if (filePath && isInside(projectRoot, filePath)) {
|
|
326
|
+
return pathToFileURL(filePath);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return null;
|
|
331
|
+
},
|
|
332
|
+
load(canonicalUrl) {
|
|
333
|
+
const filePath = fileURLToPath(canonicalUrl);
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
contents: readFileSync(filePath, "utf8"),
|
|
337
|
+
syntax: resolveSassSyntax(filePath)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function resolveSassFile(filePath) {
|
|
344
|
+
const extension = extname(filePath);
|
|
345
|
+
const candidates = extension
|
|
346
|
+
? [filePath, join(dirname(filePath), `_${basename(filePath)}`)]
|
|
347
|
+
: [
|
|
348
|
+
filePath,
|
|
349
|
+
`${filePath}.scss`,
|
|
350
|
+
`${filePath}.sass`,
|
|
351
|
+
`${filePath}.css`,
|
|
352
|
+
join(dirname(filePath), `_${basename(filePath)}.scss`),
|
|
353
|
+
join(dirname(filePath), `_${basename(filePath)}.sass`),
|
|
354
|
+
join(filePath, "_index.scss"),
|
|
355
|
+
join(filePath, "_index.sass"),
|
|
356
|
+
join(filePath, "index.scss"),
|
|
357
|
+
join(filePath, "index.sass")
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
return candidates.find((candidate) => existsSync(candidate) && statSync(candidate).isFile());
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function isFile(filePath) {
|
|
364
|
+
return existsSync(filePath) && statSync(filePath).isFile();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function resolveSassSyntax(filePath) {
|
|
368
|
+
if (filePath.endsWith(".sass")) return "indented";
|
|
369
|
+
if (filePath.endsWith(".css")) return "css";
|
|
370
|
+
return "scss";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function injectHostPreview(html) {
|
|
374
|
+
const attributes = resolveHtmlAttributes();
|
|
375
|
+
const script = `<script>(()=>{const root=document.documentElement;const attrs=${JSON.stringify(attributes)};for(const [name,value] of Object.entries(attrs)){if(value)root.setAttribute(name,value);}})();</script>`;
|
|
376
|
+
const link = `<link rel="stylesheet" href="./__foundation-host-styles.css" data-foundation-host-styles>`;
|
|
377
|
+
|
|
378
|
+
return html.replace("</head>", `${script}\n${link}\n</head>`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function resolveHtmlAttributes() {
|
|
382
|
+
const [schemeFromTheme, modeFromTheme] = options.theme.split(":");
|
|
383
|
+
const attributes = {
|
|
384
|
+
"data-theme": options.theme,
|
|
385
|
+
"data-scheme": options.scheme ?? schemeFromTheme,
|
|
386
|
+
"data-theme-mode": options.themeMode ?? modeFromTheme ?? "default",
|
|
387
|
+
"data-contrast": "auto",
|
|
388
|
+
"data-motion": "auto",
|
|
389
|
+
"data-line-height": "normal",
|
|
390
|
+
"data-letter-spacing": "normal",
|
|
391
|
+
"data-word-spacing": "normal",
|
|
392
|
+
"data-paragraph-spacing": "normal"
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
for (const attr of options.attrs) {
|
|
396
|
+
const separatorIndex = attr.indexOf("=");
|
|
397
|
+
|
|
398
|
+
if (separatorIndex < 1) {
|
|
399
|
+
throw new Error(`Invalid --attr value: ${attr}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
attributes[attr.slice(0, separatorIndex)] = attr.slice(separatorIndex + 1);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return attributes;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function serveFile(response, root, requestPathname) {
|
|
409
|
+
const filePath = resolve(root, `.${requestPathname}`);
|
|
410
|
+
|
|
411
|
+
if (!isInside(root, filePath)) {
|
|
412
|
+
send(response, 403, "Forbidden", "text/plain; charset=utf-8");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const fileStat = await stat(filePath);
|
|
417
|
+
|
|
418
|
+
if (!fileStat.isFile()) {
|
|
419
|
+
send(response, 404, "Not found", "text/plain; charset=utf-8");
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
send(response, 200, await readFile(filePath), contentType(filePath));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function isInside(root, filePath) {
|
|
427
|
+
const pathFromRoot = relative(root, filePath);
|
|
428
|
+
return pathFromRoot === "" || (!pathFromRoot.startsWith("..") && !pathFromRoot.includes(`..${sep}`));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function contentType(filePath) {
|
|
432
|
+
const extension = extname(filePath);
|
|
433
|
+
|
|
434
|
+
if (extension === ".html") return "text/html; charset=utf-8";
|
|
435
|
+
if (extension === ".js") return "text/javascript; charset=utf-8";
|
|
436
|
+
if (extension === ".css") return "text/css; charset=utf-8";
|
|
437
|
+
if (extension === ".json") return "application/json; charset=utf-8";
|
|
438
|
+
if (extension === ".svg") return "image/svg+xml";
|
|
439
|
+
if (extension === ".png") return "image/png";
|
|
440
|
+
if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
|
|
441
|
+
if (extension === ".woff") return "font/woff";
|
|
442
|
+
if (extension === ".woff2") return "font/woff2";
|
|
443
|
+
|
|
444
|
+
return "application/octet-stream";
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function send(response, status, body, type) {
|
|
448
|
+
response.writeHead(status, {
|
|
449
|
+
"Cache-Control": "no-store",
|
|
450
|
+
"Content-Type": type
|
|
451
|
+
});
|
|
452
|
+
response.end(body);
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
И небольшой скрип запуска:
|
|
457
|
+
|
|
458
|
+
```json
|
|
459
|
+
{
|
|
460
|
+
"scripts": {
|
|
461
|
+
"serve-ui-storybook": "node tools/serveFoundationUiStorybook.mjs --styles src/app/styles/shared.scss --theme light:default --port 6007"
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Для разработки самого пакета:
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
npm run storybook
|
|
470
|
+
npm run build-storybook
|
|
471
|
+
npm run serve-storybook
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Для проверки npm-артефакта:
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
npm run pack:dry-run
|
|
478
|
+
```
|
|
32
479
|
|
|
33
|
-
|
|
480
|
+
## Зависимости
|
|
34
481
|
|
|
35
|
-
|
|
482
|
+
UI-пакет напрямую опирается на `@ryuzaki13/react-foundation-lib`.
|
|
36
483
|
|
|
37
|
-
|
|
484
|
+
Часть entrypoints с OData-компонентами импортирует `@ryuzaki13/react-foundation-api/odata`. Если host-проект использует такие компоненты, он должен установить совместимую версию `@ryuzaki13/react-foundation-api` рядом с UI-пакетом. В Storybook самого пакета эта зависимость используется для демонстрационных сценариев.
|