First source commit
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -28,4 +28,6 @@ build
|
||||
|
||||
# Miscellaneous
|
||||
*.local
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
analyticsrc.json
|
||||
113
package.json
113
package.json
@@ -1,57 +1,131 @@
|
||||
{
|
||||
"name": "@armco/react-vite-rtk-template",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"name": "@armco/components-viewer",
|
||||
"description": "Components Viewer/Tester for React Components Library",
|
||||
"version": "0.0.47",
|
||||
"type": "module",
|
||||
"author": "Armco (@restruct-corporate-advantage)",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"start": "vite",
|
||||
"start": "NODE_ENV=production vite",
|
||||
"build": "tsc && vite build",
|
||||
"build:publish": "./scripts/build.sh",
|
||||
"build:publish:compile": "tsc --p ./tsconfig-build.json && vite build --config vite-publish.config.ts",
|
||||
"generate": "plop",
|
||||
"atom": "plop atom",
|
||||
"molecule": "plop molecule",
|
||||
"component": "plop component",
|
||||
"page": "plop page",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test": "NODE_ENV=development vitest",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint .",
|
||||
"type-check": "tsc"
|
||||
"type-check": "tsc",
|
||||
"publish:dry": "npm publish --dry-run",
|
||||
"publish:local": "./scripts/publish-local.sh",
|
||||
"publish:public": "./scripts/publish.sh",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@armco/analytics": "^0.2.8",
|
||||
"@lottiefiles/react-lottie-player": "^3.5.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@reduxjs/toolkit": "^1.8.1",
|
||||
"react": "^18.2.0",
|
||||
"@storybook/cli": "^7.0.23",
|
||||
"bootstrap": "^5.3.0",
|
||||
"classnames": "^2.3.2",
|
||||
"d3": "^7.9.0",
|
||||
"highcharts": "^11.2.0",
|
||||
"highcharts-react-official": "^3.2.1",
|
||||
"highlight.js": "^11.8.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lottie-react": "^2.4.0",
|
||||
"lottie-web": "^5.12.2",
|
||||
"moment": "^2.29.4",
|
||||
"react-app-polyfill": "^3.0.0",
|
||||
"react-bootstrap": "^2.7.4",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dnd": ">=16.0.0",
|
||||
"react-dnd-html5-backend": ">=16.0.0",
|
||||
"react-dnd-touch-backend": ">=16.0.0",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-redux": "^8.0.1",
|
||||
"react-router-dom": "^6.13.0"
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.13.0",
|
||||
"react-table": "^7.8.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"svgpath": "^2.6.0",
|
||||
"uuid": "^9.0.0",
|
||||
"vite-plugin-svgr": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@armco/types": "^0.0.6",
|
||||
"@babel/preset-env": "^7.24.5",
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@babel/preset-typescript": "^7.24.1",
|
||||
"@storybook/addon-essentials": "^7.0.23",
|
||||
"@storybook/addon-interactions": "^7.0.23",
|
||||
"@storybook/addon-links": "^7.0.23",
|
||||
"@storybook/blocks": "^7.0.23",
|
||||
"@storybook/react": "^7.0.23",
|
||||
"@storybook/react-vite": "^7.0.23",
|
||||
"@storybook/testing-library": "^0.0.14-next.2",
|
||||
"@testing-library/dom": "^9.2.0",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.2.5",
|
||||
"@types/bootstrap": "^5.2.6",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-resizable": "^3.0.7",
|
||||
"@types/react-table": "^7.7.19",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"chalk": "^5.3.0",
|
||||
"cherry-pick": "^0.5.0",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-storybook": "^0.6.12",
|
||||
"execa": "^8.0.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "^10.3.10",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jsdom": "^21.1.0",
|
||||
"plop": "^3.1.2",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-config-nick": "^1.0.2",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.63.4",
|
||||
"storybook": "^7.0.23",
|
||||
"ts-jest": "^29.2.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.0.0",
|
||||
"vite-plugin-dts": "^3.7.1",
|
||||
"vitest": "^0.30.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dnd": ">=16.0.0",
|
||||
"react-dnd-html5-backend": ">=16.0.0",
|
||||
"react-dnd-touch-backend": ">=16.0.0",
|
||||
"react-redux": "^8.0.1",
|
||||
"react-router-dom": "^6.13.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
"react-app/jest",
|
||||
"plugin:storybook/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"prettier"
|
||||
@@ -62,20 +136,29 @@
|
||||
}
|
||||
},
|
||||
"prettier": "prettier-config-nick",
|
||||
"main": "index.tsx",
|
||||
"types": "./build/index.d.ts",
|
||||
"main": "./build/index.js",
|
||||
"module": "./build/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ReStruct-Corporate-Advantage/.git"
|
||||
"url": "git+https://github.com/ReStruct-Corporate-Advantage/components-viewer.git"
|
||||
},
|
||||
"keywords": [
|
||||
"react",
|
||||
"vite",
|
||||
"reduxToolkit",
|
||||
"sass",
|
||||
"components",
|
||||
"atomic",
|
||||
"building-blocks",
|
||||
"foundation"
|
||||
],
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ReStruct-Corporate-Advantage/react-vite-rtk-template/issues"
|
||||
"url": "https://github.com/ReStruct-Corporate-Advantage/components-viewer/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ReStruct-Corporate-Advantage/react-vite-rtk-template#readme"
|
||||
"homepage": "https://github.com/ReStruct-Corporate-Advantage/components-viewer#readme"
|
||||
}
|
||||
|
||||
60
src/ComponentList.tsx
Executable file
60
src/ComponentList.tsx
Executable file
@@ -0,0 +1,60 @@
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { ArThemes, ComponentRepository, TreeListData } from "@armco/types"
|
||||
import {
|
||||
Adapter,
|
||||
COMPONENTS,
|
||||
ErrorBoundary,
|
||||
getCurrentTheme,
|
||||
Network,
|
||||
TreeList,
|
||||
useAppSelector,
|
||||
} from "."
|
||||
import * as atoms from "../atoms"
|
||||
import * as molecules from "../molecules"
|
||||
import "./ComponentList.component.scss"
|
||||
|
||||
const Components: ComponentRepository = { ...atoms, ...molecules }
|
||||
|
||||
interface ComponentListProps {}
|
||||
|
||||
const formattedTreeData = Adapter.adaptToTreeFromComponentConfig(COMPONENTS)
|
||||
|
||||
const ComponentList = (props: ComponentListProps): JSX.Element => {
|
||||
const navigate = useNavigate()
|
||||
const theme = useAppSelector<ArThemes>(getCurrentTheme)
|
||||
|
||||
const handleComponentSelect = (treeNode: TreeListData) => {
|
||||
const params = treeNode.data?.props
|
||||
treeNode.data?.component &&
|
||||
navigate(
|
||||
Network.stringifyUrl(`/components/${treeNode.data?.component}`, params),
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ar-ComponentList h-100 w-100 px-2">
|
||||
<TreeList
|
||||
onItemSelect={handleComponentSelect}
|
||||
data={formattedTreeData}
|
||||
title="Component List"
|
||||
firstExpanded={true}
|
||||
theme={theme}
|
||||
customTooltip={(item: TreeListData) => {
|
||||
const Component =
|
||||
typeof item.label === "string" && Components[item.label]
|
||||
return Component ? (
|
||||
<ErrorBoundary>
|
||||
<Component demo />
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
item.label
|
||||
)
|
||||
}}
|
||||
isDraggable
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ComponentList
|
||||
41
src/Editor.component.scss
Executable file
41
src/Editor.component.scss
Executable file
@@ -0,0 +1,41 @@
|
||||
.ar-Editor {
|
||||
border: 1px solid var(--ar-color-layout-border);
|
||||
.ar-Editor__tools {
|
||||
background-color: var(--ar-bg-base);
|
||||
border-bottom: 1px solid var(--ar-color-layout-border);
|
||||
}
|
||||
|
||||
.ar-Editor__prop-selector {
|
||||
min-height: 2.5rem;
|
||||
&::-webkit-scrollbar {
|
||||
height: 2px;
|
||||
background-color: #fefefe;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #cdcdcd;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #ababab;
|
||||
border-radius: 2px;
|
||||
width: 3rem;
|
||||
height: 10px;
|
||||
&:hover {
|
||||
background: #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#root.ar-Editor__frame__main {
|
||||
background-color: var(--ar-bg-tertiary);
|
||||
&.background {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
}
|
||||
}
|
||||
359
src/Editor.tsx
Executable file
359
src/Editor.tsx
Executable file
@@ -0,0 +1,359 @@
|
||||
import { ReactNode, useEffect, useState } from "react"
|
||||
import { useLocation, useNavigate } from "react-router-dom"
|
||||
import { createPortal } from "react-dom"
|
||||
import {
|
||||
ArDropdownVariants,
|
||||
ArSizes,
|
||||
ArThemes,
|
||||
ComponentDescription,
|
||||
FrameContentDefinition,
|
||||
FrameContentProps,
|
||||
PRIMITIVES,
|
||||
} from "@armco/types"
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
COMPONENTS,
|
||||
Toggle,
|
||||
Helper,
|
||||
Network,
|
||||
Dropdown,
|
||||
} from "."
|
||||
import StyleHelper from "./StyleHelper"
|
||||
import * as images from "../../static/images"
|
||||
import "./Editor.component.scss"
|
||||
|
||||
const children = {
|
||||
bg: (
|
||||
<div
|
||||
key="dummy-bg"
|
||||
className="ar-Editor__frame__dummy-child"
|
||||
style={{ height: "100vh", width: "100vw" }}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<span
|
||||
key="dummy-content"
|
||||
slot="content"
|
||||
className="bg"
|
||||
style={{ height: "10rem", width: "10rem" }}
|
||||
>
|
||||
Popover Content
|
||||
</span>
|
||||
),
|
||||
popover: (
|
||||
<div
|
||||
key="dummy-popover"
|
||||
slot="popover"
|
||||
style={{ height: "10rem", width: "10rem" }}
|
||||
>
|
||||
Popover Content
|
||||
</div>
|
||||
),
|
||||
list: (
|
||||
<div
|
||||
key="dummy-list"
|
||||
className="d-grid"
|
||||
style={{
|
||||
gridTemplateColumns: "repeat(auto-fill, 5rem)",
|
||||
gap: "1rem",
|
||||
justifyContent: "space-around",
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: 40 }, (_, i) => (
|
||||
<div
|
||||
key={"dummy-list-item-" + i}
|
||||
className="bg border border-radius-l2"
|
||||
style={{ width: "5rem", height: "5rem" }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
anchor: <Button content="Click Me!" slot="anchor" />,
|
||||
}
|
||||
const FrameContent = (
|
||||
props: FrameContentProps,
|
||||
): JSX.Element | string | null => {
|
||||
const { contentDefinition, notificationProps } = props
|
||||
const { component, props: componentProps, data } = contentDefinition || {}
|
||||
const background: string = data?.test?.background
|
||||
const SelectedComponent = component
|
||||
let childRenders: Array<JSX.Element> = []
|
||||
if (data?.type === "HOC") {
|
||||
childRenders = data.children.map(
|
||||
(name: string) => children[name as keyof object],
|
||||
)
|
||||
}
|
||||
return SelectedComponent ? (
|
||||
<div
|
||||
className={`ar-Editor__frame__main h-100 w-100 overflow-auto p-5 flex-column${
|
||||
background ? " background" : ""
|
||||
}`}
|
||||
style={{
|
||||
// @ts-ignore
|
||||
backgroundImage: background ? `url(${images[background]})` : "",
|
||||
}}
|
||||
id="root"
|
||||
>
|
||||
{data && data.type === "HOC" ? (
|
||||
<SelectedComponent {...componentProps}>
|
||||
{childRenders}
|
||||
</SelectedComponent>
|
||||
) : (
|
||||
<SelectedComponent {...componentProps} />
|
||||
)}
|
||||
{notificationProps && <Alert {...notificationProps} />}
|
||||
</div>
|
||||
) : (
|
||||
"Select an item from the side panel to view here"
|
||||
)
|
||||
}
|
||||
|
||||
const Editor = (): JSX.Element | string => {
|
||||
const [contentRef, setContentRef] = useState<HTMLIFrameElement | null>(null)
|
||||
const [notificationProps, setNotificationProps] = useState<any>(null)
|
||||
const [selectedComponentDefinition, setSelectedComponentDefinition] =
|
||||
useState<FrameContentDefinition | null>(null)
|
||||
// const [theme, setTheme] = useState<string>(ArThemes.LIGHT1)
|
||||
const [theme, setTheme] = useState<string>(ArThemes.DARK1)
|
||||
const [portal, setPortal] = useState<ReactNode>()
|
||||
const [component, setComponent] = useState<string>()
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
window.onmessage = (e) => {
|
||||
"ar" in e.data && setNotificationProps(e.data?.ar)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (theme) {
|
||||
const iframeHtml =
|
||||
contentRef?.contentWindow?.document?.getElementsByTagName("html")[0]
|
||||
iframeHtml && iframeHtml.setAttribute("ar-theme", theme)
|
||||
const iframeBody = iframeHtml?.getElementsByTagName("body")[0]
|
||||
iframeBody && (iframeBody.style.backgroundColor = "var(--ar-bg)")
|
||||
}
|
||||
}, [contentRef, theme])
|
||||
|
||||
useEffect(() => {
|
||||
const locationPathArr = location.pathname
|
||||
.split("/")
|
||||
.filter((p: string) => p)
|
||||
const componentName =
|
||||
locationPathArr.length > 1 && locationPathArr[locationPathArr.length - 1]
|
||||
if (componentName) {
|
||||
setComponent(componentName)
|
||||
const { selectedItem, hierarchy } = Helper.findComponentDescription(
|
||||
componentName,
|
||||
COMPONENTS,
|
||||
)
|
||||
if (selectedItem && hierarchy) {
|
||||
let props: { [key: string]: string | number | boolean | undefined } = {
|
||||
demo: true,
|
||||
}
|
||||
if (location.search) {
|
||||
const params = location.search.substring(1)?.split("&")
|
||||
if (params) {
|
||||
params.forEach((prop) => {
|
||||
const keyValue = prop.split("=")
|
||||
const name = keyValue && keyValue[0]
|
||||
let value: string | boolean = keyValue && keyValue[1]
|
||||
value = value === "true" ? true : decodeURIComponent(value)
|
||||
value =
|
||||
value === "false" ? false : decodeURIComponent(value as string)
|
||||
if (prop && value && selectedItem) {
|
||||
props[name] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
if (
|
||||
!selectedComponentDefinition ||
|
||||
selectedComponentDefinition.componentName !== componentName ||
|
||||
JSON.stringify(selectedComponentDefinition.props) !==
|
||||
JSON.stringify(props)
|
||||
) {
|
||||
const SelectedComponent = Helper.importComponent(
|
||||
componentName,
|
||||
hierarchy?.toLowerCase(),
|
||||
)
|
||||
setSelectedComponentDefinition({
|
||||
componentName,
|
||||
component: SelectedComponent,
|
||||
props,
|
||||
data: selectedItem,
|
||||
hierarchy,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
contentRef &&
|
||||
selectedComponentDefinition &&
|
||||
process.env.NODE_ENV !== "production"
|
||||
) {
|
||||
StyleHelper.injectComponentStyleInFrame(
|
||||
selectedComponentDefinition.data as ComponentDescription,
|
||||
selectedComponentDefinition.hierarchy,
|
||||
contentRef,
|
||||
)
|
||||
}
|
||||
}, [contentRef, selectedComponentDefinition])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedComponentDefinition) {
|
||||
const { demo, theme, ...selectedProps } =
|
||||
selectedComponentDefinition.props || {}
|
||||
const newUrl = Network.stringifyUrl("/", selectedProps).slice(1)
|
||||
selectedProps && newUrl !== location.search && navigate(newUrl)
|
||||
}
|
||||
}, [selectedComponentDefinition])
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef) {
|
||||
const bsLink =
|
||||
contentRef?.contentWindow?.document.getElementsByTagName("link")
|
||||
if (!bsLink || bsLink.length === 0) {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
StyleHelper.injectLinkInFrame(
|
||||
["index", "ComponentsViewerPage"],
|
||||
contentRef,
|
||||
)
|
||||
} else {
|
||||
StyleHelper.injectBootstrapInFrame(contentRef)
|
||||
StyleHelper.injectVariablesInFrame(contentRef)
|
||||
StyleHelper.injectAnimationInFrame(contentRef)
|
||||
StyleHelper.injectGlobalInFrame(contentRef)
|
||||
StyleHelper.injectEditorStyleInFrame(contentRef)
|
||||
StyleHelper.injectComponentStyleInFrame(
|
||||
{ name: "Alert" },
|
||||
"ATOMS",
|
||||
contentRef,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [contentRef])
|
||||
|
||||
const mountNode = contentRef?.contentWindow?.document?.body
|
||||
|
||||
useEffect(() => {
|
||||
if (mountNode) {
|
||||
if (selectedComponentDefinition && selectedComponentDefinition.props) {
|
||||
selectedComponentDefinition.props.theme = theme
|
||||
}
|
||||
setPortal(
|
||||
createPortal(
|
||||
<FrameContent
|
||||
contentDefinition={selectedComponentDefinition}
|
||||
notificationProps={notificationProps}
|
||||
/>,
|
||||
mountNode,
|
||||
),
|
||||
)
|
||||
}
|
||||
}, [selectedComponentDefinition, mountNode, notificationProps, theme])
|
||||
|
||||
const currentItemVariants = selectedComponentDefinition?.data.variants || []
|
||||
|
||||
return (
|
||||
<div className="ar-Editor w-100 h-100 d-flex flex-column">
|
||||
<div className="ar-Editor__tools">
|
||||
<div className="row justify-content-end">
|
||||
<div className="col flex-v-center justify-content-end">
|
||||
<div className="ar-Editor__prop-selector flex-v-center border-right me-3 overflow-auto py-2 px-3">
|
||||
{Object.keys(currentItemVariants)
|
||||
.filter((prop: string) => prop !== "demo")
|
||||
.map((prop: any, index: number, arr) => {
|
||||
const propsValues = currentItemVariants[prop]
|
||||
const isBoolean =
|
||||
propsValues.length === 2 &&
|
||||
propsValues.indexOf(true) > -1 &&
|
||||
propsValues.indexOf(false) > -1
|
||||
return isBoolean ? (
|
||||
<Toggle
|
||||
key={"prop-toggle-" + index + "-" + prop}
|
||||
classes="me-3"
|
||||
label={Helper.toReadable(prop)}
|
||||
onChange={(isChecked: boolean) =>
|
||||
selectedComponentDefinition?.componentName &&
|
||||
setSelectedComponentDefinition({
|
||||
...selectedComponentDefinition,
|
||||
props: {
|
||||
...(selectedComponentDefinition.props || {}),
|
||||
[prop]: isChecked,
|
||||
},
|
||||
})
|
||||
}
|
||||
isOn={Boolean(
|
||||
selectedComponentDefinition?.props &&
|
||||
selectedComponentDefinition?.props[prop],
|
||||
)}
|
||||
size={ArSizes.SMALL}
|
||||
hideStatus
|
||||
/>
|
||||
) : (
|
||||
<Dropdown
|
||||
key={"prop-dropdown-" + index + "-" + prop}
|
||||
classes="me-2"
|
||||
contentMatchAnchorWidth={false}
|
||||
options={propsValues.map((propValue: PRIMITIVES) => ({
|
||||
name: propValue,
|
||||
isSelected:
|
||||
selectedComponentDefinition?.props &&
|
||||
propValue + "" ===
|
||||
selectedComponentDefinition?.props[prop],
|
||||
}))}
|
||||
onSelectionChanged={(obj) => {
|
||||
selectedComponentDefinition?.componentName &&
|
||||
setSelectedComponentDefinition({
|
||||
...selectedComponentDefinition,
|
||||
props: {
|
||||
...(selectedComponentDefinition.props || {}),
|
||||
[prop]: obj.value,
|
||||
},
|
||||
})
|
||||
}}
|
||||
variant={ArDropdownVariants.SELECTIONSASPILLS}
|
||||
placeholder={Helper.toReadable(prop)}
|
||||
version="v2"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<span className="float-end pe-3">
|
||||
<Toggle
|
||||
isOn={theme === ArThemes.DARK1}
|
||||
toggleOnName="Dark"
|
||||
toggleOffName="Light"
|
||||
onChange={(checked: boolean) =>
|
||||
setTheme(checked ? ArThemes.DARK1 : ArThemes.LIGHT1)
|
||||
}
|
||||
value={true}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <FrameContent
|
||||
contentDefinition={selectedComponentDefinition}
|
||||
notificationProps={notificationProps}
|
||||
/> */}
|
||||
<iframe
|
||||
className="ar-Editor__frame w-100 h-100"
|
||||
ref={setContentRef}
|
||||
title="Armco Component Viewer"
|
||||
>
|
||||
{portal}
|
||||
</iframe>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Editor
|
||||
140
src/StyleHelper.ts
Normal file
140
src/StyleHelper.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { ComponentDescription } from "@armco/types"
|
||||
|
||||
//// Editor Frame Style related helpers
|
||||
class StyleHelper {
|
||||
static injectGlobalInFrame(frame: HTMLIFrameElement | null) {
|
||||
const clipCallback = (text: string) => {
|
||||
return {
|
||||
clipStart: text && text.indexOf("html {\\n font-size: 16px"),
|
||||
}
|
||||
}
|
||||
StyleHelper.injectScssInFrame(
|
||||
"/src/app/static/styles/_global.scss",
|
||||
frame,
|
||||
undefined,
|
||||
clipCallback,
|
||||
)
|
||||
}
|
||||
|
||||
static injectBootstrapInFrame(frame: HTMLIFrameElement | null) {
|
||||
StyleHelper.injectLinkInFrame(
|
||||
"/src/app/static/styles/bootstrap.min.css",
|
||||
frame,
|
||||
)
|
||||
}
|
||||
|
||||
static injectVariablesInFrame(frame: HTMLIFrameElement | null) {
|
||||
StyleHelper.injectScssInFrame(
|
||||
"/src/app/static/styles/_variables.scss",
|
||||
frame,
|
||||
)
|
||||
}
|
||||
|
||||
static injectAnimationInFrame(frame: HTMLIFrameElement | null) {
|
||||
StyleHelper.injectScssInFrame(
|
||||
"/src/app/static/styles/_animations.scss",
|
||||
frame,
|
||||
)
|
||||
}
|
||||
|
||||
static injectEditorStyleInFrame(frame: HTMLIFrameElement | null) {
|
||||
StyleHelper.injectScssInFrame(
|
||||
"/src/app/components/Editor/Editor.component.scss",
|
||||
frame,
|
||||
)
|
||||
}
|
||||
|
||||
static injectComponentStyleInFrame(
|
||||
selectedComponent: ComponentDescription,
|
||||
hierarchy: string,
|
||||
frame: HTMLIFrameElement | null,
|
||||
) {
|
||||
if (hierarchy && selectedComponent && selectedComponent.name) {
|
||||
const componentStyleElement =
|
||||
frame?.contentWindow?.document.querySelector("style.component-style")
|
||||
componentStyleElement && componentStyleElement.remove()
|
||||
const dependencyComponentsToInject = selectedComponent.uses
|
||||
? [
|
||||
{ name: selectedComponent.name, hierarchy },
|
||||
...selectedComponent.uses,
|
||||
]
|
||||
: [{ name: selectedComponent.name, hierarchy }]
|
||||
dependencyComponentsToInject.forEach(
|
||||
(component: { [key: string]: string }) => {
|
||||
const scssPath = `/src/app/components/${component.hierarchy.toLowerCase()}/${
|
||||
component.name
|
||||
}/${component.name}.component.scss`
|
||||
StyleHelper.injectScssInFrame(scssPath, frame, component)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static injectScssInFrame(
|
||||
scssPath: string,
|
||||
frame: HTMLIFrameElement | null,
|
||||
component?: { [key: string]: string },
|
||||
clipCallback?: any,
|
||||
) {
|
||||
import("sass").then((module) => {
|
||||
const { compileString } = module
|
||||
fetch(scssPath)
|
||||
.then((r) => r.text())
|
||||
.then((text) => {
|
||||
const { clipStart, clipEnd } = clipCallback
|
||||
? clipCallback(text)
|
||||
: () => ({})
|
||||
text = text
|
||||
.substring(
|
||||
clipStart ||
|
||||
text.indexOf("__vite__css = ") + '__vite__css = "'.length,
|
||||
clipEnd || text.indexOf("__vite__updateStyle(") - 2,
|
||||
)
|
||||
.replace(/\\n/g, "")
|
||||
.replace(/\\"/g, '"')
|
||||
const css = compileString(text).css
|
||||
StyleHelper.injectStyleInFrame(css, frame, component)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
static injectStyleInFrame(
|
||||
css: string,
|
||||
frame: HTMLIFrameElement | null,
|
||||
component?: { [key: string]: string },
|
||||
) {
|
||||
const style = document.createElement("style")
|
||||
if (component && component.name) {
|
||||
style.setAttribute("id", component.name)
|
||||
style.setAttribute("class", "component-style")
|
||||
}
|
||||
style.appendChild(document.createTextNode(css))
|
||||
frame?.contentWindow?.document?.head.appendChild(style)
|
||||
}
|
||||
|
||||
static injectLinkInFrame(
|
||||
link: string | Array<string>,
|
||||
frame: HTMLIFrameElement | null,
|
||||
) {
|
||||
if (Array.isArray(link)) {
|
||||
const links = document.querySelectorAll("link[rel='stylesheet']")
|
||||
link.forEach((item: string) => {
|
||||
Array.from(links).forEach((selectedLink) => {
|
||||
const cssUrl = selectedLink.getAttribute("href")
|
||||
if (cssUrl && cssUrl.startsWith(`/assets/${item}`)) {
|
||||
const clone = selectedLink.cloneNode(true)
|
||||
frame?.contentWindow?.document?.head.appendChild(clone)
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const cssLink = document.createElement("link")
|
||||
cssLink.href = link
|
||||
cssLink.rel = "stylesheet"
|
||||
cssLink.type = "text/css"
|
||||
frame?.contentWindow?.document?.head.appendChild(cssLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StyleHelper
|
||||
@@ -1,3 +0,0 @@
|
||||
.c-Home {
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createSlice } from "@reduxjs/toolkit"
|
||||
|
||||
export interface HomeState {}
|
||||
|
||||
const initialState: HomeState = {}
|
||||
|
||||
export const homeSlice = createSlice({
|
||||
name: "home",
|
||||
initialState,
|
||||
reducers: {
|
||||
increment: (state) => {},
|
||||
},
|
||||
extraReducers: (builder) => {},
|
||||
})
|
||||
|
||||
export const { increment } = homeSlice.actions
|
||||
|
||||
export default homeSlice.reducer
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from "react"
|
||||
import Home from "./Home"
|
||||
|
||||
describe("Home", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from "react"
|
||||
import "./Home.module.scss"
|
||||
|
||||
interface HomeProps {}
|
||||
|
||||
const Home = props => {
|
||||
return (
|
||||
<div className="c-Home">
|
||||
In Page Home
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
@@ -1,3 +0,0 @@
|
||||
import Home from "./Home.jsx"
|
||||
|
||||
export default Home
|
||||
@@ -1,7 +0,0 @@
|
||||
/* PLOP_INJECT_IMPORT */
|
||||
import Home from "./Home"
|
||||
|
||||
export {
|
||||
/* PLOP_INJECT_EXPORT */
|
||||
Home,
|
||||
}
|
||||
6
src/app/types/route.d.ts
vendored
6
src/app/types/route.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
interface RouteConfig {
|
||||
path: String
|
||||
class?: String
|
||||
element: String | JSX.Element | null
|
||||
children?: Array<RouteConfig>
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
class Helper {
|
||||
static populateComponentsInRoutes(routes: RouteConfig[], components: any) {
|
||||
routes &&
|
||||
routes.forEach((route) => {
|
||||
const Component: JSX.ElementType =
|
||||
components[route.element as keyof object]
|
||||
route.element = <Component />
|
||||
if (route.children) {
|
||||
Helper.populateComponentsInRoutes(route.children, components)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Helper
|
||||
1170
src/components.ts
Normal file
1170
src/components.ts
Normal file
File diff suppressed because it is too large
Load Diff
50
src/dummies.tsx
Normal file
50
src/dummies.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from "react"
|
||||
import { v4 as uuid } from "uuid"
|
||||
|
||||
export const DUMMIES = {
|
||||
Accordion: [
|
||||
{
|
||||
id: uuid(),
|
||||
title: "Slide In",
|
||||
content:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut laoreet tellus ante, et gravida massa egestas ac. Maecenas venenatis dui a eros maximus, vitae fringilla metus ultrices. Etiam at lacinia magna. Vestibulum sagittis felis sed diam ullamcorper, vel pharetra quam facilisis. Suspendisse bibendum ante id risus interdum, vel sodales felis egestas. Mauris vitae dui gravida, dictum sem id, accumsan tellus. Aliquam sodales quam efficitur, congue justo a, consectetur ante. Nunc euismod augue ac ante condimentum tincidunt. Quisque bibendum semper velit. Cras sit amet imperdiet dolor, ac aliquet mi. Morbi elementum magna eros, in venenatis nunc laoreet eget. Etiam at lorem suscipit, interdum augue id, scelerisque risus. Suspendisse at nisi lorem. Morbi libero erat, vestibulum at pulvinar at, ultrices ac turpis. Etiam ac leo gravida, semper quam in, tempus ipsum. Phasellus finibus rhoncus cursus. Maecenas dapibus ex ut congue eleifend. Sed luctus consectetur quam, at malesuada enim condimentum non.",
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
title: "Slide Out",
|
||||
content: (
|
||||
<input
|
||||
type="text"
|
||||
className="p-3 m-3"
|
||||
title="dummy"
|
||||
placeholder="Accordion content..."
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
title: "Fade",
|
||||
content:
|
||||
"Donec varius lorem vel orci aliquam, nec venenatis turpis commodo. Ut ac diam nisi. Integer at lorem ac ligula vehicula porta. Proin tempor lorem a nunc auctor, rhoncus porta elit auctor. Duis imperdiet dictum luctus. Sed eget orci mattis, sagittis quam ac, congue arcu. Praesent blandit mattis sem, eget rutrum neque auctor id. Vestibulum eu augue ut nisl tempus iaculis. Nullam id tortor sed ante tincidunt consectetur et eu odio. Aliquam accumsan turpis ac dictum interdum.",
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
title: "Rotate",
|
||||
content:
|
||||
"Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Donec metus augue, tempus vitae neque in, accumsan faucibus velit. Quisque felis lorem, tristique nec finibus consequat, viverra quis nibh. Donec arcu mi, placerat eget pretium non, molestie vitae leo. Nunc cursus id mi quis malesuada.",
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
title: "Scale",
|
||||
content:
|
||||
"Integer dapibus aliquet bibendum. Quisque id lobortis dolor, in bibendum enim. Cras mollis iaculis metus et tincidunt. Maecenas fermentum porttitor faucibus. Suspendisse feugiat ac enim nec vulputate. Aliquam egestas ante ac tempor blandit. Etiam vehicula, dui sed facilisis rhoncus, quam tortor efficitur sapien, eu imperdiet nisi magna et dui. Nulla a justo odio.",
|
||||
},
|
||||
],
|
||||
ArViz: [
|
||||
{ source: "Item 1", val: 1350, color: "#C9D6DF" },
|
||||
{ source: "Item 2", val: 2500, color: "#F7EECF" },
|
||||
{ source: "Item 3", val: 5700, color: "#E3E1B2" },
|
||||
{ source: "Item 4", val: 30000, color: "#F9CAC8" },
|
||||
{ source: "Item 5", val: 47500, color: "#D1C2E0" },
|
||||
],
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useState } from "react"
|
||||
|
||||
import { useAppSelector, useAppDispatch } from "../../app/hooks"
|
||||
import {
|
||||
decrement,
|
||||
increment,
|
||||
incrementByAmount,
|
||||
incrementAsync,
|
||||
incrementIfOdd,
|
||||
selectCount,
|
||||
} from "./counterSlice"
|
||||
|
||||
export function Counter() {
|
||||
const count = useAppSelector(selectCount)
|
||||
const dispatch = useAppDispatch()
|
||||
const [incrementAmount, setIncrementAmount] = useState("2")
|
||||
|
||||
const incrementValue = Number(incrementAmount) || 0
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="row">
|
||||
<button
|
||||
className="button"
|
||||
aria-label="Decrement value"
|
||||
onClick={() => dispatch(decrement())}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span className="value">{count}</span>
|
||||
<button
|
||||
className="button"
|
||||
aria-label="Increment value"
|
||||
onClick={() => dispatch(increment())}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="row">
|
||||
<input
|
||||
className="textbox"
|
||||
aria-label="Set increment amount"
|
||||
value={incrementAmount}
|
||||
onChange={(e) => setIncrementAmount(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => dispatch(incrementByAmount(incrementValue))}
|
||||
>
|
||||
Add Amount
|
||||
</button>
|
||||
<button
|
||||
className="asyncButton"
|
||||
onClick={() => dispatch(incrementAsync(incrementValue))}
|
||||
>
|
||||
Add Async
|
||||
</button>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => dispatch(incrementIfOdd(incrementValue))}
|
||||
>
|
||||
Add If Odd
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// A mock function to mimic making an async request for data
|
||||
export function fetchCount(amount = 1) {
|
||||
return new Promise<{ data: number }>((resolve) =>
|
||||
setTimeout(() => resolve({ data: amount }), 500),
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import counterReducer, {
|
||||
CounterState,
|
||||
increment,
|
||||
decrement,
|
||||
incrementByAmount,
|
||||
} from "./counterSlice"
|
||||
|
||||
describe("counter reducer", () => {
|
||||
const initialState: CounterState = {
|
||||
value: 3,
|
||||
status: "idle",
|
||||
}
|
||||
it("should handle initial state", () => {
|
||||
expect(counterReducer(undefined, { type: "unknown" })).toEqual({
|
||||
value: 0,
|
||||
status: "idle",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle increment", () => {
|
||||
const actual = counterReducer(initialState, increment())
|
||||
expect(actual.value).toEqual(4)
|
||||
})
|
||||
|
||||
it("should handle decrement", () => {
|
||||
const actual = counterReducer(initialState, decrement())
|
||||
expect(actual.value).toEqual(2)
|
||||
})
|
||||
|
||||
it("should handle incrementByAmount", () => {
|
||||
const actual = counterReducer(initialState, incrementByAmount(2))
|
||||
expect(actual.value).toEqual(5)
|
||||
})
|
||||
})
|
||||
@@ -1,84 +0,0 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||
import { RootState, AppThunk } from "../../app/store"
|
||||
import { fetchCount } from "./counterAPI"
|
||||
|
||||
export interface CounterState {
|
||||
value: number
|
||||
status: "idle" | "loading" | "failed"
|
||||
}
|
||||
|
||||
const initialState: CounterState = {
|
||||
value: 0,
|
||||
status: "idle",
|
||||
}
|
||||
|
||||
// The function below is called a thunk and allows us to perform async logic. It
|
||||
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
|
||||
// will call the thunk with the `dispatch` function as the first argument. Async
|
||||
// code can then be executed and other actions can be dispatched. Thunks are
|
||||
// typically used to make async requests.
|
||||
export const incrementAsync = createAsyncThunk(
|
||||
"counter/fetchCount",
|
||||
async (amount: number) => {
|
||||
const response = await fetchCount(amount)
|
||||
// The value we return becomes the `fulfilled` action payload
|
||||
return response.data
|
||||
},
|
||||
)
|
||||
|
||||
export const counterSlice = createSlice({
|
||||
name: "counter",
|
||||
initialState,
|
||||
// The `reducers` field lets us define reducers and generate associated actions
|
||||
reducers: {
|
||||
increment: (state) => {
|
||||
// Redux Toolkit allows us to write "mutating" logic in reducers. It
|
||||
// doesn"t actually mutate the state because it uses the Immer library,
|
||||
// which detects changes to a "draft state" and produces a brand new
|
||||
// immutable state based off those changes
|
||||
state.value += 1
|
||||
},
|
||||
decrement: (state) => {
|
||||
state.value -= 1
|
||||
},
|
||||
// Use the PayloadAction type to declare the contents of `action.payload`
|
||||
incrementByAmount: (state, action: PayloadAction<number>) => {
|
||||
state.value += action.payload
|
||||
},
|
||||
},
|
||||
// The `extraReducers` field lets the slice handle actions defined elsewhere,
|
||||
// including actions generated by createAsyncThunk or in other slices.
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(incrementAsync.pending, (state) => {
|
||||
state.status = "loading"
|
||||
})
|
||||
.addCase(incrementAsync.fulfilled, (state, action) => {
|
||||
state.status = "idle"
|
||||
state.value += action.payload
|
||||
})
|
||||
.addCase(incrementAsync.rejected, (state) => {
|
||||
state.status = "failed"
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const { increment, decrement, incrementByAmount } = counterSlice.actions
|
||||
|
||||
// The function below is called a selector and allows us to select a value from
|
||||
// the state. Selectors can also be defined inline where they"re used instead of
|
||||
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
|
||||
export const selectCount = (state: RootState) => state.counter.value
|
||||
|
||||
// We can also write thunks by hand, which may contain both sync and async logic.
|
||||
// Here"s an example of conditionally dispatching actions based on current state.
|
||||
export const incrementIfOdd =
|
||||
(amount: number): AppThunk =>
|
||||
(dispatch, getState) => {
|
||||
const currentValue = selectCount(getState())
|
||||
if (currentValue % 2 === 1) {
|
||||
dispatch(incrementByAmount(amount))
|
||||
}
|
||||
}
|
||||
|
||||
export default counterSlice.reducer
|
||||
@@ -2,9 +2,9 @@ import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { BrowserRouter } from "react-router-dom"
|
||||
import { Provider } from "react-redux"
|
||||
import { store } from "./app/store"
|
||||
import Router from "./app/Router"
|
||||
import "./app/static/styles/global.scss"
|
||||
import { store } from "./store"
|
||||
import Router from "./Router"
|
||||
import "./static/styles/global.scss"
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user