@conduction/docusaurus-preset 3.34.0 → 3.35.0
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/ANIMATION_ROADMAP.md +129 -0
- package/MISSING_COMPONENTS.md +11 -2
- package/package.json +3 -2
- package/src/components/AppMock/AppMock.module.css +1263 -0
- package/src/components/AppMock/__tests__/AppMock.render.test.js +148 -0
- package/src/components/AppMock/variants/DeciDeskMock.jsx +46 -5
- package/src/components/AppMock/variants/DocuDeskMock.jsx +39 -9
- package/src/components/AppMock/variants/DoriathMock.jsx +28 -13
- package/src/components/AppMock/variants/HermiqMock.jsx +35 -15
- package/src/components/AppMock/variants/HrmqMock.jsx +46 -28
- package/src/components/AppMock/variants/LarpingAppMock.jsx +13 -3
- package/src/components/AppMock/variants/OpenBuildMock.jsx +20 -5
- package/src/components/AppMock/variants/OpenCatalogiMock.jsx +23 -4
- package/src/components/AppMock/variants/OpenConnectorMock.jsx +25 -3
- package/src/components/AppMock/variants/OpenRegisterMock.jsx +13 -2
- package/src/components/AppMock/variants/ProcestMock.jsx +34 -7
- package/src/components/AppMock/variants/ShillinqMock.jsx +33 -17
- package/src/components/AppMock/variants/SoftwareCatalogMock.jsx +15 -5
- package/src/components/AppMock/variants/ZaakAfhandelAppMock.jsx +19 -4
- package/src/components/SidebarMock/SidebarMock.jsx +29 -2
- package/src/components/SidebarMock/__tests__/SidebarMock.render.test.js +115 -0
- package/src/components/WidgetMock/WidgetMock.jsx +35 -14
- package/src/components/WidgetMock/__tests__/WidgetMock.render.test.js +125 -0
- package/src/components/WidgetMock/variants/DocuDeskAnonymise.jsx +6 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SidebarMock.render.test.js — renders the real <SidebarMock> JSX to
|
|
3
|
+
* static markup and asserts on the wave-5 animation plumbing: the
|
|
4
|
+
* panel carries the `.sbLive` scope, `running={false}` puts `.static`
|
|
5
|
+
* on the panel itself (both standalone and embedded), and the
|
|
6
|
+
* travelling active-tab underline is present with the `--sb-ix` /
|
|
7
|
+
* `--sb-n` / `--sb-from` custom properties derived from the
|
|
8
|
+
* variant's active tab index.
|
|
9
|
+
*
|
|
10
|
+
* Same esbuild-bundle-then-renderToStaticMarkup technique as
|
|
11
|
+
* AppMock.render.test.js; CSS modules stubbed with an identity proxy.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const test = require('node:test');
|
|
17
|
+
const {before, after} = test;
|
|
18
|
+
const assert = require('node:assert/strict');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const fs = require('node:fs/promises');
|
|
21
|
+
const {build} = require('esbuild');
|
|
22
|
+
const React = require('react');
|
|
23
|
+
const {renderToStaticMarkup} = require('react-dom/server');
|
|
24
|
+
|
|
25
|
+
const COMPONENT = path.resolve(__dirname, '..', 'SidebarMock.jsx');
|
|
26
|
+
const PRESET_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
27
|
+
const SCRATCH = path.join(PRESET_ROOT, '.tmp-sidebar-mock-test');
|
|
28
|
+
|
|
29
|
+
/* Must stay in sync with the VARIANTS map in SidebarMock.jsx.
|
|
30
|
+
activeIx mirrors each variant's active-tab index. */
|
|
31
|
+
const KINDS = {
|
|
32
|
+
'procest-xwiki': 1,
|
|
33
|
+
'procest-timeline': 2,
|
|
34
|
+
'docudesk-signatures': 1,
|
|
35
|
+
'docudesk-pii-map': 2,
|
|
36
|
+
'openregister-metadata': 1,
|
|
37
|
+
'opencatalogi-publication-history': 1,
|
|
38
|
+
'openconnector-run-detail': 1,
|
|
39
|
+
'decidesk-decision': 1,
|
|
40
|
+
'nextcloud-activity': 0,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const cssModuleStub = {
|
|
44
|
+
name: 'css-module-stub',
|
|
45
|
+
setup(b) {
|
|
46
|
+
b.onResolve({filter: /\.module\.css$/}, (args) => ({path: args.path, namespace: 'css-stub'}));
|
|
47
|
+
b.onLoad({filter: /.*/, namespace: 'css-stub'}, () => ({
|
|
48
|
+
contents: 'export default new Proxy({}, {get: (_, p) => p});',
|
|
49
|
+
loader: 'js',
|
|
50
|
+
}));
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let SidebarMock;
|
|
55
|
+
|
|
56
|
+
before(async () => {
|
|
57
|
+
await fs.mkdir(SCRATCH, {recursive: true});
|
|
58
|
+
const tmpDir = await fs.mkdtemp(path.join(SCRATCH, 'run-'));
|
|
59
|
+
const outFile = path.join(tmpDir, 'bundle.cjs');
|
|
60
|
+
await build({
|
|
61
|
+
entryPoints: [COMPONENT],
|
|
62
|
+
outfile: outFile,
|
|
63
|
+
bundle: true,
|
|
64
|
+
format: 'cjs',
|
|
65
|
+
jsx: 'automatic',
|
|
66
|
+
jsxImportSource: 'react',
|
|
67
|
+
tsconfigRaw: {compilerOptions: {jsx: 'react-jsx', jsxImportSource: 'react'}},
|
|
68
|
+
platform: 'node',
|
|
69
|
+
external: ['react'],
|
|
70
|
+
plugins: [cssModuleStub],
|
|
71
|
+
logLevel: 'warning',
|
|
72
|
+
});
|
|
73
|
+
delete require.cache[require.resolve(outFile)];
|
|
74
|
+
SidebarMock = require(outFile).default;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
after(async () => {
|
|
78
|
+
await fs.rm(SCRATCH, {recursive: true, force: true});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function render(props) {
|
|
82
|
+
return renderToStaticMarkup(React.createElement(SidebarMock, props));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
test('SSR smoke: every kind renders the live panel, none falls through to Unknown sidebar', () => {
|
|
86
|
+
for (const kind of Object.keys(KINDS)) {
|
|
87
|
+
const html = render({kind});
|
|
88
|
+
assert.match(html, /detail rich sbLive/, `${kind}: missing sbLive panel scope`);
|
|
89
|
+
assert.doesNotMatch(html, /Unknown sidebar/, `${kind}: fell through to the unknown branch`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('running={false} puts static on the panel — standalone and embedded', () => {
|
|
94
|
+
assert.match(render({kind: 'procest-xwiki', running: false}), /detail rich sbLive static/);
|
|
95
|
+
assert.match(render({kind: 'procest-xwiki', running: false, embedded: true}), /detail rich sbLive static/);
|
|
96
|
+
assert.doesNotMatch(render({kind: 'procest-xwiki'}), /\bstatic\b/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('the travelling underline carries --sb-ix / --sb-n / --sb-from per variant', () => {
|
|
100
|
+
for (const [kind, ix] of Object.entries(KINDS)) {
|
|
101
|
+
const html = render({kind});
|
|
102
|
+
assert.match(html, /class="sb-underline"/, `${kind}: underline missing`);
|
|
103
|
+
assert.match(html, new RegExp(`--sb-ix:${ix}[;"]`), `${kind}: wrong active index`);
|
|
104
|
+
assert.match(html, /--sb-n:[0-9]/, `${kind}: tab count missing`);
|
|
105
|
+
const from = ix > 0 ? '-100%' : '100%';
|
|
106
|
+
assert.match(html, new RegExp(`--sb-from:${from}`), `${kind}: wrong travel origin`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('embedded mode drops the standalone frame but keeps the live panel', () => {
|
|
111
|
+
const html = render({kind: 'openregister-metadata', embedded: true});
|
|
112
|
+
assert.doesNotMatch(html, /smFrame/);
|
|
113
|
+
assert.match(html, /detail rich sbLive/);
|
|
114
|
+
assert.match(html, /class="sb-underline"/);
|
|
115
|
+
});
|
|
@@ -29,7 +29,19 @@
|
|
|
29
29
|
* - caption: boolean — adds a small
|
|
30
30
|
* label below
|
|
31
31
|
* the frame
|
|
32
|
+
* - running: boolean (default true) — false freezes the widget's
|
|
33
|
+
* animation on its meaningful static end state (same
|
|
34
|
+
* rendering `prefers-reduced-motion: reduce` gives).
|
|
35
|
+
* The CSS `.static` class on the frame stops keyframes.
|
|
32
36
|
* - className: string
|
|
37
|
+
*
|
|
38
|
+
* Every list-shaped variant (VARIANTS entries with `list: true`)
|
|
39
|
+
* shares one refresh primitive, styled in AppMock.module.css under
|
|
40
|
+
* `.wmLive`: a new row slides in at the top, the bottom row clips
|
|
41
|
+
* out, and the unread badge on the frame bumps as the row lands.
|
|
42
|
+
* Non-list variants (calendar grid, decks columns, activity graph,
|
|
43
|
+
* anonymise dropzone) opt out; the dropzone has its own `.zoneLive`
|
|
44
|
+
* marching-ants + file-drop treatment inside the variant itself.
|
|
33
45
|
*/
|
|
34
46
|
|
|
35
47
|
import React from 'react';
|
|
@@ -53,22 +65,22 @@ import NextcloudRss from './variants/NextcloudRss.jsx';
|
|
|
53
65
|
|
|
54
66
|
const VARIANTS = {
|
|
55
67
|
'docudesk-anonymise': { Component: DocuDeskAnonymise, label: 'DocuDesk · Anonymise drop' },
|
|
56
|
-
'docudesk-pending-sign': { Component: DocuDeskPendingSign, label: 'DocuDesk · Pending signatures' },
|
|
57
|
-
'procest-werkvoorraad': { Component: ProcestWerkvoorraad, label: 'Procest · Werkvoorraad' },
|
|
58
|
-
'procest-due-today': { Component: ProcestDueToday, label: 'Procest · Due today' },
|
|
68
|
+
'docudesk-pending-sign': { Component: DocuDeskPendingSign, label: 'DocuDesk · Pending signatures', list: true },
|
|
69
|
+
'procest-werkvoorraad': { Component: ProcestWerkvoorraad, label: 'Procest · Werkvoorraad', list: true },
|
|
70
|
+
'procest-due-today': { Component: ProcestDueToday, label: 'Procest · Due today', list: true },
|
|
59
71
|
'openregister-activity': { Component: OpenRegisterActivity, label: 'OpenRegister · Activity' },
|
|
60
|
-
'opencatalogi-publications':{ Component: OpenCatalogiPublications,label: 'OpenCatalogi · Recent publications' },
|
|
61
|
-
'openconnector-runs': { Component: OpenConnectorRuns, label: 'OpenConnector · Recent runs' },
|
|
62
|
-
'decidesk-actions': { Component: DeciDeskActions, label: 'DeciDesk · Action items' },
|
|
63
|
-
'pipelinq-deals': { Component: PipelinQDeals, label: 'PipelinQ · Deals closing' },
|
|
64
|
-
'nextcloud-mail': { Component: NextcloudMail, label: 'Nextcloud · Important mail' },
|
|
72
|
+
'opencatalogi-publications':{ Component: OpenCatalogiPublications,label: 'OpenCatalogi · Recent publications', list: true },
|
|
73
|
+
'openconnector-runs': { Component: OpenConnectorRuns, label: 'OpenConnector · Recent runs', list: true },
|
|
74
|
+
'decidesk-actions': { Component: DeciDeskActions, label: 'DeciDesk · Action items', list: true },
|
|
75
|
+
'pipelinq-deals': { Component: PipelinQDeals, label: 'PipelinQ · Deals closing', list: true },
|
|
76
|
+
'nextcloud-mail': { Component: NextcloudMail, label: 'Nextcloud · Important mail', list: true },
|
|
65
77
|
'nextcloud-calendar': { Component: NextcloudCalendar, label: 'Nextcloud · Calendar' },
|
|
66
|
-
'nextcloud-files': { Component: NextcloudFiles, label: 'Nextcloud · Recent files' },
|
|
78
|
+
'nextcloud-files': { Component: NextcloudFiles, label: 'Nextcloud · Recent files', list: true },
|
|
67
79
|
'nextcloud-decks': { Component: NextcloudDecks, label: 'Nextcloud · Decks' },
|
|
68
|
-
'nextcloud-rss': { Component: NextcloudRss, label: 'Nextcloud · RSS feed' },
|
|
80
|
+
'nextcloud-rss': { Component: NextcloudRss, label: 'Nextcloud · RSS feed', list: true },
|
|
69
81
|
};
|
|
70
82
|
|
|
71
|
-
export default function WidgetMock({ kind, size = 'md', caption = false, className }) {
|
|
83
|
+
export default function WidgetMock({ kind, size = 'md', caption = false, running = true, className }) {
|
|
72
84
|
const variant = VARIANTS[kind];
|
|
73
85
|
const wrap = [amStyles.am, styles.wm].filter(Boolean).join(' ');
|
|
74
86
|
if (!variant) {
|
|
@@ -80,12 +92,21 @@ export default function WidgetMock({ kind, size = 'md', caption = false, classNa
|
|
|
80
92
|
</div>
|
|
81
93
|
);
|
|
82
94
|
}
|
|
83
|
-
const { Component, label } = variant;
|
|
95
|
+
const { Component, label, list } = variant;
|
|
96
|
+
const frameCls = [
|
|
97
|
+
styles.wmFrame,
|
|
98
|
+
styles[`wm-size-${size}`],
|
|
99
|
+
list && amStyles.wmLive,
|
|
100
|
+
!running && amStyles.static,
|
|
101
|
+
].filter(Boolean).join(' ');
|
|
84
102
|
return (
|
|
85
103
|
<div className={wrap}>
|
|
86
104
|
<figure className={[styles.wmFigure, className].filter(Boolean).join(' ')}>
|
|
87
|
-
<div className={
|
|
88
|
-
|
|
105
|
+
<div className={frameCls}>
|
|
106
|
+
{/* Unread badge — list variants only; bumps when the fresh
|
|
107
|
+
row lands. Styled in AppMock.module.css (.wmBadge). */}
|
|
108
|
+
{list && <div className={amStyles.wmBadge}></div>}
|
|
109
|
+
<Component running={running} />
|
|
89
110
|
</div>
|
|
90
111
|
{caption && <figcaption className={styles.wmCaption}>{label}</figcaption>}
|
|
91
112
|
</figure>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WidgetMock.render.test.js — renders the real <WidgetMock> JSX to
|
|
3
|
+
* static markup (the SSR path Docusaurus takes at build time) and
|
|
4
|
+
* asserts on the output: every kind renders, the wave-5 `running`
|
|
5
|
+
* plumbing puts `.static` on the frame, the ten list-shaped variants
|
|
6
|
+
* carry the shared list-refresh scope (`.wmLive`) + unread badge,
|
|
7
|
+
* the non-list variants opt out, and the anonymise dropzone exposes
|
|
8
|
+
* its marching-ants + file-drop hooks.
|
|
9
|
+
*
|
|
10
|
+
* Uses the same esbuild-bundle-then-renderToStaticMarkup technique
|
|
11
|
+
* as AppMock.render.test.js. CSS modules are stubbed with an identity
|
|
12
|
+
* proxy (styles.foo → 'foo').
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const test = require('node:test');
|
|
18
|
+
const {before, after} = test;
|
|
19
|
+
const assert = require('node:assert/strict');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
const fs = require('node:fs/promises');
|
|
22
|
+
const {build} = require('esbuild');
|
|
23
|
+
const React = require('react');
|
|
24
|
+
const {renderToStaticMarkup} = require('react-dom/server');
|
|
25
|
+
|
|
26
|
+
const COMPONENT = path.resolve(__dirname, '..', 'WidgetMock.jsx');
|
|
27
|
+
const PRESET_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
28
|
+
const SCRATCH = path.join(PRESET_ROOT, '.tmp-widget-mock-test');
|
|
29
|
+
|
|
30
|
+
/* Must stay in sync with the VARIANTS map in WidgetMock.jsx. */
|
|
31
|
+
const LIST_KINDS = [
|
|
32
|
+
'docudesk-pending-sign', 'procest-werkvoorraad', 'procest-due-today',
|
|
33
|
+
'opencatalogi-publications', 'openconnector-runs', 'decidesk-actions',
|
|
34
|
+
'pipelinq-deals', 'nextcloud-mail', 'nextcloud-files', 'nextcloud-rss',
|
|
35
|
+
];
|
|
36
|
+
const NON_LIST_KINDS = [
|
|
37
|
+
'docudesk-anonymise', 'openregister-activity', 'nextcloud-calendar',
|
|
38
|
+
'nextcloud-decks',
|
|
39
|
+
];
|
|
40
|
+
const KINDS = [...LIST_KINDS, ...NON_LIST_KINDS];
|
|
41
|
+
|
|
42
|
+
const cssModuleStub = {
|
|
43
|
+
name: 'css-module-stub',
|
|
44
|
+
setup(b) {
|
|
45
|
+
b.onResolve({filter: /\.module\.css$/}, (args) => ({path: args.path, namespace: 'css-stub'}));
|
|
46
|
+
b.onLoad({filter: /.*/, namespace: 'css-stub'}, () => ({
|
|
47
|
+
contents: 'export default new Proxy({}, {get: (_, p) => p});',
|
|
48
|
+
loader: 'js',
|
|
49
|
+
}));
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let WidgetMock;
|
|
54
|
+
|
|
55
|
+
before(async () => {
|
|
56
|
+
await fs.mkdir(SCRATCH, {recursive: true});
|
|
57
|
+
const tmpDir = await fs.mkdtemp(path.join(SCRATCH, 'run-'));
|
|
58
|
+
const outFile = path.join(tmpDir, 'bundle.cjs');
|
|
59
|
+
await build({
|
|
60
|
+
entryPoints: [COMPONENT],
|
|
61
|
+
outfile: outFile,
|
|
62
|
+
bundle: true,
|
|
63
|
+
format: 'cjs',
|
|
64
|
+
jsx: 'automatic',
|
|
65
|
+
jsxImportSource: 'react',
|
|
66
|
+
tsconfigRaw: {compilerOptions: {jsx: 'react-jsx', jsxImportSource: 'react'}},
|
|
67
|
+
platform: 'node',
|
|
68
|
+
external: ['react'],
|
|
69
|
+
plugins: [cssModuleStub],
|
|
70
|
+
logLevel: 'warning',
|
|
71
|
+
});
|
|
72
|
+
delete require.cache[require.resolve(outFile)];
|
|
73
|
+
WidgetMock = require(outFile).default;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
after(async () => {
|
|
77
|
+
await fs.rm(SCRATCH, {recursive: true, force: true});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function render(props) {
|
|
81
|
+
return renderToStaticMarkup(React.createElement(WidgetMock, props));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
test('SSR smoke: every kind renders a frame, none falls through to Unknown widget', () => {
|
|
85
|
+
for (const kind of KINDS) {
|
|
86
|
+
const html = render({kind});
|
|
87
|
+
assert.match(html, /wmFrame/, `${kind}: frame missing`);
|
|
88
|
+
assert.doesNotMatch(html, /Unknown widget/, `${kind}: fell through to the unknown branch`);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('running={false} adds the static class to the frame; default omits it', () => {
|
|
93
|
+
for (const kind of KINDS) {
|
|
94
|
+
assert.match(render({kind, running: false}), /wmFrame[^"]*static/, `${kind}: no static frame`);
|
|
95
|
+
assert.doesNotMatch(render({kind}), /\bstatic\b/, `${kind}: static leaked into a running frame`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('the ten list variants carry the wmLive refresh scope and the unread badge', () => {
|
|
100
|
+
for (const kind of LIST_KINDS) {
|
|
101
|
+
const html = render({kind});
|
|
102
|
+
assert.match(html, /wmFrame[^"]*wmLive/, `${kind}: missing wmLive scope`);
|
|
103
|
+
assert.match(html, /class="wmBadge"/, `${kind}: missing unread badge`);
|
|
104
|
+
assert.match(html, /class="list"/, `${kind}: wmLive on a variant without a .list`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('non-list variants opt out of the refresh primitive', () => {
|
|
109
|
+
for (const kind of NON_LIST_KINDS) {
|
|
110
|
+
const html = render({kind});
|
|
111
|
+
assert.doesNotMatch(html, /wmLive/, `${kind}: wmLive on a non-list variant`);
|
|
112
|
+
assert.doesNotMatch(html, /wmBadge/, `${kind}: badge on a non-list variant`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('docudesk-anonymise: the dropzone carries the zoneLive scope and the dropping file hex', () => {
|
|
117
|
+
const html = render({kind: 'docudesk-anonymise'});
|
|
118
|
+
assert.match(html, /w w-upload zoneLive/);
|
|
119
|
+
assert.match(html, /class="zoneFile"/);
|
|
120
|
+
assert.match(html, /class="zone"/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('caption renders the variant label', () => {
|
|
124
|
+
assert.match(render({kind: 'procest-werkvoorraad', caption: true}), /Procest · Werkvoorraad/);
|
|
125
|
+
});
|
|
@@ -4,18 +4,23 @@
|
|
|
4
4
|
* Drop a document into the dashboard tile, get back a redacted copy.
|
|
5
5
|
* Visual: dashed cobalt drop-zone with a hex glyph. Status-pill is
|
|
6
6
|
* absent: this widget is action-first, not status-first.
|
|
7
|
+
*
|
|
8
|
+
* Animation (wave 5, `.zoneLive`): the dashed border marches, a file
|
|
9
|
+
* hex drops into the zone, and the zone flashes mint as the redacted
|
|
10
|
+
* copy is confirmed. Static frames show the resting dropzone.
|
|
7
11
|
*/
|
|
8
12
|
import React from 'react';
|
|
9
13
|
import styles from '../../AppMock/AppMock.module.css';
|
|
10
14
|
|
|
11
15
|
export default function DocuDeskAnonymise() {
|
|
12
16
|
return (
|
|
13
|
-
<div className={[styles.w, styles['w-upload']].join(' ')}>
|
|
17
|
+
<div className={[styles.w, styles['w-upload'], styles.zoneLive].join(' ')}>
|
|
14
18
|
<div className={styles.wHead}>
|
|
15
19
|
<div className={styles.h}></div>
|
|
16
20
|
<div className={styles.t}></div>
|
|
17
21
|
</div>
|
|
18
22
|
<div className={styles.zone}>
|
|
23
|
+
<div className={styles.zoneFile}></div>
|
|
19
24
|
<div className={styles.ico}></div>
|
|
20
25
|
<div className={styles.label}></div>
|
|
21
26
|
</div>
|