diff --git a/Cargo.lock b/Cargo.lock index 1452075c6..088989e2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1711,6 +1711,16 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "paperclip_tidy" +version = "0.1.1" +dependencies = [ + "paperclip_core", + "paperclip_project", + "paperclip_proto", + "paperclip_validate", +] + [[package]] name = "paperclip_validate" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index e748e0108..4efaae811 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "libs/validate", "libs/project", "libs/evaluator", + "libs/tidy", "libs/paperclip-loader", "libs/workspace", "libs/infer", diff --git a/TODO.md b/TODO.md index 0cee4a78a..ddc63b09c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,40 +1,49 @@ #### FOCUS -- focus on editing - - - ensure that paths are absolute - - can move components to to other files - - references are updated - - update refs OF expression moved - -- everything should be editable in the UI - - should be able to edit variant triggers - - should be able to add scripts - - prohibit things from being deleted if references to them +- layers + - select slots + - ensure that all inserts are created + - proper UI for inserts #### Immediate -- designer stops working after lint error +- post edit clean up + + - prune unused imports + - add inserts of elements -- editing +- layers panel - - circular dependencies - - ability to move tokens to other files - - ability to move mixins to other files - - ability to move components to other files - - ability to move triggers to other files - - ensure that - - all instances of each expression is updated + - ability to drop assets to the canvas -- linting +- canvas + - topbar should not move when panels hide / show +- AI - - ensure that no magic colors are present - - ensure that no magic measurements are present + - CLI tool for interacting with code + - compare before committing -- layers panel +- Online editor - - ability to drop files into file navigator + - WASM bridge - file navigator -- styles + - ability to drop files to file navigator + - display assets + +- resource panel + + - display components + - display assets (svg, png) + - ability to drop assets to canvas + +- styles panel + +- properties panel + + - text input should be text area + +- compiler + - compile style mixins as css selectors + - compile style vars diff --git a/gpt-instructions.md b/gpts/archive/syntax.md similarity index 100% rename from gpt-instructions.md rename to gpts/archive/syntax.md diff --git a/gpts/current/bridge.md b/gpts/current/bridge.md new file mode 100644 index 000000000..479363ee9 --- /dev/null +++ b/gpts/current/bridge.md @@ -0,0 +1,8 @@ +/\* + +Instructions + behavior to bridge GPT with a library +that can interact with it. + +\*/ + +You will also be interacting with another system that can only take JSON. It will give you instructions, and you should only diff --git a/gpts/current/examples.md b/gpts/current/examples.md new file mode 100644 index 000000000..da06cbc84 --- /dev/null +++ b/gpts/current/examples.md @@ -0,0 +1,74 @@ +I'm going to give you some example instructions and what you might produce as a result of these instructions + +User: "build a card component with a title slot. Give the card a grey background and some padding" + +System: + +``` +component Card { + render div { + style { + background: grey + padding: 14px + } + + render h1 { + slot title + } + + slot content + } +} +``` + +User: "Create a card element with a class attribute called "card" + +System: + +``` +div (class: "card") { + +} +``` + +User: "Create a button component with an onClick handler" + +System: + +``` +component Button { + render button(onClick: onClick) { + slot children + } +} +``` + +User: "Create a header with a text node 'hello world'!" + +System: + +``` +h1 { + text "Hello world" +} +``` + +User: "Create a responsive card component" + +System: + +``` +component Card { + variant mobile trigger { + "@media screen and (max-width: 480px")" + } + render div { + style { + font-size: 14px + } + style variant mobile { + font-size: 24px + } + } +} +``` diff --git a/gpts/current/fix.md b/gpts/current/fix.md new file mode 100644 index 000000000..6ee859717 --- /dev/null +++ b/gpts/current/fix.md @@ -0,0 +1,229 @@ +This is invalid syntax: + +``` +div { + style { + background-color: darkgray; + padding: 20px; + text-align: center; + } +} +``` + +This is the correct syntax: + +``` +div { + style { + background-color: darkgray + padding: 20px + text-align: center + } +} +``` + +This is invalid syntax: + +``` +component Footer { + render div { + text "Footer" + style { + background-color: darkgray + padding: 20px + text-align: center + "@media screen and (max-width: 480px)" { + font-size: 12px + } + } + } +} +``` + +This is the correct syntax + +``` +component Footer { + variant mobile trigger { + "@media screen and (max-width: 480px)" + } + render div { + text "Footer" + style { + background-color: darkgray + padding: 20px + text-align: center + } + style variant mobile { + font-size: 12px + } + } +} +``` + +This is invalid syntax: + +``` +render div { + text "This is a Card" + style { + background-color white + border "1px solid black" + padding 20px + margin 10px + box-shadow "0 4px 8px 0 rgba(0,0,0,0.2)" + transition "0.3s" + } + style variant mobile { + font-size 14px + padding 10px + } +} +``` + +- For style declarations, semi colons must exist between the key and value +- declaration values must not be wrapped around with quotes + +Here's the fix: + +``` +render div { + text "This is a Card" + style { + background-color: white + border: 1px solid black + padding: 20px + margin: 10px + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2) + transition: 0.3s + } + style variant mobile { + font-size: 14px + padding: 10px + } +} +``` + +This is invalid syntax: + +``` +render div { + text "This is a Responsive Card" + style { + background-color: white + border: 1px solid black + padding: 20px + margin: 10px + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2) + transition: 0.3s + max-width: 300px + } + style variant mobile { + font-size: 14px + padding: 15px + margin: 5px + max-width: 100% + } +} +``` + +- The mobile variant is undefined here. style variants + can only be defined in components since variants can only be defined + in components. +- the render keyword is reserved for components + +Here's the fix: + +``` +component Card { + variant mobile trigger { + "@media screen and (max-width: 100px)" + } + render div { + text "This is a Responsive Card" + style { + background-color: white + border: 1px solid black + padding: 20px + margin: 10px + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2) + transition: 0.3s + max-width: 300px + } + style variant mobile { + font-size: 14px + padding: 15px + margin: 5px + max-width: 100% + } + } +} +``` + +This is invalid syntax: + +```` +variant mobile trigger { + "@media screen and (max-width: 600px)" +} + +component Card { + use variant mobile + render div { + text "This is a Responsive Card" + style { + background-color: white + border: 1px solid black + padding: 20px + margin: 10px + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2) + transition: 0.3s + max-width: 300px + } + style variant mobile { + font-size: 14px + padding: 15px + margin: 5px + max-width: 100% + } + } +} +``` + +If you want to re-use a trigger, he's how you do that: + +```` + +trigger mobileTrigger { +"@media screen and (max-width: 600px)" +} + +component Card { +variant mobile trigger { +mobileTrigger +} +render div { +text "This is a Responsive Card" +style { +background-color: white +border: 1px solid black +padding: 20px +margin: 10px +box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2) +transition: 0.3s +max-width: 300px +} +style variant mobile { +font-size: 14px +padding: 15px +margin: 5px +max-width: 100% +} +} +} + +``` + +When I say "create a card component", or any instruction for producing code, I +want you to produce Paperclip code, and only paperclip code. +``` diff --git a/gpts/current/specifics.md b/gpts/current/specifics.md new file mode 100644 index 000000000..db4061681 --- /dev/null +++ b/gpts/current/specifics.md @@ -0,0 +1,25 @@ +the tone should be technical + +Paperclip is an alternative to HTML and CSS. It has entirely different syntax. +Use only the syntax that I give you. + +Do not write HTML and CSS. When you're asked to write code, use only the syntax that I give you. For example, if I say: "Build a button component", you should write: + +``` +component Button { + render button { + slot children + } +} +``` + +- style declarations don't take simicolons + +- Media queries can only be defined as triggers. + +- All CSS selectors can only be defined as triggers + +- styles with variants can only exist in components +- NEVER use semicolones in style declarations +- Never create new comments in Paperclip code +- Only produce code that is syntactically correct with Paperclip diff --git a/gpts/current/syntax.md b/gpts/current/syntax.md new file mode 100644 index 000000000..0a916494d --- /dev/null +++ b/gpts/current/syntax.md @@ -0,0 +1,490 @@ +I'm going to teach you how to be a helpful assistant for a new programming language called Paperclip. Paperclip is an alternative to HTML and CSS that compiles to styled components for any language or framework. Here's the syntax for Paperclip: + +Paperclip is basically an alternative to writing HTML and CSS. Here's a basic element: + +```paperclip +div(title: "something", class: "something-else") { + style { + color: red + font-family: sans-serif + } + text "hello" +} +``` + +Elements may contain children which are within `{` and `}` curly braces as seen above. Attributes are defined within `(` and `)`. + +you may be able to give an ID to elements and text nodes. For example: + +```paperclip +div helloWorld { +} + +div helloWorld (title: "something") { +} + +text someText "value" +``` + +Text nodes can be expressed like so: + +```paperclip +text "some text value" +``` + +You may define styles on text nodes. For example: + +```paperclip +text "something" { + style { + color: red + } +} +``` + +Note that text nodes may only contain styles. + +Styles can be re-used in Paperclip. For example: + +```paperclip +style defaultFont { + font-family: sans-serif + color: blue +} + +style titleLarge { + font-size: 2em +} + +div { + style extends defaultFont, titleLarge + text "something" +} +``` + +Here's another example: + +```paperclip +style defaultFont { + font-family: sans-serif + color: #333 +} + +style titleLarge { + font-sie: 3em +} + +style h1 extends defaultFont, titleLarge { + margin: 10px 0px +} + +div { + style extends h1 { + color: orange + } +} +``` + +Paperclip has the concept of style tokens which takes individal declaration values. For example: + +```paperclip +token fontFamily Inter, sans-serif +token fontColor #333 + +style defaultFont { + font-family: var(fontFamily) + color: var(fontColor) +} +``` + +PC files are able to import other PC files. For example, here's a `theme.pc` file: + +```paperclip +public token fontFamily Inter, sans-serif +public token fontColor #333 + +public token blue01 blue + +public style defaultFont { + font-family: var(fontFamily) + color: var(fontColor) +} +``` + +`public` indicates that the expression may be importable in other files. Here's how we may be able to use some of these values in a file called `test.pc`: + +```paperclip +import "path/to/theme.pc" as theme + +div { + style extends theme.defaultFont { + color: var(theme.blue01) + } +} +``` + +Note the path to the file being imported may be relative to the source directory specified in the `paperclip.config.json` file. For example, given this config: + +``` +{ + "srcDir": "src" +} +``` + +And given the file `src/styles/theme.pc", _that_ file may be imported in any other PC like so: + +```paperclip +import "styles/theme.pc" as theme +``` + +Note that if the `srcDir` is _not_ specified, then the srcDir is based on the directory where the `paperclip.config.json` file lives. In this case, importing the `theme.pc` can be done like so (in the absense of srcDir): + +```paperclip +import "src/styles/theme.pc" as theme.pc +``` + +Paperclip supports components. Here's a very basic example: + +```paperclip +public component Button { + render button { + text "hello world" + } +} +``` + +Slots enable developers to specify areas of a component where children may be inserted. For example: + +```paperclip +public component Button { + render button { + slot children + } +} +``` + +Slots may have default children. For example: + +```paperclip +public component Button { + render button { + slot someChild { + text "I'm a default child" + } + } +} +``` + +Here's another example using slots: + +```paperclip +public style titleLarge { + font-weight: 500 + font-size: 2em +} +public component Card { + render div { + h1 { + style extends titleLarge + slot title { + text "some title!" + } + } + div { + slot children + } + } +} +``` + +You may render component instances in Paperclip, too. For example: + +```paperclip +public component Button { + render button { + slot children + } +} + +Button { + text "hello world!" +} + +Button { + text "blarg" +} +``` + +Note that `children` is special in Paperclip since it takes children of instances. If a slot is defined with an ID other than `children`, you may use an `insert` expression in order to insert children into the corresponding slot. For example: + +```paperclip +public style titleLarge { + font-weight: 500 + font-size: 2em +} + +public component Card { + render div { + h1 { + style extends titleLarge + slot title { + text "some title!" + } + } + div { + slot children + } + } +} + +Card { + insert title { + text "hello world" { + style { + color: orange + } + } + } + div { + text "Some child" + } +} +``` + +You may define dynamic attributes for any elements or instances in a component. For example: + +```paperclip +public component Button { + render button(class: class, onClick: onClick) { + slot children + } +} +``` + +Components support the concept of style variants. For example: + +```paperclip +component Button { + variant hover trigger { + ":hover" + } + render button { + style { + background: black + } + style variant hover { + background: #333 + } + slot children + } +} + +Button { + text "click me!" +} +``` + +Note that variant triggers may contain mulitple CSS selectors. For example: + +```paperclip +component Button { + variant hover trigger { + ":hover" + ".hover" + } + render button(class: class) { + style { + background: black + } + style variant hover { + background: #333 + } + slot children + } +} + +Button { + text "click me!" +} +Button(class: "hover") { + text "hover state!" +} +``` + +you may also define media queries within variant triggers. For example: + +```paperclip +component Button { + variant hover trigger { + ":hover" + ".hover" + } + variant mobile trigger { + "@media screen and (max-width: 400px)" + } + render button(class: class) { + style { + background: black + } + style variant hover { + background: #333 + } + style variant mobile { + font-size: 1.3em + } + slot children + } +} +``` + +Paperclip supports variant combinations. For example: + +```paperclip +component Button { + variant hover trigger { + ".hover" + } + variant mobile trigger { + "@media screen and (max-width: 400px)" + } + variant supportsFlexbox trigger { + "@supports(display: flex)" + } + render button(class: class) { + style variant hover + mobile { + color: orange + } + style variant hover + mobile + supportsFlexbox { + display: flex + font-size: 1.3em + background: #333 + } + slot children + } +} +``` + +Triggers are also re-usable. For example: + +```paperclip +public trigger mobileTrigger "@media screen and (max-width: 400px)" + +public component Button { + variant mobile trigger { + mobileTrigger + } + render button { + style variant mobile + slot children + } +} +``` + +re-usable triggers enables them to be re-used in multiple places. This is especially useful for things like media queries that may want to be re-used in multiple components. + +Note that variant triggers apply to the render node. For example, here's a nested node that uses a variant trigger: + +```paperclip +token space03 4px + +public component Card { + variant hover trigger { + ":hover" + ".hover" + } + + render div root { + style { + display: flex + gap: var(space03) + } + div title { + style { + font-size: 24px + font-weight: 600px + } + style variant hover { + color: red + } + slot title { + text "some title" + } + } + div { + slot children { + slot "some children" + } + } + } +} +``` + +Paperclip supports conditional nodes. For example: + +```paperclip +public component Card { + render div { + if showSomething { + text "do that thing" + } + } +} +``` + +is compiled to the following React code: + +```paperclip +export const Card = ({showSomething}) => { + return
+ {showSomething ? "do that thing" : null} +
+}; +``` + +Note how IDs on nodes are compiled to code. For example: + +```paperclip +component Button { + render button root { + slot children + } +} +public component Card { + render div root { + Button titleButton { + text "click clack!" { + style { + color: red + } + } + } + } +} +``` + +Paperclip components may define scripts that provide these components with behavior. For example: + +```paperclip +public component Button { + script(src: "./controller.tsx", target: "react", name: "SomeButton") + render button (onClick: onClick, class: className) { + slot children + } +} +``` + +When a script is provided, Paperclip will use that script IF the target compiler specified in the script is used. In this case, it's react, so the script is used. the `name` parameter of script is used to figure out what higher-order-component to use defined within the script `src`. In this case, it's `SomeButton`. + +Components and nodes defined at the root document may be considered as frames, and these frames may contain metadata expressed as a comment. Here's what that looks like: + +```paperclip +/** + * @frame(x: -918, y: -272, width: 1024, height: 768, visible: true) + */ + +button { + text "hello" +} +``` + +This frame is used within the visual editor to render a frame of the nodes and components specified within Paperclip documents. Note that as of now, root nodes and components like this are the only things that support comments. diff --git a/libs/core/src/proto/ast_mutate/append_child.rs b/libs/core/src/proto/ast_mutate/append_child.rs index 237d422e0..93a2c3ff9 100644 --- a/libs/core/src/proto/ast_mutate/append_child.rs +++ b/libs/core/src/proto/ast_mutate/append_child.rs @@ -4,7 +4,7 @@ use paperclip_parser::core::parser_context::Options; use paperclip_parser::pc::parser::parse as parse_pc; use paperclip_proto::ast; use paperclip_proto::ast::all::Expression; -use paperclip_proto::ast::pc::Node; +use paperclip_proto::ast::pc::Element; use paperclip_proto::ast_mutate::{mutation_result, AppendChild, ExpressionInserted}; use paperclip_proto::ast::all::visit::{MutableVisitor, VisitorResult}; @@ -43,7 +43,15 @@ impl MutableVisitor<()> for EditContext { expr: &mut ast::pc::Element, ) -> VisitorResult<(), EditContext> { if expr.get_id() == &self.mutation.parent_id { - let child: Node = parse_node(&self.mutation.child_source, &self.new_id()); + let div: Element = parse_node( + format!("div {{{}}}", &self.mutation.child_source).as_str(), + &self.new_id(), + ) + .try_into() + .expect("Cannot parse node"); + + let child = div.body.get(0).expect("Child must exist"); + expr.body.push(child.clone()); self.add_change( diff --git a/libs/core/src/proto/ast_mutate/set_id.rs b/libs/core/src/proto/ast_mutate/set_id.rs index 5a19d6379..7ea04519c 100644 --- a/libs/core/src/proto/ast_mutate/set_id.rs +++ b/libs/core/src/proto/ast_mutate/set_id.rs @@ -1,4 +1,4 @@ -use crate::proto::ast_mutate::utils::{get_unique_document_body_item_name, get_unique_valid_name}; +use crate::proto::ast_mutate::utils::get_unique_valid_name; use super::utils::{get_unique_component_name, get_valid_name}; use convert_case::Case; diff --git a/libs/core/src/proto/ast_mutate/test.rs b/libs/core/src/proto/ast_mutate/test.rs index 149e2968b..17d12f7b3 100644 --- a/libs/core/src/proto/ast_mutate/test.rs +++ b/libs/core/src/proto/ast_mutate/test.rs @@ -2977,6 +2977,7 @@ case! { name: None, comment: None, parameters: vec![], + tag_name_range: None, range: None, body: vec![], tag_name: "span".to_string(), @@ -3009,6 +3010,7 @@ case! { namespace: None, name: None, comment: None, + tag_name_range: None, parameters: vec![], range: None, body: vec![], @@ -3139,6 +3141,7 @@ case! { target_expression_id: "80f4925f-2".to_string(), item: Some(paste_expression::Item::Element(Element { namespace: None, + tag_name_range: None, comment: None, name: None, parameters: vec![], @@ -3180,6 +3183,7 @@ case! { item: Some(paste_expression::Item::Element(Element { namespace: None, name: None, + tag_name_range: None, comment: None, parameters: vec![], range: None, @@ -4490,6 +4494,7 @@ case! { id: "div".to_string(), comment: None, tag_name: "div".to_string(), + tag_name_range: None, namespace: None, name: None, parameters: vec![], diff --git a/libs/designer/src/domains/api/engine.ts b/libs/designer/src/domains/api/engine.ts index ccaf85b10..3b2109ee9 100644 --- a/libs/designer/src/domains/api/engine.ts +++ b/libs/designer/src/domains/api/engine.ts @@ -84,6 +84,7 @@ import { Range } from "@paperclip-ui/proto/lib/generated/ast/base"; import { get, kebabCase } from "lodash"; import { ConfirmKind } from "../../state/confirm"; import { metadataValueMapToJSON } from "@paperclip-ui/proto/lib/virt/html-utils"; +import { ExpressionKind } from "../../ui/logic/Editor/EditorPanels/RightSidebar/StylePanel/Declarations/DeclarationValue/state"; export type DesignerEngineOptions = { protocol?: string; @@ -487,7 +488,75 @@ const createEventHandler = (actions: Actions) => { ]); }; - const handleAddLayerMenuItemClick = ( + type ResolveTargetExprInfo = { + kind: ast.ExprKind; + id: string; + }; + + // for cases where virtual node is selected but doesn't exist. E.g: instance inserts + const resolveTargetExprId = async ( + id: string, + state: DesignerState + ): Promise => { + if (!id.includes(".")) { + const info = ast.getExprInfoById(id, state.graph); + return { kind: info.kind, id }; + } + const instanceParts = id.split("."); + + const targetExpr = ast.getExprInfoById(instanceParts.pop(), state.graph); + + if (targetExpr.kind === ast.ExprKind.Slot) { + const instanceExpr = ast.getExprInfoById( + instanceParts.pop(), + state.graph + ); + + if (instanceExpr.kind === ast.ExprKind.Element) { + // children slot provided? Then return the instance + if (targetExpr.expr.name === "children") { + return { kind: targetExpr.kind, id: instanceExpr.expr.id }; + } + + // Otherwise find an insert to use + const targetInsert = instanceExpr.expr.body.find((child) => { + return child.insert?.name === targetExpr.expr.name; + }); + + // target expr already exists? Send it! + if (targetInsert) { + return { + kind: ast.ExprKind.Insert, + id: targetInsert.insert.id, + }; + } + + // No insert found? No problem! Create one! + const result = await actions.applyChanges([ + { + appendChild: { + parentId: instanceExpr.expr.id, + childSource: ` + insert ${targetExpr.expr.name} { + + } + `, + }, + }, + ]); + + const targetId = result.changes.find((change) => { + return change.expressionInserted; + }).expressionInserted.id; + + return { kind: ast.ExprKind.Insert, id: targetId }; + } + } + + throw new Error(`Cannot resolve expression`); + }; + + const handleAddLayerMenuItemClick = async ( { payload: layerKind }: AddLayerMenuItemClicked, state: DesignerState ) => { @@ -507,8 +576,11 @@ const createEventHandler = (actions: Actions) => { // start with document for safety. Some exprs cannot be added // to parent els let parentId = getCurrentDependency(state).document.id; + const targetId = getTargetExprId(state); - const targetExpr = ast.getExprInfoById(getTargetExprId(state), state.graph); + const targetExpr = targetId + ? await resolveTargetExprId(getTargetExprId(state), state) + : { kind: ast.ExprKind.Document, id: parentId }; if ( [LayerKind.Element, LayerKind.Text].includes(layerKind) && @@ -519,7 +591,7 @@ const createEventHandler = (actions: Actions) => { ast.ExprKind.Insert, ].includes(targetExpr?.kind) ) { - parentId = targetExpr.expr.id; + parentId = targetExpr.id; } actions.applyChanges([ diff --git a/libs/designer/src/domains/ui/reducers/canvas.ts b/libs/designer/src/domains/ui/reducers/canvas.ts index d7c22f0c3..4a09cf1ac 100644 --- a/libs/designer/src/domains/ui/reducers/canvas.ts +++ b/libs/designer/src/domains/ui/reducers/canvas.ts @@ -9,24 +9,19 @@ import { getTargetExprId, highlightNode, InsertMode, - newConvertToSlotPrompt, redirect, resetCurrentDocument, } from "@paperclip-ui/designer/src/state"; -import { centerTransformZoom } from "@paperclip-ui/designer/src/state/geom"; import { routes } from "@paperclip-ui/designer/src/state/routes"; import { virtHTML } from "@paperclip-ui/core/lib/proto/virt/html-utils"; import { FileChangedKind } from "@paperclip-ui/proto/lib/generated/service/designer"; import produce from "immer"; import { clamp, mapValues } from "lodash"; import { - clampCanvasTransform, handleDoubleClick, handleDragEvent, // includeExtraRects, - MAX_ZOOM, maybeCenterCanvas, - MIN_ZOOM, panCanvas, PAN_X_SENSITIVITY, PAN_Y_SENSITIVITY, diff --git a/libs/designer/src/state/pc.ts b/libs/designer/src/state/pc.ts index 2f3c0eaa9..a21cf9bb2 100644 --- a/libs/designer/src/state/pc.ts +++ b/libs/designer/src/state/pc.ts @@ -1159,7 +1159,6 @@ export const highlightNode = ( return produce(designer, (newDesigner) => { newDesigner.canvas.mousePosition = mousePosition; const info = getNodeInfoAtCurrentPoint(designer); - newDesigner.highlightedNodeId = info?.nodeId; }); }; diff --git a/libs/designer/src/ui-testing/test.pc b/libs/designer/src/ui-testing/test.pc index c09963512..916e5f10b 100644 --- a/libs/designer/src/ui-testing/test.pc +++ b/libs/designer/src/ui-testing/test.pc @@ -13,6 +13,15 @@ public component Test { style { position: relative } + slot something { + text "test" + } + } +} + +Test { + insert something { + text "something else" } } diff --git a/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/Layers/AddLayerButton.tsx b/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/Layers/AddLayerButton.tsx index 61c58af3c..cd07e5dc4 100644 --- a/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/Layers/AddLayerButton.tsx +++ b/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/Layers/AddLayerButton.tsx @@ -3,7 +3,6 @@ import { SuggestionMenu, SuggestionMenuItem } from "../../../../SuggestionMenu"; import { LayerKind } from "@paperclip-ui/designer/src/state"; import { useDispatch } from "@paperclip-ui/common"; import { DesignerEvent } from "@paperclip-ui/designer/src/events"; -import { noop } from "lodash"; import * as styles from "../ui.pc"; import { Box } from "@paperclip-ui/designer/src/ui/layout.pc"; import { BaseAddLayerButtonProps } from "../ui.pc"; diff --git a/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/Layers/Leaf.tsx b/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/Layers/Leaf.tsx index 5a0579853..0f1b28047 100644 --- a/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/Layers/Leaf.tsx +++ b/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/Layers/Leaf.tsx @@ -17,6 +17,17 @@ import { getEntityShortcuts } from "@paperclip-ui/designer/src/domains/shortcuts type DropHotSpot = "inside" | "before" | "after"; const BORDER_MARGIN = 10; +type LeafProps = { + children?: () => any; + className: string; + id?: string; + depth: number; + text: any; + altText?: any; + controls?: any; + instanceOf?: string[]; +}; + export const Leaf = ({ children, className, @@ -26,16 +37,7 @@ export const Leaf = ({ altText, controls, instanceOf, -}: { - children?: () => any; - className: string; - id?: string; - depth: number; - text: any; - altText?: any; - controls?: any; - instanceOf: string[]; -}) => { +}: LeafProps) => { const { selected, open, @@ -47,7 +49,7 @@ export const Leaf = ({ style, dropHotSpot, } = useLeaf({ - exprId: id, + targetId: id, instanceOf, }); @@ -83,21 +85,21 @@ export const Leaf = ({ }; const useLeaf = ({ - exprId, + targetId, instanceOf, }: { - exprId: string; + targetId: string; instanceOf: string[]; }) => { - const virtId = exprId && [...(instanceOf || []), exprId].join("."); + const virtId = targetId && [...(instanceOf || []), targetId].join("."); const open = useSelector(getExpandedVirtIds).includes(virtId); const selectedId = useSelector(getTargetExprId); const graph = useSelector(getGraph); const contextMenu = useCallback( - () => getEntityShortcuts(exprId, graph), - [exprId, graph] + () => getEntityShortcuts(targetId, graph), + [targetId, graph] ); const selected = selectedId === virtId; @@ -109,7 +111,7 @@ const useLeaf = ({ const [{ opacity }, dragRef] = useDrag( () => ({ type: DNDKind.Node, - item: { id: exprId }, + item: { id: targetId }, collect: (monitor) => ({ opacity: monitor.isDragging() ? 0.5 : 1, cursor: monitor.isDragging() ? "copy" : "initial", @@ -129,7 +131,7 @@ const useLeaf = ({ let isTop = offset.y < rect.top + BORDER_MARGIN; let isBottom = offset.y > rect.bottom - BORDER_MARGIN; - const expr = ast.getExprInfoById(exprId, graph); + const expr = ast.getExprInfoById(targetId, graph); const draggedExpr = ast.getExprInfoById(draggedExprId, graph); // can only insert before or after text nodes @@ -156,20 +158,20 @@ const useLeaf = ({ type: "ui/exprNavigatorDroppedNode", payload: { position: dropHotSpot, - targetId: exprId, + targetId, droppedExprId: item.id, }, }); }, canDrop({ id: draggedExprId }, _monitor) { - if (draggedExprId === exprId) { + if (draggedExprId === targetId) { return false; } // don't allow dropping a node into a node that is already in it const draggedExpr = ast.getExprInfoById(draggedExprId, graph); - return ast.flattenExpressionInfo(draggedExpr)[exprId] == null; + return ast.flattenExpressionInfo(draggedExpr)[targetId] == null; }, collect(monitor) { return { diff --git a/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/Layers/controller.tsx b/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/Layers/controller.tsx index c94b649f9..8942739b6 100644 --- a/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/Layers/controller.tsx +++ b/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/Layers/controller.tsx @@ -166,11 +166,6 @@ const NodeLeaf = memo(({ expr: node, depth, instanceOf }: LeafProps) => { /> ); } - if (node.insert) { - return ( - - ); - } return null; }); @@ -202,12 +197,29 @@ const InstanceLeaf = ({ }: LeafProps) => { const graph = useSelector(getGraph); const component = ast.getInstanceComponent(instance, graph); + const slots = ast.getComponentSlots(component, graph); + + const instanceSlots = slots.reduce((map, slot) => { + map[slot.name] = { + targetId: `${instance.id}.${slot.id}`, + insert: [], + }; + return map; + }, {} as Record); + + for (const child of instance.body) { + if (child.insert) { + instanceSlots[child.insert.name!].insert = child.insert.body; + } else if (instanceSlots.children) { + instanceSlots.children.insert.push(child); + } + } return ( 0, + container: slots.length > 0, })} text={<>{instance.name || "Instance"}} altText={{instance.tagName}} @@ -217,14 +229,18 @@ const InstanceLeaf = ({ {() => { return ( <> - {instance.body.map((child) => ( - - ))} + {Object.keys(instanceSlots).map((key) => { + const { targetId, insert } = instanceSlots[key]; + return ( + + ); + })} ); }} @@ -354,24 +370,30 @@ const ConditionLeaf = memo( } ); +type InsertLeafProps = { + insert: Node[]; + name: string; + targetId: string; + depth: number; +}; + const InsertLeaf = memo( - ({ expr: insert, depth, instanceOf }: LeafProps) => { + ({ targetId, insert, depth, name }: InsertLeafProps) => { return ( + // instance.slot denotes virtual ID which DOES exist 0 })} - text={insert.name} + id={targetId} + className={cx("slot", { container: insert.length > 0 })} + text={name} depth={depth} - instanceOf={instanceOf} > {() => ( <> - {insert.body.map((child) => ( + {insert.map((child) => ( ))} diff --git a/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/controller.tsx b/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/controller.tsx index e5c04ecf0..8b2eb048a 100644 --- a/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/controller.tsx +++ b/libs/designer/src/ui/logic/Editor/EditorPanels/LeftSidebar/controller.tsx @@ -9,7 +9,8 @@ export const LeftSidebar = (Base: React.FC) => () => { if (!show) { return null; } - return + + return ; }; const useLeftSidebar = () => { diff --git a/libs/proto/src/ast/pc.rs b/libs/proto/src/ast/pc.rs index 64eacda88..cf8eebaf2 100644 --- a/libs/proto/src/ast/pc.rs +++ b/libs/proto/src/ast/pc.rs @@ -458,7 +458,6 @@ impl TryFrom<&Node> for Script { } } } - impl TryFrom for Node { type Error = (); diff --git a/libs/tidy/Cargo.toml b/libs/tidy/Cargo.toml new file mode 100644 index 000000000..d642fa5bc --- /dev/null +++ b/libs/tidy/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "paperclip_tidy" +version = "0.1.1" +authors = ["Craig Condon "] +edition = "2021" +exclude = [] + +[dependencies] +paperclip_project = { path = "../project"} +paperclip_proto = { path = "../proto", features = ["transport"] } +paperclip_core = { path = "../core"} +paperclip_validate = { path = "../validate" } diff --git a/libs/tidy/src/lib.rs b/libs/tidy/src/lib.rs new file mode 100644 index 000000000..4cd343a62 --- /dev/null +++ b/libs/tidy/src/lib.rs @@ -0,0 +1 @@ +// use paperclip_proto::ast::graph_ext::Graph;