create-vizcraft-playground 0.1.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/index.js +171 -0
- package/package.json +44 -0
- package/template/.github/skills/vizcraft-playground/SKILL.md +573 -0
- package/template/README.md +59 -0
- package/template/eslint.config.js +23 -0
- package/template/index.html +13 -0
- package/template/package.json +37 -0
- package/template/public/vite.svg +1 -0
- package/template/scripts/generate-plugin.js +594 -0
- package/template/scripts/init-playground.js +119 -0
- package/template/src/App.scss +137 -0
- package/template/src/App.tsx +72 -0
- package/template/src/assets/react.svg +1 -0
- package/template/src/components/InfoModal/InfoModal.scss +211 -0
- package/template/src/components/InfoModal/InfoModal.tsx +85 -0
- package/template/src/components/Landing/Landing.scss +85 -0
- package/template/src/components/Landing/Landing.tsx +55 -0
- package/template/src/components/Shell.tsx +144 -0
- package/template/src/components/StepIndicator/StepIndicator.scss +151 -0
- package/template/src/components/StepIndicator/StepIndicator.tsx +73 -0
- package/template/src/components/VizInfoBeacon/VizInfoBeacon.scss +41 -0
- package/template/src/components/VizInfoBeacon/VizInfoBeacon.tsx +157 -0
- package/template/src/components/plugin-kit/CanvasStage.tsx +30 -0
- package/template/src/components/plugin-kit/ConceptPills.tsx +55 -0
- package/template/src/components/plugin-kit/PluginLayout.tsx +41 -0
- package/template/src/components/plugin-kit/SidePanel.tsx +69 -0
- package/template/src/components/plugin-kit/StageHeader.tsx +35 -0
- package/template/src/components/plugin-kit/StatBadge.tsx +35 -0
- package/template/src/components/plugin-kit/index.ts +42 -0
- package/template/src/components/plugin-kit/plugin-kit.scss +241 -0
- package/template/src/components/plugin-kit/useConceptModal.tsx +51 -0
- package/template/src/index.scss +81 -0
- package/template/src/main.tsx +14 -0
- package/template/src/playground.config.ts +27 -0
- package/template/src/plugins/hello-world/concepts.tsx +70 -0
- package/template/src/plugins/hello-world/helloWorldSlice.ts +29 -0
- package/template/src/plugins/hello-world/index.ts +48 -0
- package/template/src/plugins/hello-world/main.scss +32 -0
- package/template/src/plugins/hello-world/main.tsx +144 -0
- package/template/src/plugins/hello-world/useHelloWorldAnimation.ts +99 -0
- package/template/src/registry.ts +73 -0
- package/template/src/store/slices/simulationSlice.ts +47 -0
- package/template/src/store/store.ts +13 -0
- package/template/src/types/ModelPlugin.ts +55 -0
- package/template/src/utils/random.ts +11 -0
- package/template/tsconfig.app.json +35 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +26 -0
- package/template/vite.config.ts +7 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface CanvasStageProps {
|
|
4
|
+
/** Ref forwarded to the inner canvas div (for VizCraft mounting). */
|
|
5
|
+
canvasRef?: React.Ref<HTMLDivElement>;
|
|
6
|
+
/** Content rendered inside the stage but outside the canvas (e.g. VizInfoBeacons). */
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The main visualisation canvas wrapper with grid-dot background.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* <CanvasStage canvasRef={containerRef}>
|
|
16
|
+
* <VizInfoBeacon … />
|
|
17
|
+
* </CanvasStage>
|
|
18
|
+
*/
|
|
19
|
+
const CanvasStage: React.FC<CanvasStageProps> = ({
|
|
20
|
+
canvasRef,
|
|
21
|
+
children,
|
|
22
|
+
className,
|
|
23
|
+
}) => (
|
|
24
|
+
<div className={`vc-canvas-stage${className ? ` ${className}` : ""}`}>
|
|
25
|
+
<div className="vc-canvas-stage__canvas" ref={canvasRef} />
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export default CanvasStage;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface PillDef {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** CSS class modifier appended as `vc-pill--{variant}`.
|
|
7
|
+
* Alternatively supply `color` + `borderColor` inline. */
|
|
8
|
+
variant?: string;
|
|
9
|
+
color?: string;
|
|
10
|
+
borderColor?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ConceptPillsProps {
|
|
14
|
+
pills: PillDef[];
|
|
15
|
+
onOpen: (key: string) => void;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A horizontal row of clickable concept pills.
|
|
21
|
+
* Each pill fires `onOpen(pill.key)` — typically to open an InfoModal.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <ConceptPills
|
|
25
|
+
* pills={[
|
|
26
|
+
* { key: "kafka", label: "Kafka", variant: "kafka" },
|
|
27
|
+
* { key: "partitioning", label: "Partitioning", color: "#fde68a", borderColor: "#f59e0b" },
|
|
28
|
+
* ]}
|
|
29
|
+
* onOpen={openConcept}
|
|
30
|
+
* />
|
|
31
|
+
*/
|
|
32
|
+
const ConceptPills: React.FC<ConceptPillsProps> = ({
|
|
33
|
+
pills,
|
|
34
|
+
onOpen,
|
|
35
|
+
className,
|
|
36
|
+
}) => (
|
|
37
|
+
<div className={`vc-pills${className ? ` ${className}` : ""}`}>
|
|
38
|
+
{pills.map((p) => (
|
|
39
|
+
<button
|
|
40
|
+
key={p.key}
|
|
41
|
+
className={`vc-pill${p.variant ? ` vc-pill--${p.variant}` : ""}`}
|
|
42
|
+
style={
|
|
43
|
+
!p.variant && p.color
|
|
44
|
+
? { color: p.color, borderColor: p.borderColor ?? p.color }
|
|
45
|
+
: undefined
|
|
46
|
+
}
|
|
47
|
+
onClick={() => onOpen(p.key)}
|
|
48
|
+
>
|
|
49
|
+
{p.label}
|
|
50
|
+
</button>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export default ConceptPills;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface PluginLayoutProps {
|
|
4
|
+
/** Content rendered above the body grid (pills bar, toolbar). */
|
|
5
|
+
toolbar?: React.ReactNode;
|
|
6
|
+
/** The main visualisation area (left column). */
|
|
7
|
+
canvas: React.ReactNode;
|
|
8
|
+
/** The right-hand sidebar (right column). Omit for full-width canvas. */
|
|
9
|
+
sidebar?: React.ReactNode;
|
|
10
|
+
/** Extra class on the root. */
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Standard two-column plugin layout: toolbar → body (canvas + sidebar).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* <PluginLayout
|
|
19
|
+
* toolbar={<ConceptPills pills={pills} onOpen={openConcept} />}
|
|
20
|
+
* canvas={<CanvasStage canvasRef={ref} />}
|
|
21
|
+
* sidebar={<SidePanel>…</SidePanel>}
|
|
22
|
+
* />
|
|
23
|
+
*/
|
|
24
|
+
const PluginLayout: React.FC<PluginLayoutProps> = ({
|
|
25
|
+
toolbar,
|
|
26
|
+
canvas,
|
|
27
|
+
sidebar,
|
|
28
|
+
className,
|
|
29
|
+
}) => (
|
|
30
|
+
<div className={`vc-plugin-layout${className ? ` ${className}` : ""}`}>
|
|
31
|
+
{toolbar}
|
|
32
|
+
<div
|
|
33
|
+
className={`vc-plugin-layout__body${sidebar ? "" : " vc-plugin-layout__body--full"}`}
|
|
34
|
+
>
|
|
35
|
+
{canvas}
|
|
36
|
+
{sidebar}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export default PluginLayout;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
/* ─── SidePanel ───────────────────────────────────────── */
|
|
4
|
+
|
|
5
|
+
export interface SidePanelProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Scrollable sidebar column. Place `<SideCard>` elements inside.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <SidePanel>
|
|
15
|
+
* <SideCard label="What's happening"><p>{explanation}</p></SideCard>
|
|
16
|
+
* <SideCard variant="code" label="Source">…</SideCard>
|
|
17
|
+
* </SidePanel>
|
|
18
|
+
*/
|
|
19
|
+
const SidePanel: React.FC<SidePanelProps> = ({ children, className }) => (
|
|
20
|
+
<aside className={`vc-side-panel${className ? ` ${className}` : ""}`}>
|
|
21
|
+
{children}
|
|
22
|
+
</aside>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
/* ─── SideCard ────────────────────────────────────────── */
|
|
26
|
+
|
|
27
|
+
export interface SideCardProps {
|
|
28
|
+
/** Small uppercase label at the top of the card. */
|
|
29
|
+
label?: string;
|
|
30
|
+
/** Optional heading + sub rendered in a head row. */
|
|
31
|
+
heading?: string;
|
|
32
|
+
sub?: string;
|
|
33
|
+
/** Extra class for variant styling, e.g. `vc-side-card--explanation`. */
|
|
34
|
+
variant?: string;
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A card inside a SidePanel.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* <SideCard label="What just happened?" variant="explanation">
|
|
44
|
+
* <p>{explanation}</p>
|
|
45
|
+
* </SideCard>
|
|
46
|
+
*/
|
|
47
|
+
const SideCard: React.FC<SideCardProps> = ({
|
|
48
|
+
label,
|
|
49
|
+
heading,
|
|
50
|
+
sub,
|
|
51
|
+
variant,
|
|
52
|
+
children,
|
|
53
|
+
className,
|
|
54
|
+
}) => (
|
|
55
|
+
<div
|
|
56
|
+
className={`vc-side-card${variant ? ` vc-side-card--${variant}` : ""}${className ? ` ${className}` : ""}`}
|
|
57
|
+
>
|
|
58
|
+
{label && <div className="vc-side-card__label">{label}</div>}
|
|
59
|
+
{(heading || sub) && (
|
|
60
|
+
<div className="vc-side-card__head">
|
|
61
|
+
{heading && <h3>{heading}</h3>}
|
|
62
|
+
{sub && <span className="vc-side-card__sub">{sub}</span>}
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
{children}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
export { SidePanel, SideCard };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface StageHeaderProps {
|
|
4
|
+
title: string;
|
|
5
|
+
subtitle?: string;
|
|
6
|
+
/** Slot rendered on the right — typically StatBadge components. */
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Title + subtitle + right-aligned stats area.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* <StageHeader title="ECS Autoscaling" subtitle="Watch containers scale">
|
|
16
|
+
* <StatBadge label="Phase" value="alarm" color="#fda4af" />
|
|
17
|
+
* <StatBadge label="Tasks" value="3/5" />
|
|
18
|
+
* </StageHeader>
|
|
19
|
+
*/
|
|
20
|
+
const StageHeader: React.FC<StageHeaderProps> = ({
|
|
21
|
+
title,
|
|
22
|
+
subtitle,
|
|
23
|
+
children,
|
|
24
|
+
className,
|
|
25
|
+
}) => (
|
|
26
|
+
<div className={`vc-stage-header${className ? ` ${className}` : ""}`}>
|
|
27
|
+
<div className="vc-stage-header__text">
|
|
28
|
+
<h2>{title}</h2>
|
|
29
|
+
{subtitle && <p>{subtitle}</p>}
|
|
30
|
+
</div>
|
|
31
|
+
{children && <div className="vc-stage-header__stats">{children}</div>}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export default StageHeader;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface StatBadgeProps {
|
|
4
|
+
label: string;
|
|
5
|
+
value: React.ReactNode;
|
|
6
|
+
/** Colour applied to the value text. */
|
|
7
|
+
color?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A small stat/phase chip: uppercase label over a bold value.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* <StatBadge label="Phase" value="sync" color="#93c5fd" />
|
|
16
|
+
* <StatBadge label="Tasks" value="3/5" />
|
|
17
|
+
*/
|
|
18
|
+
const StatBadge: React.FC<StatBadgeProps> = ({
|
|
19
|
+
label,
|
|
20
|
+
value,
|
|
21
|
+
color,
|
|
22
|
+
className,
|
|
23
|
+
}) => (
|
|
24
|
+
<div className={`vc-stat-badge${className ? ` ${className}` : ""}`}>
|
|
25
|
+
<span className="vc-stat-badge__label">{label}</span>
|
|
26
|
+
<span
|
|
27
|
+
className="vc-stat-badge__value"
|
|
28
|
+
style={color ? { color } : undefined}
|
|
29
|
+
>
|
|
30
|
+
{value}
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export default StatBadge;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════
|
|
2
|
+
* VizCraft Plugin Kit
|
|
3
|
+
*
|
|
4
|
+
* Shared, composable building blocks for plugin authors.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import {
|
|
8
|
+
* ConceptPills,
|
|
9
|
+
* useConceptModal,
|
|
10
|
+
* PluginLayout,
|
|
11
|
+
* StageHeader,
|
|
12
|
+
* StatBadge,
|
|
13
|
+
* SidePanel,
|
|
14
|
+
* SideCard,
|
|
15
|
+
* CanvasStage,
|
|
16
|
+
* } from "../../components/plugin-kit";
|
|
17
|
+
*
|
|
18
|
+
* Styles:
|
|
19
|
+
* @use "../../components/plugin-kit/plugin-kit";
|
|
20
|
+
* — or import plugin-kit.scss once in your main.scss
|
|
21
|
+
* ═══════════════════════════════════════════════════════ */
|
|
22
|
+
|
|
23
|
+
export { default as ConceptPills } from "./ConceptPills";
|
|
24
|
+
export type { PillDef, ConceptPillsProps } from "./ConceptPills";
|
|
25
|
+
|
|
26
|
+
export { useConceptModal } from "./useConceptModal";
|
|
27
|
+
export type { ConceptDefinition } from "./useConceptModal";
|
|
28
|
+
|
|
29
|
+
export { default as PluginLayout } from "./PluginLayout";
|
|
30
|
+
export type { PluginLayoutProps } from "./PluginLayout";
|
|
31
|
+
|
|
32
|
+
export { default as StageHeader } from "./StageHeader";
|
|
33
|
+
export type { StageHeaderProps } from "./StageHeader";
|
|
34
|
+
|
|
35
|
+
export { default as StatBadge } from "./StatBadge";
|
|
36
|
+
export type { StatBadgeProps } from "./StatBadge";
|
|
37
|
+
|
|
38
|
+
export { SidePanel, SideCard } from "./SidePanel";
|
|
39
|
+
export type { SidePanelProps, SideCardProps } from "./SidePanel";
|
|
40
|
+
|
|
41
|
+
export { default as CanvasStage } from "./CanvasStage";
|
|
42
|
+
export type { CanvasStageProps } from "./CanvasStage";
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════
|
|
2
|
+
* VizCraft Plugin Kit — shared styles
|
|
3
|
+
*
|
|
4
|
+
* Import this once in your plugin's main.scss:
|
|
5
|
+
* @use "../../components/plugin-kit/plugin-kit";
|
|
6
|
+
*
|
|
7
|
+
* Or import globally in index.scss.
|
|
8
|
+
*
|
|
9
|
+
* All classes are prefixed `vc-` to avoid collisions
|
|
10
|
+
* with plugin-specific styles.
|
|
11
|
+
* ═══════════════════════════════════════════════════════ */
|
|
12
|
+
|
|
13
|
+
/* ── Plugin Layout ──────────────────────────────────── */
|
|
14
|
+
.vc-plugin-layout {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.vc-plugin-layout__body {
|
|
23
|
+
flex: 1;
|
|
24
|
+
min-height: 0;
|
|
25
|
+
display: grid;
|
|
26
|
+
grid-template-columns: minmax(0, 1fr) 320px;
|
|
27
|
+
gap: 1rem;
|
|
28
|
+
padding: 1rem;
|
|
29
|
+
|
|
30
|
+
&--full {
|
|
31
|
+
grid-template-columns: 1fr;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* ── Concept Pills ──────────────────────────────────── */
|
|
36
|
+
.vc-pills {
|
|
37
|
+
display: flex;
|
|
38
|
+
gap: 0.5rem;
|
|
39
|
+
flex-wrap: wrap;
|
|
40
|
+
padding: 0.65rem 1rem;
|
|
41
|
+
flex-shrink: 0;
|
|
42
|
+
border-bottom: 1px solid rgba(148, 163, 184, 0.12);
|
|
43
|
+
background: rgba(2, 6, 23, 0.52);
|
|
44
|
+
backdrop-filter: blur(12px);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.vc-pill {
|
|
48
|
+
border: 1px solid transparent;
|
|
49
|
+
border-radius: 999px;
|
|
50
|
+
padding: 0.32rem 0.75rem;
|
|
51
|
+
background: rgba(15, 23, 42, 0.9);
|
|
52
|
+
font-size: 0.72rem;
|
|
53
|
+
font-weight: 700;
|
|
54
|
+
color: #fff;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
line-height: 1.3;
|
|
57
|
+
transition:
|
|
58
|
+
transform 0.14s ease,
|
|
59
|
+
background 0.14s ease,
|
|
60
|
+
border-color 0.14s ease;
|
|
61
|
+
|
|
62
|
+
&:hover {
|
|
63
|
+
transform: translateY(-1px);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&:active {
|
|
67
|
+
transform: translateY(0);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* ── Light-theme pills (e.g. Big O) ─────────────────── */
|
|
72
|
+
.vc-pills--light {
|
|
73
|
+
background: rgba(255, 255, 255, 0.72);
|
|
74
|
+
backdrop-filter: blur(10px);
|
|
75
|
+
border-bottom-color: rgba(251, 146, 60, 0.18);
|
|
76
|
+
|
|
77
|
+
.vc-pill {
|
|
78
|
+
background: #fff;
|
|
79
|
+
color: #1f2937;
|
|
80
|
+
box-shadow: 0 14px 30px -18px rgba(15, 23, 42, 0.45);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ── Stage Header ───────────────────────────────────── */
|
|
85
|
+
.vc-stage-header {
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: flex-start;
|
|
88
|
+
justify-content: space-between;
|
|
89
|
+
gap: 0.8rem;
|
|
90
|
+
margin-bottom: 0.9rem;
|
|
91
|
+
|
|
92
|
+
h2 {
|
|
93
|
+
margin: 0;
|
|
94
|
+
font-size: 1.45rem;
|
|
95
|
+
letter-spacing: -0.02em;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
p {
|
|
99
|
+
margin: 0.35rem 0 0;
|
|
100
|
+
color: #94a3b8;
|
|
101
|
+
font-size: 0.9rem;
|
|
102
|
+
line-height: 1.5;
|
|
103
|
+
max-width: 42rem;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.vc-stage-header__stats {
|
|
108
|
+
display: flex;
|
|
109
|
+
gap: 0.55rem;
|
|
110
|
+
flex-wrap: wrap;
|
|
111
|
+
justify-content: flex-end;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ── Stat Badge ─────────────────────────────────────── */
|
|
115
|
+
.vc-stat-badge {
|
|
116
|
+
min-width: 88px;
|
|
117
|
+
padding: 0.55rem 0.75rem;
|
|
118
|
+
border-radius: 16px;
|
|
119
|
+
background: rgba(15, 23, 42, 0.78);
|
|
120
|
+
border: 1px solid rgba(148, 163, 184, 0.12);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.vc-stat-badge__label {
|
|
124
|
+
display: block;
|
|
125
|
+
color: #94a3b8;
|
|
126
|
+
font-size: 0.66rem;
|
|
127
|
+
font-weight: 700;
|
|
128
|
+
text-transform: uppercase;
|
|
129
|
+
letter-spacing: 0.07em;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.vc-stat-badge__value {
|
|
133
|
+
display: block;
|
|
134
|
+
margin-top: 0.2rem;
|
|
135
|
+
font-weight: 700;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ── Side Panel ─────────────────────────────────────── */
|
|
139
|
+
.vc-side-panel {
|
|
140
|
+
min-height: 0;
|
|
141
|
+
display: flex;
|
|
142
|
+
flex-direction: column;
|
|
143
|
+
gap: 0.8rem;
|
|
144
|
+
overflow-y: auto;
|
|
145
|
+
padding-right: 0.15rem;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ── Side Card ──────────────────────────────────────── */
|
|
149
|
+
.vc-side-card {
|
|
150
|
+
border-radius: 20px;
|
|
151
|
+
padding: 0.95rem 1rem;
|
|
152
|
+
background: rgba(7, 17, 34, 0.88);
|
|
153
|
+
border: 1px solid rgba(148, 163, 184, 0.18);
|
|
154
|
+
box-shadow: 0 20px 42px -28px rgba(0, 0, 0, 0.7);
|
|
155
|
+
|
|
156
|
+
h3 {
|
|
157
|
+
margin: 0;
|
|
158
|
+
font-size: 0.96rem;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
p {
|
|
162
|
+
margin: 0.4rem 0 0;
|
|
163
|
+
color: #cbd5e1;
|
|
164
|
+
font-size: 0.84rem;
|
|
165
|
+
line-height: 1.55;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.vc-side-card__label {
|
|
170
|
+
display: block;
|
|
171
|
+
color: #94a3b8;
|
|
172
|
+
font-size: 0.66rem;
|
|
173
|
+
font-weight: 700;
|
|
174
|
+
text-transform: uppercase;
|
|
175
|
+
letter-spacing: 0.07em;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.vc-side-card__head {
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: flex-start;
|
|
181
|
+
justify-content: space-between;
|
|
182
|
+
gap: 0.8rem;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.vc-side-card__sub {
|
|
186
|
+
display: block;
|
|
187
|
+
color: #94a3b8;
|
|
188
|
+
font-size: 0.66rem;
|
|
189
|
+
font-weight: 700;
|
|
190
|
+
text-transform: uppercase;
|
|
191
|
+
letter-spacing: 0.07em;
|
|
192
|
+
text-align: right;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.vc-side-card--explanation {
|
|
196
|
+
background:
|
|
197
|
+
linear-gradient(135deg, rgba(15, 23, 42, 0.96), rgba(12, 74, 110, 0.44)),
|
|
198
|
+
rgba(7, 17, 34, 0.88);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* ── Light-theme card (e.g. Big O) ───────────────────── */
|
|
202
|
+
.vc-side-card--light {
|
|
203
|
+
background: rgba(255, 255, 255, 0.82);
|
|
204
|
+
border-color: rgba(148, 163, 184, 0.32);
|
|
205
|
+
box-shadow: 0 14px 30px -18px rgba(15, 23, 42, 0.45);
|
|
206
|
+
|
|
207
|
+
p {
|
|
208
|
+
color: #4b5563;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ── Canvas Stage ───────────────────────────────────── */
|
|
213
|
+
.vc-canvas-stage {
|
|
214
|
+
position: relative;
|
|
215
|
+
flex: 1;
|
|
216
|
+
min-height: 0;
|
|
217
|
+
border-radius: 22px;
|
|
218
|
+
overflow: hidden;
|
|
219
|
+
border: 1px solid rgba(148, 163, 184, 0.12);
|
|
220
|
+
background:
|
|
221
|
+
linear-gradient(180deg, rgba(2, 6, 23, 0.96), rgba(8, 15, 30, 0.96)),
|
|
222
|
+
repeating-linear-gradient(
|
|
223
|
+
0deg,
|
|
224
|
+
rgba(148, 163, 184, 0.03),
|
|
225
|
+
rgba(148, 163, 184, 0.03) 1px,
|
|
226
|
+
transparent 1px,
|
|
227
|
+
transparent 34px
|
|
228
|
+
),
|
|
229
|
+
repeating-linear-gradient(
|
|
230
|
+
90deg,
|
|
231
|
+
rgba(148, 163, 184, 0.03),
|
|
232
|
+
rgba(148, 163, 184, 0.03) 1px,
|
|
233
|
+
transparent 1px,
|
|
234
|
+
transparent 34px
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.vc-canvas-stage__canvas {
|
|
239
|
+
width: 100%;
|
|
240
|
+
height: 100%;
|
|
241
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { useCallback, useState } from "react";
|
|
2
|
+
import InfoModal from "../InfoModal/InfoModal";
|
|
3
|
+
import type { InfoModalSection } from "../InfoModal/InfoModal";
|
|
4
|
+
|
|
5
|
+
export interface ConceptDefinition {
|
|
6
|
+
title: string;
|
|
7
|
+
subtitle: string;
|
|
8
|
+
accentColor: string;
|
|
9
|
+
sections: InfoModalSection[];
|
|
10
|
+
aside?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Manages concept-modal state for a plugin.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const { openConcept, ConceptModal } = useConceptModal(concepts);
|
|
18
|
+
*
|
|
19
|
+
* return (
|
|
20
|
+
* <>
|
|
21
|
+
* <ConceptPills pills={pills} onOpen={openConcept} />
|
|
22
|
+
* <ConceptModal />
|
|
23
|
+
* </>
|
|
24
|
+
* );
|
|
25
|
+
*/
|
|
26
|
+
export function useConceptModal<K extends string>(
|
|
27
|
+
concepts: Record<K, ConceptDefinition>,
|
|
28
|
+
) {
|
|
29
|
+
const [activeConcept, setActiveConcept] = useState<K | null>(null);
|
|
30
|
+
|
|
31
|
+
const openConcept = useCallback((key: K) => setActiveConcept(key), []);
|
|
32
|
+
const closeConcept = useCallback(() => setActiveConcept(null), []);
|
|
33
|
+
|
|
34
|
+
const ConceptModal: React.FC = () => {
|
|
35
|
+
if (!activeConcept) return null;
|
|
36
|
+
const c = concepts[activeConcept];
|
|
37
|
+
return (
|
|
38
|
+
<InfoModal
|
|
39
|
+
isOpen
|
|
40
|
+
onClose={closeConcept}
|
|
41
|
+
title={c.title}
|
|
42
|
+
subtitle={c.subtitle}
|
|
43
|
+
accentColor={c.accentColor}
|
|
44
|
+
sections={c.sections}
|
|
45
|
+
aside={c.aside}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return { activeConcept, openConcept, closeConcept, ConceptModal } as const;
|
|
51
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
@import "./components/plugin-kit/plugin-kit.scss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
5
|
+
line-height: 1.5;
|
|
6
|
+
font-weight: 400;
|
|
7
|
+
|
|
8
|
+
color-scheme: light dark;
|
|
9
|
+
color: rgba(255, 255, 255, 0.87);
|
|
10
|
+
background-color: #242424;
|
|
11
|
+
|
|
12
|
+
font-synthesis: none;
|
|
13
|
+
text-rendering: optimizeLegibility;
|
|
14
|
+
-webkit-font-smoothing: antialiased;
|
|
15
|
+
-moz-osx-font-smoothing: grayscale;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
a {
|
|
19
|
+
font-weight: 500;
|
|
20
|
+
color: #646cff;
|
|
21
|
+
text-decoration: inherit;
|
|
22
|
+
}
|
|
23
|
+
a:hover {
|
|
24
|
+
color: #535bf2;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
html,
|
|
28
|
+
body {
|
|
29
|
+
margin: 0;
|
|
30
|
+
padding: 0;
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 100%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
body {
|
|
36
|
+
min-height: 100vh;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#root {
|
|
40
|
+
width: 100%;
|
|
41
|
+
height: 100%;
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
h1 {
|
|
47
|
+
font-size: 3.2em;
|
|
48
|
+
line-height: 1.1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
button {
|
|
52
|
+
border-radius: 8px;
|
|
53
|
+
border: 1px solid transparent;
|
|
54
|
+
padding: 0.6em 1.2em;
|
|
55
|
+
font-size: 1em;
|
|
56
|
+
font-weight: 500;
|
|
57
|
+
font-family: inherit;
|
|
58
|
+
background-color: #1a1a1a;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
transition: border-color 0.25s;
|
|
61
|
+
}
|
|
62
|
+
button:hover {
|
|
63
|
+
border-color: #646cff;
|
|
64
|
+
}
|
|
65
|
+
button:focus,
|
|
66
|
+
button:focus-visible {
|
|
67
|
+
outline: 4px auto -webkit-focus-ring-color;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@media (prefers-color-scheme: light) {
|
|
71
|
+
:root {
|
|
72
|
+
color: #213547;
|
|
73
|
+
background-color: #ffffff;
|
|
74
|
+
}
|
|
75
|
+
a:hover {
|
|
76
|
+
color: #747bff;
|
|
77
|
+
}
|
|
78
|
+
button {
|
|
79
|
+
background-color: #f9f9f9;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import { Provider } from 'react-redux'
|
|
4
|
+
import { store } from './store/store'
|
|
5
|
+
import './index.scss'
|
|
6
|
+
import App from './App.tsx'
|
|
7
|
+
|
|
8
|
+
createRoot(document.getElementById('root')!).render(
|
|
9
|
+
<StrictMode>
|
|
10
|
+
<Provider store={store}>
|
|
11
|
+
<App />
|
|
12
|
+
</Provider>
|
|
13
|
+
</StrictMode>,
|
|
14
|
+
)
|