buner 0.0.2 → 1.0.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.
Files changed (175) hide show
  1. package/.env +15 -0
  2. package/.env.development +6 -0
  3. package/.env.eshn +5 -0
  4. package/README.md +141 -5
  5. package/bin/buner.js +566 -0
  6. package/cli/README.md +1 -0
  7. package/cli/buner.ts +234 -0
  8. package/cli/cli.ts +125 -0
  9. package/cli/create-app.ts +59 -0
  10. package/cli/helpers/copy.ts +62 -0
  11. package/cli/helpers/format-files.ts +189 -0
  12. package/cli/helpers/git.ts +77 -0
  13. package/cli/helpers/install.ts +26 -0
  14. package/cli/helpers/is-folder-empty.ts +40 -0
  15. package/cli/helpers/is-writeable.ts +14 -0
  16. package/cli/helpers/make-dir.ts +7 -0
  17. package/cli/helpers/validate-pkg.ts +17 -0
  18. package/cli/install-template.ts +77 -0
  19. package/eslint.config.mjs +187 -0
  20. package/index.html +44 -0
  21. package/integration.ts +179 -0
  22. package/migrate-scss.ts +42 -0
  23. package/package.json +135 -7
  24. package/prerender.ts +229 -0
  25. package/public/.nojekyll +1 -0
  26. package/public/400.html +1 -0
  27. package/public/401.html +21 -0
  28. package/public/403.html +252 -0
  29. package/public/404.css +51 -0
  30. package/public/404.html +29 -0
  31. package/public/__images__/awww.jpeg +0 -0
  32. package/public/__images__/bat-body.png +0 -0
  33. package/public/__images__/bat-wing.png +0 -0
  34. package/public/__images__/haunted-house-background.png +0 -0
  35. package/public/__images__/haunted-house-foreground.png +0 -0
  36. package/public/assets/fonts/crimson-text/CrimsonText-Bold.ttf +0 -0
  37. package/public/assets/fonts/crimson-text/CrimsonText-BoldItalic.ttf +0 -0
  38. package/public/assets/fonts/crimson-text/CrimsonText-Italic.ttf +0 -0
  39. package/public/assets/fonts/crimson-text/CrimsonText-Regular.ttf +0 -0
  40. package/public/assets/fonts/crimson-text/CrimsonText-SemiBold.ttf +0 -0
  41. package/public/assets/fonts/crimson-text/CrimsonText-SemiBoldItalic.ttf +0 -0
  42. package/public/assets/fonts/crimson-text/CrimsonText.woff2 +0 -0
  43. package/public/assets/fonts/crimson-text/OFL.txt +93 -0
  44. package/public/assets/fonts/work-sans/OFL.txt +93 -0
  45. package/public/assets/fonts/work-sans/README.txt +81 -0
  46. package/public/assets/fonts/work-sans/WorkSans-Italic-VariableFont_wght.ttf +0 -0
  47. package/public/assets/fonts/work-sans/WorkSans-VariableFont_wght.ttf +0 -0
  48. package/public/assets/fonts/work-sans/WorkSans.woff2 +0 -0
  49. package/public/assets/fonts/work-sans/static/WorkSans-Black.ttf +0 -0
  50. package/public/assets/fonts/work-sans/static/WorkSans-BlackItalic.ttf +0 -0
  51. package/public/assets/fonts/work-sans/static/WorkSans-Bold.ttf +0 -0
  52. package/public/assets/fonts/work-sans/static/WorkSans-BoldItalic.ttf +0 -0
  53. package/public/assets/fonts/work-sans/static/WorkSans-ExtraBold.ttf +0 -0
  54. package/public/assets/fonts/work-sans/static/WorkSans-ExtraBoldItalic.ttf +0 -0
  55. package/public/assets/fonts/work-sans/static/WorkSans-ExtraLight.ttf +0 -0
  56. package/public/assets/fonts/work-sans/static/WorkSans-ExtraLightItalic.ttf +0 -0
  57. package/public/assets/fonts/work-sans/static/WorkSans-Italic.ttf +0 -0
  58. package/public/assets/fonts/work-sans/static/WorkSans-Light.ttf +0 -0
  59. package/public/assets/fonts/work-sans/static/WorkSans-LightItalic.ttf +0 -0
  60. package/public/assets/fonts/work-sans/static/WorkSans-Medium.ttf +0 -0
  61. package/public/assets/fonts/work-sans/static/WorkSans-MediumItalic.ttf +0 -0
  62. package/public/assets/fonts/work-sans/static/WorkSans-Regular.ttf +0 -0
  63. package/public/assets/fonts/work-sans/static/WorkSans-SemiBold.ttf +0 -0
  64. package/public/assets/fonts/work-sans/static/WorkSans-SemiBoldItalic.ttf +0 -0
  65. package/public/assets/fonts/work-sans/static/WorkSans-Thin.ttf +0 -0
  66. package/public/assets/fonts/work-sans/static/WorkSans-ThinItalic.ttf +0 -0
  67. package/public/assets/images/icons.svg +67 -0
  68. package/public/assets/images/logo.svg +14 -0
  69. package/public/assets/images/root.svg +49 -0
  70. package/public/assets/vendors/axios@0.24.0/axios.js +2275 -0
  71. package/public/assets/vendors/axios@0.24.0/axios.map +1 -0
  72. package/public/assets/vendors/axios@0.24.0/axios.min.js +2 -0
  73. package/public/assets/vendors/axios@0.24.0/axios.min.map +1 -0
  74. package/public/favicon.ico +0 -0
  75. package/public/favicon.svg +3 -0
  76. package/public/icon-128.png +0 -0
  77. package/public/icon-16.png +0 -0
  78. package/public/icon-192.png +0 -0
  79. package/public/icon-48.png +0 -0
  80. package/public/icon-512.png +0 -0
  81. package/public/json/avatar.json +42 -0
  82. package/public/manifest.webmanifest +29 -0
  83. package/public/mockServiceWorker.js +349 -0
  84. package/public/pl-states.svg +4 -0
  85. package/public/samples/01.svg +1 -0
  86. package/public/samples/Airbnb.svg +3 -0
  87. package/public/samples/Facebook.svg +3 -0
  88. package/public/samples/Google.svg +8 -0
  89. package/public/samples/Microsoft.svg +7 -0
  90. package/public/samples/Spotify.svg +3 -0
  91. package/public/samples/alexandra-stolz.svg +35 -0
  92. package/public/samples/browserconfig.xml +9 -0
  93. package/public/samples/cliff-curtis.jpg +0 -0
  94. package/public/samples/emilia-clarke.jpg +0 -0
  95. package/public/samples/favicon.ico +0 -0
  96. package/public/samples/icons/android-chrome-192x192.png +0 -0
  97. package/public/samples/icons/apple-touch-icon.png +0 -0
  98. package/public/samples/icons/favicon-144x144.png +0 -0
  99. package/public/samples/icons/favicon-150x150.png +0 -0
  100. package/public/samples/icons/favicon-16x16.png +0 -0
  101. package/public/samples/icons/favicon-32x32.png +0 -0
  102. package/public/samples/icons/favicon-48x48.png +0 -0
  103. package/public/samples/icons/favicon-70x70.png +0 -0
  104. package/public/samples/icons/favicon.ico +0 -0
  105. package/public/samples/image-1.svg +166 -0
  106. package/public/samples/image-2.svg +110 -0
  107. package/public/samples/image-3.svg +113 -0
  108. package/public/samples/janet-bray.svg +36 -0
  109. package/public/samples/kate-winslet.jpg +0 -0
  110. package/public/samples/manifest.json +19 -0
  111. package/public/samples/michelle-yeoh.jpg +0 -0
  112. package/public/samples/peg-legge.svg +37 -0
  113. package/public/samples/richard-guerra.svg +42 -0
  114. package/public/samples/rose-leslie.jpg +0 -0
  115. package/public/samples/sample-1.svg +365 -0
  116. package/public/samples/sample-2.svg +129 -0
  117. package/public/samples/sample-3.svg +93 -0
  118. package/public/samples/sample-4.svg +168 -0
  119. package/public/samples/sample-5.svg +155 -0
  120. package/public/samples/sample-6.svg +445 -0
  121. package/public/samples/sample-7.svg +404 -0
  122. package/public/samples/sample-8.png +0 -0
  123. package/public/staticwebapp.config.json +138 -0
  124. package/scripts.ts +56 -0
  125. package/server.ts +29 -0
  126. package/states.ts +63 -0
  127. package/styles.ts +232 -0
  128. package/tsconfig.json +71 -25
  129. package/types.d.ts +54 -0
  130. package/vite.config.ts +3 -0
  131. package/xpack/alias.ts +21 -0
  132. package/xpack/config.ts +59 -0
  133. package/xpack/create-server.ts +68 -0
  134. package/xpack/create-vite-dev-server.ts +33 -0
  135. package/xpack/deploy/deploy-inte.ts +3 -0
  136. package/xpack/filename.ts +43 -0
  137. package/xpack/hooks/build-start.ts +17 -0
  138. package/xpack/hooks/close-bundle.ts +19 -0
  139. package/xpack/hooks/handle-hot-update.ts +22 -0
  140. package/xpack/hooks/options.ts +55 -0
  141. package/xpack/hooks/resolve-dynamic-import.ts +18 -0
  142. package/xpack/hooks/transform-index-html.ts +18 -0
  143. package/xpack/hooks/transform.ts +72 -0
  144. package/xpack/hooks/write-bundle.ts +16 -0
  145. package/xpack/manual-chunk.ts +56 -0
  146. package/xpack/paths.ts +30 -0
  147. package/xpack/renderer.ts +141 -0
  148. package/xpack/root/active-item-options.tsx +98 -0
  149. package/xpack/root/frame-controls.tsx +139 -0
  150. package/xpack/root/index.tsx +107 -0
  151. package/xpack/root/rendered-item.tsx +25 -0
  152. package/xpack/root/root-context.ts +22 -0
  153. package/xpack/root/root-nav.tsx +162 -0
  154. package/xpack/root/state-animation-html.tsx +18 -0
  155. package/xpack/root/template.tsx +23 -0
  156. package/xpack/root/use-click-outside.ts +37 -0
  157. package/xpack/scripts/color-mode.entry.ts +28 -0
  158. package/xpack/scripts/mock-api.entry.ts +11 -0
  159. package/xpack/scripts/pl-states.entry.ts +321 -0
  160. package/xpack/scripts/root.entry.ts +135 -0
  161. package/xpack/scripts/theme-critical.entry.ts +20 -0
  162. package/xpack/states.schema.json +61 -0
  163. package/xpack/styles/_border.scss +22 -0
  164. package/xpack/styles/_breakpoint.scss +117 -0
  165. package/xpack/styles/_form.scss +23 -0
  166. package/xpack/styles/_px2rem.scss +5 -0
  167. package/xpack/styles/_reset.scss +134 -0
  168. package/xpack/styles/_state-toggle.scss +121 -0
  169. package/xpack/styles/_theme.scss +68 -0
  170. package/xpack/styles/_top-panel.scss +87 -0
  171. package/xpack/styles/_xpack-root.scss +322 -0
  172. package/xpack/styles/pl-states.scss +308 -0
  173. package/xpack/styles/root.scss +129 -0
  174. package/.github/workflows/deploy.yaml +0 -32
  175. package/index.ts +0 -1
@@ -0,0 +1,141 @@
1
+ /* eslint-disable no-console */
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ import { Express } from 'express';
6
+ import { ViteDevServer } from 'vite';
7
+ import * as cheerio from 'cheerio';
8
+ import jsBeautify, { CSSBeautifyOptions, HTMLBeautifyOptions, JSBeautifyOptions } from 'js-beautify';
9
+
10
+ interface Props {
11
+ app: Express;
12
+ indexProd: string;
13
+ isProd: boolean;
14
+ viteDevServer: ViteDevServer | undefined;
15
+ resolve: (path: string) => string;
16
+ }
17
+
18
+ const beautifyOptions: HTMLBeautifyOptions | JSBeautifyOptions | CSSBeautifyOptions = {
19
+ indent_size: 2,
20
+ indent_char: ' ',
21
+ keep_array_indentation: false,
22
+ break_chained_methods: false,
23
+ indent_scripts: 'normal',
24
+ brace_style: 'expand',
25
+ space_before_conditional: true,
26
+ unescape_strings: false,
27
+ jslint_happy: false,
28
+ end_with_newline: false,
29
+ wrap_line_length: 0,
30
+ indent_inner_html: false,
31
+ comma_first: false,
32
+ e4x: false,
33
+ indent_empty_lines: false,
34
+ wrap_attributes: 'force',
35
+ };
36
+
37
+ const updateResourcePath = ($: cheerio.CheerioAPI, tagName: string, attr: string) => {
38
+ $(tagName).each((_, el) => {
39
+ const href = $(el).attr(attr);
40
+
41
+ if (href && href.startsWith('/')) {
42
+ let newPath = href;
43
+
44
+ if (process.env.VITE_DOMAIN) {
45
+ newPath = process.env.VITE_DOMAIN + newPath;
46
+ }
47
+
48
+ if (
49
+ href.startsWith('/') &&
50
+ !href.startsWith('/assets/vendors/') &&
51
+ ['.css', '.ico', '.js', '.webmanifest', '.svg'].includes(path.extname(href).toLowerCase()) &&
52
+ !/\.0x[a-z0-9]{8}\.\w+$/gi.test(href)
53
+ ) {
54
+ newPath += '?v=' + new Date().getTime();
55
+ }
56
+
57
+ if (newPath != href) {
58
+ $(el).attr(attr, newPath);
59
+ }
60
+ }
61
+ });
62
+ };
63
+
64
+ const removeDuplicateAssets = ($: cheerio.CheerioAPI, selector: string, attr: string, paths: string[]) => {
65
+ $(selector).each((_, el) => {
66
+ if ($(el).attr('data-pl-inplace') === 'true') {
67
+ return;
68
+ }
69
+
70
+ const path = $(el).attr(attr);
71
+
72
+ if (!path) {
73
+ return;
74
+ }
75
+
76
+ const index = $(el).index();
77
+ const parent = $(el).parent().clone();
78
+ const child = parent.children()[index];
79
+
80
+ parent.empty();
81
+ parent.append(child);
82
+ const html = parent.html();
83
+
84
+ $(el).after('\n<!-- ' + html + ' -->');
85
+
86
+ if (paths.includes(path)) {
87
+ $(el).remove();
88
+
89
+ return;
90
+ }
91
+
92
+ paths.push(path);
93
+ $('head').append(el);
94
+ });
95
+ };
96
+
97
+ export const _useRenderer = ({ app, indexProd, isProd, viteDevServer, resolve }: Props) => {
98
+ app.use(async (req, res) => {
99
+ try {
100
+ let template, render;
101
+
102
+ if (!isProd) {
103
+ // always read fresh template in dev
104
+ template = fs.readFileSync(resolve('index.html'), 'utf-8');
105
+ template = await viteDevServer!.transformIndexHtml(req.originalUrl, template);
106
+ render = (await viteDevServer!.ssrLoadModule(resolve('src/entry-server.tsx'))).render;
107
+ } else {
108
+ template = indexProd;
109
+ // @ts-ignore
110
+ render = (await import(resolve('dist/server/entry-server.js'))).render;
111
+ }
112
+
113
+ const context: { url?: string } = {};
114
+ const output = render(req.originalUrl);
115
+
116
+ if (context.url) {
117
+ // Somewhere a `<Redirect>` was rendered
118
+ return res.redirect(301, context.url);
119
+ }
120
+
121
+ const html = template.replace('<!--app-html-->', output.html);
122
+ const $ = cheerio.load(html);
123
+ const paths: string[] = [];
124
+
125
+ removeDuplicateAssets($, 'link[data-pl-require][href]', 'href', paths);
126
+ removeDuplicateAssets($, 'script[data-pl-require][src]', 'src', paths);
127
+ updateResourcePath($, 'link', 'href');
128
+ updateResourcePath($, 'script', 'src');
129
+ updateResourcePath($, 'img', 'src');
130
+
131
+ res
132
+ .status(200)
133
+ .set({ 'Content-Type': 'text/html' })
134
+ .end(jsBeautify.html_beautify($.html(), beautifyOptions).replace('/* app-styles */', output.styles));
135
+ } catch (e: any) {
136
+ !isProd && viteDevServer!.ssrFixStacktrace(e);
137
+ console.log(e.stack);
138
+ res.status(500).end(e.stack);
139
+ }
140
+ });
141
+ };
@@ -0,0 +1,98 @@
1
+ import { createRef, MouseEvent, useState } from 'react';
2
+ import { viteAbsoluteUrl } from '@helpers/functions';
3
+
4
+ import { useRootContext } from './root-context';
5
+ import { useOnClickOutside } from './use-click-outside';
6
+ import StateAnimationHtml from './state-animation-html';
7
+
8
+ export default function ActiveItemOptions() {
9
+ const { activeItem, isTopPanel } = useRootContext();
10
+ const key = 'pl-show-state-selector';
11
+ const keyExist = localStorage.getItem(key);
12
+ const optionItemsRef = createRef<HTMLDivElement>();
13
+ const buttonRef = createRef<HTMLButtonElement>();
14
+
15
+ const [show, setShow] = useState(false);
16
+
17
+ useOnClickOutside(optionItemsRef, () => setShow(false), [buttonRef]);
18
+
19
+ const handleStateToggle = (event: MouseEvent<HTMLButtonElement>) => {
20
+ event.preventDefault();
21
+ event.stopPropagation();
22
+
23
+ if (keyExist) {
24
+ localStorage.removeItem(key);
25
+ } else {
26
+ localStorage.setItem(key, 'true');
27
+ }
28
+
29
+ location.reload();
30
+
31
+ return false;
32
+ };
33
+
34
+ const handleThemeToggle = () => {
35
+ window.dispatchEvent(new CustomEvent('toggleTheme'));
36
+ };
37
+
38
+ const toggleCtaText = keyExist ? 'Hide state selector' : 'Show state selector';
39
+
40
+ const handleClick = () => {
41
+ setShow(!show);
42
+ };
43
+
44
+ const handleChangePanelPosition = () => {
45
+ const key = 'MSG_IS_TOP_PANEL';
46
+ const value = isTopPanel ? 'false' : 'true';
47
+
48
+ window.localStorage.setItem(key, value);
49
+ window.dispatchEvent(new StorageEvent('storage', { key, newValue: value }));
50
+
51
+ setShow(false);
52
+ };
53
+
54
+ return activeItem ? (
55
+ <div>
56
+ <button
57
+ ref={buttonRef}
58
+ aria-label={show ? 'Close' : 'Settings'}
59
+ className={`xpack-o-root__control-button xpack-o-root__button-${show ? 'close' : 'setting'}`}
60
+ onClick={handleClick}
61
+ >
62
+ <svg className="xpack-o-root__control-svg" viewBox="0 0 30 30">
63
+ <use xlinkHref={show ? '/assets/images/root.svg#close' : '/assets/images/root.svg#setting'} />
64
+ </svg>
65
+ </button>
66
+
67
+ <div ref={optionItemsRef} className={`xpack-o-root__active-item-options ${show ? 'show' : ''}`}>
68
+ <button className="xpack-o-root__nav-item pl-state-toggle" onClick={handleStateToggle}>
69
+ <StateAnimationHtml keyExist={!!keyExist} />
70
+ {toggleCtaText}
71
+ </button>
72
+
73
+ <button className="xpack-o-root__nav-item pl-state-toggle" onClick={handleThemeToggle}>
74
+ Change theme
75
+ </button>
76
+
77
+ <button className="xpack-o-root__nav-item panel-position" onClick={handleChangePanelPosition}>
78
+ {isTopPanel ? 'Set left panel' : 'Set top panel'}
79
+ </button>
80
+
81
+ <a className="xpack-o-root__nav-item" href={viteAbsoluteUrl(activeItem.path, true)} rel="noreferrer" target="_blank">
82
+ Open in new tab
83
+ </a>
84
+
85
+ <div className="xpack-o-root__nav-item-seperator" />
86
+
87
+ <a className="xpack-o-root__nav-item" href="https://tuyen.blog/optimizely-cms/frontend/get-started/" rel="noreferrer" target="_blank">
88
+ About this library
89
+ <svg className="xpack-o-root__control-svg" viewBox="0 0 30 30">
90
+ <use xlinkHref="/assets/images/root.svg#external" />
91
+ </svg>
92
+ </a>
93
+ </div>
94
+ </div>
95
+ ) : (
96
+ <></>
97
+ );
98
+ }
@@ -0,0 +1,139 @@
1
+ import { useState } from 'react';
2
+
3
+ const MIN_FRAME_SIZE = 350;
4
+ const MSG_IFRAME_SIZE = 'MSG_IFRAME_SIZE';
5
+
6
+ const randomIntFromInterval = (min: number, max: number): number => {
7
+ // min and max included
8
+ return Math.floor(Math.random() * (max - min + 1) + min);
9
+ };
10
+
11
+ export default function FrameControls() {
12
+ const [discoTimer, setDiscoTimer] = useState<NodeJS.Timeout>();
13
+
14
+ const setIFrameWidth = (width?: number) => {
15
+ const wrapper = document.getElementById('root-iframe-wrapper');
16
+
17
+ if (!wrapper) {
18
+ return;
19
+ }
20
+
21
+ if (width) {
22
+ wrapper.style.maxWidth = `min(100%, ${width}px)`;
23
+ sessionStorage.setItem(MSG_IFRAME_SIZE, width + '');
24
+ } else {
25
+ wrapper.style.removeProperty('max-width');
26
+ }
27
+ };
28
+
29
+ const handleMobileClick = () => {
30
+ if (discoTimer) {
31
+ clearInterval(discoTimer);
32
+ }
33
+
34
+ const width = randomIntFromInterval(MIN_FRAME_SIZE, 768);
35
+
36
+ setIFrameWidth(width);
37
+ };
38
+
39
+ const handleTabletClick = () => {
40
+ if (discoTimer) {
41
+ clearInterval(discoTimer);
42
+ }
43
+
44
+ const width = randomIntFromInterval(768, 1024);
45
+
46
+ setIFrameWidth(width);
47
+ };
48
+
49
+ const handleDesktopClick = () => {
50
+ if (discoTimer) {
51
+ clearInterval(discoTimer);
52
+ }
53
+
54
+ const maxWidth = document.body.clientWidth - 300;
55
+
56
+ const width = randomIntFromInterval(1024, Math.max(1024, maxWidth));
57
+
58
+ setIFrameWidth(width);
59
+ };
60
+
61
+ const handleFullClick = () => {
62
+ if (discoTimer) {
63
+ clearInterval(discoTimer);
64
+ }
65
+
66
+ setIFrameWidth(undefined);
67
+ sessionStorage.removeItem(MSG_IFRAME_SIZE);
68
+ };
69
+
70
+ const handleRandomClick = () => {
71
+ if (discoTimer) {
72
+ clearInterval(discoTimer);
73
+ }
74
+
75
+ const maxWidth = document.body.clientWidth - 300;
76
+
77
+ const width = randomIntFromInterval(MIN_FRAME_SIZE, Math.max(MIN_FRAME_SIZE, maxWidth));
78
+
79
+ setIFrameWidth(width);
80
+ sessionStorage.setItem(MSG_IFRAME_SIZE, width + '');
81
+ };
82
+
83
+ const handleDiscoClick = () => {
84
+ if (discoTimer) {
85
+ clearInterval(discoTimer);
86
+
87
+ return;
88
+ }
89
+
90
+ const timer = setInterval(() => {
91
+ const maxWidth = document.body.clientWidth - 300;
92
+
93
+ const width = randomIntFromInterval(MIN_FRAME_SIZE, Math.max(MIN_FRAME_SIZE, maxWidth));
94
+
95
+ setIFrameWidth(width);
96
+ sessionStorage.setItem(MSG_IFRAME_SIZE, width + '');
97
+ }, 2000);
98
+
99
+ setDiscoTimer(timer);
100
+ };
101
+
102
+ return (
103
+ <div className="xpack-o-root__frame-control">
104
+ <div className="xpack-o-root__actual-width" id="root-actual-iframe-width" />
105
+ <div className="xpack-o-root__controls">
106
+ <button aria-label="Mobile View" className="xpack-o-root__control-button" title="Mobile View" onClick={handleMobileClick}>
107
+ <svg className="xpack-o-root__control-svg" viewBox="0 0 24 24">
108
+ <use xlinkHref="/assets/images/root.svg#phone" />
109
+ </svg>
110
+ </button>
111
+ <button aria-label="Tablet View" className="xpack-o-root__control-button" title="Tablet View" onClick={handleTabletClick}>
112
+ <svg className="xpack-o-root__control-svg" viewBox="0 0 24 24">
113
+ <use xlinkHref="/assets/images/root.svg#tablet" />
114
+ </svg>
115
+ </button>
116
+ <button aria-label="Desktop View" className="xpack-o-root__control-button" title="Desktop View" onClick={handleDesktopClick}>
117
+ <svg className="xpack-o-root__control-svg" viewBox="0 0 24 24">
118
+ <use xlinkHref="/assets/images/root.svg#desktop" />
119
+ </svg>
120
+ </button>
121
+ <button aria-label="Full Width View" className="xpack-o-root__control-button" title="Full Width View" onClick={handleFullClick}>
122
+ <svg className="xpack-o-root__control-svg" viewBox="0 0 24 24">
123
+ <use xlinkHref="/assets/images/root.svg#hay" />
124
+ </svg>
125
+ </button>
126
+ <button aria-label="Random Width View" className="xpack-o-root__control-button" title="Random Width View" onClick={handleRandomClick}>
127
+ <svg className="xpack-o-root__control-svg" viewBox="0 0 24 24">
128
+ <use xlinkHref="/assets/images/root.svg#random" />
129
+ </svg>
130
+ </button>
131
+ <button aria-label="Disco Mode View" className="xpack-o-root__control-button" title="Disco Mode View" onClick={handleDiscoClick}>
132
+ <svg className="xpack-o-root__control-svg" viewBox="0 0 24 24">
133
+ <use xlinkHref="/assets/images/root.svg#disco-ball" />
134
+ </svg>
135
+ </button>
136
+ </div>
137
+ </div>
138
+ );
139
+ }
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+
3
+ import { RootModel, SinglePageNode } from '@_types/types';
4
+ import { useEffect, useMemo, useState } from 'react';
5
+ import { viteAbsoluteUrl } from '@helpers/functions';
6
+
7
+ import { RootContext, RootData } from './root-context';
8
+ import FrameControls from './frame-controls';
9
+ import ActiveItemOptions from './active-item-options';
10
+ import RootNav from './root-nav';
11
+
12
+ export default function Root(props: RootModel) {
13
+ const [activeItem, setActiveItem] = useState<SinglePageNode | undefined>(typeof window !== 'undefined' ? undefined : undefined);
14
+ const [isTopPanel, setTopPanel] = useState<boolean>(() =>
15
+ typeof localStorage !== 'undefined' ? localStorage.getItem('MSG_IS_TOP_PANEL') === 'true' : false
16
+ );
17
+
18
+ useEffect(() => {
19
+ if (!activeItem) {
20
+ return;
21
+ }
22
+
23
+ const frame = document.querySelector<HTMLIFrameElement>('#root-iframe');
24
+
25
+ if (!frame) {
26
+ return;
27
+ }
28
+
29
+ frame.src = viteAbsoluteUrl(activeItem.path, true);
30
+ document.title = activeItem.name + ' - ' + import.meta.env.VITE_TITLE_SUFFIX;
31
+ }, [activeItem]);
32
+
33
+ const rootData: RootData = {
34
+ activeItem,
35
+ setActiveItem,
36
+ isTopPanel,
37
+ setTopPanel,
38
+ };
39
+
40
+ const routes = useMemo(() => {
41
+ const result: SinglePageNode[] = [];
42
+
43
+ props.routes.forEach((route) => {
44
+ if (route.type === 'single') {
45
+ result.push({
46
+ path: route.path,
47
+ name: route.name,
48
+ type: 'single',
49
+ });
50
+
51
+ return;
52
+ }
53
+ route.items.forEach((item) => {
54
+ result.push({
55
+ path: item.path,
56
+ name: item.name,
57
+ type: 'single',
58
+ });
59
+ });
60
+ });
61
+
62
+ return result;
63
+ }, [props.routes]);
64
+
65
+ useEffect(() => {
66
+ const hash = window.location.hash;
67
+ const activePath = hash && hash.startsWith('#/') ? hash.substring(1) : '/pages/home';
68
+ const activeIndex = routes.findIndex((r) => r.path === activePath);
69
+
70
+ if (activeIndex >= 0) {
71
+ // eslint-disable-next-line react-hooks/set-state-in-effect
72
+ setActiveItem(routes[activeIndex]);
73
+ } else {
74
+ const homeIndex = routes.findIndex((r) => r.path === '/pages/home');
75
+
76
+ setActiveItem(routes[homeIndex]);
77
+ }
78
+ }, [routes]);
79
+
80
+ const handleStorageChange = (event: StorageEvent) => {
81
+ switch (event.key) {
82
+ case 'MSG_IS_TOP_PANEL':
83
+ setTopPanel(event.newValue === 'true');
84
+ break;
85
+ }
86
+ };
87
+
88
+ useEffect(() => {
89
+ window.addEventListener('storage', handleStorageChange);
90
+
91
+ return () => {
92
+ window.removeEventListener('storage', handleStorageChange);
93
+ };
94
+ }, []);
95
+
96
+ return routes ? (
97
+ <RootContext.Provider value={rootData}>
98
+ <div className={`xpack-o-root ${isTopPanel ? 'top-panel' : ''}`}>
99
+ <RootNav routes={props.routes} />
100
+ <FrameControls />
101
+ <ActiveItemOptions />
102
+ </div>
103
+ </RootContext.Provider>
104
+ ) : (
105
+ <></>
106
+ );
107
+ }
@@ -0,0 +1,25 @@
1
+ import { SinglePageNode } from '@_types/_root';
2
+ import { MouseEvent } from 'react';
3
+ import { viteAbsoluteUrl } from '@helpers/functions';
4
+
5
+ import { useRootContext } from './root-context';
6
+
7
+ export default function RenderedItem(item: SinglePageNode) {
8
+ const { activeItem, setActiveItem } = useRootContext();
9
+
10
+ const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
11
+ event.preventDefault();
12
+ window.location.hash = item.path;
13
+ setActiveItem(item);
14
+
15
+ return false;
16
+ };
17
+
18
+ const activeClass = activeItem && activeItem.path === item.path ? ' xpack-o-root__nav-item--active' : '';
19
+
20
+ return (
21
+ <a className={'xpack-o-root__nav-item' + activeClass} href={viteAbsoluteUrl(item.path, true)} target="inner" onClick={handleClick}>
22
+ {item.name}
23
+ </a>
24
+ );
25
+ }
@@ -0,0 +1,22 @@
1
+ import { SinglePageNode } from '@_types/types';
2
+ import { createContext, useContext } from 'react';
3
+
4
+ export interface RootData {
5
+ activeItem?: SinglePageNode;
6
+ setActiveItem: (item?: SinglePageNode) => void;
7
+ isTopPanel?: boolean;
8
+ setTopPanel: (isTopPanel: boolean) => void;
9
+ }
10
+
11
+ export const RootContext = createContext<RootData>({
12
+ setActiveItem: () => {
13
+ // empty
14
+ },
15
+ setTopPanel: () => {
16
+ // empty
17
+ },
18
+ });
19
+
20
+ export function useRootContext() {
21
+ return useContext(RootContext);
22
+ }
@@ -0,0 +1,162 @@
1
+ import { MultiplePageNode, RootItemModel } from '@_types/types';
2
+ import { debounce } from 'lodash';
3
+ import { useRef, useCallback, useEffect, useState, useMemo } from 'react';
4
+ import { viteAbsoluteUrl } from '@helpers/functions';
5
+
6
+ import { useRootContext } from './root-context';
7
+ import { useOnClickOutside } from './use-click-outside';
8
+ import RenderedItem from './rendered-item';
9
+
10
+ interface Props {
11
+ routes: RootItemModel[];
12
+ }
13
+
14
+ export default function RootNav({ routes: routesProp }: Props) {
15
+ const { activeItem } = useRootContext();
16
+ const [show, setShow] = useState(false);
17
+ const navItemRef = useRef<HTMLDivElement>(null);
18
+ const buttonRef = useRef<HTMLAnchorElement>(null);
19
+ const inputRef = useRef<HTMLInputElement>(null);
20
+ const [routes, setRoutes] = useState(routesProp);
21
+ const [routesSearch, setRoutesSearch] = useState<RootItemModel[]>([]);
22
+ const [textSearch, setTextSearch] = useState('');
23
+
24
+ const closeMenu = () => {
25
+ setShow(false);
26
+ setTextSearch('');
27
+ };
28
+
29
+ useOnClickOutside(navItemRef, closeMenu, [buttonRef]);
30
+
31
+ const handleClick = () => {
32
+ if (!show && inputRef.current) {
33
+ inputRef.current.focus();
34
+ }
35
+ setShow(!show);
36
+ };
37
+
38
+ const handleChange = useCallback(
39
+ (text: string) => {
40
+ const searchTerms = text.trim().toLocaleLowerCase().split(/\s+/);
41
+ const isSearchActive = searchTerms.length > 0 && text.trim();
42
+
43
+ const isItemIncludedText: (item: RootItemModel) => boolean = (item: RootItemModel) => {
44
+ if (!isSearchActive) return false;
45
+
46
+ if (item.type === 'single') {
47
+ return searchTerms.some((s) => item.name.toLocaleLowerCase().includes(s));
48
+ }
49
+
50
+ return searchTerms.some(
51
+ (s) => item.name.toLocaleLowerCase().includes(s) || (item as MultiplePageNode).items.some((x) => isItemIncludedText(x))
52
+ );
53
+ };
54
+
55
+ const tempRoutesSearch: RootItemModel[] = [];
56
+ const tempRoutes: RootItemModel[] = [];
57
+
58
+ routesProp.forEach((item) => {
59
+ (isItemIncludedText(item) ? tempRoutesSearch : tempRoutes).push(item);
60
+ });
61
+
62
+ setRoutesSearch(tempRoutesSearch);
63
+ setRoutes(tempRoutes);
64
+ },
65
+ [routesProp]
66
+ );
67
+
68
+ const debouncedHandleChange = useMemo(() => debounce(handleChange, 500), [handleChange]);
69
+
70
+ useEffect(() => {
71
+ debouncedHandleChange(textSearch);
72
+ }, [debouncedHandleChange, textSearch]);
73
+
74
+ return (
75
+ <>
76
+ {/*eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions*/}
77
+ <a ref={buttonRef} className="xpack-o-root__button-close" onClick={handleClick}>
78
+ <button aria-label={show ? 'Close' : 'Pages'} className="xpack-o-root__control-button xpack-o-root__button-close">
79
+ <svg className="xpack-o-root__control-svg">
80
+ <use xlinkHref={viteAbsoluteUrl('/assets/images/root.svg#' + (show ? 'close' : 'list'))} />
81
+ </svg>
82
+ </button>
83
+ </a>
84
+
85
+ <p className="xpack-o-root__title">{activeItem?.name}</p>
86
+
87
+ <div ref={navItemRef} className={`xpack-o-root__nav-items ${show ? 'show' : ''}`}>
88
+ <div className="xpack-o-root__search">
89
+ <input
90
+ ref={inputRef}
91
+ placeholder="Find a pattern"
92
+ type={'text'}
93
+ value={textSearch}
94
+ onChange={(e) => {
95
+ setTextSearch(e.target.value);
96
+ }}
97
+ />
98
+ </div>
99
+
100
+ <div className="xpack-o-root__items">
101
+ {textSearch.trim() && (
102
+ <div className="xpack-o-root__search-matches">
103
+ {routesSearch.length
104
+ ? routesSearch
105
+ .filter((r) => r.path != '/' && r.path != '/index')
106
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
107
+ .map((item, index) =>
108
+ item.path ? (
109
+ <RenderedItem key={index} {...item} />
110
+ ) : (
111
+ <details
112
+ key={index}
113
+ className="xpack-o-root__nav-item-collection"
114
+ open={
115
+ !!(item as MultiplePageNode).items.find((x: any) => {
116
+ return window.location.hash.includes(x.path);
117
+ })
118
+ }
119
+ >
120
+ <summary>{item.name}</summary>
121
+ {}
122
+ {(item as MultiplePageNode).items.map((node: any, idx: number) => (
123
+ <RenderedItem key={idx} {...node} />
124
+ ))}
125
+ </details>
126
+ )
127
+ )
128
+ : textSearch.trim() && <div className="xpack-o-root__nav-message ">No pattern matches</div>}
129
+ </div>
130
+ )}
131
+
132
+ <div className={`xpack-o-root__search-not-matches ${textSearch.trim() ? 'blur' : ''}`}>
133
+ {routes
134
+ .filter((r) => r.path != '/' && r.path != '/index')
135
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
136
+ .map((item, index) =>
137
+ item.path ? (
138
+ <RenderedItem key={index} {...item} />
139
+ ) : (
140
+ <details
141
+ key={index}
142
+ className="xpack-o-root__nav-item-collection"
143
+ open={
144
+ !!(item as MultiplePageNode).items.find((x: any) => {
145
+ return window.location.hash.includes(x.path);
146
+ })
147
+ }
148
+ >
149
+ <summary>{item.name}</summary>
150
+ {}
151
+ {(item as MultiplePageNode).items.map((node: any, idx: number) => (
152
+ <RenderedItem key={idx} {...node} />
153
+ ))}
154
+ </details>
155
+ )
156
+ )}
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </>
161
+ );
162
+ }