diff --git a/editor/components/app-runner/vanilla-app-runner.tsx b/editor/components/app-runner/vanilla-app-runner.tsx index 1ec30c96..c3fab11a 100644 --- a/editor/components/app-runner/vanilla-app-runner.tsx +++ b/editor/components/app-runner/vanilla-app-runner.tsx @@ -43,7 +43,7 @@ export function VanillaRunner({ if (ref.current && enableInspector) { ref.current.onload = () => { const matches = ref.current.contentDocument.querySelectorAll( - "div, span, button, img, image, svg" + "div, span, img, image, svg" // button, input - disabled due to interaction testing (for users) ); matches.forEach((el) => { const tint = "rgba(20, 0, 255, 0.2)"; diff --git a/editor/components/editor/editor-browser-meta-head/index.tsx b/editor/components/editor/editor-browser-meta-head/index.tsx new file mode 100644 index 00000000..e0c3104e --- /dev/null +++ b/editor/components/editor/editor-browser-meta-head/index.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import Head from "next/head"; +import { useEditorState } from "core/states"; + +export function EditorBrowserMetaHead({ + children, +}: { + children: React.ReactChild; +}) { + const [state] = useEditorState(); + + return ( + <> + + + {state?.design?.name ? `Grida | ${state?.design?.name}` : "Loading.."} + + + {children} + + ); +} diff --git a/editor/components/editor/index.ts b/editor/components/editor/index.ts index 97ceb318..5d4287c9 100644 --- a/editor/components/editor/index.ts +++ b/editor/components/editor/index.ts @@ -1,3 +1,4 @@ export * from "./editor-appbar"; +export * from "./editor-browser-meta-head"; export * from "./editor-layer-hierarchy"; export * from "./editor-sidebar"; diff --git a/editor/core/reducers/editor-reducer.ts b/editor/core/reducers/editor-reducer.ts index 7a2128d5..b50c8803 100644 --- a/editor/core/reducers/editor-reducer.ts +++ b/editor/core/reducers/editor-reducer.ts @@ -16,8 +16,14 @@ export function editorReducer(state: EditorState, action: Action): EditorState { console.info("cleard console by editorReducer#select-node"); // update router - router.query.node = node ?? state.selectedPage; - router.push(router); + router.push( + { + pathname: router.pathname, + query: { ...router.query, node: node ?? state.selectedPage }, + }, + undefined, + {} + ); return produce(state, (draft) => { const _canvas_state_store = new CanvasStateStore( @@ -42,8 +48,14 @@ export function editorReducer(state: EditorState, action: Action): EditorState { console.info("cleard console by editorReducer#select-page"); // update router - router.query.node = page; - router.push(router); + router.push( + { + pathname: router.pathname, + query: { ...router.query, node: page }, + }, + undefined, + {} + ); return produce(state, (draft) => { const _canvas_state_store = new CanvasStateStore(filekey, page); diff --git a/editor/core/states/editor-state.ts b/editor/core/states/editor-state.ts index 7b3c3965..7e9494dd 100644 --- a/editor/core/states/editor-state.ts +++ b/editor/core/states/editor-state.ts @@ -24,6 +24,11 @@ export interface EditorSnapshot { } export interface FigmaReflectRepository { + /** + * name of the file + */ + name: string; + /** * fileid; filekey */ diff --git a/editor/pages/files/[key]/index.tsx b/editor/pages/files/[key]/index.tsx index 94bb2374..43e6d673 100644 --- a/editor/pages/files/[key]/index.tsx +++ b/editor/pages/files/[key]/index.tsx @@ -8,6 +8,7 @@ import { useDesignFile } from "hooks"; import { warmup } from "scaffolds/editor"; import { FileResponse } from "@design-sdk/figma-remote-types"; +import { EditorBrowserMetaHead } from "components/editor"; export default function FileEntryEditor() { const router = useRouter(); @@ -64,6 +65,7 @@ export default function FileEntryEditor() { selectedPage: warmup.selectedPage(prevstate, pages, nodeid && [nodeid]), selectedLayersOnPreview: [], design: { + name: file.name, input: null, components: components, // styles: null, @@ -131,7 +133,9 @@ export default function FileEntryEditor() { - + + + diff --git a/editor/pages/files/index.tsx b/editor/pages/files/index.tsx index 8811055b..8ec7c948 100644 --- a/editor/pages/files/index.tsx +++ b/editor/pages/files/index.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import Head from "next/head"; import { DefaultEditorWorkspaceLayout } from "layouts/default-editor-workspace-layout"; import { Cards, @@ -19,6 +20,9 @@ export default function FilesPage() { return ( <> + + Grida | files + } diff --git a/editor/pages/index.tsx b/editor/pages/index.tsx index 57c1f8be..becaba28 100644 --- a/editor/pages/index.tsx +++ b/editor/pages/index.tsx @@ -1,13 +1,22 @@ +import React from "react"; +import Head from "next/head"; + import { HomeInput } from "scaffolds/home-input"; import { HomeDashboard } from "scaffolds/home-dashboard"; -import React from "react"; import { useAuthState } from "hooks/use-auth-state"; export default function Home() { const authstate = useAuthState(); // region - dev injected - return ; + return ( + <> + + Grida | Home + + + + ); // endregion switch (authstate) { diff --git a/editor/scaffolds/editor/warmup.ts b/editor/scaffolds/editor/warmup.ts index 906f57bd..382f9916 100644 --- a/editor/scaffolds/editor/warmup.ts +++ b/editor/scaffolds/editor/warmup.ts @@ -7,13 +7,11 @@ import { import { createInitialWorkspaceState } from "core/states"; import { workspaceReducer } from "core/reducers"; import { PendingState } from "core/utility-types"; -import { DesignInput } from "@designto/config/input"; -import { TargetNodeConfig } from "query/target-node"; import { WorkspaceAction } from "core/actions"; import { FileResponse } from "@design-sdk/figma-remote-types"; import { convert } from "@design-sdk/figma-node-conversion"; import { mapper } from "@design-sdk/figma-remote"; -import { find, visit } from "tree-visit"; +import { visit } from "tree-visit"; const pending_workspace_state = createPendingWorkspaceState(); // @@ -98,24 +96,6 @@ export function componentsFrom( .reduce(tomap, {}); } -export function initializeDesign(design: TargetNodeConfig): EditorSnapshot { - return { - selectedNodes: [design.node], - selectedLayersOnPreview: [], - selectedPage: null, - design: { - pages: [], - components: null, - // styles: null, - key: design.file, - input: DesignInput.fromApiResponse({ - ...design, - entry: design.reflect, - }), - }, - }; -} - export function safestate(initialState) { return initialState.type === "success" ? initialState.value diff --git a/externals/coli b/externals/coli index 86802c58..cafda00d 160000 --- a/externals/coli +++ b/externals/coli @@ -1 +1 @@ -Subproject commit 86802c58f19f5acc1de059faabc62dd2709291bc +Subproject commit cafda00d9e3979dd1a1692c35a6a27da58d1fc50 diff --git a/externals/design-sdk b/externals/design-sdk index 6c54a532..6265f64c 160000 --- a/externals/design-sdk +++ b/externals/design-sdk @@ -1 +1 @@ -Subproject commit 6c54a532cec8a4ca016a43a8d75461e0a961ff13 +Subproject commit 6265f64ce67e84d490c1739a77cb0df42c2f75b1 diff --git a/externals/reflect-core b/externals/reflect-core index 7e792b1f..2b4fd615 160000 --- a/externals/reflect-core +++ b/externals/reflect-core @@ -1 +1 @@ -Subproject commit 7e792b1fff930f3b1f49956b9ab2f9d40aa6f682 +Subproject commit 2b4fd6153dbc3b01e59c0a5399ba080cdb9ce139 diff --git a/packages/builder-css-styles/border/index.ts b/packages/builder-css-styles/border/index.ts index 4576467d..f8925a20 100644 --- a/packages/builder-css-styles/border/index.ts +++ b/packages/builder-css-styles/border/index.ts @@ -33,6 +33,10 @@ export function border(border: Border): CSSProperties { } export function borderSide(borderSide: BorderSide): CSSProperty.Border { + if (borderSide.style === "none") { + return "none"; + } + return `${borderSide.style ?? "solid"} ${px(borderSide.width)} ${color( borderSide.color )}`; diff --git a/packages/builder-css-styles/color/index.ts b/packages/builder-css-styles/color/index.ts index 4d0b8117..31fd85bf 100644 --- a/packages/builder-css-styles/color/index.ts +++ b/packages/builder-css-styles/color/index.ts @@ -19,8 +19,8 @@ export function color(input: CssColorInputLike | Color): string { } else if (input instanceof CssNamedColor) { return input.name; } else if (typeof input == "object") { - // with alpha - if ("r" in input && "a" in input) { + // with alpha (if alpha is 1, use rgb format instead) + if ("r" in input && "a" in input && input.a !== 1) { const a = safe_alpha_fallback(validAlphaValue(input.a)); const rgba = input as ICssRGBA; const _r = validColorValue(rgba.r) ?? 0; @@ -31,6 +31,11 @@ export function color(input: CssColorInputLike | Color): string { // no alpha else if ("r" in input && "a"! in input) { const rgb = input as RGB; + const named = namedcolor(rgb); + if (named) { + return named; + } + return `rgb(${validColorValue(rgb.r) ?? 0}, ${ validColorValue(rgb.g) ?? 0 }, ${validColorValue(rgb.b) ?? 0})`; @@ -42,6 +47,29 @@ export function color(input: CssColorInputLike | Color): string { } } +/** + * rgb value of white, black as named colors + * @param rgb + */ +const namedcolor = (rgb: RGB) => { + // black + if (rgb.r === 0 && rgb.g === 0 && rgb.b === 0) { + return "black"; + } + // white + if (rgb.r === 1 && rgb.g === 1 && rgb.b === 1) { + return "white"; + } + // blue + if (rgb.r === 0 && rgb.g === 0 && rgb.b === 1) { + return "blue"; + } + // red + if (rgb.r === 1 && rgb.g === 0 && rgb.b === 0) { + return "red"; + } +}; + const validColorValue = (f: number) => { if (f === undefined) { return; diff --git a/packages/builder-css-styles/index.ts b/packages/builder-css-styles/index.ts index 7453ef0b..951bd0c1 100644 --- a/packages/builder-css-styles/index.ts +++ b/packages/builder-css-styles/index.ts @@ -9,6 +9,7 @@ export * from "./font-weight"; export * from "./font-family"; export * from "./text-decoration"; export * from "./text-shadow"; +export * from "./text-transform"; export * from "./gradient"; export * from "./padding"; export * from "./margin"; diff --git a/packages/builder-css-styles/padding/index.ts b/packages/builder-css-styles/padding/index.ts index 93172c4d..5f8a30c1 100644 --- a/packages/builder-css-styles/padding/index.ts +++ b/packages/builder-css-styles/padding/index.ts @@ -8,8 +8,13 @@ import { px } from "../dimensions"; type PaddingValue = number | "auto"; -export function padding(p: EdgeInsets): CSSProperties { - switch (edgeInsetsShorthandMode(p)) { +export function padding( + p: EdgeInsets, + options?: { + explicit?: boolean; + } +): CSSProperties { + switch (edgeInsetsShorthandMode(p, options)) { case EdgeInsetsShorthandMode.empty: { return {}; } @@ -31,10 +36,10 @@ export function padding(p: EdgeInsets): CSSProperties { case EdgeInsetsShorthandMode.trbl: default: { return { - "padding-bottom": _makeifRequired(p?.bottom), - "padding-top": _makeifRequired(p?.top), - "padding-left": _makeifRequired(p?.left), - "padding-right": _makeifRequired(p?.right), + "padding-bottom": _makeifRequired(p?.bottom, options?.explicit), + "padding-top": _makeifRequired(p?.top, options?.explicit), + "padding-left": _makeifRequired(p?.left, options?.explicit), + "padding-right": _makeifRequired(p?.right, options?.explicit), }; } } @@ -55,8 +60,8 @@ function _pv(pv: PaddingValue) { return px(pv); } -function _makeifRequired(val: number): string | undefined { - if (val && val > 0) { +function _makeifRequired(val: number, explicit?: boolean): string | undefined { + if (explicit || (val && val > 0)) { return px(val); } } diff --git a/packages/builder-css-styles/text-shadow/index.ts b/packages/builder-css-styles/text-shadow/index.ts index 676370fe..9b2a722e 100644 --- a/packages/builder-css-styles/text-shadow/index.ts +++ b/packages/builder-css-styles/text-shadow/index.ts @@ -3,7 +3,7 @@ import { color } from "../color"; import { px } from "../dimensions"; export function textShadow(ts: TextShadowManifest[]): string { - if (ts.length === 0) { + if (!ts || ts.length === 0) { return; } diff --git a/packages/builder-css-styles/text-transform/index.ts b/packages/builder-css-styles/text-transform/index.ts new file mode 100644 index 00000000..289f02fb --- /dev/null +++ b/packages/builder-css-styles/text-transform/index.ts @@ -0,0 +1,20 @@ +import { TextTransform } from "@reflect-ui/core"; + +export function textTransform(tt: TextTransform) { + switch (tt) { + case TextTransform.capitalize: + return "capitalize"; + case TextTransform.lowercase: + return "lowercase"; + case TextTransform.uppercase: + return "uppercase"; + case TextTransform.fullwidth: + return "full-width"; + case TextTransform.fullsizekana: + return "full-size-kana"; + case TextTransform.none: + default: + // for none, we don't explicitly set it - to make it shorter. + return; + } +} diff --git a/packages/builder-css-styles/tricks/trick-flex-sizing/index.ts b/packages/builder-css-styles/tricks/trick-flex-sizing/index.ts index b3553240..33e4604d 100644 --- a/packages/builder-css-styles/tricks/trick-flex-sizing/index.ts +++ b/packages/builder-css-styles/tricks/trick-flex-sizing/index.ts @@ -30,6 +30,8 @@ export function flexsizing({ case MainAxisSize.max: { return { "align-self": "stretch", + width: width && length(width), + height: height && length(height), }; } case MainAxisSize.min: { @@ -37,7 +39,7 @@ export function flexsizing({ case Axis.horizontal: case Axis.vertical: return { - flex: "none", + flex: flex > 0 ? flex : "none", // 1+ width: width && length(width), height: height && length(height), }; diff --git a/packages/builder-react-native/rn-build-inline-style-widget/rn-inline-style-module-builder.ts b/packages/builder-react-native/rn-build-inline-style-widget/rn-inline-style-module-builder.ts index 379a57fc..74fcd7b9 100644 --- a/packages/builder-react-native/rn-build-inline-style-widget/rn-inline-style-module-builder.ts +++ b/packages/builder-react-native/rn-build-inline-style-widget/rn-inline-style-module-builder.ts @@ -11,7 +11,7 @@ import { } from "@web-builder/react-core"; import { buildJsx, - getWidgetStylesConfigMap, + StylesConfigMapBuilder, JSXWithoutStyleElementConfig, JSXWithStyleElementConfig, WidgetStyleConfigMap, @@ -48,7 +48,8 @@ export class ReactNativeInlineStyleBuilder { private readonly widgetName: string; readonly config: reactnative_config.ReactNativeInlineStyleConfig; private readonly namer: ScopedVariableNamer; - private readonly stylesConfigWidgetMap: WidgetStyleConfigMap; + private readonly stylesMapper: StylesConfigMapBuilder; + // private readonly stylesConfigWidgetMap: WidgetStyleConfigMap; constructor({ entry, @@ -64,7 +65,8 @@ export class ReactNativeInlineStyleBuilder { entry.key.id, ReservedKeywordPlatformPresets.react ); - this.stylesConfigWidgetMap = getWidgetStylesConfigMap(entry, { + + this.stylesMapper = new StylesConfigMapBuilder(entry, { namer: this.namer, rename_tag: false, }); @@ -73,7 +75,7 @@ export class ReactNativeInlineStyleBuilder { private stylesConfig( id: string ): JSXWithStyleElementConfig | JSXWithoutStyleElementConfig { - return this.stylesConfigWidgetMap.get(id); + return this.stylesMapper.map.get(id); } private jsxBuilder(widget: JsxWidget) { diff --git a/packages/builder-react-native/rn-build-styled-component-widget/rn-styled-components-module-builder.ts b/packages/builder-react-native/rn-build-styled-component-widget/rn-styled-components-module-builder.ts index a95de471..1d8d3436 100644 --- a/packages/builder-react-native/rn-build-styled-component-widget/rn-styled-components-module-builder.ts +++ b/packages/builder-react-native/rn-build-styled-component-widget/rn-styled-components-module-builder.ts @@ -9,11 +9,7 @@ import { styled_components_imports, } from "@web-builder/react-core"; import { JsxWidget } from "@web-builder/core"; -import { - buildJsx, - getWidgetStylesConfigMap, - WidgetStyleConfigMap, -} from "@web-builder/core/builders"; +import { buildJsx, StylesConfigMapBuilder } from "@web-builder/core/builders"; import { react as react_config, reactnative as rn_config, @@ -28,7 +24,7 @@ import { export class ReactNativeStyledComponentsModuleBuilder { private readonly entry: JsxWidget; private readonly widgetName: string; - private readonly styledConfigWidgetMap: WidgetStyleConfigMap; + private readonly stylesMapper: StylesConfigMapBuilder; private readonly namer: ScopedVariableNamer; readonly config: rn_config.ReactNativeStyledComponentsConfig; @@ -45,17 +41,21 @@ export class ReactNativeStyledComponentsModuleBuilder { entry.key.id, ReservedKeywordPlatformPresets.react ); - this.styledConfigWidgetMap = getWidgetStylesConfigMap(entry, { + + StylesConfigMapBuilder; + + this.stylesMapper = new StylesConfigMapBuilder(entry, { namer: this.namer, rename_tag: true /** styled component tag shoule be renamed */, }); + this.config = config; } private styledConfig( id: string ): StyledComponentJSXElementConfig | NoStyleJSXElementConfig { - return this.styledConfigWidgetMap.get(id); + return this.stylesMapper.map.get(id); } private jsxBuilder(widget: JsxWidget) { @@ -101,11 +101,10 @@ export class ReactNativeStyledComponentsModuleBuilder { } partDeclarations() { - return Array.from(this.styledConfigWidgetMap.keys()) + return Array.from(this.stylesMapper.map.keys()) .map((k) => { - return ( - this.styledConfigWidgetMap.get(k) as StyledComponentJSXElementConfig - ).styledComponent; + return (this.stylesMapper.map.get(k) as StyledComponentJSXElementConfig) + .styledComponent; }) .filter((s) => s); } diff --git a/packages/builder-react-native/rn-build-stylesheet-widget/rn-style-sheet-module-builder.ts b/packages/builder-react-native/rn-build-stylesheet-widget/rn-style-sheet-module-builder.ts index 8ef4d621..24d478b6 100644 --- a/packages/builder-react-native/rn-build-stylesheet-widget/rn-style-sheet-module-builder.ts +++ b/packages/builder-react-native/rn-build-stylesheet-widget/rn-style-sheet-module-builder.ts @@ -18,10 +18,9 @@ import { import { JsxWidget } from "@web-builder/core"; import { buildJsx, - getWidgetStylesConfigMap, + StylesConfigMapBuilder, JSXWithoutStyleElementConfig, JSXWithStyleElementConfig, - WidgetStyleConfigMap, } from "@web-builder/core/builders"; import { react as react_config, @@ -33,7 +32,7 @@ import { StyleSheetDeclaration } from "../rn-style-sheet"; export class ReactNativeStyleSheetModuleBuilder { private readonly entry: JsxWidget; private readonly widgetName: string; - private readonly stylesConfigWidgetMap: WidgetStyleConfigMap; + private readonly stylesMapper: StylesConfigMapBuilder; private readonly namer: ScopedVariableNamer; readonly config: rn_config.ReactNativeStyleSheetConfig; @@ -50,17 +49,19 @@ export class ReactNativeStyleSheetModuleBuilder { entry.key.id, ReservedKeywordPlatformPresets.react ); - this.stylesConfigWidgetMap = getWidgetStylesConfigMap(entry, { + + this.stylesMapper = new StylesConfigMapBuilder(entry, { namer: this.namer, rename_tag: false /** rn StyleSheet tag shoule not be renamed */, }); + this.config = config; } private stylesConfig( id: string ): JSXWithStyleElementConfig | JSXWithoutStyleElementConfig { - return this.stylesConfigWidgetMap.get(id); + return this.stylesMapper.map.get(id); } private jsxBuilder(widget: JsxWidget) { @@ -136,16 +137,13 @@ export class ReactNativeStyleSheetModuleBuilder { } partStyleSheetDeclaration(): StyleSheetDeclaration { - const styles = Array.from(this.stylesConfigWidgetMap.keys()).reduce( - (p, c) => { - const cfg = this.stylesConfig(c); - return { - ...p, - [cfg.id]: "style" in cfg && cfg.style, - }; - }, - {} - ); + const styles = Array.from(this.stylesMapper.map.keys()).reduce((p, c) => { + const cfg = this.stylesConfig(c); + return { + ...p, + [cfg.id]: "style" in cfg && cfg.style, + }; + }, {}); return new StyleSheetDeclaration("styles", { styles: styles, diff --git a/packages/builder-web-core/builders/build-style-map.ts b/packages/builder-web-core/builders/build-style-map.ts index 6396a5df..08c96645 100644 --- a/packages/builder-web-core/builders/build-style-map.ts +++ b/packages/builder-web-core/builders/build-style-map.ts @@ -2,6 +2,7 @@ import { CSSProperties } from "@coli.codes/css"; import { WidgetKeyId, StylableJsxWidget, JsxWidget } from "@web-builder/core"; import { JSXAttributes, JSXIdentifier, ScopedVariableNamer } from "coli"; import { buildStyledComponentConfig } from "@web-builder/styled"; +import assert from "assert"; export interface JSXWithStyleElementConfig { id: string; @@ -21,43 +22,50 @@ export type WidgetStyleConfigMap = Map< JSXWithStyleElementConfig | JSXWithoutStyleElementConfig >; -export function getWidgetStylesConfigMap( - rootWidget: JsxWidget, - preferences: { - namer: ScopedVariableNamer; - rename_tag: boolean; +interface StylesConfigMapBuilderPreference { + namer: ScopedVariableNamer; + rename_tag: boolean; +} + +export class StylesConfigMapBuilder { + readonly root: JsxWidget; + readonly preferences: StylesConfigMapBuilderPreference; + private readonly _map: WidgetStyleConfigMap = new Map(); + // + constructor(root: JsxWidget, preferences: StylesConfigMapBuilderPreference) { + this.root = root; + this.preferences = preferences; + + this._mapper(this.root); } -): WidgetStyleConfigMap { - const styledConfigWidgetMap: WidgetStyleConfigMap = new Map(); - function mapper(widget: JsxWidget) { - if (!widget) { - throw `cannot map trough ${widget}`; - } + private _mapper(widget: JsxWidget) { + assert(widget, "widget is required for building style config map"); + if (widget.jsxConfig().type === "static-tree") { return; } - const isRoot = widget.key.id == rootWidget.key.id; + const isRoot = widget.key.id == this.root.key.id; const id = widget.key.id; if (widget instanceof StylableJsxWidget) { const styledConfig = buildStyledComponentConfig(widget, { transformRootName: true, - namer: preferences.namer, - rename_tag: preferences.rename_tag, + namer: this.preferences.namer, + rename_tag: this.preferences.rename_tag, context: { root: isRoot, }, }); - styledConfigWidgetMap.set(id, styledConfig); + this._map.set(id, styledConfig); } widget.children?.map((childwidget) => { - mapper(childwidget); + this._mapper(childwidget); }); } - mapper(rootWidget); - - return styledConfigWidgetMap; + public get map(): WidgetStyleConfigMap { + return this._map; + } } diff --git a/packages/builder-web-core/widget-core/widget-with-style.ts b/packages/builder-web-core/widget-core/widget-with-style.ts index b3ff95a4..16d8171b 100644 --- a/packages/builder-web-core/widget-core/widget-with-style.ts +++ b/packages/builder-web-core/widget-core/widget-with-style.ts @@ -1,5 +1,5 @@ import { JsxWidget, IMultiChildJsxWidget, JSXElementConfig } from "."; -import { CSSProperties } from "@coli.codes/css"; +import { ElementCssProperties, ElementCssStyleData } from "@coli.codes/css"; import { Color, DimensionLength, @@ -15,13 +15,13 @@ import { WidgetKey } from "../widget-key"; import { positionAbsolute } from "@web-builder/styles"; export interface IWidgetWithStyle { - styleData(): CSSProperties; + styleData(): ElementCssStyleData; } /** * Since html based framework's widget can be represented withou any style definition, this WidgetWithStyle class indicates, that the sub instance of this class will contain style data within it. */ -export abstract class WidgetWithStyle +export abstract class WidgetWithStyle extends JsxWidget implements IWHStyleWidget, @@ -81,8 +81,8 @@ export abstract class WidgetWithStyle abstract jsxConfig(): JSXElementConfig; - private extendedStyle: CSSProperties = {}; - extendStyle(style: T) { + private extendedStyle: ElementCssProperties = {}; + extendStyle(style: T) { this.extendedStyle = { ...this.extendedStyle, ...style, @@ -102,7 +102,7 @@ export abstract class MultiChildWidgetWithStyle constructor({ key }: { key: WidgetKey }) { super({ key: key }); } - abstract styleData(): CSSProperties; + abstract styleData(): ElementCssStyleData; abstract jsxConfig(): JSXElementConfig; } diff --git a/packages/builder-web-core/widgets-native/flex/index.ts b/packages/builder-web-core/widgets-native/flex/index.ts index cb37a797..b21d1987 100644 --- a/packages/builder-web-core/widgets-native/flex/index.ts +++ b/packages/builder-web-core/widgets-native/flex/index.ts @@ -124,13 +124,15 @@ export class Flex extends MultiChildWidget implements CssMinHeightMixin { ...css.justifyContent(this.mainAxisAlignment), "flex-direction": direction(this.direction), "align-items": flex_align_items(this.crossAxisAlignment), - flex: this.flex, + flex: this.flex > 0 ? this.flex : undefined, "flex-wrap": this.flexWrap, gap: // if justify-content is set to space-between, do not set the gap. this.mainAxisAlignment == MainAxisAlignment.spaceBetween ? undefined - : this.itemSpacing && css.px(this.itemSpacing), + : this.itemSpacing > 0 + ? css.px(this.itemSpacing) + : undefined, "box-shadow": css.boxshadow(...(this.boxShadow ?? [])), ...css.border(this.border), ...css.borderRadius(this.borderRadius), diff --git a/packages/builder-web-core/widgets-native/html-button/index.ts b/packages/builder-web-core/widgets-native/html-button/index.ts index d55d7f81..26e69324 100644 --- a/packages/builder-web-core/widgets-native/html-button/index.ts +++ b/packages/builder-web-core/widgets-native/html-button/index.ts @@ -1,29 +1,196 @@ -import { JSXElementConfig, WidgetKey } from "@web-builder/core"; -import { JSX, JSXAttribute, Snippet } from "coli"; -import { ReactWidget } from ".."; +import type { CSSProperties, ElementCssStyleData } from "@coli.codes/css"; +import type { JSXElementConfig, StylableJsxWidget } from "@web-builder/core"; +import type { + IButtonStyleButton, + IButtonStyleButtonProps, + ITextStyle, + ButtonStyle, + IWHStyleWidget, + Widget, +} from "@reflect-ui/core"; +import { Text } from "@reflect-ui/core"; +import { Container } from ".."; +import { WidgetKey } from "../../widget-key"; +import { JSX } from "coli"; +import * as css from "@web-builder/styles"; -export class Button extends ReactWidget { - constructor({ key }: { key: WidgetKey }) { - super({ key }); - } +/** + * Html5 Button Will have built in support for... + * + * + * - onClick callback + * - hover styles + * - focus styles + * - disabled styles + * - active styles + * + * + * Learn more at + * - [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button) + * - [html spec](https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element) + * + */ +export class HtmlButton extends Container implements IButtonStyleButton { + _type = "button"; + + /** + * The name of the button, submitted as a pair with the button’s value as part of the form data, when that button is used to submit the form. + */ + name?: string; + + /** + * The default behavior of the button. Possible values are: + * - `submit`: The button submits the form data to the server. This is the default if the attribute is not specified for buttons associated with a
, or if the attribute is an empty or invalid value. + * - `reset`: The button resets all the controls to their initial values, like . (This behavior tends to annoy users.) + * - `button`: The button has no default behavior, and does nothing when pressed by default. It can have client-side scripts listen to the element's events, which are triggered when the events occur. + */ + type: "submit" | "reset" | "button" = "button"; + + // #region @ButtonStyleButton + autofocus?: boolean; + style: ButtonStyle; + /** + * This Boolean attribute prevents the user from interacting with the button: it cannot be pressed or focused. + */ + disabled?: boolean; + // TextManifest + child: Widget; + // #endregion @ButtonStyleButton + + constructor({ + key, + name, + autofocus, + disabled, + style, + child, + ...rest + }: { key: WidgetKey } & { + name?: string; + } & IButtonStyleButtonProps & + IWHStyleWidget) { + super({ key, ...rest }); + + // set button properties + this.name = name; + + this.autofocus = autofocus; + this.disabled = disabled; + this.style = style; + this.child = child; - children = [ // - ]; + this.children = this.makechildren(); + } + + makechildren() { + if (this.child instanceof Text) { + return [ + { + key: new WidgetKey(`${this.key.id}.text`, "text"), + styleData: () => null, + jsxConfig: () => { + return { + type: "static-tree", + tree: JSX.text((this.child as Text).data as string), + }; + }, + }, + ]; + } + + return []; + } + + styleData(): ElementCssStyleData { + const containerstyle = super.styleData(); - styleData() { - return {}; + // wip + return { + // general layouts, continer --------------------- + ...containerstyle, + // ----------------------------------------------- + + // padding + ...css.padding(this.style.padding?.default), + "box-sizing": (this.padding && "border-box") || undefined, + + // background + "background-color": this.style.backgroundColor + ? css.color(this.style.backgroundColor.default) + : undefined, + + // text styles -------------------------------------------- + color: css.color((this.style.textStyle.default as ITextStyle)?.color), + // "text-overflow": this.overflow, + "font-size": css.px(this.style.textStyle.default.fontSize), + "font-family": css.fontFamily(this.style.textStyle.default.fontFamily), + "font-weight": css.convertToCssFontWeight( + this.style.textStyle.default.fontWeight + ), + // "word-spacing": this.style.wordSpacing, + "letter-spacing": css.letterSpacing( + this.style.textStyle.default.letterSpacing + ), + "line-height": css.length(this.style.textStyle.default.lineHeight), + // "text-align": this.textAlign, + "text-decoration": css.textDecoration( + this.style.textStyle.default.decoration + ), + "text-shadow": css.textShadow(this.style.textStyle.default.textShadow), + "text-transform": css.textTransform( + this.style.textStyle.default.textTransform + ), + // text styles -------------------------------------------- + + // + width: undefined, // clear fixed width + "min-width": css.length(this.minWidth), + "min-height": css.length(this.minHeight), + + border: containerstyle["border"] ?? "none", + outline: containerstyle["outline"] ?? "none", + + // button cursor + cursor: "pointer", + + ":hover": _button_hover_style, + ":disabled": _button_disabled_style, + ":active": _button_active_style, + ":focus": _button_focus_style, + }; } + // @ts-ignore jsxConfig() { return { tag: JSX.identifier("button"), attributes: [ - new JSXAttribute( - "onClick", - Snippet.fromStatic("() => { alert(`button click`) }") - ), + // wip + // TODO: this only works for React. (wont' work for vanilla html) + // new JSXAttribute( + // "onClick", + // JSX.exp( + // // Snippet.fromStatic() + // // TODO: type check + // "() => { /* add onclick callback */ }" as any + // ) + // ), ], }; } } + +const _button_hover_style: CSSProperties = { + opacity: 0.8, +}; + +const _button_disabled_style: CSSProperties = { + opacity: 0.5, +}; + +const _button_active_style: CSSProperties = { + opacity: 1, +}; + +const _button_focus_style: CSSProperties = {}; diff --git a/packages/builder-web-core/widgets-native/html-image/index.ts b/packages/builder-web-core/widgets-native/html-image/index.ts index 38dc6d52..a22d83de 100644 --- a/packages/builder-web-core/widgets-native/html-image/index.ts +++ b/packages/builder-web-core/widgets-native/html-image/index.ts @@ -3,39 +3,64 @@ import assert from "assert"; import { JSX, JSXAttribute, StringLiteral } from "coli"; import { StylableJSXElementConfig, WidgetKey, k } from "../.."; import { SelfClosingContainer } from "../container"; +import { + BoxFit, + ImageRepeat, + cgr, + Alignment, + ImageManifest, +} from "@reflect-ui/core"; import * as css from "@web-builder/styles"; -export class ImageElement extends SelfClosingContainer { + +type HtmlImageElementProps = Omit & { + alt?: string; +}; + +export class ImageElement + extends SelfClosingContainer + implements HtmlImageElementProps +{ _type = "img"; + readonly src: string; - readonly alt: string; - width: number; - height: number; + readonly width: number; + readonly height: number; + readonly alignment?: Alignment; + readonly centerSlice?: cgr.Rect; + readonly fit?: BoxFit; + readonly repeat?: ImageRepeat; + alt?: string; constructor({ key, src, - alt, width, height, + alignment, + centerSlice, + fit, + repeat, + alt, }: { key: WidgetKey; - src: string; - alt?: string; - width?: number; - height?: number; - }) { + } & HtmlImageElementProps) { super({ key }); assert(src !== undefined, "ImageElement requires src"); + this.src = src; - this.alt = alt; this.width = width; this.height = height; + this.alignment = alignment; + this.centerSlice = centerSlice; + this.fit = fit; + this.repeat = repeat; + this.alt = alt; } styleData() { return { ...super.styleData(), - "object-fit": "cover", + "object-fit": object_fit(this.fit) ?? "cover", width: css.px(this.width), height: css.px(this.height), // "max-width": "100%", @@ -62,3 +87,25 @@ export class ImageElement extends SelfClosingContainer { }; } } + +function object_fit(fit?: BoxFit) { + switch (fit) { + case BoxFit.fill: + return "fill"; + case BoxFit.contain: + return "contain"; + case BoxFit.cover: + return "cover"; + case BoxFit.none: + return "none"; + case BoxFit.scaleDown: + return "scale-down"; + case BoxFit.fitHeight: + case BoxFit.fitWidth: + // TODO: + return; + case undefined: + default: + return; + } +} diff --git a/packages/builder-web-core/widgets-native/html-input/html-input-range.ts b/packages/builder-web-core/widgets-native/html-input/html-input-range.ts new file mode 100644 index 00000000..b6890f9a --- /dev/null +++ b/packages/builder-web-core/widgets-native/html-input/html-input-range.ts @@ -0,0 +1,234 @@ +import type { CSSProperties, ElementCssStyleData } from "@coli.codes/css"; +import type { + Color, + ISliderManifest, + IWHStyleWidget, + SystemMouseCursors, +} from "@reflect-ui/core"; +import type { StylableJSXElementConfig } from "../../widget-core"; +import { WidgetKey } from "../../widget-key"; +import { Container } from "../container"; +import { JSX, JSXAttribute, NumericLiteral, StringLiteral } from "coli"; +import * as css from "@web-builder/styles"; +import { RoundSliderThumbShape } from "@reflect-ui/core/lib/slider.thumb"; + +/** + * A jsx attibute to indicate input type as range + */ +const attr_type_range = new JSXAttribute("type", new StringLiteral("range")); + +/** + * A html native input as a slider wit type="slider" + */ +export class HtmlInputRange extends Container implements ISliderManifest { + _type = "input/range"; + + // #region ISliderManifest + activeColor?: Color; + autoFocus: boolean; + divisions: number; + inactiveColor?: Color; + max: number; + min: number; + mouseCursor?: SystemMouseCursors; + thumbColor?: Color; + thumbShape?: RoundSliderThumbShape; + initialValue: number; + // #endregion ISliderManifest + + constructor({ + key, + + activeColor, + autoFocus, + divisions, + inactiveColor, + max, + min, + mouseCursor, + thumbColor, + thumbShape, + initialValue, + + ...rest + }: { + key: WidgetKey; + } & ISliderManifest & + IWHStyleWidget) { + super({ key, ...rest }); + + this.activeColor = activeColor; + this.autoFocus = autoFocus; + this.divisions = divisions; + this.inactiveColor = inactiveColor; + this.max = max; + this.min = min; + this.mouseCursor = mouseCursor; + this.thumbColor = thumbColor; + this.thumbShape = thumbShape; + this.initialValue = initialValue; + } + + styleData(): ElementCssStyleData { + const containerstyle = super.styleData(); + + // TODO: add styling + return { + // general layouts, continer --------------------- + ...containerstyle, + // ------------------------------------------------- + + /* Override default CSS styles */ + "-webkit-appearance": "none", + appearance: "none", + /* --------------------------- */ + + // general slider styles + // "background-color": , + ...css.background(this.background), + color: css.color(this.activeColor), + // outline: "none", + + opacity: 0.7, + /* 0.2 seconds transition on hover */ + "-webkit-transition": ".2s", + transition: "opacity .2s", + + // ---------------------- + + // thumb (knob) -------------------------------- + "::-webkit-slider-thumb": this.thumbShape + ? thumbstyle({ + ...this.thumbShape, + color: this.thumbColor, + platform: "webkit", + }) + : undefined, + "::-moz-range-thumb": this.thumbShape + ? thumbstyle({ + ...this.thumbShape, + color: this.thumbColor, + platform: "moz", + }) + : undefined, + // only works on firefox (also consider using "::-webkit-slider-runnable-track") + "::-moz-range-progress": { + "background-color": css.color(this.activeColor), + height: "100%", + }, + // --------------------------------------------- + + ":hover": { + /* Fully shown on mouse-over */ + opacity: 1, + }, + }; + } + + jsxConfig(): StylableJSXElementConfig { + const attrs = [ + attr_type_range, + this.autoFocus && new JSXAttribute("autofocus", new StringLiteral("on")), + // TODO: below attributes + // this.disabled && new JSXAttribute("disabled"), + // this.readOnly && new JSXAttribute("readonly"), + this.initialValue && + new JSXAttribute( + "value", + new StringLiteral(this.initialValue.toString()) + ), + this.divisions && + new JSXAttribute("step", new StringLiteral(this.divisions.toString())), + this.min && + new JSXAttribute("min", new StringLiteral(this.min.toString())), + this.max && + new JSXAttribute("max", new StringLiteral(this.max.toString())), + ].filter(Boolean); + + return { + type: "tag-and-attr", + tag: JSX.identifier("input"), + attributes: attrs, + }; + } + + get finalStyle() { + const superstyl = super.finalStyle; + + // width override. ------------------------------------------------------------------------------------------ + // input element's width needs to be specified if the position is absolute and the left & right is specified. + let width = superstyl.width; + if ( + width === undefined && + superstyl.position === "absolute" && + superstyl.left !== undefined && + superstyl.right !== undefined + ) { + width = "calc(100% - " + superstyl.left + " - " + superstyl.right + ")"; + } + // ---------------------------------------------------------------------------------------------------------- + + return { + ...superstyl, + width, + }; + } +} + +export class HtmlSlider extends HtmlInputRange {} + +function thumbstyle({ + enabledThumbRadius, + color, + platform, +}: RoundSliderThumbShape & { + platform: "moz" | "webkit"; +} & { + color: Color; +}): CSSProperties { + const base: CSSProperties = { + width: css.px(enabledThumbRadius), + height: css.px(enabledThumbRadius), + "border-radius": "50%", + "background-color": css.color(color), + // ..._aberation_support_progress_fill_dirty({ color: activeColor }), + cursor: "pointer" /* Cursor on hover */, + }; + switch (platform) { + case "moz": { + // no overrides + return base; + } + case "webkit": { + return { + "-webkit-appearance": "none" /* Override default look */, + appearance: "none", + ...base, + }; + } + } + + return base; +} + +/** + * There are no proper way to handle the progress color in Chome, Safari. + * Moz supports the `-moz-range-progress`, `-moz-range-track`, but this is [not standard](https://developer.mozilla.org/en-US/docs/Web/CSS/::-moz-range-progress) + * + * We use this trick instead. + * https://codepen.io/okayoon/pen/PMpmjp + * + * this only works with overflow: hidden, so we cannot show overflowing round thumb. + * + * we can also do it by using linear-gradient, [but this can't be done only by using css](https://codepen.io/duplich/pen/qjYQEZ). + * + * @returns + */ +function _aberation_support_progress_fill_dirty({ color }: { color: Color }) { + if (color === undefined) { + return; + } + return { + "box-shadow": `-100vw 0 0 100vw ${css.color(color)}`, + }; +} diff --git a/packages/builder-web-core/widgets-native/html-input/html-input-text.ts b/packages/builder-web-core/widgets-native/html-input/html-input-text.ts index ad1763ff..011734eb 100644 --- a/packages/builder-web-core/widgets-native/html-input/html-input-text.ts +++ b/packages/builder-web-core/widgets-native/html-input/html-input-text.ts @@ -1,4 +1,261 @@ +import { CSSProperties, ElementCssStyleData } from "@coli.codes/css"; +import { StylableJSXElementConfig, WidgetKey } from "../.."; +import * as css from "@web-builder/styles"; +import { JSX, JSXAttribute, StringLiteral } from "coli"; +import { Container, UnstylableJSXElementConfig } from ".."; +import { + Color, + EdgeInsets, + ITextFieldManifest, + IWHStyleWidget, + TextAlign, + TextAlignVertical, + TextFieldDecoration, + TextStyle, +} from "@reflect-ui/core"; +import { SystemMouseCursors } from "@reflect-ui/core/lib/mouse-cursor"; +import { TextInputType } from "@reflect-ui/core/lib/text-input-type"; + /** - * @deprecated - not ready + * A Html input element dedicated to text related inputs. */ -export class InputText {} +export class HtmlInputText extends Container implements ITextFieldManifest { + _type = "input/text"; + + // the input element can not contain children + + decoration?: TextFieldDecoration; + autocorrect?: boolean; + autofocus?: boolean; + autofillHints?: string[]; + cursorColor?: Color; + cursorHeight?: number; + cursorRadius?: number; + cursorWidth?: number; + disabled?: boolean; + enableSuggestions?: boolean; + keyboardAppearance?: "light" | "dark"; + keyboardType?: TextInputType; + maxLines?: number; + minLines?: number; + mouseCursor?: SystemMouseCursors; + obscureText?: boolean; + obscuringCharacter?: string; + readOnly?: boolean; + restorationId?: string; + scrollPadding?: EdgeInsets; + showCursor?: boolean; + style: TextStyle; + textAlign?: TextAlign; + textAlignVertical?: TextAlignVertical; + initialValue?: string; + + constructor({ + key, + + decoration, + autocorrect, + autofocus, + autofillHints, + cursorColor, + cursorHeight, + cursorRadius, + cursorWidth, + disabled, + enableSuggestions, + keyboardAppearance, + keyboardType, + maxLines, + minLines, + mouseCursor, + obscureText, + obscuringCharacter, + readOnly, + restorationId, + scrollPadding, + showCursor, + style, + textAlign, + textAlignVertical, + initialValue, + + ...rest + }: { key: WidgetKey } & ITextFieldManifest & IWHStyleWidget) { + super({ key, ...rest }); + + this.decoration = decoration; + this.autocorrect = autocorrect; + this.autofocus = autofocus; + this.autofillHints = autofillHints; + this.cursorColor = cursorColor; + this.cursorHeight = cursorHeight; + this.cursorRadius = cursorRadius; + this.cursorWidth = cursorWidth; + this.disabled = disabled; + this.enableSuggestions = enableSuggestions; + this.keyboardAppearance = keyboardAppearance; + this.keyboardType = keyboardType; + this.maxLines = maxLines; + this.minLines = minLines; + this.mouseCursor = mouseCursor; + this.obscureText = obscureText; + this.obscuringCharacter = obscuringCharacter; + this.readOnly = readOnly; + this.restorationId = restorationId; + this.scrollPadding = scrollPadding; + this.showCursor = showCursor; + this.style = style; + this.textAlign = textAlign; + this.textAlignVertical = textAlignVertical; + this.initialValue = initialValue; + + // overrides + this.padding = this.decoration.contentPadding; + } + + styleData(): ElementCssStyleData { + const containerstyle = super.styleData(); + + // TODO: + // - support placeholder text color styling + + return { + // general layouts, continer --------------------- + ...containerstyle, + // ------------------------------------------------- + + // padding + ...css.padding(this.decoration.contentPadding), + "box-sizing": (this.padding && "border-box") || undefined, + + // border + border: + this.decoration.border.borderSide && + css.borderSide(this.decoration.border?.borderSide), + ...(("borderRadius" in this.decoration.border && + css.borderRadius(this.decoration.border["borderRadius"])) ?? + {}), + // background + "background-color": this.decoration.filled + ? css.color(this.decoration.fillColor) + : undefined, + + // text styles -------------------------------------------- + color: css.color(this.style.color), + // "text-overflow": this.overflow, + "font-size": css.px(this.style.fontSize), + "font-family": css.fontFamily(this.style.fontFamily), + "font-weight": css.convertToCssFontWeight(this.style.fontWeight), + // "word-spacing": this.style.wordSpacing, + "letter-spacing": css.letterSpacing(this.style.letterSpacing), + "line-height": css.length(this.style.lineHeight), + "text-align": this.textAlign, + "text-decoration": css.textDecoration(this.style.decoration), + "text-shadow": css.textShadow(this.style.textShadow), + "text-transform": css.textTransform(this.style.textTransform), + // text styles -------------------------------------------- + + ...(this.decoration?.placeholderStyle + ? { + "::placeholder": { + // TODO: optmiize - assign only diffferent properties values + // TODO: not all properties are assigned + color: css.color(this.decoration.placeholderStyle.color), + "font-size": css.px(this.style.fontSize), + "font-family": css.fontFamily(this.style.fontFamily), + "font-weight": css.convertToCssFontWeight(this.style.fontWeight), + }, + } + : {}), + }; + } + + // @ts-ignore + jsxConfig(): StylableJSXElementConfig | UnstylableJSXElementConfig { + const attrs = [ + new JSXAttribute( + "type", + new StringLiteral(inputtype(this.keyboardType, this.obscureText)) + ), + this.autofocus && new JSXAttribute("autofocus", new StringLiteral("on")), + this.autofillHints?.length >= 1 && + new JSXAttribute( + "autocomplete", + new StringLiteral(this.autofillHints.join(" ")) + ), + this.disabled && new JSXAttribute("disabled"), + this.initialValue && + new JSXAttribute("value", new StringLiteral(this.initialValue)), + this.decoration.placeholderText && + new JSXAttribute( + "placeholder", + new StringLiteral(this.decoration.placeholderText) + ), + this.readOnly && new JSXAttribute("readonly"), + ].filter(Boolean); + + return { + type: "tag-and-attr", + tag: JSX.identifier("input"), + attributes: attrs, + }; + } + + get finalStyle() { + const superstyl = super.finalStyle; + + // width override. ------------------------------------------------------------------------------------------ + // input element's width needs to be specified if the position is absolute and the left & right is specified. + let width = superstyl.width; + if ( + width === undefined && + superstyl.position === "absolute" && + superstyl.left !== undefined && + superstyl.right !== undefined + ) { + width = "calc(100% - " + superstyl.left + " - " + superstyl.right + ")"; + } + // ---------------------------------------------------------------------------------------------------------- + + return { + ...superstyl, + width, + }; + } +} + +/** + * Text input with additional state styles + */ +export class HtmlTextField extends HtmlInputText {} + +const inputtype = (t: TextInputType, isPassword?: boolean) => { + if (isPassword) { + return "password"; + } + + switch (t) { + case TextInputType.datetime: + return "datetime-local"; + case TextInputType.emailAddress: + return "email"; + case TextInputType.none: + return; + case TextInputType.number: + return "number"; + case TextInputType.phone: + return "tel"; + case TextInputType.url: + return "url"; + case TextInputType.visiblePassword: + return "password"; + // case TextInputType.search: + // return "search"; + case TextInputType.text: + case TextInputType.name: + case TextInputType.streetAddress: + case TextInputType.multiline: + default: + return "text"; + } +}; diff --git a/packages/builder-web-core/widgets-native/html-input/index.ts b/packages/builder-web-core/widgets-native/html-input/index.ts index e69de29b..2413a60f 100644 --- a/packages/builder-web-core/widgets-native/html-input/index.ts +++ b/packages/builder-web-core/widgets-native/html-input/index.ts @@ -0,0 +1,2 @@ +export { HtmlTextField as TextInput } from "./html-input-text"; +export { HtmlSlider as Slider } from "./html-input-range"; diff --git a/packages/builder-web-core/widgets-native/html-text-element/index.ts b/packages/builder-web-core/widgets-native/html-text-element/index.ts index 384295d8..366eac0b 100644 --- a/packages/builder-web-core/widgets-native/html-text-element/index.ts +++ b/packages/builder-web-core/widgets-native/html-text-element/index.ts @@ -73,7 +73,7 @@ export class Text extends TextChildWidget { "text-align": this.textAlign, "text-decoration": css.textDecoration(this.textStyle.decoration), "text-shadow": css.textShadow(this.textStyle.textShadow), - "text-transform": this.textStyle.textTransform, + "text-transform": css.textTransform(this.textStyle.textTransform), // ------------------------------------------ ...textWH({ width: this.width, height: this.height }), }; diff --git a/packages/builder-web-core/widgets-native/index.ts b/packages/builder-web-core/widgets-native/index.ts index 428e10d6..dfe67b16 100644 --- a/packages/builder-web-core/widgets-native/index.ts +++ b/packages/builder-web-core/widgets-native/index.ts @@ -6,6 +6,8 @@ export * from "./stack"; export * from "./html-text-element"; export * from "./html-svg"; export * from "./html-image"; +export * from "./html-button"; +export * from "./html-input"; export * from "./error-widget"; export * from "@web-builder/core/widget-tree/widget"; diff --git a/packages/builder-web-react/react-css-module-widget/react-css-module-module-builder.ts b/packages/builder-web-react/react-css-module-widget/react-css-module-module-builder.ts index e2f7dbde..9e796c61 100644 --- a/packages/builder-web-react/react-css-module-widget/react-css-module-module-builder.ts +++ b/packages/builder-web-react/react-css-module-widget/react-css-module-module-builder.ts @@ -18,7 +18,7 @@ import { import { JsxWidget } from "@web-builder/core"; import { buildJsx, - getWidgetStylesConfigMap, + StylesConfigMapBuilder, JSXWithoutStyleElementConfig, JSXWithStyleElementConfig, WidgetStyleConfigMap, @@ -34,7 +34,7 @@ import { react as react_config } from "@designto/config"; export class ReactCssModuleBuilder { private readonly entry: JsxWidget; private readonly widgetName: string; - private readonly stylesConfigWidgetMap: WidgetStyleConfigMap; + private readonly stylesMapper: StylesConfigMapBuilder; private readonly namer: ScopedVariableNamer; readonly config: react_config.ReactCssModuleConfig; @@ -51,17 +51,18 @@ export class ReactCssModuleBuilder { entry.key.id, ReservedKeywordPlatformPresets.react ); - this.stylesConfigWidgetMap = getWidgetStylesConfigMap(entry, { + this.stylesMapper = new StylesConfigMapBuilder(entry, { namer: this.namer, rename_tag: false /** css-module tag shoule not be renamed */, }); + this.config = config; } private stylesConfig( id: string ): JSXWithStyleElementConfig | JSXWithoutStyleElementConfig { - return this.stylesConfigWidgetMap.get(id); + return this.stylesMapper.map.get(id); } private jsxBuilder(widget: JsxWidget) { diff --git a/packages/builder-web-react/react-inline-css-widget/react-inline-css-module-builder.ts b/packages/builder-web-react/react-inline-css-widget/react-inline-css-module-builder.ts index 84f8ef53..d28518ad 100644 --- a/packages/builder-web-react/react-inline-css-widget/react-inline-css-module-builder.ts +++ b/packages/builder-web-react/react-inline-css-widget/react-inline-css-module-builder.ts @@ -8,7 +8,7 @@ import { } from "@web-builder/react-core"; import { buildJsx, - getWidgetStylesConfigMap, + StylesConfigMapBuilder, JSXWithoutStyleElementConfig, JSXWithStyleElementConfig, WidgetStyleConfigMap, @@ -44,7 +44,7 @@ export class ReactInlineCssBuilder { private readonly widgetName: string; readonly config: react_config.ReactInlineCssConfig; private readonly namer: ScopedVariableNamer; - private readonly stylesConfigWidgetMap: WidgetStyleConfigMap; + private readonly stylesMapper: StylesConfigMapBuilder; constructor({ entry, @@ -60,7 +60,8 @@ export class ReactInlineCssBuilder { entry.key.id, ReservedKeywordPlatformPresets.react ); - this.stylesConfigWidgetMap = getWidgetStylesConfigMap(entry, { + + this.stylesMapper = new StylesConfigMapBuilder(entry, { namer: this.namer, rename_tag: false, }); @@ -69,7 +70,7 @@ export class ReactInlineCssBuilder { private stylesConfig( id: string ): JSXWithStyleElementConfig | JSXWithoutStyleElementConfig { - return this.stylesConfigWidgetMap.get(id); + return this.stylesMapper.map.get(id); } private jsxBuilder(widget: JsxWidget) { diff --git a/packages/builder-web-react/react-styled-component-widget/react-styled-components-module-builder.ts b/packages/builder-web-react/react-styled-component-widget/react-styled-components-module-builder.ts index 0f60454f..fdf877bd 100644 --- a/packages/builder-web-react/react-styled-component-widget/react-styled-components-module-builder.ts +++ b/packages/builder-web-react/react-styled-component-widget/react-styled-components-module-builder.ts @@ -15,7 +15,7 @@ import { BlockStatement, Import, ImportDeclaration, Return } from "coli"; import { JsxWidget } from "@web-builder/core"; import { buildJsx, - getWidgetStylesConfigMap, + StylesConfigMapBuilder, WidgetStyleConfigMap, } from "@web-builder/core/builders"; import { react as react_config } from "@designto/config"; @@ -24,7 +24,7 @@ import { StyledComponentDeclaration } from "@web-builder/styled/styled-component export class ReactStyledComponentsBuilder { private readonly entry: JsxWidget; private readonly widgetName: string; - private readonly styledConfigWidgetMap: WidgetStyleConfigMap; + private readonly stylesMapper: StylesConfigMapBuilder; private readonly namer: ScopedVariableNamer; readonly config: react_config.ReactStyledComponentsConfig; @@ -41,17 +41,19 @@ export class ReactStyledComponentsBuilder { entry.key.id, ReservedKeywordPlatformPresets.react ); - this.styledConfigWidgetMap = getWidgetStylesConfigMap(entry, { + + this.stylesMapper = new StylesConfigMapBuilder(entry, { namer: this.namer, rename_tag: true /** styled component tag shoule be renamed */, }); + this.config = config; } private styledConfig( id: string ): StyledComponentJSXElementConfig | NoStyleJSXElementConfig { - return this.styledConfigWidgetMap.get(id); + return this.stylesMapper.map.get(id); } private jsxBuilder(widget: JsxWidget) { @@ -89,11 +91,10 @@ export class ReactStyledComponentsBuilder { } partDeclarations() { - return Array.from(this.styledConfigWidgetMap.keys()) + return Array.from(this.stylesMapper.map.keys()) .map((k) => { - return ( - this.styledConfigWidgetMap.get(k) as StyledComponentJSXElementConfig - ).styledComponent; + return (this.stylesMapper.map.get(k) as StyledComponentJSXElementConfig) + .styledComponent; }) .filter((s) => s); } diff --git a/packages/builder-web-styled-components/optimization/duplicated-style-optimization.ts b/packages/builder-web-styled-components/optimization/duplicated-style-optimization.ts new file mode 100644 index 00000000..283662e0 --- /dev/null +++ b/packages/builder-web-styled-components/optimization/duplicated-style-optimization.ts @@ -0,0 +1,100 @@ +/// +/// General duplicated style optimization. +/// this is based on static rule, based on the names of the layer and styles. +/// + +import type { ElementCssStyleData } from "@coli.codes/css"; + +/// 1. based on final built style +/// 2. based on pre-build style comparison +/// - suggesting the merged style name + +interface MinimalStyleRepresenation { + name: string; + style: ElementCssStyleData; +} + +type NameMatcher = + // /** + // * provide a custom regex to compare two names + // */ + // | "custom-regex"; + /** + * match exactly + */ + | "exact" + /** + * allow matching with numbered suffix + * e.g. + * - 'Wrapper' = 'Wrapper1' + * - 'Container1' = 'Container2' + * - 'View' = 'View_001' = 'View_002' + */ + | "suffix-number" + /** + * allow matching with separator (.-_) with following numbered suffix + * e.g. + * - 'Wrapper' = 'Wrapper-1' + * - 'Container' = 'Container.2' + * - 'View' = 'View_001' = 'View_002' + */ + | "suffix-separator-number"; + +function is_matching_name(a: string, b: string, matcher: NameMatcher) { + switch (matcher) { + case "exact": + return a === b; + case "suffix-number": { + // the suffix is optional. + // yes: 'Wrapper' = 'Wrapper1' = 'Wrapper2' = 'Wrapper002' + // no: 'Wrapper' !== 'Wrapper_001' !== 'Wrapper_A' !== 'Wrapper_A' + + if (a.includes(b)) { + const suffix = a.replace(b, ""); + return /(\d+)/.test(suffix); + } else if (b.includes(a)) { + const suffix = b.replace(a, ""); + return /(\d+)/.test(suffix); + } else { + return false; + } + } + + case "suffix-separator-number": + // allowed spearator is .. '.', '-', '_' + // yes: 'Wrapper' = 'Wrapper-1' = 'Wrapper.2' = 'Wrapper_001' + // no: 'Wrapper' !== 'Wrapper1' !== 'Wrapper2' !== 'Wrapper_A' !== 'Wrapper_A' + + if (a.includes(b)) { + const suffix = a.replace(b, ""); + return /^((\-|\.|\_)?\d+)$/.test(suffix); + } else if (b.includes(a)) { + const suffix = b.replace(a, ""); + return /^((\-|\.|\_)?\d+)$/.test(suffix); + } + return false; + } +} + +/** + * returns boolean based on input's name and style data. if both matches, return true. + * @param a 1st element + * @param b 2nd element + * @param options + * @returns + */ +export function is_duplicate_by_name_and_style( + a: MinimalStyleRepresenation, + b: MinimalStyleRepresenation, + options: { + name_match: NameMatcher; + } +) { + // name should be the same + if (!is_matching_name(a.name, b.name, options.name_match)) { + return false; + } + + // style should be the same + return JSON.stringify(a.style) === JSON.stringify(b.style); +} diff --git a/packages/builder-web-styled-components/optimization/duplicated-text-style-optimization.ts b/packages/builder-web-styled-components/optimization/duplicated-text-style-optimization.ts new file mode 100644 index 00000000..b4877049 --- /dev/null +++ b/packages/builder-web-styled-components/optimization/duplicated-text-style-optimization.ts @@ -0,0 +1,4 @@ +/// +/// Text style declaration optimization +/// Clear the duplicated text styles, merge it into one. +/// diff --git a/packages/builder-web-styled-components/optimization/index.ts b/packages/builder-web-styled-components/optimization/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/builder-web-styled-components/styled-component-declaration.ts b/packages/builder-web-styled-components/styled-component-declaration.ts index 8d8f80cf..fa1b7204 100644 --- a/packages/builder-web-styled-components/styled-component-declaration.ts +++ b/packages/builder-web-styled-components/styled-component-declaration.ts @@ -13,7 +13,7 @@ import { NameCases, ScopedVariableNamer, } from "@coli.codes/naming"; -import { CSSProperties, buildCssStandard } from "@coli.codes/css"; +import { CSSProperties, buildCSSStyleData } from "@coli.codes/css"; import { handle } from "@coli.codes/builder"; import { formatStyledTempplateString } from "./styled-variable-formatter"; @@ -43,14 +43,26 @@ export class StyledComponentDeclaration extends VariableDeclaration { style: CSSProperties, html5tag: Html5IdentifierNames ): TaggedTemplateExpression { - const content = formatStyledTempplateString(buildCssStandard(style)); + const { main, pseudo } = buildCSSStyleData(style); + + const pseudos = Object.keys(pseudo).map((k) => { + const b = pseudo[k]; + const lines = b.split("\n").filter((l) => l.length > 0); + return `${k} {${ + lines.length ? ["", ...lines].join("\n\t") + "\n" : "" + }}\n`; + }); + + const body = [main, ...pseudos].join("\n"); + + const _fmted_body = formatStyledTempplateString(body); return new TaggedTemplateExpression( new PropertyAccessExpression( StyledComponentDeclaration.styledIdentifier, html5tag ), { - template: new TemplateLiteral(content), + template: new TemplateLiteral(`\n${_fmted_body}\n`), } ); } diff --git a/packages/builder-web-styled-components/styled-variable-formatter.ts b/packages/builder-web-styled-components/styled-variable-formatter.ts index 43353e96..458872f1 100644 --- a/packages/builder-web-styled-components/styled-variable-formatter.ts +++ b/packages/builder-web-styled-components/styled-variable-formatter.ts @@ -9,34 +9,39 @@ * styled.span` * color: white; * width: 24px; + * ::placeholder { + * color: red; + * } * ` * ``` * * to * ``` * styled.span` - * color: white; - * width: 24px; + * color: white; + * width: 24px; + * ::placeholder { + * color: red; + * } * ` * ``` */ -export function formatStyledTempplateString(styleString: string): string { - const lines = []; - let declarationLines = 0; - styleString.split("\n").map((l) => { - if (l.length <= 1) { - // this is not a style property line. ignore it - lines.push(l); - } else { - lines.push(`\t${l}`); - declarationLines += 1; - } - }); +export function formatStyledTempplateString(body: string): string { + // format the givven css body with indentation using regex + const formatted = body + .split("\n") + .map((l) => { + if (l.length < 1) { + // this is not a style property line. ignore it + return l; + } else { + return `\t${l}`; + } + }) + .join("\n"); - if (declarationLines == 0) { - // if no actual style declration in style string. - return ""; - } + // remove linebreaks from the beginning and end of the string, while keeping the indentation + const trimmed = formatted.replace(/^\n+/, "").replace(/\n+$/, ""); - return lines.join("\n"); + return trimmed; } diff --git a/packages/builder-web-vanilla/export-inline-css-html-file/index.ts b/packages/builder-web-vanilla/export-inline-css-html-file/index.ts index e07ae79f..5d9952a2 100644 --- a/packages/builder-web-vanilla/export-inline-css-html-file/index.ts +++ b/packages/builder-web-vanilla/export-inline-css-html-file/index.ts @@ -1,22 +1,19 @@ -import { handle } from "@coli.codes/builder"; -import { buildCssStandard, CSSProperties } from "@coli.codes/css"; -import { ReservedKeywordPlatformPresets } from "@coli.codes/naming/reserved"; import { - k, - TextChildWidget, - StylableJsxWidget, - JsxWidget, -} from "@web-builder/core"; + buildCSSBody, + buildCSSStyleData, + CSSProperties, +} from "@coli.codes/css"; +import { ReservedKeywordPlatformPresets } from "@coli.codes/naming/reserved"; +import { k, JsxWidget } from "@web-builder/core"; import { buildJsx, - getWidgetStylesConfigMap, + StylesConfigMapBuilder, JSXWithoutStyleElementConfig, JSXWithStyleElementConfig, WidgetStyleConfigMap, } from "@web-builder/core/builders"; import { JSXAttribute, - JSXClosingElement, JSXElement, JSXElementLike, JSXOpeningElement, @@ -61,11 +58,13 @@ export function export_inlined_css_html_file( ReservedKeywordPlatformPresets.html ); - const styles_map: WidgetStyleConfigMap = getWidgetStylesConfigMap(widget, { + const mapper = new StylesConfigMapBuilder(widget, { namer: styledComponentNamer, rename_tag: false, // vanilla html tag will be preserved. }); + const styles_map: WidgetStyleConfigMap = mapper.map; + function getStyleConfigById( id: string ): JSXWithStyleElementConfig | JSXWithoutStyleElementConfig { @@ -119,9 +118,23 @@ export function export_inlined_css_html_file( class: ".", tag: "", }; - const stylestring = buildCssStandard(css.style); + + const style = buildCSSStyleData(css.style); const key = selectors[css.key.selector] + css.key.name; - return `${key} {${formatCssBodyString(stylestring)}}`; + + // main + const main = `${key} {${formatCssBodyString(style.main)}}`; + + // support pseudo-selectors + const pseudos = Object.keys(style.pseudo).map((k) => { + const body = style.pseudo[k]; + const pseudo = `${key}${k} {${formatCssBodyString(body)}}`; + return pseudo; + }); + + const all = [main, ...pseudos].join("\n"); + + return all; }) .join("\n\n"); const body = buildBodyHtml(widget); diff --git a/packages/designto-flutter/tokens-to-flutter-widget/index.ts b/packages/designto-flutter/tokens-to-flutter-widget/index.ts index 7cc135f1..69558c13 100644 --- a/packages/designto-flutter/tokens-to-flutter-widget/index.ts +++ b/packages/designto-flutter/tokens-to-flutter-widget/index.ts @@ -14,6 +14,8 @@ import { handle_flutter_case_no_size_stack_children, } from "../case-handling"; import { rd } from "../_utils"; +import assert from "assert"; +import { WrappingContainer } from "@designto/token/tokens"; export function buildFlutterWidgetFromTokens( widget: core.DefaultStyleWidget @@ -33,6 +35,8 @@ function compose( widget: core.DefaultStyleWidget, context: { is_root: boolean } ) { + assert(widget, "input widget is required"); + const handleChildren = ( children: core.DefaultStyleWidget[] ): flutter.Widget[] => { @@ -55,6 +59,9 @@ function compose( ..._remove_width_height_if_root_wh, }; + // const _key = keyFromWidget(widget); + let thisFlutterWidget: flutter.Widget; + const flex_props = (f: core.Flex) => { return { mainAxisAlignment: rendering.mainAxisAlignment(f.mainAxisAlignment), @@ -63,9 +70,7 @@ function compose( verticalDirection: painting.verticalDirection(f.verticalDirection), }; }; - // const _key = keyFromWidget(widget); - let thisFlutterWidget: flutter.Widget; if (widget instanceof core.Column) { const children = compose_item_spacing_children(widget.children, { itemspacing: widget.itemSpacing, @@ -106,13 +111,15 @@ function compose( }); } else if (widget instanceof core.Flex) { // FIXME: FLEX not supported yet. - // thisFlutterWidget = new flutter.Flex({ - // // direction: widget.direction, - // // ...widget, - // // ...default_props_for_layout, - // children: handleChildren(widget.children), - // // key: _key, - // }); + thisFlutterWidget = new flutter.Flex({ + ...widget, + ...default_props_for_layout, + ...flex_props(widget), + clipBehavior: null, + direction: painting.axis(widget.direction), + children: handleChildren(widget.children), + // key: _key, + }); } else if (widget instanceof core.Stack) { const _remove_overflow_if_root_overflow = { clipBehavior: context.is_root @@ -163,11 +170,23 @@ function compose( } // ------------------------------------- } + } else if (widget instanceof core.SizedBox) { + // + } else if (widget instanceof core.OverflowBox) { + // } else if (widget instanceof core.Opacity) { thisFlutterWidget = new flutter.Opacity({ opacity: widget.opacity, child: handleChild(widget.child), }); + } else if (widget instanceof core.Blurred) { + // FIXME: blur flutter control + // const isBlurVisibile = widget.blur.visible; + // if (isBlurVisibile) { + // if (widget.blur.type === "LAYER_BLUR") { + // } else if (widget.blur.type === "BACKGROUND_BLUR") { + // } + // } } else if (widget instanceof core.Rotation) { thisFlutterWidget = flutter.Transform.rotate({ angle: widget.rotation, @@ -175,18 +194,6 @@ function compose( }); } - // FIXME: blur flutter control - // else if (widget instanceof core.Blurred) { - // const isBlurVisibile = widget.blur.visible; - // if (isBlurVisibile) { - - // if (widget.blur.type === "LAYER_BLUR") { - - // } else if (widget.blur.type === "BACKGROUND_BLUR") { - // } - // } - // } - // ----- region clip path ------ else if (widget instanceof core.ClipRRect) { // FIXME: flutter clip rrect support is not ready. @@ -253,6 +260,41 @@ function compose( } } + // #region component widgets + // button + else if (widget instanceof core.ButtonStyleButton) { + // TODO: widget.icon - not supported + // thisFlutterWidget = compose_unwrapped_button(_key, widget); + } + // textfield + else if (widget instanceof core.TextField) { + // thisFlutterWidget = compose_unwrapped_text_input(_key, widget); + } else if (widget instanceof core.Slider) { + // thisFlutterWidget = compose_unwrapped_slider(_key, widget); + } + // wrapping container + else if (widget instanceof WrappingContainer) { + // #region + // mergable widgets for web + // if (widget.child instanceof core.TextField) { + // thisFlutterWidget = compose_unwrapped_text_input( + // _key, + // widget.child, + // widget + // ); + // } else if (widget.child instanceof core.ButtonStyleButton) { + // thisFlutterWidget = compose_unwrapped_button(_key, widget.child, widget); + // } else if (widget.child instanceof core.Slider) { + // thisFlutterWidget = compose_unwrapped_slider(_key, widget.child, widget); + // } else { + // throw new Error( + // `Unsupported widget type: ${widget.child.constructor.name}` + // ); + // } + // #endregion + } + // #endregion + // execution order matters - some above widgets inherits from Container, this shall be handled at the last. else if (widget instanceof core.Container) { // flutter cannot set both shape circle & border radius. diff --git a/packages/designto-token/detection/do-have-flex.ts b/packages/designto-token/detection/do-have-flex.ts new file mode 100644 index 00000000..f07e7d19 --- /dev/null +++ b/packages/designto-token/detection/do-have-flex.ts @@ -0,0 +1,5 @@ +import type { ReflectSceneNode } from "@design-sdk/figma-node"; + +export function hasFlexible(node: ReflectSceneNode) { + return node.layoutGrow >= 1; +} diff --git a/packages/designto-token/detection/index.ts b/packages/designto-token/detection/index.ts index aaaec90b..a94e234c 100644 --- a/packages/designto-token/detection/index.ts +++ b/packages/designto-token/detection/index.ts @@ -1,3 +1,4 @@ +export * from "./do-have-flex"; export * from "./do-contains-masking"; export * from "./do-have-dimmed-opacity"; export * from "./do-have-stretching"; diff --git a/packages/designto-token/main.ts b/packages/designto-token/main.ts index 7d15a886..44ba6fde 100644 --- a/packages/designto-token/main.ts +++ b/packages/designto-token/main.ts @@ -1,5 +1,7 @@ import { nodes } from "@design-sdk/core"; -import { Widget } from "@reflect-ui/core"; +import { Expanded, Widget } from "@reflect-ui/core"; +import type { Blurred, Opacity, Rotation } from "@reflect-ui/core"; +import type { Stretched } from "./tokens"; import { tokenizeText } from "./token-text"; import { tokenizeLayout } from "./token-layout"; import { tokenizeContainer } from "./token-container"; @@ -19,6 +21,7 @@ import { hasLayerBlurType, hasRotation, hasStretching, + hasFlexible, } from "./detection"; import { MaskingItemContainingNode, tokenizeMasking } from "./token-masking"; import { wrap_with_opacity } from "./token-opacity"; @@ -26,6 +29,7 @@ import { wrap_with_stretched } from "./token-stretch"; import { wrap_with_layer_blur } from "./token-effect/layer-blur"; import { wrap_with_background_blur } from "./token-effect/background-blur"; import { wrap_with_rotation } from "./token-rotation"; +import { wrap_with_expanded } from "./token-expanded"; import flags_handling_gate from "./support-flags"; export type { Widget }; @@ -167,7 +171,7 @@ function handleNode( } // - button - - // TODO: temporarily disabled - remove comment after button widget is ready + // TODO: this causes confliction with flags // const _detect_if_button = detectIf.button(node); // if (_detect_if_button.result) { // return tokenizeButton.fromManifest(node, _detect_if_button.data); @@ -201,7 +205,11 @@ function handleNode( !config.disable_flags_support && config.should_ignore_flag?.(node) !== true ) { - tokenizedTarget = flags_handling_gate(node); + try { + tokenizedTarget = flags_handling_gate(node); + } catch (e) { + console.error("error while interpreting flags.. skipping", e); + } } } // @@ -230,33 +238,40 @@ function handleNode( return tokenizedTarget; } -function post_wrap(node: nodes.ReflectSceneNode, tokenizedTarget: Widget) { - if (tokenizedTarget) { - if (hasStretching(node)) { - tokenizedTarget = wrap_with_stretched(node, tokenizedTarget); - } +export function post_wrap( + node: nodes.ReflectSceneNode, + tokenizedTarget: Widget +): Widget | Stretched | Opacity | Blurred | Rotation | Expanded { + let wrapped = tokenizedTarget; + + if (hasStretching(node)) { + wrapped = wrap_with_stretched(node, wrapped); + } + + if (hasFlexible(node)) { + wrapped = wrap_with_expanded(node, wrapped); } if (hasDimmedOpacity(node)) { - tokenizedTarget = wrap_with_opacity(node, tokenizedTarget); + wrapped = wrap_with_opacity(node, wrapped); } node.effects.map((d) => { const blurEffect = hasBlurType(d); if (blurEffect) { if (hasLayerBlurType(blurEffect)) { - tokenizedTarget = wrap_with_layer_blur(node, tokenizedTarget); + wrapped = wrap_with_layer_blur(node, wrapped); } else if (hasBackgroundBlurType(blurEffect)) { - tokenizedTarget = wrap_with_background_blur(node, tokenizedTarget); + wrapped = wrap_with_background_blur(node, wrapped); } } }); if (hasRotation(node)) { - tokenizedTarget = wrap_with_rotation(node, tokenizedTarget); + wrapped = wrap_with_rotation(node, wrapped); } - return tokenizedTarget; + return wrapped; } function handle_by_types( @@ -326,7 +341,7 @@ function handle_by_types( tokenizedTarget = tokenizeContainer.fromEllipse(node); } else { // a customized ellipse, most likely to be part of a graphical element. - tokenizedTarget = tokenizeGraphics.fromAnyNode(node); + tokenizedTarget = tokenizeGraphics.fromIrregularEllipse(node); } break; diff --git a/packages/designto-token/support-flags/index.ts b/packages/designto-token/support-flags/index.ts index 77745beb..8209c7f2 100644 --- a/packages/designto-token/support-flags/index.ts +++ b/packages/designto-token/support-flags/index.ts @@ -10,9 +10,12 @@ import { tokenize_flagged_artwork } from "./token-artwork"; import { tokenize_flagged_heading } from "./token-heading"; import { tokenize_flagged_paragraph } from "./token-p"; import { tokenize_flagged_span } from "./token-span"; +import { tokenize_flagged_textfield } from "./token-textfield"; import { tokenize_flagged_wrap } from "./token-wrap"; import { tokenize_flagged_wh_declaration } from "./token-wh"; import { tokenize_flagged_fix_wh } from "./token-wh-fix"; +import { tokenize_flagged_button } from "./token-button"; +import { tokenize_flagged_slider } from "./token-slider"; export default function handleWithFlags(node: ReflectSceneNode) { const flags = parse(node.name); @@ -20,6 +23,7 @@ export default function handleWithFlags(node: ReflectSceneNode) { } function _handle_with_flags(node, flags: FlagsParseResult) { + // #region widget altering flags // artwork const artwork_flag_alias = flags["artwork"] || @@ -41,6 +45,20 @@ function _handle_with_flags(node, flags: FlagsParseResult) { return tokenize_flagged_wrap(node, wrap_flag_alias); } + if (flags.__meta?.contains_button_flag) { + return tokenize_flagged_button(node, flags[keys.flag_key__as_button]); + } + + if (flags.__meta?.contains_input_flag) { + return tokenize_flagged_textfield(node, flags[keys.flag_key__as_input]); + } + + if (flags.__meta?.contains_slider_flag) { + return tokenize_flagged_slider(node, flags[keys.flag_key__as_slider]); + } + // #endregion + + // #region element altering flags // heading const heading_flag_alias = flags[keys.flag_key__as_h1] || @@ -58,7 +76,9 @@ function _handle_with_flags(node, flags: FlagsParseResult) { if (span_flag_alias) { return tokenize_flagged_span(node, span_flag_alias); } + // #endregion + // #region style extension flags const paragraph_flag_alias = flags[keys.flag_key__as_p]; if (paragraph_flag_alias) { return tokenize_flagged_paragraph(node, paragraph_flag_alias); @@ -84,4 +104,5 @@ function _handle_with_flags(node, flags: FlagsParseResult) { if (fix_wh_flags.length) { return tokenize_flagged_fix_wh(node, fix_wh_flags); } + // #endregion style extension flags } diff --git a/packages/designto-token/support-flags/token-button/index.ts b/packages/designto-token/support-flags/token-button/index.ts new file mode 100644 index 00000000..4a00624a --- /dev/null +++ b/packages/designto-token/support-flags/token-button/index.ts @@ -0,0 +1,132 @@ +import { ButtonStyleButton, ButtonVariant, Container } from "@reflect-ui/core"; +import type { AsButtonFlag } from "@code-features/flags/types"; +import type { + ReflectFrameNode, + ReflectSceneNode, + ReflectTextNode, +} from "@design-sdk/figma-node"; +import assert from "assert"; +import { tokenizeButton } from "../../token-widgets"; +import { WrappingContainer } from "../../tokens"; + +export function tokenize_flagged_button( + node: ReflectSceneNode, + flag: AsButtonFlag +): ButtonStyleButton | WrappingContainer { + if (flag.value === false) return; + + const validated = validate_button(node); + if (validated.error === false) { + switch (validated.__type) { + case "frame-as-button": { + const { button_base, button_text } = validated; + + const button = tokenizeButton.fromManifest(button_base, { + base: button_base, + text: button_text, + variant: ButtonVariant.custom, + }); + + return button; + } + case "text-as-button": { + const { text } = validated; + + const button = tokenizeButton.fromManifest(text, { + base: null, + text: text, + variant: ButtonVariant.flatText, + }); + + return button; + } + case "node-as-pressable": { + throw new Error("not implemented"); + // const { node } = validated; + // const button = tokenizeButton.fromManifest(node, { + // // @ts-ignore + // base: node, + // variant: ButtonVariant.custom, + // }); + // return button; + } + default: + throw new Error("unreachable"); + } + } else { + throw new Error(validated.error); + } +} + +/** + * validate if layer casted as button can be actually tokenized to button. + * + * - when applyed to frame, + * 1. the root should be a flex + * 2. the children should be a valid text node + * + * - when applyed to text, + * 1. the text should be visible + * 2. the text should be not empty + * @param node + */ +function validate_button(node: ReflectSceneNode): + | { + __type: "frame-as-button"; + error: false; + button_base: ReflectFrameNode; + button_text: ReflectTextNode; + } + | { + __type: "text-as-button"; + error: false; + text: ReflectTextNode; + } + | { + __type: "node-as-pressable"; + error: false; + node: ReflectSceneNode; + } + | { error: string } { + assert(!!node, "target must not be null or undefined"); + switch (node.type) { + case "FRAME": { + assert( + node.children.filter(valid_text_node).length > 0, + "target must have at least one valid text child" + ); + assert( + node.isAutoLayout, + "button target frame must be a autolayout frame" + ); + + const firstTextNode = node.children.find( + valid_text_node + ) as ReflectTextNode; + + return { + __type: "frame-as-button", + button_base: node, + button_text: firstTextNode, + error: false, + }; + } + case "TEXT": { + assert( + valid_text_node(node), + "target must be a valid text node with data" + ); + + return { + __type: "text-as-button", + text: node, + error: false, + }; + } + default: + return { error: "button target is not a valid frame or a text node" }; + } +} + +const valid_text_node = (node: ReflectSceneNode) => + node.type === "TEXT" && node.visible && node.data.length >= 0; diff --git a/packages/designto-token/support-flags/token-slider/index.ts b/packages/designto-token/support-flags/token-slider/index.ts new file mode 100644 index 00000000..7b8b916b --- /dev/null +++ b/packages/designto-token/support-flags/token-slider/index.ts @@ -0,0 +1,175 @@ +import { Slider, Container, Colors } from "@reflect-ui/core"; +import type { AsSliderFlag } from "@code-features/flags/types"; +import type { + ReflectEllipseNode, + ReflectFrameNode, + ReflectRectangleNode, + ReflectSceneNode, +} from "@design-sdk/figma-node"; +import { keyFromNode } from "../../key"; +import assert from "assert"; +import { tokenizeLayout } from "../../token-layout"; +import { unwrappedChild } from "../../wrappings"; +import { RoundSliderThumbShape } from "@reflect-ui/core/lib/slider.thumb"; +import { WrappingContainer } from "../../tokens"; + +/** + * + * from + * ``` + * row|col[ + * enabled text, + * other elements, + * ] + * ``` + * + * to + * ``` + * input { + * enabled text + * } + * ``` + */ +export function tokenize_flagged_slider( + node: ReflectSceneNode, + flag: AsSliderFlag +): Slider | WrappingContainer { + if (flag.value === false) return; + + const validated = validate_slider(node); + if (validated.error === false) { + const _key = keyFromNode(node); + + switch (validated.__type) { + case "frame-as-slider": { + const { slider_root, thumb, value } = validated; + + // TODO: use theme color as default if non available + const fallbackcolor = Colors.blue; + + // initial value ----------------------------- + const p_w = slider_root.width; + const v_w = value?.width ?? 0; + // calculate percentage of value by its width, round to 2 decimal point + const _initial_value = + Math.round((v_w / p_w + Number.EPSILON) * 100) / 100; + // ------------------------------------------- + + // thumb style ------------------------------- + const _thumbcolor = thumb.primaryColor ?? fallbackcolor; + // currently only round thumb is supported + const _thumbsize = + Math.max(thumb?.height ?? 0, thumb?.width ?? 0) ?? undefined; + const _thumbelevation = thumb.primaryShadow?.blurRadius ?? undefined; + // ------------------------------------------- + + // active color ------------------------------ + const _activecolor = value?.primaryColor ?? fallbackcolor; + // ------------------------------------------- + + const container = unwrappedChild( + tokenizeLayout.fromFrame( + slider_root, + slider_root.children, + { is_root: false }, + {} + ) + ) as Container; + + return new WrappingContainer({ + ...container, + key: keyFromNode(node), + child: new Slider({ + key: _key, + ...container, + activeColor: _activecolor, + divisions: 0.01, // when min - max is 0 - 1 + thumbColor: _thumbcolor, + thumbShape: new RoundSliderThumbShape({ + elevation: _thumbelevation, + enabledThumbRadius: _thumbsize, + }), + initialValue: _initial_value, + }), + }); + } + default: + throw new Error( + `unexpected type while handling slider flag ${validated.__type}` + ); + } + } else { + throw new Error(validated.error); + } +} + +type ThumbAcceptableNode = + | ReflectFrameNode + | ReflectRectangleNode + | ReflectEllipseNode; + +type ValueIndicatorAcceptableNode = ReflectFrameNode | ReflectRectangleNode; + +/** + * validate if layer casted as input can be actually tokenized to input. + * + * - when applyed to frame, + * 1. the root should be a flex + * 2. the children should be a valid text node + * + * - when applyed to text, + * 1. the text should be visible + * 2. the text should be not empty + * @param input + */ +function validate_slider(node: ReflectSceneNode): + | { + __type: "frame-as-slider"; + error: false; + slider_root: ReflectFrameNode; + thumb: ThumbAcceptableNode; + value?: ValueIndicatorAcceptableNode; + } + | { error: string } { + assert(!!node, "target must not be null or undefined"); + switch (node.type) { + case "FRAME": { + // find the root of slider + // find the value of the slider + // find the thumb of the slider + const root = node; + + const thumb = node.grandchildren.find((n) => { + // TODO: move this to detection + const _0 = + n.type === "FRAME" || n.type === "RECTANGLE" || n.type === "ELLIPSE"; + const _1 = n.width > 0 && n.height > 0; + const _2 = n.width <= 40 && n.height <= 40; + const _3 = n.width === n.height; + return _0 && _1 && _2 && _3; + }); + + assert(thumb, "thumb node is required. no qualified node found."); + + const value = node.grandchildren + .filter((n) => n.id !== thumb?.id) + .find((n) => { + const _0 = n.type === "FRAME" || n.type === "RECTANGLE"; + const _1 = n.width > 0 && n.height > 0; + const _2 = n.height === root.height; + const _3 = n.primaryColor !== root.primaryColor; + return _0 && _1 && _2 && _3; + }); + + return { + __type: "frame-as-slider", + slider_root: root, + thumb: thumb as ThumbAcceptableNode, + value: value as ValueIndicatorAcceptableNode, + error: false, + }; + } + default: + return { error: "input target is not a valid frame or a text node" }; + } +} diff --git a/packages/designto-token/support-flags/token-textfield/index.ts b/packages/designto-token/support-flags/token-textfield/index.ts new file mode 100644 index 00000000..940261b0 --- /dev/null +++ b/packages/designto-token/support-flags/token-textfield/index.ts @@ -0,0 +1,194 @@ +import { + TextFieldDecoration, + TextField, + InputBorder, + OutlineInputBorder, + Container, +} from "@reflect-ui/core"; +import type { TextStyle } from "@reflect-ui/core"; +import type { AsInputFlag } from "@code-features/flags/types"; +import type { + ReflectFrameNode, + ReflectSceneNode, + ReflectTextNode, +} from "@design-sdk/figma-node"; +import { keyFromNode } from "../../key"; +import assert from "assert"; +import { tokenizeText } from "../../token-text"; +import { detectIf } from "@reflect-ui/detection"; +import { paintToColor } from "@design-sdk/core/utils/colors"; +import { tokenizeLayout } from "../../token-layout"; +import { unwrappedChild } from "../../wrappings"; +import { WrappingContainer } from "../../tokens"; + +/** + * + * from + * ``` + * row|col[ + * enabled text, + * other elements, + * ] + * ``` + * + * to + * ``` + * input { + * enabled text + * } + * ``` + */ +export function tokenize_flagged_textfield( + node: ReflectSceneNode, + flag: AsInputFlag +): TextField | WrappingContainer { + if (flag.value === false) return; + + const validated = validate_input(node); + if (validated.error === false) { + const _key = keyFromNode(node); + + switch (validated.__type) { + case "frame-as-input": { + const { input_root, value, placeholder } = validated; + + const style = + value && (tokenizeText.fromText(value).style as TextStyle); + const placeholderStyle = + placeholder && + (tokenizeText.fromText(placeholder).style as TextStyle); + + // if value only contains '●' or '·' - e.g. ● ● ● ● ● ● it is safe to be casted as a password input. + const obscureText = /^[·\|●\s]+$/.test( + (value?.data || placeholder?.data) ?? "" + ); + + const fillcolor = + input_root.primaryFill.type === "SOLID" + ? paintToColor(input_root.primaryFill) + : null; + + const container = unwrappedChild( + tokenizeLayout.fromFrame( + input_root, + input_root.children, + { is_root: false }, + {} + ) + ) as Container; + + return new WrappingContainer({ + ...container, + key: keyFromNode(node), + child: new TextField({ + key: _key, + ...container, + obscureText: obscureText, + initialValue: value?.data, + style: style || placeholderStyle, + decoration: new TextFieldDecoration({ + border: new OutlineInputBorder({ + borderSide: container.border?.bottom, + borderRadius: container.borderRadius, + }), + contentPadding: input_root.padding, + filled: fillcolor ? true : false, + fillColor: fillcolor, + placeholderText: placeholder?.data, + placeholderStyle: placeholderStyle, + // + }), + }), + }); + } + + case "text-as-input": { + const { style } = tokenizeText.fromText(validated.input_root); + return new TextField({ + key: _key, + style: style as TextStyle, + // TODO: support decoration + initialValue: validated.input_root.data, + decoration: new TextFieldDecoration({ + border: InputBorder.none, + }), + }); + } + } + } else { + throw new Error(validated.error); + } +} + +/** + * validate if layer casted as input can be actually tokenized to input. + * + * - when applyed to frame, + * 1. the root should be a flex + * 2. the children should be a valid text node + * + * - when applyed to text, + * 1. the text should be visible + * 2. the text should be not empty + * @param input + */ +function validate_input(node: ReflectSceneNode): + | { + __type: "frame-as-input"; + error: false; + input_root: ReflectFrameNode; + placeholder?: ReflectTextNode; + value?: ReflectTextNode; + } + | { + __type: "text-as-input"; + error: false; + input_root: ReflectTextNode; + } + | { error: string } { + assert(!!node, "target must not be null or undefined"); + switch (node.type) { + case "FRAME": { + assert( + node.children.filter(valid_text_node).length > 0, + "target must have at least one valid text child" + ); + assert( + node.isAutoLayout, + "input target frame must be a autolayout frame" + ); + + const firstTextNode = node.children.find( + valid_text_node + ) as ReflectTextNode; + + // this is not accurate + const placeholder = detectIf.textfieldPlaceholder(node, firstTextNode); + + return { + __type: "frame-as-input", + input_root: node, + placeholder: placeholder.result ? placeholder.data : undefined, + value: placeholder.result ? undefined : firstTextNode, + error: false, + }; + } + case "TEXT": { + assert( + valid_text_node(node), + "target must be a valid text node with data" + ); + + return { + __type: "text-as-input", + input_root: node, + error: false, + }; + } + default: + return { error: "input target is not a valid frame or a text node" }; + } +} + +const valid_text_node = (node: ReflectSceneNode) => + node && node.type === "TEXT" && node.visible && node.data.length >= 0; diff --git a/packages/designto-token/token-expanded/index.ts b/packages/designto-token/token-expanded/index.ts new file mode 100644 index 00000000..d0fdac7a --- /dev/null +++ b/packages/designto-token/token-expanded/index.ts @@ -0,0 +1,23 @@ +import { nodes } from "@design-sdk/core"; +import { Expanded, WidgetKey } from "@reflect-ui/core"; +import type { Widget } from "@reflect-ui/core"; +import assert from "assert"; + +export function wrap_with_expanded( + node: nodes.ReflectSceneNode, + widget: Widget +): Expanded { + assert( + node.layoutGrow >= 1, + "layoug grow must be >= 1 to be wrapped with Expanded" + ); + + return new Expanded({ + key: new WidgetKey({ + ...widget.key, + id: widget.key.id + "_opacity", + }), + child: widget, + flex: node.layoutGrow, + }); +} diff --git a/packages/designto-token/token-graphics/bitmap.ts b/packages/designto-token/token-graphics/bitmap.ts index c8468545..d674ef6d 100644 --- a/packages/designto-token/token-graphics/bitmap.ts +++ b/packages/designto-token/token-graphics/bitmap.ts @@ -1,12 +1,18 @@ import { MainImageRepository } from "@design-sdk/core/assets-repository"; import type { ReflectBooleanOperationNode, + ReflectEllipseNode, ReflectSceneNode, } from "@design-sdk/figma-node"; -import { ImageWidget } from "@reflect-ui/core"; +import { BoxFit, ImageWidget } from "@reflect-ui/core"; import { ImagePaint } from "@reflect-ui/core/lib/cgr"; import { keyFromNode } from "../key"; +/** + * @deprecated TODO: update the asset repository pattern. + */ +const _asset_key = "fill-later-assets"; + function fromStar(): ImageWidget { // return new ImageWidget(); return; @@ -33,11 +39,9 @@ function fromFrame(): ImageWidget { } function fromAnyNode(node: ReflectSceneNode) { - const _tmp_img = MainImageRepository.instance - .get("fill-later-assets") - .addImage({ - key: node.id, - }); + const _tmp_img = MainImageRepository.instance.get(_asset_key).addImage({ + key: node.id, + }); const widget = new ImageWidget({ key: keyFromNode(node), @@ -56,6 +60,33 @@ function fromBooleanOperation( return fromAnyNode(booleanop); } +/** + * bake a ellipse as an bitmap for irregular shape, (e.g. with startingAngle or innerRadius) + * @param node + * @returns + */ +function fromIrregularEllipse(node: ReflectEllipseNode): ImageWidget { + const _tmp_img = MainImageRepository.instance.get(_asset_key).addImage({ + key: node.id, + }); + + const widget = new ImageWidget({ + key: keyFromNode(node), + width: node.width, + height: node.height, + // we set to contin, since the ellipse' source size will be different from actual bound box size of the ellipse. + fit: BoxFit.contain, + // TODO: suppport alignment and centerSlice due to fit: contain. + // - alignment: + // - centerSlice: + semanticLabel: "image from ellipse", + src: _tmp_img.url, + }); + widget.x = node.x; + widget.y = node.y; + return widget; +} + export const tokenizeBitmap = { fromStar: fromStar, fromPaint: fromPaint, @@ -63,5 +94,6 @@ export const tokenizeBitmap = { fromGroup: fromGroup, fromFrame: fromFrame, fromBooleanOperation: fromBooleanOperation, + fromIrregularEllipse: fromIrregularEllipse, fromAnyNode: fromAnyNode, }; diff --git a/packages/designto-token/token-graphics/vector.ts b/packages/designto-token/token-graphics/vector.ts index 308cbba7..02ad8114 100644 --- a/packages/designto-token/token-graphics/vector.ts +++ b/packages/designto-token/token-graphics/vector.ts @@ -21,6 +21,8 @@ function fromPoligon(): VectorWidget { } function fromVector(vector: ReflectVectorNode) { + // TODO: support vector.fillGeomatery. + if (!vector?.vectorPaths || vector.vectorPaths.length === 0) { // we are not sure when specifically this happens, but as reported, curvy lines does not contain a vector paths. // so we just return a image bake of it. diff --git a/packages/designto-token/token-layout/index.ts b/packages/designto-token/token-layout/index.ts index 908617bc..25759f92 100644 --- a/packages/designto-token/token-layout/index.ts +++ b/packages/designto-token/token-layout/index.ts @@ -8,7 +8,6 @@ import { Stack, Flex, Row, - Opacity, Positioned, Widget, VerticalDirection, @@ -20,9 +19,6 @@ import { Calculation, Clip, Border, - ClipRRect, - Blurred, - Rotation, IWHStyleWidget, Operation, } from "@reflect-ui/core"; @@ -442,6 +438,7 @@ function handlePositioning({ constraint.top = pos.t; break; case "MAX": + // TODO: add this custom logic - if fixed to bottom 0 , it should be fixed rather than absolute. (as a footer) constraint.bottom = pos.b; break; case "SCALE": /** scale fallbacks to stretch */ diff --git a/packages/designto-token/token-widgets/button-widget.ts b/packages/designto-token/token-widgets/button-widget.ts index a6c99bcf..497bfa53 100644 --- a/packages/designto-token/token-widgets/button-widget.ts +++ b/packages/designto-token/token-widgets/button-widget.ts @@ -2,6 +2,12 @@ import { ReflectSceneNode } from "@design-sdk/core"; import * as core from "@reflect-ui/core"; import { keyFromNode } from "../key"; import { manifests } from "@reflect-ui/detection"; +import { tokenizeText } from "../token-text"; +import { Colors, Container, EdgeInsets, WidgetKey } from "@reflect-ui/core"; +import assert from "assert"; +import { unwrappedChild } from "../wrappings"; +import { tokenizeLayout } from "../token-layout"; +import { WrappingContainer } from "../tokens"; /** * Note - this universal button is used temporarily. the button tokens will be splited into more specific usecase following material button classification. @@ -13,24 +19,67 @@ import { manifests } from "@reflect-ui/detection"; function button( node: ReflectSceneNode, manifest: manifests.DetectedButtonManifest -): core.ButtonWidget { +): core.ButtonStyleButton | WrappingContainer { + assert(manifest.text, "text is required for button composing at this point"); + // TODO: // 1. support icon - // 2. support elevated - // 3. support outlined - // 4. support base - const flattened_button_manifest: core.ButtonManifest = { - ...manifest, - minWidth: manifest.base.width, - height: manifest.base.height, - text: { ...manifest.text, style: undefined }, - base: undefined, - icon: undefined, - }; - - return new core.ButtonWidget({ - key: keyFromNode(node), - ...flattened_button_manifest, + // 2. support base + + const _key = keyFromNode(node); + + const button = new core.ButtonStyleButton({ + key: _key, + style: { + textStyle: { + default: new core.TextStyle(manifest.text.textStyle), + }, + backgroundColor: { + default: manifest.base + ? manifest.base.primaryColor + : Colors.transparent, + }, + foregroundColor: { + default: manifest.text.textStyle.color, + }, + // overlayColor: { default: manifest.base.overlayColor } + + // TODO: support multiple shadows + shadowColor: manifest.base?.primaryShadow && { + default: manifest.base.primaryShadow.color, + }, + // elevation: { default: 1}, + padding: { + default: manifest.base ? manifest.base.padding : EdgeInsets.all(0), + }, + }, + child: tokenizeText.fromText(manifest.text), + }); + + const sizing = manifest.base ?? manifest.text; + + if (manifest.base?.type === "FRAME") { + const container = unwrappedChild( + tokenizeLayout.fromFrame( + manifest.base, + manifest.base.children, + { is_root: false }, + {} + ) + ); + + return new WrappingContainer({ + ...container, + key: keyFromNode(node), + child: button, + }); + } + + return new WrappingContainer({ + key: WidgetKey.copyWith(_key, { id: _key.id + ".sizedbox" }), + width: sizing.width, + height: sizing.height, + child: button, }); } diff --git a/packages/designto-token/tokens/index.ts b/packages/designto-token/tokens/index.ts index eb5f31ec..85cb96ea 100644 --- a/packages/designto-token/tokens/index.ts +++ b/packages/designto-token/tokens/index.ts @@ -1 +1,2 @@ export * from "./stretched"; +export * from "./wrapping-container"; diff --git a/packages/designto-token/tokens/wrapping-container/index.ts b/packages/designto-token/tokens/wrapping-container/index.ts new file mode 100644 index 00000000..20eb4bdf --- /dev/null +++ b/packages/designto-token/tokens/wrapping-container/index.ts @@ -0,0 +1,8 @@ +import { Container, Widget } from "@reflect-ui/core"; + +/** + * Special token for wrapping a detected component with a container. + */ +export class WrappingContainer< + T extends Widget = Widget +> extends Container {} diff --git a/packages/designto-token/wrappings/wrapping.ts b/packages/designto-token/wrappings/wrapping.ts index 0b592855..444e364e 100644 --- a/packages/designto-token/wrappings/wrapping.ts +++ b/packages/designto-token/wrappings/wrapping.ts @@ -7,8 +7,9 @@ import { Rotation, Widget, OverflowBox, + SingleChildScrollView, } from "@reflect-ui/core"; -import { Stretched } from "../tokens"; +import { Stretched, WrappingContainer } from "../tokens"; export type WrappingToken = // layout / positioning / sizing wrappers @@ -16,31 +17,38 @@ export type WrappingToken = | Stretched | Positioned | OverflowBox + // scroll + | SingleChildScrollView // transform wrappers | Rotation | Opacity // effect wrappers | Blurred // clip wrappers - | ClipRRect; + | ClipRRect + // wrapping container + | WrappingContainer; /** * CAUTION - this is not related to `Wrap` Widget. this unwrapps a (nested) token that is wrapped with typeof `WrappingToken` * @param maybeWrapped * @returns */ -export function unwrappedChild(maybeWrapped: Widget): Widget { - if ( +export function unwrappedChild(maybeWrapped: Widget): T { + const isWrappingWidget = maybeWrapped instanceof SizedBox || maybeWrapped instanceof Stretched || maybeWrapped instanceof Positioned || maybeWrapped instanceof OverflowBox || + maybeWrapped instanceof SingleChildScrollView || maybeWrapped instanceof Rotation || maybeWrapped instanceof Opacity || maybeWrapped instanceof Blurred || - maybeWrapped instanceof ClipRRect - ) { + maybeWrapped instanceof ClipRRect || + maybeWrapped instanceof WrappingContainer; + + if (isWrappingWidget) { return unwrappedChild(maybeWrapped.child); } - return maybeWrapped; + return maybeWrapped as T; } diff --git a/packages/designto-web/tokens-to-web-widget/compose-unwrapped-button.ts b/packages/designto-web/tokens-to-web-widget/compose-unwrapped-button.ts new file mode 100644 index 00000000..21161648 --- /dev/null +++ b/packages/designto-web/tokens-to-web-widget/compose-unwrapped-button.ts @@ -0,0 +1,10 @@ +import * as web from "@web-builder/core"; +import * as core from "@reflect-ui/core"; + +export function compose_unwrapped_button( + key, + widget: core.ButtonStyleButton, + container?: core.Container +): web.HtmlButton { + return new web.HtmlButton({ ...(container ?? {}), ...widget, key }); +} diff --git a/packages/designto-web/tokens-to-web-widget/compose-unwrapped-slider.ts b/packages/designto-web/tokens-to-web-widget/compose-unwrapped-slider.ts new file mode 100644 index 00000000..21ee6990 --- /dev/null +++ b/packages/designto-web/tokens-to-web-widget/compose-unwrapped-slider.ts @@ -0,0 +1,10 @@ +import * as web from "@web-builder/core"; +import * as core from "@reflect-ui/core"; + +export function compose_unwrapped_slider( + key, + widget: core.Slider, + container?: core.Container +): web.Slider { + return new web.Slider({ ...(container ?? {}), ...widget, key }); +} diff --git a/packages/designto-web/tokens-to-web-widget/compose-unwrapped-text-field.ts b/packages/designto-web/tokens-to-web-widget/compose-unwrapped-text-field.ts new file mode 100644 index 00000000..ad84d845 --- /dev/null +++ b/packages/designto-web/tokens-to-web-widget/compose-unwrapped-text-field.ts @@ -0,0 +1,10 @@ +import * as web from "@web-builder/core"; +import * as core from "@reflect-ui/core"; + +export function compose_unwrapped_text_input( + key, + widget: core.TextField, + container?: core.Container +): web.TextInput { + return new web.TextInput({ ...(container ?? {}), ...widget, key }); +} diff --git a/packages/designto-web/tokens-to-web-widget/compose-wrapped-with-expanded.ts b/packages/designto-web/tokens-to-web-widget/compose-wrapped-with-expanded.ts new file mode 100644 index 00000000..993ed621 --- /dev/null +++ b/packages/designto-web/tokens-to-web-widget/compose-wrapped-with-expanded.ts @@ -0,0 +1,13 @@ +import type { Composer } from "."; +import { Expanded } from "@reflect-ui/core"; + +export function compose_wrapped_with_expanded( + widget: Expanded, + child_composer: Composer +) { + const child = child_composer(widget.child); + child.extendStyle({ + flex: widget.flex, + }); + return child; +} diff --git a/packages/designto-web/tokens-to-web-widget/compose-wrapped-with-stretched.ts b/packages/designto-web/tokens-to-web-widget/compose-wrapped-with-stretched.ts index f2b58aeb..713fb9a7 100644 --- a/packages/designto-web/tokens-to-web-widget/compose-wrapped-with-stretched.ts +++ b/packages/designto-web/tokens-to-web-widget/compose-wrapped-with-stretched.ts @@ -19,6 +19,8 @@ export function compose_wrapped_with_clip_stretched( const child = child_composer(widget.child); child.extendStyle({ "align-self": "stretch", + // TODO: this is required, for fixed width / height to work, but not sure this is the best place to be. + "flex-shrink": 0, [remove_size]: undefined, }); return child; diff --git a/packages/designto-web/tokens-to-web-widget/index.ts b/packages/designto-web/tokens-to-web-widget/index.ts index 45a4b6ab..ed6f993b 100644 --- a/packages/designto-web/tokens-to-web-widget/index.ts +++ b/packages/designto-web/tokens-to-web-widget/index.ts @@ -14,10 +14,15 @@ import { compose_wrapped_with_positioned } from "./compose-wrapped-with-position import { compose_wrapped_with_clip_stretched } from "./compose-wrapped-with-stretched"; import { compose_wrapped_with_sized_box } from "./compose-wrapped-with-sized-box"; import { compose_wrapped_with_overflow_box } from "./compose-wrapped-with-overflow-box"; +import { compose_wrapped_with_expanded } from "./compose-wrapped-with-expanded"; +import { compose_unwrapped_text_input } from "./compose-unwrapped-text-field"; +import { compose_unwrapped_button } from "./compose-unwrapped-button"; +import { compose_unwrapped_slider } from "./compose-unwrapped-slider"; import { compose_instanciation } from "./compose-instanciation"; import { IWHStyleWidget } from "@reflect-ui/core"; import * as reusable from "@code-features/component/tokens"; import assert from "assert"; +import { WrappingContainer } from "@designto/token/tokens"; interface WebWidgetComposerConfig { /** @@ -140,6 +145,8 @@ function compose( thisWebWidget = compose_wrapped_with_blurred(widget, handleChild); } else if (widget instanceof core.Rotation) { thisWebWidget = compose_wrapped_with_rotation(widget, handleChild); + } else if (widget instanceof core.Expanded) { + thisWebWidget = compose_wrapped_with_expanded(widget, handleChild); } // ----- region clip path ------ else if (widget instanceof core.ClipRRect) { @@ -176,9 +183,11 @@ function compose( } else if (widget instanceof core.ImageWidget) { thisWebWidget = new web.ImageElement({ ...widget, - alt: config.img_no_alt ? "" : `image of ${_key.name}`, - src: widget.src, key: _key, + src: widget.src, + alt: config.img_no_alt + ? "" + : widget.semanticLabel ?? `image of ${_key.name}`, }); } else if (widget instanceof core.IconWidget) { // TODO: not ready - svg & named icon not supported @@ -216,6 +225,37 @@ function compose( } } + // #region component widgets + // button + else if (widget instanceof core.ButtonStyleButton) { + // TODO: widget.icon - not supported + thisWebWidget = compose_unwrapped_button(_key, widget); + } + // textfield + else if (widget instanceof core.TextField) { + thisWebWidget = compose_unwrapped_text_input(_key, widget); + } else if (widget instanceof core.Slider) { + thisWebWidget = compose_unwrapped_slider(_key, widget); + } + // wrapping container + else if (widget instanceof WrappingContainer) { + // #region + // mergable widgets for web + if (widget.child instanceof core.TextField) { + thisWebWidget = compose_unwrapped_text_input(_key, widget.child, widget); + } else if (widget.child instanceof core.ButtonStyleButton) { + thisWebWidget = compose_unwrapped_button(_key, widget.child, widget); + } else if (widget.child instanceof core.Slider) { + thisWebWidget = compose_unwrapped_slider(_key, widget.child, widget); + } else { + throw new Error( + `Unsupported widget type: ${widget.child.constructor.name}` + ); + } + // #endregion + } + // #endregion + // execution order matters - some above widgets inherits from Container, this shall be handled at the last. else if (widget instanceof core.Container) { const container = new web.Container({ @@ -224,6 +264,7 @@ function compose( borderRadius: widget.borderRadius, width: widget.width, height: widget.height, + // TODO: add child: (currently no widget is being nested under Container, exept below custom filters) }); container.x = widget.x; container.y = widget.y; diff --git a/packages/reflect-detection b/packages/reflect-detection index e9529d2a..36621cc0 160000 --- a/packages/reflect-detection +++ b/packages/reflect-detection @@ -1 +1 @@ -Subproject commit e9529d2ad7bbade587dbe38a15b5ba02e665a9da +Subproject commit 36621cc08370386f6f16ac9b313b3862951f1997 diff --git a/packages/support-flags/--artwork/index.ts b/packages/support-flags/--artwork/index.ts index ce5c02e2..271c7cbb 100644 --- a/packages/support-flags/--artwork/index.ts +++ b/packages/support-flags/--artwork/index.ts @@ -1,5 +1,13 @@ export const flag_key__artwork = "artwork"; +const flag_key__as_artwork = "as-artwork"; + +export const flag_key_alias__artwork = [ + // + flag_key__artwork, + flag_key__as_artwork, +] as const; + export interface ArtworkFlag { flag: typeof flag_key__artwork; value?: boolean; diff --git a/packages/support-flags/--as-button/index.ts b/packages/support-flags/--as-button/index.ts new file mode 100644 index 00000000..4885c39d --- /dev/null +++ b/packages/support-flags/--as-button/index.ts @@ -0,0 +1,11 @@ +// primary +export const flag_key__as_button = "as-button"; + +export const flag_key_alias__as_button = [flag_key__as_button]; + +export interface AsButtonFlag { + flag: typeof flag_key__as_button; + + value: boolean; + _raw?: string; +} diff --git a/packages/support-flags/--as-input/index.ts b/packages/support-flags/--as-input/index.ts new file mode 100644 index 00000000..23d1a96a --- /dev/null +++ b/packages/support-flags/--as-input/index.ts @@ -0,0 +1,22 @@ +// primary +export const flag_key__as_input = "as-input"; +// alias +const flag_key__textfield = "textfield"; +const flag_key__text_field = "text-field"; + +export const flag_key_alias_as_input = [ + flag_key__as_input, + flag_key__textfield, + flag_key__text_field, +]; + +export interface AsInputFlag { + flag: + | typeof flag_key__as_input + | typeof flag_key__textfield + | typeof flag_key__text_field; + + value: boolean; + type: string; + _raw?: string; +} diff --git a/packages/support-flags/--as-slider/index.ts b/packages/support-flags/--as-slider/index.ts new file mode 100644 index 00000000..25ff30af --- /dev/null +++ b/packages/support-flags/--as-slider/index.ts @@ -0,0 +1,16 @@ +// priamry +export const flag_key__as_slider = "as-slider"; +// alias +const flag_key__as_range = "as-range"; + +export const flag_key_alias__as_slider = [ + flag_key__as_slider, + flag_key__as_range, +]; + +export interface AsSliderFlag { + flag: typeof flag_key__as_slider | typeof flag_key__as_range; + + value: boolean; + _raw?: string; +} diff --git a/packages/support-flags/--autofocus/index.ts b/packages/support-flags/--autofocus/index.ts new file mode 100644 index 00000000..bd0f82d7 --- /dev/null +++ b/packages/support-flags/--autofocus/index.ts @@ -0,0 +1,13 @@ +export const flag_key__autofocus = "autofocus"; +// alias +const flag_key__auto_focus = "auto-focus"; + +export const flag_key_alias__autofocus = [ + flag_key__autofocus, + flag_key__auto_focus, +]; + +export interface AutofocusFlag { + flag: typeof flag_key__autofocus; + value?: boolean; +} diff --git a/packages/support-flags/--placeholder/README.md b/packages/support-flags/--placeholder/README.md new file mode 100644 index 00000000..755d947a --- /dev/null +++ b/packages/support-flags/--placeholder/README.md @@ -0,0 +1,4 @@ +# `--placeholder` Flag + +- Visit the [design documentation](../docs/--placeholder.md) +- View it on Grida [Docs](https://grida.co/docs/flags/--placeholder) diff --git a/packages/support-flags/docs/--as-input.md b/packages/support-flags/docs/--as-input.md index 7c03ddc0..d789c000 100644 --- a/packages/support-flags/docs/--as-input.md +++ b/packages/support-flags/docs/--as-input.md @@ -16,3 +16,7 @@ stage: - `--as-input` - `--as-input=true` - `--as=input` + +## See also + +- [`--placeholder`](--placeholder) diff --git a/packages/support-flags/docs/--placeholder.md b/packages/support-flags/docs/--placeholder.md new file mode 100644 index 00000000..9a98cf64 --- /dev/null +++ b/packages/support-flags/docs/--placeholder.md @@ -0,0 +1,32 @@ +--- +title: Placeholder flag +id: "--placeholder" +locale: en +locales: + - en +stage: + - staging +--- + +# `--placeholder` Flag + +This speficies that this text node should be interpreted as placeholder of a parent text input element. + +## Syntax + +```ts +`--placeholder` | `--placeholder=${typeof boolean}` | `--is-placeholder`; +``` + +## Example + +``` +--placeholder +--placeholder=true +--placeholder=false +--is-placeholder +``` + +## See also + +- [`--as-input`](--as-input) diff --git a/packages/support-flags/keys.ts b/packages/support-flags/keys.ts index 8e84c928..6f633a1f 100644 --- a/packages/support-flags/keys.ts +++ b/packages/support-flags/keys.ts @@ -1,3 +1,5 @@ +import { flag_key_alias__autofocus, flag_key__autofocus } from "./--autofocus"; + import { flag_key_alias__as_h1, flag_key__as_h1 } from "./--as-h1"; import { flag_key_alias__as_h2, flag_key__as_h2 } from "./--as-h2"; import { flag_key_alias__as_h3, flag_key__as_h3 } from "./--as-h3"; @@ -7,6 +9,10 @@ import { flag_key_alias__as_h6, flag_key__as_h6 } from "./--as-h6"; import { flag_key_alias__as_p, flag_key__as_p } from "./--as-p"; import { flag_key_alias__as_span, flag_key__as_span } from "./--as-span"; +import { flag_key__as_button, flag_key_alias__as_button } from "./--as-button"; +import { flag_key__as_input, flag_key_alias_as_input } from "./--as-input"; +import { flag_key__as_slider, flag_key_alias__as_slider } from "./--as-slider"; + import { flag_key_alias__width, flag_key__width } from "./--width"; import { flag_key_alias__min_width, flag_key__min_width } from "./--min-width"; import { flag_key_alias__max_width, flag_key__max_width } from "./--max-width"; @@ -22,6 +28,12 @@ import { flag_key_alias__fix_height, flag_key__fix_height } from "./--fix-height import { flag_key_alias__declare, flag_key__declare } from "./--declare"; +// +export { + // + flag_key__autofocus, +}; + export { flag_key__as_h1, flag_key__as_h2, @@ -30,9 +42,16 @@ export { flag_key__as_h5, flag_key__as_h6, }; + export { flag_key__as_p }; export { flag_key__as_span }; +export { flag_key__as_button }; + +export { flag_key__as_input }; + +export { flag_key__as_slider }; + export { flag_key__width, flag_key__min_width, @@ -46,6 +65,7 @@ export { flag_key__fix_width, flag_key__fix_height }; export { flag_key__declare }; export const alias = { + autofocus: flag_key_alias__autofocus, as_h1: flag_key_alias__as_h1, as_h2: flag_key_alias__as_h2, as_h3: flag_key_alias__as_h3, @@ -54,6 +74,9 @@ export const alias = { as_h6: flag_key_alias__as_h6, as_p: flag_key_alias__as_p, as_span: flag_key_alias__as_span, + as_button: flag_key_alias__as_button, + as_input: flag_key_alias_as_input, + as_slider: flag_key_alias__as_slider, width: flag_key_alias__width, min_width: flag_key_alias__min_width, max_width: flag_key_alias__max_width, diff --git a/packages/support-flags/parse.ts b/packages/support-flags/parse.ts index 1c89f5bb..22b74fb7 100644 --- a/packages/support-flags/parse.ts +++ b/packages/support-flags/parse.ts @@ -14,14 +14,27 @@ import type { TextElementPreferenceFlag, AsParagraphFlag, AsTextSpanFlag, + AsButtonFlag, + AsInputFlag, SimpleBooleanValueFlag, FixWHFlag, DeclareSpecificationFlag, WHDeclarationFlag, + AsSliderFlag, } from "./types"; export type FlagsParseResult = Results & { __meta: { + contains_heading_flag: boolean; + contains_paragraph_flag: boolean; + contains_span_flag: boolean; + contains_button_flag: boolean; + contains_input_flag: boolean; + contains_slider_flag: boolean; + contains_wh_declaration_flag: boolean; + contains_fix_wh_flag: boolean; + contains_declare_flag: boolean; + // ... [key: string]: boolean; }; }; @@ -48,6 +61,15 @@ export function parse(name: string): FlagsParseResult { __textspan_alias_pref, //#endregion + // button + __button_alias_pref, + + // input + __input_alias_pref, + + // slider + __slider_alias_pref, + //#region __width_alias_pref, __max_width_alias_pref, @@ -83,6 +105,21 @@ export function parse(name: string): FlagsParseResult { keys.alias.as_span ); + const as_button_flag = handle_single_boolean_flag_alias( + _raw_parsed, + keys.alias.as_button + ); + + const as_input_flag = handle_single_boolean_flag_alias( + _raw_parsed, + keys.alias.as_input + ); + + const as_slider_flag = handle_single_boolean_flag_alias( + _raw_parsed, + keys.alias.as_slider + ); + const wh_declaration_flag = transform_wh_declaration_alias_from_raw(_raw_parsed); const fix_wh_flag = handle_single_boolean_flag_alias( @@ -101,6 +138,8 @@ export function parse(name: string): FlagsParseResult { ...as_heading_flag, ...(as_paragraph_flag ?? {}), ...(as_span_flag ?? {}), + ...(as_button_flag ?? {}), + ...(as_input_flag ?? {}), ...(wh_declaration_flag ?? {}), ...(fix_wh_flag ?? {}), ...(declare_flag ?? {}), @@ -108,6 +147,9 @@ export function parse(name: string): FlagsParseResult { contains_heading_flag: notempty(as_heading_flag), contains_paragraph_flag: notempty(as_paragraph_flag), contains_span_flag: notempty(as_span_flag), + contains_button_flag: notempty(as_button_flag), + contains_input_flag: notempty(as_input_flag), + contains_slider_flag: notempty(as_slider_flag), contains_wh_declaration_flag: notempty(as_span_flag), contains_fix_wh_flag: notempty(fix_wh_flag), contains_declare_flag: notempty(declare_flag), @@ -235,6 +277,18 @@ const __textspan_alias_pref = _simple_boolean_value_flag_prefernce_mapper( keys.alias.as_span ); +const __button_alias_pref = _simple_boolean_value_flag_prefernce_mapper( + keys.alias.as_button +); + +const __input_alias_pref = _simple_boolean_value_flag_prefernce_mapper( + keys.alias.as_input +); + +const __slider_alias_pref = _simple_boolean_value_flag_prefernce_mapper( + keys.alias.as_slider +); + // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- diff --git a/packages/support-flags/types.ts b/packages/support-flags/types.ts index ae944af1..fde66877 100644 --- a/packages/support-flags/types.ts +++ b/packages/support-flags/types.ts @@ -1,3 +1,4 @@ +import type { AutofocusFlag } from "./--autofocus"; import type { ArtworkFlag } from "./--artwork"; import type { AsHeading1Flag } from "./--as-h1"; import type { AsHeading2Flag } from "./--as-h2"; @@ -5,6 +6,9 @@ import type { AsHeading3Flag } from "./--as-h3"; import type { AsHeading4Flag } from "./--as-h4"; import type { AsHeading5Flag } from "./--as-h5"; import type { AsHeading6Flag } from "./--as-h6"; +import type { AsButtonFlag } from "./--as-button"; +import type { AsInputFlag } from "./--as-input"; +import type { AsSliderFlag } from "./--as-slider"; import type { AsParagraphFlag } from "./--as-p"; import type { AsTextSpanFlag } from "./--as-span"; import type { WidthFlag } from "./--width"; @@ -18,14 +22,13 @@ import type { FixHeightFlag } from "./--fix-height"; import type { DeclareSpecificationFlag } from "./--declare"; export type Flag = - // + | AutofocusFlag | ArtworkFlag | TextElementPreferenceFlag - // | WHDeclarationFlag | FixWHFlag - // - | DeclareSpecificationFlag; + | DeclareSpecificationFlag + | ComponentCastingFlag; export interface SimpleBooleanValueFlag { flag: string; @@ -76,6 +79,16 @@ export type HeadingFlag = | AsHeading5Flag | AsHeading6Flag; +/** + * Type alias for a flag taht can be used to specify the element casting + */ +export type ComponentCastingFlag = AsButtonFlag | AsInputFlag | AsSliderFlag; + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +export type { AutofocusFlag }; + // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- @@ -102,6 +115,13 @@ export type { AsTextSpanFlag }; // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- +export type { AsButtonFlag }; +export type { AsInputFlag }; +export type { AsSliderFlag }; + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + export type { WidthFlag, MinWidthFlag, MaxWidthFlag }; export type { HeightFlag, MinHeightFlag, MaxHeightFlag }; export type { FixWidthFlag, FixHeightFlag };