diff --git a/.vscode/settings.json b/.vscode/settings.json index 27fd45c8..e573647f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,11 @@ "**/.DS_Store": true, "**/dist": true, "**/node_modules": true + }, + "search.exclude": { + "**/node_modules": true, + "**/bower_components": true, + "**/*.code-search": true, + "**/yarn.lock": true } } diff --git a/CHANGELOG.md b/CHANGELOG.md index e8bd5d9a..b9c5fee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,35 @@ - [Flutter] Poligon Node support with XImage (svg) - [Lint] Primal naming & grouping linting for better code export quality. this is tracked sperately on [lint](https://github.com/bridgedxyz/lint) +## [2021.9.1] - 2021-10-11 + +> 2021.9 is a cold release + +[PR#159](https://github.com/gridaco/assistant/pull/159) + +- Instant responsive preview - a realtime application from design, seriously, with a single click. +- adoped monaco editor +- semi-stable react support with styled-component +- minimalistic navigation with hide animation on focus mode +- (fix) wrong cache loading issue on boot +- prevent thread lock on big screen +- prevent thread lock on too many remote component screen + +## [2021.8.0b] - 2021-8-26 + +**What's new** + +- Speed +- Navigation UX Renewal +- The code +- Code interaction +- Figma to Flutter +- Figma to React (React as preview feature) +- Icons loader +- Design Lint now on beta channel + +[Read thw full release notes from medium blog](https://blog.grida.co/d-figma-assistant-by-grida-supercharge-your-design-development-workflow-e6b2989216e2) + ## [2021.0.2f0] - 2021-5-21 > Deisgn to code logical separation. Design to code is now imported from [designto.codes](https://designto.codes) diff --git a/README.md b/README.md index 3955e632..08118ff6 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ yarn sketch # [OPTIONAL 3 & Contributors only] run plugin ui in webdev mode yarn web -# visit http://localhost:3000/init-webdev to work on browser +# visit http://localhost:3303/init-webdev to work on browser ``` _soon as the subpackages are released as stable, we will remove git submodule dependency for ease of use. until then, this will be the primary repository and all the edits and PRs will be caused by this project._ - [Learn more here](https://github.com/bridgedxyz/.github/blob/main/contributing/working-with-submodules.md) @@ -176,14 +176,13 @@ we release new updates in a by-monthluy cycle. Watch this repository on github o All update logs available at [CHANGELOG.md](./CHANGELOG.md) - ## Blogs + - [Flutter force week 103](https://medium.com/flutterforce/flutterforce-week-103-95b0822ef25f) - [Flutter force week 135](https://medium.com/flutterforce/flutterforce-week-135-d28b8741302a) - [Assistant initial release](https://blog.grida.co/assistant-initial-release-f75d0084df9c) - [Introducng Grida Assistant 2021.8.0b](https://blog.grida.co/figma-assistant-by-grida-supercharge-your-design-development-workflow-e6b2989216e2) - ## LEGAL > read [LICENSE](./LICENSE). diff --git a/app/__plugin__init__/index.ts b/app/__plugin__init__/index.ts index ef25073e..0c0b8ff8 100644 --- a/app/__plugin__init__/index.ts +++ b/app/__plugin__init__/index.ts @@ -1,4 +1,7 @@ // DO NOT REMOVE THIS LINE -import "../lib/pages/code/__plugin"; -import "@app/data-mapper/__plugin"; +import "@app/design-to-code/__plugin"; import "@app/design-lint/__plugin"; + +// disabled on staging +// import "@app/data-mapper/__plugin"; +// import "@app/design-text-code-syntax-highlight/__plugin"; diff --git a/app/lib/app.css b/app/lib/app.css index 3f3c5600..745c57aa 100644 --- a/app/lib/app.css +++ b/app/lib/app.css @@ -2,6 +2,11 @@ font-family: "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif; } +body { + /* for reset user agnet style -> margin: 8px */ + margin: 0px; +} + /* If you want to change the font, be sure to check the following! tippy-1 is `tippyjs' id. it is only for drop item in @code-ui/docstring diff --git a/app/lib/components/animation/animated-check-icon.tsx b/app/lib/components/animated/animated-check-icon.tsx similarity index 100% rename from app/lib/components/animation/animated-check-icon.tsx rename to app/lib/components/animated/animated-check-icon.tsx diff --git a/app/lib/components/animation/animated-progress-bar.tsx b/app/lib/components/animated/animated-progress-bar.tsx similarity index 100% rename from app/lib/components/animation/animated-progress-bar.tsx rename to app/lib/components/animated/animated-progress-bar.tsx diff --git a/app/lib/components/comming-soon-template.tsx b/app/lib/components/comming-soon-template.tsx index de9b03ec..bccb3e4e 100644 --- a/app/lib/components/comming-soon-template.tsx +++ b/app/lib/components/comming-soon-template.tsx @@ -26,10 +26,7 @@ export function CommingSoonTemplate(props: Props) { ); } -const Wrapper = styled.div` - /* -8 is for reset body margin */ - margin: 0 -8px; -`; +const Wrapper = styled.div``; const ImageBox = styled.div``; diff --git a/app/lib/components/motions/README.md b/app/lib/components/motions/README.md new file mode 100644 index 00000000..cbd275b7 --- /dev/null +++ b/app/lib/components/motions/README.md @@ -0,0 +1,3 @@ +# General motions + +Motions only ! - no symantic ui components allowed here. diff --git a/app/lib/components/motions/index.ts b/app/lib/components/motions/index.ts new file mode 100644 index 00000000..caad0e09 --- /dev/null +++ b/app/lib/components/motions/index.ts @@ -0,0 +1,2 @@ +export * from "./update-hide-by-scroll-position-and-velocity"; +export * from "./smooth-dampings"; diff --git a/app/lib/components/motions/smooth-dampings/index.ts b/app/lib/components/motions/smooth-dampings/index.ts new file mode 100644 index 00000000..8fc88471 --- /dev/null +++ b/app/lib/components/motions/smooth-dampings/index.ts @@ -0,0 +1,4 @@ +export const smooth_damping_hide_motion_transition = { + ease: [0.1, 0.25, 0.3, 1], + duration: 0.6, +}; diff --git a/app/lib/components/motions/update-hide-by-scroll-position-and-velocity/index.ts b/app/lib/components/motions/update-hide-by-scroll-position-and-velocity/index.ts new file mode 100644 index 00000000..a11f9353 --- /dev/null +++ b/app/lib/components/motions/update-hide-by-scroll-position-and-velocity/index.ts @@ -0,0 +1,121 @@ +import { useState, useEffect, RefObject } from "react"; +import { ScrollMotionValues } from "framer-motion"; +import { useElementScroll } from "framer-motion"; + +export function update_hide_by_scroll_position_and_velocity({ + scrollY, + scrollYProgress, + is_animating_by_intense_scrolling, + on_animating_by_intense_scrolling, + on_change, + options = { + top_sensitivity: 0.01, + bottom_sensitivity: 0.01, + define_intense_velocity: 1, + do_show_on_bottom_hit: true, + }, +}: { + scrollY: ScrollMotionValues["scrollY"]; + scrollYProgress: ScrollMotionValues["scrollYProgress"]; + is_animating_by_intense_scrolling: boolean; + on_animating_by_intense_scrolling: (v?: true) => void; + on_change: (hide: boolean) => void; + options?: { + top_sensitivity: number; + bottom_sensitivity: number; + define_intense_velocity: number; + do_show_on_bottom_hit: boolean; + }; +}) { + const scroll_progress_percentage = scrollYProgress.get(); + const ydiff = Math.abs(scrollY.get() - scrollY.getPrevious()); + if ( + // don't execute if diff is `<=` than 2. - this is a really small scroll + ydiff <= 2 && + // except for bottom / top + scroll_progress_percentage !== 0 && + scroll_progress_percentage !== 1 + ) { + return; + } + + const velocity = scrollYProgress.getVelocity(); + const velocity_abs = Math.abs(velocity); + if ( + // if < 20, this event is not triggered by human, or caused by extremely short scroll area, causing high velocity. + velocity_abs > 20 || + scrollYProgress.get() == scrollYProgress.getPrevious() + ) { + return; + } + const is_intense_scrolling = velocity_abs > options.define_intense_velocity; + const direction = velocity > 0 ? "down" : "up"; // this is ok. velocity can't be 0. + + if (scroll_progress_percentage >= 1 - options.bottom_sensitivity) { + if (options.do_show_on_bottom_hit) { + // bottom = show + on_change(false); + } + } else if (scroll_progress_percentage <= options.top_sensitivity) { + switch (direction) { + // top + down = hide + case "down": + if (!is_intense_scrolling) { + on_change(true); + } + break; + case "up": + // top + up = show + on_change(false); + break; + } + } else { + if (is_intense_scrolling) { + switch (direction) { + // scroll intense + down = hide + case "down": + on_change(true); + break; + // scroll intense + up = show + case "up": + on_animating_by_intense_scrolling(true); + on_change(false); + break; + } + } else { + if (!is_animating_by_intense_scrolling) { + // middle = hide + on_change(true); + } + } + } +} + +export function useScrollTriggeredAnimation(el: RefObject) { + const { scrollYProgress, scrollY } = useElementScroll(el); + const [hide, setHide] = useState(false); + let is_animating_by_intense_scrolling = false; + useEffect(() => { + return scrollYProgress.onChange(() => + update_hide_by_scroll_position_and_velocity({ + scrollYProgress, + scrollY, + is_animating_by_intense_scrolling, + on_animating_by_intense_scrolling: () => { + is_animating_by_intense_scrolling = true; + }, + on_change: (hide) => { + setHide(hide); + }, + options: { + do_show_on_bottom_hit: false, + top_sensitivity: 0.05, + bottom_sensitivity: 0.1, + define_intense_velocity: 50, + }, + }) + ); + }); + + return hide; +} diff --git a/app/lib/components/navigation/navigation-motions.tsx b/app/lib/components/navigation/navigation-motions.tsx new file mode 100644 index 00000000..cea42347 --- /dev/null +++ b/app/lib/components/navigation/navigation-motions.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { smooth_damping_hide_motion_transition } from "../motions"; + +export function AppbarContainerMotion({ + hidden, + children, +}: { + hidden: boolean; + children: JSX.Element | JSX.Element[]; +}) { + /** add this const **/ + const variants_for_container = { + visible: { opacity: 1 }, + hidden: { opacity: 0, height: 0 }, + }; + + return ( + + {children} + + ); +} + +export function AppbarContentMotion({ + hidden, + children, +}: { + hidden: boolean; + children: JSX.Element | JSX.Element[]; +}) { + const variants_for_child = { + visible: { y: 0 }, + hidden: { y: -100 }, + }; + + return ( + + {children} + + ); +} diff --git a/app/lib/components/navigation/navigator-expansion-control-button.tsx b/app/lib/components/navigation/navigator-expansion-control-button.tsx index 0c073fb6..41b90e3a 100644 --- a/app/lib/components/navigation/navigator-expansion-control-button.tsx +++ b/app/lib/components/navigation/navigator-expansion-control-button.tsx @@ -35,7 +35,5 @@ const Button = styled.div` svg { fill: #cfcfcf; - width: 25px; - height: 34px; } `; diff --git a/app/lib/components/navigation/primary-workmode-select.tsx b/app/lib/components/navigation/primary-workmode-select.tsx index 043f53b8..2e889958 100644 --- a/app/lib/components/navigation/primary-workmode-select.tsx +++ b/app/lib/components/navigation/primary-workmode-select.tsx @@ -1,7 +1,6 @@ -import { css } from "@emotion/react"; -import styled from "@emotion/styled"; import React from "react"; -import { PrimaryWorkmodeSet, WorkMode } from "../../navigation"; +import { PrimaryWorkmodeSet, WorkMode } from "../../routing"; +import { WorkmodeButton } from "./work-mode-button"; export function PrimaryWorkmodeSelect(props: { set: PrimaryWorkmodeSet; @@ -31,58 +30,3 @@ export function PrimaryWorkmodeSelect(props: { ); } - -function WorkmodeButton(props: { - name: string; - active: boolean; - onClick: () => void; -}) { - return ( - <> - - {props.name} - - - ); -} -interface Props { - active: boolean; -} - -const WorkmodeLabel = styled.h3` - display: flex; - text-transform: capitalize; - font-size: 21px; - letter-spacing: 0em; - cursor: pointer; - user-select: none; - - // reset for h3 init style - margin: 0; - margin-top: 14px; - - &:first-child { - margin-right: 12px; - } - - ${(props) => - props.active - ? css` - font-weight: 700; - line-height: 26px; - color: #000; - ` - : css` - font-weight: 400; - line-height: 25px; - color: #cfcfcf; - - &:hover { - font-size: 21px; - font-weight: 400; - line-height: 25px; - letter-spacing: 0em; - color: #606060; - } - `} -`; diff --git a/app/lib/components/navigation/route-back-button.tsx b/app/lib/components/navigation/route-back-button.tsx new file mode 100644 index 00000000..ce5d3a14 --- /dev/null +++ b/app/lib/components/navigation/route-back-button.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import styled from "@emotion/styled"; +import { useHistory } from "react-router"; +import BackArrow from "@assistant/icons/back-arrow"; + +export function RouteBackButton() { + const history = useHistory(); + + const close = () => { + history.goBack(); + }; + + return ( + + + + ); +} + +export const BackIcon = styled.div` + width: 24px; + height: 24px; + cursor: pointer; +`; diff --git a/app/lib/components/navigation/secondary-menu-dropdown.tsx b/app/lib/components/navigation/secondary-menu-dropdown.tsx index 7581ebd1..7ea5abd9 100644 --- a/app/lib/components/navigation/secondary-menu-dropdown.tsx +++ b/app/lib/components/navigation/secondary-menu-dropdown.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useHistory } from "react-router-dom"; -import { WorkMode, WorkScreen } from "../../navigation"; +import { WorkMode, WorkScreen } from "../../routing"; import { SecondaryWorkmodeMenu } from "./secondary-workmode-menu"; type Stage = "production" | "development" | string; @@ -52,7 +52,9 @@ export function SecondaryMenuDropdown() { id: WorkMode.tools, name: WorkMode.tools, stage: "development", - onSelect: () => {}, + onSelect: () => { + history.push("/toolbox/home"); + }, }, { id: WorkMode.settings, diff --git a/app/lib/components/navigation/secondary-workmode-menu.tsx b/app/lib/components/navigation/secondary-workmode-menu.tsx index b7bc33a2..ce7d77c5 100644 --- a/app/lib/components/navigation/secondary-workmode-menu.tsx +++ b/app/lib/components/navigation/secondary-workmode-menu.tsx @@ -54,4 +54,5 @@ export function SecondaryWorkmodeMenu(props: { const Wrapper = styled.div` display: flex; flex-wrap: wrap; + background: white; `; diff --git a/app/lib/components/navigation/work-mode-button.tsx b/app/lib/components/navigation/work-mode-button.tsx new file mode 100644 index 00000000..66fd460c --- /dev/null +++ b/app/lib/components/navigation/work-mode-button.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { css } from "@emotion/react"; +import styled from "@emotion/styled"; + +export function WorkmodeButton(props: { + name: string; + active: boolean; + onClick: () => void; +}) { + return ( + <> + + {props.name} + + + ); +} +interface Props { + active: boolean; +} + +const WorkmodeLabel = styled.h3` + display: flex; + text-transform: capitalize; + font-size: 21px; + letter-spacing: 0em; + cursor: pointer; + user-select: none; + + // reset for h3 init style + margin: 0; + + &:first-child { + margin-right: 12px; + } + + ${(props) => + props.active + ? css` + font-weight: 700; + line-height: 26px; + color: #000; + ` + : css` + font-weight: 400; + line-height: 25px; + color: #cfcfcf; + + &:hover { + font-size: 21px; + font-weight: 400; + line-height: 25px; + letter-spacing: 0em; + color: #606060; + } + `} +`; diff --git a/app/lib/components/navigation/workmode-screen-tabs.tsx b/app/lib/components/navigation/workmode-screen-tabs.tsx index 0c02868a..c1611f1b 100644 --- a/app/lib/components/navigation/workmode-screen-tabs.tsx +++ b/app/lib/components/navigation/workmode-screen-tabs.tsx @@ -37,6 +37,10 @@ export function WorkmodeScreenTabs(props: { userSelect: "none", }, }} + style={{ + marginLeft: "-16px", + marginRight: "16px", + }} > {layout.map((v, i) => { return ( diff --git a/app/lib/components/storybook-screen.tsx b/app/lib/components/storybook-screen.tsx index 247d06b6..b4465b4c 100644 --- a/app/lib/components/storybook-screen.tsx +++ b/app/lib/components/storybook-screen.tsx @@ -22,11 +22,7 @@ export class BoxTab extends React.Component { componentDidMount() { window.addEventListener("message", function (ev: MessageEvent) { const msg = ev.data.pluginMessage; - switch ( - msg.type - // case EK_GENERATED_CODE_PLAIN: - // case EK_PREVIEW_SOURCE: - ) { + switch (msg.type) { } }); } diff --git a/app/lib/components/upload-steps.tsx b/app/lib/components/upload-steps.tsx index 7fa8ae16..adc6ec10 100644 --- a/app/lib/components/upload-steps.tsx +++ b/app/lib/components/upload-steps.tsx @@ -5,8 +5,8 @@ import { TransparentButtonStyle, } from "@ui/core/button-style"; import { Button } from "@material-ui/core"; -import { AnimatedProgressBar } from "./animation/animated-progress-bar"; -import { AnimatedCheckIcon } from "./animation/animated-check-icon"; +import { AnimatedProgressBar } from "./animated/animated-progress-bar"; +import { AnimatedCheckIcon } from "./animated/animated-check-icon"; import { motion } from "framer-motion"; import { Preview } from "@ui/previewer"; import CheckIcon from "@assistant/icons/check"; @@ -42,7 +42,7 @@ export function UploadSteps() { return ( <> - + {isLoading && } {isLoading ? ( @@ -142,7 +142,7 @@ const Item = styled(motion.h5)` const FooterButtonWrapper = styled.div` position: absolute; - bottom: 8px; + bottom: 16px; width: calc(100% - 40px); `; const IconBox = styled.div` diff --git a/app/lib/main/global-state-atoms.ts b/app/lib/main/global-state-atoms.ts new file mode 100644 index 00000000..afee962c --- /dev/null +++ b/app/lib/main/global-state-atoms.ts @@ -0,0 +1,11 @@ +import { atom } from "recoil"; + +export interface EditorSizeProps { + width: number; + height: number; +} + +export const hide_navigation = atom({ + key: "hide_navigation", + default: false, +}); diff --git a/app/lib/main/index.tsx b/app/lib/main/index.tsx index 3d571037..a69ce5c5 100644 --- a/app/lib/main/index.tsx +++ b/app/lib/main/index.tsx @@ -21,11 +21,12 @@ import { DataMapperScreen } from "@app/data-mapper"; import { GlobalizationScreen } from "@app/i18n"; import { ToolboxScreen } from "../pages/tool-box"; import { FontReplacerScreen } from "@toolbox/font-replacer"; -import { CodeScreen } from "../pages/code/code-screen"; +import { CodeScreen } from "@app/design-to-code"; import { AboutScreen } from "../pages/about"; import { SigninScreen } from "@app/auth"; +import { ToolboxHome } from "@app/toolbox"; +import { DesignTextCdoeSyntaxHighligherScreen } from "@app/design-text-code-syntax-highlight"; // endregion screens import -// import { getDedicatedRouter, @@ -40,7 +41,7 @@ import { saveLayout, updateLayout, get_page_config_by_path, -} from "../navigation"; +} from "../routing"; import { WorkmodeScreenTabs, @@ -72,6 +73,8 @@ function Screen(props: { screen: WorkScreen }) { return ; case WorkScreen.exporter: return ; + case WorkScreen.tool_home: + return ; case WorkScreen.tool_desing_button_maker: return ; case WorkScreen.tool_font_replacer: @@ -82,6 +85,8 @@ function Screen(props: { screen: WorkScreen }) { return ; case WorkScreen.tool_data_mapper: return ; + case WorkScreen.tool_code_syntax_highlighter: + return ; case WorkScreen.scene_upload_steps_final: return ; case WorkScreen.signin: @@ -91,63 +96,17 @@ function Screen(props: { screen: WorkScreen }) { } } -function TabsLayout(props: { - workmode: WorkMode; - tabIndex: number; - isTabVisible: boolean; - onChange: (index: number, tab: WorkScreen) => void; -}) { - const history = useHistory(); - const { workmode, tabIndex, onChange } = props; - const tabs_as_page_configs = getWorkmodeTabLayout(workmode).map( - (screen, index) => { - const _ = get_page_config(screen); - return { - id: _.id, - name: _.title, - path: _.path, - }; - } - ); - - useEffect(() => { - handleTabChange(tabIndex); - }, []); - - const handleTabChange = (index: number) => { - const screen = tabs_as_page_configs[index]; - onChange(index, screen.id); - history.replace(screen.path); // since it is a movement between tabs, we don't use push. we use replace to avoid the history stack to be too long. - }; - - return ( -
- {props.isTabVisible && ( -
- -
- )} - - - {tabs_as_page_configs.map((v, i) => { - return ( - } - /> - ); - })} - -
- ); -} +// region global navigation animation state +import { RecoilRoot, useRecoilState } from "recoil"; +import { + AppbarContainerMotion, + AppbarContentMotion, +} from "../components/navigation/navigation-motions"; +import { hide_navigation } from "./global-state-atoms"; +// endregion function TabNavigationApp(props: { savedLayout: NavigationStoreState }) { + const history = useHistory(); const [workmode, setWorkmode] = useState( props.savedLayout.currentWorkmode ); @@ -155,8 +114,9 @@ function TabNavigationApp(props: { savedLayout: NavigationStoreState }) { props.savedLayout.workmodeSet ); const [tabIndex, setTabIndex] = useState(0); + const [screen, setScreen] = useState(); const [expansion, setExpansion] = useState(true); - + const isTabVisible = expansion; const on_workmode_select = (workmode: WorkMode) => { setWorkmode(workmode); setTabIndex(0); @@ -172,41 +132,93 @@ function TabNavigationApp(props: { savedLayout: NavigationStoreState }) { }); }; + // region animation state + const [whole_navigation_hidden] = useRecoilState(hide_navigation); + + useEffect(() => { + handleTabChange(tabIndex); + }, [tabIndex, workmode]); + + const handleTabChange = (index: number) => { + const screen = tabs_as_page_configs[index]; + setScreen(screen.id); + on_work_select(index, screen.id); + history.replace(screen.path); // since it is a movement between tabs, we don't use push. we use replace to avoid the history stack to be too long. + }; + + const tabs_as_page_configs = getWorkmodeTabLayout(workmode).map( + (screen, index) => { + const _ = get_page_config(screen); + return { + id: _.id, + name: _.title, + path: _.path, + }; + } + ); + + const shadow_required = screen == WorkScreen.code || !isTabVisible; + return ( - <> - - - - - setExpansion(!expansion)} - /> - - {!expansion && } - - + // root flex styled container for the whole app +
+ - {/* {expansion && ( */} - - {/* )} */} - + <> + {/* the screen's wrapping layout */} + + + {tabs_as_page_configs.map((v, i) => { + return ( + } + /> + ); + })} + + + +
); // } @@ -233,20 +245,34 @@ function RouterTabNavigationApp(props) { function Home() { const history = useHistory(); - const [savedLayout, setSavedLayout] = - useState(undefined); + const [savedLayout, setSavedLayout] = useState( + undefined + ); useEffect(() => { loadLayout() .then((d) => { setSavedLayout(d); }) + .catch((e) => { + console.log("failed loading layout", e); + }) .finally(() => {}); }, []); if (savedLayout) { - const p = get_page_config(savedLayout.currentWork).path; - history.replace(p); + try { + const p = get_page_config(savedLayout.currentWork).path; + history.replace(p); + } catch (e) { + console.log("failed to load saved layout", e); + console.log( + "this can happen during development, switching between branches, or could happen on production wehn new version lo longer has a page that is previously loaded." + ); + // if somehow, failed loading the path of the current work, we will redirect to the home page. + // this can happen during development, switching between branches, or could happen on production wehn new version lo longer has a page that is previously loaded. + history.replace("/code/preview"); + } } return <>; @@ -268,30 +294,32 @@ export default function App(props: { platform: TargetPlatform }) { const Router = getDedicatedRouter(); return ( - - {/* @ts-ignore */} - - - {/* # region unique route section */} - {standalone_pages.map((p) => { - return ( - { - return ; - }} - /> - ); - })} - {/* # endregion unique route section */} - {/* dynamic route shall be placed at the last point, since it overwrites other routes */} - - - {/* 👆 this is for preventing blank page on book up. this will be fixed and removed.*/} - - - + + + {/* @ts-ignore */} + + + {/* # region unique route section */} + {standalone_pages.map((p) => { + return ( + { + return ; + }} + /> + ); + })} + {/* # endregion unique route section */} + {/* dynamic route shall be placed at the last point, since it overwrites other routes */} + + + {/* 👆 this is for preventing blank page on book up. this will be fixed and removed.*/} + + + + ); } @@ -307,9 +335,22 @@ function _update_focused_screen_ev(screen: WorkScreen) { "*" ); } +const ScreenWrapLayout = styled.div` + display: flex; + flex-direction: column; + flex-grow: 1; + min-height: 0px; +`; -const Wrapper = styled.div` +const PrimaryWorkmodeWrapper = styled.div<{ shadow_required: boolean }>` display: flex; - padding: 0 8px; - /* margin-bottom: -8px; */ + padding: 0 16px; + background-color: #fff; + + box-shadow: ${(props) => + props.shadow_required ? "0px 4px 24px rgba(0,0,0,0.25)" : "none"}; + + > div { + width: 100%; + } `; diff --git a/app/lib/pages/about/index.tsx b/app/lib/pages/about/index.tsx index bf784987..72430d6a 100644 --- a/app/lib/pages/about/index.tsx +++ b/app/lib/pages/about/index.tsx @@ -127,7 +127,8 @@ function _social_icon( } const BackIcon = styled.div` - margin-bottom: 24px; + margin-top: 24px; + margin-left: 24px; `; const AboutTitleSection = styled.div` diff --git a/app/lib/pages/code/__plugin/design-to-code.ts b/app/lib/pages/code/__plugin/design-to-code.ts deleted file mode 100644 index e99859a9..00000000 --- a/app/lib/pages/code/__plugin/design-to-code.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { ReflectSceneNode } from "@design-sdk/core/nodes"; -import { flutter, react, token } from "@designto/code"; - -interface GenerationResultToUI { - tokens?: any; - widget: any; - app: any; -} - -type InterceptorJobProcessor = () => Promise; -export async function designToFlutter( - reflectDesign: ReflectSceneNode, - jobs: InterceptorJobProcessor -) { - const buildResult = flutter.buildApp(reflectDesign); - - // execution order matters. - // this will be fixed by having a builder instance. (currently non available) - await jobs(); - - const widget = buildResult.widget; - const app = flutter.makeApp({ - widget: widget, - scrollable: buildResult.scrollable, - }); - - return { - widget: widget, - app: app, - }; -} - -export function designToReact(reflectDesign: ReflectSceneNode) { - const tokens = token.tokenize(reflectDesign); - const widget = react.buildReactWidget(tokens); - const app = react.buildReactApp(widget, { - template: "cra", - }); - - return { - tokens: tokens, - widget: widget, - app: app.code, - }; -} - -export function designToCode() { - throw "not implemented"; -} diff --git a/app/lib/pages/code/__plugin/index.ts b/app/lib/pages/code/__plugin/index.ts deleted file mode 100644 index fbcef43b..00000000 --- a/app/lib/pages/code/__plugin/index.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - EK_GENERATED_CODE_PLAIN, - EK_IMAGE_ASSET_REPOSITORY_MAP, -} from "@core/constant/ek.constant"; -import { - onService, - _Code_Event, - _APP_EVENT_CODE_GEN_RESULT_EK, - CodeGenRequest, -} from "./events"; -import { designToFlutter, designToReact } from "./design-to-code"; -import { FigmaNodeCache } from "figma-core/node-cache"; -import { Framework } from "../framework-option"; -import { repo_assets } from "@design-sdk/core"; -onService(main_cb); - -// main callback -function main_cb(evt: _Code_Event) { - // to logic - - switch (evt.type) { - case "code-gen-request": - _handle_code_gen_request(evt); - break; - } -} - -async function _handle_code_gen_request(req: CodeGenRequest) { - //#region run code gen - const rnode = FigmaNodeCache.getLastConverted(); - if (rnode) { - const codePlatform = (() => { - switch (req.option.framework) { - case Framework.react: - return "react"; - case Framework.flutter: - return "flutter"; - default: - return "flutter"; // currently default mode is flutter due to flutter is default legacy. - } - throw `unrecognized user_interest givven "${req.option.framework}"`; - })(); - - const hostingjob = async () => { - // host images - const transportableImageAssetRepository = - await repo_assets.MainImageRepository.instance.current.makeTransportable(); - figma.ui.postMessage({ - type: EK_IMAGE_ASSET_REPOSITORY_MAP, - data: transportableImageAssetRepository, - }); - }; - - //@ts-ignore - if (codePlatform == "flutter") { - const flutterBuild = await designToFlutter(rnode, hostingjob); - figma.ui.postMessage({ - type: EK_GENERATED_CODE_PLAIN, - data: { - code: flutterBuild.widget.build().finalize(), - app: flutterBuild.app.build().finalize(), - }, - }); - } else if (codePlatform == "react") { - const reactBuild = designToReact(rnode); - figma.ui.postMessage({ - type: EK_GENERATED_CODE_PLAIN, - data: { - code: reactBuild.app, - app: reactBuild.app, - }, - }); - } - } else { - console.warn("user requested linting, but non selected to run lint on."); - } - //#endregion -} diff --git a/app/lib/pages/code/code-screen.tsx b/app/lib/pages/code/code-screen.tsx deleted file mode 100644 index c69f585c..00000000 --- a/app/lib/pages/code/code-screen.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { CodeBox, SourceInput } from "@ui/codebox"; -import { Preview } from "@ui/previewer"; -import { - EK_GENERATED_CODE_PLAIN, - EK_IMAGE_ASSET_REPOSITORY_MAP, -} from "@core/constant/ek.constant"; -import { repo_assets } from "@design-sdk/core"; -import { assistant as analytics } from "@analytics.bridged.xyz/internal"; -import { - FrameworkOption, - all_preset_options_map__prod, -} from "./framework-option"; -import styled from "@emotion/styled"; -import { make_empty_selection_state_text_content } from "./constants"; -import { format } from "./formatter"; -import copy from "copy-to-clipboard"; -import { PluginSdk } from "@plugin-sdk/app"; -import { CodeScreenFooter } from "./footer-action/code-screen-footer"; -import { CodeOptionsControl } from "./code-options-control"; -import { fromApp, CodeGenRequest } from "./__plugin/events"; -import { useSingleSelection } from "plugin-app"; - -type DesigntoCodeUserOptions = FrameworkOption; - -export function CodeScreen() { - const [app, setApp] = useState(); - const [useroption, setUseroption] = React.useState( - all_preset_options_map__prod.flutter_default - ); - const [source, setSource] = useState(); - const selection = useSingleSelection(); - - /** register event listener for events from code thread. */ - useEffect(() => { - window.addEventListener("message", onMessage); - return function cleaup() { - window.removeEventListener("message", onMessage); - }; - }, [useroption.language]); - - /** post to code thread about target framework change */ - useEffect(() => { - // 1. clear previous result. - setSource(undefined); - // 2. request new code gen. - fromApp({ - type: "code-gen-request", - option: useroption, - }); - }, [useroption.framework, selection]); - - const _make_placeholder = () => { - return make_empty_selection_state_text_content("empty"); - }; - - const _make_source = (): SourceInput => { - if (typeof source == "string") { - if (source && source.length > 0) { - return source; - } - } else { - if (source && source.raw.length > 0) { - return source; - } - } - return _make_placeholder(); - }; - - const handleSourceInput = ({ - app, - code, - }: { - app: string; - code: SourceInput; - }) => { - format(app, useroption.language, (s) => { - setApp(s); - }); - - // for SourceInput, we need type checking - if (typeof code == "string") { - // source input as string - format(code, useroption.language, (s) => { - setSource(s); - }); - } else { - // source input as { raw: string } - format(code.raw, useroption.language, (s) => { - setSource({ - raw: s, - }); - }); - } - }; - - const onMessage = (ev: MessageEvent) => { - const msg = ev.data.pluginMessage; - if (msg) { - switch (msg.type) { - case EK_GENERATED_CODE_PLAIN: - handleSourceInput({ - app: msg.data.app, - code: msg.data.code, - }); - // analytics - analytics.event_selection_to_code({ - framework: useroption.framework, - }); - - break; - case EK_IMAGE_ASSET_REPOSITORY_MAP: - const imageRepo = - msg.data as repo_assets.TransportableImageRepository; - repo_assets.ImageHostingRepository.setRepository(imageRepo); - break; - } - } else { - // ignore - } - }; - - const onCopyClicked = (e) => { - const _code: SourceInput = _make_source(); - const raw = typeof _code == "string" ? _code : _code.raw; - copy(raw); - PluginSdk.notifyCopied(); - - // ANALYTICS - analytics.event_click_copy_code(); - }; - - const onOptionChange = (op: DesigntoCodeUserOptions) => { - setUseroption(op); - }; - - return ( -
- - - {/* FIXME: add onCopyClicked to code-box */} - - - - - - - - - - - -
- ); -} - -/** - * get language by framework (default) (for code display) (non critical) - * - * -- used by code view (for styling only - used by highlight js) - */ -const _src_view_language = (framework: string): string => { - switch (framework) { - case "flutter": - return "dart"; - case "react": - return "jsx"; - default: - throw `default language for code display on framework "${framework}" is not supported`; - } -}; - -const CopyCodeButton = styled.div` - width: 24px; - height: 24px; - position: absolute; - right: 0; - margin-top: 24px; - margin-right: 20px; - cursor: pointer; -`; - -const CodeWrapper = styled.div` - /* 374 is preview and navigation height*/ - height: calc(100vh - 374px); - background: rgb(42, 39, 52); - margin: 0 -8px; - overflow-y: scroll; -`; diff --git a/app/lib/navigation/README.md b/app/lib/routing/README.md similarity index 100% rename from app/lib/navigation/README.md rename to app/lib/routing/README.md diff --git a/app/lib/navigation/index.ts b/app/lib/routing/index.ts similarity index 100% rename from app/lib/navigation/index.ts rename to app/lib/routing/index.ts diff --git a/app/lib/navigation/layout-preference.ts b/app/lib/routing/layout-preference.ts similarity index 94% rename from app/lib/navigation/layout-preference.ts rename to app/lib/routing/layout-preference.ts index c112eae1..88af6087 100644 --- a/app/lib/navigation/layout-preference.ts +++ b/app/lib/routing/layout-preference.ts @@ -22,6 +22,7 @@ export function getWorkmodeTabLayout(workspaceMode: WorkMode): TabLayout { return [WorkScreen.about]; case WorkMode.toolbox: return [ + WorkScreen.tool_code_syntax_highlighter, WorkScreen.tool_font_replacer, WorkScreen.tool_desing_button_maker, WorkScreen.tool_meta_editor, @@ -48,7 +49,7 @@ export function workScreenToName(appMode: WorkScreen): string { return "component"; case WorkScreen.layout: return "layout"; - case WorkScreen.dev: + case WorkScreen.tool_home: return "tools"; case WorkScreen.icon: return "icon"; @@ -58,6 +59,8 @@ export function workScreenToName(appMode: WorkScreen): string { return "exporter"; case WorkScreen.g11n: return "globalization"; + case WorkScreen.tool_code_syntax_highlighter: + return "syntax highlight"; case WorkScreen.tool_desing_button_maker: return "button maker"; case WorkScreen.tool_font_replacer: diff --git a/app/lib/navigation/navigation-store/README.md b/app/lib/routing/navigation-store/README.md similarity index 100% rename from app/lib/navigation/navigation-store/README.md rename to app/lib/routing/navigation-store/README.md diff --git a/app/lib/navigation/navigation-store/index.ts b/app/lib/routing/navigation-store/index.ts similarity index 100% rename from app/lib/navigation/navigation-store/index.ts rename to app/lib/routing/navigation-store/index.ts diff --git a/app/lib/navigation/navigation-store/save-workmode-work.ts b/app/lib/routing/navigation-store/save-workmode-work.ts similarity index 100% rename from app/lib/navigation/navigation-store/save-workmode-work.ts rename to app/lib/routing/navigation-store/save-workmode-work.ts diff --git a/app/lib/navigation/pages.ts b/app/lib/routing/pages.ts similarity index 80% rename from app/lib/navigation/pages.ts rename to app/lib/routing/pages.ts index 49870294..30f79d6c 100644 --- a/app/lib/navigation/pages.ts +++ b/app/lib/routing/pages.ts @@ -64,6 +64,12 @@ const page_about: PageConfig = { path: "/about", }; +const page_toolbox_home: PageConfig = { + id: WorkScreen.tool_home, + title: "Tools", + path: "/toolbox/home", +}; + const page_toolbox_font_replacer: PageConfig = { id: WorkScreen.tool_font_replacer, title: "Font replacer", @@ -94,6 +100,13 @@ const page_toolbox_data_mapper: PageConfig = { path: "/toolbox/data-mapper", }; +const page_toolbox_code_syntax_highlight: PageConfig = { + id: WorkScreen.tool_code_syntax_highlighter, + title: "Code syntax highlighter", + /** this is temporarily under design workmode. - change under toolbox after workmode preference switch is ready. */ + path: "/toolbox/code-syntax-highlight", +}; + const page_scene_upload_steps_final: PageConfig = { id: WorkScreen.scene_upload_steps_final, title: "Review your scene", // not used @@ -110,11 +123,6 @@ const page_signup: PageConfig = { * list of all pages */ const all_pages: PageConfig[] = [ - page_toolbox_data_mapper, - page_toolbox_batch_meta_editor, - page_toolbox_meta_editor, - page_toolbox_desing_button_maker, - page_toolbox_font_replacer, page_content_g11n, page_design_lint, page_design_layout, @@ -127,12 +135,30 @@ const all_pages: PageConfig[] = [ page_signup, page_about, page_scene_upload_steps_final, + // tools + page_toolbox_home, + page_toolbox_data_mapper, + page_toolbox_batch_meta_editor, + page_toolbox_meta_editor, + page_toolbox_desing_button_maker, + page_toolbox_font_replacer, + page_toolbox_home, + page_toolbox_code_syntax_highlight, ]; export const standalone_pages: PageConfig[] = [ page_signup, page_about, page_scene_upload_steps_final, + // tools + page_toolbox_home, + page_toolbox_data_mapper, + page_toolbox_batch_meta_editor, + page_toolbox_meta_editor, + page_toolbox_desing_button_maker, + page_toolbox_font_replacer, + page_toolbox_home, + page_toolbox_code_syntax_highlight, ]; export function get_page_config(id: WorkScreen): PageConfig { diff --git a/app/lib/navigation/primary-workmode-selector.ts b/app/lib/routing/primary-workmode-selector.ts similarity index 100% rename from app/lib/navigation/primary-workmode-selector.ts rename to app/lib/routing/primary-workmode-selector.ts diff --git a/app/lib/navigation/release-visibility-preference.ts b/app/lib/routing/release-visibility-preference.ts similarity index 96% rename from app/lib/navigation/release-visibility-preference.ts rename to app/lib/routing/release-visibility-preference.ts index 1c2e5f04..f0641a6e 100644 --- a/app/lib/navigation/release-visibility-preference.ts +++ b/app/lib/routing/release-visibility-preference.ts @@ -17,6 +17,7 @@ const SCREEN_VISIBILITY_PREFERENCE: Map = new Map([ [WorkScreen.g11n, "beta"], [WorkScreen.exporter, "beta"], [WorkScreen.dev, "beta"], + [WorkScreen.tool_code_syntax_highlighter, "beta"], [WorkScreen.tool_desing_button_maker, "alpha"], [WorkScreen.tool_font_replacer, "release"], [WorkScreen.tool_meta_editor, "release"], diff --git a/app/lib/navigation/router.ts b/app/lib/routing/router.ts similarity index 100% rename from app/lib/navigation/router.ts rename to app/lib/routing/router.ts diff --git a/app/lib/navigation/work-mode.ts b/app/lib/routing/work-mode.ts similarity index 100% rename from app/lib/navigation/work-mode.ts rename to app/lib/routing/work-mode.ts diff --git a/app/lib/navigation/work-screen.ts b/app/lib/routing/work-screen.ts similarity index 89% rename from app/lib/navigation/work-screen.ts rename to app/lib/routing/work-screen.ts index ae07362d..c816933d 100644 --- a/app/lib/navigation/work-screen.ts +++ b/app/lib/routing/work-screen.ts @@ -14,7 +14,9 @@ export enum WorkScreen { dev = "dev", slot = "slot", exporter = "exporter", + tool_home = "tools", tool_desing_button_maker = "tool_desing_button_maker", + tool_code_syntax_highlighter = "tool_code_syntax_highlighter", tool_font_replacer = "tool_font_replacer", tool_meta_editor = "tool_meta_editor", tool_batch_meta_editor = "tool_batch_meta_editor", diff --git a/app/package.json b/app/package.json index c2632091..917deec6 100644 --- a/app/package.json +++ b/app/package.json @@ -7,14 +7,14 @@ "author": "bridged.xyz by softmarshmallow ", "scripts": {}, "dependencies": { + "@analytics.bridged.xyz/internal": "^0.0.9", "@assistant/icons": "0.0.0", "@assistant/lint-icons": "0.0.0", - "@analytics.bridged.xyz/internal": "^0.0.9", "@base-sdk-fp/auth": "0.1.0-2", "@base-sdk/base": "0.1.0-5", "@base-sdk/functions-code-format": "^0.0.0", "@bridged.xyz/base-sdk": "^0.0.2-1", - "@code-ui/docstring": "^0.0.9", + "@code-ui/docstring": "^0.0.12", "@design-sdk/universal": "^0.0.0", "@designto/code": "0.0.1", "@emotion/react": "^11.4.0", @@ -49,4 +49,4 @@ "@types/react-dom": "^16.9.8", "@types/react-router-dom": "^5.1.8" } -} \ No newline at end of file +} diff --git a/figma-core/code-thread/broadcast-selection-preview.ts b/figma-core/code-thread/broadcast-selection-preview.ts index e1923c1a..daca62d1 100644 --- a/figma-core/code-thread/broadcast-selection-preview.ts +++ b/figma-core/code-thread/broadcast-selection-preview.ts @@ -1,4 +1,5 @@ -import { EK_PREVIEW_SOURCE } from "@core/constant/ek.constant"; +import { EK_CURRENT_SELECTION_PREVIEW_SOURCE_CHANGED } from "@core/constant/ek.constant"; +import { preset, QuickImageExportPreset } from "@plugin-sdk/core"; /** * extracts the png image of selection, broadcasts to listeners. @@ -6,22 +7,29 @@ import { EK_PREVIEW_SOURCE } from "@core/constant/ek.constant"; * @param selection */ export function broadcastSelectionPreview(selection: SceneNode) { - selection - .exportAsync({ - format: "PNG", - contentsOnly: true, - constraint: { - type: "HEIGHT", - value: 250, + exportImage(selection).then((d) => { + figma.ui.postMessage({ + type: EK_CURRENT_SELECTION_PREVIEW_SOURCE_CHANGED, + data: { + source: d, + name: selection.name, }, - }) - .then((d) => { - figma.ui.postMessage({ - type: EK_PREVIEW_SOURCE, - data: { - source: d, - name: selection.name, - }, - }); }); + }); +} + +export async function exportImage( + target: SceneNode, + options?: { + preset?: QuickImageExportPreset; + } +): Promise { + const config = preset(options?.preset ?? "small"); + try { + return await (target as ExportMixin).exportAsync(config); + } catch (_) { + console.warn( + "update required. seems figma started to support `WidgetNode`" + ); + } } diff --git a/figma-core/code-thread/runon.ts b/figma-core/code-thread/runon.ts index 8f359dde..d8c75806 100644 --- a/figma-core/code-thread/runon.ts +++ b/figma-core/code-thread/runon.ts @@ -3,8 +3,8 @@ import { EK_IMAGE_ASSET_REPOSITORY_MAP, EK_VANILLA_TRANSPORT, } from "@core/constant/ek.constant"; -import { vanilla } from "@design-sdk/core"; -import { ReflectFrameNode, ReflectSceneNode } from "@design-sdk/core/nodes"; +import * as vanilla from "@design-sdk/vanilla"; +import { ReflectFrameNode, ReflectSceneNode } from "@design-sdk/figma-node"; import { user_interest } from "./user-interest"; import { broadcastSelectionPreview } from "./broadcast-selection-preview"; import { singleFigmaNodeSelection } from "./selection"; @@ -37,8 +37,7 @@ export async function runon(rnode: ReflectSceneNode) { // region make vanilla if (user_interest == "g11n" || user_interest == "exporter") { const globalizatoinScreen = vanilla.makeVanilla(rnode as ReflectFrameNode); - const vanillaTransportableImageRepository = - await globalizatoinScreen.repository.makeTransportable(); + const vanillaTransportableImageRepository = await globalizatoinScreen.repository.makeTransportable(); figma.ui.postMessage({ type: EK_IMAGE_ASSET_REPOSITORY_MAP, data: vanillaTransportableImageRepository, diff --git a/figma-core/code-thread/selection.ts b/figma-core/code-thread/selection.ts index 2fb3afa4..243ce3da 100644 --- a/figma-core/code-thread/selection.ts +++ b/figma-core/code-thread/selection.ts @@ -1,7 +1,7 @@ import { analyzeSelection, SelectionAnalysis } from "plugin-app/utils"; import { convert } from "@design-sdk/figma"; import { Logger } from "logger"; -import { light } from "@design-sdk/core/nodes"; +import { makeReference } from "@design-sdk/figma-node"; import { runon } from "./runon"; import { FigmaNodeCache } from "../node-cache"; @@ -10,10 +10,57 @@ export let targetNodeId: string; export function onfigmaselectionchange() { // clear the console for better debugging - console.clear(); - console.warn("log cleared. optimized for new build"); + // console.clear(); + // console.warn("log cleared. optimized for new build"); const rawSelections = figma.currentPage.selection; - console.log("selection", rawSelections); + if (process.env.NODE_ENV === "development") { + console.log("selection", rawSelections); + } + + const convert_allowed_only = (raw: SceneNode) => { + // block large selection + // size determined by px + if (raw.width * raw.height > 20000000) { + figma.notify("Selection too large. we can't operate with this design."); + return; + } + + // block the "too many remote compoents" - this is required since reading through all remote components is slow, could take up more than a minute. (thread freeze until then.) + if ("children" in raw) { + const t1 = new Date(); + let breakit = false; + const timeout = 2 * 1000; + // by reading through the remote components, we can preload the remote data wich will enalbe use to do the code gen in time. + const remote_components = raw.findChildren((n) => { + const t2 = new Date(); + if (t2.getTime() - t1.getTime() > timeout) { + // if reading the components took more than 10 seconds, cancel the process. + breakit = true; + figma.notify("stopping. too many remote components. 😭"); + } + if (breakit) { + return false; + } + return n.type == "INSTANCE" && n.mainComponent.remote; + }); + console.log( + `${remote_components.length} remote components found ${ + breakit ? "with breaking" : "" + }` + ); + + // double check on end. + const t2 = new Date(); + if (t2.getTime() - t1.getTime() > timeout) { + // if reading the components took more than 10 seconds, cancel the process. + figma.notify("stopping. too many remote components. 😭"); + return; + } + } + + return convert.intoReflectNode(raw as any, raw.parent as any); + }; + const selectionType = analyzeSelection(rawSelections); /* unique and only selection setter */ FigmaNodeCache.select( ...rawSelections.map((s) => s.id) @@ -21,7 +68,6 @@ export function onfigmaselectionchange() { switch (selectionType) { case SelectionAnalysis.empty: // ignore when nothing was selected - console.log("deselection"); figma.ui.postMessage({ type: "empty", }); @@ -37,17 +83,21 @@ export function onfigmaselectionchange() { } // todo - add memoization - const rnodes = rawSelections.map((s) => { - return convert.intoReflectNode(s as any, s.parent as any); + let rnodes = rawSelections.map((s) => { + return convert_allowed_only(s); }); - Logger.debug("reflect-converted-selections", rnodes); + rnodes.filter((r) => r !== undefined); + Logger.debug("reflect-converted-selections", rnodes); // region sync selection event (search "selectionchange" for references) - figma.ui.postMessage({ - type: "selectionchange", - data: rnodes.map((n) => light.makeReference(n)), - }); - // endregion + if (rnodes.length >= 1) { + figma.ui.postMessage({ + type: "selectionchange", + data: rnodes.map((n) => makeReference(n)), + }); + } + // endregion + return; case SelectionAnalysis.single: const target = figma.currentPage.selection[0]; @@ -57,23 +107,30 @@ export function onfigmaselectionchange() { // TODO: this will not trigger unless user deselects and re select the same node. currently node cache does not have expiry control. let rnode; - const _cached = FigmaNodeCache.getLastConverted(); + const _cached = FigmaNodeCache.getConverted(singleFigmaNodeSelection.id); if (_cached) { + console.info("using cached", _cached.name); rnode = _cached; } else { - rnode = convert.intoReflectNode( - singleFigmaNodeSelection as any, - singleFigmaNodeSelection.parent as any - ); + rnode = convert_allowed_only(singleFigmaNodeSelection); + if (!rnode) { + return; + } } // Logger.debug("reflect-converted-selection", rnode); // region sync selection event (search "selectionchange" for references) - figma.ui.postMessage({ - type: "selectionchange", - data: light.makeReference(rnode), - }); + try { + const data = makeReference(rnode); + figma.ui.postMessage({ + type: "selectionchange", + data: data, + }); + } catch (_) { + figma.notify(`Oops. we don't support "${target.type}" yet.`); + console.error(_); + } // endregion FigmaNodeCache.setConverted(rnode); runon(rnode); diff --git a/figma-core/event-handlers/create-icon.ts b/figma-core/event-handlers/create-icon.ts index fbaf63d6..854ca25d 100644 --- a/figma-core/event-handlers/create-icon.ts +++ b/figma-core/event-handlers/create-icon.ts @@ -1,4 +1,4 @@ -import { IconConfig } from "@reflect-ui/core"; +import { NamedIconConfig } from "@reflect-ui/core"; import { EK_CREATE_ICON, EK_ICON_DRAG_AND_DROPPED } from "@core/constant"; import { PluginSdkService } from "@plugin-sdk/service"; import { IconPlacement, renderSvgIcon } from "../reflect-render/icons.render"; @@ -7,7 +7,7 @@ import { addEventHandler } from "../code-thread"; interface CreateIconProps { key: string; svg: string; - config: IconConfig; + config: NamedIconConfig; } function createIcon( diff --git a/figma-core/index.ts b/figma-core/index.ts index 65751d47..71e279ba 100644 --- a/figma-core/index.ts +++ b/figma-core/index.ts @@ -13,6 +13,11 @@ /* do not delete this line */ import "./event-handlers"; // NO REMOVE // ========== +// ========== +// init relaunch-data trigger +/* do not delete this line */ import "./relaunch-data"; // NO REMOVE +// ========== + import { onfigmaselectionchange } from "./code-thread/selection"; import { onfigmamessage } from "./code-thread/message-handler"; import { MainImageRepository } from "@design-sdk/core/assets-repository"; @@ -41,10 +46,16 @@ figma.on("currentpagechange", () => { // MAIN INITIALIZATION import { showUI } from "./code-thread/show-ui"; import { provideFigma } from "@design-sdk/figma"; +import { createPrimaryVisualStorePageIfNonExists } from "./physical-visual-store/page-manager/craete-page-if-non-exist"; function main() { MainImageRepository.instance = new ImageRepositories(); provideFigma(figma); showUI(); + + // disabled on staging ---- + // create primary visual store + // createPrimaryVisualStorePageIfNonExists(); + // ------------------------ } main(); diff --git a/figma-core/node-cache/index.ts b/figma-core/node-cache/index.ts index 55638731..98d22307 100644 --- a/figma-core/node-cache/index.ts +++ b/figma-core/node-cache/index.ts @@ -1,5 +1,13 @@ import { SceneNode } from "@design-sdk/figma-types"; -import { ReflectSceneNode } from "@design-sdk/core/nodes"; +import { ReflectSceneNode } from "@design-sdk/figma-node"; + +const RUNTIME_RAPID_SHORT_LIVED_CACHE_TIMEOUT_MS = 5 * 1000; // after 5 seconds, we'll assume the node is not available +const RUNTIME_SHORT_LIVED_SAME_SELECTION_CACHE_TIMEOUT_MS = 2.5 * 1000; + +interface TimedCacheContainer { + updatedAt: number; + data: T; +} /** Global runtime figma selection & conversion cache repository * @todo: add conversion cache @@ -12,17 +20,57 @@ export class FigmaNodeCache { return this._lastSelections; } - private static _lastConverted: ReflectSceneNode | null = null; + private static _conversions: { + [key: string]: TimedCacheContainer; + } = {}; + private static _lastConverted: TimedCacheContainer | null = null; static get lastConverted(): ReflectSceneNode | null { - return this._lastConverted; + return this._lastConverted.data; } static select(...ids: string[]) { this._lastSelections = ids; + + /// when deselected, update last selected nodes' updated at. + if (ids.length === 0) { + this._lastConverted = { + ...this._lastConverted, + updatedAt: Date.now(), + }; + } } static setConverted(rnode: ReflectSceneNode) { - this._lastConverted = rnode; + const record = { + updatedAt: Date.now(), + data: rnode, + }; + this._conversions[rnode.id] = record; + this._lastConverted = record; + } + + static getConverted(id: string): ReflectSceneNode | null { + // when the selection is not changed, without looking up the timeout, return the last converted node. + if (id == this._lastConverted?.data?.id) { + if ( + this._lastConverted.updatedAt + + RUNTIME_SHORT_LIVED_SAME_SELECTION_CACHE_TIMEOUT_MS > + Date.now() + ) { + return this._lastConverted.data; + } + } + // + const conversion = this._conversions[id]; + if (conversion) { + if ( + conversion.updatedAt + RUNTIME_RAPID_SHORT_LIVED_CACHE_TIMEOUT_MS > + Date.now() + ) { + return conversion.data; + } + } + return null; } static get lastId(): string | null { @@ -39,8 +87,8 @@ export class FigmaNodeCache { } static getLastConverted(): ReflectSceneNode | null { - if (this.lastId == this._lastConverted?.id) { - return this._lastConverted; + if (this.lastId && this.lastId == this._lastConverted?.data?.id) { + return this._lastConverted.data; } return null; } diff --git a/figma-core/package.json b/figma-core/package.json index 5ce2471a..9162ccc8 100644 --- a/figma-core/package.json +++ b/figma-core/package.json @@ -9,7 +9,7 @@ "@plugin-sdk/service": "0.0.0" }, "devDependencies": { - "@figma/plugin-typings": "^1.19.3", + "@figma/plugin-typings": "^1.37.0", "typescript": "^4.0.5" } } \ No newline at end of file diff --git a/figma-core/physical-visual-store/README.md b/figma-core/physical-visual-store/README.md new file mode 100644 index 00000000..e8e2136e --- /dev/null +++ b/figma-core/physical-visual-store/README.md @@ -0,0 +1,5 @@ +# Physical visual store + +This is a package for creating a physical & visual data node inside figma. this is designed for sync data between accounts and devices. + +> Since figma's `setPluginData` nor `setSharedPluginData` does not sync the data between clients. diff --git a/figma-core/physical-visual-store/checksum/is-managed-node.ts b/figma-core/physical-visual-store/checksum/is-managed-node.ts new file mode 100644 index 00000000..378f08f8 --- /dev/null +++ b/figma-core/physical-visual-store/checksum/is-managed-node.ts @@ -0,0 +1,28 @@ +/** + * to keep data container node's name clean, we add the checksum container under it as a text's content. + * @param node + * @returns + */ +export function isManagedNodeByAssistant(node: BaseNode) { + const jwt = checksum_container_name_inline_jwt(node.id); + const regex = checksum_container_regex(jwt); + if ("children" in node) { + const checksum = node.findChildren( + (c) => c.type === "TEXT" && RegExp(regex).test(c.characters) + ); + if (!checksum || checksum.length === 0) { + return false; + } + } else { + // node itself is a checksum container. this can only exist directly under page node. + return RegExp(regex).test(node.name); + } + return true; +} + +function checksum_container_name_inline_jwt(parent: string): string { + // build checksum based on parent's id. + return "--"; // TODO: +} + +const checksum_container_regex = (jwt) => `/${jwt}/g`; diff --git a/figma-core/physical-visual-store/index.ts b/figma-core/physical-visual-store/index.ts new file mode 100644 index 00000000..9ee20be3 --- /dev/null +++ b/figma-core/physical-visual-store/index.ts @@ -0,0 +1 @@ +export * from "./page-manager"; diff --git a/figma-core/physical-visual-store/page-manager/craete-page-if-non-exist.ts b/figma-core/physical-visual-store/page-manager/craete-page-if-non-exist.ts new file mode 100644 index 00000000..3718ea7a --- /dev/null +++ b/figma-core/physical-visual-store/page-manager/craete-page-if-non-exist.ts @@ -0,0 +1,32 @@ +import { creaateReadme } from "./create-readme"; +import { FigmaColorFormat } from "@design-sdk/figma"; +import { reflectColorToFigmaColor } from "@design-sdk/figma-node-conversion"; + +/** + * the empty space is for hiding following text from figma's page hierarchy. + * user can change the icon, spacing, but not the token for regex validation. + */ +const PRIMARY_ASSISTANT_VISUAL_STORE_PAGE_NAME = + "📕 __storage__ (assistant.grida.co/primary)"; +const PRIMARY_ASSISTANT_VISUAL_STORE_PAGE_NAME_REGEX = /__storage__([\w ]+)\(assistant.grida.co\/primary\)/g; +export function createPrimaryVisualStorePageIfNonExists() { + const document = figma.currentPage.parent as DocumentNode; + const existing = document.findChild((x) => + RegExp(PRIMARY_ASSISTANT_VISUAL_STORE_PAGE_NAME_REGEX).test(x.name) + ); + if (existing) { + // + } else { + const page = figma.createPage(); + document.insertChild(document.children.length, page); // add to last + page.name = PRIMARY_ASSISTANT_VISUAL_STORE_PAGE_NAME; + page.backgrounds = [ + { + type: "SOLID", + color: reflectColorToFigmaColor("#1E1E1E", FigmaColorFormat.rgb), + }, + ]; + + creaateReadme({ at: page }); + } +} diff --git a/figma-core/physical-visual-store/page-manager/create-readme.ts b/figma-core/physical-visual-store/page-manager/create-readme.ts new file mode 100644 index 00000000..458dc3b8 --- /dev/null +++ b/figma-core/physical-visual-store/page-manager/create-readme.ts @@ -0,0 +1,45 @@ +import { renderText } from "../../reflect-render/text.render"; + +export async function creaateReadme({ + at, + afterMoving = false, +}: { + at: PageNode; + afterMoving?: boolean; +}): Promise { + if (afterMoving) { + figma.currentPage = at; + } + const readmeframe = figma.createFrame(); + at.insertChild(0, readmeframe); + readmeframe.name = "README"; + readmeframe.resize(1440, 900); + readmeframe.x = 0; + readmeframe.y = 0; + readmeframe.locked = true; + + // + readmeframe.insertChild(0, await createInitialReadmeText()); + // + return readmeframe; +} + +async function createInitialReadmeText() { + const font = { + family: "Helvetica Neue", + style: "Regular", + }; + await figma.loadFontAsync(font); + const readmeText = renderText({ + text: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sapien felis, volutpat ut cursus nec, euismod vel dolor. Vestibulum et purus eget ligula tempus scelerisque eget ut mi. Curabitur quam augue, vulputate et mi imperdiet, scelerisque posuere ligula. Sed finibus, turpis ultrices mollis aliquet, ipsum lacus pulvinar sapien, sit amet fringilla ante augue sit amet leo. Curabitur dapibus finibus quam, sit amet rhoncus augue pharetra id. Sed eget laoreet ex. Integer convallis orci massa, ac convallis libero tempor ut.", + fontName: font, + fontSize: 14, + color: "#111111", + }); + readmeText.x = 24; + readmeText.y = 24; + readmeText.resize(1392, 100); + + return readmeText; +} diff --git a/figma-core/physical-visual-store/page-manager/index.ts b/figma-core/physical-visual-store/page-manager/index.ts new file mode 100644 index 00000000..e62ef4b4 --- /dev/null +++ b/figma-core/physical-visual-store/page-manager/index.ts @@ -0,0 +1 @@ +export { createPrimaryVisualStorePageIfNonExists as createPageIfNonExists } from "./craete-page-if-non-exist"; diff --git a/figma-core/physical-visual-store/used-emoji/README.md b/figma-core/physical-visual-store/used-emoji/README.md new file mode 100644 index 00000000..85c96db5 --- /dev/null +++ b/figma-core/physical-visual-store/used-emoji/README.md @@ -0,0 +1,3 @@ +# Emojis are only used inside a visual store document. + +> Grida is not fan of a emoji. (well we do love to use them in a document, but we try our best to avoid them in our products design.) diff --git a/figma-core/physical-visual-store/used-emoji/index.ts b/figma-core/physical-visual-store/used-emoji/index.ts new file mode 100644 index 00000000..8fe685dc --- /dev/null +++ b/figma-core/physical-visual-store/used-emoji/index.ts @@ -0,0 +1 @@ +const GEAR = "⚙"; // U+2699 diff --git a/figma-core/reflect-render/buttons.render/index.ts b/figma-core/reflect-render/buttons.render/index.ts index 1b6eebe6..91335e2e 100644 --- a/figma-core/reflect-render/buttons.render/index.ts +++ b/figma-core/reflect-render/buttons.render/index.ts @@ -1,5 +1,5 @@ import { Color } from "@reflect-ui/core/lib/color"; -import { converters } from "@design-sdk/figma"; +import { reflectColorToFigmaRGB } from "@design-sdk/figma-node-conversion"; import { ButtonColorScheme } from "@reflect-ui/core/lib/theme/color-schemes"; import { BUTTON_COLOR_SCHEMES_SET } from "@reflect.bridged.xyz/ui-generator/lib/seeds/color-schemes/button.color-scheme.seed"; import { BUTTON_TEXTS_SET_EN } from "@reflect.bridged.xyz/ui-generator/lib/seeds"; @@ -188,7 +188,7 @@ function generateRandomBorder(color: Color | undefined): Paint | undefined { return { type: "SOLID", - color: converters.reflectColorToFigmaRGB(color), + color: reflectColorToFigmaRGB(color), visible: chanceBy(0.5), }; } diff --git a/figma-core/reflect-render/cgrect.render/index.ts b/figma-core/reflect-render/cgrect.render/index.ts index 19903926..bdaa0e11 100644 --- a/figma-core/reflect-render/cgrect.render/index.ts +++ b/figma-core/reflect-render/cgrect.render/index.ts @@ -1,4 +1,7 @@ -import { converters } from "@design-sdk/figma"; +import { + reflectColorToFigmaRGBA, + reflectColorToFigmaRGB, +} from "@design-sdk/figma-node-conversion"; import { Color } from "@reflect-ui/core/lib/color"; // his should be repplaced by reflect's `CGRectManifest` when fully constructed @@ -25,7 +28,7 @@ export function renderCgRect( rect.fills = [ { type: "SOLID", - color: converters.reflectColorToFigmaRGB(rectManifest.color), + color: reflectColorToFigmaRGB(rectManifest.color), opacity: 1, }, ]; @@ -47,11 +50,11 @@ export function renderCgRect( ], gradientStops: [ { - color: converters.reflectColorToFigmaRGBA(startColor), + color: reflectColorToFigmaRGBA(startColor), position: 0, }, { - color: converters.reflectColorToFigmaRGBA(endColor), + color: reflectColorToFigmaRGBA(endColor), position: 1, }, ], diff --git a/figma-core/reflect-render/icons.render/index.ts b/figma-core/reflect-render/icons.render/index.ts index 18e23c4a..668777c7 100644 --- a/figma-core/reflect-render/icons.render/index.ts +++ b/figma-core/reflect-render/icons.render/index.ts @@ -1,7 +1,6 @@ -// todo - make this universal -import { converters } from "@design-sdk/figma"; +import { reflectColorToFigmaColor } from "@design-sdk/figma-node-conversion"; import { Color } from "@reflect-ui/core/lib/color"; -import { IconConfig } from "@reflect-ui/core/lib/icon/icon.config"; +import { NamedIconConfig } from "@reflect-ui/core"; import { ICON_DEFAULT_SIZE, ICON_MAX_SIZE, @@ -19,7 +18,7 @@ export function renderSvgIcon( data: string, color: Color = "#000000", placement: IconPlacement = "center", - config?: IconConfig + config?: NamedIconConfig ): FrameNode { console.log(`inserting icon with name ${name} and data ${data}`); @@ -63,7 +62,7 @@ export function renderSvgIcon( export function buildReflectIconNameForRender( name: string, - config: IconConfig + config: NamedIconConfig ): string { if (config.host == "material") { return `icons/mdi_${name}`; @@ -90,7 +89,7 @@ export function switchSvgColor( node.fills = [ { type: "SOLID", - color: converters.reflectColorToFigmaColor(sets[0].to), + color: reflectColorToFigmaColor(sets[0].to), }, ]; } diff --git a/figma-core/reflect-render/text.render/index.ts b/figma-core/reflect-render/text.render/index.ts index 0b20f5c8..0536fa37 100644 --- a/figma-core/reflect-render/text.render/index.ts +++ b/figma-core/reflect-render/text.render/index.ts @@ -1,4 +1,4 @@ -import { converters } from "@design-sdk/figma"; +import { reflectColorToFigmaColor } from "@design-sdk/figma-node-conversion"; import { Color } from "@reflect-ui/core/lib/color"; // FIXME - this should be repplaced by reflect's `TextManifest` when fully constructed @@ -21,7 +21,7 @@ export function renderText(textManifest: FigmaRenderTextManifest): TextNode { // randomize font size text.fontSize = textManifest.fontSize; - const textColor = converters.reflectColorToFigmaColor(textManifest.color); + const textColor = reflectColorToFigmaColor(textManifest.color); text.fills = [ { diff --git a/figma-core/relaunch-data/README.md b/figma-core/relaunch-data/README.md new file mode 100644 index 00000000..f82d8011 --- /dev/null +++ b/figma-core/relaunch-data/README.md @@ -0,0 +1,10 @@ +# Relaunch data management + +![](./relaunch-data-example.png) + +1. initially, set the relaunce data on root page node for ux & plugin suggestion. + +### References + +- https://www.figma.com/plugin-docs/api/properties/nodes-setrelaunchdata/ +- https://www.figma.com/plugin-docs/manifest/#relaunchbuttons diff --git a/figma-core/relaunch-data/index.ts b/figma-core/relaunch-data/index.ts new file mode 100644 index 00000000..2aca6948 --- /dev/null +++ b/figma-core/relaunch-data/index.ts @@ -0,0 +1,10 @@ +figma.once("run", () => { + figma + .getNodeById( + /** + * root node's id is always 0:0 on plugin api. [learn more](https://github.com/figma/plugin-typings/issues/13) + */ + "0:0" + ) + .setRelaunchData({ open: "" }); +}); diff --git a/figma-core/relaunch-data/relaunch-data-example.png b/figma-core/relaunch-data/relaunch-data-example.png new file mode 100644 index 00000000..292c6232 Binary files /dev/null and b/figma-core/relaunch-data/relaunch-data-example.png differ diff --git a/figma/.gitignore b/figma/.gitignore index 2eea525d..063b9b51 100644 --- a/figma/.gitignore +++ b/figma/.gitignore @@ -1 +1,2 @@ -.env \ No newline at end of file +.env +*-archive.zip \ No newline at end of file diff --git a/figma/README.md b/figma/README.md index eed6b6f9..08c38f94 100644 --- a/figma/README.md +++ b/figma/README.md @@ -22,7 +22,7 @@ $ yarn install # building for production - this will load production web hosted version in your plugin host $ yarn run build -# building for development - this will load localhost:3000/init-figma page in to your plugin host +# building for development - this will load localhost:3303/init-figma page in to your plugin host $ yarn run build:dev $ yarn run watch # same as `build:dev`, but in watch mode. (if you are not interacting with figma-core, you don't have to run this command.) ``` diff --git a/figma/manifest.json b/figma/manifest.json index bdfb4ffc..13a04e7e 100644 --- a/figma/manifest.json +++ b/figma/manifest.json @@ -6,5 +6,12 @@ "ui": "dist/ui.html", "editorType": [ "figma" + ], + "relaunchButtons": [ + { + "command": "open", + "name": "Open Assistant", + "multipleSelection": true + } ] } \ No newline at end of file diff --git a/figma/package.json b/figma/package.json index 0ad1ce82..577c4521 100644 --- a/figma/package.json +++ b/figma/package.json @@ -8,8 +8,11 @@ "@ui/skeleton": "0.0.0" }, "scripts": { - "build": "webpack -p --mode=production", - "build:dev": "webpack --mode=development", - "watch": "webpack --watch" + "clean": "rimraf dist", + "build": "yarn clean && webpack --mode=production --host=production", + "build:staging": "yarn clean && webpack --mode=production --host=staging", + "build:dev": "webpack --mode=development --host=dev", + "watch": "webpack --watch", + "dev": "yarn watch" } } \ No newline at end of file diff --git a/figma/src/ui.tsx b/figma/src/ui.tsx index 5dfb96ab..fabc3500 100644 --- a/figma/src/ui.tsx +++ b/figma/src/ui.tsx @@ -2,26 +2,27 @@ import React, { useRef, useState, useEffect } from "react"; import * as ReactDOM from "react-dom"; import { AppSkeleton } from "@ui/skeleton"; import { handle } from "./handle-proxy-requests"; +import { useSetRecoilState } from "recoil"; ReactDOM.render( , document.getElementById("react-page") ); -function LiteHostedAppConnector() { +export function LiteHostedAppConnector() { const frame = useRef(); + const resizableRef = useRef(); const [initialized, setInitialized] = useState(false); + const [size, setSize] = useState({ w: 0, h: 0 }); useEffect(() => { if (frame) { window.addEventListener("message", (event) => { - // console.log("event recievd from lite-fima-app", event.data); - if (event.data == "plugin-app-initialized") { + if (event.data === "plugin-app-initialized") { setInitialized(true); - } - - if ("pluginMessage" in event.data) { + return; + } else if ("pluginMessage" in event.data) { if (event.data.pluginMessage.__proxy_request_from_hosted_plugin) { handle(event.data.pluginMessage); return; @@ -45,38 +46,36 @@ function LiteHostedAppConnector() { } }, [frame]); - const _host = - process.env.NODE_ENV === "production" - ? "https://assistant-serve.grida.co" - : "http://localhost:3000"; + const _host = process.env.HOST; // use opacity // if (initialized) => show iframe only // eles, show iframe with opacity 0 & enable AppSkeleton // - return ( -
-