@developer_tribe/react-builder 1.0.5 → 1.0.6

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 (66) hide show
  1. package/dist/build-components/index.d.ts +1 -2
  2. package/dist/build-components/patterns.generated.d.ts +56 -439
  3. package/dist/components/AttributesEditorPanel.d.ts +2 -2
  4. package/dist/components/BottomBar.d.ts +8 -0
  5. package/dist/components/Checkbox.d.ts +1 -1
  6. package/dist/components/LoadingComponent.d.ts +1 -0
  7. package/dist/components/MobilePanelToggleButton.d.ts +8 -0
  8. package/dist/hooks/useMinimumDelay.d.ts +7 -0
  9. package/dist/hooks/useMobileEditorPanels.d.ts +12 -0
  10. package/dist/hooks/useSyncProjectPageStore.d.ts +15 -0
  11. package/dist/index.cjs.js +3 -3
  12. package/dist/index.cjs.js.map +1 -1
  13. package/dist/index.esm.js +3 -3
  14. package/dist/index.esm.js.map +1 -1
  15. package/dist/index.native.cjs.js +1 -1
  16. package/dist/index.native.cjs.js.map +1 -1
  17. package/dist/index.native.esm.js +4 -4
  18. package/dist/index.native.esm.js.map +1 -1
  19. package/dist/modals/ScreenColorsModal.d.ts +8 -0
  20. package/dist/modals/index.d.ts +1 -0
  21. package/dist/pages/tabs/BuilderPanel.d.ts +2 -2
  22. package/dist/store.d.ts +6 -0
  23. package/dist/styles.css +1 -1
  24. package/dist/utils/nodeTree.d.ts +5 -0
  25. package/package.json +1 -1
  26. package/src/RenderPage.tsx +4 -1
  27. package/src/assets/samples/carousel-sample.json +99 -81
  28. package/src/assets/samples/simple-1.json +8 -2
  29. package/src/assets/samples/simple-2.json +36 -9
  30. package/src/assets/samples/vpn-onboard-1.json +27 -23
  31. package/src/assets/samples/vpn-onboard-2.json +279 -275
  32. package/src/assets/samples/vpn-onboard-3.json +247 -246
  33. package/src/assets/samples/vpn-onboard-4.json +247 -246
  34. package/src/assets/samples/vpn-onboard-5.json +375 -369
  35. package/src/assets/samples/vpn-onboard-6.json +252 -248
  36. package/src/build-components/RenderNode.generated.tsx +0 -7
  37. package/src/build-components/View/pattern.json +2 -2
  38. package/src/build-components/index.ts +0 -5
  39. package/src/build-components/patterns.generated.ts +56 -455
  40. package/src/components/AttributesEditorPanel.tsx +12 -8
  41. package/src/components/BottomBar.tsx +236 -0
  42. package/src/components/EditorHeader.tsx +11 -4
  43. package/src/components/LoadingComponent.tsx +10 -0
  44. package/src/components/MobilePanelToggleButton.tsx +39 -0
  45. package/src/hooks/useMinimumDelay.ts +20 -0
  46. package/src/hooks/useMobileEditorPanels.ts +56 -0
  47. package/src/hooks/useSyncProjectPageStore.ts +40 -0
  48. package/src/modals/ScreenColorsModal.tsx +115 -0
  49. package/src/modals/index.ts +1 -0
  50. package/src/pages/ProjectPage.tsx +53 -243
  51. package/src/pages/tabs/BuilderPanel.tsx +14 -8
  52. package/src/store.ts +10 -6
  53. package/src/styles/base/_global.scss +12 -4
  54. package/src/styles/components/_attributes-editor.scss +9 -1
  55. package/src/styles/components/_bottom-bar.scss +113 -0
  56. package/src/styles/components/_editor-shell.scss +0 -19
  57. package/src/styles/index.scss +1 -0
  58. package/src/utils/analyseNodeByPatterns.ts +15 -0
  59. package/src/utils/nodeTree.ts +99 -0
  60. package/dist/build-components/PaywallSubscriButton/PaywallSubscriButton.d.ts +0 -5
  61. package/dist/build-components/PaywallSubscriButton/PaywallSubscriButtonProps.generated.d.ts +0 -50
  62. package/dist/pages/tabs/SideTool.d.ts +0 -8
  63. package/src/build-components/PaywallSubscriButton/PaywallSubscriButton.tsx +0 -10
  64. package/src/build-components/PaywallSubscriButton/PaywallSubscriButtonProps.generated.ts +0 -77
  65. package/src/build-components/PaywallSubscriButton/pattern.json +0 -27
  66. package/src/pages/tabs/SideTool.tsx +0 -253
@@ -1,12 +1,11 @@
1
- import { useCallback, useEffect, useState } from 'react';
2
- import type { Node, NodeData } from '../types/Node';
1
+ import { useCallback, useEffect } from 'react';
2
+ import type { Node } from '../types/Node';
3
3
  import type { Project, ProjectColors } from '../types/Project';
4
4
  import { RenderPage } from '../RenderPage';
5
5
  import { EditorHeader } from '../components/EditorHeader';
6
6
  import { AttributesEditorPanel } from '../components/AttributesEditorPanel';
7
7
  import { BuilderProvider } from '../components/BuilderProvider';
8
8
  import { BuilderPanel } from './tabs/BuilderPanel';
9
- import { SideTool } from './tabs/SideTool';
10
9
  import { AppConfig, defaultAppConfig } from '../types/PreviewConfig';
11
10
  import { useRenderStore } from '../store';
12
11
  import { logger } from '../utils/logger';
@@ -15,8 +14,18 @@ import type { LogLevel } from '../types/Project';
15
14
  import { analyseAndProccess } from '../utils/analyseNode';
16
15
  import backgroundImage from '../assets/images/background.jpg';
17
16
  import type { PaywallBenefits } from '../paywall/types/benefits';
18
- import Lottie from 'lottie-react';
19
- import loadingAnimation from '../assets/loading_animation.json';
17
+ import { LoadingComponent } from '../components/LoadingComponent';
18
+ import { MobilePanelToggleButton } from '../components/MobilePanelToggleButton';
19
+ import { BottomBar } from '../components/BottomBar';
20
+ import { useMobileEditorPanels } from '../hooks/useMobileEditorPanels';
21
+ import { useSyncProjectPageStore } from '../hooks/useSyncProjectPageStore';
22
+ import { useMinimumDelay } from '../hooks/useMinimumDelay';
23
+ import {
24
+ deleteNodeFromTree,
25
+ findNodeByKey,
26
+ isNodeRecord,
27
+ nodeHasChild,
28
+ } from '../utils/nodeTree';
20
29
  export type ProjectPageProps = {
21
30
  project: Project;
22
31
  onSaveProject: (project: Project) => void;
@@ -26,8 +35,6 @@ export type ProjectPageProps = {
26
35
  name: string;
27
36
  };
28
37
 
29
- const MOBILE_BREAKPOINT = 1000;
30
-
31
38
  export function ProjectPage({
32
39
  project,
33
40
  appConfig = defaultAppConfig,
@@ -41,27 +48,41 @@ export function ProjectPage({
41
48
  const {
42
49
  current,
43
50
  setCurrent,
51
+ setAppConfig,
44
52
  setProjectColors,
45
53
  setProjectName,
54
+ editorData,
55
+ setEditorData,
46
56
  products,
47
57
  benefits,
48
58
  } = useRenderStore((s) => ({
49
59
  current: s.current,
50
60
  setCurrent: s.setCurrent,
61
+ setAppConfig: s.setAppConfig,
51
62
  setProjectColors: s.setProjectColors,
52
63
  setProjectName: s.setProjectName,
64
+ editorData: s.editorData,
65
+ setEditorData: s.setEditorData,
53
66
  products: s.products,
54
67
  benefits: s.benefits,
55
68
  }));
56
- const [editorData, setEditorData] = useState<Node>(null);
57
- const [minLoadingDelayDone, setMinLoadingDelayDone] =
58
- useState<boolean>(false);
59
- const [mobilePanel, setMobilePanel] = useState<
60
- 'builder' | 'attributes' | null
61
- >(null);
62
- const [isMobile, setIsMobile] = useState<boolean>(() => {
63
- if (typeof window === 'undefined') return false;
64
- return window.innerWidth <= MOBILE_BREAKPOINT;
69
+ const minLoadingDelayDone = useMinimumDelay(1000, [project.data]);
70
+ const {
71
+ isMobile,
72
+ mobilePanel,
73
+ toggleMobilePanel,
74
+ closeMobilePanels,
75
+ leftPanelIsOpen,
76
+ attributesPanelIsOpen,
77
+ } = useMobileEditorPanels();
78
+
79
+ useSyncProjectPageStore({
80
+ appConfig,
81
+ name,
82
+ projectColors: resolvedProjectColors,
83
+ setAppConfig,
84
+ setProjectName,
85
+ setProjectColors,
65
86
  });
66
87
 
67
88
  const handleDeleteNode = useCallback(
@@ -73,13 +94,10 @@ export function ProjectPage({
73
94
  );
74
95
  if (!shouldDeleteRoot) return;
75
96
  setEditorData(null);
76
- setCurrent(null);
77
97
  return;
78
98
  }
79
99
  const updated: Node = deleteNodeFromTree(editorData, nodeToDelete);
80
- //@ts-ignore
81
100
  setEditorData(updated);
82
-
83
101
  if (current === nodeToDelete) {
84
102
  setCurrent(updated);
85
103
  return;
@@ -100,76 +118,30 @@ export function ProjectPage({
100
118
 
101
119
  useEffect(() => {
102
120
  logger.info('ProjectPage', 'mount', { projectName: project.name });
103
- useRenderStore.getState().setAppConfig(appConfig);
104
- logger.verbose('ProjectPage', 'appConfig applied', appConfig);
105
121
  return () => {
106
122
  logger.info('ProjectPage', 'unmount');
107
123
  };
108
- }, [appConfig, project.name]);
109
-
110
- useEffect(() => {
111
- setProjectName(name);
112
- }, [name, setProjectName]);
113
-
114
- useEffect(() => {
115
- setProjectColors(resolvedProjectColors);
116
- return () => setProjectColors(undefined);
117
- }, [resolvedProjectColors, setProjectColors]);
124
+ }, [project.name]);
118
125
 
119
126
  useEffect(() => {
120
127
  if (!logLevel) return;
121
128
  logger.setLevel(logLevel);
122
129
  }, [logLevel]);
123
130
 
124
- useEffect(() => {
125
- function handleResize() {
126
- setIsMobile(window.innerWidth <= MOBILE_BREAKPOINT);
127
- }
128
-
129
- handleResize();
130
- window.addEventListener('resize', handleResize);
131
- return () => window.removeEventListener('resize', handleResize);
132
- }, []);
133
-
134
- useEffect(() => {
135
- setMobilePanel(null);
136
- }, [isMobile]);
137
-
138
- const toggleMobilePanel = (panel: 'builder' | 'attributes') => {
139
- setMobilePanel((prev) => (prev === panel ? null : panel));
140
- };
141
-
142
- const closeMobilePanels = () => {
143
- setMobilePanel(null);
144
- };
145
-
146
- const leftPanelIsOpen = !isMobile || mobilePanel === 'builder';
147
- const attributesPanelIsOpen = !isMobile || mobilePanel === 'attributes';
148
-
149
- useEffect(() => {
150
- setMinLoadingDelayDone(false);
151
- const timer = setTimeout(() => setMinLoadingDelayDone(true), 1000);
152
- return () => clearTimeout(timer);
153
- }, [project.data]);
154
-
155
131
  useEffect(() => {
156
132
  try {
157
133
  // Reset to "loading" immediately on project change so the loader is shown
158
134
  // until a valid node is available (and for at least 2 seconds).
159
135
  setEditorData(null);
160
- setCurrent(null);
161
136
  const processed = analyseAndProccess(project.data);
162
137
  if (!processed) {
163
138
  setEditorData(null);
164
- setCurrent(null);
165
139
  return;
166
140
  }
167
141
  setEditorData(processed);
168
- setCurrent(processed);
169
142
  } catch (error) {
170
143
  console.error(error);
171
144
  setEditorData(null);
172
- setCurrent(null);
173
145
  }
174
146
  }, [project.data]);
175
147
 
@@ -190,9 +162,6 @@ export function ProjectPage({
190
162
  setEditorData(project.data);
191
163
  setCurrent(project.data);
192
164
  }}
193
- current={current}
194
- editorData={editorData}
195
- setEditorData={setEditorData}
196
165
  />
197
166
  {isMobile && (
198
167
  <div
@@ -200,48 +169,20 @@ export function ProjectPage({
200
169
  role="group"
201
170
  aria-label="Editor panels"
202
171
  >
203
- <button
204
- type="button"
205
- className={`mobile-panel-toggle__button${mobilePanel === 'builder' ? ' mobile-panel-toggle__button--active' : ''}`}
206
- aria-label="Toggle builder panel"
207
- aria-expanded={mobilePanel === 'builder'}
208
- aria-controls="split-left-panel"
172
+ <MobilePanelToggleButton
173
+ label="Builder"
174
+ isActive={mobilePanel === 'builder'}
175
+ ariaLabel="Toggle builder panel"
176
+ ariaControls="split-left-panel"
209
177
  onClick={() => toggleMobilePanel('builder')}
210
- >
211
- <span className="mobile-panel-toggle__icon" aria-hidden="true">
212
- <svg viewBox="0 0 16 12" role="presentation" focusable="false">
213
- <path
214
- d="M1 1h14M1 6h14M1 11h14"
215
- stroke="currentColor"
216
- strokeWidth="2"
217
- strokeLinecap="round"
218
- fill="none"
219
- />
220
- </svg>
221
- </span>
222
- <span className="mobile-panel-toggle__label">Builder</span>
223
- </button>
224
- <button
225
- type="button"
226
- className={`mobile-panel-toggle__button${mobilePanel === 'attributes' ? ' mobile-panel-toggle__button--active' : ''}`}
227
- aria-label="Toggle attributes panel"
228
- aria-expanded={mobilePanel === 'attributes'}
229
- aria-controls="split-attributes-panel"
178
+ />
179
+ <MobilePanelToggleButton
180
+ label="Attributes"
181
+ isActive={mobilePanel === 'attributes'}
182
+ ariaLabel="Toggle attributes panel"
183
+ ariaControls="split-attributes-panel"
230
184
  onClick={() => toggleMobilePanel('attributes')}
231
- >
232
- <span className="mobile-panel-toggle__icon" aria-hidden="true">
233
- <svg viewBox="0 0 16 12" role="presentation" focusable="false">
234
- <path
235
- d="M1 1h14M1 6h14M1 11h14"
236
- stroke="currentColor"
237
- strokeWidth="2"
238
- strokeLinecap="round"
239
- fill="none"
240
- />
241
- </svg>
242
- </span>
243
- <span className="mobile-panel-toggle__label">Attributes</span>
244
- </button>
185
+ />
245
186
  </div>
246
187
  )}
247
188
  <div className="editor-container">
@@ -261,19 +202,13 @@ export function ProjectPage({
261
202
  </button>
262
203
  )}
263
204
  <div>
264
- <BuilderPanel
265
- data={editorData}
266
- setData={setEditorData}
267
- onDeleteNode={handleDeleteNode}
268
- />
205
+ <BuilderPanel onDeleteNode={handleDeleteNode} />
269
206
  </div>
270
207
  </div>
271
208
  <div
272
209
  style={{ backgroundImage: `url(${backgroundImage})` }}
273
210
  className="split-right"
274
211
  >
275
- <SideTool data={editorData} setData={setEditorData} />
276
-
277
212
  {showLoading && (
278
213
  <div className="rb-loading-overlay" aria-busy="true">
279
214
  <LoadingComponent />
@@ -291,6 +226,7 @@ export function ProjectPage({
291
226
  }}
292
227
  >
293
228
  <RenderPage data={editorData} name={project.name} />
229
+ <BottomBar />
294
230
  </BuilderProvider>
295
231
  )}
296
232
  </div>
@@ -309,27 +245,7 @@ export function ProjectPage({
309
245
  Close
310
246
  </button>
311
247
  )}
312
- <AttributesEditorPanel
313
- attributes={editorData}
314
- projectColors={resolvedProjectColors}
315
- onChange={(data) => {
316
- setEditorData(data);
317
- let nodeKey: string | undefined = undefined;
318
- if (
319
- data &&
320
- typeof data === 'object' &&
321
- !Array.isArray(data) &&
322
- 'key' in (data as any)
323
- ) {
324
- nodeKey = (data as any).key as string | undefined;
325
- }
326
- logger.verbose(
327
- 'ProjectPage',
328
- 'attributes change',
329
- nodeKey ? { nodeKey } : undefined,
330
- );
331
- }}
332
- />
248
+ <AttributesEditorPanel projectColors={resolvedProjectColors} />
333
249
  </div>
334
250
  {isMobile && mobilePanel && (
335
251
  <button
@@ -343,109 +259,3 @@ export function ProjectPage({
343
259
  </div>
344
260
  );
345
261
  }
346
-
347
- function LoadingComponent() {
348
- return (
349
- <div className="rb-loading">
350
- <Lottie animationData={loadingAnimation as any} loop autoplay />
351
- </div>
352
- );
353
- }
354
-
355
- function deleteNodeFromTree(root: Node, target: Node): Node {
356
- if (root === null || root === undefined) return root;
357
- if (typeof root === 'string') return root;
358
-
359
- if (Array.isArray(root)) {
360
- let changed = false;
361
- const nextChildren: Node[] = [];
362
- for (const child of root) {
363
- if (child === target) {
364
- changed = true;
365
- continue;
366
- }
367
- const nextChild = deleteNodeFromTree(child, target);
368
- if (nextChild !== child) changed = true;
369
- nextChildren.push(nextChild);
370
- }
371
- return changed ? nextChildren : root;
372
- }
373
-
374
- const data = root as any;
375
- if ('children' in data) {
376
- const prev = data.children as Node;
377
- if (!prev) return root;
378
-
379
- if (Array.isArray(prev)) {
380
- let changed = false;
381
- const nextChildren: Node[] = [];
382
- for (const child of prev) {
383
- if (child === target) {
384
- changed = true;
385
- continue;
386
- }
387
- const nextChild = deleteNodeFromTree(child, target);
388
- if (nextChild !== child) changed = true;
389
- nextChildren.push(nextChild);
390
- }
391
- if (changed) {
392
- return { ...data, children: nextChildren } as Node;
393
- }
394
- return root;
395
- }
396
-
397
- if (prev === target) {
398
- return { ...data, children: '' } as Node;
399
- }
400
-
401
- const nextChild = deleteNodeFromTree(prev, target);
402
- if (nextChild !== prev) {
403
- return { ...data, children: nextChild } as Node;
404
- }
405
- }
406
-
407
- return root;
408
- }
409
-
410
- function isNodeRecord(node: Node): node is NodeData {
411
- return (
412
- node !== null &&
413
- node !== undefined &&
414
- typeof node === 'object' &&
415
- !Array.isArray(node)
416
- );
417
- }
418
-
419
- function nodeHasChild(parent: NodeData, potentialChild: Node): boolean {
420
- const { children } = parent;
421
- if (!children) return false;
422
- if (Array.isArray(children)) {
423
- return children.some((child) => child === potentialChild);
424
- }
425
- return children === potentialChild;
426
- }
427
-
428
- function findNodeByKey(root: Node, key?: string): Node | null {
429
- if (!key) return null;
430
- if (root === null || root === undefined) return null;
431
- if (typeof root === 'string') return null;
432
-
433
- if (Array.isArray(root)) {
434
- for (const child of root) {
435
- const found = findNodeByKey(child, key);
436
- if (found) return found;
437
- }
438
- return null;
439
- }
440
-
441
- const nodeData = root as NodeData;
442
- if (nodeData.key === key) {
443
- return nodeData;
444
- }
445
-
446
- if (nodeData.children) {
447
- return findNodeByKey(nodeData.children as Node, key);
448
- }
449
-
450
- return null;
451
- }
@@ -4,8 +4,8 @@ import { Builder } from '../../components/Builder';
4
4
  import { useRenderStore } from '../../store';
5
5
 
6
6
  type BuilderPanelProps = {
7
- data: Node;
8
- setData: (data: Node) => void;
7
+ data?: Node;
8
+ setData?: (data: Node) => void;
9
9
  onDeleteNode: (node: Node) => void;
10
10
  };
11
11
 
@@ -15,10 +15,16 @@ export function BuilderPanel({
15
15
  onDeleteNode,
16
16
  }: BuilderPanelProps) {
17
17
  useLogRender('BuilderPanel');
18
- const { current, setCurrent } = useRenderStore((s) => ({
19
- current: s.current,
20
- setCurrent: s.setCurrent,
21
- }));
18
+ const { current, setCurrent, editorData, setEditorData } = useRenderStore(
19
+ (s) => ({
20
+ current: s.current,
21
+ setCurrent: s.setCurrent,
22
+ editorData: s.editorData,
23
+ setEditorData: s.setEditorData,
24
+ }),
25
+ );
26
+ const effectiveData = data ?? editorData;
27
+ const effectiveSetData = setData ?? setEditorData;
22
28
  return (
23
29
  <div
24
30
  role="region"
@@ -26,8 +32,8 @@ export function BuilderPanel({
26
32
  aria-hidden={false}
27
33
  >
28
34
  <Builder
29
- data={data}
30
- setData={setData}
35
+ data={effectiveData}
36
+ setData={effectiveSetData}
31
37
  current={current}
32
38
  setCurrent={setCurrent}
33
39
  onDeleteNode={onDeleteNode}
package/src/store.ts CHANGED
@@ -1,13 +1,8 @@
1
1
  import { createWithEqualityFn } from 'zustand/traditional';
2
2
  import { shallow } from 'zustand/shallow';
3
3
  import type { Device } from './types/Device';
4
- import {
5
- defaultAppConfig,
6
- type AppConfig,
7
- type Localication,
8
- } from './types/PreviewConfig';
4
+ import { defaultAppConfig, type AppConfig } from './types/PreviewConfig';
9
5
  import { getDefaultDevice } from './utils/getDevices';
10
- import { ScreenStyle } from './RenderPage';
11
6
  import { createJSONStorage, persist } from 'zustand/middleware';
12
7
  import { Node } from './types/Node';
13
8
  import type { LogEntry, LogLevel, ProjectColors } from './types/Project';
@@ -20,6 +15,12 @@ import type {
20
15
  type RenderStore = {
21
16
  projectName: string;
22
17
  setProjectName: (name: string) => void;
18
+ /**
19
+ * Root node tree being edited (a.k.a. "editor data").
20
+ * Not persisted: it comes from the currently opened project.
21
+ */
22
+ editorData: Node | null;
23
+ setEditorData: (data: Node | null) => void;
23
24
  copiedNode: Node | null;
24
25
  setCopiedNode: (node: Node | null) => void;
25
26
  current: Node | null;
@@ -66,6 +67,8 @@ export const useRenderStore = createWithEqualityFn<RenderStore>()(
66
67
  (set) => ({
67
68
  projectName: '',
68
69
  setProjectName: (name) => set({ projectName: name }),
70
+ editorData: null,
71
+ setEditorData: (data) => set({ editorData: data, current: data }),
69
72
  copiedNode: null,
70
73
  setCopiedNode: (node) => set({ copiedNode: node }),
71
74
  current: null,
@@ -193,6 +196,7 @@ export const useRenderStore = createWithEqualityFn<RenderStore>()(
193
196
  name: 'render-store',
194
197
  partialize: (state) => ({
195
198
  // Explicitly DO NOT persist projectName (it comes from ProjectPage props)
199
+ // Explicitly DO NOT persist editorData/current (they come from the opened project)
196
200
  copiedNode: state.copiedNode ?? null,
197
201
  logLevel: state.logLevel,
198
202
  products: state.products,
@@ -165,6 +165,7 @@ body,
165
165
  display: flex;
166
166
  align-items: center;
167
167
  gap: sizes.$spaceCozy;
168
+ flex-wrap: wrap;
168
169
  font-size: 12px;
169
170
  color: colors.$mutedTextColor;
170
171
  }
@@ -200,11 +201,15 @@ body,
200
201
  display: flex;
201
202
  align-items: center;
202
203
  gap: sizes.$spaceCompact;
204
+ flex-wrap: wrap;
205
+ row-gap: sizes.$spaceTight;
203
206
  }
204
207
 
205
208
  .breadcrumb__item {
206
209
  display: inline-flex;
207
210
  align-items: center;
211
+ max-width: 100%;
212
+ overflow-wrap: anywhere;
208
213
  &--clickable {
209
214
  cursor: pointer;
210
215
  }
@@ -212,7 +217,7 @@ body,
212
217
 
213
218
  .breadcrumb__separator {
214
219
  color: colors.$borderColor;
215
- margin: 0 sizes.$spaceTight;
220
+ margin: 0;
216
221
  }
217
222
 
218
223
  .breadcrumb__link {
@@ -384,9 +389,6 @@ body,
384
389
  top: sizes.$spaceCozy;
385
390
  z-index: 5;
386
391
  padding-bottom: sizes.$spaceCozy;
387
- .side-tool {
388
- width: 100%;
389
- }
390
392
  }
391
393
  }
392
394
  .screen-preview {
@@ -395,6 +397,12 @@ body,
395
397
  height: 100%;
396
398
  }
397
399
 
400
+ // Custom blue/glow cursor while preview mode is enabled
401
+ .screen-preview--preview {
402
+ cursor: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='32'%20height='32'%20viewBox='0%200%2032%2032'%3E%3Cdefs%3E%3Cfilter%20id='g'%20x='-50%25'%20y='-50%25'%20width='200%25'%20height='200%25'%3E%3CfeDropShadow%20dx='0'%20dy='0'%20stdDeviation='1.6'%20flood-color='%2360a5fa'%20flood-opacity='0.95'/%3E%3C/filter%3E%3C/defs%3E%3Cpath%20filter='url(%23g)'%20d='M6%202%20L6%2022%20L11%2017%20L15%2027%20L18%2026%20L14%2016%20L21%2016%20Z'%20fill='%2360a5fa'%20stroke='%231d4ed8'%20stroke-width='1'/%3E%3C/svg%3E") 6 2,
403
+ pointer;
404
+ }
405
+
398
406
  /* ProjectPage loading overlay */
399
407
  .rb-loading-overlay {
400
408
  position: absolute;
@@ -190,8 +190,16 @@
190
190
  border-radius: sizes.$radiusSoft;
191
191
  font-size: sizes.$fontSizeSm;
192
192
  line-height: 1.4;
193
- width: sizes.$dimensionSizeGridMax;
193
+ // Prevent long descriptions (e.g. URLs or long tokens) from overflowing the tooltip bubble.
194
+ width: min(
195
+ sizes.$dimensionSizeGridMax,
196
+ calc(100vw - (2 * sizes.$spaceComfy))
197
+ );
194
198
  text-align: left;
199
+ white-space: normal;
200
+ overflow-wrap: anywhere;
201
+ word-break: break-word;
202
+ hyphens: auto;
195
203
  box-shadow: sizes.$shadowTooltip;
196
204
  z-index: sizes.$zIndexRaised;
197
205
  opacity: 0;
@@ -0,0 +1,113 @@
1
+ @use '../foundation/colors';
2
+
3
+ .rb-bottom-bar {
4
+ position: fixed;
5
+ left: 50%;
6
+ transform: translateX(-66%);
7
+ bottom: 10px;
8
+
9
+ height: 50px;
10
+ border-radius: 24px;
11
+
12
+ background-color: #fff;
13
+ color: colors.$textColor;
14
+
15
+ display: flex;
16
+ flex-direction: row;
17
+ align-items: center;
18
+ justify-content: flex-start;
19
+
20
+ padding: 0 10px;
21
+ gap: 10px;
22
+
23
+ border: 1px solid rgba(colors.$textColor, 0.06);
24
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
25
+ }
26
+
27
+ .rb-bottom-bar__button {
28
+ appearance: none;
29
+ border: none;
30
+ background: transparent;
31
+ padding: 8px;
32
+ /* lighter icon color */
33
+ color: colors.$mutedTextColor;
34
+ cursor: pointer;
35
+
36
+ display: inline-flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ }
40
+
41
+ .rb-bottom-bar__button.is-active {
42
+ color: colors.$accentColor;
43
+ background: rgba(colors.$accentColor, 0.08);
44
+ box-shadow: 0 0 18px rgba(colors.$accentColor, 0.28);
45
+ }
46
+
47
+ .rb-bottom-bar__button--preview.is-active {
48
+ // make preview cursor extra obvious
49
+ box-shadow:
50
+ 0 0 0 3px rgba(colors.$accentColor, 0.18),
51
+ 0 0 26px rgba(colors.$accentColor, 0.35);
52
+ }
53
+
54
+ .rb-bottom-bar__button:focus-visible {
55
+ outline: none;
56
+ box-shadow:
57
+ 0 0 0 3px rgba(colors.$accentColor, 0.18),
58
+ 0 0 18px rgba(colors.$accentColor, 0.22);
59
+ }
60
+
61
+ /* border between each tool button */
62
+ .rb-bottom-bar__button + .rb-bottom-bar__button {
63
+ border-left: 1px solid rgba(colors.$textColor, 0.06);
64
+ padding-left: 14px;
65
+ margin-left: 4px;
66
+ }
67
+
68
+ .rb-bottom-bar__button--rtl {
69
+ gap: 6px;
70
+ padding-right: 10px;
71
+ }
72
+
73
+ .rb-bottom-bar__rtl-text {
74
+ font-size: 12px;
75
+ font-weight: 700;
76
+ letter-spacing: 0.3px;
77
+ color: colors.$textColor;
78
+ }
79
+
80
+ .rb-bottom-bar__spacer {
81
+ flex: 1 1 auto;
82
+ }
83
+
84
+ .rb-bottom-bar__langs {
85
+ display: inline-flex;
86
+ align-items: center;
87
+ gap: 0;
88
+ color: colors.$textColor;
89
+ }
90
+
91
+ .rb-bottom-bar__lang {
92
+ appearance: none;
93
+ border: none;
94
+ background: transparent;
95
+ padding: 6px 6px;
96
+ cursor: pointer;
97
+ font-size: 12px;
98
+ font-weight: 700;
99
+ text-transform: lowercase;
100
+ color: inherit;
101
+ }
102
+
103
+ .rb-bottom-bar__lang.is-active {
104
+ color: colors.$accentColor;
105
+ text-shadow: 0 0 12px rgba(colors.$accentColor, 0.25);
106
+ }
107
+
108
+ /* border between each language button */
109
+ .rb-bottom-bar__lang + .rb-bottom-bar__lang {
110
+ border-left: 1px solid rgba(colors.$textColor, 0.06);
111
+ margin-left: 8px;
112
+ padding-left: 14px;
113
+ }