diff --git a/.circleci/config.yml b/.circleci/config.yml index 4d219ba911..e1a6e629fe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,11 +11,11 @@ jobs: - image: cimg/redis:7.0.5 working_directory: ~/repo - resource_class: large + resource_class: medium+ environment: - # Customize the JVM maximum heap limit - JVM_OPTS: -Xmx4g + JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC + NODE_OPTIONS: --max-old-space-size=4096 steps: - checkout @@ -28,50 +28,82 @@ jobs: - v1-dependencies- - run: cd .clj-kondo && cat config.edn + - run: cat .cljfmt.edn + - run: clj-kondo --version - run: - name: frontend styles prettier + name: "fmt check backend [clj]" + working_directory: "./backend" + command: | + yarn install + yarn run fmt:clj:check + + - run: + name: "fmt check exporter [clj]" + working_directory: "./exporter" + command: | + yarn install + yarn run fmt:clj:check + + - run: + name: "fmt check common [clj]" + working_directory: "./common" + command: | + yarn install + yarn run fmt:clj:check + + - run: + name: "fmt check frontend [clj]" working_directory: "./frontend" command: | yarn install - yarn run lint-scss + yarn run fmt:clj:check - run: name: common lint working_directory: "./common" command: | - clj-kondo --version - clj-kondo --parallel --lint src/ + yarn install + yarn run lint:clj - run: name: frontend lint working_directory: "./frontend" command: | - clj-kondo --version - clj-kondo --parallel --lint src/ + yarn install + yarn run lint:scss + yarn run lint:clj - run: name: backend lint working_directory: "./backend" command: | - clj-kondo --version - clj-kondo --parallel --lint src/ + yarn install + yarn run lint:clj - run: - working_directory: "./common" - name: common tests + name: exporter lint + working_directory: "./exporter" command: | yarn install + yarn run lint:clj + + - run: + name: "common tests" + working_directory: "./common" + command: | yarn test clojure -X:dev:test :patterns '["common-tests.*-test"]' - environment: - PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin - JVM_OPTS: -Xmx4g - NODE_OPTIONS: --max-old-space-size=4096 + - run: + name: "frontend tests" + working_directory: "./frontend" + command: | + yarn install + yarn test - run: - name: backend test + name: "backend tests" working_directory: "./backend" command: | clojure -X:dev:test :patterns '["backend-tests.*-test"]' @@ -81,18 +113,6 @@ jobs: PENPOT_TEST_DATABASE_USERNAME: penpot_test PENPOT_TEST_DATABASE_PASSWORD: penpot_test PENPOT_TEST_REDIS_URI: "redis://localhost/1" - JVM_OPTS: -Xmx4g - - - run: - name: frontend tests - working_directory: "./frontend" - command: | - yarn install - yarn test - - environment: - PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin - NODE_OPTIONS: --max-old-space-size=4096 - save_cache: paths: diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 29d5e04b96..fe1d14d908 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -4,7 +4,8 @@ promesa.core/-> clojure.core/-> promesa.exec.csp/go-loop clojure.core/loop rumext.v2/defc clojure.core/defn - rumext.v2/fnc clojure.core/fn + promesa.util/with-open clojure.core/with-open + app.common.schema.generators/let clojure.core/let app.common.data/export clojure.core/def app.common.data.macros/get-in clojure.core/get-in app.common.data.macros/with-open clojure.core/with-open @@ -14,17 +15,28 @@ :hooks {:analyze-call {app.common.data.macros/export hooks.export/export - potok.core/reify hooks.export/potok-reify app.util.services/defmethod hooks.export/service-defmethod + app.common.record/defrecord hooks.export/penpot-defrecord app.db/with-atomic hooks.export/penpot-with-atomic + potok.v2.core/reify hooks.export/potok-reify + rumext.v2/fnc hooks.export/rumext-fnc + rumext.v2/lazy-component hooks.export/rumext-lazycomponent + shadow.lazy/loadable hooks.export/rumext-lazycomponent }} :output {:exclude-files ["data_readers.clj" - "app/util/perf.cljs" - "app/common/logging.cljc" - "app/common/exceptions.cljc"]} + "src/app/util/perf.cljs" + "src/app/common/logging.cljc" + "src/app/common/exceptions.cljc" + "^(?:backend|frontend|exporter|common)/build.clj" + "^(?:backend|frontend|exporter|common)/deps.edn" + "^(?:backend|frontend|exporter|common)/scripts/" + "^(?:backend|frontend|exporter|common)/dev/" + "^(?:backend|frontend|exporter|common)/test/"] + + :linter-name true} :linters {:unsorted-required-namespaces @@ -60,4 +72,3 @@ :exclude-destructured-keys-in-fn-args false } }} - diff --git a/.clj-kondo/hooks/export.clj b/.clj-kondo/hooks/export.clj index 71a8375979..a209cf018f 100644 --- a/.clj-kondo/hooks/export.clj +++ b/.clj-kondo/hooks/export.clj @@ -37,22 +37,53 @@ (api/token-node rsym) (api/vector-node [])] other))] + + ;; (prn (api/sexpr result)) + {:node result}))) (defn penpot-with-atomic [{:keys [node]}] - (let [[_ params & other] (:children node) + (let [[params & body] (rest (:children node))] + (if (api/vector-node? params) + (let [[sym val opts] (:children params)] + (when-not (and sym val) + (throw (ex-info "No sym and val provided" {}))) + {:node (api/list-node + (list* + (api/token-node 'let) + (api/vector-node [sym val]) + opts + body))}) - result (if (api/vector-node? params) - (api/list-node - (into [(api/token-node (symbol "clojure.core" "with-open")) params] other)) - (api/list-node - (into [(api/token-node (symbol "clojure.core" "with-open")) - (api/vector-node [params params])] - other))) + {:node (api/list-node + (into [(api/token-node 'let) + (api/vector-node [params params])] + body))}))) + +(defn rumext-fnc + [{:keys [node]}] + (let [[cname mdata params & body] (rest (:children node)) + [params body] (if (api/vector-node? mdata) + [mdata (cons params body)] + [params body])] + (let [result (api/list-node + (into [(api/token-node 'fn) + params] + (cons mdata body)))] + {:node result}))) + + +(defn rumext-lazycomponent + [{:keys [node]}] + (let [[cname mdata params & body] (rest (:children node)) + [params body] (if (api/vector-node? mdata) + [mdata (cons params body)] + [params body])] + (let [result (api/list-node [(api/token-node 'constantly) nil])] + ;; (prn (api/sexpr result)) + {:node result}))) - ] - {:node result})) (defn penpot-defrecord [{:keys [:node]}] diff --git a/.cljfmt.edn b/.cljfmt.edn new file mode 100644 index 0000000000..38cfeb89b6 --- /dev/null +++ b/.cljfmt.edn @@ -0,0 +1,9 @@ +{:sort-ns-references? true + :remove-multiple-non-indenting-spaces? false + :remove-surrounding-whitespace? true + :remove-consecutive-blank-lines? false + :extra-indents {rumext.v2/fnc [[:inner 0]] + cljs.test/async [[:inner 0]] + promesa.exec/thread [[:inner 0]] + specify! [[:inner 0] [:inner 1]]} + } diff --git a/.gitignore b/.gitignore index 37d2807264..0e271d1257 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,16 @@ +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions *-init.clj +*.css.json *.jar *.orig *.penpot +*.css.json .calva .clj-kondo .cpcache @@ -14,6 +23,7 @@ /*.jpg /*.md /*.png +/*.svg /*.sql /*.txt /*.yml @@ -57,3 +67,4 @@ /web clj-profiler/ node_modules +frontend/.storybook/preview-body.html diff --git a/.vscode/settings.json b/.vscode/settings.json index 6cef7d76f2..b57665d559 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { - "files.exclude": { - "**/.clj-kondo": true, - "**/.cpcache": true, - "**/.lsp": true, - "**/.shadow-cljs": true, - "**/node_modules": true - } + "files.exclude": { + "**/.clj-kondo": true, + "**/.cpcache": true, + "**/.lsp": true, + "**/.shadow-cljs": true, + "**/node_modules": true + } } diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000000..896c0eefc0 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,9 @@ +enableGlobalCache: true + +enableImmutableCache: false + +enableImmutableInstalls: false + +enableTelemetry: false + +nodeLinker: node-modules diff --git a/CHANGES.md b/CHANGES.md index ae7fa2d073..f80e972457 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,137 @@ # CHANGELOG +## 2.0.0 - I Just Can't Get Enough + +### :rocket: Epics and highlights +- Grid CSS layout [Taiga #4915](https://tree.taiga.io/project/penpot/epic/4915) +- UI redesign [Taiga #4958](https://tree.taiga.io/project/penpot/epic/4958) +- New components System [Taiga #2662](https://tree.taiga.io/project/penpot/epic/2662) +- Swap components [Taiga #1331](https://tree.taiga.io/project/penpot/us/1331) +- Images as fill [Taiga #2983](https://tree.taiga.io/project/penpot/us/2983) +- HTML code generation [Taiga #5277](https://tree.taiga.io/project/penpot/us/5277) +- Light and dark themes [Taiga #2287](https://tree.taiga.io/project/penpot/us/2287) + +### :boom: Breaking changes & Deprecations + +- New strokes default to inside border [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847) + +### :heart: Community contributions (Thank you!) +- New Hausa, Yoruba and Igbo translations and update translation files (by All For Tech Empowerment Foundation) [Taiga #6950](https://tree.taiga.io/project/penpot/us/6950), [Taiga #6534](https://tree.taiga.io/project/penpot/us/6534) +- Hide bounding-box when editing shape (by @VasilevsVV) [#3930](https://github.com/penpot/penpot/pull/3930) +- CTRL + "+" to zoom into canvas instead of browser (by @audriu) [#3848](https://github.com/penpot/penpot/pull/3848) +- Add dev deps.edn in the project root (by @PEZ) [#3794](https://github.com/penpot/penpot/pull/3794) +- Allow passing overrides to frontend nginx config (by @m90) [#3602](https://github.com/penpot/penpot/pull/3602) +- Update index.njk to remove typo (by @fdvmoreira) [#155](https://github.com/penpot/penpot-docs/pull/155) +- Typo (by StephanEggermont) [#157](https://github.com/penpot/penpot-docs/pull/157) + +### :sparkles: New features +- Send comments with Ctrl+Enter / Cmd + Enter [Taiga #6085](https://tree.taiga.io/project/penpot/issue/6085) +- Select through stroke only rectangle [Taiga #5484](https://tree.taiga.io/project/penpot/issue/5484) +- Stroke default position [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847) +- Override browser Ctrl+ and Ctrl- zoom with Penpot Zoom [Taiga #3200](https://tree.taiga.io/project/penpot/us/3200) +- Improve the way handlers work on flex layouts [Taiga #6598](https://tree.taiga.io/project/penpot/us/6598) +- Add menu entry for toggle between light/dark theme [Taiga #6829](https://tree.taiga.io/project/penpot/issue/6829) +- Switch themes shortcut [Taiga #6644](https://tree.taiga.io/project/penpot/us/6644) +- Constraints section at design tab new position [Taiga #6830](https://tree.taiga.io/project/penpot/issue/6830) +- [PICKER] File library colors order [Taiga #5399](https://tree.taiga.io/project/penpot/us/5399) +- Onboarding invitations improvements [Taiga #5974](https://tree.taiga.io/project/penpot/us/5974) +- [PERFORMANCE] Workspace thumbnails refactor [Taiga #5828](https://tree.taiga.io/project/penpot/us/5828) +- [PERFORMANCE] Add performance optimizations to shape rendering [Taiga #5835](https://tree.taiga.io/project/penpot/us/5835) +- [PERFORMANCE] Optimize SVG output [Taiga #4134](https://tree.taiga.io/project/penpot/us/4134) +- [PERFORMANCE] Optimize svg on importation [Taiga #5879](https://tree.taiga.io/project/penpot/us/5879) +- [PERFORMANCE] Optimization tasks related to design tab file [Taiga #5760](https://tree.taiga.io/project/penpot/us/5760) +- [INSTALL] Ability to setup features by team [Taiga #6108](https://tree.taiga.io/project/penpot/us/6108) +- [IMAGES] Keep aspect ratio option [Taiga #6933](https://tree.taiga.io/project/penpot/us/6933) +- [INSPECT] UI review [Taiga #5687](https://tree.taiga.io/project/penpot/us/5687) +- [GRID LAYOUT] Phase 1 [Taiga #4303](https://tree.taiga.io/project/penpot/us/4303) +- [GRID LAYOUT] Inspect code for Grid [Taiga #5277](https://tree.taiga.io/project/penpot/us/5277) +- [GRID LAYOUT] Phase 1 polishing [Taiga #5612](https://tree.taiga.io/project/penpot/us/5612) +- [GRID LAYOUT] Improvements & Feedback [Taiga #6047](https://tree.taiga.io/project/penpot/us/6047) +- [COMPONENTS] Naming of the main component [Taiga #5291](https://tree.taiga.io/project/penpot/us/5291) +- [COMPONENTS] Rework inside of components - Library page [Taiga #2918](https://tree.taiga.io/project/penpot/us/2918) +- [COMPONENTS] Update component when updating main instance [Taiga #3794](https://tree.taiga.io/project/penpot/us/3794) +- [COMPONENTS] Main component new behavior [Taiga #3796](https://tree.taiga.io/project/penpot/us/3796) +- [COMPONENTS] Main component look & feel [Taiga #5290](https://tree.taiga.io/project/penpot/us/5290) +- [COMPONENTS] Library view [Taiga #2880](https://tree.taiga.io/project/penpot/us/2880) +- [COMPONENTS] Positioning inside a component should relative, as in boards [Taiga #2826](https://tree.taiga.io/project/penpot/us/2826) +- [COMPONENTS] Update message should show only if affecting at components that are being used at a file [Taiga #1397](https://tree.taiga.io/project/penpot/us/1397) +- [COMPONENTS] Annotations [Taiga #4957](https://tree.taiga.io/project/penpot/us/4957) +- [COMPONENTS] Synchronization order for nested components [Taiga #5439](https://tree.taiga.io/project/penpot/us/5439) +- [COMPONENTS] Libraries modal zero case [Taiga #5294](https://tree.taiga.io/project/penpot/us/5294) +- [COMPONENTS] Contextual menu casuistics [Taiga #5292](https://tree.taiga.io/project/penpot/us/5292) +- [COMPONENTS] Libraries publishing flow review [Taiga #5293](https://tree.taiga.io/project/penpot/us/5293) +- [COMPONENTS] Add loading text to Libraries modal [Taiga #6702](https://tree.taiga.io/project/penpot/us/6702) +- [COMPONENTS] Components rename and organization in bulk [Taiga #2877](https://tree.taiga.io/project/penpot/us/2877) +- [COMPONENTS] Info overlay about components V2 [Taiga #6276](https://tree.taiga.io/project/penpot/us/6276) +- [REDESIGN] New styles basics [Taiga #4967](https://tree.taiga.io/project/penpot/us/4967) +- [REDESIGN] Layers tab redesign [Taiga #4966](https://tree.taiga.io/project/penpot/us/4966) +- [REDESIGN] Design tab phase 1 [Taiga #4982](https://tree.taiga.io/project/penpot/us/4966) +- [REDESIGN] Assets tab redesign [Taiga #4984](https://tree.taiga.io/project/penpot/us/4984) +- [REDESIGN] Palette panels (colors, typographies...) [Taiga #4983](https://tree.taiga.io/project/penpot/us/4983) +- [REDESIGN] Workspace structure [Taiga #4988](https://tree.taiga.io/project/penpot/us/4988) +- [REDESIGN] Shortcut tab [Taiga #4989](https://tree.taiga.io/project/penpot/us/4989) +- [REDESIGN] Toolbar [Taiga #5500](https://tree.taiga.io/project/penpot/us/5500) +- [REDESIGN] History tab [Taiga #5481](https://tree.taiga.io/project/penpot/us/5481) +- [REDESIGN] Path options/toolbar [Taiga #5815](https://tree.taiga.io/project/penpot/us/5815) +- [REDESIGN] Design tab phase 2 [Taiga #5814](https://tree.taiga.io/project/penpot/us/5814) +- [REDESIGN] Design tab phase 3 and dashboard details [Taiga #5920](https://tree.taiga.io/project/penpot/us/5920) +- [REDESIGN] Dashboard [Taiga #5164](https://tree.taiga.io/project/penpot/us/5164) +- [REDESIGN] New Dashboard UI [Taiga #5869](https://tree.taiga.io/project/penpot/us/5869) +- [REDESIGN] Prototype tab [Taiga #4985](https://tree.taiga.io/project/penpot/us/4985) +- [REDESIGN] Code tab [Taiga #4986](https://tree.taiga.io/project/penpot/us/4986) +- [REDESIGN] Modals and alert messages [Taiga #5915](https://tree.taiga.io/project/penpot/us/5915) +- [REDESIGN] Comments page [Taiga #5917](https://tree.taiga.io/project/penpot/us/5917) +- [REDESIGN] View Mode [Taiga #5163](https://tree.taiga.io/project/penpot/us/5163) +- [REDESIGN] Miscellaneous tasks [Taiga #6050](https://tree.taiga.io/project/penpot/us/6050) +- [REDESIGN] Swap components [Taiga #6739](https://tree.taiga.io/project/penpot/us/6739) +- [REDESIGN] Font selector [Taiga #6677](https://tree.taiga.io/project/penpot/us/6677) +- [REDESIGN] Colour system of alerts and notifications [Taiga #6746](https://tree.taiga.io/project/penpot/us/6746) +- [REDESIGN] Review text in paragraphs for accessibility [Taiga #6703](https://tree.taiga.io/project/penpot/us/6703) +- [REDESIGN] Interaction icons [Taiga #6880](https://tree.taiga.io/project/penpot/us/6880) +- [REDESIGN] Panels visual separations [Taiga #6692](https://tree.taiga.io/project/penpot/us/6692) +- [REDESIGN] Onboarding slides [Taiga #6678](https://tree.taiga.io/project/penpot/us/6678) + +### :bug Bugs fixed +- Fix pixelated thumbnails [Github #3681](https://github.com/penpot/penpot/issues/3681), [Github #3661](https://github.com/penpot/penpot/issues/3661) +- Fix problem with not applying colors to boards [Github #3941](https://github.com/penpot/penpot/issues/3941) +- Fix problem with path editor undoing changes [Github #3998](https://github.com/penpot/penpot/issues/3998) +- [View mode] Open overlay places frame in the wrong position when paired with a fixed element [Taiga #6385](https://tree.taiga.io/project/penpot/issue/6385) +- Flex Layout: Fit-content not recalculated after deleting an element [Taiga #5968](https://tree.taiga.io/project/penpot/issue/5968) +- Selecting from Color Palette does not work for board when there is no existing fill [Taiga #6464](https://tree.taiga.io/project/penpot/issue/6464) +- Color thumbnails are consistently rounded in the inspect code mode [Taiga #5886](https://tree.taiga.io/project/penpot/issue/5886) +- Adding vector path points before first point of existing open path not working [Taiga #6593](https://tree.taiga.io/project/penpot/issue/6593) +- Some image formats include the extension when importing [Taiga #5485](https://tree.taiga.io/project/penpot/issue/5485) +- Gradient color tool doesn't work properly with flipped items [Taiga #6485](https://tree.taiga.io/project/penpot/issue/6485) +- [TEXT] Align options are not shown when several text are selected [Taiga #5948](https://tree.taiga.io/project/penpot/issue/5948) +- [VIEW MODE] Comments not working properly on multiple pages [Taiga #6281](https://tree.taiga.io/project/penpot/issue/6281) +- [PERFORMANCE] Alignments are slow [Taiga #5865](https://tree.taiga.io/project/penpot/issue/5865) +- [EXPORT] Exporting an element with a non-visible drop shadow displays the shadow either way [Taiga #6768](https://tree.taiga.io/project/penpot/issue/6768) +- [SAFARI] Color picker cursor is not pointing correctly [Taiga #6733](https://tree.taiga.io/project/penpot/issue/6733) +- [Import Files] When user has imported .penpot file with new file name of previously downloaded library file the default library file name is set for it [Taiga #5596](https://tree.taiga.io/project/penpot/issue/5596) +- Issue when resizing a duotone FA icon [Taiga #5935](https://tree.taiga.io/project/penpot/issue/5935) +- "Hide grid" keyboard shortcut broken [Taiga #5102](https://tree.taiga.io/project/penpot/issue/5102) +- Picking a gradient color in recent colors for a new color in the assets tab crashes Penpot [Taiga #5601](https://tree.taiga.io/project/penpot/issue/5601) +- Thumbnails not loading [Taiga #6012](https://tree.taiga.io/project/penpot/issue/6012) +- Don't show signup link/form when registration is disabled. [Taiga #1196](https://tree.taiga.io/project/penpot/issue/1196) +- Registration Page UI UX issue with small resolutions [Taiga #1693](https://tree.taiga.io/project/penpot/issue/1693) +- [LOGIN] "E-Mail-Adress" input field is set to type 'text' instead of 'eMail [Taiga #1921](https://tree.taiga.io/project/penpot/issue/1921) +- Handling correctly slashes "/" in emails [Taiga #4906](https://tree.taiga.io/project/penpot/issue/4906) +- Tab character in texts crashes the app [Taiga #4418](https://tree.taiga.io/project/penpot/issue/4418) +- Text does not match export [Taiga #4129](https://tree.taiga.io/project/penpot/issue/4129) +- Scrollbars cover the layers carets [Taiga #4431](https://tree.taiga.io/project/penpot/issue/4431) +- Horizontal ruler disappear when overlapping a board [Taiga #4138](https://tree.taiga.io/project/penpot/issue/4138) +- Resize shape + Alt key is not working [Taiga #3447](https://tree.taiga.io/project/penpot/issue/3447) +- Libraries images broken on premise [Taiga #4573](https://tree.taiga.io/project/penpot/issue/4573) +- [VIEWER] Cannot scroll down in code mode [Taiga #4655](https://tree.taiga.io/project/penpot/issue/4655) +- Strange cursor behavior after clicking viewport with text tool [Taiga #4363](https://tree.taiga.io/project/penpot/issue/4363) +- Selected color affects all of them [Taiga #5285](https://tree.taiga.io/project/penpot/issue/5285) +- Fix problem with shadow negative spread [Github #3421](https://github.com/penpot/penpot/issues/3421) +- Fix problem with linked colors to strokes [Github #3522](https://github.com/penpot/penpot/issues/3522) +- Fix problem with hand tool stuck [Github #3318](https://github.com/penpot/penpot/issues/3318) +- Fix problem with fix scrolling on nested elements [Github #3508](https://github.com/penpot/penpot/issues/3508) +- Fix problem when changing typography assets [Github #3683](https://github.com/penpot/penpot/issues/3683) + + ## 1.19.5 ### :bug: New features @@ -71,6 +203,8 @@ - Add support for local caching of google fonts (this avoids exposing the final user IP to goolge and reduces the amount of request sent to google) - Set smooth/instant autoscroll depending on distance [GitHub #3377](https://github.com/penpot/penpot/issues/3377) +- New component icon [Taiga #5290](https://tree.taiga.io/project/penpot/us/5290) +- Show a confirmation dialog when an user tries to publish an empty library [Taiga #5294](https://tree.taiga.io/project/penpot/us/5294) ### :bug: Bugs fixed @@ -132,6 +266,7 @@ - Fix create typography with section closed [Taiga #5574](https://tree.taiga.io/project/penpot/issue/5574) - Fix exports menu on viewer mode [Taiga #5568](https://tree.taiga.io/project/penpot/issue/5568) - Fix create empty comments [Taiga #5536](https://tree.taiga.io/project/penpot/issue/5536) +- Fix text changes not propagated to copy [Taiga #5364](https://tree.taiga.io/project/penpot/issue/5364) - Fix position of text cursor is a bit too high in Invitations section [Taiga #5511](https://tree.taiga.io/project/penpot/issue/5511) - Fix undo when updating several texts [Taiga #5197](https://tree.taiga.io/project/penpot/issue/5197) - Fix assets right click button for multiple selection [Taiga #5545](https://tree.taiga.io/project/penpot/issue/5545) diff --git a/README.md b/README.md index 960d7dcd0f..0b66280176 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ ![feature-readme](https://user-images.githubusercontent.com/1045247/189871786-0b44f7cf-3a0a-4445-a87b-9919ec398bf7.gif) -**:tada: [Important Notice!] :tada:** Our very first **Penpot Fest** is happening on June 28-30, Barcelona (Spain). **Secure yourself a ticket** to know everything about the present and future of Penpot and be part of the conversation! See details on the amazing venue and speakers lineup at [penpotfest.org](https://penpotfest.org)! :zap: +🎇 **Penpot Fest exceeded all expectations - it was a complete success!** 🎇 Penpot Fest is our first Design event that brought designers and developers from the Open Source communities and beyond. Watch the replay of the talks on our [Youtube channel](https://www.youtube.com/playlist?list=PLgcCPfOv5v56-fghJo2dHNBqL9zlDTslh) or [Peertube channel](https://peertube.kaleidos.net/w/p/1tWgyJTt8sKbWwCEcBimZW) Penpot is the first **Open Source** design and prototyping platform meant for cross-domain teams. Non dependent on operating systems, Penpot is web based and works with open standards (SVG). Penpot invites designers all over the world to fall in love with open source while getting developers excited about the design process in return. @@ -109,6 +109,7 @@ Every sort of contribution will be very helpful to enhance Penpot. How you’ll - Create and [share Libraries & templates](https://penpot.app/libraries-templates.html) that will be helpful for the community - Become a [translator](https://help.penpot.app/contributing-guide/translations) - Give feedback: [Mail us](mailto:support@penpot.app) +- **Contribute to Penpot's code:** [Watch this video](https://www.youtube.com/watch?v=TpN0osiY-8k) by Alejandro Alonso, CIO and developer at Penpot, where he gives us a hands-on demo of how to use Penpot’s repository and make changes in both front and back end To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing-guide](https://help.penpot.app/contributing-guide/). diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000000..836120c1e2 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,7 @@ +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/backend/deps.edn b/backend/deps.edn index 5bf32ab958..afd1e6840b 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -3,10 +3,10 @@ :deps {penpot/common {:local/root "../common"} - org.clojure/clojure {:mvn/version "1.11.1"} - org.clojure/core.async {:mvn/version "1.6.673"} + org.clojure/clojure {:mvn/version "1.12.0-alpha5"} + org.clojure/tools.namespace {:mvn/version "1.4.4"} - com.github.luben/zstd-jni {:mvn/version "1.5.5-4"} + com.github.luben/zstd-jni {:mvn/version "1.5.5-11"} io.prometheus/simpleclient {:mvn/version "0.16.0"} io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"} @@ -17,30 +17,33 @@ io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"} - io.lettuce/lettuce-core {:mvn/version "6.2.4.RELEASE"} + io.lettuce/lettuce-core {:mvn/version "6.3.0.RELEASE"} java-http-clj/java-http-clj {:mvn/version "0.4.3"} funcool/yetti - {:git/tag "v9.16" - :git/sha "7df3e08" + {:git/tag "v10.0" + :git/sha "520613f" :git/url "https://github.com/funcool/yetti.git" :exclusions [org.slf4j/slf4j-api]} - com.github.seancorfield/next.jdbc {:mvn/version "1.3.883"} + com.github.seancorfield/next.jdbc {:mvn/version "1.3.909"} metosin/reitit-core {:mvn/version "0.6.0"} + nrepl/nrepl {:mvn/version "1.1.0"} + cider/cider-nrepl {:mvn/version "0.44.0"} - org.postgresql/postgresql {:mvn/version "42.6.0"} + org.postgresql/postgresql {:mvn/version "42.7.1"} + org.xerial/sqlite-jdbc {:mvn/version "3.44.1.0"} - com.zaxxer/HikariCP {:mvn/version "5.0.1"} + com.zaxxer/HikariCP {:mvn/version "5.1.0"} io.whitfin/siphash {:mvn/version "2.0.0"} buddy/buddy-hashers {:mvn/version "2.0.167"} buddy/buddy-sign {:mvn/version "3.5.351"} - com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.6"} + com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.8"} - org.jsoup/jsoup {:mvn/version "1.16.1"} + org.jsoup/jsoup {:mvn/version "1.17.2"} org.im4java/im4java {:git/tag "1.4.0-penpot-2" :git/sha "e2b3e16" @@ -49,14 +52,13 @@ org.lz4/lz4-java {:mvn/version "1.8.0"} org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"} - integrant/integrant {:mvn/version "0.8.1"} dawran6/emoji {:mvn/version "0.1.5"} - markdown-clj/markdown-clj {:mvn/version "1.11.4"} + markdown-clj/markdown-clj {:mvn/version "1.11.7"} ;; Pretty Print specs pretty-spec/pretty-spec {:mvn/version "0.1.4"} - software.amazon.awssdk/s3 {:mvn/version "2.20.96"} + software.amazon.awssdk/s3 {:mvn/version "2.22.12"} } :paths ["src" "resources" "target/classes"] @@ -64,7 +66,6 @@ {:dev {:extra-deps {com.bhauman/rebel-readline {:mvn/version "RELEASE"} - org.clojure/tools.namespace {:mvn/version "RELEASE"} clojure-humanize/clojure-humanize {:mvn/version "0.2.2"} org.clojure/data.csv {:mvn/version "RELEASE"} com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"} @@ -73,7 +74,7 @@ :build {:extra-deps - {io.github.clojure/tools.build {:git/tag "v0.9.3" :git/sha "e537cd1"}} + {io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}} :ns-default build} :test diff --git a/backend/dev/user.clj b/backend/dev/user.clj index 1b02c6ea00..9fc59d5e11 100644 --- a/backend/dev/user.clj +++ b/backend/dev/user.clj @@ -7,7 +7,9 @@ (ns user (:require [app.common.data :as d] + [app.common.debug :as debug] [app.common.exceptions :as ex] + [app.common.files.helpers :as cfh] [app.common.fressian :as fres] [app.common.geom.matrix :as gmt] [app.common.logging :as l] @@ -19,10 +21,12 @@ [app.common.schema.generators :as sg] [app.common.spec :as us] [app.common.transit :as t] + [app.common.types.file :as ctf] [app.common.uuid :as uuid] - [app.config :as cfg] + [app.config :as cf] + [app.db :as db] [app.main :as main] - [app.srepl.helpers] + [app.srepl.helpers :as srepl.helpers] [app.srepl.main :as srepl] [app.util.blob :as blob] [app.util.json :as json] @@ -40,7 +44,7 @@ [clojure.walk :refer [macroexpand-all]] [criterium.core :as crit] [cuerdas.core :as str] - [datoteka.core] + [datoteka.fs :as fs] [integrant.core :as ig] [malli.core :as m] [malli.dev.pretty :as mdp] @@ -48,12 +52,15 @@ [malli.generator :as mg] [malli.registry :as mr] [malli.transform :as mt] - [malli.util :as mu])) + [malli.util :as mu] + [promesa.exec :as px])) (repl/disable-reload! (find-ns 'integrant.core)) +(repl/disable-reload! (find-ns 'app.common.debug)) + (set! *warn-on-reflection* true) -(defonce system nil) +(add-tap #'debug/tap-handler) ;; --- Benchmarking Tools @@ -92,20 +99,14 @@ (defn- start [] (try - (alter-var-root #'system (fn [sys] - (when sys (ig/halt! sys)) - (-> (merge main/system-config main/worker-config) - (ig/prep) - (ig/init)))) + (main/start) :started (catch Throwable cause (ex/print-throwable cause)))) (defn- stop [] - (alter-var-root #'system (fn [sys] - (when sys (ig/halt! sys)) - nil)) + (main/stop) :stopped) (defn restart @@ -118,62 +119,29 @@ (stop) (repl/refresh-all :after 'user/start)) -(defn compression-bench - [data] - (let [humanize (fn [v] (hum/filesize v :binary true :format " %.4f ")) - v1 (time (humanize (alength (blob/encode data {:version 1})))) - v3 (time (humanize (alength (blob/encode data {:version 3})))) - v4 (time (humanize (alength (blob/encode data {:version 4})))) - v5 (time (humanize (alength (blob/encode data {:version 5})))) - v6 (time (humanize (alength (blob/encode data {:version 6})))) - ] - (print-table - [{ - :v1 v1 - :v3 v3 - :v4 v4 - :v5 v5 - :v6 v6 - }]))) - -(defonce debug-tap - (do - (add-tap #(locking debug-tap - (prn "tap debug:" %))) - 1)) +;; (defn compression-bench +;; [data] +;; (let [humanize (fn [v] (hum/filesize v :binary true :format " %.4f ")) +;; v1 (time (humanize (alength (blob/encode data {:version 1})))) +;; v3 (time (humanize (alength (blob/encode data {:version 3})))) +;; v4 (time (humanize (alength (blob/encode data {:version 4})))) +;; v5 (time (humanize (alength (blob/encode data {:version 5})))) +;; v6 (time (humanize (alength (blob/encode data {:version 6})))) +;; ] +;; (print-table +;; [{ +;; :v1 v1 +;; :v3 v3 +;; :v4 v4 +;; :v5 v5 +;; :v6 v6 +;; }]))) -(sm/def! ::test - [:map {:title "Foo"} - [:x :int] - [:y {:min 0} :double] - [:bar - [:map {:title "Bar"} - [:z :string] - [:v ::sm/uuid]]] - [:items - [:vector ::dt/instant]]]) - -(sm/def! ::test2 - [:multi {:title "Foo" :dispatch :type} - [:x - [:map {:title "FooX"} - [:type [:= :x]] - [:x :int]]] - [:y - [:map - [:type [:= :x]] - [:y [::sm/one-of #{:a :b :c}]]]] - [:z - [:map {:title "FooZ"} - [:z - [:multi {:title "Bar" :dispatch :type} - [:a - [:map - [:type [:= :a]] - [:a :int]]] - [:b - [:map - [:type [:= :b]] - [:b :int]]]]]]]]) - +(defn calculate-frames + [{:keys [data]}] + (->> (vals (:pages-index data)) + (mapcat (comp vals :objects)) + (filter cfh/is-direct-child-of-root?) + (filter cfh/frame-shape?) + (count))) diff --git a/backend/package.json b/backend/package.json index c5510791c1..18b183c3ae 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,18 +1,26 @@ { - "name": "uxbox-back", - "version": "0.1.0", - "description": "The Open-Source prototyping tool", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build-emails": "./scripts/build-email-templates.sh" - }, + "name": "backend", + "version": "1.0.0", + "license": "MPL-2.0", + "author": "Kaleidos INC", + "private": true, + "packageManager": "yarn@4.0.2", "repository": { "type": "git", - "url": "git+https://github.com/uxbox/uxbox.git" + "url": "https://github.com/penpot/penpot" + }, + "dependencies": { + "luxon": "^3.4.2", + "sax": "^1.2.4" }, - "author": "Uxbox", - "license": "SEE LICENSE IN ", "devDependencies": { - "mjml": "^4.6.3" + "nodemon": "^3.0.1", + "source-map-support": "^0.5.21", + "ws": "^8.13.0" + }, + "scripts": { + "fmt:clj:check": "cljfmt check --parallel=false src/ test/", + "fmt:clj": "cljfmt fix --parallel=true src/ test/", + "lint:clj": "clj-kondo --parallel --lint src/" } } diff --git a/backend/resources/app/onboarding.edn b/backend/resources/app/onboarding.edn index 0438d25ba0..3a94d29edb 100644 --- a/backend/resources/app/onboarding.edn +++ b/backend/resources/app/onboarding.edn @@ -1,30 +1,33 @@ -[{:id "material-design-3" - :name "Material Design 3" - :file-uri "https://github.com/penpot/penpot-files/raw/main/Material%20Design%203.penpot"} - {:id "tutorial-for-beginners" +[{:id "tutorial-for-beginners" :name "Tutorial for beginners" :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/tutorial-for-beginners.penpot"} - {:id "penpot-design-system" - :name "Penpot Design System" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"} - {:id "flex-layout-playground" - :name "Flex Layout Playground" - :file-uri "https://github.com/penpot/penpot-files/raw/main/Flex%20Layout%20Playground.penpot"} + {:id "lucide-icons" + :name "Lucide Icons" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Lucide-icons.penpot"} + {:id "font-awesome" + :name "Font Awesome" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Font-Awesome.penpot"} + {:id "plants-app" + :name "Plants app" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Plants-app.penpot"} {:id "wireframing-kit" :name "Wireframing Kit" :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"} - {:id "ant-design" - :name "Ant Design UI Kit (lite)" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Ant-Design-UI-Kit-Lite.penpot"} - {:id "cocomaterial" - :name "Cocomaterial" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Cocomaterial.penpot"} - {:id "circum-icons" - :name "Circum Icons pack" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/CircumIcons.penpot"} - {:id "coreui" - :name "CoreUI" - :file-uri "https://github.com/penpot/penpot-files/raw/main/CoreUI%20DesignSystem%20(DEMO).penpot"} + {:id "black-white-mobile-templates" + :name "Black & White Mobile Templates" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Black-White-Mobile-Templates.penpot"} + {:id "avataaars" + :name "Avataaars" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Avataaars-by-Pablo-Stanley.penpot"} + {:id "ux-notes" + :name "UX Notes" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/UX-Notes.penpot"} {:id "whiteboarding-kit" :name "Whiteboarding Kit" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"}] + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"} + {:id "open-color-scheme" + :name "Open Color Scheme" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Open-Color-Scheme.penpot"} + {:id "flex-layout-playground" + :name "Flex Layout Playground" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"}] diff --git a/backend/resources/app/templates/api-doc-entry.tmpl b/backend/resources/app/templates/api-doc-entry.tmpl index c61157ec24..31c48deebf 100644 --- a/backend/resources/app/templates/api-doc-entry.tmpl +++ b/backend/resources/app/templates/api-doc-entry.tmpl @@ -25,6 +25,12 @@ SCHEMA {% endif %} + + {% if item.sse %} + + SSE + + {% endif %} - +
+ +
+ +
+ Profile Management +
+
+ +
+ +
+ + +
+ + This is a just a security double check for prevent non intentional submits. + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
@@ -107,18 +146,37 @@ Debug Main Page
- - -
- - Do not break on index lookup errors (remap operation). - Useful when importing a broken file that has broken - relations or missing pieces. - + +
+ + +
+ +
+
+ Reset file version + Allows reset file data version to a specific number/ + +
+
+ +
+
+
- + + +
+ + This is a just a security double check for prevent non intentional submits. + +
+ + +
+
diff --git a/backend/resources/app/templates/styles.css b/backend/resources/app/templates/styles.css index 410734adb6..93ef2c4904 100644 --- a/backend/resources/app/templates/styles.css +++ b/backend/resources/app/templates/styles.css @@ -116,29 +116,50 @@ nav > div:not(:last-child) { width: unset; } -.index { +.dashboard { margin-top: 40px; display: flex; } -.index > section { - padding: 10px; - background-color: #e3e3e3; +.widget { max-width: 400px; margin: 5px; height: fit-content; } -.index fieldset:not(:first-child) { +.widget input[type=submit] { + outline: none; + border: 1px solid gray; + border-radius: 2px; + padding: 3px 5px; +} + +.widget input[type=submit].danger { + outline: none; + border: 1px solid red; + border-radius: 2px; + padding: 3px 5px; +} + + + +.widget > fieldset { + padding: 10px; + background-color: #f9f9f9; +} + +.widget > fieldset:not(:last-child) { + margin-bottom: 10px; +} + + + +.dashboard fieldset:not(:first-child) { margin-top: 15px; } -/* .index > section:not(:last-child) { */ -/* margin-bottom: 10px; */ -/* } */ - -.index > section > h2 { +.widget > h2 { margin-top: 0px; } diff --git a/backend/resources/climit.edn b/backend/resources/climit.edn index 5802af5e3b..34d2184153 100644 --- a/backend/resources/climit.edn +++ b/backend/resources/climit.edn @@ -3,12 +3,28 @@ ;; Optional: queue, ommited means Integer/MAX_VALUE ;; Optional: timeout, ommited means no timeout ;; Note: queue and timeout are excluding -{:update-file-by-id {:permits 1 :queue 3} - :update-file {:permits 20} +{:update-file/global {:permits 20} + :update-file/by-profile + {:permits 1 :queue 5} - :derive-password {:permits 8} - :process-font {:permits 4 :queue 32} - :process-image {:permits 8 :queue 32} + :process-font/global {:permits 4} + :process-font/by-profile {:permits 1} - :submit-audit-events-by-profile + :process-image/global {:permits 8} + :process-image/by-profile {:permits 1} + + :auth/global {:permits 8} + + :root/global + {:permits 40} + + :root/by-profile + {:permits 10} + + :file-thumbnail-ops/global + {:permits 20} + :file-thumbnail-ops/by-profile + {:permits 2} + + :submit-audit-events/by-profile {:permits 1 :queue 3}} diff --git a/backend/resources/log4j2-devenv-repl.xml b/backend/resources/log4j2-devenv-repl.xml new file mode 100644 index 0000000000..07a84f2fd0 --- /dev/null +++ b/backend/resources/log4j2-devenv-repl.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/resources/log4j2-devenv.xml b/backend/resources/log4j2-devenv.xml index 947d06e268..3cf7ab00b7 100644 --- a/backend/resources/log4j2-devenv.xml +++ b/backend/resources/log4j2-devenv.xml @@ -6,13 +6,13 @@ alwaysWriteExceptions="true" /> - + - + @@ -21,31 +21,36 @@ - - + + + - - + + + + + + diff --git a/backend/resources/log4j2-experiments.xml b/backend/resources/log4j2-experiments.xml new file mode 100644 index 0000000000..3357aae31f --- /dev/null +++ b/backend/resources/log4j2-experiments.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/resources/log4j2.xml b/backend/resources/log4j2.xml index 685eac0fb2..591883e4ad 100644 --- a/backend/resources/log4j2.xml +++ b/backend/resources/log4j2.xml @@ -11,12 +11,9 @@ - - - + - diff --git a/backend/scripts/manage.py b/backend/scripts/manage.py index c56b33389e..564c0e2d52 100755 --- a/backend/scripts/manage.py +++ b/backend/scripts/manage.py @@ -44,11 +44,16 @@ def send_eval(expr): s.send(b":repl/quit\n\n") with s.makefile() as f: - result = json.load(f) - tag = result.get("tag", None) - if tag != "ret": - raise RuntimeError("unexpected response from PREPL") - return result.get("val", None), result.get("exception", None) + while True: + line = f.readline() + result = json.loads(line) + tag = result.get("tag", None) + if tag == "ret": + return result.get("val", None), result.get("exception", None) + elif tag == "out": + print(result.get("val"), end="") + else: + raise RuntimeError("unexpected response from PREPL") def encode(val): return json.dumps(json.dumps(val)) @@ -60,7 +65,7 @@ def print_error(res): def run_cmd(params): try: - expr = "(app.srepl.ext/run-json-cmd {})".format(encode(params)) + expr = "(app.srepl.cli/exec {})".format(encode(params)) res, failed = send_eval(expr) if failed: print_error(res) @@ -140,12 +145,22 @@ def derive_password(password): res = run_cmd(params) print(f"Derived password: \"{res}\"") + +def migrate_components_v2(): + params = { + "cmd": "migrate-v2", + "params": {} + } + + run_cmd(params) + available_commands = ( "create-profile", "update-profile", "delete-profile", "search-profile", "derive-password", + "migrate-components-v2", ) parser = argparse.ArgumentParser( @@ -217,3 +232,8 @@ elif args.action == "search-profile": email = input("Email: ") search_profile(email) + +elif args.action == "migrate-components-v2": + migrate_components_v2() + + diff --git a/backend/scripts/nrepl b/backend/scripts/nrepl new file mode 100755 index 0000000000..c24ef81656 --- /dev/null +++ b/backend/scripts/nrepl @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +clojure -J-Xms50m -J-Xmx256m -J-XX:+UseSerialGC -Sdeps '{:deps {reply/reply {:mvn/version "0.5.0"}}}' -M -m reply.main --attach localhost:6064 -e "(in-ns 'app.main)" diff --git a/backend/scripts/repl b/backend/scripts/repl index 0503d03d6d..6336331e45 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -4,13 +4,16 @@ export PENPOT_HOST=devenv export PENPOT_TENANT=dev export PENPOT_FLAGS="\ $PENPOT_FLAGS \ - enable-registration + enable-login-with-ldap \ enable-login-with-password enable-login-with-oidc \ enable-login-with-google \ enable-login-with-github \ enable-login-with-gitlab \ + enable-backend-worker \ enable-backend-asserts \ + enable-feature-fdata-pointer-map \ + enable-feature-fdata-objects-map \ enable-audit-log \ enable-transit-readable-response \ enable-demo-users \ @@ -22,7 +25,17 @@ export PENPOT_FLAGS="\ enable-rpc-rlimit \ enable-soft-rpc-rlimit \ enable-webhooks \ - enable-access-tokens"; + enable-access-tokens \ + disable-feature-components-v2 \ + enable-file-validation \ + enable-file-schema-validation"; + + +# Setup default upload media file size to 100MiB +export PENPOT_MEDIA_MAX_FILE_SIZE=104857600 + +# Setup default multipart upload size to 300MiB +export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=314572800 # export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot" # export PENPOT_DATABASE_USERNAME="penpot" @@ -37,10 +50,13 @@ export PENPOT_FLAGS="\ # export PENPOT_AUDIT_LOG_ARCHIVE_URI="http://localhost:6070/api/audit" # Initialize MINIO config -mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin -mc admin user add penpot-s3 penpot-devenv penpot-devenv -mc admin policy set penpot-s3 readwrite user=penpot-devenv -mc mb penpot-s3/penpot -p +mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin -q +mc admin user add penpot-s3 penpot-devenv penpot-devenv -q +mc admin user info penpot-s3 penpot-devenv |grep -F -q "readwrite" +if [ "$?" = "1" ]; then + mc admin policy attach penpot-s3 readwrite --user=penpot-devenv -q +fi +mc mb penpot-s3/penpot -p -q export AWS_ACCESS_KEY_ID=penpot-devenv export AWS_SECRET_ACCESS_KEY=penpot-devenv @@ -48,21 +64,23 @@ export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3 export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000 export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot -#-J-Djdk.virtualThreadScheduler.parallelism=16 - export OPTIONS=" -A:jmx-remote -A:dev \ -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \ -J-Djdk.attach.allowAttachSelf \ - -J-Dlog4j2.configurationFile=log4j2-devenv.xml \ + -J-Dpolyglot.engine.WarnInterpreterOnly=false \ + -J-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \ + -J-XX:+EnableDynamicAgentLoading \ -J-XX:-OmitStackTraceInFastThrow \ -J-XX:+UnlockDiagnosticVMOptions \ -J-XX:+DebugNonSafepoints \ - -J-Djdk.tracePinnedThreads=full \ - -J--enable-preview"; + -J-Djdk.tracePinnedThreads=full" + +# Enable preview +export OPTIONS="$OPTIONS -J--enable-preview" # Setup HEAP -export OPTIONS="$OPTIONS -J-Xms50m -J-Xmx1024m" +# export OPTIONS="$OPTIONS -J-Xms50m -J-Xmx1024m" # export OPTIONS="$OPTIONS -J-Xms1100m -J-Xmx1100m -J-XX:+AlwaysPreTouch" # Increase virtual thread pool size @@ -75,7 +93,7 @@ export OPTIONS="$OPTIONS -J-Xms50m -J-Xmx1024m" # export OPTIONS="$OPTIONS -J-Xint" # Setup GC -export OPTIONS="$OPTIONS -J-XX:+UseG1GC" +# export OPTIONS="$OPTIONS -J-XX:+UseG1GC" # Setup GC # export OPTIONS="$OPTIONS -J-XX:+UseZGC" diff --git a/backend/scripts/repl-test b/backend/scripts/repl-test new file mode 100755 index 0000000000..a1333a5317 --- /dev/null +++ b/backend/scripts/repl-test @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +source /home/penpot/backend/environ +export PENPOT_FLAGS="$PENPOT_FLAGS disable-backend-worker" + +export OPTIONS=" + -A:jmx-remote -A:dev \ + -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \ + -J-Djdk.attach.allowAttachSelf \ + -J-Dlog4j2.configurationFile=log4j2-experiments.xml \ + -J-XX:-OmitStackTraceInFastThrow \ + -J-XX:+UnlockDiagnosticVMOptions \ + -J-XX:+DebugNonSafepoints \ + -J-Djdk.tracePinnedThreads=full \ + -J-Dpolyglot.engine.WarnInterpreterOnly=false \ + -J--enable-preview"; + +# Setup HEAP +#export OPTIONS="$OPTIONS -J-Xms900m -J-Xmx900m -J-XX:+AlwaysPreTouch" +export OPTIONS="$OPTIONS -J-Xms1g -J-Xmx25g" +#export OPTIONS="$OPTIONS -J-Xms900m -J-Xmx900m -J-XX:+AlwaysPreTouch" + +export PENPOT_HTTP_SERVER_IO_THREADS=2 +export PENPOT_HTTP_SERVER_WORKER_THREADS=2 + +# Increase virtual thread pool size +# export OPTIONS="$OPTIONS -J-Djdk.virtualThreadScheduler.parallelism=16" + +# Disable C2 Compiler +# export OPTIONS="$OPTIONS -J-XX:TieredStopAtLevel=1" + +# Disable all compilers +# export OPTIONS="$OPTIONS -J-Xint" + +# Setup GC +export OPTIONS="$OPTIONS -J-XX:+UseG1GC -J-Xlog:gc:logs/gc.log" + + +# Setup GC +#export OPTIONS="$OPTIONS -J-XX:+UseZGC -J-XX:+ZGenerational -J-Xlog:gc:gc.log" + +# Enable ImageMagick v7.x support +# export OPTIONS="-J-Dim4java.useV7=true $OPTIONS"; + +export OPTIONS_EVAL="nil" +# export OPTIONS_EVAL="(set! *warn-on-reflection* true)" + +set -ex +exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main \ No newline at end of file diff --git a/backend/scripts/start-dev b/backend/scripts/start-dev index 3e525dfc7d..1a2fa2842c 100755 --- a/backend/scripts/start-dev +++ b/backend/scripts/start-dev @@ -6,31 +6,95 @@ export PENPOT_FLAGS="\ $PENPOT_FLAGS \ enable-prepl-server \ enable-urepl-server \ + enable-nrepl-server \ enable-webhooks \ enable-backend-asserts \ enable-audit-log \ enable-transit-readable-response \ enable-demo-users \ + enable-feature-fdata-pointer-map \ + enable-feature-fdata-objects-map \ disable-secure-session-cookies \ + enable-rpc-climit \ enable-smtp \ - enable-access-tokens"; + enable-access-tokens \ + disable-feature-components-v2 \ + enable-file-validation \ + enable-file-schema-validation"; -set -ex +export OPTIONS=" + -A:jmx-remote -A:dev \ + -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \ + -J-Djdk.attach.allowAttachSelf \ + -J-Dpolyglot.engine.WarnInterpreterOnly=false \ + -J-Dlog4j2.configurationFile=log4j2-devenv.xml \ + -J-XX:+EnableDynamicAgentLoading \ + -J-XX:-OmitStackTraceInFastThrow \ + -J-XX:+UnlockDiagnosticVMOptions \ + -J-XX:+DebugNonSafepoints" + +# Setup default upload media file size to 100MiB +export PENPOT_MEDIA_MAX_FILE_SIZE=104857600 + +# Setup default multipart upload size to 300MiB +export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=314572800 + +# Setup HEAP +# export OPTIONS="$OPTIONS -J-Xms50m -J-Xmx1024m" +# export OPTIONS="$OPTIONS -J-Xms1100m -J-Xmx1100m -J-XX:+AlwaysPreTouch" + +# Increase virtual thread pool size +# export OPTIONS="$OPTIONS -J-Djdk.virtualThreadScheduler.parallelism=16" + +# Disable C2 Compiler +# export OPTIONS="$OPTIONS -J-XX:TieredStopAtLevel=1" + +# Disable all compilers +# export OPTIONS="$OPTIONS -J-Xint" + +# Setup GC +# export OPTIONS="$OPTIONS -J-XX:+UseG1GC" + +# Setup GC +# export OPTIONS="$OPTIONS -J-XX:+UseZGC" + +# Enable ImageMagick v7.x support +# export OPTIONS="-J-Dim4java.useV7=true $OPTIONS"; + + +# Initialize MINIO config +mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin -q +mc admin user add penpot-s3 penpot-devenv penpot-devenv -q +mc admin user info penpot-s3 penpot-devenv |grep -F -q "readwrite" +if [ "$?" = "1" ]; then + mc admin policy attach penpot-s3 readwrite --user=penpot-devenv -q +fi +mc mb penpot-s3/penpot -p -q + +export AWS_ACCESS_KEY_ID=penpot-devenv +export AWS_SECRET_ACCESS_KEY=penpot-devenv +export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3 +export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000 +export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot if [ "$1" = "--watch" ]; then + trap "exit" INT TERM ERR + trap "kill 0" EXIT + echo "Start Watch..." - clojure -A:dev -M -m app.main & - PID=$! + clojure $OPTIONS -A:dev -M -m app.main & npx nodemon \ --watch src \ --watch ../common \ --ext "clj" \ --signal SIGKILL \ - --exec 'echo "(user/restart)" | nc -N localhost 6062' + --exec 'echo "(app.main/stop)\n\r(repl/refresh)\n\r(app.main/start)\n" | nc -N localhost 6062' + + wait; - kill -9 $PID else - clojure -A:dev -M -m app.main + set -x + clojure $OPTIONS -A:dev -M -m app.main; fi diff --git a/backend/src/app/auth/oidc.clj b/backend/src/app/auth/oidc.clj index 733665151c..34e2cee579 100644 --- a/backend/src/app/auth/oidc.clj +++ b/backend/src/app/auth/oidc.clj @@ -22,6 +22,7 @@ [app.loggers.audit :as audit] [app.main :as-alias main] [app.rpc.commands.profile :as profile] + [app.setup :as-alias setup] [app.tokens :as tokens] [app.util.json :as json] [app.util.time :as dt] @@ -31,13 +32,13 @@ [clojure.spec.alpha :as s] [cuerdas.core :as str] [integrant.core :as ig] - [yetti.response :as-alias yrs])) + [ring.response :as-alias rres])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn- obfuscate-string +(defn obfuscate-string [s] (if (< (count s) 10) (apply str (take (count s) (repeat "*"))) @@ -353,8 +354,7 @@ (get-name [props] (let [attr-kw (cf/get :oidc-name-attr "name") attr-ph (parse-attr-path provider attr-kw)] - (get-in props attr-ph))) - ] + (get-in props attr-ph)))] (let [props (qualify-props provider info) email (get-email props)] @@ -414,7 +414,7 @@ ::props])) (defn get-info - [{:keys [provider ::main/props] :as cfg} {:keys [params] :as request}] + [{:keys [provider ::setup/props] :as cfg} {:keys [params] :as request}] (when-let [error (get params :error)] (ex/raise :type :internal :code :error-on-retrieving-code @@ -475,12 +475,13 @@ [{:keys [::db/pool] :as cfg} info] (dm/with-open [conn (db/open pool)] (some->> (:email info) + (profile/clean-email) (profile/get-profile-by-email conn)))) (defn- redirect-response [uri] - {::yrs/status 302 - ::yrs/headers {"location" (str uri)}}) + {::rres/status 302 + ::rres/headers {"location" (str uri)}}) (defn- generate-error-redirect [_ cause] @@ -508,7 +509,7 @@ (if profile (let [sxf (session/create-fn cfg (:id profile)) token (or (:invitation-token info) - (tokens/generate (::main/props cfg) + (tokens/generate (::setup/props cfg) {:iss :auth :exp (dt/in-future "15m") :profile-id (:id profile)})) @@ -536,7 +537,7 @@ :iss :prepared-register :is-active true :exp (dt/in-future {:hours 48})) - token (tokens/generate (::main/props cfg) info) + token (tokens/generate (::setup/props cfg) info) params (d/without-nils {:token token :fullname (:fullname info)}) @@ -551,14 +552,14 @@ (defn- auth-handler [cfg {:keys [params] :as request}] (let [props (audit/extract-utm-params params) - state (tokens/generate (::main/props cfg) + state (tokens/generate (::setup/props cfg) {:iss :oauth :invitation-token (:invitation-token params) :props props :exp (dt/in-future "4h")}) uri (build-auth-uri cfg state)] - {::yrs/status 200 - ::yrs/body {:redirect-uri uri}})) + {::rres/status 200 + ::rres/body {:redirect-uri uri}})) (defn- callback-handler [cfg request] @@ -618,7 +619,7 @@ [_] (s/keys :req [::session/manager ::http/client - ::main/props + ::setup/props ::db/pool ::providers])) diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj new file mode 100644 index 0000000000..ace98c80ec --- /dev/null +++ b/backend/src/app/binfile/common.clj @@ -0,0 +1,490 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.binfile.common + "A binfile related file processing common code, used for different + binfile format implementations and management rpc methods." + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.exceptions :as ex] + [app.common.features :as cfeat] + [app.common.files.migrations :as fmg] + [app.common.files.validate :as fval] + [app.common.logging :as l] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.db.sql :as sql] + [app.features.components-v2 :as feat.compv2] + [app.features.fdata :as feat.fdata] + [app.loggers.audit :as-alias audit] + [app.loggers.webhooks :as-alias webhooks] + [app.util.blob :as blob] + [app.util.pointer-map :as pmap] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [clojure.set :as set] + [clojure.walk :as walk] + [cuerdas.core :as str])) + +(set! *warn-on-reflection* true) + +(def ^:dynamic *state* nil) +(def ^:dynamic *options* nil) + +(def xf-map-id + (map :id)) + +(def xf-map-media-id + (comp + (mapcat (juxt :media-id + :thumbnail-id + :woff1-file-id + :woff2-file-id + :ttf-file-id + :otf-file-id)) + (filter uuid?))) + +(def into-vec + (fnil into [])) + +(def conj-vec + (fnil conj [])) + +(defn collect-storage-objects + [state items] + (update state :storage-objects into xf-map-media-id items)) + +(defn collect-summary + [state key items] + (update state key into xf-map-media-id items)) + +(defn lookup-index + [id] + (when id + (let [val (get-in @*state* [:index id])] + (l/trc :fn "lookup-index" :id (str id) :result (some-> val str) ::l/sync? true) + (or val id)))) + +(defn remap-id + [item key] + (cond-> item + (contains? item key) + (update key lookup-index))) + +(defn- index-object + [index obj & attrs] + (reduce (fn [index attr-fn] + (let [old-id (attr-fn obj) + new-id (if (::overwrite *options*) old-id (uuid/next))] + (assoc index old-id new-id))) + index + attrs)) + +(defn update-index + ([index coll] + (update-index index coll identity)) + ([index coll attr] + (reduce #(index-object %1 %2 attr) index coll))) + +(defn decode-row + "A generic decode row helper" + [{:keys [data features] :as row}] + (cond-> row + features (assoc :features (db/decode-pgarray features #{})) + data (assoc :data (blob/decode data)))) + +(defn get-file + [cfg file-id] + (db/run! cfg (fn [{:keys [::db/conn] :as cfg}] + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)] + (when-let [file (db/get* conn :file {:id file-id} + {::db/remove-deleted false})] + (-> file + (decode-row) + (update :data feat.fdata/process-pointers deref) + (update :data feat.fdata/process-objects (partial into {})))))))) + +(defn get-project + [cfg project-id] + (db/get cfg :project {:id project-id})) + +(defn get-team + [cfg team-id] + (-> (db/get cfg :team {:id team-id}) + (decode-row))) + +(defn get-fonts + [cfg team-id] + (db/query cfg :team-font-variant + {:team-id team-id + :deleted-at nil})) + +(defn get-files-rels + "Given a set of file-id's, return all matching relations with the libraries" + [cfg ids] + + (dm/assert! + "expected a set of uuids" + (and (set? ids) + (every? uuid? ids))) + + (db/run! cfg (fn [{:keys [::db/conn]}] + (let [ids (db/create-array conn "uuid" ids) + sql (str "SELECT flr.* FROM file_library_rel AS flr " + " JOIN file AS l ON (flr.library_file_id = l.id) " + " WHERE flr.file_id = ANY(?) AND l.deleted_at IS NULL")] + (db/exec! conn [sql ids]))))) + +(def ^:private sql:get-libraries + "WITH RECURSIVE libs AS ( + SELECT fl.id + FROM file AS fl + JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id) + WHERE flr.file_id = ANY(?) + UNION + SELECT fl.id + FROM file AS fl + JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id) + JOIN libs AS l ON (flr.file_id = l.id) + ) + SELECT DISTINCT l.id + FROM libs AS l") + +(defn get-libraries + "Get all libraries ids related to provided file ids" + [cfg ids] + (db/run! cfg (fn [{:keys [::db/conn]}] + (let [ids' (db/create-array conn "uuid" ids)] + (->> (db/exec! conn [sql:get-libraries ids']) + (into #{} xf-map-id)))))) + +(defn get-file-object-thumbnails + "Return all file object thumbnails for a given file." + [cfg file-id] + (db/query cfg :file-tagged-object-thumbnail + {:file-id file-id + :deleted-at nil})) + +(defn get-file-thumbnail + "Return the thumbnail for the specified file-id" + [cfg {:keys [id revn]}] + (db/get* cfg :file-thumbnail + {:file-id id + :revn revn + :data nil} + {::sql/columns [:media-id :file-id :revn]})) + + +(def ^:private + xform:collect-media-id + (comp + (map :objects) + (mapcat vals) + (mapcat (fn [obj] + ;; NOTE: because of some bug, we ended with + ;; many shape types having the ability to + ;; have fill-image attribute (which initially + ;; designed for :path shapes). + (sequence + (keep :id) + (concat [(:fill-image obj) + (:metadata obj)] + (map :fill-image (:fills obj)) + (map :stroke-image (:strokes obj)) + (->> (:content obj) + (tree-seq map? :children) + (mapcat :fills) + (map :fill-image)))))))) + +(defn collect-used-media + "Given a fdata (file data), returns all media references." + [data] + (-> #{} + (into xform:collect-media-id (vals (:pages-index data))) + (into xform:collect-media-id (vals (:components data))) + (into (keys (:media data))))) + +(defn get-file-media + [cfg {:keys [data id] :as file}] + (db/run! cfg (fn [{:keys [::db/conn]}] + (let [ids (collect-used-media data) + ids (db/create-array conn "uuid" ids) + sql (str "SELECT * FROM file_media_object WHERE id = ANY(?)")] + + ;; We assoc the file-id again to the file-media-object row + ;; because there are cases that used objects refer to other + ;; files and we need to ensure in the exportation process that + ;; all ids matches + (->> (db/exec! conn [sql ids]) + (mapv #(assoc % :file-id id))))))) + +(def ^:private sql:get-team-files + "SELECT f.id FROM file AS f + JOIN project AS p ON (p.id = f.project_id) + WHERE p.team_id = ?") + +(defn get-team-files + "Get a set of file ids for the specified team-id" + [{:keys [::db/conn]} team-id] + (->> (db/exec! conn [sql:get-team-files team-id]) + (into #{} xf-map-id))) + +(def ^:private sql:get-team-projects + "SELECT p.id FROM project AS p + WHERE p.team_id = ? + AND p.deleted_at IS NULL") + +(defn get-team-projects + "Get a set of project ids for the team" + [{:keys [::db/conn]} team-id] + (->> (db/exec! conn [sql:get-team-projects team-id]) + (into #{} xf-map-id))) + +(def ^:private sql:get-project-files + "SELECT f.id FROM file AS f + WHERE f.project_id = ? + AND f.deleted_at IS NULL") + +(defn get-project-files + "Get a set of file ids for the project" + [{:keys [::db/conn]} project-id] + (->> (db/exec! conn [sql:get-project-files project-id]) + (into #{} xf-map-id))) + +(defn- relink-shapes + "A function responsible to analyze all file data and + replace the old :component-file reference with the new + ones, using the provided file-index." + [data] + (letfn [(process-map-form [form] + (cond-> form + ;; Relink image shapes + (and (map? (:metadata form)) + (= :image (:type form))) + (update-in [:metadata :id] lookup-index) + + ;; Relink paths with fill image + (map? (:fill-image form)) + (update-in [:fill-image :id] lookup-index) + + ;; This covers old shapes and the new :fills. + (uuid? (:fill-color-ref-file form)) + (update :fill-color-ref-file lookup-index) + + ;; This covers the old shapes and the new :strokes + (uuid? (:stroke-color-ref-file form)) + (update :stroke-color-ref-file lookup-index) + + ;; This covers all text shapes that have typography referenced + (uuid? (:typography-ref-file form)) + (update :typography-ref-file lookup-index) + + ;; This covers the component instance links + (uuid? (:component-file form)) + (update :component-file lookup-index) + + ;; This covers the shadows and grids (they have directly + ;; the :file-id prop) + (uuid? (:file-id form)) + (update :file-id lookup-index))) + + (process-form [form] + (if (map? form) + (try + (process-map-form form) + (catch Throwable cause + (l/warn :hint "failed form" :form (pr-str form) ::l/sync? true) + (throw cause))) + form))] + + (walk/postwalk process-form data))) + +(defn- relink-media + "A function responsible of process the :media attr of file data and + remap the old ids with the new ones." + [media] + (reduce-kv (fn [res k v] + (let [id (lookup-index k)] + (if (uuid? id) + (-> res + (assoc id (assoc v :id id)) + (dissoc k)) + res))) + media + media)) + +(defn- relink-colors + "A function responsible of process the :colors attr of file data and + remap the old ids with the new ones." + [colors] + (reduce-kv (fn [res k v] + (if (:image v) + (update-in res [k :image :id] lookup-index) + res)) + colors + colors)) + +(defn embed-assets + [cfg data file-id] + (letfn [(walk-map-form [form state] + (cond + (uuid? (:fill-color-ref-file form)) + (do + (vswap! state conj [(:fill-color-ref-file form) :colors (:fill-color-ref-id form)]) + (assoc form :fill-color-ref-file file-id)) + + (uuid? (:stroke-color-ref-file form)) + (do + (vswap! state conj [(:stroke-color-ref-file form) :colors (:stroke-color-ref-id form)]) + (assoc form :stroke-color-ref-file file-id)) + + (uuid? (:typography-ref-file form)) + (do + (vswap! state conj [(:typography-ref-file form) :typographies (:typography-ref-id form)]) + (assoc form :typography-ref-file file-id)) + + (uuid? (:component-file form)) + (do + (vswap! state conj [(:component-file form) :components (:component-id form)]) + (assoc form :component-file file-id)) + + :else + form)) + + (process-group-of-assets [data [lib-id items]] + ;; NOTE: there is a possibility that shape refers to an + ;; non-existant file because the file was removed. In this + ;; case we just ignore the asset. + (if-let [lib (get-file cfg lib-id)] + (reduce (partial process-asset lib) data items) + data)) + + (process-asset [lib data [bucket asset-id]] + (let [asset (get-in lib [:data bucket asset-id]) + ;; Add a special case for colors that need to have + ;; correctly set the :file-id prop (pending of the + ;; refactor that will remove it). + asset (cond-> asset + (= bucket :colors) (assoc :file-id file-id))] + (update data bucket assoc asset-id asset)))] + + (let [assets (volatile! [])] + (walk/postwalk #(cond-> % (map? %) (walk-map-form assets)) data) + (->> (deref assets) + (filter #(as-> (first %) $ (and (uuid? $) (not= $ file-id)))) + (d/group-by first rest) + (reduce (partial process-group-of-assets) data))))) + +(defn- fix-version + [file] + (let [file (fmg/fix-version file)] + ;; FIXME: We're temporarily activating all migrations because a + ;; problem in the environments messed up with the version numbers + ;; When this problem is fixed delete the following line + (if (> (:version file) 22) + (assoc file :version 22) + file))) + +(defn process-file + [{:keys [id] :as file}] + (-> file + (fix-version) + (update :data (fn [fdata] + (-> fdata + (assoc :id id) + (dissoc :recent-colors)))) + (fmg/migrate-file) + (update :data (fn [fdata] + (-> fdata + (update :pages-index relink-shapes) + (update :components relink-shapes) + (update :media relink-media) + (update :colors relink-colors) + (d/without-nils)))))) + +(defn- upsert-file! + [conn file] + (let [sql (str "INSERT INTO file (id, project_id, name, revn, version, is_shared, data, created_at, modified_at) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) " + "ON CONFLICT (id) DO UPDATE SET data=?, version=?")] + (db/exec-one! conn [sql + (:id file) + (:project-id file) + (:name file) + (:revn file) + (:version file) + (:is-shared file) + (:data file) + (:created-at file) + (:modified-at file) + (:data file) + (:version file)]))) + +(defn persist-file! + "Applies all the final validations and perist the file." + [{:keys [::db/conn ::timestamp] :as cfg} {:keys [id] :as file}] + + (dm/assert! + "expected valid timestamp" + (dt/instant? timestamp)) + + (let [file (-> file + (assoc :created-at timestamp) + (assoc :modified-at timestamp) + (assoc :ignore-sync-until (dt/plus timestamp (dt/duration {:seconds 5}))) + (update :features + (fn [features] + (let [features (cfeat/check-supported-features! features)] + (-> (::features cfg #{}) + (set/difference cfeat/frontend-only-features) + (set/union features)))))) + + _ (when (contains? cf/flags :file-schema-validation) + (fval/validate-file-schema! file)) + + _ (when (contains? cf/flags :soft-file-schema-validation) + (let [result (ex/try! (fval/validate-file-schema! file))] + (when (ex/exception? result) + (l/error :hint "file schema validation error" :cause result)))) + + file (if (contains? (:features file) "fdata/objects-map") + (feat.fdata/enable-objects-map file) + file) + + file (if (contains? (:features file) "fdata/pointer-map") + (binding [pmap/*tracked* (pmap/create-tracked)] + (let [file (feat.fdata/enable-pointer-map file)] + (feat.fdata/persist-pointers! cfg id) + file)) + file) + + params (-> file + (update :features db/encode-pgarray conn "text") + (update :data blob/encode))] + + (if (::overwrite cfg) + (upsert-file! conn params) + (db/insert! conn :file params ::db/return-keys false)) + + file)) + +(defn apply-pending-migrations! + "Apply alredy registered pending migrations to files" + [cfg] + (doseq [[feature file-id] (-> *state* deref :pending-to-migrate)] + (case feature + "components/v2" + (feat.compv2/migrate-file! cfg file-id :validate? (::validate cfg true)) + + "fdata/shape-data-type" + nil + + (ex/raise :type :internal + :code :no-migration-defined + :hint (str/ffmt "no migation for feature '%' on file importation" feature) + :feature feature)))) diff --git a/backend/src/app/binfile/v1.clj b/backend/src/app/binfile/v1.clj new file mode 100644 index 0000000000..d2b7cdf7f2 --- /dev/null +++ b/backend/src/app/binfile/v1.clj @@ -0,0 +1,779 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.binfile.v1 + "A custom, perfromance and efficiency focused binfile format impl" + (:refer-clojure :exclude [assert]) + (:require + [app.binfile.common :as bfc] + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.exceptions :as ex] + [app.common.features :as cfeat] + [app.common.fressian :as fres] + [app.common.logging :as l] + [app.common.spec :as us] + [app.common.types.file :as ctf] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.loggers.audit :as-alias audit] + [app.loggers.webhooks :as-alias webhooks] + [app.media :as media] + [app.rpc :as-alias rpc] + [app.rpc.commands.teams :as teams] + [app.rpc.doc :as-alias doc] + [app.storage :as sto] + [app.storage.tmp :as tmp] + [app.tasks.file-gc] + [app.util.events :as events] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [clojure.java.io :as jio] + [clojure.set :as set] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [datoteka.io :as io] + [promesa.util :as pu] + [yetti.adapter :as yt]) + (:import + com.github.luben.zstd.ZstdIOException + com.github.luben.zstd.ZstdInputStream + com.github.luben.zstd.ZstdOutputStream + java.io.DataInputStream + java.io.DataOutputStream + java.io.InputStream + java.io.OutputStream)) + +(set! *warn-on-reflection* true) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; DEFAULTS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Threshold in MiB when we pass from using +;; in-memory byte-array's to use temporal files. +(def temp-file-threshold + (* 1024 1024 2)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; LOW LEVEL STREAM IO API +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:const buffer-size (:xnio/buffer-size yt/defaults)) +(def ^:const penpot-magic-number 800099563638710213) + + +;; A maximum (storage) object size allowed: 100MiB +(def ^:const max-object-size + (* 1024 1024 100)) + +(def ^:dynamic *position* nil) + +(defn get-mark + [id] + (case id + :header 1 + :stream 2 + :uuid 3 + :label 4 + :obj 5 + (ex/raise :type :validation + :code :invalid-mark-id + :hint (format "invalid mark id %s" id)))) + +(defmacro assert + [expr hint] + `(when-not ~expr + (ex/raise :type :validation + :code :unexpected-condition + :hint ~hint))) + +(defmacro assert-mark + [v type] + `(let [expected# (get-mark ~type) + val# (long ~v)] + (when (not= val# expected#) + (ex/raise :type :validation + :code :unexpected-mark + :hint (format "received mark %s, expected %s" val# expected#))))) + +(defmacro assert-label + [expr label] + `(let [v# ~expr] + (when (not= v# ~label) + (ex/raise :type :assertion + :code :unexpected-label + :hint (format "received label %s, expected %s" v# ~label))))) + +;; --- PRIMITIVE IO + +(defn write-byte! + [^DataOutputStream output data] + (l/trace :fn "write-byte!" :data data :position @*position* ::l/sync? true) + (.writeByte output (byte data)) + (swap! *position* inc)) + +(defn read-byte! + [^DataInputStream input] + (let [v (.readByte input)] + (l/trace :fn "read-byte!" :val v :position @*position* ::l/sync? true) + (swap! *position* inc) + v)) + +(defn write-long! + [^DataOutputStream output data] + (l/trace :fn "write-long!" :data data :position @*position* ::l/sync? true) + (.writeLong output (long data)) + (swap! *position* + 8)) + + +(defn read-long! + [^DataInputStream input] + (let [v (.readLong input)] + (l/trace :fn "read-long!" :val v :position @*position* ::l/sync? true) + (swap! *position* + 8) + v)) + +(defn write-bytes! + [^DataOutputStream output ^bytes data] + (let [size (alength data)] + (l/trace :fn "write-bytes!" :size size :position @*position* ::l/sync? true) + (.write output data 0 size) + (swap! *position* + size))) + +(defn read-bytes! + [^InputStream input ^bytes buff] + (let [size (alength buff) + readed (.readNBytes input buff 0 size)] + (l/trace :fn "read-bytes!" :expected (alength buff) :readed readed :position @*position* ::l/sync? true) + (swap! *position* + readed) + readed)) + +;; --- COMPOSITE IO + +(defn write-uuid! + [^DataOutputStream output id] + (l/trace :fn "write-uuid!" :position @*position* :WRITTEN? (.size output) ::l/sync? true) + + (doto output + (write-byte! (get-mark :uuid)) + (write-long! (uuid/get-word-high id)) + (write-long! (uuid/get-word-low id)))) + +(defn read-uuid! + [^DataInputStream input] + (l/trace :fn "read-uuid!" :position @*position* ::l/sync? true) + (let [m (read-byte! input)] + (assert-mark m :uuid) + (let [a (read-long! input) + b (read-long! input)] + (uuid/custom a b)))) + +(defn write-obj! + [^DataOutputStream output data] + (l/trace :fn "write-obj!" :position @*position* ::l/sync? true) + (let [^bytes data (fres/encode data)] + (doto output + (write-byte! (get-mark :obj)) + (write-long! (alength data)) + (write-bytes! data)))) + +(defn read-obj! + [^DataInputStream input] + (l/trace :fn "read-obj!" :position @*position* ::l/sync? true) + (let [m (read-byte! input)] + (assert-mark m :obj) + (let [size (read-long! input)] + (assert (pos? size) "incorrect header size found on reading header") + (let [buff (byte-array size)] + (read-bytes! input buff) + (fres/decode buff))))) + +(defn write-label! + [^DataOutputStream output label] + (l/trace :fn "write-label!" :label label :position @*position* ::l/sync? true) + (doto output + (write-byte! (get-mark :label)) + (write-obj! label))) + +(defn read-label! + [^DataInputStream input] + (l/trace :fn "read-label!" :position @*position* ::l/sync? true) + (let [m (read-byte! input)] + (assert-mark m :label) + (read-obj! input))) + +(defn write-header! + [^OutputStream output version] + (l/trace :fn "write-header!" + :version version + :position @*position* + ::l/sync? true) + (let [vers (-> version name (subs 1) parse-long) + output (io/data-output-stream output)] + (doto output + (write-byte! (get-mark :header)) + (write-long! penpot-magic-number) + (write-long! vers)))) + +(defn read-header! + [^InputStream input] + (l/trace :fn "read-header!" :position @*position* ::l/sync? true) + (let [input (io/data-input-stream input) + mark (read-byte! input) + mnum (read-long! input) + vers (read-long! input)] + + (when (or (not= mark (get-mark :header)) + (not= mnum penpot-magic-number)) + (ex/raise :type :validation + :code :invalid-penpot-file + :hint "invalid penpot file")) + + (keyword (str "v" vers)))) + +(defn copy-stream! + [^OutputStream output ^InputStream input ^long size] + (let [written (io/copy! input output :size size)] + (l/trace :fn "copy-stream!" :position @*position* :size size :written written ::l/sync? true) + (swap! *position* + written) + written)) + +(defn write-stream! + [^DataOutputStream output stream size] + (l/trace :fn "write-stream!" :position @*position* ::l/sync? true :size size) + (doto output + (write-byte! (get-mark :stream)) + (write-long! size)) + + (copy-stream! output stream size)) + +(defn read-stream! + [^DataInputStream input] + (l/trace :fn "read-stream!" :position @*position* ::l/sync? true) + (let [m (read-byte! input) + s (read-long! input) + p (tmp/tempfile :prefix "penpot.binfile.")] + (assert-mark m :stream) + + (when (> s max-object-size) + (ex/raise :type :validation + :code :max-file-size-reached + :hint (str/ffmt "unable to import storage object with size % bytes" s))) + + (if (> s temp-file-threshold) + (with-open [^OutputStream output (io/output-stream p)] + (let [readed (io/copy! input output :offset 0 :size s)] + (l/trace :fn "read-stream*!" :expected s :readed readed :position @*position* ::l/sync? true) + (swap! *position* + readed) + [s p])) + [s (io/read-as-bytes input :size s)]))) + +(defmacro assert-read-label! + [input expected-label] + `(let [readed# (read-label! ~input) + expected# ~expected-label] + (when (not= readed# expected#) + (ex/raise :type :validation + :code :unexpected-label + :hint (format "unexpected label found: %s, expected: %s" readed# expected#))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; API +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- HELPERS + +(defn zstd-input-stream + ^InputStream + [input] + (ZstdInputStream. ^InputStream input)) + +(defn zstd-output-stream + ^OutputStream + [output & {:keys [level] :or {level 0}}] + (ZstdOutputStream. ^OutputStream output (int level))) + +(defn- get-files + [cfg ids] + (db/run! cfg (fn [{:keys [::db/conn]}] + (let [sql (str "SELECT id FROM file " + " WHERE id = ANY(?) ") + ids (db/create-array conn "uuid" ids)] + (->> (db/exec! conn [sql ids]) + (into [] (map :id)) + (not-empty)))))) + +;; --- EXPORT WRITER + +(defmulti write-export ::version) +(defmulti write-section ::section) + +(defn write-export! + [{:keys [::include-libraries ::embed-assets] :as cfg}] + (when (and include-libraries embed-assets) + (throw (IllegalArgumentException. + "the `include-libraries` and `embed-assets` are mutally excluding options"))) + + (write-export cfg)) + +(defmethod write-export :default + [{:keys [::output] :as options}] + (write-header! output :v1) + (pu/with-open [output (zstd-output-stream output :level 12) + output (io/data-output-stream output)] + (binding [bfc/*state* (volatile! {})] + (run! (fn [section] + (l/dbg :hint "write section" :section section ::l/sync? true) + (write-label! output section) + (let [options (-> options + (assoc ::output output) + (assoc ::section section))] + (binding [bfc/*options* options] + (write-section options)))) + + [:v1/metadata :v1/files :v1/rels :v1/sobjects])))) + +(defmethod write-section :v1/metadata + [{:keys [::output ::ids ::include-libraries] :as cfg}] + (if-let [fids (get-files cfg ids)] + (let [lids (when include-libraries + (bfc/get-libraries cfg ids)) + ids (into fids lids)] + (write-obj! output {:version cf/version :files ids}) + (vswap! bfc/*state* assoc :files ids)) + (ex/raise :type :not-found + :code :files-not-found + :hint "unable to retrieve files for export"))) + +(defmethod write-section :v1/files + [{:keys [::output ::embed-assets ::include-libraries] :as cfg}] + + ;; Initialize SIDS with empty vector + (vswap! bfc/*state* assoc :sids []) + + (doseq [file-id (-> bfc/*state* deref :files)] + (let [detach? (and (not embed-assets) (not include-libraries)) + thumbnails (->> (bfc/get-file-object-thumbnails cfg file-id) + (mapv #(dissoc % :file-id))) + + file (cond-> (bfc/get-file cfg file-id) + detach? + (-> (ctf/detach-external-references file-id) + (dissoc :libraries)) + + embed-assets + (update :data #(bfc/embed-assets cfg % file-id)) + + :always + (assoc :thumbnails thumbnails)) + + media (bfc/get-file-media cfg file)] + + (l/dbg :hint "write penpot file" + :id (str file-id) + :name (:name file) + :thumbnails (count thumbnails) + :features (:features file) + :media (count media) + ::l/sync? true) + + (doseq [item media] + (l/dbg :hint "write penpot file media object" :id (:id item) ::l/sync? true)) + + (doseq [item thumbnails] + (l/dbg :hint "write penpot file object thumbnail" :media-id (str (:media-id item)) ::l/sync? true)) + + (doto output + (write-obj! file) + (write-obj! media)) + + (vswap! bfc/*state* update :sids into bfc/xf-map-media-id media) + (vswap! bfc/*state* update :sids into bfc/xf-map-media-id thumbnails)))) + +(defmethod write-section :v1/rels + [{:keys [::output ::include-libraries] :as cfg}] + (let [ids (-> bfc/*state* deref :files set) + rels (when include-libraries + (bfc/get-files-rels cfg ids))] + (l/dbg :hint "found rels" :total (count rels) ::l/sync? true) + (write-obj! output rels))) + +(defmethod write-section :v1/sobjects + [{:keys [::sto/storage ::output]}] + (let [sids (-> bfc/*state* deref :sids) + storage (media/configure-assets-storage storage)] + + (l/dbg :hint "found sobjects" + :items (count sids) + ::l/sync? true) + + ;; Write all collected storage objects + (write-obj! output sids) + + (doseq [id sids] + (let [{:keys [size] :as obj} (sto/get-object storage id)] + (l/dbg :hint "write sobject" :id (str id) ::l/sync? true) + + (doto output + (write-uuid! id) + (write-obj! (meta obj))) + + (pu/with-open [stream (sto/get-object-data storage obj)] + (let [written (write-stream! output stream size)] + (when (not= written size) + (ex/raise :type :validation + :code :mismatch-readed-size + :hint (str/ffmt "found unexpected object size; size=% written=%" size written))))))))) + +;; --- EXPORT READER + +(defmulti read-import ::version) +(defmulti read-section ::section) + +(s/def ::profile-id ::us/uuid) +(s/def ::project-id ::us/uuid) +(s/def ::input io/input-stream?) +(s/def ::overwrite? (s/nilable ::us/boolean)) +(s/def ::ignore-index-errors? (s/nilable ::us/boolean)) + +;; FIXME: replace with schema +(s/def ::read-import-options + (s/keys :req [::db/pool ::sto/storage ::project-id ::profile-id ::input] + :opt [::overwrite? ::ignore-index-errors?])) + +(defn read-import! + "Do the importation of the specified resource in penpot custom binary + format. There are some options for customize the importation + behavior: + + `::bfc/overwrite`: if true, instead of creating new files and remapping id references, + it reuses all ids and updates existing objects; defaults to `false`." + [{:keys [::input ::bfc/timestamp] :or {timestamp (dt/now)} :as options}] + + (dm/assert! + "expected input stream" + (io/input-stream? input)) + + (dm/assert! + "expected valid instant" + (dt/instant? timestamp)) + + (let [version (read-header! input)] + (read-import (assoc options ::version version ::bfc/timestamp timestamp)))) + +(defn- read-import-v1 + [{:keys [::db/conn ::project-id ::profile-id ::input] :as cfg}] + (db/exec-one! conn ["SET idle_in_transaction_session_timeout = 0"]) + (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"]) + + (pu/with-open [input (zstd-input-stream input) + input (io/data-input-stream input)] + (binding [bfc/*state* (volatile! {:media [] :index {}})] + (let [team (teams/get-team conn + :profile-id profile-id + :project-id project-id) + + features (cfeat/get-team-enabled-features cf/flags team)] + + ;; Process all sections + (run! (fn [section] + (l/dbg :hint "reading section" :section section ::l/sync? true) + (assert-read-label! input section) + (let [options (-> cfg + (assoc ::bfc/features features) + (assoc ::section section) + (assoc ::input input))] + (binding [bfc/*options* options] + (events/tap :progress {:op :import :section section}) + (read-section options)))) + [:v1/metadata :v1/files :v1/rels :v1/sobjects]) + + (bfc/apply-pending-migrations! cfg) + + ;; Knowing that the ids of the created files are in index, + ;; just lookup them and return it as a set + (let [files (-> bfc/*state* deref :files)] + (into #{} (keep #(get-in @bfc/*state* [:index %])) files)))))) + +(defmethod read-import :v1 + [options] + (db/tx-run! options read-import-v1)) + +(defmethod read-section :v1/metadata + [{:keys [::input]}] + (let [{:keys [version files]} (read-obj! input)] + (l/dbg :hint "metadata readed" + :version (:full version) + :files (mapv str files) + ::l/sync? true) + (vswap! bfc/*state* update :index bfc/update-index files) + (vswap! bfc/*state* assoc :version version :files files))) + +(defn- remap-thumbnails + [thumbnails file-id] + (mapv (fn [thumbnail] + (-> thumbnail + (assoc :file-id file-id) + (update :object-id #(str/replace-first % #"^(.*?)/" (str file-id "/"))))) + thumbnails)) + +(defn- clean-features + [file] + (update file :features (fn [features] + (if (set? features) + (-> features + (cfeat/migrate-legacy-features) + (set/difference cfeat/backend-only-features)) + #{})))) + +(defmethod read-section :v1/files + [{:keys [::db/conn ::input ::project-id ::bfc/overwrite ::name] :as system}] + + (doseq [[idx expected-file-id] (d/enumerate (-> bfc/*state* deref :files))] + (let [file (read-obj! input) + media (read-obj! input) + + file-id (:id file) + file-id' (bfc/lookup-index file-id) + + file (clean-features file) + thumbnails (:thumbnails file)] + + (when (not= file-id expected-file-id) + (ex/raise :type :validation + :code :inconsistent-penpot-file + :found-id file-id + :expected-id expected-file-id + :hint "the penpot file seems corrupt, found unexpected uuid (file-id)")) + + (l/dbg :hint "processing file" + :id (str file-id) + :features (:features file) + :version (-> file :data :version) + :media (count media) + :thumbnails (count thumbnails) + ::l/sync? true) + + (when (seq thumbnails) + (let [thumbnails (remap-thumbnails thumbnails file-id')] + (l/dbg :hint "updated index with thumbnails" :total (count thumbnails) ::l/sync? true) + (vswap! bfc/*state* update :thumbnails bfc/into-vec thumbnails))) + + (when (seq media) + ;; Update index with media + (l/dbg :hint "update index with media" :total (count media) ::l/sync? true) + (vswap! bfc/*state* update :index bfc/update-index (map :id media)) + + ;; Store file media for later insertion + (l/dbg :hint "update media references" ::l/sync? true) + (vswap! bfc/*state* update :media into (map #(update % :id bfc/lookup-index)) media)) + + (let [file (-> file + (assoc :id file-id') + (cond-> (and (= idx 0) (some? name)) + (assoc :name name)) + (assoc :project-id project-id) + (dissoc :thumbnails) + (bfc/process-file))] + + ;; All features that are enabled and requires explicit migration are + ;; added to the state for a posterior migration step. + (doseq [feature (-> (::bfc/features system) + (set/difference cfeat/no-migration-features) + (set/difference (:features file)))] + (vswap! bfc/*state* update :pending-to-migrate (fnil conj []) [feature file-id'])) + + (l/dbg :hint "create file" :id (str file-id') ::l/sync? true) + (bfc/persist-file! system file) + + (when overwrite + (db/delete! conn :file-thumbnail {:file-id file-id'})) + + file-id')))) + +(defmethod read-section :v1/rels + [{:keys [::db/conn ::input ::bfc/timestamp]}] + (let [rels (read-obj! input) + ids (into #{} (-> bfc/*state* deref :files))] + ;; Insert all file relations + (doseq [{:keys [library-file-id] :as rel} rels] + (let [rel (-> rel + (assoc :synced-at timestamp) + (update :file-id bfc/lookup-index) + (update :library-file-id bfc/lookup-index))] + + (if (contains? ids library-file-id) + (do + (l/dbg :hint "create file library link" + :file-id (:file-id rel) + :lib-id (:library-file-id rel) + ::l/sync? true) + (db/insert! conn :file-library-rel rel)) + + (l/warn :hint "ignoring file library link" + :file-id (:file-id rel) + :lib-id (:library-file-id rel) + ::l/sync? true)))))) + +(defmethod read-section :v1/sobjects + [{:keys [::sto/storage ::db/conn ::input ::bfc/overwrite ::bfc/timestamp]}] + (let [storage (media/configure-assets-storage storage) + ids (read-obj! input) + thumb? (into #{} (map :media-id) (:thumbnails @bfc/*state*))] + + (doseq [expected-storage-id ids] + (let [id (read-uuid! input) + mdata (read-obj! input)] + + (when (not= id expected-storage-id) + (ex/raise :type :validation + :code :inconsistent-penpot-file + :hint "the penpot file seems corrupt, found unexpected uuid (storage-object-id)")) + + (l/dbg :hint "readed storage object" :id (str id) ::l/sync? true) + + (let [[size resource] (read-stream! input) + hash (sto/calculate-hash resource) + content (-> (sto/content resource size) + (sto/wrap-with-hash hash)) + + params (-> mdata + (assoc ::sto/content content) + (assoc ::sto/deduplicate? true) + (assoc ::sto/touched-at timestamp)) + + params (if (thumb? id) + (assoc params :bucket "file-object-thumbnail") + (assoc params :bucket "file-media-object")) + + sobject (sto/put-object! storage params)] + + (l/dbg :hint "persisted storage object" + :old-id (str id) + :new-id (str (:id sobject)) + :is-thumbnail (boolean (thumb? id)) + ::l/sync? true) + + (vswap! bfc/*state* update :index assoc id (:id sobject))))) + + (doseq [item (:media @bfc/*state*)] + (l/dbg :hint "inserting file media object" + :id (str (:id item)) + :file-id (str (:file-id item)) + ::l/sync? true) + + (let [file-id (bfc/lookup-index (:file-id item))] + (if (= file-id (:file-id item)) + (l/warn :hint "ignoring file media object" :file-id (str file-id) ::l/sync? true) + (db/insert! conn :file-media-object + (-> item + (assoc :file-id file-id) + (d/update-when :media-id bfc/lookup-index) + (d/update-when :thumbnail-id bfc/lookup-index)) + {::db/on-conflict-do-nothing? overwrite})))) + + (doseq [item (:thumbnails @bfc/*state*)] + (let [item (update item :media-id bfc/lookup-index)] + (l/dbg :hint "inserting file object thumbnail" + :file-id (str (:file-id item)) + :media-id (str (:media-id item)) + :object-id (:object-id item) + ::l/sync? true) + (db/insert! conn :file-tagged-object-thumbnail item + {::db/on-conflict-do-nothing? overwrite}))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; HIGH LEVEL API +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn export-files! + "Do the exportation of a specified file in custom penpot binary + format. There are some options available for customize the output: + + `::include-libraries`: additionally to the specified file, all the + linked libraries also will be included (including transitive + dependencies). + + `::embed-assets`: instead of including the libraries, embed in the + same file library all assets used from external libraries." + + [{:keys [::ids] :as cfg} output] + + (dm/assert! + "expected a set of uuid's for `::ids` parameter" + (and (set? ids) + (every? uuid? ids))) + + (dm/assert! + "expected instance of jio/IOFactory for `input`" + (satisfies? jio/IOFactory output)) + + (let [id (uuid/next) + tp (dt/tpoint) + ab (volatile! false) + cs (volatile! nil)] + (try + (l/info :hint "start exportation" :export-id (str id)) + (pu/with-open [output (io/output-stream output)] + (binding [*position* (atom 0)] + (write-export! (assoc cfg ::output output)))) + + (catch java.io.IOException _cause + ;; Do nothing, EOF means client closes connection abruptly + (vreset! ab true) + nil) + + (catch Throwable cause + (vreset! cs cause) + (vreset! ab true) + (throw cause)) + + (finally + (l/info :hint "exportation finished" :export-id (str id) + :elapsed (str (inst-ms (tp)) "ms") + :aborted @ab + :cause @cs))))) + +(defn import-files! + [cfg input] + + (dm/assert! + "expected valid profile-id and project-id on `cfg`" + (and (uuid? (::profile-id cfg)) + (uuid? (::project-id cfg)))) + + (dm/assert! + "expected instance of jio/IOFactory for `input`" + (satisfies? jio/IOFactory input)) + + (let [id (uuid/next) + tp (dt/tpoint) + cs (volatile! nil)] + + (l/info :hint "import: started" :id (str id)) + (try + (binding [*position* (atom 0)] + (pu/with-open [input (io/input-stream input)] + (read-import! (assoc cfg ::input input)))) + + (catch ZstdIOException cause + (ex/raise :type :validation + :code :invalid-penpot-file + :hint "invalid penpot file received: probably truncated" + :cause cause)) + + (catch Throwable cause + (vreset! cs cause) + (throw cause)) + + (finally + (l/info :hint "import: terminated" + :id (str id) + :elapsed (dt/format-duration (tp)) + :error? (some? @cs)))))) + diff --git a/backend/src/app/binfile/v2.clj b/backend/src/app/binfile/v2.clj new file mode 100644 index 0000000000..1a5f103425 --- /dev/null +++ b/backend/src/app/binfile/v2.clj @@ -0,0 +1,442 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.binfile.v2 + "A sqlite3 based binary file exportation with support for exportation + of entire team (or multiple teams) at once." + (:refer-clojure :exclude [read]) + (:require + [app.binfile.common :as bfc] + [app.common.data :as d] + [app.common.features :as cfeat] + [app.common.logging :as l] + [app.common.transit :as t] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.db.sql :as sql] + [app.loggers.audit :as-alias audit] + [app.loggers.webhooks :as-alias webhooks] + [app.media :as media] + [app.storage :as sto] + [app.storage.tmp :as tmp] + [app.util.events :as events] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [clojure.set :as set] + [cuerdas.core :as str] + [datoteka.io :as io] + [promesa.util :as pu]) + (:import + java.sql.DriverManager)) + +(set! *warn-on-reflection* true) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; LOW LEVEL API +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- create-database + ([cfg] + (let [path (tmp/tempfile :prefix "penpot.binfile." :suffix ".sqlite")] + (create-database cfg path))) + ([cfg path] + (let [db (DriverManager/getConnection (str "jdbc:sqlite:" path))] + (assoc cfg ::db db ::path path)))) + +(def ^:private + sql:create-kvdata-table + "CREATE TABLE kvdata ( + tag text NOT NULL, + key text NOT NULL, + val text NOT NULL, + dat blob NULL + )") + +(def ^:private + sql:create-kvdata-index + "CREATE INDEX kvdata__tag_key__idx + ON kvdata (tag, key)") + +(defn- setup-schema! + [{:keys [::db]}] + (db/exec-one! db [sql:create-kvdata-table]) + (db/exec-one! db [sql:create-kvdata-index])) + +(defn- write! + [{:keys [::db]} tag k v & [data]] + (db/insert! db :kvdata + {:tag (d/name tag) + :key (str k) + :val (t/encode-str v {:type :json-verbose}) + :dat data} + {::db/return-keys false})) + +(defn- read-blob + [{:keys [::db]} tag k] + (let [obj (db/get db :kvdata + {:tag (d/name tag) + :key (str k)} + {::sql/columns [:dat]})] + (:dat obj))) + +(defn- read-seq + ([{:keys [::db]} tag] + (->> (db/query db :kvdata + {:tag (d/name tag)} + {::sql/columns [::val]}) + (map :val) + (map t/decode-str))) + ([{:keys [::db]} tag k] + (->> (db/query db :kvdata + {:tag (d/name tag) + :key (str k)} + {::sql/columns [::val]}) + (map :val) + (map t/decode-str)))) + +(defn- read-obj + [{:keys [::db]} tag k] + (let [obj (db/get db :kvdata + {:tag (d/name tag) + :key (str k)} + {::sql/columns [:val]})] + (-> obj :val t/decode-str))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; IMPORT/EXPORT IMPL +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(declare ^:private write-project!) +(declare ^:private write-file!) + +(defn- write-team! + [cfg team-id] + + (let [team (bfc/get-team cfg team-id) + fonts (bfc/get-fonts cfg team-id)] + + (events/tap :progress + {:op :export + :section :write-team + :id team-id + :name (:name team)}) + + (l/trc :hint "write" :obj "team" + :id (str team-id) + :fonts (count fonts)) + + (when-let [photo-id (:photo-id team)] + (vswap! bfc/*state* update :storage-objects conj photo-id)) + + (vswap! bfc/*state* update :teams conj team-id) + (vswap! bfc/*state* bfc/collect-storage-objects fonts) + + (write! cfg :team team-id team) + + (doseq [{:keys [id] :as font} fonts] + (vswap! bfc/*state* update :team-font-variants conj id) + (write! cfg :team-font-variant id font)))) + +(defn- write-project! + [cfg project-id] + (let [project (bfc/get-project cfg project-id)] + (events/tap :progress + {:op :export + :section :write-project + :id project-id + :name (:name project)}) + (l/trc :hint "write" :obj "project" :id (str project-id)) + (write! cfg :project (str project-id) project) + (vswap! bfc/*state* update :projects conj project-id))) + +(defn- write-file! + [cfg file-id] + (let [file (bfc/get-file cfg file-id) + thumbs (bfc/get-file-object-thumbnails cfg file-id) + media (bfc/get-file-media cfg file) + rels (bfc/get-files-rels cfg #{file-id})] + + (events/tap :progress + {:op :export + :section :write-file + :id file-id + :name (:name file)}) + + (vswap! bfc/*state* (fn [state] + (-> state + (update :files conj file-id) + (update :file-media-objects into bfc/xf-map-id media) + (bfc/collect-storage-objects thumbs) + (bfc/collect-storage-objects media)))) + + (write! cfg :file file-id file) + (write! cfg :file-rels file-id rels) + + (run! (partial write! cfg :file-media-object file-id) media) + (run! (partial write! cfg :file-object-thumbnail file-id) thumbs) + + (when-let [thumb (bfc/get-file-thumbnail cfg file)] + (vswap! bfc/*state* bfc/collect-storage-objects [thumb]) + (write! cfg :file-thumbnail file-id thumb)) + + (l/trc :hint "write" :obj "file" + :thumbnails (count thumbs) + :rels (count rels) + :media (count media)))) + +(defn- write-storage-object! + [{:keys [::sto/storage] :as cfg} id] + (let [sobj (sto/get-object storage id) + data (with-open [input (sto/get-object-data storage sobj)] + (io/read-as-bytes input))] + + (l/trc :hint "write" :obj "storage-object" :id (str id) :size (:size sobj)) + (write! cfg :storage-object id (meta sobj) data))) + +(defn- read-storage-object! + [{:keys [::sto/storage ::bfc/timestamp] :as cfg} id] + (let [mdata (read-obj cfg :storage-object id) + data (read-blob cfg :storage-object id) + hash (sto/calculate-hash data) + + content (-> (sto/content data) + (sto/wrap-with-hash hash)) + + params (-> mdata + (assoc ::sto/content content) + (assoc ::sto/deduplicate? true) + (assoc ::sto/touched-at timestamp)) + + sobject (sto/put-object! storage params)] + + (vswap! bfc/*state* update :index assoc id (:id sobject)) + + (l/trc :hint "read" :obj "storage-object" + :id (str id) + :new-id (str (:id sobject)) + :size (:size sobject)))) + +(defn read-team! + [{:keys [::db/conn ::bfc/timestamp] :as cfg} team-id] + (l/trc :hint "read" :obj "team" :id (str team-id)) + + (let [team (read-obj cfg :team team-id) + team (-> team + (update :id bfc/lookup-index) + (update :photo-id bfc/lookup-index) + (assoc :created-at timestamp) + (assoc :modified-at timestamp))] + + (events/tap :progress + {:op :import + :section :read-team + :id team-id + :name (:name team)}) + + (db/insert! conn :team + (update team :features db/encode-pgarray conn "text") + ::db/return-keys false) + + (doseq [font (->> (read-seq cfg :team-font-variant) + (filter #(= team-id (:team-id %))))] + (let [font (-> font + (update :id bfc/lookup-index) + (update :team-id bfc/lookup-index) + (update :woff1-file-id bfc/lookup-index) + (update :woff2-file-id bfc/lookup-index) + (update :ttf-file-id bfc/lookup-index) + (update :otf-file-id bfc/lookup-index) + (assoc :created-at timestamp) + (assoc :modified-at timestamp))] + (db/insert! conn :team-font-variant font + ::db/return-keys false))) + + team)) + +(defn read-project! + [{:keys [::db/conn ::bfc/timestamp] :as cfg} project-id] + (l/trc :hint "read" :obj "project" :id (str project-id)) + + (let [project (read-obj cfg :project project-id) + project (-> project + (update :id bfc/lookup-index) + (update :team-id bfc/lookup-index) + (assoc :created-at timestamp) + (assoc :modified-at timestamp))] + + (events/tap :progress + {:op :import + :section :read-project + :id project-id + :name (:name project)}) + + (db/insert! conn :project project + ::db/return-keys false))) + +(defn read-file! + [{:keys [::db/conn ::bfc/timestamp] :as cfg} file-id] + (l/trc :hint "read" :obj "file" :id (str file-id)) + + (let [file (-> (read-obj cfg :file file-id) + (update :id bfc/lookup-index) + (update :project-id bfc/lookup-index) + (bfc/process-file))] + + (events/tap :progress + {:op :import + :section :read-file + :id file-id + :name (:name file)}) + + ;; All features that are enabled and requires explicit migration are + ;; added to the state for a posterior migration step. + (doseq [feature (-> (::bfc/features cfg) + (set/difference cfeat/no-migration-features) + (set/difference (:features file)))] + (vswap! bfc/*state* update :pending-to-migrate (fnil conj []) [feature (:id file)])) + + (bfc/persist-file! cfg file)) + + (doseq [thumbnail (read-seq cfg :file-object-thumbnail file-id)] + (let [thumbnail (-> thumbnail + (update :file-id bfc/lookup-index) + (update :media-id bfc/lookup-index)) + file-id (:file-id thumbnail) + + thumbnail (update thumbnail :object-id + #(str/replace-first % #"^(.*?)/" (str file-id "/")))] + + (db/insert! conn :file-tagged-object-thumbnail thumbnail + {::db/return-keys false}))) + + (doseq [rel (read-obj cfg :file-rels file-id)] + (let [rel (-> rel + (update :file-id bfc/lookup-index) + (update :library-file-id bfc/lookup-index) + (assoc :synced-at timestamp))] + (db/insert! conn :file-library-rel rel + ::db/return-keys false))) + + (doseq [media (read-seq cfg :file-media-object file-id)] + (let [media (-> media + (update :id bfc/lookup-index) + (update :file-id bfc/lookup-index) + (update :media-id bfc/lookup-index) + (update :thumbnail-id bfc/lookup-index))] + (db/insert! conn :file-media-object media + ::db/return-keys false + ::sql/on-conflict-do-nothing true)))) + +(def ^:private empty-summary + {:teams #{} + :files #{} + :projects #{} + :file-media-objects #{} + :team-font-variants #{} + :storage-objects #{}}) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; PUBLIC API +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn export-team! + [cfg team-id] + (let [id (uuid/next) + tp (dt/tpoint) + + cfg (-> (create-database cfg) + (update ::sto/storage media/configure-assets-storage))] + + (l/inf :hint "start" + :operation "export" + :id (str id) + :path (str (::path cfg))) + + (try + (db/tx-run! cfg (fn [cfg] + (setup-schema! cfg) + (binding [bfc/*state* (volatile! empty-summary)] + (write-team! cfg team-id) + + (run! (partial write-project! cfg) + (bfc/get-team-projects cfg team-id)) + + (run! (partial write-file! cfg) + (bfc/get-team-files cfg team-id)) + + (run! (partial write-storage-object! cfg) + (-> bfc/*state* deref :storage-objects)) + + (write! cfg :manifest "team-id" team-id) + (write! cfg :manifest "objects" (deref bfc/*state*)) + + (::path cfg)))) + (finally + (pu/close! (::db cfg)) + + (let [elapsed (tp)] + (l/inf :hint "end" + :operation "export" + :id (str id) + :elapsed (dt/format-duration elapsed))))))) + +(defn import-team! + [cfg path] + (let [id (uuid/next) + tp (dt/tpoint) + + cfg (-> (create-database cfg path) + (update ::sto/storage media/configure-assets-storage) + (assoc ::bfc/timestamp (dt/now)))] + + (l/inf :hint "start" + :operation "import" + :id (str id) + :path (str (::path cfg))) + + (try + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (db/exec-one! conn ["SET idle_in_transaction_session_timeout = 0"]) + (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"]) + + (binding [bfc/*state* (volatile! {:index {}})] + (let [objects (read-obj cfg :manifest "objects")] + + ;; We first process all storage objects, they have + ;; deduplication so we can't rely on simple reindex. This + ;; operation populates the index for all storage objects. + (run! (partial read-storage-object! cfg) (:storage-objects objects)) + + ;; Populate index with all the incoming objects + (vswap! bfc/*state* update :index + (fn [index] + (-> index + (bfc/update-index (:teams objects)) + (bfc/update-index (:projects objects)) + (bfc/update-index (:files objects)) + (bfc/update-index (:file-media-objects objects)) + (bfc/update-index (:team-font-variants objects))))) + + (let [team-id (read-obj cfg :manifest "team-id") + team (read-team! cfg team-id) + features (cfeat/get-team-enabled-features cf/flags team) + cfg (assoc cfg ::bfc/features features)] + + (run! (partial read-project! cfg) (:projects objects)) + (run! (partial read-file! cfg) (:files objects)) + + ;; (run-pending-migrations! cfg) + + team))))) + (finally + (pu/close! (::db cfg)) + + (let [elapsed (tp)] + (l/inf :hint "end" + :operation "import" + :id (str id) + :elapsed (dt/format-duration elapsed))))))) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index ed98060d2b..a9e883b8ff 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -79,6 +79,8 @@ :telemetry-uri "https://telemetry.penpot.app/" + :media-max-file-size (* 1024 1024 30) ; 30MiB + :ldap-user-query "(|(uid=:username)(mail=:username))" :ldap-attrs-username "uid" :ldap-attrs-email "mail" @@ -203,6 +205,7 @@ (s/def ::storage-assets-s3-bucket ::us/string) (s/def ::storage-assets-s3-region ::us/keyword) (s/def ::storage-assets-s3-endpoint ::us/string) +(s/def ::storage-assets-s3-io-threads ::us/integer) (s/def ::telemetry-uri ::us/string) (s/def ::telemetry-with-taiga ::us/boolean) (s/def ::tenant ::us/string) @@ -294,6 +297,7 @@ ::redis-uri ::registration-domain-whitelist ::rpc-rlimit-config + ::rpc-climit-config ::semaphore-process-font ::semaphore-process-image @@ -319,6 +323,7 @@ ::storage-assets-s3-bucket ::storage-assets-s3-region ::storage-assets-s3-endpoint + ::storage-assets-s3-io-threads ::telemetry-enabled ::telemetry-uri ::telemetry-referer diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 13046c15c0..097ada50a1 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -19,6 +19,7 @@ [app.util.json :as json] [app.util.time :as dt] [clojure.java.io :as io] + [clojure.set :as set] [clojure.spec.alpha :as s] [integrant.core :as ig] [next.jdbc :as jdbc] @@ -97,7 +98,7 @@ :with-credentials (and (contains? cfg ::username) (contains? cfg ::password)) :min-size (::min-size cfg) - :max-size (::max-size cfg)) + :max-size (::max-size cfg)) (create-pool cfg))) (defmethod ig/halt-key! ::pool @@ -145,6 +146,10 @@ [v] (instance? javax.sql.DataSource v)) +(defn connection? + [conn] + (instance? Connection conn)) + (s/def ::conn some?) (s/def ::nilable-pool (s/nilable ::pool)) (s/def ::pool pool?) @@ -227,49 +232,155 @@ `(jdbc/with-transaction ~@args))) (defn open - [pool] - (jdbc/get-connection pool)) + [system-or-pool] + (if (pool? system-or-pool) + (jdbc/get-connection system-or-pool) + (if (map? system-or-pool) + (open (::pool system-or-pool)) + (throw (IllegalArgumentException. "unable to resolve connection pool"))))) + +(defn get-update-count + [result] + (:next.jdbc/update-count result)) + +(defn get-connection + [cfg-or-conn] + (if (connection? cfg-or-conn) + cfg-or-conn + (if (map? cfg-or-conn) + (get-connection (::conn cfg-or-conn)) + (throw (IllegalArgumentException. "unable to resolve connection"))))) + +(defn connection-map? + "Check if the provided value is a map like data structure that + contains a database connection." + [o] + (and (map? o) (connection? (::conn o)))) + +(defn get-connectable + "Resolve to a connection or connection pool instance; if it is not + possible, raises an exception" + [o] + (cond + (connection? o) o + (pool? o) o + (map? o) (get-connectable (or (::conn o) (::pool o))) + :else (throw (IllegalArgumentException. "unable to resolve connectable")))) + +(def ^:private params-mapping + {::return-keys? :return-keys + ::return-keys :return-keys}) + +(defn rename-opts + [opts] + (set/rename-keys opts params-mapping)) + +(def ^:private default-insert-opts + {:builder-fn sql/as-kebab-maps + :return-keys true}) (def ^:private default-opts {:builder-fn sql/as-kebab-maps}) (defn exec! - ([ds sv] - (jdbc/execute! ds sv default-opts)) + ([ds sv] (exec! ds sv nil)) ([ds sv opts] - (jdbc/execute! ds sv (merge default-opts opts)))) + (let [conn (get-connectable ds) + opts (if (empty? opts) + default-opts + (into default-opts (rename-opts opts)))] + (jdbc/execute! conn sv opts)))) (defn exec-one! - ([ds sv] - (jdbc/execute-one! ds sv default-opts)) + ([ds sv] (exec-one! ds sv nil)) ([ds sv opts] - (jdbc/execute-one! ds sv - (-> (merge default-opts opts) - (assoc :return-keys (::return-keys? opts false)))))) + (let [conn (get-connectable ds) + opts (if (empty? opts) + default-opts + (into default-opts (rename-opts opts)))] + (jdbc/execute-one! conn sv opts)))) (defn insert! + "A helper that builds an insert sql statement and executes it. By + default returns the inserted row with all the field; you can delimit + the returned columns with the `::columns` option." [ds table params & {:as opts}] - (exec-one! ds - (sql/insert table params opts) - (merge {::return-keys? true} opts))) + (let [conn (get-connectable ds) + sql (sql/insert table params opts) + opts (if (empty? opts) + default-insert-opts + (into default-insert-opts (rename-opts opts)))] + (jdbc/execute-one! conn sql opts))) -(defn insert-multi! +(defn insert-many! + "An optimized version of `insert!` that perform insertion of multiple + values at once. + + This expands to a single SQL statement with placeholders for every + value being inserted. For large data sets, this may exceed the limit + of sql string size and/or number of parameters." [ds table cols rows & {:as opts}] - (exec! ds - (sql/insert-multi table cols rows opts) - (merge {::return-keys? true} opts))) + (let [conn (get-connectable ds) + sql (sql/insert-many table cols rows opts) + opts (if (empty? opts) + default-insert-opts + (into default-insert-opts (rename-opts opts))) + opts (update opts :return-keys boolean)] + (jdbc/execute! conn sql opts))) (defn update! + "A helper that build an UPDATE SQL statement and executes it. + + Given a connectable object, a table name, a hash map of columns and + values to set, and either a hash map of columns and values to search + on or a vector of a SQL where clause and parameters, perform an + update on the table. + + By default returns an object with the number of affected rows; a + complete row can be returned if you pass `::return-keys` with `true` + or with a vector of columns. + + Also it can be combined with the `::many` option if you perform an + update to multiple rows and you want all the affected rows to be + returned." [ds table params where & {:as opts}] - (exec-one! ds - (sql/update table params where opts) - (merge {::return-keys? true} opts))) + (let [conn (get-connectable ds) + sql (sql/update table params where opts) + opts (if (empty? opts) + default-opts + (into default-opts (rename-opts opts))) + opts (update opts :return-keys boolean)] + (if (::many opts) + (jdbc/execute! conn sql opts) + (jdbc/execute-one! conn sql opts)))) (defn delete! + "A helper that builds an DELETE SQL statement and executes it. + + Given a connectable object, a table name, and either a hash map of columns + and values to search on or a vector of a SQL where clause and parameters, + perform a delete on the table. + + By default returns an object with the number of affected rows; a + complete row can be returned if you pass `::return-keys` with `true` + or with a vector of columns. + + Also it can be combined with the `::many` option if you perform an + update to multiple rows and you want all the affected rows to be + returned." [ds table params & {:as opts}] - (exec-one! ds - (sql/delete table params opts) - (merge {::return-keys? true} opts))) + (let [conn (get-connectable ds) + sql (sql/delete table params opts) + opts (if (empty? opts) + default-opts + (into default-opts (rename-opts opts)))] + (if (::many opts) + (jdbc/execute! conn sql opts) + (jdbc/execute-one! conn sql opts)))) + +(defn query + [ds table params & {:as opts}] + (exec! ds (sql/select table params opts) opts)) (defn is-row-deleted? [{:keys [deleted-at]}] @@ -283,7 +394,7 @@ [ds table params & {:as opts}] (let [rows (exec! ds (sql/select table params opts)) rows (cond->> rows - (::remove-deleted? opts true) + (::remove-deleted opts true) (remove is-row-deleted?))] (first rows))) @@ -292,7 +403,7 @@ filters. Raises :not-found exception if no object is found." [ds table params & {:as opts}] (let [row (get* ds table params opts)] - (when (and (not row) (::check-deleted? opts true)) + (when (and (not row) (::check-deleted opts true)) (ex/raise :type :not-found :code :object-not-found :table table @@ -301,16 +412,32 @@ (defn plan [ds sql] - (jdbc/plan ds sql sql/default-opts)) + (-> (get-connectable ds) + (jdbc/plan sql sql/default-opts))) + +(defn cursor + "Return a lazy seq of rows using server side cursors" + [conn query & {:keys [chunk-size] :or {chunk-size 25}}] + (let [cname (str (gensym "cursor_")) + fquery [(str "FETCH " chunk-size " FROM " cname)]] + + ;; declare cursor + (exec-one! conn + (if (vector? query) + (into [(str "DECLARE " cname " CURSOR FOR " (nth query 0))] + (rest query)) + [(str "DECLARE " cname " CURSOR FOR " query)])) + + ;; return a lazy seq + ((fn fetch-more [] + (lazy-seq + (when-let [chunk (seq (exec! conn fquery))] + (concat chunk (fetch-more)))))))) (defn get-by-id [ds table id & {:as opts}] (get ds table {:id id} opts)) -(defn query - [ds table params & {:as opts}] - (exec! ds (sql/select table params opts))) - (defn pgobject? ([v] (instance? PGobject v)) @@ -363,6 +490,10 @@ (.createArrayOf conn ^String type (into-array Object objects)) (.createArrayOf conn ^String type objects)))) +(defn encode-pgarray + [data conn type] + (create-array conn type data)) + (defn decode-pgpoint [^PGpoint v] (gpt/point (.-x v) (.-y v))) @@ -371,10 +502,6 @@ [data] (org.postgresql.util.PGInterval. ^String data)) -(defn connection? - [conn] - (instance? Connection conn)) - (defn savepoint ([^Connection conn] (.setSavepoint conn)) @@ -382,57 +509,74 @@ (.setSavepoint conn (name label)))) (defn release! - [^Connection conn ^Savepoint sp ] + [^Connection conn ^Savepoint sp] (.releaseSavepoint conn sp)) (defn rollback! - ([^Connection conn] - (.rollback conn)) - ([^Connection conn ^Savepoint sp] - (.rollback conn sp))) + ([conn] + (if (and (map? conn) (::savepoint conn)) + (rollback! conn (::savepoint conn)) + (let [^Connection conn (get-connection conn)] + (l/trc :hint "explicit rollback requested") + (.rollback conn)))) + ([conn ^Savepoint sp] + (let [^Connection conn (get-connection conn)] + (l/trc :hint "explicit rollback requested (savepoint)") + (.rollback conn sp)))) (defn tx-run! - [cfg f] + [system f & params] (cond - (connection? cfg) - (tx-run! {::conn cfg} f) + (connection? system) + (tx-run! {::conn system} f) - (pool? cfg) - (tx-run! {::pool cfg} f) + (pool? system) + (tx-run! {::pool system} f) - (::conn cfg) - (let [conn (::conn cfg) + (::conn system) + (let [conn (::conn system) sp (savepoint conn)] (try - (let [result (f cfg)] - (release! conn sp) + (let [system' (-> system + (assoc ::savepoint sp) + (dissoc ::rollback)) + result (apply f system' params)] + (if (::rollback system) + (rollback! conn sp) + (release! conn sp)) result) (catch Throwable cause - (rollback! sp) + (.rollback ^Connection conn ^Savepoint sp) (throw cause)))) - (::pool cfg) - (with-atomic [conn (::pool cfg)] - (f (assoc cfg ::conn conn))) + (::pool system) + (with-atomic [conn (::pool system)] + (let [system' (-> system + (assoc ::conn conn) + (dissoc ::rollback)) + result (apply f system' params)] + (when (::rollback system) + (rollback! conn)) + result)) :else - (throw (IllegalArgumentException. "invalid arguments")))) + (throw (IllegalArgumentException. "invalid system/cfg provided")))) (defn run! - [cfg f] + [system f & params] (cond - (connection? cfg) - (run! {::conn cfg} f) + (connection? system) + (run! {::conn system} f) - (pool? cfg) - (run! {::pool cfg} f) + (pool? system) + (run! {::pool system} f) - (::conn cfg) - (f cfg) + (::conn system) + (apply f system params) - (::pool cfg) - (with-open [^Connection conn (open (::pool cfg))] - (f (assoc cfg ::conn conn))) + (::pool system) + (with-open [^Connection conn (open (::pool system))] + (apply f (assoc system ::conn conn) params)) :else (throw (IllegalArgumentException. "invalid arguments")))) @@ -506,11 +650,6 @@ (.setType "jsonb") (.setValue (json/encode-str data))))) -(defn get-update-count - [result] - (:next.jdbc/update-count result)) - - ;; --- Locks (def ^:private siphash-state diff --git a/backend/src/app/db/sql.clj b/backend/src/app/db/sql.clj index 854b2211cb..42641039e3 100644 --- a/backend/src/app/db/sql.clj +++ b/backend/src/app/db/sql.clj @@ -29,11 +29,14 @@ ([table key-map opts] (let [opts (merge default-opts opts) opts (cond-> opts - (:on-conflict-do-nothing opts) + (::db/on-conflict-do-nothing? opts) + (assoc :suffix "ON CONFLICT DO NOTHING") + + (::on-conflict-do-nothing opts) (assoc :suffix "ON CONFLICT DO NOTHING"))] (sql/for-insert table key-map opts)))) -(defn insert-multi +(defn insert-many [table cols rows opts] (let [opts (merge default-opts opts)] (sql/for-insert-multi table cols rows opts))) @@ -44,22 +47,30 @@ ([table where-params opts] (let [opts (merge default-opts opts) opts (cond-> opts - (::db/for-update? opts) (assoc :suffix "FOR UPDATE") - (::db/for-share? opts) (assoc :suffix "FOR KEY SHARE") - (:for-update opts) (assoc :suffix "FOR UPDATE") - (:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))] + (::order-by opts) (assoc :order-by (::order-by opts)) + (::columns opts) (assoc :columns (::columns opts)) + (::for-update opts) (assoc :suffix "FOR UPDATE") + (::for-share opts) (assoc :suffix "FOR SHARE"))] (sql/for-query table where-params opts)))) (defn update ([table key-map where-params] (update table key-map where-params nil)) ([table key-map where-params opts] - (let [opts (merge default-opts opts)] + (let [opts (into default-opts opts) + keys (::db/return-keys opts) + opts (if (vector? keys) + (assoc opts :suffix (str "RETURNING " (sql/as-cols keys opts))) + opts)] (sql/for-update table key-map where-params opts)))) (defn delete ([table where-params] (delete table where-params nil)) ([table where-params opts] - (let [opts (merge default-opts opts)] + (let [opts (merge default-opts opts) + keys (::db/return-keys opts) + opts (if (vector? keys) + (assoc opts :suffix (str "RETURNING " (sql/as-cols keys opts))) + opts)] (sql/for-delete table where-params opts)))) diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index 8fdcd1e3c4..2cc47a37ac 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -311,6 +311,12 @@ ^String (::password cfg)) (let [^MimeMessage message (create-smtp-message cfg session params)] + (l/dbg :hint "sendmail" + :id (:id params) + :to (:to params) + :subject (str/trim (:subject params)) + :body (str/join "," (map :type (:body params)))) + (.sendMessage ^Transport transport ^MimeMessage message (.getAllRecipients message)))))) diff --git a/backend/src/app/features/components_v2.clj b/backend/src/app/features/components_v2.clj new file mode 100644 index 0000000000..db4ca2536d --- /dev/null +++ b/backend/src/app/features/components_v2.clj @@ -0,0 +1,1852 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.features.components-v2 + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.exceptions :as ex] + [app.common.files.changes :as cp] + [app.common.files.changes-builder :as fcb] + [app.common.files.helpers :as cfh] + [app.common.files.libraries-helpers :as cflh] + [app.common.files.migrations :as fmg] + [app.common.files.shapes-helpers :as cfsh] + [app.common.files.validate :as cfv] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] + [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.path :as gshp] + [app.common.logging :as l] + [app.common.math :as mth] + [app.common.schema :as sm] + [app.common.svg :as csvg] + [app.common.svg.shapes-builder :as sbuilder] + [app.common.types.color :as ctc] + [app.common.types.component :as ctk] + [app.common.types.components-list :as ctkl] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.types.grid :as ctg] + [app.common.types.modifiers :as ctm] + [app.common.types.page :as ctp] + [app.common.types.pages-list :as ctpl] + [app.common.types.shape :as cts] + [app.common.types.shape-tree :as ctst] + [app.common.types.shape.path :as ctsp] + [app.common.types.shape.text :as ctsx] + [app.common.uuid :as uuid] + [app.db :as db] + [app.db.sql :as sql] + [app.features.fdata :as fdata] + [app.media :as media] + [app.rpc.commands.files :as files] + [app.rpc.commands.files-snapshot :as fsnap] + [app.rpc.commands.media :as cmd.media] + [app.storage :as sto] + [app.storage.tmp :as tmp] + [app.svgo :as svgo] + [app.util.blob :as blob] + [app.util.cache :as cache] + [app.util.events :as events] + [app.util.pointer-map :as pmap] + [app.util.time :as dt] + [buddy.core.codecs :as bc] + [clojure.set :refer [rename-keys]] + [cuerdas.core :as str] + [datoteka.io :as io] + [promesa.exec :as px] + [promesa.util :as pu])) + +(def ^:dynamic *stats* + "A dynamic var for setting up state for collect stats globally." + nil) + +(def ^:dynamic *cache* + "A dynamic var for setting up a cache instance." + nil) + +(def ^:dynamic *skip-on-graphic-error* + "A dynamic var for setting up the default error behavior for graphics processing." + nil) + +(def ^:dynamic ^:private *system* + "An internal var for making the current `system` available to all + internal functions without the need to explicitly pass it top down." + nil) + +(def ^:dynamic ^:private *file-stats* + "An internal dynamic var for collect stats by file." + nil) + +(def ^:dynamic ^:private *team-stats* + "An internal dynamic var for collect stats by team." + nil) + +(def grid-gap 50) +(def frame-gap 200) +(def max-group-size 50) + +(defn decode-row + [{:keys [features data] :as row}] + (cond-> row + (some? features) + (assoc :features (db/decode-pgarray features #{})) + + (some? data) + (assoc :data (blob/decode data)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; FILE PREPARATION BEFORE MIGRATION +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def valid-recent-color? + (sm/lazy-validator ::ctc/recent-color)) + +(def valid-color? + (sm/lazy-validator ::ctc/color)) + +(def valid-fill? + (sm/lazy-validator ::cts/fill)) + +(def valid-stroke? + (sm/lazy-validator ::cts/stroke)) + +(def valid-flow? + (sm/lazy-validator ::ctp/flow)) + +(def valid-text-content? + (sm/lazy-validator ::ctsx/content)) + +(def valid-path-content? + (sm/lazy-validator ::ctsp/content)) + +(def valid-path-segment? + (sm/lazy-validator ::ctsp/segment)) + +(def valid-rgb-color-string? + (sm/lazy-validator ::ctc/rgb-color)) + +(def valid-shape-points? + (sm/lazy-validator ::cts/points)) + +(def valid-image-attrs? + (sm/lazy-validator ::cts/image-attrs)) + +(def valid-column-grid-params? + (sm/lazy-validator ::ctg/column-params)) + +(def valid-square-grid-params? + (sm/lazy-validator ::ctg/square-params)) + + +(defn- prepare-file-data + "Apply some specific migrations or fixes to things that are allowed in v1 but not in v2, + or that are the result of old bugs." + [file-data libraries] + (let [detached-ids (volatile! #{}) + + detach-shape + (fn [container shape] + ;; Detach a shape and make necessary adjustments. + (let [is-component? (let [root-shape (ctst/get-shape container (:id container))] + (and (some? root-shape) (nil? (:parent-id root-shape)))) + parent (ctst/get-shape container (:parent-id shape)) + in-copy? (ctn/in-any-component? (:objects container) parent)] + + (letfn [(detach-recursive [container shape first?] + + ;; If the shape is inside a component, add it to detached-ids. This list is used + ;; later to process other copies that was referencing a detached nested copy. + (when is-component? + (vswap! detached-ids conj (:id shape))) + + ;; Detach the shape and all children until we find a subinstance. + (if (or first? in-copy? (not (ctk/instance-head? shape))) + (as-> container $ + (ctn/update-shape $ (:id shape) ctk/detach-shape) + (reduce #(detach-recursive %1 %2 false) + $ + (map (d/getf (:objects container)) (:shapes shape)))) + + ;; If this is a subinstance head and the initial shape whas not itself a + ;; nested copy, stop detaching and promote it to root. + (ctn/update-shape container (:id shape) #(assoc % :component-root true))))] + + (detach-recursive container shape true)))) + + fix-bad-children + (fn [file-data] + ;; Remove any child that does not exist. And also remove duplicated children. + (letfn [(fix-container [container] + (d/update-when container :objects update-vals (partial fix-shape container))) + + (fix-shape [container shape] + (let [objects (:objects container)] + (d/update-when shape :shapes + (fn [shapes] + (->> shapes + (d/removev #(nil? (get objects %))) + (into [] (distinct)))))))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + fix-missing-image-metadata + (fn [file-data] + ;; Delete broken image shapes with no metadata. + (letfn [(fix-container [container] + (d/update-when container :objects #(reduce-kv fix-shape % %))) + + (fix-shape [objects id shape] + (if (and (cfh/image-shape? shape) + (nil? (:metadata shape))) + (-> objects + (dissoc id) + (d/update-in-when [(:parent-id shape) :shapes] + (fn [shapes] (filterv #(not= id %) shapes)))) + objects))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + fix-invalid-page + (fn [file-data] + (letfn [(update-page [page] + (-> page + (update :name (fn [name] + (if (nil? name) + "Page" + name))) + (update :options fix-options))) + + (fix-background [options] + (if (and (contains? options :background) + (not (valid-rgb-color-string? (:background options)))) + (dissoc options :background) + options)) + + (fix-saved-grids [options] + (d/update-when options :saved-grids + (fn [grids] + (cond-> grids + (and (contains? grids :column) + (not (valid-column-grid-params? (:column grids)))) + (dissoc :column) + + (and (contains? grids :row) + (not (valid-column-grid-params? (:row grids)))) + (dissoc :row) + + (and (contains? grids :square) + (not (valid-square-grid-params? (:square grids)))) + (dissoc :square))))) + + (fix-options [options] + (-> options + ;; Some pages has invalid data on flows, we proceed just to + ;; delete them. + (d/update-when :flows #(filterv valid-flow? %)) + (fix-saved-grids) + (fix-background)))] + + (update file-data :pages-index update-vals update-page))) + + ;; Sometimes we found that the file has issues in the internal + ;; data structure of the local library; this function tries to + ;; fix that issues. + fix-file-data + (fn [file-data] + (letfn [(fix-colors-library [colors] + (let [colors (dissoc colors nil)] + (reduce-kv (fn [colors id color] + (if (valid-color? color) + colors + (dissoc colors id))) + colors + colors)))] + (-> file-data + (d/update-when :colors fix-colors-library) + (d/update-when :typographies dissoc nil)))) + + fix-big-geometry-shapes + (fn [file-data] + ;; At some point in time, we had a bug that generated shapes + ;; with huge geometries that did not validate the + ;; schema. Since we don't have a way to fix those shapes, we + ;; simply proceed to delete it. We ignore path type shapes + ;; because they have not been affected by the bug. + (letfn [(fix-container [container] + (d/update-when container :objects #(reduce-kv fix-shape % %))) + + (fix-shape [objects id shape] + (cond + (or (cfh/path-shape? shape) + (cfh/bool-shape? shape)) + objects + + (or (and (number? (:x shape)) (not (sm/valid-safe-number? (:x shape)))) + (and (number? (:y shape)) (not (sm/valid-safe-number? (:y shape)))) + (and (number? (:width shape)) (not (sm/valid-safe-number? (:width shape)))) + (and (number? (:height shape)) (not (sm/valid-safe-number? (:height shape))))) + (-> objects + (dissoc id) + (d/update-in-when [(:parent-id shape) :shapes] + (fn [shapes] (filterv #(not= id %) shapes)))) + + :else + objects))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + ;; Some files has totally broken shapes, we just remove them + fix-completly-broken-shapes + (fn [file-data] + (letfn [(update-object [objects id shape] + (cond + (nil? (:type shape)) + (let [ids (cfh/get-children-ids objects id)] + (-> objects + (dissoc id) + (as-> $ (reduce dissoc $ ids)) + (d/update-in-when [(:parent-id shape) :shapes] + (fn [shapes] (filterv #(not= id %) shapes))))) + + (and (cfh/text-shape? shape) + (not (valid-text-content? (:content shape)))) + (dissoc objects id) + + (and (cfh/path-shape? shape) + (not (valid-path-content? (:content shape)))) + (-> objects + (dissoc id) + (d/update-in-when [(:parent-id shape) :shapes] + (fn [shapes] (filterv #(not= id %) shapes)))) + + :else + objects)) + + (update-container [container] + (d/update-when container :objects #(reduce-kv update-object % %)))] + + (-> file-data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + + fix-shape-geometry + (fn [file-data] + (letfn [(fix-container [container] + (d/update-when container :objects update-vals fix-shape)) + + (fix-shape [shape] + (cond + (and (cfh/image-shape? shape) + (valid-image-attrs? shape) + (grc/valid-rect? (:selrect shape)) + (not (valid-shape-points? (:points shape)))) + (let [selrect (:selrect shape) + metadata (:metadata shape) + selrect (grc/make-rect + (:x selrect) + (:y selrect) + (:width metadata) + (:height metadata)) + points (grc/rect->points selrect)] + (assoc shape + :selrect selrect + :points points)) + + (and (cfh/text-shape? shape) + (valid-text-content? (:content shape)) + (not (valid-shape-points? (:points shape))) + (seq (:position-data shape))) + (let [selrect (->> (:position-data shape) + (map (juxt :x :y :width :height)) + (map #(apply grc/make-rect %)) + (grc/join-rects)) + points (grc/rect->points selrect)] + + (assoc shape + :x (:x selrect) + :y (:y selrect) + :width (:width selrect) + :height (:height selrect) + :selrect selrect + :points points)) + + (and (cfh/text-shape? shape) + (valid-text-content? (:content shape)) + (not (valid-shape-points? (:points shape))) + (grc/valid-rect? (:selrect shape))) + (let [selrect (:selrect shape) + points (grc/rect->points selrect)] + (assoc shape + :x (:x selrect) + :y (:y selrect) + :width (:width selrect) + :height (:height selrect) + :points points)) + + (and (or (cfh/rect-shape? shape) + (cfh/svg-raw-shape? shape) + (cfh/circle-shape? shape)) + (not (valid-shape-points? (:points shape))) + (grc/valid-rect? (:selrect shape))) + (let [selrect (if (grc/valid-rect? (:svg-viewbox shape)) + (:svg-viewbox shape) + (:selrect shape)) + points (grc/rect->points selrect)] + (assoc shape + :x (:x selrect) + :y (:y selrect) + :width (:width selrect) + :height (:height selrect) + :selrect selrect + :points points)) + + (and (= :icon (:type shape)) + (grc/valid-rect? (:selrect shape)) + (valid-shape-points? (:points shape))) + (-> shape + (assoc :type :rect) + (dissoc :content) + (dissoc :metadata) + (dissoc :segments) + (dissoc :x1 :y1 :x2 :y2)) + + (and (cfh/group-shape? shape) + (grc/valid-rect? (:selrect shape)) + (not (valid-shape-points? (:points shape)))) + (assoc shape :points (grc/rect->points (:selrect shape))) + + :else + shape))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + fix-empty-components + (fn [file-data] + (letfn [(fix-component [components id component] + (let [root-shape (ctst/get-shape component (:id component))] + (if (or (empty? (:objects component)) + (nil? root-shape) + (nil? (:type root-shape))) + (dissoc components id) + components)))] + + (-> file-data + (d/update-when :components #(reduce-kv fix-component % %))))) + + fix-components-with-component-root + ;;In v1 no components in the library should have component-root + (fn [file-data] + (letfn [(fix-container [container] + (d/update-when container :objects update-vals fix-shape)) + + (fix-shape [shape] + (dissoc shape :component-root))] + + (-> file-data + (update :components update-vals fix-container)))) + + fix-non-existing-component-ids + ;; Check component ids have valid values. + (fn [file-data] + (let [libraries (assoc-in libraries [(:id file-data) :data] file-data)] + (letfn [(fix-container [container] + (d/update-when container :objects update-vals fix-shape)) + + (fix-shape [shape] + (let [component-id (:component-id shape) + component-file (:component-file shape) + library (get libraries component-file)] + + (cond-> shape + (and (some? component-id) + (some? library) + (nil? (ctkl/get-component (:data library) component-id))) + (ctk/detach-shape))))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container))))) + + fix-misc-shape-issues + (fn [file-data] + (letfn [(fix-container [container] + (d/update-when container :objects update-vals fix-shape)) + + (fix-gap-value [gap] + (if (or (= gap ##Inf) + (= gap ##-Inf)) + 0 + gap)) + + (fix-shape [shape] + (cond-> shape + ;; Some shapes has invalid gap value + (contains? shape :layout-gap) + (update :layout-gap (fn [layout-gap] + (if (number? layout-gap) + {:row-gap layout-gap :column-gap layout-gap} + (-> layout-gap + (d/update-when :column-gap fix-gap-value) + (d/update-when :row-gap fix-gap-value))))) + + ;; Fix name if missing + (nil? (:name shape)) + (assoc :name (d/name (:type shape))) + + ;; Remove v2 info from components that have been copied and pasted + ;; from a v2 file + (some? (:main-instance shape)) + (dissoc :main-instance) + + (and (contains? shape :transform) + (not (gmt/valid-matrix? (:transform shape)))) + (assoc :transform (gmt/matrix)) + + (and (contains? shape :transform-inverse) + (not (gmt/valid-matrix? (:transform-inverse shape)))) + (assoc :transform-inverse (gmt/matrix)) + + ;; Fix broken fills + (seq (:fills shape)) + (update :fills (fn [fills] (filterv valid-fill? fills))) + + ;; Fix broken strokes + (seq (:strokes shape)) + (update :strokes (fn [strokes] (filterv valid-stroke? strokes))) + + ;; Fix some broken layout related attrs, probably + ;; of copypaste on flex layout betatest period + (true? (:layout shape)) + (assoc :layout :flex)))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + ;; There are some bugs in the past that allows convert text to + ;; path and this fix tries to identify this cases and fix them converting + ;; the shape back to text shape + + fix-text-shapes-converted-to-path + (fn [file-data] + (letfn [(fix-container [container] + (d/update-when container :objects update-vals fix-shape)) + + (fix-shape [shape] + (if (and (cfh/path-shape? shape) + (contains? shape :content) + (some? (:selrect shape)) + (valid-text-content? (:content shape))) + (let [selrect (:selrect shape)] + (-> shape + (assoc :x (:x selrect)) + (assoc :y (:y selrect)) + (assoc :width (:width selrect)) + (assoc :height (:height selrect)) + (assoc :type :text))) + shape))] + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + fix-broken-paths + (fn [file-data] + (letfn [(fix-container [container] + (d/update-when container :objects update-vals fix-shape)) + + (fix-shape [shape] + (cond + (and (cfh/path-shape? shape) + (seq (:content shape)) + (not (valid-path-content? (:content shape)))) + (let [shape (update shape :content fix-path-content)] + (if (not (valid-path-content? (:content shape))) + shape + (let [[points selrect] (gshp/content->points+selrect shape (:content shape))] + (-> shape + (dissoc :bool-content) + (dissoc :bool-type) + (assoc :points points) + (assoc :selrect selrect))))) + + ;; When we fount a bool shape with no content, + ;; we convert it to a simple rect + (and (cfh/bool-shape? shape) + (not (seq (:bool-content shape)))) + (let [selrect (or (:selrect shape) + (grc/make-rect)) + points (grc/rect->points selrect)] + (-> shape + (assoc :x (:x selrect)) + (assoc :y (:y selrect)) + (assoc :width (:height selrect)) + (assoc :height (:height selrect)) + (assoc :selrect selrect) + (assoc :points points) + (assoc :type :rect) + (assoc :transform (gmt/matrix)) + (assoc :transform-inverse (gmt/matrix)) + (dissoc :bool-content) + (dissoc :shapes) + (dissoc :content))) + + :else + shape)) + + (fix-path-content [content] + (let [[seg1 :as content] (filterv valid-path-segment? content)] + (if (and seg1 (not= :move-to (:command seg1))) + (let [params (select-keys (:params seg1) [:x :y])] + (into [{:command :move-to :params params}] content)) + content)))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + fix-recent-colors + (fn [file-data] + ;; Remove invalid colors in :recent-colors + (d/update-when file-data :recent-colors + (fn [colors] + (filterv valid-recent-color? colors)))) + + fix-broken-parents + (fn [file-data] + ;; Find children shapes whose parent-id is not set to the parent that contains them. + ;; Remove them from the parent :shapes list. + (letfn [(fix-container [container] + (d/update-when container :objects #(reduce-kv fix-shape % %))) + + (fix-shape [objects id shape] + (reduce (fn [objects child-id] + (let [child (get objects child-id)] + (cond-> objects + (and (some? child) (not= id (:parent-id child))) + (d/update-in-when [id :shapes] + (fn [shapes] (filterv #(not= child-id %) shapes)))))) + objects + (:shapes shape)))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + fix-orphan-shapes + (fn [file-data] + ;; Find shapes that are not listed in their parent's children list. + ;; Remove them, and also their children + (letfn [(fix-container [container] + (reduce fix-shape container (ctn/shapes-seq container))) + + (fix-shape + [container shape] + (if-not (or (= (:id shape) uuid/zero) + (nil? (:parent-id shape))) + (let [parent (ctst/get-shape container (:parent-id shape)) + exists? (d/index-of (:shapes parent) (:id shape))] + (if (nil? exists?) + (let [ids (cfh/get-children-ids-with-self (:objects container) (:id shape))] + (update container :objects #(reduce dissoc % ids))) + container)) + container))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + remove-nested-roots + (fn [file-data] + ;; Remove :component-root in head shapes that are nested. + (letfn [(fix-container [container] + (d/update-when container :objects update-vals (partial fix-shape container))) + + (fix-shape [container shape] + (let [parent (ctst/get-shape container (:parent-id shape))] + (if (and (ctk/instance-root? shape) + (ctn/in-any-component? (:objects container) parent)) + (dissoc shape :component-root) + shape)))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + add-not-nested-roots + (fn [file-data] + ;; Add :component-root in head shapes that are not nested. + (letfn [(fix-container [container] + (d/update-when container :objects update-vals (partial fix-shape container))) + + (fix-shape [container shape] + (let [parent (ctst/get-shape container (:parent-id shape))] + (if (and (ctk/subinstance-head? shape) + (not (ctn/in-any-component? (:objects container) parent))) + (assoc shape :component-root true) + shape)))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + fix-orphan-copies + (fn [file-data] + ;; Detach shapes that were inside a copy (have :shape-ref) but now they aren't. + (letfn [(fix-container [container] + (reduce fix-shape container (ctn/shapes-seq container))) + + (fix-shape [container shape] + (let [shape (ctst/get-shape container (:id shape)) ; Get the possibly updated shape + parent (ctst/get-shape container (:parent-id shape))] + (if (and (ctk/in-component-copy? shape) + (not (ctk/instance-head? shape)) + (not (ctk/in-component-copy? parent))) + (detach-shape container shape) + container)))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + fix-components-without-id + (fn [file-data] + ;; We have detected some components that have no :id attribute. + ;; Regenerate it from the components map. + (letfn [(fix-component [id component] + (if (some? (:id component)) + component + (assoc component :id id)))] + + (-> file-data + (d/update-when :components #(d/mapm fix-component %))))) + + remap-refs + (fn [file-data] + ;; Remap shape-refs so that they point to the near main. + ;; At the same time, if there are any dangling ref, detach the shape and its children. + (let [count (volatile! 0) + + fix-shape + (fn [container shape] + (if (ctk/in-component-copy? shape) + ;; First look for the direct shape. + (let [root (ctn/get-component-shape (:objects container) shape) + libraries (assoc-in libraries [(:id file-data) :data] file-data) + library (get libraries (:component-file root)) + component (ctkl/get-component (:data library) (:component-id root) true) + direct-shape (ctf/get-component-shape (:data library) component (:shape-ref shape))] + (if (some? direct-shape) + ;; If it exists, there is nothing else to do. + container + ;; If not found, find the near shape. + (let [near-shape (d/seek #(= (:shape-ref %) (:shape-ref shape)) + (ctf/get-component-shapes (:data library) component))] + (if (some? near-shape) + ;; If found, update the ref to point to the near shape. + (do + (vswap! count inc) + (ctn/update-shape container (:id shape) #(assoc % :shape-ref (:id near-shape)))) + ;; If not found, it may be a fostered component. Try to locate a direct shape + ;; in the head component. + (let [head (ctn/get-head-shape (:objects container) shape) + library-2 (get libraries (:component-file head)) + component-2 (ctkl/get-component (:data library-2) (:component-id head) true) + direct-shape-2 (ctf/get-component-shape (:data library-2) component-2 (:shape-ref shape))] + (if (some? direct-shape-2) + ;; If it exists, there is nothing else to do. + container + ;; If not found, detach shape and all children. + ;; container + (do + (vswap! count inc) + (detach-shape container shape)))))))) + container)) + + fix-container + (fn [container] + (reduce fix-shape container (ctn/shapes-seq container)))] + + [(-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)) + @count])) + + remap-refs-recur + ;; remapping refs can generate cascade changes so we call it until no changes are done + (fn [file-data] + (loop [f-data file-data] + (let [[f-data count] (remap-refs f-data)] + (if (= count 0) + f-data + (recur f-data))))) + + fix-converted-copies + (fn [file-data] + ;; If the user has created a copy and then converted into a path or bool, + ;; detach it because the synchronization will no longer work. + (letfn [(fix-container [container] + (reduce fix-shape container (ctn/shapes-seq container))) + + (fix-shape [container shape] + (if (and (ctk/instance-head? shape) + (or (cfh/path-shape? shape) + (cfh/bool-shape? shape))) + (detach-shape container shape) + container))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + wrap-non-group-component-roots + (fn [file-data] + ;; Some components have a root that is not a group nor a frame + ;; (e.g. a path or a svg-raw). We need to wrap them in a frame + ;; for this one to became the root. + (letfn [(fix-component [component] + (let [root-shape (ctst/get-shape component (:id component))] + (if (or (cfh/group-shape? root-shape) + (cfh/frame-shape? root-shape)) + component + (let [new-id (uuid/next) + frame (-> (cts/setup-shape + {:type :frame + :id (:id component) + :x (:x (:selrect root-shape)) + :y (:y (:selrect root-shape)) + :width (:width (:selrect root-shape)) + :height (:height (:selrect root-shape)) + :name (:name component) + :shapes [new-id] + :show-content true}) + (assoc :frame-id nil + :parent-id nil)) + root-shape' (assoc root-shape + :id new-id + :parent-id (:id frame) + :frame-id (:id frame))] + (update component :objects assoc + (:id frame) frame + (:id root-shape') root-shape')))))] + + (-> file-data + (d/update-when :components update-vals fix-component)))) + + detach-non-group-instance-roots + (fn [file-data] + ;; If there is a copy instance whose root is not a frame or a group, it cannot + ;; be easily repaired, and anyway it's not working in production, so detach it. + (letfn [(fix-container [container] + (reduce fix-shape container (ctn/shapes-seq container))) + + (fix-shape [container shape] + (if (and (ctk/instance-head? shape) + (not (#{:group :frame} (:type shape)))) + (detach-shape container shape) + container))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + transform-to-frames + (fn [file-data] + ;; Transform component and copy heads fron group to frames, and set the + ;; frame-id of its childrens + (letfn [(fix-container [container] + (d/update-when container :objects update-vals fix-shape)) + + (fix-shape [shape] + (if (or (nil? (:parent-id shape)) (ctk/instance-head? shape)) + (let [frame? (= :frame (:type shape)) + not-group? (not= :group (:type shape))] + (assoc shape ; Old groups must be converted + :type :frame ; to frames and conform to spec + :fills (if not-group? (d/nilv (:fills shape) []) []) ; Groups never should have fill + :shapes (or (:shapes shape) []) + :hide-in-viewer (if frame? (boolean (:hide-in-viewer shape)) true) + :show-content (if frame? (boolean (:show-content shape)) true) + :rx (or (:rx shape) 0) + :ry (or (:ry shape) 0))) + shape))] + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + remap-frame-ids + (fn [file-data] + ;; Remap the frame-ids of the primary childs of the head instances + ;; to point to the head instance. + (letfn [(fix-container + [container] + (d/update-when container :objects update-vals (partial fix-shape container))) + + (fix-shape + [container shape] + (let [parent (ctst/get-shape container (:parent-id shape))] + (if (ctk/instance-head? parent) + (assoc shape :frame-id (:id parent)) + shape)))] + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + fix-frame-ids + (fn [file-data] + ;; Ensure that frame-id of all shapes point to the parent or to the frame-id + ;; of the parent, and that the destination is indeed a frame. + (letfn [(fix-container [container] + (d/update-when container :objects #(cfh/reduce-objects % fix-shape %))) + + (fix-shape [objects shape] + (let [parent (when (:parent-id shape) + (get objects (:parent-id shape))) + error? (when (some? parent) + (if (= (:type parent) :frame) + (not= (:frame-id shape) (:id parent)) + (not= (:frame-id shape) (:frame-id parent))))] + (if error? + (let [nearest-frame (cfh/get-frame objects (:parent-id shape)) + frame-id (or (:id nearest-frame) uuid/zero)] + (update objects (:id shape) assoc :frame-id frame-id)) + objects)))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + fix-component-nil-objects + (fn [file-data] + ;; Ensure that objects of all components is not null + (letfn [(fix-component [component] + (if (and (contains? component :objects) (nil? (:objects component))) + (if (:deleted component) + (assoc component :objects {}) + (dissoc component :objects)) + component))] + (-> file-data + (d/update-when :components update-vals fix-component)))) + + fix-false-copies + (fn [file-data] + ;; Find component heads that are not main-instance but have not :shape-ref. + ;; Also shapes that have :shape-ref but are not in a copy. + (letfn [(fix-container [container] + (reduce fix-shape container (ctn/shapes-seq container))) + + (fix-shape + [container shape] + (if (or (and (ctk/instance-head? shape) + (not (ctk/main-instance? shape)) + (not (ctk/in-component-copy? shape))) + (and (ctk/in-component-copy? shape) + (nil? (ctn/get-head-shape (:objects container) shape {:allow-main? true})))) + (detach-shape container shape) + container))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)))) + + + fix-component-root-without-component + (fn [file-data] + ;; Ensure that if component-root is set component-file and component-id are set too + (letfn [(fix-container [container] + (d/update-when container :objects update-vals fix-shape)) + + (fix-shape [shape] + (cond-> shape + (and (ctk/instance-root? shape) + (or (not (ctk/instance-head? shape)) + (not (some? (:component-file shape))))) + (dissoc :component-id + :component-file + :component-root)))] + (-> file-data + (update :pages-index update-vals fix-container)))) + + + fix-copies-names + (fn [file-data] + ;; Rename component heads to add the component path to the name + (letfn [(fix-container [container] + (d/update-when container :objects #(cfh/reduce-objects % fix-shape %))) + + (fix-shape [objects shape] + (let [root (ctn/get-component-shape objects shape) + libraries (assoc-in libraries [(:id file-data) :data] file-data) + library (get libraries (:component-file root)) + component (ctkl/get-component (:data library) (:component-id root) true) + path (str/trim (:path component))] + (if (and (ctk/instance-head? shape) + (some? component) + (= (:name component) (:name shape)) + (not (str/empty? path))) + (update objects (:id shape) assoc :name (str path " / " (:name component))) + objects)))] + + (-> file-data + (update :pages-index update-vals fix-container)))) + + fix-copies-of-detached + (fn [file-data] + ;; Find any copy that is referencing a shape inside a component that have + ;; been detached in a previous fix. If so, undo the nested copy, converting + ;; it into a direct copy. + ;; + ;; WARNING: THIS SHOULD BE CALLED AT THE END OF THE PROCESS. + (letfn [(fix-container [container] + (reduce fix-shape container (ctn/shapes-seq container))) + (fix-shape [container shape] + (cond-> container + (@detached-ids (:shape-ref shape)) + (detach-shape shape)))] + + (-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container))))] + + (-> file-data + (fix-file-data) + (fix-invalid-page) + (fix-misc-shape-issues) + (fix-recent-colors) + (fix-missing-image-metadata) + (fix-text-shapes-converted-to-path) + (fix-broken-paths) + (fix-big-geometry-shapes) + (fix-shape-geometry) + (fix-empty-components) + (fix-components-with-component-root) + (fix-non-existing-component-ids) + (fix-completly-broken-shapes) + (fix-bad-children) + (fix-broken-parents) + (fix-orphan-shapes) + (fix-orphan-copies) + (remove-nested-roots) + (add-not-nested-roots) + (fix-components-without-id) + (fix-converted-copies) + (remap-refs-recur) + (wrap-non-group-component-roots) + (detach-non-group-instance-roots) + (transform-to-frames) + (remap-frame-ids) + (fix-frame-ids) + (fix-component-nil-objects) + (fix-false-copies) + (fix-component-root-without-component) + (fix-copies-names) + (fix-copies-of-detached)))); <- Do not add fixes after this and fix-orphan-copies call + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; COMPONENTS MIGRATION +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- get-asset-groups + [assets generic-name] + (let [;; Group by first element of the path. + groups (d/group-by #(first (cfh/split-path (:path %))) assets) + ;; If there is a group called as the generic-name we have to preserve it + unames (into #{} (keep str) (keys groups)) + groups (rename-keys groups {generic-name (cfh/generate-unique-name unames generic-name)}) + + ;; Split large groups in chunks of max-group-size elements + groups (loop [groups (seq groups) + result {}] + (if (empty? groups) + result + (let [[group-name assets] (first groups) + group-name (if (or (nil? group-name) (str/empty? group-name)) + generic-name + group-name)] + (if (<= (count assets) max-group-size) + (recur (next groups) + (assoc result group-name assets)) + (let [splits (-> (partition-all max-group-size assets) + (d/enumerate))] + (recur (next groups) + (reduce (fn [result [index split]] + (let [split-name (str group-name " " (inc index))] + (assoc result split-name split))) + result + splits))))))) + + ;; Sort assets in each group by path + groups (update-vals groups (fn [assets] + (sort-by (fn [{:keys [path name]}] + (str/lower (cfh/merge-path-item path name))) + assets)))] + + ;; Sort groups by name + (into (sorted-map) groups))) + +(defn- create-frame + [name position width height] + (cts/setup-shape + {:type :frame + :x (:x position) + :y (:y position) + :width (+ width grid-gap) + :height (+ height grid-gap) + :name name + :frame-id uuid/zero + :parent-id uuid/zero})) + +(defn- migrate-components + "If there is any component in the file library, add a new 'Library + backup', generate main instances for all components there and remove + shapes from library components. Mark the file with the :components-v2 option." + [file-data libraries] + (let [file-data (prepare-file-data file-data libraries) + components (ctkl/components-seq file-data)] + (if (empty? components) + (assoc-in file-data [:options :components-v2] true) + (let [[file-data page-id start-pos] + (ctf/get-or-add-library-page file-data frame-gap) + + migrate-component-shape + (fn [shape delta component-file component-id frame-id] + (cond-> shape + (nil? (:parent-id shape)) + (assoc :parent-id frame-id + :main-instance true + :component-root true + :component-file component-file + :component-id component-id) + + (nil? (:frame-id shape)) + (assoc :frame-id frame-id) + + :always + (gsh/move delta))) + + add-main-instance + (fn [file-data component frame-id position] + (let [shapes (cfh/get-children-with-self (:objects component) + (:id component)) + + ;; Let's calculate the top shame name from the components path and name + root-shape (-> (first shapes) + (assoc :name (cfh/merge-path-item (:path component) (:name component)))) + + shapes (assoc shapes 0 root-shape) + + orig-pos (gpt/point (:x root-shape) (:y root-shape)) + delta (gpt/subtract position orig-pos) + + xf-shape (map #(migrate-component-shape % + delta + (:id file-data) + (:id component) + frame-id)) + new-shapes + (into [] xf-shape shapes) + + find-frame-id ; if its parent is a frame, the frame-id should be the parent-id + (fn [page shape] + (let [parent (ctst/get-shape page (:parent-id shape))] + (if (= :frame (:type parent)) + (:id parent) + (:frame-id parent)))) + + add-shapes + (fn [page] + (reduce (fn [page shape] + (ctst/add-shape (:id shape) + shape + page + (find-frame-id page shape) + (:parent-id shape) + nil ; <- As shapes are ordered, we can safely add each + true)) ; one at the end of the parent's children list. + page + new-shapes)) + + update-component + (fn [component] + (-> component + (assoc :main-instance-id (:id root-shape) + :main-instance-page page-id) + (dissoc :objects)))] + + (-> file-data + (ctpl/update-page page-id add-shapes) + (ctkl/update-component (:id component) update-component)))) + + add-instance-grid + (fn [fdata frame-id grid assets] + (reduce (fn [result [component position]] + (events/tap :progress {:op :migrate-component + :id (:id component) + :name (:name component)}) + (add-main-instance result component frame-id (gpt/add position + (gpt/point grid-gap grid-gap)))) + fdata + (d/zip assets grid))) + + add-instance-grids + (fn [fdata] + (let [components (ctkl/components-seq fdata) + groups (get-asset-groups components "Components")] + (loop [groups (seq groups) + fdata fdata + position start-pos] + (if (empty? groups) + fdata + (let [[group-name assets] (first groups) + grid (ctst/generate-shape-grid + (map (partial ctf/get-component-root fdata) assets) + position + grid-gap) + {:keys [width height]} (meta grid) + frame (create-frame group-name position width height) + fdata (ctpl/update-page fdata + page-id + #(ctst/add-shape (:id frame) + frame + % + (:id frame) + (:id frame) + nil + true))] + (recur (next groups) + (add-instance-grid fdata (:id frame) grid assets) + (gpt/add position (gpt/point 0 (+ height (* 2 grid-gap) frame-gap)))))))))] + + (let [total (count components)] + (some-> *stats* (swap! update :processed-components (fnil + 0) total)) + (some-> *team-stats* (swap! update :processed-components (fnil + 0) total)) + (some-> *file-stats* (swap! assoc :processed-components total))) + + (add-instance-grids file-data))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; GRAPHICS MIGRATION +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- create-shapes-for-bitmap + "Convert a media object that contains a bitmap image into shapes, + one shape of type :image and one group that contains it." + [{:keys [name width height id mtype]} frame-id position] + (let [frame-shape (-> (cts/setup-shape + {:type :frame + :x (:x position) + :y (:y position) + :width width + :height height + :name name + :frame-id frame-id + :parent-id frame-id}) + (assoc + :proportion (float (/ width height)) + :proportion-lock true)) + + img-shape (cts/setup-shape + {:type :image + :x (:x position) + :y (:y position) + :width width + :height height + :metadata {:id id + :width width + :height height + :mtype mtype} + :name name + :frame-id (:id frame-shape) + :parent-id (:id frame-shape) + :constraints-h :scale + :constraints-v :scale})] + [frame-shape [img-shape]])) + +(defn- parse-datauri + [data] + (let [[mtype b64-data] (str/split data ";base64," 2) + mtype (subs mtype (inc (str/index-of mtype ":"))) + data (-> b64-data bc/str->bytes bc/b64->bytes)] + [mtype data])) + +(defn- extract-name + [href] + (let [query-idx (d/nilv (str/last-index-of href "?") 0) + href (if (> query-idx 0) (subs href 0 query-idx) href) + filename (->> (str/split href "/") (last)) + ext-idx (str/last-index-of filename ".")] + (if (> ext-idx 0) (subs filename 0 ext-idx) filename))) + +(defn- collect-and-persist-images + [svg-data file-id media-id] + (letfn [(process-image [{:keys [href] :as item}] + (try + (let [item (if (str/starts-with? href "data:") + (let [[mtype data] (parse-datauri href) + size (alength data) + path (tmp/tempfile :prefix "penpot.media.download.") + written (io/write-to-file! data path :size size)] + + (when (not= written size) + (ex/raise :type :internal + :code :mismatch-write-size + :hint "unexpected state: unable to write to file")) + + (-> item + (assoc :size size) + (assoc :path path) + (assoc :filename "tempfile") + (assoc :mtype mtype))) + + (let [result (cmd.media/download-image *system* href)] + (-> (merge item result) + (assoc :name (extract-name href)))))] + + ;; The media processing adds the data to the + ;; input map and returns it. + (media/run {:cmd :info :input item})) + + (catch Throwable _ + (l/wrn :hint "unable to process embedded images on svg file" + :file-id (str file-id) + :media-id (str media-id)) + nil))) + + (persist-image [acc {:keys [path size width height mtype href] :as item}] + (let [storage (::sto/storage *system*) + conn (::db/conn *system*) + hash (sto/calculate-hash path) + content (-> (sto/content path size) + (sto/wrap-with-hash hash)) + params {::sto/content content + ::sto/deduplicate? true + ::sto/touched-at (:ts item) + :content-type mtype + :bucket "file-media-object"} + image (sto/put-object! storage params) + fmo-id (uuid/next)] + + (db/exec-one! conn + [cmd.media/sql:create-file-media-object + fmo-id + file-id true (:name item "image") + (:id image) + nil + width + height + mtype]) + + (assoc acc href {:id fmo-id + :mtype mtype + :width width + :height height})))] + + (let [images (->> (csvg/collect-images svg-data) + (transduce (keep process-image) + (completing persist-image) {}))] + (assoc svg-data :image-data images)))) + +(defn- resolve-sobject-id + [id] + (let [fmobject (db/get *system* :file-media-object {:id id} + {::sql/columns [:media-id]})] + (:media-id fmobject))) + +(defn- get-sobject-content + [id] + (let [storage (::sto/storage *system*) + sobject (sto/get-object storage id)] + (with-open [stream (sto/get-object-data storage sobject)] + (slurp stream)))) + +(defn- create-shapes-for-svg + [{:keys [id] :as mobj} file-id objects frame-id position] + (let [get-svg (fn [sid] + (let [svg-text (get-sobject-content sid) + svg-text (svgo/optimize *system* svg-text)] + (-> (csvg/parse svg-text) + (assoc :name (:name mobj))))) + + sid (resolve-sobject-id id) + svg-data (if (cache/cache? *cache*) + (cache/get *cache* sid (px/wrap-bindings get-svg)) + (get-svg sid)) + + svg-data (collect-and-persist-images svg-data file-id id)] + + (sbuilder/create-svg-shapes svg-data position objects frame-id frame-id #{} false))) + +(defn- process-media-object + [fdata page-id frame-id mobj position shape-cb] + (let [page (ctpl/get-page fdata page-id) + file-id (get fdata :id) + + [shape children] + (if (= (:mtype mobj) "image/svg+xml") + (create-shapes-for-svg mobj file-id (:objects page) frame-id position) + (create-shapes-for-bitmap mobj frame-id position)) + + shape (assoc shape :name (-> "Graphics" + (cfh/merge-path-item (:path mobj)) + (cfh/merge-path-item (:name mobj)))) + + changes + (-> (fcb/empty-changes nil) + (fcb/set-save-undo? false) + (fcb/with-page page) + (fcb/with-objects (:objects page)) + (fcb/with-library-data fdata) + (fcb/delete-media (:id mobj)) + (fcb/add-objects (cons shape children))) + + ;; NOTE: this is a workaround for `generate-add-component`, it + ;; is needed because that function always starts from empty + ;; changes; so in this case we need manually add all shapes to + ;; the page and then use that page for the + ;; `generate-add-component` function + page + (reduce (fn [page shape] + (ctst/add-shape (:id shape) + shape + page + frame-id + frame-id + nil + true)) + page + (cons shape children)) + + [_ _ changes2] + (cflh/generate-add-component nil + [shape] + (:objects page) + (:id page) + file-id + true + nil + cfsh/prepare-create-artboard-from-selection) + changes (fcb/concat-changes changes changes2)] + + (shape-cb shape) + (:redo-changes changes))) + +(defn- create-media-grid + [fdata page-id frame-id grid media-group shape-cb] + (letfn [(process [fdata mobj position] + (let [position (gpt/add position (gpt/point grid-gap grid-gap)) + tp (dt/tpoint) + err (volatile! false)] + (try + (let [changes (process-media-object fdata page-id frame-id mobj position shape-cb)] + (cp/process-changes fdata changes false)) + + (catch Throwable cause + (vreset! err true) + (let [cause (pu/unwrap-exception cause) + edata (ex-data cause)] + (cond + (instance? org.xml.sax.SAXParseException cause) + (l/inf :hint "skip processing media object: invalid svg found" + :file-id (str (:id fdata)) + :id (str (:id mobj))) + + (instance? org.graalvm.polyglot.PolyglotException cause) + (l/inf :hint "skip processing media object: invalid svg found" + :file-id (str (:id fdata)) + :id (str (:id mobj))) + + (= (:type edata) :not-found) + (l/inf :hint "skip processing media object: underlying object does not exist" + :file-id (str (:id fdata)) + :id (str (:id mobj))) + + :else + (let [skip? *skip-on-graphic-error*] + (l/wrn :hint "unable to process file media object" + :skiped skip? + :file-id (str (:id fdata)) + :id (str (:id mobj)) + :cause cause) + (when-not skip? + (throw cause)))) + nil)) + (finally + (let [elapsed (tp)] + (l/trc :hint "graphic processed" + :file-id (str (:id fdata)) + :media-id (str (:id mobj)) + :error @err + :elapsed (dt/format-duration elapsed)))))))] + + (->> (d/zip media-group grid) + (reduce (fn [fdata [mobj position]] + (events/tap :progress {:op :migrate-graphic + :id (:id mobj) + :name (:name mobj)}) + (or (process fdata mobj position) fdata)) + (assoc-in fdata [:options :components-v2] true))))) + +(defn- fix-graphics-size + [fdata new-grid page-id frame-id] + (let [modify-shape (fn [page shape-id modifiers] + (ctn/update-shape page shape-id #(gsh/transform-shape % modifiers))) + + resize-frame (fn [page] + (let [{:keys [width height]} (meta new-grid) + + frame (ctst/get-shape page frame-id) + width (+ width grid-gap) + height (+ height grid-gap) + + modif-frame (ctm/resize nil + (gpt/point (/ width (:width frame)) + (/ height (:height frame))) + (gpt/point (:x frame) (:y frame)))] + + (modify-shape page frame-id modif-frame))) + + move-components (fn [page] + (let [frame (get (:objects page) frame-id) + shapes (map (d/getf (:objects page)) (:shapes frame))] + (->> (d/zip shapes new-grid) + (reduce (fn [page [shape position]] + (let [position (gpt/add position (gpt/point grid-gap grid-gap)) + modif-shape (ctm/move nil + (gpt/point (- (:x position) (:x (:selrect shape))) + (- (:y position) (:y (:selrect shape))))) + children-ids (cfh/get-children-ids-with-self (:objects page) (:id shape))] + (reduce #(modify-shape %1 %2 modif-shape) + page + children-ids))) + page))))] + (-> fdata + (ctpl/update-page page-id resize-frame) + (ctpl/update-page page-id move-components)))) + +(defn- migrate-graphics + [fdata] + (if (empty? (:media fdata)) + fdata + (let [[fdata page-id start-pos] + (ctf/get-or-add-library-page fdata frame-gap) + + media (->> (vals (:media fdata)) + (map (fn [{:keys [width height] :as media}] + (let [points (-> (grc/make-rect 0 0 width height) + (grc/rect->points))] + (assoc media :points points))))) + + groups (get-asset-groups media "Graphics")] + + (let [total (count media)] + (some-> *stats* (swap! update :processed-graphics (fnil + 0) total)) + (some-> *team-stats* (swap! update :processed-graphics (fnil + 0) total)) + (some-> *file-stats* (swap! assoc :processed-graphics total))) + + (loop [groups (seq groups) + fdata fdata + position start-pos] + (if (empty? groups) + fdata + (let [[group-name assets] (first groups) + grid (ctst/generate-shape-grid assets position grid-gap) + {:keys [width height]} (meta grid) + frame (create-frame group-name position width height) + fdata (ctpl/update-page fdata + page-id + #(ctst/add-shape (:id frame) + frame + % + (:id frame) + (:id frame) + nil + true)) + new-shapes (volatile! []) + + add-shape (fn [shape] + (vswap! new-shapes conj shape)) + + fdata' (create-media-grid fdata page-id (:id frame) grid assets add-shape) + + ;; When svgs had different width&height and viewport, sometimes the old graphics + ;; importer didn't calculate well the media object size. So, after migration we + ;; recalculate grid size from the actual size of the created shapes. + new-grid (ctst/generate-shape-grid @new-shapes position grid-gap) + + {new-width :width new-height :height} (meta new-grid) + + fdata'' (if-not (and (mth/close? width new-width) (mth/close? height new-height)) + (do + (l/inf :hint "fixing graphics sizes" + :file-id (str (:id fdata)) + :group group-name) + (fix-graphics-size fdata' new-grid page-id (:id frame))) + fdata')] + + (recur (next groups) + fdata'' + (gpt/add position (gpt/point 0 (+ height (* 2 grid-gap) frame-gap)))))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; PRIVATE HELPERS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- migrate-fdata + [fdata libs] + (let [migrated? (dm/get-in fdata [:options :components-v2])] + (if migrated? + fdata + (let [fdata (migrate-components fdata libs) + fdata (migrate-graphics fdata)] + (update fdata :options assoc :components-v2 true))))) + +(defn- fix-version + [file] + (let [file (fmg/fix-version file)] + (if (> (:version file) 22) + (assoc file :version 22) + file))) + +(defn- get-file + [system id] + (binding [pmap/*load-fn* (partial fdata/load-pointer system id)] + (-> (db/get system :file {:id id} + {::db/remove-deleted false + ::db/check-deleted false}) + (decode-row) + (update :data assoc :id id) + (update :data fdata/process-pointers deref) + (update :data fdata/process-objects (partial into {})) + (fix-version) + (fmg/migrate-file)))) + +(defn get-team + [system team-id] + (-> (db/get system :team {:id team-id} + {::db/remove-deleted false + ::db/check-deleted false}) + (update :features db/decode-pgarray #{}))) + +(defn- validate-file! + [file libs] + (cfv/validate-file! file libs) + (cfv/validate-file-schema! file)) + +(defn- persist-file! + [{:keys [::db/conn] :as system} {:keys [id] :as file}] + (let [file (if (contains? (:features file) "fdata/objects-map") + (fdata/enable-objects-map file) + file) + + file (if (contains? (:features file) "fdata/pointer-map") + (binding [pmap/*tracked* (pmap/create-tracked)] + (let [file (fdata/enable-pointer-map file)] + (fdata/persist-pointers! system id) + file)) + file) + + ;; Ensure all files has :data with id + file (update file :data assoc :id id)] + + (db/update! conn :file + {:data (blob/encode (:data file)) + :features (db/create-array conn "text" (:features file)) + :revn (:revn file)} + {:id (:id file)}))) + +(defn- process-file! + [{:keys [::db/conn] :as system} {:keys [id] :as file} & {:keys [validate?]}] + (let [libs (->> (files/get-file-libraries conn id) + (into [file] (comp (map :id) + (map (partial get-file system)))) + (d/index-by :id)) + + file (-> file + (update :data migrate-fdata libs) + (update :features conj "components/v2"))] + + (when validate? + (validate-file! file libs)) + + file)) + +(def ^:private sql:get-and-lock-team-files + "SELECT f.id + FROM file AS f + JOIN project AS p ON (p.id = f.project_id) + WHERE p.team_id = ? + AND p.deleted_at IS NULL + AND f.deleted_at IS NULL + FOR UPDATE") + +(defn get-and-lock-team-files + [conn team-id] + (->> (db/cursor conn [sql:get-and-lock-team-files team-id]) + (map :id))) + +(defn update-team! + [system {:keys [id] :as team}] + (let [conn (db/get-connection system) + params (-> team + (update :features db/encode-pgarray conn "text") + (dissoc :id))] + (db/update! conn :team + params + {:id id}) + team)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; PUBLIC API +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn migrate-file! + [system file-id & {:keys [validate? skip-on-graphic-error? label]}] + (let [tpoint (dt/tpoint) + err (volatile! false)] + + (binding [*file-stats* (atom {}) + *skip-on-graphic-error* skip-on-graphic-error?] + (try + (l/dbg :hint "migrate:file:start" + :file-id (str file-id) + :validate validate? + :skip-on-graphic-error skip-on-graphic-error?) + + (db/tx-run! (update system ::sto/storage media/configure-assets-storage) + (fn [system] + (binding [*system* system] + (when (string? label) + (fsnap/take-file-snapshot! system {:file-id file-id + :label (str "migration/" label)})) + (let [file (get-file system file-id) + file (process-file! system file :validate? validate?)] + + (events/tap :progress + {:op :migrate-file + :name (:name file) + :id (:id file)}) + + (persist-file! system file))))) + + (catch Throwable cause + (vreset! err true) + (l/wrn :hint "error on processing file" + :file-id (str file-id) + :cause cause) + (throw cause)) + + (finally + (let [elapsed (tpoint) + components (get @*file-stats* :processed-components 0) + graphics (get @*file-stats* :processed-graphics 0)] + + (if (cache/cache? *cache*) + (let [cache-stats (cache/stats *cache*)] + (l/dbg :hint "migrate:file:end" + :file-id (str file-id) + :graphics graphics + :components components + :validate validate? + :crt (mth/to-fixed (:hit-rate cache-stats) 2) + :crq (str (:req-count cache-stats)) + :error @err + :elapsed (dt/format-duration elapsed))) + (l/dbg :hint "migrate:file:end" + :file-id (str file-id) + :graphics graphics + :components components + :validate validate? + :error @err + :elapsed (dt/format-duration elapsed))) + + (some-> *stats* (swap! update :processed-files (fnil inc 0))) + (some-> *team-stats* (swap! update :processed-files (fnil inc 0))))))))) + +(defn migrate-team! + [system team-id & {:keys [validate? skip-on-graphic-error? label]}] + + (l/dbg :hint "migrate:team:start" + :team-id (dm/str team-id)) + + (let [tpoint (dt/tpoint) + err (volatile! false) + + migrate-file + (fn [system file-id] + (migrate-file! system file-id + :label label + :validate? validate? + :skip-on-graphic-error? skip-on-graphic-error?)) + migrate-team + (fn [{:keys [::db/conn] :as system} team-id] + (let [{:keys [id features] :as team} (get-team system team-id)] + (if (contains? features "components/v2") + (l/inf :hint "team already migrated") + (let [features (-> features + (disj "ephimeral/v2-migration") + (conj "components/v2") + (conj "layout/grid") + (conj "styles/v2"))] + + (events/tap :progress + {:op :migrate-team + :name (:name team) + :id id}) + + (run! (partial migrate-file system) + (get-and-lock-team-files conn id)) + + (->> (assoc team :features features) + (update-team! conn))))))] + + (binding [*team-stats* (atom {})] + (try + (db/tx-run! system migrate-team team-id) + + (catch Throwable cause + (vreset! err true) + (l/wrn :hint "error on processing team" + :team-id (str team-id) + :cause cause) + (throw cause)) + + (finally + (let [elapsed (tpoint) + components (get @*team-stats* :processed-components 0) + graphics (get @*team-stats* :processed-graphics 0) + files (get @*team-stats* :processed-files 0)] + + (when-not @err + (some-> *stats* (swap! update :processed-teams (fnil inc 0)))) + + (if (cache/cache? *cache*) + (let [cache-stats (cache/stats *cache*)] + (l/dbg :hint "migrate:team:end" + :team-id (dm/str team-id) + :files files + :components components + :graphics graphics + :crt (mth/to-fixed (:hit-rate cache-stats) 2) + :crq (str (:req-count cache-stats)) + :error @err + :elapsed (dt/format-duration elapsed))) + + (l/dbg :hint "migrate:team:end" + :team-id (dm/str team-id) + :files files + :components components + :graphics graphics + :elapsed (dt/format-duration elapsed))))))))) diff --git a/backend/src/app/features/fdata.clj b/backend/src/app/features/fdata.clj new file mode 100644 index 0000000000..baa63f6936 --- /dev/null +++ b/backend/src/app/features/fdata.clj @@ -0,0 +1,122 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.features.fdata + "A `fdata/*` related feature migration helpers" + (:require + [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.db :as db] + [app.db.sql :as-alias sql] + [app.util.blob :as blob] + [app.util.objects-map :as omap] + [app.util.pointer-map :as pmap])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; OBJECTS-MAP +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn enable-objects-map + [file] + (let [update-page + (fn [page] + (if (and (pmap/pointer-map? page) + (not (pmap/loaded? page))) + page + (update page :objects omap/wrap))) + + update-data + (fn [fdata] + (update fdata :pages-index d/update-vals update-page))] + + (-> file + (update :data update-data) + (update :features conj "fdata/objects-map")))) + +(defn process-objects + "Apply a function to all objects-map on the file. Usualy used for convert + the objects-map instances to plain maps" + [fdata update-fn] + (if (contains? fdata :pages-index) + (update fdata :pages-index d/update-vals + (fn [page] + (update page :objects + (fn [objects] + (if (omap/objects-map? objects) + (update-fn objects) + objects))))) + fdata)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; POINTER-MAP +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn load-pointer + "A database loader pointer helper" + [system file-id id] + (let [{:keys [content]} (db/get system :file-data-fragment + {:id id :file-id file-id} + {::sql/columns [:content] + ::db/check-deleted false})] + + (l/trc :hint "load pointer" + :file-id (str file-id) + :id (str id) + :found (some? content)) + + (when-not content + (ex/raise :type :internal + :code :fragment-not-found + :hint "fragment not found" + :file-id file-id + :fragment-id id)) + + (blob/decode content))) + +(defn persist-pointers! + "Given a database connection and the final file-id, persist all + pointers to the underlying storage (the database)." + [system file-id] + (let [conn (db/get-connection system)] + (doseq [[id item] @pmap/*tracked*] + (when (pmap/modified? item) + (l/trc :hint "persist pointer" :file-id (str file-id) :id (str id)) + (let [content (-> item deref blob/encode)] + (db/insert! conn :file-data-fragment + {:id id + :file-id file-id + :content content})))))) + +(defn process-pointers + "Apply a function to all pointers on the file. Usuly used for + dereference the pointer to a plain value before some processing." + [fdata update-fn] + (let [update-fn' (fn [val] + (if (pmap/pointer-map? val) + (update-fn val) + val))] + (-> fdata + (d/update-vals update-fn') + (update :pages-index d/update-vals update-fn')))) + +(defn get-used-pointer-ids + "Given a file, return all pointer ids used in the data." + [fdata] + (->> (concat (vals fdata) + (vals (:pages-index fdata))) + (into #{} (comp (filter pmap/pointer-map?) + (map pmap/get-id))))) + +(defn enable-pointer-map + "Enable the fdata/pointer-map feature on the file." + [file] + (-> file + (update :data (fn [fdata] + (-> fdata + (update :pages-index d/update-vals pmap/wrap) + (d/update-when :components pmap/wrap)))) + (update :features conj "fdata/pointer-map"))) diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index 59ba338614..a696d54776 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -23,15 +23,15 @@ [app.metrics :as mtx] [app.rpc :as-alias rpc] [app.rpc.doc :as-alias rpc.doc] - [app.worker :as wrk] + [app.setup :as-alias setup] [clojure.spec.alpha :as s] [integrant.core :as ig] [promesa.exec :as px] [reitit.core :as r] [reitit.middleware :as rr] - [yetti.adapter :as yt] - [yetti.request :as yrq] - [yetti.response :as-alias yrs])) + [ring.request :as rreq] + [ring.response :as-alias rres] + [yetti.adapter :as yt])) (declare router-handler) @@ -53,8 +53,8 @@ [_ cfg] (merge {::port 6060 ::host "0.0.0.0" - ::max-body-size (* 1024 1024 30) ; 30 MiB - ::max-multipart-body-size (* 1024 1024 120)} ; 120 MiB + ::max-body-size (* 1024 1024 30) ; default 30 MiB + ::max-multipart-body-size (* 1024 1024 120)} ; default 120 MiB (d/without-nils cfg))) (defmethod ig/pre-init-spec ::server [_] @@ -63,8 +63,7 @@ ::max-multipart-body-size ::router ::handler - ::io-threads - ::wrk/executor])) + ::io-threads])) (defmethod ig/init-key ::server [_ {:keys [::handler ::router ::host ::port] :as cfg}] @@ -75,11 +74,9 @@ :http/max-multipart-body-size (::max-multipart-body-size cfg) :xnio/io-threads (or (::io-threads cfg) (max 3 (px/get-available-processors))) - :xnio/worker-threads (or (::worker-threads cfg) - (max 6 (px/get-available-processors))) - :xnio/dispatch true - :socket/backlog 4069 - :ring/async true} + :xnio/dispatch :virtual + :ring/compat :ring2 + :socket/backlog 4069} handler (cond (some? router) @@ -102,13 +99,13 @@ (yt/stop! server)) (defn- not-found-handler - [_ respond _] - (respond {::yrs/status 404})) + [_] + {::rres/status 404}) (defn- router-handler [router] (letfn [(resolve-handler [request] - (if-let [match (r/match-by-path router (yrq/path request))] + (if-let [match (r/match-by-path router (rreq/path request))] (let [params (:path-params match) result (:result match) handler (or (:handler result) not-found-handler) @@ -120,18 +117,15 @@ (let [{:keys [body] :as response} (errors/handle cause request)] (cond-> response (map? body) - (-> (update ::yrs/headers assoc "content-type" "application/transit+json") - (assoc ::yrs/body (t/encode-str body {:type :json-verbose}))))))] + (-> (update ::rres/headers assoc "content-type" "application/transit+json") + (assoc ::rres/body (t/encode-str body {:type :json-verbose}))))))] - (fn [request respond _] - (let [handler (resolve-handler request) - exchange (yrq/exchange request)] - (handler - (fn [response] - (yt/dispatch! exchange (partial respond response))) - (fn [cause] - (let [response (on-error cause request)] - (yt/dispatch! exchange (partial respond response))))))))) + (fn [request] + (let [handler (resolve-handler request)] + (try + (handler) + (catch Throwable cause + (on-error cause request))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HTTP ROUTER @@ -143,7 +137,7 @@ ::rpc/routes ::rpc.doc/routes ::oidc/routes - ::main/props + ::setup/props ::assets/routes ::debug/routes ::db/pool @@ -160,8 +154,7 @@ [session/soft-auth cfg] [actoken/soft-auth cfg] [mw/errors errors/handle] - [mw/restrict-methods] - [mw/with-dispatch :vthread]]} + [mw/restrict-methods]]} (::mtx/routes cfg) (::assets/routes cfg) diff --git a/backend/src/app/http/access_token.clj b/backend/src/app/http/access_token.clj index 3f39e41211..0d1865f100 100644 --- a/backend/src/app/http/access_token.clj +++ b/backend/src/app/http/access_token.clj @@ -10,14 +10,15 @@ [app.config :as cf] [app.db :as db] [app.main :as-alias main] + [app.setup :as-alias setup] [app.tokens :as tokens] - [yetti.request :as yrq])) + [ring.request :as rreq])) (def header-re #"^Token\s+(.*)") (defn- get-token [request] - (some->> (yrq/get-header request "authorization") + (some->> (rreq/get-header request "authorization") (re-matches header-re) (second))) @@ -30,7 +31,7 @@ "SELECT perms, profile_id, expires_at FROM access_token WHERE id = ? - AND (expires_at IS NULL + AND (expires_at IS NULL OR (expires_at > now()));") (defn- get-token-data @@ -42,7 +43,7 @@ (defn- wrap-soft-auth "Soft Authentication, will be executed synchronously on the undertow worker thread." - [handler {:keys [::main/props]}] + [handler {:keys [::setup/props]}] (letfn [(handle-request [request] (try (let [token (get-token request) @@ -54,9 +55,8 @@ (l/trace :hint "exception on decoding malformed token" :cause cause) request)))] - (fn [request respond raise] - (let [request (handle-request request)] - (handler request respond raise))))) + (fn [request] + (handler (handle-request request))))) (defn- wrap-authz "Authorization middleware, will be executed synchronously on vthread." diff --git a/backend/src/app/http/assets.clj b/backend/src/app/http/assets.clj index efd494249f..06c3318490 100644 --- a/backend/src/app/http/assets.clj +++ b/backend/src/app/http/assets.clj @@ -16,7 +16,7 @@ [app.util.time :as dt] [clojure.spec.alpha :as s] [integrant.core :as ig] - [yetti.response :as-alias yrs])) + [ring.response :as-alias rres])) (def ^:private cache-max-age (dt/duration {:hours 24})) @@ -37,11 +37,11 @@ (defn- serve-object-from-s3 [{:keys [::sto/storage] :as cfg} obj] (let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})] - {::yrs/status 307 - ::yrs/headers {"location" (str url) - "x-host" (cond-> host port (str ":" port)) - "x-mtype" (-> obj meta :content-type) - "cache-control" (str "max-age=" (inst-ms cache-max-age))}})) + {::rres/status 307 + ::rres/headers {"location" (str url) + "x-host" (cond-> host port (str ":" port)) + "x-mtype" (-> obj meta :content-type) + "cache-control" (str "max-age=" (inst-ms cache-max-age))}})) (defn- serve-object-from-fs [{:keys [::path]} obj] @@ -51,8 +51,8 @@ headers {"x-accel-redirect" (:path purl) "content-type" (:content-type mdata) "cache-control" (str "max-age=" (inst-ms cache-max-age))}] - {::yrs/status 204 - ::yrs/headers headers})) + {::rres/status 204 + ::rres/headers headers})) (defn- serve-object "Helper function that returns the appropriate response depending on @@ -70,7 +70,7 @@ obj (sto/get-object storage id)] (if obj (serve-object cfg obj) - {::yrs/status 404}))) + {::rres/status 404}))) (defn- generic-handler "A generic handler helper/common code for file-media based handlers." @@ -81,7 +81,7 @@ sobj (sto/get-object storage (kf mobj))] (if sobj (serve-object cfg sobj) - {::yrs/status 404}))) + {::rres/status 404}))) (defn file-objects-handler "Handler that serves storage objects by file media id." diff --git a/backend/src/app/http/awsns.clj b/backend/src/app/http/awsns.clj index 681e7045f7..88060bb20a 100644 --- a/backend/src/app/http/awsns.clj +++ b/backend/src/app/http/awsns.clj @@ -13,6 +13,7 @@ [app.db.sql :as sql] [app.http.client :as http] [app.main :as-alias main] + [app.setup :as-alias setup] [app.tokens :as tokens] [app.worker :as-alias wrk] [clojure.spec.alpha :as s] @@ -20,8 +21,8 @@ [integrant.core :as ig] [jsonista.core :as j] [promesa.exec :as px] - [yetti.request :as yrq] - [yetti.response :as-alias yrs])) + [ring.request :as rreq] + [ring.response :as-alias rres])) (declare parse-json) (declare handle-request) @@ -30,16 +31,15 @@ (defmethod ig/pre-init-spec ::routes [_] (s/keys :req [::http/client - ::main/props - ::db/pool - ::wrk/executor])) + ::setup/props + ::db/pool])) (defmethod ig/init-key ::routes - [_ {:keys [::wrk/executor] :as cfg}] + [_ cfg] (letfn [(handler [request] - (let [data (-> request yrq/body slurp)] - (px/run! executor #(handle-request cfg data))) - {::yrs/status 200})] + (let [data (-> request rreq/body slurp)] + (px/run! :vthread (partial handle-request cfg data))) + {::rres/status 200})] ["/sns" {:handler handler :allowed-methods #{:post}}])) @@ -107,7 +107,7 @@ [cfg headers] (let [tdata (get headers "x-penpot-data")] (when-not (str/empty? tdata) - (let [result (tokens/verify (::main/props cfg) {:token tdata :iss :profile-identity})] + (let [result (tokens/verify (::setup/props cfg) {:token tdata :iss :profile-identity})] (:profile-id result))))) (defn- parse-notification diff --git a/backend/src/app/http/client.clj b/backend/src/app/http/client.clj index cf30dbb46d..9ef4cc4b2a 100644 --- a/backend/src/app/http/client.clj +++ b/backend/src/app/http/client.clj @@ -8,7 +8,6 @@ "Http client abstraction layer." (:require [app.common.spec :as us] - [app.worker :as wrk] [clojure.spec.alpha :as s] [integrant.core :as ig] [java-http-clj.core :as http] @@ -21,12 +20,11 @@ (s/keys :req [::client])) (defmethod ig/pre-init-spec ::client [_] - (s/keys :req [::wrk/executor])) + (s/keys :req [])) (defmethod ig/init-key ::client - [_ {:keys [::wrk/executor] :as cfg}] - (http/build-client {:executor executor - :connect-timeout 30000 ;; 10s + [_ _] + (http/build-client {:connect-timeout 30000 ;; 10s :follow-redirects :always})) (defn send! @@ -57,8 +55,8 @@ convention." ([cfg-or-client request] (let [client (resolve-client cfg-or-client)] - (send! client request {}))) + (send! client request {:sync? true}))) ([cfg-or-client request options] (let [client (resolve-client cfg-or-client)] - (send! client request options)))) + (send! client request (merge {:sync? true} options))))) diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 227852c4c5..a453c68720 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -7,6 +7,8 @@ (ns app.http.debug (:refer-clojure :exclude [error-handler]) (:require + [app.binfile.v1 :as bf.v1] + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.pprint :as pp] @@ -14,10 +16,14 @@ [app.config :as cf] [app.db :as db] [app.http.session :as session] - [app.rpc.commands.binfile :as binf] + [app.main :as-alias main] + [app.rpc.commands.auth :as auth] [app.rpc.commands.files-create :refer [create-file]] [app.rpc.commands.profile :as profile] + [app.setup :as-alias setup] + [app.srepl.helpers :as srepl] [app.storage :as-alias sto] + [app.storage.tmp :as tmp] [app.util.blob :as blob] [app.util.template :as tmpl] [app.util.time :as dt] @@ -28,55 +34,41 @@ [integrant.core :as ig] [markdown.core :as md] [markdown.transformers :as mdt] - [yetti.request :as yrq] - [yetti.response :as yrs])) + [ring.request :as rreq] + [ring.response :as rres])) ;; (selmer.parser/cache-off!) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; HELPERS -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn authorized? - [pool {:keys [::session/profile-id]}] - (or (= "devenv" (cf/get :host)) - (let [profile (ex/ignoring (profile/get-profile pool profile-id)) - admins (or (cf/get :admins) #{})] - (contains? admins (:email profile))))) - -(defn prepare-response - [body] - (let [headers {"content-type" "application/transit+json"}] - {::yrs/status 200 - ::yrs/body body - ::yrs/headers headers})) - -(defn prepare-download-response - [body filename] - (let [headers {"content-disposition" (str "attachment; filename=" filename) - "content-type" "application/octet-stream"}] - {::yrs/status 200 - ::yrs/body body - ::yrs/headers headers})) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; INDEX ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn index-handler - [{:keys [::db/pool]} request] - (when-not (authorized? pool request) - (ex/raise :type :authentication - :code :only-admins-allowed)) - {::yrs/status 200 - ::yrs/headers {"content-type" "text/html"} - ::yrs/body (-> (io/resource "app/templates/debug.tmpl") - (tmpl/render {}))}) + [_cfg _request] + {::rres/status 200 + ::rres/headers {"content-type" "text/html"} + ::rres/body (-> (io/resource "app/templates/debug.tmpl") + (tmpl/render {}))}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; FILE CHANGES ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn prepare-response + [body] + (let [headers {"content-type" "application/transit+json"}] + {::rres/status 200 + ::rres/body body + ::rres/headers headers})) + +(defn prepare-download-response + [body filename] + (let [headers {"content-disposition" (str "attachment; filename=" filename) + "content-type" "application/octet-stream"}] + {::rres/status 200 + ::rres/body body + ::rres/headers headers})) + (def sql:retrieve-range-of-changes "select revn, changes from file_change where file_id=? and revn >= ? and revn <= ? order by revn") @@ -85,10 +77,6 @@ (defn- retrieve-file-data [{:keys [::db/pool]} {:keys [params ::session/profile-id] :as request}] - (when-not (authorized? pool request) - (ex/raise :type :authentication - :code :only-admins-allowed)) - (let [file-id (some-> params :file-id parse-uuid) revn (some-> params :revn parse-long) filename (str file-id)] @@ -111,15 +99,18 @@ (contains? params :clone) (let [profile (profile/get-profile pool profile-id) - project-id (:default-project-id profile) - data (blob/decode data)] - (create-file pool {:id (uuid/next) - :name (str "Cloned file: " filename) - :project-id project-id - :profile-id profile-id - :data data}) - {::yrs/status 201 - ::yrs/body "OK CREATED"}) + project-id (:default-project-id profile)] + + (db/run! pool (fn [{:keys [::db/conn] :as cfg}] + (create-file cfg {:id file-id + :name (str "Cloned file: " filename) + :project-id project-id + :profile-id profile-id}) + (db/update! conn :file + {:data data} + {:id file-id}) + {::rres/status 201 + ::rres/body "OK CREATED"}))) :else (prepare-response (blob/decode data)))))) @@ -133,38 +124,41 @@ [{:keys [::db/pool]} {:keys [::session/profile-id params] :as request}] (let [profile (profile/get-profile pool profile-id) project-id (:default-project-id profile) - data (some-> params :file :path io/read-as-bytes blob/decode)] + data (some-> params :file :path io/read-as-bytes)] (if (and data project-id) - (let [fname (str "Imported file *: " (dt/now)) - overwrite? (contains? params :overwrite?) - file-id (or (and overwrite? (ex/ignoring (-> params :file :filename parse-uuid))) - (uuid/next))] + (let [fname (str "Imported file *: " (dt/now)) + reuse-id? (contains? params :reuseid) + file-id (or (and reuse-id? (ex/ignoring (-> params :file :filename parse-uuid))) + (uuid/next))] - (if (and overwrite? file-id + (if (and reuse-id? file-id (is-file-exists? pool file-id)) (do (db/update! pool :file - {:data (blob/encode data)} + {:data data + :deleted-at nil} {:id file-id}) - {::yrs/status 200 - ::yrs/body "OK UPDATED"}) + {::rres/status 200 + ::rres/body "OK UPDATED"}) - (do - (create-file pool {:id file-id - :name fname - :project-id project-id - :profile-id profile-id - :data data}) - {::yrs/status 201 - ::yrs/body "OK CREATED"}))) + (db/run! pool (fn [{:keys [::db/conn] :as cfg}] + (create-file cfg {:id file-id + :name fname + :project-id project-id + :profile-id profile-id}) + (db/update! conn :file + {:data data} + {:id file-id}) + {::rres/status 201 + ::rres/body "OK CREATED"})))) - {::yrs/status 500 - ::yrs/body "ERROR"}))) + {::rres/status 500 + ::rres/body "ERROR"}))) (defn file-data-handler [cfg request] - (case (yrq/method request) + (case (rreq/method request) :get (retrieve-file-data cfg request) :post (upload-file-data cfg request) (ex/raise :type :http @@ -172,10 +166,6 @@ (defn file-changes-handler [{:keys [::db/pool]} {:keys [params] :as request}] - (when-not (authorized? pool request) - (ex/raise :type :authentication - :code :only-admins-allowed)) - (letfn [(retrieve-changes [file-id revn] (if (str/includes? revn ":") (let [[start end] (->> (str/split revn #":") @@ -242,24 +232,19 @@ (-> (io/resource "app/templates/error-report.v3.tmpl") (tmpl/render (-> content (assoc :id id) - (assoc :created-at (dt/format-instant created-at :rfc1123)))))) - ] - - (when-not (authorized? pool request) - (ex/raise :type :authentication - :code :only-admins-allowed)) + (assoc :created-at (dt/format-instant created-at :rfc1123))))))] (if-let [report (get-report request)] (let [result (case (:version report) 1 (render-template-v1 report) 2 (render-template-v2 report) 3 (render-template-v3 report))] - {::yrs/status 200 - ::yrs/body result - ::yrs/headers {"content-type" "text/html; charset=utf-8" - "x-robots-tag" "noindex"}}) - {::yrs/status 404 - ::yrs/body "not found"}))) + {::rres/status 200 + ::rres/body result + ::rres/headers {"content-type" "text/html; charset=utf-8" + "x-robots-tag" "noindex"}}) + {::rres/status 404 + ::rres/body "not found"}))) (def sql:error-reports "SELECT id, created_at, @@ -269,17 +254,14 @@ LIMIT 200") (defn error-list-handler - [{:keys [::db/pool]} request] - (when-not (authorized? pool request) - (ex/raise :type :authentication - :code :only-admins-allowed)) + [{:keys [::db/pool]} _request] (let [items (->> (db/exec! pool [sql:error-reports]) (map #(update % :created-at dt/format-instant :rfc1123)))] - {::yrs/status 200 - ::yrs/body (-> (io/resource "app/templates/error-list.tmpl") - (tmpl/render {:items items})) - ::yrs/headers {"content-type" "text/html; charset=utf-8" - "x-robots-tag" "noindex"}})) + {::rres/status 200 + ::rres/body (-> (io/resource "app/templates/error-list.tmpl") + (tmpl/render {:items items})) + ::rres/headers {"content-type" "text/html; charset=utf-8" + "x-robots-tag" "noindex"}})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; EXPORT/IMPORT @@ -288,9 +270,10 @@ (defn export-handler [{:keys [::db/pool] :as cfg} {:keys [params ::session/profile-id] :as request}] - (let [file-ids (->> (:file-ids params) - (remove empty?) - (mapv parse-uuid)) + (let [file-ids (into #{} + (comp (remove empty?) + (map parse-uuid)) + (:file-ids params)) libs? (contains? params :includelibs) clone? (contains? params :clone) embed? (contains? params :embedassets)] @@ -299,31 +282,30 @@ (ex/raise :type :validation :code :missing-arguments)) - (let [path (-> cfg - (assoc ::binf/file-ids file-ids) - (assoc ::binf/embed-assets? embed?) - (assoc ::binf/include-libraries? libs?) - (binf/export-to-tmpfile!))] + (let [path (tmp/tempfile :prefix "penpot.export.")] + (with-open [output (io/output-stream path)] + (-> cfg + (assoc ::bf.v1/ids file-ids) + (assoc ::bf.v1/embed-assets embed?) + (assoc ::bf.v1/include-libraries libs?) + (bf.v1/export-files! output))) + (if clone? (let [profile (profile/get-profile pool profile-id) - project-id (:default-project-id profile)] - (binf/import! - (assoc cfg - ::binf/input path - ::binf/overwrite? false - ::binf/ignore-index-errors? true - ::binf/profile-id profile-id - ::binf/project-id project-id)) - - {::yrs/status 200 - ::yrs/headers {"content-type" "text/plain"} - ::yrs/body "OK CLONED"}) - - {::yrs/status 200 - ::yrs/body (io/input-stream path) - ::yrs/headers {"content-type" "application/octet-stream" - "content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}})))) + project-id (:default-project-id profile) + cfg (assoc cfg + ::bf.v1/overwrite false + ::bf.v1/profile-id profile-id + ::bf.v1/project-id project-id)] + (bf.v1/import-files! cfg path) + {::rres/status 200 + ::rres/headers {"content-type" "text/plain"} + ::rres/body "OK CLONED"}) + {::rres/status 200 + ::rres/body (io/input-stream path) + ::rres/headers {"content-type" "application/octet-stream" + "content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}})))) (defn import-handler @@ -336,26 +318,108 @@ (let [profile (profile/get-profile pool profile-id) project-id (:default-project-id profile) overwrite? (contains? params :overwrite) - migrate? (contains? params :migrate) - ignore-index-errors? (contains? params :ignore-index-errors)] + migrate? (contains? params :migrate)] (when-not project-id (ex/raise :type :validation :code :missing-project :hint "project not found")) - (binf/import! - (assoc cfg - ::binf/input (-> params :file :path) - ::binf/overwrite? overwrite? - ::binf/migrate? migrate? - ::binf/ignore-index-errors? ignore-index-errors? - ::binf/profile-id profile-id - ::binf/project-id project-id)) + (let [path (-> params :file :path) + cfg (assoc cfg + ::bf.v1/overwrite overwrite? + ::bf.v1/migrate migrate? + ::bf.v1/profile-id profile-id + ::bf.v1/project-id project-id)] + (bf.v1/import-files! cfg path) + {::rres/status 200 + ::rres/headers {"content-type" "text/plain"} + ::rres/body "OK"}))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; ACTIONS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- resend-email-notification + [{:keys [::db/pool ::setup/props] :as cfg} {:keys [params] :as request}] + + (when-not (contains? params :force) + (ex/raise :type :validation + :code :missing-force + :hint "missing force checkbox")) + + (let [profile (some->> params + :email + (profile/clean-email) + (profile/get-profile-by-email pool))] + + (when-not profile + (ex/raise :type :validation + :code :missing-profile + :hint "unable to find profile by email")) + + (cond + (contains? params :block) + (do + (db/update! pool :profile {:is-blocked true} {:id (:id profile)}) + (db/delete! pool :http-session {:profile-id (:id profile)}) + + {::rres/status 200 + ::rres/headers {"content-type" "text/plain"} + ::rres/body (str/ffmt "PROFILE '%' BLOCKED" (:email profile))}) + + (contains? params :unblock) + (do + (db/update! pool :profile {:is-blocked false} {:id (:id profile)}) + {::rres/status 200 + ::rres/headers {"content-type" "text/plain"} + ::rres/body (str/ffmt "PROFILE '%' UNBLOCKED" (:email profile))}) + + (contains? params :resend) + (if (:is-blocked profile) + {::rres/status 200 + ::rres/headers {"content-type" "text/plain"} + ::rres/body "PROFILE ALREADY BLOCKED"} + (do + (auth/send-email-verification! pool props profile) + {::rres/status 200 + ::rres/headers {"content-type" "text/plain"} + ::rres/body (str/ffmt "RESENDED FOR '%'" (:email profile))})) + + :else + (do + (db/update! pool :profile {:is-active true} {:id (:id profile)}) + {::rres/status 200 + ::rres/headers {"content-type" "text/plain"} + ::rres/body (str/ffmt "PROFILE '%' ACTIVATED" (:email profile))})))) + + +(defn- reset-file-version + [cfg {:keys [params] :as request}] + (let [file-id (some-> params :file-id d/parse-uuid) + version (some-> params :version d/parse-integer)] + + (when-not (contains? params :force) + (ex/raise :type :validation + :code :missing-force + :hint "missing force checkbox")) + + (when (nil? file-id) + (ex/raise :type :validation + :code :invalid-file-id + :hint "provided invalid file id")) + + (when (nil? version) + (ex/raise :type :validation + :code :invalid-version + :hint "provided invalid version")) + + (db/tx-run! cfg srepl/process-file! file-id #(assoc % :version version)) + + {::rres/status 200 + ::rres/headers {"content-type" "text/plain"} + ::rres/body "OK"})) - {::yrs/status 200 - ::yrs/headers {"content-type" "text/plain"} - ::yrs/body "OK"})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; OTHER SMALL VIEWS/HANDLERS @@ -366,13 +430,13 @@ [{:keys [::db/pool]} _] (try (db/exec-one! pool ["select count(*) as count from server_prop;"]) - {::yrs/status 200 - ::yrs/body "OK"} + {::rres/status 200 + ::rres/body "OK"} (catch Throwable cause (l/warn :hint "unable to execute query on health handler" :cause cause) - {::yrs/status 503 - ::yrs/body "KO"}))) + {::rres/status 503 + ::rres/body "KO"}))) (defn changelog-handler [_ _] @@ -381,16 +445,23 @@ (md->html [text] (md/md-to-html-string text :replacement-transformers (into [transform-emoji] mdt/transformer-vector)))] (if-let [clog (io/resource "changelog.md")] - {::yrs/status 200 - ::yrs/headers {"content-type" "text/html; charset=utf-8"} - ::yrs/body (-> clog slurp md->html)} - {::yrs/status 404 - ::yrs/body "NOT FOUND"}))) + {::rres/status 200 + ::rres/headers {"content-type" "text/html; charset=utf-8"} + ::rres/body (-> clog slurp md->html)} + {::rres/status 404 + ::rres/body "NOT FOUND"}))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; INIT ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn authorized? + [pool {:keys [::session/profile-id]}] + (or (= "devenv" (cf/get :host)) + (let [profile (ex/ignoring (profile/get-profile pool profile-id)) + admins (or (cf/get :admins) #{})] + (contains? admins (:email profile))))) + (def with-authorization {:compile (fn [& _] @@ -414,6 +485,10 @@ ["/changelog" {:handler (partial changelog-handler cfg)}] ["/error/:id" {:handler (partial error-handler cfg)}] ["/error" {:handler (partial error-list-handler cfg)}] + ["/actions/resend-email-verification" + {:handler (partial resend-email-notification cfg)}] + ["/actions/reset-file-version" + {:handler (partial reset-file-version cfg)}] ["/file/export" {:handler (partial export-handler cfg)}] ["/file/import" {:handler (partial import-handler cfg)}] ["/file/data" {:handler (partial file-data-handler cfg)}] diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index 4b22cb493f..18350d21d8 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -9,21 +9,21 @@ (:require [app.common.exceptions :as ex] [app.common.logging :as l] - [app.common.schema :as sm] + [app.common.schema :as-alias sm] [app.config :as cf] [app.http :as-alias http] [app.http.access-token :as-alias actoken] [app.http.session :as-alias session] [clojure.spec.alpha :as s] [cuerdas.core :as str] - [yetti.request :as yrq] - [yetti.response :as yrs])) + [ring.request :as rreq] + [ring.response :as rres])) (defn- parse-client-ip [request] - (or (some-> (yrq/get-header request "x-forwarded-for") (str/split ",") first) - (yrq/get-header request "x-real-ip") - (yrq/remote-addr request))) + (or (some-> (rreq/get-header request "x-forwarded-for") (str/split ",") first) + (rreq/get-header request "x-real-ip") + (rreq/remote-addr request))) (defn request->context "Extracts error report relevant context data from request." @@ -34,184 +34,210 @@ {:request/path (:path request) :request/method (:method request) :request/params (:params request) - :request/user-agent (yrq/get-header request "user-agent") + :request/user-agent (rreq/get-header request "user-agent") :request/ip-addr (parse-client-ip request) :request/profile-id (:uid claims) - :version/frontend (or (yrq/get-header request "x-frontend-version") "unknown") + :version/frontend (or (rreq/get-header request "x-frontend-version") "unknown") :version/backend (:full cf/version)})) +(defmulti handle-error + (fn [cause _ _] + (-> cause ex-data :type))) + (defmulti handle-exception - (fn [err & _rest] - (let [edata (ex-data err)] - (or (:type edata) - (class err))))) + (fn [cause _ _] + (class cause))) -(defmethod handle-exception :authentication - [err _] - {::yrs/status 401 - ::yrs/body (ex-data err)}) +(defmethod handle-error :authentication + [err _ _] + {::rres/status 401 + ::rres/body (ex-data err)}) -(defmethod handle-exception :authorization - [err _] - {::yrs/status 403 - ::yrs/body (ex-data err)}) +(defmethod handle-error :authorization + [err _ _] + {::rres/status 403 + ::rres/body (ex-data err)}) -(defmethod handle-exception :restriction - [err _] - {::yrs/status 400 - ::yrs/body (ex-data err)}) +(defmethod handle-error :restriction + [err _ _] + (let [{:keys [code] :as data} (ex-data err)] + (if (= code :method-not-allowed) + {::rres/status 405 + ::rres/body data} + {::rres/status 400 + ::rres/body data}))) -(defmethod handle-exception :rate-limit - [err _] +(defmethod handle-error :rate-limit + [err _ _] (let [headers (-> err ex-data ::http/headers)] - {::yrs/status 429 - ::yrs/headers headers})) + {::rres/status 429 + ::rres/headers headers})) -(defmethod handle-exception :concurrency-limit - [err _] +(defmethod handle-error :concurrency-limit + [err _ _] (let [headers (-> err ex-data ::http/headers)] - {::yrs/status 429 - ::yrs/headers headers})) + {::rres/status 429 + ::rres/headers headers})) -(defmethod handle-exception :validation - [err request] +(defmethod handle-error :validation + [err request parent-cause] (let [{:keys [code] :as data} (ex-data err)] (cond - (= code :spec-validation) + (or (= code :spec-validation) + (= code :params-validation) + (= code :schema-validation) + (= code :data-validation)) (let [explain (ex/explain data)] - {::yrs/status 400 - ::yrs/body (-> data - (dissoc ::s/problems ::s/value ::s/spec) - (cond-> explain (assoc :explain explain)))}) - - (= code :params-validation) - (let [explain (::sm/explain data) - payload (sm/humanize-data explain)] - {::yrs/status 400 - ::yrs/body (-> data - (dissoc ::sm/explain) - (assoc :data payload))}) + {::rres/status 400 + ::rres/body (-> data + (dissoc ::s/problems ::s/value ::s/spec ::sm/explain) + (cond-> explain (assoc :explain explain)))}) (= code :request-body-too-large) - {::yrs/status 413 ::yrs/body data} + {::rres/status 413 ::rres/body data} (= code :invalid-image) (binding [l/*context* (request->context request)] - (l/error :hint "unexpected error on processing image" :cause err) - {::yrs/status 400 ::yrs/body data}) + (let [cause (or parent-cause err)] + (l/error :hint "unexpected error on processing image" :cause cause) + {::rres/status 400 ::rres/body data})) :else - {::yrs/status 400 ::yrs/body data}))) + {::rres/status 400 ::rres/body data}))) -(defmethod handle-exception :assertion - [error request] +(defmethod handle-error :assertion + [error request parent-cause] (binding [l/*context* (request->context request)] - (let [{:keys [code] :as data} (ex-data error)] + (let [{:keys [code] :as data} (ex-data error) + cause (or parent-cause error)] (cond (= code :data-validation) - (let [explain (::sm/explain data) - payload (sm/humanize-data explain)] - (l/error :hint "Data assertion error" :message (ex-message error) :cause error) - {::yrs/status 500 - ::yrs/body {:type :server-error - :code :assertion - :data (-> data - (dissoc ::sm/explain) - (assoc :data payload))}}) + (let [explain (ex/explain data)] + (l/error :hint "data assertion error" :cause cause) + {::rres/status 500 + ::rres/body {:type :server-error + :code :assertion + :data (-> data + (dissoc ::sm/explain) + (cond-> explain (assoc :explain explain)))}}) (= code :spec-validation) (let [explain (ex/explain data)] - (l/error :hint "Spec assertion error" :message (ex-message error) :cause error) - {::yrs/status 500 - ::yrs/body {:type :server-error - :code :assertion - :data (-> data - (dissoc ::s/problems ::s/value ::s/spec) - (cond-> explain (assoc :explain explain)))}}) + (l/error :hint "spec assertion error" :cause cause) + {::rres/status 500 + ::rres/body {:type :server-error + :code :assertion + :data (-> data + (dissoc ::s/problems ::s/value ::s/spec) + (cond-> explain (assoc :explain explain)))}}) :else (do - (l/error :hint "Assertion error" :message (ex-message error) :cause error) - {::yrs/status 500 - ::yrs/body {:type :server-error - :code :assertion - :data data}}))))) + (l/error :hint "assertion error" :cause cause) + {::rres/status 500 + ::rres/body {:type :server-error + :code :assertion + :data data}}))))) +(defmethod handle-error :not-found + [err _ _] + {::rres/status 404 + ::rres/body (ex-data err)}) -(defmethod handle-exception :not-found - [err _] - {::yrs/status 404 - ::yrs/body (ex-data err)}) - -(defmethod handle-exception :internal - [error request] +(defmethod handle-error :internal + [error request parent-cause] (binding [l/*context* (request->context request)] - (l/error :hint "Internal error" :message (ex-message error) :cause error) - {::yrs/status 500 - ::yrs/body {:type :server-error - :code :unhandled - :hint (ex-message error) - :data (ex-data error)}})) + (let [cause (or parent-cause error)] + (l/error :hint "internal error" :cause cause) + {::rres/status 500 + ::rres/body {:type :server-error + :code :unhandled + :hint (ex-message error) + :data (ex-data error)}}))) + +(defmethod handle-error :default + [error request parent-cause] + (let [edata (ex-data error)] + ;; This is a special case for the idle-in-transaction error; + ;; when it happens, the connection is automatically closed and + ;; next-jdbc combines the two errors in a single ex-info. We + ;; only need the :handling error, because the :rollback error + ;; will be always "connection closed". + (if (and (ex/exception? (:rollback edata)) + (ex/exception? (:handling edata))) + (handle-exception (:handling edata) request error) + (handle-exception error request parent-cause)))) (defmethod handle-exception org.postgresql.util.PSQLException - [error request] - (let [state (.getSQLState ^java.sql.SQLException error)] + [error request parent-cause] + (let [state (.getSQLState ^java.sql.SQLException error) + cause (or parent-cause error)] (binding [l/*context* (request->context request)] - (l/error :hint "PSQL error" :message (ex-message error) :cause error) + (l/error :hint "PSQL error" + :cause cause) (cond (= state "57014") - {::yrs/status 504 - ::yrs/body {:type :server-error - :code :statement-timeout - :hint (ex-message error)}} + {::rres/status 504 + ::rres/body {:type :server-error + :code :statement-timeout + :hint (ex-message error)}} (= state "25P03") - {::yrs/status 504 - ::yrs/body {:type :server-error - :code :idle-in-transaction-timeout - :hint (ex-message error)}} + {::rres/status 504 + ::rres/body {:type :server-error + :code :idle-in-transaction-timeout + :hint (ex-message error)}} :else - {::yrs/status 500 - ::yrs/body {:type :server-error - :code :unexpected - :hint (ex-message error) - :state state}})))) + {::rres/status 500 + ::rres/body {:type :server-error + :code :unexpected + :hint (ex-message error) + :state state}})))) (defmethod handle-exception :default - [error request] - (let [edata (ex-data error)] + [error request parent-cause] + (let [edata (ex-data error) + cause (or parent-cause error)] (cond ;; This means that exception is not a controlled exception. (nil? edata) (binding [l/*context* (request->context request)] - (l/error :hint "Unexpected error" :message (ex-message error) :cause error) - {::yrs/status 500 - ::yrs/body {:type :server-error - :code :unexpected - :hint (ex-message error)}}) - - ;; This is a special case for the idle-in-transaction error; - ;; when it happens, the connection is automatically closed and - ;; next-jdbc combines the two errors in a single ex-info. We - ;; only need the :handling error, because the :rollback error - ;; will be always "connection closed". - (and (ex/exception? (:rollback edata)) - (ex/exception? (:handling edata))) - (handle-exception (:handling edata) request) + (l/error :hint "unexpected error" :cause cause) + {::rres/status 500 + ::rres/body {:type :server-error + :code :unexpected + :hint (ex-message error)}}) :else (binding [l/*context* (request->context request)] - (l/error :hint "Unhandled error" :message (ex-message error) :cause error) - {::yrs/status 500 - ::yrs/body {:type :server-error - :code :unhandled - :hint (ex-message error) - :data edata}})))) + (l/error :hint "unhandled error" :cause cause) + {::rres/status 500 + ::rres/body {:type :server-error + :code :unhandled + :hint (ex-message error) + :data edata}})))) + +(defmethod handle-exception java.util.concurrent.CompletionException + [cause request _] + (let [cause' (ex-cause cause)] + (if (ex/error? cause') + (handle-error cause' request cause) + (handle-exception cause' request cause)))) + +(defmethod handle-exception java.util.concurrent.ExecutionException + [cause request _] + (let [cause' (ex-cause cause)] + (if (ex/error? cause') + (handle-error cause' request cause) + (handle-exception cause' request cause)))) (defn handle [cause request] - (if (or (instance? java.util.concurrent.CompletionException cause) - (instance? java.util.concurrent.ExecutionException cause)) - (handle-exception (ex-cause cause) request) - (handle-exception cause request))) + (if (ex/error? cause) + (handle-error cause request nil) + (handle-exception cause request nil))) + +(defn handle' + [cause request] + (::rres/body (handle cause request))) diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index f71f9da952..4ea815f07f 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -12,13 +12,10 @@ [app.config :as cf] [app.util.json :as json] [cuerdas.core :as str] - [promesa.core :as p] - [promesa.exec :as px] - [promesa.util :as pu] + [ring.request :as rreq] + [ring.response :as rres] [yetti.adapter :as yt] - [yetti.middleware :as ymw] - [yetti.request :as yrq] - [yetti.response :as yrs]) + [yetti.middleware :as ymw]) (:import com.fasterxml.jackson.core.JsonParseException com.fasterxml.jackson.core.io.JsonEOFException @@ -46,17 +43,17 @@ (defn wrap-parse-request [handler] (letfn [(process-request [request] - (let [header (yrq/get-header request "content-type")] + (let [header (rreq/get-header request "content-type")] (cond (str/starts-with? header "application/transit+json") - (with-open [^InputStream is (yrq/body request)] + (with-open [^InputStream is (rreq/body request)] (let [params (t/read! (t/reader is))] (-> request (assoc :body-params params) (update :params merge params)))) (str/starts-with? header "application/json") - (with-open [^InputStream is (yrq/body request)] + (with-open [^InputStream is (rreq/body request)] (let [params (json/decode is json-mapper)] (-> request (assoc :body-params params) @@ -65,37 +62,36 @@ :else request))) - (handle-error [raise cause] + (handle-error [cause] (cond (instance? RuntimeException cause) (if-let [cause (ex-cause cause)] - (handle-error raise cause) - (raise cause)) + (handle-error cause) + (throw cause)) (instance? RequestTooBigException cause) - (raise (ex/error :type :validation - :code :request-body-too-large - :hint (ex-message cause))) - + (ex/raise :type :validation + :code :request-body-too-large + :hint (ex-message cause)) (or (instance? JsonEOFException cause) (instance? JsonParseException cause) (instance? MismatchedInputException cause)) - (raise (ex/error :type :validation - :code :malformed-json - :hint (ex-message cause) - :cause cause)) + (ex/raise :type :validation + :code :malformed-json + :hint (ex-message cause) + :cause cause) :else - (raise cause)))] + (throw cause)))] - (fn [request respond raise] - (if (= (yrq/method request) :post) + (fn [request] + (if (= (rreq/method request) :post) (let [request (ex/try! (process-request request))] (if (ex/exception? request) - (handle-error raise request) - (handler request respond raise))) - (handler request respond raise))))) + (handle-error request) + (handler request))) + (handler request))))) (def parse-request {:name ::parse-request @@ -113,7 +109,7 @@ (defn wrap-format-response [handler] (letfn [(transit-streamable-body [data opts] - (reify yrs/StreamableResponseBody + (reify rres/StreamableResponseBody (-write-body-to-stream [_ _ output-stream] (try (with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)] @@ -128,7 +124,7 @@ (.close ^OutputStream output-stream)))))) (json-streamable-body [data] - (reify yrs/StreamableResponseBody + (reify rres/StreamableResponseBody (-write-body-to-stream [_ _ output-stream] (try (with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)] @@ -143,24 +139,24 @@ (.close ^OutputStream output-stream)))))) (format-response-with-json [response _] - (let [body (::yrs/body response)] + (let [body (::rres/body response)] (if (or (boolean? body) (coll? body)) (-> response - (update ::yrs/headers assoc "content-type" "application/json") - (assoc ::yrs/body (json-streamable-body body))) + (update ::rres/headers assoc "content-type" "application/json") + (assoc ::rres/body (json-streamable-body body))) response))) (format-response-with-transit [response request] - (let [body (::yrs/body response)] + (let [body (::rres/body response)] (if (or (boolean? body) (coll? body)) - (let [qs (yrq/query request) + (let [qs (rreq/query request) opts (if (or (contains? cf/flags :transit-readable-response) (str/includes? qs "transit_verbose")) {:type :json-verbose} {:type :json})] (-> response - (update ::yrs/headers assoc "content-type" "application/transit+json") - (assoc ::yrs/body (transit-streamable-body body opts)))) + (update ::rres/headers assoc "content-type" "application/transit+json") + (assoc ::rres/body (transit-streamable-body body opts)))) response))) (format-from-params [{:keys [query-params] :as request}] @@ -169,7 +165,7 @@ (format-response [response request] (let [accept (or (format-from-params request) - (yrq/get-header request "accept"))] + (rreq/get-header request "accept"))] (cond (or (= accept "application/transit+json") (str/includes? accept "application/transit+json")) @@ -186,11 +182,9 @@ (cond-> response (map? response) (format-response request)))] - (fn [request respond raise] - (handler request - (fn [response] - (respond (process-response response request))) - raise)))) + (fn [request] + (let [response (handler request)] + (process-response response request))))) (def format-response {:name ::format-response @@ -198,12 +192,11 @@ (defn wrap-errors [handler on-error] - (fn [request respond raise] - (handler request respond (fn [cause] - (try - (respond (on-error cause request)) - (catch Throwable cause - (raise cause))))))) + (fn [request] + (try + (handler request) + (catch Throwable cause + (on-error cause request))))) (def errors {:name ::errors @@ -221,11 +214,11 @@ (defn wrap-cors [handler] (fn [request] - (let [response (if (= (yrq/method request) :options) - {::yrs/status 200} + (let [response (if (= (rreq/method request) :options) + {::rres/status 200} (handler request)) - origin (yrq/get-header request "origin")] - (update response ::yrs/headers with-cors-headers origin)))) + origin (rreq/get-header request "origin")] + (update response ::rres/headers with-cors-headers origin)))) (def cors {:name ::cors @@ -239,18 +232,8 @@ (fn [data _] (when-let [allowed (:allowed-methods data)] (fn [handler] - (fn [request respond raise] - (let [method (yrq/method request)] + (fn [request] + (let [method (rreq/method request)] (if (contains? allowed method) - (handler request respond raise) - (respond {::yrs/status 405})))))))}) - -(def with-dispatch - {:name ::with-dispatch - :compile - (fn [& _] - (fn [handler executor] - (let [executor (px/resolve-executor executor)] - (fn [request respond raise] - (->> (px/submit! executor (partial handler request)) - (p/fnly (pu/handler respond raise)))))))}) + (handler request) + {::rres/status 405}))))))}) diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index ea8002688c..c4a3f0ba68 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -15,11 +15,13 @@ [app.db.sql :as sql] [app.http.session.tasks :as-alias tasks] [app.main :as-alias main] + [app.setup :as-alias setup] [app.tokens :as tokens] [app.util.time :as dt] [clojure.spec.alpha :as s] [cuerdas.core :as str] [integrant.core :as ig] + [ring.request :as rreq] [yetti.request :as yrq])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -137,12 +139,12 @@ (declare ^:private gen-token) (defn create-fn - [{:keys [::manager ::main/props]} profile-id] + [{:keys [::manager ::setup/props]} profile-id] (us/assert! ::manager manager) (us/assert! ::us/uuid profile-id) (fn [request response] - (let [uagent (yrq/get-header request "user-agent") + (let [uagent (rreq/get-header request "user-agent") params {:profile-id profile-id :user-agent uagent :created-at (dt/now)} @@ -195,7 +197,7 @@ (neg? (compare default-renewal-max-age elapsed))))) (defn- wrap-soft-auth - [handler {:keys [::manager ::main/props]}] + [handler {:keys [::manager ::setup/props]}] (us/assert! ::manager manager) (letfn [(handle-request [request] (try @@ -209,9 +211,8 @@ (l/trace :hint "exception on decoding malformed token" :cause cause) request)))] - (fn [request respond raise] - (let [request (handle-request request)] - (handler request respond raise))))) + (fn [request] + (handler (handle-request request))))) (defn- wrap-authz [handler {:keys [::manager]}] @@ -221,12 +222,15 @@ request (cond-> request (some? session) (assoc ::profile-id (:profile-id session) - ::id (:id session)))] + ::id (:id session))) + response (handler request)] - (cond-> (handler request) - (renew-session? session) - (-> (assign-auth-token-cookie session) - (assign-authenticated-cookie session)))))) + (if (renew-session? session) + (let [session (update! manager session)] + (-> response + (assign-auth-token-cookie session) + (assign-authenticated-cookie session))) + response)))) (def soft-auth {:name ::soft-auth @@ -245,6 +249,7 @@ renewal (dt/plus created-at default-renewal-max-age) expires (dt/plus created-at max-age) secure? (contains? cf/flags :secure-session-cookies) + strict? (contains? cf/flags :strict-session-cookies) cors? (contains? cf/flags :cors) name (cf/get :auth-token-cookie-name default-auth-token-cookie-name) comment (str "Renewal at: " (dt/format-instant renewal :rfc1123)) @@ -253,7 +258,7 @@ :expires expires :value token :comment comment - :same-site (if cors? :none :lax) + :same-site (if cors? :none (if strict? :strict :lax)) :secure secure?}] (update response :cookies assoc name cookie))) diff --git a/backend/src/app/http/sse.clj b/backend/src/app/http/sse.clj new file mode 100644 index 0000000000..8688010916 --- /dev/null +++ b/backend/src/app/http/sse.clj @@ -0,0 +1,67 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.http.sse + "SSE (server sent events) helpers" + (:refer-clojure :exclude [tap]) + (:require + [app.common.data :as d] + [app.common.logging :as l] + [app.common.transit :as t] + [app.http.errors :as errors] + [app.util.events :as events] + [promesa.exec :as px] + [promesa.exec.csp :as sp] + [promesa.util :as pu] + [ring.response :as rres]) + (:import + java.io.OutputStream)) + +(defn- write! + [^OutputStream output ^bytes data] + (l/trc :hint "writting data" :data data :length (alength data)) + (.write output data) + (.flush output)) + +(defn- encode + [[name data]] + (try + (let [data (with-out-str + (println "event:" (d/name name)) + (println "data:" (t/encode-str data {:type :json-verbose})) + (println))] + (.getBytes data "UTF-8")) + (catch Throwable cause + (l/err :hint "unexpected error on encoding value on sse stream" + :cause cause) + nil))) + +;; ---- PUBLIC API + +(def default-headers + {"Content-Type" "text/event-stream;charset=UTF-8" + "Cache-Control" "no-cache, no-store, max-age=0, must-revalidate" + "Pragma" "no-cache"}) + +(defn response + [handler & {:keys [buf] :or {buf 32} :as opts}] + (fn [request] + {::rres/headers default-headers + ::rres/status 200 + ::rres/body (reify rres/StreamableResponseBody + (-write-body-to-stream [_ _ output] + (binding [events/*channel* (sp/chan :buf buf :xf (keep encode))] + (let [listener (events/start-listener + (partial write! output) + (partial pu/close! output))] + (try + (let [result (handler)] + (events/tap :end result)) + (catch Throwable cause + (events/tap :error (errors/handle' cause request))) + (finally + (sp/close! events/*channel*) + (px/await! listener)))))))})) diff --git a/backend/src/app/http/websocket.clj b/backend/src/app/http/websocket.clj index bb29839a11..864de29879 100644 --- a/backend/src/app/http/websocket.clj +++ b/backend/src/app/http/websocket.clj @@ -10,7 +10,7 @@ [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.pprint :as pp] - [app.common.spec :as us] + [app.common.schema :as sm] [app.common.uuid :as uuid] [app.db :as db] [app.http.session :as session] @@ -21,6 +21,7 @@ [clojure.spec.alpha :as s] [integrant.core :as ig] [promesa.exec.csp :as sp] + [ring.websocket :as rws] [yetti.websocket :as yws])) (def recv-labels @@ -116,21 +117,21 @@ :profile-id profile-id :session-id session-id}] - ;; Close profile subscription if exists - (when-let [ch (:channel psub)] - (sp/close! ch) - (mbus/purge! msgbus [ch])) + ;; Close profile subscription if exists + (when-let [ch (:channel psub)] + (sp/close! ch) + (mbus/purge! msgbus [ch])) - ;; Close team subscription if exists - (when-let [ch (:channel tsub)] - (sp/close! ch) - (mbus/purge! msgbus [ch])) + ;; Close team subscription if exists + (when-let [ch (:channel tsub)] + (sp/close! ch) + (mbus/purge! msgbus [ch])) - ;; Close file subscription if exists - (when-let [{:keys [topic channel]} fsub] - (sp/close! channel) - (mbus/purge! msgbus [channel]) - (mbus/pub! msgbus :topic topic :message msg)))) + ;; Close file subscription if exists + (when-let [{:keys [topic channel]} fsub] + (sp/close! channel) + (mbus/purge! msgbus [channel]) + (mbus/pub! msgbus :topic topic :message msg)))) (defmethod handle-message :subscribe-team [{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::ws/output-ch ::session-id]} {:keys [team-id] :as params}] @@ -178,7 +179,7 @@ (let [message {:type :presence :file-id file-id :session-id session-id - :profile-id profile-id}] + :profile-id profile-id}] (mbus/pub! msgbus :topic file-id :message message))) @@ -277,19 +278,23 @@ :inc 1) message) - -(s/def ::session-id ::us/uuid) -(s/def ::handler-params - (s/keys :req-un [::session-id])) +(def ^:private schema:params + (sm/define + [:map {:title "params"} + [:session-id ::sm/uuid]])) (defn- http-handler [cfg {:keys [params ::session/profile-id] :as request}] - (let [{:keys [session-id]} (us/conform ::handler-params params)] + (let [{:keys [session-id]} (sm/conform! schema:params params)] (cond (not profile-id) (ex/raise :type :authentication :hint "Authentication required.") + ;; WORKAROUND: we use the adapter specific predicate for + ;; performance reasons; for now, the ring default impl for + ;; `upgrade-request?` parses all requests headers before perform + ;; any checking. (not (yws/upgrade-request? request)) (ex/raise :type :validation :code :websocket-request-expected @@ -298,14 +303,13 @@ :else (do (l/trace :hint "websocket request" :profile-id profile-id :session-id session-id) - (->> (ws/handler - ::ws/on-rcv-message (partial on-rcv-message cfg) - ::ws/on-snd-message (partial on-snd-message cfg) - ::ws/on-connect (partial on-connect cfg) - ::ws/handler (partial handle-message cfg) - ::profile-id profile-id - ::session-id session-id) - (yws/upgrade request)))))) + {::rws/listener (ws/listener request + ::ws/on-rcv-message (partial on-rcv-message cfg) + ::ws/on-snd-message (partial on-snd-message cfg) + ::ws/on-connect (partial on-connect cfg) + ::ws/handler (partial handle-message cfg) + ::profile-id profile-id + ::session-id session-id)})))) (defmethod ig/pre-init-spec ::routes [_] (s/keys :req [::mbus/msgbus @@ -318,5 +322,4 @@ (defmethod ig/init-key ::routes [_ cfg] ["/ws/notifications" {:middleware [[session/authz cfg]] - :handler (partial http-handler cfg) - :allowed-methods #{:get}}]) + :handler (partial http-handler cfg)}]) diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index df1ad5bf05..d89809f37a 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -9,31 +9,25 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.spec :as us] - [app.common.transit :as t] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] [app.http :as-alias http] [app.http.access-token :as-alias actoken] - [app.http.client :as http.client] [app.loggers.audit.tasks :as-alias tasks] [app.loggers.webhooks :as-alias webhooks] - [app.main :as-alias main] [app.rpc :as-alias rpc] [app.rpc.retry :as rtry] - [app.tokens :as tokens] + [app.setup :as-alias setup] [app.util.services :as-alias sv] [app.util.time :as dt] [app.worker :as wrk] [clojure.spec.alpha :as s] [cuerdas.core :as str] [integrant.core :as ig] - [lambdaisland.uri :as u] - [promesa.exec :as px] - [yetti.request :as yrq])) + [ring.request :as rreq])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HELPERS @@ -41,9 +35,9 @@ (defn parse-client-ip [request] - (or (some-> (yrq/get-header request "x-forwarded-for") (str/split ",") first) - (yrq/get-header request "x-real-ip") - (some-> (yrq/remote-addr request) str))) + (or (some-> (rreq/get-header request "x-forwarded-for") (str/split ",") first) + (rreq/get-header request "x-real-ip") + (some-> (rreq/remote-addr request) str))) (defn extract-utm-params "Extracts additional data from params and namespace them under @@ -133,7 +127,7 @@ [_ {:keys [::db/pool] :as cfg}] (cond (db/read-only? pool) - (l/warn :hint "audit: disabled (db is read-only)") + (l/warn :hint "audit disabled (db is read-only)") :else cfg)) @@ -187,33 +181,45 @@ false)})) (defn- handle-event! - [conn-or-pool event] - (us/verify! ::event event) + [cfg event] (let [params {:id (uuid/next) :name (::name event) :type (::type event) :profile-id (::profile-id event) :ip-addr (::ip-addr event) :context (::context event) - :props (::props event)}] + :props (::props event)} + tnow (dt/now)] (when (contains? cf/flags :audit-log) ;; NOTE: this operation may cause primary key conflicts on inserts ;; because of the timestamp precission (two concurrent requests), in ;; this case we just retry the operation. - (rtry/with-retry {::rtry/when rtry/conflict-exception? - ::rtry/max-retries 6 - ::rtry/label "persist-audit-log" - ::db/conn (dm/check db/connection? conn-or-pool)} - (let [now (dt/now)] - (db/insert! conn-or-pool :audit-log - (-> params - (update :props db/tjson) - (update :context db/tjson) - (update :ip-addr db/inet) - (assoc :created-at now) - (assoc :tracked-at now) - (assoc :source "backend")))))) + (let [params (-> params + (assoc :created-at tnow) + (assoc :tracked-at tnow) + (update :props db/tjson) + (update :context db/tjson) + (update :ip-addr db/inet) + (assoc :source "backend"))] + (db/insert! cfg :audit-log params))) + + (when (and (or (contains? cf/flags :telemetry) + (cf/get :telemetry-enabled)) + (not (contains? cf/flags :audit-log))) + ;; NOTE: this operation may cause primary key conflicts on inserts + ;; because of the timestamp precission (two concurrent requests), in + ;; this case we just retry the operation. + ;; + ;; NOTE: this is only executed when general audit log is disabled + (let [params (-> params + (assoc :created-at tnow) + (assoc :tracked-at tnow) + (assoc :props (db/tjson {})) + (assoc :context (db/tjson {})) + (assoc :ip-addr (db/inet "0.0.0.0")) + (assoc :source "backend"))] + (db/insert! cfg :audit-log params))) (when (and (contains? cf/flags :webhooks) (::webhooks/event? event)) @@ -226,7 +232,7 @@ :else label) dedupe? (boolean (and batch-key batch-timeout))] - (wrk/submit! ::wrk/conn conn-or-pool + (wrk/submit! ::wrk/conn (::db/conn cfg) ::wrk/task :process-webhook-event ::wrk/queue :webhooks ::wrk/max-retries 0 @@ -243,144 +249,13 @@ (defn submit! "Submit audit event to the collector." [cfg params] - (let [conn (or (::db/conn cfg) (::db/pool cfg))] - (us/assert! ::db/pool-or-conn conn) - (try - (handle-event! conn (d/without-nils params)) - (catch Throwable cause - (l/error :hint "audit: unexpected error processing event" :cause cause))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; TASK: ARCHIVE -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; This is a task responsible to send the accumulated events to -;; external service for archival. - -(declare archive-events) - -(s/def ::tasks/uri ::us/string) - -(defmethod ig/pre-init-spec ::tasks/archive-task [_] - (s/keys :req [::db/pool ::main/props ::http.client/client])) - -(defmethod ig/init-key ::tasks/archive - [_ cfg] - (fn [params] - ;; NOTE: this let allows overwrite default configured values from - ;; the repl, when manually invoking the task. - (let [enabled (or (contains? cf/flags :audit-log-archive) - (:enabled params false)) - uri (cf/get :audit-log-archive-uri) - uri (or uri (:uri params)) - cfg (assoc cfg ::uri uri)] - - (when (and enabled (not uri)) - (ex/raise :type :internal - :code :task-not-configured - :hint "archive task not configured, missing uri")) - - (when enabled - (loop [total 0] - (let [n (archive-events cfg)] - (if n - (do - (px/sleep 100) - (recur (+ total ^long n))) - (when (pos? total) - (l/debug :hint "events archived" :total total))))))))) - -(def ^:private sql:retrieve-batch-of-audit-log - "select * - from audit_log - where archived_at is null - order by created_at asc - limit 128 - for update skip locked;") - -(defn archive-events - [{:keys [::db/pool ::uri] :as cfg}] - (letfn [(decode-row [{:keys [props ip-addr context] :as row}] - (cond-> row - (db/pgobject? props) - (assoc :props (db/decode-transit-pgobject props)) - - (db/pgobject? context) - (assoc :context (db/decode-transit-pgobject context)) - - (db/pgobject? ip-addr "inet") - (assoc :ip-addr (db/decode-inet ip-addr)))) - - (row->event [row] - (select-keys row [:type - :name - :source - :created-at - :tracked-at - :profile-id - :ip-addr - :props - :context])) - - (send [events] - (let [token (tokens/generate (::main/props cfg) - {:iss "authentication" - :iat (dt/now) - :uid uuid/zero}) - body (t/encode {:events events}) - headers {"content-type" "application/transit+json" - "origin" (cf/get :public-uri) - "cookie" (u/map->query-string {:auth-token token})} - params {:uri uri - :timeout 6000 - :method :post - :headers headers - :body body} - resp (http.client/req! cfg params {:sync? true})] - (if (= (:status resp) 204) - true - (do - (l/error :hint "unable to archive events" - :resp-status (:status resp) - :resp-body (:body resp)) - false)))) - - (mark-as-archived [conn rows] - (db/exec-one! conn ["update audit_log set archived_at=now() where id = ANY(?)" - (->> (map :id rows) - (into-array java.util.UUID) - (db/create-array conn "uuid"))]))] - - (db/with-atomic [conn pool] - (let [rows (db/exec! conn [sql:retrieve-batch-of-audit-log]) - xform (comp (map decode-row) - (map row->event)) - events (into [] xform rows)] - (when-not (empty? events) - (l/trace :hint "archive events chunk" :uri uri :events (count events)) - (when (send events) - (mark-as-archived conn rows) - (count events))))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; GC Task -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(def ^:private sql:clean-archived - "delete from audit_log - where archived_at is not null") - -(defn- clean-archived - [{:keys [::db/pool]}] - (let [result (db/exec-one! pool [sql:clean-archived]) - result (:next.jdbc/update-count result)] - (l/debug :hint "delete archived audit log entries" :deleted result) - result)) - -(defmethod ig/pre-init-spec ::tasks/gc [_] - (s/keys :req [::db/pool])) - -(defmethod ig/init-key ::tasks/gc - [_ cfg] - (fn [_] - (clean-archived cfg))) + (try + (let [event (d/without-nils params) + cfg (-> cfg + (assoc ::rtry/when rtry/conflict-exception?) + (assoc ::rtry/max-retries 6) + (assoc ::rtry/label "persist-audit-log"))] + (us/verify! ::event event) + (rtry/invoke! cfg db/tx-run! handle-event! event)) + (catch Throwable cause + (l/error :hint "unexpected error processing event" :cause cause)))) diff --git a/backend/src/app/loggers/audit/archive_task.clj b/backend/src/app/loggers/audit/archive_task.clj new file mode 100644 index 0000000000..046fb8068d --- /dev/null +++ b/backend/src/app/loggers/audit/archive_task.clj @@ -0,0 +1,140 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.loggers.audit.archive-task + (:require + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.common.transit :as t] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.http.client :as http] + [app.setup :as-alias setup] + [app.tokens :as tokens] + [app.util.time :as dt] + [clojure.spec.alpha :as s] + [integrant.core :as ig] + [lambdaisland.uri :as u] + [promesa.exec :as px])) + +;; This is a task responsible to send the accumulated events to +;; external service for archival. + +(defn- decode-row + [{:keys [props ip-addr context] :as row}] + (cond-> row + (db/pgobject? props) + (assoc :props (db/decode-transit-pgobject props)) + + (db/pgobject? context) + (assoc :context (db/decode-transit-pgobject context)) + + (db/pgobject? ip-addr "inet") + (assoc :ip-addr (db/decode-inet ip-addr)))) + +(def ^:private event-keys + [:type + :name + :source + :created-at + :tracked-at + :profile-id + :ip-addr + :props + :context]) + +(defn- row->event + [row] + (select-keys row event-keys)) + +(defn- send! + [{:keys [::uri] :as cfg} events] + (let [token (tokens/generate (::setup/props cfg) + {:iss "authentication" + :iat (dt/now) + :uid uuid/zero}) + body (t/encode {:events events}) + headers {"content-type" "application/transit+json" + "origin" (cf/get :public-uri) + "cookie" (u/map->query-string {:auth-token token})} + params {:uri uri + :timeout 12000 + :method :post + :headers headers + :body body} + resp (http/req! cfg params)] + (if (= (:status resp) 204) + true + (do + (l/error :hint "unable to archive events" + :resp-status (:status resp) + :resp-body (:body resp)) + false)))) + +(defn- mark-archived! + [{:keys [::db/conn]} rows] + (let [ids (db/create-array conn "uuid" (map :id rows))] + (db/exec-one! conn ["update audit_log set archived_at=now() where id = ANY(?)" ids]))) + +(def ^:private xf:create-event + (comp (map decode-row) + (map row->event))) + +(def ^:private sql:get-audit-log-chunk + "SELECT * + FROM audit_log + WHERE archived_at is null + ORDER BY created_at ASC + LIMIT 128 + FOR UPDATE + SKIP LOCKED") + +(defn- get-event-rows + [{:keys [::db/conn] :as cfg}] + (->> (db/exec! conn [sql:get-audit-log-chunk]) + (not-empty))) + +(defn- archive-events! + [{:keys [::uri] :as cfg}] + (db/tx-run! cfg (fn [cfg] + (when-let [rows (get-event-rows cfg)] + (let [events (into [] xf:create-event rows)] + (l/trc :hint "archive events chunk" :uri uri :events (count events)) + (when (send! cfg events) + (mark-archived! cfg rows) + (count events))))))) + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req [::db/pool ::setup/props ::http/client])) + +(defmethod ig/init-key ::handler + [_ cfg] + (fn [params] + ;; NOTE: this let allows overwrite default configured values from + ;; the repl, when manually invoking the task. + (let [enabled (or (contains? cf/flags :audit-log-archive) + (:enabled params false)) + + uri (cf/get :audit-log-archive-uri) + uri (or uri (:uri params)) + cfg (assoc cfg ::uri uri)] + + (when (and enabled (not uri)) + (ex/raise :type :internal + :code :task-not-configured + :hint "archive task not configured, missing uri")) + + (when enabled + (loop [total 0] + (if-let [n (archive-events! cfg)] + (do + (px/sleep 100) + (recur (+ total ^long n))) + + (when (pos? total) + (l/dbg :hint "events archived" :total total)))))))) + diff --git a/backend/src/app/loggers/audit/gc_task.clj b/backend/src/app/loggers/audit/gc_task.clj new file mode 100644 index 0000000000..7f94217a49 --- /dev/null +++ b/backend/src/app/loggers/audit/gc_task.clj @@ -0,0 +1,31 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.loggers.audit.gc-task + (:require + [app.common.logging :as l] + [app.db :as db] + [clojure.spec.alpha :as s] + [integrant.core :as ig])) + +(def ^:private sql:clean-archived + "DELETE FROM audit_log + WHERE archived_at IS NOT NULL") + +(defn- clean-archived! + [{:keys [::db/pool]}] + (let [result (db/exec-one! pool [sql:clean-archived]) + result (db/get-update-count result)] + (l/debug :hint "delete archived audit log entries" :deleted result) + result)) + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req [::db/pool])) + +(defmethod ig/init-key ::handler + [_ cfg] + (fn [_] + (clean-archived! cfg))) diff --git a/backend/src/app/loggers/database.clj b/backend/src/app/loggers/database.clj index 85e5c6e3f2..e2892d13f8 100644 --- a/backend/src/app/loggers/database.clj +++ b/backend/src/app/loggers/database.clj @@ -39,33 +39,40 @@ (defn record->report [{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}] (us/assert! ::l/record record) + (if (or (instance? java.util.concurrent.CompletionException cause) + (instance? java.util.concurrent.ExecutionException cause)) + (-> record + (assoc ::trace (ex/format-throwable cause :data? false :explain? false :header? false :summary? false)) + (assoc ::l/cause (ex-cause cause)) + (record->report)) - (let [data (ex-data cause) - ctx (-> context - (assoc :tenant (cf/get :tenant)) - (assoc :host (cf/get :host)) - (assoc :public-uri (cf/get :public-uri)) - (assoc :logger/name logger) - (assoc :logger/level level) - (dissoc :request/params :value :params :data))] - (merge - {:context (-> (into (sorted-map) ctx) - (pp/pprint-str :width 200 :length 50 :level 10)) - :props (pp/pprint-str props :width 200 :length 50) - :hint (or (ex-message cause) @message) - :trace (ex/format-throwable cause :data? false :explain? false :header? false :summary? false)} + (let [data (ex-data cause) + ctx (-> context + (assoc :tenant (cf/get :tenant)) + (assoc :host (cf/get :host)) + (assoc :public-uri (cf/get :public-uri)) + (assoc :logger/name logger) + (assoc :logger/level level) + (dissoc :request/params :value :params :data))] + (merge + {:context (-> (into (sorted-map) ctx) + (pp/pprint-str :length 50)) + :props (pp/pprint-str props :length 50) + :hint (or (ex-message cause) @message) + :trace (or (::trace record) + (ex/format-throwable cause :data? false :explain? false :header? false :summary? false))} - (when-let [params (or (:request/params context) (:params context))] - {:params (pp/pprint-str params :width 200 :length 50 :level 10)}) + (when-let [params (or (:request/params context) (:params context))] + {:params (pp/pprint-str params :length 30 :level 12)}) - (when-let [value (:value context)] - {:value (pp/pprint-str value :width 200 :length 50 :level 10)}) + (when-let [value (:value context)] + {:value (pp/pprint-str value :length 30 :level 12)}) - (when-let [data (some-> data (dissoc ::s/problems ::s/value ::s/spec ::sm/explain :hint))] - {:data (pp/pprint-str data :width 200)}) + (when-let [data (some-> data (dissoc ::s/problems ::s/value ::s/spec ::sm/explain :hint))] + {:data (pp/pprint-str data :length 30 :level 12)}) - (when-let [explain (ex/explain data {:level 10 :length 50})] - {:explain explain})))) + (when-let [explain (ex/explain data :length 30 :level 12)] + {:explain explain}))))) (defn error-record? [{:keys [::l/level ::l/cause]}] @@ -89,11 +96,11 @@ (defmethod ig/init-key ::reporter [_ cfg] - (let [input (sp/chan :buf (sp/sliding-buffer 32) + (let [input (sp/chan :buf (sp/sliding-buffer 64) :xf (filter error-record?))] (add-watch l/log-record ::reporter #(sp/put! input %4)) - (px/thread {:name "penpot/database-reporter" :virtual true} + (px/thread {:name "penpot/database-reporter"} (l/info :hint "initializing database error persistence") (try (loop [] diff --git a/backend/src/app/loggers/webhooks.clj b/backend/src/app/loggers/webhooks.clj index a3015a4fb5..00ebd3f383 100644 --- a/backend/src/app/loggers/webhooks.clj +++ b/backend/src/app/loggers/webhooks.clj @@ -111,9 +111,11 @@ " where id=?") err (:id whook)] - res (db/exec-one! pool sql {::db/return-keys? true})] + res (db/exec-one! pool sql {::db/return-keys true})] (when (>= (:error-count res) max-errors) - (db/update! pool :webhook {:is-active false} {:id (:id whook)}))) + (db/update! pool :webhook + {:is-active false} + {:id (:id whook)}))) (db/update! pool :webhook {:updated-at (dt/now) @@ -182,5 +184,4 @@ "invalid-uri" (instance? java.net.http.HttpConnectTimeoutException cause) - "timeout" - )) + "timeout")) diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 0f1eb8854e..3c61e6b35e 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -21,7 +21,6 @@ [app.http.session :as-alias session] [app.http.session.tasks :as-alias session.tasks] [app.http.websocket :as http.ws] - [app.loggers.audit.tasks :as-alias audit.tasks] [app.loggers.webhooks :as-alias webhooks] [app.metrics :as-alias mtx] [app.metrics.definition :as-alias mdef] @@ -33,11 +32,18 @@ [app.srepl :as-alias srepl] [app.storage :as-alias sto] [app.storage.fs :as-alias sto.fs] + [app.storage.gc-deleted :as-alias sto.gc-deleted] + [app.storage.gc-touched :as-alias sto.gc-touched] [app.storage.s3 :as-alias sto.s3] + [app.svgo :as-alias svgo] [app.util.time :as dt] [app.worker :as-alias wrk] + [cider.nrepl :refer [cider-nrepl-handler]] + [clojure.test :as test] + [clojure.tools.namespace.repl :as repl] [cuerdas.core :as str] [integrant.core :as ig] + [nrepl.server :as nrepl] [promesa.exec :as px]) (:gen-class)) @@ -155,12 +161,6 @@ {::mdef/name "penpot_executors_running_threads" ::mdef/help "Current number of threads with state RUNNING." ::mdef/labels ["name"] - ::mdef/type :gauge} - - :executors-queued-submissions - {::mdef/name "penpot_executors_queued_submissions" - ::mdef/help "Current number of queued submissions." - ::mdef/labels ["name"] ::mdef/type :gauge}}) (def system-config @@ -175,13 +175,12 @@ ;; Default thread pool for IO operations ::wrk/executor - {::wrk/parallelism (cf/get :default-executor-parallelism - (+ 3 (* (px/get-available-processors) 3)))} + {} ::wrk/monitor {::mtx/metrics (ig/ref ::mtx/metrics) - ::wrk/name "default" - ::wrk/executor (ig/ref ::wrk/executor)} + ::wrk/executor (ig/ref ::wrk/executor) + ::wrk/name "default"} :app.migrations/migrations {::db/pool (ig/ref ::db/pool)} @@ -204,15 +203,15 @@ :app.storage.tmp/cleaner {::wrk/executor (ig/ref ::wrk/executor)} - ::sto/gc-deleted-task + ::sto.gc-deleted/handler {::db/pool (ig/ref ::db/pool) ::sto/storage (ig/ref ::sto/storage)} - ::sto/gc-touched-task + ::sto.gc-touched/handler {::db/pool (ig/ref ::db/pool)} ::http.client/client - {::wrk/executor (ig/ref ::wrk/executor)} + {} ::session/manager {::db/pool (ig/ref ::db/pool)} @@ -221,16 +220,14 @@ {::db/pool (ig/ref ::db/pool)} ::http.awsns/routes - {::props (ig/ref ::setup/props) + {::setup/props (ig/ref ::setup/props) ::db/pool (ig/ref ::db/pool) - ::http.client/client (ig/ref ::http.client/client) - ::wrk/executor (ig/ref ::wrk/executor)} + ::http.client/client (ig/ref ::http.client/client)} ::http/server {::http/port (cf/get :http-server-port) ::http/host (cf/get :http-server-host) ::http/router (ig/ref ::http/router) - ::wrk/executor (ig/ref ::wrk/executor) ::http/io-threads (cf/get :http-server-io-threads) ::http/max-body-size (cf/get :http-server-max-body-size) ::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)} @@ -264,7 +261,7 @@ ::oidc/routes {::http.client/client (ig/ref ::http.client/client) ::db/pool (ig/ref ::db/pool) - ::props (ig/ref ::setup/props) + ::setup/props (ig/ref ::setup/props) ::oidc/providers {:google (ig/ref ::oidc.providers/google) :github (ig/ref ::oidc.providers/github) :gitlab (ig/ref ::oidc.providers/gitlab) @@ -276,7 +273,7 @@ ::db/pool (ig/ref ::db/pool) ::rpc/routes (ig/ref ::rpc/routes) ::rpc.doc/routes (ig/ref ::rpc.doc/routes) - ::props (ig/ref ::setup/props) + ::setup/props (ig/ref ::setup/props) ::mtx/routes (ig/ref ::mtx/routes) ::oidc/routes (ig/ref ::oidc/routes) ::http.debug/routes (ig/ref ::http.debug/routes) @@ -284,11 +281,11 @@ ::http.ws/routes (ig/ref ::http.ws/routes) ::http.awsns/routes (ig/ref ::http.awsns/routes)} - :app.http.debug/routes + ::http.debug/routes {::db/pool (ig/ref ::db/pool) - ::wrk/executor (ig/ref ::wrk/executor) ::session/manager (ig/ref ::session/manager) - ::sto/storage (ig/ref ::sto/storage)} + ::sto/storage (ig/ref ::sto/storage) + ::setup/props (ig/ref ::setup/props)} ::http.ws/routes {::db/pool (ig/ref ::db/pool) @@ -300,8 +297,7 @@ {::http.assets/path (cf/get :assets-path) ::http.assets/cache-max-age (dt/duration {:hours 24}) ::http.assets/cache-max-agesignature-max-age (dt/duration {:hours 24 :minutes 5}) - ::sto/storage (ig/ref ::sto/storage) - ::wrk/executor (ig/ref ::wrk/executor)} + ::sto/storage (ig/ref ::sto/storage)} :app.rpc/climit {::mtx/metrics (ig/ref ::mtx/metrics) @@ -320,14 +316,12 @@ ::mtx/metrics (ig/ref ::mtx/metrics) ::mbus/msgbus (ig/ref ::mbus/msgbus) ::rds/redis (ig/ref ::rds/redis) + ::svgo/optimizer (ig/ref ::svgo/optimizer) ::rpc/climit (ig/ref ::rpc/climit) ::rpc/rlimit (ig/ref ::rpc/rlimit) ::setup/templates (ig/ref ::setup/templates) - ::props (ig/ref ::setup/props) - - :pool (ig/ref ::db/pool) - } + ::setup/props (ig/ref ::setup/props)} :app.rpc.doc/routes {:methods (ig/ref :app.rpc/methods)} @@ -335,24 +329,24 @@ :app.rpc/routes {::rpc/methods (ig/ref :app.rpc/methods) ::db/pool (ig/ref ::db/pool) - ::wrk/executor (ig/ref ::wrk/executor) ::session/manager (ig/ref ::session/manager) - ::props (ig/ref ::setup/props)} + ::setup/props (ig/ref ::setup/props)} ::wrk/registry {::mtx/metrics (ig/ref ::mtx/metrics) ::wrk/tasks {:sendmail (ig/ref ::email/handler) :objects-gc (ig/ref :app.tasks.objects-gc/handler) + :orphan-teams-gc (ig/ref :app.tasks.orphan-teams-gc/handler) :file-gc (ig/ref :app.tasks.file-gc/handler) :file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler) - :storage-gc-deleted (ig/ref ::sto/gc-deleted-task) - :storage-gc-touched (ig/ref ::sto/gc-touched-task) :tasks-gc (ig/ref :app.tasks.tasks-gc/handler) :telemetry (ig/ref :app.tasks.telemetry/handler) + :storage-gc-deleted (ig/ref ::sto.gc-deleted/handler) + :storage-gc-touched (ig/ref ::sto.gc-touched/handler) :session-gc (ig/ref ::session.tasks/gc) - :audit-log-archive (ig/ref ::audit.tasks/archive) - :audit-log-gc (ig/ref ::audit.tasks/gc) + :audit-log-archive (ig/ref :app.loggers.audit.archive-task/handler) + :audit-log-gc (ig/ref :app.loggers.audit.gc-task/handler) :process-webhook-event (ig/ref ::webhooks/process-event-handler) @@ -380,6 +374,9 @@ {::db/pool (ig/ref ::db/pool) ::sto/storage (ig/ref ::sto/storage)} + :app.tasks.orphan-teams-gc/handler + {::db/pool (ig/ref ::db/pool)} + :app.tasks.file-gc/handler {::db/pool (ig/ref ::db/pool) ::sto/storage (ig/ref ::sto/storage)} @@ -390,7 +387,7 @@ :app.tasks.telemetry/handler {::db/pool (ig/ref ::db/pool) ::http.client/client (ig/ref ::http.client/client) - ::props (ig/ref ::setup/props)} + ::setup/props (ig/ref ::setup/props)} [::srepl/urepl ::srepl/server] {::srepl/port (cf/get :urepl-port 6062) @@ -404,18 +401,21 @@ ::setup/props {::db/pool (ig/ref ::db/pool) - ::key (cf/get :secret-key) + ::setup/key (cf/get :secret-key) ;; NOTE: this dependency is only necessary for proper initialization ordering, props ;; module requires the migrations to run before initialize. ::migrations (ig/ref :app.migrations/migrations)} - ::audit.tasks/archive - {::props (ig/ref ::setup/props) + ::svgo/optimizer + {} + + :app.loggers.audit.archive-task/handler + {::setup/props (ig/ref ::setup/props) ::db/pool (ig/ref ::db/pool) ::http.client/client (ig/ref ::http.client/client)} - ::audit.tasks/gc + :app.loggers.audit.gc-task/handler {::db/pool (ig/ref ::db/pool)} ::webhooks/process-event-handler @@ -434,20 +434,18 @@ ::sto/storage {::db/pool (ig/ref ::db/pool) - ::wrk/executor (ig/ref ::wrk/executor) ::sto/backends {:assets-s3 (ig/ref [::assets :app.storage.s3/backend]) :assets-fs (ig/ref [::assets :app.storage.fs/backend])}} [::assets :app.storage.s3/backend] - {::sto.s3/region (cf/get :storage-assets-s3-region) - ::sto.s3/endpoint (cf/get :storage-assets-s3-endpoint) - ::sto.s3/bucket (cf/get :storage-assets-s3-bucket) - ::wrk/executor (ig/ref ::wrk/executor)} + {::sto.s3/region (cf/get :storage-assets-s3-region) + ::sto.s3/endpoint (cf/get :storage-assets-s3-endpoint) + ::sto.s3/bucket (cf/get :storage-assets-s3-bucket) + ::sto.s3/io-threads (cf/get :storage-assets-s3-io-threads)} [::assets :app.storage.fs/backend] - {::sto.fs/directory (cf/get :storage-assets-fs-directory)} - }) + {::sto.fs/directory (cf/get :storage-assets-fs-directory)}}) (def worker-config @@ -464,6 +462,9 @@ {:cron #app/cron "0 0 0 * * ?" ;; daily :task :objects-gc} + {:cron #app/cron "0 0 0 * * ?" ;; daily + :task :orphan-teams-gc} + {:cron #app/cron "0 0 0 * * ?" ;; daily :task :storage-gc-deleted} @@ -492,7 +493,7 @@ ::mtx/metrics (ig/ref ::mtx/metrics) ::db/pool (ig/ref ::db/pool)} - [::default ::wrk/worker] + [::default ::wrk/runner] {::wrk/parallelism (cf/get ::worker-default-parallelism 1) ::wrk/queue :default ::rds/redis (ig/ref ::rds/redis) @@ -500,7 +501,7 @@ ::mtx/metrics (ig/ref ::mtx/metrics) ::db/pool (ig/ref ::db/pool)} - [::webhook ::wrk/worker] + [::webhook ::wrk/runner] {::wrk/parallelism (cf/get ::worker-webhook-parallelism 1) ::wrk/queue :webhooks ::rds/redis (ig/ref ::rds/redis) @@ -521,22 +522,65 @@ (merge worker-config)) (ig/prep) (ig/init)))) - (l/info :hint "welcome to penpot" - :flags (str/join "," (map name cf/flags)) - :worker? (contains? cf/flags :backend-worker) - :version (:full cf/version))) + (l/inf :hint "welcome to penpot" + :flags (str/join "," (map name cf/flags)) + :worker? (contains? cf/flags :backend-worker) + :version (:full cf/version))) (defn stop [] (alter-var-root #'system (fn [sys] (when sys (ig/halt! sys)) nil))) +(defn restart + [] + (stop) + (repl/refresh :after 'app.main/start)) + +(defn restart-all + [] + (stop) + (repl/refresh-all :after 'app.main/start)) + +(defmacro run-bench + [& exprs] + `(do + (require 'criterium.core) + (criterium.core/with-progress-reporting (crit/quick-bench (do ~@exprs) :verbose)))) + +(defn run-tests + ([] (run-tests #"^backend-tests.*-test$")) + ([o] + (repl/refresh) + (cond + (instance? java.util.regex.Pattern o) + (test/run-all-tests o) + + (symbol? o) + (if-let [sns (namespace o)] + (do (require (symbol sns)) + (test/test-vars [(resolve o)])) + (test/test-ns o))))) + +(repl/disable-reload! (find-ns 'integrant.core)) (defn -main [& _args] (try - (start) + (let [p (promise)] + (when (contains? cf/flags :nrepl-server) + (l/inf :hint "start nrepl server" :port 6064) + (nrepl/start-server :bind "0.0.0.0" :port 6064 :handler cider-nrepl-handler)) + + (start) + (deref p)) (catch Throwable cause - (l/error :hint (ex-message cause) - :cause cause) + (binding [*out* *err*] + (println "==== ERROR ====")) + (.printStackTrace cause) + (when-let [cause' (ex-cause cause)] + (binding [*out* *err*] + (println "==== CAUSE ====")) + (.printStackTrace cause')) + (px/sleep 500) (System/exit -1)))) diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index 9964c2824d..8d2315352a 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -14,11 +14,11 @@ [app.common.schema.generators :as sg] [app.common.schema.openapi :as-alias oapi] [app.common.spec :as us] + [app.common.svg :as csvg] [app.config :as cf] [app.db :as-alias db] [app.storage :as-alias sto] [app.storage.tmp :as tmp] - [app.util.svg :as svg] [app.util.time :as dt] [buddy.core.bytes :as bb] [buddy.core.codecs :as bc] @@ -32,9 +32,6 @@ org.im4java.core.IMOperation org.im4java.core.Info)) -(def default-max-file-size - (* 1024 1024 30)) ; 30 MiB - (s/def ::path fs/path?) (s/def ::filename string?) (s/def ::size integer?) @@ -83,13 +80,14 @@ (defn validate-media-size! [upload] - (when (> (:size upload) (cf/get :media-max-file-size default-max-file-size)) - (ex/raise :type :restriction - :code :media-max-file-size-reached - :hint (str/ffmt "the uploaded file size % is greater than the maximum %" - (:size upload) - default-max-file-size))) - upload) + (let [max-size (cf/get :media-max-file-size)] + (when (> (:size upload) max-size) + (ex/raise :type :restriction + :code :media-max-file-size-reached + :hint (str/ffmt "the uploaded file size % is greater than the maximum %" + (:size upload) + max-size))) + upload)) (defmulti process :cmd) (defmulti process-error class) @@ -201,7 +199,7 @@ (us/assert ::input input) (let [{:keys [path mtype]} input] (if (= mtype "image/svg+xml") - (let [info (some-> path slurp svg/pre-process svg/parse get-basic-info-from-svg)] + (let [info (some-> path slurp csvg/parse get-basic-info-from-svg)] (when-not info (ex/raise :type :validation :code :invalid-svg-file diff --git a/backend/src/app/metrics.clj b/backend/src/app/metrics.clj index d5d929bc95..3848c0773f 100644 --- a/backend/src/app/metrics.clj +++ b/backend/src/app/metrics.clj @@ -94,7 +94,7 @@ writer (StringWriter.)] (TextFormat/write004 writer samples) {:headers {"content-type" TextFormat/CONTENT_TYPE_004} - :body (.toString writer)})) + :body (.toString writer)})) diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 00407df219..86f0fa6f5f 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -324,10 +324,62 @@ {:name "0104-mod-file-thumbnail-table" :fn (mg/resource "app/migrations/sql/0104-mod-file-thumbnail-table.sql")} + {:name "0105-mod-file-change-table" + :fn (mg/resource "app/migrations/sql/0105-mod-file-change-table.sql")} + {:name "0105-mod-server-error-report-table" :fn (mg/resource "app/migrations/sql/0105-mod-server-error-report-table.sql")} - ]) + {:name "0106-add-file-tagged-object-thumbnail-table" + :fn (mg/resource "app/migrations/sql/0106-add-file-tagged-object-thumbnail-table.sql")} + + {:name "0106-mod-team-table" + :fn (mg/resource "app/migrations/sql/0106-mod-team-table.sql")} + + {:name "0107-mod-file-tagged-object-thumbnail-table" + :fn (mg/resource "app/migrations/sql/0107-mod-file-tagged-object-thumbnail-table.sql")} + + {:name "0107-add-deletion-protection-trigger-function" + :fn (mg/resource "app/migrations/sql/0107-add-deletion-protection-trigger-function.sql")} + + {:name "0108-mod-file-thumbnail-table" + :fn (mg/resource "app/migrations/sql/0108-mod-file-thumbnail-table.sql")} + + {:name "0109-mod-file-tagged-object-thumbnail-table" + :fn (mg/resource "app/migrations/sql/0109-mod-file-tagged-object-thumbnail-table.sql")} + + {:name "0110-mod-file-media-object-table" + :fn (mg/resource "app/migrations/sql/0110-mod-file-media-object-table.sql")} + + {:name "0111-mod-file-data-fragment-table" + :fn (mg/resource "app/migrations/sql/0111-mod-file-data-fragment-table.sql")} + + {:name "0112-mod-profile-table" + :fn (mg/resource "app/migrations/sql/0112-mod-profile-table.sql")} + + {:name "0113-mod-team-font-variant-table" + :fn (mg/resource "app/migrations/sql/0113-mod-team-font-variant-table.sql")} + + {:name "0114-mod-team-table" + :fn (mg/resource "app/migrations/sql/0114-mod-team-table.sql")} + + {:name "0115-mod-project-table" + :fn (mg/resource "app/migrations/sql/0115-mod-project-table.sql")} + + {:name "0116-mod-file-table" + :fn (mg/resource "app/migrations/sql/0116-mod-file-table.sql")} + + {:name "0117-mod-file-object-thumbnail-table" + :fn (mg/resource "app/migrations/sql/0117-mod-file-object-thumbnail-table.sql")} + + {:name "0118-mod-task-table" + :fn (mg/resource "app/migrations/sql/0118-mod-task-table.sql")} + + {:name "0119-mod-file-table" + :fn (mg/resource "app/migrations/sql/0119-mod-file-table.sql")} + + {:name "0120-mod-audit-log-table" + :fn (mg/resource "app/migrations/sql/0120-mod-audit-log-table.sql")}]) (defn apply-migrations! [pool name migrations] diff --git a/backend/src/app/migrations/sql/0105-mod-file-change-table.sql b/backend/src/app/migrations/sql/0105-mod-file-change-table.sql new file mode 100644 index 0000000000..d8385df9e7 --- /dev/null +++ b/backend/src/app/migrations/sql/0105-mod-file-change-table.sql @@ -0,0 +1,9 @@ +ALTER TABLE file_change + ADD COLUMN label text NULL; + +ALTER TABLE file_change + ALTER COLUMN label SET STORAGE external; + +CREATE INDEX file_change__label__idx + ON file_change (file_id, label) + WHERE label is not null; diff --git a/backend/src/app/migrations/sql/0106-add-file-tagged-object-thumbnail-table.sql b/backend/src/app/migrations/sql/0106-add-file-tagged-object-thumbnail-table.sql new file mode 100644 index 0000000000..8d2fed8b98 --- /dev/null +++ b/backend/src/app/migrations/sql/0106-add-file-tagged-object-thumbnail-table.sql @@ -0,0 +1,10 @@ +CREATE TABLE file_tagged_object_thumbnail ( + file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE DEFERRABLE, + tag text DEFAULT 'frame', + object_id text NOT NULL, + + media_id uuid NOT NULL REFERENCES storage_object(id) ON DELETE CASCADE DEFERRABLE, + created_at timestamptz NOT NULL DEFAULT now(), + + PRIMARY KEY(file_id, tag, object_id) +); diff --git a/backend/src/app/migrations/sql/0106-mod-team-table.sql b/backend/src/app/migrations/sql/0106-mod-team-table.sql new file mode 100644 index 0000000000..6cfe89a5c2 --- /dev/null +++ b/backend/src/app/migrations/sql/0106-mod-team-table.sql @@ -0,0 +1 @@ +ALTER TABLE team ADD COLUMN features text[] NULL DEFAULT null; diff --git a/backend/src/app/migrations/sql/0107-add-deletion-protection-trigger-function.sql b/backend/src/app/migrations/sql/0107-add-deletion-protection-trigger-function.sql new file mode 100644 index 0000000000..1ccf9b8b79 --- /dev/null +++ b/backend/src/app/migrations/sql/0107-add-deletion-protection-trigger-function.sql @@ -0,0 +1,8 @@ +CREATE OR REPLACE FUNCTION raise_deletion_protection() + RETURNS TRIGGER AS $$ + BEGIN + RAISE EXCEPTION 'unable to proceed to delete row on "%"', TG_TABLE_NAME + USING HINT = 'disable deletion protection with "SET rules.deletion_protection TO off"'; + RETURN NULL; + END; +$$ LANGUAGE plpgsql; diff --git a/backend/src/app/migrations/sql/0107-mod-file-tagged-object-thumbnail-table.sql b/backend/src/app/migrations/sql/0107-mod-file-tagged-object-thumbnail-table.sql new file mode 100644 index 0000000000..d405b7063b --- /dev/null +++ b/backend/src/app/migrations/sql/0107-mod-file-tagged-object-thumbnail-table.sql @@ -0,0 +1,2 @@ +CREATE INDEX file_tagged_object_thumbnail__media_id__idx + ON file_tagged_object_thumbnail (media_id); diff --git a/backend/src/app/migrations/sql/0108-mod-file-thumbnail-table.sql b/backend/src/app/migrations/sql/0108-mod-file-thumbnail-table.sql new file mode 100644 index 0000000000..b7d05bdc7f --- /dev/null +++ b/backend/src/app/migrations/sql/0108-mod-file-thumbnail-table.sql @@ -0,0 +1,25 @@ +--- Add missing index for deleted_at column, we include all related +--- columns because we expect the index to be small and expect use +--- index-only scans. +CREATE INDEX IF NOT EXISTS file_thumbnail__deleted_at__idx + ON file_thumbnail (deleted_at, file_id, revn, media_id) + WHERE deleted_at IS NOT NULL; + +--- Add missing for media_id column, used mainly for refs checking +CREATE INDEX IF NOT EXISTS file_thumbnail__media_id__idx ON file_thumbnail (media_id); + +--- Remove CASCADE from media_id and file_id foreign constraint +ALTER TABLE file_thumbnail + DROP CONSTRAINT file_thumbnail_file_id_fkey, + ADD FOREIGN KEY (file_id) REFERENCES file(id) DEFERRABLE; + +ALTER TABLE file_thumbnail + DROP CONSTRAINT file_thumbnail_media_id_fkey, + ADD FOREIGN KEY (media_id) REFERENCES storage_object(id) DEFERRABLE; + +--- Add deletion protection +CREATE OR REPLACE TRIGGER deletion_protection__tgr +BEFORE DELETE ON file_thumbnail FOR EACH STATEMENT + WHEN ((current_setting('rules.deletion_protection', true) IN ('on', '')) OR + (current_setting('rules.deletion_protection', true) IS NULL)) + EXECUTE PROCEDURE raise_deletion_protection(); diff --git a/backend/src/app/migrations/sql/0109-mod-file-tagged-object-thumbnail-table.sql b/backend/src/app/migrations/sql/0109-mod-file-tagged-object-thumbnail-table.sql new file mode 100644 index 0000000000..3184a6576f --- /dev/null +++ b/backend/src/app/migrations/sql/0109-mod-file-tagged-object-thumbnail-table.sql @@ -0,0 +1,26 @@ +ALTER TABLE file_tagged_object_thumbnail + ADD COLUMN updated_at timestamptz NULL, + ADD COLUMN deleted_at timestamptz NULL; + +--- Add index for deleted_at column, we include all related columns +--- because we expect the index to be small and expect use index-only +--- scans. +CREATE INDEX IF NOT EXISTS file_tagged_object_thumbnail__deleted_at__idx + ON file_tagged_object_thumbnail (deleted_at, file_id, object_id, media_id) + WHERE deleted_at IS NOT NULL; + +--- Remove CASCADE from media_id and file_id foreign constraint +ALTER TABLE file_tagged_object_thumbnail + DROP CONSTRAINT file_tagged_object_thumbnail_media_id_fkey, + ADD FOREIGN KEY (media_id) REFERENCES storage_object(id) DEFERRABLE; + +ALTER TABLE file_tagged_object_thumbnail + DROP CONSTRAINT file_tagged_object_thumbnail_file_id_fkey, + ADD FOREIGN KEY (file_id) REFERENCES file(id) DEFERRABLE; + +--- Add deletion protection +CREATE OR REPLACE TRIGGER deletion_protection__tgr +BEFORE DELETE ON file_tagged_object_thumbnail FOR EACH STATEMENT + WHEN ((current_setting('rules.deletion_protection', true) IN ('on', '')) OR + (current_setting('rules.deletion_protection', true) IS NULL)) + EXECUTE PROCEDURE raise_deletion_protection(); diff --git a/backend/src/app/migrations/sql/0110-mod-file-media-object-table.sql b/backend/src/app/migrations/sql/0110-mod-file-media-object-table.sql new file mode 100644 index 0000000000..49cbebc96c --- /dev/null +++ b/backend/src/app/migrations/sql/0110-mod-file-media-object-table.sql @@ -0,0 +1,27 @@ +--- Fix legacy naming +ALTER INDEX media_object_pkey RENAME TO file_media_object_pkey; +ALTER INDEX media_object__file_id__idx RENAME TO file_media_object__file_id__idx; + +--- Create index for the deleted_at column +CREATE INDEX IF NOT EXISTS file_media_object__deleted_at__idx + ON file_media_object (deleted_at, id, media_id) + WHERE deleted_at IS NOT NULL; + +--- Drop now unnecesary trigger because this will be handled by the +--- application code +DROP TRIGGER file_media_object__on_delete__tgr ON file_media_object; +DROP FUNCTION on_delete_file_media_object ( ) CASCADE; +DROP TRIGGER file_media_object__on_insert__tgr ON file_media_object; +DROP FUNCTION on_media_object_insert () CASCADE; + +--- Remove CASCADE from file FOREIGN KEY +ALTER TABLE file_media_object + DROP CONSTRAINT file_media_object_file_id_fkey, + ADD FOREIGN KEY (file_id) REFERENCES file(id) DEFERRABLE; + +--- Add deletion protection +CREATE OR REPLACE TRIGGER deletion_protection__tgr +BEFORE DELETE ON file_media_object FOR EACH STATEMENT + WHEN ((current_setting('rules.deletion_protection', true) IN ('on', '')) OR + (current_setting('rules.deletion_protection', true) IS NULL)) + EXECUTE PROCEDURE raise_deletion_protection(); diff --git a/backend/src/app/migrations/sql/0111-mod-file-data-fragment-table.sql b/backend/src/app/migrations/sql/0111-mod-file-data-fragment-table.sql new file mode 100644 index 0000000000..8397124c3f --- /dev/null +++ b/backend/src/app/migrations/sql/0111-mod-file-data-fragment-table.sql @@ -0,0 +1,9 @@ +ALTER TABLE file_data_fragment + ADD COLUMN deleted_at timestamptz NULL; + +--- Add index for deleted_at column, we include all related columns +--- because we expect the index to be small and expect use index-only +--- scans. +CREATE INDEX IF NOT EXISTS file_data_fragment__deleted_at__idx + ON file_data_fragment (deleted_at, file_id, id) + WHERE deleted_at IS NOT NULL; diff --git a/backend/src/app/migrations/sql/0112-mod-profile-table.sql b/backend/src/app/migrations/sql/0112-mod-profile-table.sql new file mode 100644 index 0000000000..2db8d75b0a --- /dev/null +++ b/backend/src/app/migrations/sql/0112-mod-profile-table.sql @@ -0,0 +1,15 @@ +ALTER TABLE profile + DROP CONSTRAINT profile_photo_id_fkey, + ADD FOREIGN KEY (photo_id) REFERENCES storage_object(id) DEFERRABLE, + DROP CONSTRAINT profile_default_project_id_fkey, + ADD FOREIGN KEY (default_project_id) REFERENCES project(id) DEFERRABLE, + DROP CONSTRAINT profile_default_team_id_fkey, + ADD FOREIGN KEY (default_team_id) REFERENCES team(id) DEFERRABLE; + +--- Add deletion protection +CREATE OR REPLACE TRIGGER deletion_protection__tgr +BEFORE DELETE ON profile FOR EACH STATEMENT + WHEN ((current_setting('rules.deletion_protection', true) IN ('on', '')) OR + (current_setting('rules.deletion_protection', true) IS NULL)) + EXECUTE PROCEDURE raise_deletion_protection(); + diff --git a/backend/src/app/migrations/sql/0113-mod-team-font-variant-table.sql b/backend/src/app/migrations/sql/0113-mod-team-font-variant-table.sql new file mode 100644 index 0000000000..b9caa08f6e --- /dev/null +++ b/backend/src/app/migrations/sql/0113-mod-team-font-variant-table.sql @@ -0,0 +1,20 @@ +--- Remove ON DELETE SET NULL from foreign constraint on +--- storage_object table +ALTER TABLE team_font_variant + DROP CONSTRAINT team_font_variant_otf_file_id_fkey, + ADD FOREIGN KEY (otf_file_id) REFERENCES storage_object(id) DEFERRABLE, + DROP CONSTRAINT team_font_variant_ttf_file_id_fkey, + ADD FOREIGN KEY (ttf_file_id) REFERENCES storage_object(id) DEFERRABLE, + DROP CONSTRAINT team_font_variant_woff1_file_id_fkey, + ADD FOREIGN KEY (woff1_file_id) REFERENCES storage_object(id) DEFERRABLE, + DROP CONSTRAINT team_font_variant_woff2_file_id_fkey, + ADD FOREIGN KEY (woff2_file_id) REFERENCES storage_object(id) DEFERRABLE, + DROP CONSTRAINT team_font_variant_team_id_fkey, + ADD FOREIGN KEY (team_id) REFERENCES team(id) DEFERRABLE; + +--- Add deletion protection +CREATE OR REPLACE TRIGGER deletion_protection__tgr +BEFORE DELETE ON team_font_variant FOR EACH STATEMENT + WHEN ((current_setting('rules.deletion_protection', true) IN ('on', '')) OR + (current_setting('rules.deletion_protection', true) IS NULL)) + EXECUTE PROCEDURE raise_deletion_protection(); diff --git a/backend/src/app/migrations/sql/0114-mod-team-table.sql b/backend/src/app/migrations/sql/0114-mod-team-table.sql new file mode 100644 index 0000000000..8c76756437 --- /dev/null +++ b/backend/src/app/migrations/sql/0114-mod-team-table.sql @@ -0,0 +1,10 @@ +--- Add deletion protection +CREATE OR REPLACE TRIGGER deletion_protection__tgr +BEFORE DELETE ON team FOR EACH STATEMENT + WHEN ((current_setting('rules.deletion_protection', true) IN ('on', '')) OR + (current_setting('rules.deletion_protection', true) IS NULL)) + EXECUTE PROCEDURE raise_deletion_protection(); + +ALTER TABLE team + DROP CONSTRAINT team_photo_id_fkey, + ADD FOREIGN KEY (photo_id) REFERENCES storage_object(id) DEFERRABLE; diff --git a/backend/src/app/migrations/sql/0115-mod-project-table.sql b/backend/src/app/migrations/sql/0115-mod-project-table.sql new file mode 100644 index 0000000000..f37470dce8 --- /dev/null +++ b/backend/src/app/migrations/sql/0115-mod-project-table.sql @@ -0,0 +1,3 @@ +ALTER TABLE project + DROP CONSTRAINT project_team_id_fkey, + ADD FOREIGN KEY (team_id) REFERENCES team(id) DEFERRABLE; diff --git a/backend/src/app/migrations/sql/0116-mod-file-table.sql b/backend/src/app/migrations/sql/0116-mod-file-table.sql new file mode 100644 index 0000000000..1d3bce11a2 --- /dev/null +++ b/backend/src/app/migrations/sql/0116-mod-file-table.sql @@ -0,0 +1,3 @@ +ALTER TABLE file + DROP CONSTRAINT file_project_id_fkey, + ADD FOREIGN KEY (project_id) REFERENCES project(id) DEFERRABLE; diff --git a/backend/src/app/migrations/sql/0117-mod-file-object-thumbnail-table.sql b/backend/src/app/migrations/sql/0117-mod-file-object-thumbnail-table.sql new file mode 100644 index 0000000000..e3f6cb6d4b --- /dev/null +++ b/backend/src/app/migrations/sql/0117-mod-file-object-thumbnail-table.sql @@ -0,0 +1,12 @@ +ALTER TABLE file_object_thumbnail + DROP CONSTRAINT file_object_thumbnail_file_id_fkey, + ADD FOREIGN KEY (file_id) REFERENCES file(id) DEFERRABLE, + DROP CONSTRAINT file_object_thumbnail_media_id_fkey, + ADD FOREIGN KEY (media_id) REFERENCES storage_object(id) DEFERRABLE; + +--- Mark all related storage_object row as touched +-- UPDATE storage_object SET touched_at = now() +-- WHERE id IN (SELECT DISTINCT media_id +-- FROM file_object_thumbnail +-- WHERE media_id IS NOT NULL) +-- AND touched_at IS NULL; diff --git a/backend/src/app/migrations/sql/0118-mod-task-table.sql b/backend/src/app/migrations/sql/0118-mod-task-table.sql new file mode 100644 index 0000000000..d6ede0e971 --- /dev/null +++ b/backend/src/app/migrations/sql/0118-mod-task-table.sql @@ -0,0 +1,12 @@ +-- Removes the partitioning. +CREATE TABLE new_task (LIKE task INCLUDING ALL); +INSERT INTO new_task SELECT * FROM task; +ALTER TABLE task RENAME TO old_task; +ALTER TABLE new_task RENAME TO task; +DROP TABLE old_task; +ALTER INDEX new_task_label_name_queue_idx RENAME TO task__label_name_queue__idx; +ALTER INDEX new_task_scheduled_at_queue_idx RENAME TO task__scheduled_at_queue__idx; +ALTER TABLE task DROP CONSTRAINT new_task_pkey; +ALTER TABLE task ADD PRIMARY KEY (id); +ALTER TABLE task ALTER COLUMN created_at SET DEFAULT now(); +ALTER TABLE task ALTER COLUMN modified_at SET DEFAULT now(); diff --git a/backend/src/app/migrations/sql/0119-mod-file-table.sql b/backend/src/app/migrations/sql/0119-mod-file-table.sql new file mode 100644 index 0000000000..28a296a91f --- /dev/null +++ b/backend/src/app/migrations/sql/0119-mod-file-table.sql @@ -0,0 +1,2 @@ +ALTER TABLE file + ADD COLUMN version integer NULL; diff --git a/backend/src/app/migrations/sql/0120-mod-audit-log-table.sql b/backend/src/app/migrations/sql/0120-mod-audit-log-table.sql new file mode 100644 index 0000000000..e9b4b83c51 --- /dev/null +++ b/backend/src/app/migrations/sql/0120-mod-audit-log-table.sql @@ -0,0 +1,11 @@ +CREATE TABLE new_audit_log (LIKE audit_log INCLUDING ALL); +INSERT INTO new_audit_log SELECT * FROM audit_log; +ALTER TABLE audit_log RENAME TO old_audit_log; +ALTER TABLE new_audit_log RENAME TO audit_log; +DROP TABLE old_audit_log; + +DROP INDEX new_audit_log_id_archived_at_idx; +ALTER TABLE audit_log DROP CONSTRAINT new_audit_log_pkey; +ALTER TABLE audit_log ADD PRIMARY KEY (id); +ALTER TABLE audit_log ALTER COLUMN created_at SET DEFAULT now(); +ALTER TABLE audit_log ALTER COLUMN tracked_at SET DEFAULT now(); diff --git a/backend/src/app/redis.clj b/backend/src/app/redis.clj index b730ab1063..58023fe00e 100644 --- a/backend/src/app/redis.clj +++ b/backend/src/app/redis.clj @@ -91,7 +91,7 @@ (s/def ::connect? ::us/boolean) (s/def ::io-threads ::us/integer) (s/def ::worker-threads ::us/integer) -(s/def ::cache some?) +(s/def ::cache cache/cache?) (s/def ::redis (s/keys :req [::resources @@ -168,7 +168,7 @@ (defn- shutdown-resources [{:keys [::resources ::cache ::timer]}] - (cache/invalidate-all! cache) + (cache/invalidate! cache) (when resources (.shutdown ^ClientResources resources)) @@ -211,7 +211,8 @@ (defn get-or-connect [{:keys [::cache] :as state} key options] (us/assert! ::redis state) - (let [connection (cache/get cache key (fn [_] (connect* state options)))] + (let [create (fn [_] (connect* state options)) + connection (cache/get cache key create)] (-> state (dissoc ::cache) (assoc ::connection connection)))) diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 63077eec78..89eee548d4 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -27,15 +27,16 @@ [app.rpc.helpers :as rph] [app.rpc.retry :as retry] [app.rpc.rlimit :as rlimit] + [app.setup :as-alias setup] [app.storage :as-alias sto] [app.util.services :as sv] [app.util.time :as dt] - [app.worker :as-alias wrk] [clojure.spec.alpha :as s] + [cuerdas.core :as str] [integrant.core :as ig] [promesa.core :as p] - [yetti.request :as yrq] - [yetti.response :as yrs])) + [ring.request :as rreq] + [ring.response :as rres])) (s/def ::profile-id ::us/uuid) @@ -58,36 +59,45 @@ (defn- handle-response [request result] - (if (fn? result) - (result request) - (let [mdata (meta result)] - (-> {::yrs/status (::http/status mdata 200) - ::yrs/headers (::http/headers mdata {}) - ::yrs/body (rph/unwrap result)} - (handle-response-transformation request mdata) - (handle-before-comple-hook mdata))))) + (let [mdata (meta result) + response (if (fn? result) + (result request) + (let [result (rph/unwrap result)] + {::rres/status (::http/status mdata 200) + ::rres/headers (::http/headers mdata {}) + ::rres/body result}))] + (-> response + (handle-response-transformation request mdata) + (handle-before-comple-hook mdata)))) (defn- rpc-handler "Ring handler that dispatches cmd requests and convert between internal async flow into ring async flow." - [methods {:keys [params path-params] :as request}] - (let [type (keyword (:type path-params)) - etag (yrq/get-header request "if-none-match") - profile-id (or (::session/profile-id request) - (::actoken/profile-id request)) + [methods {:keys [params path-params method] :as request}] + (let [handler-name (:type path-params) + etag (rreq/get-header request "if-none-match") + profile-id (or (::session/profile-id request) + (::actoken/profile-id request)) - data (-> params - (assoc ::request-at (dt/now)) - (assoc ::session/id (::session/id request)) - (assoc ::cond/key etag) - (cond-> (uuid? profile-id) - (assoc ::profile-id profile-id))) + data (-> params + (assoc ::request-at (dt/now)) + (assoc ::session/id (::session/id request)) + (assoc ::cond/key etag) + (cond-> (uuid? profile-id) + (assoc ::profile-id profile-id))) - data (vary-meta data assoc ::http/request request) - method (get methods type default-handler)] + data (vary-meta data assoc ::http/request request) + handler-fn (get methods (keyword handler-name) default-handler)] + + (when (and (or (= method :get) + (= method :head)) + (not (str/starts-with? handler-name "get-"))) + (ex/raise :type :restriction + :code :method-not-allowed + :hint "method not allowed for this request")) (binding [cond/*enabled* true] - (let [response (method data)] + (let [response (handler-fn data)] (handle-response request response))))) (defn- wrap-metrics @@ -141,31 +151,33 @@ (defn- wrap-params-validation [_ f mdata] (if-let [schema (::sm/params mdata)] - (let [schema (sm/schema schema) - valid? (sm/validator schema) - explain (sm/explainer schema) - decode (sm/decoder schema sm/default-transformer)] - + (let [validate (sm/validator schema) + explain (sm/explainer schema) + decode (sm/decoder schema)] (fn [cfg params] (let [params (decode params)] - (if (valid? params) + (if (validate params) (f cfg params) - (ex/raise :type :validation - :code :params-validation - ::sm/explain (explain params)))))) + + (let [params (d/without-qualified params)] + (ex/raise :type :validation + :code :params-validation + ::sm/explain (explain params))))))) f)) (defn- wrap-output-validation [_ f mdata] (if (contains? cf/flags :rpc-output-validation) (or (when-let [schema (::sm/result mdata)] - (let [schema (sm/schema schema) - valid? (sm/validator schema) - explain (sm/explainer schema)] + (let [schema (if (sm/lazy-schema? schema) + schema + (sm/define schema)) + validate (sm/validator schema) + explain (sm/explainer schema)] (fn [cfg params] (let [response (f cfg params)] (when (map? response) - (when-not (valid? response) + (when-not (validate response) (ex/raise :type :validation :code :data-validation ::sm/explain (explain response)))) @@ -189,7 +201,7 @@ (defn- wrap [cfg f mdata] - (l/debug :hint "register method" :name (::sv/name mdata)) + (l/trc :hint "register method" :name (::sv/name mdata)) (let [f (wrap-all cfg f mdata)] (partial f cfg))) @@ -214,6 +226,7 @@ 'app.rpc.commands.files-share 'app.rpc.commands.files-temp 'app.rpc.commands.files-update + 'app.rpc.commands.files-snapshot 'app.rpc.commands.files-thumbnails 'app.rpc.commands.ldap 'app.rpc.commands.management @@ -236,11 +249,9 @@ ::ldap/provider ::sto/storage ::mtx/metrics - ::main/props - ::wrk/executor] + ::setup/props] :opt [::climit - ::rlimit] - :req-un [::db/pool])) + ::rlimit])) (defmethod ig/init-key ::methods [_ cfg] @@ -255,8 +266,7 @@ (defmethod ig/pre-init-spec ::routes [_] (s/keys :req [::methods ::db/pool - ::main/props - ::wrk/executor + ::setup/props ::session/manager])) (defmethod ig/init-key ::routes diff --git a/backend/src/app/rpc/climit.clj b/backend/src/app/rpc/climit.clj index 060ab00154..7dbec08619 100644 --- a/backend/src/app/rpc/climit.clj +++ b/backend/src/app/rpc/climit.clj @@ -23,32 +23,31 @@ [clojure.spec.alpha :as s] [datoteka.fs :as fs] [integrant.core :as ig] - [promesa.core :as p] [promesa.exec :as px] [promesa.exec.bulkhead :as pbh]) (:import - clojure.lang.ExceptionInfo)) + clojure.lang.ExceptionInfo + java.util.concurrent.atomic.AtomicLong)) (set! *warn-on-reflection* true) -(defn- create-bulkhead-cache - [{:keys [::wrk/executor]} config] - (letfn [(load-fn [key] - (let [config (get config (nth key 0))] - (l/trace :hint "insert into cache" :key key) - (pbh/create :permits (or (:permits config) (:concurrency config)) - :queue (or (:queue config) (:queue-size config)) - :timeout (:timeout config) - :executor executor - :type (:type config :semaphore)))) +(defn- id->str + ([id] + (-> (str id) + (subs 1))) + ([id key] + (if key + (str (-> (str id) (subs 1)) "/" key) + (id->str id)))) - (on-remove [_ _ cause] - (l/trace :hint "evict from cache" :key key :reason (str cause)))] - - (cache/create :executor :same-thread +(defn- create-cache + [{:keys [::wrk/executor]}] + (letfn [(on-remove [key _ cause] + (let [[id skey] key] + (l/trc :hint "disposed" :id (id->str id skey) :reason (str cause))))] + (cache/create :executor executor :on-remove on-remove - :keepalive "5m" - :load-fn load-fn))) + :keepalive "5m"))) (s/def ::config/permits ::us/integer) (s/def ::config/queue ::us/integer) @@ -65,160 +64,200 @@ (s/def ::path ::fs/path) (defmethod ig/pre-init-spec ::rpc/climit [_] - (s/keys :req [::wrk/executor ::mtx/metrics ::path])) + (s/keys :req [::mtx/metrics ::wrk/executor ::path])) (defmethod ig/init-key ::rpc/climit - [_ {:keys [::path ::mtx/metrics ::wrk/executor] :as cfg}] + [_ {:keys [::path ::mtx/metrics] :as cfg}] (when (contains? cf/flags :rpc-climit) (when-let [params (some->> path slurp edn/read-string)] - (l/info :hint "initializing concurrency limit" :config (str path)) + (l/inf :hint "initializing concurrency limit" :config (str path)) (us/verify! ::config params) - {::cache (create-bulkhead-cache cfg params) + {::cache (create-cache cfg) ::config params - ::wrk/executor executor ::mtx/metrics metrics}))) (s/def ::cache cache/cache?) (s/def ::instance - (s/keys :req [::cache ::config ::wrk/executor])) + (s/keys :req [::cache ::config])) (s/def ::rpc/climit (s/nilable ::instance)) +(defn- create-limiter + [config [id skey]] + (l/trc :hint "created" :id (id->str id skey)) + (pbh/create :permits (or (:permits config) (:concurrency config)) + :queue (or (:queue config) (:queue-size config)) + :timeout (:timeout config) + :type :semaphore)) + +(defmacro ^:private measure-and-log! + [metrics mlabels stats id action limit-id limit-label profile-id elapsed] + `(let [mpermits# (:max-permits ~stats) + mqueue# (:max-queue ~stats) + permits# (:permits ~stats) + queue# (:queue ~stats) + queue# (- queue# mpermits#) + queue# (if (neg? queue#) 0 queue#) + level# (if (pos? queue#) :warn :trace)] + + (mtx/run! ~metrics + :id :rpc-climit-queue + :val queue# + :labels ~mlabels) + + (mtx/run! ~metrics + :id :rpc-climit-permits + :val permits# + :labels ~mlabels) + + (l/log level# + :hint ~action + :req ~id + :id ~limit-id + :label ~limit-label + :profile-id (str ~profile-id) + :permits permits# + :queue queue# + :max-permits mpermits# + :max-queue mqueue# + ~@(if (some? elapsed) + [:elapsed `(dt/format-duration ~elapsed)] + [])))) + +(def ^:private idseq (AtomicLong. 0)) + +(defn- invoke + [limiter metrics limit-id limit-key limit-label profile-id f params] + (let [tpoint (dt/tpoint) + mlabels (into-array String [(id->str limit-id)]) + limit-id (id->str limit-id limit-key) + stats (pbh/get-stats limiter) + id (.incrementAndGet ^AtomicLong idseq)] + + (try + (measure-and-log! metrics mlabels stats id "enqueued" limit-id limit-label profile-id nil) + (px/invoke! limiter (fn [] + (let [elapsed (tpoint) + stats (pbh/get-stats limiter)] + (measure-and-log! metrics mlabels stats id "acquired" limit-id limit-label profile-id elapsed) + (mtx/run! metrics + :id :rpc-climit-timing + :val (inst-ms elapsed) + :labels mlabels) + (apply f params)))) + + (catch ExceptionInfo cause + (let [{:keys [type code]} (ex-data cause)] + (if (= :bulkhead-error type) + (let [elapsed (tpoint)] + (measure-and-log! metrics mlabels stats id "reject" limit-id limit-label profile-id elapsed) + (ex/raise :type :concurrency-limit + :code code + :hint "concurrency limit reached" + :cause cause)) + (throw cause)))) + + (finally + (let [elapsed (tpoint) + stats (pbh/get-stats limiter)] + (measure-and-log! metrics mlabels stats id "finished" limit-id limit-label profile-id elapsed)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; MIDDLEWARE +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:private noop-fn (constantly nil)) +(def ^:private global-limits + [[:root/global noop-fn] + [:root/by-profile ::rpc/profile-id]]) + +(defn- get-limits + [cfg] + (when-let [ref (get cfg ::id)] + (cond + (keyword? ref) + [[ref]] + + (and (vector? ref) + (keyword (first ref))) + [ref] + + (and (vector? ref) + (vector? (first ref))) + (rseq ref) + + :else + (throw (IllegalArgumentException. "unable to normalize limit"))))) + +(defn wrap + [{:keys [::rpc/climit ::mtx/metrics]} handler mdata] + (let [cache (::cache climit) + config (::config climit) + label (::sv/name mdata)] + + (if climit + (reduce (fn [handler [limit-id key-fn]] + (if-let [config (get config limit-id)] + (let [key-fn (or key-fn noop-fn)] + (l/trc :hint "instrumenting method" + :method label + :limit (id->str limit-id) + :timeout (:timeout config) + :permits (:permits config) + :queue (:queue config) + :keyed (not= key-fn noop-fn)) + + (if (and (= key-fn ::rpc/profile-id) + (false? (::rpc/auth mdata true))) + + ;; We don't enforce by-profile limit on methods that does + ;; not require authentication + handler + + (fn [cfg params] + (let [limit-key (key-fn params) + cache-key [limit-id limit-key] + limiter (cache/get cache cache-key (partial create-limiter config)) + profile-id (if (= key-fn ::rpc/profile-id) + limit-key + (get params ::rpc/profile-id))] + (invoke limiter metrics limit-id limit-key label profile-id handler [cfg params]))))) + + (do + (l/wrn :hint "no config found for specified queue" :id (id->str limit-id)) + handler))) + + handler + (concat global-limits (get-limits mdata))) + handler))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PUBLIC API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn- build-exec-chain + [{:keys [::label ::profile-id ::rpc/climit ::mtx/metrics] :as cfg} f] + (let [config (get climit ::config) + cache (get climit ::cache)] + (reduce (fn [handler [limit-id limit-key :as ckey]] + (if-let [config (get config limit-id)] + (fn [& params] + (let [limiter (cache/get cache ckey (partial create-limiter config))] + (invoke limiter metrics limit-id limit-key label profile-id handler params))) + + (do + (l/wrn :hint "config not found" :label label :id limit-id) + f))) + f + (get-limits cfg)))) + (defn invoke! - [cache metrics id key f] - (let [limiter (cache/get cache [id key]) - tpoint (dt/tpoint) - labels (into-array String [(name id)]) - - wrapped - (fn [] - (let [elapsed (tpoint) - stats (pbh/get-stats limiter)] - (l/trace :hint "executed" - :id (name id) - :key key - :fnh (hash f) - :permits (:permits stats) - :queue (:queue stats) - :max-permits (:max-permits stats) - :max-queue (:max-queue stats) - :elapsed (dt/format-duration elapsed)) - (mtx/run! metrics - :id :rpc-climit-timing - :val (inst-ms elapsed) - :labels labels) - (try - (f) - (finally - (let [elapsed (tpoint)] - (l/trace :hint "finished" - :id (name id) - :key key - :fnh (hash f) - :permits (:permits stats) - :queue (:queue stats) - :max-permits (:max-permits stats) - :max-queue (:max-queue stats) - :elapsed (dt/format-duration elapsed))))))) - measure! - (fn [stats] - (mtx/run! metrics - :id :rpc-climit-queue - :val (:queue stats) - :labels labels) - (mtx/run! metrics - :id :rpc-climit-permits - :val (:permits stats) - :labels labels))] - - (try - (let [stats (pbh/get-stats limiter)] - (measure! stats) - (l/trace :hint "enqueued" - :id (name id) - :key key - :fnh (hash f) - :permits (:permits stats) - :queue (:queue stats) - :max-permits (:max-permits stats) - :max-queue (:max-queue stats)) - (pbh/invoke! limiter wrapped)) - (catch ExceptionInfo cause - (let [{:keys [type code]} (ex-data cause)] - (if (= :bulkhead-error type) - (ex/raise :type :concurrency-limit - :code code - :hint "concurrency limit reached") - (throw cause)))) - - (finally - (measure! (pbh/get-stats limiter)))))) - - -(defn run! - [{:keys [::id ::cache ::mtx/metrics]} f] - (if (and cache id) - (invoke! cache metrics id nil f) - (f))) - -(defn submit! - [{:keys [::id ::cache ::wrk/executor ::mtx/metrics]} f] - (let [f (partial px/submit! executor (px/wrap-bindings f))] - (if (and cache id) - (p/await! (invoke! cache metrics id nil f)) - (p/await! (f))))) - -(defn configure - ([{:keys [::rpc/climit]} id] - (us/assert! ::rpc/climit climit) - (assoc climit ::id id)) - ([{:keys [::rpc/climit]} id executor] - (us/assert! ::rpc/climit climit) - (-> climit - (assoc ::id id) - (assoc ::wrk/executor executor)))) - -(defmacro with-dispatch! - "Dispatch blocking operation to a separated thread protected with the - specified concurrency limiter. If climit is not active, the function - will be scheduled to execute without concurrency monitoring." - [instance & body] - (if (vector? instance) - `(-> (app.rpc.climit/configure ~@instance) - (app.rpc.climit/run! (^:once fn* [] ~@body))) - `(run! ~instance (^:once fn* [] ~@body)))) - -(defmacro with-dispatch - "Dispatch blocking operation to a separated thread protected with - the specified semaphore. - DEPRECATED" - [& params] - `(with-dispatch! ~@params)) - -(def noop-fn (constantly nil)) - -(defn wrap - [{:keys [::rpc/climit ::mtx/metrics]} f {:keys [::id ::key-fn] :or {key-fn noop-fn} :as mdata}] - (if (and (some? climit) (some? id)) - (if-let [config (get-in climit [::config id])] - (let [cache (::cache climit)] - (l/debug :hint "wrap: instrumenting method" - :limit (name id) - :service-name (::sv/name mdata) - :timeout (:timeout config) - :permits (:permits config) - :queue (:queue config) - :keyed? (some? key-fn)) - (fn [cfg params] - (invoke! cache metrics id (key-fn params) (partial f cfg params)))) - - (do - (l/warn :hint "no config found for specified queue" :id id) - f)) - - f)) + "Run a function in context of climit. + Intended to be used in virtual threads." + [{:keys [::executor] :as cfg} f & params] + (let [f (if (some? executor) + (fn [& params] (px/await! (px/submit! executor (fn [] (apply f params))))) + f) + f (build-exec-chain cfg f)] + (apply f params))) diff --git a/backend/src/app/rpc/commands/access_token.clj b/backend/src/app/rpc/commands/access_token.clj index dd10f33719..06a6e516ce 100644 --- a/backend/src/app/rpc/commands/access_token.clj +++ b/backend/src/app/rpc/commands/access_token.clj @@ -13,6 +13,7 @@ [app.rpc :as-alias rpc] [app.rpc.doc :as-alias doc] [app.rpc.quotes :as quotes] + [app.setup :as-alias setup] [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] @@ -23,7 +24,7 @@ (dissoc row :perms)) (defn create-access-token - [{:keys [::db/conn ::main/props]} profile-id name expiration] + [{:keys [::db/conn ::setup/props]} profile-id name expiration] (let [created-at (dt/now) token-id (uuid/next) token (tokens/generate props {:iss "access-token" @@ -47,7 +48,7 @@ [{:keys [::db/pool] :as system} profile-id name expiration] (db/with-atomic [conn pool] (let [props (:app.setup/props system)] - (create-access-token {::db/conn conn ::main/props props} + (create-access-token {::db/conn conn ::setup/props props} profile-id name expiration)))) diff --git a/backend/src/app/rpc/commands/audit.clj b/backend/src/app/rpc/commands/audit.clj index 8049595c91..5db758b464 100644 --- a/backend/src/app/rpc/commands/audit.clj +++ b/backend/src/app/rpc/commands/audit.clj @@ -19,7 +19,20 @@ [app.rpc.climit :as-alias climit] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.util.services :as sv])) + [app.util.services :as sv] + [app.util.time :as dt])) + +(def ^:private event-columns + [:id + :name + :source + :type + :tracked-at + :created-at + :profile-id + :ip-addr + :props + :context]) (defn- event->row [event] [(uuid/next) @@ -27,28 +40,42 @@ (:source event) (:type event) (:timestamp event) + (:created-at event) (:profile-id event) (db/inet (:ip-addr event)) (db/tjson (:props event)) (db/tjson (d/without-nils (:context event)))]) -(def ^:private event-columns - [:id :name :source :type :tracked-at - :profile-id :ip-addr :props :context]) +(defn- adjust-timestamp + [{:keys [timestamp created-at] :as event}] + (let [margin (inst-ms (dt/diff timestamp created-at))] + (if (or (neg? margin) + (> margin 3600000)) + ;; If event is in future or lags more than 1 hour, we reasign + ;; timestamp to the server creation date + (-> event + (assoc :timestamp created-at) + (update :context assoc :original-timestamp timestamp)) + event))) (defn- handle-events [{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}] (let [request (-> params meta ::http/request) ip-addr (audit/parse-client-ip request) + tnow (dt/now) xform (comp - (map #(assoc % :profile-id profile-id)) - (map #(assoc % :ip-addr ip-addr)) - (map #(assoc % :source "frontend")) + (map (fn [event] + (-> event + (assoc :created-at tnow) + (assoc :profile-id profile-id) + (assoc :ip-addr ip-addr) + (assoc :source "frontend")))) (filter :profile-id) + (map adjust-timestamp) (map event->row)) events (sequence xform events)] (when (seq events) - (db/insert-multi! pool :audit-log event-columns events)))) + (db/insert-many! pool :audit-log event-columns events)))) (def schema:event [:map {:title "Event"} @@ -64,7 +91,7 @@ [:events [:vector schema:event]]]) (sv/defmethod ::push-audit-events - {::climit/id :submit-audit-events-by-profile + {::climit/id :submit-audit-events/by-profile ::climit/key-fn ::rpc/profile-id ::sm/params schema:push-audit-events ::audit/skip true diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index fd0e206004..e87979007f 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -10,6 +10,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.features :as cfeat] [app.common.logging :as l] [app.common.schema :as sm] [app.common.uuid :as uuid] @@ -20,10 +21,12 @@ [app.loggers.audit :as audit] [app.main :as-alias main] [app.rpc :as-alias rpc] + [app.rpc.climit :as-alias climit] [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] + [app.setup :as-alias setup] [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] @@ -38,7 +41,7 @@ ;; ---- COMMAND: login with password (defn login-with-password - [{:keys [::db/pool] :as cfg} {:keys [email password] :as params}] + [cfg {:keys [email password] :as params}] (when-not (or (contains? cf/flags :login) (contains? cf/flags :login-with-password)) @@ -46,18 +49,20 @@ :code :login-disabled :hint "login is disabled in this instance")) - (letfn [(check-password [conn profile password] + (letfn [(check-password [cfg profile password] (if (= (:password profile) "!") (ex/raise :type :validation :code :account-without-password :hint "the current account does not have password") (let [result (profile/verify-password cfg password (:password profile))] (when (:update result) - (l/trace :hint "updating profile password" :id (:id profile) :email (:email profile)) - (profile/update-profile-password! conn (assoc profile :password password))) + (l/trc :hint "updating profile password" + :id (str (:id profile)) + :email (:email profile)) + (profile/update-profile-password! cfg (assoc profile :password password))) (:valid result)))) - (validate-profile [conn profile] + (validate-profile [cfg profile] (when-not profile (ex/raise :type :validation :code :wrong-credentials)) @@ -67,7 +72,7 @@ (when (:is-blocked profile) (ex/raise :type :restriction :code :profile-blocked)) - (when-not (check-password conn profile password) + (when-not (check-password cfg profile password) (ex/raise :type :validation :code :wrong-credentials)) (when-let [deleted-at (:deleted-at profile)] @@ -75,27 +80,30 @@ (ex/raise :type :validation :code :wrong-credentials))) - profile)] + profile) - (db/with-atomic [conn pool] - (let [profile (->> (profile/get-profile-by-email conn email) - (validate-profile conn) - (profile/strip-private-attrs)) + (login [{:keys [::db/conn] :as cfg}] + (let [profile (->> (profile/clean-email email) + (profile/get-profile-by-email conn) + (validate-profile cfg) + (profile/strip-private-attrs)) - invitation (when-let [token (:invitation-token params)] - (tokens/verify (::main/props cfg) {:token token :iss :team-invitation})) + invitation (when-let [token (:invitation-token params)] + (tokens/verify (::setup/props cfg) {:token token :iss :team-invitation})) - ;; If invitation member-id does not matches the profile-id, we just proceed to ignore the - ;; invitation because invitations matches exactly; and user can't login with other email and - ;; accept invitation with other email - response (if (and (some? invitation) (= (:id profile) (:member-id invitation))) - {:invitation-token (:invitation-token params)} - (assoc profile :is-admin (let [admins (cf/get :admins)] - (contains? admins (:email profile)))))] - (-> response - (rph/with-transform (session/create-fn cfg (:id profile))) - (rph/with-meta {::audit/props (audit/profile->props profile) - ::audit/profile-id (:id profile)})))))) + ;; If invitation member-id does not matches the profile-id, we just proceed to ignore the + ;; invitation because invitations matches exactly; and user can't login with other email and + ;; accept invitation with other email + response (if (and (some? invitation) (= (:id profile) (:member-id invitation))) + {:invitation-token (:invitation-token params)} + (assoc profile :is-admin (let [admins (cf/get :admins)] + (contains? admins (:email profile)))))] + (-> response + (rph/with-transform (session/create-fn cfg (:id profile))) + (rph/with-meta {::audit/props (audit/profile->props profile) + ::audit/profile-id (:id profile)}))))] + + (db/tx-run! cfg login))) (def schema:login-with-password [:map {:title "login-with-password"} @@ -107,6 +115,7 @@ "Performs authentication using penpot password." {::rpc/auth false ::doc/added "1.15" + ::climit/id :auth/global ::sm/params schema:login-with-password} [cfg params] (login-with-password cfg params)) @@ -125,12 +134,13 @@ (defn recover-profile [{:keys [::db/pool] :as cfg} {:keys [token password]}] (letfn [(validate-token [token] - (let [tdata (tokens/verify (::main/props cfg) {:token token :iss :password-recovery})] + (let [tdata (tokens/verify (::setup/props cfg) {:token token :iss :password-recovery})] (:profile-id tdata))) (update-password [conn profile-id] (let [pwd (profile/derive-password cfg password)] - (db/update! conn :profile {:password pwd} {:id profile-id})))] + (db/update! conn :profile {:password pwd} {:id profile-id}) + nil))] (db/with-atomic [conn pool] (->> (validate-token token) @@ -145,7 +155,8 @@ (sv/defmethod ::recover-profile {::rpc/auth false ::doc/added "1.15" - ::sm/params schema:recover-profile} + ::sm/params schema:recover-profile + ::climit/id :auth/global} [cfg params] (recover-profile cfg params)) @@ -160,7 +171,7 @@ :code :registration-disabled))) (when (contains? params :invitation-token) - (let [invitation (tokens/verify (::main/props cfg) {:token (:invitation-token params) :iss :team-invitation})] + (let [invitation (tokens/verify (::setup/props cfg) {:token (:invitation-token params) :iss :team-invitation})] (when-not (= (:email params) (:member-email invitation)) (ex/raise :type :restriction :code :email-does-not-match-invitation @@ -193,11 +204,12 @@ (pos? (compare elapsed register-retry-threshold)))) (defn prepare-register - [{:keys [::db/pool] :as cfg} params] + [{:keys [::db/pool] :as cfg} {:keys [email] :as params}] (validate-register-attempt! cfg params) - (let [profile (when-let [profile (profile/get-profile-by-email pool (:email params))] + (let [email (profile/clean-email email) + profile (when-let [profile (profile/get-profile-by-email pool email)] (cond (:is-blocked profile) (ex/raise :type :restriction @@ -212,7 +224,7 @@ :code :email-already-exists :hint "profile already exists"))) - params {:email (:email params) + params {:email email :password (:password params) :invitation-token (:invitation-token params) :backend "penpot" @@ -222,7 +234,7 @@ params (d/without-nils params) - token (tokens/generate (::main/props cfg) params)] + token (tokens/generate (::setup/props cfg) params)] (with-meta {:token token} {::audit/profile-id uuid/zero}))) @@ -251,7 +263,8 @@ (merge (:props params)) (merge {:viewed-tutorial? false :viewed-walkthrough? false - :nudge {:big 10 :small 1}}) + :nudge {:big 10 :small 1} + :v2-info-shown true}) (db/tjson)) password (or (:password params) "!") @@ -291,13 +304,17 @@ (defn create-profile-rels! [conn {:keys [id] :as profile}] - (let [team (teams/create-team conn {:profile-id id - :name "Default" - :is-default true})] + (let [features (cfeat/get-enabled-features cf/flags) + team (teams/create-team conn + {:profile-id id + :name "Default" + :features features + :is-default true})] (-> (db/update! conn :profile {:default-team-id (:id team) :default-project-id (:default-project-id team)} - {:id id}) + {:id id} + {::db/return-keys true}) (profile/decode-row)))) @@ -324,7 +341,7 @@ (defn register-profile [{:keys [::db/conn] :as cfg} {:keys [token fullname] :as params}] - (let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register}) + (let [claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register}) params (-> claims (into params) (assoc :fullname fullname)) @@ -341,7 +358,7 @@ (create-profile-rels! conn)))) invitation (when-let [token (:invitation-token params)] - (tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))] + (tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))] ;; If profile is filled in claims, means it tries to register ;; again, so we proceed to update the modified-at attr @@ -352,7 +369,6 @@ {::audit/type "fact" ::audit/name "register-profile-retry" ::audit/profile-id id})) - (cond ;; If invitation token comes in params, this is because the ;; user comes from team-invitation process; in this case, @@ -362,7 +378,7 @@ ;; email. (and (some? invitation) (= (:email profile) (:member-email invitation))) (let [claims (assoc invitation :member-id (:id profile)) - token (tokens/generate (::main/props cfg) claims) + token (tokens/generate (::setup/props cfg) claims) resp {:invitation-token token}] (-> resp (rph/with-transform (session/create-fn cfg (:id profile))) @@ -389,12 +405,11 @@ ;; In all other cases, send a verification email. :else (do - (send-email-verification! conn (::main/props cfg) profile) + (send-email-verification! conn (::setup/props cfg) profile) (rph/with-meta profile {::audit/replace-props (audit/profile->props profile) ::audit/profile-id (:id profile)}))))) - (def schema:register-profile [:map {:title "register-profile"} [:token schema:token] @@ -403,7 +418,8 @@ (sv/defmethod ::register-profile {::rpc/auth false ::doc/added "1.15" - ::sm/params schema:register-profile} + ::sm/params schema:register-profile + ::climit/id :auth/global} [{:keys [::db/pool] :as cfg} params] (db/with-atomic [conn pool] (-> (assoc cfg ::db/conn conn) @@ -414,14 +430,14 @@ (defn request-profile-recovery [{:keys [::db/pool] :as cfg} {:keys [email] :as params}] (letfn [(create-recovery-token [{:keys [id] :as profile}] - (let [token (tokens/generate (::main/props cfg) + (let [token (tokens/generate (::setup/props cfg) {:iss :password-recovery :exp (dt/in-future "15m") :profile-id id})] (assoc profile :token token))) (send-email-notification [conn profile] - (let [ptoken (tokens/generate (::main/props cfg) + (let [ptoken (tokens/generate (::setup/props cfg) {:iss :profile-identity :profile-id (:id profile) :exp (dt/in-future {:days 30})})] @@ -435,7 +451,8 @@ nil))] (db/with-atomic [conn pool] - (when-let [profile (profile/get-profile-by-email conn email)] + (when-let [profile (->> (profile/clean-email email) + (profile/get-profile-by-email conn))] (when-not (eml/allow-send-emails? conn profile) (ex/raise :type :validation :code :profile-is-muted diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 140d637f6b..8f2216e636 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -7,17 +7,11 @@ (ns app.rpc.commands.binfile (:refer-clojure :exclude [assert]) (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.exceptions :as ex] - [app.common.files.features :as ffeat] - [app.common.fressian :as fres] + [app.binfile.v1 :as bf.v1] [app.common.logging :as l] - [app.common.pages.migrations :as pmg] - [app.common.spec :as us] - [app.common.uuid :as uuid] - [app.config :as cf] + [app.common.schema :as sm] [app.db :as db] + [app.http.sse :as sse] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] [app.media :as media] @@ -25,944 +19,85 @@ [app.rpc.commands.files :as files] [app.rpc.commands.projects :as projects] [app.rpc.doc :as-alias doc] - [app.rpc.helpers :as rph] - [app.storage :as sto] - [app.storage.tmp :as tmp] [app.tasks.file-gc] - [app.util.blob :as blob] - [app.util.objects-map :as omap] - [app.util.pointer-map :as pmap] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s] - [clojure.walk :as walk] - [cuerdas.core :as str] - [datoteka.io :as io] - [yetti.adapter :as yt] - [yetti.response :as yrs]) - (:import - com.github.luben.zstd.ZstdInputStream - com.github.luben.zstd.ZstdOutputStream - java.io.DataInputStream - java.io.DataOutputStream - java.io.InputStream - java.io.OutputStream)) + [app.worker :as-alias wrk] + [promesa.exec :as px] + [ring.response :as rres])) (set! *warn-on-reflection* true) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; DEFAULTS -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; Threshold in MiB when we pass from using -;; in-memory byte-array's to use temporal files. -(def temp-file-threshold - (* 1024 1024 2)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; LOW LEVEL STREAM IO API -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(def ^:const buffer-size (:xnio/buffer-size yt/defaults)) -(def ^:const penpot-magic-number 800099563638710213) -(def ^:const max-object-size (* 1024 1024 100)) ; Only allow 100MiB max file size. - -(def ^:dynamic *position* nil) - -(defn get-mark - [id] - (case id - :header 1 - :stream 2 - :uuid 3 - :label 4 - :obj 5 - (ex/raise :type :validation - :code :invalid-mark-id - :hint (format "invalid mark id %s" id)))) - -(defmacro assert - [expr hint] - `(when-not ~expr - (ex/raise :type :validation - :code :unexpected-condition - :hint ~hint))) - -(defmacro assert-mark - [v type] - `(let [expected# (get-mark ~type) - val# (long ~v)] - (when (not= val# expected#) - (ex/raise :type :validation - :code :unexpected-mark - :hint (format "received mark %s, expected %s" val# expected#))))) - -(defmacro assert-label - [expr label] - `(let [v# ~expr] - (when (not= v# ~label) - (ex/raise :type :assertion - :code :unexpected-label - :hint (format "received label %s, expected %s" v# ~label))))) - -;; --- PRIMITIVE IO - -(defn write-byte! - [^DataOutputStream output data] - (l/trace :fn "write-byte!" :data data :position @*position* ::l/sync? true) - (.writeByte output (byte data)) - (swap! *position* inc)) - -(defn read-byte! - [^DataInputStream input] - (let [v (.readByte input)] - (l/trace :fn "read-byte!" :val v :position @*position* ::l/sync? true) - (swap! *position* inc) - v)) - -(defn write-long! - [^DataOutputStream output data] - (l/trace :fn "write-long!" :data data :position @*position* ::l/sync? true) - (.writeLong output (long data)) - (swap! *position* + 8)) - - -(defn read-long! - [^DataInputStream input] - (let [v (.readLong input)] - (l/trace :fn "read-long!" :val v :position @*position* ::l/sync? true) - (swap! *position* + 8) - v)) - -(defn write-bytes! - [^DataOutputStream output ^bytes data] - (let [size (alength data)] - (l/trace :fn "write-bytes!" :size size :position @*position* ::l/sync? true) - (.write output data 0 size) - (swap! *position* + size))) - -(defn read-bytes! - [^InputStream input ^bytes buff] - (let [size (alength buff) - readed (.readNBytes input buff 0 size)] - (l/trace :fn "read-bytes!" :expected (alength buff) :readed readed :position @*position* ::l/sync? true) - (swap! *position* + readed) - readed)) - -;; --- COMPOSITE IO - -(defn write-uuid! - [^DataOutputStream output id] - (l/trace :fn "write-uuid!" :position @*position* :WRITTEN? (.size output) ::l/sync? true) - - (doto output - (write-byte! (get-mark :uuid)) - (write-long! (uuid/get-word-high id)) - (write-long! (uuid/get-word-low id)))) - -(defn read-uuid! - [^DataInputStream input] - (l/trace :fn "read-uuid!" :position @*position* ::l/sync? true) - (let [m (read-byte! input)] - (assert-mark m :uuid) - (let [a (read-long! input) - b (read-long! input)] - (uuid/custom a b)))) - -(defn write-obj! - [^DataOutputStream output data] - (l/trace :fn "write-obj!" :position @*position* ::l/sync? true) - (let [^bytes data (fres/encode data)] - (doto output - (write-byte! (get-mark :obj)) - (write-long! (alength data)) - (write-bytes! data)))) - -(defn read-obj! - [^DataInputStream input] - (l/trace :fn "read-obj!" :position @*position* ::l/sync? true) - (let [m (read-byte! input)] - (assert-mark m :obj) - (let [size (read-long! input)] - (assert (pos? size) "incorrect header size found on reading header") - (let [buff (byte-array size)] - (read-bytes! input buff) - (fres/decode buff))))) - -(defn write-label! - [^DataOutputStream output label] - (l/trace :fn "write-label!" :label label :position @*position* ::l/sync? true) - (doto output - (write-byte! (get-mark :label)) - (write-obj! label))) - -(defn read-label! - [^DataInputStream input] - (l/trace :fn "read-label!" :position @*position* ::l/sync? true) - (let [m (read-byte! input)] - (assert-mark m :label) - (read-obj! input))) - -(defn write-header! - [^OutputStream output version] - (l/trace :fn "write-header!" - :version version - :position @*position* - ::l/sync? true) - (let [vers (-> version name (subs 1) parse-long) - output (io/data-output-stream output)] - (doto output - (write-byte! (get-mark :header)) - (write-long! penpot-magic-number) - (write-long! vers)))) - -(defn read-header! - [^InputStream input] - (l/trace :fn "read-header!" :position @*position* ::l/sync? true) - (let [input (io/data-input-stream input) - mark (read-byte! input) - mnum (read-long! input) - vers (read-long! input)] - - (when (or (not= mark (get-mark :header)) - (not= mnum penpot-magic-number)) - (ex/raise :type :validation - :code :invalid-penpot-file - :hint "invalid penpot file")) - - (keyword (str "v" vers)))) - -(defn copy-stream! - [^OutputStream output ^InputStream input ^long size] - (let [written (io/copy! input output :size size)] - (l/trace :fn "copy-stream!" :position @*position* :size size :written written ::l/sync? true) - (swap! *position* + written) - written)) - -(defn write-stream! - [^DataOutputStream output stream size] - (l/trace :fn "write-stream!" :position @*position* ::l/sync? true :size size) - (doto output - (write-byte! (get-mark :stream)) - (write-long! size)) - - (copy-stream! output stream size)) - -(defn read-stream! - [^DataInputStream input] - (l/trace :fn "read-stream!" :position @*position* ::l/sync? true) - (let [m (read-byte! input) - s (read-long! input) - p (tmp/tempfile :prefix "penpot.binfile.")] - (assert-mark m :stream) - - (when (> s max-object-size) - (ex/raise :type :validation - :code :max-file-size-reached - :hint (str/ffmt "unable to import storage object with size % bytes" s))) - - (if (> s temp-file-threshold) - (with-open [^OutputStream output (io/output-stream p)] - (let [readed (io/copy! input output :offset 0 :size s)] - (l/trace :fn "read-stream*!" :expected s :readed readed :position @*position* ::l/sync? true) - (swap! *position* + readed) - [s p])) - [s (io/read-as-bytes input :size s)]))) - -(defmacro assert-read-label! - [input expected-label] - `(let [readed# (read-label! ~input) - expected# ~expected-label] - (when (not= readed# expected#) - (ex/raise :type :validation - :code :unexpected-label - :hint (format "unexpected label found: %s, expected: %s" readed# expected#))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; API -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; --- HELPERS - -(defn zstd-input-stream - ^InputStream - [input] - (ZstdInputStream. ^InputStream input)) - -(defn zstd-output-stream - ^OutputStream - [output & {:keys [level] :or {level 0}}] - (ZstdOutputStream. ^OutputStream output (int level))) - -(defn- get-files - [cfg ids] - (letfn [(get-files* [{:keys [::db/conn]}] - (let [sql (str "SELECT id FROM file " - " WHERE id = ANY(?) ") - ids (db/create-array conn "uuid" ids)] - (->> (db/exec! conn [sql ids]) - (into [] (map :id)) - (not-empty))))] - - (db/run! cfg get-files*))) - -(defn- get-file - [cfg file-id] - (letfn [(get-file* [{:keys [::db/conn]}] - (binding [pmap/*load-fn* (partial files/load-pointer conn file-id)] - (some-> (db/get* conn :file {:id file-id} {::db/remove-deleted? false}) - (files/decode-row) - (files/process-pointers deref))))] - - (db/run! cfg get-file*))) - -(defn- get-file-media - [{:keys [::db/pool]} {:keys [data id] :as file}] - (dm/with-open [conn (db/open pool)] - (let [ids (app.tasks.file-gc/collect-used-media data) - ids (db/create-array conn "uuid" ids) - sql (str "SELECT * FROM file_media_object WHERE id = ANY(?)")] - - ;; We assoc the file-id again to the file-media-object row - ;; because there are cases that used objects refer to other - ;; files and we need to ensure in the exportation process that - ;; all ids matches - (->> (db/exec! conn [sql ids]) - (mapv #(assoc % :file-id id)))))) - -(def ^:private storage-object-id-xf - (comp - (mapcat (juxt :media-id :thumbnail-id)) - (filter uuid?))) - -(def ^:private sql:file-libraries - "WITH RECURSIVE libs AS ( - SELECT fl.id - FROM file AS fl - JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id) - WHERE flr.file_id = ANY(?) - UNION - SELECT fl.id - FROM file AS fl - JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id) - JOIN libs AS l ON (flr.file_id = l.id) - ) - SELECT DISTINCT l.id - FROM libs AS l") - -(defn- get-libraries - [{:keys [::db/pool]} ids] - (dm/with-open [conn (db/open pool)] - (let [ids (db/create-array conn "uuid" ids)] - (map :id (db/exec! pool [sql:file-libraries ids]))))) - -(defn- get-library-relations - [cfg ids] - (db/run! cfg (fn [{:keys [::db/conn]}] - (let [ids (db/create-array conn "uuid" ids) - sql (str "SELECT flr.* FROM file_library_rel AS flr " - " WHERE flr.file_id = ANY(?)")] - (db/exec! conn [sql ids]))))) - -(defn- create-or-update-file - [conn params] - (let [sql (str "INSERT INTO file (id, project_id, name, revn, is_shared, data, created_at, modified_at) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?) " - "ON CONFLICT (id) DO UPDATE SET data=?")] - (db/exec-one! conn [sql - (:id params) - (:project-id params) - (:name params) - (:revn params) - (:is-shared params) - (:data params) - (:created-at params) - (:modified-at params) - (:data params)]))) - -;; --- GENERAL PURPOSE DYNAMIC VARS - -(def ^:dynamic *state* nil) -(def ^:dynamic *options* nil) - -;; --- EXPORT WRITER - -(defn- embed-file-assets - [data cfg file-id] - (letfn [(walk-map-form [form state] - (cond - (uuid? (:fill-color-ref-file form)) - (do - (vswap! state conj [(:fill-color-ref-file form) :colors (:fill-color-ref-id form)]) - (assoc form :fill-color-ref-file file-id)) - - (uuid? (:stroke-color-ref-file form)) - (do - (vswap! state conj [(:stroke-color-ref-file form) :colors (:stroke-color-ref-id form)]) - (assoc form :stroke-color-ref-file file-id)) - - (uuid? (:typography-ref-file form)) - (do - (vswap! state conj [(:typography-ref-file form) :typographies (:typography-ref-id form)]) - (assoc form :typography-ref-file file-id)) - - (uuid? (:component-file form)) - (do - (vswap! state conj [(:component-file form) :components (:component-id form)]) - (assoc form :component-file file-id)) - - :else - form)) - - (process-group-of-assets [data [lib-id items]] - ;; NOTE: there is a possibility that shape refers to an - ;; non-existant file because the file was removed. In this - ;; case we just ignore the asset. - (if-let [lib (get-file cfg lib-id)] - (reduce (partial process-asset lib) data items) - data)) - - (process-asset [lib data [bucket asset-id]] - (let [asset (get-in lib [:data bucket asset-id]) - ;; Add a special case for colors that need to have - ;; correctly set the :file-id prop (pending of the - ;; refactor that will remove it). - asset (cond-> asset - (= bucket :colors) (assoc :file-id file-id))] - (update data bucket assoc asset-id asset)))] - - (let [assets (volatile! [])] - (walk/postwalk #(cond-> % (map? %) (walk-map-form assets)) data) - (->> (deref assets) - (filter #(as-> (first %) $ (and (uuid? $) (not= $ file-id)))) - (d/group-by first rest) - (reduce (partial process-group-of-assets) data))))) - -(defmulti write-export ::version) -(defmulti write-section ::section) - -(s/def ::output io/output-stream?) -(s/def ::file-ids (s/every ::us/uuid :kind vector? :min-count 1)) -(s/def ::include-libraries? (s/nilable ::us/boolean)) -(s/def ::embed-assets? (s/nilable ::us/boolean)) - -(s/def ::write-export-options - (s/keys :req [::db/pool ::sto/storage ::output ::file-ids] - :opt [::include-libraries? ::embed-assets?])) - -(defn write-export! - "Do the exportation of a specified file in custom penpot binary - format. There are some options available for customize the output: - - `::include-libraries?`: additionally to the specified file, all the - linked libraries also will be included (including transitive - dependencies). - - `::embed-assets?`: instead of including the libraries, embed in the - same file library all assets used from external libraries." - [{:keys [::include-libraries? ::embed-assets?] :as options}] - - (us/assert! ::write-export-options options) - (us/verify! - :expr (not (and include-libraries? embed-assets?)) - :hint "the `include-libraries?` and `embed-assets?` are mutally excluding options") - (write-export options)) - -(defmethod write-export :default - [{:keys [::output] :as options}] - (write-header! output :v1) - (with-open [output (zstd-output-stream output :level 12)] - (with-open [output (io/data-output-stream output)] - (binding [*state* (volatile! {})] - (run! (fn [section] - (l/debug :hint "write section" :section section ::l/sync? true) - (write-label! output section) - (let [options (-> options - (assoc ::output output) - (assoc ::section section))] - (binding [*options* options] - (write-section options)))) - - [:v1/metadata :v1/files :v1/rels :v1/sobjects]))))) - -(defmethod write-section :v1/metadata - [{:keys [::output ::file-ids ::include-libraries?] :as cfg}] - (if-let [fids (get-files cfg file-ids)] - (let [lids (when include-libraries? - (get-libraries cfg file-ids)) - ids (into fids lids)] - (write-obj! output {:version cf/version :files ids}) - (vswap! *state* assoc :files ids)) - (ex/raise :type :not-found - :code :files-not-found - :hint "unable to retrieve files for export"))) - -(defmethod write-section :v1/files - [{:keys [::output ::embed-assets?] :as cfg}] - - ;; Initialize SIDS with empty vector - (vswap! *state* assoc :sids []) - - (doseq [file-id (-> *state* deref :files)] - (let [file (cond-> (get-file cfg file-id) - embed-assets? - (update :data embed-file-assets cfg file-id)) - - media (get-file-media cfg file)] - - (l/debug :hint "write penpot file" - :id file-id - :name (:name file) - :media (count media) - ::l/sync? true) - - (doseq [item media] - (l/debug :hint "write penpot file media object" :id (:id item) ::l/sync? true)) - - (doto output - (write-obj! file) - (write-obj! media)) - - (vswap! *state* update :sids into storage-object-id-xf media)))) - -(defmethod write-section :v1/rels - [{:keys [::output ::include-libraries?] :as cfg}] - (let [ids (-> *state* deref :files) - rels (when include-libraries? - (get-library-relations cfg ids))] - (l/debug :hint "found rels" :total (count rels) ::l/sync? true) - (write-obj! output rels))) - -(defmethod write-section :v1/sobjects - [{:keys [::sto/storage ::output]}] - (let [sids (-> *state* deref :sids) - storage (media/configure-assets-storage storage)] - - (l/debug :hint "found sobjects" - :items (count sids) - ::l/sync? true) - - ;; Write all collected storage objects - (write-obj! output sids) - - (doseq [id sids] - (let [{:keys [size] :as obj} (sto/get-object storage id)] - (l/debug :hint "write sobject" :id id ::l/sync? true) - (doto output - (write-uuid! id) - (write-obj! (meta obj))) - - (with-open [^InputStream stream (sto/get-object-data storage obj)] - (let [written (write-stream! output stream size)] - (when (not= written size) - (ex/raise :type :validation - :code :mismatch-readed-size - :hint (str/ffmt "found unexpected object size; size=% written=%" size written))))))))) - -;; --- EXPORT READER - -(declare lookup-index) -(declare update-index) -(declare relink-media) -(declare relink-shapes) - -(defmulti read-import ::version) -(defmulti read-section ::section) - -(s/def ::project-id ::us/uuid) -(s/def ::input io/input-stream?) -(s/def ::overwrite? (s/nilable ::us/boolean)) -(s/def ::migrate? (s/nilable ::us/boolean)) -(s/def ::ignore-index-errors? (s/nilable ::us/boolean)) - -(s/def ::read-import-options - (s/keys :req [::db/pool ::sto/storage ::project-id ::input] - :opt [::overwrite? ::migrate? ::ignore-index-errors?])) - -(defn read-import! - "Do the importation of the specified resource in penpot custom binary - format. There are some options for customize the importation - behavior: - - `::overwrite?`: if true, instead of creating new files and remapping id references, - it reuses all ids and updates existing objects; defaults to `false`. - - `::migrate?`: if true, applies the migration before persisting the - file data; defaults to `false`. - - `::ignore-index-errors?`: if true, do not fail on index lookup errors, can - happen with broken files; defaults to: `false`. - " - - [{:keys [::input ::timestamp] :or {timestamp (dt/now)} :as options}] - (us/verify! ::read-import-options options) - (let [version (read-header! input)] - (read-import (assoc options ::version version ::timestamp timestamp)))) - -(defmethod read-import :v1 - [{:keys [::db/pool ::input] :as options}] - (with-open [input (zstd-input-stream input)] - (with-open [input (io/data-input-stream input)] - (db/with-atomic [conn pool] - (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED;"]) - (binding [*state* (volatile! {:media [] :index {}})] - (run! (fn [section] - (l/debug :hint "reading section" :section section ::l/sync? true) - (assert-read-label! input section) - (let [options (-> options - (assoc ::section section) - (assoc ::input input) - (assoc ::db/conn conn))] - (binding [*options* options] - (read-section options)))) - [:v1/metadata :v1/files :v1/rels :v1/sobjects]) - - ;; Knowing that the ids of the created files are in - ;; index, just lookup them and return it as a set - (let [files (-> *state* deref :files)] - (into #{} (keep #(get-in @*state* [:index %])) files))))))) - -(defmethod read-section :v1/metadata - [{:keys [::input]}] - (let [{:keys [version files]} (read-obj! input)] - (l/debug :hint "metadata readed" :version (:full version) :files files ::l/sync? true) - (vswap! *state* update :index update-index files) - (vswap! *state* assoc :version version :files files))) - -(defn- postprocess-file - [data] - (let [omap-wrap ffeat/*wrap-with-objects-map-fn* - pmap-wrap ffeat/*wrap-with-pointer-map-fn*] - (-> data - (update :pages-index update-vals #(update % :objects omap-wrap)) - (update :pages-index update-vals pmap-wrap) - (update :components update-vals #(d/update-when % :objects omap-wrap)) - (update :components pmap-wrap)))) - -(defmethod read-section :v1/files - [{:keys [::db/conn ::input ::migrate? ::project-id ::timestamp ::overwrite?]}] - (doseq [expected-file-id (-> *state* deref :files)] - (let [file (read-obj! input) - media' (read-obj! input) - file-id (:id file) - features (files/get-default-features)] - - (when (not= file-id expected-file-id) - (ex/raise :type :validation - :code :inconsistent-penpot-file - :found-id file-id - :expected-id expected-file-id - :hint "the penpot file seems corrupt, found unexpected uuid (file-id)")) - - ;; Update index using with media - (l/debug :hint "update index with media" ::l/sync? true) - (vswap! *state* update :index update-index (map :id media')) - - ;; Store file media for later insertion - (l/debug :hint "update media references" ::l/sync? true) - (vswap! *state* update :media into (map #(update % :id lookup-index)) media') - - (l/debug :hint "processing file" :file-id file-id ::features features ::l/sync? true) - - (binding [ffeat/*current* features - ffeat/*wrap-with-objects-map-fn* (if (features "storage/objects-map") omap/wrap identity) - ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity) - pmap/*tracked* (atom {})] - - (let [file-id' (lookup-index file-id) - data (-> (:data file) - (assoc :id file-id') - (cond-> migrate? (pmg/migrate-data)) - (update :pages-index relink-shapes) - (update :components relink-shapes) - (update :media relink-media) - (postprocess-file)) - - params {:id file-id' - :project-id project-id - :features (db/create-array conn "text" features) - :name (:name file) - :revn (:revn file) - :is-shared (:is-shared file) - :data (blob/encode data) - :created-at timestamp - :modified-at timestamp}] - - (l/debug :hint "create file" :id file-id' ::l/sync? true) - - (if overwrite? - (create-or-update-file conn params) - (db/insert! conn :file params)) - - (files/persist-pointers! conn file-id') - - (when overwrite? - (db/delete! conn :file-thumbnail {:file-id file-id'}))))))) - -(defmethod read-section :v1/rels - [{:keys [::db/conn ::input ::timestamp]}] - (let [rels (read-obj! input) - ids (into #{} (-> *state* deref :files))] - ;; Insert all file relations - (doseq [{:keys [library-file-id] :as rel} rels] - (let [rel (-> rel - (assoc :synced-at timestamp) - (update :file-id lookup-index) - (update :library-file-id lookup-index))] - - (if (contains? ids library-file-id) - (do - (l/debug :hint "create file library link" - :file-id (:file-id rel) - :lib-id (:library-file-id rel) - ::l/sync? true) - (db/insert! conn :file-library-rel rel)) - - (l/warn :hint "ignoring file library link" - :file-id (:file-id rel) - :lib-id (:library-file-id rel) - ::l/sync? true)))))) - -(defmethod read-section :v1/sobjects - [{:keys [::sto/storage ::db/conn ::input ::overwrite?]}] - (let [storage (media/configure-assets-storage storage) - ids (read-obj! input)] - - (doseq [expected-storage-id ids] - (let [id (read-uuid! input) - mdata (read-obj! input)] - - (when (not= id expected-storage-id) - (ex/raise :type :validation - :code :inconsistent-penpot-file - :hint "the penpot file seems corrupt, found unexpected uuid (storage-object-id)")) - - (l/debug :hint "readed storage object" :id id ::l/sync? true) - - (let [[size resource] (read-stream! input) - hash (sto/calculate-hash resource) - content (-> (sto/content resource size) - (sto/wrap-with-hash hash)) - params (-> mdata - (assoc ::sto/deduplicate? true) - (assoc ::sto/content content) - (assoc ::sto/touched-at (dt/now)) - (assoc :bucket "file-media-object")) - - sobject (sto/put-object! storage params)] - - (l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/sync? true) - (vswap! *state* update :index assoc id (:id sobject))))) - - (doseq [item (:media @*state*)] - (l/debug :hint "inserting file media object" - :id (:id item) - :file-id (:file-id item) - ::l/sync? true) - - (let [file-id (lookup-index (:file-id item))] - (if (= file-id (:file-id item)) - (l/warn :hint "ignoring file media object" :file-id (:file-id item) ::l/sync? true) - (db/insert! conn :file-media-object - (-> item - (assoc :file-id file-id) - (d/update-when :media-id lookup-index) - (d/update-when :thumbnail-id lookup-index)) - {:on-conflict-do-nothing overwrite?})))))) - -(defn- lookup-index - [id] - (let [val (get-in @*state* [:index id])] - (l/trc :fn "lookup-index" :id id :val val ::l/sync? true) - (when (and (not (::ignore-index-errors? *options*)) (not val)) - (ex/raise :type :validation - :code :incomplete-index - :hint "looks like index has missing data")) - (or val id))) - -(defn- update-index - [index coll] - (loop [items (seq coll) - index index] - (if-let [id (first items)] - (let [new-id (if (::overwrite? *options*) id (uuid/next))] - (l/debug :fn "update-index" :id id :new-id new-id ::l/sync? true) - (recur (rest items) - (assoc index id new-id))) - index))) - -(defn- relink-shapes - "A function responsible to analyze all file data and - replace the old :component-file reference with the new - ones, using the provided file-index." - [data] - (letfn [(process-map-form [form] - (cond-> form - ;; Relink image shapes - (and (map? (:metadata form)) - (= :image (:type form))) - (update-in [:metadata :id] lookup-index) - - ;; Relink paths with fill image - (map? (:fill-image form)) - (update-in [:fill-image :id] lookup-index) - - ;; This covers old shapes and the new :fills. - (uuid? (:fill-color-ref-file form)) - (update :fill-color-ref-file lookup-index) - - ;; This covers the old shapes and the new :strokes - (uuid? (:storage-color-ref-file form)) - (update :stroke-color-ref-file lookup-index) - - ;; This covers all text shapes that have typography referenced - (uuid? (:typography-ref-file form)) - (update :typography-ref-file lookup-index) - - ;; This covers the component instance links - (uuid? (:component-file form)) - (update :component-file lookup-index) - - ;; This covers the shadows and grids (they have directly - ;; the :file-id prop) - (uuid? (:file-id form)) - (update :file-id lookup-index)))] - - (walk/postwalk (fn [form] - (if (map? form) - (try - (process-map-form form) - (catch Throwable cause - (l/warn :hint "failed form" :form (pr-str form) ::l/sync? true) - (throw cause))) - form)) - data))) - -(defn- relink-media - "A function responsible of process the :media attr of file data and - remap the old ids with the new ones." - [media] - (reduce-kv (fn [res k v] - (let [id (lookup-index k)] - (if (uuid? id) - (-> res - (assoc id (assoc v :id id)) - (dissoc k)) - res))) - media - media)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; HIGH LEVEL API -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn export! - [cfg output] - (let [id (uuid/next) - tp (dt/tpoint) - ab (volatile! false) - cs (volatile! nil)] - (try - (l/info :hint "start exportation" :export-id id) - (dm/with-open [output (io/output-stream output)] - (binding [*position* (atom 0)] - (write-export! (assoc cfg ::output output)))) - - (catch java.io.IOException _cause - ;; Do nothing, EOF means client closes connection abruptly - (vreset! ab true) - nil) - - (catch Throwable cause - (vreset! cs cause) - (vreset! ab true) - (throw cause)) - - (finally - (l/info :hint "exportation finished" :export-id id - :elapsed (str (inst-ms (tp)) "ms") - :aborted @ab - :cause @cs))))) - -(defn export-to-tmpfile! - [cfg] - (let [path (tmp/tempfile :prefix "penpot.export.")] - (dm/with-open [output (io/output-stream path)] - (export! cfg output) - path))) - -(defn import! - [{:keys [::input] :as cfg}] - (let [id (uuid/next) - tp (dt/tpoint) - cs (volatile! nil)] - (l/info :hint "import: started" :import-id id) - (try - (binding [*position* (atom 0)] - (dm/with-open [input (io/input-stream input)] - (read-import! (assoc cfg ::input input)))) - - (catch Throwable cause - (vreset! cs cause) - (throw cause)) - - (finally - (l/info :hint "import: terminated" - :import-id id - :elapsed (dt/format-duration (tp)) - :error? (some? @cs) - :cause @cs - ))))) - ;; --- Command: export-binfile -(s/def ::file-id ::us/uuid) -(s/def ::include-libraries? ::us/boolean) -(s/def ::embed-assets? ::us/boolean) - -(s/def ::export-binfile - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::include-libraries? ::embed-assets?])) +(def ^:private + schema:export-binfile + (sm/define + [:map {:title "export-binfile"} + [:name :string] + [:file-id ::sm/uuid] + [:include-libraries :boolean] + [:embed-assets :boolean]])) (sv/defmethod ::export-binfile "Export a penpot file in a binary format." {::doc/added "1.15" - ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id include-libraries? embed-assets?] :as params}] + ::webhooks/event? true + ::sm/result schema:export-binfile} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id include-libraries embed-assets] :as params}] (files/check-read-permissions! pool profile-id file-id) - (let [body (reify yrs/StreamableResponseBody - (-write-body-to-stream [_ _ output-stream] - (-> cfg - (assoc ::file-ids [file-id]) - (assoc ::embed-assets? embed-assets?) - (assoc ::include-libraries? include-libraries?) - (export! output-stream))))] + (fn [_] + {::rres/status 200 + ::rres/headers {"content-type" "application/octet-stream"} + ::rres/body (reify rres/StreamableResponseBody + (-write-body-to-stream [_ _ output-stream] + (try + (-> cfg + (assoc ::bf.v1/ids #{file-id}) + (assoc ::bf.v1/embed-assets embed-assets) + (assoc ::bf.v1/include-libraries include-libraries) + (bf.v1/export-files! output-stream)) + (catch Throwable cause + (l/err :hint "exception on exporting file" + :file-id (str file-id) + :cause cause)))))})) - (fn [_] - {::yrs/status 200 - ::yrs/body body - ::yrs/headers {"content-type" "application/octet-stream"}}))) +;; --- Command: import-binfile -(s/def ::file ::media/upload) -(s/def ::import-binfile - (s/keys :req [::rpc/profile-id] - :req-un [::project-id ::file])) +(defn- import-binfile + [{:keys [::wrk/executor ::bf.v1/project-id] :as cfg} input] + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + ;; NOTE: the importation process performs some operations that + ;; are not very friendly with virtual threads, and for avoid + ;; unexpected blocking of other concurrent operations we + ;; dispatch that operation to a dedicated executor. + (let [result (px/submit! executor (partial bf.v1/import-files! cfg input))] + (db/update! conn :project + {:modified-at (dt/now)} + {:id project-id}) + (deref result))))) + +(def ^:private + schema:import-binfile + (sm/define + [:map {:title "import-binfile"} + [:name :string] + [:project-id ::sm/uuid] + [:file ::media/upload]])) (sv/defmethod ::import-binfile "Import a penpot file in a binary format." {::doc/added "1.15" - ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id file] :as params}] - (db/with-atomic [conn pool] - (projects/check-read-permissions! conn profile-id project-id) - (let [ids (import! (assoc cfg - ::input (:path file) - ::project-id project-id - ::ignore-index-errors? true))] - - (db/update! conn :project - {:modified-at (dt/now)} - {:id project-id}) - - (rph/with-meta ids - {::audit/props {:file nil :file-ids ids}})))) + ::webhooks/event? true + ::sse/stream? true + ::sm/params schema:import-binfile} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name project-id file] :as params}] + (projects/check-read-permissions! pool profile-id project-id) + (let [cfg (-> cfg + (assoc ::bf.v1/project-id project-id) + (assoc ::bf.v1/profile-id profile-id) + (assoc ::bf.v1/name name))] + (with-meta + (sse/response #(import-binfile cfg (:path file))) + {::audit/props {:file nil}}))) diff --git a/backend/src/app/rpc/commands/comments.clj b/backend/src/app/rpc/commands/comments.clj index b8352f6220..4949f1a435 100644 --- a/backend/src/app/rpc/commands/comments.clj +++ b/backend/src/app/rpc/commands/comments.clj @@ -9,9 +9,11 @@ [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.geom.point :as gpt] - [app.common.spec :as us] + [app.common.schema :as sm] [app.common.uuid :as uuid] [app.db :as db] + [app.db.sql :as sql] + [app.features.fdata :as feat.fdata] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] [app.rpc :as-alias rpc] @@ -22,18 +24,21 @@ [app.rpc.retry :as rtry] [app.util.pointer-map :as pmap] [app.util.services :as sv] - [app.util.time :as dt] - [clojure.spec.alpha :as s])) + [app.util.time :as dt])) ;; --- GENERAL PURPOSE INTERNAL HELPERS -(defn decode-row +(defn- decode-row [{:keys [participants position] :as row}] (cond-> row (db/pgpoint? position) (assoc :position (db/decode-pgpoint position)) (db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants)))) -(def sql:get-file +(def xf-decode-row + (map decode-row)) + +(def ^:privateqpage-name + sql:get-file "select f.id, f.modified_at, f.revn, f.features, f.project_id, p.team_id, f.data from file as f @@ -43,15 +48,19 @@ (defn- get-file "A specialized version of get-file for comments module." - [conn file-id page-id] - (binding [pmap/*load-fn* (partial files/load-pointer conn file-id)] - (if-let [{:keys [data] :as file} (some-> (db/exec-one! conn [sql:get-file file-id]) (files/decode-row))] - (-> file - (assoc :page-name (dm/get-in data [:pages-index page-id :name])) - (assoc :page-id page-id)) + [cfg file-id page-id] + (let [file (db/exec-one! cfg [sql:get-file file-id])] + (when-not file (ex/raise :type :not-found :code :object-not-found - :hint "file not found")))) + :hint "file not found")) + + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)] + (let [{:keys [data] :as file} (files/decode-row file)] + (-> file + (assoc :page-name (dm/get-in data [:pages-index page-id :name])) + (assoc :page-id page-id) + (dissoc :data)))))) (defn- get-comment-thread [conn thread-id & {:as opts}] @@ -59,8 +68,8 @@ (decode-row))) (defn- get-comment - [conn comment-id & {:keys [for-update?]}] - (db/get-by-id conn :comment comment-id {:for-update for-update?})) + [conn comment-id & {:as opts}] + (db/get-by-id conn :comment comment-id opts)) (defn- get-next-seqn [conn file-id] @@ -89,23 +98,25 @@ (declare ^:private get-comment-threads) -(s/def ::team-id ::us/uuid) -(s/def ::file-id ::us/uuid) -(s/def ::share-id (s/nilable ::us/uuid)) - -(s/def ::get-comment-threads - (s/and (s/keys :req [::rpc/profile-id] - :opt-un [::file-id ::share-id ::team-id]) - #(or (:file-id %) (:team-id %)))) +(def ^:private + schema:get-comment-threads + [:and + [:map {:title "get-comment-threads"} + [:file-id {:optional true} ::sm/uuid] + [:team-id {:optional true} ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]] + [::sm/contains-any #{:file-id :team-id}]]) (sv/defmethod ::get-comment-threads - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}] - (dm/with-open [conn (db/open pool)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (get-comment-threads conn profile-id file-id))) + {::doc/added "1.15" + ::sm/params schema:get-comment-threads} + [cfg {:keys [::rpc/profile-id file-id share-id] :as params}] -(def sql:comment-threads + (db/run! cfg (fn [{:keys [::db/conn]}] + (files/check-comment-permissions! conn profile-id file-id share-id) + (get-comment-threads conn profile-id file-id)))) + +(def ^:private sql:comment-threads "select distinct on (ct.id) ct.*, f.name as file_name, @@ -130,23 +141,24 @@ (defn- get-comment-threads [conn profile-id file-id] (->> (db/exec! conn [sql:comment-threads profile-id file-id]) - (into [] (map decode-row)))) + (into [] xf-decode-row))) ;; --- COMMAND: Get Unread Comment Threads (declare ^:private get-unread-comment-threads) -(s/def ::team-id ::us/uuid) -(s/def ::get-unread-comment-threads - (s/keys :req [::rpc/profile-id] - :req-un [::team-id])) +(def ^:private + schema:get-unread-comment-threads + [:map {:title "get-unread-comment-threads"} + [:team-id ::sm/uuid]]) (sv/defmethod ::get-unread-comment-threads - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] - (dm/with-open [conn (db/open pool)] - (teams/check-read-permissions! conn profile-id team-id) - (get-unread-comment-threads conn profile-id team-id))) + {::doc/added "1.15" + ::sm/params schema:get-unread-comment-threads} + [cfg {:keys [::rpc/profile-id team-id] :as params}] + (db/run! cfg (fn [{:keys [::db/conn]}] + (teams/check-read-permissions! conn profile-id team-id) + (get-unread-comment-threads conn profile-id team-id)))) (def sql:comment-threads-by-team "select distinct on (ct.id) @@ -178,62 +190,60 @@ (defn- get-unread-comment-threads [conn profile-id team-id] (->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id]) - (into [] (map decode-row)))) - + (into [] xf-decode-row))) ;; --- COMMAND: Get Single Comment Thread -(s/def ::get-comment-thread - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::us/id] - :opt-un [::share-id])) +(def ^:private + schema:get-comment-thread + [:map {:title "get-comment-thread"} + [:file-id ::sm/uuid] + [:id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::get-comment-thread - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}] - (dm/with-open [conn (db/open pool)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (let [sql (str "with threads as (" sql:comment-threads ")" - "select * from threads where id = ?")] - (-> (db/exec-one! conn [sql profile-id file-id id]) - (decode-row))))) + {::doc/added "1.15" + ::sm/params schema:get-comment-thread} + [cfg {:keys [::rpc/profile-id file-id id share-id] :as params}] + (db/run! cfg (fn [{:keys [::db/conn]}] + (files/check-comment-permissions! conn profile-id file-id share-id) + (let [sql (str "with threads as (" sql:comment-threads ")" + "select * from threads where id = ?")] + (-> (db/exec-one! conn [sql profile-id file-id id]) + (decode-row)))))) ;; --- COMMAND: Retrieve Comments (declare ^:private get-comments) -(s/def ::thread-id ::us/uuid) -(s/def ::get-comments - (s/keys :req [::rpc/profile-id] - :req-un [::thread-id] - :opt-un [::share-id])) +(def ^:private + schema:get-comments + [:map {:title "get-comments"} + [:thread-id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::get-comments - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}] - (dm/with-open [conn (db/open pool)] - (let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (get-comments conn thread-id)))) - -(def sql:comments - "select c.* from comment as c - where c.thread_id = ? - order by c.created_at asc") + {::doc/added "1.15" + ::sm/params schema:get-comments} + [cfg {:keys [::rpc/profile-id thread-id share-id]}] + (db/run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (get-comments conn thread-id))))) (defn- get-comments [conn thread-id] (->> (db/query conn :comment {:thread-id thread-id} {:order-by [[:created-at :asc]]}) - (into [] (map decode-row)))) + (into [] xf-decode-row))) ;; --- COMMAND: Get file comments users ;; All the profiles that had comment the file, plus the current ;; profile. -(def sql:file-comment-users +(def ^:private sql:file-comment-users "WITH available_profiles AS ( SELECT DISTINCT owner_id AS id FROM comment @@ -252,20 +262,22 @@ [conn file-id profile-id] (db/exec! conn [sql:file-comment-users file-id profile-id])) -(s/def ::get-profiles-for-file-comments - (s/keys :req [::rpc/profile-id] - :req-un [::file-id] - :opt-un [::share-id])) +(def ^:private + schema:get-profiles-for-file-comments + [:map {:title "get-profiles-for-file-comments"} + [:file-id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::get-profiles-for-file-comments "Retrieves a list of profiles with limited set of properties of all participants on comment threads of the file." {::doc/added "1.15" - ::doc/changes ["1.15" "Imported from queries and renamed."]} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}] - (dm/with-open [conn (db/open pool)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (get-file-comments-users conn file-id profile-id))) + ::doc/changes ["1.15" "Imported from queries and renamed."] + ::sm/params schema:get-profiles-for-file-comments} + [cfg {:keys [::rpc/profile-id file-id share-id]}] + (db/run! cfg (fn [{:keys [::db/conn]}] + (files/check-comment-permissions! conn profile-id file-id share-id) + (get-file-comments-users conn file-id profile-id)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MUTATION COMMANDS @@ -275,55 +287,52 @@ ;; --- COMMAND: Create Comment Thread -(s/def ::page-id ::us/uuid) -(s/def ::position ::gpt/point) -(s/def ::content ::us/string) -(s/def ::frame-id ::us/uuid) - -(s/def ::create-comment-thread - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::position ::content ::page-id ::frame-id] - :opt-un [::share-id])) +(def ^:private + schema:create-comment-thread + [:map {:title "create-comment-thread"} + [:file-id ::sm/uuid] + [:position ::gpt/point] + [:content :string] + [:page-id ::sm/uuid] + [:frame-id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::create-comment-thread {::doc/added "1.15" - ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} - {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}] + ::webhooks/event? true + ::rtry/enabled true + ::rtry/when rtry/conflict-exception? + ::sm/params schema:create-comment-thread} + [cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}] - (db/with-atomic [conn pool] - (let [{:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)] - (files/check-comment-permissions! conn profile-id file-id share-id) + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (files/check-comment-permissions! cfg profile-id file-id share-id) + (let [{:keys [team-id project-id page-name]} (get-file conn file-id page-id)] - (run! (partial quotes/check-quote! conn) - (list {::quotes/id ::quotes/comment-threads-per-file - ::quotes/profile-id profile-id - ::quotes/team-id team-id - ::quotes/project-id project-id - ::quotes/file-id file-id} - {::quotes/id ::quotes/comments-per-file - ::quotes/profile-id profile-id - ::quotes/team-id team-id - ::quotes/project-id project-id - ::quotes/file-id file-id})) - - (rtry/with-retry {::rtry/when rtry/conflict-exception? - ::rtry/max-retries 3 - ::rtry/label "create-comment-thread" - ::db/conn conn} - (create-comment-thread conn - {:created-at request-at - :profile-id profile-id - :file-id file-id - :page-id page-id - :page-name page-name - :position position - :content content - :frame-id frame-id}))))) + (run! (partial quotes/check-quote! cfg) + (list {::quotes/id ::quotes/comment-threads-per-file + ::quotes/profile-id profile-id + ::quotes/team-id team-id + ::quotes/project-id project-id + ::quotes/file-id file-id} + {::quotes/id ::quotes/comments-per-file + ::quotes/profile-id profile-id + ::quotes/team-id team-id + ::quotes/project-id project-id + ::quotes/file-id file-id})) + (create-comment-thread conn {:created-at request-at + :profile-id profile-id + :file-id file-id + :page-id page-id + :page-name page-name + :position position + :content content + :frame-id frame-id}))))) (defn- create-comment-thread [conn {:keys [profile-id file-id page-id page-name created-at position content frame-id]}] + (let [;; NOTE: we take the next seq number from a separate query because the whole ;; operation can be retried on conflict, and in this case the new seq shold be ;; retrieved from the database. @@ -363,208 +372,228 @@ ;; --- COMMAND: Update Comment Thread Status -(s/def ::id ::us/uuid) -(s/def ::share-id (s/nilable ::us/uuid)) - -(s/def ::update-comment-thread-status - (s/keys :req [::rpc/profile-id] - :req-un [::id] - :opt-un [::share-id])) +(def ^:private + schema:update-comment-thread-status + [:map {:title "update-comment-thread-status"} + [:id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::update-comment-thread-status - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] - (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (upsert-comment-thread-status! conn profile-id id)))) + {::doc/added "1.15" + ::sm/params schema:update-comment-thread-status} + [cfg {:keys [::rpc/profile-id id share-id]}] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (upsert-comment-thread-status! conn profile-id id))))) ;; --- COMMAND: Update Comment Thread -(s/def ::is-resolved ::us/boolean) -(s/def ::update-comment-thread - (s/keys :req [::rpc/profile-id] - :req-un [::id ::is-resolved] - :opt-un [::share-id])) +(def ^:private + schema:update-comment-thread + [:map {:title "update-comment-thread"} + [:id ::sm/uuid] + [:is-resolved :boolean] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::update-comment-thread - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}] - (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (db/update! conn :comment-thread - {:is-resolved is-resolved} - {:id id}) - nil))) + {::doc/added "1.15" + ::sm/params schema:update-comment-thread} + [cfg {:keys [::rpc/profile-id id is-resolved share-id]}] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (db/update! conn :comment-thread + {:is-resolved is-resolved} + {:id id}) + nil)))) ;; --- COMMAND: Add Comment -(declare get-comment-thread) -(declare create-comment) +(declare ^:private get-comment-thread) -(s/def ::create-comment - (s/keys :req [::rpc/profile-id] - :req-un [::thread-id ::content] - :opt-un [::share-id])) +(def ^:private + schema:create-comment + [:map {:title "create-comment"} + [:thread-id ::sm/uuid] + [:content :string] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::create-comment {::doc/added "1.15" - ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content] :as params}] - (db/with-atomic [conn pool] - (let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true) - {:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)] + ::webhooks/event? true + ::sm/params schema:create-comment} + [cfg {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content]}] + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::sql/for-update true) + {:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)] - (files/check-comment-permissions! conn profile-id (:id file) share-id) - (quotes/check-quote! conn - {::quotes/id ::quotes/comments-per-file - ::quotes/profile-id profile-id - ::quotes/team-id team-id - ::quotes/project-id project-id - ::quotes/file-id (:id file)}) + (files/check-comment-permissions! conn profile-id file-id share-id) + (quotes/check-quote! conn + {::quotes/id ::quotes/comments-per-file + ::quotes/profile-id profile-id + ::quotes/team-id team-id + ::quotes/project-id project-id + ::quotes/file-id file-id}) - ;; Update the page-name cached attribute on comment thread table. - (when (not= page-name (:page-name thread)) - (db/update! conn :comment-thread - {:page-name page-name} - {:id thread-id})) + ;; Update the page-name cached attribute on comment thread table. + (when (not= page-name (:page-name thread)) + (db/update! conn :comment-thread + {:page-name page-name} + {:id thread-id})) - (let [comment (db/insert! conn :comment - {:id (uuid/next) - :created-at request-at - :modified-at request-at - :thread-id thread-id - :owner-id profile-id - :content content}) - props {:file-id file-id - :share-id nil}] + (let [comment (db/insert! conn :comment + {:id (uuid/next) + :created-at request-at + :modified-at request-at + :thread-id thread-id + :owner-id profile-id + :content content}) + props {:file-id file-id + :share-id nil}] - ;; Update thread modified-at attribute and assoc the current - ;; profile to the participant set. - (db/update! conn :comment-thread - {:modified-at request-at - :participants (-> (:participants thread #{}) - (conj profile-id) - (db/tjson))} - {:id thread-id}) + ;; Update thread modified-at attribute and assoc the current + ;; profile to the participant set. + (db/update! conn :comment-thread + {:modified-at request-at + :participants (-> (:participants thread #{}) + (conj profile-id) + (db/tjson))} + {:id thread-id}) - ;; Update the current profile status in relation to the - ;; current thread. - (upsert-comment-thread-status! conn profile-id thread-id request-at) + ;; Update the current profile status in relation to the + ;; current thread. + (upsert-comment-thread-status! conn profile-id thread-id request-at) + + (vary-meta comment assoc ::audit/props props)))))) - (vary-meta comment assoc ::audit/props props))))) ;; --- COMMAND: Update Comment -(s/def ::update-comment - (s/keys :req [::rpc/profile-id] - :req-un [::id ::content] - :opt-un [::share-id])) +(def ^:private + schema:update-comment + [:map {:title "update-comment"} + [:id ::sm/uuid] + [:content :string] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::update-comment - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}] - (db/with-atomic [conn pool] - (let [{:keys [thread-id owner-id] :as comment} (get-comment conn id ::db/for-update? true) - {:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)] + {::doc/added "1.15" + ::sm/params schema:update-comment} + [cfg {:keys [::rpc/profile-id ::rpc/request-at id share-id content]}] + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (let [{:keys [thread-id owner-id] :as comment} (get-comment conn id ::sql/for-update true) + {:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::sql/for-update true)] - (files/check-comment-permissions! conn profile-id file-id share-id) + (files/check-comment-permissions! conn profile-id file-id share-id) - ;; Don't allow edit comments to not owners - (when-not (= owner-id profile-id) - (ex/raise :type :validation - :code :not-allowed)) + ;; Don't allow edit comments to not owners + (when-not (= owner-id profile-id) + (ex/raise :type :validation + :code :not-allowed)) - (let [{:keys [page-name] :as file} (get-file conn file-id page-id)] - (db/update! conn :comment - {:content content - :modified-at request-at} - {:id id}) + (let [{:keys [page-name]} (get-file cfg file-id page-id)] + (db/update! conn :comment + {:content content + :modified-at request-at} + {:id id}) - (db/update! conn :comment-thread - {:modified-at request-at - :page-name page-name} - {:id thread-id}) - nil)))) + (db/update! conn :comment-thread + {:modified-at request-at + :page-name page-name} + {:id thread-id}) + nil))))) ;; --- COMMAND: Delete Comment Thread -(s/def ::delete-comment-thread - (s/keys :req [::rpc/profile-id] - :req-un [::id] - :opt-un [::share-id])) +(def ^:private + schema:delete-comment-thread + [:map {:title "delete-comment-thread"} + [:id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::delete-comment-thread - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] - (db/with-atomic [conn pool] - (let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (when-not (= owner-id profile-id) - (ex/raise :type :validation - :code :not-allowed)) + {::doc/added "1.15" + ::sm/params schema:delete-comment-thread} + [cfg {:keys [::rpc/profile-id id share-id]}] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (when-not (= owner-id profile-id) + (ex/raise :type :validation + :code :not-allowed)) - (db/delete! conn :comment-thread {:id id}) - nil))) + (db/delete! conn :comment-thread {:id id}) + nil)))) ;; --- COMMAND: Delete comment -(s/def ::delete-comment - (s/keys :req [::rpc/profile-id] - :req-un [::id] - :opt-un [::share-id])) +(def ^:private + schema:delete-comment + [:map {:title "delete-comment"} + [:id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::delete-comment - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] - (db/with-atomic [conn pool] - (let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::db/for-update? true) - {:keys [file-id] :as thread} (get-comment-thread conn thread-id)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (when-not (= owner-id profile-id) - (ex/raise :type :validation - :code :not-allowed)) - (db/delete! conn :comment {:id id})))) - + {::doc/added "1.15" + ::sm/params schema:delete-comment} + [cfg {:keys [::rpc/profile-id id share-id]}] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::sql/for-update true) + {:keys [file-id] :as thread} (get-comment-thread conn thread-id)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (when-not (= owner-id profile-id) + (ex/raise :type :validation + :code :not-allowed)) + (db/delete! conn :comment {:id id}) + nil)))) ;; --- COMMAND: Update comment thread position -(s/def ::update-comment-thread-position - (s/keys :req [::rpc/profile-id] - :req-un [::id ::position ::frame-id] - :opt-un [::share-id])) +(def ^:private + schema:update-comment-thread-position + [:map {:title "update-comment-thread-position"} + [:id ::sm/uuid] + [:position ::gpt/point] + [:frame-id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::update-comment-thread-position - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}] - (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (db/update! conn :comment-thread - {:modified-at (::rpc/request-at params) - :position (db/pgpoint position) - :frame-id frame-id} - {:id (:id thread)}) - nil))) + {::doc/added "1.15" + ::sm/params schema:update-comment-thread-position} + [cfg {:keys [::rpc/profile-id ::rpc/request-at id position frame-id share-id]}] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (db/update! conn :comment-thread + {:modified-at request-at + :position (db/pgpoint position) + :frame-id frame-id} + {:id (:id thread)}) + nil)))) ;; --- COMMAND: Update comment frame -(s/def ::update-comment-thread-frame - (s/keys :req [::rpc/profile-id] - :req-un [::id ::frame-id] - :opt-un [::share-id])) +(def ^:private + schema:update-comment-thread-frame + [:map {:title "update-comment-thread-frame"} + [:id ::sm/uuid] + [:frame-id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::update-comment-thread-frame - {::doc/added "1.15"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}] - (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] - (files/check-comment-permissions! conn profile-id file-id share-id) - (db/update! conn :comment-thread - {:modified-at (::rpc/request-at params) - :frame-id frame-id} - {:id id}) - nil))) + {::doc/added "1.15" + ::sm/params schema:update-comment-thread-frame} + [cfg {:keys [::rpc/profile-id ::rpc/request-at id frame-id share-id]}] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (db/update! conn :comment-thread + {:modified-at request-at + :frame-id frame-id} + {:id id}) + nil)))) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index d3514cdd49..601907e105 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -9,16 +9,19 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] - [app.common.pages.helpers :as cph] - [app.common.pages.migrations :as pmg] + [app.common.features :as cfeat] + [app.common.files.helpers :as cfh] + [app.common.files.migrations :as fmg] + [app.common.logging :as l] [app.common.schema :as sm] [app.common.schema.desc-js-like :as-alias smdj] - [app.common.schema.generators :as sg] [app.common.spec :as us] [app.common.types.components-list :as ctkl] [app.common.types.file :as ctf] [app.config :as cf] [app.db :as db] + [app.db.sql :as-alias sql] + [app.features.fdata :as feat.fdata] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] [app.rpc :as-alias rpc] @@ -32,7 +35,6 @@ [app.util.pointer-map :as pmap] [app.util.services :as sv] [app.util.time :as dt] - [clojure.set :as set] [clojure.spec.alpha :as s] [cuerdas.core :as str])) @@ -43,25 +45,6 @@ (when media-id (str (cf/get :public-uri) "/assets/by-id/" media-id))) -(def supported-features - #{"storage/objects-map" - "storage/pointer-map" - "internal/geom-record" - "internal/shape-record" - "fdata/pointer-map" - "fdata/objects-map" - "fdata/shape-data-type" - "components/v2"}) - -(defn get-default-features - [] - (cond-> #{} - (contains? cf/flags :fdata-storage-pointer-map) - (conj "storage/pointer-map") - - (contains? cf/flags :fdata-storage-objects-map) - (conj "storage/objects-map"))) - ;; --- SPECS (s/def ::features ::us/set-of-strings) @@ -87,6 +70,17 @@ changes (assoc :changes (blob/decode changes)) data (assoc :data (blob/decode data))))) +(defn check-version! + [file] + (let [version (:version file)] + (when (> version fmg/version) + (ex/raise :type :restriction + :code :file-version-not-supported + :hint "file version is greated that the maximum" + :file-version version + :max-version fmg/version)) + file)) + ;; --- FILE PERMISSIONS (def ^:private sql:file-permissions @@ -183,182 +177,113 @@ :code :object-not-found :hint "not found")))) -;; --- HELPERS - -(defn get-team-id - [conn project-id] - (:team-id (db/get-by-id conn :project project-id {:columns [:team-id]}))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; FEATURES: pointer-map -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn check-features-compatibility! - "Function responsible to check if provided features are supported by - the current backend" - [features] - (let [not-supported (set/difference features supported-features)] - (when (seq not-supported) - (ex/raise :type :restriction - :code :features-not-supported - :feature (first not-supported) - :hint (format "features %s not supported" (str/join "," (map name not-supported))))) - features)) - -(defn load-pointer - [conn file-id id] - (let [row (db/get conn :file-data-fragment - {:id id :file-id file-id} - {:columns [:content] - ::db/check-deleted? false})] - (blob/decode (:content row)))) - -(defn- load-all-pointers! - [{:keys [data] :as file}] - (doseq [[_id page] (:pages-index data)] - (when (pmap/pointer-map? page) - (pmap/load! page))) - (doseq [[_id component] (:components data)] - (when (pmap/pointer-map? component) - (pmap/load! component))) - file) - -(defn persist-pointers! - [conn file-id] - (doseq [[id item] @pmap/*tracked*] - (when (pmap/modified? item) - (let [content (-> item deref blob/encode)] - (db/insert! conn :file-data-fragment - {:id id - :file-id file-id - :content content}))))) - -(defn process-pointers - [file update-fn] - (update file :data (fn resolve-fn [data] - (cond-> data - (contains? data :pages-index) - (update :pages-index resolve-fn) - - :always - (update-vals (fn [val] - (if (pmap/pointer-map? val) - (update-fn val) - val))))))) - - -(defn get-all-pointer-ids - "Given a file, return all pointer ids used in the data." - [fdata] - (->> (concat (vals fdata) - (vals (:pages-index fdata))) - (into #{} (comp (filter pmap/pointer-map?) - (map pmap/get-id))))) - -(defn handle-file-features! - [{:keys [features] :as file} client-features] - - ;; Check features compatibility between the currently supported features on - ;; the current backend instance and the file retrieved from the database - (check-features-compatibility! features) - - (cond-> file - (and (contains? features "components/v2") - (not (contains? client-features "components/v2"))) - (as-> file (ex/raise :type :restriction - :code :feature-mismatch - :feature "components/v2" - :hint "file has 'components/v2' feature enabled but frontend didn't specifies it" - :file-id (:id file))) - - ;; This operation is needed for backward comapatibility with frontends that - ;; does not support pointer-map resolution mechanism; this just resolves the - ;; pointers on backend and return a complete file. - (and (contains? features "storage/pointer-map") - (not (contains? client-features "storage/pointer-map"))) - (process-pointers deref) - - (and (contains? features "fdata/pointer-map") - (not (contains? client-features "fdata/pointer-map"))) - (process-pointers deref))) - - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; QUERY COMMANDS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; --- COMMAND QUERY: get-file (by id) -(sm/def! ::features - [:schema - {:title "FileFeatures" - ::smdj/inline true - :gen/gen (sg/subseq supported-features)} - ::sm/set-of-strings]) +(def schema:file + (sm/define + [:map {:title "File"} + [:id ::sm/uuid] + [:features ::cfeat/features] + [:has-media-trimmed :boolean] + [:comment-thread-seqn {:min 0} :int] + [:name :string] + [:revn {:min 0} :int] + [:modified-at ::dt/instant] + [:is-shared :boolean] + [:project-id ::sm/uuid] + [:created-at ::dt/instant] + [:data {:optional true} :any]])) -(sm/def! ::file - [:map {:title "File"} - [:id ::sm/uuid] - [:features ::features] - [:has-media-trimmed :boolean] - [:comment-thread-seqn {:min 0} :int] - [:name :string] - [:revn {:min 0} :int] - [:modified-at ::dt/instant] - [:is-shared :boolean] - [:project-id ::sm/uuid] - [:created-at ::dt/instant] - [:data {:optional true} :any]]) +(def schema:permissions-mixin + (sm/define + [:map {:title "PermissionsMixin"} + [:permissions ::perms/permissions]])) -(sm/def! ::permissions-mixin - [:map {:title "PermissionsMixin"} - [:permissions ::perms/permissions]]) +(def schema:file-with-permissions + (sm/define + [:merge {:title "FileWithPermissions"} + schema:file + schema:permissions-mixin])) -(sm/def! ::file-with-permissions - [:merge {:title "FileWithPermissions"} - ::file - ::permissions-mixin]) +(def ^:private + schema:get-file + (sm/define + [:map {:title "get-file"} + [:features {:optional true} ::cfeat/features] + [:id ::sm/uuid] + [:project-id {:optional true} ::sm/uuid]])) -(sm/def! ::get-file - [:map {:title "get-file"} - [:features {:optional true} ::features] - [:id ::sm/uuid] - [:project-id {:optional true} ::sm/uuid]]) +(defn- migrate-file + [{:keys [::db/conn] :as cfg} {:keys [id] :as file}] + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id) + pmap/*tracked* (pmap/create-tracked)] + (let [;; For avoid unnecesary overhead of creating multiple pointers and + ;; handly internally with objects map in their worst case (when + ;; probably all shapes and all pointers will be readed in any + ;; case), we just realize/resolve them before applying the + ;; migration to the file + file (-> file + (update :data feat.fdata/process-pointers deref) + (update :data feat.fdata/process-objects (partial into {})) + (fmg/migrate-file)) + + ;; When file is migrated, we break the rule of no perform + ;; mutations on get operations and update the file with all + ;; migrations applied + ;; + ;; WARN: he following code will not work on read-only mode, + ;; it is a known issue; we keep is not implemented until we + ;; really need this. + file (if (contains? (:features file) "fdata/objects-map") + (feat.fdata/enable-objects-map file) + file) + file (if (contains? (:features file) "fdata/pointer-map") + (feat.fdata/enable-pointer-map file) + file)] + + (db/update! conn :file + {:data (blob/encode (:data file)) + :version (:version file) + :features (db/create-array conn "text" (:features file))} + {:id id}) + + (when (contains? (:features file) "fdata/pointer-map") + (feat.fdata/persist-pointers! cfg id)) + + file))) (defn get-file - ([conn id client-features] - (get-file conn id client-features nil)) - ([conn id client-features project-id] - ;; here we check if client requested features are supported - (check-features-compatibility! client-features) - (binding [pmap/*load-fn* (partial load-pointer conn id) - pmap/*tracked* (atom {})] + [{:keys [::db/conn] :as cfg} id & {:keys [project-id + migrate? + include-deleted? + lock-for-update?] + :or {include-deleted? false + lock-for-update? false + migrate? true}}] + (dm/assert! + "expected cfg with valid connection" + (db/connection-map? cfg)) - (let [params (merge {:id id} - (when (some? project-id) - {:project-id project-id})) - - file (-> (db/get conn :file params) - (decode-row) - (pmg/migrate-file) - (handle-file-features! client-features))] - - ;; NOTE: when file is migrated, we break the rule of no perform - ;; mutations on get operations and update the file with all - ;; migrations applied - (when (pmg/migrated? file) - (let [features (db/create-array conn "text" (:features file))] - (db/update! conn :file - {:data (blob/encode (:data file)) - :features features} - {:id id}) - (persist-pointers! conn id))) - - file)))) + (let [params (merge {:id id} + (when (some? project-id) + {:project-id project-id})) + file (-> (db/get conn :file params + {::db/check-deleted (not include-deleted?) + ::db/remove-deleted (not include-deleted?) + ::sql/for-update lock-for-update?}) + (decode-row))] + (if (and migrate? (fmg/need-migration? file)) + (migrate-file cfg file) + file))) (defn get-minimal-file - [{:keys [::db/pool] :as cfg} id] - (db/get pool :file {:id id} {:columns [:id :modified-at :revn]})) + [cfg id & {:as opts}] + (let [opts (assoc opts ::sql/columns [:id :modified-at :revn])] + (db/get cfg :file {:id id} opts))) (defn get-file-etag [{:keys [::rpc/profile-id]} {:keys [modified-at revn]}] @@ -369,27 +294,46 @@ {::doc/added "1.17" ::cond/get-object #(get-minimal-file %1 (:id %2)) ::cond/key-fn get-file-etag - ::sm/params ::get-file - ::sm/result ::file-with-permissions} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features project-id] :as params}] - (db/with-atomic [conn pool] - (let [perms (get-permissions conn profile-id id)] - (check-read-permissions! perms) - (let [file (-> (get-file conn id features project-id) - (assoc :permissions perms))] - (vary-meta file assoc ::cond/key (get-file-etag params file)))))) + ::sm/params schema:get-file + ::sm/result schema:file-with-permissions} + [cfg {:keys [::rpc/profile-id id project-id] :as params}] + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (let [perms (get-permissions conn profile-id id)] + (check-read-permissions! perms) + (let [team (teams/get-team conn + :profile-id profile-id + :project-id project-id + :file-id id) + file (-> (get-file cfg id :project-id project-id) + (assoc :permissions perms) + (check-version!)) + + _ (-> (cfeat/get-team-enabled-features cf/flags team) + (cfeat/check-client-features! (:features params)) + (cfeat/check-file-features! (:features file) (:features params))) + + ;; This operation is needed for backward comapatibility with frontends that + ;; does not support pointer-map resolution mechanism; this just resolves the + ;; pointers on backend and return a complete file. + file (if (and (contains? (:features file) "fdata/pointer-map") + (not (contains? (:features params) "fdata/pointer-map"))) + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] + (update file :data feat.fdata/process-pointers deref)) + file)] + + (vary-meta file assoc ::cond/key (get-file-etag params file))))))) ;; --- COMMAND QUERY: get-file-fragment (by id) -(sm/def! ::file-fragment +(def schema:file-fragment [:map {:title "FileFragment"} [:id ::sm/uuid] [:file-id ::sm/uuid] [:created-at ::dt/instant] [:content any?]]) -(sm/def! ::get-file-fragment +(def schema:get-file-fragment [:map {:title "get-file-fragment"} [:file-id ::sm/uuid] [:fragment-id ::sm/uuid] @@ -401,11 +345,12 @@ (update :content blob/decode))) (sv/defmethod ::get-file-fragment - "Retrieve a file by its ID. Only authenticated users." + "Retrieve a file fragment by its ID. Only authenticated users." {::doc/added "1.17" - ::sm/params ::get-file-fragment - ::sm/result ::file-fragment} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }] + ::rpc/auth false + ::sm/params schema:get-file-fragment + ::sm/result schema:file-fragment} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id]}] (dm/with-open [conn (db/open pool)] (let [perms (get-permissions conn profile-id file-id share-id)] (check-read-permissions! perms) @@ -424,7 +369,9 @@ f.is_shared, ft.media_id from file as f - left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn) + left join file_thumbnail as ft on (ft.file_id = f.id + and ft.revn = f.revn + and ft.deleted_at is null) where f.project_id = ? and f.deleted_at is null order by f.modified_at desc") @@ -439,27 +386,35 @@ (assoc :thumbnail-uri (resolve-public-uri media-id))) (dissoc row :media-id)))))) +(def schema:get-project-files + [:map {:title "get-project-files"} + [:project-id ::sm/uuid]]) + +(def schema:files + [:vector schema:file]) + (sv/defmethod ::get-project-files "Get all files for the specified project." {::doc/added "1.17" - ::sm/params [:map {:title "get-project-files"} - [:project-id ::sm/uuid]] - ::sm/result [:vector ::file]} + ::sm/params schema:get-project-files + ::sm/result schema:files} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id]}] (dm/with-open [conn (db/open pool)] (projects/check-read-permissions! conn profile-id project-id) (get-project-files conn project-id))) - ;; --- COMMAND QUERY: has-file-libraries (declare get-has-file-libraries) +(def schema:has-file-libraries + [:map {:title "has-file-libraries"} + [:file-id ::sm/uuid]]) + (sv/defmethod ::has-file-libraries "Checks if the file has libraries. Returns a boolean" {::doc/added "1.15.1" - ::sm/params [:map {:title "has-file-libraries"} - [:file-id ::sm/uuid]] + ::sm/params schema:has-file-libraries ::sm/result :boolean} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}] (dm/with-open [conn (db/open pool)] @@ -486,10 +441,17 @@ "Given the page data and the object-id returns the page data with all other not needed objects removed from the `:objects` data structure." - [{:keys [objects] :as page} object-id] - (let [objects (->> (cph/get-children-with-self objects object-id) - (filter some?))] - (assoc page :objects (d/index-by :id objects)))) + [page id-or-ids] + (update page :objects (fn [objects] + (reduce (fn [result object-id] + (->> (cfh/get-children-with-self objects object-id) + (filter some?) + (d/index-by :id) + (merge result))) + {} + (if (uuid? id-or-ids) + [id-or-ids] + id-or-ids))))) (defn- prune-thumbnails "Given the page data, removes the `:thumbnail` prop from all @@ -498,30 +460,42 @@ (update page :objects update-vals #(dissoc % :thumbnail))) (defn get-page - [conn {:keys [file-id page-id object-id features]}] + [{:keys [::db/conn] :as cfg} {:keys [profile-id file-id page-id object-id] :as params}] + (when (and (uuid? object-id) (not (uuid? page-id))) (ex/raise :type :validation :code :params-validation :hint "page-id is required when object-id is provided")) - (let [file (get-file conn file-id features) - page-id (or page-id (-> file :data :pages first)) - page (dm/get-in file [:data :pages-index page-id]) - page (if (pmap/pointer-map? page) + (let [team (teams/get-team conn + :profile-id profile-id + :file-id file-id) + + file (get-file cfg file-id) + + _ (-> (cfeat/get-team-enabled-features cf/flags team) + (cfeat/check-client-features! (:features params)) + (cfeat/check-file-features! (:features file) (:features params))) + + page (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)] + (let [page-id (or page-id (-> file :data :pages first)) + page (dm/get-in file [:data :pages-index page-id])] + (if (pmap/pointer-map? page) (deref page) - page)] + page)))] + (cond-> (prune-thumbnails page) - (uuid? object-id) + (some? object-id) (prune-objects object-id)))) -(sm/def! ::get-page - [:map {:title "GetPage"} +(def schema:get-page + [:map {:title "get-page"} [:file-id ::sm/uuid] [:page-id {:optional true} ::sm/uuid] [:share-id {:optional true} ::sm/uuid] - [:object-id {:optional true} ::sm/uuid] - [:features {:optional true} ::features]]) + [:object-id {:optional true} [:or ::sm/uuid ::sm/coll-of-uuid]] + [:features {:optional true} ::cfeat/features]]) (sv/defmethod ::get-page "Retrieves the page data from file and returns it. If no page-id is @@ -532,15 +506,15 @@ If you specify the object-id, the page-id parameter becomes mandatory. - Mainly used for rendering purposes." + Mainly used for rendering purposes on the exporter. It does not + accepts client features." {::doc/added "1.17" - ::sm/params ::get-page} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}] - (dm/with-open [conn (db/open pool)] - (let [perms (get-permissions conn profile-id file-id share-id)] - (check-read-permissions! perms) - (binding [pmap/*load-fn* (partial load-pointer conn file-id)] - (get-page conn params))))) + ::sm/params schema:get-page} + [cfg {:keys [::rpc/profile-id file-id share-id] :as params}] + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (check-read-permissions! conn profile-id file-id share-id) + (get-page cfg (assoc params :profile-id profile-id))))) ;; --- COMMAND QUERY: get-team-shared-files @@ -556,58 +530,57 @@ ft.media_id from file as f inner join project as p on (p.id = f.project_id) - left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn) + left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn and ft.deleted_at is null) where f.is_shared = true and f.deleted_at is null and p.deleted_at is null and p.team_id = ? order by f.modified_at desc") -(defn get-team-shared-files - [conn team-id] + +(defn- get-library-summary + [cfg {:keys [id data] :as file}] (letfn [(assets-sample [assets limit] - (let [sorted-assets (->> (vals assets) - (sort-by #(str/lower (:name %))))] - {:count (count sorted-assets) - :sample (into [] (take limit sorted-assets))})) + (let [sorted-assets (->> (vals assets) + (sort-by #(str/lower (:name %))))] + {:count (count sorted-assets) + :sample (into [] (take limit sorted-assets))}))] - (library-summary [{:keys [id data] :as file}] - (binding [pmap/*load-fn* (partial load-pointer conn id)] - (let [load-objects (fn [component] - (binding [pmap/*load-fn* (partial load-pointer conn id)] - (ctf/load-component-objects data component))) - components-sample (-> (assets-sample (ctkl/components data) 4) - (update :sample - #(map load-objects %)))] - {:components components-sample - :media (assets-sample (:media data) 3) - :colors (assets-sample (:colors data) 3) - :typographies (assets-sample (:typographies data) 3)})))] + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] + (let [load-objects (fn [component] + (ctf/load-component-objects data component)) + components-sample (-> (assets-sample (ctkl/components data) 4) + (update :sample #(mapv load-objects %)))] + {:components components-sample + :media (assets-sample (:media data) 3) + :colors (assets-sample (:colors data) 3) + :typographies (assets-sample (:typographies data) 3)})))) - (->> (db/exec! conn [sql:team-shared-files team-id]) - (into #{} (comp - (map decode-row) - (map (fn [row] - (if-let [media-id (:media-id row)] - (-> row - (dissoc :media-id) - (assoc :thumbnail-uri (resolve-public-uri media-id))) - (dissoc row :media-id)))) - (map #(assoc % :library-summary (library-summary %))) - (map #(dissoc % :data))))))) +(defn- get-team-shared-files + [{:keys [::db/conn] :as cfg} {:keys [team-id profile-id]}] + (teams/check-read-permissions! conn profile-id team-id) + (->> (db/exec! conn [sql:team-shared-files team-id]) + (into #{} (comp + (map decode-row) + (map (fn [row] + (if-let [media-id (:media-id row)] + (-> row + (dissoc :media-id) + (assoc :thumbnail-uri (resolve-public-uri media-id))) + (dissoc row :media-id)))) + (map #(assoc % :library-summary (get-library-summary cfg %))) + (map #(dissoc % :data)))))) -(s/def ::get-team-shared-files - (s/keys :req [::rpc/profile-id] - :req-un [::team-id])) +(def ^:private schema:get-team-shared-files + [:map {:title "get-team-shared-files"} + [:team-id ::sm/uuid]]) (sv/defmethod ::get-team-shared-files "Get all file (libraries) for the specified team." - {::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] - (dm/with-open [conn (db/open pool)] - (teams/check-read-permissions! conn profile-id team-id) - (get-team-shared-files conn team-id))) - + {::doc/added "1.17" + ::sm/params schema:get-team-shared-files} + [cfg {:keys [::rpc/profile-id] :as params}] + (db/tx-run! cfg get-team-shared-files (assoc params :profile-id profile-id))) ;; --- COMMAND QUERY: get-file-libraries @@ -639,17 +612,20 @@ [conn file-id] (into [] (comp + ;; FIXME: :is-indirect set to false to all rows looks + ;; completly useless (map #(assoc % :is-indirect false)) (map decode-row)) (db/exec! conn [sql:get-file-libraries file-id]))) -(s/def ::get-file-libraries - (s/keys :req [::rpc/profile-id] - :req-un [::file-id])) +(def ^:private schema:get-file-libraries + [:map {:title "get-file-libraries"} + [:file-id ::sm/uuid]]) (sv/defmethod ::get-file-libraries "Get libraries used by the specified file." - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:get-file-libraries} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}] (dm/with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) @@ -670,12 +646,14 @@ [conn file-id] (db/exec! conn [sql:library-using-files file-id])) -(s/def ::get-library-file-references - (s/keys :req [::rpc/profile-id] :req-un [::file-id])) +(def ^:private schema:get-library-file-references + [:map {:title "get-library-file-references"} + [:file-id ::sm/uuid]]) (sv/defmethod ::get-library-file-references "Returns all the file references that use specified file (library) id." - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:get-library-file-references} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (dm/with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) @@ -696,7 +674,9 @@ row_number() over w as row_num from file as f inner join project as p on (p.id = f.project_id) - left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn) + left join file_thumbnail as ft on (ft.file_id = f.id + and ft.revn = f.revn + and ft.deleted_at is null) where p.team_id = ? and p.deleted_at is null and f.deleted_at is null @@ -715,17 +695,49 @@ (assoc :thumbnail-uri (resolve-public-uri media-id))) (dissoc row :media-id)))))) -(s/def ::get-team-recent-files - (s/keys :req [::rpc/profile-id] - :req-un [::team-id])) +(def ^:private schema:get-team-recent-files + [:map {:title "get-team-recent-files"} + [:team-id ::sm/uuid]]) (sv/defmethod ::get-team-recent-files - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:get-team-recent-files} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] (dm/with-open [conn (db/open pool)] (teams/check-read-permissions! conn profile-id team-id) (get-team-recent-files conn team-id))) + +;; --- COMMAND QUERY: get-file-summary + +(defn- get-file-summary + [{:keys [::db/conn] :as cfg} {:keys [profile-id id project-id] :as params}] + (check-read-permissions! conn profile-id id) + (let [team (teams/get-team conn + :profile-id profile-id + :project-id project-id + :file-id id) + + file (get-file cfg id :project-id project-id)] + + (-> (cfeat/get-team-enabled-features cf/flags team) + (cfeat/check-client-features! (:features params)) + (cfeat/check-file-features! (:features file) (:features params))) + + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] + {:name (:name file) + :components-count (count (ctkl/components-seq (:data file))) + :graphics-count (count (get-in file [:data :media] [])) + :colors-count (count (get-in file [:data :colors] [])) + :typography-count (count (get-in file [:data :typographies] []))}))) + +(sv/defmethod ::get-file-summary + "Retrieve a file summary by its ID. Only authenticated users." + {::doc/added "1.20" + ::sm/params schema:get-file} + [cfg {:keys [::rpc/profile-id] :as params}] + (db/tx-run! cfg get-file-summary (assoc params :profile-id profile-id))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MUTATION COMMANDS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -737,7 +749,8 @@ (db/update! conn :file {:name name :modified-at (dt/now)} - {:id id})) + {:id id} + {::db/return-keys true})) (sv/defmethod ::rename-file {::doc/added "1.17" @@ -775,96 +788,180 @@ ;; --- MUTATION COMMAND: set-file-shared -(defn unlink-files - [conn {:keys [id] :as params}] - (db/delete! conn :file-library-rel {:library-file-id id})) - -(defn set-file-shared - [conn {:keys [id is-shared] :as params}] - (db/update! conn :file - {:is-shared is-shared} - {:id id})) - (def sql:get-referenced-files "SELECT f.id FROM file_library_rel AS flr INNER JOIN file AS f ON (f.id = flr.file_id) WHERE flr.library_file_id = ? + AND (f.deleted_at IS NULL OR f.deleted_at > now()) ORDER BY f.created_at ASC;") -(defn absorb-library +(defn- absorb-library-by-file! + [cfg ldata file-id] + + (dm/assert! + "expected cfg with valid connection" + (db/connection-map? cfg)) + + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id) + pmap/*tracked* (pmap/create-tracked)] + (let [file (-> (get-file cfg file-id + :include-deleted? true + :lock-for-update? true) + (update :data ctf/absorb-assets ldata))] + + (l/trc :hint "library absorbed" + :library-id (str (:id ldata)) + :file-id (str file-id)) + + (db/update! cfg :file + {:revn (inc (:revn file)) + :data (blob/encode (:data file)) + :modified-at (dt/now)} + {:id file-id}) + + (feat.fdata/persist-pointers! cfg file-id)))) + +(defn- absorb-library! "Find all files using a shared library, and absorb all library assets into the file local libraries" - [conn {:keys [id] :as params}] - (let [library (db/get-by-id conn :file id)] - (when (:is-shared library) - (let [ldata (binding [pmap/*load-fn* (partial load-pointer conn id)] - (-> library decode-row load-all-pointers! pmg/migrate-file :data)) - rows (db/exec! conn [sql:get-referenced-files id])] - (doseq [file-id (map :id rows)] - (binding [pmap/*load-fn* (partial load-pointer conn file-id) - pmap/*tracked* (atom {})] - (let [file (-> (db/get-by-id conn :file file-id - ::db/check-deleted? false - ::db/remove-deleted? false) - (decode-row) - (load-all-pointers!) - (pmg/migrate-file)) - data (ctf/absorb-assets (:data file) ldata)] - (db/update! conn :file - {:revn (inc (:revn file)) - :data (blob/encode data) - :modified-at (dt/now)} - {:id file-id}) - (persist-pointers! conn file-id)))))))) + [cfg {:keys [id] :as library}] -(s/def ::set-file-shared - (s/keys :req [::rpc/profile-id] - :req-un [::id ::is-shared])) + (dm/assert! + "expected cfg with valid connection" + (db/connection-map? cfg)) + + (let [ldata (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] + (-> library :data (feat.fdata/process-pointers deref))) + ids (->> (db/exec! cfg [sql:get-referenced-files id]) + (map :id))] + + (l/trc :hint "absorbing library" + :library-id (str id) + :files (str/join "," (map str ids))) + + (run! (partial absorb-library-by-file! cfg ldata) ids))) + +(defn- set-file-shared + [{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}] + (check-edition-permissions! conn profile-id id) + (let [file (db/get-by-id conn :file id {:columns [:id :name :is-shared]}) + file (cond + (and (true? (:is-shared file)) + (false? (:is-shared params))) + ;; When we disable shared flag on an already shared + ;; file, we need to perform more complex operation, + ;; so in this case we retrieve the complete file and + ;; perform all required validations. + (let [file (-> (get-file cfg id :lock-for-update? true) + (check-version!) + (assoc :is-shared false)) + team (teams/get-team conn + :profile-id profile-id + :project-id (:project-id file))] + + (-> (cfeat/get-team-enabled-features cf/flags team) + (cfeat/check-client-features! (:features params)) + (cfeat/check-file-features! (:features file))) + + (absorb-library! cfg file) + + (db/delete! conn :file-library-rel {:library-file-id id}) + (db/update! conn :file + {:is-shared false + :modified-at (dt/now)} + {:id id}) + file) + + (and (false? (:is-shared file)) + (true? (:is-shared params))) + (let [file (assoc file :is-shared true)] + (db/update! conn :file + {:is-shared true + :modified-at (dt/now)} + {:id id}) + file) + + :else + (ex/raise :type :validation + :code :invalid-shared-state + :hint "unexpected state found" + :params-is-shared (:is-shared params) + :file-is-shared (:is-shared file)))] + + (rph/with-meta + (select-keys file [:id :name :is-shared]) + {::audit/props {:name (:name file) + :project-id (:project-id file) + :is-shared (:is-shared file)}}))) + +(def ^:private + schema:set-file-shared + (sm/define + [:map {:title "set-file-shared"} + [:id ::sm/uuid] + [:is-shared :boolean]])) (sv/defmethod ::set-file-shared {::doc/added "1.17" - ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id is-shared] :as params}] - (db/with-atomic [conn pool] - (check-edition-permissions! conn profile-id id) - (when-not is-shared - (absorb-library conn params) - (unlink-files conn params)) - - (let [file (set-file-shared conn params)] - (rph/with-meta - (select-keys file [:id :name :is-shared]) - {::audit/props {:name (:name file) - :project-id (:project-id file) - :is-shared (:is-shared file)}})))) + ::webhooks/event? true + ::sm/params schema:set-file-shared} + [cfg {:keys [::rpc/profile-id] :as params}] + (db/tx-run! cfg set-file-shared (assoc params :profile-id profile-id))) ;; --- MUTATION COMMAND: delete-file -(defn mark-file-deleted - [conn {:keys [id] :as params}] +(defn- mark-file-deleted! + [conn file-id] (db/update! conn :file {:deleted-at (dt/now)} - {:id id})) + {:id file-id} + {::db/return-keys [:id :name :is-shared :project-id :created-at :modified-at]})) -(s/def ::delete-file - (s/keys :req [::rpc/profile-id] - :req-un [::id])) +(def ^:private + schema:delete-file + (sm/define + [:map {:title "delete-file"} + [:id ::sm/uuid]])) + +(defn- delete-file + [{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}] + (check-edition-permissions! conn profile-id id) + (let [file (mark-file-deleted! conn id)] + + ;; NOTE: when a file is a shared library, then we proceed to load + ;; the whole file, proceed with feature checking and properly execute + ;; the absorb-library procedure + (when (:is-shared file) + (let [file (-> (get-file cfg id + :lock-for-update? true + :include-deleted? true) + (check-version!)) + + team (teams/get-team conn + :profile-id profile-id + :project-id (:project-id file))] + + + + (-> (cfeat/get-team-enabled-features cf/flags team) + (cfeat/check-client-features! (:features params)) + (cfeat/check-file-features! (:features file))) + + (absorb-library! cfg file))) + + (rph/with-meta (rph/wrap) + {::audit/props {:project-id (:project-id file) + :name (:name file) + :created-at (:created-at file) + :modified-at (:modified-at file)}}))) (sv/defmethod ::delete-file {::doc/added "1.17" - ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] - (db/with-atomic [conn pool] - (check-edition-permissions! conn profile-id id) - (absorb-library conn params) - (let [file (mark-file-deleted conn params)] - - (rph/with-meta (rph/wrap) - {::audit/props {:project-id (:project-id file) - :name (:name file) - :created-at (:created-at file) - :modified-at (:modified-at file)}})))) + ::webhooks/event? true + ::sm/params schema:delete-file} + [cfg {:keys [::rpc/profile-id] :as params}] + (db/tx-run! cfg delete-file (assoc params :profile-id profile-id))) ;; --- MUTATION COMMAND: link-file-to-library @@ -877,13 +974,17 @@ [conn {:keys [file-id library-id] :as params}] (db/exec-one! conn [sql:link-file-to-library file-id library-id])) -(s/def ::link-file-to-library - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::library-id])) +(def ^:private + schema:link-file-to-library + (sm/define + [:map {:title "link-file-to-library"} + [:file-id ::sm/uuid] + [:library-id ::sm/uuid]])) (sv/defmethod ::link-file-to-library {::doc/added "1.17" - ::webhooks/event? true} + ::webhooks/event? true + ::sm/params schema:link-file-to-library} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}] (when (= file-id library-id) (ex/raise :type :validation @@ -902,18 +1003,20 @@ {:file-id file-id :library-file-id library-id})) -(s/def ::unlink-file-from-library - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::library-id])) +(def ^:private schema:unlink-file-to-library + [:map {:title "unlink-file-to-library"} + [:file-id ::sm/uuid] + [:library-id ::sm/uuid]]) (sv/defmethod ::unlink-file-from-library {::doc/added "1.17" - ::webhooks/event? true} + ::webhooks/event? true + ::sm/params schema:unlink-file-to-library} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) - (unlink-file-from-library conn params))) - + (unlink-file-from-library conn params) + nil)) ;; --- MUTATION COMMAND: update-sync @@ -922,17 +1025,18 @@ (db/update! conn :file-library-rel {:synced-at (dt/now)} {:file-id file-id - :library-file-id library-id})) + :library-file-id library-id} + {::db/return-keys true})) -(s/def ::update-file-library-sync-status - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::library-id])) - -;; TODO: improve naming +(def ^:private schema:update-file-library-sync-status + [:map {:title "update-file-library-sync-status"} + [:file-id ::sm/uuid] + [:library-id ::sm/uuid]]) (sv/defmethod ::update-file-library-sync-status "Update the synchronization status of a file->library link" - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:update-file-library-sync-status} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) @@ -944,8 +1048,10 @@ (defn ignore-sync [conn {:keys [file-id date] :as params}] (db/update! conn :file - {:ignore-sync-until date} - {:id file-id})) + {:ignore-sync-until date + :modified-at (dt/now)} + {:id file-id} + {::db/return-keys true})) (s/def ::ignore-file-library-sync-status (s/keys :req [::rpc/profile-id] diff --git a/backend/src/app/rpc/commands/files_create.clj b/backend/src/app/rpc/commands/files_create.clj index 8f279d5773..cc15830d47 100644 --- a/backend/src/app/rpc/commands/files_create.clj +++ b/backend/src/app/rpc/commands/files_create.clj @@ -7,24 +7,28 @@ (ns app.rpc.commands.files-create (:require [app.common.data :as d] - [app.common.files.features :as ffeat] + [app.common.data.macros :as dm] + [app.common.features :as cfeat] + [app.common.files.defaults :refer [version]] + [app.common.schema :as sm] [app.common.types.file :as ctf] [app.common.uuid :as uuid] + [app.config :as cf] [app.db :as db] + [app.features.fdata :as feat.fdata] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] [app.rpc :as-alias rpc] - [app.rpc.commands.files :as files] [app.rpc.commands.projects :as projects] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.permissions :as perms] [app.rpc.quotes :as quotes] [app.util.blob :as blob] - [app.util.objects-map :as omap] [app.util.pointer-map :as pmap] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s])) + [clojure.set :as set])) (defn create-file-role! [conn {:keys [file-id profile-id role]}] @@ -34,75 +38,117 @@ (db/insert! conn :file-profile-rel)))) (defn create-file - [conn {:keys [id name project-id is-shared revn - modified-at deleted-at create-page - ignore-sync-until features] - :or {is-shared false revn 0 create-page true} - :as params}] + [{:keys [::db/conn] :as cfg} + {:keys [id name project-id is-shared revn + modified-at deleted-at create-page + ignore-sync-until features] + :or {is-shared false revn 0 create-page true} + :as params}] - (let [id (or id (uuid/next)) - features (->> features - (into (files/get-default-features)) - (files/check-features-compatibility!)) + (dm/assert! + "expected a valid connection" + (db/connection? conn)) - pointers (atom {}) - data (binding [pmap/*tracked* pointers - ffeat/*current* features - ffeat/*wrap-with-objects-map-fn* (if (features "storate/objects-map") omap/wrap identity) - ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity)] - (if create-page + (binding [pmap/*tracked* (pmap/create-tracked) + cfeat/*current* features] + (let [id (or id (uuid/next)) + + data (if create-page (ctf/make-file-data id) - (ctf/make-file-data id nil))) + (ctf/make-file-data id nil)) - features (db/create-array conn "text" features) - file (db/insert! conn :file - (d/without-nils - {:id id - :project-id project-id - :name name - :revn revn - :is-shared is-shared - :data (blob/encode data) - :features features - :ignore-sync-until ignore-sync-until - :modified-at modified-at - :deleted-at deleted-at}))] + file {:id id + :project-id project-id + :name name + :revn revn + :is-shared is-shared + :version version + :data data + :features features + :ignore-sync-until ignore-sync-until + :modified-at modified-at + :deleted-at deleted-at} - (binding [pmap/*tracked* pointers] - (files/persist-pointers! conn id)) + file (if (contains? features "fdata/objects-map") + (feat.fdata/enable-objects-map file) + file) - (->> (assoc params :file-id id :role :owner) - (create-file-role! conn)) + file (if (contains? features "fdata/pointer-map") + (feat.fdata/enable-pointer-map file) + file) - (db/update! conn :project - {:modified-at (dt/now)} - {:id project-id}) + file (d/without-nils file)] - (files/decode-row file))) + (db/insert! conn :file + (-> file + (update :data blob/encode) + (update :features db/encode-pgarray conn "text")) + {::db/return-keys false}) -(s/def ::create-file - (s/keys :req [::rpc/profile-id] - :req-un [::files/name - ::files/project-id] - :opt-un [::files/id - ::files/is-shared - ::files/features])) + (when (contains? features "fdata/pointer-map") + (feat.fdata/persist-pointers! cfg id)) + + (->> (assoc params :file-id id :role :owner) + (create-file-role! conn)) + + (db/update! conn :project + {:modified-at (dt/now)} + {:id project-id}) + + file))) + +(def ^:private schema:create-file + [:map {:title "create-file"} + [:name :string] + [:project-id ::sm/uuid] + [:id {:optional true} ::sm/uuid] + [:is-shared {:optional true} :boolean] + [:features {:optional true} ::cfeat/features]]) (sv/defmethod ::create-file {::doc/added "1.17" ::doc/module :files - ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] - (db/with-atomic [conn pool] - (projects/check-edition-permissions! conn profile-id project-id) - (let [team-id (files/get-team-id conn project-id) - params (assoc params :profile-id profile-id)] + ::webhooks/event? true + ::sm/params schema:create-file} + [cfg {:keys [::rpc/profile-id project-id] :as params}] + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (projects/check-edition-permissions! conn profile-id project-id) + (let [team (teams/get-team conn + :profile-id profile-id + :project-id project-id) + team-id (:id team) - (run! (partial quotes/check-quote! conn) - (list {::quotes/id ::quotes/files-per-project - ::quotes/team-id team-id - ::quotes/profile-id profile-id - ::quotes/project-id project-id})) + ;; When we create files, we only need to respect the team + ;; features, because some features can be enabled + ;; globally, but the team is still not migrated properly. + features (-> (cfeat/get-team-enabled-features cf/flags team) + (cfeat/check-client-features! (:features params))) - (-> (create-file conn params) - (vary-meta assoc ::audit/props {:team-id team-id}))))) + ;; We also include all no migration features declared by + ;; client; that enables the ability to enable a runtime + ;; feature on frontend and make it permanent on file + features (-> (:features params #{}) + (set/intersection cfeat/no-migration-features) + (set/union features)) + + params (-> params + (assoc :profile-id profile-id) + (assoc :features features))] + + (run! (partial quotes/check-quote! conn) + (list {::quotes/id ::quotes/files-per-project + ::quotes/team-id team-id + ::quotes/profile-id profile-id + ::quotes/project-id project-id})) + + ;; When newly computed features does not match exactly with + ;; the features defined on team row, we update it. + (when (not= features (:features team)) + (let [features (db/create-array conn "text" features)] + (db/update! conn :team + {:features features} + {:id team-id}))) + + (-> (create-file cfg params) + (vary-meta assoc ::audit/props {:team-id team-id})))))) diff --git a/backend/src/app/rpc/commands/files_snapshot.clj b/backend/src/app/rpc/commands/files_snapshot.clj new file mode 100644 index 0000000000..1e9c3081a5 --- /dev/null +++ b/backend/src/app/rpc/commands/files_snapshot.clj @@ -0,0 +1,191 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.rpc.commands.files-snapshot + (:require + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.common.schema :as sm] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.db.sql :as-alias sql] + [app.main :as-alias main] + [app.media :as media] + [app.rpc :as-alias rpc] + [app.rpc.commands.files :as files] + [app.rpc.commands.profile :as profile] + [app.rpc.doc :as-alias doc] + [app.storage :as sto] + [app.util.services :as sv] + [app.util.time :as dt] + [cuerdas.core :as str])) + +(defn check-authorized! + [{:keys [::db/pool]} profile-id] + (when-not (or (= "devenv" (cf/get :host)) + (let [profile (ex/ignoring (profile/get-profile pool profile-id)) + admins (or (cf/get :admins) #{})] + (contains? admins (:email profile)))) + (ex/raise :type :authentication + :code :authentication-required + :hint "only admins allowed"))) + +(defn get-file-snapshots + [{:keys [::db/conn]} {:keys [file-id limit start-at] + :or {limit Long/MAX_VALUE}}] + (let [query (str "select id, label, revn, created_at " + " from file_change " + " where file_id = ? " + " and created_at < ? " + " and data is not null " + " order by created_at desc " + " limit ?") + start-at (or start-at (dt/now)) + limit (min limit 20)] + + (->> (db/exec! conn [query file-id start-at limit]) + (mapv (fn [row] + (update row :created-at dt/format-instant :rfc1123)))))) + +(def ^:private schema:get-file-snapshots + [:map [:file-id ::sm/uuid]]) + +(sv/defmethod ::get-file-snapshots + {::doc/added "1.20" + ::doc/skip true + ::sm/params schema:get-file-snapshots} + [cfg {:keys [::rpc/profile-id] :as params}] + (check-authorized! cfg profile-id) + (db/run! cfg get-file-snapshots params)) + +(defn restore-file-snapshot! + [{:keys [::db/conn ::sto/storage] :as cfg} {:keys [file-id id]}] + (let [storage (media/configure-assets-storage storage conn) + file (files/get-minimal-file conn file-id {::db/for-update true}) + snapshot (db/get* conn :file-change + {:file-id file-id + :id id} + {::db/for-share true})] + + (when-not snapshot + (ex/raise :type :not-found + :code :snapshot-not-found + :hint "unable to find snapshot with the provided label" + :id id + :file-id file-id)) + + (when-not (:data snapshot) + (ex/raise :type :precondition + :code :snapshot-without-data + :hint "snapshot has no data" + :label (:label snapshot) + :file-id file-id)) + + (l/dbg :hint "restoring snapshot" + :file-id (str file-id) + :label (:label snapshot) + :snapshot-id (str (:id snapshot))) + + (db/update! conn :file + {:data (:data snapshot) + :revn (inc (:revn file)) + :features (:features snapshot)} + {:id file-id}) + + ;; clean object thumbnails + (let [sql (str "update file_tagged_object_thumbnail " + " set deleted_at = now() " + " where file_id=? returning media_id") + res (db/exec! conn [sql file-id])] + + (doseq [media-id (into #{} (keep :media-id) res)] + (sto/touch-object! storage media-id))) + + ;; clean object thumbnails + (let [sql (str "update file_thumbnail " + " set deleted_at = now() " + " where file_id=? returning media_id") + res (db/exec! conn [sql file-id])] + (doseq [media-id (into #{} (keep :media-id) res)] + (sto/touch-object! storage media-id))) + + {:id (:id snapshot) + :label (:label snapshot)})) + +(defn- resolve-snapshot-by-label + [conn file-id label] + (->> (db/query conn :file-change + {:file-id file-id + :label label} + {::sql/order-by [[:created-at :desc]] + ::sql/columns [:file-id :id :label]}) + (first))) + +(def ^:private + schema:restore-file-snapshot + [:and + [:map + [:file-id ::sm/uuid] + [:id {:optional true} ::sm/uuid] + [:label {:optional true} :string]] + [::sm/contains-any #{:id :label}]]) + +(sv/defmethod ::restore-file-snapshot + {::doc/added "1.20" + ::doc/skip true + ::sm/params schema:restore-file-snapshot} + [cfg {:keys [::rpc/profile-id file-id id label] :as params}] + (check-authorized! cfg profile-id) + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (let [params (cond-> params + (and (not id) (string? label)) + (merge (resolve-snapshot-by-label conn file-id label)))] + (restore-file-snapshot! cfg params))))) + +(defn take-file-snapshot! + [cfg {:keys [file-id label]}] + (let [conn (db/get-connection cfg) + file (db/get conn :file {:id file-id}) + id (uuid/next)] + + (l/debug :hint "creating file snapshot" + :file-id (str file-id) + :label label) + + (db/insert! conn :file-change + {:id id + :revn (:revn file) + :data (:data file) + :features (:features file) + :file-id (:id file) + :label label} + {::db/return-keys false}) + + {:id id :label label})) + +(defn generate-snapshot-label + [] + (let [ts (-> (dt/now) + (dt/format-instant) + (str/replace #"[T:\.]" "-") + (str/rtrim "Z"))] + (str "snapshot-" ts))) + +(def ^:private schema:take-file-snapshot + [:map [:file-id ::sm/uuid]]) + +(sv/defmethod ::take-file-snapshot + {::doc/added "1.20" + ::doc/skip true + ::sm/params schema:take-file-snapshot} + [cfg {:keys [::rpc/profile-id] :as params}] + (check-authorized! cfg profile-id) + (db/tx-run! cfg (fn [cfg] + (let [params (update params :label (fn [label] + (or label (generate-snapshot-label))))] + (take-file-snapshot! cfg params))))) + diff --git a/backend/src/app/rpc/commands/files_temp.clj b/backend/src/app/rpc/commands/files_temp.clj index 85902a0faf..bc183cfd95 100644 --- a/backend/src/app/rpc/commands/files_temp.clj +++ b/backend/src/app/rpc/commands/files_temp.clj @@ -7,105 +7,165 @@ (ns app.rpc.commands.files-temp (:require [app.common.exceptions :as ex] - [app.common.pages :as cp] - [app.common.spec :as us] + [app.common.features :as cfeat] + [app.common.files.changes :as cpc] + [app.common.schema :as sm] [app.common.uuid :as uuid] + [app.config :as cf] [app.db :as db] + [app.db.sql :as sql] + [app.features.components-v2 :as feat.compv2] + [app.features.fdata :as fdata] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] - [app.rpc.commands.files-create :refer [create-file]] + [app.rpc.commands.files-create :as files.create] [app.rpc.commands.files-update :as-alias files.update] [app.rpc.commands.projects :as projects] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.util.blob :as blob] + [app.util.pointer-map :as pmap] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s])) + [clojure.set :as set])) ;; --- MUTATION COMMAND: create-temp-file -(s/def ::create-page ::us/boolean) - -(s/def ::create-temp-file - (s/keys :req [::rpc/profile-id] - :req-un [::files/name - ::files/project-id] - :opt-un [::files/id - ::files/is-shared - ::files/features - ::create-page])) +(def ^:private schema:create-temp-file + [:map {:title "create-temp-file"} + [:name :string] + [:project-id ::sm/uuid] + [:id {:optional true} ::sm/uuid] + [:is-shared :boolean] + [:features ::cfeat/features] + [:create-page :boolean]]) (sv/defmethod ::create-temp-file {::doc/added "1.17" - ::doc/module :files} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] - (db/with-atomic [conn pool] - (projects/check-edition-permissions! conn profile-id project-id) - (create-file conn (assoc params :profile-id profile-id :deleted-at (dt/in-future {:days 1}))))) + ::doc/module :files + ::sm/params schema:create-temp-file} + [cfg {:keys [::rpc/profile-id project-id] :as params}] + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (projects/check-edition-permissions! conn profile-id project-id) + (let [team (teams/get-team conn :profile-id profile-id :project-id project-id) + + ;; When we create files, we only need to respect the team + ;; features, because some features can be enabled + ;; globally, but the team is still not migrated properly. + input-features (:features params #{}) + + ;; If the imported project doesn't contain v2 we need to remove it + team-features + (cond-> (cfeat/get-team-enabled-features cf/flags team) + (not (contains? input-features "components/v2")) + (disj "components/v2")) + + + ;; We also include all no migration features declared by + ;; client; that enables the ability to enable a runtime + ;; feature on frontend and make it permanent on file + features (-> input-features + (set/intersection cfeat/no-migration-features) + (set/union team-features)) + + params (-> params + (assoc :profile-id profile-id) + (assoc :deleted-at (dt/in-future {:days 1})) + (assoc :features features))] + + (files.create/create-file cfg params))))) ;; --- MUTATION COMMAND: update-temp-file -(defn update-temp-file - [conn {:keys [profile-id session-id id revn changes] :as params}] - (db/insert! conn :file-change - {:id (uuid/next) - :session-id session-id - :profile-id profile-id - :created-at (dt/now) - :file-id id - :revn revn - :data nil - :changes (blob/encode changes)})) -(s/def ::update-temp-file - (s/keys :req [::rpc/profile-id] - :req-un [::files.update/changes - ::files.update/revn - ::files.update/session-id - ::files/id])) +(def ^:private schema:update-temp-file + [:map {:title "update-temp-file"} + [:changes [:vector ::cpc/change]] + [:revn {:min 0} :int] + [:session-id ::sm/uuid] + [:id ::sm/uuid]]) (sv/defmethod ::update-temp-file {::doc/added "1.17" - ::doc/module :files} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] - (db/with-atomic [conn pool] - (update-temp-file conn (assoc params :profile-id profile-id)) - nil)) + ::doc/module :files + ::sm/params schema:update-temp-file} + [cfg {:keys [::rpc/profile-id session-id id revn changes] :as params}] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (db/insert! conn :file-change + {:id (uuid/next) + :session-id session-id + :profile-id profile-id + :created-at (dt/now) + :file-id id + :revn revn + :data nil + :changes (blob/encode changes)}) + nil))) ;; --- MUTATION COMMAND: persist-temp-file (defn persist-temp-file - [conn {:keys [id] :as params}] - (let [file (db/get-by-id conn :file id) - revs (db/query conn :file-change - {:file-id id} - {:order-by [[:revn :asc]]}) - revn (count revs)] + [{:keys [::db/conn] :as cfg} {:keys [id ::rpc/profile-id] :as params}] + (let [file (files/get-file cfg id + :migrate? false + :lock-for-update? true)] (when (nil? (:deleted-at file)) (ex/raise :type :validation :code :cant-persist-already-persisted-file)) - (let [data - (->> revs - (mapcat #(->> % :changes blob/decode)) - (cp/process-changes (blob/decode (:data file))))] + (let [changes (->> (db/cursor conn + (sql/select :file-change {:file-id id} + {:order-by [[:revn :asc]]}) + {:chunk-size 10}) + (sequence (mapcat (comp blob/decode :changes)))) + + file (update file :data cpc/process-changes changes) + + file (if (contains? (:features file) "fdata/objects-map") + (fdata/enable-objects-map file) + file) + + file (if (contains? (:features file) "fdata/pointer-map") + (binding [pmap/*tracked* (pmap/create-tracked)] + (let [file (fdata/enable-pointer-map file)] + (fdata/persist-pointers! cfg id) + file)) + file)] + + ;; Delete changes from the changes history + (db/delete! conn :file-change {:file-id id}) + (db/update! conn :file {:deleted-at nil - :revn revn - :data (blob/encode data)} - {:id id})) - nil)) + :revn 1 + :data (blob/encode (:data file))} + {:id id}) -(s/def ::persist-temp-file - (s/keys :req [::rpc/profile-id] - :req-un [::files/id])) + (let [team (teams/get-team conn :profile-id profile-id :project-id (:project-id file)) + file-features (:features file) + team-features (cfeat/get-team-enabled-features cf/flags team)] + (when (and (contains? team-features "components/v2") + (not (contains? file-features "components/v2"))) + ;; Migrate components v2 + (feat.compv2/migrate-file! cfg + (:id file) + :max-procs 2 + :validate? true + :throw-on-validate? true))) + + nil))) + +(def ^:private schema:persist-temp-file + [:map {:title "persist-temp-file"} + [:id ::sm/uuid]]) (sv/defmethod ::persist-temp-file {::doc/added "1.17" - ::doc/module :files} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] - (db/with-atomic [conn pool] - (files/check-edition-permissions! conn profile-id id) - (persist-temp-file conn params))) + ::doc/module :files + ::sm/params schema:persist-temp-file} + [cfg {:keys [::rpc/profile-id id] :as params}] + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (files/check-edition-permissions! conn profile-id id) + (persist-temp-file cfg params)))) diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj index 499592e75a..a44a8bdbd5 100644 --- a/backend/src/app/rpc/commands/files_thumbnails.clj +++ b/backend/src/app/rpc/commands/files_thumbnails.clj @@ -8,22 +8,27 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.exceptions :as ex] + [app.common.features :as cfeat] + [app.common.files.helpers :as cfh] + [app.common.files.migrations :as fmg] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] [app.common.schema :as sm] - [app.common.spec :as us] + [app.common.thumbnails :as thc] [app.common.types.shape-tree :as ctt] + [app.config :as cf] [app.db :as db] - [app.db.sql :as sql] + [app.db.sql :as-alias sql] + [app.features.fdata :as feat.fdata] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] [app.media :as media] [app.rpc :as-alias rpc] + [app.rpc.climit :as-alias climit] [app.rpc.commands.files :as files] + [app.rpc.commands.teams :as teams] [app.rpc.cond :as-alias cond] [app.rpc.doc :as-alias doc] - [app.rpc.helpers :as rph] + [app.rpc.retry :as rtry] [app.storage :as sto] [app.util.pointer-map :as pmap] [app.util.services :as sv] @@ -38,88 +43,60 @@ ;; --- COMMAND QUERY: get-file-object-thumbnails +(defn- get-object-thumbnails-by-tag + [conn file-id tag] + (let [sql (str/concat + "select object_id, media_id, tag " + " from file_tagged_object_thumbnail" + " where file_id=? and tag=? and deleted_at is null") + res (db/exec! conn [sql file-id tag])] + (->> res + (d/index-by :object-id (fn [row] + (files/resolve-public-uri (:media-id row)))) + (d/without-nils)))) + (defn- get-object-thumbnails ([conn file-id] (let [sql (str/concat - "select object_id, data, media_id " - " from file_object_thumbnail" - " where file_id=?") + "select object_id, media_id, tag " + " from file_tagged_object_thumbnail" + " where file_id=? and deleted_at is null") res (db/exec! conn [sql file-id])] (->> res (d/index-by :object-id (fn [row] - (or (some-> row :media-id files/resolve-public-uri) - (:data row)))) + (files/resolve-public-uri (:media-id row)))) (d/without-nils)))) ([conn file-id object-ids] (let [sql (str/concat - "select object_id, data, media_id " - " from file_object_thumbnail" - " where file_id=? and object_id = ANY(?)") + "select object_id, media_id, tag " + " from file_tagged_object_thumbnail" + " where file_id=? and object_id = ANY(?) and deleted_at is null") ids (db/create-array conn "text" (seq object-ids)) res (db/exec! conn [sql file-id ids])] - (d/index-by :object-id - (fn [row] - (or (some-> row :media-id files/resolve-public-uri) - (:data row))) - res)))) + + (->> res + (d/index-by :object-id (fn [row] + (files/resolve-public-uri (:media-id row)))) + (d/without-nils))))) (sv/defmethod ::get-file-object-thumbnails "Retrieve a file object thumbnails." {::doc/added "1.17" ::doc/module :files ::sm/params [:map {:title "get-file-object-thumbnails"} - [:file-id ::sm/uuid]] + [:file-id ::sm/uuid] + [:tag {:optional true} :string]] ::sm/result [:map-of :string :string] ::cond/get-object #(files/get-minimal-file %1 (:file-id %2)) ::cond/reuse-key? true ::cond/key-fn files/get-file-etag} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id tag] :as params}] (dm/with-open [conn (db/open pool)] (files/check-read-permissions! conn profile-id file-id) - (get-object-thumbnails conn file-id))) - -;; --- COMMAND QUERY: get-file-thumbnail - -(defn get-file-thumbnail - [conn file-id revn] - (let [sql (sql/select :file-thumbnail - (cond-> {:file-id file-id} - revn (assoc :revn revn)) - {:limit 1 - :order-by [[:revn :desc]]}) - row (db/exec-one! conn sql)] - - (when-not row - (ex/raise :type :not-found - :code :file-thumbnail-not-found)) - - (when-not (:data row) - (ex/raise :type :not-found - :code :file-thumbnail-not-found)) - - {:data (:data row) - :props (some-> (:props row) db/decode-transit-pgobject) - :revn (:revn row) - :file-id (:file-id row)})) - -(s/def ::revn ::us/integer) -(s/def ::file-id ::us/uuid) - -(s/def ::get-file-thumbnail - (s/keys :req [::rpc/profile-id] - :req-un [::file-id] - :opt-un [::revn])) - -(sv/defmethod ::get-file-thumbnail - {::doc/added "1.17" - ::doc/module :files - ::doc/deprecated "1.19"} - [{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}] - (dm/with-open [conn (db/open pool)] - (files/check-read-permissions! conn profile-id file-id) - (-> (get-file-thumbnail conn file-id revn) - (rph/with-http-cache long-cache-duration)))) + (if tag + (get-object-thumbnails-by-tag conn file-id tag) + (get-object-thumbnails conn file-id)))) ;; --- COMMAND QUERY: get-file-data-for-thumbnail @@ -127,25 +104,13 @@ ;; loading all pages into memory for find the frame set for thumbnail. (defn get-file-data-for-thumbnail - [conn {:keys [data id] :as file}] + [{:keys [::db/conn] :as cfg} {:keys [data id] :as file}] (letfn [;; function responsible on finding the frame marked to be ;; used as thumbnail; the returned frame always have ;; the :page-id set to the page that it belongs. - - (get-thumbnail-frame [data] - ;; NOTE: this is a hack for avoid perform blocking - ;; operation inside the for loop, clojure lazy-seq uses - ;; synchronized blocks that does not plays well with - ;; virtual threads, so we need to perform the load - ;; operation first. This operation forces all pointer maps - ;; load into the memory. - (->> (-> data :pages-index vals) - (filter pmap/pointer-map?) - (run! pmap/load!)) - - ;; Then proceed to find the frame set for thumbnail - - (d/seek :use-for-thumbnail? + (get-thumbnail-frame [{:keys [data]}] + (d/seek #(or (:use-for-thumbnail %) + (:use-for-thumbnail? %)) ; NOTE: backward comp (remove on v1.21) (for [page (-> data :pages-index vals) frame (-> page :objects ctt/get-frames)] (assoc frame :page-id (:id page))))) @@ -154,28 +119,28 @@ ;; all unneeded shapes if a concrete frame is provided. If no ;; frame, the objects is returned untouched. (filter-objects [objects frame-id] - (d/index-by :id (cph/get-children-with-self objects frame-id))) + (d/index-by :id (cfh/get-children-with-self objects frame-id))) ;; function responsible of assoc available thumbnails ;; to frames and remove all children shapes from objects if ;; thumbnails is available (assoc-thumbnails [objects page-id thumbnails] (loop [objects objects - frames (filter cph/frame-shape? (vals objects))] + frames (filter cfh/frame-shape? (vals objects))] (if-let [frame (-> frames first)] - (let [frame-id (:id frame) - object-id (str page-id frame-id) - frame (if-let [thumb (get thumbnails object-id)] - (assoc frame :thumbnail thumb :shapes []) - (dissoc frame :thumbnail)) + (let [frame-id (:id frame) + object-id (thc/fmt-object-id (:id file) page-id frame-id "frame") + frame (if-let [thumb (get thumbnails object-id)] + (assoc frame :thumbnail thumb :shapes []) + (dissoc frame :thumbnail)) children-ids - (cph/get-children-ids objects frame-id) + (cfh/get-children-ids objects frame-id) bounds (when (:show-content frame) - (gsh/selection-rect (concat [frame] (->> children-ids (map (d/getf objects)))))) + (gsh/shapes->rect (cons frame (map (d/getf objects) children-ids)))) frame (cond-> frame @@ -192,226 +157,193 @@ objects)))] - (binding [pmap/*load-fn* (partial files/load-pointer conn id)] - (let [frame (get-thumbnail-frame data) - frame-id (:id frame) - page-id (or (:page-id frame) - (-> data :pages first)) + (let [frame (get-thumbnail-frame file) + frame-id (:id frame) + page-id (or (:page-id frame) + (-> data :pages first)) - page (dm/get-in data [:pages-index page-id]) - page (cond-> page (pmap/pointer-map? page) deref) - frame-ids (if (some? frame) (list frame-id) (map :id (ctt/get-frames (:objects page)))) + page (dm/get-in data [:pages-index page-id]) + page (cond-> page (pmap/pointer-map? page) deref) + frame-ids (if (some? frame) (list frame-id) (map :id (ctt/get-frames (:objects page)))) - obj-ids (map #(str page-id %) frame-ids) - thumbs (get-object-thumbnails conn id obj-ids)] + obj-ids (map #(thc/fmt-object-id (:id file) page-id % "frame") frame-ids) + thumbs (get-object-thumbnails conn id obj-ids)] - (cond-> page - ;; If we have frame, we need to specify it on the page level - ;; and remove the all other unrelated objects. - (some? frame-id) - (-> (assoc :thumbnail-frame-id frame-id) - (update :objects filter-objects frame-id)) + (cond-> page + ;; If we have frame, we need to specify it on the page level + ;; and remove the all other unrelated objects. + (some? frame-id) + (-> (assoc :thumbnail-frame-id frame-id) + (update :objects filter-objects frame-id)) - ;; Assoc the available thumbnails and prune not visible shapes - ;; for avoid transfer unnecessary data. - :always - (update :objects assoc-thumbnails page-id thumbs)))))) + ;; Assoc the available thumbnails and prune not visible shapes + ;; for avoid transfer unnecessary data. + :always + (update :objects assoc-thumbnails page-id thumbs))))) + +(def ^:private + schema:get-file-data-for-thumbnail + (sm/define + [:map {:title "get-file-data-for-thumbnail"} + [:file-id ::sm/uuid] + [:features {:optional true} ::cfeat/features]])) + +(def ^:private + schema:partial-file + (sm/define + [:map {:title "PartialFile"} + [:id ::sm/uuid] + [:revn {:min 0} :int] + [:page :any]])) (sv/defmethod ::get-file-data-for-thumbnail "Retrieves the data for generate the thumbnail of the file. Used mainly for render thumbnails on dashboard." - {::doc/added "1.17" ::doc/module :files - ::sm/params [:map {:title "get-file-data-for-thumbnail"} - [:file-id ::sm/uuid] - [:features {:optional true} ::files/features]] - ::sm/result [:map {:title "PartialFile"} - [:id ::sm/uuid] - [:revn {:min 0} :int] - [:page :any]]} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}] - (dm/with-open [conn (db/open pool)] - (files/check-read-permissions! conn profile-id file-id) - ;; NOTE: we force here the "storage/pointer-map" feature, because - ;; it used internally only and is independent if user supports it - ;; or not. - (let [feat (into #{"storage/pointer-map"} features) - file (files/get-file conn file-id feat)] - {:file-id file-id - :revn (:revn file) - :page (get-file-data-for-thumbnail conn file)}))) + ::sm/params schema:get-file-data-for-thumbnail + ::sm/result schema:partial-file} + [cfg {:keys [::rpc/profile-id file-id] :as params}] + (db/run! cfg (fn [{:keys [::db/conn] :as cfg}] + (files/check-read-permissions! conn profile-id file-id) + + (let [team (teams/get-team conn + :profile-id profile-id + :file-id file-id) + + file (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)] + (-> (files/get-file cfg file-id :migrate? false) + (update :data feat.fdata/process-pointers deref) + (fmg/migrate-file)))] + + (-> (cfeat/get-team-enabled-features cf/flags team) + (cfeat/check-client-features! (:features params)) + (cfeat/check-file-features! (:features file) (:features params))) + + {:file-id file-id + :revn (:revn file) + :page (get-file-data-for-thumbnail cfg file)})))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MUTATION COMMANDS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; --- MUTATION COMMAND: upsert-file-object-thumbnail - -(def sql:upsert-object-thumbnail - "insert into file_object_thumbnail(file_id, object_id, data) - values (?, ?, ?) - on conflict(file_id, object_id) do - update set data = ?;") - -(defn upsert-file-object-thumbnail! - [conn {:keys [file-id object-id data]}] - (if data - (db/exec-one! conn [sql:upsert-object-thumbnail file-id object-id data data]) - (db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id}))) - -(s/def ::data (s/nilable ::us/string)) -(s/def ::object-id ::us/string) - -(s/def ::upsert-file-object-thumbnail - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::object-id] - :opt-un [::data])) - -(sv/defmethod ::upsert-file-object-thumbnail - {::doc/added "1.17" - ::doc/module :files - ::doc/deprecated "1.19" - ::audit/skip true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (files/check-edition-permissions! conn profile-id file-id) - - (when-not (db/read-only? conn) - (upsert-file-object-thumbnail! conn params) - nil))) - - -;; --- MUTATION COMMAND: create-file-object-thumbnail - -(def ^:private sql:create-object-thumbnail - "insert into file_object_thumbnail(file_id, object_id, media_id) - values (?, ?, ?) - on conflict(file_id, object_id) do - update set media_id = ?;") +;; MUTATION COMMAND: create-file-object-thumbnail (defn- create-file-object-thumbnail! - [{:keys [::db/conn ::sto/storage]} file-id object-id media] + [{:keys [::db/conn ::sto/storage]} file-id object-id media tag] - (let [path (:path media) + (let [thumb (db/get* conn :file-tagged-object-thumbnail + {:file-id file-id + :object-id object-id + :tag tag} + {::db/remove-deleted false + ::sql/for-update true}) + + path (:path media) mtype (:mtype media) hash (sto/calculate-hash path) data (-> (sto/content path) (sto/wrap-with-hash hash)) + tnow (dt/now) + media (sto/put-object! storage {::sto/content data - ::sto/deduplicate? false + ::sto/deduplicate? true + ::sto/touched-at tnow :content-type mtype :bucket "file-object-thumbnail"})] - (db/exec-one! conn [sql:create-object-thumbnail file-id object-id - (:id media) (:id media)]))) + (if (some? thumb) + (do + ;; We mark the old media id as touched if it does not matches + (when (not= (:id media) (:media-id thumb)) + (sto/touch-object! storage (:media-id thumb))) + (db/update! conn :file-tagged-object-thumbnail + {:media-id (:id media) + :deleted-at nil + :updated-at tnow} + {:file-id file-id + :object-id object-id + :tag tag})) + (db/insert! conn :file-tagged-object-thumbnail + {:file-id file-id + :object-id object-id + :created-at tnow + :updated-at tnow + :tag tag + :media-id (:id media)})))) - -(def schema:create-file-object-thumbnail +(def ^:private + schema:create-file-object-thumbnail [:map {:title "create-file-object-thumbnail"} [:file-id ::sm/uuid] [:object-id :string] - [:media ::media/upload]]) + [:media ::media/upload] + [:tag {:optional true} :string]]) (sv/defmethod ::create-file-object-thumbnail - {:doc/added "1.19" + {::doc/added "1.19" ::doc/module :files + ::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id] + [:file-thumbnail-ops/global]] + ::rtry/enabled true + ::rtry/when rtry/conflict-exception? ::audit/skip true ::sm/params schema:create-file-object-thumbnail} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id media]}] - (db/with-atomic [conn pool] - (files/check-edition-permissions! conn profile-id file-id) - (media/validate-media-type! media) - (media/validate-media-size! media) + [cfg {:keys [::rpc/profile-id file-id object-id media tag]}] + (media/validate-media-type! media) + (media/validate-media-size! media) - (when-not (db/read-only? conn) - (-> cfg - (update ::sto/storage media/configure-assets-storage) - (assoc ::db/conn conn) - (create-file-object-thumbnail! file-id object-id media)) - nil))) + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (files/check-edition-permissions! conn profile-id file-id) + (when-not (db/read-only? conn) + (let [cfg (-> cfg + (update ::sto/storage media/configure-assets-storage) + (assoc ::rtry/when rtry/conflict-exception?) + (assoc ::rtry/max-retries 5) + (assoc ::rtry/label "create-file-object-thumbnail"))] + (create-file-object-thumbnail! cfg file-id object-id media (or tag "frame"))))))) ;; --- MUTATION COMMAND: delete-file-object-thumbnail (defn- delete-file-object-thumbnail! [{:keys [::db/conn ::sto/storage]} file-id object-id] - (when-let [{:keys [media-id]} (db/get* conn :file-object-thumbnail - {:file-id file-id - :object-id object-id} - {::db/for-update? true})] - (when media-id - (sto/del-object! storage media-id)) - - (db/delete! conn :file-object-thumbnail + (when-let [{:keys [media-id tag]} (db/get* conn :file-tagged-object-thumbnail + {:file-id file-id + :object-id object-id} + {::sql/for-update true})] + (sto/touch-object! storage media-id) + (db/update! conn :file-tagged-object-thumbnail + {:deleted-at (dt/now)} {:file-id file-id - :object-id object-id}) - nil)) + :object-id object-id + :tag tag}))) (s/def ::delete-file-object-thumbnail (s/keys :req [::rpc/profile-id] :req-un [::file-id ::object-id])) (sv/defmethod ::delete-file-object-thumbnail - {:doc/added "1.19" + {::doc/added "1.19" ::doc/module :files + ::doc/deprecated "1.20" + ::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id] + [:file-thumbnail-ops/global]] ::audit/skip true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id]}] - - (db/with-atomic [conn pool] - (files/check-edition-permissions! conn profile-id file-id) - - (when-not (db/read-only? conn) - (-> cfg - (update ::sto/storage media/configure-assets-storage) - (assoc ::db/conn conn) - (delete-file-object-thumbnail! file-id object-id)) - nil))) - -;; --- MUTATION COMMAND: upsert-file-thumbnail - -(def ^:private sql:upsert-file-thumbnail - "insert into file_thumbnail (file_id, revn, data, props) - values (?, ?, ?, ?::jsonb) - on conflict(file_id, revn) do - update set data = ?, props=?, updated_at=now();") - -(defn- upsert-file-thumbnail! - [conn {:keys [file-id revn data props]}] - (let [props (db/tjson (or props {}))] - (db/exec-one! conn [sql:upsert-file-thumbnail - file-id revn data props data props]))) - -(s/def ::revn ::us/integer) -(s/def ::props map?) - -(s/def ::upsert-file-thumbnail - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::revn ::props ::data])) - -(sv/defmethod ::upsert-file-thumbnail - "Creates or updates the file thumbnail. Mainly used for paint the - grid thumbnails." - {::doc/added "1.17" - ::doc/module :files - ::doc/deprecated "1.19" - ::audit/skip true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (files/check-edition-permissions! conn profile-id file-id) - (when-not (db/read-only? conn) - (upsert-file-thumbnail! conn params) - nil))) + [cfg {:keys [::rpc/profile-id file-id object-id]}] + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (files/check-edition-permissions! conn profile-id file-id) + (when-not (db/read-only? conn) + (-> cfg + (update ::sto/storage media/configure-assets-storage conn) + (delete-file-object-thumbnail! file-id object-id)) + nil)))) ;; --- MUTATION COMMAND: create-file-thumbnail -(def ^:private sql:create-file-thumbnail - "insert into file_thumbnail (file_id, revn, media_id, props) - values (?, ?, ?, ?::jsonb) - on conflict(file_id, revn) do - update set media_id=?, props=?, updated_at=now();") - (defn- create-file-thumbnail! [{:keys [::db/conn ::sto/storage]} {:keys [file-id revn props media] :as params}] (media/validate-media-type! media) @@ -423,35 +355,67 @@ hash (sto/calculate-hash path) data (-> (sto/content path) (sto/wrap-with-hash hash)) + tnow (dt/now) media (sto/put-object! storage {::sto/content data - ::sto/deduplicate? false + ::sto/deduplicate? true + ::sto/touched-at tnow :content-type mtype - :bucket "file-thumbnail"})] - (db/exec-one! conn [sql:create-file-thumbnail file-id revn - (:id media) props - (:id media) props]) + :bucket "file-thumbnail"}) + + thumb (db/get* conn :file-thumbnail + {:file-id file-id + :revn revn} + {::db/remove-deleted false + ::sql/for-update true})] + + (if (some? thumb) + (do + ;; We mark the old media id as touched if it does not match + (when (not= (:id media) (:media-id thumb)) + (sto/touch-object! storage (:media-id thumb))) + + (db/update! conn :file-thumbnail + {:media-id (:id media) + :deleted-at nil + :updated-at tnow + :props props} + {:file-id file-id + :revn revn})) + + (db/insert! conn :file-thumbnail + {:file-id file-id + :revn revn + :created-at tnow + :updated-at tnow + :props props + :media-id (:id media)})) + media)) +(def ^:private + schema:create-file-thumbnail + [:map {:title "create-file-thumbnail"} + [:file-id ::sm/uuid] + [:revn :int] + [:media ::media/upload]]) + (sv/defmethod ::create-file-thumbnail "Creates or updates the file thumbnail. Mainly used for paint the grid thumbnails." {::doc/added "1.19" ::doc/module :files ::audit/skip true - ::sm/params [:map {:title "create-file-thumbnail"} - [:file-id ::sm/uuid] - [:revn :int] - [:media ::media/upload]] - } + ::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id] + [:file-thumbnail-ops/global]] + ::rtry/enabled true + ::rtry/when rtry/conflict-exception? + ::sm/params schema:create-file-thumbnail} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (files/check-edition-permissions! conn profile-id file-id) - (when-not (db/read-only? conn) - (let [media (-> cfg - (update ::sto/storage media/configure-assets-storage) - (assoc ::db/conn conn) - (create-file-thumbnail! params))] - - {:uri (files/resolve-public-uri (:id media))})))) + [cfg {:keys [::rpc/profile-id file-id] :as params}] + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (files/check-edition-permissions! conn profile-id file-id) + (when-not (db/read-only? conn) + (let [cfg (update cfg ::sto/storage media/configure-assets-storage) + media (create-file-thumbnail! cfg params)] + {:uri (files/resolve-public-uri (:id media))}))))) diff --git a/backend/src/app/rpc/commands/files_update.clj b/backend/src/app/rpc/commands/files_update.clj index 5cc49366df..cf9e9b590a 100644 --- a/backend/src/app/rpc/commands/files_update.clj +++ b/backend/src/app/rpc/commands/files_update.clj @@ -6,18 +6,19 @@ (ns app.rpc.commands.files-update (:require + [app.common.data :as d] [app.common.exceptions :as ex] - [app.common.files.features :as ffeat] + [app.common.features :as cfeat] + [app.common.files.changes :as cpc] + [app.common.files.migrations :as fmg] + [app.common.files.validate :as val] [app.common.logging :as l] - [app.common.pages :as cp] - [app.common.pages.changes :as cpc] - [app.common.pages.migrations :as pmg] [app.common.schema :as sm] - [app.common.schema.generators :as smg] - [app.common.spec :as us] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.features.fdata :as feat.fdata] + [app.http.errors :as errors] [app.loggers.audit :as audit] [app.loggers.webhooks :as webhooks] [app.metrics :as mtx] @@ -25,72 +26,39 @@ [app.rpc :as-alias rpc] [app.rpc.climit :as climit] [app.rpc.commands.files :as files] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.util.blob :as blob] - [app.util.objects-map :as omap] [app.util.pointer-map :as pmap] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s])) - -;; --- SPECS - -(s/def ::changes - (s/coll-of map? :kind vector?)) - -(s/def ::hint-origin ::us/keyword) -(s/def ::hint-events - (s/every ::us/keyword :kind vector?)) - -(s/def ::change-with-metadata - (s/keys :req-un [::changes] - :opt-un [::hint-origin - ::hint-events])) - -(s/def ::changes-with-metadata - (s/every ::change-with-metadata :kind vector?)) - -(s/def ::session-id ::us/uuid) -(s/def ::revn ::us/integer) -(s/def ::update-file - (s/and - (s/keys :req [::rpc/profile-id] - :req-un [::files/id ::session-id ::revn] - :opt-un [::changes ::changes-with-metadata ::features]) - (fn [o] - (or (contains? o :changes) - (contains? o :changes-with-metadata))))) - + [app.worker :as-alias wrk] + [clojure.set :as set] + [promesa.exec :as px])) ;; --- SCHEMA -(sm/def! ::changes - [:vector ::cpc/change]) - -(sm/def! ::change-with-metadata - [:map {:title "ChangeWithMetadata"} - [:changes ::changes] - [:hint-origin {:optional true} :keyword] - [:hint-events {:optional true} [:vector :string]]]) - -(sm/def! ::update-file-params - [:map {:title "UpdateFileParams"} +(def ^:private + schema:update-file + [:map {:title "update-file"} [:id ::sm/uuid] [:session-id ::sm/uuid] [:revn {:min 0} :int] - [:features {:optional true - :gen/max 3 - :gen/gen (smg/subseq files/supported-features)} - ::sm/set-of-strings] - [:changes {:optional true} ::changes] + [:features {:optional true} ::cfeat/features] + [:changes {:optional true} [:vector ::cpc/change]] [:changes-with-metadata {:optional true} - [:vector ::change-with-metadata]]]) + [:vector [:map + [:changes [:vector ::cpc/change]] + [:hint-origin {:optional true} :keyword] + [:hint-events {:optional true} [:vector :string]]]]] + [:skip-validate {:optional true} :boolean]]) -(sm/def! ::update-file-result - [:vector {:title "UpdateFileResults"} - [:map {:title "UpdateFileResult"} - [:changes ::changes] +(def ^:private + schema:update-file-result + [:vector {:title "update-file-result"} + [:map + [:changes [:vector ::cpc/change]] [:file-id ::sm/uuid] [:id ::sm/uuid] [:revn {:min 0} :int] @@ -102,14 +70,26 @@ ;; to all clients using it. (def ^:private library-change-types - #{:add-color :mod-color :del-color - :add-media :mod-media :del-media - :add-component :mod-component :del-component - :add-typography :mod-typography :del-typography}) + #{:add-color + :mod-color + :del-color + :add-media + :mod-media + :del-media + :add-component + :mod-component + :del-component + :restore-component + :add-typography + :mod-typography + :del-typography}) (def ^:private file-change-types - #{:add-obj :mod-obj :del-obj - :reg-objects :mov-objects}) + #{:add-obj + :mod-obj + :del-obj + :reg-objects + :mov-objects}) (defn- library-change? [{:keys [type] :as change}] @@ -136,24 +116,18 @@ (defn- wrap-with-pointer-map-context [f] - (fn [{:keys [::db/conn] :as cfg} {:keys [id] :as file}] - (binding [pmap/*tracked* (atom {}) - pmap/*load-fn* (partial files/load-pointer conn id) - ffeat/*wrap-with-pointer-map-fn* pmap/wrap] + (fn [cfg {:keys [id] :as file}] + (binding [pmap/*tracked* (pmap/create-tracked) + pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] (let [result (f cfg file)] - (files/persist-pointers! conn id) + (feat.fdata/persist-pointers! cfg id) result)))) -(defn- wrap-with-objects-map-context - [f] - (fn [cfg file] - (binding [ffeat/*wrap-with-objects-map-fn* omap/wrap] - (f cfg file)))) - (declare get-lagged-changes) (declare send-notifications!) (declare update-file) (declare update-file*) +(declare update-file-data) (declare take-snapshot?) ;; If features are specified from params and the final feature @@ -161,79 +135,85 @@ ;; database. (sv/defmethod ::update-file - {::climit/id :update-file-by-id - ::climit/key-fn :id + {::climit/id [[:update-file/by-profile ::rpc/profile-id] + [:update-file/global]] ::webhooks/event? true ::webhooks/batch-timeout (dt/duration "2m") ::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id) - ::sm/params ::update-file-params - ::sm/result ::update-file-result - + ::sm/params schema:update-file + ::sm/result schema:update-file-result ::doc/module :files ::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] - (db/with-atomic [conn pool] - (files/check-edition-permissions! conn profile-id id) - (db/xact-lock! conn id) - (let [cfg (assoc cfg ::db/conn conn) - params (assoc params :profile-id profile-id) - tpoint (dt/tpoint)] - (-> (update-file cfg params) - (rph/with-defer #(let [elapsed (tpoint)] - (l/trace :hint "update-file" :time (dt/format-duration elapsed)))))))) + [cfg {:keys [::rpc/profile-id id] :as params}] + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (files/check-edition-permissions! conn profile-id id) + (db/xact-lock! conn id) + + (let [file (get-file conn id) + team (teams/get-team conn + :profile-id profile-id + :team-id (:team-id file)) + + features (-> (cfeat/get-team-enabled-features cf/flags team) + (cfeat/check-client-features! (:features params)) + (cfeat/check-file-features! (:features file) (:features params))) + + params (assoc params + :profile-id profile-id + :features features + :team team + :file file) + + tpoint (dt/tpoint)] + + ;; When newly computed features does not match exactly with + ;; the features defined on team row, we update it. + (when (not= features (:features team)) + (let [features (db/create-array conn "text" features)] + (db/update! conn :team + {:features features} + {:id (:id team)}))) + + (binding [l/*context* (some-> (meta params) + (get :app.http/request) + (errors/request->context))] + (-> (update-file cfg params) + (rph/with-defer #(let [elapsed (tpoint)] + (l/trace :hint "update-file" :time (dt/format-duration elapsed)))))))))) (defn update-file - [{:keys [::db/conn ::mtx/metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}] - (let [file (get-file conn id) - features (->> (concat (:features file) - (:features params)) - (into (files/get-default-features)) - (files/check-features-compatibility!))] + [{:keys [::mtx/metrics] :as cfg} + {:keys [file features changes changes-with-metadata] :as params}] + (let [features (-> features + (set/difference cfeat/frontend-only-features) + (set/union (:features file))) - (files/check-edition-permissions! conn profile-id (:id file)) + update-fn (cond-> update-file* + (contains? features "fdata/pointer-map") + (wrap-with-pointer-map-context)) - (binding [ffeat/*current* features - ffeat/*previous* (:features file)] + changes (if changes-with-metadata + (->> changes-with-metadata (mapcat :changes) vec) + (vec changes))] - (let [update-fn (cond-> update-file* - (contains? features "storage/pointer-map") - (wrap-with-pointer-map-context) + (when (> (:revn params) + (:revn file)) + (ex/raise :type :validation + :code :revn-conflict + :hint "The incoming revision number is greater that stored version." + :context {:incoming-revn (:revn params) + :stored-revn (:revn file)})) - (contains? features "fdata/pointer-map") - (wrap-with-pointer-map-context) + (mtx/run! metrics {:id :update-file-changes :inc (count changes)}) - (contains? features "storage/objects-map") - (wrap-with-objects-map-context) - - (contains? features "fdata/objects-map") - (wrap-with-objects-map-context)) - - file (assoc file :features features) - changes (if changes-with-metadata - (->> changes-with-metadata (mapcat :changes) vec) - (vec changes)) - - params (-> params - (assoc :file file) - (assoc :changes changes) - (assoc ::created-at (dt/now)))] - - (when (> (:revn params) - (:revn file)) - (ex/raise :type :validation - :code :revn-conflict - :hint "The incoming revision number is greater that stored version." - :context {:incoming-revn (:revn params) - :stored-revn (:revn file)})) - - (mtx/run! metrics {:id :update-file-changes :inc (count changes)}) - - (when (not= features (:features file)) - (let [features (db/create-array conn "text" features)] - (db/update! conn :file - {:features features} - {:id id}))) + (binding [cfeat/*current* features + cfeat/*previous* (:features file)] + (let [file (assoc file :features features) + params (-> params + (assoc :file file) + (assoc :changes changes) + (assoc ::created-at (dt/now)))] (-> (update-fn cfg params) (vary-meta assoc ::audit/replace-props @@ -243,25 +223,13 @@ :project-id (:project-id file) :team-id (:team-id file)})))))) -(defn- update-file-data - [file changes] - (-> file - (update :revn inc) - (update :data (fn [data] - (-> data - (blob/decode) - (assoc :id (:id file)) - (pmg/migrate-data) - (cp/process-changes changes) - (blob/encode)))))) - (defn- update-file* - [{:keys [::db/conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}] - (let [;; Process the file data in the CLIMIT context; scheduling it - ;; to be executed on a separated executor for avoid to do the - ;; CPU intensive operation on vthread. - file (-> (climit/configure cfg :update-file) - (climit/submit! (partial update-file-data file changes)))] + [{:keys [::db/conn ::wrk/executor] :as cfg} + {:keys [profile-id file changes session-id ::created-at skip-validate] :as params}] + (let [;; Process the file data on separated thread for avoid to do + ;; the CPU intensive operation on vthread. + file (px/invoke! executor (partial update-file-data cfg file changes skip-validate)) + features (db/create-array conn "text" (:features file))] (db/insert! conn :file-change {:id (uuid/next) @@ -273,11 +241,14 @@ :features (db/create-array conn "text" (:features file)) :data (when (take-snapshot? file) (:data file)) - :changes (blob/encode changes)}) + :changes (blob/encode changes)} + {::db/return-keys false}) (db/update! conn :file {:revn (:revn file) :data (:data file) + :version (:version file) + :features features :data-backend nil :modified-at created-at :has-media-trimmed false} @@ -294,6 +265,89 @@ ;; Retrieve and return lagged data (get-lagged-changes conn params)))) +(defn- soft-validate-file-schema! + [file] + (try + (val/validate-file-schema! file) + (catch Throwable cause + (l/error :hint "file schema validation error" :cause cause)))) + +(defn- soft-validate-file! + [file libs] + (try + (val/validate-file! file libs) + (catch Throwable cause + (l/error :hint "file validation error" + :cause cause)))) + +(defn- update-file-data + [{:keys [::db/conn] :as cfg} file changes skip-validate] + (let [file (update file :data (fn [data] + (-> data + (blob/decode) + (assoc :id (:id file))))) + + ;; For avoid unnecesary overhead of creating multiple pointers + ;; and handly internally with objects map in their worst + ;; case (when probably all shapes and all pointers will be + ;; readed in any case), we just realize/resolve them before + ;; applying the migration to the file + file (if (fmg/need-migration? file) + (-> file + (update :data feat.fdata/process-pointers deref) + (update :data feat.fdata/process-objects (partial into {})) + (fmg/migrate-file)) + file) + + ;; WARNING: this ruins performance; maybe we need to find + ;; some other way to do general validation + libs (when (and (or (contains? cf/flags :file-validation) + (contains? cf/flags :soft-file-validation)) + (not skip-validate)) + (->> (files/get-file-libraries conn (:id file)) + (into [file] (map (fn [{:keys [id]}] + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id) + pmap/*tracked* nil] + ;; We do not resolve the objects maps here + ;; because there is a lower probability that all + ;; shapes needed to be loded into memory, so we + ;; leeave it on lazy status + (-> (files/get-file cfg id :migrate? false) + (update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved + (update :data feat.fdata/process-objects (partial into {})) + (fmg/migrate-file)))))) + (d/index-by :id))) + + + file (-> (files/check-version! file) + (update :revn inc) + (update :data cpc/process-changes changes) + (update :data d/without-nils))] + + (when (contains? cf/flags :soft-file-validation) + (soft-validate-file! file libs)) + + (when (contains? cf/flags :soft-file-schema-validation) + (soft-validate-file-schema! file)) + + (when (and (contains? cf/flags :file-validation) + (not skip-validate)) + (val/validate-file! file libs)) + + (when (and (contains? cf/flags :file-schema-validation) + (not skip-validate)) + (val/validate-file-schema! file)) + + (cond-> file + (contains? cfeat/*current* "fdata/objects-map") + (feat.fdata/enable-objects-map) + + (contains? cfeat/*current* "fdata/pointer-map") + (feat.fdata/enable-pointer-map) + + :always + (update :data blob/encode)))) + (defn- take-snapshot? "Defines the rule when file `data` snapshot should be saved." [{:keys [revn modified-at] :as file}] @@ -321,7 +375,7 @@ (vec))) (defn- send-notifications! - [{:keys [::db/conn] :as cfg} {:keys [file changes session-id] :as params}] + [cfg {:keys [file team changes session-id] :as params}] (let [lchanges (filter library-change? changes) msgbus (::mbus/msgbus cfg)] @@ -335,14 +389,12 @@ :changes changes}) (when (and (:is-shared file) (seq lchanges)) - (let [team-id (or (:team-id file) - (files/get-team-id conn (:project-id file)))] - (mbus/pub! msgbus - :topic team-id - :message {:type :library-change - :profile-id (:profile-id params) - :file-id (:id file) - :session-id session-id - :revn (:revn file) - :modified-at (dt/now) - :changes lchanges}))))) + (mbus/pub! msgbus + :topic (:id team) + :message {:type :library-change + :profile-id (:profile-id params) + :file-id (:id file) + :session-id session-id + :revn (:revn file) + :modified-at (dt/now) + :changes lchanges})))) diff --git a/backend/src/app/rpc/commands/fonts.clj b/backend/src/app/rpc/commands/fonts.clj index 256132e847..0942da601d 100644 --- a/backend/src/app/rpc/commands/fonts.clj +++ b/backend/src/app/rpc/commands/fonts.clj @@ -8,14 +8,15 @@ (:require [app.common.data.macros :as dm] [app.common.exceptions :as ex] - [app.common.spec :as us] + [app.common.schema :as sm] [app.common.uuid :as uuid] [app.db :as db] + [app.db.sql :as-alias sql] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] [app.media :as media] [app.rpc :as-alias rpc] - [app.rpc.climit :as climit] + [app.rpc.climit :as-alias climit] [app.rpc.commands.files :as files] [app.rpc.commands.projects :as projects] [app.rpc.commands.teams :as teams] @@ -25,38 +26,28 @@ [app.storage :as sto] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s])) + [app.worker :as-alias wrk] + [promesa.exec :as px])) (def valid-weight #{100 200 300 400 500 600 700 800 900 950}) (def valid-style #{"normal" "italic"}) -(s/def ::data (s/map-of ::us/string any?)) -(s/def ::file-id ::us/uuid) -(s/def ::font-id ::us/uuid) -(s/def ::id ::us/uuid) -(s/def ::name ::us/not-empty-string) -(s/def ::project-id ::us/uuid) -(s/def ::share-id ::us/uuid) -(s/def ::style valid-style) -(s/def ::team-id ::us/uuid) -(s/def ::weight valid-weight) - ;; --- QUERY: Get font variants -(s/def ::get-font-variants - (s/and - (s/keys :req [::rpc/profile-id] - :opt-un [::team-id - ::file-id - ::project-id - ::share-id]) - (fn [o] - (or (contains? o :team-id) - (contains? o :file-id) - (contains? o :project-id))))) +(def ^:private + schema:get-font-variants + [:schema {:title "get-font-variants"} + [:and + [:map + [:team-id {:optional true} ::sm/uuid] + [:file-id {:optional true} ::sm/uuid] + [:project-id {:optional true} ::sm/uuid] + [:share-id {:optional true} ::sm/uuid]] + [::sm/contains-any #{:team-id :file-id :project-id}]]]) (sv/defmethod ::get-font-variants - {::doc/added "1.18"} + {::doc/added "1.18" + ::sm/params schema:get-font-variants} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id share-id] :as params}] (dm/with-open [conn (db/open pool)] (cond @@ -80,34 +71,39 @@ perms (files/get-permissions conn profile-id file-id share-id)] (files/check-read-permissions! perms) (db/query conn :team-font-variant - {:team-id (:team-id project) - :deleted-at nil}))))) + {:team-id (:team-id project) + :deleted-at nil}))))) (declare create-font-variant) -(s/def ::create-font-variant - (s/keys :req [::rpc/profile-id] - :req-un [::team-id - ::data - ::font-id - ::font-family - ::font-weight - ::font-style])) +(def ^:private schema:create-font-variant + [:map {:title "create-font-variant"} + [:team-id ::sm/uuid] + [:data [:map-of :string :any]] + [:font-id ::sm/uuid] + [:font-family :string] + [:font-weight [::sm/one-of {:format "number"} valid-weight]] + [:font-style [::sm/one-of {:format "string"} valid-style]]]) (sv/defmethod ::create-font-variant {::doc/added "1.18" - ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] - (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] - (teams/check-edition-permissions! pool profile-id team-id) - (quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team - ::quotes/profile-id profile-id - ::quotes/team-id team-id}) - (create-font-variant cfg (assoc params :profile-id profile-id)))) + ::climit/id [[:process-font/by-profile ::rpc/profile-id] + [:process-font/global]] + ::webhooks/event? true + ::sm/params schema:create-font-variant} + [cfg {:keys [::rpc/profile-id team-id] :as params}] + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] + (teams/check-edition-permissions! conn profile-id team-id) + (quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team + ::quotes/profile-id profile-id + ::quotes/team-id team-id}) + (create-font-variant cfg (assoc params :profile-id profile-id)))))) (defn create-font-variant - [{:keys [::sto/storage ::db/pool] :as cfg} {:keys [data] :as params}] + [{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [data] :as params}] (letfn [(generate-missing! [data] (let [data (media/run {:cmd :generate-fonts :input data})] (when (and (not (contains? data "font/otf")) @@ -135,6 +131,7 @@ ttf-params (prepare-font data "font/ttf") wf1-params (prepare-font data "font/woff") wf2-params (prepare-font data "font/woff2")] + (cond-> {} (some? otf-params) (assoc :otf (sto/put-object! storage otf-params)) @@ -146,7 +143,7 @@ (assoc :woff2 (sto/put-object! storage wf2-params))))) (insert-font-variant! [{:keys [woff1 woff2 otf ttf]}] - (db/insert! pool :team-font-variant + (db/insert! conn :team-font-variant {:id (uuid/next) :team-id (:team-id params) :font-id (:font-id params) @@ -156,74 +153,114 @@ :woff1-file-id (:id woff1) :woff2-file-id (:id woff2) :otf-file-id (:id otf) - :ttf-file-id (:id ttf)})) - ] + :ttf-file-id (:id ttf)}))] - (let [data (-> (climit/configure cfg :process-font) - (climit/submit! (partial generate-missing! data))) + (let [data (px/invoke! executor (partial generate-missing! data)) assets (persist-fonts-files! data) result (insert-font-variant! assets)] (vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys)))))) ;; --- UPDATE FONT FAMILY -(s/def ::update-font - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::id ::name])) +(def ^:private + schema:update-font + [:map {:title "update-font"} + [:team-id ::sm/uuid] + [:id ::sm/uuid] + [:name :string]]) (sv/defmethod ::update-font {::doc/added "1.18" - ::webhooks/event? true} - [{:keys [::db/pool]} {:keys [::rpc/profile-id team-id id name]}] - (db/with-atomic [conn pool] - (teams/check-edition-permissions! conn profile-id team-id) - (rph/with-meta - (db/update! conn :team-font-variant - {:font-family name} - {:font-id id - :team-id team-id}) - {::audit/replace-props {:id id - :name name - :team-id team-id - :profile-id profile-id}}))) + ::webhooks/event? true + ::sm/params schema:update-font} + [cfg {:keys [::rpc/profile-id team-id id name]}] + (db/tx-run! cfg + (fn [{:keys [::db/conn]}] + (teams/check-edition-permissions! conn profile-id team-id) + + (db/update! conn :team-font-variant + {:font-family name} + {:font-id id + :team-id team-id}) + + (rph/with-meta (rph/wrap nil) + {::audit/replace-props {:id id + :name name + :team-id team-id + :profile-id profile-id}})))) ;; --- DELETE FONT -(s/def ::delete-font - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::id])) +(def ^:private + schema:delete-font + [:map {:title "delete-font"} + [:team-id ::sm/uuid] + [:id ::sm/uuid]]) (sv/defmethod ::delete-font {::doc/added "1.18" - ::webhooks/event? true} - [{:keys [::db/pool]} {:keys [::rpc/profile-id id team-id]}] - (db/with-atomic [conn pool] - (teams/check-edition-permissions! conn profile-id team-id) - (let [font (db/update! conn :team-font-variant - {:deleted-at (dt/now)} - {:font-id id :team-id team-id})] - (rph/with-meta (rph/wrap) - {::audit/props {:id id - :team-id team-id - :name (:font-family font) - :profile-id profile-id}})))) + ::webhooks/event? true + ::sm/params schema:delete-font} + [cfg {:keys [::rpc/profile-id id team-id]}] + (db/tx-run! cfg + (fn [{:keys [::db/conn ::sto/storage] :as cfg}] + (teams/check-edition-permissions! conn profile-id team-id) + (let [fonts (db/query conn :team-font-variant + {:team-id team-id + :font-id id + :deleted-at nil} + {::sql/for-update true}) + storage (media/configure-assets-storage storage conn) + tnow (dt/now)] + + (when-not (seq fonts) + (ex/raise :type :not-found + :code :object-not-found)) + + (doseq [font fonts] + (db/update! conn :team-font-variant + {:deleted-at tnow} + {:id (:id font)}) + (some->> (:woff1-file-id font) (sto/touch-object! storage)) + (some->> (:woff2-file-id font) (sto/touch-object! storage)) + (some->> (:ttf-file-id font) (sto/touch-object! storage)) + (some->> (:otf-file-id font) (sto/touch-object! storage))) + + (rph/with-meta (rph/wrap) + {::audit/props {:id id + :team-id team-id + :name (:font-family (peek fonts)) + :profile-id profile-id}}))))) ;; --- DELETE FONT VARIANT -(s/def ::delete-font-variant - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::id])) +(def ^:private schema:delete-font-variant + [:map {:title "delete-font-variant"} + [:team-id ::sm/uuid] + [:id ::sm/uuid]]) (sv/defmethod ::delete-font-variant {::doc/added "1.18" - ::webhooks/event? true} - [{:keys [::db/pool]} {:keys [::rpc/profile-id id team-id]}] - (db/with-atomic [conn pool] - (teams/check-edition-permissions! conn profile-id team-id) - (let [variant (db/update! conn :team-font-variant - {:deleted-at (dt/now)} - {:id id :team-id team-id})] - (rph/with-meta (rph/wrap) - {::audit/props {:font-family (:font-family variant) - :font-id (:font-id variant)}})))) + ::webhooks/event? true + ::sm/params schema:delete-font-variant} + [cfg {:keys [::rpc/profile-id id team-id]}] + (db/tx-run! cfg + (fn [{:keys [::db/conn ::sto/storage] :as cfg}] + (teams/check-edition-permissions! conn profile-id team-id) + (let [variant (db/get conn :team-font-variant + {:id id :team-id team-id} + {::sql/for-update true}) + storage (media/configure-assets-storage storage conn)] + (db/update! conn :team-font-variant + {:deleted-at (dt/now)} + {:id (:id variant)}) + + (some->> (:woff1-file-id variant) (sto/touch-object! storage)) + (some->> (:woff2-file-id variant) (sto/touch-object! storage)) + (some->> (:ttf-file-id variant) (sto/touch-object! storage)) + (some->> (:otf-file-id variant) (sto/touch-object! storage)) + + (rph/with-meta (rph/wrap) + {::audit/props {:font-family (:font-family variant) + :font-id (:font-id variant)}}))))) diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj index c166f7c1aa..780f0e100c 100644 --- a/backend/src/app/rpc/commands/ldap.clj +++ b/backend/src/app/rpc/commands/ldap.clj @@ -18,6 +18,7 @@ [app.rpc.commands.profile :as profile] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] + [app.setup :as-alias setup] [app.tokens :as tokens] [app.util.services :as sv] [clojure.spec.alpha :as s])) @@ -40,7 +41,7 @@ {::rpc/auth false ::doc/added "1.15" ::doc/module :auth} - [{:keys [::main/props ::ldap/provider] :as cfg} params] + [{:keys [::setup/props ::ldap/provider] :as cfg} params] (when-not provider (ex/raise :type :restriction :code :ldap-not-initialized @@ -78,13 +79,13 @@ ::audit/profile-id (:id profile)})))))) (defn- login-or-register - [{:keys [::db/pool] :as cfg} info] - (db/with-atomic [conn pool] - (or (some->> (:email info) - (profile/get-profile-by-email conn) - (profile/decode-row)) - (->> (assoc info :is-active true :is-demo false) - (auth/create-profile! conn) - (auth/create-profile-rels! conn) - (profile/strip-private-attrs))))) - + [cfg info] + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (or (some->> (:email info) + (profile/clean-email) + (profile/get-profile-by-email conn)) + (->> (assoc info :is-active true :is-demo false) + (auth/create-profile! conn) + (auth/create-profile-rels! conn) + (profile/strip-private-attrs)))))) diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index 147717aac0..5d01d9ec60 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -7,276 +7,252 @@ (ns app.rpc.commands.management "A collection of RPC methods for manage the files, projects and team organization." (:require - [app.common.data :as d] + [app.binfile.common :as bfc] + [app.binfile.v1 :as bf.v1] [app.common.exceptions :as ex] - [app.common.pages.migrations :as pmg] + [app.common.features :as cfeat] [app.common.schema :as sm] - [app.common.spec :as us] [app.common.uuid :as uuid] + [app.config :as cf] [app.db :as db] + [app.http.sse :as sse] [app.loggers.webhooks :as-alias webhooks] [app.rpc :as-alias rpc] - [app.rpc.commands.binfile :as binfile] [app.rpc.commands.files :as files] [app.rpc.commands.projects :as proj] - [app.rpc.commands.teams :as teams :refer [create-project-role create-project]] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.setup :as-alias setup] [app.setup.templates :as tmpl] - [app.util.blob :as blob] - [app.util.pointer-map :as pmap] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s] - [clojure.walk :as walk] + [app.worker :as-alias wrk] [promesa.exec :as px])) ;; --- COMMAND: Duplicate File -(declare duplicate-file) +(defn duplicate-file + [{:keys [::db/conn ::bfc/timestamp] :as cfg} {:keys [profile-id file-id name reset-shared-flag] :as params}] + (let [;; We don't touch the original file on duplication + file (bfc/get-file cfg file-id) + project-id (:project-id file) + file (-> file + (update :id bfc/lookup-index) + (update :project-id bfc/lookup-index) + (cond-> (string? name) + (assoc :name name)) + (cond-> (true? reset-shared-flag) + (assoc :is-shared false))) -(s/def ::id ::us/uuid) -(s/def ::project-id ::us/uuid) -(s/def ::file-id ::us/uuid) -(s/def ::team-id ::us/uuid) -(s/def ::name ::us/string) + flibs (bfc/get-files-rels cfg #{file-id}) + fmeds (bfc/get-file-media cfg file)] -(s/def ::duplicate-file - (s/keys :req [::rpc/profile-id] - :req-un [::file-id] - :opt-un [::name])) + (when (uuid? profile-id) + (proj/check-edition-permissions! conn profile-id project-id)) + + (vswap! bfc/*state* update :index bfc/update-index fmeds :id) + + ;; Process and persist file + (let [file (->> (bfc/process-file file) + (bfc/persist-file! cfg))] + + ;; The file profile creation is optional, so when no profile is + ;; present (when this function is called from profile less + ;; environment: SREPL) we just omit the creation of the relation + (when (uuid? profile-id) + (db/insert! conn :file-profile-rel + {:file-id (:id file) + :profile-id profile-id + :is-owner true + :is-admin true + :can-edit true} + {::db/return-keys? false})) + + (doseq [params (sequence (comp + (map #(bfc/remap-id % :file-id)) + (map #(bfc/remap-id % :library-file-id)) + (map #(assoc % :synced-at timestamp)) + (map #(assoc % :created-at timestamp))) + flibs)] + (db/insert! conn :file-library-rel params ::db/return-keys false)) + + (doseq [params (sequence (comp + (map #(bfc/remap-id % :id)) + (map #(assoc % :created-at timestamp)) + (map #(bfc/remap-id % :file-id))) + fmeds)] + (db/insert! conn :file-media-object params ::db/return-keys false)) + + file))) + +(def ^:private + schema:duplicate-file + (sm/define + [:map {:title "duplicate-file"} + [:file-id ::sm/uuid] + [:name {:optional true} :string]])) (sv/defmethod ::duplicate-file "Duplicate a single file in the same team." {::doc/added "1.16" - ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] - (db/with-atomic [conn pool] - (duplicate-file conn (assoc params :profile-id profile-id)))) + ::webhooks/event? true + ::sm/params schema:duplicate-file} + [cfg {:keys [::rpc/profile-id file-id] :as params}] + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"]) -(defn- remap-id - [item index key] - (cond-> item - (contains? item key) - (assoc key (get index (get item key) (get item key))))) - -(defn- process-file - [conn {:keys [id] :as file} index] - (letfn [(process-form [form] - (cond-> form - ;; Relink library items - (and (map? form) - (uuid? (:component-file form))) - (update :component-file #(get index % %)) - - (and (map? form) - (uuid? (:fill-color-ref-file form))) - (update :fill-color-ref-file #(get index % %)) - - (and (map? form) - (uuid? (:stroke-color-ref-file form))) - (update :stroke-color-ref-file #(get index % %)) - - (and (map? form) - (uuid? (:typography-ref-file form))) - (update :typography-ref-file #(get index % %)) - - ;; Relink Image Shapes - (and (map? form) - (map? (:metadata form)) - (= :image (:type form))) - (update-in [:metadata :id] #(get index % %)))) - - ;; A function responsible to analyze all file data and - ;; replace the old :component-file reference with the new - ;; ones, using the provided file-index - (relink-shapes [data] - (walk/postwalk process-form data)) - - ;; A function responsible of process the :media attr of file - ;; data and remap the old ids with the new ones. - (relink-media [media] - (reduce-kv (fn [res k v] - (let [id (get index k)] - (if (uuid? id) - (-> res - (assoc id (assoc v :id id)) - (dissoc k)) - res))) - media - media))] - (-> file - (update :id #(get index %)) - (update :data - (fn [data] - (binding [pmap/*load-fn* (partial files/load-pointer conn id) - pmap/*tracked* (atom {})] - (let [file-id (get index id) - data (-> data - (blob/decode) - (assoc :id file-id) - (pmg/migrate-data) - (update :pages-index relink-shapes) - (update :components relink-shapes) - (update :media relink-media) - (d/without-nils) - (files/process-pointers pmap/clone) - (blob/encode))] - (files/persist-pointers! conn file-id) - data))))))) - -(def sql:retrieve-used-libraries - "select flr.* - from file_library_rel as flr - inner join file as l on (flr.library_file_id = l.id) - where flr.file_id = ? - and l.deleted_at is null") - -(def sql:retrieve-used-media-objects - "select fmo.* - from file_media_object as fmo - inner join storage_object as so on (fmo.media_id = so.id) - where fmo.file_id = ? - and so.deleted_at is null") - -(defn duplicate-file* - [conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag]}] - (let [flibs (or flibs (db/exec! conn [sql:retrieve-used-libraries (:id file)])) - fmeds (or fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)])) - - ;; memo uniform creation/modification date - now (dt/now) - ignore (dt/plus now (dt/duration {:seconds 5})) - - ;; add to the index all file media objects. - index (reduce #(assoc %1 (:id %2) (uuid/next)) index fmeds) - - flibs-xf (comp - (map #(remap-id % index :file-id)) - (map #(remap-id % index :library-file-id)) - (map #(assoc % :synced-at now)) - (map #(assoc % :created-at now))) - - ;; remap all file-library-rel row - flibs (sequence flibs-xf flibs) - - fmeds-xf (comp - (map #(assoc % :id (get index (:id %)))) - (map #(assoc % :created-at now)) - (map #(remap-id % index :file-id))) - - ;; remap all file-media-object rows - fmeds (sequence fmeds-xf fmeds) - - file (cond-> file - (some? project-id) - (assoc :project-id project-id) - - (some? name) - (assoc :name name) - - (true? reset-shared-flag) - (assoc :is-shared false)) - - file (-> file - (assoc :created-at now) - (assoc :modified-at now) - (assoc :ignore-sync-until ignore)) - - file (process-file conn file index)] - - (db/insert! conn :file file) - (db/insert! conn :file-profile-rel - {:file-id (:id file) - :profile-id profile-id - :is-owner true - :is-admin true - :can-edit true}) - - (doseq [params flibs] - (db/insert! conn :file-library-rel params)) - - (doseq [params fmeds] - (db/insert! conn :file-media-object params)) - - file)) - -(defn duplicate-file - [conn {:keys [profile-id file-id] :as params}] - (let [file (db/get-by-id conn :file file-id) - index {file-id (uuid/next)} - params (assoc params :index index :file file)] - (proj/check-edition-permissions! conn profile-id (:project-id file)) - (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"]) - (-> (duplicate-file* conn params {:reset-shared-flag true}) - (update :data blob/decode) - (update :features db/decode-pgarray #{})))) + (binding [bfc/*state* (volatile! {:index {file-id (uuid/next)}})] + (duplicate-file (assoc cfg ::bfc/timestamp (dt/now)) + (-> params + (assoc :profile-id profile-id) + (assoc :reset-shared-flag true))))))) ;; --- COMMAND: Duplicate Project -(declare duplicate-project) +(defn duplicate-project + [{:keys [::db/conn ::bfc/timestamp] :as cfg} {:keys [profile-id project-id name] :as params}] + (binding [bfc/*state* (volatile! {:index {project-id (uuid/next)}})] + (let [project (-> (db/get-by-id conn :project project-id) + (assoc :created-at timestamp) + (assoc :modified-at timestamp) + (assoc :is-pinned false) + (update :id bfc/lookup-index) + (cond-> (string? name) + (assoc :name name))) -(s/def ::duplicate-project - (s/keys :req [::rpc/profile-id] - :req-un [::project-id] - :opt-un [::name])) + files (bfc/get-project-files cfg project-id)] + + ;; Update index with the project files and the project-id + (vswap! bfc/*state* update :index bfc/update-index files) + + + ;; Check if the source team-id allow creating new project for current user + (teams/check-edition-permissions! conn profile-id (:team-id project)) + + ;; create the duplicated project and assign the current profile as + ;; a project owner + (let [project (teams/create-project conn project)] + ;; The project profile creation is optional, so when no profile is + ;; present (when this function is called from profile less + ;; environment: SREPL) we just omit the creation of the relation + (when (uuid? profile-id) + (teams/create-project-role conn profile-id (:id project) :owner)) + + (doseq [file-id files] + (let [params (-> params + (dissoc :name) + (assoc :file-id file-id) + (assoc :reset-shared-flag false))] + (duplicate-file cfg params))) + + project)))) + +(def ^:private + schema:duplicate-project + (sm/define + [:map {:title "duplicate-project"} + [:project-id ::sm/uuid] + [:name {:optional true} :string]])) (sv/defmethod ::duplicate-project "Duplicate an entire project with all the files" {::doc/added "1.16" - ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} params] - (db/with-atomic [conn pool] - (duplicate-project conn (assoc params :profile-id (::rpc/profile-id params))))) + ::webhooks/event? true + ::sm/params schema:duplicate-project} + [cfg {:keys [::rpc/profile-id] :as params}] + (db/tx-run! cfg (fn [cfg] + ;; Defer all constraints + (db/exec-one! cfg ["SET CONSTRAINTS ALL DEFERRED"]) + (-> (assoc cfg ::bfc/timestamp (dt/now)) + (duplicate-project (assoc params :profile-id profile-id)))))) -(defn duplicate-project - [conn {:keys [profile-id project-id name] :as params}] +(defn duplicate-team + [{:keys [::db/conn ::bfc/timestamp] :as cfg} & {:keys [profile-id team-id name] :as params}] - ;; Defer all constraints - (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"]) + ;; Check if the source team-id allowed to be read by the user if + ;; profile-id is present; it can be ommited if this function is + ;; called from SREPL helpers where no profile is available + (when (uuid? profile-id) + (teams/check-read-permissions! conn profile-id team-id)) - (let [project (-> (db/get-by-id conn :project project-id) - (assoc :is-pinned false)) + (binding [bfc/*state* (volatile! {:index {team-id (uuid/next)}})] + (let [projs (bfc/get-team-projects cfg team-id) + files (bfc/get-team-files cfg team-id) + frels (bfc/get-files-rels cfg files) - files (db/query conn :file - {:project-id (:id project) - :deleted-at nil} - {:columns [:id]}) + team (-> (db/get-by-id conn :team team-id) + (assoc :created-at timestamp) + (assoc :modified-at timestamp) + (update :id bfc/lookup-index) + (cond-> (string? name) + (assoc :name name))) - project (cond-> project - (string? name) - (assoc :name name) + fonts (db/query conn :team-font-variant + {:team-id team-id})] - :always - (assoc :id (uuid/next)))] + (vswap! bfc/*state* update :index + (fn [index] + (-> index + (bfc/update-index projs) + (bfc/update-index files) + (bfc/update-index fonts :id)))) - ;; Check if the source team-id allow creating new project for current user - (teams/check-edition-permissions! conn profile-id (:team-id project)) + ;; FIXME: disallow clone default team + ;; Create the new team in the database + (db/insert! conn :team team) - ;; create the duplicated project and assign the current profile as - ;; a project owner - (create-project conn project) - (create-project-role conn profile-id (:id project) :owner) + ;; Duplicate team <-> profile relations + (doseq [params frels] + (let [params (-> params + (assoc :id (uuid/next)) + (update :team-id bfc/lookup-index) + (assoc :created-at timestamp) + (assoc :modified-at timestamp))] + (db/insert! conn :team-profile-rel params + {::db/return-keys false}))) - ;; duplicate all files - (let [index (reduce #(assoc %1 (:id %2) (uuid/next)) {} files) - params (-> params - (dissoc :name) - (assoc :project-id (:id project)) - (assoc :index index))] - (doseq [{:keys [id]} files] - (let [file (db/get-by-id conn :file id) - params (assoc params :file file) - opts {:reset-shared-flag false}] - (duplicate-file* conn params opts)))) + ;; Duplicate team fonts + (doseq [font fonts] + (let [params (-> font + (update :id bfc/lookup-index) + (update :team-id bfc/lookup-index) + (assoc :created-at timestamp) + (assoc :modified-at timestamp))] + (db/insert! conn :team-font-variant params + {::db/return-keys false}))) - ;; return the created project - project)) + ;; Duplicate projects; We don't reuse the `duplicate-project` + ;; here because we handle files duplication by whole team + ;; instead of by project and we want to preserve some project + ;; props which are reset on the `duplicate-project` impl + (doseq [project-id projs] + (let [project (db/get conn :project {:id project-id}) + project (-> project + (assoc :created-at timestamp) + (assoc :modified-at timestamp) + (update :id bfc/lookup-index) + (update :team-id bfc/lookup-index))] + (teams/create-project conn project) + + ;; The project profile creation is optional, so when no profile is + ;; present (when this function is called from profile less + ;; environment: SREPL) we just omit the creation of the relation + (when (uuid? profile-id) + (teams/create-project-role conn profile-id (:id project) :owner)))) + + (doseq [file-id files] + (let [params (-> params + (dissoc :name) + (assoc :file-id file-id) + (assoc :reset-shared-flag false))] + (duplicate-file cfg params))) + + team))) ;; --- COMMAND: Move file -(def sql:retrieve-files - "select id, project_id from file where id = ANY(?)") +(def sql:get-files + "select id, features, project_id from file where id = ANY(?)") (def sql:move-files "update file set project_id = ? where id = ANY(?)") @@ -297,14 +273,20 @@ and rel.library_file_id = br.library_file_id") (defn move-files - [conn {:keys [profile-id ids project-id] :as params}] + [{:keys [::db/conn] :as cfg} {:keys [profile-id ids project-id] :as params}] (let [fids (db/create-array conn "uuid" ids) - files (db/exec! conn [sql:retrieve-files fids]) + files (->> (db/exec! conn [sql:get-files fids]) + (map files/decode-row)) source (into #{} (map :project-id) files) pids (->> (conj source project-id) (db/create-array conn "uuid"))] + (when (contains? source project-id) + (ex/raise :type :validation + :code :cant-move-to-same-project + :hint "Unable to move a file to the same project")) + ;; Check if we have permissions on the destination project (proj/check-edition-permissions! conn profile-id project-id) @@ -312,10 +294,15 @@ (doseq [project-id source] (proj/check-edition-permissions! conn profile-id project-id)) - (when (contains? source project-id) - (ex/raise :type :validation - :code :cant-move-to-same-project - :hint "Unable to move a file to the same project")) + ;; Check the team compatibility + (let [orig-team (teams/get-team conn :profile-id profile-id :project-id (first source)) + dest-team (teams/get-team conn :profile-id profile-id :project-id project-id)] + (cfeat/check-teams-compatibility! orig-team dest-team) + + ;; Check if all pending to move files are compaib + (let [features (cfeat/get-team-enabled-features cf/flags dest-team)] + (doseq [file files] + (cfeat/check-file-features! features (:features file))))) ;; move all files to the project (db/exec-one! conn [sql:move-files project-id fids]) @@ -337,36 +324,51 @@ nil)) -(s/def ::ids (s/every ::us/uuid :kind set?)) -(s/def ::move-files - (s/keys :req [::rpc/profile-id] - :req-un [::ids ::project-id])) +(def ^:private + schema:move-files + (sm/define + [:map {:title "move-files"} + [:ids ::sm/set-of-uuid] + [:project-id ::sm/uuid]])) (sv/defmethod ::move-files "Move a set of files from one project to other." {::doc/added "1.16" - ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] - (db/with-atomic [conn pool] - (move-files conn (assoc params :profile-id profile-id)))) + ::webhooks/event? true + ::sm/params schema:move-files} + [cfg {:keys [::rpc/profile-id] :as params}] + (db/tx-run! cfg #(move-files % (assoc params :profile-id profile-id)))) ;; --- COMMAND: Move project (defn move-project - [conn {:keys [profile-id team-id project-id] :as params}] + [{:keys [::db/conn] :as cfg} {:keys [profile-id team-id project-id] :as params}] (let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]}) pids (->> (db/query conn :project {:team-id (:team-id project)} {:columns [:id]}) (map :id) (db/create-array conn "uuid"))] - (teams/check-edition-permissions! conn profile-id (:team-id project)) - (teams/check-edition-permissions! conn profile-id team-id) - (when (= team-id (:team-id project)) (ex/raise :type :validation :code :cant-move-to-same-team :hint "Unable to move a project to same team")) + (teams/check-edition-permissions! conn profile-id (:team-id project)) + (teams/check-edition-permissions! conn profile-id team-id) + + ;; Check the teams compatibility + (let [orig-team (teams/get-team conn :profile-id profile-id :team-id (:team-id project)) + dest-team (teams/get-team conn :profile-id profile-id :team-id team-id)] + (cfeat/check-teams-compatibility! orig-team dest-team) + + ;; Check if all pending to move files are compaib + (let [features (cfeat/get-team-enabled-features cf/flags dest-team)] + (doseq [file (->> (db/query conn :file + {:project-id project-id} + {:columns [:features]}) + (map files/decode-row))] + (cfeat/check-file-features! features (:features file))))) + ;; move project to the destination team (db/update! conn :project {:team-id team-id} @@ -377,67 +379,65 @@ nil)) -(s/def ::move-project - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::project-id])) +(def ^:private + schema:move-project + (sm/define + [:map {:title "move-project"} + [:team-id ::sm/uuid] + [:project-id ::sm/uuid]])) (sv/defmethod ::move-project - "Move projects between teams." + "Move projects between teams" {::doc/added "1.16" - ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] - (db/with-atomic [conn pool] - (move-project conn (assoc params :profile-id profile-id)))) + ::webhooks/event? true + ::sm/params schema:move-project} + [cfg {:keys [::rpc/profile-id] :as params}] + (db/tx-run! cfg #(move-project % (assoc params :profile-id profile-id)))) ;; --- COMMAND: Clone Template -(defn- clone-template! - [{:keys [::db/conn] :as cfg} {:keys [profile-id template-id project-id]}] - (let [template (tmpl/get-template-stream cfg template-id) - project (db/get-by-id conn :project project-id {:columns [:id :team-id]})] +(defn- clone-template + [{:keys [::wrk/executor ::bf.v1/project-id] :as cfg} template] + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + ;; NOTE: the importation process performs some operations that + ;; are not very friendly with virtual threads, and for avoid + ;; unexpected blocking of other concurrent operations we + ;; dispatch that operation to a dedicated executor. + (let [result (px/submit! executor (partial bf.v1/import-files! cfg template))] + (db/update! conn :project + {:modified-at (dt/now)} + {:id project-id}) + (deref result))))) +(def ^:private + schema:clone-template + (sm/define + [:map {:title "clone-template"} + [:project-id ::sm/uuid] + [:template-id ::sm/word-string]])) + +(sv/defmethod ::clone-template + "Clone into the specified project the template by its id." + {::doc/added "1.16" + ::sse/stream? true + ::webhooks/event? true + ::sm/params schema:clone-template} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}] + (let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]}) + _ (teams/check-edition-permissions! pool profile-id (:team-id project)) + template (tmpl/get-template-stream cfg template-id) + params (-> cfg + (assoc ::bf.v1/project-id (:id project)) + (assoc ::bf.v1/profile-id profile-id))] (when-not template (ex/raise :type :not-found :code :template-not-found :hint "template not found")) - (teams/check-edition-permissions! conn profile-id (:team-id project)) - - (-> cfg - ;; FIXME: maybe reuse the conn instead of creating more - ;; connections in the import process? - (dissoc ::db/conn) - (assoc ::binfile/input template) - (assoc ::binfile/project-id (:id project)) - (assoc ::binfile/ignore-index-errors? true) - (assoc ::binfile/migrate? true) - (binfile/import!)))) - -(def schema:clone-template - [:map {:title "clone-template"} - [:project-id ::sm/uuid] - [:template-id ::sm/word-string]]) - -(sv/defmethod ::clone-template - "Clone into the specified project the template by its id." - {::doc/added "1.16" - ::webhooks/event? true - ::sm/params schema:clone-template} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] - (db/with-atomic [conn pool] - (-> (assoc cfg ::db/conn conn) - (clone-template! (assoc params :profile-id profile-id))))) + (sse/response #(clone-template params template)))) ;; --- COMMAND: Get list of builtin templates -(s/def ::retrieve-list-of-builtin-templates any?) - -(sv/defmethod ::retrieve-list-of-builtin-templates - {::doc/added "1.10" - ::doc/deprecated "1.19"} - [cfg _params] - (mapv #(select-keys % [:id :name]) (::setup/templates cfg))) - (sv/defmethod ::get-builtin-templates {::doc/added "1.19"} [cfg _params] diff --git a/backend/src/app/rpc/commands/media.clj b/backend/src/app/rpc/commands/media.clj index 93885adf35..1bdcd3c502 100644 --- a/backend/src/app/rpc/commands/media.clj +++ b/backend/src/app/rpc/commands/media.clj @@ -23,9 +23,12 @@ [app.storage :as sto] [app.storage.tmp :as tmp] [app.util.services :as sv] + [app.util.time :as dt] + [app.worker :as-alias wrk] [clojure.spec.alpha :as s] [cuerdas.core :as str] - [datoteka.io :as io])) + [datoteka.io :as io] + [promesa.exec :as px])) (def default-max-file-size (* 1024 1024 10)) ; 10 MiB @@ -54,20 +57,25 @@ :opt-un [::id])) (sv/defmethod ::upload-file-media-object - {::doc/added "1.17"} + {::doc/added "1.17" + ::climit/id [[:process-image/by-profile ::rpc/profile-id] + [:process-image/global]]} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}] (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] + (files/check-edition-permissions! pool profile-id file-id) (media/validate-media-type! content) (media/validate-media-size! content) - (let [object (create-file-media-object cfg params) - props {:name (:name params) - :file-id file-id - :is-local (:is-local params) - :size (:size content) - :mtype (:mtype content)}] - (with-meta object - {::audit/replace-props props})))) + + (db/run! cfg (fn [cfg] + (let [object (create-file-media-object cfg params) + props {:name (:name params) + :file-id file-id + :is-local (:is-local params) + :size (:size content) + :mtype (:mtype content)}] + (with-meta object + {::audit/replace-props props})))))) (defn- big-enough-for-thumbnail? "Checks if the provided image info is big enough for @@ -142,17 +150,20 @@ (assoc ::image (process-main-image info))))) (defn create-file-media-object - [{:keys [::sto/storage ::db/pool] :as cfg} + [{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [id file-id is-local name content]}] - (let [result (-> (climit/configure cfg :process-image) - (climit/submit! (partial process-image content))) - + (let [result (px/invoke! executor (partial process-image content)) image (sto/put-object! storage (::image result)) thumb (when-let [params (::thumb result)] (sto/put-object! storage params))] - (db/exec-one! pool [sql:create-file-media-object + (db/update! conn :file + {:modified-at (dt/now) + :has-media-trimmed false} + {:id file-id}) + + (db/exec-one! conn [sql:create-file-media-object (or id (uuid/next)) file-id is-local name (:id image) @@ -176,9 +187,9 @@ [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id) - (create-file-media-object-from-url cfg params))) + (create-file-media-object-from-url cfg (assoc params :profile-id profile-id)))) -(defn- download-image +(defn download-image [{:keys [::http/client]} uri] (letfn [(parse-and-validate [{:keys [headers] :as response}] (let [size (some-> (get headers "content-length") d/parse-integer) @@ -209,7 +220,6 @@ {:method :get :uri uri} {:response-type :input-stream :sync? true}) {:keys [size mtype]} (parse-and-validate response) - path (tmp/tempfile :prefix "penpot.media.download.") written (io/write-to-file! body path :size size)] @@ -223,14 +233,22 @@ :path path :mtype mtype}))) - (defn- create-file-media-object-from-url [cfg {:keys [url name] :as params}] (let [content (download-image cfg url) params (-> params (assoc :content content) (assoc :name (or name (:filename content))))] - (create-file-media-object cfg params))) + + ;; NOTE: we use the climit here in a dynamic invocation because we + ;; don't want saturate the process-image limit with IO (download + ;; of external image) + (-> cfg + (assoc ::climit/id [[:process-image/by-profile (:profile-id params)] + [:process-image/global]]) + (assoc ::climit/profile-id (:profile-id params)) + (assoc ::climit/label "create-file-media-object-from-url") + (climit/invoke! db/run! cfg create-file-media-object params)))) ;; --- Clone File Media object (Upload and create from url) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index c79e4c773e..ccb6a8b2eb 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -8,12 +8,12 @@ (:require [app.auth :as auth] [app.common.data :as d] - [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.schema :as sm] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.db.sql :as-alias sql] [app.email :as eml] [app.http.session :as session] [app.loggers.audit :as audit] @@ -23,11 +23,14 @@ [app.rpc.climit :as climit] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] + [app.setup :as-alias setup] [app.storage :as sto] [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] - [cuerdas.core :as str])) + [app.worker :as-alias wrk] + [cuerdas.core :as str] + [promesa.exec :as px])) (declare check-profile-existence!) (declare decode-row) @@ -37,30 +40,39 @@ (declare strip-private-attrs) (declare verify-password) -(def schema:profile - [:map {:title "Profile"} - [:id ::sm/uuid] - [:fullname [::sm/word-string {:max 250}]] - [:email ::sm/email] - [:is-active {:optional true} :boolean] - [:is-blocked {:optional true} :boolean] - [:is-demo {:optional true} :boolean] - [:is-muted {:optional true} :boolean] - [:created-at {:optional true} ::sm/inst] - [:modified-at {:optional true} ::sm/inst] - [:default-project-id {:optional true} ::sm/uuid] - [:default-team-id {:optional true} ::sm/uuid] - [:props {:optional true} - [:map-of {:title "ProfileProps"} :keyword :any]]]) +(defn clean-email + "Clean and normalizes email address string" + [email] + (let [email (str/lower email) + email (if (str/starts-with? email "mailto:") + (subs email 7) + email)] + email)) -(def profile? - (sm/pred-fn schema:profile)) +(def ^:private + schema:profile + (sm/define + [:map {:title "Profile"} + [:id ::sm/uuid] + [:fullname [::sm/word-string {:max 250}]] + [:email ::sm/email] + [:is-active {:optional true} :boolean] + [:is-blocked {:optional true} :boolean] + [:is-demo {:optional true} :boolean] + [:is-muted {:optional true} :boolean] + [:created-at {:optional true} ::sm/inst] + [:modified-at {:optional true} ::sm/inst] + [:default-project-id {:optional true} ::sm/uuid] + [:default-team-id {:optional true} ::sm/uuid] + [:props {:optional true} + [:map-of {:title "ProfileProps"} :keyword :any]]])) ;; --- QUERY: Get profile (own) (sv/defmethod ::get-profile {::rpc/auth false ::doc/added "1.18" + ::sm/params [:map] ::sm/result schema:profile} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id]}] ;; We need to return the anonymous profile object in two cases, when @@ -81,11 +93,13 @@ ;; --- MUTATION: Update Profile (own) -(def schema:update-profile - [:map {:title "update-profile"} - [:fullname [::sm/word-string {:max 250}]] - [:lang {:optional true} [:string {:max 5}]] - [:theme {:optional true} [:string {:max 250}]]]) +(def ^:private + schema:update-profile + (sm/define + [:map {:title "update-profile"} + [:fullname [::sm/word-string {:max 250}]] + [:lang {:optional true} [:string {:max 5}]] + [:theme {:optional true} [:string {:max 250}]]])) (sv/defmethod ::update-profile {::doc/added "1.0" @@ -93,23 +107,18 @@ ::sm/result schema:profile} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}] - (dm/assert! - "expected valid profile data" - (profile? params)) - (db/with-atomic [conn pool] ;; NOTE: we need to retrieve the profile independently if we use ;; it or not for explicit locking and avoid concurrent updates of ;; the same row/object. - (let [profile (-> (db/get-by-id conn :profile profile-id ::db/for-update? true) + (let [profile (-> (db/get-by-id conn :profile profile-id ::sql/for-update true) (decode-row)) ;; Update the profile map with direct params profile (-> profile (assoc :fullname fullname) (assoc :lang lang) - (assoc :theme theme)) - ] + (assoc :theme theme))] (db/update! conn :profile {:fullname fullname @@ -130,32 +139,32 @@ (declare update-profile-password!) (declare invalidate-profile-session!) -(def schema:update-profile-password - [:map {:title "update-profile-password"} - [:password [::sm/word-string {:max 500}]] - ;; Social registered users don't have old-password - [:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]]) +(def ^:private + schema:update-profile-password + (sm/define + [:map {:title "update-profile-password"} + [:password [::sm/word-string {:max 500}]] + ;; Social registered users don't have old-password + [:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]])) (sv/defmethod ::update-profile-password - {:doc/added "1.0" + {::doc/added "1.0" ::sm/params schema:update-profile-password - ::sm/result :nil} + ::climit/id :auth/global} + [cfg {:keys [::rpc/profile-id password] :as params}] - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}] - (db/with-atomic [conn pool] - (let [cfg (assoc cfg ::db/conn conn) - profile (validate-password! cfg (assoc params :profile-id profile-id)) - session-id (::session/id params)] + (db/tx-run! cfg (fn [cfg] + (let [profile (validate-password! cfg (assoc params :profile-id profile-id)) + session-id (::session/id params)] - (when (= (str/lower (:email profile)) - (str/lower (:password params))) - (ex/raise :type :validation - :code :email-as-password - :hint "you can't use your email as password")) + (when (= (:email profile) (str/lower (:password params))) + (ex/raise :type :validation + :code :email-as-password + :hint "you can't use your email as password")) - (update-profile-password! conn (assoc profile :password password)) - (invalidate-profile-session! cfg profile-id session-id) - nil))) + (update-profile-password! cfg (assoc profile :password password)) + (invalidate-profile-session! cfg profile-id session-id) + nil)))) (defn- invalidate-profile-session! "Removes all sessions except the current one." @@ -165,7 +174,7 @@ (defn- validate-password! [{:keys [::db/conn] :as cfg} {:keys [profile-id old-password] :as params}] - (let [profile (db/get-by-id conn :profile profile-id ::db/for-update? true)] + (let [profile (db/get-by-id conn :profile profile-id ::sql/for-update true)] (when (and (not= (:password profile) "!") (not (:valid (verify-password cfg old-password (:password profile))))) (ex/raise :type :validation @@ -173,20 +182,23 @@ profile)) (defn update-profile-password! - [conn {:keys [id password] :as profile}] + [{:keys [::db/conn] :as cfg} {:keys [id password] :as profile}] (when-not (db/read-only? conn) (db/update! conn :profile - {:password (auth/derive-password password)} - {:id id}))) + {:password (derive-password cfg password)} + {:id id}) + nil)) ;; --- MUTATION: Update Photo (declare upload-photo) (declare update-profile-photo) -(def schema:update-profile-photo - [:map {:title "update-profile-photo"} - [:file ::media/upload]]) +(def ^:private + schema:update-profile-photo + (sm/define + [:map {:title "update-profile-photo"} + [:file ::media/upload]])) (sv/defmethod ::update-profile-photo {:doc/added "1.1" @@ -200,8 +212,9 @@ (defn update-profile-photo [{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id file] :as params}] + (let [photo (upload-photo cfg params) - profile (db/get-by-id pool :profile profile-id ::db/for-update? true)] + profile (db/get-by-id pool :profile profile-id ::sql/for-update true)] ;; Schedule deletion of old photo (when-let [id (:photo-id profile)] @@ -237,9 +250,12 @@ :content-type (:mtype thumb)})) (defn upload-photo - [{:keys [::sto/storage] :as cfg} {:keys [file]}] - (let [params (-> (climit/configure cfg :process-image) - (climit/submit! (partial generate-thumbnail! file)))] + [{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file]}] + (let [params (-> cfg + (assoc ::climit/id :process-image/global) + (assoc ::climit/label "upload-photo") + (assoc ::climit/executor executor) + (climit/invoke! generate-thumbnail! file))] (sto/put-object! storage params))) @@ -248,9 +264,11 @@ (declare ^:private request-email-change!) (declare ^:private change-email-immediately!) -(def schema:request-email-change - [:map {:title "request-email-change"} - [:email ::sm/email]]) +(def ^:private + schema:request-email-change + (sm/define + [:map {:title "request-email-change"} + [:email ::sm/email]])) (sv/defmethod ::request-email-change {::doc/added "1.0" @@ -261,7 +279,7 @@ cfg (assoc cfg ::conn conn) params (assoc params :profile profile - :email (str/lower email))] + :email (clean-email email))] (if (contains? cf/flags :smtp) (request-email-change! cfg params) (change-email-immediately! cfg params))))) @@ -279,12 +297,12 @@ (defn- request-email-change! [{:keys [::conn] :as cfg} {:keys [profile email] :as params}] - (let [token (tokens/generate (::main/props cfg) + (let [token (tokens/generate (::setup/props cfg) {:iss :change-email :exp (dt/in-future "15m") :profile-id (:id profile) :email email}) - ptoken (tokens/generate (::main/props cfg) + ptoken (tokens/generate (::setup/props cfg) {:iss :profile-identity :profile-id (:id profile) :exp (dt/in-future {:days 30})})] @@ -315,16 +333,18 @@ ;; --- MUTATION: Update Profile Props -(def schema:update-profile-props - [:map {:title "update-profile-props"} - [:props [:map-of :keyword :any]]]) +(def ^:private + schema:update-profile-props + (sm/define + [:map {:title "update-profile-props"} + [:props [:map-of :keyword :any]]])) (sv/defmethod ::update-profile-props {::doc/added "1.0" ::sm/params schema:update-profile-props} [{:keys [::db/pool]} {:keys [::rpc/profile-id props]}] (db/with-atomic [conn pool] - (let [profile (get-profile conn profile-id ::db/for-update? true) + (let [profile (get-profile conn profile-id ::sql/for-update true) props (reduce-kv (fn [props k v] ;; We don't accept namespaced keys (if (simple-ident? k) @@ -398,10 +418,9 @@ where email = ? and deleted_at is null) as val") -(defn check-profile-existence! +(defn- check-profile-existence! [conn {:keys [email] :as params}] - (let [email (str/lower email) - result (db/exec-one! conn [sql:profile-existence email])] + (let [result (db/exec-one! conn [sql:profile-existence email])] (when (:val result) (ex/raise :type :validation :code :email-already-exists)) @@ -416,7 +435,7 @@ (defn get-profile-by-email "Returns a profile looked up by email or `nil` if not match found." [conn email] - (->> (db/exec! conn [sql:profile-by-email (str/lower email)]) + (->> (db/exec! conn [sql:profile-by-email (clean-email email)]) (map decode-row) (first))) @@ -431,15 +450,13 @@ (into {} (filter (fn [[k _]] (simple-ident? k))) props)) (defn derive-password - [cfg password] + [{:keys [::wrk/executor]} password] (when password - (-> (climit/configure cfg :derive-password) - (climit/submit! (partial auth/derive-password password))))) + (px/invoke! executor (partial auth/derive-password password)))) (defn verify-password - [cfg password password-data] - (-> (climit/configure cfg :derive-password) - (climit/submit! (partial auth/verify-password password password-data)))) + [{:keys [::wrk/executor]} password password-data] + (px/invoke! executor (partial auth/verify-password password password-data))) (defn decode-row [{:keys [props] :as row}] diff --git a/backend/src/app/rpc/commands/projects.clj b/backend/src/app/rpc/commands/projects.clj index ac1ce660ec..caa3fe7a0a 100644 --- a/backend/src/app/rpc/commands/projects.clj +++ b/backend/src/app/rpc/commands/projects.clj @@ -9,6 +9,7 @@ [app.common.data.macros :as dm] [app.common.spec :as us] [app.db :as db] + [app.db.sql :as-alias sql] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as webhooks] [app.rpc :as-alias rpc] @@ -50,11 +51,11 @@ is-owner (boolean (some :is-owner rows)) is-admin (boolean (some :is-admin rows)) can-edit (boolean (some :can-edit rows))] - (when (seq rows) - {:is-owner is-owner - :is-admin (or is-owner is-admin) - :can-edit (or is-owner is-admin can-edit) - :can-read true}))) + (when (seq rows) + {:is-owner is-owner + :is-admin (or is-owner is-admin) + :can-edit (or is-owner is-admin can-edit) + :can-read true}))) (def has-edit-permissions? (perms/make-edition-predicate-fn get-permissions)) @@ -189,8 +190,8 @@ {:project-id (:id project) :profile-id profile-id :team-id team-id - :is-pinned true}) - (assoc project :is-pinned true)))) + :is-pinned false}) + (assoc project :is-pinned false)))) ;; --- MUTATION: Toggle Project Pin @@ -233,7 +234,7 @@ [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) - (let [project (db/get-by-id conn :project id ::db/for-update? true)] + (let [project (db/get-by-id conn :project id ::sql/for-update true)] (db/update! conn :project {:name name} {:id id}) @@ -259,7 +260,8 @@ (check-edition-permissions! conn profile-id id) (let [project (db/update! conn :project {:deleted-at (dt/now)} - {:id id :is-default false})] + {:id id :is-default false} + {::db/return-keys true})] (rph/with-meta (rph/wrap) {::audit/props {:team-id (:team-id project) :name (:name project) diff --git a/backend/src/app/rpc/commands/search.clj b/backend/src/app/rpc/commands/search.clj index 1c4026d6db..5710156458 100644 --- a/backend/src/app/rpc/commands/search.clj +++ b/backend/src/app/rpc/commands/search.clj @@ -9,6 +9,7 @@ [app.common.spec :as us] [app.db :as db] [app.rpc :as-alias rpc] + [app.rpc.commands.files :refer [resolve-public-uri]] [app.rpc.doc :as-alias doc] [app.util.services :as sv] [clojure.spec.alpha :as s])) @@ -37,12 +38,15 @@ ) select distinct f.id, + f.revn, f.project_id, f.created_at, f.modified_at, f.name, - f.is_shared + f.is_shared, + ft.media_id from file as f + left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn) inner join projects as pr on (f.project_id = pr.id) where f.name ilike ('%' || ? || '%') and (f.deleted_at is null or f.deleted_at > now()) @@ -50,10 +54,16 @@ (defn search-files [conn profile-id team-id search-term] - (db/exec! conn [sql:search-files - profile-id team-id - profile-id team-id - search-term])) + (->> (db/exec! conn [sql:search-files + profile-id team-id + profile-id team-id + search-term]) + (mapv (fn [row] + (if-let [media-id (:media-id row)] + (-> row + (dissoc :media-id) + (assoc :thumbnail-uri (resolve-public-uri media-id))) + (dissoc row :media-id)))))) (s/def ::team-id ::us/uuid) (s/def ::search-files ::us/string) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index e2e88e6209..f62f8bc6ae 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.features :as cfeat] [app.common.logging :as l] [app.common.schema :as sm] [app.common.spec :as us] @@ -25,6 +26,7 @@ [app.rpc.helpers :as rph] [app.rpc.permissions :as perms] [app.rpc.quotes :as quotes] + [app.setup :as-alias setup] [app.storage :as sto] [app.tokens :as tokens] [app.util.services :as sv] @@ -55,11 +57,11 @@ is-owner (boolean (some :is-owner rows)) is-admin (boolean (some :is-admin rows)) can-edit (boolean (some :can-edit rows))] - (when (seq rows) - {:is-owner is-owner - :is-admin (or is-owner is-admin) - :can-edit (or is-owner is-admin can-edit) - :can-read true}))) + (when (seq rows) + {:is-owner is-owner + :is-admin (or is-owner is-admin) + :can-edit (or is-owner is-admin can-edit) + :can-read true}))) (def has-admin-permissions? (perms/make-admin-predicate-fn get-permissions)) @@ -82,24 +84,23 @@ (defn decode-row [{:keys [features] :as row}] (cond-> row - features (assoc :features (db/decode-pgarray features #{})))) + (some? features) (assoc :features (db/decode-pgarray features #{})))) ;; --- Query: Teams -(declare retrieve-teams) +(declare get-teams) -(def counter (volatile! 0)) - -(s/def ::get-teams - (s/keys :req [::rpc/profile-id])) +(def ^:private schema:get-teams + [:map {:title "get-teams"}]) (sv/defmethod ::get-teams - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:get-teams} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (dm/with-open [conn (db/open pool)] - (retrieve-teams conn profile-id))) + (get-teams conn profile-id))) -(def sql:teams +(def sql:get-teams-with-permissions "select t.*, tp.is_owner, tp.is_admin, @@ -124,37 +125,77 @@ (dissoc :is-owner :is-admin :can-edit) (assoc :permissions permissions)))) -(defn retrieve-teams +(defn get-teams [conn profile-id] (let [profile (profile/get-profile conn profile-id)] - (->> (db/exec! conn [sql:teams (:default-team-id profile) profile-id]) + (->> (db/exec! conn [sql:get-teams-with-permissions (:default-team-id profile) profile-id]) (map decode-row) - (mapv process-permissions)))) + (map process-permissions) + (vec)))) ;; --- Query: Team (by ID) -(declare retrieve-team) +(declare get-team) -(s/def ::get-team - (s/keys :req [::rpc/profile-id] - :req-un [::id])) +(def ^:private schema:get-team + [:and + [:map {:title "get-team"} + [:id {:optional true} ::sm/uuid] + [:file-id {:optional true} ::sm/uuid]] + + [:fn (fn [params] + (or (contains? params :id) + (contains? params :file-id)))]]) (sv/defmethod ::get-team - {::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}] - (dm/with-open [conn (db/open pool)] - (retrieve-team conn profile-id id))) + {::doc/added "1.17" + ::sm/params schema:get-team} + [{:keys [::db/pool]} {:keys [::rpc/profile-id id file-id]}] + (get-team pool :profile-id profile-id :team-id id :file-id file-id)) -(defn retrieve-team - [conn profile-id team-id] - (let [profile (profile/get-profile conn profile-id) - sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?") - result (db/exec-one! conn [sql (:default-team-id profile) profile-id team-id])] +(defn get-team + [conn & {:keys [profile-id team-id project-id file-id] :as params}] + + (dm/assert! + "connection or pool is mandatory" + (or (db/connection? conn) + (db/pool? conn))) + + (dm/assert! + "profile-id is mandatory" + (uuid? profile-id)) + + (let [{:keys [default-team-id] :as profile} (profile/get-profile conn profile-id) + result (cond + (some? team-id) + (let [sql (str "WITH teams AS (" sql:get-teams-with-permissions + ") SELECT * FROM teams WHERE id=?")] + (db/exec-one! conn [sql default-team-id profile-id team-id])) + + (some? project-id) + (let [sql (str "WITH teams AS (" sql:get-teams-with-permissions ") " + "SELECT t.* FROM teams AS t " + " JOIN project AS p ON (p.team_id = t.id) " + " WHERE p.id=?")] + (db/exec-one! conn [sql default-team-id profile-id project-id])) + + (some? file-id) + (let [sql (str "WITH teams AS (" sql:get-teams-with-permissions ") " + "SELECT t.* FROM teams AS t " + " JOIN project AS p ON (p.team_id = t.id) " + " JOIN file AS f ON (f.project_id = p.id) " + " WHERE f.id=?")] + (db/exec-one! conn [sql default-team-id profile-id file-id])) + + :else + (throw (IllegalArgumentException. "invalid arguments")))] (when-not result (ex/raise :type :not-found :code :team-does-not-exist)) - (-> result decode-row process-permissions))) + (-> result + (decode-row) + (process-permissions)))) ;; --- Query: Team Members @@ -170,44 +211,48 @@ join profile as p on (p.id = tp.profile_id) where tp.team_id = ?") -(defn retrieve-team-members +(defn get-team-members [conn team-id] (db/exec! conn [sql:team-members team-id])) -(s/def ::team-id ::us/uuid) -(s/def ::get-team-members - (s/keys :req [::rpc/profile-id] - :req-un [::team-id])) +(def ^:private schema:get-team-memebrs + [:map {:title "get-team-members"} + [:team-id ::sm/uuid]]) (sv/defmethod ::get-team-members - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:get-team-memebrs} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] (dm/with-open [conn (db/open pool)] (check-read-permissions! conn profile-id team-id) - (retrieve-team-members conn team-id))) - + (get-team-members conn team-id))) ;; --- Query: Team Users -(declare retrieve-users) -(declare retrieve-team-for-file) +(declare get-users) +(declare get-team-for-file) -(s/def ::get-team-users - (s/and (s/keys :req [::rpc/profile-id] - :opt-un [::team-id ::file-id]) - #(or (:team-id %) (:file-id %)))) +(def ^:private schema:get-team-users + [:and {:title "get-team-users"} + [:map + [:team-id {:optional true} ::sm/uuid] + [:file-id {:optional true} ::sm/uuid]] + [:fn #(or (contains? % :team-id) + (contains? % :file-id))]]) (sv/defmethod ::get-team-users - {::doc/added "1.17"} + "Get team users by team-id or by file-id" + {::doc/added "1.17" + ::sm/params schema:get-team-users} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id]}] (dm/with-open [conn (db/open pool)] (if team-id (do (check-read-permissions! conn profile-id team-id) - (retrieve-users conn team-id)) - (let [{team-id :id} (retrieve-team-for-file conn file-id)] + (get-users conn team-id)) + (let [{team-id :id} (get-team-for-file conn file-id)] (check-read-permissions! conn profile-id team-id) - (retrieve-users conn team-id))))) + (get-users conn team-id))))) ;; This is a similar query to team members but can contain more data ;; because some user can be explicitly added to project or file (not @@ -238,44 +283,44 @@ join file as f on (p.id = f.project_id) where f.id = ?") -(defn retrieve-users +(defn get-users [conn team-id] (db/exec! conn [sql:team-users team-id team-id team-id])) -(defn retrieve-team-for-file +(defn get-team-for-file [conn file-id] (->> [sql:team-by-file file-id] (db/exec-one! conn))) ;; --- Query: Team Stats -(declare retrieve-team-stats) +(declare get-team-stats) -(s/def ::get-team-stats - (s/keys :req [::rpc/profile-id] - :req-un [::team-id])) +(def ^:private schema:get-team-stats + [:map {:title "get-team-stats"} + [:team-id ::sm/uuid]]) (sv/defmethod ::get-team-stats - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:get-team-stats} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] (dm/with-open [conn (db/open pool)] (check-read-permissions! conn profile-id team-id) - (retrieve-team-stats conn team-id))) + (get-team-stats conn team-id))) (def sql:team-stats "select (select count(*) from project where team_id = ?) as projects, (select count(*) from file as f join project as p on (p.id = f.project_id) where p.team_id = ?) as files") -(defn retrieve-team-stats +(defn get-team-stats [conn team-id] (db/exec-one! conn [sql:team-stats team-id team-id])) - ;; --- Query: Team invitations -(s/def ::get-team-invitations - (s/keys :req [::rpc/profile-id] - :req-un [::team-id])) +(def ^:private schema:get-team-invitations + [:map {:title "get-team-invitations"} + [:team-id ::sm/uuid]]) (def sql:team-invitations "select email_to as email, role, (valid_until < now()) as expired @@ -287,7 +332,8 @@ (mapv #(update % :role keyword)))) (sv/defmethod ::get-team-invitations - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:get-team-invitations} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] (dm/with-open [conn (db/open pool)] (check-read-permissions! conn profile-id team-id) @@ -302,25 +348,32 @@ (declare ^:private create-team-role) (declare ^:private create-team-default-project) -(s/def ::create-team - (s/keys :req [::rpc/profile-id] - :req-un [::name] - :opt-un [::id])) +(def ^:private schema:create-team + [:map {:title "create-team"} + [:name :string] + [:features {:optional true} ::cfeat/features] + [:id {:optional true} ::sm/uuid]]) (sv/defmethod ::create-team - {::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] - (db/with-atomic [conn pool] - (quotes/check-quote! conn {::quotes/id ::quotes/teams-per-profile - ::quotes/profile-id profile-id}) + {::doc/added "1.17" + ::sm/params schema:create-team} + [cfg {:keys [::rpc/profile-id] :as params}] + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (quotes/check-quote! conn {::quotes/id ::quotes/teams-per-profile + ::quotes/profile-id profile-id}) - (create-team conn (assoc params :profile-id profile-id)))) + (let [features (-> (cfeat/get-enabled-features cf/flags) + (cfeat/check-client-features! (:features params)))] + (create-team cfg (assoc params + :profile-id profile-id + :features features)))))) (defn create-team "This is a complete team creation process, it creates the team object and all related objects (default role and default project)." - [conn params] - (let [team (create-team* conn params) + [cfg-or-conn params] + (let [conn (db/get-connection cfg-or-conn) + team (create-team* conn params) params (assoc params :team-id (:id team) :role :owner) @@ -329,13 +382,16 @@ (assoc team :default-project-id (:id project)))) (defn- create-team* - [conn {:keys [id name is-default] :as params}] + [conn {:keys [id name is-default features] :as params}] (let [id (or id (uuid/next)) - is-default (if (boolean? is-default) is-default false)] - (db/insert! conn :team - {:id id - :name name - :is-default is-default}))) + is-default (if (boolean? is-default) is-default false) + features (db/create-array conn "text" features) + team (db/insert! conn :team + {:id id + :name name + :features features + :is-default is-default})] + (decode-row team))) (defn- create-team-role [conn {:keys [profile-id team-id role] :as params}] @@ -361,14 +417,16 @@ ;; namespace too. (defn create-project - [conn {:keys [id team-id name is-default] :as params}] + [conn {:keys [id team-id name is-default created-at modified-at]}] (let [id (or id (uuid/next)) - is-default (if (boolean? is-default) is-default false)] - (db/insert! conn :project - {:id id - :name name - :team-id team-id - :is-default is-default}))) + is-default (if (boolean? is-default) is-default false) + params {:id id + :name name + :team-id team-id + :is-default is-default + :created-at created-at + :modified-at modified-at}] + (db/insert! conn :project (d/without-nils params)))) (defn create-project-role [conn profile-id project-id role] @@ -401,7 +459,7 @@ (defn leave-team [conn {:keys [profile-id id reassign-to]}] (let [perms (get-permissions conn profile-id id) - members (retrieve-team-members conn id)] + members (get-team-members conn id)] (cond ;; we can only proceed if there are more members in the team @@ -485,10 +543,15 @@ (s/def ::team-id ::us/uuid) (s/def ::member-id ::us/uuid) +(s/def ::role #{:owner :admin :editor}) + ;; Temporarily disabled viewer role ;; https://tree.taiga.io/project/penpot/issue/1083 -;; (s/def ::role #{:owner :admin :editor :viewer}) -(s/def ::role #{:owner :admin :editor}) +(def valid-roles + #{:owner :admin :editor #_:viewer}) + +(def schema:role + [::sm/one-of valid-roles]) (defn role->params [role] @@ -505,7 +568,7 @@ ;; convenience, if this becomes a bottleneck or problematic, ;; we will change it to more efficient fetch mechanisms. (let [perms (get-permissions conn profile-id team-id) - members (retrieve-team-members conn team-id) + members (get-team-members conn team-id) member (d/seek #(= member-id (:id %)) members) is-owner? (:is-owner perms) @@ -601,7 +664,7 @@ (defn update-team-photo [{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id team-id] :as params}] - (let [team (retrieve-team pool profile-id team-id) + (let [team (get-team pool :profile-id profile-id :team-id team-id) photo (profile/upload-photo cfg params)] (db/with-atomic [conn pool] @@ -613,8 +676,8 @@ ;; Save new photo (db/update! pool :team - {:photo-id (:id photo)} - {:id team-id}) + {:photo-id (:id photo)} + {:id team-id}) (assoc team :photo-id (:id photo))))) @@ -629,7 +692,7 @@ (defn- create-invitation-token [cfg {:keys [profile-id valid-until team-id member-id member-email role]}] - (tokens/generate (::main/props cfg) + (tokens/generate (::setup/props cfg) {:iss :team-invitation :exp valid-until :profile-id profile-id @@ -640,14 +703,15 @@ (defn- create-profile-identity-token [cfg profile] - (tokens/generate (::main/props cfg) + (tokens/generate (::setup/props cfg) {:iss :profile-identity :profile-id (:id profile) :exp (dt/in-future {:days 30})})) (defn- create-invitation [{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}] - (let [member (profile/get-profile-by-email conn email)] + (let [email (profile/clean-email email) + member (profile/get-profile-by-email conn email)] (when (and member (not (eml/allow-send-emails? conn member))) (ex/raise :type :validation @@ -675,7 +739,8 @@ (role->params role))] ;; Insert the invited member to the team - (db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true}) + (db/insert! conn :team-profile-rel params + {::db/on-conflict-do-nothing? true}) ;; If profile is not yet verified, mark it as verified because ;; accepting an invitation link serves as verification. @@ -740,7 +805,8 @@ (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id team-id) profile (db/get-by-id conn :profile profile-id) - team (db/get-by-id conn :team team-id)] + team (db/get-by-id conn :team team-id) + emails (into #{} (map profile/clean-email) emails)] (run! (partial quotes/check-quote! conn) (list {::quotes/id ::quotes/invitations-per-team @@ -771,7 +837,7 @@ ;; We don't re-send inviation to already existing members (remove (partial contains? members)) (map (fn [email] - {:email (str/lower email) + {:email email :team team :profile profile :role role})) @@ -789,21 +855,32 @@ (s/merge ::create-team (s/keys :req-un [::emails ::role]))) + +(def ^:private schema:create-team-with-invitations + [:map {:title "create-team-with-invitations"} + [:name :string] + [:features {:optional true} ::cfeat/features] + [:id {:optional true} ::sm/uuid] + [:emails ::sm/set-of-emails] + [:role schema:role]]) + (sv/defmethod ::create-team-with-invitations - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:create-team-with-invitations} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}] (db/with-atomic [conn pool] (let [params (assoc params :profile-id profile-id) - team (create-team conn params) + cfg (assoc cfg ::db/conn conn) + team (create-team cfg params) profile (db/get-by-id conn :profile profile-id) - cfg (assoc cfg ::db/conn conn)] + emails (into #{} (map profile/clean-email) emails)] ;; Create invitations for all provided emails. (->> emails (map (fn [email] {:team team :profile profile - :email (str/lower email) + :email email :role role})) (run! (partial create-invitation cfg))) @@ -840,17 +917,20 @@ {::doc/added "1.17"} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}] (check-read-permissions! pool profile-id team-id) - (let [invit (-> (db/get pool :team-invitation + (let [email (profile/clean-email email) + invit (-> (db/get pool :team-invitation {:team-id team-id - :email-to (str/lower email)}) + :email-to email}) (update :role keyword)) + member (profile/get-profile-by-email pool (:email-to invit)) token (create-invitation-token cfg {:team-id (:team-id invit) :profile-id profile-id :valid-until (:valid-until invit) :role (:role invit) :member-id (:id member) - :member-email (or (:email member) (:email-to invit))})] + :member-email (or (:email member) + (profile/clean-email (:email-to invit)))})] {:token token})) ;; --- Mutation: Update invitation role @@ -871,7 +951,7 @@ (db/update! conn :team-invitation {:role (name role) :updated-at (dt/now)} - {:team-id team-id :email-to (str/lower email)}) + {:team-id team-id :email-to (profile/clean-email email)}) nil))) ;; --- Mutation: Delete invitation @@ -892,5 +972,6 @@ (let [invitation (db/delete! conn :team-invitation {:team-id team-id - :email-to (str/lower email)})] + :email-to (profile/clean-email email)} + {::db/return-keys true})] (rph/wrap nil {::audit/props {:invitation-id (:id invitation)}}))))) diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index 0d8ef83918..e072c90d6c 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -18,6 +18,7 @@ [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.rpc.quotes :as quotes] + [app.setup :as-alias setup] [app.tokens :as tokens] [app.tokens.spec.team-invitation :as-alias spec.team-invitation] [app.util.services :as sv] @@ -38,24 +39,25 @@ ::doc/module :auth} [{:keys [::db/pool] :as cfg} {:keys [token] :as params}] (db/with-atomic [conn pool] - (let [claims (tokens/verify (::main/props cfg) {:token token}) + (let [claims (tokens/verify (::setup/props cfg) {:token token}) cfg (assoc cfg :conn conn)] (process-token cfg params claims)))) (defmethod process-token :change-email [{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}] - (when (profile/get-profile-by-email conn email) - (ex/raise :type :validation - :code :email-already-exists)) + (let [email (profile/clean-email email)] + (when (profile/get-profile-by-email conn email) + (ex/raise :type :validation + :code :email-already-exists)) - (db/update! conn :profile - {:email email} - {:id profile-id}) + (db/update! conn :profile + {:email email} + {:id profile-id}) - (rph/with-meta claims - {::audit/name "update-profile-email" - ::audit/props {:email email} - ::audit/profile-id profile-id})) + (rph/with-meta claims + {::audit/name "update-profile-email" + ::audit/props {:email email} + ::audit/profile-id profile-id}))) (defmethod process-token :verify-email [{:keys [conn] :as cfg} _ {:keys [profile-id] :as claims}] @@ -105,7 +107,7 @@ ::quotes/team-id team-id}) ;; Insert the invited member to the team - (db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true}) + (db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true}) ;; If profile is not yet verified, mark it as verified because ;; accepting an invitation link serves as verification. diff --git a/backend/src/app/rpc/commands/viewer.clj b/backend/src/app/rpc/commands/viewer.clj index e7db69eced..c2887ef967 100644 --- a/backend/src/app/rpc/commands/viewer.clj +++ b/backend/src/app/rpc/commands/viewer.clj @@ -6,27 +6,49 @@ (ns app.rpc.commands.viewer (:require - [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.features :as cfeat] [app.common.schema :as sm] + [app.config :as cf] [app.db :as db] [app.rpc :as-alias rpc] [app.rpc.commands.comments :as comments] [app.rpc.commands.files :as files] + [app.rpc.commands.teams :as teams] [app.rpc.cond :as-alias cond] [app.rpc.doc :as-alias doc] [app.util.services :as sv])) ;; --- QUERY: View Only Bundle -(defn- get-project - [conn id] - (db/get-by-id conn :project id {:columns [:id :name :team-id]})) +(defn- remove-not-allowed-pages + [data allowed] + (-> data + (update :pages (fn [pages] (filterv #(contains? allowed %) pages))) + (update :pages-index select-keys allowed))) + +(defn- get-view-only-bundle + [{:keys [::db/conn] :as cfg} {:keys [profile-id file-id ::perms] :as params}] + (let [file (files/get-file cfg file-id) + + project (db/get conn :project + {:id (:project-id file)} + {:columns [:id :name :team-id]}) + + team (-> (db/get conn :team {:id (:team-id project)}) + (teams/decode-row)) + + _ (-> (cfeat/get-team-enabled-features cf/flags team) + (cfeat/check-client-features! (:features params)) + (cfeat/check-file-features! (:features file))) + + file (cond-> file + (= :share-link (:type perms)) + (update :data remove-not-allowed-pages (:pages perms)) + + :always + (update :data select-keys [:id :options :pages :pages-index :components])) -(defn- get-bundle - [conn file-id profile-id features] - (let [file (files/get-file conn file-id features) - project (get-project conn (:project-id file)) libs (files/get-file-libraries conn file-id) users (comments/get-file-comments-users conn file-id profile-id) links (->> (db/query conn :share-link {:file-id file-id}) @@ -41,54 +63,43 @@ (dissoc :flags))))) fonts (db/query conn :team-font-variant - {:team-id (:team-id project) + {:team-id (:id team) :deleted-at nil})] - {:file file - :users users + {:users users :fonts fonts :project project :share-links links - :libraries libs})) + :libraries libs + :file file + :team team + :permissions perms})) -(defn- remove-not-allowed-pages - [data allowed] - (-> data - (update :pages (fn [pages] (filterv #(contains? allowed %) pages))) - (update :pages-index select-keys allowed))) - -(defn get-view-only-bundle - [conn {:keys [profile-id file-id share-id features] :as params}] - (let [perms (files/get-permissions conn profile-id file-id share-id) - bundle (-> (get-bundle conn file-id profile-id features) - (assoc :permissions perms))] - - ;; When we have neither profile nor share, we just return a not - ;; found response to the user. - (when-not perms - (ex/raise :type :not-found - :code :object-not-found - :hint "object not found")) - - (update bundle :file - (fn [file] - (cond-> file - (= :share-link (:type perms)) - (update :data remove-not-allowed-pages (:pages perms)) - - :always - (update :data select-keys [:id :options :pages :pages-index :components])))))) - -(sm/def! ::get-view-only-bundle +(def schema:get-view-only-bundle [:map {:title "get-view-only-bundle"} [:file-id ::sm/uuid] [:share-id {:optional true} ::sm/uuid] - [:features {:optional true} ::files/features]]) + [:features {:optional true} ::cfeat/features]]) (sv/defmethod ::get-view-only-bundle {::rpc/auth false ::doc/added "1.17" - ::sm/params ::get-view-only-bundle} - [{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}] - (dm/with-open [conn (db/open pool)] - (get-view-only-bundle conn (assoc params :profile-id profile-id)))) + ::sm/params schema:get-view-only-bundle} + [system {:keys [::rpc/profile-id file-id share-id] :as params}] + (db/run! system + (fn [{:keys [::db/conn] :as system}] + (let [perms (files/get-permissions conn profile-id file-id share-id) + params (-> params + (assoc ::perms perms) + (assoc :profile-id profile-id))] + + ;; When we have neither profile nor share, we just return a not + ;; found response to the user. + (when-not perms + (ex/raise :type :not-found + :code :object-not-found + :hint "object not found")) + + (get-view-only-bundle system params))))) + + diff --git a/backend/src/app/rpc/commands/webhooks.clj b/backend/src/app/rpc/commands/webhooks.clj index 1c6b812c56..13a5d02101 100644 --- a/backend/src/app/rpc/commands/webhooks.clj +++ b/backend/src/app/rpc/commands/webhooks.clj @@ -95,7 +95,8 @@ :mtype mtype :error-code nil :error-count 0} - {:id id}) + {:id id} + {::db/return-keys true}) (decode-row))) (sv/defmethod ::create-webhook diff --git a/backend/src/app/rpc/cond.clj b/backend/src/app/rpc/cond.clj index b683ded138..3fe03c8210 100644 --- a/backend/src/app/rpc/cond.clj +++ b/backend/src/app/rpc/cond.clj @@ -29,7 +29,7 @@ [app.util.services :as-alias sv] [buddy.core.codecs :as bc] [buddy.core.hash :as bh] - [yetti.response :as yrs])) + [ring.response :as-alias rres])) (def ^{:dynamic true @@ -51,13 +51,13 @@ [_ f {:keys [::get-object ::key-fn ::reuse-key?] :as mdata}] (if (and (ifn? get-object) (ifn? key-fn)) (do - (l/debug :hint "instrumenting method" :service (::sv/name mdata)) + (l/trc :hint "instrumenting method" :service (::sv/name mdata)) (fn [cfg {:keys [::key] :as params}] (if *enabled* (let [key' (when (or key reuse-key?) (some->> (get-object cfg params) (key-fn params) (fmt-key)))] (if (and (some? key) (= key key')) - (fn [_] {::yrs/status 304}) + (fn [_] {::rres/status 304}) (let [result (f cfg params) etag (or (and reuse-key? key') (some-> result meta ::key fmt-key) diff --git a/backend/src/app/rpc/doc.clj b/backend/src/app/rpc/doc.clj index 24451e553d..185f3fc4c2 100644 --- a/backend/src/app/rpc/doc.clj +++ b/backend/src/app/rpc/doc.clj @@ -16,6 +16,7 @@ [app.common.schema.openapi :as oapi] [app.common.schema.registry :as sr] [app.config :as cf] + [app.http.sse :as-alias sse] [app.loggers.webhooks :as-alias webhooks] [app.rpc :as-alias rpc] [app.util.json :as json] @@ -27,7 +28,7 @@ [integrant.core :as ig] [malli.transform :as mt] [pretty-spec.core :as ps] - [yetti.response :as yrs])) + [ring.response :as-alias rres])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; DOC (human readable) @@ -55,6 +56,7 @@ :module (or (some-> (::module mdata) d/name) (-> (:ns mdata) (str/split ".") last)) :auth (::rpc/auth mdata true) + :sse (::sse/stream? mdata false) :webhook (::webhooks/event? mdata false) :docs (::sv/docstring mdata) :deprecated (::deprecated mdata) @@ -86,11 +88,11 @@ (let [params (:query-params request) pstyle (:type params "js") context (assoc context :param-style pstyle)] - {::yrs/status 200 - ::yrs/body (-> (io/resource "app/templates/api-doc.tmpl") - (tmpl/render context))})) + {::rres/status 200 + ::rres/body (-> (io/resource "app/templates/api-doc.tmpl") + (tmpl/render context))})) (fn [_] - {::yrs/status 404}))) + {::rres/status 404}))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; OPENAPI / SWAGGER (v3.1) @@ -141,8 +143,7 @@ {:name (-> mdata ::sv/name d/name) :module (-> (:ns mdata) (str/split ".") last) - :repr {:post rpost}})) - ] + :repr {:post rpost}}))] (let [definitions (atom {}) options {:registry sr/default-registry @@ -158,27 +159,27 @@ (map (fn [doc] [(str/ffmt "/command/%" (:name doc)) (:repr doc)])) (into {})))] - {:openapi "3.0.0" - :info {:version (:main cf/version)} - :servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri)) + {:openapi "3.0.0" + :info {:version (:main cf/version)} + :servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri)) ;; :description "penpot backend" - }] - :security - {:api_key []} + }] + :security + {:api_key []} - :paths paths - :components {:schemas @definitions}}))) + :paths paths + :components {:schemas @definitions}}))) (defn openapi-json-handler [context] (if (contains? cf/flags :backend-openapi-doc) (fn [_] - {::yrs/status 200 - ::yrs/headers {"content-type" "application/json; charset=utf-8"} - ::yrs/body (json/encode context)}) + {::rres/status 200 + ::rres/headers {"content-type" "application/json; charset=utf-8"} + ::rres/body (json/encode context)}) (fn [_] - {::yrs/status 404}))) + {::rres/status 404}))) (defn openapi-handler [] @@ -189,12 +190,12 @@ context {:public-uri (cf/get :public-uri) :swagger-js swagger-js :swagger-css swagger-cs}] - {::yrs/status 200 - ::yrs/headers {"content-type" "text/html"} - ::yrs/body (-> (io/resource "app/templates/openapi.tmpl") - (tmpl/render context))})) + {::rres/status 200 + ::rres/headers {"content-type" "text/html"} + ::rres/body (-> (io/resource "app/templates/openapi.tmpl") + (tmpl/render context))})) (fn [_] - {::yrs/status 404}))) + {::rres/status 404}))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MODULE INIT diff --git a/backend/src/app/rpc/helpers.clj b/backend/src/app/rpc/helpers.clj index 69d1a2d717..87b91f545b 100644 --- a/backend/src/app/rpc/helpers.clj +++ b/backend/src/app/rpc/helpers.clj @@ -11,7 +11,7 @@ [app.common.data.macros :as dm] [app.http :as-alias http] [app.rpc :as-alias rpc] - [yetti.response :as-alias yrs])) + [ring.response :as-alias rres])) ;; A utilty wrapper object for wrap service responses that does not ;; implements the IObj interface that make possible attach metadata to @@ -77,4 +77,4 @@ (fn [_ response] (let [exp (if (integer? max-age) max-age (inst-ms max-age)) val (dm/fmt "max-age=%" (int (/ exp 1000.0)))] - (update response ::yrs/headers assoc "cache-control" val))))) + (update response ::rres/headers assoc "cache-control" val))))) diff --git a/backend/src/app/rpc/quotes.clj b/backend/src/app/rpc/quotes.clj index 4cdc3800d8..3244bd03f9 100644 --- a/backend/src/app/rpc/quotes.clj +++ b/backend/src/app/rpc/quotes.clj @@ -7,8 +7,10 @@ (ns app.rpc.quotes "Penpot resource usage quotes." (:require + [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.logging :as l] + [app.common.schema :as sm] [app.common.spec :as us] [app.config :as cf] [app.db :as db] @@ -23,21 +25,15 @@ ;; PUBLIC API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::conn ::db/pool-or-conn) -(s/def ::file-id ::us/uuid) -(s/def ::team-id ::us/uuid) -(s/def ::project-id ::us/uuid) -(s/def ::profile-id ::us/uuid) -(s/def ::incr (s/and int? pos?)) -(s/def ::target ::us/string) - -(s/def ::quote - (s/keys :req [::id ::profile-id] - :opt [::conn - ::team-id - ::project-id - ::file-id - ::incr])) +(def ^:private schema:quote + (sm/define + [:map {:title "Quote"} + [::team-id {:optional true} ::sm/uuid] + [::project-id {:optional true} ::sm/uuid] + [::file-id {:optional true} ::sm/uuid] + [::incr {:optional true} [:int {:min 0}]] + [::id :keyword] + [::profile-id ::sm/uuid]])) (def ^:private enabled (volatile! true)) @@ -52,15 +48,22 @@ (vswap! enabled (constantly false))) (defn check-quote! - [conn quote] - (us/assert! ::db/pool-or-conn conn) - (us/assert! ::quote quote) + [ds quote] + (dm/assert! + "expected valid quote map" + (sm/validate schema:quote quote)) + (when (contains? cf/flags :quotes) (when @enabled - (check-quote (assoc quote ::conn conn ::target (name (::id quote))))))) + ;; This approach add flexibility on how and where the + ;; check-quote! can be called (in or out of transaction) + (db/run! ds (fn [cfg] + (-> (merge cfg quote) + (assoc ::target (name (::id quote))) + (check-quote))))))) (defn- send-notification! - [{:keys [::conn] :as params}] + [{:keys [::db/conn] :as params}] (l/warn :hint "max quote reached" :target (::target params) :profile-id (some-> params ::profile-id str) @@ -93,7 +96,7 @@ :content content}]})))) (defn- generic-check! - [{:keys [::conn ::incr ::quote-sql ::count-sql ::default ::target] :or {incr 1} :as params}] + [{:keys [::db/conn ::incr ::quote-sql ::count-sql ::default ::target] :or {incr 1} :as params}] (let [quote (->> (db/exec! conn quote-sql) (map :quote) (reduce max (- Integer/MAX_VALUE))) @@ -347,7 +350,6 @@ (assoc ::count-sql [sql:get-comments-per-file file-id]) (generic-check!))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; QUOTE: DEFAULT ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/rpc/retry.clj b/backend/src/app/rpc/retry.clj index 9cb048ea91..5e2d620131 100644 --- a/backend/src/app/rpc/retry.clj +++ b/backend/src/app/rpc/retry.clj @@ -6,8 +6,8 @@ (ns app.rpc.retry (:require + [app.common.exceptions :as ex] [app.common.logging :as l] - [app.db :as db] [app.util.services :as sv]) (:import org.postgresql.util.PSQLException)) @@ -15,49 +15,41 @@ (defn conflict-exception? "Check if exception matches a insertion conflict on postgresql." [e] - (and (instance? PSQLException e) - (= "23505" (.getSQLState ^PSQLException e)))) + (when-let [cause (ex/instance? PSQLException e)] + (= "23505" (.getSQLState ^PSQLException cause)))) + +(def ^:private always-false + (constantly false)) + +(defn invoke! + [{:keys [::max-retries] :or {max-retries 3} :as cfg} f & args] + (loop [rnum 1] + (let [match? (get cfg ::when always-false) + result (try + (apply f cfg args) + (catch Throwable cause + (if (and (match? cause) (<= rnum max-retries)) + ::retry + (throw cause))))] + (if (= ::retry result) + (let [label (get cfg ::label "anonymous")] + (l/warn :hint "retrying operation" :label label :retry rnum) + (recur (inc rnum))) + result)))) -(def ^:private always-false (constantly false)) (defn wrap-retry - [_ f {:keys [::matches ::sv/name] :or {matches always-false} :as mdata}] + [_ f {:keys [::sv/name] :as mdata}] - (when (::enabled mdata) - (l/debug :hint "wrapping retry" :name name)) - - (if-let [max-retries (::max-retries mdata)] - (fn [cfg params] - ((fn run [retry] - (try - (f cfg params) - (catch Throwable cause - (if (matches cause) - (let [current-retry (inc retry)] - (l/trace :hint "running retry algorithm" :retry current-retry) - (if (<= current-retry max-retries) - (run current-retry) - (throw cause))) - (throw cause))))) 1)) + (if (::enabled mdata) + (let [max-retries (get mdata ::max-retries 3) + matches? (get mdata ::when always-false)] + (l/trc :hint "wrapping retry" :name name :max-retries max-retries) + (fn [cfg params] + (-> cfg + (assoc ::max-retries max-retries) + (assoc ::when matches?) + (assoc ::label name) + (invoke! f params)))) f)) -(defmacro with-retry - [{:keys [::when ::max-retries ::label ::db/conn] :or {max-retries 3}} & body] - `(let [conn# ~conn] - (assert (or (nil? conn#) (db/connection? conn#)) "invalid database connection") - (loop [tnum# 1] - (let [result# (let [sp# (some-> conn# db/savepoint)] - (try - (let [result# (do ~@body)] - (some->> sp# (db/release! conn#)) - result#) - (catch Throwable cause# - (some->> sp# (db/rollback! conn#)) - (if (and (~when cause#) (<= tnum# ~max-retries)) - ::retry - (throw cause#)))))] - (if (= ::retry result#) - (do - (l/warn :hint "retrying operation" :label ~label :retry tnum#) - (recur (inc tnum#))) - result#))))) diff --git a/backend/src/app/setup.clj b/backend/src/app/setup.clj index 8e889e2b46..d187f3e5fb 100644 --- a/backend/src/app/setup.clj +++ b/backend/src/app/setup.clj @@ -50,16 +50,15 @@ :cause cause)))) instance-id))) -(s/def ::main/key ::us/string) -(s/def ::main/props - (s/map-of ::us/keyword some?)) +(s/def ::key ::us/string) +(s/def ::props (s/map-of ::us/keyword some?)) (defmethod ig/pre-init-spec ::props [_] (s/keys :req [::db/pool] - :opt [::main/key])) + :opt [::key])) (defmethod ig/init-key ::props - [_ {:keys [::db/pool ::main/key] :as cfg}] + [_ {:keys [::db/pool ::key] :as cfg}] (db/with-atomic [conn pool] (db/xact-lock! conn 0) (when-not key diff --git a/backend/src/app/setup/templates.clj b/backend/src/app/setup/templates.clj index c51bc98c8d..3c70c7dbc6 100644 --- a/backend/src/app/setup/templates.clj +++ b/backend/src/app/setup/templates.clj @@ -19,14 +19,18 @@ [datoteka.fs :as fs] [integrant.core :as ig])) -(def ^:private schema:template - [:map {:title "Template"} - [:id ::sm/word-string] - [:name ::sm/word-string] - [:file-uri ::sm/word-string]]) +(def ^:private + schema:template + (sm/define + [:map {:title "Template"} + [:id ::sm/word-string] + [:name ::sm/word-string] + [:file-uri ::sm/word-string]])) -(def ^:private schema:templates - [:vector schema:template]) +(def ^:private + schema:templates + (sm/define + [:vector schema:template])) (defmethod ig/init-key ::setup/templates [_ _] @@ -35,13 +39,13 @@ (dm/verify! "expected a valid templates file" - (sm/valid? schema:templates templates)) + (sm/check! schema:templates templates)) (doseq [{:keys [id path] :as template} templates] (let [path (or path (fs/join dest id))] (if (fs/exists? path) - (l/debug :hint "template file" :id id :state "present" :path (dm/str path)) - (l/debug :hint "template file" :id id :state "absent")))) + (l/dbg :hint "template file" :id id :state "present" :path (dm/str path)) + (l/dbg :hint "template file" :id id :state "absent")))) templates)) diff --git a/backend/src/app/srepl.clj b/backend/src/app/srepl.clj index fcb802c02f..1a87bcf7d7 100644 --- a/backend/src/app/srepl.clj +++ b/backend/src/app/srepl.clj @@ -10,7 +10,7 @@ [app.common.logging :as l] [app.common.spec :as us] [app.config :as cf] - [app.srepl.ext] + [app.srepl.cli] [app.srepl.main] [app.util.json :as json] [app.util.locks :as locks] @@ -36,7 +36,9 @@ lock (locks/create)] (ccs/prepl *in* (fn [m] - (binding [*out* out, *flush-on-newline* true, *print-readably* true] + (binding [*out* out, + *flush-on-newline* true, + *print-readably* true] (locks/locking lock (println (json/encode-str m)))))))) @@ -44,13 +46,10 @@ (s/def ::port ::us/integer) (s/def ::host ::us/not-empty-string) -(s/def ::flag #{:urepl-server :prepl-server}) -(s/def ::type #{::prepl ::urepl}) -(s/def ::key (s/tuple ::type ::us/keyword)) (defmethod ig/pre-init-spec ::server [_] - (s/keys :req [::flag ::host ::port])) + (s/keys :req [::host ::port])) (defmethod ig/prep-key ::server [[type _] cfg] @@ -59,6 +58,12 @@ (defmethod ig/init-key ::server [[type _] {:keys [::flag ::port ::host] :as cfg}] (when (contains? cf/flags flag) + + (l/inf :hint "initializing repl server" + :name (name type) + :port port + :host host) + (let [accept (case type ::prepl 'app.srepl/json-repl ::urepl 'app.srepl/user-repl) @@ -67,14 +72,8 @@ :name (name type) :accept accept}] - (l/info :msg "initializing repl server" - :name (name type) - :port port - :host host) - (ccs/start-server params) - - params))) + (assoc params :type type)))) (defmethod ig/halt-key! ::server [_ params] diff --git a/backend/src/app/srepl/binfile.clj b/backend/src/app/srepl/binfile.clj new file mode 100644 index 0000000000..ae203eb164 --- /dev/null +++ b/backend/src/app/srepl/binfile.clj @@ -0,0 +1,39 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.srepl.binfile + (:require + [app.binfile.v2 :as binfile.v2] + [app.db :as db] + [app.main :as main] + [app.srepl.helpers :as h] + [cuerdas.core :as str])) + +(defn export-team! + [team-id] + (let [team-id (h/parse-uuid team-id)] + (binfile.v2/export-team! main/system team-id))) + +(defn import-team! + [path & {:keys [owner rollback?] :or {rollback? true}}] + (db/tx-run! (assoc main/system ::db/rollback rollback?) + (fn [cfg] + (let [team (binfile.v2/import-team! cfg path) + owner (cond + (string? owner) + (db/get* cfg :profile {:email (str/lower owner)}) + (uuid? owner) + (db/get* cfg :profile {:id owner}))] + + (when owner + (db/insert! cfg :team-profile-rel + {:team-id (:id team) + :profile-id (:id owner) + :is-admin true + :is-owner true + :can-edit true})) + + team)))) diff --git a/backend/src/app/srepl/ext.clj b/backend/src/app/srepl/cli.clj similarity index 60% rename from backend/src/app/srepl/ext.clj rename to backend/src/app/srepl/cli.clj index 4c880ecb45..6bcca5c0c8 100644 --- a/backend/src/app/srepl/ext.clj +++ b/backend/src/app/srepl/cli.clj @@ -4,14 +4,17 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.srepl.ext +(ns app.srepl.cli "PREPL API for external usage (CLI or ADMIN)" (:require [app.auth :as auth] [app.common.exceptions :as ex] [app.common.uuid :as uuid] [app.db :as db] + [app.main :as main] [app.rpc.commands.auth :as cmd.auth] + [app.srepl.components-v2 :refer [migrate-teams!]] + [app.util.events :as events] [app.util.json :as json] [app.util.time :as dt] [cuerdas.core :as str])) @@ -21,18 +24,18 @@ (or (deref (requiring-resolve 'app.main/system)) (deref (requiring-resolve 'user/system)))) -(defmulti ^:private run-json-cmd* ::cmd) +(defmulti ^:private exec-command ::cmd) -(defn run-json-cmd +(defn exec "Entry point with external tools integrations that uses PREPL interface for interacting with running penpot backend." [data] - (let [data (json/decode data) - params (merge {::cmd (keyword (:cmd data "default"))} - (:params data))] - (run-json-cmd* params))) + (let [data (json/decode data)] + (-> {::cmd (keyword (:cmd data "default"))} + (merge (:params data)) + (exec-command)))) -(defmethod run-json-cmd* :create-profile +(defmethod exec-command :create-profile [{:keys [fullname email password is-active] :or {is-active true}}] (when-let [system (get-current-system)] @@ -46,7 +49,7 @@ (->> (cmd.auth/create-profile! conn params) (cmd.auth/create-profile-rels! conn)))))) -(defmethod run-json-cmd* :update-profile +(defmethod exec-command :update-profile [{:keys [fullname email password is-active]}] (when-let [system (get-current-system)] (db/with-atomic [conn (:app.db/pool system)] @@ -63,11 +66,10 @@ (let [res (db/update! conn :profile params {:email email - :deleted-at nil} - {::db/return-keys? false})] - (pos? (:next.jdbc/update-count res)))))))) + :deleted-at nil})] + (pos? (db/get-update-count res)))))))) -(defmethod run-json-cmd* :delete-profile +(defmethod exec-command :delete-profile [{:keys [email soft]}] (when-not email (ex/raise :type :assertion @@ -80,14 +82,12 @@ (let [res (if soft (db/update! conn :profile {:deleted-at (dt/now)} - {:email email :deleted-at nil} - {::db/return-keys? false}) + {:email email :deleted-at nil}) (db/delete! conn :profile - {:email email} - {::db/return-keys? false}))] - (pos? (:next.jdbc/update-count res)))))) + {:email email}))] + (pos? (db/get-update-count res)))))) -(defmethod run-json-cmd* :search-profile +(defmethod exec-command :search-profile [{:keys [email]}] (when-not email (ex/raise :type :assertion @@ -101,11 +101,44 @@ " where email similar to ? order by created_at desc limit 100")] (db/exec! conn [sql email]))))) -(defmethod run-json-cmd* :derive-password +(defmethod exec-command :derive-password [{:keys [password]}] (auth/derive-password password)) -(defmethod run-json-cmd* :default +(defmethod exec-command :migrate-v2 + [_] + (letfn [(on-progress-report [{:keys [elapsed completed errors]}] + (println (str/ffmt "-> Progress: completed: %, errors: %, elapsed: %" + completed errors elapsed))) + + (on-progress [{:keys [op name]}] + (case op + :migrate-team + (println (str/ffmt "-> Migrating team: \"%\"" name)) + :migrate-file + (println (str/ffmt "=> Migrating file: \"%\"" name)) + nil)) + + (on-event [[type payload]] + (case type + :progress-report (on-progress-report payload) + :progress (on-progress payload) + :error (on-error payload) + nil)) + + (on-error [cause] + (println "EE:" (ex-message cause)))] + + (println "The components/v2 migration started...") + + (try + (let [result (-> (partial migrate-teams! main/system {:rollback? true}) + (events/run-with! on-event))] + (println (str/ffmt "Migration process finished (elapsed: %)" (:elapsed result)))) + (catch Throwable cause + (on-error cause))))) + +(defmethod exec-command :default [{:keys [::cmd]}] (ex/raise :type :internal :code :not-implemented diff --git a/backend/src/app/srepl/components_v2.clj b/backend/src/app/srepl/components_v2.clj new file mode 100644 index 0000000000..0922904938 --- /dev/null +++ b/backend/src/app/srepl/components_v2.clj @@ -0,0 +1,635 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.srepl.components-v2 + (:require + [app.common.data :as d] + [app.common.logging :as l] + [app.common.uuid :as uuid] + [app.db :as db] + [app.features.components-v2 :as feat] + [app.main :as main] + [app.srepl.helpers :as h] + [app.svgo :as svgo] + [app.util.cache :as cache] + [app.util.events :as events] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [cuerdas.core :as str] + [promesa.exec :as px] + [promesa.exec.semaphore :as ps] + [promesa.util :as pu])) + +(def ^:dynamic *scope* nil) +(def ^:dynamic *semaphore* nil) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; PRIVATE HELPERS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- report-progress-files + [tpoint] + (fn [_ _ oldv newv] + (when (or (not= (:processed-files oldv) + (:processed-files newv)) + (not= (:errors oldv) + (:errors newv))) + (let [completed (:processed-files newv 0) + errors (:errors newv 0) + elapsed (dt/format-duration (tpoint))] + (events/tap :progress-report + {:elapsed elapsed + :completed completed + :errors errors}) + (l/dbg :hint "progress" + :completed completed + :elapsed elapsed))))) + +(defn- report-progress-teams + [tpoint] + (fn [_ _ oldv newv] + (when (or (not= (:processed-teams oldv) + (:processed-teams newv)) + (not= (:errors oldv) + (:errors newv))) + (let [completed (:processed-teams newv 0) + errors (:errors newv 0) + elapsed (dt/format-duration (tpoint))] + (events/tap :progress-report + {:elapsed elapsed + :completed completed + :errors errors}) + (l/dbg :hint "progress" + :completed completed + :elapsed elapsed))))) + +(def ^:private sql:get-teams-by-created-at + "WITH teams AS ( + SELECT id, features + FROM team + WHERE deleted_at IS NULL + ORDER BY created_at DESC + ) SELECT * FROM TEAMS %(pred)s") + +(def ^:private sql:get-teams-by-graphics + "WITH teams AS ( + SELECT t.id, t.features, + (SELECT count(*) + FROM file_media_object AS fmo + JOIN file AS f ON (f.id = fmo.file_id) + JOIN project AS p ON (p.id = f.project_id) + WHERE p.team_id = t.id + AND fmo.mtype = 'image/svg+xml' + AND fmo.is_local = false) AS graphics + FROM team AS t + WHERE t.deleted_at IS NULL + ORDER BY 3 ASC + ) + SELECT * FROM teams %(pred)s") + +(def ^:private sql:get-teams-by-activity + "WITH teams AS ( + SELECT t.id, t.features, + (SELECT coalesce(max(date_trunc('month', f.modified_at)), date_trunc('month', t.modified_at)) + FROM file AS f + JOIN project AS p ON (f.project_id = p.id) + WHERE p.team_id = t.id) AS updated_at, + (SELECT coalesce(count(*), 0) + FROM file AS f + JOIN project AS p ON (f.project_id = p.id) + WHERE p.team_id = t.id) AS total_files + FROM team AS t + WHERE t.deleted_at IS NULL + ORDER BY 3 DESC, 4 DESC + ) + SELECT * FROM teams %(pred)s") + +(def ^:private sql:get-teams-by-report + "WITH teams AS ( + SELECT t.id t.features, mr.name + FROM migration_team_report AS mr + JOIN team AS t ON (t.id = mr.team_id) + WHERE t.deleted_at IS NULL + AND mr.error IS NOT NULL + ORDER BY mr.created_at + ) SELECT id, features FROM teams %(pred)s") + +(def ^:private sql:get-files-by-created-at + "SELECT id, features + FROM file + WHERE deleted_at IS NULL + ORDER BY created_at DESC") + +(def ^:private sql:get-files-by-modified-at + "SELECT id, features + FROM file + WHERE deleted_at IS NULL + ORDER BY modified_at DESC") + +(def ^:private sql:get-files-by-graphics + "WITH files AS ( + SELECT f.id, f.features, + (SELECT count(*) FROM file_media_object AS fmo + WHERE fmo.mtype = 'image/svg+xml' + AND fmo.is_local = false + AND fmo.file_id = f.id) AS graphics + FROM file AS f + WHERE f.deleted_at IS NULL + ORDER BY 3 ASC + ) SELECT * FROM files %(pred)s") + +(def ^:private sql:get-files-by-report + "WITH files AS ( + SELECT f.id, f.features, mr.label + FROM migration_file_report AS mr + JOIN file AS f ON (f.id = mr.file_id) + WHERE f.deleted_at IS NULL + AND mr.error IS NOT NULL + ORDER BY mr.created_at + ) SELECT id, features FROM files %(pred)s") + +(defn- read-pred + [entries] + (let [entries (if (and (vector? entries) + (keyword? (first entries))) + [entries] + entries)] + (loop [params [] + queries [] + entries (seq entries)] + (if-let [[op val field] (first entries)] + (let [field (name field) + cond (case op + :lt (str/ffmt "% < ?" field) + :lte (str/ffmt "% <= ?" field) + :gt (str/ffmt "% > ?" field) + :gte (str/ffmt "% >= ?" field) + :eq (str/ffmt "% = ?" field))] + (recur (conj params val) + (conj queries cond) + (rest entries))) + + (let [sql (apply str "WHERE " (str/join " AND " queries))] + (apply vector sql params)))))) + +(defn- get-teams + [conn query pred] + (let [query (d/nilv query :created-at) + sql (case query + :created-at sql:get-teams-by-created-at + :activity sql:get-teams-by-activity + :graphics sql:get-teams-by-graphics + :report sql:get-teams-by-report) + sql (if pred + (let [[pred-sql & pred-params] (read-pred pred)] + (apply vector + (str/format sql {:pred pred-sql}) + pred-params)) + [(str/format sql {:pred ""})])] + + (->> (db/cursor conn sql {:chunk-size 500}) + (map feat/decode-row) + (remove (fn [{:keys [features]}] + (contains? features "components/v2"))) + (map :id)))) + +(defn- get-files + [conn query pred] + (let [query (d/nilv query :created-at) + sql (case query + :created-at sql:get-files-by-created-at + :modified-at sql:get-files-by-modified-at + :graphics sql:get-files-by-graphics + :report sql:get-files-by-report) + sql (if pred + (let [[pred-sql & pred-params] (read-pred pred)] + (apply vector + (str/format sql {:pred pred-sql}) + pred-params)) + [(str/format sql {:pred ""})])] + + (->> (db/cursor conn sql {:chunk-size 500}) + (map feat/decode-row) + (remove (fn [{:keys [features]}] + (contains? features "components/v2"))) + (map :id)))) + +(def ^:private sql:team-report-table + "CREATE UNLOGGED TABLE IF NOT EXISTS migration_team_report ( + id bigserial NOT NULL, + label text NOT NULL, + team_id UUID NOT NULL, + error text NULL, + created_at timestamptz NOT NULL DEFAULT now(), + elapsed bigint NOT NULL, + PRIMARY KEY (label, created_at, id))") + +(def ^:private sql:file-report-table + "CREATE UNLOGGED TABLE IF NOT EXISTS migration_file_report ( + id bigserial NOT NULL, + label text NOT NULL, + file_id UUID NOT NULL, + error text NULL, + created_at timestamptz NOT NULL DEFAULT now(), + elapsed bigint NOT NULL, + PRIMARY KEY (label, created_at, id))") + +(defn- create-report-tables! + [system] + (db/exec-one! system [sql:team-report-table]) + (db/exec-one! system [sql:file-report-table])) + +(defn- clean-team-reports! + [system label] + (db/delete! system :migration-team-report {:label label})) + +(defn- team-report! + [system team-id label elapsed error] + (db/insert! system :migration-team-report + {:label label + :team-id team-id + :elapsed (inst-ms elapsed) + :error error} + {::db/return-keys false})) + +(defn- clean-file-reports! + [system label] + (db/delete! system :migration-file-report {:label label})) + +(defn- file-report! + [system file-id label elapsed error] + (db/insert! system :migration-file-report + {:label label + :file-id file-id + :elapsed (inst-ms elapsed) + :error error} + {::db/return-keys false})) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; PUBLIC API +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn migrate-file! + [file-id & {:keys [rollback? validate? label cache skip-on-graphic-error?] + :or {rollback? true + validate? false + skip-on-graphic-error? true}}] + (l/dbg :hint "migrate:start" :rollback rollback?) + (let [tpoint (dt/tpoint) + file-id (h/parse-uuid file-id) + cache (if (int? cache) + (cache/create :executor (::wrk/executor main/system) + :max-items cache) + nil)] + + (binding [feat/*stats* (atom {}) + feat/*cache* cache] + (try + (-> (assoc main/system ::db/rollback rollback?) + (feat/migrate-file! file-id + :validate? validate? + :skip-on-graphic-error? skip-on-graphic-error? + :label label)) + + (-> (deref feat/*stats*) + (assoc :elapsed (dt/format-duration (tpoint)))) + + (catch Throwable cause + (l/wrn :hint "migrate:error" :cause cause)) + + (finally + (let [elapsed (dt/format-duration (tpoint))] + (l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed))))))) + +(defn migrate-team! + [team-id & {:keys [rollback? skip-on-graphic-error? validate? label cache] + :or {rollback? true + validate? true + skip-on-graphic-error? true}}] + + (l/dbg :hint "migrate:start" :rollback rollback?) + + (let [team-id (h/parse-uuid team-id) + stats (atom {}) + tpoint (dt/tpoint) + + cache (if (int? cache) + (cache/create :executor (::wrk/executor main/system) + :max-items cache) + nil)] + + (add-watch stats :progress-report (report-progress-files tpoint)) + + (binding [feat/*stats* stats + feat/*cache* cache] + (try + (-> (assoc main/system ::db/rollback rollback?) + (feat/migrate-team! team-id + :label label + :validate? validate? + :skip-on-graphics-error? skip-on-graphic-error?)) + + (-> (deref feat/*stats*) + (assoc :elapsed (dt/format-duration (tpoint)))) + + (catch Throwable cause + (l/dbg :hint "migrate:error" :cause cause)) + + (finally + (let [elapsed (dt/format-duration (tpoint))] + (l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed))))))) + +(defn migrate-teams! + "A REPL helper for migrate all teams. + + This function starts multiple concurrent team migration processes + until thw maximum number of jobs is reached which by default has the + value of `1`. This is controled with the `:max-jobs` option. + + If you want to run this on multiple machines you will need to specify + the total number of partitions and the current partition. + + In order to get the report table populated, you will need to provide + a correct `:label`. That label is also used for persist a file + snaphot before continue with the migration." + [& {:keys [max-jobs max-items max-time rollback? validate? query + pred max-procs cache skip-on-graphic-error? + label partitions current-partition] + :or {validate? false + rollback? true + max-jobs 1 + current-partition 1 + skip-on-graphic-error? true + max-items Long/MAX_VALUE}}] + + (when (int? partitions) + (when-not (int? current-partition) + (throw (IllegalArgumentException. "missing `current-partition` parameter"))) + (when-not (<= 0 current-partition partitions) + (throw (IllegalArgumentException. "invalid value on `current-partition` parameter")))) + + (let [stats (atom {}) + tpoint (dt/tpoint) + mtime (some-> max-time dt/duration) + + factory (px/thread-factory :virtual false :prefix "penpot/migration/") + executor (px/cached-executor :factory factory) + + max-procs (or max-procs max-jobs) + sjobs (ps/create :permits max-jobs) + sprocs (ps/create :permits max-procs) + + cache (if (int? cache) + (cache/create :executor (::wrk/executor main/system) + :max-items cache) + nil) + migrate-team + (fn [team-id] + (let [tpoint (dt/tpoint)] + (try + (db/tx-run! (assoc main/system ::db/rollback rollback?) + (fn [system] + (db/exec-one! system ["SET idle_in_transaction_session_timeout = 0"]) + (feat/migrate-team! system team-id + :label label + :validate? validate? + :skip-on-graphic-error? skip-on-graphic-error?))) + + (when (string? label) + (team-report! main/system team-id label (tpoint) nil)) + + (catch Throwable cause + (l/wrn :hint "unexpected error on processing team (skiping)" + :team-id (str team-id)) + + (events/tap :error + (ex-info "unexpected error on processing team (skiping)" + {:team-id team-id} + cause)) + + (swap! stats update :errors (fnil inc 0)) + + (when (string? label) + (team-report! main/system team-id label (tpoint) (ex-message cause)))) + + (finally + (ps/release! sjobs))))) + + process-team + (fn [team-id] + (ps/acquire! sjobs) + (let [ts (tpoint)] + (if (and mtime (neg? (compare mtime ts))) + (do + (l/inf :hint "max time constraint reached" + :team-id (str team-id) + :elapsed (dt/format-duration ts)) + (ps/release! sjobs) + (reduced nil)) + + (px/run! executor (partial migrate-team team-id)))))] + + (l/dbg :hint "migrate:start" + :label label + :rollback rollback? + :max-jobs max-jobs + :max-items max-items) + + (add-watch stats :progress-report (report-progress-teams tpoint)) + + (binding [feat/*stats* stats + feat/*cache* cache + svgo/*semaphore* sprocs] + (try + (when (string? label) + (create-report-tables! main/system) + (clean-team-reports! main/system label)) + + (db/tx-run! main/system + (fn [{:keys [::db/conn] :as system}] + (db/exec! conn ["SET statement_timeout = 0"]) + (db/exec! conn ["SET idle_in_transaction_session_timeout = 0"]) + + (run! process-team + (->> (get-teams conn query pred) + (filter (fn [team-id] + (if (int? partitions) + (= current-partition (-> (uuid/hash-int team-id) + (mod partitions) + (inc))) + true))) + (take max-items))) + + ;; Close and await tasks + (pu/close! executor))) + + (-> (deref stats) + (assoc :elapsed (dt/format-duration (tpoint)))) + + (catch Throwable cause + (l/dbg :hint "migrate:error" :cause cause) + (events/tap :error cause)) + + (finally + (let [elapsed (dt/format-duration (tpoint))] + (l/dbg :hint "migrate:end" + :rollback rollback? + :elapsed elapsed))))))) + + +(defn migrate-files! + "A REPL helper for migrate all files. + + This function starts multiple concurrent file migration processes + until thw maximum number of jobs is reached which by default has the + value of `1`. This is controled with the `:max-jobs` option. + + If you want to run this on multiple machines you will need to specify + the total number of partitions and the current partition. + + In order to get the report table populated, you will need to provide + a correct `:label`. That label is also used for persist a file + snaphot before continue with the migration." + [& {:keys [max-jobs max-items max-time rollback? validate? query + pred max-procs cache skip-on-graphic-error? + label partitions current-partition] + :or {validate? false + rollback? true + max-jobs 1 + current-partition 1 + skip-on-graphic-error? true + max-items Long/MAX_VALUE}}] + + (when (int? partitions) + (when-not (int? current-partition) + (throw (IllegalArgumentException. "missing `current-partition` parameter"))) + (when-not (<= 0 current-partition partitions) + (throw (IllegalArgumentException. "invalid value on `current-partition` parameter")))) + + (let [stats (atom {}) + tpoint (dt/tpoint) + mtime (some-> max-time dt/duration) + + factory (px/thread-factory :virtual false :prefix "penpot/migration/") + executor (px/cached-executor :factory factory) + + max-procs (or max-procs max-jobs) + sjobs (ps/create :permits max-jobs) + sprocs (ps/create :permits max-procs) + + cache (if (int? cache) + (cache/create :executor (::wrk/executor main/system) + :max-items cache) + nil) + + migrate-file + (fn [file-id] + (let [tpoint (dt/tpoint)] + (try + (db/tx-run! (assoc main/system ::db/rollback rollback?) + (fn [system] + (db/exec-one! system ["SET idle_in_transaction_session_timeout = 0"]) + (feat/migrate-file! system file-id + :label label + :validate? validate? + :skip-on-graphic-error? skip-on-graphic-error?))) + + (when (string? label) + (file-report! main/system file-id label (tpoint) nil)) + + (catch Throwable cause + (l/wrn :hint "unexpected error on processing file (skiping)" + :file-id (str file-id)) + + (events/tap :error + (ex-info "unexpected error on processing file (skiping)" + {:file-id file-id} + cause)) + + (swap! stats update :errors (fnil inc 0)) + + (when (string? label) + (file-report! main/system file-id label (tpoint) (ex-message cause)))) + + (finally + (ps/release! sjobs))))) + + process-file + (fn [file-id] + (ps/acquire! sjobs) + (let [ts (tpoint)] + (if (and mtime (neg? (compare mtime ts))) + (do + (l/inf :hint "max time constraint reached" + :file-id (str file-id) + :elapsed (dt/format-duration ts)) + (ps/release! sjobs) + (reduced nil)) + + (px/run! executor (partial migrate-file file-id)))))] + + (l/dbg :hint "migrate:start" + :label label + :rollback rollback? + :max-jobs max-jobs + :max-items max-items) + + (add-watch stats :progress-report (report-progress-files tpoint)) + + (binding [feat/*stats* stats + feat/*cache* cache + svgo/*semaphore* sprocs] + (try + (when (string? label) + (create-report-tables! main/system) + (clean-file-reports! main/system label)) + + (db/tx-run! main/system + (fn [{:keys [::db/conn] :as system}] + (db/exec! conn ["SET statement_timeout = 0"]) + (db/exec! conn ["SET idle_in_transaction_session_timeout = 0"]) + + (run! process-file + (->> (get-files conn query pred) + (filter (fn [file-id] + (if (int? partitions) + (= current-partition (-> (uuid/hash-int file-id) + (mod partitions) + (inc))) + true))) + (take max-items))) + + ;; Close and await tasks + (pu/close! executor))) + + (-> (deref stats) + (assoc :elapsed (dt/format-duration (tpoint)))) + + (catch Throwable cause + (l/dbg :hint "migrate:error" :cause cause) + (events/tap :error cause)) + + (finally + (let [elapsed (dt/format-duration (tpoint))] + (l/dbg :hint "migrate:end" + :rollback rollback? + :elapsed elapsed))))))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; FILE PROCESS HELPERS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn delete-broken-files + [{:keys [id data] :as file}] + (if (-> data :options :components-v2 true?) + (do + (l/wrn :hint "found old components-v2 format" + :file-id (str id) + :file-name (:name file)) + (assoc file :deleted-at (dt/now))) + file)) diff --git a/backend/src/app/srepl/fixes.clj b/backend/src/app/srepl/fixes.clj index a294a25d37..ee40421dfc 100644 --- a/backend/src/app/srepl/fixes.clj +++ b/backend/src/app/srepl/fixes.clj @@ -5,71 +5,235 @@ ;; Copyright (c) KALEIDOS INC (ns app.srepl.fixes - "A collection of adhoc fixes scripts." + "A misc of fix functions" + (:refer-clojure :exclude [parse-uuid]) (:require + [app.binfile.common :as bfc] + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.changes :as cpc] + [app.common.files.helpers :as cfh] + [app.common.files.repair :as cfr] + [app.common.files.validate :as cfv] [app.common.logging :as l] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] [app.common.uuid :as uuid] + [app.db :as db] + [app.features.fdata :as feat.fdata] [app.srepl.helpers :as h])) -(defn repair-orphaned-shapes - "There are some shapes whose parent has been deleted. This function - detects them and puts them as children of the root node." - ([data] - (letfn [(is-orphan? [shape objects] - (and (some? (:parent-id shape)) - (nil? (get objects (:parent-id shape))))) +(defn disable-fdata-features + [{:keys [id features] :as file} _] + (when (or (contains? features "fdata/pointer-map") + (contains? features "fdata/objects-map")) + (l/warn :hint "disable fdata features" :file-id (str id)) + (-> file + (update :data feat.fdata/process-pointers deref) + (update :data feat.fdata/process-objects (partial into {})) + (update :features disj "fdata/pointer-map" "fdata/objects-map")))) - (update-page [page] - (let [objects (:objects page) - orphans (into #{} (filter #(is-orphan? % objects)) (vals objects))] - (if (seq orphans) - (do - (l/info :hint "found a file with orphans" :file-id (:id data) :broken-shapes (count orphans)) - (-> page - (h/update-shapes (fn [shape] - (if (contains? orphans shape) - (assoc shape :parent-id uuid/zero) - shape))) - (update-in [:objects uuid/zero :shapes] into (map :id) orphans))) - page)))] +(def sql:get-fdata-files + "SELECT id FROM file + WHERE deleted_at is NULL + AND (features @> '{fdata/pointer-map}' OR + features @> '{fdata/objects-map}') + ORDER BY created_at DESC") - (h/update-pages data update-page))) +(defn find-fdata-pointers + [{:keys [id features data] :as file} _] + (when (contains? features "fdata/pointer-map") + (let [pointers (feat.fdata/get-used-pointer-ids data)] + (l/warn :hint "found pointers" :file-id (str id) :pointers pointers) + nil))) - ;; special arity for to be called from h/analyze-files to search for - ;; files with possible issues +(defn repair-file-media + "A helper intended to be used with `srepl.main/process-files!` that + fixes all not propertly referenced file-media-object for a file" + [{:keys [id data] :as file} & _] + (let [conn (db/get-connection h/*system*) + used (bfc/collect-used-media data) + ids (db/create-array conn "uuid" used) + sql (str "SELECT * FROM file_media_object WHERE id = ANY(?)") + rows (db/exec! conn [sql ids]) + index (reduce (fn [index media] + (if (not= (:file-id media) id) + (let [media-id (uuid/next)] + (l/wrn :hint "found not referenced media" + :file-id (str id) + :media-id (str (:id media))) - ([file state] - (repair-orphaned-shapes (:data file)) - (update state :total (fnil inc 0)))) + (db/insert! conn :file-media-object + (-> media + (assoc :file-id id) + (assoc :id media-id))) + (assoc index (:id media) media-id)) + index)) + {} + rows)] -(defn rename-layout-attrs - ([file] - (let [found? (volatile! false)] - (letfn [(update-shape - [shape] - (when (or (= (:layout-flex-dir shape) :reverse-row) - (= (:layout-flex-dir shape) :reverse-column) - (= (:layout-wrap-type shape) :no-wrap)) - (vreset! found? true)) - (cond-> shape - (= (:layout-flex-dir shape) :reverse-row) - (assoc :layout-flex-dir :row-reverse) - (= (:layout-flex-dir shape) :reverse-column) - (assoc :layout-flex-dir :column-reverse) - (= (:layout-wrap-type shape) :no-wrap) - (assoc :layout-wrap-type :nowrap))) + (when (seq index) + (binding [bfc/*state* (atom {:index index})] + (update file :data (fn [fdata] + (-> fdata + (update :pages-index #'bfc/relink-shapes) + (update :components #'bfc/relink-shapes) + (update :media #'bfc/relink-media) + (d/without-nils)))))))) - (update-page - [page] - (h/update-shapes page update-shape))] - (let [new-file (update file :data h/update-pages update-page)] - (when @found? - (l/info :hint "Found attrs to rename in file" - :id (:id file) - :name (:name file))) - new-file)))) +(defn repair-file + "Internal helper for validate and repair the file. The operation is + applied multiple times untile file is fixed or max iteration counter + is reached (default 10)" + [file libs & {:keys [max-iterations] :or {max-iterations 10}}] - ([file state] - (rename-layout-attrs file) - (update state :total (fnil inc 0)))) \ No newline at end of file + (let [validate-and-repair + (fn [file libs iteration] + (when-let [errors (not-empty (cfv/validate-file file libs))] + (l/trc :hint "repairing file" + :file-id (str (:id file)) + :iteration iteration + :errors (count errors)) + (let [changes (cfr/repair-file file libs errors)] + (-> file + (update :revn inc) + (update :data cpc/process-changes changes))))) + + process-file + (fn [file libs] + (loop [file file + iteration 0] + (if (< iteration max-iterations) + (if-let [file (validate-and-repair file libs iteration)] + (recur file (inc iteration)) + file) + (do + (l/wrn :hint "max retry num reached on repairing file" + :file-id (str (:id file)) + :iteration iteration) + file)))) + + file' + (process-file file libs)] + + (when (not= (:revn file) (:revn file')) + (l/trc :hint "file repaired" :file-id (str (:id file)))) + + file')) + +(defn fix-touched-shapes-group + [file _] + ;; Remove :shapes-group from the touched elements + (letfn [(fix-fdata [data] + (-> data + (update :pages-index update-vals fix-container))) + + (fix-container [container] + (d/update-when container :objects update-vals fix-shape)) + + (fix-shape [shape] + (d/update-when shape :touched + (fn [touched] + (disj touched :shapes-group))))] + file (-> file + (update :data fix-fdata)))) + +(defn add-swap-slots + [file libs _opts] + ;; Detect swapped copies and try to generate a valid swap-slot. + (letfn [(process-fdata [data] + ;; Walk through all containers in the file, both pages and deleted components. + (reduce process-container data (ctf/object-containers-seq data))) + + (process-container [data container] + ;; Walk through all shapes in depth-first tree order. + (l/dbg :hint "Processing container" :type (:type container) :name (:name container)) + (let [root-shape (ctn/get-container-root container)] + (ctf/update-container data + container + #(reduce process-shape % (ctn/get-direct-children container root-shape))))) + + (process-shape [container shape] + ;; Look for head copies in the first level (either component roots or inside main components). + ;; Even if they have been swapped, we don't add slot to them because there is no way to know + ;; the original shape. Only children. + (if (and (ctk/instance-head? shape) + (ctk/in-component-copy? shape) + (nil? (ctk/get-swap-slot shape))) + (process-copy-head container shape) + (reduce process-shape container (ctn/get-direct-children container shape)))) + + (process-copy-head [container head-shape] + ;; Process recursively all children, comparing each one with the corresponding child in the main + ;; component, looking by position. If the shape-ref does not point to the found child, then it has + ;; been swapped and need to set up a slot. + (l/trc :hint "Processing copy-head" :id (:id head-shape) :name (:name head-shape)) + (let [component-shape (ctf/find-ref-shape file container libs head-shape :include-deleted? true :with-context? true) + component-container (:container (meta component-shape))] + (loop [container container + children (map #(ctn/get-shape container %) (:shapes head-shape)) + component-children (map #(ctn/get-shape component-container %) (:shapes component-shape))] + (let [child (first children) + component-child (first component-children)] + (if (or (nil? child) (nil? component-child)) + container + (let [container (if (and (not (ctk/is-main-of? component-child child true)) + (nil? (ctk/get-swap-slot child)) + (ctk/instance-head? child)) + (let [slot (guess-swap-slot component-child component-container)] + (l/dbg :hint "child" :id (:id child) :name (:name child) :slot slot) + (ctn/update-shape container (:id child) + #(update % :touched + cfh/set-touched-group + (ctk/build-swap-slot-group slot)))) + container)] + (recur (process-copy-head container child) + (rest children) + (rest component-children)))))))) + + (guess-swap-slot [shape container] + ;; To guess the slot, we must follow the chain until we find the definitive main. But + ;; we cannot navigate by shape-ref, because main shapes may also have been swapped. So + ;; chain by position, too. + (if-let [slot (ctk/get-swap-slot shape)] + slot + (if-not (ctk/in-component-copy? shape) + (:id shape) + (let [head-copy (ctn/get-component-shape (:objects container) shape)] + (if (= (:id head-copy) (:id shape)) + (:id shape) + (let [head-main (ctf/find-ref-shape file + container + libs + head-copy + :include-deleted? true + :with-context? true) + container-main (:container (meta head-main)) + shape-main (find-match-by-position shape + head-copy + container + head-main + container-main)] + (guess-swap-slot shape-main container-main))))))) + + (find-match-by-position [shape-copy head-copy container-copy head-main container-main] + ;; Find the shape in the main that has the same position under its parent than + ;; the copy under its one. To get the parent we must process recursively until + ;; the component head, because mains may also have been swapped. + (let [parent-copy (ctn/get-shape container-copy (:parent-id shape-copy)) + parent-main (if (= (:id parent-copy) (:id head-copy)) + head-main + (find-match-by-position parent-copy + head-copy + container-copy + head-main + container-main)) + index (cfh/get-position-on-parent (:objects container-copy) + (:id shape-copy)) + shape-main-id (dm/get-in parent-main [:shapes index])] + (ctn/get-shape container-main shape-main-id)))] + + file (-> file + (update :data process-fdata)))) diff --git a/backend/src/app/srepl/helpers.clj b/backend/src/app/srepl/helpers.clj index d1b893e10c..38ea61dd89 100644 --- a/backend/src/app/srepl/helpers.clj +++ b/backend/src/app/srepl/helpers.clj @@ -6,149 +6,183 @@ (ns app.srepl.helpers "A main namespace for server repl." - #_:clj-kondo/ignore + (:refer-clojure :exclude [parse-uuid]) (:require - [app.auth :refer [derive-password]] [app.common.data :as d] - [app.common.exceptions :as ex] - [app.common.files.features :as ffeat] - [app.common.logging :as l] - [app.common.pages :as cp] - [app.common.pages.migrations :as pmg] - [app.common.pprint :refer [pprint]] - [app.common.spec :as us] - [app.common.uuid :as uuid] - [app.config :as cfg] + [app.common.files.migrations :as fmg] + [app.common.files.validate :as cfv] [app.db :as db] - [app.db.sql :as sql] - [app.main :refer [system]] + [app.features.components-v2 :as feat.comp-v2] + [app.features.fdata :as feat.fdata] + [app.main :as main] [app.rpc.commands.files :as files] + [app.rpc.commands.files-snapshot :as fsnap] [app.util.blob :as blob] - [app.util.objects-map :as omap] - [app.util.pointer-map :as pmap] - [app.util.time :as dt] - [clojure.spec.alpha :as s] - [clojure.stacktrace :as strace] - [clojure.walk :as walk] - [cuerdas.core :as str] - [expound.alpha :as expound])) + [app.util.pointer-map :as pmap])) -(def ^:dynamic *conn* nil) +(def ^:dynamic *system* nil) -(defn reset-password! - "Reset a password to a specific one for a concrete user or all users - if email is `:all` keyword." - [system & {:keys [email password] :or {password "123123"} :as params}] - (us/verify! (contains? params :email) "`email` parameter is mandatory") - (db/with-atomic [conn (:app.db/pool system)] - (let [password (derive-password password)] - (if (= email :all) - (db/exec! conn ["update profile set password=?" password]) - (let [email (str/lower email)] - (db/exec! conn ["update profile set password=? where email=?" password email])))))) +(defn println! + [& params] + (locking println + (apply println params))) + +(defn parse-uuid + [v] + (if (string? v) + (d/parse-uuid v) + v)) + +(defn get-file + "Get the migrated data of one file." + ([id] (get-file (or *system* main/system) id nil)) + ([system id & {:keys [raw?] :as opts}] + (db/run! system + (fn [system] + (let [file (files/get-file system id :migrate? false)] + (if raw? + file + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer system id)] + (-> file + (update :data feat.fdata/process-pointers deref) + (update :data feat.fdata/process-objects (partial into {})) + (fmg/migrate-file))))))))) + +(defn update-file! + [system {:keys [id] :as file}] + (let [conn (db/get-connection system) + file (if (contains? (:features file) "fdata/objects-map") + (feat.fdata/enable-objects-map file) + file) + + file (if (contains? (:features file) "fdata/pointer-map") + (binding [pmap/*tracked* (pmap/create-tracked)] + (let [file (feat.fdata/enable-pointer-map file)] + (feat.fdata/persist-pointers! system id) + file)) + file) + + file (-> file + (update :features db/encode-pgarray conn "text") + (update :data blob/encode))] + + (db/update! conn :file + {:revn (:revn file) + :data (:data file) + :version (:version file) + :features (:features file) + :deleted-at (:deleted-at file) + :created-at (:created-at file) + :modified-at (:modified-at file) + :data-backend nil + :has-media-trimmed false} + {:id (:id file)}))) + +(defn update-team! + [system {:keys [id] :as team}] + (let [conn (db/get-connection system) + params (-> team + (update :features db/encode-pgarray conn "text") + (dissoc :id))] + (db/update! conn :team + params + {:id id}) + team)) + +(defn get-raw-file + "Get the migrated data of one file." + ([id] (get-raw-file (or *system* main/system) id)) + ([system id] + (db/run! system + (fn [system] + (files/get-file system id :migrate? false))))) (defn reset-file-data! "Hardcode replace of the data of one file." [system id data] - (db/with-atomic [conn (:app.db/pool system)] - (db/update! conn :file - {:data data} - {:id id}))) + (db/tx-run! system + (fn [system] + (db/update! system :file + {:data data} + {:id id})))) -(defn get-file - "Get the migrated data of one file." - [system id] - (db/with-atomic [conn (:app.db/pool system)] - (binding [pmap/*load-fn* (partial files/load-pointer conn id)] - (-> (db/get-by-id conn :file id) - (update :data blob/decode) - (update :data pmg/migrate-data) - (files/process-pointers deref))))) -(defn update-file! - "Apply a function to the data of one file. Optionally save the changes or not. - The function receives the decoded and migrated file data." - [system & {:keys [update-fn id save? migrate? inc-revn?] - :or {save? false migrate? true inc-revn? true}}] - (db/with-atomic [conn (:app.db/pool system)] - (let [file (-> (db/get-by-id conn :file id {::db/for-update? true}) - (update :features db/decode-pgarray #{}))] - (binding [*conn* conn - pmap/*tracked* (atom {}) - pmap/*load-fn* (partial files/load-pointer conn id) - ffeat/*wrap-with-pointer-map-fn* - (if (contains? (:features file) "storage/pointer-map") pmap/wrap identity) - ffeat/*wrap-with-objects-map-fn* - (if (contains? (:features file) "storage/objectd-map") omap/wrap identity)] - (let [file (-> file - (update :data blob/decode) - (cond-> migrate? (update :data pmg/migrate-data)) - (update-fn) - (cond-> inc-revn? (update :revn inc)))] - (when save? - (let [features (db/create-array conn "text" (:features file)) - data (blob/encode (:data file))] - (db/update! conn :file - {:data data - :revn (:revn file) - :features features} - {:id id}) +(def ^:private sql:snapshots-with-file + "WITH files AS ( + SELECT f.id AS file_id, + (SELECT fc.id + FROM file_change AS fc + WHERE fc.label = ? + AND fc.file_id = f.id + ORDER BY fc.created_at DESC + LIMIT 1) AS id + FROM file AS f + ) SELECT * FROM files + WHERE file_id = ANY(?) + AND id IS NOT NULL") - (when (contains? (:features file) "storage/pointer-map") - (files/persist-pointers! conn id)))) +(defn get-file-snapshots + "Get a seq parirs of file-id and snapshot-id for a set of files + and specified label" + [conn label ids] + (db/exec! conn [sql:snapshots-with-file label + (db/create-array conn "uuid" ids)])) - (dissoc file :data)))))) +(defn take-team-snapshot! + [system team-id label] + (let [conn (db/get-connection system)] + (->> (feat.comp-v2/get-and-lock-team-files conn team-id) + (map (fn [file-id] + {:file-id file-id + :label label})) + (reduce (fn [result params] + (fsnap/take-file-snapshot! conn params) + (inc result)) + 0)))) -(def ^:private sql:retrieve-files-chunk - "SELECT id, name, created_at, data FROM file - WHERE created_at < ? AND deleted_at is NULL - ORDER BY created_at desc LIMIT ?") +(defn restore-team-snapshot! + [system team-id label] + (let [conn (db/get-connection system) + ids (->> (feat.comp-v2/get-and-lock-team-files conn team-id) + (into #{})) -(defn analyze-files - "Apply a function to all files in the database, reading them in - batches. Do not change data. + snap (get-file-snapshots conn label ids) - The `on-file` parameter should be a function that receives the file - and the previous state and returns the new state." - [system & {:keys [chunk-size max-items start-at on-file on-error on-end] - :or {chunk-size 10 max-items Long/MAX_VALUE}}] - (letfn [(get-chunk [conn cursor] - (let [rows (db/exec! conn [sql:retrieve-files-chunk cursor chunk-size])] - [(some->> rows peek :created-at) (seq rows)])) + ids' (into #{} (map :file-id) snap) + team (-> (feat.comp-v2/get-team conn team-id) + (update :features disj "components/v2"))] - (get-candidates [conn] - (->> (d/iteration (partial get-chunk conn) - :vf second - :kf first - :initk (or start-at (dt/now))) - (take max-items) - (map #(update % :data blob/decode)))) + (when (not= ids ids') + (throw (RuntimeException. "no uniform snapshot available"))) - (on-error* [file cause] - (println "unexpected exception happened on processing file: " (:id file)) - (strace/print-stack-trace cause))] + (feat.comp-v2/update-team! conn team) + (reduce (fn [result params] + (fsnap/restore-file-snapshot! conn params) + (inc result)) + 0 + snap))) - (db/with-atomic [conn (:app.db/pool system)] - (loop [state {} - files (get-candidates conn)] - (if-let [file (first files)] - (let [state' (try - (on-file file state) - (catch Throwable cause - (let [on-error (or on-error on-error*)] - (on-error file cause))))] - (recur (or state' state) (rest files))) +(defn process-file! + [system file-id update-fn & {:keys [label validate? with-libraries?] :or {validate? true} :as opts}] - (if (fn? on-end) - (on-end state) - state)))))) + (when (string? label) + (fsnap/take-file-snapshot! system {:file-id file-id :label label})) -(defn update-pages - "Apply a function to all pages of one file. The function receives a page and returns an updated page." - [data f] - (update data :pages-index update-vals f)) + (let [conn (db/get-connection system) + file (get-file system file-id opts) + libs (when with-libraries? + (->> (files/get-file-libraries conn file-id) + (into [file] (map (fn [{:keys [id]}] + (get-file system id)))) + (d/index-by :id))) -(defn update-shapes - "Apply a function to all shapes of one page The function receives a shape and returns an updated shape" - [page f] - (update page :objects update-vals f)) + file' (if with-libraries? + (update-fn file libs opts) + (update-fn file opts))] + + (when (and (some? file') + (not (identical? file file'))) + (when validate? (cfv/validate-file-schema! file')) + (let [file' (update file' :revn inc)] + (update-file! system file') + true)))) diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index 4977829c47..2f538d6f66 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -8,84 +8,101 @@ "A collection of adhoc fixes scripts." #_:clj-kondo/ignore (:require + [app.auth :refer [derive-password]] + [app.binfile.common :as bfc] [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.exceptions :as ex] + [app.common.features :as cfeat] + [app.common.files.validate :as cfv] [app.common.logging :as l] [app.common.pprint :as p] [app.common.spec :as us] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.features.components-v2 :as feat.comp-v2] + [app.features.fdata :as feat.fdata] + [app.main :as main] [app.msgbus :as mbus] [app.rpc.commands.auth :as auth] + [app.rpc.commands.files :as files] + [app.rpc.commands.files-snapshot :as fsnap] + [app.rpc.commands.management :as mgmt] [app.rpc.commands.profile :as profile] - [app.srepl.fixes :as f] + [app.srepl.fixes :as fixes] [app.srepl.helpers :as h] [app.util.blob :as blob] - [app.util.objects-map :as omap] [app.util.pointer-map :as pmap] [app.util.time :as dt] [app.worker :as wrk] - [clojure.pprint :refer [pprint]] - [cuerdas.core :as str])) + [clojure.pprint :refer [print-table]] + [clojure.stacktrace :as strace] + [clojure.tools.namespace.repl :as repl] + [cuerdas.core :as str] + [promesa.exec :as px] + [promesa.exec.semaphore :as ps] + [promesa.util :as pu])) -(defn print-available-tasks - [system] - (let [tasks (:app.worker/registry system)] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TASKS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn print-tasks + [] + (let [tasks (:app.worker/registry main/system)] (p/pprint (keys tasks) :level 200))) (defn run-task! - ([system name] - (run-task! system name {})) - ([system name params] - (let [tasks (:app.worker/registry system)] - (if-let [task-fn (get tasks name)] + ([tname] + (run-task! tname {})) + ([tname params] + (let [tasks (:app.worker/registry main/system) + tname (if (keyword? tname) (name tname) name)] + (if-let [task-fn (get tasks tname)] (task-fn params) - (println (format "no task '%s' found" name)))))) + (println (format "no task '%s' found" tname)))))) (defn schedule-task! - ([system name] - (schedule-task! system name {})) - ([system name props] - (let [pool (:app.db/pool system)] + ([name] + (schedule-task! name {})) + ([name props] + (let [pool (:app.db/pool main/system)] (wrk/submit! ::wrk/conn pool ::wrk/task name ::wrk/props props)))) (defn send-test-email! - [system destination] - (us/verify! - :expr (some? system) - :hint "system should be provided") - + [destination] (us/verify! :expr (string? destination) :hint "destination should be provided") - (let [handler (:app.email/sendmail system)] + (let [handler (:app.email/sendmail main/system)] (handler {:body "test email" :subject "test email" :to [destination]}))) (defn resend-email-verification-email! - [system email] - (us/verify! - :expr (some? system) - :hint "system should be provided") - - (let [sprops (:app.setup/props system) - pool (:app.db/pool system) + [email] + (let [sprops (:app.setup/props main/system) + pool (:app.db/pool main/system) + email (profile/clean-email email) profile (profile/get-profile-by-email pool email)] (auth/send-email-verification! pool sprops profile) :email-sent)) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; PROFILES MANAGEMENT +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (defn mark-profile-as-active! "Mark the profile blocked and removes all the http sessiones associated with the profile-id." - [system email] - (db/with-atomic [conn (:app.db/pool system)] + [email] + (db/with-atomic [conn (:app.db/pool main/system)] (when-let [profile (db/get* conn :profile {:email (str/lower email)} {:columns [:id :email]})] @@ -96,8 +113,8 @@ (defn mark-profile-as-blocked! "Mark the profile blocked and removes all the http sessiones associated with the profile-id." - [system email] - (db/with-atomic [conn (:app.db/pool system)] + [email] + (db/with-atomic [conn (:app.db/pool main/system)] (when-let [profile (db/get* conn :profile {:email (str/lower email)} {:columns [:id :email]})] @@ -106,68 +123,77 @@ (db/delete! conn :http-session {:profile-id (:id profile)}) :blocked)))) +(defn reset-password! + "Reset a password to a specific one for a concrete user or all users + if email is `:all` keyword." + [& {:keys [email password] :or {password "123123"} :as params}] + (us/verify! (contains? params :email) "`email` parameter is mandatory") + (db/with-atomic [conn (:app.db/pool main/system)] + (let [password (derive-password password)] + (if (= email :all) + (db/exec! conn ["update profile set password=?" password]) + (let [email (str/lower email)] + (db/exec! conn ["update profile set password=? where email=?" password email])))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; FEATURES +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(declare process-file!) (defn enable-objects-map-feature-on-file! - [system & {:keys [save? id]}] - (letfn [(update-file [{:keys [features] :as file}] - (if (contains? features "storage/objects-map") - file - (-> file - (update :data migrate) - (update :features conj "storage/objects-map")))) - - (migrate [data] - (-> data - (update :pages-index update-vals #(update % :objects omap/wrap)) - (update :components update-vals #(update % :objects omap/wrap))))] - - (h/update-file! system - :id id - :update-fn update-file - :save? save?))) + [file-id & {:as opts}] + (process-file! file-id feat.fdata/enable-objects-map opts)) (defn enable-pointer-map-feature-on-file! - [system & {:keys [save? id]}] - (letfn [(update-file [{:keys [features] :as file}] - (if (contains? features "storage/pointer-map") - file - (-> file - (update :data migrate) - (update :features conj "storage/pointer-map")))) - - (migrate [data] - (-> data - (update :pages-index update-vals pmap/wrap) - (update :components pmap/wrap)))] - - (h/update-file! system - :id id - :update-fn update-file - :save? save?))) + [file-id & {:as opts}] + (process-file! file-id feat.fdata/enable-pointer-map opts)) (defn enable-storage-features-on-file! - [system & {:as params}] - (enable-objects-map-feature-on-file! system params) - (enable-pointer-map-feature-on-file! system params)) + [file-id & {:as opts}] + (enable-objects-map-feature-on-file! file-id opts) + (enable-pointer-map-feature-on-file! file-id opts)) -(defn instrument-var - [var] - (alter-var-root var (fn [f] - (let [mf (meta f)] - (if (::original mf) - f - (with-meta - (fn [& params] - (tap> params) - (let [result (apply f params)] - (tap> result) - result)) - {::original f})))))) +(defn enable-team-feature! + [team-id feature] + (dm/verify! + "feature should be supported" + (contains? cfeat/supported-features feature)) + + (let [team-id (h/parse-uuid team-id)] + (db/tx-run! main/system + (fn [{:keys [::db/conn]}] + (let [team (-> (db/get conn :team {:id team-id}) + (update :features db/decode-pgarray #{})) + features (conj (:features team) feature)] + (when (not= features (:features team)) + (db/update! conn :team + {:features (db/create-array conn "text" features)} + {:id team-id}) + :enabled)))))) + +(defn disable-team-feature! + [team-id feature] + (dm/verify! + "feature should be supported" + (contains? cfeat/supported-features feature)) + + (let [team-id (h/parse-uuid team-id)] + (db/tx-run! main/system + (fn [{:keys [::db/conn]}] + (let [team (-> (db/get conn :team {:id team-id}) + (update :features db/decode-pgarray #{})) + features (disj (:features team) feature)] + (when (not= features (:features team)) + (db/update! conn :team + {:features (db/create-array conn "text" features)} + {:id team-id}) + :disabled)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; NOTIFICATIONS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn uninstrument-var - [var] - (alter-var-root var (fn [f] - (or (::original (meta f)) f)))) (defn notify! [{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level] @@ -204,18 +230,13 @@ {:columns [:profile-id]}) (map :profile-id))) - (parse-uuid [v] - (if (uuid? v) - v - (d/parse-uuid v))) - (resolve-dest [dest] (cond (uuid? dest) [dest] (string? dest) - (some-> dest parse-uuid resolve-dest) + (some-> dest h/parse-uuid resolve-dest) (nil? dest) (resolve-dest uuid/zero) @@ -253,22 +274,245 @@ (coll? param) (sequence (comp (mapcat resolve-team) - (keep parse-uuid)) + (keep h/parse-uuid)) param) (uuid? param) (resolve-team param) (string? param) - (some-> param parse-uuid resolve-team)) + (some-> param h/parse-uuid resolve-team)) (= op :profile-id) (if (coll? param) - (sequence (keep parse-uuid) param) - (resolve-dest param)))))) - ] + (sequence (keep h/parse-uuid) param) + (resolve-dest param))))))] (->> (resolve-dest dest) (filter some?) (into #{}) (run! send)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SNAPSHOTS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn take-file-snapshot! + "An internal helper that persist the file snapshot using non-gc + collectable file-changes entry." + [& {:keys [file-id label]}] + (let [file-id (h/parse-uuid file-id)] + (db/tx-run! main/system fsnap/take-file-snapshot! {:file-id file-id :label label}))) + +(defn restore-file-snapshot! + [file-id label] + (let [file-id (h/parse-uuid file-id)] + (db/tx-run! main/system + (fn [{:keys [::db/conn] :as system}] + (when-let [snapshot (->> (h/get-file-snapshots conn label #{file-id}) + (map :id) + (first))] + (fsnap/restore-file-snapshot! system + {:id (:id snapshot) + :file-id file-id})))))) + +(defn list-file-snapshots! + [file-id & {:keys [limit]}] + (let [file-id (h/parse-uuid file-id)] + (db/tx-run! main/system + (fn [system] + (let [params {:file-id file-id :limit limit}] + (->> (fsnap/get-file-snapshots system (d/without-nils params)) + (print-table [:label :id :revn :created-at]))))))) + +(defn take-team-snapshot! + [team-id & {:keys [label rollback?] :or {rollback? true}}] + (let [team-id (h/parse-uuid team-id) + label (or label (fsnap/generate-snapshot-label))] + (-> (assoc main/system ::db/rollback rollback?) + (db/tx-run! h/take-team-snapshot! team-id label)))) + +(defn restore-team-snapshot! + "Restore a snapshot on all files of the team. The snapshot should + exists for all files; if is not the case, an exception is raised." + [team-id label & {:keys [rollback?] :or {rollback? true}}] + (let [team-id (h/parse-uuid team-id)] + (-> (assoc main/system ::db/rollback rollback?) + (db/tx-run! h/restore-team-snapshot! team-id label)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; FILE VALIDATION & REPAIR +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn validate-file + "Validate structure, referencial integrity and semantic coherence of + all contents of a file. Returns a list of errors." + [file-id] + (let [file-id (h/parse-uuid file-id)] + (db/tx-run! (assoc main/system ::db/rollback true) + (fn [{:keys [::db/conn] :as system}] + (let [file (h/get-file system file-id) + libs (->> (files/get-file-libraries conn file-id) + (into [file] (map (fn [{:keys [id]}] + (h/get-file system id)))) + (d/index-by :id))] + (cfv/validate-file file libs)))))) + +(defn repair-file! + "Repair the list of errors detected by validation." + [file-id & {:keys [rollback?] :or {rollback? true} :as opts}] + (let [system (assoc main/system ::db/rollback rollback?) + file-id (h/parse-uuid file-id) + opts (assoc opts :with-libraries? true)] + (db/tx-run! system h/process-file! file-id fixes/repair-file opts))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; PROCESSING +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def sql:get-files + "SELECT id FROM file + WHERE deleted_at is NULL + ORDER BY created_at DESC") + +(defn process-file! + "Apply a function to the file. Optionally save the changes or not. + The function receives the decoded and migrated file data." + [file-id update-fn & {:keys [rollback?] :or {rollback? true} :as opts}] + (db/tx-run! (assoc main/system ::db/rollback rollback?) + (fn [system] + (binding [h/*system* system] + (h/process-file! system file-id update-fn opts))))) + +(defn process-team-files! + "Apply a function to each file of the specified team." + [team-id update-fn & {:keys [rollback? label] :or {rollback? true} :as opts}] + (let [team-id (h/parse-uuid team-id) + opts (dissoc opts :label)] + (db/tx-run! (assoc main/system ::db/rollback rollback?) + (fn [{:keys [::db/conn] :as system}] + (when (string? label) + (h/take-team-snapshot! system team-id label)) + + (binding [h/*system* system] + (->> (feat.comp-v2/get-and-lock-team-files conn team-id) + (reduce (fn [result file-id] + (if (h/process-file! system file-id update-fn opts) + (inc result) + result)) + 0))))))) + +(defn process-files! + "Apply a function to all files in the database" + [update-fn & {:keys [max-items + max-jobs + rollback? + query] + :or {max-jobs 1 + max-items Long/MAX_VALUE + rollback? true + query sql:get-files} + :as opts}] + + (l/dbg :hint "process:start" + :rollback rollback? + :max-jobs max-jobs + :max-items max-items) + + (let [tpoint (dt/tpoint) + factory (px/thread-factory :virtual false :prefix "penpot/file-process/") + executor (px/cached-executor :factory factory) + sjobs (ps/create :permits max-jobs) + + process-file + (fn [file-id idx tpoint] + (try + (l/trc :hint "process:file:start" :file-id (str file-id) :index idx) + (let [system (assoc main/system ::db/rollback rollback?)] + (db/tx-run! system (fn [system] + (binding [h/*system* system] + (h/process-file! system file-id update-fn opts))))) + + (catch Throwable cause + (l/wrn :hint "unexpected error on processing file (skiping)" + :file-id (str file-id) + :index idx + :cause cause)) + (finally + (ps/release! sjobs) + (let [elapsed (dt/format-duration (tpoint))] + (l/trc :hint "process:file:end" + :file-id (str file-id) + :index idx + :elapsed elapsed))))) + + process-files + (fn [{:keys [::db/conn] :as system}] + (db/exec! conn ["SET statement_timeout = 0"]) + (db/exec! conn ["SET idle_in_transaction_session_timeout = 0"]) + + (try + (reduce (fn [idx file-id] + (ps/acquire! sjobs) + (px/run! executor (partial process-file file-id idx (dt/tpoint))) + (inc idx)) + 0 + (->> (db/cursor conn [query] {:chunk-size 1}) + (take max-items) + (map :id))) + (finally + ;; Close and await tasks + (pu/close! executor))))] + + (try + (db/tx-run! main/system process-files) + + (catch Throwable cause + (l/dbg :hint "process:error" :cause cause)) + + (finally + (let [elapsed (dt/format-duration (tpoint))] + (l/dbg :hint "process:end" + :rollback rollback? + :elapsed elapsed)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; MISC +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn instrument-var + [var] + (alter-var-root var (fn [f] + (let [mf (meta f)] + (if (::original mf) + f + (with-meta + (fn [& params] + (tap> params) + (let [result (apply f params)] + (tap> result) + result)) + {::original f})))))) + +(defn uninstrument-var + [var] + (alter-var-root var (fn [f] + (or (::original (meta f)) f)))) + + +(defn duplicate-team + [team-id & {:keys [name]}] + (let [team-id (h/parse-uuid team-id)] + (db/tx-run! main/system + (fn [{:keys [::db/conn] :as cfg}] + (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"]) + (let [team (-> (assoc cfg ::bfc/timestamp (dt/now)) + (mgmt/duplicate-team :team-id team-id :name name)) + rels (db/query conn :team-profile-rel {:team-id team-id})] + + (doseq [rel rels] + (let [params (-> rel + (assoc :id (uuid/next)) + (assoc :team-id (:id team)))] + (db/insert! conn :team-profile-rel params + {::db/return-keys false})))))))) diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj index be0159e0fa..f6924aedb4 100644 --- a/backend/src/app/storage.clj +++ b/backend/src/app/storage.clj @@ -9,8 +9,6 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.exceptions :as ex] - [app.common.logging :as l] [app.common.spec :as us] [app.common.uuid :as uuid] [app.db :as db] @@ -18,11 +16,12 @@ [app.storage.impl :as impl] [app.storage.s3 :as ss3] [app.util.time :as dt] - [app.worker :as wrk] [clojure.spec.alpha :as s] [datoteka.fs :as fs] [integrant.core :as ig] - [promesa.core :as p])) + [promesa.core :as p]) + (:import + java.io.InputStream)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Storage Module State @@ -40,7 +39,7 @@ :fs ::sfs/backend)))) (defmethod ig/pre-init-spec ::storage [_] - (s/keys :req [::db/pool ::wrk/executor ::backends])) + (s/keys :req [::db/pool ::backends])) (defmethod ig/init-key ::storage [_ {:keys [::backends ::db/pool] :as cfg}] @@ -79,10 +78,15 @@ (defn- create-database-object [{:keys [::backend ::db/pool-or-conn]} {:keys [::content ::expired-at ::touched-at] :as params}] - (let [id (uuid/random) + (let [id (or (:id params) (uuid/random)) mdata (cond-> (get-metadata params) (satisfies? impl/IContentHash content) - (assoc :hash (impl/get-hash content))) + (assoc :hash (impl/get-hash content)) + + :always + (dissoc :id)) + + ;; FIXME: touch object on deduplicated put operation ?? ;; NOTE: for now we don't reuse the deleted objects, but in ;; futute we can consider reusing deleted objects if we @@ -173,12 +177,12 @@ (let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id) rs (db/update! pool-or-conn :storage-object {:touched-at (dt/now)} - {:id id} - {::db/return-keys? false})] + {:id id})] (pos? (db/get-update-count rs)))) (defn get-object-data "Return an input stream instance of the object content." + ^InputStream [storage object] (us/assert! ::storage storage) (when (or (nil? (:expired-at object)) @@ -223,224 +227,8 @@ (let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id) res (db/update! pool-or-conn :storage-object {:deleted-at (dt/now)} - {:id id} - {::db/return-keys? false})] + {:id id})] (pos? (db/get-update-count res)))) (dm/export impl/resolve-backend) (dm/export impl/calculate-hash) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Garbage Collection: Permanently delete objects -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; A task responsible to permanently delete already marked as deleted -;; storage files. The storage objects are practically never marked to -;; be deleted directly by the api call. The touched-gc is responsible -;; of collecting the usage of the object and mark it as deleted. Only -;; the TMP files are are created with expiration date in future. - -(declare sql:retrieve-deleted-objects-chunk) - -(defmethod ig/pre-init-spec ::gc-deleted-task [_] - (s/keys :req [::storage ::db/pool])) - -(defmethod ig/prep-key ::gc-deleted-task - [_ cfg] - (assoc cfg ::min-age (dt/duration {:hours 2}))) - -(defmethod ig/init-key ::gc-deleted-task - [_ {:keys [::db/pool ::storage ::min-age]}] - (letfn [(get-to-delete-chunk [cursor] - (let [sql (str "select s.* " - " from storage_object as s " - " where s.deleted_at is not null " - " and s.deleted_at < ? " - " order by s.deleted_at desc " - " limit 25") - rows (db/exec! pool [sql cursor])] - [(some-> rows peek :deleted-at) - (some->> (seq rows) (d/group-by #(-> % :backend keyword) :id #{}) seq)])) - - (get-to-delete-chunks [min-age] - (d/iteration get-to-delete-chunk - :initk (dt/minus (dt/now) min-age) - :vf second - :kf first)) - - (delete-in-bulk! [backend-id ids] - (try - (db/with-atomic [conn pool] - (let [sql "delete from storage_object where id = ANY(?)" - ids' (db/create-array conn "uuid" ids) - - total (-> (db/exec-one! conn [sql ids']) - (db/get-update-count))] - - (-> (impl/resolve-backend storage backend-id) - (impl/del-objects-in-bulk ids)) - - (doseq [id ids] - (l/dbg :hint "gc-deleted: permanently delete storage object" :backend backend-id :id id)) - - total)) - - (catch Throwable cause - (l/err :hint "gc-deleted: unexpected error on bulk deletion" - :ids (vec ids) - :cause cause) - 0)))] - - (fn [params] - (let [min-age (or (some-> params :min-age dt/duration) min-age)] - (loop [total 0 - chunks (get-to-delete-chunks min-age)] - (if-let [[backend-id ids] (first chunks)] - (let [deleted (delete-in-bulk! backend-id ids)] - (recur (+ total deleted) - (rest chunks))) - (do - (l/inf :hint "gc-deleted: task finished" - :min-age (dt/format-duration min-age) - :total total) - {:deleted total}))))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Garbage Collection: Analyze touched objects -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; This task is part of the garbage collection process of storage -;; objects and is responsible on analyzing the touched objects and -;; mark them for deletion if corresponds. -;; -;; For example: when file_media_object is deleted, the depending -;; storage_object are marked as touched. This means that some files -;; that depend on a concrete storage_object are no longer exists and -;; maybe this storage_object is no longer necessary and can be -;; eligible for elimination. This task periodically analyzes touched -;; objects and mark them as freeze (means that has other references -;; and the object is still valid) or deleted (no more references to -;; this object so is ready to be deleted). - -(declare sql:retrieve-touched-objects-chunk) -(declare sql:retrieve-file-media-object-nrefs) -(declare sql:retrieve-team-font-variant-nrefs) -(declare sql:retrieve-profile-nrefs) - -(defmethod ig/pre-init-spec ::gc-touched-task [_] - (s/keys :req [::db/pool])) - -(defmethod ig/init-key ::gc-touched-task - [_ {:keys [::db/pool]}] - (letfn [(get-team-font-variant-nrefs [conn id] - (-> (db/exec-one! conn [sql:retrieve-team-font-variant-nrefs id id id id]) :nrefs)) - - (get-file-media-object-nrefs [conn id] - (-> (db/exec-one! conn [sql:retrieve-file-media-object-nrefs id id]) :nrefs)) - - (get-profile-nrefs [conn id] - (-> (db/exec-one! conn [sql:retrieve-profile-nrefs id id]) :nrefs)) - - (mark-freeze-in-bulk [conn ids] - (db/exec-one! conn ["update storage_object set touched_at=null where id = ANY(?)" - (db/create-array conn "uuid" ids)])) - - (mark-delete-in-bulk [conn ids] - (db/exec-one! conn ["update storage_object set deleted_at=now(), touched_at=null where id = ANY(?)" - (db/create-array conn "uuid" ids)])) - - ;; NOTE: A getter that retrieves the key witch will be used - ;; for group ids; previously we have no value, then we - ;; introduced the `:reference` prop, and then it is renamed - ;; to `:bucket` and now is string instead. This is - ;; implemented in this way for backward comaptibilty. - - ;; NOTE: we use the "file-media-object" as default value for - ;; backward compatibility because when we deploy it we can - ;; have old backend instances running in the same time as - ;; the new one and we can still have storage-objects created - ;; without bucket value. And we know that if it does not - ;; have value, it means :file-media-object. - - (get-bucket [{:keys [metadata]}] - (or (some-> metadata :bucket) - (some-> metadata :reference d/name) - "file-media-object")) - - (retrieve-touched-chunk [conn cursor] - (let [rows (->> (db/exec! conn [sql:retrieve-touched-objects-chunk cursor]) - (mapv #(d/update-when % :metadata db/decode-transit-pgobject)))] - (when (seq rows) - [(-> rows peek :created-at) - (d/group-by get-bucket :id #{} rows)]))) - - (retrieve-touched [conn] - (d/iteration (partial retrieve-touched-chunk conn) - :initk (dt/now) - :vf second - :kf first)) - - (process-objects! [conn get-fn ids bucket] - (loop [to-freeze #{} - to-delete #{} - ids (seq ids)] - (if-let [id (first ids)] - (let [nrefs (get-fn conn id)] - (if (pos? nrefs) - (do - (l/debug :hint "gc-touched: processing storage object" - :id id :status "freeze" - :bucket bucket :refs nrefs) - (recur (conj to-freeze id) to-delete (rest ids))) - (do - (l/debug :hint "gc-touched: processing storage object" - :id id :status "delete" - :bucket bucket :refs nrefs) - (recur to-freeze (conj to-delete id) (rest ids))))) - (do - (some->> (seq to-freeze) (mark-freeze-in-bulk conn)) - (some->> (seq to-delete) (mark-delete-in-bulk conn)) - [(count to-freeze) (count to-delete)])))) - ] - - (fn [_] - (db/with-atomic [conn pool] - (loop [to-freeze 0 - to-delete 0 - groups (retrieve-touched conn)] - (if-let [[bucket ids] (first groups)] - (let [[f d] (case bucket - "file-media-object" (process-objects! conn get-file-media-object-nrefs ids bucket) - "team-font-variant" (process-objects! conn get-team-font-variant-nrefs ids bucket) - "profile" (process-objects! conn get-profile-nrefs ids bucket) - (ex/raise :type :internal - :code :unexpected-unknown-reference - :hint (dm/fmt "unknown reference %" bucket)))] - (recur (+ to-freeze (long f)) - (+ to-delete (long d)) - (rest groups))) - (do - (l/info :hint "gc-touched: task finished" :to-freeze to-freeze :to-delete to-delete) - {:freeze to-freeze :delete to-delete}))))))) - -(def sql:retrieve-touched-objects-chunk - "SELECT so.* - FROM storage_object AS so - WHERE so.touched_at IS NOT NULL - AND so.created_at < ? - ORDER by so.created_at DESC - LIMIT 500;") - -(def sql:retrieve-file-media-object-nrefs - "select ((select count(*) from file_media_object where media_id = ?) + - (select count(*) from file_media_object where thumbnail_id = ?)) as nrefs") - -(def sql:retrieve-team-font-variant-nrefs - "select ((select count(*) from team_font_variant where woff1_file_id = ?) + - (select count(*) from team_font_variant where woff2_file_id = ?) + - (select count(*) from team_font_variant where otf_file_id = ?) + - (select count(*) from team_font_variant where ttf_file_id = ?)) as nrefs") - -(def sql:retrieve-profile-nrefs - "select ((select count(*) from profile where photo_id = ?) + - (select count(*) from team where photo_id = ?)) as nrefs") diff --git a/backend/src/app/storage/fs.clj b/backend/src/app/storage/fs.clj index 358fdc1e56..d2e6c88545 100644 --- a/backend/src/app/storage/fs.clj +++ b/backend/src/app/storage/fs.clj @@ -18,8 +18,8 @@ [datoteka.io :as io] [integrant.core :as ig]) (:import - java.nio.file.Path - java.nio.file.Files)) + java.nio.file.Files + java.nio.file.Path)) ;; --- BACKEND INIT diff --git a/backend/src/app/storage/gc_deleted.clj b/backend/src/app/storage/gc_deleted.clj new file mode 100644 index 0000000000..8d1d0e5ad1 --- /dev/null +++ b/backend/src/app/storage/gc_deleted.clj @@ -0,0 +1,125 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.storage.gc-deleted + "A task responsible to permanently delete already marked as deleted + storage files. The storage objects are practically never marked to + be deleted directly by the api call. + + The touched-gc is responsible of collecting the usage of the object + and mark it as deleted. Only the TMP files are are created with + expiration date in future." + (:require + [app.common.data :as d] + [app.common.logging :as l] + [app.db :as db] + [app.storage :as-alias sto] + [app.storage.impl :as impl] + [app.util.time :as dt] + [clojure.spec.alpha :as s] + [integrant.core :as ig])) + +(def ^:private sql:lock-sobjects + "SELECT id FROM storage_object + WHERE id = ANY(?::uuid[]) + FOR UPDATE + SKIP LOCKED") + +(defn- lock-ids + "Perform a select before delete for proper object locking and + prevent concurrent operations and we proceed only with successfully + locked objects." + [conn ids] + (let [ids (db/create-array conn "uuid" ids)] + (->> (db/exec! conn [sql:lock-sobjects ids]) + (into #{} (map :id)) + (not-empty)))) + + +(def ^:private sql:delete-sobjects + "DELETE FROM storage_object + WHERE id = ANY(?::uuid[])") + +(defn- delete-sobjects! + [conn ids] + (let [ids (db/create-array conn "uuid" ids)] + (-> (db/exec-one! conn [sql:delete-sobjects ids]) + (db/get-update-count)))) + +(defn- delete-in-bulk! + [cfg backend-id ids] + ;; We run the deletion on a separate transaction. This is + ;; because if some exception is raised inside procesing + ;; one chunk, it does not affects the rest of the chunks. + (try + (db/tx-run! cfg + (fn [{:keys [::db/conn ::sto/storage]}] + (when-let [ids (lock-ids conn ids)] + (let [total (delete-sobjects! conn ids)] + (-> (impl/resolve-backend storage backend-id) + (impl/del-objects-in-bulk ids)) + + (doseq [id ids] + (l/dbg :hint "permanently delete storage object" + :id (str id) + :backend (name backend-id))) + total)))) + (catch Throwable cause + (l/err :hint "unexpected error on bulk deletion" + :ids ids + :cause cause)))) + + +(defn- group-by-backend + [items] + (d/group-by (comp keyword :backend) :id #{} items)) + +(def ^:private sql:get-deleted-sobjects + "SELECT s.* FROM storage_object AS s + WHERE s.deleted_at IS NOT NULL + AND s.deleted_at < now() - ?::interval + ORDER BY s.deleted_at ASC") + +(defn- get-buckets + [conn min-age] + (let [age (db/interval min-age)] + (sequence + (comp (partition-all 25) + (mapcat group-by-backend)) + (db/cursor conn [sql:get-deleted-sobjects age])))) + + +(defn- clean-deleted! + [{:keys [::db/conn ::min-age] :as cfg}] + (reduce (fn [total [backend-id ids]] + (let [deleted (delete-in-bulk! cfg backend-id ids)] + (+ total (or deleted 0)))) + 0 + (get-buckets conn min-age))) + + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req [::sto/storage ::db/pool])) + +(defmethod ig/prep-key ::handler + [_ cfg] + (assoc cfg ::min-age (dt/duration {:hours 2}))) + +(defmethod ig/init-key ::handler + [_ {:keys [::min-age] :as cfg}] + (fn [params] + (let [min-age (dt/duration (or (:min-age params) min-age))] + (db/tx-run! cfg (fn [cfg] + (let [cfg (assoc cfg ::min-age min-age) + total (clean-deleted! cfg)] + + (l/inf :hint "task finished" + :min-age (dt/format-duration min-age) + :total total) + + {:deleted total})))))) + + diff --git a/backend/src/app/storage/gc_touched.clj b/backend/src/app/storage/gc_touched.clj new file mode 100644 index 0000000000..bd499bb655 --- /dev/null +++ b/backend/src/app/storage/gc_touched.clj @@ -0,0 +1,208 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.storage.gc-touched + "This task is part of the garbage collection process of storage + objects and is responsible on analyzing the touched objects and mark + them for deletion if corresponds. + + For example: when file_media_object is deleted, the depending + storage_object are marked as touched. This means that some files + that depend on a concrete storage_object are no longer exists and + maybe this storage_object is no longer necessary and can be eligible + for elimination. This task periodically analyzes touched objects and + mark them as freeze (means that has other references and the object + is still valid) or deleted (no more references to this object so is + ready to be deleted)." + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.db :as db] + [app.storage :as-alias sto] + [app.storage.impl :as impl] + [clojure.spec.alpha :as s] + [integrant.core :as ig])) + +(def ^:private sql:get-team-font-variant-nrefs + "SELECT ((SELECT count(*) FROM team_font_variant WHERE woff1_file_id = ?) + + (SELECT count(*) FROM team_font_variant WHERE woff2_file_id = ?) + + (SELECT count(*) FROM team_font_variant WHERE otf_file_id = ?) + + (SELECT count(*) FROM team_font_variant WHERE ttf_file_id = ?)) AS nrefs") + +(defn- get-team-font-variant-nrefs + [conn id] + (-> (db/exec-one! conn [sql:get-team-font-variant-nrefs id id id id]) + (get :nrefs))) + + +(def ^:private + sql:get-file-media-object-nrefs + "SELECT ((SELECT count(*) FROM file_media_object WHERE media_id = ?) + + (SELECT count(*) FROM file_media_object WHERE thumbnail_id = ?)) AS nrefs") + +(defn- get-file-media-object-nrefs + [conn id] + (-> (db/exec-one! conn [sql:get-file-media-object-nrefs id id]) + (get :nrefs))) + + +(def ^:private sql:get-profile-nrefs + "SELECT ((SELECT count(*) FROM profile WHERE photo_id = ?) + + (SELECT count(*) FROM team WHERE photo_id = ?)) AS nrefs") + +(defn- get-profile-nrefs + [conn id] + (-> (db/exec-one! conn [sql:get-profile-nrefs id id]) + (get :nrefs))) + + +(def ^:private + sql:get-file-object-thumbnail-nrefs + "SELECT (SELECT count(*) FROM file_tagged_object_thumbnail WHERE media_id = ?) AS nrefs") + +(defn- get-file-object-thumbnails + [conn id] + (-> (db/exec-one! conn [sql:get-file-object-thumbnail-nrefs id]) + (get :nrefs))) + + +(def ^:private + sql:get-file-thumbnail-nrefs + "SELECT (SELECT count(*) FROM file_thumbnail WHERE media_id = ?) AS nrefs") + +(defn- get-file-thumbnails + [conn id] + (-> (db/exec-one! conn [sql:get-file-thumbnail-nrefs id]) + (get :nrefs))) + + +(def ^:private sql:mark-freeze-in-bulk + "UPDATE storage_object + SET touched_at = NULL + WHERE id = ANY(?::uuid[])") + +(defn- mark-freeze-in-bulk! + [conn ids] + (let [ids (db/create-array conn "uuid" ids)] + (db/exec-one! conn [sql:mark-freeze-in-bulk ids]))) + + +(def ^:private sql:mark-delete-in-bulk + "UPDATE storage_object + SET deleted_at = now(), + touched_at = NULL + WHERE id = ANY(?::uuid[])") + +(defn- mark-delete-in-bulk! + [conn ids] + (let [ids (db/create-array conn "uuid" ids)] + (db/exec-one! conn [sql:mark-delete-in-bulk ids]))) + +;; NOTE: A getter that retrieves the key which will be used for group +;; ids; previously we have no value, then we introduced the +;; `:reference` prop, and then it is renamed to `:bucket` and now is +;; string instead. This is implemented in this way for backward +;; comaptibilty. + +;; NOTE: we use the "file-media-object" as default value for +;; backward compatibility because when we deploy it we can +;; have old backend instances running in the same time as +;; the new one and we can still have storage-objects created +;; without bucket value. And we know that if it does not +;; have value, it means :file-media-object. + +(defn- lookup-bucket + [{:keys [metadata]}] + (or (some-> metadata :bucket) + (some-> metadata :reference d/name) + "file-media-object")) + +(defn- process-objects! + [conn get-fn ids bucket] + (loop [to-freeze #{} + to-delete #{} + ids (seq ids)] + (if-let [id (first ids)] + (let [nrefs (get-fn conn id)] + (if (pos? nrefs) + (do + (l/debug :hint "processing object" + :id (str id) + :status "freeze" + :bucket bucket :refs nrefs) + (recur (conj to-freeze id) to-delete (rest ids))) + (do + (l/debug :hint "processing object" + :id (str id) + :status "delete" + :bucket bucket :refs nrefs) + (recur to-freeze (conj to-delete id) (rest ids))))) + (do + (some->> (seq to-freeze) (mark-freeze-in-bulk! conn)) + (some->> (seq to-delete) (mark-delete-in-bulk! conn)) + [(count to-freeze) (count to-delete)])))) + +(defn- process-bucket! + [conn bucket ids] + (case bucket + "file-media-object" (process-objects! conn get-file-media-object-nrefs ids bucket) + "team-font-variant" (process-objects! conn get-team-font-variant-nrefs ids bucket) + "file-object-thumbnail" (process-objects! conn get-file-object-thumbnails ids bucket) + "file-thumbnail" (process-objects! conn get-file-thumbnails ids bucket) + "profile" (process-objects! conn get-profile-nrefs ids bucket) + (ex/raise :type :internal + :code :unexpected-unknown-reference + :hint (dm/fmt "unknown reference %" bucket)))) + + +(def ^:private + sql:get-touched-storage-objects + "SELECT so.* + FROM storage_object AS so + WHERE so.touched_at IS NOT NULL + ORDER BY touched_at ASC + FOR UPDATE + SKIP LOCKED") + +(defn- group-by-bucket + [row] + (d/group-by lookup-bucket :id #{} row)) + +(defn- get-buckets + [conn] + (sequence + (comp (map impl/decode-row) + (partition-all 25) + (mapcat group-by-bucket)) + (db/cursor conn sql:get-touched-storage-objects))) + +(defn- process-touched! + [{:keys [::db/conn]}] + (loop [buckets (get-buckets conn) + freezed 0 + deleted 0] + (if-let [[bucket ids] (first buckets)] + (let [[nfo ndo] (process-bucket! conn bucket ids)] + (recur (rest buckets) + (+ freezed nfo) + (+ deleted ndo))) + (do + (l/inf :hint "task finished" + :to-freeze freezed + :to-delete deleted) + + {:freeze freezed :delete deleted})))) + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req [::db/pool])) + +(defmethod ig/init-key ::handler + [_ cfg] + (fn [_] + (db/tx-run! cfg process-touched!))) + diff --git a/backend/src/app/storage/impl.clj b/backend/src/app/storage/impl.clj index 4a564b58f3..156d86b872 100644 --- a/backend/src/app/storage/impl.clj +++ b/backend/src/app/storage/impl.clj @@ -9,9 +9,8 @@ (:require [app.common.data.macros :as dm] [app.common.exceptions :as ex] - [app.db :as-alias db] + [app.db :as db] [app.storage :as-alias sto] - [app.worker :as-alias wrk] [buddy.core.codecs :as bc] [buddy.core.hash :as bh] [clojure.java.io :as jio] @@ -23,6 +22,13 @@ java.nio.file.Path java.util.UUID)) +(defn decode-row + "Decode the storage-object row fields" + [{:keys [metadata] :as row}] + (cond-> row + (some? metadata) + (assoc :metadata (db/decode-transit-pgobject metadata)))) + ;; --- API Definition (defmulti put-object (fn [cfg _ _] (::sto/type cfg))) @@ -201,7 +207,7 @@ (str "blake2b:" result))) (defn resolve-backend - [{:keys [::db/pool ::wrk/executor] :as storage} backend-id] + [{:keys [::db/pool] :as storage} backend-id] (let [backend (get-in storage [::sto/backends backend-id])] (when-not backend (ex/raise :type :internal @@ -209,7 +215,6 @@ :hint (dm/fmt "backend '%' not configured" backend-id))) (-> backend (assoc ::sto/id backend-id) - (assoc ::wrk/executor executor) (assoc ::db/pool pool)))) (defrecord StorageObject [id size created-at expired-at touched-at backend]) diff --git a/backend/src/app/storage/s3.clj b/backend/src/app/storage/s3.clj index ffd873c42c..1bbb38b16a 100644 --- a/backend/src/app/storage/s3.clj +++ b/backend/src/app/storage/s3.clj @@ -17,7 +17,6 @@ [app.storage.impl :as impl] [app.storage.tmp :as tmp] [app.util.time :as dt] - [app.worker :as wrk] [clojure.java.io :as io] [clojure.spec.alpha :as s] [datoteka.fs :as fs] @@ -52,6 +51,7 @@ software.amazon.awssdk.services.s3.model.DeleteObjectsRequest software.amazon.awssdk.services.s3.model.DeleteObjectsResponse software.amazon.awssdk.services.s3.model.GetObjectRequest + software.amazon.awssdk.services.s3.model.NoSuchKeyException software.amazon.awssdk.services.s3.model.ObjectIdentifier software.amazon.awssdk.services.s3.model.PutObjectRequest software.amazon.awssdk.services.s3.model.S3Error @@ -77,9 +77,10 @@ (s/def ::bucket ::us/string) (s/def ::prefix ::us/string) (s/def ::endpoint ::us/string) +(s/def ::io-threads ::us/integer) (defmethod ig/pre-init-spec ::backend [_] - (s/keys :opt [::region ::bucket ::prefix ::endpoint ::wrk/executor])) + (s/keys :opt [::region ::bucket ::prefix ::endpoint ::io-threads])) (defmethod ig/prep-key ::backend [_ {:keys [::prefix ::region] :as cfg}] @@ -114,8 +115,7 @@ ::client ::presigner] :opt [::prefix - ::sto/id - ::wrk/executor])) + ::sto/id])) ;; --- API IMPL @@ -127,17 +127,19 @@ (defmethod impl/get-object-data :s3 [backend object] (us/assert! ::backend backend) - (letfn [(no-such-key? [cause] - (instance? software.amazon.awssdk.services.s3.model.NoSuchKeyException cause)) - (handle-not-found [cause] - (ex/raise :type :not-found - :code :object-not-found - :hint "s3 object not found" - :cause cause))] - (-> (get-object-data backend object) - (p/catch no-such-key? handle-not-found) - (p/await!)))) + (let [result (p/await (get-object-data backend object))] + (if (ex/exception? result) + (cond + (ex/instance? NoSuchKeyException result) + (ex/raise :type :not-found + :code :object-not-found + :hint "s3 object not found" + :cause result) + :else + (throw result)) + + result))) (defmethod impl/get-object-bytes :s3 [backend object] @@ -161,7 +163,6 @@ ;; --- HELPERS -(def default-eventloop-threads 4) (def default-timeout (dt/duration {:seconds 30})) @@ -171,35 +172,35 @@ (Region/of (name region))) (defn- build-s3-client - [{:keys [::region ::endpoint ::wrk/executor]}] - (let [aconfig (-> (ClientAsyncConfiguration/builder) - (.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor) - (.build)) + [{:keys [::region ::endpoint ::io-threads]}] + (let [executor (px/resolve-executor :virtual) + aconfig (-> (ClientAsyncConfiguration/builder) + (.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor) + (.build)) - sconfig (-> (S3Configuration/builder) - (cond-> (some? endpoint) (.pathStyleAccessEnabled true)) - (.build)) + sconfig (-> (S3Configuration/builder) + (cond-> (some? endpoint) (.pathStyleAccessEnabled true)) + (.build)) - hclient (-> (NettyNioAsyncHttpClient/builder) - (.eventLoopGroupBuilder (-> (SdkEventLoopGroup/builder) - (.numberOfThreads (int default-eventloop-threads)))) - (.connectionAcquisitionTimeout default-timeout) - (.connectionTimeout default-timeout) - (.readTimeout default-timeout) - (.writeTimeout default-timeout) - (.build)) + thr-num (or io-threads (min 16 (px/get-available-processors))) + hclient (-> (NettyNioAsyncHttpClient/builder) + (.eventLoopGroupBuilder (-> (SdkEventLoopGroup/builder) + (.numberOfThreads (int thr-num)))) + (.connectionAcquisitionTimeout default-timeout) + (.connectionTimeout default-timeout) + (.readTimeout default-timeout) + (.writeTimeout default-timeout) + (.build)) - client (let [builder (S3AsyncClient/builder) - builder (.serviceConfiguration ^S3AsyncClientBuilder builder ^S3Configuration sconfig) - builder (.asyncConfiguration ^S3AsyncClientBuilder builder ^ClientAsyncConfiguration aconfig) - builder (.httpClient ^S3AsyncClientBuilder builder ^NettyNioAsyncHttpClient hclient) - builder (.region ^S3AsyncClientBuilder builder (lookup-region region)) - builder (cond-> ^S3AsyncClientBuilder builder - (some? endpoint) - (.endpointOverride (URI. endpoint)))] - (.build ^S3AsyncClientBuilder builder)) - - ] + client (let [builder (S3AsyncClient/builder) + builder (.serviceConfiguration ^S3AsyncClientBuilder builder ^S3Configuration sconfig) + builder (.asyncConfiguration ^S3AsyncClientBuilder builder ^ClientAsyncConfiguration aconfig) + builder (.httpClient ^S3AsyncClientBuilder builder ^NettyNioAsyncHttpClient hclient) + builder (.region ^S3AsyncClientBuilder builder (lookup-region region)) + builder (cond-> ^S3AsyncClientBuilder builder + (some? endpoint) + (.endpointOverride (URI. endpoint)))] + (.build ^S3AsyncClientBuilder builder))] (reify clojure.lang.IDeref @@ -226,6 +227,7 @@ [id subscriber sem content] (px/thread {:name "penpot/s3/uploader" + :virtual true :daemon true} (l/trace :hint "start upload thread" :object-id (str id) @@ -267,15 +269,15 @@ (Optional/of (long (impl/get-size content)))) (^void subscribe [_ ^Subscriber subscriber] - (let [sem (Semaphore. 0) - thr (upload-thread id subscriber sem content)] - (.onSubscribe subscriber - (reify Subscription - (cancel [_] - (px/interrupt! thr) - (.release sem 1)) - (request [_ n] - (.release sem (int n))))))))) + (let [sem (Semaphore. 0) + thr (upload-thread id subscriber sem content)] + (.onSubscribe subscriber + (reify Subscription + (cancel [_] + (px/interrupt! thr) + (.release sem 1)) + (request [_ n] + (.release sem (int n))))))))) (defn- put-object @@ -299,7 +301,7 @@ [path] (proxy [FilterInputStream] [(io/input-stream path)] (close [] - (fs/delete path) + (ex/ignoring (fs/delete path)) (proxy-super close)))) (defn- get-object-data @@ -313,7 +315,7 @@ ;; to the filesystem and then read with buffered inputstream; if ;; not, read the contento into memory using bytearrays. (if (> ^long size (* 1024 1024 2)) - (let [path (tmp/tempfile :prefix "penpot.storage.s3.") + (let [path (tmp/tempfile :prefix "penpot.storage.s3." :min-age "6h") rxf (AsyncResponseTransformer/toFile ^Path path)] (->> (.getObject ^S3AsyncClient client ^GetObjectRequest gor diff --git a/backend/src/app/storage/tmp.clj b/backend/src/app/storage/tmp.clj index 057e82dad2..92cda29ebb 100644 --- a/backend/src/app/storage/tmp.clj +++ b/backend/src/app/storage/tmp.clj @@ -19,6 +19,8 @@ [promesa.exec :as px] [promesa.exec.csp :as sp])) +(def default-tmp-dir "/tmp/penpot") + (declare ^:private remove-temp-file) (declare ^:private io-loop) @@ -29,10 +31,11 @@ (defmethod ig/prep-key ::cleaner [_ cfg] - (assoc cfg ::min-age (dt/duration "30m"))) + (assoc cfg ::min-age (dt/duration "60m"))) (defmethod ig/init-key ::cleaner [_ cfg] + (fs/create-dir default-tmp-dir) (px/fn->thread (partial io-loop cfg) {:name "penpot/storage/tmp-cleaner" :virtual true})) @@ -42,14 +45,15 @@ (defn- io-loop [{:keys [::min-age] :as cfg}] - (l/info :hint "started tmp file cleaner") + (l/inf :hint "started tmp cleaner" :default-min-age (dt/format-duration min-age)) (try (loop [] - (when-let [path (sp/take! queue)] - (l/debug :hint "schedule tempfile deletion" :path path + (when-let [[path min-age'] (sp/take! queue)] + (let [min-age (or min-age' min-age)] + (l/dbg :hint "schedule tempfile deletion" :path path :expires-at (dt/plus (dt/now) min-age)) - (px/schedule! (inst-ms min-age) (partial remove-temp-file cfg path)) - (recur))) + (px/schedule! (inst-ms min-age) (partial remove-temp-file cfg path)) + (recur)))) (catch InterruptedException _ (l/trace :hint "cleaner interrupted")) (finally @@ -57,11 +61,11 @@ (defn- remove-temp-file "Permanently delete tempfile" - [{:keys [::wrk/executor path]}] + [{:keys [::wrk/executor]} path] (when (fs/exists? path) (px/run! executor (fn [] - (l/debug :hint "permanently delete tempfile" :path path) + (l/dbg :hint "permanently delete tempfile" :path path) (fs/delete path))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -69,18 +73,14 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn tempfile - "Returns a tmpfile candidate (without creating it)" - [& {:keys [suffix prefix] + [& {:keys [suffix prefix min-age] :or {prefix "penpot." suffix ".tmp"}}] - (let [candidate (fs/tempfile :suffix suffix :prefix prefix)] - (sp/offer! queue candidate) - candidate)) - -(defn create-tempfile - [& {:keys [suffix prefix] - :or {prefix "penpot." - suffix ".tmp"}}] - (let [path (fs/create-tempfile :suffix suffix :prefix prefix)] - (sp/offer! queue path) + (let [path (fs/create-tempfile + :perms "rw-r--r--" + :dir default-tmp-dir + :suffix suffix + :prefix prefix)] + (fs/delete-on-exit! path) + (sp/offer! queue [path (some-> min-age dt/duration)]) path)) diff --git a/backend/src/app/svgo.clj b/backend/src/app/svgo.clj new file mode 100644 index 0000000000..a846fa7680 --- /dev/null +++ b/backend/src/app/svgo.clj @@ -0,0 +1,42 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.svgo + "A SVG Optimizer service" + (:require + [app.common.jsrt :as jsrt] + [app.common.logging :as l] + [app.worker :as-alias wrk] + [integrant.core :as ig] + [promesa.exec.semaphore :as ps] + [promesa.util :as pu])) + +(def ^:dynamic *semaphore* + "A dynamic variable that can optionally contain a traffic light to + appropriately delimit the use of resources, managed externally." + nil) + +(defn optimize + [{pool ::optimizer} data] + (try + (some-> *semaphore* ps/acquire!) + (jsrt/run! pool + (fn [context] + (jsrt/set! context "svgData" data) + (jsrt/eval! context "penpotSvgo.optimize(svgData, {plugins: ['safeAndFastPreset']})"))) + (finally + (some-> *semaphore* ps/release!)))) + +(defmethod ig/init-key ::optimizer + [_ _] + (l/inf :hint "initializing svg optimizer pool") + (let [init (jsrt/resource->source "app/common/svg/optimizer.js")] + (jsrt/pool :init init))) + +(defmethod ig/halt-key! ::optimizer + [_ pool] + (l/info :hint "stopping svg optimizer pool") + (pu/close! pool)) diff --git a/backend/src/app/tasks/file_gc.clj b/backend/src/app/tasks/file_gc.clj index ac83404a18..ed7815f688 100644 --- a/backend/src/app/tasks/file_gc.clj +++ b/backend/src/app/tasks/file_gc.clj @@ -10,16 +10,18 @@ file is eligible to be garbage collected after some period of inactivity (the default threshold is 72h)." (:require - [app.common.data :as d] + [app.binfile.common :as bfc] + [app.common.files.migrations :as fmg] + [app.common.files.validate :as cfv] [app.common.logging :as l] - [app.common.pages.migrations :as pmg] + [app.common.thumbnails :as thc] [app.common.types.components-list :as ctkl] [app.common.types.file :as ctf] [app.common.types.shape-tree :as ctt] [app.config :as cf] [app.db :as db] + [app.features.fdata :as feat.fdata] [app.media :as media] - [app.rpc.commands.files :as files] [app.storage :as sto] [app.util.blob :as blob] [app.util.pointer-map :as pmap] @@ -28,8 +30,260 @@ [clojure.spec.alpha :as s] [integrant.core :as ig])) -(declare ^:private get-candidates) -(declare ^:private process-file) +(declare ^:private clean-file!) + +(defn- decode-file + [cfg {:keys [id] :as file}] + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] + (-> file + (update :features db/decode-pgarray #{}) + (update :data blob/decode) + (update :data feat.fdata/process-pointers deref) + (update :data feat.fdata/process-objects (partial into {})) + (update :data assoc :id id) + (fmg/migrate-file)))) + +(defn- update-file! + [{:keys [::db/conn] :as cfg} {:keys [id] :as file}] + (let [file (if (contains? (:features file) "fdata/objects-map") + (feat.fdata/enable-objects-map file) + file) + + file (if (contains? (:features file) "fdata/pointer-map") + (binding [pmap/*tracked* (pmap/create-tracked)] + (let [file (feat.fdata/enable-pointer-map file)] + (feat.fdata/persist-pointers! cfg id) + file)) + file) + + file (-> file + (update :features db/encode-pgarray conn "text") + (update :data blob/encode))] + + (db/update! conn :file + {:has-media-trimmed true + :features (:features file) + :version (:version file) + :data (:data file)} + {:id id} + {::db/return-keys true}))) + +(def ^:private + sql:get-candidates + "SELECT f.id, + f.data, + f.revn, + f.version, + f.features, + f.modified_at + FROM file AS f + WHERE f.has_media_trimmed IS false + AND f.modified_at < now() - ?::interval + ORDER BY f.modified_at DESC + FOR UPDATE + SKIP LOCKED") + +(defn- get-candidates + [{:keys [::db/conn ::min-age ::file-id]}] + (if (uuid? file-id) + (do + (l/warn :hint "explicit file id passed on params" :file-id (str file-id)) + (db/query conn :file {:id file-id})) + + (let [min-age (db/interval min-age)] + (db/cursor conn [sql:get-candidates min-age] {:chunk-size 1})))) + +(def ^:private sql:mark-file-media-object-deleted + "UPDATE file_media_object + SET deleted_at = now() + WHERE file_id = ? AND id != ALL(?::uuid[]) + RETURNING id") + +(defn- clean-file-media! + "Performs the garbage collection of file media objects." + [{:keys [::db/conn]} {:keys [id data] :as file}] + (let [used (bfc/collect-used-media data) + ids (db/create-array conn "uuid" used) + unused (->> (db/exec! conn [sql:mark-file-media-object-deleted id ids]) + (into #{} (map :id)))] + + (l/dbg :hint "clean" :rel "file-media-object" :file-id (str id) :total (count unused)) + + (doseq [id unused] + (l/trc :hint "mark deleted" + :rel "file-media-object" + :id (str id) + :file-id (str id))) + + file)) + +(def ^:private sql:mark-file-object-thumbnails-deleted + "UPDATE file_tagged_object_thumbnail + SET deleted_at = now() + WHERE file_id = ? AND object_id != ALL(?::text[]) + RETURNING object_id") + +(defn- clean-file-object-thumbnails! + [{:keys [::db/conn]} {:keys [data] :as file}] + (let [file-id (:id file) + using (->> (vals (:pages-index data)) + (into #{} (comp + (mapcat (fn [{:keys [id objects]}] + (->> (ctt/get-frames objects) + (map #(assoc % :page-id id))))) + (mapcat (fn [{:keys [id page-id]}] + (list + (thc/fmt-object-id file-id page-id id "frame") + (thc/fmt-object-id file-id page-id id "component"))))))) + + ids (db/create-array conn "text" using) + unused (->> (db/exec! conn [sql:mark-file-object-thumbnails-deleted file-id ids]) + (into #{} (map :object-id)))] + + (l/dbg :hint "clean" :rel "file-object-thumbnail" :file-id (str file-id) :total (count unused)) + + (doseq [object-id unused] + (l/trc :hint "mark deleted" + :rel "file-tagged-object-thumbnail" + :object-id object-id + :file-id (str file-id))) + + file)) + +(def ^:private sql:mark-file-thumbnails-deleted + "UPDATE file_thumbnail + SET deleted_at = now() + WHERE file_id = ? AND revn < ? + RETURNING revn") + +(defn- clean-file-thumbnails! + [{:keys [::db/conn]} {:keys [id revn] :as file}] + (let [unused (->> (db/exec! conn [sql:mark-file-thumbnails-deleted id revn]) + (into #{} (map :revn)))] + + (l/dbg :hint "clean" :rel "file-thumbnail" :file-id (str id) :total (count unused)) + + (doseq [revn unused] + (l/trc :hint "mark deleted" + :rel "file-thumbnail" + :revn revn + :file-id (str id))) + + file)) + + +(def ^:private sql:get-files-for-library + "SELECT f.id, f.data, f.modified_at, f.features, f.version + FROM file AS f + LEFT JOIN file_library_rel AS fl ON (fl.file_id = f.id) + WHERE fl.library_file_id = ? + AND f.deleted_at IS null + ORDER BY f.modified_at ASC") + +(defn- clean-deleted-components! + "Performs the garbage collection of unreferenced deleted components." + [{:keys [::db/conn] :as cfg} {:keys [data] :as file}] + (let [file-id (:id file) + + get-used-components + (fn [data components] + ;; Find which of the components are used in the file. + (into #{} + (filter #(ctf/used-in? data file-id % :component)) + components)) + + get-unused-components + (fn [components files] + ;; Find and return a set of unused components (on all files). + (reduce (fn [components {:keys [data]}] + (if (seq components) + (->> (get-used-components data components) + (set/difference components)) + (reduced components))) + + components + files)) + + process-fdata + (fn [data unused] + (reduce (fn [data id] + (l/trc :hint "delete component" + :component-id (str id) + :file-id (str file-id)) + (ctkl/delete-component data id)) + data + unused)) + + deleted (into #{} (ctkl/deleted-components-seq data)) + + unused (->> (db/cursor conn [sql:get-files-for-library file-id] {:chunk-size 1}) + (map (partial decode-file cfg)) + (cons file) + (get-unused-components deleted) + (mapv :id) + (set)) + + file (update file :data process-fdata unused)] + + + (l/dbg :hint "clean" :rel "components" :file-id (str file-id) :total (count unused)) + file)) + +(def ^:private sql:get-changes + "SELECT id, data FROM file_change + WHERE file_id = ? AND data IS NOT NULL + ORDER BY created_at ASC") + +(def ^:private sql:mark-deleted-data-fragments + "UPDATE file_data_fragment + SET deleted_at = now() + WHERE file_id = ? + AND id != ALL(?::uuid[]) + AND deleted_at IS NULL + RETURNING id") + +(def ^:private xf:collect-pointers + (comp (map :data) + (map blob/decode) + (mapcat feat.fdata/get-used-pointer-ids))) + +(defn- clean-data-fragments! + [{:keys [::db/conn]} {:keys [id] :as file}] + (let [used (into #{} xf:collect-pointers + (cons file (db/cursor conn [sql:get-changes id]))) + + unused (let [ids (db/create-array conn "uuid" used)] + (->> (db/exec! conn [sql:mark-deleted-data-fragments id ids]) + (into #{} (map :id))))] + + (l/dbg :hint "clean" :rel "file-data-fragment" :file-id (str id) :total (count unused)) + (doseq [id unused] + (l/trc :hint "mark deleted" + :rel "file-data-fragment" + :id (str id) + :file-id (str id))))) + +(defn- clean-media! + [cfg file] + (let [file (->> file + (clean-file-media! cfg) + (clean-file-thumbnails! cfg) + (clean-file-object-thumbnails! cfg) + (clean-deleted-components! cfg))] + (cfv/validate-file-schema! file) + file)) + +(defn- process-file! + [cfg file] + (try + (let [file (decode-file cfg file) + file (clean-media! cfg file) + file (update-file! cfg file)] + (clean-data-fragments! cfg file)) + (catch Throwable cause + (l/err :hint "error on cleaning file (skiping)" + :file-id (str (:id file)) + :cause cause)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HANDLER @@ -43,262 +297,28 @@ (assoc cfg ::min-age cf/deletion-delay)) (defmethod ig/init-key ::handler - [_ {:keys [::db/pool] :as cfg}] + [_ cfg] (fn [{:keys [file-id] :as params}] + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (let [min-age (dt/duration (or (:min-age params) (::min-age cfg))) + cfg (-> cfg + (update ::sto/storage media/configure-assets-storage conn) + (assoc ::file-id file-id) + (assoc ::min-age min-age)) - (db/with-atomic [conn pool] - (let [min-age (dt/duration (or (:min-age params) (::min-age cfg))) - cfg (-> cfg - (update ::sto/storage media/configure-assets-storage conn) - (assoc ::db/conn conn) - (assoc ::file-id file-id) - (assoc ::min-age min-age)) + total (reduce (fn [total file] + (process-file! cfg file) + (inc total)) + 0 + (get-candidates cfg))] - total (reduce (fn [total file] - (process-file cfg file) - (inc total)) - 0 - (get-candidates cfg))] + (l/inf :hint "task finished" + :min-age (dt/format-duration min-age) + :processed total) - (l/info :hint "task finished" :min-age (dt/format-duration min-age) :processed total) + ;; Allow optional rollback passed by params + (when (:rollback? params) + (db/rollback! conn)) - ;; Allow optional rollback passed by params - (when (:rollback? params) - (db/rollback! conn)) - - {:processed total})))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; IMPL -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(def ^:private - sql:get-candidates-chunk - "select f.id, - f.data, - f.revn, - f.features, - f.modified_at - from file as f - where f.has_media_trimmed is false - and f.modified_at < now() - ?::interval - and f.modified_at < ? - order by f.modified_at desc - limit 1 - for update skip locked") - -(defn- get-candidates - [{:keys [::db/conn ::min-age ::file-id]}] - (if (uuid? file-id) - (do - (l/warn :hint "explicit file id passed on params" :file-id file-id) - (->> (db/query conn :file {:id file-id}) - (map #(update % :features db/decode-pgarray #{})))) - (let [interval (db/interval min-age) - get-chunk (fn [cursor] - (let [rows (db/exec! conn [sql:get-candidates-chunk interval cursor])] - [(some->> rows peek :modified-at) - (map #(update % :features db/decode-pgarray #{}) rows)]))] - - (d/iteration get-chunk - :vf second - :kf first - :initk (dt/now))))) - -(defn collect-used-media - "Given a fdata (file data), returns all media references." - [data] - (let [xform (comp - (map :objects) - (mapcat vals) - (keep (fn [{:keys [type] :as obj}] - (case type - :path (get-in obj [:fill-image :id]) - :bool (get-in obj [:fill-image :id]) - ;; NOTE: because of some bug, we ended with - ;; many shape types having the ability to - ;; have fill-image attribute (which initially - ;; designed for :path shapes). - :group (get-in obj [:fill-image :id]) - :image (get-in obj [:metadata :id]) - - nil)))) - pages (concat - (vals (:pages-index data)) - (vals (:components data)))] - (-> #{} - (into xform pages) - (into (keys (:media data)))))) - -(defn- clean-file-media! - "Performs the garbage collection of file media objects." - [conn file-id data] - (let [used (collect-used-media data) - unused (->> (db/query conn :file-media-object {:file-id file-id}) - (remove #(contains? used (:id %))))] - - (doseq [mobj unused] - (l/debug :hint "delete file media object" - :id (:id mobj) - :media-id (:media-id mobj) - :thumbnail-id (:thumbnail-id mobj)) - - ;; NOTE: deleting the file-media-object in the database - ;; automatically marks as touched the referenced storage - ;; objects. The touch mechanism is needed because many files can - ;; point to the same storage objects and we can't just delete - ;; them. - (db/delete! conn :file-media-object {:id (:id mobj)})))) - -(defn- clean-file-object-thumbnails! - [{:keys [::db/conn ::sto/storage]} file-id data] - (let [stored (->> (db/query conn :file-object-thumbnail - {:file-id file-id} - {:columns [:object-id]}) - (into #{} (map :object-id))) - - using (into #{} - (mapcat (fn [{:keys [id objects]}] - (->> (ctt/get-frames objects) - (map #(str id (:id %)))))) - (vals (:pages-index data))) - - unused (set/difference stored using)] - - (when (seq unused) - (let [sql (str "delete from file_object_thumbnail " - " where file_id=? and object_id=ANY(?)" - " returning media_id") - res (db/exec! conn [sql file-id (db/create-array conn "text" unused)])] - - (doseq [media-id (into #{} (keep :media-id) res)] - ;; Mark as deleted the storage object related with the - ;; photo-id field. - (l/trace :hint "mark storage object as deleted" :id media-id) - (sto/del-object! storage media-id)) - - (l/debug :hint "delete file object thumbnails" - :file-id file-id - :total (count res)))))) - -(defn- clean-file-thumbnails! - [{:keys [::db/conn ::sto/storage]} file-id revn] - (let [sql (str "delete from file_thumbnail " - " where file_id=? and revn < ? " - " returning media_id") - res (db/exec! conn [sql file-id revn])] - - (when (seq res) - (doseq [media-id (into #{} (keep :media-id) res)] - ;; Mark as deleted the storage object related with the - ;; media-id field. - (l/trace :hint "mark storage object as deleted" :id media-id) - (sto/del-object! storage media-id)) - - (l/debug :hint "delete file thumbnails" - :file-id file-id - :total (count res))))) - -(def ^:private - sql:get-files-for-library - "select f.data, f.modified_at - from file as f - left join file_library_rel as fl on (fl.file_id = f.id) - where fl.library_file_id = ? - and f.modified_at < ? - and f.deleted_at is null - order by f.modified_at desc - limit 1") - -(defn- clean-deleted-components! - "Performs the garbage collection of unreferenced deleted components." - [conn file-id data] - (letfn [(get-files-chunk [cursor] - (let [rows (db/exec! conn [sql:get-files-for-library file-id cursor])] - [(some-> rows peek :modified-at) - (map (comp blob/decode :data) rows)])) - - (get-used-components [fdata components] - ;; Find which of the components are used in the file. - (into #{} - (filter #(ctf/used-in? fdata file-id % :component)) - components)) - - (get-unused-components [components files-data] - ;; Find and return a set of unused components (on all files). - (reduce (fn [components fdata] - (if (seq components) - (->> (get-used-components fdata components) - (set/difference components)) - (reduced components))) - - components - files-data))] - - (let [deleted (into #{} (ctkl/deleted-components-seq data)) - unused (->> (d/iteration get-files-chunk :vf second :kf first :initk (dt/now)) - (cons data) - (get-unused-components deleted) - (mapv :id))] - - (when (seq unused) - (l/debug :hint "clean deleted components" :total (count unused)) - - (let [data (reduce ctkl/delete-component data unused)] - (db/update! conn :file - {:data (blob/encode data)} - {:id file-id})))))) - -(defn- clean-data-fragments! - [conn file-id data] - (letfn [(get-pointers-chunk [cursor] - (let [sql (str "select id, data, created_at " - " from file_change " - " where file_id = ? " - " and data is not null " - " and created_at < ? " - " order by created_at desc " - " limit 1;") - rows (db/exec! conn [sql file-id cursor])] - [(some-> rows peek :created-at) - (mapcat (comp files/get-all-pointer-ids blob/decode :data) rows)]))] - - (let [used (into (files/get-all-pointer-ids data) - (d/iteration get-pointers-chunk - :vf second - :kf first - :initk (dt/now))) - - sql (str "select id from file_data_fragment " - " where file_id = ? AND id != ALL(?::uuid[])") - used (db/create-array conn "uuid" used) - rows (db/exec! conn [sql file-id used])] - - (doseq [fragment-id (map :id rows)] - (l/trace :hint "remove unused file data fragment" :id (str fragment-id)) - (db/delete! conn :file-data-fragment {:id fragment-id :file-id file-id}))))) - -(defn- process-file - [{:keys [::db/conn] :as cfg} {:keys [id data revn modified-at features] :as file}] - (l/debug :hint "processing file" :id id :modified-at modified-at) - - (binding [pmap/*load-fn* (partial files/load-pointer conn id) - pmap/*tracked* (atom {})] - (let [data (-> (blob/decode data) - (assoc :id id) - (pmg/migrate-data))] - - (clean-file-media! conn id data) - (clean-file-object-thumbnails! cfg id data) - (clean-file-thumbnails! cfg id revn) - (clean-deleted-components! conn id data) - - (when (contains? features "storage/pointer-map") - (clean-data-fragments! conn id data)) - - ;; Mark file as trimmed - (db/update! conn :file - {:has-media-trimmed true} - {:id id}) - - (files/persist-pointers! conn id)))) + {:processed total}))))) diff --git a/backend/src/app/tasks/file_xlog_gc.clj b/backend/src/app/tasks/file_xlog_gc.clj index 5ee2e1bbc5..c88f42a840 100644 --- a/backend/src/app/tasks/file_xlog_gc.clj +++ b/backend/src/app/tasks/file_xlog_gc.clj @@ -17,7 +17,8 @@ (def ^:private sql:delete-files-xlog "delete from file_change - where created_at < now() - ?::interval") + where created_at < now() - ?::interval + and label is NULL") (defmethod ig/pre-init-spec ::handler [_] (s/keys :req [::db/pool])) diff --git a/backend/src/app/tasks/objects_gc.clj b/backend/src/app/tasks/objects_gc.clj index 9ad011ea4c..c5e74ce3ac 100644 --- a/backend/src/app/tasks/objects_gc.clj +++ b/backend/src/app/tasks/objects_gc.clj @@ -8,7 +8,6 @@ "A maintenance task that performs a general purpose garbage collection of deleted or unreachable objects." (:require - [app.common.data :as d] [app.common.logging :as l] [app.config :as cf] [app.db :as db] @@ -18,12 +17,15 @@ [clojure.spec.alpha :as s] [integrant.core :as ig])) -(declare ^:private delete-profiles!) -(declare ^:private delete-teams!) -(declare ^:private delete-fonts!) -(declare ^:private delete-projects!) +(declare ^:private delete-file-data-fragments!) +(declare ^:private delete-file-media-objects!) +(declare ^:private delete-file-object-thumbnails!) +(declare ^:private delete-file-thumbnails!) (declare ^:private delete-files!) -(declare ^:private delete-orphan-teams!) +(declare ^:private delete-fonts!) +(declare ^:private delete-profiles!) +(declare ^:private delete-projects!) +(declare ^:private delete-teams!) (defmethod ig/pre-init-spec ::handler [_] (s/keys :req [::db/pool ::sto/storage])) @@ -33,211 +35,306 @@ (assoc cfg ::min-age cf/deletion-delay)) (defmethod ig/init-key ::handler - [_ {:keys [::db/pool ::sto/storage] :as cfg}] + [_ cfg] (fn [params] - (db/with-atomic [conn pool] - (let [min-age (or (:min-age params) (::min-age cfg)) - _ (l/info :hint "gc started" - :min-age (dt/format-duration min-age) - :rollback? (boolean (:rollback? params))) + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + ;; Disable deletion protection for the current transaction + (db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"]) + (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"]) - storage (media/configure-assets-storage storage conn) - cfg (-> cfg - (assoc ::min-age (db/interval min-age)) - (assoc ::conn conn) - (assoc ::storage storage)) + (let [min-age (dt/duration (or (:min-age params) (::min-age cfg))) + cfg (-> cfg + (assoc ::min-age (db/interval min-age)) + (update ::sto/storage media/configure-assets-storage conn)) - htotal (+ (delete-profiles! cfg) - (delete-teams! cfg) - (delete-projects! cfg) - (delete-files! cfg) - (delete-fonts! cfg)) - stotal (delete-orphan-teams! cfg)] + total (reduce + 0 + [(delete-profiles! cfg) + (delete-teams! cfg) + (delete-fonts! cfg) + (delete-projects! cfg) + (delete-files! cfg) + (delete-file-thumbnails! cfg) + (delete-file-object-thumbnails! cfg) + (delete-file-data-fragments! cfg) + (delete-file-media-objects! cfg)])] - (l/info :hint "gc finished" - :deleted htotal - :orphans stotal - :rollback? (boolean (:rollback? params))) + (l/info :hint "task finished" + :deleted total + :rollback? (boolean (:rollback? params))) - (when (:rollback? params) - (db/rollback! conn)) + (when (:rollback? params) + (db/rollback! conn)) - {:processed (+ stotal htotal) - :orphans stotal})))) + {:processed total}))))) -(def ^:private sql:get-profiles-chunk - "select id, photo_id, created_at from profile - where deleted_at is not null - and deleted_at < now() - ?::interval - and created_at < ? - order by created_at desc - limit 10 - for update skip locked") +(def ^:private sql:get-profiles + "SELECT id, photo_id FROM profile + WHERE deleted_at IS NOT NULL + AND deleted_at < now() - ?::interval + ORDER BY deleted_at ASC + FOR UPDATE + SKIP LOCKED") (defn- delete-profiles! - [{:keys [::conn ::min-age ::storage] :as cfg}] - (letfn [(get-chunk [cursor] - (let [rows (db/exec! conn [sql:get-profiles-chunk min-age cursor])] - [(some->> rows peek :created-at) rows])) + [{:keys [::db/conn ::min-age ::sto/storage] :as cfg}] + (->> (db/cursor conn [sql:get-profiles min-age]) + (reduce (fn [total {:keys [id photo-id]}] + (l/trc :hint "permanently delete" :rel "profile" :id (str id)) - (process-profile [total {:keys [id photo-id]}] - (l/debug :hint "permanently delete profile" :id (str id)) + ;; Mark as deleted the storage object + (some->> photo-id (sto/touch-object! storage)) - ;; Mark as deleted the storage object related with the - ;; photo-id field. - (some->> photo-id (sto/touch-object! storage)) + ;; And finally, permanently delete the profile. The + ;; relevant objects will be deleted using DELETE + ;; CASCADE database triggers. This may leave orphan + ;; teams, but there is a special task for deleting + ;; orphaned teams. + (db/delete! conn :profile {:id id}) - ;; And finally, permanently delete the profile. - (db/delete! conn :profile {:id id}) + (inc total)) + 0))) - (inc total))] - - (->> (d/iteration get-chunk :vf second :kf first :initk (dt/now)) - (reduce process-profile 0)))) - -(def ^:private sql:get-teams-chunk - "select id, photo_id, created_at from team - where deleted_at is not null - and deleted_at < now() - ?::interval - and created_at < ? - order by created_at desc - limit 10 - for update skip locked") +(def ^:private sql:get-teams + "SELECT deleted_at, id, photo_id FROM team + WHERE deleted_at IS NOT NULL + AND deleted_at < now() - ?::interval + ORDER BY deleted_at ASC + FOR UPDATE + SKIP LOCKED") (defn- delete-teams! - [{:keys [::conn ::min-age ::storage] :as cfg}] - (letfn [(get-chunk [cursor] - (let [rows (db/exec! conn [sql:get-teams-chunk min-age cursor])] - [(some->> rows peek :created-at) rows])) + [{:keys [::db/conn ::min-age ::sto/storage] :as cfg}] - (process-team [total {:keys [id photo-id]}] - (l/debug :hint "permanently delete team" :id (str id)) + (->> (db/cursor conn [sql:get-teams min-age]) + (reduce (fn [total {:keys [id photo-id deleted-at]}] + (l/trc :hint "permanently delete" + :rel "team" + :id (str id) + :deleted-at (dt/format-instant deleted-at)) - ;; Mark as deleted the storage object related with the - ;; photo-id field. - (some->> photo-id (sto/touch-object! storage)) + ;; Mark as deleted the storage object + (some->> photo-id (sto/touch-object! storage)) - ;; And finally, permanently delete the team. - (db/delete! conn :team {:id id}) + ;; And finally, permanently delete the team. + (db/delete! conn :team {:id id}) - (inc total))] + ;; Mark for deletion in cascade + (db/update! conn :team-font-variant + {:deleted-at deleted-at} + {:team-id id}) - (->> (d/iteration get-chunk :vf second :kf first :initk (dt/now)) - (reduce process-team 0)))) + (db/update! conn :project + {:deleted-at deleted-at} + {:team-id id}) -(def ^:private sql:get-orphan-teams-chunk - "select t.id, t.created_at - from team as t - left join team_profile_rel as tpr - on (t.id = tpr.team_id) - where tpr.profile_id is null - and t.created_at < ? - order by t.created_at desc - limit 10 - for update of t skip locked;") + (inc total)) + 0))) -(defn- delete-orphan-teams! - "Find all orphan teams (with no members and mark them for - deletion (soft delete)." - [{:keys [::conn] :as cfg}] - (letfn [(get-chunk [cursor] - (let [rows (db/exec! conn [sql:get-orphan-teams-chunk cursor])] - [(some->> rows peek :created-at) rows])) - - (process-team [total {:keys [id]}] - (let [result (db/update! conn :team - {:deleted-at (dt/now)} - {:id id :deleted-at nil} - {::db/return-keys? false}) - count (db/get-update-count result)] - (when (pos? count) - (l/debug :hint "mark team for deletion" :id (str id) )) - - (+ total count)))] - - (->> (d/iteration get-chunk :vf second :kf first :initk (dt/now)) - (reduce process-team 0)))) - -(def ^:private sql:get-fonts-chunk - "select id, created_at, woff1_file_id, woff2_file_id, otf_file_id, ttf_file_id - from team_font_variant - where deleted_at is not null - and deleted_at < now() - ?::interval - and created_at < ? - order by created_at desc - limit 10 - for update skip locked") +(def ^:private sql:get-fonts + "SELECT id, team_id, deleted_at, woff1_file_id, woff2_file_id, otf_file_id, ttf_file_id + FROM team_font_variant + WHERE deleted_at IS NOT NULL + AND deleted_at < now() - ?::interval + ORDER BY deleted_at ASC + FOR UPDATE + SKIP LOCKED") (defn- delete-fonts! - [{:keys [::conn ::min-age ::storage] :as cfg}] - (letfn [(get-chunk [cursor] - (let [rows (db/exec! conn [sql:get-fonts-chunk min-age cursor])] - [(some->> rows peek :created-at) rows])) + [{:keys [::db/conn ::min-age ::sto/storage] :as cfg}] + (->> (db/cursor conn [sql:get-fonts min-age]) + (reduce (fn [total {:keys [id team-id deleted-at] :as font}] + (l/trc :hint "permanently delete" + :rel "team-font-variant" + :id (str id) + :team-id (str team-id) + :deleted-at (dt/format-instant deleted-at)) - (process-font [total {:keys [id] :as font}] - (l/debug :hint "permanently delete font variant" :id (str id)) + ;; Mark as deleted the all related storage objects + (some->> (:woff1-file-id font) (sto/touch-object! storage)) + (some->> (:woff2-file-id font) (sto/touch-object! storage)) + (some->> (:otf-file-id font) (sto/touch-object! storage)) + (some->> (:ttf-file-id font) (sto/touch-object! storage)) - ;; Mark as deleted the all related storage objects - (some->> (:woff1-file-id font) (sto/touch-object! storage)) - (some->> (:woff2-file-id font) (sto/touch-object! storage)) - (some->> (:otf-file-id font) (sto/touch-object! storage)) - (some->> (:ttf-file-id font) (sto/touch-object! storage)) + ;; And finally, permanently delete the team font variant + (db/delete! conn :team-font-variant {:id id}) - ;; And finally, permanently delete the team font variant - (db/delete! conn :team-font-variant {:id id}) + (inc total)) + 0))) - (inc total))] - - (->> (d/iteration get-chunk :vf second :kf first :initk (dt/now)) - (reduce process-font 0)))) - -(def ^:private sql:get-projects-chunk - "select id, created_at - from project - where deleted_at is not null - and deleted_at < now() - ?::interval - and created_at < ? - order by created_at desc - limit 10 - for update skip locked") +(def ^:private sql:get-projects + "SELECT id, deleted_at, team_id + FROM project + WHERE deleted_at IS NOT NULL + AND deleted_at < now() - ?::interval + ORDER BY deleted_at ASC + FOR UPDATE + SKIP LOCKED") (defn- delete-projects! - [{:keys [::conn ::min-age] :as cfg}] - (letfn [(get-chunk [cursor] - (let [rows (db/exec! conn [sql:get-projects-chunk min-age cursor])] - [(some->> rows peek :created-at) rows])) + [{:keys [::db/conn ::min-age] :as cfg}] + (->> (db/cursor conn [sql:get-projects min-age]) + (reduce (fn [total {:keys [id team-id deleted-at]}] + (l/trc :hint "permanently delete" + :rel "project" + :id (str id) + :team-id (str team-id) + :deleted-at (dt/format-instant deleted-at)) - (process-project [total {:keys [id]}] - (l/debug :hint "permanently delete project" :id (str id)) - ;; And finally, permanently delete the project. - (db/delete! conn :project {:id id}) + ;; And finally, permanently delete the project. + (db/delete! conn :project {:id id}) - (inc total))] + ;; Mark files to be deleted + (db/update! conn :file + {:deleted-at deleted-at} + {:project-id id}) - (->> (d/iteration get-chunk :vf second :kf first :initk (dt/now)) - (reduce process-project 0)))) + (inc total)) + 0))) -(def ^:private sql:get-files-chunk - "select id, created_at - from file - where deleted_at is not null - and deleted_at < now() - ?::interval - and created_at < ? - order by created_at desc - limit 10 - for update skip locked") +(def ^:private sql:get-files + "SELECT id, deleted_at, project_id + FROM file + WHERE deleted_at IS NOT NULL + AND deleted_at < now() - ?::interval + ORDER BY deleted_at ASC + FOR UPDATE + SKIP LOCKED") (defn- delete-files! - [{:keys [::conn ::min-age] :as cfg}] - (letfn [(get-chunk [cursor] - (let [rows (db/exec! conn [sql:get-files-chunk min-age cursor])] - [(some->> rows peek :created-at) rows])) + [{:keys [::db/conn ::min-age] :as cfg}] + (->> (db/cursor conn [sql:get-files min-age]) + (reduce (fn [total {:keys [id deleted-at project-id]}] + (l/trc :hint "permanently delete" + :rel "file" + :id (str id) + :project-id (str project-id) + :deleted-at (dt/format-instant deleted-at)) - (process-file [total {:keys [id]}] - (l/debug :hint "permanently delete file" :id (str id)) - ;; And finally, permanently delete the file. - (db/delete! conn :file {:id id}) - (inc total))] + ;; And finally, permanently delete the file. + (db/delete! conn :file {:id id}) - (->> (d/iteration get-chunk :vf second :kf first :initk (dt/now)) - (reduce process-file 0)))) + ;; Mark file media objects to be deleted + (db/update! conn :file-media-object + {:deleted-at deleted-at} + {:file-id id}) + + ;; Mark thumbnails to be deleted + (db/update! conn :file-thumbnail + {:deleted-at deleted-at} + {:file-id id}) + + (db/update! conn :file-tagged-object-thumbnail + {:deleted-at deleted-at} + {:file-id id}) + + (inc total)) + 0))) + + +(def ^:private sql:get-file-thumbnails + "SELECT file_id, revn, media_id, deleted_at + FROM file_thumbnail + WHERE deleted_at IS NOT NULL + AND deleted_at < now() - ?::interval + ORDER BY deleted_at ASC + FOR UPDATE + SKIP LOCKED") + +(defn delete-file-thumbnails! + [{:keys [::db/conn ::min-age ::sto/storage] :as cfg}] + (->> (db/cursor conn [sql:get-file-thumbnails min-age]) + (reduce (fn [total {:keys [file-id revn media-id deleted-at]}] + (l/trc :hint "permanently delete" + :rel "file-thumbnail" + :file-id (str file-id) + :revn revn + :deleted-at (dt/format-instant deleted-at)) + + ;; Mark as deleted the storage object + (some->> media-id (sto/touch-object! storage)) + + ;; And finally, permanently delete the object + (db/delete! conn :file-thumbnail {:file-id file-id :revn revn}) + + (inc total)) + 0))) + +(def ^:private sql:get-file-object-thumbnails + "SELECT file_id, object_id, media_id, deleted_at + FROM file_tagged_object_thumbnail + WHERE deleted_at IS NOT NULL + AND deleted_at < now() - ?::interval + ORDER BY deleted_at ASC + FOR UPDATE + SKIP LOCKED") + +(defn delete-file-object-thumbnails! + [{:keys [::db/conn ::min-age ::sto/storage] :as cfg}] + (->> (db/cursor conn [sql:get-file-object-thumbnails min-age]) + (reduce (fn [total {:keys [file-id object-id media-id deleted-at]}] + (l/trc :hint "permanently delete" + :rel "file-tagged-object-thumbnail" + :file-id (str file-id) + :object-id object-id + :deleted-at (dt/format-instant deleted-at)) + + ;; Mark as deleted the storage object + (some->> media-id (sto/touch-object! storage)) + + ;; And finally, permanently delete the object + (db/delete! conn :file-tagged-object-thumbnail {:file-id file-id :object-id object-id}) + + (inc total)) + 0))) + +(def ^:private sql:get-file-data-fragments + "SELECT file_id, id, deleted_at + FROM file_data_fragment + WHERE deleted_at IS NOT NULL + AND deleted_at < now() - ?::interval + ORDER BY deleted_at ASC + FOR UPDATE + SKIP LOCKED") + +(defn- delete-file-data-fragments! + [{:keys [::db/conn ::min-age] :as cfg}] + (->> (db/cursor conn [sql:get-file-data-fragments min-age]) + (reduce (fn [total {:keys [file-id id deleted-at]}] + (l/trc :hint "permanently delete" + :rel "file-data-fragment" + :id (str id) + :file-id (str file-id) + :deleted-at (dt/format-instant deleted-at)) + + (db/delete! conn :file-data-fragment {:file-id file-id :id id}) + + (inc total)) + 0))) + +(def ^:private sql:get-file-media-objects + "SELECT id, file_id, media_id, thumbnail_id, deleted_at + FROM file_media_object + WHERE deleted_at IS NOT NULL + AND deleted_at < now() - ?::interval + ORDER BY deleted_at ASC + FOR UPDATE + SKIP LOCKED") + +(defn- delete-file-media-objects! + [{:keys [::db/conn ::min-age ::sto/storage] :as cfg}] + (->> (db/cursor conn [sql:get-file-media-objects min-age]) + (reduce (fn [total {:keys [id file-id deleted-at] :as fmo}] + (l/trc :hint "permanently delete" + :rel "file-media-object" + :id (str id) + :file-id (str file-id) + :deleted-at (dt/format-instant deleted-at)) + + ;; Mark as deleted the all related storage objects + (some->> (:media-id fmo) (sto/touch-object! storage)) + (some->> (:thumbnail-id fmo) (sto/touch-object! storage)) + + (db/delete! conn :file-media-object {:id id}) + + (inc total)) + 0))) diff --git a/backend/src/app/tasks/orphan_teams_gc.clj b/backend/src/app/tasks/orphan_teams_gc.clj new file mode 100644 index 0000000000..c04123a831 --- /dev/null +++ b/backend/src/app/tasks/orphan_teams_gc.clj @@ -0,0 +1,59 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.tasks.orphan-teams-gc + "A maintenance task that performs orphan teams GC." + (:require + [app.common.logging :as l] + [app.db :as db] + [app.util.time :as dt] + [clojure.spec.alpha :as s] + [integrant.core :as ig])) + +(declare ^:private delete-orphan-teams!) + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req [::db/pool])) + +(defmethod ig/init-key ::handler + [_ cfg] + (fn [params] + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (l/inf :hint "gc started" :rollback? (boolean (:rollback? params))) + (let [total (delete-orphan-teams! cfg)] + (l/inf :hint "task finished" + :teams total + :rollback? (boolean (:rollback? params))) + + (when (:rollback? params) + (db/rollback! conn)) + + {:processed total}))))) + +(def ^:private sql:get-orphan-teams + "SELECT t.id + FROM team AS t + LEFT JOIN team_profile_rel AS tpr + ON (t.id = tpr.team_id) + WHERE tpr.profile_id IS NULL + AND t.deleted_at IS NULL + ORDER BY t.created_at ASC + FOR UPDATE OF t + SKIP LOCKED") + +(defn- delete-orphan-teams! + "Find all orphan teams (with no members) and mark them for + deletion (soft delete)." + [{:keys [::db/conn] :as cfg}] + (->> (db/cursor conn sql:get-orphan-teams) + (map :id) + (reduce (fn [total team-id] + (l/trc :hint "mark orphan team for deletion" :id (str team-id)) + (db/update! conn :team + {:deleted-at (dt/now)} + {:id team-id}) + (inc total)) + 0))) diff --git a/backend/src/app/tasks/tasks_gc.clj b/backend/src/app/tasks/tasks_gc.clj index 69dd11dfd7..77f1f92fa8 100644 --- a/backend/src/app/tasks/tasks_gc.clj +++ b/backend/src/app/tasks/tasks_gc.clj @@ -16,8 +16,7 @@ (def ^:private sql:delete-completed-tasks - "delete from task_completed - where scheduled_at < now() - ?::interval") + "DELETE FROM task WHERE scheduled_at < now() - ?::interval") (defmethod ig/pre-init-spec ::handler [_] (s/keys :req [::db/pool])) diff --git a/backend/src/app/tasks/telemetry.clj b/backend/src/app/tasks/telemetry.clj index 7754f699e7..ec07c67b3b 100644 --- a/backend/src/app/tasks/telemetry.clj +++ b/backend/src/app/tasks/telemetry.clj @@ -15,30 +15,201 @@ [app.db :as db] [app.http.client :as http] [app.main :as-alias main] + [app.setup :as-alias setup] [app.util.json :as json] [clojure.spec.alpha :as s] [integrant.core :as ig] [promesa.exec :as px])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; TASK ENTRY POINT +;; IMPL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare get-stats) -(declare send!) -(declare get-subscriptions-newsletter-updates) -(declare get-subscriptions-newsletter-news) +(defn- send! + [cfg data] + (let [request {:method :post + :uri (cf/get :telemetry-uri) + :headers {"content-type" "application/json"} + :body (json/encode-str data)} + response (http/req! cfg request)] + (when (> (:status response) 206) + (ex/raise :type :internal + :code :invalid-response + :response-status (:status response) + :response-body (:body response))))) + +(defn- get-subscriptions-newsletter-updates + [conn] + (let [sql "SELECT email FROM profile where props->>'~:newsletter-updates' = 'true'"] + (->> (db/exec! conn [sql]) + (mapv :email)))) + +(defn- get-subscriptions-newsletter-news + [conn] + (let [sql "SELECT email FROM profile where props->>'~:newsletter-news' = 'true'"] + (->> (db/exec! conn [sql]) + (mapv :email)))) + +(defn- get-num-teams + [conn] + (-> (db/exec-one! conn ["SELECT count(*) AS count FROM team"]) :count)) + +(defn- get-num-projects + [conn] + (-> (db/exec-one! conn ["SELECT count(*) AS count FROM project"]) :count)) + +(defn- get-num-files + [conn] + (-> (db/exec-one! conn ["SELECT count(*) AS count FROM file"]) :count)) + +(defn- get-num-file-changes + [conn] + (let [sql (str "SELECT count(*) AS count " + " FROM file_change " + " where date_trunc('day', created_at) = date_trunc('day', now())")] + (-> (db/exec-one! conn [sql]) :count))) + +(defn- get-num-touched-files + [conn] + (let [sql (str "SELECT count(distinct file_id) AS count " + " FROM file_change " + " where date_trunc('day', created_at) = date_trunc('day', now())")] + (-> (db/exec-one! conn [sql]) :count))) + +(defn- get-num-users + [conn] + (-> (db/exec-one! conn ["SELECT count(*) AS count FROM profile"]) :count)) + +(defn- get-num-fonts + [conn] + (-> (db/exec-one! conn ["SELECT count(*) AS count FROM team_font_variant"]) :count)) + +(defn- get-num-comments + [conn] + (-> (db/exec-one! conn ["SELECT count(*) AS count FROM comment"]) :count)) + +(def sql:team-averages + "with projects_by_team AS ( + SELECT t.id, count(p.id) AS num_projects + FROM team AS t + LEFT JOIN project AS p ON (p.team_id = t.id) + GROUP BY 1 + ), files_by_project AS ( + SELECT p.id, count(f.id) AS num_files + FROM project AS p + LEFT JOIN file AS f ON (f.project_id = p.id) + GROUP BY 1 + ), comment_threads_by_file AS ( + SELECT f.id, count(ct.id) AS num_comment_threads + FROM file AS f + LEFT JOIN comment_thread AS ct ON (ct.file_id = f.id) + GROUP BY 1 + ), users_by_team AS ( + SELECT t.id, count(tp.profile_id) AS num_users + FROM team AS t + LEFT JOIN team_profile_rel AS tp ON(tp.team_id = t.id) + GROUP BY 1 + ) + SELECT (SELECT avg(num_projects)::integer FROM projects_by_team) AS avg_projects_on_team, + (SELECT max(num_projects)::integer FROM projects_by_team) AS max_projects_on_team, + (SELECT avg(num_files)::integer FROM files_by_project) AS avg_files_on_project, + (SELECT max(num_files)::integer FROM files_by_project) AS max_files_on_project, + (SELECT avg(num_comment_threads)::integer FROM comment_threads_by_file) AS avg_comment_threads_on_file, + (SELECT max(num_comment_threads)::integer FROM comment_threads_by_file) AS max_comment_threads_on_file, + (SELECT avg(num_users)::integer FROM users_by_team) AS avg_users_on_team, + (SELECT max(num_users)::integer FROM users_by_team) AS max_users_on_team") + +(defn- get-team-averages + [conn] + (->> [sql:team-averages] + (db/exec-one! conn))) + +(defn- get-enabled-auth-providers + [conn] + (let [sql (str "SELECT auth_backend AS backend, count(*) AS total " + " FROM profile GROUP BY 1") + rows (db/exec! conn [sql])] + (->> rows + (map (fn [{:keys [backend total]}] + (let [backend (or backend "penpot")] + [(keyword (str "auth-backend-" backend)) + total]))) + (into {})))) + +(defn- get-jvm-stats + [] + (let [^Runtime runtime (Runtime/getRuntime)] + {:jvm-heap-current (.totalMemory runtime) + :jvm-heap-max (.maxMemory runtime) + :jvm-cpus (.availableProcessors runtime) + :os-arch (System/getProperty "os.arch") + :os-name (System/getProperty "os.name") + :os-version (System/getProperty "os.version") + :user-tz (System/getProperty "user.timezone")})) + +(def ^:private sql:get-counters + "SELECT name, count(*) AS count + FROM audit_log + WHERE source = 'backend' + AND tracked_at >= date_trunc('day', now()) + GROUP BY 1 + ORDER BY 2 DESC") + +(defn- get-action-counters + [conn] + (let [counters (->> (db/exec! conn [sql:get-counters]) + (d/index-by (comp keyword :name) :count)) + total (reduce + 0 (vals counters))] + {:total-accomulated-events total + :event-counters counters})) + +(def ^:private sql:clean-counters + "DELETE FROM audit_log + WHERE ip_addr = '0.0.0.0'::inet -- we know this is from telemetry + AND tracked_at < (date_trunc('day', now()) - '1 day'::interval)") + +(defn- clean-counters-data! + [conn] + (when-not (contains? cf/flags :audit-log) + (db/exec-one! conn [sql:clean-counters]))) + +(defn- get-stats + [conn] + (let [referer (if (cf/get :telemetry-with-taiga) + "taiga" + (cf/get :telemetry-referer))] + (-> {:referer referer + :public-uri (cf/get :public-uri) + :total-teams (get-num-teams conn) + :total-projects (get-num-projects conn) + :total-files (get-num-files conn) + :total-users (get-num-users conn) + :total-fonts (get-num-fonts conn) + :total-comments (get-num-comments conn) + :total-file-changes (get-num-file-changes conn) + :total-touched-files (get-num-touched-files conn)} + (merge + (get-team-averages conn) + (get-jvm-stats) + (get-enabled-auth-providers conn) + (get-action-counters conn)) + (d/without-nils)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TASK ENTRY POINT +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defmethod ig/pre-init-spec ::handler [_] (s/keys :req [::http/client ::db/pool - ::main/props])) + ::setup/props])) (defmethod ig/init-key ::handler - [_ {:keys [::db/pool ::main/props] :as cfg}] + [_ {:keys [::db/pool ::setup/props] :as cfg}] (fn [{:keys [send? enabled?] :or {send? true enabled? false}}] (let [subs {:newsletter-updates (get-subscriptions-newsletter-updates pool) :newsletter-news (get-subscriptions-newsletter-news pool)} + enabled? (or enabled? (contains? cf/flags :telemetry) (cf/get :telemetry-enabled)) @@ -46,6 +217,10 @@ data {:subscriptions subs :version (:full cf/version) :instance-id (:instance-id props)}] + + (when enabled? + (clean-counters-data! pool)) + (cond ;; If we have telemetry enabled, then proceed the normal ;; operation. @@ -61,7 +236,8 @@ ;; onboarding dialog or the profile section, then proceed to ;; send a limited telemetry data, that consists in the list of ;; subscribed emails and the running penpot version. - (seq subs) + (or (seq (:newsletter-updates subs)) + (seq (:newsletter-news subs))) (do (when send? (px/sleep (rand-int 10000)) @@ -70,152 +246,3 @@ :else data)))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; IMPL -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- send! - [cfg data] - (let [response (http/req! cfg - {:method :post - :uri (cf/get :telemetry-uri) - :headers {"content-type" "application/json"} - :body (json/encode-str data)} - {:sync? true})] - (when (> (:status response) 206) - (ex/raise :type :internal - :code :invalid-response - :response-status (:status response) - :response-body (:body response))))) - -(defn- get-subscriptions-newsletter-updates - [conn] - (let [sql "select email from profile where props->>'~:newsletter-updates' = 'true'"] - (->> (db/exec! conn [sql]) - (mapv :email)))) - -(defn- get-subscriptions-newsletter-news - [conn] - (let [sql "select email from profile where props->>'~:newsletter-news' = 'true'"] - (->> (db/exec! conn [sql]) - (mapv :email)))) - -(defn- retrieve-num-teams - [conn] - (-> (db/exec-one! conn ["select count(*) as count from team;"]) :count)) - -(defn- retrieve-num-projects - [conn] - (-> (db/exec-one! conn ["select count(*) as count from project;"]) :count)) - -(defn- retrieve-num-files - [conn] - (-> (db/exec-one! conn ["select count(*) as count from file;"]) :count)) - -(defn- retrieve-num-file-changes - [conn] - (let [sql (str "select count(*) as count " - " from file_change " - " where date_trunc('day', created_at) = date_trunc('day', now())")] - (-> (db/exec-one! conn [sql]) :count))) - -(defn- retrieve-num-touched-files - [conn] - (let [sql (str "select count(distinct file_id) as count " - " from file_change " - " where date_trunc('day', created_at) = date_trunc('day', now())")] - (-> (db/exec-one! conn [sql]) :count))) - -(defn- retrieve-num-users - [conn] - (-> (db/exec-one! conn ["select count(*) as count from profile;"]) :count)) - -(defn- retrieve-num-fonts - [conn] - (-> (db/exec-one! conn ["select count(*) as count from team_font_variant;"]) :count)) - -(defn- retrieve-num-comments - [conn] - (-> (db/exec-one! conn ["select count(*) as count from comment;"]) :count)) - -(def sql:team-averages - "with projects_by_team as ( - select t.id, count(p.id) as num_projects - from team as t - left join project as p on (p.team_id = t.id) - group by 1 - ), files_by_project as ( - select p.id, count(f.id) as num_files - from project as p - left join file as f on (f.project_id = p.id) - group by 1 - ), comment_threads_by_file as ( - select f.id, count(ct.id) as num_comment_threads - from file as f - left join comment_thread as ct on (ct.file_id = f.id) - group by 1 - ), users_by_team as ( - select t.id, count(tp.profile_id) as num_users - from team as t - left join team_profile_rel as tp on(tp.team_id = t.id) - group by 1 - ) - select (select avg(num_projects)::integer from projects_by_team) as avg_projects_on_team, - (select max(num_projects)::integer from projects_by_team) as max_projects_on_team, - (select avg(num_files)::integer from files_by_project) as avg_files_on_project, - (select max(num_files)::integer from files_by_project) as max_files_on_project, - (select avg(num_comment_threads)::integer from comment_threads_by_file) as avg_comment_threads_on_file, - (select max(num_comment_threads)::integer from comment_threads_by_file) as max_comment_threads_on_file, - (select avg(num_users)::integer from users_by_team) as avg_users_on_team, - (select max(num_users)::integer from users_by_team) as max_users_on_team;") - -(defn- retrieve-team-averages - [conn] - (->> [sql:team-averages] - (db/exec-one! conn))) - -(defn- retrieve-enabled-auth-providers - [conn] - (let [sql (str "select auth_backend as backend, count(*) as total " - " from profile group by 1") - rows (db/exec! conn [sql])] - (->> rows - (map (fn [{:keys [backend total]}] - (let [backend (or backend "penpot")] - [(keyword (str "auth-backend-" backend)) - total]))) - (into {})))) - -(defn- retrieve-jvm-stats - [] - (let [^Runtime runtime (Runtime/getRuntime)] - {:jvm-heap-current (.totalMemory runtime) - :jvm-heap-max (.maxMemory runtime) - :jvm-cpus (.availableProcessors runtime) - :os-arch (System/getProperty "os.arch") - :os-name (System/getProperty "os.name") - :os-version (System/getProperty "os.version") - :user-tz (System/getProperty "user.timezone")})) - -(defn get-stats - [conn] - (let [referer (if (cf/get :telemetry-with-taiga) - "taiga" - (cf/get :telemetry-referer))] - (-> {:referer referer - :public-uri (cf/get :public-uri) - :total-teams (retrieve-num-teams conn) - :total-projects (retrieve-num-projects conn) - :total-files (retrieve-num-files conn) - :total-users (retrieve-num-users conn) - :total-fonts (retrieve-num-fonts conn) - :total-comments (retrieve-num-comments conn) - :total-file-changes (retrieve-num-file-changes conn) - :total-touched-files (retrieve-num-touched-files conn)} - (d/merge - (retrieve-team-averages conn) - (retrieve-jvm-stats) - (retrieve-enabled-auth-providers conn)) - (d/without-nils)))) - diff --git a/backend/src/app/util/async.clj b/backend/src/app/util/async.clj deleted file mode 100644 index 217f54ac02..0000000000 --- a/backend/src/app/util/async.clj +++ /dev/null @@ -1,113 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.util.async - (:require - [app.common.exceptions :as ex] - [clojure.core.async :as a] - [clojure.core.async.impl.protocols :as ap] - [clojure.spec.alpha :as s]) - (:import - java.util.concurrent.Executor - java.util.concurrent.RejectedExecutionException)) - -(s/def ::executor #(instance? Executor %)) -(s/def ::channel #(satisfies? ap/Channel %)) - -(defonce processors - (delay (.availableProcessors (Runtime/getRuntime)))) - -(defmacro go-try - [& body] - `(a/go - (try - ~@body - (catch Exception e# e#)))) - -(defmacro thread - [& body] - `(a/thread - (try - ~@body - (catch Exception e# - e#)))) - -(defmacro ~ch a/close!)))) - -(defn thread-call - [^Executor executor f] - (let [ch (a/chan 1) - f' (fn [] - (try - (let [ret (ex/try* f identity)] - (when (some? ret) (a/>!! ch ret))) - (finally - (a/close! ch))))] - (try - (.execute executor f') - (catch RejectedExecutionException _cause - (a/close! ch))) - - ch)) - -(defmacro with-thread - [executor & body] - (if (= executor ::default) - `(a/thread-call (^:once fn* [] (try ~@body (catch Exception e# e#)))) - `(thread-call ~executor (^:once fn* [] ~@body)))) - -(defn batch - [in {:keys [max-batch-size - max-batch-age - buffer-size - init] - :or {max-batch-size 200 - max-batch-age (* 30 1000) - buffer-size 128 - init #{}} - :as opts}] - (let [out (a/chan buffer-size)] - (a/go-loop [tch (a/timeout max-batch-age) buf init] - (let [[val port] (a/alts! [tch in])] - (cond - (identical? port tch) - (if (empty? buf) - (recur (a/timeout max-batch-age) buf) - (do - (a/>! out [:timeout buf]) - (recur (a/timeout max-batch-age) init))) - - (nil? val) - (if (empty? buf) - (a/close! out) - (do - (a/offer! out [:timeout buf]) - (a/close! out))) - - (identical? port in) - (let [buf (conj buf val)] - (if (>= (count buf) max-batch-size) - (do - (a/>! out [:size buf]) - (recur (a/timeout max-batch-age) init)) - (recur tch buf)))))) - out)) - -(defn thread-sleep - [ms] - (Thread/sleep (long ms))) diff --git a/backend/src/app/util/cache.clj b/backend/src/app/util/cache.clj index c5aa733e6f..65861e1797 100644 --- a/backend/src/app/util/cache.clj +++ b/backend/src/app/util/cache.clj @@ -9,61 +9,71 @@ (:refer-clojure :exclude [get]) (:require [app.util.time :as dt] - [promesa.core :as p] [promesa.exec :as px]) (:import com.github.benmanes.caffeine.cache.AsyncCache - com.github.benmanes.caffeine.cache.AsyncLoadingCache - com.github.benmanes.caffeine.cache.CacheLoader + com.github.benmanes.caffeine.cache.Cache com.github.benmanes.caffeine.cache.Caffeine com.github.benmanes.caffeine.cache.RemovalListener + com.github.benmanes.caffeine.cache.stats.CacheStats java.time.Duration java.util.concurrent.Executor java.util.function.Function)) (set! *warn-on-reflection* true) -(defn create-listener +(defprotocol ICache + (get [_ k] [_ k load-fn] "get cache entry") + (invalidate! [_] [_ k] "invalidate cache")) + +(defprotocol ICacheStats + (stats [_] "get stats")) + +(defn- create-listener [f] (reify RemovalListener (onRemoval [_ key val cause] (when val (f key val cause))))) -(defn create-loader - [f] - (reify CacheLoader - (load [_ key] - (f key)))) +(defn- get-stats + [^Cache cache] + (let [^CacheStats stats (.stats cache)] + {:hit-rate (.hitRate stats) + :hit-count (.hitCount stats) + :req-count (.requestCount stats) + :miss-count (.missCount stats) + :miss-rate (.missRate stats)})) (defn create - [& {:keys [executor on-remove load-fn keepalive]}] - (as-> (Caffeine/newBuilder) builder - (if on-remove (.removalListener builder (create-listener on-remove)) builder) - (if executor (.executor builder ^Executor (px/resolve-executor executor)) builder) - (if keepalive (.expireAfterAccess builder ^Duration (dt/duration keepalive)) builder) - (if load-fn - (.buildAsync builder ^CacheLoader (create-loader load-fn)) - (.buildAsync builder)))) + [& {:keys [executor on-remove max-size keepalive]}] + (let [cache (as-> (Caffeine/newBuilder) builder + (if (fn? on-remove) (.removalListener builder (create-listener on-remove)) builder) + (if executor (.executor builder ^Executor (px/resolve-executor executor)) builder) + (if keepalive (.expireAfterAccess builder ^Duration (dt/duration keepalive)) builder) + (if (int? max-size) (.maximumSize builder (long max-size)) builder) + (.recordStats builder) + (.buildAsync builder)) + cache (.synchronous ^AsyncCache cache)] + (reify + ICache + (get [_ k] + (.getIfPresent ^Cache cache ^Object k)) + (get [_ k load-fn] + (.get ^Cache cache + ^Object k + ^Function (reify Function + (apply [_ k] + (load-fn k))))) + (invalidate! [_] + (.invalidateAll ^Cache cache)) + (invalidate! [_ k] + (.invalidateAll ^Cache cache ^Object k)) -(defn invalidate-all! - [^AsyncCache cache] - (.invalidateAll (.synchronous cache))) - -(defn get - ([cache key] - (assert (instance? AsyncLoadingCache cache) "should be AsyncLoadingCache instance") - (p/await! (.get ^AsyncLoadingCache cache ^Object key))) - ([cache key not-found-fn] - (assert (instance? AsyncCache cache) "should be AsyncCache instance") - (p/await! (.get ^AsyncCache cache - ^Object key - ^Function (reify - Function - (apply [_ key] - (not-found-fn key))))))) + ICacheStats + (stats [_] + (get-stats cache))))) (defn cache? [o] - (or (instance? AsyncCache o) - (instance? AsyncLoadingCache o))) + (satisfies? ICache o)) diff --git a/backend/src/app/util/events.clj b/backend/src/app/util/events.clj new file mode 100644 index 0000000000..a41843c6b1 --- /dev/null +++ b/backend/src/app/util/events.clj @@ -0,0 +1,64 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.events + "A generic asynchronous events notifications subsystem; used mainly + for mark event points in functions and be able to attach listeners + to them. Mainly used in http.sse for progress reporting." + (:refer-clojure :exclude [tap run!]) + (:require + [app.common.data.macros :as dm] + [app.common.exceptions :as ex] + [app.common.logging :as l] + [promesa.exec :as px] + [promesa.exec.csp :as sp])) + +(def ^:dynamic *channel* nil) + +(defn channel + [] + (sp/chan :buf 32)) + +(defn tap + [type data] + (when-let [channel *channel*] + (sp/put! channel [type data]) + nil)) + +(defn start-listener + [on-event on-close] + + (dm/assert! + "expected active events channel" + (sp/chan? *channel*)) + + (px/thread + {:virtual true} + (try + (loop [] + (when-let [event (sp/take! *channel*)] + (let [result (ex/try! (on-event event))] + (if (ex/exception? result) + (do + (l/wrn :hint "unexpected exception" :cause result) + (sp/close! *channel*)) + (recur))))) + (finally + (on-close))))) + +(defn run-with! + "A high-level facility for to run a function in context of event + emiter." + [f on-event] + + (binding [*channel* (sp/chan :buf 32)] + (let [listener (start-listener on-event (constantly nil))] + (try + (f) + (finally + (sp/close! *channel*) + (px/await! listener)))))) + diff --git a/backend/src/app/util/locks.clj b/backend/src/app/util/locks.clj index ad4944a570..90cbfd6eec 100644 --- a/backend/src/app/util/locks.clj +++ b/backend/src/app/util/locks.clj @@ -8,8 +8,8 @@ "A syntactic helpers for using locks." (:refer-clojure :exclude [locking]) (:import - java.util.concurrent.locks.ReentrantLock - java.util.concurrent.locks.Lock)) + java.util.concurrent.locks.Lock + java.util.concurrent.locks.ReentrantLock)) (defn create [] diff --git a/backend/src/app/util/objects_map.clj b/backend/src/app/util/objects_map.clj index eecd395e30..19a7bdea63 100644 --- a/backend/src/app/util/objects_map.clj +++ b/backend/src/app/util/objects_map.clj @@ -335,8 +335,7 @@ Iterable (iterator [this] (when-not loaded? (load! this)) - (ObjectsMapIterator. (.iterator ^Iterable positions) this)) - ) + (ObjectsMapIterator. (.iterator ^Iterable positions) this))) (defn create ([] diff --git a/backend/src/app/util/pointer_map.clj b/backend/src/app/util/pointer_map.clj index cfe79cd3a5..16ce73bb0a 100644 --- a/backend/src/app/util/pointer_map.clj +++ b/backend/src/app/util/pointer_map.clj @@ -37,7 +37,6 @@ (:require [app.common.fressian :as fres] - [app.common.logging :as l] [app.common.transit :as t] [app.common.uuid :as uuid] [app.util.time :as dt] @@ -60,10 +59,15 @@ (declare create) +(defn create-tracked + [] + (atom {})) + (defprotocol IPointerMap (get-id [_]) (load! [_]) (modified? [_]) + (loaded? [_]) (clone [_])) (deftype PointerMap [id mdata @@ -73,8 +77,6 @@ IPointerMap (load! [_] - (l/trace :hint "pointer-map:load" :id id) - (when-not *load-fn* (throw (UnsupportedOperationException. "load is not supported when *load-fn* is not bind"))) @@ -86,6 +88,7 @@ (or odata {})) (modified? [_] modified?) + (loaded? [_] loaded?) (get-id [_] id) (clone [this] @@ -162,32 +165,36 @@ (assoc [this key val] (when-not loaded? (load! this)) - (let [odata (assoc odata key val) - mdata (assoc mdata :created-at (dt/now)) - id (if modified? id (uuid/next)) - pmap (PointerMap. id - mdata - odata - true - true)] - (some-> *tracked* (swap! assoc id pmap)) - pmap)) + (let [odata' (assoc odata key val)] + (if (identical? odata odata') + this + (let [mdata (assoc mdata :created-at (dt/now)) + id (if modified? id (uuid/next)) + pmap (PointerMap. id + mdata + odata' + true + true)] + (some-> *tracked* (swap! assoc id pmap)) + pmap)))) (assocEx [_ _ _] (throw (UnsupportedOperationException. "method not implemented"))) (without [this key] (when-not loaded? (load! this)) - (let [odata (dissoc odata key) - mdata (assoc mdata :created-at (dt/now)) - id (if modified? id (uuid/next)) - pmap (PointerMap. id - mdata - odata - true - true)] - (some-> *tracked* (swap! assoc id pmap)) - pmap)) + (let [odata' (dissoc odata key)] + (if (identical? odata odata') + this + (let [mdata (assoc mdata :created-at (dt/now)) + id (if modified? id (uuid/next)) + pmap (PointerMap. id + mdata + odata' + true + true)] + (some-> *tracked* (swap! assoc id pmap)) + pmap)))) Counted (count [this] @@ -221,7 +228,15 @@ (do (some-> *tracked* (swap! assoc (get-id data) data)) data) - (into (create) data))) + (let [mdata (assoc (meta data) :created-at (dt/now)) + id (uuid/next) + pmap (PointerMap. id + mdata + data + true + true)] + (some-> *tracked* (swap! assoc id pmap)) + pmap))) (fres/add-handlers! {:name "penpot/pointer-map/v1" diff --git a/backend/src/app/util/svg.clj b/backend/src/app/util/svg.clj deleted file mode 100644 index 5647b16621..0000000000 --- a/backend/src/app/util/svg.clj +++ /dev/null @@ -1,51 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.util.svg - (:require - [app.common.data.macros :as dm] - [app.common.exceptions :as ex] - [app.common.logging :as l] - [clojure.xml :as xml] - [cuerdas.core :as str]) - (:import - javax.xml.XMLConstants - java.io.InputStream - javax.xml.parsers.SAXParserFactory - clojure.lang.XMLHandler - org.apache.commons.io.IOUtils)) - -(defn- secure-parser-factory - [^InputStream input ^XMLHandler handler] - (.. (doto (SAXParserFactory/newInstance) - (.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true) - (.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true)) - (newSAXParser) - (parse input handler))) - -(defn parse - [^String data] - (try - (dm/with-open [istream (IOUtils/toInputStream data "UTF-8")] - (xml/parse istream secure-parser-factory)) - (catch Exception e - (l/warn :hint "error on processing svg" - :message (ex-message e)) - (ex/raise :type :validation - :code :invalid-svg-file - :hint "invalid svg file" - :cause e)))) - - -;; --- PROCESSORS - -(defn strip-doctype - [data] - (cond-> data - (str/includes? data "]*>" ""))) - -(def pre-process strip-doctype) diff --git a/backend/src/app/util/time.clj b/backend/src/app/util/time.clj index 3fee8b93c2..7785966245 100644 --- a/backend/src/app/util/time.clj +++ b/backend/src/app/util/time.clj @@ -123,7 +123,6 @@ FileTime (inst-ms* [v] (.toMillis ^FileTime v))) - (defmethod print-method Duration [mv ^java.io.Writer writer] (.write writer (str "#app/duration \"" (str/lower (subs (str mv) 2)) "\""))) @@ -209,9 +208,16 @@ ([v] (.format DateTimeFormatter/ISO_INSTANT ^Instant v)) ([v fmt] (case fmt - :iso (.format DateTimeFormatter/ISO_INSTANT ^Instant v) - :rfc1123 (.format DateTimeFormatter/RFC_1123_DATE_TIME - ^ZonedDateTime (instant->zoned-date-time v))))) + :iso + (.format DateTimeFormatter/ISO_INSTANT ^Instant v) + + :iso-local-time + (.format DateTimeFormatter/ISO_LOCAL_TIME + ^ZonedDateTime (instant->zoned-date-time v)) + + :rfc1123 + (.format DateTimeFormatter/RFC_1123_DATE_TIME + ^ZonedDateTime (instant->zoned-date-time v))))) (defmethod print-method Instant [mv ^java.io.Writer writer] @@ -371,8 +377,7 @@ ::sm/decode instant :gen/gen (tgen/fmap (fn [i] (in-past i)) tgen/pos-int) ::oapi/type "string" - ::oapi/format "iso" - }}) + ::oapi/format "iso"}}) (sm/def! ::duration {:type :durations @@ -383,5 +388,4 @@ :title "duration" ::sm/decode duration ::oapi/type "string" - ::oapi/format "duration" - }}) + ::oapi/format "duration"}}) diff --git a/backend/src/app/util/websocket.clj b/backend/src/app/util/websocket.clj index 1b8e165609..70d8eb4062 100644 --- a/backend/src/app/util/websocket.clj +++ b/backend/src/app/util/websocket.clj @@ -15,8 +15,9 @@ [app.util.time :as dt] [promesa.exec :as px] [promesa.exec.csp :as sp] - [yetti.request :as yr] - [yetti.util :as yu] + [promesa.util :as pu] + [ring.request :as rreq] + [ring.websocket :as rws] [yetti.websocket :as yws]) (:import java.nio.ByteBuffer)) @@ -50,7 +51,7 @@ (declare start-io-loop!) -(defn handler +(defn listener "A WebSocket upgrade handler factory. Returns a handler that can be used to upgrade to websocket connection. This handler implements the basic custom protocol on top of websocket connection with all the @@ -61,166 +62,158 @@ It also accepts some options that allows you parametrize the protocol behavior. The options map will be used as-as for the initial data of the `ws` data structure" - [& {:keys [::on-rcv-message - ::on-snd-message - ::on-connect - ::input-buff-size - ::output-buff-size - ::idle-timeout] - :or {input-buff-size 64 - output-buff-size 64 - idle-timeout 60000 - on-connect identity - on-snd-message identity-3 - on-rcv-message identity-3} - :as options}] + [request & {:keys [::on-rcv-message + ::on-snd-message + ::on-connect + ::input-buff-size + ::output-buff-size + ::idle-timeout] + :or {input-buff-size 64 + output-buff-size 64 + idle-timeout 60000 + on-connect identity + on-snd-message identity-3 + on-rcv-message identity-3} + :as options}] (assert (fn? on-rcv-message) "'on-rcv-message' should be a function") (assert (fn? on-snd-message) "'on-snd-message' should be a function") (assert (fn? on-connect) "'on-connect' should be a function") - (fn [{:keys [::yws/channel] :as request}] - (let [input-ch (sp/chan :buf input-buff-size) - output-ch (sp/chan :buf output-buff-size) - hbeat-ch (sp/chan :buf (sp/sliding-buffer 6)) - close-ch (sp/chan) + (let [input-ch (sp/chan :buf input-buff-size) + output-ch (sp/chan :buf output-buff-size) + hbeat-ch (sp/chan :buf (sp/sliding-buffer 6)) + close-ch (sp/chan) + ip-addr (parse-client-ip request) + uagent (rreq/get-header request "user-agent") + id (uuid/next) + state (atom {}) + beats (atom #{}) + options (-> options + (update ::handler wrap-handler) + (assoc ::id id) + (assoc ::state state) + (assoc ::beats beats) + (assoc ::created-at (dt/now)) + (assoc ::input-ch input-ch) + (assoc ::heartbeat-ch hbeat-ch) + (assoc ::output-ch output-ch) + (assoc ::close-ch close-ch) + (assoc ::remote-addr ip-addr) + (assoc ::user-agent uagent))] - ip-addr (parse-client-ip request) - uagent (yr/get-header request "user-agent") - id (uuid/next) - state (atom {}) - beats (atom #{}) - - options (-> options - (update ::handler wrap-handler) - (assoc ::id id) - (assoc ::state state) - (assoc ::beats beats) - (assoc ::created-at (dt/now)) - (assoc ::input-ch input-ch) - (assoc ::heartbeat-ch hbeat-ch) - (assoc ::output-ch output-ch) - (assoc ::close-ch close-ch) + {:on-open + (fn on-open [channel] + (l/dbg :fn "on-open" :conn-id (str id)) + (let [options (-> options (assoc ::channel channel) - (assoc ::remote-addr ip-addr) - (assoc ::user-agent uagent) (on-connect)) + timeout (dt/duration idle-timeout)] - on-ws-open - (fn [channel] - (l/trace :fn "on-ws-open" :conn-id id) - (let [timeout (dt/duration idle-timeout) - name (str "penpot/websocket/io-loop/" id)] - (yws/idle-timeout! channel timeout) - (px/fn->thread (partial start-io-loop! options) - {:name name :virtual true}))) + (yws/set-idle-timeout! channel timeout) + (px/submit! :vthread (partial start-io-loop! options)))) - on-ws-terminate - (fn [_ code reason] - (l/trace :fn "on-ws-terminate" - :conn-id id - :code code - :reason reason) - (sp/close! close-ch)) + :on-close + (fn on-close [_channel code reason] + (l/dbg :fn "on-close" + :conn-id (str id) + :code code + :reason reason) + (sp/close! close-ch)) - on-ws-error - (fn [_ cause] - (sp/close! close-ch cause)) + :on-error + (fn on-error [_channel cause] + (sp/close! close-ch cause)) - on-ws-message - (fn [_ message] - (sp/offer! input-ch message) - (swap! state assoc ::last-activity-at (dt/now))) + :on-message + (fn on-message [_channel message] + (when (string? message) + (sp/offer! input-ch message) + (swap! state assoc ::last-activity-at (dt/now)))) - on-ws-pong - (fn [_ buffers] - ;; (l/trace :fn "on-ws-pong" :buffers (pr-str buffers)) - (sp/put! hbeat-ch (yu/copy-many buffers)))] - - (yws/on-close! channel (fn [_] - (sp/close! close-ch))) - - {:on-open on-ws-open - :on-error on-ws-error - :on-close on-ws-terminate - :on-text on-ws-message - :on-pong on-ws-pong}))) + :on-pong + (fn on-pong [_channel data] + (sp/put! hbeat-ch data))})) (defn- handle-ping! [{:keys [::id ::beats ::channel] :as wsp} beat-id] - (l/trace :hint "ping" :beat beat-id :conn-id id) - (yws/ping! channel (encode-beat beat-id)) + (l/trc :hint "send ping" :beat beat-id :conn-id (str id)) + (rws/ping channel (encode-beat beat-id)) (let [issued (swap! beats conj (long beat-id))] (not (>= (count issued) max-missed-heartbeats)))) (defn- start-io-loop! - [{:keys [::id ::close-ch ::input-ch ::output-ch ::heartbeat-ch ::channel ::handler ::beats ::on-rcv-message ::on-snd-message] :as wsp}] - (px/thread - {:name (str "penpot/websocket/io-loop/" id) - :virtual true} - (try - (handler wsp {:type :open}) - (loop [i 0] - (let [ping-ch (sp/timeout-chan heartbeat-interval) - [msg p] (sp/alts! [close-ch input-ch output-ch heartbeat-ch ping-ch])] - (when (yws/connected? channel) - (cond - (identical? p ping-ch) - (if (handle-ping! wsp i) - (recur (inc i)) - (yws/close! channel 8802 "missing to many pings")) + [{:keys [::id ::close-ch ::input-ch ::output-ch ::heartbeat-ch + ::channel ::handler ::beats ::on-rcv-message ::on-snd-message] + :as wsp}] + (try + (handler wsp {:type :open}) + (loop [i 0] + (let [ping-ch (sp/timeout-chan heartbeat-interval) + [msg p] (sp/alts! [close-ch input-ch output-ch heartbeat-ch ping-ch])] + (when (rws/open? channel) + (cond + (identical? p ping-ch) + (if (handle-ping! wsp i) + (recur (inc i)) + (do + (l/trc :hint "closing" :reason "missing to many pings") + (rws/close channel 8802 "missing to many pings"))) - (or (identical? p close-ch) (nil? msg)) - (do :nothing) + (or (identical? p close-ch) (nil? msg)) + (do :nothing) - (identical? p heartbeat-ch) - (let [beat (decode-beat msg)] - ;; (l/trace :hint "pong" :beat beat :conn-id id) - (swap! beats disj beat) - (recur i)) + (identical? p heartbeat-ch) + (let [beat (decode-beat msg)] + (l/trc :hint "pong received" :beat beat :conn-id (str id)) + (swap! beats disj beat) + (recur i)) - (identical? p input-ch) - (let [message (t/decode-str msg) - message (on-rcv-message message) - {:keys [request-id] :as response} (handler wsp message)] - (when (map? response) - (sp/put! output-ch - (cond-> response - (some? request-id) - (assoc :request-id request-id)))) - (recur i)) + (identical? p input-ch) + (let [message (t/decode-str msg) + message (on-rcv-message message) + {:keys [request-id] :as response} (handler wsp message)] + (when (map? response) + (sp/put! output-ch + (cond-> response + (some? request-id) + (assoc :request-id request-id)))) + (recur i)) - (identical? p output-ch) - (let [message (on-snd-message msg) - message (t/encode-str message {:type :json-verbose})] - ;; (l/trace :hint "writing message to output" :message msg) - (yws/send! channel message) - (recur i)))))) + (identical? p output-ch) + (let [message (on-snd-message msg) + message (t/encode-str message {:type :json-verbose})] + (rws/send channel message) + (recur i)))))) - (catch java.nio.channels.ClosedChannelException _) - (catch java.net.SocketException _) - (catch java.io.IOException _) + (catch InterruptedException _cause + (l/dbg :hint "websocket thread interrumpted" :conn-id id)) - (catch InterruptedException _ - (l/debug :hint "websocket thread interrumpted" :conn-id id)) - - (catch Throwable cause - (l/error :hint "unhandled exception on websocket thread" + (catch Throwable cause + (let [cause (pu/unwrap-exception cause)] + (if (or (instance? java.nio.channels.ClosedChannelException cause) + (instance? java.net.SocketException cause) + (instance? java.io.IOException cause)) + nil + (l/err :hint "unhandled exception on websocket thread" :conn-id id - :cause cause)) - - (finally + :cause cause)))) + (finally + (try (handler wsp {:type :close}) - (when (yws/connected? channel) + (when (rws/open? channel) ;; NOTE: we need to ignore all exceptions here because ;; there can be a race condition that first returns that ;; channel is connected but on closing, will raise that ;; channel is already closed. (ex/ignoring - (yws/close! channel 8899 "terminated"))) + (rws/close channel 8899 "terminated"))) (when-let [on-disconnect (::on-disconnect wsp)] (on-disconnect)) - (l/trace :hint "websocket thread terminated" :conn-id id))))) + (catch Throwable cause + (throw cause))) + + (l/trc :hint "websocket thread terminated" :conn-id id)))) diff --git a/backend/src/app/worker.clj b/backend/src/app/worker.clj index 448fbaaec1..a648080f3b 100644 --- a/backend/src/app/worker.clj +++ b/backend/src/app/worker.clj @@ -8,67 +8,25 @@ "Async tasks abstraction (impl)." (:require [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.spec :as us] - [app.common.transit :as t] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] [app.metrics :as mtx] - [app.redis :as rds] [app.util.time :as dt] [clojure.spec.alpha :as s] [cuerdas.core :as str] - [integrant.core :as ig] - [promesa.core :as p] - [promesa.exec :as px]) - (:import - java.util.concurrent.ExecutorService - java.util.concurrent.ForkJoinPool - java.util.concurrent.Future)) + [integrant.core :as ig])) (set! *warn-on-reflection* true) -(s/def ::executor #(instance? ExecutorService %)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Executor -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(s/def ::parallelism ::us/integer) - -(defmethod ig/pre-init-spec ::executor [_] - (s/keys :req [::parallelism])) - -(defmethod ig/init-key ::executor - [skey {:keys [::parallelism]}] - (let [prefix (if (vector? skey) (-> skey first name) "default") - tname (str "penpot/" prefix "/%s") - ttype (cf/get :worker-executor-type :fjoin)] - (case ttype - :fjoin - (let [factory (px/forkjoin-thread-factory :name tname)] - (px/forkjoin-executor {:factory factory - :core-size (px/get-available-processors) - :parallelism parallelism - :async true})) - - :cached - (let [factory (px/thread-factory :name tname)] - (px/cached-executor :factory factory))))) - -(defmethod ig/halt-key! ::executor - [_ instance] - (px/shutdown! instance)) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; TASKS REGISTRY ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn- wrap-task-handler - [metrics tname f] +(defn- wrap-with-metrics + [f metrics tname] (let [labels (into-array String [tname])] (fn [params] (let [tp (dt/tpoint)] @@ -81,6 +39,7 @@ :labels labels}))))))) (s/def ::registry (s/map-of ::us/string fn?)) +(s/def ::tasks (s/map-of keyword? fn?)) (defmethod ig/pre-init-spec ::registry [_] (s/keys :req [::mtx/metrics ::tasks])) @@ -88,541 +47,13 @@ (defmethod ig/init-key ::registry [_ {:keys [::mtx/metrics ::tasks]}] (l/inf :hint "registry initialized" :tasks (count tasks)) - (reduce-kv (fn [registry k v] + (reduce-kv (fn [registry k f] (let [tname (name k)] (l/trc :hint "register task" :name tname) - (assoc registry tname (wrap-task-handler metrics tname v)))) + (assoc registry tname (wrap-with-metrics f metrics tname)))) {} tasks)) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; EXECUTOR MONITOR -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(s/def ::name ::us/keyword) - -(defmethod ig/pre-init-spec ::monitor [_] - (s/keys :req [::name ::executor ::mtx/metrics])) - -(defmethod ig/prep-key ::monitor - [_ cfg] - (merge {::interval (dt/duration "2s")} - (d/without-nils cfg))) - -(defmethod ig/init-key ::monitor - [_ {:keys [::executor ::mtx/metrics ::interval ::name]}] - (letfn [(monitor! [^ForkJoinPool executor prev-steals] - (let [running (.getRunningThreadCount executor) - queued (.getQueuedSubmissionCount executor) - active (.getPoolSize executor) - steals (.getStealCount executor) - labels (into-array String [(d/name name)]) - - steals-inc (- steals prev-steals) - steals-inc (if (neg? steals-inc) 0 steals-inc)] - - (mtx/run! metrics - :id :executor-active-threads - :labels labels - :val active) - (mtx/run! metrics - :id :executor-running-threads - :labels labels :val running) - (mtx/run! metrics - :id :executors-queued-submissions - :labels labels - :val queued) - (mtx/run! metrics - :id :executors-completed-tasks - :labels labels - :inc steals-inc) - - steals))] - - (px/thread - {:name "penpot/executors-monitor" :virtual true} - (l/inf :hint "monitor: started" :name name) - (try - (loop [steals 0] - (when-not (px/shutdown? executor) - (px/sleep interval) - (recur (long (monitor! executor steals))))) - (catch InterruptedException _cause - (l/trc :hint "monitor: interrupted" :name name)) - (catch Throwable cause - (l/err :hint "monitor: unexpected error" :name name :cause cause)) - (finally - (l/inf :hint "monitor: terminated" :name name)))))) - -(defmethod ig/halt-key! ::monitor - [_ thread] - (px/interrupt! thread)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; SCHEDULER -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- decode-task-row - [{:keys [props] :as row}] - (cond-> row - (db/pgobject? props) - (assoc :props (db/decode-transit-pgobject props)))) - -(s/def ::wait-duration ::dt/duration) - -(defmethod ig/pre-init-spec ::dispatcher [_] - (s/keys :req [::mtx/metrics - ::db/pool - ::rds/redis] - :opt [::wait-duration - ::batch-size])) - -(defmethod ig/prep-key ::dispatcher - [_ cfg] - (merge {::batch-size 100 - ::wait-duration (dt/duration "5s")} - (d/without-nils cfg))) - -(def ^:private sql:select-next-tasks - "select id, queue from task as t - where t.scheduled_at <= now() - and (t.status = 'new' or t.status = 'retry') - and queue ~~* ?::text - order by t.priority desc, t.scheduled_at - limit ? - for update skip locked") - -(defmethod ig/init-key ::dispatcher - [_ {:keys [::db/pool ::rds/redis ::batch-size] :as cfg}] - (letfn [(get-tasks [conn] - (let [prefix (str (cf/get :tenant) ":%")] - (seq (db/exec! conn [sql:select-next-tasks prefix batch-size])))) - - (push-tasks! [conn rconn [queue tasks]] - (let [ids (mapv :id tasks) - key (str/ffmt "taskq:%" queue) - res (rds/rpush! rconn key (mapv t/encode ids)) - sql [(str "update task set status = 'scheduled'" - " where id = ANY(?)") - (db/create-array conn "uuid" ids)]] - - (db/exec-one! conn sql) - (l/trc :hist "dispatcher: queue tasks" - :queue queue - :tasks (count ids) - :queued res))) - - (run-batch! [rconn] - (try - (db/with-atomic [conn pool] - (if-let [tasks (get-tasks conn)] - (->> (group-by :queue tasks) - (run! (partial push-tasks! conn rconn))) - (px/sleep (::wait-duration cfg)))) - (catch InterruptedException cause - (throw cause)) - (catch Exception cause - (cond - (rds/exception? cause) - (do - (l/wrn :hint "dispatcher: redis exception (will retry in an instant)" :cause cause) - (px/sleep (::rds/timeout rconn))) - - (db/sql-exception? cause) - (do - (l/wrn :hint "dispatcher: database exception (will retry in an instant)" :cause cause) - (px/sleep (::rds/timeout rconn))) - - :else - (do - (l/err :hint "dispatcher: unhandled exception (will retry in an instant)" :cause cause) - (px/sleep (::rds/timeout rconn))))))) - - (dispatcher [] - (l/inf :hint "dispatcher: started") - (try - (dm/with-open [rconn (rds/connect redis)] - (loop [] - (run-batch! rconn) - (recur))) - (catch InterruptedException _ - (l/trc :hint "dispatcher: interrupted")) - (catch Throwable cause - (l/err :hint "dispatcher: unexpected exception" :cause cause)) - (finally - (l/inf :hint "dispatcher: terminated"))))] - - (if (db/read-only? pool) - (l/wrn :hint "dispatcher: not started (db is read-only)") - (px/fn->thread dispatcher :name "penpot/worker/dispatcher" :virtual true)))) - -(defmethod ig/halt-key! ::dispatcher - [_ thread] - (some-> thread px/interrupt!)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; WORKER -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(declare ^:private run-worker-loop!) -(declare ^:private start-worker!) -(declare ^:private get-error-context) - -(defmethod ig/pre-init-spec ::worker [_] - (s/keys :req [::parallelism - ::mtx/metrics - ::db/pool - ::rds/redis - ::queue - ::registry])) - -(defmethod ig/prep-key ::worker - [_ cfg] - (merge {::parallelism 1} - (d/without-nils cfg))) - -(defmethod ig/init-key ::worker - [_ {:keys [::db/pool ::queue ::parallelism] :as cfg}] - (let [queue (d/name queue) - cfg (assoc cfg ::queue queue)] - (if (db/read-only? pool) - (l/wrn :hint "worker: not started (db is read-only)" :queue queue :parallelism parallelism) - (doall - (->> (range parallelism) - (map #(assoc cfg ::worker-id %)) - (map start-worker!)))))) - -(defmethod ig/halt-key! ::worker - [_ threads] - (run! px/interrupt! threads)) - -(defn- start-worker! - [{:keys [::rds/redis ::worker-id ::queue] :as cfg}] - (px/thread - {:name (format "penpot/worker/runner:%s" worker-id)} - (l/inf :hint "worker: started" :worker-id worker-id :queue queue) - (try - (dm/with-open [rconn (rds/connect redis)] - (let [tenant (cf/get :tenant "main") - cfg (-> cfg - (assoc ::queue (str/ffmt "taskq:%:%" tenant queue)) - (assoc ::rds/rconn rconn) - (assoc ::timeout (dt/duration "5s")))] - (loop [] - (when (px/interrupted?) - (throw (InterruptedException. "interrupted"))) - - (run-worker-loop! cfg) - (recur)))) - - (catch InterruptedException _ - (l/debug :hint "worker: interrupted" - :worker-id worker-id - :queue queue)) - (catch Throwable cause - (l/err :hint "worker: unexpected exception" - :worker-id worker-id - :queue queue - :cause cause)) - (finally - (l/inf :hint "worker: terminated" - :worker-id worker-id - :queue queue))))) - -(defn- run-worker-loop! - [{:keys [::db/pool ::rds/rconn ::timeout ::queue ::registry ::worker-id]}] - (letfn [(handle-task-retry [{:keys [task error inc-by delay] :or {inc-by 1 delay 1000}}] - (let [explain (ex-message error) - nretry (+ (:retry-num task) inc-by) - now (dt/now) - delay (->> (iterate #(* % 2) delay) (take nretry) (last))] - (db/update! pool :task - {:error explain - :status "retry" - :modified-at now - :scheduled-at (dt/plus now delay) - :retry-num nretry} - {:id (:id task)}) - nil)) - - (handle-task-failure [{:keys [task error]}] - (let [explain (ex-message error)] - (db/update! pool :task - {:error explain - :modified-at (dt/now) - :status "failed"} - {:id (:id task)}) - nil)) - - (handle-task-completion [{:keys [task]}] - (let [now (dt/now)] - (db/update! pool :task - {:completed-at now - :modified-at now - :status "completed"} - {:id (:id task)}) - nil)) - - (decode-payload [^bytes payload] - (try - (let [task-id (t/decode payload)] - (if (uuid? task-id) - task-id - (l/err :hint "worker: received unexpected payload (uuid expected)" - :payload task-id))) - (catch Throwable cause - (l/err :hint "worker: unable to decode payload" - :payload payload - :length (alength payload) - :cause cause)))) - - (handle-task [{:keys [name] :as task}] - (let [task-fn (get registry name)] - (if task-fn - (task-fn task) - (l/wrn :hint "no task handler found" :name name)) - {:status :completed :task task})) - - (handle-task-exception [cause task] - (let [edata (ex-data cause)] - (if (and (< (:retry-num task) - (:max-retries task)) - (= ::retry (:type edata))) - (cond-> {:status :retry :task task :error cause} - (dt/duration? (:delay edata)) - (assoc :delay (:delay edata)) - - (= ::noop (:strategy edata)) - (assoc :inc-by 0)) - (do - (l/err :hint "worker: unhandled exception on task" - ::l/context (get-error-context cause task) - :cause cause) - (if (>= (:retry-num task) (:max-retries task)) - {:status :failed :task task :error cause} - {:status :retry :task task :error cause}))))) - - (get-task [task-id] - (ex/try! - (some-> (db/get* pool :task {:id task-id}) - (decode-task-row)))) - - (run-task [task-id] - (loop [task (get-task task-id)] - (cond - (ex/exception? task) - (if (or (db/connection-error? task) - (db/serialization-error? task)) - (do - (l/wrn :hint "worker: connection error on retrieving task from database (retrying in some instants)" - :worker-id worker-id - :cause task) - (px/sleep (::rds/timeout rconn)) - (recur (get-task task-id))) - (do - (l/err :hint "worker: unhandled exception on retrieving task from database (retrying in some instants)" - :worker-id worker-id - :cause task) - (px/sleep (::rds/timeout rconn)) - (recur (get-task task-id)))) - - (nil? task) - (l/wrn :hint "worker: no task found on the database" - :worker-id worker-id - :task-id task-id) - - :else - (try - (l/trc :hint "executing task" - :name (:name task) - :id (str (:id task)) - :queue queue - :worker-id worker-id - :retry (:retry-num task)) - (handle-task task) - (catch InterruptedException cause - (throw cause)) - (catch Throwable cause - (handle-task-exception cause task)))))) - - (process-result [{:keys [status] :as result}] - (ex/try! - (case status - :retry (handle-task-retry result) - :failed (handle-task-failure result) - :completed (handle-task-completion result) - nil))) - - (run-task-loop [task-id] - (loop [result (run-task task-id)] - (when-let [cause (process-result result)] - (if (or (db/connection-error? cause) - (db/serialization-error? cause)) - (do - (l/wrn :hint "worker: database exeption on processing task result (retrying in some instants)" - :cause cause) - (px/sleep (::rds/timeout rconn)) - (recur result)) - (do - (l/err :hint "worker: unhandled exception on processing task result (retrying in some instants)" - :cause cause) - (px/sleep (::rds/timeout rconn)) - (recur result))))))] - - (try - (let [[_ payload] (rds/blpop! rconn timeout queue)] - (some-> payload - decode-payload - run-task-loop)) - - (catch InterruptedException cause - (throw cause)) - - (catch Exception cause - (if (rds/timeout-exception? cause) - (do - (l/err :hint "worker: redis pop operation timeout, consider increasing redis timeout (will retry in some instants)" - :timeout timeout - :cause cause) - (px/sleep timeout)) - - (l/err :hint "worker: unhandled exception" :cause cause)))))) - -(defn- get-error-context - [_ item] - {:params item}) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; CRON -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(declare schedule-cron-task) -(declare synchronize-cron-entries!) - -(s/def ::fn (s/or :var var? :fn fn?)) -(s/def ::id keyword?) -(s/def ::cron dt/cron?) -(s/def ::props (s/nilable map?)) -(s/def ::task keyword?) - -(s/def ::cron-task - (s/keys :req-un [::cron ::task] - :opt-un [::props ::id])) - -(s/def ::entries (s/coll-of (s/nilable ::cron-task))) - -(defmethod ig/pre-init-spec ::cron [_] - (s/keys :req [::db/pool ::entries ::registry])) - -(defmethod ig/init-key ::cron - [_ {:keys [::entries ::registry ::db/pool] :as cfg}] - (if (db/read-only? pool) - (l/wrn :hint "cron: not started (db is read-only)") - (let [running (atom #{}) - entries (->> entries - (filter some?) - ;; If id is not defined, use the task as id. - (map (fn [{:keys [id task] :as item}] - (if (some? id) - (assoc item :id (d/name id)) - (assoc item :id (d/name task))))) - (map (fn [item] - (update item :task d/name))) - (map (fn [{:keys [task] :as item}] - (let [f (get registry task)] - (when-not f - (ex/raise :type :internal - :code :task-not-found - :hint (str/fmt "task %s not configured" task))) - (-> item - (dissoc :task) - (assoc :fn f)))))) - - cfg (assoc cfg ::entries entries ::running running)] - - (l/inf :hint "cron: started" :tasks (count entries)) - (synchronize-cron-entries! cfg) - - (->> (filter some? entries) - (run! (partial schedule-cron-task cfg))) - - (reify - clojure.lang.IDeref - (deref [_] @running) - - java.lang.AutoCloseable - (close [_] - (l/inf :hint "cron: terminated") - (doseq [item @running] - (when-not (.isDone ^Future item) - (.cancel ^Future item true)))))))) - -(defmethod ig/halt-key! ::cron - [_ instance] - (some-> instance d/close!)) - -(def sql:upsert-cron-task - "insert into scheduled_task (id, cron_expr) - values (?, ?) - on conflict (id) - do update set cron_expr=?") - -(defn- synchronize-cron-entries! - [{:keys [::db/pool ::entries]}] - (db/with-atomic [conn pool] - (doseq [{:keys [id cron]} entries] - (l/trc :hint "register cron task" :id id :cron (str cron)) - (db/exec-one! conn [sql:upsert-cron-task id (str cron) (str cron)])))) - -(defn- lock-scheduled-task! - [conn id] - (let [sql (str "SELECT id FROM scheduled_task " - " WHERE id=? FOR UPDATE SKIP LOCKED")] - (some? (db/exec-one! conn [sql (d/name id)])))) - -(defn- execute-cron-task - [{:keys [::db/pool] :as cfg} {:keys [id] :as task}] - (px/thread - {:name (str "penpot/cront-task/" id)} - (try - (db/with-atomic [conn pool] - (db/exec-one! conn ["SET statement_timeout=0;"]) - (db/exec-one! conn ["SET idle_in_transaction_session_timeout=0;"]) - (when (lock-scheduled-task! conn id) - (l/dbg :hint "cron: execute task" :task-id id) - ((:fn task) task)) - (db/rollback! conn)) - - (catch InterruptedException _ - (l/debug :hint "cron: task interrupted" :task-id id)) - - (catch Throwable cause - (binding [l/*context* (get-error-context cause task)] - (l/err :hint "cron: unhandled exception on running task" - :task-id id - :cause cause))) - (finally - (when-not (px/interrupted? :current) - (schedule-cron-task cfg task)))))) - -(defn- ms-until-valid - [cron] - (s/assert dt/cron? cron) - (let [now (dt/now) - next (dt/next-valid-instant-from cron now)] - (dt/diff now next))) - -(defn- schedule-cron-task - [{:keys [::running] :as cfg} {:keys [cron id] :as task}] - (let [ts (ms-until-valid cron) - ft (px/schedule! ts (partial execute-cron-task cfg task))] - - (l/dbg :hint "cron: schedule task" :task-id id - :ts (dt/format-duration ts) - :at (dt/format-instant (dt/in-future ts))) - (swap! running #(into #{ft} (filter p/pending?) %)))) - - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SUBMIT API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -645,8 +76,12 @@ (def ^:private sql:remove-not-started-tasks - "delete from task - where name=? and queue=? and label=? and status = 'new' and scheduled_at > now()") + "DELETE FROM task + WHERE name=? + AND queue=? + AND label=? + AND status = 'new' + AND scheduled_at > now()") (s/def ::label string?) (s/def ::task (s/or :kw keyword? :str string?)) @@ -670,6 +105,7 @@ [& {:keys [::task ::delay ::queue ::priority ::max-retries ::conn ::dedupe ::label] :or {delay 0 queue :default priority 100 max-retries 3 label ""} :as options}] + (us/verify! ::submit-options options) (let [duration (dt/duration delay) interval (db/interval duration) @@ -681,7 +117,6 @@ deleted (when dedupe (-> (db/exec-one! conn [sql:remove-not-started-tasks task queue label]) :next.jdbc/update-count))] - (l/trc :hint "submit task" :name task :queue queue diff --git a/backend/src/app/worker/cron.clj b/backend/src/app/worker/cron.clj new file mode 100644 index 0000000000..689fcba90d --- /dev/null +++ b/backend/src/app/worker/cron.clj @@ -0,0 +1,157 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.worker.cron + (:require + [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.db :as db] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [app.worker.runner :refer [get-error-context]] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [integrant.core :as ig] + [promesa.core :as p] + [promesa.exec :as px]) + (:import + java.util.concurrent.Future)) + +(set! *warn-on-reflection* true) + +(def sql:upsert-cron-task + "insert into scheduled_task (id, cron_expr) + values (?, ?) + on conflict (id) + do update set cron_expr=?") + +(defn- synchronize-cron-entries! + [{:keys [::db/pool ::entries]}] + (db/with-atomic [conn pool] + (doseq [{:keys [id cron]} entries] + (l/trc :hint "register cron task" :id id :cron (str cron)) + (db/exec-one! conn [sql:upsert-cron-task id (str cron) (str cron)])))) + +(defn- lock-scheduled-task! + [conn id] + (let [sql (str "SELECT id FROM scheduled_task " + " WHERE id=? FOR UPDATE SKIP LOCKED")] + (some? (db/exec-one! conn [sql (d/name id)])))) + +(declare ^:private schedule-cron-task) + +(defn- execute-cron-task + [cfg {:keys [id] :as task}] + (px/thread + {:name (str "penpot/cron-task/" id)} + (let [tpoint (dt/tpoint)] + (try + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (db/exec-one! conn ["SET LOCAL statement_timeout=0;"]) + (db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout=0;"]) + (when (lock-scheduled-task! conn id) + (l/dbg :hint "start task" :task-id id) + ((:fn task) task) + (let [elapsed (dt/format-duration (tpoint))] + (l/dbg :hint "end task" :task-id id :elapsed elapsed))))) + + (catch InterruptedException _ + (let [elapsed (dt/format-duration (tpoint))] + (l/debug :hint "task interrupted" :task-id id :elapsed elapsed))) + + (catch Throwable cause + (let [elapsed (dt/format-duration (tpoint))] + (binding [l/*context* (get-error-context cause task)] + (l/err :hint "unhandled exception on running task" + :task-id id + :elapsed elapsed + :cause cause)))) + (finally + (when-not (px/interrupted? :current) + (schedule-cron-task cfg task))))))) + +(defn- ms-until-valid + [cron] + (s/assert dt/cron? cron) + (let [now (dt/now) + next (dt/next-valid-instant-from cron now)] + (dt/diff now next))) + +(defn- schedule-cron-task + [{:keys [::running] :as cfg} {:keys [cron id] :as task}] + (let [ts (ms-until-valid cron) + ft (px/schedule! ts (partial execute-cron-task cfg task))] + + (l/dbg :hint "schedule task" :task-id id + :ts (dt/format-duration ts) + :at (dt/format-instant (dt/in-future ts))) + + (swap! running #(into #{ft} (filter p/pending?) %)))) + + +(s/def ::fn (s/or :var var? :fn fn?)) +(s/def ::id keyword?) +(s/def ::cron dt/cron?) +(s/def ::props (s/nilable map?)) +(s/def ::task keyword?) + +(s/def ::task-item + (s/keys :req-un [::cron ::task] + :opt-un [::props ::id])) + +(s/def ::wrk/entries (s/coll-of (s/nilable ::task-item))) + +(defmethod ig/pre-init-spec ::wrk/cron [_] + (s/keys :req [::db/pool ::wrk/entries ::wrk/registry])) + +(defmethod ig/init-key ::wrk/cron + [_ {:keys [::wrk/entries ::wrk/registry ::db/pool] :as cfg}] + (if (db/read-only? pool) + (l/wrn :hint "service not started (db is read-only)") + (let [running (atom #{}) + entries (->> entries + (filter some?) + ;; If id is not defined, use the task as id. + (map (fn [{:keys [id task] :as item}] + (if (some? id) + (assoc item :id (d/name id)) + (assoc item :id (d/name task))))) + (map (fn [item] + (update item :task d/name))) + (map (fn [{:keys [task] :as item}] + (let [f (get registry task)] + (when-not f + (ex/raise :type :internal + :code :task-not-found + :hint (str/fmt "task %s not configured" task))) + (-> item + (dissoc :task) + (assoc :fn f)))))) + + cfg (assoc cfg ::entries entries ::running running)] + + (l/inf :hint "started" :tasks (count entries)) + (synchronize-cron-entries! cfg) + + (->> (filter some? entries) + (run! (partial schedule-cron-task cfg))) + + (reify + clojure.lang.IDeref + (deref [_] @running) + + java.lang.AutoCloseable + (close [_] + (l/inf :hint "terminated") + (doseq [item @running] + (when-not (.isDone ^Future item) + (.cancel ^Future item true)))))))) + +(defmethod ig/halt-key! ::wrk/cron + [_ instance] + (some-> instance d/close!)) + diff --git a/backend/src/app/worker/dispatcher.clj b/backend/src/app/worker/dispatcher.clj new file mode 100644 index 0000000000..dbdb060427 --- /dev/null +++ b/backend/src/app/worker/dispatcher.clj @@ -0,0 +1,110 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.worker.dispatcher + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.logging :as l] + [app.common.transit :as t] + [app.config :as cf] + [app.db :as db] + [app.metrics :as mtx] + [app.redis :as rds] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [integrant.core :as ig] + [promesa.exec :as px])) + +(set! *warn-on-reflection* true) + +(defmethod ig/pre-init-spec ::wrk/dispatcher [_] + (s/keys :req [::mtx/metrics ::db/pool ::rds/redis])) + +(defmethod ig/prep-key ::wrk/dispatcher + [_ cfg] + (merge {::batch-size 100 + ::wait-duration (dt/duration "5s")} + (d/without-nils cfg))) + +(def ^:private sql:select-next-tasks + "select id, queue from task as t + where t.scheduled_at <= now() + and (t.status = 'new' or t.status = 'retry') + and queue ~~* ?::text + order by t.priority desc, t.scheduled_at + limit ? + for update skip locked") + +(defmethod ig/init-key ::wrk/dispatcher + [_ {:keys [::db/pool ::rds/redis ::batch-size] :as cfg}] + (letfn [(get-tasks [conn] + (let [prefix (str (cf/get :tenant) ":%")] + (seq (db/exec! conn [sql:select-next-tasks prefix batch-size])))) + + (push-tasks! [conn rconn [queue tasks]] + (let [ids (mapv :id tasks) + key (str/ffmt "taskq:%" queue) + res (rds/rpush! rconn key (mapv t/encode ids)) + sql [(str "update task set status = 'scheduled'" + " where id = ANY(?)") + (db/create-array conn "uuid" ids)]] + + (db/exec-one! conn sql) + (l/trc :hist "queue tasks" + :queue queue + :tasks (count ids) + :queued res))) + + (run-batch! [rconn] + (try + (db/with-atomic [conn pool] + (if-let [tasks (get-tasks conn)] + (->> (group-by :queue tasks) + (run! (partial push-tasks! conn rconn))) + (px/sleep (::wait-duration cfg)))) + (catch InterruptedException cause + (throw cause)) + (catch Exception cause + (cond + (rds/exception? cause) + (do + (l/wrn :hint "redis exception (will retry in an instant)" :cause cause) + (px/sleep (::rds/timeout rconn))) + + (db/sql-exception? cause) + (do + (l/wrn :hint "database exception (will retry in an instant)" :cause cause) + (px/sleep (::rds/timeout rconn))) + + :else + (do + (l/err :hint "unhandled exception (will retry in an instant)" :cause cause) + (px/sleep (::rds/timeout rconn))))))) + + (dispatcher [] + (l/inf :hint "started") + (try + (dm/with-open [rconn (rds/connect redis)] + (loop [] + (run-batch! rconn) + (recur))) + (catch InterruptedException _ + (l/trc :hint "interrupted")) + (catch Throwable cause + (l/err :hint " unexpected exception" :cause cause)) + (finally + (l/inf :hint "terminated"))))] + + (if (db/read-only? pool) + (l/wrn :hint "not started (db is read-only)") + (px/fn->thread dispatcher :name "penpot/worker/dispatcher" :virtual false)))) + +(defmethod ig/halt-key! ::wrk/dispatcher + [_ thread] + (some-> thread px/interrupt!)) diff --git a/backend/src/app/worker/executor.clj b/backend/src/app/worker/executor.clj new file mode 100644 index 0000000000..c1d10122c7 --- /dev/null +++ b/backend/src/app/worker/executor.clj @@ -0,0 +1,116 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.worker.executor + "Async tasks abstraction (impl)." + (:require + [app.common.data :as d] + [app.common.logging :as l] + [app.common.spec :as us] + [app.metrics :as mtx] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [clojure.spec.alpha :as s] + [integrant.core :as ig] + [promesa.exec :as px]) + (:import + java.util.concurrent.Executor + java.util.concurrent.ThreadPoolExecutor)) + +(set! *warn-on-reflection* true) + +(s/def ::wrk/executor #(instance? Executor %)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; EXECUTOR +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defmethod ig/pre-init-spec ::wrk/executor [_] + (s/keys :req [])) + +(defmethod ig/init-key ::wrk/executor + [_ _] + (let [factory (px/thread-factory :prefix "penpot/default/") + executor (px/cached-executor :factory factory :keepalive 60000)] + (l/inf :hint "executor started") + (reify + java.lang.AutoCloseable + (close [_] + (l/inf :hint "stoping executor") + (px/shutdown! executor)) + + clojure.lang.IDeref + (deref [_] + {:active (.getPoolSize ^ThreadPoolExecutor executor) + :running (.getActiveCount ^ThreadPoolExecutor executor) + :completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)}) + + Executor + (execute [_ runnable] + (.execute ^Executor executor ^Runnable runnable))))) + +(defmethod ig/halt-key! ::wrk/executor + [_ instance] + (.close ^java.lang.AutoCloseable instance)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; MONITOR +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(s/def ::name ::us/keyword) + +(defmethod ig/pre-init-spec ::wrk/monitor [_] + (s/keys :req [::wrk/name ::wrk/executor ::mtx/metrics])) + +(defmethod ig/prep-key ::wrk/monitor + [_ cfg] + (merge {::interval (dt/duration "2s")} + (d/without-nils cfg))) + +(defmethod ig/init-key ::wrk/monitor + [_ {:keys [::wrk/executor ::mtx/metrics ::interval ::wrk/name]}] + (letfn [(monitor! [executor prev-completed] + (let [labels (into-array String [(d/name name)]) + stats (deref executor) + + completed (:completed stats) + completed-inc (- completed prev-completed) + completed-inc (if (neg? completed-inc) 0 completed-inc)] + + (mtx/run! metrics + :id :executor-active-threads + :labels labels + :val (:active stats)) + + (mtx/run! metrics + :id :executor-running-threads + :labels labels + :val (:running stats)) + + (mtx/run! metrics + :id :executors-completed-tasks + :labels labels + :inc completed-inc) + + completed-inc))] + + (px/thread + {:name "penpot/executors-monitor" :virtual true} + (l/inf :hint "monitor started" :name name) + (try + (loop [completed 0] + (px/sleep interval) + (recur (long (monitor! executor completed)))) + (catch InterruptedException _cause + (l/trc :hint "monitor: interrupted" :name name)) + (catch Throwable cause + (l/err :hint "monitor: unexpected error" :name name :cause cause)) + (finally + (l/inf :hint "monitor: terminated" :name name)))))) + +(defmethod ig/halt-key! ::wrk/monitor + [_ thread] + (px/interrupt! thread)) diff --git a/backend/src/app/worker/runner.clj b/backend/src/app/worker/runner.clj new file mode 100644 index 0000000000..40332ab235 --- /dev/null +++ b/backend/src/app/worker/runner.clj @@ -0,0 +1,272 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.worker.runner + "Async tasks abstraction (impl)." + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.common.transit :as t] + [app.config :as cf] + [app.db :as db] + [app.metrics :as mtx] + [app.redis :as rds] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [integrant.core :as ig] + [promesa.exec :as px])) + +(set! *warn-on-reflection* true) + +(defn- decode-task-row + [{:keys [props] :as row}] + (cond-> row + (db/pgobject? props) + (assoc :props (db/decode-transit-pgobject props)))) + +(defn get-error-context + [_ item] + {:params item}) + +(defn- run-worker-loop! + [{:keys [::db/pool ::rds/rconn ::wrk/registry ::timeout ::queue ::id]}] + (letfn [(handle-task-retry [{:keys [task error inc-by delay] :or {inc-by 1 delay 1000}}] + (let [explain (ex-message error) + nretry (+ (:retry-num task) inc-by) + now (dt/now) + delay (->> (iterate #(* % 2) delay) (take nretry) (last))] + (db/update! pool :task + {:error explain + :status "retry" + :modified-at now + :scheduled-at (dt/plus now delay) + :retry-num nretry} + {:id (:id task)}) + nil)) + + (handle-task-failure [{:keys [task error]}] + (let [explain (ex-message error)] + (db/update! pool :task + {:error explain + :modified-at (dt/now) + :status "failed"} + {:id (:id task)}) + nil)) + + (handle-task-completion [{:keys [task]}] + (let [now (dt/now)] + (db/update! pool :task + {:completed-at now + :modified-at now + :status "completed"} + {:id (:id task)}) + nil)) + + (decode-payload [^bytes payload] + (try + (let [task-id (t/decode payload)] + (if (uuid? task-id) + task-id + (l/err :hint "received unexpected payload (uuid expected)" + :payload task-id))) + (catch Throwable cause + (l/err :hint "unable to decode payload" + :payload payload + :length (alength payload) + :cause cause)))) + + (handle-task [{:keys [name] :as task}] + (let [task-fn (get registry name)] + (if task-fn + (task-fn task) + (l/wrn :hint "no task handler found" :name name)) + {:status :completed :task task})) + + (handle-task-exception [cause task] + (let [edata (ex-data cause)] + (if (and (< (:retry-num task) + (:max-retries task)) + (= ::retry (:type edata))) + (cond-> {:status :retry :task task :error cause} + (dt/duration? (:delay edata)) + (assoc :delay (:delay edata)) + + (= ::noop (:strategy edata)) + (assoc :inc-by 0)) + (do + (l/err :hint "unhandled exception on task" + ::l/context (get-error-context cause task) + :cause cause) + (if (>= (:retry-num task) (:max-retries task)) + {:status :failed :task task :error cause} + {:status :retry :task task :error cause}))))) + + (get-task [task-id] + (ex/try! + (some-> (db/get* pool :task {:id task-id}) + (decode-task-row)))) + + (run-task [task-id] + (loop [task (get-task task-id)] + (cond + (ex/exception? task) + (if (or (db/connection-error? task) + (db/serialization-error? task)) + (do + (l/wrn :hint "connection error on retrieving task from database (retrying in some instants)" + :id id + :cause task) + (px/sleep (::rds/timeout rconn)) + (recur (get-task task-id))) + (do + (l/err :hint "unhandled exception on retrieving task from database (retrying in some instants)" + :id id + :cause task) + (px/sleep (::rds/timeout rconn)) + (recur (get-task task-id)))) + + (nil? task) + (l/wrn :hint "no task found on the database" + :id id + :task-id task-id) + + :else + (try + (l/trc :hint "start task" + :queue queue + :runner-id id + :name (:name task) + :task-id (str task-id) + :retry (:retry-num task)) + (let [tpoint (dt/tpoint) + result (handle-task task) + elapsed (dt/format-duration (tpoint))] + + (l/trc :hint "end task" + :queue queue + :runner-id id + :name (:name task) + :task-id (str task-id) + :retry (:retry-num task) + :elapsed elapsed) + + result) + + (catch InterruptedException cause + (throw cause)) + (catch Throwable cause + (handle-task-exception cause task)))))) + + (process-result [{:keys [status] :as result}] + (ex/try! + (case status + :retry (handle-task-retry result) + :failed (handle-task-failure result) + :completed (handle-task-completion result) + nil))) + + (run-task-loop [task-id] + (loop [result (run-task task-id)] + (when-let [cause (process-result result)] + (if (or (db/connection-error? cause) + (db/serialization-error? cause)) + (do + (l/wrn :hint "database exeption on processing task result (retrying in some instants)" + :cause cause) + (px/sleep (::rds/timeout rconn)) + (recur result)) + (do + (l/err :hint "unhandled exception on processing task result (retrying in some instants)" + :cause cause) + (px/sleep (::rds/timeout rconn)) + (recur result))))))] + + (try + (let [queue (str/ffmt "taskq:%" queue) + [_ payload] (rds/blpop! rconn timeout queue)] + (some-> payload + decode-payload + run-task-loop)) + + (catch InterruptedException cause + (throw cause)) + + (catch Exception cause + (if (rds/timeout-exception? cause) + (do + (l/err :hint "redis pop operation timeout, consider increasing redis timeout (will retry in some instants)" + :timeout timeout + :cause cause) + (px/sleep timeout)) + + (l/err :hint "unhandled exception" :cause cause)))))) + +(defn- start-thread! + [{:keys [::rds/redis ::id ::queue] :as cfg}] + (px/thread + {:name (format "penpot/worker/runner:%s" id)} + (l/inf :hint "started" :id id :queue queue) + (try + (dm/with-open [rconn (rds/connect redis)] + (let [tenant (cf/get :tenant "main") + cfg (-> cfg + (assoc ::queue (str/ffmt "%:%" tenant queue)) + (assoc ::rds/rconn rconn) + (assoc ::timeout (dt/duration "5s")))] + (loop [] + (when (px/interrupted?) + (throw (InterruptedException. "interrupted"))) + + (run-worker-loop! cfg) + (recur)))) + + (catch InterruptedException _ + (l/debug :hint "interrupted" + :id id + :queue queue)) + (catch Throwable cause + (l/err :hint "unexpected exception" + :id id + :queue queue + :cause cause)) + (finally + (l/inf :hint "terminated" + :id id + :queue queue))))) + +(s/def ::wrk/queue keyword?) + +(defmethod ig/pre-init-spec ::runner [_] + (s/keys :req [::wrk/parallelism + ::mtx/metrics + ::db/pool + ::rds/redis + ::wrk/queue + ::wrk/registry])) + +(defmethod ig/prep-key ::wrk/runner + [_ cfg] + (merge {::wrk/parallelism 1} + (d/without-nils cfg))) + +(defmethod ig/init-key ::wrk/runner + [_ {:keys [::db/pool ::wrk/queue ::wrk/parallelism] :as cfg}] + (let [queue (d/name queue) + cfg (assoc cfg ::queue queue)] + (if (db/read-only? pool) + (l/wrn :hint "not started (db is read-only)" :queue queue :parallelism parallelism) + (doall + (->> (range parallelism) + (map #(assoc cfg ::id %)) + (map start-thread!)))))) + +(defmethod ig/halt-key! ::wrk/runner + [_ threads] + (run! px/interrupt! threads)) diff --git a/backend/test/backend_tests/bounce_handling_test.clj b/backend/test/backend_tests/bounce_handling_test.clj index efb03d1368..13774e042e 100644 --- a/backend/test/backend_tests/bounce_handling_test.clj +++ b/backend/test/backend_tests/bounce_handling_test.clj @@ -28,7 +28,7 @@ (defn bounce-report [{:keys [token email] :or {email "user@example.com"}}] {"notificationType" "Bounce", - "bounce" {"feedbackId""010701776d7dd251-c08d280d-9f47-41aa-b959-0094fec779d9-000000", + "bounce" {"feedbackId" "010701776d7dd251-c08d280d-9f47-41aa-b959-0094fec779d9-000000", "bounceType" "Permanent", "bounceSubType" "General", "bouncedRecipients" [{"emailAddress" email, @@ -102,7 +102,7 @@ (t/deftest test-parse-bounce-report (let [profile (th/create-profile* 1) props (:app.setup/props th/*system*) - cfg {:app.main/props props} + cfg {:app.setup/props props} report (bounce-report {:token (tokens/generate props {:iss :profile-identity :profile-id (:id profile)})}) @@ -113,13 +113,12 @@ (t/is (= "permanent" (:kind result))) (t/is (= "general" (:category result))) (t/is (= ["user@example.com"] (mapv :email (:recipients result)))) - (t/is (= (:id profile) (:profile-id result))) - )) + (t/is (= (:id profile) (:profile-id result))))) (t/deftest test-parse-complaint-report (let [profile (th/create-profile* 1) props (:app.setup/props th/*system*) - cfg {:app.main/props props} + cfg {:app.setup/props props} report (complaint-report {:token (tokens/generate props {:iss :profile-identity :profile-id (:id profile)})}) @@ -129,26 +128,24 @@ (t/is (= "abuse" (:kind result))) (t/is (= nil (:category result))) (t/is (= ["user@example.com"] (into [] (:recipients result)))) - (t/is (= (:id profile) (:profile-id result))) - )) + (t/is (= (:id profile) (:profile-id result))))) (t/deftest test-parse-complaint-report-without-token (let [props (:app.setup/props th/*system*) - cfg {:app.main/props props} + cfg {:app.setup/props props} report (complaint-report {:token ""}) result (#'awsns/parse-notification cfg report)] (t/is (= "complaint" (:type result))) (t/is (= "abuse" (:kind result))) (t/is (= nil (:category result))) (t/is (= ["user@example.com"] (into [] (:recipients result)))) - (t/is (= nil (:profile-id result))) - )) + (t/is (= nil (:profile-id result))))) (t/deftest test-process-bounce-report (let [profile (th/create-profile* 1) props (:app.setup/props th/*system*) pool (:app.db/pool th/*system*) - cfg {:app.main/props props :app.db/pool pool} + cfg {:app.setup/props props :app.db/pool pool} report (bounce-report {:token (tokens/generate props {:iss :profile-identity :profile-id (:id profile)})}) @@ -169,15 +166,13 @@ (t/is (= "user@example.com" (get-in rows [0 :email])))) (let [prof (db/get-by-id pool :profile (:id profile))] - (t/is (false? (:is-muted prof)))) - - )) + (t/is (false? (:is-muted prof)))))) (t/deftest test-process-complaint-report (let [profile (th/create-profile* 1) props (:app.setup/props th/*system*) pool (:app.db/pool th/*system*) - cfg {:app.main/props props + cfg {:app.setup/props props :app.db/pool pool} report (complaint-report {:token (tokens/generate props {:iss :profile-identity @@ -201,15 +196,13 @@ (let [prof (db/get-by-id pool :profile (:id profile))] - (t/is (false? (:is-muted prof)))) - - )) + (t/is (false? (:is-muted prof)))))) (t/deftest test-process-bounce-report-to-self (let [profile (th/create-profile* 1) props (:app.setup/props th/*system*) pool (:app.db/pool th/*system*) - cfg {:app.main/props props :app.db/pool pool} + cfg {:app.setup/props props :app.db/pool pool} report (bounce-report {:email (:email profile) :token (tokens/generate props {:iss :profile-identity @@ -231,7 +224,7 @@ (let [profile (th/create-profile* 1) props (:app.setup/props th/*system*) pool (:app.db/pool th/*system*) - cfg {:app.main/props props :app.db/pool pool} + cfg {:app.setup/props props :app.db/pool pool} report (complaint-report {:email (:email profile) :token (tokens/generate props {:iss :profile-identity diff --git a/backend/test/backend_tests/email_sending_test.clj b/backend/test/backend_tests/email_sending_test.clj index 8d572bc819..a61825ae49 100644 --- a/backend/test/backend_tests/email_sending_test.clj +++ b/backend/test/backend_tests/email_sending_test.clj @@ -6,9 +6,9 @@ (ns backend-tests.email-sending-test (:require - [backend-tests.helpers :as th] [app.db :as db] [app.email :as emails] + [backend-tests.helpers :as th] [clojure.test :as t] [promesa.core :as p])) diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index dbd7f464dd..61b5f42bf2 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -10,17 +10,18 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.features :as cfeat] [app.common.flags :as flags] - [app.common.pages :as cp] [app.common.pprint :as pp] - [app.common.spec :as us] [app.common.schema :as sm] + [app.common.spec :as us] + [app.common.transit :as tr] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] [app.main :as main] - [app.media :as-alias mtx] [app.media] + [app.media :as-alias mtx] [app.migrations] [app.msgbus :as-alias mbus] [app.rpc :as-alias rpc] @@ -37,14 +38,18 @@ [clojure.spec.alpha :as s] [clojure.test :as t] [cuerdas.core :as str] - [datoteka.core :as fs] + [datoteka.fs :as fs] [environ.core :refer [env]] [expound.alpha :as expound] [integrant.core :as ig] [mockery.core :as mk] [promesa.core :as p] + [promesa.exec :as px] + [ring.response :as rres] [yetti.request :as yrq]) (:import + java.io.PipedInputStream + java.io.PipedOutputStream java.util.UUID org.postgresql.ds.PGSimpleDataSource)) @@ -66,8 +71,11 @@ :enable-email-verification :enable-smtp :enable-quotes - :enable-fdata-storage-pointer-map - :enable-fdata-storage-objets-map]) + :enable-rpc-climit + :enable-feature-fdata-pointer-map + :enable-feature-fdata-objets-map + :enable-feature-components-v2 + :disable-file-validation]) (def test-init-sql ["alter table project_profile_rel set unlogged;\n" @@ -90,6 +98,7 @@ "alter table file_library_rel set unlogged;\n" "alter table file_thumbnail set unlogged;\n" "alter table file_object_thumbnail set unlogged;\n" + "alter table file_tagged_object_thumbnail set unlogged;\n" "alter table file_media_object set unlogged;\n" "alter table file_data_fragment set unlogged;\n" "alter table file set unlogged;\n" @@ -103,12 +112,11 @@ ;; "alter table task set unlogged;\n" ;; "alter table task_default set unlogged;\n" ;; "alter table task_completed set unlogged;\n" - "alter table audit_log_default set unlogged ;\n" + "alter table audit_log set unlogged ;\n" "alter table storage_object set unlogged;\n" "alter table server_error_report set unlogged;\n" "alter table server_prop set unlogged;\n" - "alter table global_complaint_report set unlogged;\n" -]) + "alter table global_complaint_report set unlogged;\n"]) (defn state-init [next] @@ -116,7 +124,10 @@ app.config/config config app.loggers.audit/submit! (constantly nil) app.auth/derive-password identity - app.auth/verify-password (fn [a b] {:valid (= a b)})] + app.auth/verify-password (fn [a b] {:valid (= a b)}) + app.common.features/get-enabled-features (fn [& _] app.common.features/supported-features)] + + (fs/create-dir "/tmp/penpot") (let [templates [{:id "test" :name "test" @@ -145,8 +156,8 @@ :app.loggers.database/reporter :app.worker/cron :app.worker/dispatcher - [:app.main/default :app.worker/worker] - [:app.main/webhook :app.worker/worker])) + [:app.main/default :app.worker/runner] + [:app.main/webhook :app.worker/runner])) _ (ig/load-namespaces system) system (-> (ig/prep system) (ig/init))] @@ -167,12 +178,11 @@ " WHERE table_schema = 'public' " " AND table_name != 'migrations';")] (db/with-atomic [conn *pool*] + (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"]) + (db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"]) (let [result (->> (db/exec! conn [sql]) (map :table-name) - (remove #(= "task" %))) - sql (str "TRUNCATE " - (apply str (interpose ", " result)) - " CASCADE;")] + (remove #(= "task" %)))] (doseq [table result] (db/exec! conn [(str "delete from " table ";")])))) @@ -183,6 +193,7 @@ (let [path (fs/path "/tmp/penpot")] (when (fs/exists? path) (fs/delete (fs/path "/tmp/penpot"))) + (fs/create-dir "/tmp/penpot") (next))) (defn serial @@ -205,65 +216,73 @@ ;; --- FACTORIES (defn create-profile* - ([i] (create-profile* *pool* i {})) - ([i params] (create-profile* *pool* i params)) - ([pool i params] + ([i] (create-profile* *system* i {})) + ([i params] (create-profile* *system* i params)) + ([system i params] (let [params (merge {:id (mk-uuid "profile" i) :fullname (str "Profile " i) :email (str "profile" i ".test@nodomain.com") :password "123123" :is-demo false} params)] - (dm/with-open [conn (db/open pool)] - (->> params - (cmd.auth/create-profile! conn) - (cmd.auth/create-profile-rels! conn)))))) + (db/run! system + (fn [{:keys [::db/conn]}] + (->> params + (cmd.auth/create-profile! conn) + (cmd.auth/create-profile-rels! conn))))))) (defn create-project* - ([i params] (create-project* *pool* i params)) - ([pool i {:keys [profile-id team-id] :as params}] + ([i params] (create-project* *system* i params)) + ([system i {:keys [profile-id team-id] :as params}] (us/assert uuid? profile-id) (us/assert uuid? team-id) - (dm/with-open [conn (db/open pool)] - (->> (merge {:id (mk-uuid "project" i) - :name (str "project" i)} - params) - (#'teams/create-project conn))))) + + (db/run! system + (fn [{:keys [::db/conn]}] + (->> (merge {:id (mk-uuid "project" i) + :name (str "project" i)} + params) + (#'teams/create-project conn)))))) (defn create-file* ([i params] - (create-file* *pool* i params)) - ([pool i {:keys [profile-id project-id] :as params}] - (us/assert uuid? profile-id) - (us/assert uuid? project-id) - (db/with-atomic [conn (db/open pool)] - (files.create/create-file conn - (merge {:id (mk-uuid "file" i) - :name (str "file" i) - :components-v2 true} - params))))) + (create-file* *system* i params)) + ([system i {:keys [profile-id project-id] :as params}] + (dm/assert! "expected uuid" (uuid? profile-id)) + (dm/assert! "expected uuid" (uuid? project-id)) + (db/run! system + (fn [system] + (let [features (cfeat/get-enabled-features cf/flags)] + (files.create/create-file system + (merge {:id (mk-uuid "file" i) + :name (str "file" i) + :features features} + params))))))) (defn mark-file-deleted* - ([params] (mark-file-deleted* *pool* params)) + ([params] + (mark-file-deleted* *system* params)) ([conn {:keys [id] :as params}] - (#'files/mark-file-deleted conn {:id id}))) + (#'files/mark-file-deleted! conn id))) (defn create-team* - ([i params] (create-team* *pool* i params)) - ([pool i {:keys [profile-id] :as params}] + ([i params] (create-team* *system* i params)) + ([system i {:keys [profile-id] :as params}] (us/assert uuid? profile-id) - (dm/with-open [conn (db/open pool)] - (let [id (mk-uuid "team" i)] + (dm/with-open [conn (db/open system)] + (let [id (mk-uuid "team" i) + features (cfeat/get-enabled-features cf/flags)] (teams/create-team conn {:id id :profile-id profile-id + :features features :name (str "team" i)}))))) (defn create-file-media-object* - ([params] (create-file-media-object* *pool* params)) - ([pool {:keys [name width height mtype file-id is-local media-id] - :or {name "sample" width 100 height 100 mtype "image/svg+xml" is-local true}}] + ([params] (create-file-media-object* *system* params)) + ([system {:keys [name width height mtype file-id is-local media-id] + :or {name "sample" width 100 height 100 mtype "image/svg+xml" is-local true}}] - (dm/with-open [conn (db/open pool)] + (dm/with-open [conn (db/open system)] (db/insert! conn :file-media-object {:id (uuid/next) :file-id file-id @@ -275,14 +294,14 @@ :mtype mtype})))) (defn link-file-to-library* - ([params] (link-file-to-library* *pool* params)) - ([pool {:keys [file-id library-id] :as params}] - (dm/with-open [conn (db/open pool)] + ([params] (link-file-to-library* *system* params)) + ([system {:keys [file-id library-id] :as params}] + (dm/with-open [conn (db/open system)] (#'files/link-file-to-library conn {:file-id file-id :library-id library-id})))) (defn create-complaint-for - [pool {:keys [id created-at type]}] - (dm/with-open [conn (db/open pool)] + [system {:keys [id created-at type]}] + (dm/with-open [conn (db/open system)] (db/insert! conn :profile-complaint-report {:profile-id id :created-at (or created-at (dt/now)) @@ -290,8 +309,8 @@ :content (db/tjson {})}))) (defn create-global-complaint-for - [pool {:keys [email type created-at]}] - (dm/with-open [conn (db/open pool)] + [system {:keys [email type created-at]}] + (dm/with-open [conn (db/open system)] (db/insert! conn :global-complaint-report {:email email :type (name type) @@ -299,71 +318,72 @@ :content (db/tjson {})}))) (defn create-team-role* - ([params] (create-team-role* *pool* params)) - ([pool {:keys [team-id profile-id role] :or {role :owner}}] - (dm/with-open [conn (db/open pool)] + ([params] (create-team-role* *system* params)) + ([system {:keys [team-id profile-id role] :or {role :owner}}] + (dm/with-open [conn (db/open system)] (#'teams/create-team-role conn {:team-id team-id :profile-id profile-id :role role})))) (defn create-project-role* - ([params] (create-project-role* *pool* params)) - ([pool {:keys [project-id profile-id role] :or {role :owner}}] - (dm/with-open [conn (db/open pool)] + ([params] (create-project-role* *system* params)) + ([system {:keys [project-id profile-id role] :or {role :owner}}] + (dm/with-open [conn (db/open system)] (#'teams/create-project-role conn {:project-id project-id - :profile-id profile-id - :role role})))) + :profile-id profile-id + :role role})))) (defn create-file-role* - ([params] (create-file-role* *pool* params)) - ([pool {:keys [file-id profile-id role] :or {role :owner}}] - (dm/with-open [conn (db/open pool)] + ([params] (create-file-role* *system* params)) + ([system {:keys [file-id profile-id role] :or {role :owner}}] + (dm/with-open [conn (db/open system)] (files.create/create-file-role! conn {:file-id file-id :profile-id profile-id :role role})))) (defn update-file* - ([params] (update-file* *pool* params)) - ([pool {:keys [file-id changes session-id profile-id revn] - :or {session-id (uuid/next) revn 0}}] - (dm/with-open [conn (db/open pool)] - (let [features #{"components/v2"} - cfg (-> (select-keys *system* [::mbus/msgbus ::mtx/metrics]) - (assoc ::db/conn conn))] - (files.update/update-file cfg - {:id file-id - :revn revn - :features features - :changes changes - :session-id session-id - :profile-id profile-id}))))) + ([params] (update-file* *system* params)) + ([system {:keys [file-id changes session-id profile-id revn] + :or {session-id (uuid/next) revn 0}}] + (db/tx-run! system (fn [{:keys [::db/conn] :as system}] + (let [file (files.update/get-file conn file-id)] + (files.update/update-file system + {:id file-id + :revn revn + :file file + :features (:features file) + :changes changes + :session-id session-id + :profile-id profile-id})))))) (declare command!) (defn update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}] - (let [params {::type :update-file - ::rpc/profile-id profile-id - :id file-id - :session-id (uuid/random) - :revn revn - :components-v2 true - :changes changes} - out (command! params)] + (let [features (cfeat/get-enabled-features cf/flags) + params {::type :update-file + ::rpc/profile-id profile-id + :id file-id + :session-id (uuid/random) + :revn revn + :features features + :changes changes} + out (command! params)] (t/is (nil? (:error out))) (:result out))) (defn create-webhook* - ([params] (create-webhook* *pool* params)) - ([pool {:keys [team-id id uri mtype is-active] - :or {is-active true - mtype "application/json" - uri "http://example.com/webhook"}}] - (db/insert! pool :webhook - {:id (or id (uuid/next)) - :team-id team-id - :uri uri - :is-active is-active - :mtype mtype}))) + ([params] (create-webhook* *system* params)) + ([system {:keys [team-id id uri mtype is-active] + :or {is-active true + mtype "application/json" + uri "http://example.com/webhook"}}] + (db/run! system (fn [{:keys [::db/conn]}] + (db/insert! conn :webhook + {:id (or id (uuid/next)) + :team-id team-id + :uri uri + :is-active is-active + :mtype mtype}))))) ;; --- RPC HELPERS @@ -416,12 +436,12 @@ (us/pretty-explain data)) (= :params-validation (:code data)) - (app.common.pprint/pprint - (sm/humanize-data (::sm/explain data))) + (println + (sm/humanize-explain (::sm/explain data))) (= :data-validation (:code data)) - (app.common.pprint/pprint - (sm/humanize-data (::sm/explain data))) + (println + (sm/humanize-explain (::sm/explain data))) (= :service-error (:type data)) (print-error! (.getCause ^Throwable error)) @@ -479,7 +499,7 @@ (defn tempfile [source] (let [rsc (io/resource source) - tmp (fs/create-tempfile)] + tmp (fs/create-tempfile :dir "/tmp/penpot" :prefix "test-")] (io/copy (io/file rsc) (io/file tmp)) tmp)) @@ -495,6 +515,10 @@ [sql] (db/exec! *pool* sql)) +(defn db-exec-one! + [sql] + (db/exec-one! *pool* sql)) + (defn db-delete! [& params] (apply db/delete! *pool* params)) @@ -541,3 +565,28 @@ (assoc :return-list []) (assoc :call-args nil) (assoc :call-args-list []))))) + +(defn- slurp' + [input & opts] + (let [sw (java.io.StringWriter.)] + (with-open [^java.io.Reader r (java.io.InputStreamReader. input "UTF-8")] + (io/copy r sw) + (.toString sw)))) + +(defn consume-sse + [callback] + (let [{:keys [::rres/status ::rres/body ::rres/headers] :as response} (callback {}) + output (PipedOutputStream.) + input (PipedInputStream. output)] + + (try + (px/exec! :virtual #(rres/-write-body-to-stream body nil output)) + (into [] + (map (fn [event] + (let [[item1 item2] (re-seq #"(.*): (.*)\n?" event)] + [(keyword (nth item1 2)) + (tr/decode-str (nth item2 2))]))) + (-> (slurp' input) + (str/split "\n\n"))) + (finally + (.close input))))) diff --git a/backend/test/backend_tests/http_middleware_access_token_test.clj b/backend/test/backend_tests/http_middleware_access_token_test.clj index ddc170355b..0b658d853a 100644 --- a/backend/test/backend_tests/http_middleware_access_token_test.clj +++ b/backend/test/backend_tests/http_middleware_access_token_test.clj @@ -31,17 +31,17 @@ request (volatile! nil) handler (#'app.http.access-token/wrap-soft-auth - (fn [req & _] (vreset! request req)) + (fn [req] (vreset! request req)) system)] (with-mocks [m1 {:target 'app.http.access-token/get-token :return nil}] - (handler {} nil nil) + (handler {}) (t/is (= {} @request))) (with-mocks [m1 {:target 'app.http.access-token/get-token :return (:token token)}] - (handler {} nil nil) + (handler {}) (let [token-id (get @request :app.http.access-token/id)] (t/is (= token-id (:id token)))))))) diff --git a/backend/test/backend_tests/loggers_webhooks_test.clj b/backend/test/backend_tests/loggers_webhooks_test.clj index ac4af54a90..ab3a4e82ed 100644 --- a/backend/test/backend_tests/loggers_webhooks_test.clj +++ b/backend/test/backend_tests/loggers_webhooks_test.clj @@ -65,9 +65,7 @@ ;; Refresh webhook (let [whk' (th/db-get :webhook {:id (:id whk)})] - (t/is (nil? (:error-code whk')))) - - ))) + (t/is (nil? (:error-code whk'))))))) (t/deftest run-webhook-handler-2 (with-mocks [http-mock {:target 'app.http.client/req! :return {:status 400}}] @@ -114,6 +112,4 @@ (let [whk' (th/db-get :webhook {:id (:id whk)})] (t/is (= "unexpected-status:400" (:error-code whk'))) (t/is (= 3 (:error-count whk'))) - (t/is (false? (:is-active whk')))) - - ))) + (t/is (false? (:is-active whk'))))))) diff --git a/backend/test/backend_tests/rpc_access_tokens_test.clj b/backend/test/backend_tests/rpc_access_tokens_test.clj index 30e12c028d..fe0269d609 100644 --- a/backend/test/backend_tests/rpc_access_tokens_test.clj +++ b/backend/test/backend_tests/rpc_access_tokens_test.clj @@ -9,8 +9,8 @@ [app.common.uuid :as uuid] [app.db :as db] [app.http :as http] - [app.storage :as sto] [app.rpc :as-alias rpc] + [app.storage :as sto] [backend-tests.helpers :as th] [clojure.test :as t] [mockery.core :refer [with-mocks]])) diff --git a/backend/test/backend_tests/rpc_audit_test.clj b/backend/test/backend_tests/rpc_audit_test.clj index 233728dac3..78d0e4d410 100644 --- a/backend/test/backend_tests/rpc_audit_test.clj +++ b/backend/test/backend_tests/rpc_audit_test.clj @@ -9,8 +9,8 @@ [app.common.pprint :as pp] [app.common.uuid :as uuid] [app.db :as db] - [app.util.time :as dt] [app.rpc :as-alias rpc] + [app.util.time :as dt] [backend-tests.helpers :as th] [clojure.test :as t])) @@ -25,7 +25,7 @@ (def http-request (reify - yetti.request/Request + ring.request/Request (get-header [_ name] (case name "x-forwarded-for" "127.0.0.44")))) @@ -91,8 +91,6 @@ (t/is (= 1 (count rows))) (t/is (= (:id prof) (:profile-id row))) (t/is (= "navigate" (:name row))) - (t/is (= "frontend" (:source row)))) - - ))) + (t/is (= "frontend" (:source row))))))) diff --git a/backend/test/backend_tests/rpc_comment_test.clj b/backend/test/backend_tests/rpc_comment_test.clj index 0cb812bad9..9e0f864742 100644 --- a/backend/test/backend_tests/rpc_comment_test.clj +++ b/backend/test/backend_tests/rpc_comment_test.clj @@ -17,7 +17,7 @@ [app.util.time :as dt] [backend-tests.helpers :as th] [clojure.test :as t] - [datoteka.core :as fs] + [datoteka.fs :as fs] [mockery.core :refer [with-mocks]])) (t/use-fixtures :once th/state-init) @@ -285,6 +285,4 @@ (t/is (th/success? out)) (let [threads (th/db-query :comment-thread {:file-id (:id file-1)})] - (t/is (= 0 (count threads)))))) - - ))) + (t/is (= 0 (count threads))))))))) diff --git a/backend/test/backend_tests/rpc_cond_middleware_test.clj b/backend/test/backend_tests/rpc_cond_middleware_test.clj index dfbee87d87..e74a9c5497 100644 --- a/backend/test/backend_tests/rpc_cond_middleware_test.clj +++ b/backend/test/backend_tests/rpc_cond_middleware_test.clj @@ -6,6 +6,7 @@ (ns backend-tests.rpc-cond-middleware-test (:require + [app.common.features :as cfeat] [app.common.uuid :as uuid] [app.db :as db] [app.http :as http] @@ -14,7 +15,7 @@ [backend-tests.helpers :as th] [backend-tests.storage-test :refer [configure-storage-backend]] [clojure.test :as t] - [datoteka.core :as fs])) + [datoteka.fs :as fs])) (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) @@ -27,19 +28,22 @@ :project-id (:id project)}) params {::th/type :get-file :id (:id file1) - ::rpc/profile-id (:id profile)}] + ::rpc/profile-id (:id profile) + :features cfeat/supported-features}] (binding [cond/*enabled* true] - (let [{:keys [error result]} (th/command! params)] + (let [{:keys [error result] :as out} (th/command! params)] + ;; NOTE: Fails on print because fipps used for pretty print + ;; tries to load pointers + ;; (th/print-result! out) (t/is (nil? error)) (t/is (map? result)) (t/is (contains? (meta result) :app.http/headers)) (t/is (contains? (meta result) :app.rpc.cond/key)) - (let [etag (-> result meta :app.http/headers (get "etag")) + (let [etag (-> result meta :app.http/headers (get "etag")) {:keys [error result]} (th/command! (assoc params ::cond/key etag))] (t/is (nil? error)) (t/is (fn? result)) - (t/is (= 304 (-> (result nil) :yetti.response/status)))) - )))) + (t/is (= 304 (-> (result nil) :ring.response/status)))))))) diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index d39fd2d393..a684227c85 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -6,6 +6,11 @@ (ns backend-tests.rpc-file-test (:require + [app.common.features :as cfeat] + [app.common.pprint :as pp] + [app.common.pprint :as pp] + [app.common.thumbnails :as thc] + [app.common.types.shape :as cts] [app.common.uuid :as uuid] [app.db :as db] [app.db.sql :as sql] @@ -15,7 +20,7 @@ [app.util.time :as dt] [backend-tests.helpers :as th] [clojure.test :as t] - [datoteka.core :as fs])) + [cuerdas.core :as str])) (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) @@ -119,8 +124,7 @@ (t/is (nil? (:error out))) (let [result (:result out)] - (t/is (= 0 (count result)))))) - )) + (t/is (= 0 (count result)))))))) (t/deftest file-gc-with-fragments (letfn [(update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}] @@ -129,7 +133,7 @@ :id file-id :session-id (uuid/random) :revn revn - :components-v2 true + :features cfeat/supported-features :changes changes} out (th/command! params)] ;; (th/print-result! out) @@ -145,12 +149,12 @@ shape-id (uuid/random)] ;; Preventive file-gc - (let [res (th/run-task! "file-gc" {:min-age 0})] + (let [res (th/run-task! :file-gc {:min-age 0})] (t/is (= 1 (:processed res)))) ;; Check the number of fragments before adding the page (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] - (t/is (= 1 (count rows)))) + (t/is (= 2 (count rows)))) ;; Add page (update-file! @@ -162,18 +166,21 @@ :name "test" :id page-id}]) - ;; Check the number of fragments - (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] - (t/is (= 2 (count rows)))) - - ;; Check the number of fragments - (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] - (t/is (= 2 (count rows)))) - - ;; The file-gc should remove unused fragments - (let [res (th/run-task! "file-gc" {:min-age 0})] + ;; The file-gc should mark for remove unused fragments + (let [res (th/run-task! :file-gc {:min-age 0})] (t/is (= 1 (:processed res)))) + ;; Check the number of fragments + (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] + (t/is (= 5 (count rows)))) + + ;; The objects-gc should remove unused fragments + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 1 (:processed res)))) + + ;; Check the number of fragments + (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] + (t/is (= 4 (count rows)))) ;; Add shape to page that should add a new fragment (update-file! @@ -187,24 +194,30 @@ :parent-id uuid/zero :frame-id uuid/zero :components-v2 true - :obj {:id shape-id - :name "image" - :frame-id uuid/zero - :parent-id uuid/zero - :type :rect}}]) + :obj (cts/setup-shape + {:id shape-id + :name "image" + :frame-id uuid/zero + :parent-id uuid/zero + :type :rect})}]) ;; Check the number of fragments (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] - (t/is (= 3 (count rows)))) + (t/is (= 5 (count rows)))) - ;; The file-gc should remove unused fragments - (let [res (th/run-task! "file-gc" {:min-age 0})] + ;; The file-gc should mark for remove unused fragments + (let [res (th/run-task! :file-gc {:min-age 0})] + (t/is (= 1 (:processed res)))) + + ;; The objects-gc should remove unused fragments + (let [res (th/run-task! :objects-gc {:min-age 0})] (t/is (= 1 (:processed res)))) ;; Check the number of fragments; should be 3 because changes ;; are also holding pointers to fragments; - (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] - (t/is (= 3 (count rows)))) + (let [rows (th/db-query :file-data-fragment {:file-id (:id file) + :deleted-at nil})] + (t/is (= 6 (count rows)))) ;; Lets proceed to delete all changes (th/db-delete! :file-change {:file-id (:id file)}) @@ -212,20 +225,166 @@ {:has-media-trimmed false} {:id (:id file)}) - ;; The file-gc should remove fragments related to changes ;; snapshots previously deleted. - (let [res (th/run-task! "file-gc" {:min-age 0})] + (let [res (th/run-task! :file-gc {:min-age 0})] (t/is (= 1 (:processed res)))) ;; Check the number of fragments; (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] - (t/is (= 2 (count rows)))) + ;; (pp/pprint rows) + (t/is (= 8 (count rows))) + (t/is (= 2 (count (remove (comp some? :deleted-at) rows))))) - ))) + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 6 (:processed res)))) + (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] + (t/is (= 2 (count rows))))))) (t/deftest file-gc-task-with-thumbnails + (letfn [(add-file-media-object [& {:keys [profile-id file-id]}] + (let [mfile {:filename "sample.jpg" + :path (th/tempfile "backend_tests/test_files/sample.jpg") + :mtype "image/jpeg" + :size 312043} + params {::th/type :upload-file-media-object + ::rpc/profile-id profile-id + :file-id file-id + :is-local true + :name "testfile" + :content mfile} + out (th/command! params)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (:result out))) + + (update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}] + (let [params {::th/type :update-file + ::rpc/profile-id profile-id + :id file-id + :session-id (uuid/random) + :revn revn + :features cfeat/supported-features + :changes changes} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (:result out)))] + + (let [storage (:app.storage/storage th/*system*) + + profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + + fmo1 (add-file-media-object :profile-id (:id profile) :file-id (:id file)) + fmo2 (add-file-media-object :profile-id (:id profile) :file-id (:id file)) + shid (uuid/random) + + page-id (first (get-in file [:data :pages]))] + + + ;; Update file inserting a new image object + (update-file! + :file-id (:id file) + :profile-id (:id profile) + :revn 0 + :changes + [{:type :add-obj + :page-id page-id + :id shid + :parent-id uuid/zero + :frame-id uuid/zero + :components-v2 true + :obj (cts/setup-shape + {:id shid + :name "image" + :frame-id uuid/zero + :parent-id uuid/zero + :type :image + :metadata {:id (:id fmo1) :width 100 :height 100 :mtype "image/jpeg"}})}]) + + ;; Check that reference storage objects on filemediaobjects + ;; are the same because of deduplication feature. + (t/is (= (:media-id fmo1) (:media-id fmo2))) + (t/is (= (:thumbnail-id fmo1) (:thumbnail-id fmo2))) + + ;; If we launch gc-touched-task, we should have 2 items to + ;; freeze because of the deduplication (we have uploaded 2 times + ;; the same files). + + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 2 (:freeze res))) + (t/is (= 0 (:delete res)))) + + ;; run the file-gc task immediately without forced min-age + (let [res (th/run-task! :file-gc)] + (t/is (= 0 (:processed res)))) + + ;; run the task again + (let [res (th/run-task! :file-gc {:min-age 0})] + (t/is (= 1 (:processed res)))) + + ;; retrieve file and check trimmed attribute + (let [row (th/db-get :file {:id (:id file)})] + (t/is (true? (:has-media-trimmed row)))) + + ;; check file media objects + (let [rows (th/db-query :file-media-object {:file-id (:id file)})] + (t/is (= 2 (count rows))) + (t/is (= 1 (count (remove (comp some? :deleted-at) rows))))) + + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 2 (:processed res)))) + + ;; check file media objects + (let [rows (th/db-query :file-media-object {:file-id (:id file)})] + (t/is (= 1 (count rows))) + (t/is (= 1 (count (remove (comp some? :deleted-at) rows))))) + + ;; The underlying storage objects are still available. + (t/is (some? (sto/get-object storage (:media-id fmo2)))) + (t/is (some? (sto/get-object storage (:thumbnail-id fmo2)))) + (t/is (some? (sto/get-object storage (:media-id fmo1)))) + (t/is (some? (sto/get-object storage (:thumbnail-id fmo1)))) + + ;; proceed to remove usage of the file + (update-file! + :file-id (:id file) + :profile-id (:id profile) + :revn 0 + :changes [{:type :del-obj + :page-id (first (get-in file [:data :pages])) + :id shid}]) + + ;; Now, we have deleted the usage of pointers to the + ;; file-media-objects, if we paste file-gc, they should be marked + ;; as deleted. + (let [res (th/run-task! :file-gc {:min-age 0})] + (t/is (= 1 (:processed res)))) + + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 2 (:processed res)))) + + ;; Now that file-gc have deleted the file-media-object usage, + ;; lets execute the touched-gc task, we should see that two of + ;; them are marked to be deleted. + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 0 (:freeze res))) + (t/is (= 2 (:delete res)))) + + ;; Finally, check that some of the objects that are marked as + ;; deleted we are unable to retrieve them using standard storage + ;; public api. + (t/is (nil? (sto/get-object storage (:media-id fmo2)))) + (t/is (nil? (sto/get-object storage (:thumbnail-id fmo2)))) + (t/is (nil? (sto/get-object storage (:media-id fmo1)))) + (t/is (nil? (sto/get-object storage (:thumbnail-id fmo1))))))) + +(t/deftest file-gc-image-fills-and-strokes (letfn [(add-file-media-object [& {:keys [profile-id file-id]}] (let [mfile {:filename "sample.jpg" :path (th/tempfile "backend_tests/test_files/sample.jpg") @@ -265,11 +424,19 @@ fmo1 (add-file-media-object :profile-id (:id profile) :file-id (:id file)) fmo2 (add-file-media-object :profile-id (:id profile) :file-id (:id file)) - shid (uuid/random) + fmo3 (add-file-media-object :profile-id (:id profile) :file-id (:id file)) + fmo4 (add-file-media-object :profile-id (:id profile) :file-id (:id file)) + fmo5 (add-file-media-object :profile-id (:id profile) :file-id (:id file)) + s-shid (uuid/random) + t-shid (uuid/random) page-id (first (get-in file [:data :pages]))] + (let [rows (th/db-query :file-data-fragment {:file-id (:id file) + :deleted-at nil})] + (t/is (= (count rows) 1))) + ;; Update file inserting a new image object (update-file! :file-id (:id file) @@ -278,52 +445,75 @@ :changes [{:type :add-obj :page-id page-id - :id shid + :id s-shid :parent-id uuid/zero :frame-id uuid/zero :components-v2 true - :obj {:id shid - :name "image" - :frame-id uuid/zero - :parent-id uuid/zero - :type :image - :metadata {:id (:id fmo1) :width 200 :height 200 :mtype "image/jpeg"}}}]) - - ;; Check that reference storage objects on filemediaobjects - ;; are the same because of deduplication feature. - (t/is (= (:media-id fmo1) (:media-id fmo2))) - (t/is (= (:thumbnail-id fmo1) (:thumbnail-id fmo2))) - - ;; If we launch gc-touched-task, we should have 2 items to - ;; freeze because of the deduplication (we have uploaded 2 times - ;; the same files). - - (let [task (:app.storage/gc-touched-task th/*system*) - res (task {:min-age (dt/duration 0)})] - (t/is (= 2 (:freeze res))) - (t/is (= 0 (:delete res)))) + :obj (cts/setup-shape + {:id s-shid + :name "image" + :frame-id uuid/zero + :parent-id uuid/zero + :type :image + :metadata {:id (:id fmo1) :width 100 :height 100 :mtype "image/jpeg"} + :fills [{:opacity 1 :fill-image {:id (:id fmo2) :width 100 :height 100 :mtype "image/jpeg"}}] + :strokes [{:opacity 1 :stroke-image {:id (:id fmo3) :width 100 :height 100 :mtype "image/jpeg"}}]})} + {:type :add-obj + :page-id page-id + :id t-shid + :parent-id uuid/zero + :frame-id uuid/zero + :components-v2 true + :obj (cts/setup-shape + {:id t-shid + :name "text" + :frame-id uuid/zero + :parent-id uuid/zero + :type :text + :content {:type "root" + :children [{:type "paragraph-set" + :children [{:type "paragraph" + :children [{:fills [{:fill-opacity 1 + :fill-image {:id (:id fmo4) + :width 417 + :height 354 + :mtype "image/png" + :name "text fill image"}}] + :text "hi"} + {:fills [{:fill-opacity 1 + :fill-color "#000000"}] + :text "bye"}]}]}]} + :strokes [{:opacity 1 :stroke-image {:id (:id fmo5) :width 100 :height 100 :mtype "image/jpeg"}}]})}]) ;; run the file-gc task immediately without forced min-age - (let [res (th/run-task! "file-gc")] + (let [res (th/run-task! :file-gc)] (t/is (= 0 (:processed res)))) ;; run the task again - (let [res (th/run-task! "file-gc" {:min-age 0})] + (let [res (th/run-task! :file-gc {:min-age 0})] (t/is (= 1 (:processed res)))) + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 1 (:processed res)))) + + (let [rows (th/db-query :file-data-fragment {:file-id (:id file) + :deleted-at nil})] + (t/is (= (count rows) 2))) + ;; retrieve file and check trimmed attribute (let [row (th/db-get :file {:id (:id file)})] (t/is (true? (:has-media-trimmed row)))) ;; check file media objects (let [rows (th/db-exec! ["select * from file_media_object where file_id = ?" (:id file)])] - (t/is (= 1 (count rows)))) + (t/is (= 5 (count rows)))) ;; The underlying storage objects are still available. + (t/is (some? (sto/get-object storage (:media-id fmo5)))) + (t/is (some? (sto/get-object storage (:media-id fmo4)))) + (t/is (some? (sto/get-object storage (:media-id fmo3)))) (t/is (some? (sto/get-object storage (:media-id fmo2)))) - (t/is (some? (sto/get-object storage (:thumbnail-id fmo2)))) (t/is (some? (sto/get-object storage (:media-id fmo1)))) - (t/is (some? (sto/get-object storage (:thumbnail-id fmo1)))) ;; proceed to remove usage of the file (update-file! @@ -332,31 +522,233 @@ :revn 0 :changes [{:type :del-obj :page-id (first (get-in file [:data :pages])) - :id shid}]) + :id s-shid} + {:type :del-obj + :page-id (first (get-in file [:data :pages])) + :id t-shid}]) ;; Now, we have deleted the usage of pointers to the ;; file-media-objects, if we paste file-gc, they should be marked ;; as deleted. - (let [task (:app.tasks.file-gc/handler th/*system*) - res (task {:min-age (dt/duration 0)})] + + (let [res (th/run-task! :file-gc {:min-age 0})] (t/is (= 1 (:processed res)))) + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 6 (:processed res)))) + + (let [rows (th/db-query :file-data-fragment {:file-id (:id file) + :deleted-at nil})] + (t/is (= (count rows) 3))) + ;; Now that file-gc have deleted the file-media-object usage, ;; lets execute the touched-gc task, we should see that two of ;; them are marked to be deleted. - (let [task (:app.storage/gc-touched-task th/*system*) - res (task {:min-age (dt/duration 0)})] + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] (t/is (= 0 (:freeze res))) (t/is (= 2 (:delete res)))) ;; Finally, check that some of the objects that are marked as ;; deleted we are unable to retrieve them using standard storage ;; public api. + (t/is (nil? (sto/get-object storage (:media-id fmo5)))) + (t/is (nil? (sto/get-object storage (:media-id fmo4)))) + (t/is (nil? (sto/get-object storage (:media-id fmo3)))) (t/is (nil? (sto/get-object storage (:media-id fmo2)))) - (t/is (nil? (sto/get-object storage (:thumbnail-id fmo2)))) - (t/is (nil? (sto/get-object storage (:media-id fmo1)))) - (t/is (nil? (sto/get-object storage (:thumbnail-id fmo1)))) - ))) + (t/is (nil? (sto/get-object storage (:media-id fmo1))))))) + +(t/deftest file-gc-task-with-object-thumbnails + (letfn [(insert-file-object-thumbnail! [& {:keys [profile-id file-id page-id frame-id]}] + (let [object-id (thc/fmt-object-id file-id page-id frame-id "frame") + mfile {:filename "sample.jpg" + :path (th/tempfile "backend_tests/test_files/sample.jpg") + :mtype "image/jpeg" + :size 312043} + + params {::th/type :create-file-object-thumbnail + ::rpc/profile-id profile-id + :file-id file-id + :object-id object-id + :tag "frame" + :media mfile} + out (th/command! params)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (:result out))) + + (update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}] + (let [params {::th/type :update-file + ::rpc/profile-id profile-id + :id file-id + :session-id (uuid/random) + :revn revn + :features cfeat/supported-features + :changes changes} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (:result out)))] + + (let [storage (:app.storage/storage th/*system*) + profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + + file-id (get file :id) + page-id (first (get-in file [:data :pages])) + + frame-id-1 (uuid/random) + frame-id-2 (uuid/random) + + fot-1 (insert-file-object-thumbnail! :profile-id (:id profile) + :file-id file-id + :page-id page-id + :frame-id frame-id-1) + fot-2 (insert-file-object-thumbnail! :profile-id (:id profile) + :page-id page-id + :file-id file-id + :frame-id frame-id-2)] + + ;; Add a two frames + + (update-file! + :file-id (:id file) + :profile-id (:id profile) + :revn 0 + :changes + [{:type :add-obj + :page-id page-id + :id frame-id-1 + :parent-id uuid/zero + :frame-id uuid/zero + :obj (cts/setup-shape + {:id frame-id-2 + :name "Board" + :frame-id uuid/zero + :parent-id uuid/zero + :type :frame})} + + {:type :add-obj + :page-id page-id + :id frame-id-2 + :parent-id uuid/zero + :frame-id uuid/zero + :obj (cts/setup-shape + {:id frame-id-2 + :name "Board" + :frame-id uuid/zero + :parent-id uuid/zero + :type :frame})}]) + + ;; Check that reference storage objects are the same because of + ;; deduplication feature. + (t/is (= (:media-id fot-1) (:media-id fot-2))) + + ;; If we launch gc-touched-task, we should have 1 item to freeze + ;; because of the deduplication (we have uploaded 2 times the + ;; same files). + + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 1 (:freeze res))) + (t/is (= 0 (:delete res)))) + + ;; run the file-gc task immediately without forced min-age + (let [res (th/run-task! :file-gc)] + (t/is (= 0 (:processed res)))) + + ;; run the task again + (let [res (th/run-task! :file-gc {:min-age 0})] + (t/is (= 1 (:processed res)))) + + ;; retrieve file and check trimmed attribute + (let [row (th/db-get :file {:id (:id file)})] + (t/is (true? (:has-media-trimmed row)))) + + ;; check file media objects + (let [rows (th/db-exec! ["select * from file_tagged_object_thumbnail where file_id = ?" file-id])] + ;; (pp/pprint rows) + (t/is (= 2 (count rows)))) + + ;; check file media objects + (let [rows (th/db-exec! ["select * from storage_object where deleted_at is null"])] + ;; (pp/pprint rows) + (t/is (= 1 (count rows)))) + + ;; The underlying storage objects are available. + (t/is (some? (sto/get-object storage (:media-id fot-1)))) + (t/is (some? (sto/get-object storage (:media-id fot-2)))) + + ;; proceed to remove one frame + (update-file! + :file-id file-id + :profile-id (:id profile) + :revn 0 + :changes [{:type :del-obj + :page-id page-id + :id frame-id-2}]) + + (let [res (th/run-task! :file-gc {:min-age 0})] + (t/is (= 1 (:processed res)))) + + (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id file-id})] + (t/is (= 2 (count rows))) + (t/is (= 1 (count (remove (comp some? :deleted-at) rows)))) + (t/is (= (thc/fmt-object-id file-id page-id frame-id-1 "frame") + (-> rows first :object-id)))) + + ;; Now that file-gc have marked for deletion the object + ;; thumbnail lets execute the objects-gc task which remove + ;; the rows and mark as touched the storage object rows + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 3 (:processed res)))) + + ;; Now that objects-gc have deleted the object thumbnail lets + ;; execute the touched-gc task + (let [res (th/run-task! "storage-gc-touched" {:min-age 0})] + (t/is (= 1 (:freeze res)))) + + ;; check file media objects + (let [rows (th/db-query :storage-object {:deleted-at nil})] + ;; (pp/pprint rows) + (t/is (= 1 (count rows)))) + + ;; proceed to remove one frame + (update-file! + :file-id file-id + :profile-id (:id profile) + :revn 0 + :changes [{:type :del-obj + :page-id page-id + :id frame-id-1}]) + + (let [res (th/run-task! :file-gc {:min-age 0})] + (t/is (= 1 (:processed res)))) + + (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id file-id})] + (t/is (= 1 (count rows))) + (t/is (= 0 (count (remove (comp some? :deleted-at) rows))))) + + (let [res (th/run-task! :objects-gc {:min-age 0})] + ;; (pp/pprint res) + (t/is (= 2 (:processed res)))) + + ;; We still have th storage objects in the table + (let [rows (th/db-query :storage-object {:deleted-at nil})] + ;; (pp/pprint rows) + (t/is (= 1 (count rows)))) + + ;; Now that file-gc have deleted the object thumbnail lets + ;; execute the touched-gc task + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 1 (:delete res)))) + + ;; check file media objects + (let [rows (th/db-query :storage-object {:deleted-at nil})] + ;; (pp/pprint rows) + (t/is (= 0 (count rows))))))) + (t/deftest permissions-checks-creating-file (let [profile1 (th/create-profile* 1) @@ -442,8 +834,8 @@ error (:error out)] ;; (th/print-result! out) - (t/is (th/ex-info? error)) - (t/is (th/ex-of-type? error :not-found)))) + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found)))) (t/deftest permissions-checks-link-to-library-2 (let [profile1 (th/create-profile* 1) @@ -464,17 +856,16 @@ error (:error out)] ;; (th/print-result! out) - (t/is (th/ex-info? error)) - (t/is (th/ex-of-type? error :not-found)))) + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found)))) (t/deftest deletion - (let [task (:app.tasks.objects-gc/handler th/*system*) - profile1 (th/create-profile* 1) + (let [profile1 (th/create-profile* 1) file (th/create-file* 1 {:project-id (:default-project-id profile1) :profile-id (:id profile1)})] ;; file is not deleted because it does not meet all ;; conditions to be deleted. - (let [result (task {:min-age (dt/duration 0)})] + (let [result (th/run-task! :objects-gc {:min-age 0})] (t/is (= 0 (:processed result)))) ;; query the list of files @@ -505,7 +896,7 @@ (t/is (= 0 (count result))))) ;; run permanent deletion (should be noop) - (let [result (task {:min-age (dt/duration {:minutes 1})})] + (let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})] (t/is (= 0 (:processed result)))) ;; query the list of file libraries of a after hard deletion @@ -519,7 +910,7 @@ (t/is (= 0 (count result))))) ;; run permanent deletion - (let [result (task {:min-age (dt/duration 0)})] + (let [result (th/run-task! :objects-gc {:min-age 0})] (t/is (= 1 (:processed result)))) ;; query the list of file libraries of a after hard deletion @@ -531,8 +922,7 @@ (let [error (:error out) error-data (ex-data error)] (t/is (th/ex-info? error)) - (t/is (= (:type error-data) :not-found)))) - )) + (t/is (= (:type error-data) :not-found)))))) (t/deftest object-thumbnails-ops @@ -547,38 +937,42 @@ shape2-id (uuid/next) changes [{:type :add-obj - :page-id page-id - :id frame1-id - :parent-id uuid/zero - :frame-id uuid/zero - :obj {:id frame1-id - :use-for-thumbnail? true - :name "test-frame1" - :type :frame}} - {:type :add-obj - :page-id page-id - :id shape1-id - :parent-id frame1-id - :frame-id frame1-id - :obj {:id shape1-id - :name "test-shape1" - :type :rect}} - {:type :add-obj - :page-id page-id - :id frame2-id - :parent-id uuid/zero - :frame-id uuid/zero - :obj {:id frame2-id - :name "test-frame2" - :type :frame}} - {:type :add-obj - :page-id page-id - :id shape2-id - :parent-id frame2-id - :frame-id frame2-id - :obj {:id shape2-id - :name "test-shape2" - :type :rect}}]] + :page-id page-id + :id frame1-id + :parent-id uuid/zero + :frame-id uuid/zero + :obj (cts/setup-shape + {:id frame1-id + :use-for-thumbnail? true + :name "test-frame1" + :type :frame})} + {:type :add-obj + :page-id page-id + :id shape1-id + :parent-id frame1-id + :frame-id frame1-id + :obj (cts/setup-shape + {:id shape1-id + :name "test-shape1" + :type :rect})} + {:type :add-obj + :page-id page-id + :id frame2-id + :parent-id uuid/zero + :frame-id uuid/zero + :obj (cts/setup-shape + {:id frame2-id + :name "test-frame2" + :type :frame})} + {:type :add-obj + :page-id page-id + :id shape2-id + :parent-id frame2-id + :frame-id frame2-id + :obj (cts/setup-shape + {:id shape2-id + :name "test-shape2" + :type :rect})}]] ;; Update the file (th/update-file* {:file-id (:id file) :profile-id (:id prof) @@ -592,25 +986,25 @@ (let [data {::th/type :get-page ::rpc/profile-id (:id prof) :file-id (:id file) - :components-v2 true} + :features cfeat/supported-features} {:keys [error result] :as out} (th/command! data)] ;; (th/print-result! out) + (t/is (nil? error)) (t/is (map? result)) (t/is (contains? result :objects)) (t/is (contains? (:objects result) frame1-id)) (t/is (contains? (:objects result) shape1-id)) (t/is (contains? (:objects result) frame2-id)) (t/is (contains? (:objects result) shape2-id)) - (t/is (contains? (:objects result) uuid/zero)) - ) + (t/is (contains? (:objects result) uuid/zero))) ;; Query :page RPC method with page-id (let [data {::th/type :get-page ::rpc/profile-id (:id prof) :file-id (:id file) :page-id page-id - :components-v2 true} + :features cfeat/supported-features} {:keys [error result] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (map? result)) @@ -627,7 +1021,7 @@ :file-id (:id file) :page-id page-id :object-id frame1-id - :components-v2 true} + :features cfeat/supported-features} {:keys [error result] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (nil? error)) @@ -644,51 +1038,52 @@ ::rpc/profile-id (:id prof) :file-id (:id file) :object-id frame1-id - :components-v2 true} + :features cfeat/supported-features} out (th/command! data)] ;; (th/print-result! out) (t/is (not (th/success? out))) (let [{:keys [type code]} (-> out :error ex-data)] (t/is (= :validation type)) - (t/is (= :params-validation code)))) - - ) + (t/is (= :params-validation code))))) (t/testing "RPC :file-data-for-thumbnail" ;; Insert a thumbnail data for the frame-id - (let [data {::th/type :upsert-file-object-thumbnail + (let [data {::th/type :create-file-object-thumbnail ::rpc/profile-id (:id prof) :file-id (:id file) - :object-id (str page-id frame1-id) - :data "random-data-1"} - + :object-id (thc/fmt-object-id (:id file) page-id frame1-id "frame") + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} {:keys [error result] :as out} (th/command! data)] (t/is (nil? error)) - (t/is (nil? result))) + (t/is (map? result))) ;; Check the result (let [data {::th/type :get-file-data-for-thumbnail ::rpc/profile-id (:id prof) :file-id (:id file) - :components-v2 true} + :features cfeat/supported-features} {:keys [error result] :as out} (th/command! data)] ;; (th/print-result! out) + (t/is (nil? error)) (t/is (map? result)) (t/is (contains? result :page)) (t/is (contains? result :revn)) (t/is (contains? result :file-id)) (t/is (= (:id file) (:file-id result))) - (t/is (= "random-data-1" (get-in result [:page :objects frame1-id :thumbnail]))) + (t/is (str/starts-with? (get-in result [:page :objects frame1-id :thumbnail]) + "http://localhost:3449/assets/by-id/")) (t/is (= [] (get-in result [:page :objects frame1-id :shapes])))) ;; Delete thumbnail data - (let [data {::th/type :upsert-file-object-thumbnail + (let [data {::th/type :delete-file-object-thumbnail ::rpc/profile-id (:id prof) :file-id (:id file) - :object-id (str page-id frame1-id) - :data nil} + :object-id (thc/fmt-object-id (:id file) page-id frame1-id "frame")} {:keys [error result] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (nil? error)) @@ -698,7 +1093,7 @@ (let [data {::th/type :get-file-data-for-thumbnail ::rpc/profile-id (:id prof) :file-id (:id file) - :components-v2 true} + :features cfeat/supported-features} {:keys [error result] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (map? result)) @@ -712,140 +1107,121 @@ (t/testing "TASK :file-gc" ;; insert object snapshot for known frame - (let [data {::th/type :upsert-file-object-thumbnail + (let [data {::th/type :create-file-object-thumbnail ::rpc/profile-id (:id prof) :file-id (:id file) - :object-id (str page-id frame1-id) - :data "new-data"} + :object-id (thc/fmt-object-id (:id file) page-id frame1-id "frame") + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} {:keys [error result] :as out} (th/command! data)] (t/is (nil? error)) - (t/is (nil? result))) + (t/is (map? result))) ;; Wait to file be ellegible for GC (th/sleep 300) - ;; run the task again - (let [task (:app.tasks.file-gc/handler th/*system*) - res (task {:min-age (dt/duration 0)})] + ;; run the task + (let [res (th/run-task! :file-gc {:min-age 0})] (t/is (= 1 (:processed res)))) ;; check that object thumbnails are still here - (let [res (th/db-exec! ["select * from file_object_thumbnail"])] - (t/is (= 1 (count res))) - (t/is (= "new-data" (get-in res [0 :data])))) + (let [res (th/db-exec! ["select * from file_tagged_object_thumbnail"])] + ;; (th/print-result! res) + (t/is (= 1 (count res)))) ;; insert object snapshot for for unknown frame - (let [data {::th/type :upsert-file-object-thumbnail + (let [data {::th/type :create-file-object-thumbnail ::rpc/profile-id (:id prof) :file-id (:id file) - :object-id (str page-id (uuid/next)) - :data "new-data-2"} + :object-id (thc/fmt-object-id (:id file) page-id (uuid/next) "frame") + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} {:keys [error result] :as out} (th/command! data)] (t/is (nil? error)) - (t/is (nil? result))) + (t/is (map? result))) ;; Mark file as modified (th/db-exec! ["update file set has_media_trimmed=false where id=?" (:id file)]) ;; check that we have all object thumbnails - (let [res (th/db-exec! ["select * from file_object_thumbnail"])] + (let [res (th/db-exec! ["select * from file_tagged_object_thumbnail"])] (t/is (= 2 (count res)))) ;; run the task again - (let [task (:app.tasks.file-gc/handler th/*system*) - res (task {:min-age (dt/duration 0)})] + (let [res (th/run-task! :file-gc {:min-age 0})] (t/is (= 1 (:processed res)))) ;; check that the unknown frame thumbnail is deleted - (let [res (th/db-exec! ["select * from file_object_thumbnail"])] - (t/is (= 1 (count res))) - (t/is (= "new-data" (get-in res [0 :data]))))) + (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})] + (t/is (= 2 (count rows))) + (t/is (= 1 (count (remove (comp some? :deleted-at) rows))))) - )) + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 3 (:processed res)))) + (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})] + (t/is (= 1 (count rows))))))) (t/deftest file-thumbnail-ops (let [prof (th/create-profile* 1 {:is-active true}) file (th/create-file* 1 {:profile-id (:id prof) :project-id (:default-project-id prof) :revn 2 - :is-shared false}) - data {::th/type :get-file-thumbnail - ::rpc/profile-id (:id prof) - :file-id (:id file)}] + :is-shared false})] - (t/testing "query a thumbnail with single revn" - - ;; insert an entry on the database with a test value for the thumbnail of this frame - (th/db-insert! :file-thumbnail - {:file-id (:file-id data) - :revn 1 - :data "testvalue1"}) - - (let [{:keys [result error] :as out} (th/command! data)] - ;; (th/print-result! out) - (t/is (nil? error)) - (t/is (= 4 (count result))) - (t/is (= "testvalue1" (:data result))) - (t/is (= 1 (:revn result))))) - - (t/testing "query thumbnail with two revisions" - ;; insert an entry on the database with a test value for the thumbnail of this frame - (th/db-insert! :file-thumbnail - {:file-id (:file-id data) - :revn 2 - :data "testvalue2"}) - - (let [{:keys [result error] :as out} (th/command! data)] - ;; (th/print-result! out) - (t/is (nil? error)) - (t/is (= 4 (count result))) - (t/is (= "testvalue2" (:data result))) - (t/is (= 2 (:revn result)))) - - ;; Then query the specific revn - (let [{:keys [result error] :as out} (th/command! (assoc data :revn 1))] - ;; (th/print-result! out) - (t/is (nil? error)) - (t/is (= 4 (count result))) - (t/is (= "testvalue1" (:data result))) - (t/is (= 1 (:revn result))))) - - (t/testing "upsert file-thumbnail" - (let [data {::th/type :upsert-file-thumbnail + (t/testing "create a file thumbnail" + ;; insert object snapshot for known frame + (let [data {::th/type :create-file-thumbnail ::rpc/profile-id (:id prof) :file-id (:id file) - :data "foobar" - :props {:baz 1} - :revn 2} - {:keys [result error] :as out} (th/command! data)] - ;; (th/print-result! out) - (t/is (nil? error)) - (t/is (nil? result)))) + :revn 1 + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + {:keys [error result] :as out} (th/command! data)] - (t/testing "query last result" - (let [{:keys [result error] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (nil? error)) - (t/is (= 4 (count result))) - (t/is (= "foobar" (:data result))) - (t/is (= {:baz 1} (:props result))) - (t/is (= 2 (:revn result))))) + (t/is (map? result))) + + (let [data {::th/type :create-file-thumbnail + ::rpc/profile-id (:id prof) + :file-id (:id file) + :revn 2 + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + {:keys [error result] :as out} (th/command! data)] + + ;; (th/print-result! out) + (t/is (nil? error)) + (t/is (map? result))) + + (let [rows (th/db-query :file-thumbnail {:file-id (:id file)})] + (t/is (= 2 (count rows))))) (t/testing "gc task" ;; make the file eligible for GC waiting 300ms (configured ;; timeout for testing) - (th/sleep 300) - - ;; run the task again - (let [task (:app.tasks.file-gc/handler th/*system*) - res (task {:min-age (dt/duration 0)})] + (let [res (th/run-task! :file-gc {:min-age 0})] (t/is (= 1 (:processed res)))) - ;; Then query the specific revn - (let [{:keys [result error] :as out} (th/command! (assoc data :revn 1))] - (t/is (th/ex-of-type? error :not-found)) - (t/is (th/ex-of-code? error :file-thumbnail-not-found)))) - )) + (let [rows (th/db-query :file-thumbnail {:file-id (:id file)})] + (t/is (= 2 (count rows))) + (t/is (= 1 (count (remove (comp some? :deleted-at) rows))))) + + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 2 (:processed res)))) + + (let [rows (th/db-query :file-thumbnail {:file-id (:id file)})] + (t/is (= 1 (count rows))))))) + + diff --git a/backend/test/backend_tests/rpc_file_thumbnails_test.clj b/backend/test/backend_tests/rpc_file_thumbnails_test.clj index 14b0f72da7..f0cfc96375 100644 --- a/backend/test/backend_tests/rpc_file_thumbnails_test.clj +++ b/backend/test/backend_tests/rpc_file_thumbnails_test.clj @@ -6,6 +6,9 @@ (ns backend-tests.rpc-file-thumbnails-test (:require + [app.common.pprint :as pp] + [app.common.thumbnails :as thc] + [app.common.types.shape :as cts] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] @@ -18,7 +21,7 @@ [clojure.java.io :as io] [clojure.test :as t] [cuerdas.core :as str] - [datoteka.core :as fs] + [datoteka.fs :as fs] [mockery.core :refer [with-mocks]])) (t/use-fixtures :once th/state-init) @@ -46,11 +49,12 @@ :parent-id uuid/zero :frame-id uuid/zero :components-v2 true - :obj {:id shid - :name "Artboard" - :frame-id uuid/zero - :parent-id uuid/zero - :type :frame}}]) + :obj (cts/setup-shape + {:id shid + :name "Artboard" + :frame-id uuid/zero + :parent-id uuid/zero + :type :frame})}]) data1 {::th/type :create-file-object-thumbnail ::rpc/profile-id (:id profile) @@ -64,7 +68,7 @@ data2 {::th/type :create-file-object-thumbnail ::rpc/profile-id (:id profile) :file-id (:id file) - :object-id (str page-id shid) + :object-id (thc/fmt-object-id (:id file) page-id shid "frame") :media {:filename "sample.jpg" :size 7923 :path (th/tempfile "backend_tests/test_files/sample2.jpg") @@ -72,13 +76,17 @@ (let [out (th/command! data1)] (t/is (nil? (:error out))) - (t/is (nil? (:result out)))) + (t/is (map? (:result out)))) (let [out (th/command! data2)] (t/is (nil? (:error out))) - (t/is (nil? (:result out)))) + (t/is (map? (:result out)))) - (let [[row1 row2 :as rows] (th/db-query :file-object-thumbnail + ;; run the task again + (let [res (th/run-task! "storage-gc-touched" {:min-age 0})] + (t/is (= 2 (:freeze res)))) + + (let [[row1 row2 :as rows] (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)} {:order-by [[:created-at :asc]]})] @@ -106,11 +114,14 @@ ;; Run the File GC task that should remove unused file object ;; thumbnails - (let [result (th/run-task! :file-gc {:min-age (dt/duration 0)})] + (let [result (th/run-task! :file-gc {:min-age 0})] (t/is (= 1 (:processed result)))) + (let [result (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 2 (:processed result)))) + ;; check if row2 related thumbnail row still exists - (let [[row :as rows] (th/db-query :file-object-thumbnail + (let [[row :as rows] (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)} {:order-by [[:created-at :asc]]})] (t/is (= 1 (count rows))) @@ -119,27 +130,29 @@ (t/is (uuid? (:media-id row2)))) ;; Check if storage objects still exists after file-gc - (t/is (nil? (sto/get-object storage (:media-id row1)))) + (t/is (some? (sto/get-object storage (:media-id row1)))) (t/is (some? (sto/get-object storage (:media-id row2)))) + ;; run the task again + (let [res (th/run-task! "storage-gc-touched" {:min-age 0})] + (t/is (= 1 (:delete res))) + (t/is (= 0 (:freeze res)))) + ;; check that storage object is still exists but is marked as deleted - (let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted? false})] + (let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted false})] (t/is (some? (:deleted-at row)))) ;; Run the storage gc deleted task, it should permanently delete ;; all storage objects related to the deleted thumbnails - (let [result (th/run-task! :storage-gc-deleted {:min-age (dt/duration 0)})] + (let [result (th/run-task! :storage-gc-deleted {:min-age 0})] (t/is (= 1 (:deleted result)))) - ;; check that storage object is still exists but is marked as deleted - (let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted? false})] - (t/is (nil? row))) - + (t/is (nil? (sto/get-object storage (:media-id row1)))) (t/is (some? (sto/get-object storage (:media-id row2)))) - - ))) - + ;; check that storage object is still exists but is marked as deleted + (let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted false})] + (t/is (nil? row)))))) (t/deftest create-file-thumbnail (let [storage (::sto/storage th/*system*) @@ -149,14 +162,7 @@ :is-shared false :revn 3}) - data1 {::th/type :upsert-file-thumbnail - ::rpc/profile-id (:id profile) - :file-id (:id file) - :props {} - :revn 1 - :data "data:base64,1234123124"} - - data2 {::th/type :create-file-thumbnail + data1 {::th/type :create-file-thumbnail ::rpc/profile-id (:id profile) :file-id (:id file) :revn 2 @@ -165,7 +171,7 @@ :path (th/tempfile "backend_tests/test_files/sample2.jpg") :mtype "image/jpeg"}} - data3 {::th/type :create-file-thumbnail + data2 {::th/type :create-file-thumbnail ::rpc/profile-id (:id profile) :file-id (:id file) :revn 3 @@ -175,43 +181,34 @@ :mtype "image/jpeg"}}] (let [out (th/command! data1)] - (t/is (nil? (:error out))) - (t/is (nil? (:result out)))) - - (let [out (th/command! data2)] ;; (th/print-result! out) (t/is (nil? (:error out))) (t/is (contains? (:result out) :uri))) - (let [out (th/command! data3)] + (let [out (th/command! data2)] (t/is (nil? (:error out))) (t/is (contains? (:result out) :uri))) - (let [[row1 row2 row3 :as rows] (th/db-query :file-thumbnail - {:file-id (:id file)} - {:order-by [[:created-at :asc]]})] - (t/is (= 3 (count rows))) + (let [[row1 row2 :as rows] (th/db-query :file-thumbnail + {:file-id (:id file)} + {:order-by [[:revn :asc]]})] + (t/is (= 2 (count rows))) (t/is (= (:file-id data1) (:file-id row1))) (t/is (= (:revn data1) (:revn row1))) - (t/is (nil? (:media-id row1))) - + (t/is (uuid? (:media-id row1))) (t/is (= (:file-id data2) (:file-id row2))) (t/is (= (:revn data2) (:revn row2))) (t/is (uuid? (:media-id row2))) - (t/is (= (:file-id data3) (:file-id row3))) - (t/is (= (:revn data3) (:revn row3))) - (t/is (uuid? (:media-id row3))) - - (let [sobject (sto/get-object storage (:media-id row2)) + (let [sobject (sto/get-object storage (:media-id row1)) mobject (meta sobject)] (t/is (= "blake2b:05870e3f8ee885841ee3799924d80805179ab57e6fde84a605d1068fd3138de9" (:hash mobject))) (t/is (= "file-thumbnail" (:bucket mobject))) (t/is (= "image/jpeg" (:content-type mobject))) (t/is (= 7923 (:size sobject)))) - (let [sobject (sto/get-object storage (:media-id row3)) + (let [sobject (sto/get-object storage (:media-id row2)) mobject (meta sobject)] (t/is (= "blake2b:4fdb63b8f3ffc81256ea79f13e53f366723b188554b5afed91b20897c14a1a8e" (:hash mobject))) (t/is (= "file-thumbnail" (:bucket mobject))) @@ -220,39 +217,67 @@ ;; Run the File GC task that should remove unused file object ;; thumbnails - (let [result (th/run-task! :file-gc {:min-age (dt/duration 0)})] + (let [result (th/run-task! :file-gc {:min-age 0})] (t/is (= 1 (:processed result)))) - ;; check if row2 related thumbnail row still exists + (let [result (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 2 (:processed result)))) + + ;; check if row1 related thumbnail row still exists (let [[row :as rows] (th/db-query :file-thumbnail {:file-id (:id file)} {:order-by [[:created-at :asc]]})] (t/is (= 1 (count rows))) - (t/is (= (:file-id data2) (:file-id row))) - (t/is (= (:object-id data2) (:object-id row))) - (t/is (uuid? (:media-id row2)))) + (t/is (= (:file-id data1) (:file-id row))) + (t/is (= (:object-id data1) (:object-id row))) + (t/is (uuid? (:media-id row1)))) + + (let [result (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 1 (:delete result)))) ;; Check if storage objects still exists after file-gc (t/is (nil? (sto/get-object storage (:media-id row1)))) - (t/is (nil? (sto/get-object storage (:media-id row2)))) - (t/is (some? (sto/get-object storage (:media-id row3)))) + (t/is (some? (sto/get-object storage (:media-id row2)))) - (let [row (th/db-get :storage-object {:id (:media-id row2)} {::db/remove-deleted? false})] + (let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted false})] (t/is (some? (:deleted-at row)))) ;; Run the storage gc deleted task, it should permanently delete ;; all storage objects related to the deleted thumbnails - (let [result (th/run-task! :storage-gc-deleted {:min-age (dt/duration 0)})] + (let [result (th/run-task! :storage-gc-deleted {:min-age 0})] (t/is (= 1 (:deleted result)))) - ;; check that storage object is still exists but is marked as deleted - (let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted? false})] - (t/is (nil? row))) + (t/is (some? (sto/get-object storage (:media-id row2))))))) - (t/is (some? (sto/get-object storage (:media-id row3))))) +(t/deftest error-on-direct-storage-obj-deletion + (let [storage (::sto/storage th/*system*) + profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false + :revn 3}) + + data1 {::th/type :create-file-thumbnail + ::rpc/profile-id (:id profile) + :file-id (:id file) + :revn 2 + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}}] + + (let [out (th/command! data1)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (contains? (:result out) :uri))) + + (let [[row1 :as rows] (th/db-query :file-thumbnail {:file-id (:id file)})] + (t/is (= 1 (count rows))) + + (t/is (thrown? org.postgresql.util.PSQLException + (th/db-delete! :storage-object {:id (:media-id row1)})))))) - )) (t/deftest get-file-object-thumbnail (let [storage (::sto/storage th/*system*) @@ -261,52 +286,34 @@ :project-id (:default-project-id profile) :is-shared false}) - data1 {::th/type :upsert-file-object-thumbnail - ::rpc/profile-id (:id profile) - :file-id (:id file) - :object-id "test-key-1" - :data "data:base64,1234123124"} + data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile) + :file-id (:id file) + :object-id "test-key-2" + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}}] - data2 {::th/type :create-file-object-thumbnail - ::rpc/profile-id (:id profile) - :file-id (:id file) - :object-id "test-key-2" - :media {:filename "sample.jpg" - :size 7923 - :path (th/tempfile "backend_tests/test_files/sample2.jpg") - :mtype "image/jpeg"}}] - - (let [out (th/command! data1)] + (let [out (th/command! data)] (t/is (nil? (:error out))) - (t/is (nil? (:result out)))) + (t/is (map? (:result out)))) - (let [out (th/command! data2)] - (t/is (nil? (:error out))) - (t/is (nil? (:result out)))) - - (let [[row1 row2 :as rows] (th/db-query :file-object-thumbnail - {:file-id (:id file)} - {:order-by [[:created-at :asc]]})] - (t/is (= 2 (count rows))) - - (t/is (= (:file-id data1) (:file-id row1))) - (t/is (= (:object-id data1) (:object-id row1))) - (t/is (nil? (:media-id row1))) - (t/is (string? (:data row1))) - - (t/is (= (:file-id data2) (:file-id row2))) - (t/is (= (:object-id data2) (:object-id row2))) - (t/is (uuid? (:media-id row2)))) + (let [[row :as rows] (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file)} + {:order-by [[:created-at :asc]]})] + (t/is (= 1 (count rows))) + (t/is (= (:file-id data) (:file-id row))) + (t/is (= (:object-id data) (:object-id row))) + (t/is (uuid? (:media-id row)))) (let [params {::th/type :get-file-object-thumbnails ::rpc/profile-id (:id profile) :file-id (:id file)} out (th/command! params)] + ;; (th/print-result! out) + (let [result (:result out)] - (t/is (contains? result "test-key-1")) (t/is (contains? result "test-key-2")))))) - - - diff --git a/backend/test/backend_tests/rpc_font_test.clj b/backend/test/backend_tests/rpc_font_test.clj index d1c3bdd60a..2d64044351 100644 --- a/backend/test/backend_tests/rpc_font_test.clj +++ b/backend/test/backend_tests/rpc_font_test.clj @@ -92,3 +92,188 @@ :font-family :font-weight :font-style)))) + +(t/deftest font-deletion-1 + (let [prof (th/create-profile* 1 {:is-active true}) + team-id (:default-team-id prof) + proj-id (:default-project-id prof) + font-id (uuid/custom 10 1) + + data1 (-> (io/resource "backend_tests/test_files/font-1.woff") + io/input-stream + io/read-as-bytes) + + data2 (-> (io/resource "backend_tests/test_files/font-2.woff") + io/input-stream + io/read-as-bytes)] + + ;; Create front variant + (let [params {::th/type :create-font-variant + ::rpc/profile-id (:id prof) + :team-id team-id + :font-id font-id + :font-family "somefont" + :font-weight 400 + :font-style "normal" + :data {"font/woff" data1}} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out)))) + + (let [params {::th/type :create-font-variant + ::rpc/profile-id (:id prof) + :team-id team-id + :font-id font-id + :font-family "somefont" + :font-weight 500 + :font-style "normal" + :data {"font/woff" data2}} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out)))) + + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 6 (:freeze res)))) + + (let [params {::th/type :delete-font + ::rpc/profile-id (:id prof) + :team-id team-id + :id font-id} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 6 (:freeze res))) + (t/is (= 0 (:delete res)))) + + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 2 (:processed res)))) + + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 0 (:freeze res))) + (t/is (= 6 (:delete res)))))) + +(t/deftest font-deletion-2 + (let [prof (th/create-profile* 1 {:is-active true}) + team-id (:default-team-id prof) + proj-id (:default-project-id prof) + font-id (uuid/custom 10 1) + + data1 (-> (io/resource "backend_tests/test_files/font-1.woff") + io/input-stream + io/read-as-bytes) + + data2 (-> (io/resource "backend_tests/test_files/font-2.woff") + io/input-stream + io/read-as-bytes)] + + ;; Create front variant + (let [params {::th/type :create-font-variant + ::rpc/profile-id (:id prof) + :team-id team-id + :font-id font-id + :font-family "somefont" + :font-weight 400 + :font-style "normal" + :data {"font/woff" data1}} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out)))) + + (let [params {::th/type :create-font-variant + ::rpc/profile-id (:id prof) + :team-id team-id + :font-id (uuid/custom 10 2) + :font-family "somefont" + :font-weight 400 + :font-style "normal" + :data {"font/woff" data2}} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out)))) + + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 6 (:freeze res)))) + + (let [params {::th/type :delete-font + ::rpc/profile-id (:id prof) + :team-id team-id + :id font-id} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 3 (:freeze res))) + (t/is (= 0 (:delete res)))) + + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 1 (:processed res)))) + + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 0 (:freeze res))) + (t/is (= 3 (:delete res)))))) + +(t/deftest font-deletion-3 + (let [prof (th/create-profile* 1 {:is-active true}) + team-id (:default-team-id prof) + proj-id (:default-project-id prof) + font-id (uuid/custom 10 1) + + data1 (-> (io/resource "backend_tests/test_files/font-1.woff") + io/input-stream + io/read-as-bytes) + + data2 (-> (io/resource "backend_tests/test_files/font-2.woff") + io/input-stream + io/read-as-bytes) + params1 {::th/type :create-font-variant + ::rpc/profile-id (:id prof) + :team-id team-id + :font-id font-id + :font-family "somefont" + :font-weight 400 + :font-style "normal" + :data {"font/woff" data1}} + + params2 {::th/type :create-font-variant + ::rpc/profile-id (:id prof) + :team-id team-id + :font-id font-id + :font-family "somefont" + :font-weight 500 + :font-style "normal" + :data {"font/woff" data2}} + + out1 (th/command! params1) + out2 (th/command! params2)] + + ;; (th/print-result! out1) + (t/is (nil? (:error out1))) + (t/is (nil? (:error out2))) + + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 6 (:freeze res)))) + + (let [params {::th/type :delete-font-variant + ::rpc/profile-id (:id prof) + :team-id team-id + :id (-> out1 :result :id)} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 3 (:freeze res))) + (t/is (= 0 (:delete res)))) + + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 1 (:processed res)))) + + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] + (t/is (= 0 (:freeze res))) + (t/is (= 3 (:delete res)))))) diff --git a/backend/test/backend_tests/rpc_management_test.clj b/backend/test/backend_tests/rpc_management_test.clj index 82eb350b44..63018af333 100644 --- a/backend/test/backend_tests/rpc_management_test.clj +++ b/backend/test/backend_tests/rpc_management_test.clj @@ -6,6 +6,7 @@ (ns backend-tests.rpc-management-test (:require + [app.common.pprint :as pp] [app.common.uuid :as uuid] [app.db :as db] [app.http :as http] @@ -15,7 +16,7 @@ [backend-tests.storage-test :refer [configure-storage-backend]] [buddy.core.bytes :as b] [clojure.test :as t] - [datoteka.core :as fs])) + [datoteka.fs :as fs])) (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) @@ -68,12 +69,12 @@ (t/is (not= (:id file1) (:id result))) ;; Check that the new file has a correct file library relation - (let [[item :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id result)})] + (let [[item :as rows] (th/db-query :file-library-rel {:file-id (:id result)})] (t/is (= 1 (count rows))) (t/is (= (:id file2) (:library-file-id item)))) ;; Check that the new file has a correct file media objects - (let [[item :as rows] (db/query th/*pool* :file-media-object {:file-id (:id result)})] + (let [[item :as rows] (th/db-query :file-media-object {:file-id (:id result)})] (t/is (= 1 (count rows))) ;; Check that both items have different ids @@ -90,10 +91,8 @@ (t/is (not (contains? (get-in result [:data :media]) (:id mobj))))) ;; Check the total number of files - (let [rows (db/query th/*pool* :file {:project-id (:id project)})] - (t/is (= 3 (count rows)))) - - )))) + (let [rows (th/db-query :file {:project-id (:id project)})] + (t/is (= 3 (count rows)))))))) (t/deftest duplicate-file-with-deleted-relations (let [storage (-> (:app.storage/storage th/*system*) @@ -140,18 +139,16 @@ (t/is (not= (:id file1) (:id result))) ;; Check that there are no relation to a deleted library - (let [[item :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id result)})] + (let [[item :as rows] (th/db-query :file-library-rel {:file-id (:id result)})] (t/is (= 0 (count rows)))) ;; Check that the new file has no media objects - (let [[item :as rows] (db/query th/*pool* :file-media-object {:file-id (:id result)})] + (let [[item :as rows] (th/db-query :file-media-object {:file-id (:id result)})] (t/is (= 0 (count rows)))) ;; Check the total number of files - (let [rows (db/query th/*pool* :file {:project-id (:id project)})] - (t/is (= 3 (count rows)))) - - )))) + (let [rows (th/db-query :file {:project-id (:id project)})] + (t/is (= 3 (count rows)))))))) (t/deftest duplicate-project (let [storage (-> (:app.storage/storage th/*system*) @@ -199,16 +196,16 @@ (t/is (not= (:id project) (:id result))) ;; Check the total number of projects (previously is 2, now is 3) - (let [rows (db/query th/*pool* :project {:team-id (:default-team-id profile)})] + (let [rows (th/db-query :project {:team-id (:default-team-id profile)})] (t/is (= 3 (count rows)))) ;; Check that the new project has the same files - (let [p1-files (db/query th/*pool* :file - {:project-id (:id project)} - {:order-by [:name]}) - p2-files (db/query th/*pool* :file - {:project-id (:id result)} - {:order-by [:name]})] + (let [p1-files (th/db-query :file + {:project-id (:id project)} + {:order-by [:name]}) + p2-files (th/db-query :file + {:project-id (:id result)} + {:order-by [:name]})] (t/is (= (count p1-files) (count p2-files))) @@ -223,9 +220,7 @@ (when (= (:id fa) (:id file2)) (t/is (false? (b/equals? (:data fa) - (:data fb)))))) - - ))))) + (:data fb))))))))))) (t/deftest duplicate-project-with-deleted-files (let [storage (-> (:app.storage/storage th/*system*) @@ -265,16 +260,16 @@ (t/is (not= (:id project) (:id result))) ;; Check the total number of projects (previously is 2, now is 3) - (let [rows (db/query th/*pool* :project {:team-id (:default-team-id profile)})] + (let [rows (th/db-query :project {:team-id (:default-team-id profile)})] (t/is (= 3 (count rows)))) ;; Check that the new project has only the second file - (let [p1-files (db/query th/*pool* :file - {:project-id (:id project)} - {:order-by [:name]}) - p2-files (db/query th/*pool* :file - {:project-id (:id result)} - {:order-by [:name]})] + (let [p1-files (th/db-query :file + {:project-id (:id project)} + {:order-by [:name]}) + p2-files (th/db-query :file + {:project-id (:id result)} + {:order-by [:name]})] (t/is (= (count (rest p1-files)) (count p2-files))) @@ -289,9 +284,7 @@ (when (= (:id fa) (:id file2)) (t/is (false? (b/equals? (:data fa) - (:data fb)))))) - - ))))) + (:data fb))))))))))) (t/deftest move-file-on-same-team (let [profile (th/create-profile* 1 {:is-active true}) @@ -325,11 +318,11 @@ (t/is (th/ex-of-code? error :cant-move-to-same-project))) ;; initially project1 should have 2 files - (let [rows (db/query th/*pool* :file {:project-id (:id project1)})] + (let [rows (th/db-query :file {:project-id (:id project1)})] (t/is (= 2 (count rows)))) ;; initially project2 should be empty - (let [rows (db/query th/*pool* :file {:project-id (:id project2)})] + (let [rows (th/db-query :file {:project-id (:id project2)})] (t/is (= 0 (count rows)))) ;; move a file1 to project2 (in the same team) @@ -344,23 +337,22 @@ (t/is (nil? (:result out))) ;; project1 now should contain 1 file - (let [rows (db/query th/*pool* :file {:project-id (:id project1)})] + (let [rows (th/db-query :file {:project-id (:id project1)})] (t/is (= 1 (count rows)))) ;; project2 now should contain 1 file - (let [rows (db/query th/*pool* :file {:project-id (:id project2)})] + (let [rows (th/db-query :file {:project-id (:id project2)})] (t/is (= 1 (count rows)))) ;; file1 should be still linked to file2 - (let [[item :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file1)})] + (let [[item :as rows] (th/db-query :file-library-rel {:file-id (:id file1)})] (t/is (= 1 (count rows))) (t/is (= (:file-id item) (:id file1))) (t/is (= (:library-file-id item) (:id file2)))) ;; should be no libraries on file2 - (let [rows (db/query th/*pool* :file-library-rel {:file-id (:id file2)})] - (t/is (= 0 (count rows)))) - ))) + (let [rows (th/db-query :file-library-rel {:file-id (:id file2)})] + (t/is (= 0 (count rows))))))) ;; TODO: move a library to other team @@ -392,27 +384,27 @@ ;; --- initial data checks ;; the project1 should have 3 files - (let [rows (db/query th/*pool* :file {:project-id (:id project1)})] + (let [rows (th/db-query :file {:project-id (:id project1)})] (t/is (= 3 (count rows)))) ;; should be no files on project2 - (let [rows (db/query th/*pool* :file {:project-id (:id project2)})] + (let [rows (th/db-query :file {:project-id (:id project2)})] (t/is (= 0 (count rows)))) ;; the file1 should be linked to file2 - (let [[item :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file1)})] + (let [[item :as rows] (th/db-query :file-library-rel {:file-id (:id file1)})] (t/is (= 1 (count rows))) (t/is (= (:file-id item) (:id file1))) (t/is (= (:library-file-id item) (:id file2)))) ;; the file2 should be linked to file3 - (let [[item :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file2)})] + (let [[item :as rows] (th/db-query :file-library-rel {:file-id (:id file2)})] (t/is (= 1 (count rows))) (t/is (= (:file-id item) (:id file2))) (t/is (= (:library-file-id item) (:id file3)))) ;; should be no libraries on file3 - (let [rows (db/query th/*pool* :file-library-rel {:file-id (:id file3)})] + (let [rows (th/db-query :file-library-rel {:file-id (:id file3)})] (t/is (= 0 (count rows)))) ;; move to other project in other team @@ -426,27 +418,26 @@ (t/is (nil? (:result out))) ;; project1 now should have 2 file - (let [[item1 item2 :as rows] (db/query th/*pool* :file {:project-id (:id project1)} - {:order-by [:created-at]})] + (let [[item1 item2 :as rows] (th/db-query :file {:project-id (:id project1)} + {:order-by [:created-at]})] ;; (clojure.pprint/pprint rows) (t/is (= 2 (count rows))) (t/is (= (:id item1) (:id file2)))) ;; project2 now should have 1 file - (let [[item :as rows] (db/query th/*pool* :file {:project-id (:id project2)})] + (let [[item :as rows] (th/db-query :file {:project-id (:id project2)})] (t/is (= 1 (count rows))) (t/is (= (:id item) (:id file1)))) ;; the moved file1 should not have any link to libraries - (let [rows (db/query th/*pool* :file-library-rel {:file-id (:id file1)})] + (let [rows (th/db-query :file-library-rel {:file-id (:id file1)})] (t/is (zero? (count rows)))) ;; the file2 should still be linked to file3 - (let [[item :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file2)})] + (let [[item :as rows] (th/db-query :file-library-rel {:file-id (:id file2)})] (t/is (= 1 (count rows))) (t/is (= (:file-id item) (:id file2))) - (t/is (= (:library-file-id item) (:id file3)))) - ))) + (t/is (= (:library-file-id item) (:id file3))))))) (t/deftest move-library-to-other-team @@ -471,21 +462,21 @@ ;; --- initial data checks ;; the project1 should have 2 files - (let [rows (db/query th/*pool* :file {:project-id (:id project1)})] + (let [rows (th/db-query :file {:project-id (:id project1)})] (t/is (= 2 (count rows)))) ;; should be no files on project2 - (let [rows (db/query th/*pool* :file {:project-id (:id project2)})] + (let [rows (th/db-query :file {:project-id (:id project2)})] (t/is (= 0 (count rows)))) ;; the file1 should be linked to file2 - (let [[item :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file1)})] + (let [[item :as rows] (th/db-query :file-library-rel {:file-id (:id file1)})] (t/is (= 1 (count rows))) (t/is (= (:file-id item) (:id file1))) (t/is (= (:library-file-id item) (:id file2)))) ;; should be no libraries on file2 - (let [rows (db/query th/*pool* :file-library-rel {:file-id (:id file2)})] + (let [rows (th/db-query :file-library-rel {:file-id (:id file2)})] (t/is (= 0 (count rows)))) ;; move the library to other project @@ -499,25 +490,23 @@ (t/is (nil? (:result out))) ;; project1 now should have 1 file - (let [[item :as rows] (db/query th/*pool* :file {:project-id (:id project1)} - {:order-by [:created-at]})] + (let [[item :as rows] (th/db-query :file {:project-id (:id project1)} + {:order-by [:created-at]})] (t/is (= 1 (count rows))) (t/is (= (:id item) (:id file1)))) ;; project2 now should have 1 file - (let [[item :as rows] (db/query th/*pool* :file {:project-id (:id project2)})] + (let [[item :as rows] (th/db-query :file {:project-id (:id project2)})] (t/is (= 1 (count rows))) (t/is (= (:id item) (:id file2)))) ;; the file1 should not have any link to libraries - (let [rows (db/query th/*pool* :file-library-rel {:file-id (:id file1)})] + (let [rows (th/db-query :file-library-rel {:file-id (:id file1)})] (t/is (zero? (count rows)))) ;; the file2 should not have any link to libraries - (let [rows (db/query th/*pool* :file-library-rel {:file-id (:id file2)})] - (t/is (zero? (count rows)))) - - ))) + (let [rows (th/db-query :file-library-rel {:file-id (:id file2)})] + (t/is (zero? (count rows))))))) (t/deftest move-project (let [profile (th/create-profile* 1 {:is-active true}) @@ -549,16 +538,17 @@ ;; --- initial data checks ;; the project1 should have 2 files - (let [rows (db/query th/*pool* :file {:project-id (:id project1)})] + (let [rows (th/db-query :file {:project-id (:id project1)})] (t/is (= 2 (count rows)))) ;; the project2 should have 1 file - (let [rows (db/query th/*pool* :file {:project-id (:id project2)})] + (let [rows (th/db-query :file {:project-id (:id project2)})] (t/is (= 1 (count rows)))) ;; the file1 should be linked to file2 and file3 - (let [[item1 item2 :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file1)} - {:order-by [:created-at]})] + (let [[item1 item2 :as rows] (th/db-query :file-library-rel + {:file-id (:id file1)} + {:order-by [:created-at]})] (t/is (= 2 (count rows))) (t/is (= (:file-id item1) (:id file1))) (t/is (= (:library-file-id item1) (:id file2))) @@ -566,15 +556,14 @@ (t/is (= (:library-file-id item2) (:id file3)))) ;; the file2 should not be linked to any file - (let [[rows] (db/query th/*pool* :file-library-rel {:file-id (:id file2)})] + (let [[rows] (th/db-query :file-library-rel {:file-id (:id file2)})] (t/is (= 0 (count rows)))) ;; the file3 should not be linked to any file - (let [[rows] (db/query th/*pool* :file-library-rel {:file-id (:id file3)})] + (let [[rows] (th/db-query :file-library-rel {:file-id (:id file3)})] (t/is (= 0 (count rows)))) ;; move project1 to other team - ;; TODO: correct team change of project (let [data {::th/type :move-project ::rpc/profile-id (:id profile) :project-id (:id project1) @@ -585,26 +574,28 @@ (t/is (nil? (:result out))) ;; project1 now should still have 2 files - (let [[item1 item2 :as rows] (db/query th/*pool* :file {:project-id (:id project1)} - {:order-by [:created-at]})] + (let [[item1 item2 :as rows] (th/db-query :file + {:project-id (:id project1)} + {:order-by [:created-at]})] ;; (clojure.pprint/pprint rows) (t/is (= 2 (count rows))) (t/is (= (:id item1) (:id file1))) (t/is (= (:id item2) (:id file2)))) ;; project2 now should still have 1 file - (let [[item :as rows] (db/query th/*pool* :file {:project-id (:id project2)})] + (let [[item :as rows] (th/db-query :file {:project-id (:id project2)})] + ;; (pp/pprint rows) (t/is (= 1 (count rows))) (t/is (= (:id item) (:id file3)))) ;; the file1 should be linked to file2 but not file3 - (let [[item1 :as rows] (db/query th/*pool* :file-library-rel {:file-id (:id file1)} - {:order-by [:created-at]})] + (let [[item1 :as rows] (th/db-query :file-library-rel + {:file-id (:id file1)} + {:order-by [:created-at]})] + (t/is (= 1 (count rows))) (t/is (= (:file-id item1) (:id file1))) - (t/is (= (:library-file-id item1) (:id file2)))) - - ))) + (t/is (= (:library-file-id item1) (:id file2))))))) (t/deftest clone-template (let [prof (th/create-profile* 1 {:is-active true}) @@ -618,13 +609,15 @@ (t/is (nil? (:error out))) (let [result (:result out)] - (t/is (set? result)) - (t/is (uuid? (first result))) - (t/is (= 1 (count result)))))) + (t/is (fn? result)) -(t/deftest retrieve-list-of-buitin-templates + (let [events (th/consume-sse result)] + (t/is (= 6 (count events))) + (t/is (= :end (first (last events)))))))) + +(t/deftest get-list-of-buitin-templates (let [prof (th/create-profile* 1 {:is-active true}) - data {::th/type :retrieve-list-of-builtin-templates + data {::th/type :get-builtin-templates ::rpc/profile-id (:id prof)} out (th/command! data)] ;; (th/print-result! out) diff --git a/backend/test/backend_tests/rpc_media_test.clj b/backend/test/backend_tests/rpc_media_test.clj index f5c372a899..5147b1e12f 100644 --- a/backend/test/backend_tests/rpc_media_test.clj +++ b/backend/test/backend_tests/rpc_media_test.clj @@ -12,7 +12,7 @@ [app.storage :as sto] [backend-tests.helpers :as th] [clojure.test :as t] - [datoteka.core :as fs])) + [datoteka.fs :as fs])) (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) @@ -88,8 +88,7 @@ (t/is (sto/object? mobj1)) (t/is (sto/object? mobj2)) (t/is (= 312043 (:size mobj1))) - (t/is (= 3887 (:size mobj2))))) - )) + (t/is (= 3887 (:size mobj2))))))) (t/deftest media-object-upload-idempotency @@ -208,8 +207,7 @@ (t/is (sto/object? mobj1)) (t/is (sto/object? mobj2)) (t/is (= 312043 (:size mobj1))) - (t/is (= 3887 (:size mobj2))))) - )) + (t/is (= 3887 (:size mobj2))))))) (t/deftest media-object-upload-idempotency-command diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index d7180461bb..cbaff60380 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -6,11 +6,11 @@ (ns backend-tests.rpc-profile-test (:require + [app.auth :as auth] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] [app.rpc :as-alias rpc] - [app.auth :as auth] [app.rpc.commands.profile :as profile] [app.tokens :as tokens] [app.util.time :as dt] @@ -18,7 +18,7 @@ [clojure.java.io :as io] [clojure.test :as t] [cuerdas.core :as str] - [datoteka.core :as fs] + [datoteka.fs :as fs] [mockery.core :refer [with-mocks]])) ;; TODO: profile deletion with teams @@ -116,8 +116,7 @@ out (th/command! data)] ;; (th/print-result! out) - (t/is (nil? (:error out))))) - )) + (t/is (nil? (:error out))))))) (t/deftest profile-deletion-simple (let [prof (th/create-profile* 1) @@ -127,7 +126,7 @@ ;; profile is not deleted because it does not meet all ;; conditions to be deleted. - (let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})] + (let [result (th/run-task! :objects-gc {:min-age 0})] (t/is (= 0 (:processed result)))) ;; Request profile to be deleted @@ -146,12 +145,20 @@ (t/is (= 1 (count (:result out))))) ;; execute permanent deletion task - (let [result (th/run-task! :objects-gc {:min-age (dt/duration "-1m")})] - (t/is (= 2 (:processed result)))) + (let [result (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 1 (:processed result)))) (let [row (th/db-get :team {:id (:default-team-id prof)} - {::db/remove-deleted? false})] + {::db/remove-deleted false})] + (t/is (nil? (:deleted-at row)))) + + (let [result (th/run-task! :orphan-teams-gc {:min-age 0})] + (t/is (= 1 (:processed result)))) + + (let [row (th/db-get :team + {:id (:default-team-id prof)} + {::db/remove-deleted false})] (t/is (dt/instant? (:deleted-at row)))) ;; query profile after delete @@ -162,68 +169,6 @@ (let [result (:result out)] (t/is (= uuid/zero (:id result))))))) -(t/deftest profile-immediate-deletion - (let [prof1 (th/create-profile* 1) - prof2 (th/create-profile* 2) - file (th/create-file* 1 {:profile-id (:id prof1) - :project-id (:default-project-id prof1) - :is-shared false}) - - team (th/create-team* 1 {:profile-id (:id prof1)}) - _ (th/create-team-role* {:team-id (:id team) - :profile-id (:id prof2) - :role :admin})] - - ;; profile is not deleted because it does not meet all - ;; conditions to be deleted. - (let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})] - (t/is (= 0 (:orphans result))) - (t/is (= 0 (:processed result)))) - - ;; just delete the profile - (th/db-delete! :profile {:id (:id prof1)}) - - ;; query files after profile deletion, expecting not found - (let [params {::th/type :get-project-files - ::rpc/profile-id (:id prof1) - :project-id (:default-project-id prof1)} - out (th/command! params)] - ;; (th/print-result! out) - (t/is (not (th/success? out))) - (let [edata (-> out :error ex-data)] - (t/is (= :not-found (:type edata))))) - - ;; the files and projects still exists on the database - (let [files (th/db-query :file {:project-id (:default-project-id prof1)}) - projects (th/db-query :project {:team-id (:default-team-id prof1)})] - (t/is (= 1 (count files))) - (t/is (= 1 (count projects)))) - - ;; execute the gc task - (let [result (th/run-task! :objects-gc {:min-age (dt/duration "-1m")})] - (t/is (= 1 (:processed result))) - (t/is (= 1 (:orphans result)))) - - ;; Check the deletion flag on the default profile team - (let [row (th/db-get :team - {:id (:default-team-id prof1)} - {::db/remove-deleted? false})] - (t/is (dt/instant? (:deleted-at row)))) - - ;; Check the deletion flag on the shared team - (let [row (th/db-get :team - {:id (:id team)} - {::db/remove-deleted? false})] - (t/is (nil? (:deleted-at row)))) - - ;; Check the roles on the shared team - (let [rows (th/db-query :team-profile-rel {:team-id (:id team)})] - (t/is (= 1 (count rows))) - (t/is (= (:id prof2) (get-in rows [0 :profile-id]))) - (t/is (= false (get-in rows [0 :is-owner])))) - - )) - (t/deftest registration-domain-whitelist (let [whitelist #{"gmail.com" "hey.com" "ya.ru"}] (t/testing "allowed email domain" @@ -334,10 +279,7 @@ :accept-newsletter-subscription true} out (th/command! data)] (t/is (th/success? out)) - (t/is (= 1 (:call-count @mock)))) - - )) - )) + (t/is (= 1 (:call-count @mock)))))))) (t/deftest prepare-and-register-with-invitation-and-disabled-registration-1 @@ -389,8 +331,7 @@ (t/is (not (th/success? out))) (let [edata (-> out :error ex-data)] (t/is (= :restriction (:type edata))) - (t/is (= :email-does-not-match-invitation (:code edata)))) - ))) + (t/is (= :email-does-not-match-invitation (:code edata))))))) (t/deftest prepare-register-with-registration-disabled (with-redefs [app.config/flags #{}] @@ -411,10 +352,10 @@ :password "foobar"} out (th/command! data)] - (t/is (not (th/success? out))) - (let [edata (-> out :error ex-data)] - (t/is (= :validation (:type edata))) - (t/is (= :email-already-exists (:code edata)))))) + (t/is (not (th/success? out))) + (let [edata (-> out :error ex-data)] + (t/is (= :validation (:type edata))) + (t/is (= :email-already-exists (:code edata)))))) (t/deftest register-profile-with-bounced-email (let [pool (:app.db/pool th/*system*) @@ -550,9 +491,7 @@ (t/is (= 2 (:call-count @mock))) (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :validation)) - (t/is (th/ex-of-code? error :email-has-permanent-bounces))) - - ))) + (t/is (th/ex-of-code? error :email-has-permanent-bounces)))))) (t/deftest update-profile-password @@ -563,8 +502,7 @@ :password "foobarfoobar"} out (th/command! data)] (t/is (nil? (:error out))) - (t/is (nil? (:result out))) - )) + (t/is (nil? (:result out))))) (t/deftest update-profile-password-bad-old-password diff --git a/backend/test/backend_tests/rpc_project_test.clj b/backend/test/backend_tests/rpc_project_test.clj index 30733f83ab..f35105a97f 100644 --- a/backend/test/backend_tests/rpc_project_test.clj +++ b/backend/test/backend_tests/rpc_project_test.clj @@ -6,12 +6,12 @@ (ns backend-tests.rpc-project-test (:require - [backend-tests.helpers :as th] [app.common.uuid :as uuid] [app.db :as db] - [app.rpc :as-alias rpc] [app.http :as http] + [app.rpc :as-alias rpc] [app.util.time :as dt] + [backend-tests.helpers :as th] [clojure.test :as t])) (t/use-fixtures :once th/state-init) @@ -104,8 +104,7 @@ ;; (th/print-result! out) (t/is (nil? (:error out))) (let [result (:result out)] - (t/is (= 1 (count result))))) - )) + (t/is (= 1 (count result))))))) (t/deftest permissions-checks-create-project (let [profile1 (th/create-profile* 1) @@ -173,14 +172,13 @@ (t/deftest test-deletion - (let [task (:app.tasks.objects-gc/handler th/*system*) - profile1 (th/create-profile* 1) + (let [profile1 (th/create-profile* 1) project (th/create-project* 1 {:team-id (:default-team-id profile1) :profile-id (:id profile1)})] ;; project is not deleted because it does not meet all ;; conditions to be deleted. - (let [result (task {:min-age (dt/duration 0)})] + (let [result (th/run-task! :objects-gc {:min-age 0})] (t/is (= 0 (:processed result)))) ;; query the list of projects @@ -188,6 +186,7 @@ ::rpc/profile-id (:id profile1) :team-id (:default-team-id profile1)} out (th/command! data)] + ;; (th/print-result! out) (t/is (nil? (:error out))) (let [result (:result out)] @@ -211,7 +210,7 @@ (t/is (= 1 (count result))))) ;; run permanent deletion (should be noop) - (let [result (task {:min-age (dt/duration {:minutes 1})})] + (let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})] (t/is (= 0 (:processed result)))) ;; query the list of files of a after soft deletion @@ -225,7 +224,7 @@ (t/is (= 0 (count result))))) ;; run permanent deletion - (let [result (task {:min-age (dt/duration 0)})] + (let [result (th/run-task! :objects-gc {:min-age 0})] (t/is (= 1 (:processed result)))) ;; query the list of files of a after hard deletion @@ -237,5 +236,4 @@ (let [error (:error out) error-data (ex-data error)] (t/is (th/ex-info? error)) - (t/is (= (:type error-data) :not-found)))) - )) + (t/is (= (:type error-data) :not-found)))))) diff --git a/backend/test/backend_tests/rpc_quotes_test.clj b/backend/test/backend_tests/rpc_quotes_test.clj index 08918ab1bd..5907da58b8 100644 --- a/backend/test/backend_tests/rpc_quotes_test.clj +++ b/backend/test/backend_tests/rpc_quotes_test.clj @@ -14,7 +14,7 @@ [app.rpc.quotes :as-alias quotes] [backend-tests.helpers :as th] [clojure.test :as t] - [datoteka.core :as fs] + [datoteka.fs :as fs] [mockery.core :refer [with-mocks]])) (t/use-fixtures :once th/state-init) @@ -123,9 +123,7 @@ :quote 5}) (check-ok! 4) - (check-ko! 5) - - ))) + (check-ko! 5)))) (t/deftest invitations-per-team-quote (with-mocks [mock {:target 'app.config/get @@ -268,7 +266,7 @@ :team-id (:default-team-id profile-2)}) data {::th/type :create-file ::rpc/profile-id (:id profile-1) - :project-id (:id project-1)} + :project-id (:id project-1)} check-ok! (fn [n] (let [data (assoc data :name (str "file" n)) out (th/command! data)] @@ -339,6 +337,4 @@ :quote 4}) (check-ok! 4) - (check-ko! 5) - - ))) + (check-ko! 5)))) diff --git a/backend/test/backend_tests/rpc_team_test.clj b/backend/test/backend_tests/rpc_team_test.clj index 1685837ae1..65acef49d8 100644 --- a/backend/test/backend_tests/rpc_team_test.clj +++ b/backend/test/backend_tests/rpc_team_test.clj @@ -16,7 +16,7 @@ [app.util.time :as dt] [backend-tests.helpers :as th] [clojure.test :as t] - [datoteka.core :as fs] + [datoteka.fs :as fs] [mockery.core :refer [with-mocks]])) (t/use-fixtures :once th/state-init) @@ -100,9 +100,7 @@ (let [edata (-> out :error ex-data)] (t/is (= :validation (:type edata))) - (t/is (= :member-is-muted (:code edata))))) - - ))) + (t/is (= :member-is-muted (:code edata)))))))) (t/deftest invitation-tokens @@ -159,9 +157,7 @@ (t/is (= :editor (:role claims))) (t/is (= (:id team) (:team-id claims))) (t/is (= (first (:emails data)) (:member-email claims))) - (t/is (= (:id profile2) (:member-id claims))))) - - ))) + (t/is (= (:id profile2) (:member-id claims)))))))) (t/deftest accept-invitation-tokens @@ -243,9 +239,7 @@ (t/is (not (th/success? out))) (let [edata (-> out :error ex-data)] (t/is (= :validation (:type edata))) - (t/is (= :invalid-token (:code edata)))))) - - ))) + (t/is (= :invalid-token (:code edata))))))))) (t/deftest create-team-invitations-with-email-verification-disabled (with-mocks [mock {:target 'app.email/send! :return nil}] @@ -275,77 +269,6 @@ (t/is (= 1 (count members))) (t/is (true? (-> members first :can-edit)))))))) -(t/deftest team-deletion - (let [profile1 (th/create-profile* 1 {:is-active true}) - team (th/create-team* 1 {:profile-id (:id profile1)}) - pool (:app.db/pool th/*system*) - data {::th/type :delete-team - ::rpc/profile-id (:id profile1) - :team-id (:id team)}] - - ;; team is not deleted because it does not meet all - ;; conditions to be deleted. - (let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})] - (t/is (= 0 (:processed result)))) - - ;; query the list of teams - (let [data {::th/type :get-teams - ::rpc/profile-id (:id profile1)} - out (th/command! data)] - ;; (th/print-result! out) - (t/is (th/success? out)) - (let [result (:result out)] - (t/is (= 2 (count result))) - (t/is (= (:id team) (get-in result [1 :id]))) - (t/is (= (:default-team-id profile1) (get-in result [0 :id]))))) - - ;; Request team to be deleted - (let [params {::th/type :delete-team - ::rpc/profile-id (:id profile1) - :id (:id team)} - out (th/command! params)] - (t/is (th/success? out))) - - ;; query the list of teams after soft deletion - (let [data {::th/type :get-teams - ::rpc/profile-id (:id profile1)} - out (th/command! data)] - ;; (th/print-result! out) - (t/is (th/success? out)) - (let [result (:result out)] - (t/is (= 1 (count result))) - (t/is (= (:default-team-id profile1) (get-in result [0 :id]))))) - - ;; run permanent deletion (should be noop) - (let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})] - (t/is (= 0 (:processed result)))) - - ;; query the list of projects after hard deletion - (let [data {::th/type :get-projects - ::rpc/profile-id (:id profile1) - :team-id (:id team)} - out (th/command! data)] - ;; (th/print-result! out) - (t/is (not (th/success? out))) - (let [edata (-> out :error ex-data)] - (t/is (= :not-found (:type edata))))) - - ;; run permanent deletion - (let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})] - (t/is (= 1 (:processed result)))) - - ;; query the list of projects of a after hard deletion - (let [data {::th/type :get-projects - ::rpc/profile-id (:id profile1) - :team-id (:id team)} - out (th/command! data)] - ;; (th/print-result! out) - - (t/is (not (th/success? out))) - (let [edata (-> out :error ex-data)] - (t/is (= :not-found (:type edata))))) - )) - (t/deftest query-team-invitations (let [prof (th/create-profile* 1 {:is-active true}) team (th/create-team* 1 {:profile-id (:id prof)}) @@ -425,3 +348,118 @@ (t/is (th/success? out)) (t/is (nil? (:result out))) (t/is (nil? res))))) + + +(t/deftest team-deletion-1 + (let [profile1 (th/create-profile* 1 {:is-active true}) + team (th/create-team* 1 {:profile-id (:id profile1)}) + pool (:app.db/pool th/*system*) + data {::th/type :delete-team + ::rpc/profile-id (:id profile1) + :team-id (:id team)}] + + ;; team is not deleted because it does not meet all + ;; conditions to be deleted. + (let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})] + (t/is (= 0 (:processed result)))) + + ;; query the list of teams + (let [data {::th/type :get-teams + ::rpc/profile-id (:id profile1)} + out (th/command! data)] + ;; (th/print-result! out) + (t/is (th/success? out)) + (let [result (:result out)] + (t/is (= 2 (count result))) + (t/is (= (:id team) (get-in result [1 :id]))) + (t/is (= (:default-team-id profile1) (get-in result [0 :id]))))) + + ;; Request team to be deleted + (let [params {::th/type :delete-team + ::rpc/profile-id (:id profile1) + :id (:id team)} + out (th/command! params)] + (t/is (th/success? out))) + + ;; query the list of teams after soft deletion + (let [data {::th/type :get-teams + ::rpc/profile-id (:id profile1)} + out (th/command! data)] + ;; (th/print-result! out) + (t/is (th/success? out)) + (let [result (:result out)] + (t/is (= 1 (count result))) + (t/is (= (:default-team-id profile1) (get-in result [0 :id]))))) + + ;; run permanent deletion (should be noop) + (let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})] + (t/is (= 0 (:processed result)))) + + ;; query the list of projects after hard deletion + (let [data {::th/type :get-projects + ::rpc/profile-id (:id profile1) + :team-id (:id team)} + out (th/command! data)] + ;; (th/print-result! out) + (t/is (not (th/success? out))) + (let [edata (-> out :error ex-data)] + (t/is (= :not-found (:type edata))))) + + ;; run permanent deletion + (let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})] + (t/is (= 2 (:processed result)))) + + ;; query the list of projects of a after hard deletion + (let [data {::th/type :get-projects + ::rpc/profile-id (:id profile1) + :team-id (:id team)} + out (th/command! data)] + ;; (th/print-result! out) + + (t/is (not (th/success? out))) + (let [edata (-> out :error ex-data)] + (t/is (= :not-found (:type edata))))))) + + +(t/deftest team-deletion-2 + (let [storage (-> (:app.storage/storage th/*system*) + (assoc ::sto/backend :assets-fs)) + prof (th/create-profile* 1) + + team (th/create-team* 1 {:profile-id (:id prof)}) + + proj (th/create-project* 1 {:profile-id (:id prof) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id prof) + :project-id (:default-project-id team) + :is-shared false}) + + mfile {:filename "sample.jpg" + :path (th/tempfile "backend_tests/test_files/sample.jpg") + :mtype "image/jpeg" + :size 312043}] + + + (let [params {::th/type :upload-file-media-object + ::rpc/profile-id (:id prof) + :file-id (:id file) + :is-local true + :name "testfile" + :content mfile} + + out (th/command! params)] + (t/is (nil? (:error out)))) + + (let [params {::th/type :delete-team + ::rpc/profile-id (:id prof) + :id (:id team)} + out (th/command! params)] + #_(th/print-result! out) + (t/is (nil? (:error out)))) + + (let [rows (th/db-exec! ["select * from team where id = ?" (:id team)])] + (t/is (= 1 (count rows))) + (t/is (dt/instant? (:deleted-at (first rows))))) + + (let [result (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 5 (:processed result)))))) diff --git a/backend/test/backend_tests/rpc_viewer_test.clj b/backend/test/backend_tests/rpc_viewer_test.clj index 50333d860d..6c68c12e34 100644 --- a/backend/test/backend_tests/rpc_viewer_test.clj +++ b/backend/test/backend_tests/rpc_viewer_test.clj @@ -6,12 +6,12 @@ (ns backend-tests.rpc-viewer-test (:require - [backend-tests.helpers :as th] [app.common.uuid :as uuid] [app.db :as db] [app.rpc :as-alias rpc] + [backend-tests.helpers :as th] [clojure.test :as t] - [datoteka.core :as fs])) + [datoteka.fs :as fs])) (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) @@ -104,6 +104,4 @@ (t/is (nil? (:error out))) (let [result (:result out)] (t/is (contains? result :file)) - (t/is (contains? result :project))))) - - )) + (t/is (contains? result :project))))))) diff --git a/backend/test/backend_tests/rpc_webhooks_test.clj b/backend/test/backend_tests/rpc_webhooks_test.clj index 8b91262dc5..76c3de763c 100644 --- a/backend/test/backend_tests/rpc_webhooks_test.clj +++ b/backend/test/backend_tests/rpc_webhooks_test.clj @@ -140,9 +140,7 @@ error-data (ex-data error)] (t/is (th/ex-info? error)) (t/is (= (:type error-data) :not-found)) - (t/is (= (:code error-data) :object-not-found))))) - - ))) + (t/is (= (:code error-data) :object-not-found)))))))) (t/deftest webhooks-quotes (with-mocks [http-mock {:target 'app.http.client/req! diff --git a/backend/test/backend_tests/storage_test.clj b/backend/test/backend_tests/storage_test.clj index ee6045d306..7e21ec970e 100644 --- a/backend/test/backend_tests/storage_test.clj +++ b/backend/test/backend_tests/storage_test.clj @@ -15,7 +15,7 @@ [backend-tests.helpers :as th] [clojure.test :as t] [cuerdas.core :as str] - [datoteka.core :as fs] + [datoteka.fs :as fs] [datoteka.io :as io] [mockery.core :refer [with-mocks]])) @@ -50,8 +50,7 @@ (t/is (= "data" (:other (meta object)))) (t/is (= "text/plain" (:content-type (meta object)))) (t/is (= "content" (slurp (sto/get-object-data storage object)))) - (t/is (= "content" (slurp (sto/get-object-path storage object)))) - )) + (t/is (= "content" (slurp (sto/get-object-path storage object)))))) (t/deftest put-and-retrieve-expired-object (let [storage (-> (:app.storage/storage th/*system*) @@ -59,8 +58,7 @@ content (sto/content "content") object (sto/put-object! storage {::sto/content content ::sto/expired-at (dt/in-future {:seconds 1}) - :content-type "text/plain" - })] + :content-type "text/plain"})] (t/is (sto/object? object)) (t/is (dt/instant? (:expired-at object))) @@ -71,8 +69,7 @@ (t/is (nil? (sto/get-object storage (:id object)))) (t/is (nil? (sto/get-object-data storage object))) (t/is (nil? (sto/get-object-url storage object))) - (t/is (nil? (sto/get-object-path storage object))) - )) + (t/is (nil? (sto/get-object-path storage object))))) (t/deftest put-and-delete-object (let [storage (-> (:app.storage/storage th/*system*) @@ -92,8 +89,7 @@ ;; But you can't retrieve the object again because in database is ;; marked as deleted/expired. - (t/is (nil? (sto/get-object storage (:id object)))) - )) + (t/is (nil? (sto/get-object storage (:id object)))))) (t/deftest test-deleted-gc-task (let [storage (-> (:app.storage/storage th/*system*) @@ -103,16 +99,13 @@ content3 (sto/content "content3") object1 (sto/put-object! storage {::sto/content content1 ::sto/expired-at (dt/now) - :content-type "text/plain" - }) + :content-type "text/plain"}) object2 (sto/put-object! storage {::sto/content content2 ::sto/expired-at (dt/in-past {:hours 2}) - :content-type "text/plain" - }) + :content-type "text/plain"}) object3 (sto/put-object! storage {::sto/content content3 ::sto/expired-at (dt/in-past {:hours 1}) - :content-type "text/plain" - })] + :content-type "text/plain"})] (th/sleep 200) @@ -120,7 +113,7 @@ (let [res (th/run-task! :storage-gc-deleted {})] (t/is (= 1 (:deleted res)))) - (let [res (db/exec-one! th/*pool* ["select count(*) from storage_object;"])] + (let [res (th/db-exec-one! ["select count(*) from storage_object;"])] (t/is (= 2 (:count res)))))) (t/deftest test-touched-gc-task-1 @@ -163,31 +156,34 @@ (t/is (= (:media-id result-1) (:media-id result-2))) - ;; now we proceed to manually delete one file-media-object - (db/exec-one! th/*pool* ["delete from file_media_object where id = ?" (:id result-1)]) + (th/db-update! :file-media-object + {:deleted-at (dt/now)} + {:id (:id result-1)}) + + ;; run the objects gc task for permanent deletion + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 1 (:processed res)))) ;; check that we still have all the storage objects - (let [res (db/exec-one! th/*pool* ["select count(*) from storage_object"])] + (let [res (th/db-exec-one! ["select count(*) from storage_object"])] (t/is (= 2 (:count res)))) ;; now check if the storage objects are touched - (let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where touched_at is not null"])] + (let [res (th/db-exec-one! ["select count(*) from storage_object where touched_at is not null"])] (t/is (= 2 (:count res)))) ;; run the touched gc task - (let [task (:app.storage/gc-touched-task th/*system*) - res (task {})] + (let [res (th/run-task! :storage-gc-touched {})] (t/is (= 2 (:freeze res))) (t/is (= 0 (:delete res)))) ;; now check that there are no touched objects - (let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where touched_at is not null"])] + (let [res (th/db-exec-one! ["select count(*) from storage_object where touched_at is not null"])] (t/is (= 0 (:count res)))) ;; now check that all objects are marked to be deleted - (let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where deleted_at is not null"])] - (t/is (= 0 (:count res)))) - ))) + (let [res (th/db-exec-one! ["select count(*) from storage_object where deleted_at is not null"])] + (t/is (= 0 (:count res))))))) (t/deftest test-touched-gc-task-2 @@ -239,31 +235,35 @@ (t/is (nil? (:error out2))) ;; run the touched gc task - (let [task (:app.storage/gc-touched-task th/*system*) - res (task {})] + (let [res (th/run-task! :storage-gc-touched {})] (t/is (= 5 (:freeze res))) (t/is (= 0 (:delete res))) (let [result-1 (:result out1) result-2 (:result out2)] - ;; now we proceed to manually delete one team-font-variant - (db/exec-one! th/*pool* ["delete from team_font_variant where id = ?" (:id result-2)]) + (th/db-update! :team-font-variant + {:deleted-at (dt/now)} + {:id (:id result-2)}) + + ;; run the objects gc task for permanent deletion + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 1 (:processed res)))) ;; revert touched state to all storage objects - (db/exec-one! th/*pool* ["update storage_object set touched_at=now()"]) + (th/db-exec-one! ["update storage_object set touched_at=now()"]) ;; Run the task again - (let [res (task {})] + (let [res (th/run-task! :storage-gc-touched {})] (t/is (= 2 (:freeze res))) (t/is (= 3 (:delete res)))) ;; now check that there are no touched objects - (let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where touched_at is not null"])] + (let [res (th/db-exec-one! ["select count(*) from storage_object where touched_at is not null"])] (t/is (= 0 (:count res)))) ;; now check that all objects are marked to be deleted - (let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where deleted_at is not null"])] + (let [res (th/db-exec-one! ["select count(*) from storage_object where deleted_at is not null"])] (t/is (= 3 (:count res)))))))) (t/deftest test-touched-gc-task-3 @@ -297,28 +297,28 @@ result-2 (:result out2)] ;; now we proceed to manually mark all storage objects touched - (db/exec-one! th/*pool* ["update storage_object set touched_at=now()"]) + (th/db-exec! ["update storage_object set touched_at=now()"]) ;; run the touched gc task - (let [task (:app.storage/gc-touched-task th/*system*) - res (task {})] + (let [res (th/run-task! "storage-gc-touched" {:min-age 0})] (t/is (= 2 (:freeze res))) (t/is (= 0 (:delete res)))) ;; check that we have all object in the db - (let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where deleted_at is null"])] - (t/is (= 2 (:count res))))) + (let [rows (th/db-exec! ["select * from storage_object"])] + (t/is (= 2 (count rows))))) - ;; now we proceed to manually delete all team_font_variant - (db/exec-one! th/*pool* ["delete from file_media_object"]) + ;; now we proceed to manually delete all file_media_object + (th/db-exec! ["update file_media_object set deleted_at = now()"]) + + (let [res (th/run-task! "objects-gc" {:min-age 0})] + (t/is (= 2 (:processed res)))) ;; run the touched gc task - (let [task (:app.storage/gc-touched-task th/*system*) - res (task {})] + (let [res (th/run-task! "storage-gc-touched" {:min-age 0})] (t/is (= 0 (:freeze res))) (t/is (= 2 (:delete res)))) ;; check that we have all no objects - (let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where deleted_at is null"])] - (t/is (= 0 (:count res)))))) - + (let [rows (th/db-exec! ["select * from storage_object where deleted_at is null"])] + (t/is (= 0 (count rows)))))) diff --git a/backend/test/backend_tests/tasks_telemetry_test.clj b/backend/test/backend_tests/tasks_telemetry_test.clj index 70a2a6c91a..40c7b01df7 100644 --- a/backend/test/backend_tests/tasks_telemetry_test.clj +++ b/backend/test/backend_tests/tasks_telemetry_test.clj @@ -6,9 +6,9 @@ (ns backend-tests.tasks-telemetry-test (:require - [backend-tests.helpers :as th] [app.db :as db] [app.util.time :as dt] + [backend-tests.helpers :as th] [clojure.pprint :refer [pprint]] [clojure.test :as t] [mockery.core :refer [with-mocks]])) diff --git a/backend/test/backend_tests/util_objects_map_test.clj b/backend/test/backend_tests/util_objects_map_test.clj index b85c841c60..29a954597d 100644 --- a/backend/test/backend_tests/util_objects_map_test.clj +++ b/backend/test/backend_tests/util_objects_map_test.clj @@ -60,9 +60,7 @@ (t/testing "error on non-uuid keys" (let [obj (omap/wrap {})] - (t/is (thrown? IllegalArgumentException (assoc obj :foo "bar"))))) - - ) + (t/is (thrown? IllegalArgumentException (assoc obj :foo "bar")))))) (t/deftest internal-operation (t/testing "modified & compact" @@ -83,8 +81,7 @@ (t/is (= (get obj1 id1) (get obj2 id1))) (t/is (= (get obj1 id2) (get obj2 id2))) (t/is (= (count obj1) (count obj2))) - (t/is (= (hash obj1) (hash obj2))))) - ) + (t/is (= (hash obj1) (hash obj2)))))) (t/deftest internal-encode-decode (sg/check! @@ -95,11 +92,11 @@ obj3 (assoc obj2 uuid/zero 1) obj4 (omap/create (deref obj3))] ;; (app.common.pprint/pprint data) - (t/is (= (hash obj1) (hash obj2))) - (t/is (not= (hash obj2) (hash obj3))) - (t/is (bytes? (deref obj3))) - (t/is (pos? (alength (deref obj3)))) - (t/is (= (hash obj3) (hash obj4))))))) + (t/is (= (hash obj1) (hash obj2))) + (t/is (not= (hash obj2) (hash obj3))) + (t/is (bytes? (deref obj3))) + (t/is (pos? (alength (deref obj3)))) + (t/is (= (hash obj3) (hash obj4))))))) (t/deftest fressian-encode-decode (sg/check! diff --git a/backend/test/backend_tests/util_pointer_map_test.clj b/backend/test/backend_tests/util_pointer_map_test.clj index 85715dc957..52f15b3186 100644 --- a/backend/test/backend_tests/util_pointer_map_test.clj +++ b/backend/test/backend_tests/util_pointer_map_test.clj @@ -55,8 +55,7 @@ (t/is (pmap/pointer-map? obj2)) (t/is (identical? tmp obj2)) (t/is (= 0 (count obj1))) - (t/is (= 0 (count obj2))))) - ) + (t/is (= 0 (count obj2)))))) (t/deftest internal-tracking @@ -118,7 +117,5 @@ (t/is (not (contains? obj1 :b))) (t/is (= 1 (get obj1 :a))) (t/is (= nil (get obj1 :b))) - (t/is (= ::empty (get obj1 :b ::empty)))))) - - ) + (t/is (= ::empty (get obj1 :b ::empty))))))) diff --git a/backend/yarn.lock b/backend/yarn.lock index 3fee055411..1144439c63 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1,1133 +1,1174 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/runtime@^7.8.7": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" - integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== - dependencies: - regenerator-runtime "^0.13.4" - -"@types/node@*": - version "14.14.14" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae" - integrity sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ== - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -binary-extensions@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" - integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== - -boolbase@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -camel-case@3.0.x: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" - integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= - dependencies: - no-case "^2.2.0" - upper-case "^1.1.1" - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -cheerio@1.0.0-rc.3, cheerio@^1.0.0-rc.3: - version "1.0.0-rc.3" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" - integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA== - dependencies: - css-select "~1.2.0" - dom-serializer "~0.1.1" - entities "~1.1.1" - htmlparser2 "^3.9.1" - lodash "^4.15.0" - parse5 "^3.0.1" - -chokidar@^3.0.0: - version "3.4.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" - integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.1.2" - -clean-css@4.2.x: - version "4.2.3" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" - integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== - dependencies: - source-map "~0.6.0" - -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -commander@2.17.x: - version "2.17.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" - integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== - -commander@^2.19.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== - -commander@~2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" - integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -config-chain@^1.1.12: - version "1.1.12" - resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" - integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - -css-select@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" - integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= - dependencies: - boolbase "~1.0.0" - css-what "2.1" - domutils "1.5.1" - nth-check "~1.0.1" - -css-what@2.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" - integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== - -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -dom-serializer@0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== - dependencies: - domelementtype "^2.0.1" - entities "^2.0.0" - -dom-serializer@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" - integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.0.0" - entities "^2.0.0" - -dom-serializer@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" - integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== - dependencies: - domelementtype "^1.3.0" - entities "^1.1.1" - -domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domelementtype@^2.0.1, domelementtype@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" - integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== - -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - -domhandler@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a" - integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA== - dependencies: - domelementtype "^2.0.1" - -domhandler@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e" - integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA== - dependencies: - domelementtype "^2.1.0" - -domutils@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" - integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= - dependencies: - dom-serializer "0" - domelementtype "1" - -domutils@^1.5.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -domutils@^2.0.0: - version "2.4.4" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" - integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.0.1" - domhandler "^4.0.0" - -editorconfig@^0.15.3: - version "0.15.3" - resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" - integrity sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g== - dependencies: - commander "^2.19.0" - lru-cache "^4.1.5" - semver "^5.6.0" - sigmund "^1.0.1" - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -entities@^1.1.1, entities@~1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - -entities@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" - integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== - -escape-goat@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-3.0.0.tgz#e8b5fb658553fe8a3c4959c316c6ebb8c842b19c" - integrity sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw== - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== - dependencies: - is-glob "^4.0.1" - -glob@^7.1.1, glob@^7.1.3: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -he@1.2.x: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -html-minifier@^3.5.3: - version "3.5.21" - resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c" - integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA== - dependencies: - camel-case "3.0.x" - clean-css "4.2.x" - commander "2.17.x" - he "1.2.x" - param-case "2.1.x" - relateurl "0.2.x" - uglify-js "3.4.x" - -htmlparser2@^3.9.1, htmlparser2@^3.9.2: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - -htmlparser2@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" - integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q== - dependencies: - domelementtype "^2.0.1" - domhandler "^3.0.0" - domutils "^2.0.0" - entities "^2.0.0" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ini@^1.3.4: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -js-beautify@^1.6.14: - version "1.13.0" - resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.13.0.tgz#a056d5d3acfd4918549aae3ab039f9f3c51eebb2" - integrity sha512-/Tbp1OVzZjbwzwJQFIlYLm9eWQ+3aYbBXLSaqb1mEJzhcQAfrqMMQYtjb6io+U6KpD0ID4F+Id3/xcjH3l/sqA== - dependencies: - config-chain "^1.1.12" - editorconfig "^0.15.3" - glob "^7.1.3" - mkdirp "^1.0.4" - nopt "^5.0.0" - -"js-tokens@^3.0.0 || ^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -juice@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/juice/-/juice-7.0.0.tgz#509bed6adbb6e4bbaa7fbfadac4e2e83e8c89ba3" - integrity sha512-AjKQX31KKN+uJs+zaf+GW8mBO/f/0NqSh2moTMyvwBY+4/lXIYTU8D8I2h6BAV3Xnz6GGsbalUyFqbYMe+Vh+Q== - dependencies: - cheerio "^1.0.0-rc.3" - commander "^5.1.0" - mensch "^0.3.4" - slick "^1.12.2" - web-resource-inliner "^5.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash@^4.15.0, lodash@^4.17.15: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== - -loose-envify@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lower-case@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" - integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= - -lru-cache@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -mensch@^0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/mensch/-/mensch-0.3.4.tgz#770f91b46cb16ea5b204ee735768c3f0c491fecd" - integrity sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g== - -mime@^2.4.6: - version "2.4.7" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.7.tgz#962aed9be0ed19c91fd7dc2ece5d7f4e89a90d74" - integrity sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA== - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -mjml-accordion@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-accordion/-/mjml-accordion-4.7.1.tgz#61492a63f84ec7bebb16644caea31d8e3eaecec8" - integrity sha512-oYwC/CLOUWJ6pRt2saDHj/HytGOHO5B5lKNqUAhKPye5HFNZykKEV5ChmZ2NfGsGU+9BhQ7H5DaCafp4fDmPAg== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-body@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-body/-/mjml-body-4.7.1.tgz#d8ede15fea556f2c4d62243ab68bb6d9b344bd67" - integrity sha512-JCrkit+kjCfQyKuVyWSOonM2LGs/o3+63R9l2SleFeXf3+0CaKWaZr/Exzvaeo28c+1o3yRqXbJIpD22SEtJfQ== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-button@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-button/-/mjml-button-4.7.1.tgz#8f62352a1b27c720e6bbba65d736874ac766573f" - integrity sha512-N3WkTMPOvKw2y6sakt1YfYDbOB8apumm1OApPG6J18CHcrX03BwhHPrdfu1JwlRNGwx4kCDdb6zNCGPwuZxkCg== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-carousel@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-carousel/-/mjml-carousel-4.7.1.tgz#ab8e4fe8b9f95f9be304502e16b7edfa815b7932" - integrity sha512-eH3rRyX23ES0BKOn+UUV39+yGNmZVApBVVV0A5znDaNWskCg6/g6ZhEHi4nkWpj+aP2lJKI0HX1nrMfJg0Mxhg== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-cli@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-cli/-/mjml-cli-4.7.1.tgz#0ba1ef4613e26cc31c148b4f2242034d74c4dc46" - integrity sha512-xzCtJVKYVhGorvTmnbcMUfZlmJdBnu1UBD9A1H8UUBGMNE/Hs9QpHs9PLCMp8JR/uhSu15IgVjhFN0oSVndMRQ== - dependencies: - "@babel/runtime" "^7.8.7" - chokidar "^3.0.0" - glob "^7.1.1" - lodash "^4.17.15" - mjml-core "4.7.1" - mjml-migrate "4.7.1" - mjml-parser-xml "4.7.1" - mjml-validator "4.7.1" - yargs "^15.3.1" - -mjml-column@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-column/-/mjml-column-4.7.1.tgz#0639466687691b3bd182bdca39718ae46a853abc" - integrity sha512-CGw81TnGiuPR1GblLOez8xeoeAz1SEFjMpqapazjgXUuF5xUxg3qH55Wt4frpXe3VypeZWVYeumr6CwoNaPbKg== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-core@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-core/-/mjml-core-4.7.1.tgz#9c5da30479cc8c8206e6295220fc297ca1cb4378" - integrity sha512-AMACoq/h440m7SM86As8knW0bNQgjNIzsP/cMF6X9RO07GfszgbaWUq/XCaRNi+q8bWvBJSCXbngDJySVc5ALw== - dependencies: - "@babel/runtime" "^7.8.7" - cheerio "1.0.0-rc.3" - html-minifier "^3.5.3" - js-beautify "^1.6.14" - juice "^7.0.0" - lodash "^4.17.15" - mjml-migrate "4.7.1" - mjml-parser-xml "4.7.1" - mjml-validator "4.7.1" - -mjml-divider@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-divider/-/mjml-divider-4.7.1.tgz#d2e416ce3f0fec8763ed5c41ba97d296785eca81" - integrity sha512-7+uCUJdqEr6w8AzpF8lhRheelYEgOwiK0KJGlAQN3LF+h2S1rTPEzEB67qL2x5cU+80kPlxtxoQWImDBy0vXqg== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-group@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-group/-/mjml-group-4.7.1.tgz#88b4c396a00f3b7cb4452ca4047c0f65646b6320" - integrity sha512-mAYdhocCzetdhPSws/9/sQ4hcz4kQPX2dNitQmbxNVwoMFYXjp/WcLEfGc5u13Ue7dPfcV6c9lB/Uu5o3NmRvw== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-head-attributes@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-head-attributes/-/mjml-head-attributes-4.7.1.tgz#7b620bb45438909c6b33afe1069a7d70bb8d5b37" - integrity sha512-nB/bQ3I98Dvy/IkI4nqxTCnLonULkIKc8KrieRTrtPkUV3wskBzngpCgnjKvFPbHWiGlwjHDzcFJc7G0uWeqog== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-head-breakpoint@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-head-breakpoint/-/mjml-head-breakpoint-4.7.1.tgz#1a0ab3c22cda6c6b019e0a45e2361723f2897c94" - integrity sha512-0KB5SweIWDvwHkn4VCUsEhCQgfY/0wkNUnSXNoftaRujv0NQFQfOOH4eINy0NZYfDfrE4WYe08z+olHprp+T2A== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-head-font@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-head-font/-/mjml-head-font-4.7.1.tgz#884b626559ce8836412dc45b7040ec80d29ff040" - integrity sha512-9YGzBcQ2htZ6j266fiLLfzcxqDEDLTvfKtypTjaeRb1w3N8S5wL+/zJA5ZjRL6r39Ij5ZPQSlSDC32KPiwhGkA== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-head-html-attributes@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-head-html-attributes/-/mjml-head-html-attributes-4.7.1.tgz#3764ab44ad530c6c3cdbd6db92813ed0db07fc0b" - integrity sha512-2TK2nGpq4rGaghbVx2UNm5TXeZ5BTGYEvtSPoYPNu02KRCj6tb+uedAgFXwJpX+ogRfIfPK50ih+9ZMoHwf2IQ== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-head-preview@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-head-preview/-/mjml-head-preview-4.7.1.tgz#f06423bf07b1889c39658c830a8d3f4a6f17b93e" - integrity sha512-UHlvvgldiPDODq/5zKMsmXgRb/ZyKygKDUVQSM5bm3HvpKXeyYxJZazcIGmlGICEqv1ced1WGINhCg72dSfN+Q== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-head-style@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-head-style/-/mjml-head-style-4.7.1.tgz#df8e055852fa3b28a50ac820ec449e8915a1ecbf" - integrity sha512-8Gij99puN1SoOx5tGBjgkh4iCpI+zbwGBiB2Y8VwJrwXQxdJ1Qa902dQP5djoFFG39Bthii/48cS/d1bHigGPQ== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-head-title@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-head-title/-/mjml-head-title-4.7.1.tgz#8c0216881457f02be592fd5ebe59d7968825b874" - integrity sha512-vK3r+DApTXw2EoK/fh8dQOsO438Z7Ksy6iBIb7h04x33d4Z41r6+jtgxGXoKFXnjgr8MyLX5HZyyie5obW+hZg== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-head@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-head/-/mjml-head-4.7.1.tgz#ce79193702dd4b1eae5b9cf9864f8ac8bd25269d" - integrity sha512-jUcJ674CT1oT8NTQWTjQQBFZu4yklK0oppfGFJ1cq76ze3isMiyhSnGnOHw6FkjLnZtb3gXXaGKX7UZM+UMk/w== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-hero@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-hero/-/mjml-hero-4.7.1.tgz#49bc1c844ae7221f2f4e8d862d111c41f2cf8b2c" - integrity sha512-x+29V8zJAs8EV/eTtGbR921pCpitMQOAkyvNANW/3JLDTL2Oio1OYvGPVC3z1wOT9LKuRTxVzNHVt/bBw02CSQ== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-image@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-image/-/mjml-image-4.7.1.tgz#0b4da3d46b1f79c9472df965c70f15bc1255a425" - integrity sha512-l3uRR2jaM0Bpz4ctdWuxQUFgg+ol6Nt+ODOrnHsGMwpmFOh4hTPTky6KaF0LCXxYmGbI0FoGBna+hVNnkBsQCA== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-migrate@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-migrate/-/mjml-migrate-4.7.1.tgz#a086af92fd94b797b5ba1a59ba2ae73a34c65911" - integrity sha512-RgrJ9fHg6iRHC2H4pjRDWilBQ1eTH2jRu1ayDplbnepGoql83vLZaYaWc5Q+J+NsaNI16x+bgNB3fQdBiK+mng== - dependencies: - "@babel/runtime" "^7.8.7" - js-beautify "^1.6.14" - lodash "^4.17.15" - mjml-core "4.7.1" - mjml-parser-xml "4.7.1" - yargs "^15.3.1" - -mjml-navbar@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-navbar/-/mjml-navbar-4.7.1.tgz#8f5d7be62ddf118c85332b4509321c996e879c62" - integrity sha512-awdu8zT7xhS+9aCVunqtocUs8KA2xb+UhJ8UGbxVBpYbTNj3rCL9aWUXqWVwMk1la+3ypCkFuDuTl6dIoWPWlA== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-parser-xml@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-parser-xml/-/mjml-parser-xml-4.7.1.tgz#adda1ba42c9f5b21d4817a4f0cbaff4b8a97eeec" - integrity sha512-UWfuRpN45k3GUEv2yl8n5Uf98Tg6FyCsyRnqZGo83mgZzlJRDYTdKII9RjZM646/S8+Q8e9qxi3AsL00j6sZsQ== - dependencies: - "@babel/runtime" "^7.8.7" - htmlparser2 "^3.9.2" - lodash "^4.17.15" - -mjml-raw@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-raw/-/mjml-raw-4.7.1.tgz#a862dfabb540b78b6d31feef156f704c19ed308b" - integrity sha512-mCQFEXINTkC8i7ydP1Km99e0FaZTeu79AoYnTBAILd4QO+RuD3n/PimBGrcGrOUex0JIKa2jyVQOcSCBuG4WpA== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-section@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-section/-/mjml-section-4.7.1.tgz#b76b7e59090ca380758b74dbd2b67e0d2f35d097" - integrity sha512-PlhCMsl/bpFwwgQGUopi9OgOGWgRPpEJVKE8hk4He8GXzbfIuDj4DZ9QJSkwIoZ0fZtcgz11Wwb19i9BZcozVw== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-social@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-social/-/mjml-social-4.7.1.tgz#8d1555314744bdbca9dde769a7af535df0ee38d6" - integrity sha512-tN/6V3m59izO9rqWpUokHxhwkk2GHkltzIlhI936hAJHh8hFyEO6+ZwQBZm738G00qgfICmQvX5FNq4upkCYjw== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-spacer@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-spacer/-/mjml-spacer-4.7.1.tgz#96a7e59329dc9db7bb914a2e3d67b4f478f33cdf" - integrity sha512-gQu1+nA9YGnoolfNPvzfVe/RJ8WqS8ho0hthlhiLOC2RnEnmqH7HHSzCFXm4OeN0VgvDQsM7mfYQGl82O58Y+g== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-table@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-table/-/mjml-table-4.7.1.tgz#4b75185b150d3a4f4bf29d6fb3918de3dc6f87db" - integrity sha512-rPkOtufMiVreb7I7vXk6rDm9i1DXncODnM5JJNhA9Z1dAQwXiz6V5904gAi2cEYfe0M2m0XQ8P5ZCtvqxGkfGA== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-text@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-text/-/mjml-text-4.7.1.tgz#135a7d2c7aaebf4c41bde1cd763965c50375e371" - integrity sha512-hrjxbY59v6hu/Pn0NO+6TMlrdAlRa3M7GVALx/YWYV3hi59zjYfot8Au7Xq64XdcbcI4eiBVbP/AVr8w03HsOw== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - -mjml-validator@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-validator/-/mjml-validator-4.7.1.tgz#9ccadec3090ea6cc18956b292e94c1f0372fa47b" - integrity sha512-Qxubbz5WE182iLSTd/XRuezMr6UE7/u73grDCw0bTIcQsaTAIkWQn2tBI3jj0chWOw+sxwK2C6zPm9B0Cv7BGA== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - warning "^3.0.0" - -mjml-wrapper@4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml-wrapper/-/mjml-wrapper-4.7.1.tgz#a354dcf186ab6f56fa39d5c820e31cd40e6217b7" - integrity sha512-6i+ZATUyqIO5YBnx+RFKZ3+6mg3iOCS/EdXGYZSonZ/EHqlt+RJa3fG2BB4dacXqAjghfl6Lk+bLoR47P3xYIQ== - dependencies: - "@babel/runtime" "^7.8.7" - lodash "^4.17.15" - mjml-core "4.7.1" - mjml-section "4.7.1" - -mjml@^4.6.3: - version "4.7.1" - resolved "https://registry.yarnpkg.com/mjml/-/mjml-4.7.1.tgz#cf3398a4d43d694ec75768f4319875f0d9846ba0" - integrity sha512-nwMrmhTI+Aeh9Gav9LHX/i8k8yDi/QpX5h535BlT5oP4NaAUmyxP/UeYUn9yxtPcIzDlM5ullFnRv/71jyHpkQ== - dependencies: - mjml-accordion "4.7.1" - mjml-body "4.7.1" - mjml-button "4.7.1" - mjml-carousel "4.7.1" - mjml-cli "4.7.1" - mjml-column "4.7.1" - mjml-core "4.7.1" - mjml-divider "4.7.1" - mjml-group "4.7.1" - mjml-head "4.7.1" - mjml-head-attributes "4.7.1" - mjml-head-breakpoint "4.7.1" - mjml-head-font "4.7.1" - mjml-head-html-attributes "4.7.1" - mjml-head-preview "4.7.1" - mjml-head-style "4.7.1" - mjml-head-title "4.7.1" - mjml-hero "4.7.1" - mjml-image "4.7.1" - mjml-migrate "4.7.1" - mjml-navbar "4.7.1" - mjml-raw "4.7.1" - mjml-section "4.7.1" - mjml-social "4.7.1" - mjml-spacer "4.7.1" - mjml-table "4.7.1" - mjml-text "4.7.1" - mjml-validator "4.7.1" - mjml-wrapper "4.7.1" - -mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -no-case@^2.2.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" - integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== - dependencies: - lower-case "^1.1.1" - -node-fetch@^2.6.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - -nopt@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== - dependencies: - abbrev "1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -nth-check@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" - integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== - dependencies: - boolbase "~1.0.0" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -param-case@2.1.x: - version "2.1.1" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" - integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= - dependencies: - no-case "^2.2.0" - -parse5@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" - integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA== - dependencies: - "@types/node" "*" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - -proto-list@~1.2.1: - version "1.2.4" - resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" - integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -readable-stream@^3.1.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== - dependencies: - picomatch "^2.2.1" - -regenerator-runtime@^0.13.4: - version "0.13.7" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" - integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== - -relateurl@0.2.x: - version "0.2.7" - resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" - integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -sigmund@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= - -slick@^1.12.2: - version "1.12.2" - resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7" - integrity sha1-vQSN23TefRymkV+qSldXCzVQwtc= - -source-map@~0.6.0, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -uglify-js@3.4.x: - version "3.4.10" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" - integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== - dependencies: - commander "~2.19.0" - source-map "~0.6.1" - -upper-case@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" - integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= - -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -valid-data-url@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/valid-data-url/-/valid-data-url-3.0.1.tgz#826c1744e71b5632e847dd15dbd45b9fb38aa34f" - integrity sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA== - -warning@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" - integrity sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w= - dependencies: - loose-envify "^1.0.0" - -web-resource-inliner@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/web-resource-inliner/-/web-resource-inliner-5.0.0.tgz#ac30db8096931f20a7c1b3ade54ff444e2e20f7b" - integrity sha512-AIihwH+ZmdHfkJm7BjSXiEClVt4zUFqX4YlFAzjL13wLtDuUneSaFvDBTbdYRecs35SiU7iNKbMnN+++wVfb6A== - dependencies: - ansi-colors "^4.1.1" - escape-goat "^3.0.0" - htmlparser2 "^4.0.0" - mime "^2.4.6" - node-fetch "^2.6.0" - valid-data-url "^3.0.0" - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -y18n@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" - integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@^15.3.1: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + languageName: node + linkType: hard + +"@npmcli/agent@npm:^2.0.0": + version: 2.2.0 + resolution: "@npmcli/agent@npm:2.2.0" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.1" + checksum: 7b89590598476dda88e79c473766b67c682aae6e0ab0213491daa6083dcc0c171f86b3868f5506f22c09aa5ea69ad7dfb78f4bf39a8dca375d89a42f408645b3 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.0 + resolution: "@npmcli/fs@npm:3.1.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 162b4a0b8705cd6f5c2470b851d1dc6cd228c86d2170e1769d738c1fbb69a87160901411c3c035331e9e99db72f1f1099a8b734bf1637cc32b9a5be1660e4e1e + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + languageName: node + linkType: hard + +"abbrev@npm:1": + version: 1.1.1 + resolution: "abbrev@npm:1.1.1" + checksum: 3f762677702acb24f65e813070e306c61fafe25d4b2583f9dfc935131f774863f3addd5741572ed576bd69cabe473c5af18e1e108b829cb7b6b4747884f726e6 + languageName: node + linkType: hard + +"abbrev@npm:^2.0.0": + version: 2.0.0 + resolution: "abbrev@npm:2.0.0" + checksum: f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 + languageName: node + linkType: hard + +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0": + version: 7.1.0 + resolution: "agent-base@npm:7.1.0" + dependencies: + debug: "npm:^4.3.4" + checksum: fc974ab57ffdd8421a2bc339644d312a9cca320c20c3393c9d8b1fd91731b9bbabdb985df5fc860f5b79d81c3e350daa3fcb31c5c07c0bb385aafc817df004ce + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: "npm:^2.0.0" + indent-string: "npm:^4.0.0" + checksum: a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: cbe16dbd2c6b2735d1df7976a7070dd277326434f0212f43abf6d87674095d247968209babdaad31bb00882fa68807256ba9be340eec2f1004de14ca75f52a08 + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: 5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c + languageName: node + linkType: hard + +"anymatch@npm:~3.1.2": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac + languageName: node + linkType: hard + +"backend@workspace:.": + version: 0.0.0-use.local + resolution: "backend@workspace:." + dependencies: + luxon: "npm:^3.4.2" + nodemon: "npm:^3.0.1" + sax: "npm:^1.2.4" + source-map-support: "npm:^0.5.21" + ws: "npm:^8.13.0" + languageName: unknown + linkType: soft + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + languageName: node + linkType: hard + +"binary-extensions@npm:^2.0.0": + version: 2.2.0 + resolution: "binary-extensions@npm:2.2.0" + checksum: d73d8b897238a2d3ffa5f59c0241870043aa7471335e89ea5e1ff48edb7c2d0bb471517a3e4c5c3f4c043615caa2717b5f80a5e61e07503d51dc85cb848e665d + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.11 + resolution: "brace-expansion@npm:1.1.11" + dependencies: + balanced-match: "npm:^1.0.0" + concat-map: "npm:0.0.1" + checksum: 695a56cd058096a7cb71fb09d9d6a7070113c7be516699ed361317aca2ec169f618e28b8af352e02ab4233fb54eb0168460a40dc320bab0034b36ab59aaad668 + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: "npm:^1.0.0" + checksum: b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f + languageName: node + linkType: hard + +"braces@npm:~3.0.2": + version: 3.0.2 + resolution: "braces@npm:3.0.2" + dependencies: + fill-range: "npm:^7.0.1" + checksum: 321b4d675791479293264019156ca322163f02dc06e3c4cab33bb15cd43d80b51efef69b0930cfde3acd63d126ebca24cd0544fa6f261e093a0fb41ab9dda381 + languageName: node + linkType: hard + +"buffer-from@npm:^1.0.0": + version: 1.1.2 + resolution: "buffer-from@npm:1.1.2" + checksum: 124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 + languageName: node + linkType: hard + +"cacache@npm:^18.0.0": + version: 18.0.1 + resolution: "cacache@npm:18.0.1" + dependencies: + "@npmcli/fs": "npm:^3.1.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^10.0.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^4.0.0" + ssri: "npm:^10.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^3.0.0" + checksum: a31666805a80a8b16ad3f85faf66750275a9175a3480896f4f6d31b5d53ef190484fabd71bdb6d2ea5603c717fbef09f4af03d6a65b525c8ef0afaa44c361866 + languageName: node + linkType: hard + +"chokidar@npm:^3.5.2": + version: 3.5.3 + resolution: "chokidar@npm:3.5.3" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 1076953093e0707c882a92c66c0f56ba6187831aa51bb4de878c1fec59ae611a3bf02898f190efec8e77a086b8df61c2b2a3ea324642a0558bdf8ee6c5dc9ca1 + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: 594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: 37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 + languageName: node + linkType: hard + +"concat-map@npm:0.0.1": + version: 0.0.1 + resolution: "concat-map@npm:0.0.1" + checksum: c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.4": + version: 4.3.4 + resolution: "debug@npm:4.3.4" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: cedbec45298dd5c501d01b92b119cd3faebe5438c3917ff11ae1bff86a6c722930ac9c8659792824013168ba6db7c4668225d845c633fbdafbbf902a6389f736 + languageName: node + linkType: hard + +"debug@npm:^3.2.7": + version: 3.2.7 + resolution: "debug@npm:3.2.7" + dependencies: + ms: "npm:^2.1.1" + checksum: 37d96ae42cbc71c14844d2ae3ba55adf462ec89fd3a999459dec3833944cd999af6007ff29c780f1c61153bcaaf2c842d1e4ce1ec621e4fc4923244942e4a02a + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: 36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 + languageName: node + linkType: hard + +"fill-range@npm:^7.0.1": + version: 7.0.1 + resolution: "fill-range@npm:7.0.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: 7cdad7d426ffbaadf45aeb5d15ec675bbd77f7597ad5399e3d2766987ed20bda24d5fac64b3ee79d93276f5865608bb22344a26b9b1ae6c4d00bd94bf611623f + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: "npm:^7.0.0" + signal-exit: "npm:^4.0.1" + checksum: 9700a0285628abaeb37007c9a4d92bd49f67210f09067638774338e146c8e9c825c5c877f072b2f75f41dc6a2d0be8664f79ffc03f6576649f54a84fb9b47de0 + languageName: node + linkType: hard + +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + languageName: node + linkType: hard + +"fsevents@npm:~2.3.2": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"glob-parent@npm:~5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: "npm:^4.0.1" + checksum: cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee + languageName: node + linkType: hard + +"glob@npm:^10.2.2, glob@npm:^10.3.10": + version: 10.3.10 + resolution: "glob@npm:10.3.10" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^2.3.5" + minimatch: "npm:^9.0.1" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry: "npm:^1.10.1" + bin: + glob: dist/esm/bin.mjs + checksum: 13d8a1feb7eac7945f8c8480e11cd4a44b24d26503d99a8d8ac8d5aefbf3e9802a2b6087318a829fad04cb4e829f25c5f4f1110c68966c498720dd261c7e344d + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + languageName: node + linkType: hard + +"has-flag@npm:^3.0.0": + version: 3.0.0 + resolution: "has-flag@npm:3.0.0" + checksum: 1c6c83b14b8b1b3c25b0727b8ba3e3b647f99e9e6e13eb7322107261de07a4c1be56fc0d45678fc376e09772a3a1642ccdaf8fc69bdf123b6c086598397ce473 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.0 + resolution: "http-proxy-agent@npm:7.0.0" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: a11574ff39436cee3c7bc67f259444097b09474605846ddd8edf0bf4ad8644be8533db1aa463426e376865047d05dc22755e638632819317c0c2f1b2196657c8 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.2 + resolution: "https-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:4" + checksum: 7735eb90073db087e7e79312e3d97c8c04baf7ea7ca7b013382b6a45abbaa61b281041a98f4e13c8c80d88f843785bcc84ba189165b4b4087b1e3496ba656d77 + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + languageName: node + linkType: hard + +"ignore-by-default@npm:^1.0.1": + version: 1.0.1 + resolution: "ignore-by-default@npm:1.0.1" + checksum: 9ab6e70e80f7cc12735def7ecb5527cfa56ab4e1152cd64d294522827f2dcf1f6d85531241537dc3713544e88dd888f65cb3c49c7b2cddb9009087c75274e533 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + languageName: node + linkType: hard + +"ip@npm:^2.0.0": + version: 2.0.0 + resolution: "ip@npm:2.0.0" + checksum: 8d186cc5585f57372847ae29b6eba258c68862055e18a75cc4933327232cb5c107f89800ce29715d542eef2c254fbb68b382e780a7414f9ee7caf60b7a473958 + languageName: node + linkType: hard + +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: "npm:^2.0.0" + checksum: a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: 5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + languageName: node + linkType: hard + +"is-glob@npm:^4.0.1, is-glob@npm:~4.0.1": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: "npm:^2.1.1" + checksum: 17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a + languageName: node + linkType: hard + +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811 + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + languageName: node + linkType: hard + +"jackspeak@npm:^2.3.5": + version: 2.3.6 + resolution: "jackspeak@npm:2.3.6" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: f01d8f972d894cd7638bc338e9ef5ddb86f7b208ce177a36d718eac96ec86638a6efa17d0221b10073e64b45edc2ce15340db9380b1f5d5c5d000cbc517dc111 + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.1.0 + resolution: "lru-cache@npm:10.1.0" + checksum: 778bc8b2626daccd75f24c4b4d10632496e21ba064b126f526c626fbdbc5b28c472013fccd45d7646b9e1ef052444824854aed617b59cd570d01a8b7d651fc1e + languageName: node + linkType: hard + +"lru-cache@npm:^6.0.0": + version: 6.0.0 + resolution: "lru-cache@npm:6.0.0" + dependencies: + yallist: "npm:^4.0.0" + checksum: cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 + languageName: node + linkType: hard + +"luxon@npm:^3.4.2": + version: 3.4.4 + resolution: "luxon@npm:3.4.4" + checksum: 02e26a0b039c11fd5b75e1d734c8f0332c95510f6a514a9a0991023e43fb233884da02d7f966823ffb230632a733fc86d4a4b1e63c3fbe00058b8ee0f8c728af + languageName: node + linkType: hard + +"make-fetch-happen@npm:^13.0.0": + version: 13.0.0 + resolution: "make-fetch-happen@npm:13.0.0" + dependencies: + "@npmcli/agent": "npm:^2.0.0" + cacache: "npm:^18.0.0" + http-cache-semantics: "npm:^4.1.1" + is-lambda: "npm:^1.0.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^3.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.3" + promise-retry: "npm:^2.0.1" + ssri: "npm:^10.0.0" + checksum: 43b9f6dcbc6fe8b8604cb6396957c3698857a15ba4dbc38284f7f0e61f248300585ef1eb8cc62df54e9c724af977e45b5cdfd88320ef7f53e45070ed3488da55 + languageName: node + linkType: hard + +"minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 + languageName: node + linkType: hard + +"minimatch@npm:^9.0.1": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 85f407dcd38ac3e180f425e86553911d101455ca3ad5544d6a7cec16286657e4f8a9aa6695803025c55e31e35a91a2252b5dc8e7d527211278b8b65b4dbd5eac + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.4 + resolution: "minipass-fetch@npm:3.0.4" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.1.2" + dependenciesMeta: + encoding: + optional: true + checksum: 1b63c1f3313e88eeac4689f1b71c9f086598db9a189400e3ee960c32ed89e06737fa23976c9305c2d57464fb3fcdc12749d3378805c9d6176f5569b0d0ee8a75 + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3": + version: 7.0.4 + resolution: "minipass@npm:7.0.4" + checksum: 6c7370a6dfd257bf18222da581ba89a5eaedca10e158781232a8b5542a90547540b4b9b7e7f490e4cda43acfbd12e086f0453728ecf8c19e0ef6921bc5958ac5 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: 64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: 46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf + languageName: node + linkType: hard + +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc + languageName: node + linkType: hard + +"ms@npm:^2.1.1": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 + languageName: node + linkType: hard + +"negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: 3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 10.0.1 + resolution: "node-gyp@npm:10.0.1" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^13.0.0" + nopt: "npm:^7.0.0" + proc-log: "npm:^3.0.0" + semver: "npm:^7.3.5" + tar: "npm:^6.1.2" + which: "npm:^4.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: abddfff7d873312e4ed4a5fb75ce893a5c4fb69e7fcb1dfa71c28a6b92a7f1ef6b62790dffb39181b5a82728ba8f2f32d229cf8cbe66769fe02cea7db4a555aa + languageName: node + linkType: hard + +"nodemon@npm:^3.0.1": + version: 3.0.1 + resolution: "nodemon@npm:3.0.1" + dependencies: + chokidar: "npm:^3.5.2" + debug: "npm:^3.2.7" + ignore-by-default: "npm:^1.0.1" + minimatch: "npm:^3.1.2" + pstree.remy: "npm:^1.1.8" + semver: "npm:^7.5.3" + simple-update-notifier: "npm:^2.0.0" + supports-color: "npm:^5.5.0" + touch: "npm:^3.1.0" + undefsafe: "npm:^2.0.5" + bin: + nodemon: bin/nodemon.js + checksum: 471a218227949b38926de78237004c91e226b63ee06f433cf85c2f1c1f8b6bfbef9bceaa8d27786e7cfb539eb84da357d01741884d08a3ae172bebecd0f1de5b + languageName: node + linkType: hard + +"nopt@npm:^7.0.0": + version: 7.2.0 + resolution: "nopt@npm:7.2.0" + dependencies: + abbrev: "npm:^2.0.0" + bin: + nopt: bin/nopt.js + checksum: 9bd7198df6f16eb29ff16892c77bcf7f0cc41f9fb5c26280ac0def2cf8cf319f3b821b3af83eba0e74c85807cc430a16efe0db58fe6ae1f41e69519f585b6aff + languageName: node + linkType: hard + +"nopt@npm:~1.0.10": + version: 1.0.10 + resolution: "nopt@npm:1.0.10" + dependencies: + abbrev: "npm:1" + bin: + nopt: ./bin/nopt.js + checksum: ddfbd892116a125fd68849ef564dd5b1f0a5ba0dbbf18782e9499e2efad8f4d3790635b47c6b5d3f7e014069e7b3ce5b8112687e9ae093fcd2678188c866fe28 + languageName: node + linkType: hard + +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": + version: 3.0.0 + resolution: "normalize-path@npm:3.0.0" + checksum: e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 + languageName: node + linkType: hard + +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: 592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + languageName: node + linkType: hard + +"path-scurry@npm:^1.10.1": + version: 1.10.1 + resolution: "path-scurry@npm:1.10.1" + dependencies: + lru-cache: "npm:^9.1.1 || ^10.0.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: e5dc78a7348d25eec61ab166317e9e9c7b46818aa2c2b9006c507a6ff48c672d011292d9662527213e558f5652ce0afcc788663a061d8b59ab495681840c0c1e + languageName: node + linkType: hard + +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be + languageName: node + linkType: hard + +"proc-log@npm:^3.0.0": + version: 3.0.0 + resolution: "proc-log@npm:3.0.0" + checksum: f66430e4ff947dbb996058f6fd22de2c66612ae1a89b097744e17fb18a4e8e7a86db99eda52ccf15e53f00b63f4ec0b0911581ff2aac0355b625c8eac509b0dc + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + languageName: node + linkType: hard + +"pstree.remy@npm:^1.1.8": + version: 1.1.8 + resolution: "pstree.remy@npm:1.1.8" + checksum: 30f78c88ce6393cb3f7834216cb6e282eb83c92ccb227430d4590298ab2811bc4a4745f850a27c5178e79a8f3e316591de0fec87abc19da648c2b3c6eb766d14 + languageName: node + linkType: hard + +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: "npm:^2.2.1" + checksum: 6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + languageName: node + linkType: hard + +"sax@npm:^1.2.4": + version: 1.3.0 + resolution: "sax@npm:1.3.0" + checksum: 599dbe0ba9d8bd55e92d920239b21d101823a6cedff71e542589303fa0fa8f3ece6cf608baca0c51be846a2e88365fac94a9101a9c341d94b98e30c4deea5bea + languageName: node + linkType: hard + +"semver@npm:^7.3.5, semver@npm:^7.5.3": + version: 7.5.4 + resolution: "semver@npm:7.5.4" + dependencies: + lru-cache: "npm:^6.0.0" + bin: + semver: bin/semver.js + checksum: 5160b06975a38b11c1ab55950cb5b8a23db78df88275d3d8a42ccf1f29e55112ac995b3a26a522c36e3b5f76b0445f1eef70d696b8c7862a2b4303d7b0e7609e + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + languageName: node + linkType: hard + +"simple-update-notifier@npm:^2.0.0": + version: 2.0.0 + resolution: "simple-update-notifier@npm:2.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 2a00bd03bfbcbf8a737c47ab230d7920f8bfb92d1159d421bdd194479f6d01ebc995d13fbe13d45dace23066a78a3dc6642999b4e3b38b847e6664191575b20c + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.1": + version: 8.0.2 + resolution: "socks-proxy-agent@npm:8.0.2" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:^4.3.4" + socks: "npm:^2.7.1" + checksum: a842402fc9b8848a31367f2811ca3cd14c4106588b39a0901cd7a69029998adfc6456b0203617c18ed090542ad0c24ee4e9d4c75a0c4b75071e214227c177eb7 + languageName: node + linkType: hard + +"socks@npm:^2.7.1": + version: 2.7.1 + resolution: "socks@npm:2.7.1" + dependencies: + ip: "npm:^2.0.0" + smart-buffer: "npm:^4.2.0" + checksum: 43f69dbc9f34fc8220bc51c6eea1c39715ab3cfdb115d6e3285f6c7d1a603c5c75655668a5bbc11e3c7e2c99d60321fb8d7ab6f38cda6a215fadd0d6d0b52130 + languageName: node + linkType: hard + +"source-map-support@npm:^0.5.21": + version: 0.5.21 + resolution: "source-map-support@npm:0.5.21" + dependencies: + buffer-from: "npm:^1.0.0" + source-map: "npm:^0.6.0" + checksum: 9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d + languageName: node + linkType: hard + +"source-map@npm:^0.6.0": + version: 0.6.1 + resolution: "source-map@npm:0.6.1" + checksum: ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.5 + resolution: "ssri@npm:10.0.5" + dependencies: + minipass: "npm:^7.0.3" + checksum: b091f2ae92474183c7ac5ed3f9811457e1df23df7a7e70c9476eaa9a0c4a0c8fc190fb45acefbf023ca9ee864dd6754237a697dc52a0fb182afe65d8e77443d8 + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: 1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: 1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 + languageName: node + linkType: hard + +"supports-color@npm:^5.5.0": + version: 5.5.0 + resolution: "supports-color@npm:5.5.0" + dependencies: + has-flag: "npm:^3.0.0" + checksum: 6ae5ff319bfbb021f8a86da8ea1f8db52fac8bd4d499492e30ec17095b58af11f0c55f8577390a749b1c4dde691b6a0315dab78f5f54c9b3d83f8fb5905c1c05 + languageName: node + linkType: hard + +"tar@npm:^6.1.11, tar@npm:^6.1.2": + version: 6.2.0 + resolution: "tar@npm:6.2.0" + dependencies: + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: 02ca064a1a6b4521fef88c07d389ac0936730091f8c02d30ea60d472e0378768e870769ab9e986d87807bfee5654359cf29ff4372746cc65e30cbddc352660d8 + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: "npm:^7.0.0" + checksum: 487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892 + languageName: node + linkType: hard + +"touch@npm:^3.1.0": + version: 3.1.0 + resolution: "touch@npm:3.1.0" + dependencies: + nopt: "npm:~1.0.10" + bin: + nodetouch: ./bin/nodetouch.js + checksum: dacb4a639401b83b0a40b56c0565e01096e5ecf38b22a4840d9eeb642a5bea136c6a119e4543f9b172349a5ee343b10cda0880eb47f7d7ddfd6eac59dcf53244 + languageName: node + linkType: hard + +"undefsafe@npm:^2.0.5": + version: 2.0.5 + resolution: "undefsafe@npm:2.0.5" + checksum: 96c0466a5fbf395917974a921d5d4eee67bca4b30d3a31ce7e621e0228c479cf893e783a109af6e14329b52fe2f0cb4108665fad2b87b0018c0df6ac771261d5 + languageName: node + linkType: hard + +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: "npm:^4.0.0" + checksum: 6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + languageName: node + linkType: hard + +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" + dependencies: + isexe: "npm:^3.1.1" + bin: + node-which: bin/which.js + checksum: 449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 + languageName: node + linkType: hard + +"ws@npm:^8.13.0": + version: 8.14.2 + resolution: "ws@npm:8.14.2" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 35b4c2da048b8015c797fd14bcb5a5766216ce65c8a5965616a5440ca7b6c3681ee3cbd0ea0c184a59975556e9d58f2002abf8485a14d11d3371770811050a16 + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + languageName: node + linkType: hard diff --git a/common/.gitignore b/common/.gitignore new file mode 100644 index 0000000000..836120c1e2 --- /dev/null +++ b/common/.gitignore @@ -0,0 +1,7 @@ +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/common/build.clj b/common/build.clj index 0719c72aa0..0d864ee779 100644 --- a/common/build.clj +++ b/common/build.clj @@ -12,4 +12,4 @@ (b/javac {:src-dirs ["src"] :class-dir class-dir :basis basis - :javac-opts ["-source" "17" "-target" "17"]})) + :javac-opts ["-source" "21" "-target" "21" "-proc:none"]})) diff --git a/common/deps.edn b/common/deps.edn index 28c4b5b9c6..9819697cf7 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -1,37 +1,47 @@ {:deps {org.clojure/clojure {:mvn/version "1.11.1"} - org.clojure/data.json {:mvn/version "2.4.0"} + org.clojure/data.json {:mvn/version "2.5.0"} org.clojure/tools.cli {:mvn/version "1.0.219"} - org.clojure/clojurescript {:mvn/version "1.11.60"} + org.clojure/clojurescript {:mvn/version "1.11.132"} org.clojure/test.check {:mvn/version "1.1.1"} org.clojure/data.fressian {:mvn/version "1.0.0"} ;; Logging - org.apache.logging.log4j/log4j-api {:mvn/version "2.20.0"} - org.apache.logging.log4j/log4j-core {:mvn/version "2.20.0"} - org.apache.logging.log4j/log4j-web {:mvn/version "2.20.0"} - org.apache.logging.log4j/log4j-jul {:mvn/version "2.20.0"} - org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.20.0"} - org.slf4j/slf4j-api {:mvn/version "2.0.7"} - pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.30"} + org.apache.logging.log4j/log4j-api {:mvn/version "2.22.1"} + org.apache.logging.log4j/log4j-core {:mvn/version "2.22.1"} + org.apache.logging.log4j/log4j-web {:mvn/version "2.22.1"} + org.apache.logging.log4j/log4j-jul {:mvn/version "2.22.1"} + org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.22.1"} + org.slf4j/slf4j-api {:mvn/version "2.0.10"} + pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.32"} - selmer/selmer {:mvn/version "1.12.58"} + selmer/selmer {:mvn/version "1.12.59"} criterium/criterium {:mvn/version "0.4.6"} - metosin/jsonista {:mvn/version "0.3.7"} - metosin/malli {:mvn/version "0.11.0"} + metosin/jsonista {:mvn/version "0.3.8"} + metosin/malli {:mvn/version "0.14.0"} expound/expound {:mvn/version "0.9.0"} com.cognitect/transit-clj {:mvn/version "1.0.333"} com.cognitect/transit-cljs {:mvn/version "0.8.280"} java-http-clj/java-http-clj {:mvn/version "0.4.3"} + integrant/integrant {:mvn/version "0.8.1"} - funcool/cuerdas {:mvn/version "2022.06.16-403"} - funcool/promesa {:mvn/version "11.0.671"} - funcool/datoteka {:mvn/version "3.0.66" - :exclusions [funcool/promesa]} + org.apache.commons/commons-pool2 {:mvn/version "2.12.0"} + org.graalvm.js/js {:mvn/version "23.0.2"} - lambdaisland/uri {:mvn/version "1.15.125" + funcool/tubax {:mvn/version "2021.05.20-0"} + funcool/cuerdas {:mvn/version "2023.11.09-407"} + funcool/promesa + {:git/sha "0c5ed6ad033515a2df4b55addea044f60e9653d0" + :git/url "https://github.com/funcool/promesa"} + + funcool/datoteka + {:git/sha "5ac3781" + :git/tag "3.0.0" + :git/url "https://github.com/funcool/datoteka"} + + lambdaisland/uri {:mvn/version "1.16.134" :exclusions [org.clojure/data.json]} frankiesardo/linked {:mvn/version "1.3.0"} @@ -41,14 +51,20 @@ ;; exception printing fipp/fipp {:mvn/version "0.6.26"} + + io.github.eerohele/pp + {:git/tag "2024-01-04.60" + :git/sha "e8a9773"} + io.aviso/pretty {:mvn/version "1.4.4"} environ/environ {:mvn/version "1.2.0"}} - :paths ["src" "target/classes"] + :paths ["src" "vendor" "target/classes"] :aliases {:dev {:extra-deps {org.clojure/tools.namespace {:mvn/version "RELEASE"} - thheller/shadow-cljs {:mvn/version "2.20.16"} + thheller/shadow-cljs {:mvn/version "2.27.4"} + com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"} com.bhauman/rebel-readline {:mvn/version "RELEASE"} criterium/criterium {:mvn/version "RELEASE"} mockery/mockery {:mvn/version "RELEASE"}} @@ -56,7 +72,7 @@ :build {:extra-deps - {io.github.clojure/tools.build {:git/tag "v0.9.3" :git/sha "e537cd1"}} + {io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}} :ns-default build} :test diff --git a/common/dev/user.clj b/common/dev/user.clj index 3f56de5f4e..3cb650922f 100644 --- a/common/dev/user.clj +++ b/common/dev/user.clj @@ -6,13 +6,18 @@ (ns user (:require + [app.common.pprint :as pp] + [app.common.schema :as sm] + [app.common.schema.desc-js-like :as smdj] + [app.common.schema.desc-native :as smdn] + [app.common.schema.generators :as sg] [clojure.java.io :as io] [clojure.pprint :refer [pprint print-table]] [clojure.repl :refer :all] [clojure.spec.alpha :as s] [clojure.spec.gen.alpha :as sgen] [clojure.test :as test] - [clojure.test.check.generators :as gen] + [clojure.test.check.generators :as tgen] [clojure.tools.namespace.repl :as repl] [clojure.walk :refer [macroexpand-all]] [criterium.core :as crit])) diff --git a/common/package.json b/common/package.json index 74298bf21b..4d2e78a791 100644 --- a/common/package.json +++ b/common/package.json @@ -1,20 +1,31 @@ { - "name": "penpot-common", + "name": "common", "version": "1.0.0", "main": "index.js", "license": "MPL-2.0", - "dependencies": { - "luxon": "^3.3.0" + "author": "Kaleidos INC", + "private": true, + "packageManager": "yarn@4.0.2", + "repository": { + "type": "git", + "url": "https://github.com/penpot/penpot" }, - "scripts": { - "compile-and-watch-test": "clojure -M:dev:shadow-cljs watch test", - "compile-test": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'", - "run-test": "node target/test.js", - "test": "yarn run compile-test && yarn run run-test" + "dependencies": { + "luxon": "^3.4.2", + "sax": "^1.2.4" }, "devDependencies": { - "shadow-cljs": "2.20.16", + "shadow-cljs": "2.27.4", "source-map-support": "^0.5.21", - "ws": "^8.11.0" + "ws": "^8.13.0" + }, + "scripts": { + "fmt:clj:check": "cljfmt check --parallel=false src/ test/", + "fmt:clj": "cljfmt fix --parallel=true src/ test/", + "lint:clj": "clj-kondo --parallel --lint src/", + "test:watch": "clojure -M:dev:shadow-cljs watch test", + "test:compile": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'", + "test:run": "node target/test.js", + "test": "yarn run test:compile && yarn run test:run" } } diff --git a/common/scripts/repl b/common/scripts/repl index e139dba257..1efe773de4 100755 --- a/common/scripts/repl +++ b/common/scripts/repl @@ -1,7 +1,18 @@ #!/usr/bin/env bash export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS" -export OPTIONS="-A:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-XX:+UseG1GC -J-XX:-OmitStackTraceInFastThrow -J-Xms50m -J-Xmx1024m"; + +export OPTIONS=" + -A:dev \ + -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \ + -J-Djdk.attach.allowAttachSelf \ + -J-Dpolyglot.engine.WarnInterpreterOnly=false \ + -J-XX:+EnableDynamicAgentLoading \ + -J-XX:-OmitStackTraceInFastThrow \ + -J-XX:+UnlockDiagnosticVMOptions \ + -J-XX:+DebugNonSafepoints \ + -J-Djdk.tracePinnedThreads=full" + export OPTIONS_EVAL="nil" # export OPTIONS_EVAL="(set! *warn-on-reflection* true)" diff --git a/common/shadow-cljs.edn b/common/shadow-cljs.edn index a06131055d..274f6dae1a 100644 --- a/common/shadow-cljs.edn +++ b/common/shadow-cljs.edn @@ -1,8 +1,4 @@ {:deps {:aliases [:dev]} - ;; :http {:port 3448} - ;; :nrepl {:port 3447} - :jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"] - :builds {:test {:target :node-test diff --git a/common/src/app/common/attrs.cljc b/common/src/app/common/attrs.cljc index 7bc8d5001e..0c25331785 100644 --- a/common/src/app/common/attrs.cljc +++ b/common/src/app/common/attrs.cljc @@ -6,8 +6,9 @@ (ns app.common.attrs (:require - [app.common.geom.shapes.transforms :as gtr] - [app.common.math :as mth])) + [app.common.geom.shapes :as gsh] + [app.common.math :as mth] + [app.common.text :as txt])) (defn- get-attr [obj attr] @@ -24,7 +25,8 @@ value (if-let [points (:points obj)] (if (not= points :multiple) - (let [rect (gtr/selection-rect [obj])] + ;; FIXME: consider using gsh/shape->rect ?? + (let [rect (gsh/shapes->rect [obj])] (if (= attr :ox) (:x rect) (:y rect))) :multiple) (get obj attr ::unset))) @@ -112,3 +114,15 @@ (persistent! result))))) +(defn get-text-attrs-multi + "Gets the multi attributes for a text shape. Splits the content by type and gets the attributes depending + on the node type" + [{:keys [content]} defaults attrs] + (let [root-attrs (->> attrs (filter (set txt/root-attrs))) + paragraph-attrs (->> attrs (filter (set txt/paragraph-attrs))) + text-node-attrs (->> attrs (filter (set txt/text-node-attrs)))] + (merge + defaults + (get-attrs-multi (->> (txt/node-seq txt/is-root-node? content)) root-attrs) + (get-attrs-multi (->> (txt/node-seq txt/is-paragraph-node? content)) paragraph-attrs) + (get-attrs-multi (->> (txt/node-seq txt/is-text-node? content)) text-node-attrs)))) diff --git a/common/src/app/common/colors.cljc b/common/src/app/common/colors.cljc index 4e7a5dfd09..69de60bd51 100644 --- a/common/src/app/common/colors.cljc +++ b/common/src/app/common/colors.cljc @@ -5,19 +5,453 @@ ;; Copyright (c) KALEIDOS INC (ns app.common.colors - (:refer-clojure :exclude [test])) + (:refer-clojure :exclude [test]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.math :as mth] + [cuerdas.core :as str])) (def black "#000000") -(def canvas "#E8E9EA") + (def default-layout "#DE4762") -(def gray-10 "#E3E3E3") (def gray-20 "#B1B2B5") -(def gray-30 "#7B7D85") -(def gray-40 "#64666A") -(def gray-50 "#303236") (def info "#59B9E2") (def test "#fabada") (def white "#FFFFFF") -(def primary "#31EFB8") -(def danger "#E65244") (def warning "#FC8802") + +;; new-css-system colors +(def new-primary "#7efff5") +(def new-danger "#ff3277") +(def new-warning "#fe4811") +(def new-primary-light "#6911d4") +(def background-quaternary "#2e3434") +(def background-quaternary-light "#eef0f2") +(def canvas "#E8E9EA") + +(def names + {"aliceblue" "#f0f8ff" + "antiquewhite" "#faebd7" + "aqua" "#00ffff" + "aquamarine" "#7fffd4" + "azure" "#f0ffff" + "beige" "#f5f5dc" + "bisque" "#ffe4c4" + "black" "#000000" + "blanchedalmond" "#ffebcd" + "blue" "#0000ff" + "blueviolet" "#8a2be2" + "brown" "#a52a2a" + "burlywood" "#deb887" + "cadetblue" "#5f9ea0" + "chartreuse" "#7fff00" + "chocolate" "#d2691e" + "coral" "#ff7f50" + "cornflowerblue" "#6495ed" + "cornsilk" "#fff8dc" + "crimson" "#dc143c" + "cyan" "#00ffff" + "darkblue" "#00008b" + "darkcyan" "#008b8b" + "darkgoldenrod" "#b8860b" + "darkgray" "#a9a9a9" + "darkgreen" "#006400" + "darkgrey" "#a9a9a9" + "darkkhaki" "#bdb76b" + "darkmagenta" "#8b008b" + "darkolivegreen" "#556b2f" + "darkorange" "#ff8c00" + "darkorchid" "#9932cc" + "darkred" "#8b0000" + "darksalmon" "#e9967a" + "darkseagreen" "#8fbc8f" + "darkslateblue" "#483d8b" + "darkslategray" "#2f4f4f" + "darkslategrey" "#2f4f4f" + "darkturquoise" "#00ced1" + "darkviolet" "#9400d3" + "deeppink" "#ff1493" + "deepskyblue" "#00bfff" + "dimgray" "#696969" + "dimgrey" "#696969" + "dodgerblue" "#1e90ff" + "firebrick" "#b22222" + "floralwhite" "#fffaf0" + "forestgreen" "#228b22" + "fuchsia" "#ff00ff" + "gainsboro" "#dcdcdc" + "ghostwhite" "#f8f8ff" + "gold" "#ffd700" + "goldenrod" "#daa520" + "gray" "#808080" + "green" "#008000" + "greenyellow" "#adff2f" + "grey" "#808080" + "honeydew" "#f0fff0" + "hotpink" "#ff69b4" + "indianred" "#cd5c5c" + "indigo" "#4b0082" + "ivory" "#fffff0" + "khaki" "#f0e68c" + "lavender" "#e6e6fa" + "lavenderblush" "#fff0f5" + "lawngreen" "#7cfc00" + "lemonchiffon" "#fffacd" + "lightblue" "#add8e6" + "lightcoral" "#f08080" + "lightcyan" "#e0ffff" + "lightgoldenrodyellow" "#fafad2" + "lightgray" "#d3d3d3" + "lightgreen" "#90ee90" + "lightgrey" "#d3d3d3" + "lightpink" "#ffb6c1" + "lightsalmon" "#ffa07a" + "lightseagreen" "#20b2aa" + "lightskyblue" "#87cefa" + "lightslategray" "#778899" + "lightslategrey" "#778899" + "lightsteelblue" "#b0c4de" + "lightyellow" "#ffffe0" + "lime" "#00ff00" + "limegreen" "#32cd32" + "linen" "#faf0e6" + "magenta" "#ff00ff" + "maroon" "#800000" + "mediumaquamarine" "#66cdaa" + "mediumblue" "#0000cd" + "mediumorchid" "#ba55d3" + "mediumpurple" "#9370db" + "mediumseagreen" "#3cb371" + "mediumslateblue" "#7b68ee" + "mediumspringgreen" "#00fa9a" + "mediumturquoise" "#48d1cc" + "mediumvioletred" "#c71585" + "midnightblue" "#191970" + "mintcream" "#f5fffa" + "mistyrose" "#ffe4e1" + "moccasin" "#ffe4b5" + "navajowhite" "#ffdead" + "navy" "#000080" + "oldlace" "#fdf5e6" + "olive" "#808000" + "olivedrab" "#6b8e23" + "orange" "#ffa500" + "orangered" "#ff4500" + "orchid" "#da70d6" + "palegoldenrod" "#eee8aa" + "palegreen" "#98fb98" + "paleturquoise" "#afeeee" + "palevioletred" "#db7093" + "papayawhip" "#ffefd5" + "peachpuff" "#ffdab9" + "peru" "#cd853f" + "pink" "#ffc0cb" + "plum" "#dda0dd" + "powderblue" "#b0e0e6" + "purple" "#800080" + "red" "#ff0000" + "rosybrown" "#bc8f8f" + "royalblue" "#4169e1" + "saddlebrown" "#8b4513" + "salmon" "#fa8072" + "sandybrown" "#f4a460" + "seagreen" "#2e8b57" + "seashell" "#fff5ee" + "sienna" "#a0522d" + "silver" "#c0c0c0" + "skyblue" "#87ceeb" + "slateblue" "#6a5acd" + "slategray" "#708090" + "slategrey" "#708090" + "snow" "#fffafa" + "springgreen" "#00ff7f" + "steelblue" "#4682b4" + "tan" "#d2b48c" + "teal" "#008080" + "thistle" "#d8bfd8" + "tomato" "#ff6347" + "turquoise" "#40e0d0" + "violet" "#ee82ee" + "wheat" "#f5deb3" + "white" "#ffffff" + "whitesmoke" "#f5f5f5" + "yellow" "#ffff00" + "yellowgreen" "#9acd32"}) + +(def ^:private hex-color-re + #"\#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})") + +(def ^:private rgb-color-re + #"(?:|rgb)\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\)") + +(defn valid-hex-color? + [color] + (and (string? color) + (some? (re-matches hex-color-re color)))) + +(defn parse-rgb + [color] + (let [result (re-matches rgb-color-re color)] + (when (some? result) + (let [r (parse-long (nth result 1)) + g (parse-long (nth result 2)) + b (parse-long (nth result 3))] + (when (and (<= 0 r 255) (<= 0 g 255) (<= 0 b 255)) + [r g b]))))) + +(defn valid-rgb-color? + [color] + (if (string? color) + (let [result (parse-rgb color)] + (some? result)) + false)) + +(defn- normalize-hex + [color] + (if (= (count color) 4) ; of the form #RGB + (-> color + (str/replace #"\#(.)(.)(.)" "#$1$1$2$2$3$3") + (str/lower)) + (str/lower color))) + +(defn rgb->str + [[r g b a]] + (if (some? a) + (str/ffmt "rgba(%,%,%,%)" r g b a) + (str/ffmt "rgb(%,%,%)" r g b))) + +(defn rgb->hsv + [[red green blue]] + (let [max (d/max red green blue) + min (d/min red green blue) + val max] + (if (= min max) + [0 0 val] + (let [delta (- max min) + sat (/ delta max) + hue (if (= red max) + (/ (- green blue) delta) + (if (= green max) + (+ 2 (/ (- blue red) delta)) + (+ 4 (/ (- red green) delta)))) + hue (* 60 hue) + hue (if (< hue 0) + (+ hue 360) + hue) + hue (if (> hue 360) + (- hue 360) + hue)] + [hue sat val])))) + +(defn hsv->rgb + [[h s brightness]] + (if (= s 0) + [brightness brightness brightness] + (let [sextant (int (mth/floor (/ h 60))) + remainder (- (/ h 60) sextant) + val1 (int (* brightness (- 1 s))) + val2 (int (* brightness (- 1 (* s remainder)))) + val3 (int (* brightness (- 1 (* s (- 1 remainder)))))] + (case sextant + 1 [val2 brightness val1] + 2 [val1 brightness val3] + 3 [val1 val2 brightness] + 4 [val3 val1 brightness] + 5 [brightness val1 val2] + 6 [brightness val3 val1] + 0 [brightness val3 val1])))) + +(defn hex->rgb + [color] + (try + (let [rgb #?(:clj (Integer/parseInt (subs color 1) 16) + :cljs (js/parseInt (subs color 1) 16)) + r (bit-shift-right rgb 16) + g (bit-and (bit-shift-right rgb 8) 255) + b (bit-and rgb 255)] + [r g b]) + (catch #?(:clj Throwable :cljs :default) _cause + [0 0 0]))) + +(defn- int->hex + "Convert integer to hex string" + [v] + #?(:clj (Integer/toHexString v) + :cljs (.toString v 16))) + +(defn rgb->hex + [[r g b]] + (let [r (int r) + g (int g) + b (int b)] + (if (or (not= r (bit-and r 255)) + (not= g (bit-and g 255)) + (not= b (bit-and b 255))) + (throw (ex-info "not valid rgb" {:r r :g g :b b})) + (let [rgb (bit-or (bit-shift-left r 16) + (bit-shift-left g 8) b)] + (if (< r 16) + (dm/str "#" (subs (int->hex (bit-or 0x1000000 rgb)) 1)) + (dm/str "#" (int->hex rgb))))))) + +(defn rgb->hsl + [[r g b]] + (let [norm-r (/ r 255.0) + norm-g (/ g 255.0) + norm-b (/ b 255.0) + max (d/max norm-r norm-g norm-b) + min (d/min norm-r norm-g norm-b) + l (/ (+ max min) 2.0) + h (if (= max min) 0 + (if (= max norm-r) + (* 60 (/ (- norm-g norm-b) (- max min))) + (if (= max norm-g) + (+ 120 (* 60 (/ (- norm-b norm-r) (- max min)))) + (+ 240 (* 60 (/ (- norm-r norm-g) (- max min))))))) + s (if (and (> l 0) (<= l 0.5)) + (/ (- max min) (* 2 l)) + (/ (- max min) (- 2 (* 2 l))))] + [(mod (+ h 360) 360) s l])) + +(defn hex->hsv + [v] + (-> v hex->rgb rgb->hsv)) + +(defn hex->rgba + [data opacity] + (-> (hex->rgb data) + (conj opacity))) + +(defn hex->hsl [hex] + (try + (-> hex hex->rgb rgb->hsl) + (catch #?(:clj Throwable :cljs :default) _e + [0 0 0]))) + +(defn hex->hsla + [data opacity] + (-> (hex->hsl data) + (conj opacity))) + +#?(:cljs + (defn format-hsla + [[h s l a]] + (let [precision 2 + rounded-s (* 100 (parse-double (d/format-precision s precision))) + rounded-l (* 100 (parse-double (d/format-precision l precision)))] + (str/concat "" h ", " rounded-s "%, " rounded-l "%, " a)))) + +(defn- hue->rgb + "Helper for hsl->rgb" + [v1 v2 vh] + (let [vh (if (< vh 0) + (+ vh 1) + (if (> vh 1) + (- vh 1) + vh))] + (cond + (< (* 6 vh) 1) (+ v1 (* (- v2 v1) 6 vh)) + (< (* 2 vh) 1) v2 + (< (* 3 vh) 2) (+ v1 (* (- v2 v1) (- (/ 2 3) vh) 6)) + :else v1))) + +(defn hsl->rgb + [[h s l]] + (if (= s 0) + (let [o (* l 255)] + [o o o]) + (let [norm-h (/ h 360.0) + temp2 (if (< l 0.5) + (* l (+ 1 s)) + (- (+ l s) + (* s l))) + temp1 (- (* l 2) temp2)] + + [(mth/round (* 255 (hue->rgb temp1 temp2 (+ norm-h (/ 1 3))))) + (mth/round (* 255 (hue->rgb temp1 temp2 norm-h))) + (mth/round (* 255 (hue->rgb temp1 temp2 (- norm-h (/ 1 3)))))]))) + +(defn hsl->hex + [v] + (-> v hsl->rgb rgb->hex)) + +(defn hsl->hsv + [hsl] + (-> hsl hsl->rgb rgb->hsv)) + +(defn hsv->hex + [hsv] + (-> hsv hsv->rgb rgb->hex)) + +(defn hsv->hsl + [hsv] + (-> hsv hsv->hex hex->hsl)) + +(defn expand-hex + [v] + (cond + (re-matches #"^[0-9A-Fa-f]$" v) + (dm/str v v v v v v) + + (re-matches #"^[0-9A-Fa-f]{2}$" v) + (dm/str v v v) + + (re-matches #"^[0-9A-Fa-f]{3}$" v) + (let [a (nth v 0) + b (nth v 1) + c (nth v 2)] + (dm/str a a b b c c)) + + :else + v)) + +(defn prepend-hash + [color] + (if (= "#" (subs color 0 1)) + color + (dm/str "#" color))) + +(defn remove-hash + [color] + (if (str/starts-with? color "#") + (subs color 1) + color)) + +(defn color-string? + [color] + (and (string? color) + (or (valid-hex-color? color) + (valid-rgb-color? color) + (contains? names color)))) + +(defn parse + [color] + (when (string? color) + (if (or (valid-hex-color? color) + (valid-hex-color? (dm/str "#" color))) + (normalize-hex color) + (or (some-> (parse-rgb color) (rgb->hex)) + (get names (str/lower color)))))) + +(def color-names + (into [] (keys names))) + +(def empty-color + (into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity])) + +(defn next-rgb + "Given a color in rgb returns the next color" + [[r g b]] + (cond + (and (= 255 r) (= 255 g) (= 255 b)) + (throw (ex-info "cannot get next color" {:r r :g g :b b})) + + (and (= 255 g) (= 255 b)) + [(inc r) 0 0] + + (= 255 b) + [r (inc g) 0] + + :else + [r g (inc b)])) diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index d1a9bb0db7..7368036314 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -9,15 +9,16 @@ data resources." (:refer-clojure :exclude [read-string hash-map merge name update-vals parse-double group-by iteration concat mapcat - parse-uuid]) + parse-uuid max min]) #?(:cljs (:require-macros [app.common.data])) (:require - #?(:cljs [cljs.reader :as r] - :clj [clojure.edn :as r]) #?(:cljs [cljs.core :as c] :clj [clojure.core :as c]) + #?(:cljs [cljs.reader :as r] + :clj [clojure.edn :as r]) + #?(:cljs [goog.array :as garray]) [app.common.math :as mth] [clojure.set :as set] [cuerdas.core :as str] @@ -56,6 +57,14 @@ #?(:cljs (instance? lkm/LinkedMap o) :clj (instance? LinkedMap o))) +(defn vec2 + "Creates a optimized vector compatible type of length 2 backed + internally with MapEntry impl because it has faster access method + for its fields." + [o1 o2] + #?(:clj (clojure.lang.MapEntry. o1 o2) + :cljs (cljs.core/->MapEntry o1 o2 nil))) + #?(:clj (defmethod print-method clojure.lang.PersistentQueue [q, w] ;; Overload the printer for queues so they look like fish @@ -145,10 +154,6 @@ (transient-concat c1 more) (transient-concat [] (cons c1 more))))) -(defn preconj - [coll elem] - (into [elem] coll)) - (defn enumerate ([items] (enumerate items 0)) ([items start] @@ -219,29 +224,71 @@ [coll] (into [] (remove nil?) coll)) + (defn without-nils "Given a map, return a map removing key-value pairs when value is `nil`." - ([] (remove (comp nil? val))) + ([] + (remove (comp nil? val))) ([data] - (into {} (without-nils) data))) + (reduce-kv (fn [data k v] + (if (nil? v) + (dissoc data k) + data)) + data + data))) (defn without-qualified ([] (remove (comp qualified-keyword? key))) ([data] - (into {} (without-qualified) data))) + (reduce-kv (fn [data k _] + (if (qualified-keyword? k) + (dissoc data k) + data)) + data + data))) (defn without-keys "Return a map without the keys provided in the `keys` parameter." [data keys] - (persistent! - (reduce dissoc! - (if (editable-collection? data) - (transient data) - (transient {})) - keys))) + (if (editable-collection? data) + (persistent! (reduce dissoc! (transient data) keys)) + (reduce dissoc data keys))) + +(defn patch-object + "Changes is some attributes that need to change in object. + When the attribute is nil it will be removed. + + For example + - object: {:a 1 :b {:foo 1 :bar 2} :c 10} + - changes: {:a 2 :b {:foo nil :k 3}} + - result: {:a 2 :b {:bar 2 :k 3} :c 10} + " + ([changes] + #(patch-object % changes)) + + ([object changes] + (if object + (->> changes + (reduce-kv + (fn [object key value] + (cond + (map? value) + (let [current (get object key)] + (assoc object key (patch-object current value))) + + (and (nil? value) (record? object)) + (assoc object key nil) + + (nil? value) + (dissoc object key value) + + :else + (assoc object key value))) + object)) + changes))) (defn remove-at-index "Takes a vector and returns a vector with an element in the @@ -262,12 +309,24 @@ (defn zip [col1 col2] (map vector col1 col2)) +(defn zip-all + "Return a zip of both collections, extended to the lenght of the longest one, + and padding the shorter one with nils as needed." + [col1 col2] + (let [diff (- (count col1) (count col2))] + (cond (pos? diff) (zip col1 (c/concat col2 (repeat nil))) + (neg? diff) (zip (c/concat col1 (repeat nil)) col2) + :else (zip col1 col2)))) + (defn mapm "Map over the values of a map" ([mfn] - (map (fn [[key val]] [key (mfn key val)]))) + (map (fn [[key val]] (vec2 key (mfn key val))))) ([mfn coll] - (into {} (mapm mfn) coll))) + (reduce-kv (fn [coll k v] + (assoc coll k (mfn k v))) + coll + coll))) (defn removev "Returns a vector of the items in coll for which (fn item) returns logical false" @@ -344,6 +403,13 @@ [coll] (partial get coll)) +(defn update-vals + [m f] + (reduce-kv (fn [acc k v] + (assoc acc k (f v))) + m + m)) + (defn update-in-when [m key-seq f & args] (let [found (get-in m key-seq sentinel)] @@ -481,6 +547,13 @@ (->> (apply c/iteration args) (concat-all))) +(defn add-at-index + "Insert an element in a vector at an arbitrary index" + [coll index element] + (assert (vector? coll)) + (let [[before after] (split-at index coll)] + (concat-vec [] before [element] after))) + (defn insert-at-index "Insert a list of elements at the given index of a previous list. Replace all existing elems." @@ -546,8 +619,8 @@ (defn num-string? [v] ;; https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number #?(:cljs (and (string? v) - (not (js/isNaN v)) - (not (js/isNaN (parse-double v)))) + (not (js/isNaN v)) + (not (js/isNaN (parse-double v)))) :clj (not= (parse-double v :nan) :nan))) @@ -590,23 +663,47 @@ ([a] (mth/finite? a)) ([a b] - (and (mth/finite? a) - (mth/finite? b))) + (and ^boolean (mth/finite? a) + ^boolean (mth/finite? b))) ([a b c] - (and (mth/finite? a) - (mth/finite? b) - (mth/finite? c))) + (and ^boolean (mth/finite? a) + ^boolean (mth/finite? b) + ^boolean (mth/finite? c))) ([a b c d] - (and (mth/finite? a) - (mth/finite? b) - (mth/finite? c) - (mth/finite? d))) + (and ^boolean (mth/finite? a) + ^boolean (mth/finite? b) + ^boolean (mth/finite? c) + ^boolean (mth/finite? d))) ([a b c d & others] - (and (mth/finite? a) - (mth/finite? b) - (mth/finite? c) - (mth/finite? d) - (every? mth/finite? others)))) + (and ^boolean (mth/finite? a) + ^boolean (mth/finite? b) + ^boolean (mth/finite? c) + ^boolean (mth/finite? d) + ^boolean (every? mth/finite? others)))) + +(defn safe+ + [a b] + (if (mth/finite? a) (+ a b) a)) + +(defn max + ([a] a) + ([a b] (mth/max a b)) + ([a b c] (mth/max a b c)) + ([a b c d] (mth/max a b c d)) + ([a b c d e] (mth/max a b c d e)) + ([a b c d e f] (mth/max a b c d e f)) + ([a b c d e f & other] + (reduce max (mth/max a b c d e f) other))) + +(defn min + ([a] a) + ([a b] (mth/min a b)) + ([a b c] (mth/min a b c)) + ([a b c d] (mth/min a b c d)) + ([a b c d e] (mth/min a b c d e)) + ([a b c d e f] (mth/min a b c d e f)) + ([a b c d e f & other] + (reduce min (mth/min a b c d e f) other))) (defn check-num "Function that checks if a number is nil or nan. Will return 0 when not @@ -619,20 +716,19 @@ (defn name "Improved version of name that won't fail if the input is not a keyword" - ([maybe-keyword] (name maybe-keyword nil)) - ([maybe-keyword default-value] - (cond - (keyword? maybe-keyword) - (c/name maybe-keyword) + [maybe-keyword] + (cond + (nil? maybe-keyword) + nil - (string? maybe-keyword) - maybe-keyword + (keyword? maybe-keyword) + (c/name maybe-keyword) - (nil? maybe-keyword) default-value + (string? maybe-keyword) + maybe-keyword - :else - (or default-value - (str maybe-keyword))))) + :else + (str maybe-keyword))) (defn prefix-keyword "Given a keyword and a prefix will return a new keyword with the prefix attached @@ -759,49 +855,46 @@ (toString 16) (padStart 2 "0")))) +(defn unstable-sort + ([items] + (unstable-sort compare items)) + ([comp-fn items] + #?(:cljs + (let [items (to-array items)] + (garray/sort items comp-fn) + (seq items)) + :clj + (sort comp-fn items)))) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; String Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def stylize-re1 (re-pattern "(?u)(\\p{Lu}+[\\p{Ll}\\u0027\\p{Ps}\\p{Pe}]*)")) -(def stylize-re2 (re-pattern "(?u)[^\\p{L}\\p{N}\\u0027\\p{Ps}\\p{Pe}\\?!]+")) +(def ^:const trail-zeros-regex-1 #"\.0+$") +(def ^:const trail-zeros-regex-2 #"(\.\d*[^0])0+$") -(defn- stylize-split - [s] - (some-> s - (name) - (str/replace stylize-re1 "-$1") - (str/split stylize-re2) - (seq))) +#?(:cljs + (defn format-precision + "Creates a number with predetermined precision and then removes the trailing 0. + Examples: + 12.0123, 0 => 12 + 12.0123, 1 => 12 + 12.0123, 2 => 12.01" + [num precision] -(defn- stylize-join - ([coll every-fn join-with] - (when (seq coll) - (str/join join-with (map every-fn coll)))) - ([[fst & rst] first-fn rest-fn join-with] - (when (string? fst) - (str/join join-with (cons (first-fn fst) (map rest-fn rst)))))) - -(defn stylize - ([s every-fn join-with] - (stylize s every-fn every-fn join-with)) - ([s first-fn rest-fn join-with] - (let [remove-empty #(seq (remove empty? %))] - (some-> (stylize-split s) - (remove-empty) - (stylize-join first-fn rest-fn join-with))))) - -(defn camel - "Output will be: lowerUpperUpperNoSpaces - accepts strings and keywords" - [s] - (stylize s str/lower str/capital "")) - -(defn kebab - "Output will be: lower-cased-and-separated-with-dashes - accepts strings and keywords" - [s] - (stylize s str/lower "-")) + (if (number? num) + (try + (let [num-str (mth/to-fixed num precision) + ;; Remove all trailing zeros after the comma 100.00000 + num-str (str/replace num-str trail-zeros-regex-1 "")] + ;; Remove trailing zeros after a decimal number: 0.001|00| + (if-let [m (re-find trail-zeros-regex-2 num-str)] + (str/replace num-str (first m) (second m)) + num-str)) + (catch :default _ + (str num))) + (str num)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Util protocols @@ -815,3 +908,25 @@ (extend-protocol ICloseable AutoCloseable (close! [this] (.close this)))) + +(defn take-until + "Returns a lazy sequence of successive items from coll until + (pred item) returns true, including that item" + ([pred] + (halt-when pred (fn [r h] (conj r h)))) + + ([pred coll] + (transduce (take-until pred) conj [] coll))) + +(defn safe-subvec + "Wrapper around subvec so it doesn't throw an exception but returns nil instead" + ([v start] + (when (and (some? v) + (> start 0) (< start (count v))) + (subvec v start))) + ([v start end] + (let [size (count v)] + (when (and (some? v) + (>= start 0) (< start size) + (>= end 0) (<= start end) (<= end size)) + (subvec v start end))))) diff --git a/common/src/app/common/data/macros.cljc b/common/src/app/common/data/macros.cljc index 836656d938..7740ef3626 100644 --- a/common/src/app/common/data/macros.cljc +++ b/common/src/app/common/data/macros.cljc @@ -7,14 +7,14 @@ #_:clj-kondo/ignore (ns app.common.data.macros "Data retrieval & manipulation specific macros." - (:refer-clojure :exclude [get-in select-keys str with-open]) + (:refer-clojure :exclude [get-in select-keys str with-open min max]) #?(:cljs (:require-macros [app.common.data.macros])) (:require #?(:clj [clojure.core :as c] :cljs [cljs.core :as c]) [app.common.data :as d] - [cuerdas.core :as str] - [cljs.analyzer.api :as aapi])) + [cljs.analyzer.api :as aapi] + [cuerdas.core :as str])) (defmacro select-keys "A macro version of `select-keys`. Useful when keys vector is known @@ -24,7 +24,7 @@ keys in contrast to clojure.core/select-keys" [target keys] (assert (vector? keys) "keys expected to be a vector") - `{ ~@(mapcat (fn [key] [key (list `c/get target key)]) keys) ~@[] }) + `{~@(mapcat (fn [key] [key (list `c/get target key)]) keys) ~@[]}) (defmacro get-in "A macro version of `get-in`. Useful when the keys vector is known at @@ -120,13 +120,9 @@ "A macro based, optimized variant of `get` that access the property directly on CLJS, on CLJ works as get." [obj prop] - ;; `(do - ;; (when-not (record? ~obj) - ;; (js/console.trace (pr-str ~obj))) - ;; (c/get ~obj ~prop))) (if (:ns &env) - (list (symbol ".") (with-meta obj {:tag 'js}) (symbol (str "-" (c/name prop)))) - `(c/get ~obj ~prop))) + (list 'js* (c/str "(~{}?." (str/snake prop) "?? ~{})") obj (list 'cljs.core/get obj prop)) + (list `c/get obj prop))) (def ^:dynamic *assert-context* nil) @@ -144,7 +140,7 @@ :else (str "expr assert: " (pr-str expr)))] (when *assert* - `(binding [*assert-context* true] + `(binding [*assert-context* ~hint] (when-not ~expr (let [hint# ~hint params# {:type :assertion @@ -154,7 +150,7 @@ (defmacro verify! ([expr] - `(assert! nil ~expr)) + `(verify! nil ~expr)) ([hint expr] (let [hint (cond (vector? hint) @@ -165,7 +161,7 @@ :else (str "expr assert: " (pr-str expr)))] - `(binding [*assert-context* true] + `(binding [*assert-context* ~hint] (when-not ~expr (let [hint# ~hint params# {:type :assertion diff --git a/common/src/app/common/debug.clj b/common/src/app/common/debug.clj new file mode 100644 index 0000000000..f23c498ed1 --- /dev/null +++ b/common/src/app/common/debug.clj @@ -0,0 +1,36 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.debug + (:require + [app.common.logging :as l] + [app.common.pprint :as pp])) + +(defn pprint + [expr] + (l/raw! :debug + (binding [*print-level* pp/default-level + *print-length* pp/default-length] + (with-out-str + (println "tap dbg:") + (pp/pprint expr {:max-width pp/default-width}))))) + + +(def store (atom {})) + +(defn get-stored + [] + (deref store)) + +(defn tap-handler + [v] + (if (and (vector? v) + (keyword (first v))) + (let [[command obj] v] + (case command + (:print :prn :pprint) (pprint obj) + :store (reset! store obj))) + (pprint v))) diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc index 2efdb30bd4..5cceeb7222 100644 --- a/common/src/app/common/exceptions.cljc +++ b/common/src/app/common/exceptions.cljc @@ -7,10 +7,12 @@ (ns app.common.exceptions "A helpers for work with exceptions." #?(:cljs (:require-macros [app.common.exceptions])) + (:refer-clojure :exclude [instance?]) (:require #?(:clj [clojure.stacktrace :as strace]) [app.common.pprint :as pp] [app.common.schema :as sm] + [clojure.core :as c] [clojure.spec.alpha :as s] [cuerdas.core :as str] [expound.alpha :as expound]) @@ -20,6 +22,9 @@ #?(:clj (set! *warn-on-reflection* true)) +(def ^:dynamic *data-length* 8) +(def ^:dynamic *data-level* 8) + (defmacro error [& {:keys [type hint] :as params}] `(ex-info ~(or hint (name type)) @@ -32,11 +37,6 @@ [& params] `(throw (error ~@params))) -;; FIXME deprecate -(defn try* - [f on-error] - (try (f) (catch #?(:clj Throwable :cljs :default) e (on-error e)))) - ;; http://clj-me.cgrand.net/2013/09/11/macros-closures-and-unexpected-object-retention/ ;; Explains the use of ^:once metadata @@ -54,145 +54,163 @@ (defn ex-info? [v] - (instance? #?(:clj clojure.lang.IExceptionInfo :cljs cljs.core.ExceptionInfo) v)) + (c/instance? #?(:clj clojure.lang.IExceptionInfo :cljs cljs.core.ExceptionInfo) v)) (defn error? [v] - (instance? #?(:clj clojure.lang.IExceptionInfo :cljs cljs.core.ExceptionInfo) v)) + (c/instance? #?(:clj clojure.lang.IExceptionInfo :cljs cljs.core.ExceptionInfo) v)) (defn exception? [v] - (instance? #?(:clj java.lang.Throwable :cljs js/Error) v)) + (c/instance? #?(:clj java.lang.Throwable :cljs js/Error) v)) #?(:clj (defn runtime-exception? [v] - (instance? RuntimeException v))) + (c/instance? RuntimeException v))) + +#?(:clj + (defn instance? + [class cause] + (loop [cause cause] + (if (c/instance? class cause) + cause + (when-let [cause (ex-cause cause)] + (recur cause)))))) + +;; NOTE: idea for a macro for error handling +;; (pu/try-let [cause (p/await (get-object-data backend object))] +;; (ex/instance? NoSuchKeyException cause) +;; (ex/raise :type :not-found +;; :code :object-not-found +;; :hint "s3 object not found" +;; :cause cause)) (defn explain - ([data] (explain data nil)) - ([data {:keys [level length] :or {level 8 length 10} :as opts}] - (cond - ;; ;; NOTE: a special case for spec validation errors on integrant - (and (= (:reason data) :integrant.core/build-failed-spec) - (contains? data :explain)) - (explain (:explain data) opts) + [data & {:as opts}] + (cond + ;; NOTE: a special case for spec validation errors on integrant + (and (= (:reason data) :integrant.core/build-failed-spec) + (contains? data :explain)) + (explain (:explain data) opts) + + (and (contains? data ::s/problems) + (contains? data ::s/value) + (contains? data ::s/spec)) + (binding [s/*explain-out* expound/printer] + (with-out-str + (s/explain-out (update data ::s/problems #(take (:length opts 10) %))))) + + (contains? data ::sm/explain) + (sm/humanize-explain (::sm/explain data) opts))) + +#?(:clj + (defn format-throwable + [^Throwable cause & {:keys [summary? detail? header? data? explain? chain? data-level data-length trace-length] + :or {summary? true + detail? true + header? true + data? true + explain? true + chain? true + data-length *data-length* + data-level *data-level*}}] + + (letfn [(print-trace-element [^StackTraceElement e] + (let [class (.getClassName e) + method (.getMethodName e)] + (let [match (re-matches #"^([A-Za-z0-9_.-]+)\$(\w+)__\d+$" (str class))] + (if (and match (= "invoke" method)) + (apply printf "%s/%s" (rest match)) + (printf "%s.%s" class method)))) + (printf "(%s:%d)" (or (.getFileName e) "") (.getLineNumber e))) + + (print-explain [explain] + (print " xp: ") + (let [[line & lines] (str/lines explain)] + (print line) + (newline) + (doseq [line lines] + (println " " line)))) + + (print-data [data] + (when (seq data) + (print " dt: ") + (let [[line & lines] (str/lines (pp/pprint-str data :level data-level :length data-length))] + (print line) + (newline) + (doseq [line lines] + (println " " line))))) + + (print-trace-title [^Throwable cause] + (print " → ") + (printf "%s: %s" (.getName (class cause)) + (-> (ex-message cause) + (str/lines) + (first) + (str/prune 130))) + + (when-let [^StackTraceElement e (first (.getStackTrace ^Throwable cause))] + (printf " (%s:%d)" (or (.getFileName e) "") (.getLineNumber e))) + + (newline)) + + (print-summary [^Throwable cause] + (let [causes (loop [cause (ex-cause cause) + result []] + (if cause + (recur (ex-cause cause) + (conj result cause)) + result))] + (when header? + (println "SUMMARY:")) + (print-trace-title cause) + (doseq [cause causes] + (print-trace-title cause)))) + + (print-trace [^Throwable cause] + (print-trace-title cause) + (let [st (.getStackTrace cause)] + (print " at: ") + (if-let [e (first st)] + (print-trace-element e) + (print "[empty stack trace]")) + (newline) + + (doseq [e (if (nil? trace-length) (rest st) (take (dec trace-length) (rest st)))] + (print " ") + (print-trace-element e) + (newline)))) + + (print-detail [^Throwable cause] + (print-trace cause) + (when-let [data (ex-data cause)] + (when data? + (print-data (dissoc data ::s/problems ::s/spec ::s/value ::sm/explain))) + (when explain? + (if-let [explain (explain data {:length data-length :level data-level})] + (print-explain explain))))) + + (print-all [^Throwable cause] + (when summary? + (print-summary cause)) + + (when detail? + (when header? + (println "DETAIL:")) + + (print-detail cause) + (when chain? + (loop [cause cause] + (when-let [cause (ex-cause cause)] + (newline) + (print-detail cause) + (recur cause))))))] - (and (contains? data ::s/problems) - (contains? data ::s/value) - (contains? data ::s/spec)) - (binding [s/*explain-out* expound/printer] (with-out-str - (s/explain-out (update data ::s/problems #(take length %))))) - - (contains? data ::sm/explain) - (-> (sm/humanize-data (::sm/explain data)) - (pp/pprint-str {:level level :length length}))))) + (print-all cause))))) #?(:clj -(defn format-throwable - [^Throwable cause & {:keys [summary? detail? header? data? explain? chain? data-level data-length trace-length] - :or {summary? true - detail? true - header? true - data? true - explain? true - chain? true - data-length 10 - data-level 8}}] - - (letfn [(print-trace-element [^StackTraceElement e] - (let [class (.getClassName e) - method (.getMethodName e)] - (let [match (re-matches #"^([A-Za-z0-9_.-]+)\$(\w+)__\d+$" (str class))] - (if (and match (= "invoke" method)) - (apply printf "%s/%s" (rest match)) - (printf "%s.%s" class method)))) - (printf "(%s:%d)" (or (.getFileName e) "") (.getLineNumber e))) - - (print-explain [explain] - (print " xp: ") - (let [[line & lines] (str/lines explain)] - (print line) - (newline) - (doseq [line lines] - (println " " line)))) - - (print-data [data] - (when (seq data) - (print " dt: ") - (let [[line & lines] (str/lines (pp/pprint-str data :level data-level :length data-length ))] - (print line) - (newline) - (doseq [line lines] - (println " " line))))) - - (print-trace-title [^Throwable cause] - (print " → ") - (printf "%s: %s" (.getName (class cause)) (first (str/lines (ex-message cause)))) - - (when-let [^StackTraceElement e (first (.getStackTrace ^Throwable cause))] - (printf " (%s:%d)" (or (.getFileName e) "") (.getLineNumber e))) - - (newline)) - - (print-summary [^Throwable cause] - (let [causes (loop [cause (ex-cause cause) - result []] - (if cause - (recur (ex-cause cause) - (conj result cause)) - result))] - (when header? - (println "SUMMARY:")) - (print-trace-title cause) - (doseq [cause causes] - (print-trace-title cause)))) - - (print-trace [^Throwable cause] - (print-trace-title cause) - (let [st (.getStackTrace cause)] - (print " at: ") - (if-let [e (first st)] - (print-trace-element e) - (print "[empty stack trace]")) - (newline) - - (doseq [e (if (nil? trace-length) (rest st) (take (dec trace-length) (rest st)))] - (print " ") - (print-trace-element e) - (newline)))) - - (print-detail [^Throwable cause] - (print-trace cause) - (when-let [data (ex-data cause)] - (when data? - (print-data (dissoc data ::s/problems ::s/spec ::s/value ::sm/explain))) - (when explain? - (if-let [explain (explain data {:length data-length :level data-level})] - (print-explain explain))))) - - (print-all [^Throwable cause] - (when summary? - (print-summary cause)) - - (when detail? - (when header? - (println "DETAIL:")) - - (print-detail cause) - (when chain? - (loop [cause cause] - (when-let [cause (ex-cause cause)] - (newline) - (print-detail cause) - (recur cause)))))) - ] - - (with-out-str - (print-all cause))))) - -#?(:clj -(defn print-throwable - [cause & {:as opts}] - (println (format-throwable cause opts)))) + (defn print-throwable + [cause & {:as opts}] + (println (format-throwable cause opts)))) diff --git a/common/src/app/common/features.cljc b/common/src/app/common/features.cljc new file mode 100644 index 0000000000..90b27587f0 --- /dev/null +++ b/common/src/app/common/features.cljc @@ -0,0 +1,311 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.features + (:require + [app.common.exceptions :as ex] + [app.common.schema :as sm] + [app.common.schema.desc-js-like :as-alias smdj] + [app.common.schema.generators :as smg] + [clojure.set :as set] + [cuerdas.core :as str])) + +;; The default behavior when a user interacts with penpot and runtime +;; and global features: +;; +;; - If user enables on runtime a frontend-only feature, this feature +;; and creates and/or modifies files, the feature is only availble +;; until next refresh (it is not persistent) +;; +;; - If user enables on runtime a non-migration feature, on modifying +;; a file or creating a new one, the feature becomes persistent on +;; the file and the team. All the other files of the team eventually +;; will have that feature assigned (on file modification) +;; +;; - If user enables on runtime a migration feature, that feature will +;; be ignored until a migration is explicitly executed or team +;; explicitly marked with that feature. +;; +;; The features stored on the file works as metadata information about +;; features enabled on the file and for compatibility check when a +;; user opens the file. The features stored on global, runtime or team +;; works as activators. + +(def ^:dynamic *previous* #{}) +(def ^:dynamic *current* #{}) +(def ^:dynamic *new* nil) + +(def ^:dynamic *wrap-with-objects-map-fn* identity) +(def ^:dynamic *wrap-with-pointer-map-fn* identity) + +;; A set of supported features +(def supported-features + #{"fdata/objects-map" + "fdata/pointer-map" + "fdata/shape-data-type" + "components/v2" + "styles/v2" + "layout/grid"}) + +;; A set of features enabled by default for each file, they are +;; implicit and are enabled by default and can't be disabled. The +;; features listed in this set are mainly freatures addedby file +;; migrations process, so all features referenced in migrations should +;; be here. +(def default-enabled-features + #{"fdata/shape-data-type" + "styles/v2" + "layout/grid"}) + +;; A set of features which only affects on frontend and can be enabled +;; and disabled freely by the user any time. This features does not +;; persist on file features field but can be permanently enabled on +;; team feature field +(def frontend-only-features + #{"styles/v2"}) + +;; Features that are mainly backend only or there are a proper +;; fallback when frontend reports no support for it +(def backend-only-features + #{"fdata/objects-map" + "fdata/pointer-map"}) + +;; This is a set of features that does not require an explicit +;; migration like components/v2 or the migration is not mandatory to +;; be applied (per example backend can operate in both modes with or +;; without migration applied) +(def no-migration-features + (-> #{"fdata/objects-map" + "fdata/pointer-map" + "layout/grid"} + (into frontend-only-features))) + +(sm/def! ::features + [:schema + {:title "FileFeatures" + ::smdj/inline true + :gen/gen (smg/subseq supported-features)} + ::sm/set-of-strings]) + +(defn- flag->feature + "Translate a flag to a feature name" + [flag] + (case flag + :feature-components-v2 "components/v2" + :feature-styles-v2 "styles/v2" + :feature-grid-layout "layout/grid" + :feature-fdata-objects-map "fdata/objects-map" + :feature-fdata-pointer-map "fdata/pointer-map" + nil)) + +(defn migrate-legacy-features + "A helper that translates old feature names to new names" + [features] + (cond-> (or features #{}) + (contains? features "storage/pointer-map") + (-> (conj "fdata/pointer-map") + (disj "storage/pointer-map")) + + (contains? features "storage/objects-map") + (-> (conj "fdata/objects-map") + (disj "storage/objects-map")) + + (or (contains? features "internal/geom-record") + (contains? features "internal/shape-record")) + (-> (conj "fdata/shape-data-type") + (disj "internal/geom-record") + (disj "internal/shape-record")))) + +(def xf-supported-features + (filter (partial contains? supported-features))) + +(def xf-remove-ephimeral + (remove #(str/starts-with? % "ephimeral/"))) + +(def xf-flag-to-feature + (keep flag->feature)) + +(defn get-enabled-features + "Get the globally enabled fratures set." + [flags] + (into default-enabled-features xf-flag-to-feature flags)) + +(defn get-team-enabled-features + "Get the team enabled features. + + Team features are defined as: all features found on team plus all + no-migration features enabled globally." + [flags team] + (let [enabled-features (get-enabled-features flags) + team-features (into #{} xf-remove-ephimeral (:features team))] + (-> enabled-features + (set/intersection no-migration-features) + (set/union default-enabled-features) + (set/union team-features)))) + +(defn check-client-features! + "Function used for check feature compability between currently enabled + features set on backend with the enabled featured set by the + frontend client" + [enabled-features client-features] + (when (set? client-features) + (let [not-supported (-> enabled-features + (set/difference client-features) + (set/difference frontend-only-features) + (set/difference backend-only-features))] + (when (seq not-supported) + (ex/raise :type :restriction + :code :feature-not-supported + :feature (first not-supported) + :hint (str/ffmt "client declares no support for '%' features" + (str/join "," not-supported))))) + + (let [not-supported (set/difference client-features supported-features)] + (when (seq not-supported) + (ex/raise :type :restriction + :code :feature-not-supported + :feature (first not-supported) + :hint (str/ffmt "backend does not support '%' features requested by client" + (str/join "," not-supported)))))) + + enabled-features) + +(defn check-supported-features! + "Check if a given set of features are supported by this + backend. Usually used for check if imported file features are + supported by the current backend" + [enabled-features] + (let [not-supported (set/difference enabled-features supported-features)] + (when (seq not-supported) + (ex/raise :type :restriction + :code :feature-not-supported + :feature (first not-supported) + :hint (str/ffmt "features '%' not supported" + (str/join "," not-supported))))) + enabled-features) + +(defn check-file-features! + "Function used for check feature compability between currently + enabled features set on backend with the provided featured set by + the penpot file" + ([enabled-features file-features] + (check-file-features! enabled-features file-features #{})) + ([enabled-features file-features client-features] + (let [file-features (into #{} xf-remove-ephimeral file-features) + ;; We should ignore all features that does not match with the + ;; `no-migration-features` set because we can't enable them + ;; as-is, because they probably need migrations + client-features (set/intersection client-features no-migration-features)] + (let [not-supported (-> enabled-features + (set/union client-features) + (set/difference file-features) + ;; NOTE: we don't want to raise a feature-mismatch + ;; exception for features which don't require an + ;; explicit file migration process or has no real + ;; effect on file data structure + (set/difference no-migration-features))] + (when (seq not-supported) + (ex/raise :type :restriction + :code :file-feature-mismatch + :feature (first not-supported) + :hint (str/ffmt "enabled features '%' not present in file (missing migration)" + (str/join "," not-supported))))) + + (check-supported-features! file-features) + + (let [not-supported (-> file-features + (set/difference enabled-features) + (set/difference client-features) + (set/difference backend-only-features) + (set/difference frontend-only-features))] + + (when (seq not-supported) + (ex/raise :type :restriction + :code :file-feature-mismatch + :feature (first not-supported) + :hint (str/ffmt "file features '%' not enabled" + (str/join "," not-supported)))))) + + enabled-features)) + +(defn check-teams-compatibility! + [{source-features :features} {destination-features :features}] + (when (contains? source-features "ephimeral/migration") + (ex/raise :type :restriction + :code :migration-in-progress + :hint "the source team is in migration process")) + + (when (contains? destination-features "ephimeral/migration") + (ex/raise :type :restriction + :code :migration-in-progress + :hint "the destination team is in migration process")) + + (let [not-supported (-> (or source-features #{}) + (set/difference destination-features) + (set/difference no-migration-features) + (set/difference default-enabled-features) + (seq))] + (when not-supported + (ex/raise :type :restriction + :code :team-feature-mismatch + :feature (first not-supported) + :hint (str/ffmt "the destination team does not have support '%' features" + (str/join "," not-supported))))) + + (let [not-supported (-> (or destination-features #{}) + (set/difference source-features) + (set/difference no-migration-features) + (set/difference default-enabled-features) + (seq))] + (when not-supported + (ex/raise :type :restriction + :code :team-feature-mismatch + :feature (first not-supported) + :hint (str/ffmt "the source team does not have support '%' features" + (str/join "," not-supported)))))) + + +(defn check-paste-features! + "Function used for check feature compability between currently enabled + features set on the application with the provided featured set by + the paste data (frontend clipboard)." + [enabled-features paste-features] + (let [not-supported (-> enabled-features + (set/difference paste-features) + ;; NOTE: we don't want to raise a feature-mismatch + ;; exception for features which don't require an + ;; explicit file migration process or has no real + ;; effect on file data structure + (set/difference no-migration-features))] + + (when (seq not-supported) + (ex/raise :type :restriction + :code :missing-features-in-paste-content + :feature (first not-supported) + :hint (str/ffmt "expected features '%' not present in pasted content" + (str/join "," not-supported))))) + + (let [not-supported (set/difference enabled-features supported-features)] + (when (seq not-supported) + (ex/raise :type :restriction + :code :paste-feature-not-supported + :feature (first not-supported) + :hint (str/ffmt "features '%' not supported in the application" + (str/join "," not-supported))))) + + (let [not-supported (-> paste-features + (set/difference enabled-features) + (set/difference backend-only-features) + (set/difference frontend-only-features))] + + (when (seq not-supported) + (ex/raise :type :restriction + :code :paste-feature-not-enabled + :feature (first not-supported) + :hint (str/ffmt "paste features '%' not enabled on the application" + (str/join "," not-supported)))))) + + diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/files/builder.cljc similarity index 83% rename from common/src/app/common/file_builder.cljc rename to common/src/app/common/files/builder.cljc index 9903d0f9fa..d10b494d95 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/files/builder.cljc @@ -4,18 +4,18 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.file-builder - "A version parsing helper." +(ns app.common.files.builder (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] - [app.common.geom.matrix :as gmt] + [app.common.files.changes :as ch] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.pages.changes :as ch] [app.common.pprint :as pp] [app.common.schema :as sm] + [app.common.svg :as csvg] [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] [app.common.types.file :as ctf] @@ -25,9 +25,9 @@ [app.common.uuid :as uuid] [cuerdas.core :as str])) -(def root-frame uuid/zero) -(def conjv (fnil conj [])) -(def conjs (fnil conj #{})) +(def ^:private root-id uuid/zero) +(def ^:private conjv (fnil conj [])) +(def ^:private conjs (fnil conj #{})) (defn- commit-change ([file change] @@ -38,35 +38,33 @@ :or {add-container? false fail-on-spec? false}}] (let [component-id (:current-component-id file) - change (cond-> change - (and add-container? (some? component-id)) - (cond-> - :always - (assoc :component-id component-id) + change (cond-> change + (and add-container? (some? component-id)) + (-> (assoc :component-id component-id) + (cond-> (some? (:current-frame-id file)) + (assoc :frame-id (:current-frame-id file)))) - (some? (:current-frame-id file)) - (assoc :frame-id (:current-frame-id file))) + (and add-container? (nil? component-id)) + (assoc :page-id (:current-page-id file) + :frame-id (:current-frame-id file))) + valid? (ch/check-change! change)] - (and add-container? (nil? component-id)) - (assoc :page-id (:current-page-id file) - :frame-id (:current-frame-id file)))] + (when-not valid? + (let [explain (sm/explain ::ch/change change)] + (pp/pprint (sm/humanize-explain explain)) + (when fail-on-spec? + (ex/raise :type :assertion + :code :data-validation + :hint "invalid change" + ::sm/explain explain)))) - (when fail-on-spec? - (dm/verify! (ch/change? change))) + (cond-> file + valid? + (-> (update :changes conjv change) + (update :data ch/process-changes [change] false)) - (let [valid? (ch/change? change)] - (when-not valid? - (pp/pprint change {:level 100}) - (sm/pretty-explain ::ch/change change)) - - - (cond-> file - valid? - (-> (update :changes conjv change) - (update :data ch/process-changes [change] false)) - - (not valid?) - (update :errors conjv change)))))) + (not valid?) + (update :errors conjv change))))) (defn- lookup-objects ([file] @@ -91,50 +89,6 @@ (commit-change file change {:add-container? true :fail-on-spec? fail-on-spec?}))) -(defn setup-rect-selrect [{:keys [x y width height transform] :as obj}] - (when-not (d/num? x y width height) - (ex/raise :type :assertion - :code :invalid-condition - :hint "Coords not valid for object")) - - (let [rect (gsh/make-rect x y width height) - center (gsh/center-rect rect) - selrect (gsh/rect->selrect rect) - - points (-> (gsh/rect->points rect) - (gsh/transform-points center transform))] - - (-> obj - (assoc :selrect selrect) - (assoc :points points)))) - -(defn- setup-path-selrect - [{:keys [content center transform transform-inverse] :as obj}] - - (when (or (empty? content) (nil? center)) - (ex/raise :type :assertion - :code :invalid-condition - :hint "Path not valid")) - - (let [transform (gmt/transform-in center transform) - transform-inverse (gmt/transform-in center transform-inverse) - - content' (gsh/transform-content content transform-inverse) - selrect (gsh/content->selrect content') - points (-> (gsh/rect->points selrect) - (gsh/transform-points transform))] - - (-> obj - (dissoc :center) - (assoc :selrect selrect) - (assoc :points points)))) - -(defn- setup-selrect - [obj] - (if (= (:type obj) :path) - (setup-path-selrect obj) - (setup-rect-selrect obj))) - (defn- generate-name [type data] (if (= type :svg-raw) @@ -203,10 +157,10 @@ (assoc :current-page-id page-id) ;; Current frame-id - (assoc :current-frame-id root-frame) + (assoc :current-frame-id root-id) ;; Current parent stack we'll be nesting - (assoc :parent-stack [root-frame]) + (assoc :parent-stack [root-id]) ;; Last object id added (assoc :last-id nil)))) @@ -220,11 +174,8 @@ (clear-names))) (defn add-artboard [file data] - (let [obj (-> (cts/make-minimal-shape :frame) - (merge data) - (check-name file :frame) - (setup-selrect) - (d/without-nils))] + (let [obj (-> (cts/setup-shape (assoc data :type :frame)) + (check-name file :frame))] (-> file (commit-shape obj) (assoc :current-frame-id (:id obj)) @@ -237,19 +188,15 @@ parent (lookup-shape file parent-id) current-frame-id (or (:frame-id parent) (when (nil? (:current-component-id file)) - root-frame))] + root-id))] (-> file (assoc :current-frame-id current-frame-id) (update :parent-stack pop)))) (defn add-group [file data] (let [frame-id (:current-frame-id file) - selrect cts/empty-selrect - name (:name data) - obj (-> (cts/make-minimal-group frame-id selrect name) - (merge data) - (check-name file :group) - (d/without-nils))] + obj (-> (cts/setup-shape (assoc data :type :group :frame-id frame-id)) + (check-name file :group))] (-> file (commit-shape obj) (assoc :last-id (:id obj)) @@ -271,7 +218,7 @@ :id group-id} {:add-container? true}) - (:masked-group? group) + (:masked-group group) (let [mask (first children)] (commit-change file @@ -309,15 +256,8 @@ (defn add-bool [file data] (let [frame-id (:current-frame-id file) - name (:name data) - obj (-> {:id (uuid/next) - :type :bool - :name name - :shapes [] - :frame-id frame-id} - (merge data) - (check-name file :bool) - (d/without-nils))] + obj (-> (cts/setup-shape (assoc data :type :bool :frame-id frame-id)) + (check-name file :bool))] (-> file (commit-shape obj) (assoc :last-id (:id obj)) @@ -361,12 +301,13 @@ (-> file (update :parent-stack pop)))) -(defn create-shape [file type data] - (let [obj (-> (cts/make-minimal-shape type) - (merge data) - (check-name file :type) - (setup-selrect) - (d/without-nils))] +(defn create-shape + [file type data] + (let [obj (-> (assoc data :type type) + (update :svg-attrs csvg/attrs->props) + (cts/setup-shape) + (check-name file :type))] + (-> file (commit-shape obj) (assoc :last-id (:id obj)) @@ -558,23 +499,36 @@ {:type :del-media :id id})))) + (defn start-component ([file data] (start-component file data :group)) ([file data root-type] - (let [selrect (or (gsh/make-selrect (:x data) (:y data) (:width data) (:height data)) - cts/empty-selrect) + ;; FIXME: data probably can be a shape instance, then we can use gsh/shape->rect + (let [selrect (or (grc/make-rect (:x data) (:y data) (:width data) (:height data)) + grc/empty-rect) name (:name data) path (:path data) main-instance-id (:main-instance-id data) main-instance-page (:main-instance-page data) - obj (-> (cts/make-shape root-type selrect data) - (dissoc :path - :main-instance-id - :main-instance-page - :main-instance-x - :main-instance-y) - (check-name file root-type) - (d/without-nils))] + attrs (-> data + (assoc :type root-type) + (assoc :x (:x selrect)) + (assoc :y (:y selrect)) + (assoc :width (:width selrect)) + (assoc :height (:height selrect)) + (assoc :selrect selrect) + (dissoc :path) + (dissoc :main-instance-id) + (dissoc :main-instance-page) + (dissoc :main-instance-x) + (dissoc :main-instance-y)) + + obj (-> (cts/setup-shape attrs) + (check-name file root-type) + ;; Components need to have nil values for frame and parent + (assoc :frame-id nil) + (assoc :parent-id nil))] + (-> file (commit-change {:type :add-component @@ -586,19 +540,24 @@ :shapes [obj]}) (assoc :last-id (:id obj)) - (update :parent-stack conjv (:id obj)) + (assoc :parent-stack [(:id obj)]) (assoc :current-component-id (:id obj)) - (assoc :current-frame-id (when (= (:type obj) :frame) - (:id obj))))))) + (assoc :current-frame-id (if (= (:type obj) :frame) (:id obj) uuid/zero)))))) (defn finish-component [file] (let [component-id (:current-component-id file) + component-data (ctkl/get-component (:data file) component-id) + component (lookup-shape file component-id) children (->> component :shapes (mapv #(lookup-shape file %))) file (cond + ;; Components-v2 component we skip this step + (and component-data (:main-instance-id component-data)) + file + (empty? children) (commit-change file @@ -606,7 +565,7 @@ :id component-id :skip-undelete? true}) - (:masked-group? component) + (:masked-group component) (let [mask (first children)] (commit-change file @@ -662,7 +621,7 @@ (gpt/point main-instance-x main-instance-y) true - {:main-instance? true + {:main-instance true :force-id main-instance-id})] (as-> file $ (reduce #(commit-change %1 @@ -705,7 +664,7 @@ (gpt/point x y) components-v2 - #_{:main-instance? true + #_{:main-instance true :force-id main-instance-id})] (as-> file $ @@ -736,8 +695,8 @@ (defn update-object [file old-obj new-obj] (let [page-id (:current-page-id file) - new-obj (setup-selrect new-obj) - attrs (d/concat-set (keys old-obj) (keys new-obj)) + new-obj (cts/setup-shape new-obj) + attrs (d/concat-set (keys old-obj) (keys new-obj)) generate-operation (fn [changes attr] (let [old-val (get old-obj attr) @@ -800,3 +759,7 @@ :page-id page-id :option :guides :value new-guides})))) + +(defn strip-image-extension [filename] + (let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"] + (str/replace filename image-extensions-re ""))) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/files/changes.cljc similarity index 71% rename from common/src/app/common/pages/changes.cljc rename to common/src/app/common/files/changes.cljc index 620e635d24..978cc8edf3 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -4,19 +4,16 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.pages.changes - #_:clj-kondo/ignore +(ns app.common.files.changes (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] - [app.common.math :as mth] - [app.common.pages.common :refer [component-sync-attrs]] - [app.common.pages.helpers :as cph] [app.common.schema :as sm] [app.common.schema.desc-native :as smd] - [app.common.spec :as us] + [app.common.types.color :as ctc] [app.common.types.colors-list :as ctcl] [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] @@ -34,25 +31,27 @@ ;; SCHEMAS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(sm/def! ::operation - [:multi {:dispatch :type :title "Operation" ::smd/simplified true} - [:set - [:map {:title "SetOperation"} - [:type [:= :set]] - [:attr :keyword] - [:val :any] - [:ignore-touched {:optional true} :boolean] - [:ignore-geometry {:optional true} :boolean]]] - [:set-touched - [:map {:title "SetTouchedOperation"} - [:type [:= :set-touched]] - [:touched [:maybe [:set :keyword]]]]] - [:set-remote-synced - [:map {:title "SetRemoteSyncedOperation"} - [:type [:= :set-remote-synced]] - [:remote-synced? [:maybe :boolean]]]]]) +(def ^:private + schema:operation + (sm/define + [:multi {:dispatch :type :title "Operation" ::smd/simplified true} + [:set + [:map {:title "SetOperation"} + [:type [:= :set]] + [:attr :keyword] + [:val :any] + [:ignore-touched {:optional true} :boolean] + [:ignore-geometry {:optional true} :boolean]]] + [:set-touched + [:map {:title "SetTouchedOperation"} + [:type [:= :set-touched]] + [:touched [:maybe [:set :keyword]]]]] + [:set-remote-synced + [:map {:title "SetRemoteSyncedOperation"} + [:type [:= :set-remote-synced]] + [:remote-synced {:optional true} [:maybe :boolean]]]]])) -(sm/def! ::change +(sm/define! ::change [:schema [:multi {:dispatch :type :title "Change" ::smd/simplified true} [:set-option @@ -68,22 +67,21 @@ [:map {:title "AddObjChange"} [:type [:= :add-obj]] [:id ::sm/uuid] - [:obj [:map-of {:gen/max 10} :keyword :any]] + [:obj :map] [:page-id {:optional true} ::sm/uuid] [:component-id {:optional true} ::sm/uuid] - [:frame-id {:optional true} ::sm/uuid] - [:parent-id {:optional true} ::sm/uuid] + [:frame-id ::sm/uuid] + [:parent-id {:optional true} [:maybe ::sm/uuid]] [:index {:optional true} [:maybe :int]] [:ignore-touched {:optional true} :boolean]]] - [:mod-obj [:map {:title "ModObjChange"} [:type [:= :mod-obj]] [:id ::sm/uuid] [:page-id {:optional true} ::sm/uuid] [:component-id {:optional true} ::sm/uuid] - [:operations [:vector {:gen/max 5} ::operation]]]] + [:operations [:vector {:gen/max 5} schema:operation]]]] [:del-obj [:map {:title "DelObjChange"} @@ -97,6 +95,7 @@ [:map {:title "FixObjChange"} [:type [:= :fix-obj]] [:id ::sm/uuid] + [:fix {:optional true} :keyword] [:page-id {:optional true} ::sm/uuid] [:component-id {:optional true} ::sm/uuid]]] @@ -108,8 +107,18 @@ [:ignore-touched {:optional true} :boolean] [:parent-id ::sm/uuid] [:shapes :any] - [:index {:optional true} :int] - [:after-shape {:optional true} :any]]] + [:index {:optional true} [:maybe :int]] + [:after-shape {:optional true} :any] + [:component-swap {:optional true} :boolean]]] + + [:reorder-children + [:map {:title "ReorderChildrenChange"} + [:type [:= :reorder-children]] + [:page-id {:optional true} ::sm/uuid] + [:component-id {:optional true} ::sm/uuid] + [:ignore-touched {:optional true} :boolean] + [:parent-id ::sm/uuid] + [:shapes :any]]] [:add-page [:map {:title "AddPageChange"} @@ -160,7 +169,7 @@ [:add-recent-color [:map {:title "AddRecentColorChange"} [:type [:= :add-recent-color]] - [:color :any]]] + [:color ::ctc/recent-color]]] [:add-media [:map {:title "AddMediaChange"} @@ -196,12 +205,14 @@ [:map {:title "DelComponentChange"} [:type [:= :del-component]] [:id ::sm/uuid] + [:main-instance {:optional true} :any] [:skip-undelete? {:optional true} :boolean]]] [:restore-component [:map {:title "RestoreComponentChange"} [:type [:= :restore-component]] - [:id ::sm/uuid]]] + [:id ::sm/uuid] + [:page-id ::sm/uuid]]] [:purge-component [:map {:title "PurgeComponentChange"} @@ -223,14 +234,14 @@ [:type [:= :del-typography]] [:id ::sm/uuid]]]]]) -(sm/def! ::changes +(sm/define! ::changes [:sequential {:gen/max 2} ::change]) -(def change? - (sm/pred-fn ::change)) +(def check-change! + (sm/check-fn ::change)) -(def changes? - (sm/pred-fn [:sequential ::change])) +(def check-changes! + (sm/check-fn ::changes)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Specific helpers @@ -256,7 +267,10 @@ ;; If object has changed or is new verify is correct (when (and (some? shape-new) (not= shape-old shape-new)) - (dm/verify! (cts/shape? shape-new)))))] + (dm/verify! + "expected valid shape" + (and (cts/check-shape! shape-new) + (cts/shape? shape-new))))))] (->> (into #{} (map :page-id) items) (mapcat (fn [page-id] @@ -281,7 +295,9 @@ ;; When verify? false we spec the schema validation. Currently used to make just ;; 1 validation even if the changes are applied twice (when verify? - (dm/verify! (changes? items))) + (dm/verify! + "expected valid changes" + (check-changes! items))) (let [result (reduce #(or (process-change %1 %2) %1) data items)] ;; Validate result shapes (only on the backend) @@ -340,6 +356,51 @@ (d/update-in-when $ [:components component-id :objects] update-fn)) (check-modify-component $)))) +(defmethod process-change :reorder-children + [data {:keys [parent-id shapes page-id component-id]}] + (let [changed? (atom false) + + update-fn + (fn [objects] + (let [old-shapes (dm/get-in objects [parent-id :shapes]) + + id->idx + (update-vals + (->> shapes + d/enumerate + (group-by second)) + (comp first first)) + + new-shapes + (into [] (sort-by #(d/nilv (id->idx %) -1) < old-shapes))] + + (reset! changed? (not= old-shapes new-shapes)) + + (cond-> objects + @changed? + (d/assoc-in-when [parent-id :shapes] new-shapes)))) + + check-modify-component + (fn [data] + (if @changed? + ;; When a shape is modified, if it belongs to a main component instance, + ;; the component needs to be marked as modified. + (let [objects (if page-id + (-> data :pages-index (get page-id) :objects) + (-> data :components (get component-id) :objects)) + shape (get objects parent-id) + component-root (ctn/get-component-shape objects shape {:allow-main? true})] + (if (and (some? component-root) (ctk/main-instance? component-root)) + (ctkl/set-component-modified data (:component-id component-root)) + data)) + data))] + + (as-> data $ + (if page-id + (d/update-in-when $ [:pages-index page-id :objects] update-fn) + (d/update-in-when $ [:components component-id :objects] update-fn)) + (check-modify-component $)))) + (defmethod process-change :del-obj [data {:keys [page-id component-id id ignore-touched]}] (if page-id @@ -347,10 +408,16 @@ (d/update-in-when data [:components component-id] ctst/delete-shape id ignore-touched))) (defmethod process-change :fix-obj - [data {:keys [page-id component-id] :as params}] - (if page-id - (d/update-in-when data [:pages-index page-id] ctst/fix-shape-children params) - (d/update-in-when data [:components component-id] ctst/fix-shape-children params))) + [data {:keys [page-id component-id id] :as params}] + (letfn [(fix-container [container] + (case (:fix params :broken-children) + :broken-children (ctst/fix-broken-children container id) + (ex/raise :type :internal + :code :fix-not-implemented + :fix (:fix params))))] + (if page-id + (d/update-in-when data [:pages-index page-id] fix-container) + (d/update-in-when data [:components component-id] fix-container)))) ;; FIXME: remove, seems like this method is already unused ;; reg-objects operation "regenerates" the geometry and selrect of the parent groups @@ -361,7 +428,7 @@ (let [lookup (d/getf objects) update-fn #(d/update-when %1 %2 update-group %1) xform (comp - (mapcat #(cons % (cph/get-parent-ids objects %))) + (mapcat #(cons % (cfh/get-parent-ids objects %))) (filter #(contains? #{:group :bool} (-> % lookup :type))) (distinct))] @@ -391,7 +458,7 @@ (= :bool (:type group)) (gsh/update-bool-selrect group children objects) - (:masked-group? group) + (:masked-group group) (set-mask-selrect group children) :else @@ -402,26 +469,32 @@ (d/update-in-when data [:components component-id :objects] reg-objects)))) (defmethod process-change :mov-objects - [data {:keys [parent-id shapes index page-id component-id ignore-touched after-shape]}] + [data {:keys [parent-id shapes index page-id component-id ignore-touched after-shape component-swap syncing]}] (letfn [(calculate-invalid-targets [objects shape-id] (let [reduce-fn #(into %1 (calculate-invalid-targets objects %2))] (->> (get-in objects [shape-id :shapes]) (reduce reduce-fn #{shape-id})))) ;; Avoid placing a shape as a direct or indirect child of itself, - ;; or inside its main component if it's in a copy. + ;; or inside its main component if it's in a copy, + ;; or inside a copy, or from a copy (is-valid-move? [objects shape-id] - (let [invalid-targets (calculate-invalid-targets objects shape-id)] - (and (contains? objects shape-id) + (let [invalid-targets (calculate-invalid-targets objects shape-id) + shape (get objects shape-id)] + (and shape (not (invalid-targets parent-id)) - (not (cph/components-nesting-loop? objects shape-id parent-id)) - #_(cph/valid-frame-target? objects parent-id shape-id)))) + (not (cfh/components-nesting-loop? objects shape-id parent-id)) + (or component-swap ;; On a component swap it's allowed to change the structure of a copy + syncing ;; If we are syncing the changes of a main component, it's allowed to change the structure of a copy + (and + (not (ctk/in-component-copy? (get objects (:parent-id shape)))) ;; We don't want to change the structure of component copies + (not (ctk/in-component-copy? (get objects parent-id)))))))) ;; We need to check the origin and target frames (insert-items [prev-shapes index shapes] (let [prev-shapes (or prev-shapes [])] (if index (d/insert-at-index prev-shapes index shapes) - (cph/append-at-the-end prev-shapes shapes)))) + (cfh/append-at-the-end prev-shapes shapes)))) (add-to-parent [parent index shapes] (let [parent (-> parent @@ -434,8 +507,7 @@ (and (:shape-ref parent) (#{:group :frame} (:type parent)) (not ignore-touched)) - (-> (update :touched cph/set-touched-group :shapes-group) - (dissoc :remote-synced?))))) + (dissoc :remote-synced)))) (remove-from-old-parent [old-objects objects shape-id] (let [prev-parent-id (dm/get-in old-objects [shape-id :parent-id])] @@ -452,9 +524,7 @@ (-> objects (d/update-in-when [pid :shapes] d/without-obj sid) (d/update-in-when [pid :shapes] d/vec-without-nils) - (cond-> component? (d/update-when pid #(-> % - (update :touched cph/set-touched-group :shapes-group) - (dissoc :remote-synced?))))))))) + (cond-> component? (d/update-when pid #(dissoc % :remote-synced)))))))) (update-parent-id [objects id] (-> objects (d/update-when id assoc :parent-id parent-id))) @@ -539,11 +609,15 @@ (defmethod process-change :add-recent-color [data {:keys [color]}] ;; Moves the color to the top of the list and then truncates up to 15 - (update data :recent-colors (fn [rc] - (let [rc (conj (filterv (comp not #{color}) (or rc [])) color)] - (if (> (count rc) 15) - (subvec rc 1) - rc))))) + (update + data + :recent-colors + (fn [rc] + (let [rc (->> rc (d/removev (partial ctc/eq-recent-color? color))) + rc (-> rc (conj color))] + (cond-> rc + (> (count rc) 15) + (subvec 1)))))) ;; -- Media @@ -557,7 +631,7 @@ (defmethod process-change :del-media [data {:keys [id]}] - (update data :media dissoc id)) + (d/update-when data :media dissoc id)) ;; -- Components @@ -570,8 +644,8 @@ (ctkl/mod-component data params)) (defmethod process-change :del-component - [data {:keys [id skip-undelete?]}] - (ctf/delete-component data id skip-undelete?)) + [data {:keys [id skip-undelete? main-instance]}] + (ctf/delete-component data id skip-undelete? main-instance)) (defmethod process-change :restore-component [data {:keys [id page-id]}] @@ -600,14 +674,13 @@ (defmethod process-operation :set [on-changed shape op] (let [attr (:attr op) - group (get component-sync-attrs attr) + group (get ctk/sync-attrs attr) val (:val op) shape-val (get shape attr) - ignore (:ignore-touched op) - ignore-geometry (:ignore-geometry op) + ignore (or (:ignore-touched op) (= attr :position-data)) ;; position-data is a derived attribute and + ignore-geometry (:ignore-geometry op) ;; never triggers touched by itself is-geometry? (and (or (= group :geometry-group) - (and (= group :content-group) (= (:type shape) :path)) - (= attr :position-data)) + (and (= group :content-group) (= (:type shape) :path))) (not (#{:width :height} attr))) ;; :content in paths are also considered geometric ;; TODO: the check of :width and :height probably may be removed ;; after the check added in data/workspace/modifiers/check-delta @@ -635,8 +708,8 @@ ;; geometric (position, width or transformation). (and in-copy? group (not ignore) (not equal?) (not (and ignore-geometry is-geometry?))) - (-> (update :touched cph/set-touched-group group) - (dissoc :remote-synced?)) + (-> (update :touched cfh/set-touched-group group) + (dissoc :remote-synced)) (nil? val) (dissoc attr) @@ -654,11 +727,11 @@ (defmethod process-operation :set-remote-synced [_ shape op] - (let [remote-synced? (:remote-synced? op) + (let [remote-synced (:remote-synced op) in-copy? (ctk/in-component-copy? shape)] - (if (or (not in-copy?) (not remote-synced?)) - (dissoc shape :remote-synced?) - (assoc shape :remote-synced? true)))) + (if (or (not in-copy?) (not remote-synced)) + (dissoc shape :remote-synced) + (assoc shape :remote-synced true)))) (defmethod process-operation :default [_ _ op] @@ -677,47 +750,58 @@ (defmulti components-changed (fn [_ change] (:type change))) (defmethod components-changed :mod-obj - [file-data {:keys [id page-id _component-id operations]}] - (when page-id - (let [page (ctpl/get-page file-data page-id) - shape-and-parents (map #(ctn/get-shape page %) - (cons id (cph/get-parent-ids (:objects page) id))) - need-sync? (fn [operation] - ; We need to trigger a sync if the shape has changed any - ; attribute that participates in components synchronization. - (and (= (:type operation) :set) - (component-sync-attrs (:attr operation)))) - any-sync? (some need-sync? operations)] - (when any-sync? - (let [xform (comp (filter :main-instance?) ; Select shapes that are main component instances + [file-data {:keys [id page-id component-id operations]}] + (let [need-sync? (fn [operation] + ; We need to trigger a sync if the shape has changed any + ; attribute that participates in components synchronization. + (and (= (:type operation) :set) + (get ctk/sync-attrs (:attr operation)))) + any-sync? (some need-sync? operations)] + (when any-sync? + (if page-id + (let [page (ctpl/get-page file-data page-id) + shape-and-parents (map #(ctn/get-shape page %) + (cons id (cfh/get-parent-ids (:objects page) id))) + xform (comp (filter :main-instance) ; Select shapes that are main component instances (map :component-id))] - (into #{} xform shape-and-parents)))))) + (into #{} xform shape-and-parents)) + (when component-id + #{component-id}))))) (defmethod components-changed :mov-objects [file-data {:keys [page-id _component-id parent-id shapes] :as change}] (when page-id - (let [page (ctpl/get-page file-data page-id) - - xform (comp (filter :main-instance?) + (let [page (ctpl/get-page file-data page-id) + xform (comp (filter :main-instance) (map :component-id)) check-shape (fn [shape-id others] (let [all-parents (map (partial ctn/get-shape page) - (concat others (cph/get-parent-ids (:objects page) shape-id)))] + (concat others (cfh/get-parent-ids (:objects page) shape-id)))] (into #{} xform all-parents)))] (reduce #(set/union %1 (check-shape %2 [])) (check-shape parent-id [parent-id]) shapes)))) +(defmethod components-changed :add-obj + [file-data {:keys [parent-id page-id _component-id] :as change}] + (when page-id + (let [page (ctpl/get-page file-data page-id) + parents (map (partial ctn/get-shape page) + (cons parent-id (cfh/get-parent-ids (:objects page) parent-id))) + xform (comp (filter :main-instance) + (map :component-id))] + (into #{} xform parents)))) + (defmethod components-changed :del-obj [file-data {:keys [id page-id _component-id] :as change}] (when page-id (let [page (ctpl/get-page file-data page-id) shape-and-parents (map (partial ctn/get-shape page) - (cons id (cph/get-parent-ids (:objects page) id))) - xform (comp (filter :main-instance?) + (cons id (cfh/get-parent-ids (:objects page) id))) + xform (comp (filter :main-instance) (map :component-id))] (into #{} xform shape-and-parents)))) @@ -725,3 +809,56 @@ [_ _] nil) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Copies changes detection +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Analyze one change and checks if if modifies any shape belonging to +;; frames. Return the ids of the frames affected + +(defn- parents-frames + "Go trough the parents and get all of them that are a frame." + [id objects] + (->> (cfh/get-parents-with-self objects id) + (filter cfh/frame-shape?))) + +(defmulti frames-changed (fn [_ change] (:type change))) + +(defmethod frames-changed :mod-obj + [file-data {:keys [id page-id _component-id operations]}] + (when page-id + (let [page (ctpl/get-page file-data page-id) + need-sync? (fn [operation] + ; Check if the shape has changed any + ; attribute that participates in components synchronization. + (and (= (:type operation) :set) + (get ctk/sync-attrs (:attr operation)))) + any-sync? (some need-sync? operations)] + (when any-sync? + (parents-frames id (:objects page)))))) + +(defmethod frames-changed :mov-objects + [file-data {:keys [page-id _component-id parent-id shapes] :as change}] + (when page-id + (let [page (ctpl/get-page file-data page-id)] + (concat + (parents-frames parent-id (:objects page)) + (mapcat #(parents-frames (:parent-id %) (:objects page)) shapes))))) + +(defmethod frames-changed :add-obj + [file-data {:keys [parent-id page-id _component-id] :as change}] + (when page-id + (let [page (ctpl/get-page file-data page-id)] + (parents-frames parent-id (:objects page))))) + +(defmethod frames-changed :del-obj + [file-data {:keys [id page-id _component-id] :as change}] + (when page-id + (let [page (ctpl/get-page file-data page-id)] + (parents-frames id (:objects page))))) + +(defmethod frames-changed :default + [_ _] + nil) + + diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc similarity index 53% rename from common/src/app/common/pages/changes_builder.cljc rename to common/src/app/common/files/changes_builder.cljc index be93f9cc72..b0ab2b5bd5 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -4,32 +4,49 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.pages.changes-builder +(ns app.common.files.changes-builder (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.files.features :as ffeat] + [app.common.features :as cfeat] + [app.common.files.changes :as cfc] + [app.common.files.helpers :as cfh] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.common.pages :as cp] - [app.common.pages.helpers :as cph] + [app.common.schema :as sm] [app.common.types.component :as ctk] [app.common.types.file :as ctf] + [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid])) ;; Auxiliary functions to help create a set of changes (undo + redo) +(sm/define! ::changes + [:map {:title "changes"} + [:redo-changes vector?] + [:undo-changes seq?] + [:origin {:optional true} any?] + [:save-undo? {:optional true} boolean?] + [:stack-undo? {:optional true} boolean?] + [:undo-group {:optional true} any?]]) + +(def check-changes! + (sm/check-fn ::changes)) + (defn empty-changes ([origin page-id] (let [changes (empty-changes origin)] (with-meta changes {::page-id page-id}))) - + ([] + {:redo-changes [] + :undo-changes '()}) ([origin] {:redo-changes [] - :undo-changes [] + :undo-changes '() :origin origin})) (defn set-save-undo? @@ -43,8 +60,8 @@ (defn set-undo-group [changes undo-group] (cond-> changes - (some? undo-group) - (assoc :undo-group undo-group))) + (some? undo-group) + (assoc :undo-group undo-group))) (defn with-page [changes page] @@ -54,19 +71,28 @@ (defn with-container [changes container] - (if (cph/page? container) + (if (cfh/page? container) (vary-meta changes assoc ::page-id (:id container)) (vary-meta changes assoc ::component-id (:id container)))) (defn with-objects [changes objects] - (let [fdata (binding [ffeat/*current* #{"components/v2"}] + (let [fdata (binding [cfeat/*current* #{"components/v2"}] (ctf/make-file-data (uuid/next) uuid/zero)) fdata (assoc-in fdata [:pages-index uuid/zero :objects] objects)] (vary-meta changes assoc ::file-data fdata ::applied-changes-count 0))) +(defn with-file-data + [changes fdata] + (let [page-id (::page-id (meta changes)) + fdata (assoc-in fdata [:pages-index uuid/zero] + (get-in fdata [:pages-index page-id]))] + (vary-meta changes assoc + ::file-data fdata + ::applied-changes-count 0))) + (defn with-library-data [changes data] (vary-meta changes assoc @@ -83,36 +109,44 @@ [changes f] (update changes :redo-changes #(mapv f %))) +;; redo-changes is a vector and :undo-changes is a list (defn concat-changes [changes1 changes2] - {:redo-changes (d/concat-vec (:redo-changes changes1) (:redo-changes changes2)) - :undo-changes (d/concat-vec (:undo-changes changes1) (:undo-changes changes2)) - :origin (:origin changes1) - :undo-group (:undo-group changes1) - :tags (:tags changes1)}) + (-> changes1 + (update :redo-changes d/concat-vec (:redo-changes changes2)) + (update :undo-changes #(concat (:undo-changes changes2) %)))) ; TODO: remove this when not needed -(defn- assert-page-id +(defn- assert-page-id! [changes] - (assert (contains? (meta changes) ::page-id) "Give a page-id or call (with-page) before using this function")) + (dm/assert! + "Give a page-id or call (with-page) before using this function" + (contains? (meta changes) ::page-id))) -(defn- assert-container-id +(defn- assert-container-id! [changes] - (assert (or (contains? (meta changes) ::page-id) - (contains? (meta changes) ::component-id)) - "Give a page-id or call (with-container) before using this function")) + (dm/assert! + "Give a page-id or call (with-container) before using this function" + (or (contains? (meta changes) ::page-id) + (contains? (meta changes) ::component-id)))) -(defn- assert-page +(defn- assert-page! [changes] - (assert (contains? (meta changes) ::page) "Call (with-page) before using this function")) + (dm/assert! + "Call (with-page) before using this function" + (contains? (meta changes) ::page))) -(defn- assert-objects +(defn- assert-objects! [changes] - (assert (contains? (meta changes) ::file-data) "Call (with-objects) before using this function")) + (dm/assert! + "Call (with-objects) before using this function" + (contains? (meta changes) ::file-data))) -(defn- assert-library +(defn- assert-library! [changes] - (assert (contains? (meta changes) ::library-data) "Call (with-library-data) before using this function")) + (dm/assert! + "Call (with-library-data) before using this function" + (contains? (meta changes) ::library-data))) (defn- lookup-objects [changes] @@ -121,18 +155,22 @@ (defn- apply-changes-local [changes] + (dm/assert! + "expected valid changes" + (check-changes! changes)) + (if-let [file-data (::file-data (meta changes))] (let [index (::applied-changes-count (meta changes)) redo-changes (:redo-changes changes) new-changes (if (< index (count redo-changes)) (->> (subvec (:redo-changes changes) index) (map #(-> % - (assoc :page-id uuid/zero) - (dissoc :component-id)))) + (assoc :page-id uuid/zero) + (dissoc :component-id)))) []) - new-file-data (cp/process-changes file-data new-changes)] + new-file-data (cfc/process-changes file-data new-changes)] (vary-meta changes assoc ::file-data new-file-data - ::applied-changes-count (count redo-changes))) + ::applied-changes-count (count redo-changes))) changes)) ;; Page changes @@ -141,40 +179,40 @@ [changes id name] (-> changes (update :redo-changes conj {:type :add-page :id id :name name}) - (update :undo-changes d/preconj {:type :del-page :id id}) + (update :undo-changes conj {:type :del-page :id id}) (apply-changes-local))) (defn add-page [changes id page] (-> changes (update :redo-changes conj {:type :add-page :id id :page page}) - (update :undo-changes d/preconj {:type :del-page :id id}) + (update :undo-changes conj {:type :del-page :id id}) (apply-changes-local))) (defn mod-page [changes page new-name] (-> changes (update :redo-changes conj {:type :mod-page :id (:id page) :name new-name}) - (update :undo-changes d/preconj {:type :mod-page :id (:id page) :name (:name page)}) + (update :undo-changes conj {:type :mod-page :id (:id page) :name (:name page)}) (apply-changes-local))) (defn del-page [changes page] (-> changes (update :redo-changes conj {:type :del-page :id (:id page)}) - (update :undo-changes d/preconj {:type :add-page :id (:id page) :page page}) + (update :undo-changes conj {:type :add-page :id (:id page) :page page}) (apply-changes-local))) (defn move-page [changes page-id index prev-index] (-> changes (update :redo-changes conj {:type :mov-page :id page-id :index index}) - (update :undo-changes d/preconj {:type :mov-page :id page-id :index prev-index}) + (update :undo-changes conj {:type :mov-page :id page-id :index prev-index}) (apply-changes-local))) (defn set-page-option [changes option-key option-val] - (assert-page changes) + (assert-page! changes) (let [page-id (::page-id (meta changes)) page (::page (meta changes)) old-val (get-in page [:options option-key])] @@ -184,15 +222,15 @@ :page-id page-id :option option-key :value option-val}) - (update :undo-changes d/preconj {:type :set-option - :page-id page-id - :option option-key - :value old-val}) + (update :undo-changes conj {:type :set-option + :page-id page-id + :option option-key + :value old-val}) (apply-changes-local)))) (defn update-page-option [changes option-key update-fn & args] - (assert-page changes) + (assert-page! changes) (let [page-id (::page-id (meta changes)) page (::page (meta changes)) old-val (get-in page [:options option-key]) @@ -203,10 +241,10 @@ :page-id page-id :option option-key :value new-val}) - (update :undo-changes d/preconj {:type :set-option - :page-id page-id - :option option-key - :value old-val}) + (update :undo-changes conj {:type :set-option + :page-id page-id + :option option-key + :value old-val}) (apply-changes-local)))) ;; Shape tree changes @@ -216,8 +254,11 @@ (add-object changes obj nil)) ([changes obj {:keys [index ignore-touched] :or {index ::undefined ignore-touched false}}] - (assert-page-id changes) - (assert-objects changes) + + ;; FIXME: add shape validation + + (assert-page-id! changes) + (assert-objects! changes) (let [obj (cond-> obj (not= index ::undefined) (assoc ::index index)) @@ -233,7 +274,7 @@ :frame-id (:frame-id obj) :index (::index obj) :ignore-touched ignore-touched - :obj (dissoc obj ::index :parent-id)} + :obj (dissoc obj ::index)} del-change {:type :del-obj @@ -250,9 +291,9 @@ (-> changes (update :redo-changes conj add-change) (cond-> - (and (ctk/in-component-copy? parent) (not ignore-touched)) - (update :undo-changes d/preconj restore-touched-change)) - (update :undo-changes d/preconj del-change) + (and (ctk/in-component-copy? parent) (not ignore-touched)) + (update :undo-changes conj restore-touched-change)) + (update :undo-changes conj del-change) (apply-changes-local))))) (defn add-objects @@ -266,11 +307,14 @@ (defn change-parent ([changes parent-id shapes] - (change-parent changes parent-id shapes nil)) + (change-parent changes parent-id shapes nil {})) ([changes parent-id shapes index] - (assert-page-id changes) - (assert-objects changes) + (change-parent changes parent-id shapes index {})) + + ([changes parent-id shapes index options] + (assert-page-id! changes) + (assert-objects! changes) (let [objects (lookup-objects changes) parent (get objects parent-id) @@ -281,19 +325,22 @@ :shapes (->> shapes reverse (mapv :id))} (some? index) - (assoc :index index)) + (assoc :index index) + (:component-swap options) + (assoc :component-swap true) + (:ignore-touched options) + (assoc :ignore-touched true)) mk-undo-change - (fn [change-set shape] - (let [prev-sibling (cph/get-prev-sibling objects (:id shape))] - (d/preconj - change-set - {:type :mov-objects - :page-id (::page-id (meta changes)) - :parent-id (:parent-id shape) - :shapes [(:id shape)] - :after-shape prev-sibling - :index 0}))) ; index is used in case there is no after-shape (moving bottom shapes) + (fn [undo-changes shape] + (let [prev-sibling (cfh/get-prev-sibling objects (:id shape))] + (conj undo-changes + {:type :mov-objects + :page-id (::page-id (meta changes)) + :parent-id (:parent-id shape) + :shapes [(:id shape)] + :after-shape prev-sibling + :index 0}))) ; index is used in case there is no after-shape (moving bottom shapes) restore-touched-change {:type :mod-obj @@ -305,20 +352,22 @@ (-> changes (update :redo-changes conj set-parent-change) (cond-> - (ctk/in-component-copy? parent) - (update :undo-changes d/preconj restore-touched-change)) + (ctk/in-component-copy? parent) + (update :undo-changes conj restore-touched-change)) (update :undo-changes #(reduce mk-undo-change % shapes)) (apply-changes-local))))) (defn changed-attrs "Returns the list of attributes that will change when `update-fn` is applied" - [object update-fn {:keys [attrs]}] + [object objects update-fn {:keys [attrs with-objects?]}] (let [changed? (fn [old new attr] (let [old-val (get old attr) new-val (get new attr)] (not= old-val new-val))) - new-obj (update-fn object)] + new-obj (if with-objects? + (update-fn object objects) + (update-fn object))] (when-not (= object new-obj) (let [attrs (or attrs (d/concat-set (keys object) (keys new-obj)))] (filter (partial changed? object new-obj) attrs))))) @@ -329,90 +378,98 @@ ([changes ids update-fn] (update-shapes changes ids update-fn nil)) - ([changes ids update-fn {:keys [attrs ignore-geometry? ignore-touched] - :or {ignore-geometry? false ignore-touched false}}] - (assert-container-id changes) - (assert-objects changes) + ([changes ids update-fn {:keys [attrs ignore-geometry? ignore-touched with-objects?] + :or {ignore-geometry? false ignore-touched false with-objects? false}}] + (assert-container-id! changes) + (assert-objects! changes) (let [page-id (::page-id (meta changes)) component-id (::component-id (meta changes)) - objects (lookup-objects changes) + objects (lookup-objects changes) - generate-operation - (fn [operations attr old new ignore-geometry?] - (let [old-val (get old attr) - new-val (get new attr)] - (if (= old-val new-val) - operations - (-> operations - (update :rops conj {:type :set :attr attr :val new-val - :ignore-geometry ignore-geometry? - :ignore-touched ignore-touched}) - (update :uops d/preconj {:type :set :attr attr :val old-val - :ignore-touched true}))))) + generate-operations + (fn [attrs old new] + (loop [rops [] + uops '() + attrs (seq attrs)] + (if-let [attr (first attrs)] + (let [old-val (get old attr) + new-val (get new attr) + changed? (not= old-val new-val) + + rops + (cond-> rops + changed? + (conj {:type :set :attr attr :val new-val + :ignore-geometry ignore-geometry? + :ignore-touched ignore-touched})) + + uops + (cond-> uops + changed? + (conj {:type :set :attr attr :val old-val + :ignore-touched true}))] + + (recur rops uops (rest attrs))) + [rops uops]))) update-shape (fn [changes id] (let [old-obj (get objects id) - new-obj (update-fn old-obj)] + new-obj (if with-objects? (update-fn old-obj objects) (update-fn old-obj))] (if (= old-obj new-obj) changes - (let [attrs (or attrs (d/concat-set (keys old-obj) (keys new-obj))) + (let [[rops uops] (-> (or attrs (d/concat-set (keys old-obj) (keys new-obj))) + (generate-operations old-obj new-obj)) - {rops :rops uops :uops} - (reduce #(generate-operation %1 %2 old-obj new-obj ignore-geometry?) - {:rops [] :uops []} - attrs) + uops (cond-> uops + (seq uops) + (conj {:type :set-touched :touched (:touched old-obj)})) - uops (cond-> uops - (seq uops) - (d/preconj {:type :set-touched :touched (:touched old-obj)})) + change (cond-> {:type :mod-obj :id id} + (some? page-id) + (assoc :page-id page-id) - change (cond-> {:type :mod-obj - :id id} - - (some? page-id) - (assoc :page-id page-id) - - (some? component-id) - (assoc :component-id component-id))] + (some? component-id) + (assoc :component-id component-id))] (cond-> changes (seq rops) (update :redo-changes conj (assoc change :operations rops)) (seq uops) - (update :undo-changes d/preconj (assoc change :operations uops)))))))] + (update :undo-changes conj (assoc change :operations (vec uops))))))))] - (-> (reduce update-shape changes ids) - (apply-changes-local))))) + (-> (reduce update-shape changes ids) + (apply-changes-local))))) (defn remove-objects ([changes ids] (remove-objects changes ids nil)) ([changes ids {:keys [ignore-touched] :or {ignore-touched false}}] - (assert-page-id changes) - (assert-objects changes) + (assert-page-id! changes) + (assert-objects! changes) (let [page-id (::page-id (meta changes)) objects (lookup-objects changes) add-redo-change (fn [change-set id] (conj change-set - {:type :del-obj - :page-id page-id - :ignore-touched ignore-touched - :id id})) + (cond-> {:type :del-obj + :page-id page-id + :id id} + ignore-touched + (assoc :ignore-touched true)))) add-undo-change-shape (fn [change-set id] (let [shape (get objects id)] - (d/preconj + (conj change-set {:type :add-obj :id id :page-id page-id - :parent-id (:frame-id shape) + :parent-id (:parent-id shape) :frame-id (:frame-id shape) - :index (cph/get-position-on-parent objects id) + :index (cfh/get-position-on-parent objects id) :obj (cond-> shape (contains? shape :shapes) (assoc :shapes []))}))) @@ -420,8 +477,8 @@ add-undo-change-parent (fn [change-set id] (let [shape (get objects id) - prev-sibling (cph/get-prev-sibling objects (:id shape))] - (d/preconj + prev-sibling (cfh/get-prev-sibling objects (:id shape))] + (conj change-set {:type :mov-objects :page-id page-id @@ -440,16 +497,16 @@ (defn resize-parents [changes ids] - (assert-page-id changes) - (assert-objects changes) + (assert-page-id! changes) + (assert-objects! changes) (let [page-id (::page-id (meta changes)) objects (lookup-objects changes) xform (comp - (mapcat #(cons % (cph/get-parent-ids objects %))) - (map (d/getf objects)) - (filter #(contains? #{:group :bool} (:type %))) - (distinct)) + (mapcat #(cons % (cfh/get-parent-ids objects %))) + (map (d/getf objects)) + (filter #(contains? #{:group :bool} (:type %))) + (distinct)) all-parents (sequence xform ids) generate-operation @@ -468,7 +525,7 @@ (every? #(apply gpt/close? %) (d/zip old-val new-val)) (= attr :selrect) - (gsh/close-selrect? old-val new-val) + (grc/close-rect? old-val new-val) :else (= old-val new-val))] @@ -476,7 +533,7 @@ operations (-> operations (update :rops conj {:type :set :attr attr :val new-val :ignore-touched true}) - (update :uops d/preconj {:type :set :attr attr :val old-val :ignore-touched true}))))) + (update :uops conj {:type :set :attr attr :val old-val :ignore-touched true}))))) resize-parent (fn [changes parent] @@ -490,7 +547,7 @@ (gsh/update-bool-selrect parent children objects) (= (:type parent) :group) - (if (:masked-group? parent) + (if (:masked-group parent) (gsh/update-mask-selrect parent children) (gsh/update-group-selrect parent children)))] (if resized-parent @@ -506,7 +563,7 @@ (if (seq rops) (-> changes (update :redo-changes conj (assoc change :operations rops)) - (update :undo-changes d/preconj (assoc change :operations uops)) + (update :undo-changes conj (assoc change :operations uops)) (apply-changes-local)) changes)) changes)))] @@ -525,184 +582,188 @@ [changes color] (-> changes (update :redo-changes conj {:type :add-color :color color}) - (update :undo-changes d/preconj {:type :del-color :id (:id color)}) + (update :undo-changes conj {:type :del-color :id (:id color)}) (apply-changes-local))) (defn update-color [changes color] - (assert-library changes) + (assert-library! changes) (let [library-data (::library-data (meta changes)) prev-color (get-in library-data [:colors (:id color)])] (-> changes (update :redo-changes conj {:type :mod-color :color color}) - (update :undo-changes d/preconj {:type :mod-color :color prev-color}) + (update :undo-changes conj {:type :mod-color :color prev-color}) (apply-changes-local)))) (defn delete-color [changes color-id] - (assert-library changes) + (assert-library! changes) (let [library-data (::library-data (meta changes)) prev-color (get-in library-data [:colors color-id])] (-> changes (update :redo-changes conj {:type :del-color :id color-id}) - (update :undo-changes d/preconj {:type :add-color :color prev-color}) + (update :undo-changes conj {:type :add-color :color prev-color}) (apply-changes-local)))) (defn add-media [changes object] (-> changes (update :redo-changes conj {:type :add-media :object object}) - (update :undo-changes d/preconj {:type :del-media :id (:id object)}) + (update :undo-changes conj {:type :del-media :id (:id object)}) (apply-changes-local))) (defn update-media [changes object] - (assert-library changes) + (assert-library! changes) (let [library-data (::library-data (meta changes)) prev-object (get-in library-data [:media (:id object)])] (-> changes (update :redo-changes conj {:type :mod-media :object object}) - (update :undo-changes d/preconj {:type :mod-media :object prev-object}) + (update :undo-changes conj {:type :mod-media :object prev-object}) (apply-changes-local)))) (defn delete-media [changes id] - (assert-library changes) + (assert-library! changes) (let [library-data (::library-data (meta changes)) prev-object (get-in library-data [:media id])] (-> changes (update :redo-changes conj {:type :del-media :id id}) - (update :undo-changes d/preconj {:type :add-media :object prev-object}) + (update :undo-changes conj {:type :add-media :object prev-object}) (apply-changes-local)))) (defn add-typography [changes typography] (-> changes (update :redo-changes conj {:type :add-typography :typography typography}) - (update :undo-changes d/preconj {:type :del-typography :id (:id typography)}) + (update :undo-changes conj {:type :del-typography :id (:id typography)}) (apply-changes-local))) (defn update-typography [changes typography] - (assert-library changes) + (assert-library! changes) (let [library-data (::library-data (meta changes)) prev-typography (get-in library-data [:typographies (:id typography)])] (-> changes (update :redo-changes conj {:type :mod-typography :typography typography}) - (update :undo-changes d/preconj {:type :mod-typography :typography prev-typography}) + (update :undo-changes conj {:type :mod-typography :typography prev-typography}) (apply-changes-local)))) (defn delete-typography [changes typography-id] - (assert-library changes) + (assert-library! changes) (let [library-data (::library-data (meta changes)) prev-typography (get-in library-data [:typographies typography-id])] (-> changes (update :redo-changes conj {:type :del-typography :id typography-id}) - (update :undo-changes d/preconj {:type :add-typography :typography prev-typography}) + (update :undo-changes conj {:type :add-typography :typography prev-typography}) (apply-changes-local)))) (defn add-component ([changes id path name new-shapes updated-shapes main-instance-id main-instance-page] (add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page nil)) ([changes id path name new-shapes updated-shapes main-instance-id main-instance-page annotation] - (assert-page-id changes) - (assert-objects changes) - (let [page-id (::page-id (meta changes)) - objects (lookup-objects changes) - lookupf (d/getf objects) + (assert-page-id! changes) + (assert-objects! changes) + (let [page-id (::page-id (meta changes)) + objects (lookup-objects changes) + lookupf (d/getf objects) - mk-change (fn [shape] - {:type :mod-obj - :page-id page-id - :id (:id shape) - :operations [{:type :set - :attr :component-id - :val (:component-id shape)} - {:type :set - :attr :component-file - :val (:component-file shape)} - {:type :set - :attr :component-root? - :val (:component-root? shape)} - {:type :set - :attr :main-instance? - :val (:main-instance? shape)} - {:type :set - :attr :shape-ref - :val (:shape-ref shape)} - {:type :set - :attr :touched - :val (:touched shape)}]}) ] - (-> changes - (update :redo-changes - (fn [redo-changes] - (-> redo-changes - (conj (cond-> {:type :add-component - :id id - :path path - :name name - :main-instance-id main-instance-id - :main-instance-page main-instance-page - :annotation annotation} - (some? new-shapes) ;; this will be null in components-v2 - (assoc :shapes (vec new-shapes)))) - (into (map mk-change) updated-shapes)))) - (update :undo-changes - (fn [undo-changes] - (-> undo-changes - (d/preconj {:type :del-component - :id id - :skip-undelete? true}) - (into (comp (map :id) - (map lookupf) - (map mk-change)) - updated-shapes)))) - (apply-changes-local))))) + mk-change (fn [shape] + {:type :mod-obj + :page-id page-id + :id (:id shape) + :operations [{:type :set + :attr :component-id + :val (:component-id shape)} + {:type :set + :attr :component-file + :val (:component-file shape)} + {:type :set + :attr :component-root + :val (:component-root shape)} + {:type :set + :attr :main-instance + :val (:main-instance shape)} + {:type :set + :attr :shape-ref + :val (:shape-ref shape)} + {:type :set + :attr :touched + :val (:touched shape)}]})] + (-> changes + (update :redo-changes + (fn [redo-changes] + (-> redo-changes + (conj (cond-> {:type :add-component + :id id + :path path + :name name + :main-instance-id main-instance-id + :main-instance-page main-instance-page + :annotation annotation} + (some? new-shapes) ;; this will be null in components-v2 + (assoc :shapes (vec new-shapes)))) + (into (map mk-change) updated-shapes)))) + (update :undo-changes + (fn [undo-changes] + (-> undo-changes + (conj {:type :del-component + :id id + :skip-undelete? true}) + (into (comp (map :id) + (map lookupf) + (map mk-change)) + updated-shapes)))) + (apply-changes-local))))) (defn update-component [changes id update-fn] - (assert-library changes) + (assert-library! changes) (let [library-data (::library-data (meta changes)) prev-component (get-in library-data [:components id]) new-component (update-fn prev-component)] - (if new-component + (if prev-component (-> changes (update :redo-changes conj {:type :mod-component :id id :name (:name new-component) :path (:path new-component) + :main-instance-id (:main-instance-id new-component) + :main-instance-page (:main-instance-page new-component) :annotation (:annotation new-component) - :objects (:objects new-component)}) ;; this won't exist in components-v2 - (update :undo-changes d/preconj {:type :mod-component - :id id - :name (:name prev-component) - :path (:path prev-component) - :annotation (:annotation prev-component) - :objects (:objects prev-component)})) + :objects (:objects new-component) ;; this won't exist in components-v2 (except for deleted components) + :modified-at (:modified-at new-component)}) + (update :undo-changes conj {:type :mod-component + :id id + :name (:name prev-component) + :path (:path prev-component) + :main-instance-id (:main-instance-id prev-component) + :main-instance-page (:main-instance-page prev-component) + :annotation (:annotation prev-component) + :objects (:objects prev-component)})) changes))) (defn delete-component - [changes id] - (assert-library changes) + [changes id page-id] + (assert-library! changes) (-> changes (update :redo-changes conj {:type :del-component :id id}) - (update :undo-changes d/preconj {:type :restore-component - :id id}))) + (update :undo-changes conj {:type :restore-component + :id id + :page-id page-id}))) (defn restore-component - ([changes id] - (restore-component changes id nil)) - ([changes id page-id] - (assert-library changes) - (-> changes - (update :redo-changes conj {:type :restore-component - :id id - :page-id page-id}) - (update :undo-changes d/preconj {:type :del-component - :id id})))) - + [changes id page-id main-instance] + (assert-library! changes) + (-> changes + (update :redo-changes conj {:type :restore-component + :id id + :page-id page-id}) + (update :undo-changes conj {:type :del-component + :id id + :main-instance main-instance}))) (defn ignore-remote [changes] (letfn [(add-ignore-remote @@ -712,3 +773,42 @@ (-> changes (update :redo-changes add-ignore-remote) (update :undo-changes add-ignore-remote)))) + +(defn reorder-grid-children + [changes ids] + (assert-page-id! changes) + (assert-objects! changes) + + (let [page-id (::page-id (meta changes)) + objects (lookup-objects changes) + + reorder-grid + (fn [changes grid] + (let [old-shapes (:shapes grid) + grid (ctl/reorder-grid-children grid) + new-shapes (->> (:shapes grid) + (filterv #(contains? objects %))) + + redo-change + {:type :reorder-children + :parent-id (:id grid) + :page-id page-id + :shapes new-shapes} + + undo-change + {:type :reorder-children + :parent-id (:id grid) + :page-id page-id + :shapes old-shapes}] + (-> changes + (update :redo-changes conj redo-change) + (update :undo-changes conj undo-change) + (apply-changes-local)))) + + changes + (->> ids + (map (d/getf objects)) + (filter ctl/grid-layout?) + (reduce reorder-grid changes))] + + changes)) diff --git a/common/src/app/common/files/defaults.cljc b/common/src/app/common/files/defaults.cljc new file mode 100644 index 0000000000..61cd7f1188 --- /dev/null +++ b/common/src/app/common/files/defaults.cljc @@ -0,0 +1,9 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.files.defaults) + +(def version 46) diff --git a/common/src/app/common/files/features.cljc b/common/src/app/common/files/features.cljc deleted file mode 100644 index 34d40800e8..0000000000 --- a/common/src/app/common/files/features.cljc +++ /dev/null @@ -1,17 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.common.files.features) - -;; A set of enabled by default file features. Will be used in feature -;; negotiation on obtaining files from backend. - -(def enabled #{}) - -(def ^:dynamic *previous* #{}) -(def ^:dynamic *current* #{}) -(def ^:dynamic *wrap-with-objects-map-fn* identity) -(def ^:dynamic *wrap-with-pointer-map-fn* identity) diff --git a/common/src/app/common/pages/focus.cljc b/common/src/app/common/files/focus.cljc similarity index 94% rename from common/src/app/common/pages/focus.cljc rename to common/src/app/common/files/focus.cljc index 0463ae1eb2..d066bc062c 100644 --- a/common/src/app/common/pages/focus.cljc +++ b/common/src/app/common/files/focus.cljc @@ -4,10 +4,10 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.pages.focus +(ns app.common.files.focus (:require [app.common.data :as d] - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cph] [app.common.types.shape-tree :as ctt] [app.common.uuid :as uuid])) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/files/helpers.cljc similarity index 66% rename from common/src/app/common/pages/helpers.cljc rename to common/src/app/common/files/helpers.cljc index f9bb170b90..287bb7bb86 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/files/helpers.cljc @@ -4,13 +4,14 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.pages.helpers +(ns app.common.files.helpers (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.geom.shapes.common :as gco] + [app.common.schema :as sm] [app.common.types.components-list :as ctkl] [app.common.types.pages-list :as ctpl] - [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [clojure.set :as set] [cuerdas.core :as str])) @@ -22,67 +23,93 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn root? - [{:keys [id type]}] - (and (= type :frame) (= id uuid/zero))) + [shape] + (and (= (dm/get-prop shape :type) :frame) + (= (dm/get-prop shape :id) uuid/zero))) + +(defn is-direct-child-of-root? + ([objects id] + (is-direct-child-of-root? (get objects id))) + ([shape] + (and (some? shape) + (= (dm/get-prop shape :frame-id) uuid/zero)))) (defn root-frame? ([objects id] - (root-frame? (get objects id))) - ([{:keys [frame-id type]}] - (and (= type :frame) - (= frame-id uuid/zero)))) + (if (= id uuid/zero) + false + (root-frame? (get objects id)))) + ([shape] + (and (some? shape) + (not= (dm/get-prop shape :id) uuid/zero) + (= (dm/get-prop shape :type) :frame) + (= (dm/get-prop shape :frame-id) uuid/zero)))) (defn frame-shape? ([objects id] (frame-shape? (get objects id))) - ([{:keys [type]}] - (= type :frame))) + ([shape] + (and (some? shape) + (= :frame (dm/get-prop shape :type))))) (defn group-shape? ([objects id] (group-shape? (get objects id))) - ([{:keys [type]}] - (= type :group))) + ([shape] + (and (some? shape) + (= :group (dm/get-prop shape :type))))) (defn mask-shape? - [{:keys [type masked-group?]}] - (and (= type :group) masked-group?)) + ([shape] + (and ^boolean (group-shape? shape) + ^boolean (:masked-group shape))) + ([objects id] + (mask-shape? (get objects id)))) (defn bool-shape? - [{:keys [type]}] - (= type :bool)) - -(defn group-like-shape? - [{:keys [type]}] - (or (= :group type) (= :bool type))) + [shape] + (and (some? shape) + (= :bool (dm/get-prop shape :type)))) (defn text-shape? - [{:keys [type]}] - (= type :text)) + [shape] + (and (some? shape) + (= :text (dm/get-prop shape :type)))) (defn rect-shape? + [shape] + (and (some? shape) + (= :rect (dm/get-prop shape :type)))) + +(defn circle-shape? [{:keys [type]}] - (= type :rect)) + (= type :circle)) (defn image-shape? - [{:keys [type]}] - (= type :image)) + [shape] + (and (some? shape) + (= :image (dm/get-prop shape :type)))) (defn svg-raw-shape? - [{:keys [type]}] - (= type :svg-raw)) + ([objects id] + (svg-raw-shape? (get objects id))) + ([shape] + (and (some? shape) + (= :svg-raw (dm/get-prop shape :type))))) (defn path-shape? ([objects id] (path-shape? (get objects id))) - ([{:keys [type]}] - (= type :path))) + ([shape] + (and (some? shape) + (= :path (dm/get-prop shape :type))))) (defn unframed-shape? "Checks if it's a non-frame shape in the top level." [shape] - (and (not (frame-shape? shape)) - (= (:frame-id shape) uuid/zero))) + (and (some? shape) + (not (frame-shape? shape)) + (= (dm/get-prop shape :frame-id) uuid/zero))) (defn has-children? ([objects id] @@ -90,10 +117,19 @@ ([shape] (d/not-empty? (:shapes shape)))) +(defn group-like-shape? + ([objects id] + (group-like-shape? (get objects id))) + ([shape] + (or ^boolean (group-shape? shape) + ^boolean (bool-shape? shape) + ^boolean (and (svg-raw-shape? shape) (has-children? shape))))) + +;; ---- ACCESSORS + (defn get-children-ids [objects id] - (letfn [(get-children-ids-rec - [id processed] + (letfn [(get-children-ids-rec [id processed] (when (not (contains? processed id)) (when-let [shapes (-> (get objects id) :shapes (some-> vec))] (into shapes (mapcat #(get-children-ids-rec % (conj processed id))) shapes))))] @@ -111,26 +147,69 @@ [objects id] (mapv (d/getf objects) (get-children-ids-with-self objects id))) +(defn get-child + "Return the child of the given object with the given id (allow that the + id may point to the object itself)." + [objects id child-id] + (let [shape (get objects id)] + (if (= id child-id) + shape + (some #(get-child objects % child-id) (:shapes shape))))) + (defn get-parent - "Retrieve the id of the parent for the shape-id (if exists)" + "Retrieve the parent for the shape-id (if exists)" [objects id] - (let [lookup (d/getf objects)] - (-> id lookup :parent-id lookup))) + (when-let [shape (get objects id)] + (get objects (dm/get-prop shape :parent-id)))) (defn get-parent-id "Retrieve the id of the parent for the shape-id (if exists)" [objects id] - (-> objects (get id) :parent-id)) + (when-let [shape (get objects id)] + (dm/get-prop shape :parent-id))) (defn get-parent-ids + "Returns a vector of parents of the specified shape." + [objects shape-id] + (loop [result [] + id shape-id] + (let [parent-id (get-parent-id objects id)] + (if (and (some? parent-id) (not= parent-id id)) + (recur (conj result parent-id) parent-id) + result)))) + +(defn get-parent-ids-seq + "Returns a sequence of parents of the specified shape." + [objects shape-id] + (let [parent-id (get-parent-id objects shape-id)] + (when (and (some? parent-id) (not= parent-id shape-id)) + (lazy-seq (cons parent-id (get-parent-ids-seq objects parent-id)))))) + +(defn get-parent-ids-seq-with-self + "Returns a sequence of parents of the specified shape, including itself." + [objects shape-id] + (cons shape-id (get-parent-ids-seq objects shape-id))) + +(defn get-parents "Returns a vector of parents of the specified shape." [objects shape-id] (loop [result [] id shape-id] (let [parent-id (dm/get-in objects [id :parent-id])] (if (and (some? parent-id) (not= parent-id id)) - (recur (conj result parent-id) parent-id) + (recur (conj result (get objects parent-id)) parent-id) result)))) +(defn get-parent-seq + "Returns a vector of parents of the specified shape." + ([objects shape-id] + (get-parent-seq objects (get objects shape-id) shape-id)) + + ([objects shape shape-id] + (let [parent-id (dm/get-prop shape :parent-id) + parent (get objects parent-id)] + (when (and (some? parent) (not= parent-id shape-id)) + (lazy-seq (cons parent (get-parent-seq objects parent parent-id))))))) + (defn get-parents-with-self [objects id] (let [lookup (d/getf objects)] @@ -139,12 +218,12 @@ (defn hidden-parent? "Checks the parent for the hidden property" [objects shape-id] - (let [parent-id (dm/get-in objects [shape-id :parent-id])] - (cond - (or (nil? parent-id) (nil? shape-id) (= shape-id uuid/zero) (= parent-id uuid/zero)) false - (dm/get-in objects [parent-id :hidden]) true - :else - (recur objects parent-id)))) + (let [parent-id (get-parent-id objects shape-id)] + (if (or (nil? parent-id) (nil? shape-id) (= shape-id uuid/zero) (= parent-id uuid/zero)) + false + (if ^boolean (dm/get-in objects [parent-id :hidden]) + true + (recur objects parent-id))))) (defn get-parent-ids-with-index "Returns a tuple with the list of parents and a map with the position within each parent" @@ -152,10 +231,10 @@ (loop [parent-list [] parent-indices {} current shape-id] - (let [parent-id (dm/get-in objects [current :parent-id]) - parent (get objects parent-id)] + (let [parent-id (get-parent-id objects current) + parent (get objects parent-id)] (if (and (some? parent) (not= parent-id current)) - (let [parent-list (conj parent-list parent-id) + (let [parent-list (conj parent-list parent-id) parent-indices (assoc parent-indices parent-id (d/index-of (:shapes parent) current))] (recur parent-list parent-indices parent-id)) [parent-list parent-indices])))) @@ -163,7 +242,7 @@ (defn get-siblings-ids [objects id] (let [parent (get-parent objects id)] - (into [] (->> (:shapes parent) (remove #(= % id)))))) + (into [] (remove #(= % id)) (:shapes parent)))) (defn get-frame "Get the frame that contains the shape. If the shape is already a @@ -175,7 +254,7 @@ (map? shape-or-id) (if (frame-shape? shape-or-id) shape-or-id - (get objects (:frame-id shape-or-id))) + (get objects (dm/get-prop shape-or-id :frame-id))) (= uuid/zero shape-or-id) (get objects uuid/zero) @@ -229,12 +308,18 @@ (defn get-immediate-children "Retrieve resolved shape objects that are immediate children of the specified shape-id" - ([objects] (get-immediate-children objects uuid/zero)) - ([objects shape-id] + ([objects] (get-immediate-children objects uuid/zero nil)) + ([objects shape-id] (get-immediate-children objects shape-id nil)) + ([objects shape-id {:keys [remove-hidden remove-blocked] :or {remove-hidden false remove-blocked false}}] (let [lookup (d/getf objects)] (->> (lookup shape-id) (:shapes) - (keep lookup))))) + (keep (fn [cid] + (when-let [child (lookup cid)] + (when (and (or (not remove-hidden) (not (:hidden child))) + (or (not remove-blocked) (not (:blocked child)))) + child)))) + (remove gco/invalid-geometry?))))) (declare indexed-shapes) @@ -275,7 +360,8 @@ (defn set-touched-group [touched group] - (conj (or touched #{}) group)) + (when group + (conj (or touched #{}) group))) (defn touched-group? [shape group] @@ -327,6 +413,41 @@ ;; ALGORITHMS & TRANSFORMATIONS FOR SHAPES ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn get-used-names + "Return a set with the all unique names used in the + elements (any entity thas has a :name)" + [elements] + (let [elements (if (map? elements) + (vals elements) + elements)] + (into #{} (keep :name) elements))) + +(defn- extract-numeric-suffix + [basename] + (if-let [[_ p1 p2] (re-find #"(.*) ([0-9]+)$" basename)] + [p1 (+ 1 (d/parse-integer p2))] + [basename 1])) + +(defn generate-unique-name + "A unique name generator" + [used basename] + (dm/assert! + "expected a set of strings" + (sm/check-set-of-strings! used)) + + (dm/assert! + "expected a string for `basename`." + (string? basename)) + + (if-not (contains? used basename) + basename + (let [[prefix initial] (extract-numeric-suffix basename)] + (loop [counter initial] + (let [candidate (str prefix " " counter)] + (if (contains? used candidate) + (recur (inc counter)) + candidate)))))) + (defn walk-pages "Go through all pages of a file and apply a function to each one" ;; The function receives two parameters (page-id and page), and @@ -372,7 +493,7 @@ (letfn [(red-fn [cur-idx id] (let [[prev-idx _] (first cur-idx) prev-idx (or prev-idx 0) - cur-idx (conj cur-idx [(inc prev-idx) id])] + cur-idx (conj cur-idx (d/vec2 (inc prev-idx) id))] (rec-index cur-idx id))) (rec-index [cur-idx id] (let [object (get objects id)] @@ -397,10 +518,11 @@ (defn order-by-indexed-shapes [objects ids] - (->> (indexed-shapes objects) - (sort-by first) - (filter (comp (into #{} ids) second)) - (map second))) + (let [ids (if (set? ids) ids (set ids))] + (->> (indexed-shapes objects) + (filter (fn [o] (contains? ids (val o)))) + (sort-by key) + (map val)))) (defn get-index-replacement "Given a collection of shapes, calculate their positions @@ -430,6 +552,17 @@ [path-vec] (str/join " / " path-vec)) +(defn join-path-with-dot + "Regenerate a path as a string, from a vector." + [path-vec] + (str/join "\u00A0\u2022\u00A0" path-vec)) + +(defn clean-path + "Remove empty items from the path." + [path] + (->> (split-path path) + (join-path))) + (defn parse-path-name "Parse a string in the form 'group / subgroup / name'. Retrieve the path and the name in separated values, normalizing spaces." @@ -448,27 +581,60 @@ path) name)) +(defn merge-path-item-with-dot + "Put the item at the end of the path." + [path name] + (if-not (empty? path) + (if-not (empty? name) + (str path "\u00A0\u2022\u00A0" name) + path) + name)) + (defn compact-path "Separate last item of the path, and truncate the others if too long: 'one' -> ['' 'one' false] 'one / two / three' -> ['one / two' 'three' false] 'one / two / three / four' -> ['one / two / ...' 'four' true] 'one-item-but-very-long / two' -> ['...' 'two' true] " - [path max-length] + [path max-length dot?] (let [path-split (split-path path) - last-item (last path-split)] + last-item (last path-split) + merge-path (if dot? + merge-path-item-with-dot + merge-path-item)] (loop [other-items (seq (butlast path-split)) other-path ""] (if-let [item (first other-items)] (let [full-path (-> other-path - (merge-path-item item) - (merge-path-item last-item))] + (merge-path item) + (merge-path last-item))] (if (> (count full-path) max-length) - [(merge-path-item other-path "...") last-item true] + [(merge-path other-path "...") last-item true] (recur (next other-items) - (merge-path-item other-path item)))) + (merge-path other-path item)))) [other-path last-item false])))) +(defn butlast-path + "Remove the last item of the path." + [path] + (let [split (split-path path)] + (if (= 1 (count split)) + "" + (join-path (butlast split))))) + +(defn butlast-path-with-dots + "Remove the last item of the path." + [path] + (let [split (split-path path)] + (if (= 1 (count split)) + "" + (join-path-with-dot (butlast split))))) + +(defn last-path + "Returns the last item of the path." + [path] + (last (split-path path))) + (defn compact-name "Append the first item of the path and the name." [path name] @@ -489,8 +655,9 @@ ;; Implemented with transients for performance. 30~50% better (letfn [(process-shape [objects [id shape]] (let [frame-id (if (= :frame (:type shape)) id (:frame-id shape)) - cur (-> (or (get objects frame-id) (transient {})) - (assoc! id shape))] + cur (-> (or (get objects frame-id) + (transient {})) + (assoc! id shape))] (assoc! objects frame-id cur)))] (update-vals (->> objects @@ -574,22 +741,6 @@ (d/seek root-frame?) :id)) -(defn comparator-layout-z-index - [[idx-a child-a] [idx-b child-b]] - (cond - (> (ctl/layout-z-index child-a) (ctl/layout-z-index child-b)) 1 - (< (ctl/layout-z-index child-a) (ctl/layout-z-index child-b)) -1 - (> idx-a idx-b) 1 - (< idx-a idx-b) -1 - :else 0)) - -(defn sort-layout-children-z-index - [children] - (->> children - (d/enumerate) - (sort comparator-layout-z-index) - (mapv second))) - (defn common-parent-frame "Search for the common frame for the selected shapes. Otherwise returns the root frame" [objects selected] @@ -611,3 +762,22 @@ [frame-id (get-parent-ids objects frame-id)]))] (recur frame-id frame-parents (rest selected)))))) + +(defn fixed-scroll? + [shape] + ^boolean + (and (:fixed-scroll shape) + (= (:parent-id shape) (:frame-id shape)) + (not= (:frame-id shape) uuid/zero))) + +(defn fixed? + [objects shape-id] + (let [ids-to-check + (concat + [shape-id] + (get-children-ids objects shape-id) + (->> (get-parent-ids objects shape-id) + (take-while #(and (not= % uuid/zero) (not (root-frame? objects %))))))] + (boolean + (->> ids-to-check + (d/seek (fn [id] () (fixed-scroll? (get objects id)))))))) diff --git a/common/src/app/common/pages/indices.cljc b/common/src/app/common/files/indices.cljc similarity index 82% rename from common/src/app/common/pages/indices.cljc rename to common/src/app/common/files/indices.cljc index 7a5e8ef1ae..032762c0a5 100644 --- a/common/src/app/common/pages/indices.cljc +++ b/common/src/app/common/files/indices.cljc @@ -4,18 +4,11 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.pages.indices +(ns app.common.files.indices (:require - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cfh] [app.common.uuid :as uuid])) -(defn generate-child-parent-index - [objects] - (reduce-kv - (fn [index id obj] - (assoc index id (:parent-id obj))) - {} objects)) - (defn generate-child-all-parents-index "Creates an index where the key is the shape id and the value is a set with all the parents" @@ -25,7 +18,7 @@ ([objects shapes] (let [shape->entry (fn [shape] - [(:id shape) (cph/get-parent-ids objects (:id shape))])] + [(:id shape) (cfh/get-parent-ids objects (:id shape))])] (into {} (map shape->entry) shapes)))) (defn create-clip-index @@ -42,7 +35,7 @@ (not= uuid/zero (:id shape))) (conj shape) - (:masked-group? shape) + (:masked-group shape) (conj (get objects (->> shape :shapes first))) (= :bool (:type shape)) diff --git a/common/src/app/common/files/libraries_helpers.cljc b/common/src/app/common/files/libraries_helpers.cljc new file mode 100644 index 0000000000..8b7f34acae --- /dev/null +++ b/common/src/app/common/files/libraries_helpers.cljc @@ -0,0 +1,103 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.files.libraries-helpers + (:require + [app.common.data :as d] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] + [app.common.uuid :as uuid])) + +(defn generate-add-component-changes + [changes root objects file-id page-id components-v2] + (let [name (:name root) + [path name] (cfh/parse-path-name name) + + [root-shape new-shapes updated-shapes] + (if-not components-v2 + (ctn/make-component-shape root objects file-id components-v2) + (ctn/convert-shape-in-component root objects file-id)) + + changes (-> changes + (pcb/add-component (:id root-shape) + path + name + new-shapes + updated-shapes + (:id root) + page-id))] + [root-shape changes])) + +(defn generate-add-component + "If there is exactly one id, and it's a frame (or a group in v1), and not already a component, + use it as root. Otherwise, create a frame (v2) or group (v1) that contains all ids. Then, make a + component with it, and link all shapes to their corresponding one in the component." + [it shapes objects page-id file-id components-v2 prepare-create-group prepare-create-board] + + (let [changes (pcb/empty-changes it page-id) + shapes-count (count shapes) + first-shape (first shapes) + + from-singe-frame? + (and (= 1 shapes-count) + (cfh/frame-shape? first-shape)) + + [root changes old-root-ids] + (if (and (= shapes-count 1) + (or (and (cfh/group-shape? first-shape) + (not components-v2)) + (cfh/frame-shape? first-shape)) + (not (ctk/instance-head? first-shape))) + [first-shape + (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects)) + (:shapes first-shape)] + + (let [root-name (if (= 1 shapes-count) + (:name first-shape) + "Component 1") + + shape-ids (into (d/ordered-set) (map :id) shapes) + + [root changes] + (if-not components-v2 + (prepare-create-group it ; These functions needs to be passed as argument + objects ; to avoid a circular dependence + page-id + shapes + root-name + (not (ctk/instance-head? first-shape))) + (prepare-create-board changes + (uuid/next) + (:parent-id first-shape) + objects + shape-ids + nil + root-name + true))] + + [root changes shape-ids])) + + changes + (cond-> changes + (not from-singe-frame?) + (pcb/update-shapes + (:shapes root) + (fn [shape] + (assoc shape :constraints-h :scale :constraints-v :scale)))) + + objects' (assoc objects (:id root) root) + + [root-shape changes] (generate-add-component-changes changes root objects' file-id page-id components-v2) + + changes (pcb/update-shapes changes + old-root-ids + #(dissoc % :component-root) + [:component-root])] + + [root (:id root-shape) changes])) diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc new file mode 100644 index 0000000000..b62521b8f2 --- /dev/null +++ b/common/src/app/common/files/migrations.cljc @@ -0,0 +1,938 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.files.migrations + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.features :as cfeat] + [app.common.files.changes :as cpc] + [app.common.files.defaults :as cfd] + [app.common.files.helpers :as cfh] + [app.common.geom.matrix :as gmt] + [app.common.geom.rect :as grc] + [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.path :as gsp] + [app.common.geom.shapes.text :as gsht] + [app.common.logging :as l] + [app.common.math :as mth] + [app.common.schema :as sm] + [app.common.svg :as csvg] + [app.common.text :as txt] + [app.common.types.shape :as cts] + [app.common.types.shape.shadow :as ctss] + [app.common.uuid :as uuid] + [cuerdas.core :as str])) + +#?(:cljs (l/set-level! :info)) + +(declare ^:private migrations) + +(def version cfd/version) + +(defn need-migration? + [file] + (or (nil? (:version file)) + (not= cfd/version (:version file)))) + +(defn- apply-migrations + [data migrations from-version] + + (loop [migrations migrations + data data] + (if-let [[to-version migrate-fn] (first migrations)] + (let [migrate-fn (or migrate-fn identity)] + (l/inf :hint "migrate file" + :op (if (>= from-version to-version) "down" "up") + :file-id (str (:id data)) + :version to-version) + (recur (rest migrations) + (migrate-fn data))) + data))) + +(defn migrate-data + [data migrations from-version to-version] + (if (= from-version to-version) + data + (let [migrations (if (< from-version to-version) + (->> migrations + (drop-while #(<= (get % :id) from-version)) + (take-while #(<= (get % :id) to-version)) + (map (juxt :id :migrate-up))) + (->> (reverse migrations) + (drop-while #(> (get % :id) from-version)) + (take-while #(> (get % :id) to-version)) + (map (juxt :id :migrate-down))))] + (apply-migrations data migrations from-version)))) + +(defn fix-version + "Fixes the file versioning numbering" + [{:keys [version] :as file}] + (if (int? version) + file + (let [version (or (-> file :data :version) 0)] + (-> file + (assoc :version version) + (update :data dissoc :version))))) + +(defn migrate-file + [{:keys [id data features version] :as file}] + (binding [cfeat/*new* (atom #{})] + (let [version (or version (:version data)) + file (-> file + (assoc :version cfd/version) + (update :data (fn [data] + (-> data + (assoc :id id) + (dissoc :version) + (migrate-data migrations version cfd/version)))) + (update :features (fnil into #{}) (deref cfeat/*new*)) + ;; NOTE: in some future we can consider to apply + ;; a migration to the whole database and remove + ;; this code from this function that executes on + ;; each file migration operation + (update :features cfeat/migrate-legacy-features))] + + (if (or (not= version (:version file)) + (not= features (:features file))) + (vary-meta file assoc ::migrated true) + file)))) + +(defn migrated? + [file] + (true? (-> file meta ::migrated))) + +;; -- MIGRATIONS -- + +(defn migrate-up-2 + "Ensure that all :shape attributes on shapes are vectors" + [data] + (letfn [(update-object [object] + (d/update-when object :shapes + (fn [shapes] + (if (seq? shapes) + (into [] shapes) + shapes)))) + (update-page [page] + (update page :objects update-vals update-object))] + + (update data :pages-index update-vals update-page))) + +(defn migrate-up-3 + "Changes paths formats" + [data] + (letfn [(migrate-path [shape] + (if-not (contains? shape :content) + (let [content (gsp/segments->content (:segments shape) (:close? shape)) + selrect (gsh/content->selrect content) + points (grc/rect->points selrect)] + (-> shape + (dissoc :segments) + (dissoc :close?) + (assoc :content content) + (assoc :selrect selrect) + (assoc :points points))) + ;; If the shape contains :content is already in the new format + shape)) + + (fix-frames-selrects [frame] + (if (= (:id frame) uuid/zero) + frame + (let [selrect (gsh/shape->rect frame)] + (-> frame + (assoc :selrect selrect) + (assoc :points (grc/rect->points selrect)))))) + + (fix-empty-points [shape] + (if (empty? (:points shape)) + (-> shape + (update :selrect (fn [selrect] + (if (map? selrect) + (grc/make-rect selrect) + selrect))) + (cts/setup-shape)) + shape)) + + (update-object [object] + (cond-> object + (= :curve (:type object)) + (assoc :type :path) + + (#{:curve :path} (:type object)) + (migrate-path) + + (cfh/frame-shape? object) + (fix-frames-selrects) + + (and (empty? (:points object)) (not= (:id object) uuid/zero)) + (fix-empty-points))) + + (update-page [page] + (update page :objects update-vals update-object))] + + (update data :pages-index update-vals update-page))) + +(defn migrate-up-5 + "Put the id of the local file in :component-file in instances of + local components" + [data] + (letfn [(update-object [object] + (if (and (some? (:component-id object)) + (nil? (:component-file object))) + (assoc object :component-file (:id data)) + object)) + + (update-page [page] + (update page :objects update-vals update-object))] + + (update data :pages-index update-vals update-page))) + +(defn migrate-up-6 + "Fixes issues with selrect/points for shapes with width/height = 0 (line-like paths)" + [data] + (letfn [(fix-line-paths [shape] + (if (= (:type shape) :path) + (let [{:keys [width height]} (grc/points->rect (:points shape))] + (if (or (mth/almost-zero? width) (mth/almost-zero? height)) + (let [selrect (gsh/content->selrect (:content shape)) + points (grc/rect->points selrect) + transform (gmt/matrix) + transform-inv (gmt/matrix)] + (assoc shape + :selrect selrect + :points points + :transform transform + :transform-inverse transform-inv)) + shape)) + shape)) + + (update-container [container] + (update container :objects update-vals fix-line-paths))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-7 + "Remove interactions pointing to deleted frames" + [data] + (letfn [(update-object [page object] + (d/update-when object :interactions + (fn [interactions] + (filterv #(get-in page [:objects (:destination %)]) interactions)))) + + (update-page [page] + (update page :objects update-vals (partial update-object page)))] + + (update data :pages-index update-vals update-page))) + +(defn migrate-up-8 + "Remove groups without any shape, both in pages and components" + [data] + (letfn [(clean-parents [obj deleted?] + (d/update-when obj :shapes + (fn [shapes] + (into [] (remove deleted?) shapes)))) + + (obj-is-empty? [obj] + (and (= (:type obj) :group) + (or (empty? (:shapes obj)) + (nil? (:selrect obj))))) + + (clean-objects [objects] + (loop [entries (seq objects) + deleted #{} + result objects] + (let [[id obj :as entry] (first entries)] + (if entry + (if (obj-is-empty? obj) + (recur (rest entries) + (conj deleted id) + (dissoc result id)) + (recur (rest entries) + deleted + result)) + [(count deleted) + (d/mapm #(clean-parents %2 deleted) result)])))) + + (clean-container [container] + (loop [n 0 + objects (:objects container)] + (let [[deleted objects] (clean-objects objects)] + (if (and (pos? deleted) (< n 1000)) + (recur (inc n) objects) + (assoc container :objects objects)))))] + + (-> data + (update :pages-index update-vals clean-container) + (update :components update-vals clean-container)))) + +(defn migrate-up-9 + [data] + (letfn [(find-empty-groups [objects] + (->> (vals objects) + (filter (fn [shape] + (and (= :group (:type shape)) + (or (empty? (:shapes shape)) + (every? (fn [child-id] + (not (contains? objects child-id))) + (:shapes shape)))))) + (map :id))) + + (calculate-changes [[page-id page]] + (let [objects (:objects page) + eids (find-empty-groups objects)] + + (map (fn [id] + {:type :del-obj + :page-id page-id + :id id}) + eids)))] + + (loop [data data] + (let [changes (mapcat calculate-changes (:pages-index data))] + (if (seq changes) + (recur (cpc/process-changes data changes)) + data))))) + +(defn migrate-up-10 + [data] + (letfn [(update-page [page] + (d/update-in-when page [:objects uuid/zero] dissoc :points :selrect))] + (update data :pages-index update-vals update-page))) + +(defn migrate-up-11 + [data] + (letfn [(update-object [objects shape] + (if (cfh/frame-shape? shape) + (d/update-when shape :shapes (fn [shapes] + (filterv (fn [id] (contains? objects id)) shapes))) + shape)) + + (update-page [page] + (update page :objects (fn [objects] + (update-vals objects (partial update-object objects)))))] + + (update data :pages-index update-vals update-page))) + +(defn migrate-up-12 + [data] + (letfn [(update-grid [grid] + (cond-> grid + (= :auto (:size grid)) + (assoc :size nil))) + + (update-page [page] + (d/update-in-when page [:options :saved-grids] update-vals update-grid))] + + (update data :pages-index update-vals update-page))) + +(defn migrate-up-13 + "Add rx and ry to images" + [data] + (letfn [(fix-radius [shape] + (if-not (or (contains? shape :rx) (contains? shape :r1)) + (-> shape + (assoc :rx 0) + (assoc :ry 0)) + shape)) + + (update-object [object] + (cond-> object + (cfh/image-shape? object) + (fix-radius))) + + (update-page [page] + (update page :objects update-vals update-object))] + + (update data :pages-index update-vals update-page))) + +(defn migrate-up-14 + [data] + (letfn [(process-shape [shape] + (let [fill-color (str/upper (:fill-color shape)) + fill-opacity (:fill-opacity shape)] + (cond-> shape + (and (= 1 fill-opacity) + (or (= "#B1B2B5" fill-color) + (= "#7B7D85" fill-color))) + (dissoc :fill-color :fill-opacity)))) + + (update-container [container] + (if (contains? container :objects) + (loop [objects (:objects container) + shapes (->> (vals objects) + (filter cfh/image-shape?))] + (if-let [shape (first shapes)] + (let [{:keys [id frame-id] :as shape'} (process-shape shape)] + (if (identical? shape shape') + (recur objects (rest shapes)) + (recur (-> objects + (assoc id shape') + (d/update-when frame-id dissoc :thumbnail)) + (rest shapes)))) + (assoc container :objects objects))) + container))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-16 + "Add fills and strokes" + [data] + (letfn [(assign-fills [shape] + (let [attrs {:fill-color (:fill-color shape) + :fill-color-gradient (:fill-color-gradient shape) + :fill-color-ref-file (:fill-color-ref-file shape) + :fill-color-ref-id (:fill-color-ref-id shape) + :fill-opacity (:fill-opacity shape)} + clean-attrs (d/without-nils attrs)] + (cond-> shape + (d/not-empty? clean-attrs) + (assoc :fills [clean-attrs])))) + + (assign-strokes [shape] + (let [attrs {:stroke-style (:stroke-style shape) + :stroke-alignment (:stroke-alignment shape) + :stroke-width (:stroke-width shape) + :stroke-color (:stroke-color shape) + :stroke-color-ref-id (:stroke-color-ref-id shape) + :stroke-color-ref-file (:stroke-color-ref-file shape) + :stroke-opacity (:stroke-opacity shape) + :stroke-color-gradient (:stroke-color-gradient shape) + :stroke-cap-start (:stroke-cap-start shape) + :stroke-cap-end (:stroke-cap-end shape)} + clean-attrs (d/without-nils attrs)] + (cond-> shape + (d/not-empty? clean-attrs) + (assoc :strokes [clean-attrs])))) + + (update-object [object] + (cond-> object + (and (not (cfh/text-shape? object)) + (not (contains? object :strokes))) + (assign-strokes) + + (and (not (cfh/text-shape? object)) + (not (contains? object :fills))) + (assign-fills))) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-17 + [data] + (letfn [(affected-object? [object] + (and (cfh/image-shape? object) + (some? (:fills object)) + (= 1 (count (:fills object))) + (some? (:fill-color object)) + (some? (:fill-opacity object)) + (let [color-old (str/upper (:fill-color object)) + color-new (str/upper (get-in object [:fills 0 :fill-color])) + opacity-old (:fill-opacity object) + opacity-new (get-in object [:fills 0 :fill-opacity])] + (and (= color-old color-new) + (or (= "#B1B2B5" color-old) + (= "#7B7D85" color-old)) + (= 1 opacity-old opacity-new))))) + + (update-object [object] + (cond-> object + (affected-object? object) + (assoc :fills []))) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-18 + "Remove position-data to solve a bug with the text positioning" + [data] + (letfn [(update-object [object] + (cond-> object + (cfh/text-shape? object) + (dissoc :position-data))) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-19 + [data] + (letfn [(update-object [object] + (cond-> object + (and (cfh/text-shape? object) + (d/not-empty? (:position-data object)) + (not (gsht/overlaps-position-data? object (:position-data object)))) + (dissoc :position-data))) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-25 + [data] + (some-> cfeat/*new* (swap! conj "fdata/shape-data-type")) + (letfn [(update-object [object] + (if (cfh/root? object) + object + (-> object + (update :selrect grc/make-rect) + (cts/map->Shape)))) + (update-container [container] + (d/update-when container :objects update-vals update-object))] + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-26 + [data] + (letfn [(update-object [object] + (cond-> object + (nil? (:transform object)) + (assoc :transform (gmt/matrix)) + + (nil? (:transform-inverse object)) + (assoc :transform-inverse (gmt/matrix)))) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-27 + [data] + (letfn [(update-object [object] + (cond-> object + (contains? object :main-instance?) + (-> (assoc :main-instance (:main-instance? object)) + (dissoc :main-instance?)) + + (contains? object :component-root?) + (-> (assoc :component-root (:component-root? object)) + (dissoc :component-root?)) + + (contains? object :remote-synced?) + (-> (assoc :remote-synced (:remote-synced? object)) + (dissoc :remote-synced?)) + + (contains? object :masked-group?) + (-> (assoc :masked-group (:masked-group? object)) + (dissoc :masked-group?)) + + (contains? object :saved-component-root?) + (-> (assoc :saved-component-root (:saved-component-root? object)) + (dissoc :saved-component-root?)))) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-28 + [data] + (letfn [(update-object [objects object] + (let [frame-id (:frame-id object) + calculated-frame-id + (or (->> (cfh/get-parent-ids objects (:id object)) + (map (d/getf objects)) + (d/seek cfh/frame-shape?) + :id) + ;; If we cannot find any we let the frame-id as it was before + frame-id)] + (when (not= frame-id calculated-frame-id) + (l/trc :hint "Fix wrong frame-id" + :shape (:name object) + :id (:id object) + :current (dm/get-in objects [frame-id :name]) + :calculated (get-in objects [calculated-frame-id :name]))) + (assoc object :frame-id calculated-frame-id))) + + (update-container [container] + (d/update-when container :objects #(update-vals % (partial update-object %))))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-29 + [data] + (letfn [(valid-ref? [ref] + (or (uuid? ref) + (nil? ref))) + + (valid-node? [node] + (and (valid-ref? (:typography-ref-file node)) + (valid-ref? (:typography-ref-id node)) + (valid-ref? (:fill-color-ref-file node)) + (valid-ref? (:fill-color-ref-id node)))) + + (fix-ref [ref] + (if (valid-ref? ref) ref nil)) + + (fix-node [node] + (-> node + (d/update-when :typography-ref-file fix-ref) + (d/update-when :typography-ref-id fix-ref) + (d/update-when :fill-color-ref-file fix-ref) + (d/update-when :fill-color-ref-id fix-ref))) + + (update-object [object] + (let [invalid-node? (complement valid-node?)] + (cond-> object + (cfh/text-shape? object) + (update :content #(txt/transform-nodes invalid-node? fix-node %))))) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-31 + [data] + (letfn [(update-object [object] + (cond-> object + (contains? object :use-for-thumbnail?) + (-> (assoc :use-for-thumbnail (:use-for-thumbnail? object)) + (dissoc :use-for-thumbnail?)))) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-32 + [data] + (some-> cfeat/*new* (swap! conj "fdata/shape-data-type")) + (letfn [(update-object [object] + (as-> object object + (if (contains? object :svg-attrs) + (update object :svg-attrs csvg/attrs->props) + object) + (if (contains? object :svg-viewbox) + (update object :svg-viewbox grc/make-rect) + object))) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-33 + [data] + (letfn [(update-object [object] + ;; Ensure all root objects are well formed shapes. + (if (= (:id object) uuid/zero) + (-> object + (assoc :parent-id uuid/zero) + (assoc :frame-id uuid/zero) + ;; We explicitly dissoc them and let the shape-setup + ;; to regenerate it with valid values. + (dissoc :selrect) + (dissoc :points) + (cts/setup-shape)) + object)) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + (-> data + (update :pages-index update-vals update-container)))) + +(defn migrate-up-34 + [data] + (letfn [(update-object [object] + (if (or (cfh/path-shape? object) + (cfh/bool-shape? object)) + (dissoc object :x :y :width :height) + object)) + (update-container [container] + (d/update-when container :objects update-vals update-object))] + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-36 + [data] + (letfn [(update-container [container] + (d/update-when container :objects (fn [objects] + (if (contains? objects nil) + (dissoc objects nil) + objects))))] + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-37 + "Clean nil values on data" + [data] + (d/without-nils data)) + +(defn migrate-up-38 + [data] + (letfn [(fix-gradient [{:keys [type] :as gradient}] + (if (string? type) + (assoc gradient :type (keyword type)) + gradient)) + + (update-fill [fill] + (d/update-when fill :fill-color-gradient fix-gradient)) + + (update-object [object] + (d/update-when object :fills #(mapv update-fill %))) + + (update-shape [shape] + (let [shape (update-object shape)] + (if (cfh/text-shape? shape) + (-> shape + (update :content (partial txt/transform-nodes identity update-fill)) + (d/update-when :position-data #(mapv update-object %))) + shape))) + + (update-container [container] + (d/update-when container :objects update-vals update-shape))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-39 + [data] + (letfn [(update-shape [shape] + (cond + (and (cfh/bool-shape? shape) + (not (contains? shape :bool-content))) + (assoc shape :bool-content []) + + (and (cfh/path-shape? shape) + (not (contains? shape :content))) + (assoc shape :content []) + + :else + shape)) + + (update-container [container] + (d/update-when container :objects update-vals update-shape))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-40 + [data] + (letfn [(update-shape [{:keys [content shapes] :as shape}] + ;; Fix frame shape that in reallity is a path shape + (if (and (cfh/frame-shape? shape) + (contains? shape :selrect) + (seq content) + (not (seq shapes)) + (contains? (first content) :command)) + (-> shape + (assoc :type :path) + (assoc :x nil) + (assoc :y nil) + (assoc :width nil) + (assoc :height nil)) + shape)) + + (update-container [container] + (d/update-when container :objects update-vals update-shape))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-41 + [data] + (letfn [(update-shape [shape] + (cond + (or (cfh/bool-shape? shape) + (cfh/path-shape? shape)) + shape + + ;; Fix all shapes that has geometry broken but still + ;; preservers the selrect, so we recalculate the + ;; geometry from selrect. + (and (contains? shape :selrect) + (or (nil? (:x shape)) + (nil? (:y shape)) + (nil? (:width shape)) + (nil? (:height shape)))) + (let [selrect (:selrect shape)] + (-> shape + (assoc :x (:x selrect)) + (assoc :y (:y selrect)) + (assoc :width (:width selrect)) + (assoc :height (:height selrect)))) + + :else + shape)) + + (update-container [container] + (d/update-when container :objects update-vals update-shape))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-42 + [data] + (letfn [(update-object [object] + (if (and (or (cfh/frame-shape? object) + (cfh/group-shape? object) + (cfh/bool-shape? object)) + (not (:shapes object))) + (assoc object :shapes []) + object)) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(def ^:private valid-fill? + (sm/lazy-validator ::cts/fill)) + +(defn migrate-up-43 + [data] + (letfn [(number->string [v] + (if (number? v) + (str v) + v)) + + (update-text-node [node] + (-> node + (d/update-when :fills #(filterv valid-fill? %)) + (d/update-when :font-size number->string) + (d/update-when :font-weight number->string) + (d/without-nils))) + + (update-object [object] + (if (cfh/text-shape? object) + (update object :content #(txt/transform-nodes identity update-text-node %)) + object)) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(def ^:private valid-shadow? + (sm/lazy-validator ::ctss/shadow)) + +(defn migrate-up-44 + [data] + (letfn [(fix-shadow [shadow] + (let [color (if (string? (:color shadow)) + {:color (:color shadow) + :opacity 1} + (d/without-nils (:color shadow)))] + (assoc shadow :color color))) + + (update-object [object] + (d/update-when object :shadow + #(into [] + (comp (map fix-shadow) + (filter valid-shadow?)) + %))) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defn migrate-up-45 + [data] + (letfn [(fix-shape [shape] + (let [frame-id (or (:frame-id shape) + uuid/zero) + parent-id (or (:parent-id shape) + frame-id)] + (assoc shape :frame-id frame-id + :parent-id parent-id))) + + (update-container [container] + (d/update-when container :objects update-vals fix-shape))] + (-> data + (update :pages-index update-vals update-container)))) + +(defn migrate-up-46 + [data] + (letfn [(update-object [object] + (dissoc object :thumbnail)) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(def migrations + "A vector of all applicable migrations" + [{:id 2 :migrate-up migrate-up-2} + {:id 3 :migrate-up migrate-up-3} + {:id 5 :migrate-up migrate-up-5} + {:id 6 :migrate-up migrate-up-6} + {:id 7 :migrate-up migrate-up-7} + {:id 8 :migrate-up migrate-up-8} + {:id 9 :migrate-up migrate-up-9} + {:id 10 :migrate-up migrate-up-10} + {:id 11 :migrate-up migrate-up-11} + {:id 12 :migrate-up migrate-up-12} + {:id 13 :migrate-up migrate-up-13} + {:id 14 :migrate-up migrate-up-14} + {:id 16 :migrate-up migrate-up-16} + {:id 17 :migrate-up migrate-up-17} + {:id 18 :migrate-up migrate-up-18} + {:id 19 :migrate-up migrate-up-19} + {:id 25 :migrate-up migrate-up-25} + {:id 26 :migrate-up migrate-up-26} + {:id 27 :migrate-up migrate-up-27} + {:id 28 :migrate-up migrate-up-28} + {:id 29 :migrate-up migrate-up-29} + {:id 31 :migrate-up migrate-up-31} + {:id 32 :migrate-up migrate-up-32} + {:id 33 :migrate-up migrate-up-33} + {:id 34 :migrate-up migrate-up-34} + {:id 36 :migrate-up migrate-up-36} + {:id 37 :migrate-up migrate-up-37} + {:id 38 :migrate-up migrate-up-38} + {:id 39 :migrate-up migrate-up-39} + {:id 40 :migrate-up migrate-up-40} + {:id 41 :migrate-up migrate-up-41} + {:id 42 :migrate-up migrate-up-42} + {:id 43 :migrate-up migrate-up-43} + {:id 44 :migrate-up migrate-up-44} + {:id 45 :migrate-up migrate-up-45} + {:id 46 :migrate-up migrate-up-46}]) diff --git a/common/src/app/common/pages/diff.cljc b/common/src/app/common/files/page_diff.cljc similarity index 99% rename from common/src/app/common/pages/diff.cljc rename to common/src/app/common/files/page_diff.cljc index dfcd4bf29d..e347309eca 100644 --- a/common/src/app/common/pages/diff.cljc +++ b/common/src/app/common/files/page_diff.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.pages.diff +(ns app.common.files.page-diff "Given a page in its old version and the new will retrieve a map with the differences that will have an impact in the snap data" (:require diff --git a/common/src/app/common/files/repair.cljc b/common/src/app/common/files/repair.cljc new file mode 100644 index 0000000000..4c42d9225b --- /dev/null +++ b/common/src/app/common/files/repair.cljc @@ -0,0 +1,480 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.files.repair + (:require + [app.common.data :as d] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] + [app.common.logging :as log] + [app.common.types.component :as ctk] + [app.common.types.components-list :as ctkl] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] + [app.common.types.shape :as cts] + [app.common.uuid :as uuid])) + +(log/set-level! :debug) + +(defmulti repair-error + (fn [code _error _file-data _libraries] code)) + +(defmethod repair-error :invalid-geometry + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Reset geometry to minimal + (log/debug :hint " -> reset geometry") + (-> shape + (assoc :x 0) + (assoc :y 0) + (assoc :width 0.01) + (assoc :height 0.01) + (cts/setup-rect)))] + (log/dbg :hint "repairing shape :invalid-geometry" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :parent-not-found + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Set parent to root frame. + (log/debug :hint " -> set to " :parent-id uuid/zero) + (assoc shape :parent-id uuid/zero))] + + (log/dbg :hint "repairing shape :parent-not-found" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :child-not-in-parent + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [parent-shape] + ; Add shape to parent's children list + (log/debug :hint " -> add children to" :parent-id (:id parent-shape)) + (update parent-shape :shapes conj (:id shape)))] + + (log/dbg :hint "repairing shape :child-not-in-parent" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:parent-id shape)] repair-shape)))) + +(defmethod repair-error :duplicated-children + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Remove duplicated + (log/debug :hint " -> remove duplicated children") + (update shape :shapes distinct))] + + (log/dbg :hint "repairing shape :duplicated-children" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :child-not-found + [_ {:keys [shape page-id args] :as error} file-data _] + (let [repair-shape + (fn [parent-shape] + (log/debug :hint " -> remove child" :child-id (:child-id args)) + (update parent-shape :shapes (fn [shapes] + (d/removev #(= (:child-id args) %) shapes))))] + (log/dbg :hint "repairing shape :child-not-found" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :invalid-parent + [_ {:keys [shape page-id args] :as error} file-data _] + (log/dbg :hint "repairing shape :invalid-parent" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/change-parent (:parent-id args) [shape] nil {:component-swap true}))) + +(defmethod repair-error :frame-not-found + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Locate the first frame in parents and set frame-id to it. + (let [page (ctpl/get-page file-data page-id) + frame (cfh/get-frame (:objects page) (:parent-id shape)) + frame-id (or (:id frame) uuid/zero)] + (log/debug :hint " -> set to " :frame-id frame-id) + (assoc shape :frame-id frame-id)))] + + (log/dbg :hint "repairing shape :frame-not-found" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :invalid-frame + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Locate the first frame in parents and set frame-id to it. + (let [page (ctpl/get-page file-data page-id) + frame (cfh/get-frame (:objects page) (:parent-id shape)) + frame-id (or (:id frame) uuid/zero)] + (log/debug :hint " -> set to " :frame-id frame-id) + (assoc shape :frame-id frame-id)))] + + (log/dbg :hint "repairing shape :invalid-frame" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :component-not-main + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Set the :shape as main instance root + (log/debug :hint " -> set :main-instance") + (assoc shape :main-instance true))] + + (log/dbg :hint "repairing shape :component-not-main" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :component-main-external + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Set :component-file to local file + (log/debug :hint " -> set :component-file to local file") + (assoc shape :component-file (:id file-data)))] + ; There is no solution that may recover it with confidence + ;; (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.") + ;; shape)] + + (log/dbg :hint "repairing shape :component-main-external" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :component-not-found + [_ {:keys [shape page-id] :as error} file-data _] + (let [page (ctpl/get-page file-data page-id) + shape-ids (cfh/get-children-ids-with-self (:objects page) (:id shape)) + + repair-shape + (fn [shape] + ; Detach the shape and convert it to non instance. + (log/debug :hint " -> detach shape" :shape-id (:id shape)) + (ctk/detach-shape shape))] + ; There is no solution that may recover it with confidence + ;; (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.") + ;; shape)] + + (log/dbg :hint "repairing shape :component-not-found" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes shape-ids repair-shape)))) + +(defmethod repair-error :invalid-main-instance-id + [_ {:keys [shape page-id] :as error} file-data _] + (let [component (ctkl/get-component file-data (:component-id shape)) + + repair-component + (fn [component] + ; Assign main instance in the component to current shape + (log/debug :hint " -> assign main-instance-id" :component-id (:id component)) + (assoc component :main-instance-id (:id shape))) + + detach-shape + (fn [shape] + (log/debug :hint " -> detach shape" :shape-id (:id shape)) + (ctk/detach-shape shape))] + + (log/dbg :hint "repairing shape :invalid-main-instance-id" :id (:id shape) :name (:name shape) :page-id page-id) + (if (and (some? component) (not (:deleted component))) + (-> (pcb/empty-changes nil page-id) + (pcb/with-library-data file-data) + (pcb/update-component (:component-id shape) repair-component)) + + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] detach-shape))))) + +(defmethod repair-error :invalid-main-instance-page + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-component + (fn [component] + ; Assign main instance in the component to current shape + (log/debug :hint " -> assign main-instance-page" :component-id (:id component)) + (assoc component :main-instance-page page-id))] + (log/dbg :hint "repairing shape :invalid-main-instance-page" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-library-data file-data) + (pcb/update-component (:component-id shape) repair-component)))) + +(defmethod repair-error :invalid-main-instance + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; There is no solution that may recover it with confidence + (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.") + shape)] + + (log/dbg :hint "repairing shape :invalid-main-instance" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :component-main + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Unset the :shape as main instance root + (log/debug :hint " -> unset :main-instance") + (dissoc shape :main-instance))] + + (log/dbg :hint "repairing shape :component-main" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :should-be-component-root + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Convert the shape in a top copy root. + (log/debug :hint " -> set :component-root") + (assoc shape :component-root true))] + + (log/dbg :hint "repairing shape :should-be-component-root" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :should-not-be-component-root + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Convert the shape in a nested copy root. + (log/debug :hint " -> unset :component-root") + (dissoc shape :component-root))] + + (log/dbg :hint "repairing shape :should-not-be-component-root" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :ref-shape-not-found + [_ {:keys [shape page-id] :as error} file-data libraries] + (let [matching-shape (let [page (ctpl/get-page file-data page-id) + root-shape (ctn/get-component-shape (:objects page) shape) + component-file (if (= (:component-file root-shape) (:id file-data)) + file-data + (-> (get libraries (:component-file root-shape)) :data)) + component (when component-file + (ctkl/get-component component-file (:component-id root-shape) true)) + component-shapes (ctf/get-component-shapes file-data component)] + + ;; Check if the shape points to the remote main. If so, reassign to the near main. + (if-let [near-shape-1 (d/seek #(= (:shape-ref %) (:shape-ref shape)) component-shapes)] + near-shape-1 + ;; Check if it points to any random shape in the page. If so, try to find a matchng + ;; shape in the near main component. + (when-let [random-shape (ctn/get-shape page (:shape-ref shape))] + (if-let [near-shape-2 (d/seek #(= (:id %) (:shape-ref random-shape)) component-shapes)] + near-shape-2 + ;; If not, check if it's a fostered copy and find a direct main. + (let [head-shape (ctn/get-head-shape (:objects page) shape) + component-file (if (= (:component-file head-shape) (:id file-data)) + file-data + (-> (get libraries (:component-file head-shape)) :data)) + component (when component-file + (ctkl/get-component component-file (:component-id head-shape) true)) + component-shapes (ctf/get-component-shapes file-data component)] + (if-let [near-shape-3 (d/seek #(= (:id %) (:shape-ref random-shape)) component-shapes)] + near-shape-3 + nil)))))) + reassign-shape + (fn [shape] + (log/debug :hint " -> reassign shape-ref to" :shape-ref (:id matching-shape)) + (assoc shape :shape-ref (:id matching-shape))) + + detach-shape + (fn [shape] + (log/debug :hint " -> detach shape" :shape-id (:id shape)) + (ctk/detach-shape shape))] + + ; If the shape still refers to the remote component, try to find the corresponding near one + ; and link to it. If not, detach the shape. + (log/dbg :hint "repairing shape :ref-shape-not-found" :id (:id shape) :name (:name shape) :page-id page-id) + (if (some? matching-shape) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] reassign-shape)) + (let [page (ctpl/get-page file-data page-id) + shape-ids (cfh/get-children-ids-with-self (:objects page) (:id shape))] + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes shape-ids detach-shape)))))) + +(defmethod repair-error :shape-ref-in-main + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Remove shape-ref + (log/debug :hint " -> unset :shape-ref") + (dissoc shape :shape-ref))] + + (log/dbg :hint "repairing shape :shape-ref-in-main" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :root-main-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Convert the shape in a nested main head. + (log/debug :hint " -> unset :component-root") + (dissoc shape :component-root))] + + (log/dbg :hint "repairing shape :root-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :nested-main-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Convert the shape in a top main head. + (log/debug :hint " -> set :component-root") + (assoc shape :component-root true))] + + (log/dbg :hint "repairing shape :nested-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape) + (pcb/change-parent uuid/zero [shape] nil {:component-swap true})))) + +(defmethod repair-error :root-copy-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Convert the shape in a nested copy head. + (log/debug :hint " -> unset :component-root") + (dissoc shape :component-root))] + + (log/dbg :hint "repairing shape :root-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :nested-copy-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Convert the shape in a top copy root. + (log/debug :hint " -> set :component-root") + (assoc shape :component-root true))] + + (log/dbg :hint "repairing shape :nested-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :not-head-main-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Detach the shape and convert it to non instance. + (log/debug :hint " -> detach shape" :shape-id (:id shape)) + (ctk/detach-shape shape))] + + (log/dbg :hint "repairing shape :not-head-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :not-head-copy-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Detach the shape and convert it to non instance. + (log/debug :hint " -> detach shape" :shape-id (:id shape)) + (ctk/detach-shape shape))] + + (log/dbg :hint "repairing shape :not-head-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :not-component-not-allowed + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; There is no solution that may recover it with confidence + (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.") + shape)] + + (log/dbg :hint "repairing shape :not-component-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :instance-head-not-frame + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Convert the shape in a frame. + (log/debug :hint " -> set :type :frame") + (assoc shape :type :frame + :fills [] + :hide-in-viewer true + :rx 0 + :ry 0))] + + (log/dbg :hint "repairing shape :instance-head-not-frame" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :component-nil-objects-not-allowed + [_ {:keys [shape] :as error} file-data _] + (let [repair-component + (fn [component] + ; Remove the objects key, or set it to {} if the component is deleted + (if (:deleted component) + (do + (log/debug :hint " -> set :objects {}") + (assoc component :objects {})) + (do + (log/debug :hint " -> remove :objects") + (dissoc component :objects))))] + + (log/dbg :hint "repairing component :component-nil-objects-not-allowed" :id (:id shape) :name (:name shape)) + (-> (pcb/empty-changes nil) + (pcb/with-library-data file-data) + (pcb/update-component (:id shape) repair-component)))) + +(defmethod repair-error :default + [_ error file _] + (log/error :hint "Unknown error code, don't know how to repair" :code (:code error)) + file) + +(defn repair-file + [{:keys [data id] :as file} libraries errors] + (log/dbg :hint "repairing file" :id (str id) :errors (count errors)) + (let [{:keys [redo-changes]} + (reduce (fn [changes error] + (pcb/concat-changes changes + (repair-error (:code error) + error + data + libraries))) + (pcb/empty-changes nil) + errors)] + redo-changes)) diff --git a/common/src/app/common/files/shapes_helpers.cljc b/common/src/app/common/files/shapes_helpers.cljc new file mode 100644 index 0000000000..e07e0631af --- /dev/null +++ b/common/src/app/common/files/shapes_helpers.cljc @@ -0,0 +1,210 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.files.shapes-helpers + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] + [app.common.geom.shapes :as gsh] + [app.common.types.shape :as cts] + [app.common.types.shape.layout :as ctl] + [app.common.uuid :as uuid])) + +(defn prepare-add-shape + [changes shape objects] + (let [index (:index (meta shape)) + id (:id shape) + + mod? (:mod? (meta shape)) + [row column :as cell] (when-not mod? (:cell (meta shape))) + + changes (-> changes + (pcb/with-objects objects) + (cond-> (some? index) + (pcb/add-object shape {:index index})) + (cond-> (nil? index) + (pcb/add-object shape)) + (cond-> (some? (:parent-id shape)) + (pcb/change-parent (:parent-id shape) [shape] index)) + (cond-> (some? cell) + (pcb/update-shapes [(:parent-id shape)] #(ctl/push-into-cell % [id] row column))) + (cond-> (ctl/grid-layout? objects (:parent-id shape)) + (pcb/update-shapes [(:parent-id shape)] ctl/assign-cells {:with-objects? true})))] + [shape changes])) + +(defn prepare-move-shapes-into-frame + [changes frame-id shapes objects] + (let [parent-id (dm/get-in objects [frame-id :parent-id]) + shapes (remove #(= % parent-id) shapes) + to-move (->> shapes + (map (d/getf objects)) + (not-empty))] + (if to-move + (-> changes + (cond-> (not (ctl/any-layout? objects frame-id)) + (pcb/update-shapes shapes ctl/remove-layout-item-data)) + (pcb/update-shapes shapes #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true))) + (pcb/change-parent frame-id to-move 0) + (cond-> (ctl/grid-layout? objects frame-id) + (-> (pcb/update-shapes [frame-id] ctl/assign-cells {:with-objects? true}) + (pcb/reorder-grid-children [frame-id])))) + changes))) + +(defn prepare-create-artboard-from-selection + ([changes id parent-id objects selected index frame-name without-fill?] + (prepare-create-artboard-from-selection + changes id parent-id objects selected index frame-name without-fill? nil)) + + ([changes id parent-id objects selected index frame-name without-fill? target-cell-id] + (when-let [selected-objs (->> selected + (map (d/getf objects)) + (not-empty))] + + (let [;; We calculate here the ordered selection because it is used + ;; multiple times and this avoid the need of creating the index + ;; manytimes for single operation. + selected' (cfh/order-by-indexed-shapes objects selected) + new-index (or index + (->> (first selected') + (cfh/get-position-on-parent objects) + (inc))) + + srect (gsh/shapes->rect selected-objs) + selected-id (first selected) + selected-obj (get objects selected-id) + + frame-id (get selected-obj :frame-id) + parent-id (or parent-id (get selected-obj :parent-id)) + base-parent (get objects parent-id) + + layout-props + (when (and (= 1 (count selected)) + (ctl/any-layout? base-parent)) + (select-keys selected-obj ctl/layout-item-props)) + + target-cell-id + (if (and (nil? target-cell-id) + (ctl/grid-layout? objects parent-id)) + ;; Find the top-left grid cell of the selected elements + (let [ncols (count (:layout-grid-columns base-parent))] + (->> selected + (map #(ctl/get-cell-by-shape-id base-parent %)) + (apply min-key (fn [{:keys [row column]}] (+ (* ncols row) column))) + :id)) + target-cell-id) + + attrs + {:type :frame + :x (:x srect) + :y (:y srect) + :width (:width srect) + :height (:height srect)} + + shape + (cts/setup-shape + (cond-> attrs + (some? id) + (assoc :id id) + + (some? frame-name) + (assoc :name frame-name) + + :always + (assoc :frame-id frame-id + :parent-id parent-id + :shapes (into [] selected)) + + (some? layout-props) + (d/patch-object layout-props) + + ;; Frames from shapes will not be displayed in viewer and no clipped + (or (not= frame-id uuid/zero) without-fill?) + (assoc :fills [] :hide-in-viewer true :show-content true))) + + shape + (with-meta shape {:index new-index}) + + [shape changes] + (prepare-add-shape changes shape objects) + + changes + (prepare-move-shapes-into-frame changes (:id shape) selected' objects) + + changes + (cond-> changes + (ctl/grid-layout? objects (:parent-id shape)) + (-> (pcb/update-shapes + [(:parent-id shape)] + (fn [parent objects] + ;; This restores the grid layout before adding and moving the shapes + ;; this is done because the add+move could have altered the layout and we + ;; want to do it after both operations are completed. Also here we could + ;; asign the new element to a target-cell + (-> parent + (assoc :layout-grid-cells (:layout-grid-cells base-parent)) + (assoc :layout-grid-rows (:layout-grid-rows base-parent)) + (assoc :layout-grid-columns (:layout-grid-columns base-parent)) + + (cond-> (some? target-cell-id) + (assoc-in [:layout-grid-cells target-cell-id :shapes] [(:id shape)])) + (ctl/assign-cells objects))) + {:with-objects? true}) + + (pcb/reorder-grid-children [(:parent-id shape)])))] + + [shape changes])))) + + +(defn prepare-create-empty-artboard + [changes frame-id parent-id objects index frame-name without-fill? target-cell-id] + + (let [base-parent (get objects parent-id) + + attrs {:type :frame + :x 0 + :y 0 + :width 0.01 + :height 0.01} + + shape (cts/setup-shape + (cond-> attrs + (some? frame-id) + (assoc :id frame-id) + + (some? frame-name) + (assoc :name frame-name) + + :always + (assoc :frame-id frame-id + :parent-id parent-id + :shapes []) + + :always + (with-meta {:index index}) + + (or (not= frame-id uuid/zero) without-fill?) + (assoc :fills [] :hide-in-viewer true))) + + [shape changes] + (prepare-add-shape changes shape objects) + + changes + (cond-> changes + (ctl/grid-layout? objects (:parent-id shape)) + (-> (cond-> (some? target-cell-id) + (pcb/update-shapes + [(:parent-id shape)] + (fn [parent] + (-> parent + (assoc :layout-grid-cells (:layout-grid-cells base-parent)) + (assoc-in [:layout-grid-cells target-cell-id :shapes] [frame-id]) + (assoc :position :auto))))) + (pcb/update-shapes [(:parent-id shape)] ctl/assign-cells {:with-objects? true}) + (pcb/reorder-grid-children [(:parent-id shape)])))] + + [shape changes])) diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc new file mode 100644 index 0000000000..17c0f130ce --- /dev/null +++ b/common/src/app/common/files/validate.cljc @@ -0,0 +1,519 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.files.validate + (:require + [app.common.data.macros :as dm] + [app.common.exceptions :as ex] + [app.common.files.helpers :as cfh] + [app.common.schema :as sm] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] + [app.common.types.shape-tree :as ctst] + [app.common.uuid :as uuid] + [cuerdas.core :as str])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SCHEMA +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def error-codes + #{:invalid-geometry + :parent-not-found + :child-not-in-parent + :duplicated-children + :child-not-found + :frame-not-found + :invalid-frame + :component-not-main + :component-main-external + :component-not-found + :invalid-main-instance-id + :invalid-main-instance-page + :invalid-main-instance + :invalid-parent + :component-main + :should-be-component-root + :should-not-be-component-root + :ref-shape-not-found + :shape-ref-in-main + :root-main-not-allowed + :nested-main-not-allowed + :root-copy-not-allowed + :nested-copy-not-allowed + :not-head-main-not-allowed + :not-head-copy-not-allowed + :not-component-not-allowed + :component-nil-objects-not-allowed + :instance-head-not-frame}) + +(def ^:private + schema:error + (sm/define + [:map {:title "ValidationError"} + [:code {:optional false} [::sm/one-of error-codes]] + [:hint {:optional false} :string] + [:shape {:optional true} :map] ; Cannot validate a shape because here it may be broken + [:shape-id {:optional true} ::sm/uuid] + [:file-id ::sm/uuid] + [:page-id ::sm/uuid]])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; ERROR HANDLING +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:dynamic ^:private *errors* nil) + +(defn- library-exists? + [file libraries shape] + (or (= (:component-file shape) (:id file)) + (contains? libraries (:component-file shape)))) + +(defn- report-error + [code hint shape file page & {:as args}] + (let [error {:code code + :hint hint + :shape shape + :file-id (:id file) + :page-id (:id page) + :shape-id (:id shape) + :args args}] + + (dm/assert! + "expected a valid `*errors*` dynamic binding" + (some? *errors*)) + + (dm/assert! + "expected valid error" + (sm/check! schema:error error)) + + (vswap! *errors* conj error))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; PRIVATE API: VALIDATION FUNCTIONS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(declare check-shape) + +(defn- check-geometry + "Validate that the shape has valid coordinates, selrect and points." + [shape file page] + (when (and (not (or (cfh/path-shape? shape) + (cfh/bool-shape? shape))) + (or (nil? (:x shape)) ; This may occur in root shape (uuid/zero) in old files + (nil? (:y shape)) + (nil? (:width shape)) + (nil? (:height shape)) + (nil? (:selrect shape)) + (nil? (:points shape)))) + (report-error :invalid-geometry + "Shape geometry is invalid" + shape file page))) + +(defn- check-parent-children + "Validate parent and children exists, and the link is bidirectional." + [shape file page] + (let [parent (ctst/get-shape page (:parent-id shape)) + shape-id (:id shape) + shapes (:shapes shape)] + + (if (nil? parent) + (report-error :parent-not-found + (str/ffmt "Parent % not found" (:parent-id shape)) + shape file page) + (do + (when-not (cfh/root? shape) + (when-not (some #(= shape-id %) (:shapes parent)) + (report-error :child-not-in-parent + (str/ffmt "Shape % not in parent's children list" shape-id) + shape file page))) + + (when-not (= (count shapes) (count (distinct shapes))) + (report-error :duplicated-children + (str/ffmt "Shape % has duplicated children" shape-id) + shape file page)) + + (doseq [child-id shapes] + (let [child (ctst/get-shape page child-id)] + (if (nil? child) + (report-error :child-not-found + (str/ffmt "Child % not found in parent %" child-id shape-id) + shape file page + :parent-id shape-id + :child-id child-id) + (when (not= (:parent-id child) shape-id) + (report-error :invalid-parent + (str/ffmt "Child % has invalid parent %" child-id shape-id) + child file page + :parent-id shape-id))))))))) + +(defn- check-frame + "Validate that the frame-id shape exists and is indeed a frame. Also + it must point to the parent shape (if this is a frame) or to the + frame-id of the parent (if not)." + [{:keys [frame-id] :as shape} file page] + (let [frame (ctst/get-shape page frame-id)] + (if (nil? frame) + (report-error :frame-not-found + (str/ffmt "Frame % not found" frame-id) + shape file page) + (if (not= (:type frame) :frame) + (report-error :invalid-frame + (str/ffmt "Frame % is not actually a frame" frame-id) + shape file page) + (let [parent (ctst/get-shape page (:parent-id shape))] + (when (some? parent) + (if (= (:type parent) :frame) + (when-not (= frame-id (:id parent)) + (report-error :invalid-frame + (str/ffmt "Frame-id should point to parent %" (:id parent)) + shape file page)) + (when-not (= frame-id (:frame-id parent)) + (report-error :invalid-frame + (str/ffmt "Frame-id should point to parent frame %" frame-id) + shape file page))))))))) + +(defn- check-component-main-head + "Validate shape is a main instance head, component exists + and its main-instance points to this shape." + [shape file page libraries] + (when (nil? (:main-instance shape)) + (report-error :component-not-main + "Shape expected to be main instance" + shape file page)) + (when-not (= (:component-file shape) (:id file)) + (report-error :component-main-external + "Main instance should refer to a component in the same file" + shape file page)) + (let [component (ctf/resolve-component shape file libraries :include-deleted? true)] + (if (nil? component) + (report-error :component-not-found + (str/ffmt "Component % not found in file %" (:component-id shape) (:component-file shape)) + shape file page) + (do + (when-not (= (:main-instance-id component) (:id shape)) + (report-error :invalid-main-instance-id + (str/ffmt "Main instance id of component % is not valid" (:component-id shape)) + shape file page)) + (when-not (= (:main-instance-page component) (:id page)) + (let [component-page (ctf/get-component-page (:data file) component) + main-component (ctst/get-shape component-page (:main-instance-id component))] + ;; We must check if the same component has main instances in different pages. + ;; In that case one of those instances shouldn't be main + (if (:main-instance main-component) + (report-error :component-main + "Shape not expected to be main instance" + shape file page) + (report-error :invalid-main-instance-page + (str/ffmt "Main instance page of component % is not valid" (:component-id shape)) + shape file page)))))))) + +(defn- check-component-not-main-head + "Validate shape is a not-main instance head, component + exists and its main-instance does not point to this + shape." + [shape file page libraries] + (when (true? (:main-instance shape)) + (report-error :component-not-main + "Shape not expected to be main instance" + shape file page)) + + (let [library-exists (library-exists? file libraries shape) + component (when library-exists + (ctf/resolve-component shape file libraries {:include-deleted? true}))] + (if (nil? component) + (when library-exists + (report-error :component-not-found + (str/ffmt "Component % not found in file %" (:component-id shape) (:component-file shape)) + shape file page)) + (when (and (= (:main-instance-id component) (:id shape)) + (= (:main-instance-page component) (:id page))) + (report-error :invalid-main-instance + (str/ffmt "Main instance of component % should not be this shape" (:id component)) + shape file page))))) + +(defn- check-component-not-main-not-head + "Validate that this shape is not main instance and not head." + [shape file page] + (when (true? (:main-instance shape)) + (report-error :component-main + "Shape not expected to be main instance" + shape file page)) + (when (or (some? (:component-id shape)) + (some? (:component-file shape))) + (report-error :component-main + "Shape not expected to be component head" + shape file page))) + +(defn- check-component-root + "Validate that this shape is an instance root." + [shape file page] + (when (nil? (:component-root shape)) + (report-error :should-be-component-root + "Shape should be component root" + shape file page))) + +(defn- check-component-not-root + "Validate that this shape is not an instance root." + [shape file page] + (when (true? (:component-root shape)) + (report-error :should-not-be-component-root + "Shape should not be component root" + shape file page))) + +(defn- check-component-ref + "Validate that the referenced shape exists in the near component." + [shape file page libraries] + (let [library-exists (library-exists? file libraries shape) + ref-shape (when library-exists + (ctf/find-ref-shape file page libraries shape :include-deleted? true))] + (when (and library-exists (nil? ref-shape)) + (report-error :ref-shape-not-found + (str/ffmt "Referenced shape % not found in near component" (:shape-ref shape)) + shape file page)))) + +(defn- check-component-not-ref + "Validate that this shape does not reference other one." + [shape file page] + (when (some? (:shape-ref shape)) + (report-error :shape-ref-in-main + "Shape inside main instance should not have shape-ref" + shape file page))) + +(defn- check-shape-main-root-top + "Root shape of a top main instance: + + - :main-instance + - :component-id + - :component-file + - :component-root" + [shape file page libraries] + (check-component-main-head shape file page libraries) + (check-component-root shape file page) + (check-component-not-ref shape file page) + (run! #(check-shape % file page libraries :context :main-top) (:shapes shape))) + +(defn- check-shape-main-root-nested + "Root shape of a nested main instance + - :main-instance + - :component-id + - :component-file" + [shape file page libraries] + (check-component-main-head shape file page libraries) + (check-component-not-root shape file page) + (check-component-not-ref shape file page) + (run! #(check-shape % file page libraries :context :main-nested) (:shapes shape))) + +(defn- check-shape-copy-root-top + "Root shape of a top copy instance + - :component-id + - :component-file + - :component-root + - :shape-ref" + [shape file page libraries] + ;; We propagate have to propagate to nested shapes if library is valid or not + (let [library-exists (library-exists? file libraries shape)] + (check-component-not-main-head shape file page libraries) + (check-component-root shape file page) + (check-component-ref shape file page libraries) + (run! #(check-shape % file page libraries :context :copy-top :library-exists library-exists) (:shapes shape)))) + +(defn- check-shape-copy-root-nested + "Root shape of a nested copy instance + - :component-id + - :component-file + - :shape-ref" + [shape file page libraries library-exists] + (check-component-not-main-head shape file page libraries) + (check-component-not-root shape file page) + ;; We can have situations where the nested copy and the ancestor copy come from different libraries and some of them have been dettached + ;; so we only validate the shape-ref if the ancestor is from a valid library + (when library-exists + (check-component-ref shape file page libraries)) + (run! #(check-shape % file page libraries :context :copy-nested) (:shapes shape))) + +(defn- check-shape-main-not-root + "Not-root shape of a main instance (not any attribute)" + [shape file page libraries] + (check-component-not-main-not-head shape file page) + (check-component-not-root shape file page) + (check-component-not-ref shape file page) + (run! #(check-shape % file page libraries :context :main-any) (:shapes shape))) + +(defn- check-shape-copy-not-root + "Not-root shape of a copy instance :shape-ref" + [shape file page libraries] + (check-component-not-main-not-head shape file page) + (check-component-not-root shape file page) + (check-component-ref shape file page libraries) + (run! #(check-shape % file page libraries :context :copy-any) (:shapes shape))) + +(defn- check-shape-not-component + "Shape is not in a component or is a fostered children (not any + attribute)" + [shape file page libraries] + (check-component-not-main-not-head shape file page) + (check-component-not-root shape file page) + (check-component-not-ref shape file page) + (run! #(check-shape % file page libraries :context :not-component) (:shapes shape))) + +(defn- check-shape + "Validate referential integrity and semantic coherence of + a shape and all its children. Report all errors found. + + The context is the situation of the parent in respect to components: + - :not-component + - :main-top + - :main-nested + - :copy-top + - :copy-nested + - :main-any + - :copy-any + " + [shape-id file page libraries & {:keys [context library-exists] :or {context :not-component library-exists false}}] + (let [shape (ctst/get-shape page shape-id)] + (when (some? shape) + (check-geometry shape file page) + (check-parent-children shape file page) + (check-frame shape file page) + + (if (ctk/instance-head? shape) + (if (not= :frame (:type shape)) + (report-error :instance-head-not-frame + "Instance head should be a frame" + shape file page) + + (if (ctk/instance-root? shape) + (if (ctk/main-instance? shape) + (if (not= context :not-component) + (report-error :root-main-not-allowed + "Root main component not allowed inside other component" + shape file page) + (check-shape-main-root-top shape file page libraries)) + + (if (not= context :not-component) + (report-error :root-copy-not-allowed + "Root copy component not allowed inside other component" + shape file page) + (check-shape-copy-root-top shape file page libraries))) + + (if (ctk/main-instance? shape) + ;; mains can't be nested into mains + (if (or (= context :not-component) (= context :main-top)) + (report-error :nested-main-not-allowed + "Nested main component only allowed inside other component" + shape file page) + (check-shape-main-root-nested shape file page libraries)) + + (if (= context :not-component) + (report-error :nested-copy-not-allowed + "Nested copy component only allowed inside other component" + shape file page) + (check-shape-copy-root-nested shape file page libraries library-exists))))) + + (if (ctk/in-component-copy? shape) + (if-not (#{:copy-top :copy-nested :copy-any} context) + (report-error :not-head-copy-not-allowed + "Non-root copy only allowed inside a copy" + shape file page) + (check-shape-copy-not-root shape file page libraries)) + + (if (ctn/inside-component-main? (:objects page) shape) + (if-not (#{:main-top :main-nested :main-any} context) + (report-error :not-head-main-not-allowed + "Non-root main only allowed inside a main component" + shape file page) + (check-shape-main-not-root shape file page libraries)) + + (if (#{:main-top :main-nested :main-any} context) + (report-error :not-component-not-allowed + "Not compoments are not allowed inside a main" + shape file page) + (check-shape-not-component shape file page libraries)))))))) + +(defn- check-component + "Validate semantic coherence of a component. Report all errors found." + [component file] + (when (and (contains? component :objects) (nil? (:objects component))) + (report-error :component-nil-objects-not-allowed + "Objects list cannot be nil" + component file nil))) + +(defn- get-orphan-shapes + [{:keys [objects] :as page}] + (let [xf (comp (map #(contains? objects (:parent-id %))) + (map :id))] + (into [] xf (vals objects)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; PUBLIC API: VALIDATION FUNCTIONS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn validate-file + "Validate full referential integrity and semantic coherence on file data. + + Return a list of errors or `nil`" + [{:keys [data features] :as file} libraries] + (when (contains? features "components/v2") + (binding [*errors* (volatile! [])] + + (doseq [page (filter :id (ctpl/pages-seq data))] + (check-shape uuid/zero file page libraries) + (->> (get-orphan-shapes page) + (run! #(check-shape % file page libraries)))) + + (->> (vals (:components data)) + (run! #(check-component % file))) + + (-> *errors* deref not-empty)))) + +(defn validate-shape + "Validate a shape and all its children. Returns a list of errors." + [shape-id file page libraries] + (binding [*errors* (volatile! [])] + (check-shape shape-id file page libraries) + (deref *errors*))) + +(defn validate-component + "Validate a component. Returns a list of errors." + [component file] + (binding [*errors* (volatile! [])] + (check-component component file) + (deref *errors*))) + +(def ^:private valid-fdata? + "Structural validation of file data using defined schema" + (sm/lazy-validator ::ctf/data)) + +(def ^:private get-fdata-explain + "Get schema explain data for file data" + (sm/lazy-explainer ::ctf/data)) + +(defn validate-file-schema! + "Validates the file itself, without external dependencies, it + performs the schema checking and some semantical validation of the + content." + [{:keys [id data] :as file}] + (when-not (valid-fdata? data) + (ex/raise :type :validation + :code :schema-validation + :hint (str/ffmt "invalid file data structure found on file '%'" id) + :file-id id + ::sm/explain (get-fdata-explain data)))) + +(defn validate-file! + "Validate full referential integrity and semantic coherence on file data. + + Raises an exception" + [file libraries] + (when-let [errors (validate-file file libraries)] + (ex/raise :type :validation + :code :referential-integrity + :hint "error on validating file referential integrity" + :file-id (:id file) + :details errors))) diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index bda2433208..93b88f87eb 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -33,5 +33,3 @@ :else (recur (rest flags) result))))))) - - diff --git a/common/src/app/common/fressian.clj b/common/src/app/common/fressian.clj index f42f9130ea..282620698a 100644 --- a/common/src/app/common/fressian.clj +++ b/common/src/app/common/fressian.clj @@ -7,12 +7,8 @@ (ns app.common.fressian (:require [app.common.data :as d] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] [clojure.data.fressian :as fres]) (:import - app.common.geom.matrix.Matrix - app.common.geom.point.Point clojure.lang.Ratio java.io.ByteArrayInputStream java.io.ByteArrayOutputStream @@ -297,39 +293,3 @@ [data] (with-open [^ByteArrayInputStream input (ByteArrayInputStream. ^bytes data)] (-> input reader read!))) - -;; --- ADDITIONAL - -(add-handlers! - {:name "penpot/point" - :class app.common.geom.point.Point - :wfn (fn [n w ^Point o] - (write-tag! w n 1) - (write-list! w (List/of (.-x o) (.-y o)))) - :rfn (fn [^Reader rdr] - (let [^List x (read-object! rdr)] - (Point. (.get x 0) (.get x 1))))} - - {:name "penpot/matrix" - :class app.common.geom.matrix.Matrix - :wfn (fn [^String n ^Writer w o] - (write-tag! w n 1) - (write-list! w (List/of (.-a ^Matrix o) - (.-b ^Matrix o) - (.-c ^Matrix o) - (.-d ^Matrix o) - (.-e ^Matrix o) - (.-f ^Matrix o)))) - :rfn (fn [^Reader rdr] - (let [^List x (read-object! rdr)] - (Matrix. (.get x 0) (.get x 1) (.get x 2) (.get x 3) (.get x 4) (.get x 5))))}) - - -;; Backward compatibility for 1.19 with v1.20; - -(add-handlers! - {:name "penpot/geom/rect" - :rfn read-map-like} - {:name "penpot/shape" - :rfn read-map-like}) - diff --git a/common/src/app/common/geom/align.cljc b/common/src/app/common/geom/align.cljc index cc17003402..e7b8bcc518 100644 --- a/common/src/app/common/geom/align.cljc +++ b/common/src/app/common/geom/align.cljc @@ -6,7 +6,10 @@ (ns app.common.geom.align (:require - [app.common.geom.shapes :as gsh])) + [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] + [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.points :as gpo])) ;; --- Alignment @@ -22,10 +25,27 @@ the shape with the given rectangle. If the shape is a group, move also all of its recursive children." [shape rect axis] - (let [wrapper-rect (gsh/selection-rect [shape]) + (let [wrapper-rect (gsh/shapes->rect [shape]) align-pos (calc-align-pos wrapper-rect rect axis) - delta {:x (- (:x align-pos) (:x wrapper-rect)) - :y (- (:y align-pos) (:y wrapper-rect))}] + delta (gpt/point (- (:x align-pos) (:x wrapper-rect)) + (- (:y align-pos) (:y wrapper-rect)))] + (gsh/move shape delta))) + +(defn align-to-parent + "Does the same calc as align-to-rect but relative to a parent shape." + [shape parent axis] + (let [parent-bounds (:points parent) + wrapper-rect + (-> (gsh/transform-points (:points shape) (gsh/shape->center parent) (:transform-inverse parent)) + (grc/points->rect)) + + align-pos (calc-align-pos wrapper-rect (:selrect parent) axis) + + xv #(gpo/start-hv parent-bounds %) + yv #(gpo/start-vv parent-bounds %) + + delta (-> (xv (- (:x align-pos) (:x wrapper-rect))) + (gpt/add (yv (- (:y align-pos) (:y wrapper-rect)))))] (gsh/move shape delta))) (defn calc-align-pos @@ -44,8 +64,8 @@ :y (:y wrapper-rect)}) :vtop (let [top (:y rect)] - {:x (:x wrapper-rect) - :y top}) + {:x (:x wrapper-rect) + :y top}) :vcenter (let [center (+ (:y rect) (/ (:height rect) 2))] {:x (:x wrapper-rect) @@ -70,32 +90,34 @@ other-coord (if (= axis :horizontal) :y :x) size (if (= axis :horizontal) :width :height) ;; The rectangle that wraps the whole selection - wrapper-rect (gsh/selection-rect shapes) + wrapper-rect (gsh/shapes->rect shapes) ;; Sort shapes by the center point in the given axis - sorted-shapes (sort-by #(coord (gsh/center-shape %)) shapes) + sorted-shapes (sort-by #(coord (gsh/shape->center %)) shapes) ;; Each shape wrapped in its own rectangle - wrapped-shapes (map #(gsh/selection-rect [%]) sorted-shapes) + wrapped-shapes (map #(gsh/shapes->rect [%]) sorted-shapes) ;; The total space between shapes space (reduce - (size wrapper-rect) (map size wrapped-shapes)) unit-space (/ space (- (count wrapped-shapes) 1)) + ;; Calculate the distance we need to move each shape. ;; The new position of each one is the position of the ;; previous one plus its size plus the unit space. - deltas (loop [shapes' wrapped-shapes - start-pos (coord wrapper-rect) - deltas []] + deltas + (loop [shapes' wrapped-shapes + start-pos (coord wrapper-rect) + deltas []] - (let [first-shape (first shapes') - delta (- start-pos (coord first-shape)) - new-pos (+ start-pos (size first-shape) unit-space)] + (let [first-shape (first shapes') + delta (- start-pos (coord first-shape)) + new-pos (+ start-pos (size first-shape) unit-space)] - (if (= (count shapes') 1) - (conj deltas delta) - (recur (rest shapes') - new-pos - (conj deltas delta)))))] + (if (= (count shapes') 1) + (conj deltas delta) + (recur (rest shapes') + new-pos + (conj deltas delta)))))] - (map #(gsh/move %1 {coord %2 other-coord 0}) + (map #(gsh/move %1 (assoc (gpt/point) coord %2 other-coord 0)) sorted-shapes deltas))) ;; Adjust to viewport @@ -103,28 +125,32 @@ (defn adjust-to-viewport ([viewport srect] (adjust-to-viewport viewport srect nil)) ([viewport srect {:keys [padding] :or {padding 0}}] - (let [gprop (/ (:width viewport) (:height viewport)) - srect (-> srect - (update :x #(- % padding)) - (update :y #(- % padding)) - (update :width #(+ % padding padding)) - (update :height #(+ % padding padding))) - width (:width srect) + (let [gprop (/ (:width viewport) + (:height viewport)) + srect (-> srect + (update :x #(- % padding)) + (update :y #(- % padding)) + (update :width #(+ % padding padding)) + (update :height #(+ % padding padding))) + width (:width srect) height (:height srect) - lprop (/ width height)] + lprop (/ width height)] (cond - (> gprop lprop) - (let [width' (* (/ width lprop) gprop) - padding (/ (- width' width) 2)] - (-> srect - (update :x #(- % padding)) - (assoc :width width'))) + (> gprop lprop) + (let [width' (* (/ width lprop) gprop) + padding (/ (- width' width) 2)] + (-> srect + (update :x #(- % padding)) + (assoc :width width') + (grc/update-rect :position))) - (< gprop lprop) - (let [height' (/ (* height lprop) gprop) - padding (/ (- height' height) 2)] - (-> srect - (update :y #(- % padding)) - (assoc :height height'))) + (< gprop lprop) + (let [height' (/ (* height lprop) gprop) + padding (/ (- height' height) 2)] + (-> srect + (update :y #(- % padding)) + (assoc :height height') + (grc/update-rect :position))) - :else srect)))) + :else + (grc/update-rect srect :position))))) diff --git a/common/src/app/common/geom/bounds_map.cljc b/common/src/app/common/geom/bounds_map.cljc new file mode 100644 index 0000000000..55230c9c0b --- /dev/null +++ b/common/src/app/common/geom/bounds_map.cljc @@ -0,0 +1,133 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.bounds-map + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.points :as gpo] + [app.common.geom.shapes.transforms :as gtr] + [app.common.math :as mth] + [app.common.types.modifiers :as ctm] + [app.common.uuid :as uuid])) + +(defn objects->bounds-map + [objects] + (d/lazy-map + (keys objects) + #(gco/shape->points (get objects %)))) + +(defn- create-bounds + "Create the bounds object for the current shape in this context" + ([shape bounds-map objects] + (create-bounds shape bounds-map objects nil nil)) + + ([shape bounds-map objects modif-tree] + (create-bounds shape bounds-map objects modif-tree nil)) + + ([{:keys [id] :as shape} bounds-map objects modif-tree current-ref] + (if (cfh/group-shape? shape) + (let [modifiers (dm/get-in modif-tree [id :modifiers]) + + children + (cond->> (cfh/get-immediate-children objects id) + (cfh/mask-shape? shape) + (take 1)) + shape-bounds (if current-ref @current-ref @(get bounds-map id)) + current-bounds + (cond-> shape-bounds + (not (ctm/empty? modifiers)) + (gtr/transform-bounds modifiers)) + + children-bounds + (->> children + (mapv #(deref (get bounds-map (:id %)))))] + (gpo/merge-parent-coords-bounds children-bounds current-bounds)) + + ;; Shape + (let [modifiers (dm/get-in modif-tree [id :modifiers]) + shape-bounds (if current-ref @current-ref @(get bounds-map id))] + (cond-> shape-bounds + (not (ctm/empty? modifiers)) + (gtr/transform-bounds modifiers)))))) + +#?(:clj + (defn- resolve-modif-tree-ids + [objects modif-tree] + ;; These are the new bounds calculated. Are the "modified" plus any groups they belong to + (let [ids (keys modif-tree)] + (into (set ids) + (mapcat #(->> (cfh/get-parent-ids-seq objects %) + (take-while (partial cfh/group-like-shape? objects)))) + ids))) + + :cljs + ;; More performant version using javascript mutable sets + (defn- resolve-modif-tree-ids + [objects modif-tree] + + (let [base-ids (keys modif-tree) + ids (js/Set. base-ids)] + (loop [base-ids (seq base-ids)] + (when (some? base-ids) + (let [cid (first base-ids)] + (loop [new-ids + (->> (cfh/get-parent-seq objects cid) + (take-while #(and (cfh/group-like-shape? %) + (not (.has ids %)))) + (seq))] + (when (some? new-ids) + (.add ids (first new-ids)) + (recur (next new-ids)))) + (recur (next base-ids))))) + ids))) + +(defn transform-bounds-map + ([bounds-map objects modif-tree] + (transform-bounds-map bounds-map objects modif-tree nil)) + ([bounds-map objects modif-tree ids] + ;; We use the volatile in order to solve the dependencies problem. We want the groups to reference the new + ;; bounds instead of the old ones. The current as last parameter is to fix a possible infinite loop + ;; with self-references + (let [bm-holder (volatile! nil) + + ids (or ids (resolve-modif-tree-ids objects modif-tree)) + + new-bounds-map + (loop [tr-bounds-map (transient bounds-map) + ids (seq ids)] + (if (not ids) + (persistent! tr-bounds-map) + (let [shape-id (first ids)] + (recur + (cond-> tr-bounds-map + (not= uuid/zero shape-id) + (assoc! shape-id + (delay (create-bounds (get objects shape-id) + @bm-holder + objects + modif-tree + (get bounds-map shape-id))))) + (next ids)))))] + (vreset! bm-holder new-bounds-map) + new-bounds-map))) + +;; Tool for debugging +(defn bounds-map + [objects bounds-map] + (letfn [(parse-bound [[id bounds*]] + (let [bounds (deref bounds*) + shape (get objects id)] + (when (and shape bounds) + [(:name shape) + {:x (mth/round (:x (gpo/origin bounds)) 2) + :y (mth/round (:y (gpo/origin bounds)) 2) + :width (mth/round (gpo/width-points bounds) 2) + :height (mth/round (gpo/height-points bounds) 2)}])))] + + (into {} (keep parse-bound) bounds-map))) diff --git a/frontend/src/app/util/geom/grid.cljs b/common/src/app/common/geom/grid.cljc similarity index 99% rename from frontend/src/app/util/geom/grid.cljs rename to common/src/app/common/geom/grid.cljc index dea9569ad8..ef66418f04 100644 --- a/frontend/src/app/util/geom/grid.cljs +++ b/common/src/app/common/geom/grid.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.util.geom.grid +(ns app.common.geom.grid (:require [app.common.data :as d] [app.common.geom.point :as gpt] diff --git a/common/src/app/common/geom/line.cljc b/common/src/app/common/geom/line.cljc new file mode 100644 index 0000000000..6ab28d5fc1 --- /dev/null +++ b/common/src/app/common/geom/line.cljc @@ -0,0 +1,18 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.line) + +(defn line-value + [[{px :x py :y} {vx :x vy :y}] {:keys [x y]}] + (let [a vy + b (- vx) + c (+ (* (- vy) px) (* vx py))] + (+ (* a x) (* b y) c))) + +(defn is-inside-lines? + [line-1 line-2 pos] + (< (* (line-value line-1 pos) (line-value line-2 pos)) 0)) diff --git a/common/src/app/common/geom/matrix.cljc b/common/src/app/common/geom/matrix.cljc index c298154b08..d435d861cc 100644 --- a/common/src/app/common/geom/matrix.cljc +++ b/common/src/app/common/geom/matrix.cljc @@ -6,36 +6,54 @@ (ns app.common.geom.matrix (:require + #?(:clj [app.common.fressian :as fres]) #?(:cljs [cljs.pprint :as pp] :clj [clojure.pprint :as pp]) [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.math :as mth] + [app.common.record :as cr] [app.common.schema :as sm] [app.common.schema.generators :as sg] [app.common.schema.openapi :as-alias oapi] [app.common.spec :as us] - [clojure.spec.alpha :as s])) + [app.common.transit :as t] + [clojure.spec.alpha :as s]) + #?(:clj + (:import + java.util.List))) + (def precision 6) ;; --- Matrix Impl -(defrecord Matrix [^double a - ^double b - ^double c - ^double d - ^double e - ^double f] +(cr/defrecord Matrix [^double a + ^double b + ^double c + ^double d + ^double e + ^double f] Object - (toString [_] + (toString [this] (dm/fmt "matrix(%, %, %, %, %, %)" - (mth/to-fixed a precision) - (mth/to-fixed b precision) - (mth/to-fixed c precision) - (mth/to-fixed d precision) - (mth/to-fixed e precision) - (mth/to-fixed f precision)))) + (mth/to-fixed (.-a this) precision) + (mth/to-fixed (.-b this) precision) + (mth/to-fixed (.-c this) precision) + (mth/to-fixed (.-d this) precision) + (mth/to-fixed (.-e this) precision) + (mth/to-fixed (.-f this) precision)))) + +(defn format-precision + [mtx precision] + (when mtx + (dm/fmt "matrix(%, %, %, %, %, %)" + (mth/to-fixed (.-a mtx) precision) + (mth/to-fixed (.-b mtx) precision) + (mth/to-fixed (.-c mtx) precision) + (mth/to-fixed (.-d mtx) precision) + (mth/to-fixed (.-e mtx) precision) + (mth/to-fixed (.-f mtx) precision)))) (defn matrix? "Return true if `v` is Matrix instance." @@ -45,11 +63,12 @@ (defn matrix "Create a new matrix instance." ([] - (Matrix. 1 0 0 1 0 0)) + (pos->Matrix 1 0 0 1 0 0)) ([a b c d e f] - (Matrix. a b c d e f))) + (pos->Matrix a b c d e f))) -(def number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?") +(def number-regex + #"[+-]?\d*(\.\d+)?([eE][+-]?\d+)?") (defn str->matrix [matrix-str] @@ -58,8 +77,8 @@ (map (comp d/parse-double first)))] (apply matrix params))) -(sm/def! ::matrix-map - [:map {:title "MatrixMap"} +(def ^:private schema:matrix-attrs + [:map {:title "MatrixAttrs"} [:a ::sm/safe-double] [:b ::sm/safe-double] [:c ::sm/safe-double] @@ -67,6 +86,10 @@ [:e ::sm/safe-double] [:f ::sm/safe-double]]) +(def valid-matrix? + (sm/lazy-validator + [:and [:fn matrix?] schema:matrix-attrs])) + (sm/def! ::matrix (letfn [(decode [o] (if (map? o) @@ -83,7 +106,7 @@ (dm/get-prop o :f) ","))] {:type ::matrix - :pred matrix? + :pred valid-matrix? :type-properties {:title "matrix" :description "Matrix instance" @@ -93,8 +116,8 @@ (sg/small-double) (sg/small-double) (sg/small-double) - (sg/small-double) ) - (sg/fmap #(apply ->Matrix %))) + (sg/small-double)) + (sg/fmap #(apply pos->Matrix %))) ::oapi/type "string" ::oapi/format "matrix" ::oapi/decode decode @@ -114,24 +137,54 @@ (s/def ::matrix (s/and ::matrix-attrs matrix?)) - (defn close? [^Matrix m1 ^Matrix m2] - (and (mth/close? (.-a m1) (.-a m2)) - (mth/close? (.-b m1) (.-b m2)) - (mth/close? (.-c m1) (.-c m2)) - (mth/close? (.-d m1) (.-d m2)) - (mth/close? (.-e m1) (.-e m2)) - (mth/close? (.-f m1) (.-f m2)))) + (and ^boolean (mth/close? (.-a m1) (.-a m2)) + ^boolean (mth/close? (.-b m1) (.-b m2)) + ^boolean (mth/close? (.-c m1) (.-c m2)) + ^boolean (mth/close? (.-d m1) (.-d m2)) + ^boolean (mth/close? (.-e m1) (.-e m2)) + ^boolean (mth/close? (.-f m1) (.-f m2)))) (defn unit? [^Matrix m1] - (and (some? m1) - (mth/close? (.-a m1) 1) - (mth/close? (.-b m1) 0) - (mth/close? (.-c m1) 0) - (mth/close? (.-d m1) 1) - (mth/close? (.-e m1) 0) - (mth/close? (.-f m1) 0))) + (and ^boolean (some? m1) + ^boolean (mth/close? (.-a m1) 1) + ^boolean (mth/close? (.-b m1) 0) + ^boolean (mth/close? (.-c m1) 0) + ^boolean (mth/close? (.-d m1) 1) + ^boolean (mth/close? (.-e m1) 0) + ^boolean (mth/close? (.-f m1) 0))) + +(defn multiply! + [^Matrix m1 ^Matrix m2] + (let [m1a (.-a m1) + m1b (.-b m1) + m1c (.-c m1) + m1d (.-d m1) + m1e (.-e m1) + m1f (.-f m1) + m2a (.-a m2) + m2b (.-b m2) + m2c (.-c m2) + m2d (.-d m2) + m2e (.-e m2) + m2f (.-f m2)] + #?@(:cljs + [(set! (.-a m1) (+ (* m1a m2a) (* m1c m2b))) + (set! (.-b m1) (+ (* m1b m2a) (* m1d m2b))) + (set! (.-c m1) (+ (* m1a m2c) (* m1c m2d))) + (set! (.-d m1) (+ (* m1b m2c) (* m1d m2d))) + (set! (.-e m1) (+ (* m1a m2e) (* m1c m2f) m1e)) + (set! (.-f m1) (+ (* m1b m2e) (* m1d m2f) m1f)) + m1] + :clj + [(pos->Matrix + (+ (* m1a m2a) (* m1c m2b)) + (+ (* m1b m2a) (* m1d m2b)) + (+ (* m1a m2c) (* m1c m2d)) + (+ (* m1b m2c) (* m1d m2d)) + (+ (* m1a m2e) (* m1c m2f) m1e) + (+ (* m1b m2e) (* m1d m2f) m1f))]))) (defn multiply ([^Matrix m1 ^Matrix m2] @@ -156,7 +209,7 @@ m2e (.-e m2) m2f (.-f m2)] - (Matrix. + (pos->Matrix (+ (* m1a m2a) (* m1c m2b)) (+ (* m1b m2a) (* m1d m2b)) (+ (* m1a m2c) (* m1c m2d)) @@ -165,51 +218,28 @@ (+ (* m1b m2e) (* m1d m2f) m1f))))) ([m1 m2 & others] - (reduce multiply (multiply m1 m2) others))) - -(defn multiply! - [^Matrix m1 ^Matrix m2] - (let [m1a (.-a m1) - m1b (.-b m1) - m1c (.-c m1) - m1d (.-d m1) - m1e (.-e m1) - m1f (.-f m1) - m2a (.-a m2) - m2b (.-b m2) - m2c (.-c m2) - m2d (.-d m2) - m2e (.-e m2) - m2f (.-f m2)] - #?@(:cljs [(set! (.-a m1) (+ (* m1a m2a) (* m1c m2b))) - (set! (.-b m1) (+ (* m1b m2a) (* m1d m2b))) - (set! (.-c m1) (+ (* m1a m2c) (* m1c m2d))) - (set! (.-d m1) (+ (* m1b m2c) (* m1d m2d))) - (set! (.-e m1) (+ (* m1a m2e) (* m1c m2f) m1e)) - (set! (.-f m1) (+ (* m1b m2e) (* m1d m2f) m1f)) - m1] - :clj [(Matrix. - (+ (* m1a m2a) (* m1c m2b)) - (+ (* m1b m2a) (* m1d m2b)) - (+ (* m1a m2c) (* m1c m2d)) - (+ (* m1b m2c) (* m1d m2d)) - (+ (* m1a m2e) (* m1c m2f) m1e) - (+ (* m1b m2e) (* m1d m2f) m1f))]))) + (reduce multiply! (multiply m1 m2) others))) (defn add-translate "Given two TRANSLATE matrixes (only e and f have significative values), combine them. Quicker than multiplying them, for this precise case." - ([{m1e :e m1f :f} {m2e :e m2f :f}] - (Matrix. 1 0 0 1 (+ m1e m2e) (+ m1f m2f))) + ([^Matrix m1 ^Matrix m2] + (let [m1e (dm/get-prop m1 :e) + m1f (dm/get-prop m1 :f) + m2e (dm/get-prop m2 :e) + m2f (dm/get-prop m2 :f)] + (pos->Matrix 1 0 0 1 (+ m1e m2e) (+ m1f m2f)))) ([m1 m2 & others] (reduce add-translate (add-translate m1 m2) others))) +;; FIXME: optimize? + (defn substract [{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f} {m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}] - (Matrix. + (pos->Matrix (- m1a m2a) (- m1b m2b) (- m1c m2c) (- m1d m2d) (- m1e m2e) (- m1f m2f))) @@ -221,13 +251,24 @@ (defn translate-matrix ([pt] - (assert (gpt/point? pt)) - (Matrix. 1 0 0 1 - (dm/get-prop pt :x) - (dm/get-prop pt :y))) + (dm/assert! (gpt/point? pt)) + (pos->Matrix 1 0 0 1 + (dm/get-prop pt :x) + (dm/get-prop pt :y))) ([x y] - (Matrix. 1 0 0 1 x y))) + (pos->Matrix 1 0 0 1 x y))) + + +(defn translate-matrix-neg + ([pt] + (dm/assert! (gpt/point? pt)) + (pos->Matrix 1 0 0 1 + (- (dm/get-prop pt :x)) + (- (dm/get-prop pt :y)))) + + ([x y] + (pos->Matrix 1 0 0 1 (- x) (- y)))) (defn scale-matrix ([pt center] @@ -235,10 +276,10 @@ sy (dm/get-prop pt :y) cx (dm/get-prop center :x) cy (dm/get-prop center :y)] - (Matrix. sx 0 0 sy (- cx (* cx sx)) (- cy (* cy sy))))) + (pos->Matrix sx 0 0 sy (- cx (* cx sx)) (- cy (* cy sy))))) ([pt] - (assert (gpt/point? pt)) - (Matrix. (dm/get-prop pt :x) 0 0 (dm/get-prop pt :y) 0 0))) + (dm/assert! (gpt/point? pt)) + (pos->Matrix (dm/get-prop pt :x) 0 0 (dm/get-prop pt :y) 0 0))) (defn rotate-matrix ([angle point] @@ -252,15 +293,15 @@ ns (- s) tx (+ (* c nx) (* ns ny) cx) ty (+ (* s nx) (* c ny) cy)] - (Matrix. c s ns c tx ty))) + (pos->Matrix c s ns c tx ty))) ([angle] (let [a (mth/radians angle)] - (Matrix. (mth/cos a) - (mth/sin a) - (- (mth/sin a)) - (mth/cos a) - 0 - 0)))) + (pos->Matrix (mth/cos a) + (mth/sin a) + (- (mth/sin a)) + (mth/cos a) + 0 + 0)))) (defn skew-matrix ([angle-x angle-y point] @@ -270,7 +311,7 @@ ([angle-x angle-y] (let [m1 (mth/tan (mth/radians angle-x)) m2 (mth/tan (mth/radians angle-y))] - (Matrix. 1 m2 m1 1 0 0)))) + (pos->Matrix 1 m2 m1 1 0 0)))) (defn rotate "Apply rotation transformation to the matrix." @@ -331,6 +372,7 @@ (translate (gpt/negate pt))) mtx)) +;; FIXME: performance (defn determinant "Determinant for the affinity transform" [{:keys [a b c d _ _]}] @@ -340,14 +382,14 @@ "Gets the inverse of the affinity transform `mtx`" [{:keys [a b c d e f] :as mtx}] (let [det (determinant mtx)] - (when-not (mth/almost-zero? det) + (when-not ^boolean (mth/almost-zero? det) (let [a' (/ d det) b' (/ (- b) det) c' (/ (- c) det) d' (/ a det) e' (/ (- (* c f) (* d e)) det) f' (/ (- (* b e) (* a f)) det)] - (Matrix. a' b' c' d' e' f'))))) + (pos->Matrix a' b' c' d' e' f'))))) (defn round [mtx] @@ -371,8 +413,41 @@ point)) (defn move? - [{:keys [a b c d _ _]}] - (and (mth/almost-zero? (- a 1)) - (mth/almost-zero? b) - (mth/almost-zero? c) - (mth/almost-zero? (- d 1)))) + [m] + (and ^boolean (mth/almost-zero? (- (dm/get-prop m :a) 1)) + ^boolean (mth/almost-zero? (dm/get-prop m :b)) + ^boolean (mth/almost-zero? (dm/get-prop m :c)) + ^boolean (mth/almost-zero? (- (dm/get-prop m :d) 1)))) + +#?(:clj + (fres/add-handlers! + {:name "penpot/matrix" + :class Matrix + :wfn (fn [n w o] + (fres/write-tag! w n 1) + (fres/write-list! w (List/of (.-a ^Matrix o) + (.-b ^Matrix o) + (.-c ^Matrix o) + (.-d ^Matrix o) + (.-e ^Matrix o) + (.-f ^Matrix o)))) + :rfn (fn [rdr] + (let [^List x (fres/read-object! rdr)] + (pos->Matrix (.get x 0) + (.get x 1) + (.get x 2) + (.get x 3) + (.get x 4) + (.get x 5))))})) + +(t/add-handlers! + {:id "matrix" + :class Matrix + :wfn #(into {} %) + :rfn (fn [m] + (pos->Matrix (get m :a) + (get m :b) + (get m :c) + (get m :d) + (get m :e) + (get m :f)))}) diff --git a/common/src/app/common/geom/modif_tree.cljc b/common/src/app/common/geom/modif_tree.cljc new file mode 100644 index 0000000000..1f9ae1cf27 --- /dev/null +++ b/common/src/app/common/geom/modif_tree.cljc @@ -0,0 +1,55 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.modif-tree + (:require + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.types.modifiers :as ctm])) + +(defn add-modifiers + "Add the given modifiers to the map of modifiers." + [modif-tree id modifiers] + (if (ctm/empty? modifiers) + modif-tree + (let [old-modifiers + (dm/get-in modif-tree [id :modifiers]) + new-modifiers + (ctm/add-modifiers old-modifiers modifiers)] + (cond-> modif-tree + (ctm/empty? new-modifiers) + (dissoc id) + + (not (ctm/empty? new-modifiers)) + (assoc-in [id :modifiers] new-modifiers))))) + +(defn merge-modif-tree + "Merge two maps of modifiers into a single one" + [modif-tree other-tree] + (reduce + (fn [modif-tree [id {:keys [modifiers]}]] + (add-modifiers modif-tree id modifiers)) + modif-tree + other-tree)) + +(defn apply-structure-modifiers + "Only applies the structure modifiers to the objects tree map" + [objects modif-tree] + (letfn [(update-children-structure-modifiers + [objects ids modifiers] + (reduce #(update %1 %2 ctm/apply-structure-modifiers modifiers) objects ids)) + + (apply-shape [objects [id {:keys [modifiers]}]] + (cond-> objects + (ctm/has-structure? modifiers) + (update id ctm/apply-structure-modifiers modifiers) + + (and (ctm/has-structure? modifiers) + (ctm/has-structure-child? modifiers)) + (update-children-structure-modifiers + (cfh/get-children-ids objects id) + (ctm/select-child-structre-modifiers modifiers))))] + (reduce apply-shape objects modif-tree))) diff --git a/common/src/app/common/geom/modifiers.cljc b/common/src/app/common/geom/modifiers.cljc new file mode 100644 index 0000000000..813fd784a2 --- /dev/null +++ b/common/src/app/common/geom/modifiers.cljc @@ -0,0 +1,371 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.modifiers + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.bounds-map :as cgb] + [app.common.geom.modif-tree :as cgt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.constraints :as gct] + [app.common.geom.shapes.flex-layout :as gcfl] + [app.common.geom.shapes.grid-layout :as gcgl] + [app.common.geom.shapes.min-size-layout] + [app.common.geom.shapes.pixel-precision :as gpp] + [app.common.geom.shapes.points :as gpo] + [app.common.geom.shapes.transforms :as gtr] + [app.common.geom.shapes.tree-seq :as cgst] + [app.common.types.modifiers :as ctm] + [app.common.types.shape.layout :as ctl] + [app.common.uuid :as uuid])) + +;;#?(:cljs +;; (defn modif->js +;; [modif-tree objects] +;; (clj->js (into {} +;; (map (fn [[k v]] +;; [(get-in objects [k :name]) v])) +;; modif-tree)))) + +(defn- set-children-modifiers + "Propagates the modifiers from a parent too its children applying constraints if necesary" + [modif-tree children objects bounds parent transformed-parent-bounds ignore-constraints] + (let [modifiers (dm/get-in modif-tree [(:id parent) :modifiers])] + ;; Move modifiers don't need to calculate constraints + (cond + (ctm/empty? modifiers) + modif-tree + + (ctm/only-move? modifiers) + (reduce #(cgt/add-modifiers %1 %2 modifiers) modif-tree children) + + ;; Check the constraints, then resize + :else + (let [parent-id (:id parent) + parent-bounds (gtr/transform-bounds @(get bounds parent-id) (ctm/select-parent modifiers))] + (->> children + (reduce + (fn [modif-tree child-id] + (if-let [child (get objects child-id)] + (let [child-bounds @(get bounds child-id) + child-modifiers + (gct/calc-child-modifiers + parent child modifiers ignore-constraints + child-bounds + parent-bounds transformed-parent-bounds)] + + (cgt/add-modifiers modif-tree child-id child-modifiers)) + modif-tree)) + modif-tree)))))) + +(defn- set-flex-layout-modifiers + [modif-tree children objects bounds parent transformed-parent-bounds] + + (letfn [(apply-modifiers [bounds child] + [(-> @(get bounds (:id child)) + (gpo/parent-coords-bounds @transformed-parent-bounds)) + child]) + + (set-child-modifiers [[layout-line modif-tree] [child-bounds child]] + (let [[modifiers layout-line] + (gcfl/layout-child-modifiers parent transformed-parent-bounds child child-bounds layout-line)] + [layout-line (cgt/add-modifiers modif-tree (:id child) modifiers)]))] + + (let [bounds (cgb/transform-bounds-map bounds objects modif-tree children) + + children + (->> children + (keep (d/getf objects)) + (remove gco/invalid-geometry?) + (map (partial apply-modifiers bounds))) + + layout-data (gcfl/calc-layout-data parent @transformed-parent-bounds children bounds objects) + children (into [] (cond-> children (not (:reverse? layout-data)) reverse)) + max-idx (dec (count children)) + layout-lines (:layout-lines layout-data)] + (loop [modif-tree modif-tree + layout-line (first layout-lines) + pending (rest layout-lines) + from-idx 0] + (if (and (some? layout-line) (<= from-idx max-idx)) + (let [to-idx (+ from-idx (:num-children layout-line)) + children (subvec children from-idx to-idx) + + [_ modif-tree] + (reduce set-child-modifiers [layout-line modif-tree] children)] + (recur modif-tree (first pending) (rest pending) to-idx)) + + modif-tree))))) + +(defn- set-grid-layout-modifiers + [modif-tree objects bounds parent transformed-parent-bounds] + + (letfn [(apply-modifiers [bounds child] + [(-> @(get bounds (:id child)) + (gpo/parent-coords-bounds @transformed-parent-bounds)) + child]) + + (set-child-modifiers [modif-tree grid-data cell-data [child-bounds child]] + (let [modifiers + (gcgl/child-modifiers parent transformed-parent-bounds child child-bounds grid-data cell-data)] + (cgt/add-modifiers modif-tree (:id child) modifiers)))] + + (let [bounds (cgb/transform-bounds-map bounds objects modif-tree (:shapes parent)) + + children + (->> (cfh/get-immediate-children objects (:id parent) {:remove-hidden true}) + (map (partial apply-modifiers bounds))) + grid-data (gcgl/calc-layout-data parent @transformed-parent-bounds children bounds objects)] + (loop [modif-tree modif-tree + bound+child (first children) + pending (rest children)] + (if (some? bound+child) + (let [cell-data (gcgl/get-cell-data grid-data @transformed-parent-bounds bound+child) + modif-tree (cond-> modif-tree + (some? cell-data) + (set-child-modifiers grid-data cell-data bound+child))] + (recur modif-tree (first pending) (rest pending))) + modif-tree))))) + +(defn- set-modifiers-constraints + "Propagate modifiers to its children" + [objects bounds ignore-constraints modif-tree parent] + (let [parent-id (:id parent) + children (:shapes parent) + root? (= uuid/zero parent-id) + modifiers (-> (dm/get-in modif-tree [parent-id :modifiers]) + (ctm/select-geometry)) + has-modifiers? (ctm/child-modifiers? modifiers) + parent? (or (cfh/group-like-shape? parent) (cfh/frame-shape? parent)) + transformed-parent-bounds (delay (gtr/transform-bounds @(get bounds parent-id) modifiers))] + + (cond-> modif-tree + (and has-modifiers? parent? (not root?)) + (set-children-modifiers children objects bounds parent transformed-parent-bounds ignore-constraints)))) + +(defn- set-modifiers-layout + "Propagate modifiers to its children" + ([objects bounds ignore-constraints parent] + (set-modifiers-layout objects bounds ignore-constraints {} parent)) + ([objects bounds ignore-constraints modif-tree parent] + (let [parent-id (:id parent) + root? (= uuid/zero parent-id) + modifiers (-> (dm/get-in modif-tree [parent-id :modifiers]) + (ctm/select-geometry)) + has-modifiers? (ctm/child-modifiers? modifiers) + flex-layout? (ctl/flex-layout? parent) + grid-layout? (ctl/grid-layout? parent) + parent? (or (cfh/group-like-shape? parent) (cfh/frame-shape? parent)) + + transformed-parent-bounds (delay (gtr/transform-bounds @(get bounds parent-id) modifiers)) + + children-modifiers + (if (or flex-layout? grid-layout?) + (->> (:shapes parent) + (filter #(ctl/position-absolute? objects %))) + (:shapes parent)) + + children-layout + (when (or flex-layout? grid-layout?) + (->> (:shapes parent) + (remove #(ctl/position-absolute? objects %))))] + + (cond-> modif-tree + (and has-modifiers? parent? (not root?)) + (set-children-modifiers children-modifiers objects bounds parent transformed-parent-bounds ignore-constraints) + + flex-layout? + (set-flex-layout-modifiers children-layout objects bounds parent transformed-parent-bounds) + + grid-layout? + (set-grid-layout-modifiers objects bounds parent transformed-parent-bounds))))) + +(defn propagate-modifiers-constraints + ([objects bounds ignore-constraints shapes] + (propagate-modifiers-constraints objects bounds ignore-constraints {} shapes)) + ([objects bounds ignore-constraints modif-tree shapes] + (reduce #(set-modifiers-constraints objects bounds ignore-constraints %1 %2) modif-tree shapes))) + +(defn propagate-modifiers-layouts + ([objects bounds ignore-constraints shapes] + (propagate-modifiers-layouts objects bounds ignore-constraints {} shapes)) + ([objects bounds ignore-constraints modif-tree shapes] + (reduce #(set-modifiers-layout objects bounds ignore-constraints %1 %2) modif-tree shapes))) + +(defn- calc-auto-modifiers + "Calculates the modifiers to adjust the bounds for auto-width/auto-height shapes" + [objects bounds parent] + (let [parent-id (:id parent) + parent-bounds (get bounds parent-id) + + set-parent-auto-width + (fn [modifiers auto-width] + (let [origin (gpo/origin @parent-bounds) + current-width (gpo/width-points @parent-bounds) + scale-width (/ auto-width current-width)] + (-> modifiers + (ctm/resize (gpt/point scale-width 1) origin (:transform parent) (:transform-inverse parent))))) + + set-parent-auto-height + (fn [modifiers auto-height] + (let [origin (gpo/origin @parent-bounds) + current-height (gpo/height-points @parent-bounds) + scale-height (/ auto-height current-height)] + (-> modifiers + (ctm/resize (gpt/point 1 scale-height) origin (:transform parent) (:transform-inverse parent))))) + + children (->> (cfh/get-immediate-children objects parent-id) + (remove ctl/position-absolute?) + (remove gco/invalid-geometry?)) + + auto? (or (ctl/auto? parent) + (and (ctl/grid-layout? objects (:parent-id parent)) + (ctl/fill? parent))) + auto-width? (or (ctl/auto-width? parent) + (and (ctl/grid-layout? objects (:parent-id parent)) + (ctl/fill-width? parent))) + auto-height? (or (ctl/auto-height? parent) + (and (ctl/grid-layout? objects (:parent-id parent)) + (ctl/fill-height? parent))) + + content-bounds + (when (and (d/not-empty? children) auto?) + (cond + (ctl/flex-layout? parent) + (gcfl/layout-content-bounds bounds parent children objects) + + (ctl/grid-layout? parent) + (let [children (->> children + (map (fn [child] [@(get bounds (:id child)) child]))) + layout-data (gcgl/calc-layout-data parent @parent-bounds children bounds objects)] + (gcgl/layout-content-bounds bounds parent layout-data)))) + + auto-width (when content-bounds (gpo/width-points content-bounds)) + auto-height (when content-bounds (gpo/height-points content-bounds))] + (cond-> (ctm/empty) + (and (some? auto-width) auto-width?) + (set-parent-auto-width auto-width) + + (and (some? auto-height) auto-height?) + (set-parent-auto-height auto-height)))) + +(defn find-auto-layouts + [objects shapes] + + (letfn [(mk-check-auto-layout [objects] + (fn [shape] + ;; Auto-width/height can change the positions in the parent so we need to recalculate + ;; also if the child is fill width/height inside a grid layout + (when (or (ctl/auto? shape) + (and (ctl/grid-layout? objects (:parent-id shape)) (ctl/fill? shape))) + (:id shape))))] + (into (d/ordered-set) + (keep (mk-check-auto-layout objects)) + shapes))) + +(defn sizing-auto-modifiers + "Recalculates the layouts to adjust the sizing: auto new sizes" + [modif-tree sizing-auto-layouts objects bounds ignore-constraints] + + (let [calculate-modifiers + (fn [[modif-tree bounds] layout-id] + (let [layout (get objects layout-id) + auto-modifiers (calc-auto-modifiers objects bounds layout)] + + (if (and (ctm/empty? auto-modifiers) (not (ctl/grid-layout? layout))) + [modif-tree bounds] + + (let [from-layout + (->> (cfh/get-parent-ids objects layout-id) + (d/seek sizing-auto-layouts)) + + shapes + (if from-layout + (cgst/resolve-subtree from-layout layout-id objects) + (cgst/resolve-tree #{layout-id} objects)) + + auto-modif-tree {layout-id {:modifiers auto-modifiers}} + auto-modif-tree (propagate-modifiers-layouts objects bounds ignore-constraints auto-modif-tree shapes) + + bounds (cgb/transform-bounds-map bounds objects auto-modif-tree) + modif-tree (cgt/merge-modif-tree modif-tree auto-modif-tree)] + [modif-tree bounds]))))] + (->> sizing-auto-layouts + (reverse) + (reduce calculate-modifiers [modif-tree bounds]) + (first)))) + +(defn set-objects-modifiers + "Applies recursively the modifiers and calculate the layouts and constraints for all the items to be placed correctly" + ([modif-tree objects] + (set-objects-modifiers modif-tree objects nil)) + + ([modif-tree objects params] + (set-objects-modifiers nil modif-tree objects params)) + + ([old-modif-tree modif-tree objects + {:keys [ignore-constraints snap-pixel? snap-precision snap-ignore-axis] + :or {ignore-constraints false + snap-pixel? false + snap-precision 1 + snap-ignore-axis nil}}] + + (let [;; Apply structure modifiers. Things that are not related to geometry + objects + (-> objects + (cond-> (some? old-modif-tree) + (cgt/apply-structure-modifiers old-modif-tree)) + (cgt/apply-structure-modifiers modif-tree)) + + ;; Creates the sequence of shapes with the shapes that are modified + shapes-tree + (cgst/resolve-tree (-> modif-tree keys set) objects) + + bounds-map + (cond-> (cgb/objects->bounds-map objects) + (some? old-modif-tree) + (cgb/transform-bounds-map objects old-modif-tree)) + + ;; Round the transforms if the snap-to-pixel is active + modif-tree + (cond-> modif-tree + snap-pixel? + (gpp/adjust-pixel-precision objects snap-precision snap-ignore-axis)) + + ;; Propagates the modifiers to the normal shapes with constraints + modif-tree + (propagate-modifiers-constraints objects bounds-map ignore-constraints modif-tree shapes-tree) + + bounds-map + (cgb/transform-bounds-map bounds-map objects modif-tree) + + modif-tree-layout + (propagate-modifiers-layouts objects bounds-map ignore-constraints shapes-tree) + + modif-tree + (cgt/merge-modif-tree modif-tree modif-tree-layout) + + ;; Calculate hug layouts positions + bounds-map + (cgb/transform-bounds-map bounds-map objects modif-tree-layout) + + ;; Find layouts with auto width/height + sizing-auto-layouts (find-auto-layouts objects shapes-tree) + + modif-tree + (sizing-auto-modifiers modif-tree sizing-auto-layouts objects bounds-map ignore-constraints) + + modif-tree + (if old-modif-tree + (cgt/merge-modif-tree old-modif-tree modif-tree) + modif-tree)] + + ;;#?(:cljs + ;; (.log js/console ">result" (modif->js modif-tree objects))) + modif-tree))) diff --git a/common/src/app/common/geom/point.cljc b/common/src/app/common/geom/point.cljc index 949ae2ba4d..0a04fa7476 100644 --- a/common/src/app/common/geom/point.cljc +++ b/common/src/app/common/geom/point.cljc @@ -7,24 +7,30 @@ (ns app.common.geom.point (:refer-clojure :exclude [divide min max abs]) (:require - #?(:cljs [cljs.pprint :as pp] - :clj [clojure.pprint :as pp]) + #?(:clj [app.common.fressian :as fres]) #?(:cljs [cljs.core :as c] :clj [clojure.core :as c]) + #?(:cljs [cljs.pprint :as pp] + :clj [clojure.pprint :as pp]) [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.math :as mth] + [app.common.record :as cr] [app.common.schema :as sm] [app.common.schema.generators :as sg] [app.common.schema.openapi :as-alias oapi] [app.common.spec :as us] + [app.common.transit :as t] [clojure.spec.alpha :as s] - [cuerdas.core :as str])) + [cuerdas.core :as str]) + #?(:clj + (:import + java.util.List))) ;; --- Point Impl -(defrecord Point [x y]) +(cr/defrecord Point [x y]) (defn s [pt] @@ -35,12 +41,6 @@ [v] (instance? Point v)) -(sm/def! ::point-map - [:map {:title "PointMap"} - [:x ::sm/safe-number] - [:y ::sm/safe-number]]) - - ;; FIXME: deprecated (s/def ::x ::us/safe-number) (s/def ::y ::us/safe-number) @@ -51,13 +51,23 @@ (s/def ::point (s/and ::point-attrs point?)) + +(def ^:private schema:point-attrs + [:map {:title "PointAttrs"} + [:x ::sm/safe-number] + [:y ::sm/safe-number]]) + +(def valid-point? + (sm/lazy-validator + [:and [:fn point?] schema:point-attrs])) + (sm/def! ::point (letfn [(decode [p] (if (map? p) (map->Point p) (if (string? p) (let [[x y] (->> (str/split p #",") (mapv parse-double))] - (Point. x y)) + (pos->Point x y)) p))) (encode [p] @@ -65,13 +75,13 @@ (dm/get-prop p :y)))] {:type ::point - :pred point? + :pred valid-point? :type-properties {:title "point" :description "Point" :error/message "expected a valid point" :gen/gen (->> (sg/tuple (sg/small-int) (sg/small-int)) - (sg/fmap #(apply ->Point %))) + (sg/fmap #(apply pos->Point %))) ::oapi/type "string" ::oapi/format "point" ::oapi/decode decode @@ -85,7 +95,7 @@ (defn point "Create a Point instance." - ([] (Point. 0 0)) + ([] (pos->Point 0 0)) ([v] (cond (point? v) @@ -95,12 +105,12 @@ (point v v) (point-like? v) - (Point. (:x v) (:y v)) + (pos->Point (:x v) (:y v)) :else (ex/raise :hint "invalid arguments (on pointer constructor)" :value v))) ([x y] - (Point. x y))) + (pos->Point x y))) (defn close? [p1 p2] @@ -119,25 +129,29 @@ "Returns the addition of the supplied value to both coordinates of the point as a new point." [p1 p2] - (assert (and (point? p1) - (point? p2)) - "arguments should be pointer instance") - (Point. (+ (dm/get-prop p1 :x) - (dm/get-prop p2 :x)) - (+ (dm/get-prop p1 :y) - (dm/get-prop p2 :y)))) + (dm/assert! + "arguments should be point instance" + (and (point? p1) + (point? p2))) + + (pos->Point (+ (dm/get-prop p1 :x) + (dm/get-prop p2 :x)) + (+ (dm/get-prop p1 :y) + (dm/get-prop p2 :y)))) (defn subtract "Returns the subtraction of the supplied value to both coordinates of the point as a new point." [p1 p2] - (assert (and (point? p1) - (point? p2)) - "arguments should be pointer instance") - (Point. (- (dm/get-prop p1 :x) - (dm/get-prop p2 :x)) - (- (dm/get-prop p1 :y) - (dm/get-prop p2 :y)))) + (dm/assert! + "arguments should be pointer instance" + (and (point? p1) + (point? p2))) + + (pos->Point (- (dm/get-prop p1 :x) + (dm/get-prop p2 :x)) + (- (dm/get-prop p1 :y) + (dm/get-prop p2 :y)))) (defn multiply "Returns the subtraction of the supplied value to both @@ -146,20 +160,20 @@ (assert (and (point? p1) (point? p2)) "arguments should be pointer instance") - (Point. (* (dm/get-prop p1 :x) - (dm/get-prop p2 :x)) - (* (dm/get-prop p1 :y) - (dm/get-prop p2 :y)))) + (pos->Point (* (dm/get-prop p1 :x) + (dm/get-prop p2 :x)) + (* (dm/get-prop p1 :y) + (dm/get-prop p2 :y)))) (defn divide [p1 p2] (assert (and (point? p1) (point? p2)) "arguments should be pointer instance") - (Point. (/ (dm/get-prop p1 :x) - (dm/get-prop p2 :x)) - (/ (dm/get-prop p1 :y) - (dm/get-prop p2 :y)))) + (pos->Point (/ (dm/get-prop p1 :x) + (dm/get-prop p2 :x)) + (/ (dm/get-prop p1 :y) + (dm/get-prop p2 :y)))) (defn min ([] nil) @@ -168,10 +182,10 @@ (cond (nil? p1) p2 (nil? p2) p1 - :else (Point. (c/min (dm/get-prop p1 :x) - (dm/get-prop p2 :x)) - (c/min (dm/get-prop p1 :y) - (dm/get-prop p2 :y)))))) + :else (pos->Point (c/min (dm/get-prop p1 :x) + (dm/get-prop p2 :x)) + (c/min (dm/get-prop p1 :y) + (dm/get-prop p2 :y)))))) (defn max ([] nil) ([p1] p1) @@ -179,21 +193,21 @@ (cond (nil? p1) p2 (nil? p2) p1 - :else (Point. (c/max (dm/get-prop p1 :x) - (dm/get-prop p2 :x)) - (c/max (dm/get-prop p1 :y) - (dm/get-prop p2 :y)))))) + :else (pos->Point (c/max (dm/get-prop p1 :x) + (dm/get-prop p2 :x)) + (c/max (dm/get-prop p1 :y) + (dm/get-prop p2 :y)))))) (defn inverse [pt] (assert (point? pt) "point instance expected") - (Point. (/ 1.0 (dm/get-prop pt :x)) - (/ 1.0 (dm/get-prop pt :y)))) + (pos->Point (/ 1.0 (dm/get-prop pt :x)) + (/ 1.0 (dm/get-prop pt :y)))) (defn negate [pt] (assert (point? pt) "point instance expected") - (Point. (- (dm/get-prop pt :x)) - (- (dm/get-prop pt :y)))) + (pos->Point (- (dm/get-prop pt :x)) + (- (dm/get-prop pt :y)))) (defn distance "Calculate the distance between two points." @@ -217,8 +231,8 @@ (dm/get-prop p2 :x)) dy (- (dm/get-prop p1 :y) (dm/get-prop p2 :y))] - (Point. (mth/abs dx) - (mth/abs dy)))) + (pos->Point (mth/abs dx) + (mth/abs dy)))) (defn length [pt] @@ -285,8 +299,8 @@ (assert (number? angle) "expected number") (let [len (length p) angle (mth/radians angle)] - (Point. (* (mth/cos angle) len) - (* (mth/sin angle) len)))) + (pos->Point (* (mth/cos angle) len) + (* (mth/sin angle) len)))) (defn quadrant "Return the quadrant of the angle of the point." @@ -306,22 +320,21 @@ ([pt decimals] (assert (point? pt) "expected point instance") (assert (number? decimals) "expected number instance") - (Point. (mth/precision (dm/get-prop pt :x) decimals) - (mth/precision (dm/get-prop pt :y) decimals)))) + (pos->Point (mth/precision (dm/get-prop pt :x) decimals) + (mth/precision (dm/get-prop pt :y) decimals)))) (defn round-step "Round the coordinates to the closest half-point" [pt step] (assert (point? pt) "expected point instance") - (Point. (mth/round (dm/get-prop pt :x) step) - (mth/round (dm/get-prop pt :y) step))) + (pos->Point (mth/round (dm/get-prop pt :x) step) + (mth/round (dm/get-prop pt :y) step))) (defn transform "Transform a point applying a matrix transformation." [p m] (when (point? p) - (if (nil? m) - p + (if (some? m) (let [x (dm/get-prop p :x) y (dm/get-prop p :y) a (dm/get-prop m :a) @@ -330,18 +343,51 @@ d (dm/get-prop m :d) e (dm/get-prop m :e) f (dm/get-prop m :f)] - (Point. (+ (* x a) (* y c) e) - (+ (* x b) (* y d) f)))))) + (pos->Point (+ (* x a) (* y c) e) + (+ (* x b) (* y d) f))) + p))) +(defn transform! + [p m] + + (dm/assert! + "expected valid rect and matrix instances" + (and (some? p) (some? m))) + + (let [x (dm/get-prop p :x) + y (dm/get-prop p :y) + a (dm/get-prop m :a) + b (dm/get-prop m :b) + c (dm/get-prop m :c) + d (dm/get-prop m :d) + e (dm/get-prop m :e) + f (dm/get-prop m :f)] + #?(:clj + (pos->Point (+ (* x a) (* y c) e) + (+ (* x b) (* y d) f)) + :cljs + (do + (set! (.-x p) (+ (* x a) (* y c) e)) + (set! (.-y p) (+ (* x b) (* y d) f)) + p)))) + +(defn matrix->point + "Returns a result of transform an identity point with the provided + matrix instance" + [m] + (let [e (dm/get-prop m :e) + f (dm/get-prop m :f)] + (pos->Point e f))) + ;; Vector functions (defn to-vec [p1 p2] (subtract p2 p1)) (defn scale [p scalar] - (Point. (* (dm/get-prop p :x) scalar) - (* (dm/get-prop p :y) scalar))) + (pos->Point (* (dm/get-prop p :x) scalar) + (* (dm/get-prop p :y) scalar))) (defn dot [p1 p2] @@ -354,14 +400,14 @@ [p1] (let [p-length (length p1)] (if (mth/almost-zero? p-length) - (Point. 0 0) - (Point. (/ (dm/get-prop p1 :x) p-length) - (/ (dm/get-prop p1 :y) p-length))))) + (pos->Point 0 0) + (pos->Point (/ (dm/get-prop p1 :x) p-length) + (/ (dm/get-prop p1 :y) p-length))))) (defn perpendicular [pt] - (Point. (- (dm/get-prop pt :y)) - (dm/get-prop pt :x))) + (pos->Point (- (dm/get-prop pt :y)) + (dm/get-prop pt :x))) (defn project "V1 perpendicular projection on vector V2" @@ -412,7 +458,7 @@ [p1 p2 t] (let [x (mth/lerp (dm/get-prop p1 :x) (dm/get-prop p2 :x) t) y (mth/lerp (dm/get-prop p1 :y) (dm/get-prop p2 :y) t)] - (Point. x y))) + (pos->Point x y))) (defn rotate "Rotates the point around center with an angle" @@ -434,7 +480,7 @@ y (+ (* sa (- px cx)) (* ca (- py cy)) cy)] - (Point. x y))) + (pos->Point x y))) (defn scale-from "Moves a point in the vector that creates with center with a scale @@ -450,10 +496,16 @@ [p] (let [x (dm/get-prop p :x) y (dm/get-prop p :y)] - (Point. (if (mth/almost-zero? x) 0.001 x) - (if (mth/almost-zero? y) 0.001 y)))) + (pos->Point (if (mth/almost-zero? x) 0.001 x) + (if (mth/almost-zero? y) 0.001 y)))) +(defn resize + "Creates a new vector with the same direction but different length" + [vector new-length] + (let [old-length (length vector)] + (scale vector (/ new-length old-length)))) +;; FIXME: perfromance (defn abs [point] (-> point @@ -464,3 +516,20 @@ (defmethod pp/simple-dispatch Point [obj] (pr obj)) +#?(:clj + (fres/add-handlers! + {:name "penpot/point" + :class Point + :wfn (fn [n w ^Point o] + (fres/write-tag! w n 1) + (fres/write-list! w (List/of (.-x o) (.-y o)))) + :rfn (fn [rdr] + (let [^List x (fres/read-object! rdr)] + (pos->Point (.get x 0) (.get x 1))))})) + +(t/add-handlers! + {:id "point" + :class Point + :wfn #(into {} %) + :rfn map->Point}) + diff --git a/common/src/app/common/geom/proportions.cljc b/common/src/app/common/geom/proportions.cljc index 8294e4301c..342145b68d 100644 --- a/common/src/app/common/geom/proportions.cljc +++ b/common/src/app/common/geom/proportions.cljc @@ -4,40 +4,45 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.geom.proportions) +(ns app.common.geom.proportions + (:require + [app.common.data :as d])) ;; --- Proportions (defn assign-proportions [shape] (let [{:keys [width height]} (:selrect shape)] - (assoc shape :proportion (/ width height)))) - -;; --- Setup Proportions - + (assoc shape :proportion (float (/ width height))))) ; Note: we need to convert explicitly to float. + ; In Clojure (not clojurescript) when we divide +;; --- Setup Proportions ; two integers it does not create a float, but + ; a clojure.lang.Ratio object. (defn setup-proportions-image [{:keys [metadata] :as shape}] (let [{:keys [width height]} metadata] (assoc shape - :proportion (/ width height) + :proportion (float (/ width height)) :proportion-lock true))) -(defn setup-proportions-svg - [{:keys [width height] :as shape}] +(defn setup-proportions-size + [{{:keys [width height]} :selrect :as shape}] (assoc shape - :proportion (/ width height) + :proportion (float (/ width height)) :proportion-lock true)) (defn setup-proportions-const [shape] (assoc shape - :proportion 1 + :proportion 1.0 :proportion-lock false)) (defn setup-proportions - [shape] - (case (:type shape) - :svg-raw (setup-proportions-svg shape) - :image (setup-proportions-image shape) - :text shape - (setup-proportions-const shape))) + [{:keys [type] :as shape}] + (let [image-fill? (and (d/not-empty? (:fills shape)) + (every? #(some? (:fill-image %)) (:fills shape)))] + (cond + (= type :svg-raw) (setup-proportions-size shape) + (= type :image) (setup-proportions-image shape) + (= type :text) shape + image-fill? (setup-proportions-size shape) + :else (setup-proportions-const shape)))) diff --git a/common/src/app/common/geom/rect.cljc b/common/src/app/common/geom/rect.cljc new file mode 100644 index 0000000000..48d620adfc --- /dev/null +++ b/common/src/app/common/geom/rect.cljc @@ -0,0 +1,399 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.rect + (:require + #?(:clj [app.common.fressian :as fres]) + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.geom.point :as gpt] + [app.common.math :as mth] + [app.common.record :as rc] + [app.common.schema :as sm] + [app.common.schema.generators :as sg] + [app.common.transit :as t])) + +(rc/defrecord Rect [x y width height x1 y1 x2 y2]) + +(defn rect? + [o] + (instance? Rect o)) + +#?(:clj + (fres/add-handlers! + {:name "penpot/geom/rect" + :class Rect + :wfn fres/write-map-like + :rfn (comp map->Rect fres/read-map-like)})) + +(t/add-handlers! + {:id "rect" + :class Rect + :wfn #(into {} %) + :rfn map->Rect}) + +(defn make-rect + ([] (make-rect 0 0 0.01 0.01)) + ([data] + (if (rect? data) + data + (let [{:keys [x y width height]} data] + (make-rect (d/nilv x 0) + (d/nilv y 0) + (d/nilv width 0.01) + (d/nilv height 0.01))))) + + ([p1 p2] + (dm/assert! + "expected `p1` and `p2` to be points" + (and (gpt/point? p1) + (gpt/point? p2))) + + (let [xp1 (dm/get-prop p1 :x) + yp1 (dm/get-prop p1 :y) + xp2 (dm/get-prop p2 :x) + yp2 (dm/get-prop p2 :y) + x1 (mth/min xp1 xp2) + y1 (mth/min yp1 yp2) + x2 (mth/max xp1 xp2) + y2 (mth/max yp1 yp2)] + (make-rect x1 y1 (- x2 x1) (- y2 y1)))) + + ([x y width height] + (if (d/num? x y width height) + (let [w (mth/max width 0.01) + h (mth/max height 0.01)] + (pos->Rect x y w h x y (+ x w) (+ y h))) + (make-rect)))) + +(def ^:private schema:rect-attrs + [:map {:title "RectAttrs"} + [:x ::sm/safe-number] + [:y ::sm/safe-number] + [:width ::sm/safe-number] + [:height ::sm/safe-number] + [:x1 ::sm/safe-number] + [:y1 ::sm/safe-number] + [:x2 ::sm/safe-number] + [:y2 ::sm/safe-number]]) + +(sm/define! ::rect + [:and + {:gen/gen (->> (sg/tuple (sg/small-double) + (sg/small-double) + (sg/small-double) + (sg/small-double)) + (sg/fmap #(apply make-rect %)))} + [:fn rect?] + schema:rect-attrs]) + +(def valid-rect? + (sm/lazy-validator + [:and [:fn rect?] schema:rect-attrs])) + +(def empty-rect + (make-rect 0 0 0.01 0.01)) + +(defn update-rect + [rect type] + (case type + :size + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (assoc rect + :x2 (+ x w) + :y2 (+ y h))) + + :corners + (let [x1 (dm/get-prop rect :x1) + y1 (dm/get-prop rect :y1) + x2 (dm/get-prop rect :x2) + y2 (dm/get-prop rect :y2)] + (assoc rect + :x (mth/min x1 x2) + :y (mth/min y1 y2) + :width (mth/abs (- x2 x1)) + :height (mth/abs (- y2 y1)))) + + :position + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (assoc rect + :x1 x + :y1 y + :x2 (+ x w) + :y2 (+ y h))))) + +(defn update-rect! + [rect type] + (case type + (:size :position) + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (rc/assoc! rect + :x1 x + :y1 y + :x2 (+ x w) + :y2 (+ y h))) + + :corners + (let [x1 (dm/get-prop rect :x1) + y1 (dm/get-prop rect :y1) + x2 (dm/get-prop rect :x2) + y2 (dm/get-prop rect :y2)] + (rc/assoc! rect + :x (mth/min x1 x2) + :y (mth/min y1 y2) + :width (mth/abs (- x2 x1)) + :height (mth/abs (- y2 y1)))))) + +(defn close-rect? + [rect1 rect2] + + (dm/assert! + "expected two rects" + (and (rect? rect1) + (rect? rect2))) + + (and ^boolean (mth/close? (dm/get-prop rect1 :x) + (dm/get-prop rect2 :x)) + ^boolean (mth/close? (dm/get-prop rect1 :y) + (dm/get-prop rect2 :y)) + ^boolean (mth/close? (dm/get-prop rect1 :width) + (dm/get-prop rect2 :width)) + ^boolean (mth/close? (dm/get-prop rect1 :height) + (dm/get-prop rect2 :height)))) + +(defn rect->points + [rect] + (dm/assert! + "expected rect instance" + (rect? rect)) + + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (when (d/num? x y) + (let [w (mth/max w 0.01) + h (mth/max h 0.01)] + [(gpt/point x y) + (gpt/point (+ x w) y) + (gpt/point (+ x w) (+ y h)) + (gpt/point x (+ y h))])))) + +(defn rect->point + "Extract the position part of the rect" + [rect] + (gpt/point (dm/get-prop rect :x) + (dm/get-prop rect :y))) + +(defn rect->center + [rect] + (dm/assert! (rect? rect)) + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (when (d/num? x y w h) + (gpt/point (+ x (/ w 2.0)) + (+ y (/ h 2.0)))))) + +(defn rect->lines + [rect] + (dm/assert! (rect? rect)) + + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (when (d/num? x y) + (let [w (mth/max w 0.01) + h (mth/max h 0.01)] + [[(gpt/point x y) (gpt/point (+ x w) y)] + [(gpt/point (+ x w) y) (gpt/point (+ x w) (+ y h))] + [(gpt/point (+ x w) (+ y h)) (gpt/point x (+ y h))] + [(gpt/point x (+ y h)) (gpt/point x y)]])))) + +(defn points->rect + [points] + (when-let [points (seq points)] + (loop [minx ##Inf + miny ##Inf + maxx ##-Inf + maxy ##-Inf + pts points] + (if-let [pt (first pts)] + (let [x (dm/get-prop pt :x) + y (dm/get-prop pt :y)] + (recur (mth/min minx x) + (mth/min miny y) + (mth/max maxx x) + (mth/max maxy y) + (rest pts))) + (when (d/num? minx miny maxx maxy) + (make-rect minx miny (- maxx minx) (- maxy miny))))))) + +;; FIXME: measure performance +(defn bounds->rect + [[pa pb pc pd]] + (let [ax (dm/get-prop pa :x) + ay (dm/get-prop pa :y) + bx (dm/get-prop pb :x) + by (dm/get-prop pb :y) + cx (dm/get-prop pc :x) + cy (dm/get-prop pc :y) + dx (dm/get-prop pd :x) + dy (dm/get-prop pd :y) + minx (mth/min ax bx cx dx) + miny (mth/min ay by cy dy) + maxx (mth/max ax bx cx dx) + maxy (mth/max ay by cy dy)] + (when (d/num? minx miny maxx maxy) + (make-rect minx miny (- maxx minx) (- maxy miny))))) + +(def ^:private xf-keep-x (keep #(dm/get-prop % :x))) +(def ^:private xf-keep-y (keep #(dm/get-prop % :y))) +(def ^:private xf-keep-x2 (keep #(dm/get-prop % :x2))) +(def ^:private xf-keep-y2 (keep #(dm/get-prop % :y2))) + +(defn squared-points + [points] + (when (d/not-empty? points) + (let [minx (transduce xf-keep-x d/min ##Inf points) + miny (transduce xf-keep-y d/min ##Inf points) + maxx (transduce xf-keep-x2 d/max ##-Inf points) + maxy (transduce xf-keep-y2 d/max ##-Inf points)] + (when (d/num? minx miny maxx maxy) + [(gpt/point minx miny) + (gpt/point maxx miny) + (gpt/point maxx maxy) + (gpt/point minx maxy)])))) + +(defn join-rects [rects] + (when (seq rects) + (let [minx (transduce xf-keep-x d/min ##Inf rects) + miny (transduce xf-keep-y d/min ##Inf rects) + maxx (transduce xf-keep-x2 d/max ##-Inf rects) + maxy (transduce xf-keep-y2 d/max ##-Inf rects)] + (when (d/num? minx miny maxx maxy) + (make-rect minx miny (- maxx minx) (- maxy miny)))))) + +(defn center->rect + ([point size] + (center->rect point size size)) + ([point w h] + (when (some? point) + (let [x (dm/get-prop point :x) + y (dm/get-prop point :y)] + (when (d/num? x y w h) + (make-rect (- x (/ w 2)) + (- y (/ h 2)) + w + h)))))) + +(defn s= + [a b] + (mth/almost-zero? (- a b))) + +(defn overlaps-rects? + "Check for two rects to overlap. Rects won't overlap only if + one of them is fully to the left or the top" + [rect-a rect-b] + (let [x1a (dm/get-prop rect-a :x) + y1a (dm/get-prop rect-a :y) + x2a (+ x1a (dm/get-prop rect-a :width)) + y2a (+ y1a (dm/get-prop rect-a :height)) + + x1b (dm/get-prop rect-b :x) + y1b (dm/get-prop rect-b :y) + x2b (+ x1b (dm/get-prop rect-b :width)) + y2b (+ y1b (dm/get-prop rect-b :height))] + + (and (or (> x2a x1b) (s= x2a x1b)) + (or (>= x2b x1a) (s= x2b x1a)) + (or (<= y1b y2a) (s= y1b y2a)) + (or (<= y1a y2b) (s= y1a y2b))))) + +(defn contains-point? + [rect point] + (assert (gpt/point? point)) + (let [x1 (:x rect) + y1 (:y rect) + x2 (+ (:x rect) (:width rect)) + y2 (+ (:y rect) (:height rect)) + + px (:x point) + py (:y point)] + + (and (or (> px x1) (s= px x1)) + (or (< px x2) (s= px x2)) + (or (> py y1) (s= py y1)) + (or (< py y2) (s= py y2))))) + +(defn contains-rect? + "Check if a rect srb is contained inside sra" + [sra srb] + (let [ax1 (dm/get-prop sra :x1) + ax2 (dm/get-prop sra :x2) + ay1 (dm/get-prop sra :y1) + ay2 (dm/get-prop sra :y2) + bx1 (dm/get-prop srb :x1) + bx2 (dm/get-prop srb :x2) + by1 (dm/get-prop srb :y1) + by2 (dm/get-prop srb :y2)] + (and (>= bx1 ax1) + (<= bx2 ax2) + (>= by1 ay1) + (<= by2 ay2)))) + +(defn corners->rect + ([p1 p2] + (corners->rect (:x p1) (:y p1) (:x p2) (:y p2))) + ([xp1 yp1 xp2 yp2] + (make-rect (mth/min xp1 xp2) + (mth/min yp1 yp2) + (abs (- xp1 xp2)) + (abs (- yp1 yp2))))) + +(defn clip-rect + [selrect bounds] + (when (rect? selrect) + (dm/assert! (rect? bounds)) + (let [x1 (dm/get-prop selrect :x1) + y1 (dm/get-prop selrect :y1) + x2 (dm/get-prop selrect :x2) + y2 (dm/get-prop selrect :y2) + bx1 (dm/get-prop bounds :x1) + by1 (dm/get-prop bounds :y1) + bx2 (dm/get-prop bounds :x2) + by2 (dm/get-prop bounds :y2)] + (corners->rect (mth/max bx1 x1) + (mth/max by1 y1) + (mth/min bx2 x2) + (mth/min by2 y2))))) +(defn fix-aspect-ratio + [bounds aspect-ratio] + (if aspect-ratio + (let [width (dm/get-prop bounds :width) + height (dm/get-prop bounds :height) + target-height (* width aspect-ratio) + target-width (* height (/ 1 aspect-ratio))] + (cond-> bounds + (> target-height height) + (-> (assoc :height target-height) + (update :y - (/ (- target-height height) 2))) + + (< target-height height) + (-> (assoc :width target-width) + (update :x - (/ (- target-width width) 2))))) + bounds)) diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 800e682506..5555f9386e 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -9,35 +9,37 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes.bool :as gsb] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.constraints :as gct] [app.common.geom.shapes.corners :as gsc] [app.common.geom.shapes.intersect :as gsi] - [app.common.geom.shapes.modifiers :as gsm] [app.common.geom.shapes.path :as gsp] - [app.common.geom.shapes.rect :as gpr] - [app.common.geom.shapes.text :as gst] [app.common.geom.shapes.transforms :as gtr] [app.common.math :as mth])) ;; --- Outer Rect -(defn selection-rect - "Returns a rect that contains all the shapes and is aware of the - rotation of each shape. Mainly used for multiple selection." - [shapes] - (->> shapes - (map (comp gpr/points->selrect :points)) - (gpr/join-selrects))) - (defn translate-to-frame - [shape {:keys [x y]}] - (gtr/move shape (gpt/negate (gpt/point x y))) ) + [shape frame] + (->> (gpt/point (- (dm/get-prop frame :x)) + (- (dm/get-prop frame :y))) + (gtr/move shape))) (defn translate-from-frame - [shape {:keys [x y]}] - (gtr/move shape (gpt/point x y)) ) + [shape frame] + (gtr/move shape (gpt/point (dm/get-prop frame :x) + (dm/get-prop frame :y)))) + +(defn shape->rect + [shape] + (let [x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + w (dm/get-prop shape :width) + h (dm/get-prop shape :height)] + (when (d/num? x y w h) + (grc/make-rect x y w h)))) ;; --- Helpers @@ -45,7 +47,7 @@ "Returns a rect that wraps the shape after all transformations applied." [shape] ;; TODO: perhaps we need to store this calculation in a shape attribute - (gpr/points->rect (:points shape))) + (grc/points->rect (:points shape))) (defn left-bound "Returns the lowest x coord of the shape BEFORE applying transformations." @@ -82,21 +84,38 @@ (update :width (comp inc inc)) (update :height (comp inc inc)))))) -(defn selrect->areas [bounds selrect] - (let [{bound-x1 :x1 bound-x2 :x2 bound-y1 :y1 bound-y2 :y2} bounds - {sr-x1 :x1 sr-x2 :x2 sr-y1 :y1 sr-y2 :y2} selrect] - {:left (gpr/corners->selrect bound-x1 sr-y1 sr-x1 sr-y2) - :top (gpr/corners->selrect sr-x1 bound-y1 sr-x2 sr-y1) - :right (gpr/corners->selrect sr-x2 sr-y1 bound-x2 sr-y2) - :bottom (gpr/corners->selrect sr-x1 sr-y2 sr-x2 bound-y2)})) +(defn get-areas + [bounds selrect] + (let [bound-x1 (dm/get-prop bounds :x1) + bound-x2 (dm/get-prop bounds :x2) + bound-y1 (dm/get-prop bounds :y1) + bound-y2 (dm/get-prop bounds :y2) + sr-x1 (dm/get-prop selrect :x1) + sr-x2 (dm/get-prop selrect :x2) + sr-y1 (dm/get-prop selrect :y1) + sr-y2 (dm/get-prop selrect :y2)] + {:left (grc/corners->rect bound-x1 sr-y1 sr-x1 sr-y2) + :top (grc/corners->rect sr-x1 bound-y1 sr-x2 sr-y1) + :right (grc/corners->rect sr-x2 sr-y1 bound-x2 sr-y2) + :bottom (grc/corners->rect sr-x1 sr-y2 sr-x2 bound-y2)})) -(defn distance-selrect [selrect other] - (let [{:keys [x1 y1]} other - {:keys [x2 y2]} selrect] +(defn distance-selrect + [selrect other] + + (dm/assert! + (and (grc/rect? selrect) + (grc/rect? other))) + + (let [x1 (dm/get-prop other :x1) + y1 (dm/get-prop other :y1) + x2 (dm/get-prop selrect :x2) + y2 (dm/get-prop selrect :y2)] (gpt/point (- x1 x2) (- y1 y2)))) (defn distance-shapes [shape other] - (distance-selrect (:selrect shape) (:selrect other))) + (distance-selrect + (dm/get-prop shape :selrect) + (dm/get-prop other :selrect))) (defn close-attrs? "Compares two shapes attributes to see if they are equal or almost @@ -131,28 +150,11 @@ (= val1 val2))))) ;; EXPORTS -(dm/export gco/center-shape) -(dm/export gco/center-selrect) -(dm/export gco/center-rect) -(dm/export gco/center-points) +(dm/export gco/shape->center) +(dm/export gco/shapes->rect) +(dm/export gco/points->center) (dm/export gco/transform-points) (dm/export gco/shape->points) -(dm/export gco/shapes->rect) - -(dm/export gpr/make-rect) -(dm/export gpr/make-selrect) -(dm/export gpr/rect->selrect) -(dm/export gpr/rect->points) -(dm/export gpr/points->selrect) -(dm/export gpr/points->rect) -(dm/export gpr/center->rect) -(dm/export gpr/center->selrect) -(dm/export gpr/join-rects) -(dm/export gpr/join-selrects) -(dm/export gpr/contains-selrect?) -(dm/export gpr/contains-point?) -(dm/export gpr/close-selrect?) -(dm/export gpr/clip-selrect) (dm/export gtr/move) (dm/export gtr/absolute-move) @@ -170,17 +172,21 @@ (dm/export gtr/transform-bounds) (dm/export gtr/move-position-data) (dm/export gtr/apply-objects-modifiers) +(dm/export gtr/apply-children-modifiers) +(dm/export gtr/update-shapes-geometry) ;; Constratins (dm/export gct/calc-child-modifiers) ;; PATHS +;; FIXME: rename (dm/export gsp/content->selrect) (dm/export gsp/transform-content) (dm/export gsp/open-path?) ;; Intersection (dm/export gsi/overlaps?) +(dm/export gsi/overlaps-path?) (dm/export gsi/has-point?) (dm/export gsi/has-point-rect?) (dm/export gsi/rect-contains-shape?) @@ -196,8 +202,5 @@ (dm/export gsc/shape-corners-1) (dm/export gsc/shape-corners-4) -;; Modifiers -(dm/export gsm/set-objects-modifiers) - -;; Text -(dm/export gst/position-data-selrect) +;; Rect +(dm/export grc/rect->points) diff --git a/common/src/app/common/geom/shapes/bool.cljc b/common/src/app/common/geom/shapes/bool.cljc index a3a4645440..48116a88db 100644 --- a/common/src/app/common/geom/shapes/bool.cljc +++ b/common/src/app/common/geom/shapes/bool.cljc @@ -7,8 +7,9 @@ (ns app.common.geom.shapes.bool (:require [app.common.data :as d] - [app.common.path.bool :as pb] - [app.common.path.shapes-to-path :as stp])) + [app.common.files.helpers :as cpf] + [app.common.svg.path.bool :as pb] + [app.common.svg.path.shapes-to-path :as stp])) (defn calc-bool-content [shape objects] @@ -16,6 +17,7 @@ (let [extract-content-xf (comp (map (d/getf objects)) (filter (comp not :hidden)) + (remove cpf/svg-raw-shape?) (map #(stp/convert-to-path % objects)) (map :content)) diff --git a/common/src/app/common/geom/shapes/bounds.cljc b/common/src/app/common/geom/shapes/bounds.cljc index 92139415f7..5612837b4f 100644 --- a/common/src/app/common/geom/shapes/bounds.cljc +++ b/common/src/app/common/geom/shapes/bounds.cljc @@ -7,32 +7,29 @@ (ns app.common.geom.shapes.bounds (:require [app.common.data :as d] - [app.common.geom.shapes.rect :as gsr] - [app.common.math :as mth] - [app.common.pages.helpers :as cph])) + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.rect :as grc] + [app.common.math :as mth])) (defn shape-stroke-margin [shape stroke-width] - (if (= (:type shape) :path) - ;; TODO: Calculate with the stroke offset (not implemented yet - (mth/sqrt (* 2 stroke-width stroke-width)) - (- (mth/sqrt (* 2 stroke-width stroke-width)) stroke-width))) + (if (cfh/path-shape? shape) + ;; TODO: Calculate with the stroke offset (not implemented yet) + (+ stroke-width (mth/sqrt (* 2 stroke-width stroke-width))) + (mth/sqrt (* 2 stroke-width stroke-width)))) -(defn blur-filters [type value] - (->> [value] - (remove :hidden) - (filter #(= (:type %) type)) - (map #(hash-map :id (str "filter_" (:id %)) - :type (:type %) - :params %)))) - -(defn shadow-filters [type filters] - (->> filters - (remove :hidden) - (filter #(= (:style %) type)) - (map #(hash-map :id (str "filter_" (:id %)) - :type (:style %) - :params %)))) +(defn- apply-filters + [attr type filters] + (sequence + (comp + (remove :hidden) + (filter #(= (attr %) type)) + (map (fn [item] + {:id (dm/str "filter_" (:id item)) + :type type + :params item}))) + filters)) (defn shape->filters [shape] @@ -41,134 +38,160 @@ ;; Background blur won't work in current SVG specification ;; We can revisit this in the future - #_(->> shape :blur (blur-filters :background-blur)) + #_(->> shape :blur (into []) (blur-filters :background-blur)) - (->> shape :shadow (shadow-filters :drop-shadow)) + (->> shape :shadow (apply-filters :style :drop-shadow)) [{:id "shape" :type :blend-filters}] - (->> shape :shadow (shadow-filters :inner-shadow)) - (->> shape :blur (blur-filters :layer-blur)))) + (->> shape :shadow (apply-filters :style :inner-shadow)) + (->> shape :blur list (apply-filters :type :layer-blur)))) -(defn calculate-filter-bounds [{:keys [x y width height]} filter-entry] - (let [{:keys [offset-x offset-y blur spread] :or {offset-x 0 offset-y 0 blur 0 spread 0}} (:params filter-entry) - filter-x (min x (+ x offset-x (- spread) (- blur) -5)) - filter-y (min y (+ y offset-y (- spread) (- blur) -5)) - filter-width (+ width (mth/abs offset-x) (* spread 2) (* blur 2) 10) - filter-height (+ height (mth/abs offset-y) (* spread 2) (* blur 2) 10)] - (gsr/make-selrect filter-x filter-y filter-width filter-height))) +(defn- calculate-filter-bounds + [selrect filter-entry] + (let [x (dm/get-prop selrect :x) + y (dm/get-prop selrect :y) + w (dm/get-prop selrect :width) + h (dm/get-prop selrect :height) + + {:keys [offset-x offset-y blur spread] + :or {offset-x 0 offset-y 0 blur 0 spread 0}} + (:params filter-entry) + + filter-x (mth/min x (+ x offset-x (- spread) (- blur) -5)) + filter-y (mth/min y (+ y offset-y (- spread) (- blur) -5)) + filter-w (+ w (mth/abs offset-x) (* spread 2) (* blur 2) 10) + filter-h (+ h (mth/abs offset-y) (* spread 2) (* blur 2) 10)] + (grc/make-rect filter-x filter-y filter-w filter-h))) (defn get-rect-filter-bounds [selrect filters blur-value] - (let [filter-bounds (->> filters - (filter #(= :drop-shadow (:type %))) - (map (partial calculate-filter-bounds selrect)) - (concat [selrect]) - (gsr/join-selrects)) - delta-blur (* blur-value 2) - - result - (-> filter-bounds - (update :x - delta-blur) - (update :y - delta-blur) - (update :x1 - delta-blur) - (update :y1 - delta-blur) - (update :x2 + delta-blur) - (update :y2 + delta-blur) - (update :width + (* delta-blur 2)) - (update :height + (* delta-blur 2)))] - - result)) + (let [bounds-xf (comp + (filter #(= :drop-shadow (:type %))) + (map (partial calculate-filter-bounds selrect))) + delta-blur (* blur-value 2)] + (-> (into [selrect] bounds-xf filters) + (grc/join-rects) + (update :x - delta-blur) + (update :y - delta-blur) + (update :x1 - delta-blur) + (update :y1 - delta-blur) + (update :x2 + delta-blur) + (update :y2 + delta-blur) + (update :width + (* delta-blur 2)) + (update :height + (* delta-blur 2))))) (defn get-shape-filter-bounds - ([shape] - (let [svg-root? (and (= :svg-raw (:type shape)) (not= :svg (get-in shape [:content :tag])))] - (if svg-root? - (:selrect shape) - - (let [filters (shape->filters shape) - blur-value (or (-> shape :blur :value) 0)] - (get-rect-filter-bounds (-> shape :points gsr/points->selrect) filters blur-value)))))) + [shape] + (if (and (cfh/svg-raw-shape? shape) + (not= :svg (dm/get-in shape [:content :tag]))) + (dm/get-prop shape :selrect) + (let [filters (shape->filters shape) + blur-value (or (-> shape :blur :value) 0) + srect (-> (dm/get-prop shape :points) + (grc/points->rect))] + (get-rect-filter-bounds srect filters blur-value)))) (defn calculate-padding ([shape] (calculate-padding shape false)) - ([shape ignore-margin?] - (let [stroke-width (apply max 0 (map #(case (:stroke-alignment % :center) - :center (/ (:stroke-width % 0) 2) - :outer (:stroke-width % 0) - 0) (:strokes shape))) + (let [strokes (:strokes shape) - margin (if ignore-margin? - 0 - (apply max 0 (map #(shape-stroke-margin % stroke-width) (:strokes shape)))) + stroke-width + (->> strokes + (map #(case (get % :stroke-alignment :center) + :center (/ (:stroke-width % 0) 2) + :outer (:stroke-width % 0) + 0)) + (reduce d/max 0)) - shadow-width (apply max 0 (map #(case (:style % :drop-shadow) - :drop-shadow (+ (mth/abs (:offset-x %)) (* (:spread %) 2) (* (:blur %) 2) 10) - 0) (:shadow shape))) + stroke-margin + (if ignore-margin? + 0 + (shape-stroke-margin shape stroke-width)) - shadow-height (apply max 0 (map #(case (:style % :drop-shadow) - :drop-shadow (+ (mth/abs (:offset-y %)) (* (:spread %) 2) (* (:blur %) 2) 10) - 0) (:shadow shape)))] + shadow-width + (->> (:shadow shape) + (remove :hidden) + (map #(case (:style % :drop-shadow) + :drop-shadow (+ (mth/abs (:offset-x %)) (* (:spread %) 2) (* (:blur %) 2) 10) + 0)) + (reduce d/max 0)) - {:horizontal (+ stroke-width margin shadow-width) - :vertical (+ stroke-width margin shadow-height)}))) + shadow-height + (->> (:shadow shape) + (remove :hidden) + (map #(case (:style % :drop-shadow) + :drop-shadow (+ (mth/abs (:offset-y %)) (* (:spread %) 2) (* (:blur %) 2) 10) + 0)) + (reduce d/max 0))] + {:horizontal (mth/ceil (+ stroke-margin shadow-width)) + :vertical (mth/ceil (+ stroke-margin shadow-height))}))) (defn- add-padding [bounds padding] - (-> bounds - (update :x - (:horizontal padding)) - (update :x1 - (:horizontal padding)) - (update :x2 + (:horizontal padding)) - (update :y - (:vertical padding)) - (update :y1 - (:vertical padding)) - (update :y2 + (:vertical padding)) - (update :width + (* 2 (:horizontal padding))) - (update :height + (* 2 (:vertical padding))))) + (let [h-padding (:horizontal padding) + v-padding (:vertical padding)] + (-> bounds + (update :x - h-padding) + (update :x1 - h-padding) + (update :x2 + h-padding) + (update :y - v-padding) + (update :y1 - v-padding) + (update :y2 + v-padding) + (update :width + (* 2 h-padding)) + (update :height + (* 2 v-padding))))) + +(defn calculate-base-bounds + ([shape] + (calculate-base-bounds shape true)) + ([shape ignore-margin?] + (-> (get-shape-filter-bounds shape) + (add-padding (calculate-padding shape ignore-margin?))))) (defn get-object-bounds - [objects shape] + ([objects shape] + (get-object-bounds objects shape nil)) + ([objects shape {:keys [ignore-margin?] :or {ignore-margin? true}}] + (let [base-bounds (calculate-base-bounds shape ignore-margin?) + bounds + (cond + (or (empty? (:shapes shape)) + (or (:masked-group shape) (= :bool (:type shape))) + (and (cfh/frame-shape? shape) (not (:show-content shape)))) + [base-bounds] - (let [calculate-base-bounds - (fn [shape] - (-> (get-shape-filter-bounds shape) - (add-padding (calculate-padding shape true)))) + :else + (cfh/reduce-objects + objects - bounds - (cond - (empty? (:shapes shape)) - [(calculate-base-bounds shape)] + (fn [shape] + (and (not (:hidden shape)) + (d/not-empty? (:shapes shape)) + (or (not (cfh/frame-shape? shape)) + (:show-content shape)) - (:masked-group? shape) - [(calculate-base-bounds shape)] + (or (not (cfh/group-shape? shape)) + (not (:masked-group shape))))) + (:id shape) - (and (cph/frame-shape? shape) (not (:show-content shape))) - [(calculate-base-bounds shape)] + (fn [result child] + (cond-> result + (not (:hidden child)) + (conj (calculate-base-bounds child)))) - :else - (cph/reduce-objects - objects + [base-bounds])) - (fn [shape] - (and (d/not-empty? (:shapes shape)) - (or (not (cph/frame-shape? shape)) - (:show-content shape)) + children-bounds + (cond->> (grc/join-rects bounds) + (not (cfh/frame-shape? shape)) (or (:children-bounds shape))) - (or (not (cph/group-shape? shape)) - (not (:masked-group? shape))))) + filters (shape->filters shape) + blur-value (or (-> shape :blur :value) 0)] - (:id shape) - - (fn [result child] - (conj result (calculate-base-bounds child))) - - [(calculate-base-bounds shape)])) - - children-bounds - (cond->> (gsr/join-selrects bounds) - (not (cph/frame-shape? shape)) (or (:children-bounds shape))) - - filters (shape->filters shape) - blur-value (or (-> shape :blur :value) 0)] - - (get-rect-filter-bounds children-bounds filters blur-value))) + (get-rect-filter-bounds children-bounds filters blur-value)))) +(defn get-frame-bounds + ([shape] + (get-frame-bounds shape nil)) + ([shape {:keys [ignore-margin?] :or {ignore-margin? false}}] + (get-object-bounds [] shape {:ignore-margin? ignore-margin?}))) diff --git a/common/src/app/common/geom/shapes/common.cljc b/common/src/app/common/geom/shapes/common.cljc index 4d09d08a0e..746de65b28 100644 --- a/common/src/app/common/geom/shapes/common.cljc +++ b/common/src/app/common/geom/shapes/common.cljc @@ -7,10 +7,15 @@ (ns app.common.geom.shapes.common (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.shapes.rect :as gpr] - [app.common.math :as mth])) + [app.common.geom.rect :as grc] + [app.common.math :as mth] + [app.common.record :as cr])) + +(def ^:private xf-keep-x (keep #(dm/get-prop % :x))) +(def ^:private xf-keep-y (keep #(dm/get-prop % :y))) (defn shapes->rect "Returns a rect that contains all the shapes and is aware of the @@ -18,82 +23,70 @@ [shapes] (->> shapes (keep (fn [shape] - (-> (:points shape) - (gpr/points->rect)))) - (gpr/join-rects))) + (-> (dm/get-prop shape :points) + (grc/points->rect)))) + (grc/join-rects))) -(defn center-rect - [{:keys [x y width height]}] - (when (d/num? x y width height) - (gpt/point (+ x (/ width 2.0)) - (+ y (/ height 2.0))))) - -(defn center-selrect - "Calculate the center of the selrect." - [selrect] - (center-rect selrect)) - -(defn center-points [points] - (let [ptx (into [] (keep :x) points) - pty (into [] (keep :y) points) - minx (reduce min ##Inf ptx) - miny (reduce min ##Inf pty) - maxx (reduce max ##-Inf ptx) - maxy (reduce max ##-Inf pty)] +(defn points->center + [points] + (let [ptx (into [] xf-keep-x points) + pty (into [] xf-keep-y points) + minx (reduce d/min ##Inf ptx) + miny (reduce d/min ##Inf pty) + maxx (reduce d/max ##-Inf ptx) + maxy (reduce d/max ##-Inf pty)] (gpt/point (/ (+ minx maxx) 2.0) (/ (+ miny maxy) 2.0)))) -(defn center-bounds [[a b c d]] - (let [xa (:x a) - ya (:y a) - xb (:x b) - yb (:y b) - xc (:x c) - yc (:y c) - xd (:x d) - yd (:y d) - minx (min xa xb xc xd) - miny (min ya yb yc yd) - maxx (max xa xb xc xd) - maxy (max ya yb yc yd)] - (gpt/point (/ (+ minx maxx) 2.0) - (/ (+ miny maxy) 2.0)))) - -(defn center-shape +(defn shape->center "Calculate the center of the shape." [shape] - (center-rect (:selrect shape))) + (grc/rect->center (dm/get-prop shape :selrect))) (defn transform-points ([points matrix] (transform-points points nil matrix)) ([points center matrix] - (if (and (d/not-empty? points) (gmt/matrix? matrix)) - (let [prev (if center (gmt/translate-matrix center) (gmt/matrix)) - post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix)) - - tr-point (fn [point] - (gpt/transform point (gmt/multiply prev matrix post)))] - (mapv tr-point points)) + (if (and ^boolean (gmt/matrix? matrix) + ^boolean (seq points)) + (let [prev (if (some? center) (gmt/translate-matrix center) (cr/clone gmt/base)) + post (if (some? center) (gmt/translate-matrix-neg center) gmt/base) + mtx (-> prev + (gmt/multiply! matrix) + (gmt/multiply! post))] + (mapv #(gpt/transform % mtx) points)) points))) (defn transform-selrect - [{:keys [x1 y1 x2 y2] :as sr} matrix] - (let [[c1 c2] (transform-points [(gpt/point x1 y1) (gpt/point x2 y2)] matrix)] - (gpr/corners->selrect c1 c2))) + [selrect matrix] + + (dm/assert! + "expected valid rect and matrix instances" + (and (grc/rect? selrect) + (gmt/matrix? matrix))) + + (let [x1 (dm/get-prop selrect :x1) + y1 (dm/get-prop selrect :y1) + x2 (dm/get-prop selrect :x2) + y2 (dm/get-prop selrect :y2) + p1 (gpt/point x1 y1) + p2 (gpt/point x2 y2) + c1 (gpt/transform! p1 matrix) + c2 (gpt/transform! p2 matrix)] + (grc/corners->rect c1 c2))) (defn invalid-geometry? [{:keys [points selrect]}] - (or (mth/nan? (:x selrect)) - (mth/nan? (:y selrect)) - (mth/nan? (:width selrect)) - (mth/nan? (:height selrect)) - (some (fn [p] - (or (mth/nan? (:x p)) - (mth/nan? (:y p)))) - points))) + (or ^boolean (mth/nan? (:x selrect)) + ^boolean (mth/nan? (:y selrect)) + ^boolean (mth/nan? (:width selrect)) + ^boolean (mth/nan? (:height selrect)) + ^boolean (some (fn [p] + (or ^boolean (mth/nan? (:x p)) + ^boolean (mth/nan? (:y p)))) + points))) (defn shape->points [{:keys [transform points]}] diff --git a/common/src/app/common/geom/shapes/constraints.cljc b/common/src/app/common/geom/shapes/constraints.cljc index cda192609b..120d222901 100644 --- a/common/src/app/common/geom/shapes/constraints.cljc +++ b/common/src/app/common/geom/shapes/constraints.cljc @@ -6,7 +6,9 @@ (ns app.common.geom.shapes.constraints (:require + [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.intersect :as gsi] [app.common.geom.shapes.points :as gpo] [app.common.geom.shapes.transforms :as gtr] @@ -204,19 +206,22 @@ disp-start (displacement start-before start-after before-side-vector after-side-vector) ;; We get the current axis side and grow it on both side by the end+start displacements - before-vec (side-vector axis child-points-after) - after-vec (side-vector-resize axis child-points-after disp-start disp-end) + before-vec (side-vector axis child-points-after) + after-vec (side-vector-resize axis child-points-after disp-start disp-end) ;; after-vec will contain the side length of the grown side ;; we scale the shape by the diference and translate it by the start ;; displacement (so its left+top position is constant) - scale (/ (gpt/length after-vec) (max 0.01 (gpt/length before-vec))) + scale (/ (gpt/length after-vec) (mth/max 0.01 (gpt/length before-vec))) - resize-origin (gpo/origin child-points-after) + resize-origin (gpo/origin child-points-after) - [_ transform transform-inverse] (gtr/calculate-geometry parent-points-after) + center (gco/points->center parent-points-after) + selrect (gtr/calculate-selrect parent-points-after center) + transform (gtr/calculate-transform parent-points-after center selrect) + transform-inverse (when (some? transform) (gmt/inverse transform)) + resize-vector (get-scale axis scale)] - resize-vector (get-scale axis scale)] (-> (ctm/empty) (ctm/resize resize-vector resize-origin transform transform-inverse) (ctm/move disp-start)))) @@ -275,11 +280,14 @@ (/ (gpo/height-points child-bb-before) (max 0.01 (gpo/height-points child-bb-after)))) resize-vector (gpt/point scale-x scale-y) - resize-origin (gpo/origin transformed-child-bounds) - [_ transform transform-inverse] (gtr/calculate-geometry transformed-parent-bounds)] + resize-origin (gpo/origin child-bb-after) - (-> modifiers - (ctm/resize resize-vector resize-origin transform transform-inverse)))) + center (gco/points->center child-bb-after) + selrect (gtr/calculate-selrect child-bb-after center) + transform (gtr/calculate-transform child-bb-after center selrect) + transform-inverse (when (some? transform) (gmt/inverse transform))] + + (ctm/resize modifiers resize-vector resize-origin transform transform-inverse))) (defn calc-child-modifiers [parent child modifiers ignore-constraints child-bounds parent-bounds transformed-parent-bounds] @@ -291,7 +299,7 @@ ignore-constraints :scale - (and (ctl/any-layout? parent) (not (ctl/layout-absolute? child))) + (and (ctl/any-layout? parent) (not (ctl/position-absolute? child))) :left :else @@ -302,7 +310,7 @@ ignore-constraints :scale - (and (ctl/any-layout? parent) (not (ctl/layout-absolute? child))) + (and (ctl/any-layout? parent) (not (ctl/position-absolute? child))) :top :else @@ -318,7 +326,9 @@ reset-modifiers? (and (gpo/axis-aligned? parent-bounds) (gpo/axis-aligned? child-bounds) - (gpo/axis-aligned? transformed-parent-bounds)) + (gpo/axis-aligned? transformed-parent-bounds) + (not= :scale constraints-h) + (not= :scale constraints-v)) modifiers (if reset-modifiers? @@ -327,13 +337,14 @@ child-bounds (gtr/transform-bounds child-bounds modifiers) parent-bounds transformed-parent-bounds)) - transformed-child-bounds (if reset-modifiers? - child-bounds - (gtr/transform-bounds child-bounds modifiers))] + transformed-child-bounds + (if reset-modifiers? + child-bounds + (gtr/transform-bounds child-bounds modifiers))] ;; If the parent is a layout we don't need to calculate its constraints. Finish ;; after normalize the children (to keep proper proportions) - (if (ctl/any-layout? parent) + (if (and (ctl/any-layout? parent) (not (ctl/position-absolute? child))) modifiers (let [child-points-before (gpo/parent-coords-bounds child-bounds parent-bounds) child-points-after (gpo/parent-coords-bounds transformed-child-bounds transformed-parent-bounds) diff --git a/common/src/app/common/geom/shapes/effects.cljc b/common/src/app/common/geom/shapes/effects.cljc index 912b2de6c0..8cac7e25b1 100644 --- a/common/src/app/common/geom/shapes/effects.cljc +++ b/common/src/app/common/geom/shapes/effects.cljc @@ -1,3 +1,9 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + (ns app.common.geom.shapes.effects) (defn update-shadow-scale diff --git a/common/src/app/common/geom/shapes/flex_layout.cljc b/common/src/app/common/geom/shapes/flex_layout.cljc index 3d196204bf..52eb913ab7 100644 --- a/common/src/app/common/geom/shapes/flex_layout.cljc +++ b/common/src/app/common/geom/shapes/flex_layout.cljc @@ -9,14 +9,15 @@ [app.common.data.macros :as dm] [app.common.geom.shapes.flex-layout.bounds :as fbo] [app.common.geom.shapes.flex-layout.drop-area :as fdr] - [app.common.geom.shapes.flex-layout.lines :as fli] - [app.common.geom.shapes.flex-layout.modifiers :as fmo])) + [app.common.geom.shapes.flex-layout.layout-data :as fld] + [app.common.geom.shapes.flex-layout.modifiers :as fmo] + [app.common.geom.shapes.flex-layout.params :as fp])) (dm/export fbo/layout-content-bounds) (dm/export fbo/layout-content-points) (dm/export fbo/child-layout-bound-points) (dm/export fdr/get-drop-index) (dm/export fdr/get-drop-areas) -(dm/export fli/calc-layout-data) +(dm/export fld/calc-layout-data) (dm/export fmo/layout-child-modifiers) - +(dm/export fp/calculate-params) diff --git a/common/src/app/common/geom/shapes/flex_layout/bounds.cljc b/common/src/app/common/geom/shapes/flex_layout/bounds.cljc index 358dbe34e0..56defa9cc3 100644 --- a/common/src/app/common/geom/shapes/flex_layout/bounds.cljc +++ b/common/src/app/common/geom/shapes/flex_layout/bounds.cljc @@ -7,122 +7,153 @@ (ns app.common.geom.shapes.flex-layout.bounds (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes.points :as gpo] [app.common.types.shape.layout :as ctl])) +;; Setted in app.common.geom.shapes.common-layout +;; We do it this way because circular dependencies +(def -child-min-width nil) + +(defn child-min-width + [child child-bounds bounds objects] + (-child-min-width child child-bounds bounds objects)) + +(def -child-min-height nil) + +(defn child-min-height + [child child-bounds bounds objects] + (-child-min-height child child-bounds bounds objects)) + (defn child-layout-bound-points "Returns the bounds of the children as points" - [parent child parent-bounds child-bounds] + ([parent child parent-bounds child-bounds bounds objects] + (child-layout-bound-points parent child parent-bounds child-bounds (gpt/point) bounds objects)) - (let [row? (ctl/row? parent) - col? (ctl/col? parent) + ([parent child parent-bounds child-bounds correct-v bounds objects] + (let [row? (ctl/row? parent) + col? (ctl/col? parent) - hv (partial gpo/start-hv parent-bounds) - vv (partial gpo/start-vv parent-bounds) + hv (partial gpo/start-hv parent-bounds) + vv (partial gpo/start-vv parent-bounds) - v-start? (ctl/v-start? parent) - v-center? (ctl/v-center? parent) - v-end? (ctl/v-end? parent) - h-start? (ctl/h-start? parent) - h-center? (ctl/h-center? parent) - h-end? (ctl/h-end? parent) + v-start? (ctl/v-start? parent) + v-center? (ctl/v-center? parent) + v-end? (ctl/v-end? parent) + h-start? (ctl/h-start? parent) + h-center? (ctl/h-center? parent) + h-end? (ctl/h-end? parent) - fill-w? (ctl/fill-width? child) - fill-h? (ctl/fill-height? child) + base-p (gpo/origin child-bounds) - base-p (gpo/origin child-bounds) + width (gpo/width-points child-bounds) + height (gpo/height-points child-bounds) - width (gpo/width-points child-bounds) - height (gpo/height-points child-bounds) + min-width (child-min-width child child-bounds bounds objects) + min-height (child-min-height child child-bounds bounds objects) - min-width (if fill-w? - (ctl/child-min-width child) - width) + ;; This is the leftmost (when row) or topmost (when col) point + ;; Will be added always to the bounds and then calculated the other limits + ;; from there + base-p + (cond-> base-p + (and row? v-center?) + (gpt/add (vv (/ height 2))) - min-height (if fill-h? - (ctl/child-min-height child) - height) + (and row? v-end?) + (gpt/add (vv height)) - ;; This is the leftmost (when row) or topmost (when col) point - ;; Will be added always to the bounds and then calculated the other limits - ;; from there - base-p (cond-> base-p - (and row? v-center?) - (gpt/add (vv (/ height 2))) + (and col? h-center?) + (gpt/add (hv (/ width 2))) - (and row? v-end?) - (gpt/add (vv height)) + (and col? h-end?) + (gpt/add (hv width))) - (and col? h-center?) - (gpt/add (hv (/ width 2))) + ;; We need some height/width to calculate the bounds. We stablish the minimum + min-width (max min-width 0.01) + min-height (max min-height 0.01) - (and col? h-end?) - (gpt/add (hv width))) + base-p (gpt/add base-p correct-v) - ;; We need some height/width to calculate the bounds. We stablish the minimum - min-width (max min-width 0.01) - min-height (max min-height 0.01)] + result + [base-p + (gpt/add base-p (hv 0.01)) + (gpt/add base-p (vv 0.01))] - (cond-> [base-p - (gpt/add base-p (hv 0.01)) - (gpt/add base-p (vv 0.01))] + result + (cond-> result + col? + (conj (gpt/add base-p (vv min-height))) - col? - (conj (gpt/add base-p (vv min-height))) + row? + (conj (gpt/add base-p (hv min-width))) - row? - (conj (gpt/add base-p (hv min-width))) + (and col? h-start?) + (conj (gpt/add base-p (hv min-width))) - (and col? h-start?) - (conj (gpt/add base-p (hv min-width))) + (and col? h-center?) + (conj (gpt/add base-p (hv (/ min-width 2))) + (gpt/subtract base-p (hv (/ min-width 2)))) - (and col? h-center?) - (conj (gpt/add base-p (hv (/ min-width 2))) - (gpt/subtract base-p (hv (/ min-width 2)))) + (and col? h-end?) + (conj (gpt/subtract base-p (hv min-width))) - (and col? h-end?) - (conj (gpt/subtract base-p (hv min-width))) + (and row? v-start?) + (conj (gpt/add base-p (vv min-height))) - (and row? v-start?) - (conj (gpt/add base-p (vv min-height))) + (and row? v-center?) + (conj (gpt/add base-p (vv (/ min-height 2))) + (gpt/subtract base-p (vv (/ min-height 2)))) - (and row? v-center?) - (conj (gpt/add base-p (vv (/ min-height 2))) - (gpt/subtract base-p (vv (/ min-height 2)))) + (and row? v-end?) + (conj (gpt/subtract base-p (vv min-height)))) - (and row? v-end?) - (conj (gpt/subtract base-p (vv min-height)))))) + correct-v + (cond-> correct-v + (and row? (ctl/fill-width? child)) + (gpt/subtract (hv (+ width min-width))) + + (and col? (ctl/fill-height? child)) + (gpt/subtract (vv (+ height min-height))))] + [result correct-v]))) (defn layout-content-points - [bounds parent children] + [bounds parent children objects] - (let [parent-id (:id parent) + (let [parent-id (dm/get-prop parent :id) parent-bounds @(get bounds parent-id) - get-child-bounds - (fn [child] - (let [child-id (:id child) - child-bounds @(get bounds child-id) - [margin-top margin-right margin-bottom margin-left] (ctl/child-margins child) + reverse? (ctl/reverse? parent) + children (cond->> children (not reverse?) reverse)] - child-bounds - (if (or (ctl/fill-width? child) (ctl/fill-height? child)) - (child-layout-bound-points parent child parent-bounds child-bounds) - child-bounds) + (loop [children (seq children) + result (transient []) + correct-v (gpt/point 0)] - child-bounds - (when (d/not-empty? child-bounds) - (-> (gpo/parent-coords-bounds child-bounds parent-bounds) - (gpo/pad-points (- margin-top) (- margin-right) (- margin-bottom) (- margin-left))))] + (if (not children) + (persistent! result) - child-bounds))] + (let [child (first children) + child-id (dm/get-prop child :id) + child-bounds @(get bounds child-id) + [margin-top margin-right margin-bottom margin-left] (ctl/child-margins child) - (->> children - (remove ctl/layout-absolute?) - (map get-child-bounds)))) + [child-bounds correct-v] + (if (or (ctl/fill-width? child) (ctl/fill-height? child)) + (child-layout-bound-points parent child parent-bounds child-bounds correct-v bounds objects) + [(->> child-bounds (map #(gpt/add % correct-v))) correct-v]) + + child-bounds + (when (d/not-empty? child-bounds) + (-> (gpo/parent-coords-bounds child-bounds parent-bounds) + (gpo/pad-points (- margin-top) (- margin-right) (- margin-bottom) (- margin-left))))] + + (recur (next children) + (cond-> result (some? child-bounds) (conj! child-bounds)) + correct-v)))))) (defn layout-content-bounds - [bounds {:keys [layout-padding] :as parent} children] + [bounds {:keys [layout-padding] :as parent} children objects] (let [parent-id (:id parent) parent-bounds @(get bounds parent-id) @@ -140,9 +171,9 @@ layout-gap-row 0) - col-pad (if (or(and row? space-evenly?) - (and row? space-around?) - (and col? content-evenly?)) + col-pad (if (or (and row? space-evenly?) + (and row? space-around?) + (and col? content-evenly?)) layout-gap-col 0) @@ -153,7 +184,7 @@ pad-left (+ (or pad-left 0) col-pad) layout-points - (layout-content-points bounds parent children)] + (layout-content-points bounds parent children objects)] (if (d/not-empty? layout-points) (-> layout-points diff --git a/common/src/app/common/geom/shapes/flex_layout/drop_area.cljc b/common/src/app/common/geom/shapes/flex_layout/drop_area.cljc index f1c019184a..e9a5fa4915 100644 --- a/common/src/app/common/geom/shapes/flex_layout/drop_area.cljc +++ b/common/src/app/common/geom/shapes/flex_layout/drop_area.cljc @@ -7,14 +7,14 @@ (ns app.common.geom.shapes.flex-layout.drop-area (:require [app.common.data :as d] + [app.common.files.helpers :as cfh] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes.common :as gco] - [app.common.geom.shapes.flex-layout.lines :as fli] + [app.common.geom.shapes.flex-layout.layout-data :as fld] [app.common.geom.shapes.points :as gpo] - [app.common.geom.shapes.rect :as gsr] [app.common.geom.shapes.transforms :as gtr] - [app.common.pages.helpers :as cph] [app.common.types.modifiers :as ctm] [app.common.types.shape.layout :as ctl])) @@ -59,16 +59,16 @@ (if row? (let [half-point-width (+ (- box-x x) (/ box-width 2))] - [(gsr/make-rect x y width height) - (-> (gsr/make-rect x y half-point-width height) + [(grc/make-rect x y width height) + (-> (grc/make-rect x y half-point-width height) (assoc :index (if reverse? (inc index) index))) - (-> (gsr/make-rect (+ x half-point-width) y (- width half-point-width) height) + (-> (grc/make-rect (+ x half-point-width) y (- width half-point-width) height) (assoc :index (if reverse? index (inc index))))]) (let [half-point-height (+ (- box-y y) (/ box-height 2))] - [(gsr/make-rect x y width height) - (-> (gsr/make-rect x y width half-point-height) + [(grc/make-rect x y width height) + (-> (grc/make-rect x y width half-point-height) (assoc :index (if reverse? (inc index) index))) - (-> (gsr/make-rect x (+ y half-point-height) width (- height half-point-height)) + (-> (grc/make-rect x (+ y half-point-height) width (- height half-point-height)) (assoc :index (if reverse? index (inc index))))])))) (defn drop-line-area @@ -83,7 +83,7 @@ v-center? (and col? (ctl/v-center? frame)) v-end? (and row? (ctl/v-end? frame)) - center (gco/center-shape frame) + center (gco/shape->center frame) start-p (gmt/transform-point-center start-p center transform-inverse) line-width @@ -136,7 +136,7 @@ :else (+ line-height (- box-y prev-y) (/ layout-gap-row 2)))] - (gsr/make-rect x y width height))) + (grc/make-rect x y width height))) (defn layout-drop-areas "Retrieve the layout drop areas to move shapes inside layouts" @@ -190,28 +190,29 @@ (-> (ctm/empty) (ctm/resize (gpt/point (if flip-x -1.0 1.0) (if flip-y -1.0 1.0)) - (gco/center-shape shape) + (gco/shape->center shape) transform transform-inverse))] [(gtr/transform-shape shape modifiers) modifiers]) [shape nil])) (defn get-drop-areas - [frame objects] + [frame objects bounds] (let [[frame modifiers] (get-flip-modifiers frame) - children (->> (cph/get-immediate-children objects (:id frame)) + children (->> (cfh/get-immediate-children objects (:id frame)) (remove :hidden) (map #(cond-> % (some? modifiers) (gtr/transform-shape modifiers))) (map #(vector (gpo/parent-coords-bounds (:points %) (:points frame)) %))) - layout-data (fli/calc-layout-data frame children (:points frame)) + layout-data (fld/calc-layout-data frame (:points frame) children bounds objects) drop-areas (layout-drop-areas frame layout-data children)] drop-areas)) (defn get-drop-index [frame-id objects position] (let [frame (get objects frame-id) - drop-areas (get-drop-areas frame objects) - position (gmt/transform-point-center position (gco/center-shape frame) (:transform-inverse frame)) - area (d/seek #(gsr/contains-point? % position) drop-areas)] + bounds (d/lazy-map (keys objects) #(gco/shape->points (get objects %))) + drop-areas (get-drop-areas frame objects bounds) + position (gmt/transform-point-center position (gco/shape->center frame) (:transform-inverse frame)) + area (d/seek #(grc/contains-point? % position) drop-areas)] (:index area))) diff --git a/common/src/app/common/geom/shapes/flex_layout/lines.cljc b/common/src/app/common/geom/shapes/flex_layout/layout_data.cljc similarity index 67% rename from common/src/app/common/geom/shapes/flex_layout/lines.cljc rename to common/src/app/common/geom/shapes/flex_layout/layout_data.cljc index 1aa12f6e90..c9e4f7c57e 100644 --- a/common/src/app/common/geom/shapes/flex_layout/lines.cljc +++ b/common/src/app/common/geom/shapes/flex_layout/layout_data.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.geom.shapes.flex-layout.lines +(ns app.common.geom.shapes.flex-layout.layout-data (:require [app.common.data :as d] [app.common.geom.shapes.flex-layout.positions :as flp] @@ -14,24 +14,38 @@ (def conjv (fnil conj [])) +;; Setted in app.common.geom.shapes.min-size-layout +;; We do it this way because circular dependencies +(def -child-min-width nil) + +(defn child-min-width + [child child-bounds bounds objects] + (-child-min-width child child-bounds bounds objects)) + +(def -child-min-height nil) + +(defn child-min-height + [child child-bounds bounds objects] + (-child-min-height child child-bounds bounds objects)) + (defn layout-bounds - [{:keys [layout-padding] :as shape} shape-bounds] - (let [;; Add padding to the bounds - {pad-top :p1 pad-right :p2 pad-bottom :p3 pad-left :p4} layout-padding] + [parent shape-bounds] + (let [[pad-top pad-right pad-bottom pad-left] (ctl/paddings parent)] (gpo/pad-points shape-bounds pad-top pad-right pad-bottom pad-left))) (defn init-layout-lines "Calculates the lines basic data and accumulated values. The positions will be calculated in a different operation" - [shape children layout-bounds] + [shape children layout-bounds bounds objects auto?] (let [col? (ctl/col? shape) row? (ctl/row? shape) space-around? (ctl/space-around? shape) space-evenly? (ctl/space-evenly? shape) - wrap? (and (ctl/wrap? shape) - (or col? (not (ctl/auto-width? shape))) - (or row? (not (ctl/auto-height? shape)))) + auto-width? (or (ctl/auto-width? shape) auto?) + auto-height? (or (ctl/auto-height? shape) auto?) + + wrap? (and (ctl/wrap? shape) (or col? (not auto-width?)) (or row? (not auto-height?))) [layout-gap-row layout-gap-col] (ctl/gaps shape) @@ -39,11 +53,11 @@ layout-height (gpo/height-points layout-bounds)] (loop [line-data nil - result [] + result (transient []) children (seq children)] - (if (empty? children) - (cond-> result (some? line-data) (conj line-data)) + (if (not children) + (persistent! (cond-> result (some? line-data) (conj! line-data))) (let [[child-bounds child] (first children) {:keys [line-min-width line-min-height @@ -53,51 +67,48 @@ child-width (gpo/width-points child-bounds) child-height (gpo/height-points child-bounds) - child-min-width (ctl/child-min-width child) - child-min-height (ctl/child-min-height child) child-max-width (ctl/child-max-width child) child-max-height (ctl/child-max-height child) - [child-margin-top child-margin-right child-margin-bottom child-margin-left] - (ctl/child-margins child) - - child-margin-width (+ child-margin-left child-margin-right) - child-margin-height (+ child-margin-top child-margin-bottom) + child-margin-width (ctl/child-width-margin child) + child-margin-height (ctl/child-height-margin child) fill-width? (ctl/fill-width? child) fill-height? (ctl/fill-height? child) ;; We need this info later to calculate the child resizes when fill child-data {:id (:id child) - :child-min-width (if fill-width? child-min-width child-width) - :child-min-height (if fill-height? child-min-height child-height) + :child-min-width (child-min-width child child-bounds bounds objects) + :child-min-height (child-min-height child child-bounds bounds objects) :child-max-width (if fill-width? child-max-width child-width) :child-max-height (if fill-height? child-max-height child-height)} - next-min-width (+ child-margin-width (if fill-width? child-min-width child-width)) - next-min-height (+ child-margin-height (if fill-height? child-min-height child-height)) - next-max-width (+ child-margin-width (if fill-width? child-max-width child-width)) - next-max-height (+ child-margin-height (if fill-height? child-max-height child-height)) + next-min-width (+ child-margin-width (:child-min-width child-data)) + next-min-height (+ child-margin-height (:child-min-height child-data)) + next-max-width (+ child-margin-width (:child-max-width child-data)) + next-max-height (+ child-margin-height (:child-max-height child-data)) - total-gap-col (cond - space-evenly? - (* layout-gap-col (+ num-children 2)) + total-gap-col + (cond + space-evenly? + (* layout-gap-col (+ num-children 2)) - space-around? - (* layout-gap-col (+ num-children 1)) + space-around? + (* layout-gap-col (+ num-children 1)) - :else - (* layout-gap-col num-children)) + :else + (* layout-gap-col num-children)) - total-gap-row (cond - space-evenly? - (* layout-gap-row (+ num-children 2)) + total-gap-row + (cond + space-evenly? + (* layout-gap-row (+ num-children 2)) - space-around? - (* layout-gap-row (+ num-children 1)) + space-around? + (* layout-gap-row (+ num-children 1)) - :else - (* layout-gap-row num-children)) + :else + (* layout-gap-row num-children)) next-line-min-width (+ line-min-width next-min-width total-gap-col) next-line-min-height (+ line-min-height next-min-height total-gap-row)] @@ -116,7 +127,7 @@ :num-children (inc num-children) :children-data (conjv children-data child-data)} result - (rest children)) + (next children)) (recur {:line-min-width next-min-width :line-min-height next-min-height @@ -124,29 +135,31 @@ :line-max-height next-max-height :num-children 1 :children-data [child-data]} - (cond-> result (some? line-data) (conj line-data)) - (rest children)))))))) + (cond-> result (some? line-data) (conj! line-data)) + (next children)))))))) (defn add-space-to-items ;; Distributes the remainder space between the lines [prop prop-min prop-max to-share items] (let [num-items (->> items (remove #(mth/close? (get % prop) (get % prop-max))) count) per-line-target (/ to-share num-items)] - (loop [current (first items) - items (rest items) + + (loop [items (seq items) remainder to-share - result []] - (if (nil? current) - [result remainder] - (let [cur-val (or (get current prop) (get current prop-min) 0) + result (transient [])] + + (if (not items) + [(persistent! result) remainder] + + (let [current (first items) + cur-val (or (get current prop) (get current prop-min) 0) max-val (get current prop-max) cur-inc (if (> (+ cur-val per-line-target) max-val) (- max-val cur-val) per-line-target) current (assoc current prop (+ cur-val cur-inc)) - remainder (- remainder cur-inc) - result (conj result current)] - (recur (first items) (rest items) remainder result)))))) + remainder (- remainder cur-inc)] + (recur (next items) remainder (conj! result current))))))) (defn distribute-space [prop prop-min prop-max min-value bound-value items] @@ -161,11 +174,11 @@ (recur remainder items)))))) (defn add-lines-positions - [parent layout-bounds layout-lines] + [parent layout-bounds auto? layout-lines] (let [row? (ctl/row? parent) col? (ctl/col? parent) - auto-width? (ctl/auto-width? parent) - auto-height? (ctl/auto-height? parent) + auto-width? (or (ctl/auto-width? parent) auto?) + auto-height? (or (ctl/auto-height? parent) auto?) space-evenly? (ctl/space-evenly? parent) space-around? (ctl/space-around? parent) @@ -179,7 +192,7 @@ [(+ total-width line-width) (+ total-height line-height)]) (add-ranges [[total-min-width total-min-height total-max-width total-max-height] - {:keys [line-min-width line-min-height line-max-width line-max-height]}] + {:keys [line-min-width line-min-height line-max-width line-max-height]}] [(+ total-min-width line-min-width) (+ total-min-height line-min-height) (+ total-max-width line-max-width) @@ -188,36 +201,24 @@ (add-starts [total-width total-height num-lines [result base-p] layout-line] (let [start-p (flp/get-start-line parent layout-bounds layout-line base-p total-width total-height num-lines) next-p (flp/get-next-line parent layout-bounds layout-line base-p total-width total-height num-lines)] + [(-> result (conj! (assoc layout-line :start-p start-p))) + next-p])) - [(conj result (assoc layout-line :start-p start-p)) - next-p]))] + (get-layout-width [{:keys [num-children]}] + (let [num-gap (cond space-evenly? (inc num-children) + space-around? num-children + :else (dec num-children))] + (- layout-width (* layout-gap-col num-gap)))) + + (get-layout-height [{:keys [num-children]}] + (let [num-gap (cond space-evenly? (inc num-children) + space-around? num-children + :else (dec num-children))] + (- layout-height (* layout-gap-row num-gap))))] (let [[total-min-width total-min-height total-max-width total-max-height] (->> layout-lines (reduce add-ranges [0 0 0 0])) - get-layout-width (fn [{:keys [num-children]}] - (let [num-gap (cond - space-evenly? - (inc num-children) - - space-around? - num-children - - :else - (dec num-children))] - (- layout-width (* layout-gap-col num-gap)))) - get-layout-height (fn [{:keys [num-children]}] - (let [num-gap (cond - space-evenly? - (inc num-children) - - space-around? - num-children - - :else - (dec num-children))] - (- layout-height (* layout-gap-row num-gap)))) - num-lines (count layout-lines) ;; When align-items is stretch we need to adjust the main axis size to grow for the full content @@ -235,36 +236,39 @@ rest-layout-width (- layout-width (* (dec num-lines) layout-gap-col)) ;; Distributes the space between the layout lines based on its max/min constraints + layout-lines (cond->> layout-lines row? (map #(assoc % :line-width - (if (ctl/auto-width? parent) + (if auto-width? (:line-min-width %) (max (:line-min-width %) (min (get-layout-width %) (:line-max-width %)))))) col? (map #(assoc % :line-height - (if (ctl/auto-height? parent) + (if auto-height? (:line-min-height %) (max (:line-min-height %) (min (get-layout-height %) (:line-max-height %)))))) - (and row? (or (>= total-min-height rest-layout-height) (ctl/auto-height? parent))) + (and row? (or (>= total-min-height rest-layout-height) auto-height?)) (map #(assoc % :line-height (:line-min-height %))) - (and row? (<= total-max-height rest-layout-height) (not (ctl/auto-height? parent))) + (and row? (<= total-max-height rest-layout-height) (not auto-height?)) (map #(assoc % :line-height (+ (:line-max-height %) stretch-height-fix))) - (and row? (< total-min-height rest-layout-height total-max-height) (not (ctl/auto-height? parent))) - (distribute-space :line-height :line-min-height :line-max-height total-min-height rest-layout-height) - - (and col? (or (>= total-min-width rest-layout-width) (ctl/auto-width? parent))) + (and col? (or (>= total-min-width rest-layout-width) auto-width?)) (map #(assoc % :line-width (:line-min-width %))) - (and col? (<= total-max-width rest-layout-width) (not (ctl/auto-width? parent))) - (map #(assoc % :line-width (+ (:line-max-width %) stretch-width-fix))) + (and col? (<= total-max-width rest-layout-width) (not auto-width?)) + (map #(assoc % :line-width (+ (:line-max-width %) stretch-width-fix)))) - (and col? (< total-min-width rest-layout-width total-max-width) (not (ctl/auto-width? parent))) + layout-lines + (cond->> layout-lines + (and row? (< total-min-height rest-layout-height total-max-height) (not auto-height?)) + (distribute-space :line-height :line-min-height :line-max-height total-min-height rest-layout-height) + + (and col? (< total-min-width rest-layout-width total-max-width) (not auto-width?)) (distribute-space :line-width :line-min-width :line-max-width total-min-width rest-layout-width)) ;; Add information to limit the growth of width: 100% shapes to the bounds of the layout @@ -274,19 +278,21 @@ (->> layout-lines (reduce (fn [[result rest-layout-height] {:keys [line-height] :as line}] - [(conj result (assoc line :to-bound-height rest-layout-height)) + [(conj! result (assoc line :to-bound-height rest-layout-height)) (- rest-layout-height line-height layout-gap-row)]) - [[] layout-height]) - (first)) + [(transient []) layout-height]) + (first) + (persistent!)) col? (->> layout-lines (reduce (fn [[result rest-layout-width] {:keys [line-width] :as line}] - [(conj result (assoc line :to-bound-width rest-layout-width)) + [(conj! result (assoc line :to-bound-width rest-layout-width)) (- rest-layout-width line-width layout-gap-col)]) - [[] layout-width]) - (first)) + [(transient []) layout-width]) + (first) + (persistent!)) :else layout-lines) @@ -295,19 +301,22 @@ base-p (flp/get-base-line parent layout-bounds total-width total-height num-lines)] - (first (reduce (partial add-starts total-width total-height num-lines) [[] base-p] layout-lines)))))) + (->> layout-lines + (reduce (partial add-starts total-width total-height num-lines) [(transient []) base-p]) + (first) + (persistent!)))))) (defn add-line-spacing "Calculates the baseline for a flex layout" - [shape layout-bounds {:keys [num-children line-width line-height] :as line-data}] + [shape layout-bounds auto? {:keys [num-children line-width line-height] :as line-data}] (let [width (gpo/width-points layout-bounds) height (gpo/height-points layout-bounds) row? (ctl/row? shape) col? (ctl/col? shape) - auto-height? (ctl/auto-height? shape) - auto-width? (ctl/auto-width? shape) + auto-height? (or (ctl/auto-height? shape) auto?) + auto-width? (or (ctl/auto-width? shape) auto?) space-between? (ctl/space-between? shape) space-evenly? (ctl/space-evenly? shape) space-around? (ctl/space-around? shape) @@ -403,22 +412,29 @@ (defn calc-layout-data "Digest the layout data to pass it to the constrains" - [shape children shape-bounds] + ([shape shape-bounds children bounds objects] + (calc-layout-data shape shape-bounds children bounds objects false)) - (let [layout-bounds (layout-bounds shape shape-bounds) - reverse? (ctl/reverse? shape) - children (cond->> children (not reverse?) reverse) + ([shape shape-bounds children bounds objects auto?] - ;; Don't take into account absolute children - children (->> children (remove (comp ctl/layout-absolute? second))) + (let [layout-bounds (layout-bounds shape shape-bounds) + reverse? (ctl/reverse? shape) + children (cond->> children (not reverse?) reverse) - ;; Creates the layout lines information - layout-lines - (->> (init-layout-lines shape children layout-bounds) - (add-lines-positions shape layout-bounds) - (into [] (comp (map (partial add-line-spacing shape layout-bounds)) - (map (partial add-children-resizes shape)))))] + ignore-child? + (fn [[_ child]] + (ctl/position-absolute? child)) - {:layout-lines layout-lines - :layout-bounds layout-bounds - :reverse? reverse?})) + ;; Don't take into account absolute children + children (->> children (remove ignore-child?)) + + ;; Creates the layout lines information + layout-lines + (->> (init-layout-lines shape children layout-bounds bounds objects auto?) + (add-lines-positions shape layout-bounds auto?) + (into [] (comp (map (partial add-line-spacing shape layout-bounds auto?)) + (map (partial add-children-resizes shape)))))] + + {:layout-lines layout-lines + :layout-bounds layout-bounds + :reverse? reverse?}))) diff --git a/common/src/app/common/geom/shapes/flex_layout/modifiers.cljc b/common/src/app/common/geom/shapes/flex_layout/modifiers.cljc index 9247f20bfe..4d95c38c47 100644 --- a/common/src/app/common/geom/shapes/flex_layout/modifiers.cljc +++ b/common/src/app/common/geom/shapes/flex_layout/modifiers.cljc @@ -34,7 +34,8 @@ (let [line-width (min line-width (or to-bound-width line-width)) target-width (max (- line-width (ctl/child-width-margin child)) 0.01) max-width (max (ctl/child-max-width child) 0.01) - target-width (mth/clamp target-width (ctl/child-min-width child) max-width) + child-min-width (get-in children-data [(:id child) :child-min-width]) + target-width (mth/clamp target-width child-min-width max-width) fill-scale (/ target-width child-width)] {:width target-width :modifiers (ctm/resize-modifiers (gpt/point fill-scale 1) child-origin transform transform-inverse)}))) @@ -58,13 +59,13 @@ (let [line-height (min line-height (or to-bound-height line-height)) target-height (max (- line-height (ctl/child-height-margin child)) 0.01) max-height (max (ctl/child-max-height child) 0.01) - target-height (mth/clamp target-height (ctl/child-min-height child) max-height) + child-min-height (get-in children-data [(:id child) :child-min-height]) + target-height (mth/clamp target-height child-min-height max-height) fill-scale (/ target-height child-height)] {:height target-height :modifiers (ctm/resize-modifiers (gpt/point 1 fill-scale) child-origin transform transform-inverse)}))) -(defn layout-child-modifiers - "Calculates the modifiers for the layout" +(defn fill-modifiers [parent parent-bounds child child-bounds layout-line] (let [child-origin (gpo/origin child-bounds) child-width (gpo/width-points child-bounds) @@ -83,15 +84,27 @@ (calc-fill-height-data parent transform transform-inverse child child-origin child-height layout-line)) child-width (or (:width fill-width) child-width) - child-height (or (:height fill-height) child-height) + child-height (or (:height fill-height) child-height)] + + [child-width + child-height + (-> (ctm/empty) + (cond-> fill-width (ctm/add-modifiers (:modifiers fill-width))) + (cond-> fill-height (ctm/add-modifiers (:modifiers fill-height))))])) + +(defn layout-child-modifiers + "Calculates the modifiers for the layout" + [parent parent-bounds child child-bounds layout-line] + (let [child-origin (gpo/origin child-bounds) + + [child-width child-height fill-modifiers] + (fill-modifiers parent parent-bounds child child-bounds layout-line) [corner-p layout-line] (fpo/get-child-position parent child child-width child-height layout-line) - move-vec (gpt/to-vec child-origin corner-p) modifiers (-> (ctm/empty) - (cond-> fill-width (ctm/add-modifiers (:modifiers fill-width))) - (cond-> fill-height (ctm/add-modifiers (:modifiers fill-height))) + (ctm/add-modifiers fill-modifiers) (ctm/move move-vec))] [modifiers layout-line])) diff --git a/common/src/app/common/geom/shapes/flex_layout/params.cljc b/common/src/app/common/geom/shapes/flex_layout/params.cljc new file mode 100644 index 0000000000..b1e15c70bd --- /dev/null +++ b/common/src/app/common/geom/shapes/flex_layout/params.cljc @@ -0,0 +1,99 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.shapes.flex-layout.params + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gco] + [app.common.math :as mth] + [app.common.types.shape-tree :as ctt])) + +(defn calculate-params + "Given the shapes calculate its flex parameters (horizontal vs vertical, gaps, etc)" + ([objects shapes] + (calculate-params objects shapes nil)) + + ([objects shapes parent] + (when (d/not-empty? shapes) + (let [shapes (->> shapes (remove :hidden)) + + points + (->> shapes + (map :id) + (ctt/sort-z-index objects) + (map (comp gco/shape->center (d/getf objects)))) + + start (first points) + end (reduce (fn [acc p] (gpt/add acc (gpt/to-vec start p))) points) + + angle (gpt/signed-angle-with-other + (gpt/to-vec start end) + (gpt/point 1 0)) + + angle (mod angle 360) + + t1 (min (abs (- angle 0)) (abs (- angle 360))) + t2 (abs (- angle 90)) + t3 (abs (- angle 180)) + t4 (abs (- angle 270)) + + tmin (min t1 t2 t3 t4) + + direction + (cond + (mth/close? tmin t1) :row + (mth/close? tmin t2) :column-reverse + (mth/close? tmin t3) :row-reverse + (mth/close? tmin t4) :column) + + selrects (->> shapes + (mapv :selrect)) + min-x (->> selrects + (mapv #(min (:x1 %) (:x2 %))) + (apply min)) + max-x (->> selrects + (mapv #(max (:x1 %) (:x2 %))) + (apply max)) + all-width (->> selrects + (map :width) + (reduce +)) + column-gap (if (and (> (count shapes) 1) + (or (= direction :row) (= direction :row-reverse))) + (/ (- (- max-x min-x) all-width) + (dec (count shapes))) + 0) + + min-y (->> selrects + (mapv #(min (:y1 %) (:y2 %))) + (apply min)) + max-y (->> selrects + (mapv #(max (:y1 %) (:y2 %))) + (apply max)) + all-height (->> selrects + (map :height) + (reduce +)) + row-gap (if (and (> (count shapes) 1) + (or (= direction :column) (= direction :column-reverse))) + (/ (- (- max-y min-y) all-height) + (dec (count shapes))) + 0) + + layout-gap {:row-gap (max row-gap 0) + :column-gap (max column-gap 0)} + + parent-selrect (:selrect parent) + + padding (when (and (not (nil? parent)) (> (count shapes) 0)) + {:p1 (- min-y (:y1 parent-selrect)) + :p2 (- min-x (:x1 parent-selrect))})] + + (cond-> {:layout-flex-dir direction :layout-gap layout-gap} + (not (nil? padding)) + (assoc :layout-padding {:p1 (:p1 padding) + :p2 (:p2 padding) + :p3 (:p1 padding) + :p4 (:p2 padding)})))))) diff --git a/common/src/app/common/geom/shapes/grid_layout.cljc b/common/src/app/common/geom/shapes/grid_layout.cljc index eb45960f89..29eef9699f 100644 --- a/common/src/app/common/geom/shapes/grid_layout.cljc +++ b/common/src/app/common/geom/shapes/grid_layout.cljc @@ -7,13 +7,17 @@ (ns app.common.geom.shapes.grid-layout (:require [app.common.data.macros :as dm] + [app.common.geom.shapes.grid-layout.bounds :as glpb] [app.common.geom.shapes.grid-layout.layout-data :as glld] + [app.common.geom.shapes.grid-layout.params :as glpr] [app.common.geom.shapes.grid-layout.positions :as glp])) (dm/export glld/calc-layout-data) (dm/export glld/get-cell-data) (dm/export glp/child-modifiers) - -(defn get-drop-index - [frame objects _position] - (dec (count (get-in objects [frame :shapes])))) +(dm/export glp/get-position-grid-coord) +(dm/export glp/get-drop-cell) +(dm/export glp/cell-bounds) +(dm/export glpb/layout-content-points) +(dm/export glpb/layout-content-bounds) +(dm/export glpr/calculate-params) diff --git a/common/src/app/common/geom/shapes/grid_layout/areas.cljc b/common/src/app/common/geom/shapes/grid_layout/areas.cljc new file mode 100644 index 0000000000..cb37d63d38 --- /dev/null +++ b/common/src/app/common/geom/shapes/grid_layout/areas.cljc @@ -0,0 +1,93 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +;; Based on the code in: +;; https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Rectangle_difference +(ns app.common.geom.shapes.grid-layout.areas + (:refer-clojure :exclude [contains?])) + +(defn area->cell-props [[column row column-span row-span]] + {:row row + :column column + :row-span row-span + :column-span column-span}) + +(defn make-area + ([{:keys [column row column-span row-span]}] + (make-area column row column-span row-span)) + ([x y width height] + [x y width height])) + +(defn contains? + [[a-x a-y a-width a-height :as a] + [b-x b-y b-width b-height :as b]] + (and (>= b-x a-x) + (>= b-y a-y) + (<= (+ b-x b-width) (+ a-x a-width)) + (<= (+ b-y b-height) (+ a-y a-height)))) + +(defn intersects? + [[a-x a-y a-width a-height] + [b-x b-y b-width b-height]] + (not (or (<= (+ b-x b-width) a-x) + (<= (+ b-y b-height) a-y) + (>= b-x (+ a-x a-width)) + (>= b-y (+ a-y a-height))))) + +(defn top-rect + [[a-x a-y a-width _] + [_ b-y _ _]] + (let [height (- b-y a-y)] + (when (> height 0) + (make-area a-x a-y a-width height)))) + +(defn bottom-rect + [[a-x a-y a-width a-height] + [_ b-y _ b-height]] + + (let [y (+ b-y b-height) + height (- a-height (- y a-y))] + (when (and (> height 0) (< y (+ a-y a-height))) + (make-area a-x y a-width height)))) + +(defn left-rect + [[a-x a-y _ a-height] + [b-x b-y _ b-height]] + + (let [rb-y (+ b-y b-height) + ra-y (+ a-y a-height) + y1 (max a-y b-y) + y2 (min ra-y rb-y) + height (- y2 y1) + width (- b-x a-x)] + (when (and (> width 0) (> height 0)) + (make-area a-x y1 width height)))) + +(defn right-rect + [[a-x a-y a-width a-height] + [b-x b-y b-width b-height]] + + (let [rb-y (+ b-y b-height) + ra-y (+ a-y a-height) + y1 (max a-y b-y) + y2 (min ra-y rb-y) + height (- y2 y1) + rb-x (+ b-x b-width) + width (- a-width (- rb-x a-x))] + (when (and (> width 0) (> height 0)) + (make-area rb-x y1 width height)))) + +(defn difference + [area-a area-b] + (if (or (nil? area-b) + (not (intersects? area-a area-b)) + (contains? area-b area-a)) + [] + + (into [] + (keep #(% area-a area-b)) + [top-rect left-rect right-rect bottom-rect]))) + diff --git a/common/src/app/common/geom/shapes/grid_layout/bounds.cljc b/common/src/app/common/geom/shapes/grid_layout/bounds.cljc new file mode 100644 index 0000000000..e5b9cf011c --- /dev/null +++ b/common/src/app/common/geom/shapes/grid_layout/bounds.cljc @@ -0,0 +1,47 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.shapes.grid-layout.bounds + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.points :as gpo])) + +(defn layout-content-points + [bounds parent {:keys [row-tracks column-tracks]}] + (let [parent-id (:id parent) + parent-bounds @(get bounds parent-id) + + hv #(gpo/start-hv parent-bounds %) + vv #(gpo/start-vv parent-bounds %)] + (d/concat-vec + (->> row-tracks + (mapcat #(vector (:start-p %) + (gpt/add (:start-p %) (vv (:size %)))))) + (->> column-tracks + (mapcat #(vector (:start-p %) + (gpt/add (:start-p %) (hv (:size %))))))))) + +(defn layout-content-bounds + [bounds {:keys [layout-padding] :as parent} layout-data] + + (let [parent-id (:id parent) + parent-bounds @(get bounds parent-id) + + {pad-top :p1 pad-right :p2 pad-bottom :p3 pad-left :p4} layout-padding + pad-top (or pad-top 0) + pad-right (or pad-right 0) + pad-bottom (or pad-bottom 0) + pad-left (or pad-left 0) + + layout-points (layout-content-points bounds parent layout-data)] + + (if (d/not-empty? layout-points) + (-> layout-points + (gpo/merge-parent-coords-bounds parent-bounds) + (gpo/pad-points (- pad-top) (- pad-right) (- pad-bottom) (- pad-left))) + ;; Cannot create some bounds from the children so we return the parent's + parent-bounds))) diff --git a/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc b/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc index e797b1c643..aab5858450 100644 --- a/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc +++ b/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc @@ -4,137 +4,604 @@ ;; ;; Copyright (c) KALEIDOS INC +;; Each track has specified minimum and maximum sizing functions (which may be the same) +;; - Fixed +;; - Percent +;; - Auto +;; - Flex +;; +;; Min functions: +;; - Fixed: value +;; - Percent: value to pixels +;; - Auto: auto +;; - Flex: auto +;; +;; Max functions: +;; - Fixed: value +;; - Percent: value to pixels +;; - Auto: max-content +;; - Flex: flex + +;; Algorithm +;; - Initialize tracks: +;; * base = size or 0 +;; * max = size or INF +;; +;; - Resolve intrinsic sizing +;; 1. Shim baseline-aligned items so their intrinsic size contributions reflect their baseline alignment +;; +;; 2. Size tracks to fit non-spanning items +;; base-size = max (children min contribution) floored 0 +;; +;; 3. Increase sizes to accommodate spanning items crossing content-sized tracks +;; +;; 4. Increase sizes to accommodate spanning items crossing flexible tracks: +;; +;; 5. If any track still has an infinite growth limit set its growth limit to its base size. + +;; - Distribute extra space accross spaned tracks +;; - Maximize tracks +;; +;; - Expand flexible tracks +;; - Find `fr` size +;; +;; - Stretch auto tracks + (ns app.common.geom.shapes.grid-layout.layout-data (:require + [app.common.data :as d] [app.common.geom.point :as gpt] - [app.common.geom.shapes.points :as gpo])) + [app.common.geom.shapes.points :as gpo] + [app.common.math :as mth] + [app.common.types.shape.layout :as ctl])) -#_(defn set-sample-data - [parent children] +;; Setted in app.common.geom.shapes.common-layout +;; We do it this way because circular dependencies +(def -child-min-width nil) - (let [parent (assoc parent - :layout-grid-columns - [{:type :percent :value 25} - {:type :percent :value 25} - {:type :fixed :value 100} - ;;{:type :auto} - ;;{:type :flex :value 1} - ] +(defn child-min-width + [child child-bounds bounds objects] + (+ (ctl/child-width-margin child) + (-child-min-width child child-bounds bounds objects true))) - :layout-grid-rows - [{:type :percent :value 50} - {:type :percent :value 50} - ;;{:type :fixed :value 100} - ;;{:type :auto} - ;;{:type :flex :value 1} - ]) +(def -child-min-height nil) - num-rows (count (:layout-grid-rows parent)) - num-columns (count (:layout-grid-columns parent)) +(defn child-min-height + [child child-bounds bounds objects] + (+ (ctl/child-height-margin child) + (-child-min-height child child-bounds bounds objects true))) - layout-grid-cells - (into - {} - (for [[row-idx _row] (d/enumerate (:layout-grid-rows parent)) - [col-idx _col] (d/enumerate (:layout-grid-columns parent))] - (let [[_bounds shape] (nth children (+ (* row-idx num-columns) col-idx) nil) - cell-data {:id (uuid/next) - :row (inc row-idx) - :column (inc col-idx) - :row-span 1 - :col-span 1 - :shapes (when shape [(:id shape)])}] - [(:id cell-data) cell-data]))) +(defn layout-bounds + [parent shape-bounds] + (let [[pad-top pad-right pad-bottom pad-left] (ctl/paddings parent)] + (gpo/pad-points shape-bounds pad-top pad-right pad-bottom pad-left))) - parent (assoc parent :layout-grid-cells layout-grid-cells)] +(defn calculate-initial-track-size + [total-value {:keys [type value] :as track}] - [parent children])) + (let [[size max-size] + (case type + :percent + (let [value (/ (* total-value value) 100)] + [value value]) -(defn calculate-initial-track-values - [{:keys [type value]} total-value] + :fixed + [value value] - (case type - :percent - (let [value (/ (* total-value value) 100) ] - value) + ;; flex, auto + [0.01 ##Inf])] + (assoc track :size size :max-size max-size))) - :fixed - value +(defn set-auto-base-size + [track-list children shape-cells bounds objects type] - :auto - 0 - )) + (let [[prop prop-span size-fn] + (if (= type :column) + [:column :column-span child-min-width] + [:row :row-span child-min-height])] + + (reduce (fn [tracks [child-bounds child-shape]] + (let [cell (get shape-cells (:id child-shape)) + idx (dec (get cell prop)) + track (get tracks idx)] + (cond-> tracks + (and (= (get cell prop-span) 1) + (contains? #{:flex :auto} (:type track))) + (update-in [idx :size] max (size-fn child-shape child-bounds bounds objects))))) + track-list + children))) + +(defn tracks-total-size + [track-list] + (let [calc-tracks-total-size + (fn [acc {:keys [size]}] + (+ acc size))] + (->> track-list (reduce calc-tracks-total-size 0)))) + +(defn tracks-total-frs + [track-list] + (let [calc-tracks-total-frs + (fn [acc {:keys [type value]}] + (let [value (max 1 value)] + (cond-> acc + (= type :flex) + (+ value))))] + (->> track-list (reduce calc-tracks-total-frs 0)))) + +(defn tracks-total-autos + [track-list] + (let [calc-tracks-total-autos + (fn [acc {:keys [type]}] + (cond-> acc (= type :auto) (inc)))] + (->> track-list (reduce calc-tracks-total-autos 0)))) + + +(defn set-fr-value + "Tries to assign the fr value distributing the excess between the free spaces" + [track-list fr-value auto?] + + (let [flex? #(= :flex (:type (second %))) + + ;; Fixes the assignments so they respect the min size constraint + ;; returns pending with the necessary space to allocate and free-frs + ;; are the addition of the fr tracks with free space + assign-fn + (fn [[assign-fr pending free-frs] [idx t]] + (let [fr (:value t) + current (get assign-fr idx (* fr-value fr)) + full? (<= current (:size t)) + cur-pending (if full? (- (:size t) current) 0)] + [(assoc assign-fr idx (if full? (:size t) current)) + (+ pending cur-pending) + (cond-> free-frs (not full?) (+ fr))])) + + ;; Sets the assigned-fr map removing the pending/free-frs + change-fn + (fn [delta] + (fn [assign-fr [idx t]] + (let [fr (:value t) + current (get assign-fr idx) + full? (<= current (:size t))] + (cond-> assign-fr + (not full?) + (update idx - (* delta fr)))))) + + assign-fr + (loop [assign-fr {}] + (let [[assign-fr pending free-frs] + (->> (d/enumerate track-list) + (filter flex?) + (reduce assign-fn [assign-fr 0 0]))] + + ;; When auto, we don't need to remove the excess + (if (or auto? + (= free-frs 0) + (mth/almost-zero? pending)) + assign-fr + + (let [delta (/ pending free-frs) + assign-fr + (->> (d/enumerate track-list) + (filter flex?) + (reduce (change-fn delta) assign-fr))] + + (recur assign-fr))))) + + ;; Apply assign-fr to the track-list + track-list + (reduce + (fn [track-list [idx assignment]] + (-> track-list + (update-in [idx :size] max assignment))) + track-list + assign-fr)] + + track-list)) + +(defn add-auto-size + [track-list add-size] + (->> track-list + (mapv (fn [{:keys [type size max-size] :as track}] + (cond-> track + (= :auto type) + (assoc :size (min (+ size add-size) max-size))))))) + +(defn has-flex-track? + [type track-list cell] + (let [[prop prop-span] + (if (= type :column) + [:column :column-span] + [:row :row-span]) + from-idx (dec (get cell prop)) + to-idx (+ (dec (get cell prop)) (get cell prop-span)) + tracks (subvec track-list from-idx to-idx)] + (some? (->> tracks (d/seek #(= :flex (:type %))))))) + +(defn size-to-allocate + [type parent [child-bounds child] cell bounds objects] + (let [[row-gap column-gap] (ctl/gaps parent) + [sfn gap prop-span] + (if (= type :column) + [child-min-width column-gap :column-span] + [child-min-height row-gap :row-span]) + span (get cell prop-span)] + (- (sfn child child-bounds bounds objects) (* gap (dec span))))) + +(defn allocate-auto-tracks + [allocations indexed-tracks to-allocate] + (if (empty? indexed-tracks) + [allocations to-allocate] + (let [[idx track] (first indexed-tracks) + old-allocated (get allocations idx 0.01) + auto-track? (= :auto (:type track)) + + allocated + (if auto-track? + (max old-allocated + (/ to-allocate (count indexed-tracks)) + (:size track)) + (:size track))] + (recur (cond-> allocations + auto-track? + (assoc idx allocated)) + (rest indexed-tracks) + (- to-allocate allocated))))) + +(defn allocate-flex-tracks + [allocations indexed-tracks to-allocate fr-value] + (if (empty? indexed-tracks) + allocations + (let [[idx track] (first indexed-tracks) + old-allocated (get allocations idx 0.01) + + auto-track? (= :auto (:type track)) + flex-track? (= :flex (:type track)) + + fr (if flex-track? (:value track) 0) + + target-allocation (* fr-value fr) + + allocated (if (or auto-track? flex-track?) + (max target-allocation + old-allocated + (:size track)) + (:size track))] + (recur (cond-> allocations (or flex-track? auto-track?) + (assoc idx allocated)) + (rest indexed-tracks) + (- to-allocate allocated) + fr-value)))) + +(defn set-auto-multi-span + [parent track-list children-map shape-cells bounds objects type] + + (let [[prop prop-span] + (if (= type :column) + [:column :column-span] + [:row :row-span]) + + ;; First calculate allocation without applying so we can modify them on the following tracks + allocated + (->> shape-cells + (vals) + (filter #(> (get % prop-span) 1)) + (remove #(has-flex-track? type track-list %)) + (sort-by prop-span -) + (reduce + (fn [allocated cell] + (let [shape-id (first (:shapes cell)) + + from-idx (dec (get cell prop)) + to-idx (+ (dec (get cell prop)) (get cell prop-span)) + + indexed-tracks (subvec (d/enumerate track-list) from-idx to-idx) + to-allocate (size-to-allocate type parent (get children-map shape-id) cell bounds objects) + + ;; Remove the size and the tracks that are not allocated + [to-allocate indexed-tracks] + (->> indexed-tracks + (reduce (fn find-auto-allocations + [[to-allocate result] [_ track :as idx-track]] + (if (= :auto (:type track)) + ;; If auto, we don't change allocate and add the track + [to-allocate (conj result idx-track)] + ;; If fixed, we remove from allocate and don't add the track + [(- to-allocate (:size track)) result])) + [to-allocate []])) + + non-assigned-indexed-tracks + (->> indexed-tracks + (remove (fn [[idx _]] (contains? allocated idx)))) + + ;; First we try to assign into the non-assigned tracks + [allocated to-allocate] + (allocate-auto-tracks allocated non-assigned-indexed-tracks (max to-allocate 0)) + + ;; In the second pass we use every track for the rest of the space + [allocated _] + (allocate-auto-tracks allocated indexed-tracks (max to-allocate 0))] + + allocated)) + {})) + + ;; Apply the allocations to the tracks + track-list + (into [] + (map-indexed #(update %2 :size max (get allocated %1))) + track-list)] + track-list)) + +(defn set-flex-multi-span + [parent track-list children-map shape-cells bounds objects type] + + (let [[prop prop-span] + (if (= type :column) + [:column :column-span] + [:row :row-span]) + + ;; First calculate allocation without applying so we can modify them on the following tracks + allocate-fr-tracks + (->> shape-cells + (vals) + (filter #(> (get % prop-span) 1)) + (filter #(has-flex-track? type track-list %)) + (sort-by prop-span -) + (reduce + (fn [alloc cell] + (let [shape-id (first (:shapes cell)) + from-idx (dec (get cell prop)) + to-idx (+ (dec (get cell prop)) (get cell prop-span)) + indexed-tracks (subvec (d/enumerate track-list) from-idx to-idx) + + to-allocate (size-to-allocate type parent (get children-map shape-id) cell bounds objects) + + ;; Remove the size and the tracks that are not allocated + [to-allocate total-frs indexed-tracks] + (->> indexed-tracks + (reduce (fn find-lex-allocations + [[to-allocate total-fr result] [_ track :as idx-track]] + (if (= :flex (:type track)) + ;; If flex, we don't change allocate and add the track + [to-allocate (+ total-fr (:value track)) (conj result idx-track)] + + ;; If fixed or auto, we remove from allocate and don't add the track + [(- to-allocate (:size track)) total-fr result])) + [to-allocate 0 []])) + + to-allocate (max to-allocate 0) + fr-value (/ to-allocate total-frs)] + (allocate-flex-tracks alloc indexed-tracks to-allocate fr-value))) + {})) + + ;; Apply the allocations to the tracks + track-list + (into [] + (map-indexed #(update %2 :size max (get allocate-fr-tracks %1))) + track-list)] + track-list)) + +(defn min-fr-value + [tracks] + (loop [tracks (seq tracks) + min-fr 0.01] + (if (empty? tracks) + min-fr + (let [{:keys [size type value]} (first tracks) + min-fr (if (= type :flex) (max min-fr (/ size value)) min-fr)] + (recur (rest tracks) min-fr))))) (defn calc-layout-data - [parent _children transformed-parent-bounds] + ([parent transformed-parent-bounds children bounds objects] + (calc-layout-data parent transformed-parent-bounds children bounds objects false)) - (let [height (gpo/height-points transformed-parent-bounds) - width (gpo/width-points transformed-parent-bounds) + ([parent transformed-parent-bounds children bounds objects auto?] + (let [hv #(gpo/start-hv transformed-parent-bounds %) + vv #(gpo/start-vv transformed-parent-bounds %) - ;; Initialize tracks - column-tracks - (->> (:layout-grid-columns parent) - (map (fn [track] - (let [initial (calculate-initial-track-values track width)] - (assoc track :value initial))))) + layout-bounds (layout-bounds parent transformed-parent-bounds) - row-tracks - (->> (:layout-grid-rows parent) - (map (fn [track] - (let [initial (calculate-initial-track-values track height)] - (assoc track :value initial))))) + bound-height (gpo/height-points layout-bounds) + bound-width (gpo/width-points layout-bounds) + bound-corner (gpo/origin layout-bounds) - ;; Go through cells to adjust auto sizes + [row-gap column-gap] (ctl/gaps parent) + auto-height? (or (ctl/auto-height? parent) auto?) + auto-width? (or (ctl/auto-width? parent) auto?) + {:keys [layout-grid-columns layout-grid-rows layout-grid-cells]} parent + num-columns (count layout-grid-columns) + num-rows (count layout-grid-rows) - ;; Once auto sizes have been calculated we get calculate the `fr` with the remainining size and adjust the size + column-total-gap (* column-gap (dec num-columns)) + row-total-gap (* row-gap (dec num-rows)) + ;; Map shape->cell + shape-cells + (into {} + (mapcat (fn [[_ cell]] + (->> (:shapes cell) (map #(vector % cell))))) + layout-grid-cells) - ;; Adjust final distances + children + (->> children + (remove #(ctl/position-absolute? (second %)))) - acc-track-distance - (fn [[result next-distance] data] - (let [result (conj result (assoc data :distance next-distance)) - next-distance (+ next-distance (:value data))] - [result next-distance])) + children-map + (into {} + (map #(vector (:id (second %)) %)) + children) - column-tracks - (->> column-tracks - (reduce acc-track-distance [[] 0]) - first) + ;; Initialize tracks + column-tracks + (->> layout-grid-columns + (mapv (partial calculate-initial-track-size bound-width))) - row-tracks - (->> row-tracks - (reduce acc-track-distance [[] 0]) - first) + row-tracks + (->> layout-grid-rows + (mapv (partial calculate-initial-track-size bound-height))) - shape-cells - (into {} - (mapcat (fn [[_ cell]] - (->> (:shapes cell) - (map #(vector % cell))))) - (:layout-grid-cells parent)) - ] + ;; Go through cells to adjust auto sizes for span=1. Base is the max of its children + column-tracks (set-auto-base-size column-tracks children shape-cells bounds objects :column) + row-tracks (set-auto-base-size row-tracks children shape-cells bounds objects :row) - {:row-tracks row-tracks - :column-tracks column-tracks - :shape-cells shape-cells})) + ;; Adjust multi-spaned cells with no flex columns + column-tracks (set-auto-multi-span parent column-tracks children-map shape-cells bounds objects :column) + row-tracks (set-auto-multi-span parent row-tracks children-map shape-cells bounds objects :row) + + ;; Calculate the `fr` unit and adjust the size + column-total-size-nofr (tracks-total-size (->> column-tracks (remove #(= :flex (:type %))))) + row-total-size-nofr (tracks-total-size (->> row-tracks (remove #(= :flex (:type %))))) + + column-frs (tracks-total-frs column-tracks) + row-frs (tracks-total-frs row-tracks) + + ;; Assign minimum size to the multi-span flex tracks. We do this after calculating + ;; the fr size because will affect only the minimum. The maximum will be set by the + ;; fracion + column-tracks (set-flex-multi-span parent column-tracks children-map shape-cells bounds objects :column) + row-tracks (set-flex-multi-span parent row-tracks children-map shape-cells bounds objects :row) + + ;; Once auto sizes have been calculated we get calculate the `fr` unit with the remainining size and adjust the size + free-column-space (max 0 (- bound-width (+ column-total-size-nofr column-total-gap))) + free-row-space (max 0 (- bound-height (+ row-total-size-nofr row-total-gap))) + + ;; Get the minimum values for fr's + min-column-fr (min-fr-value column-tracks) + min-row-fr (min-fr-value row-tracks) + + column-fr (if auto-width? min-column-fr (mth/finite (/ free-column-space column-frs) 0)) + row-fr (if auto-height? min-row-fr (mth/finite (/ free-row-space row-frs) 0)) + + column-tracks (set-fr-value column-tracks column-fr auto-width?) + row-tracks (set-fr-value row-tracks row-fr auto-height?) + + ;; Distribute free space between `auto` tracks + column-total-size (tracks-total-size column-tracks) + row-total-size (tracks-total-size row-tracks) + + free-column-space (max 0 (if auto-width? 0 (- bound-width (+ column-total-size column-total-gap)))) + free-row-space (max 0 (if auto-height? 0 (- bound-height (+ row-total-size row-total-gap)))) + column-autos (tracks-total-autos column-tracks) + row-autos (tracks-total-autos row-tracks) + + column-add-auto (/ free-column-space column-autos) + row-add-auto (/ free-row-space row-autos) + + column-tracks (cond-> column-tracks + (= :stretch (:layout-justify-content parent)) + (add-auto-size column-add-auto)) + + row-tracks (cond-> row-tracks + (= :stretch (:layout-align-content parent)) + (add-auto-size row-add-auto)) + + column-total-size (tracks-total-size column-tracks) + row-total-size (tracks-total-size row-tracks) + + num-columns (count column-tracks) + column-gap + (case (:layout-justify-content parent) + auto-width? + column-gap + + :space-evenly + (max column-gap (/ (- bound-width column-total-size) (inc num-columns))) + + :space-around + (max column-gap (/ (- bound-width column-total-size) num-columns)) + + :space-between + (max column-gap (if (= num-columns 1) column-gap (/ (- bound-width column-total-size) (dec num-columns)))) + + column-gap) + + num-rows (count row-tracks) + row-gap + (case (:layout-align-content parent) + auto-height? + row-gap + + :space-evenly + (max row-gap (/ (- bound-height row-total-size) (inc num-rows))) + + :space-around + (max row-gap (/ (- bound-height row-total-size) num-rows)) + + :space-between + (max row-gap (if (= num-rows 1) row-gap (/ (- bound-height row-total-size) (dec num-rows)))) + + row-gap) + + start-p + (cond-> bound-corner + (and (not auto-width?) (= :end (:layout-justify-content parent))) + (gpt/add (hv (- bound-width (+ column-total-size column-total-gap)))) + + (and (not auto-width?) (= :center (:layout-justify-content parent))) + (gpt/add (hv (/ (- bound-width (+ column-total-size column-total-gap)) 2))) + + (and (not auto-height?) (= :end (:layout-align-content parent))) + (gpt/add (vv (- bound-height (+ row-total-size row-total-gap)))) + + (and (not auto-height?) (= :center (:layout-align-content parent))) + (gpt/add (vv (/ (- bound-height (+ row-total-size row-total-gap)) 2))) + + (and (not auto-width?) (= :space-around (:layout-justify-content parent))) + (gpt/add (hv (/ column-gap 2))) + + (and (not auto-width?) (= :space-evenly (:layout-justify-content parent))) + (gpt/add (hv column-gap)) + + (and (not auto-height?) (= :space-around (:layout-align-content parent))) + (gpt/add (vv (/ row-gap 2))) + + (and (not auto-height?) (= :space-evenly (:layout-align-content parent))) + (gpt/add (vv row-gap))) + + column-tracks + (->> column-tracks + (reduce (fn [[tracks start-p] {:keys [size] :as track}] + [(conj tracks (assoc track :start-p start-p)) + (gpt/add start-p (hv (+ size column-gap)))]) + [[] start-p]) + (first)) + + row-tracks + (->> row-tracks + (reduce (fn [[tracks start-p] {:keys [size] :as track}] + [(conj tracks (assoc track :start-p start-p)) + (gpt/add start-p (vv (+ size row-gap)))]) + [[] start-p]) + (first))] + + {:origin start-p + :layout-bounds layout-bounds + :row-tracks row-tracks + :column-tracks column-tracks + :shape-cells shape-cells + :column-gap column-gap + :row-gap row-gap + + ;; Convenient informaton for visualization + :column-total-size column-total-size + :column-total-gap column-total-gap + :row-total-size row-total-size + :row-total-gap row-total-gap}))) (defn get-cell-data - [{:keys [row-tracks column-tracks shape-cells]} transformed-parent-bounds [_child-bounds child]] + [{:keys [origin row-tracks column-tracks shape-cells]} _transformed-parent-bounds [_ child]] - (let [origin (gpo/origin transformed-parent-bounds) - hv #(gpo/start-hv transformed-parent-bounds %) - vv #(gpo/start-vv transformed-parent-bounds %) - - grid-cell (get shape-cells (:id child))] - - (when (some? grid-cell) + (let [grid-cell (get shape-cells (:id child))] + (when (and (some? grid-cell) (d/not-empty? grid-cell)) (let [column (nth column-tracks (dec (:column grid-cell)) nil) row (nth row-tracks (dec (:row grid-cell)) nil) - start-p (-> origin - (gpt/add (hv (:distance column))) - (gpt/add (vv (:distance row))))] + column-start-p (:start-p column) + row-start-p (:start-p row) + + start-p (gpt/add origin + (gpt/add + (gpt/to-vec origin column-start-p) + (gpt/to-vec origin row-start-p)))] (assoc grid-cell :start-p start-p))))) diff --git a/common/src/app/common/geom/shapes/grid_layout/params.cljc b/common/src/app/common/geom/shapes/grid_layout/params.cljc new file mode 100644 index 0000000000..befd93e688 --- /dev/null +++ b/common/src/app/common/geom/shapes/grid_layout/params.cljc @@ -0,0 +1,167 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.shapes.grid-layout.params + (:require + [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] + [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.points :as gpo] + [app.common.math :as mth] + [app.common.types.shape.layout :as ctl] + [clojure.set :as set])) + +;; Small functions to help with ranges +(defn rect->range + "Creates ranges" + [axis rect] + (let [start (get (gpt/point rect) axis)] + (if (= axis :x) + [start (+ start (:width rect))] + [start (+ start (:height rect))]))) + +(defn overlaps-range? + "Return true if the ranges overlaps in the given axis" + [axis [start-a end-a] rect] + (let [[start-b end-b] (rect->range axis rect)] + (or (< start-a start-b end-a) + (< start-b start-a end-b) + (mth/close? start-a start-b) + (mth/close? end-a end-b)))) + +(defn join-range + "Creates a new range given the rect" + [axis [start-a end-a :as range] rect] + (if (not range) + (rect->range axis rect) + (let [[start-b end-b] (rect->range axis rect)] + [(min start-a start-b) (max end-a end-b)]))) + +(defn size-range + [[start end]] + (- end start)) + +(defn calculate-tracks + "Given the geometry and the axis calculates the tracks for the given shapes" + [axis shapes-by-axis] + (loop [pending (seq shapes-by-axis) + result [] + index 1 + current-track #{} + current-range nil] + (if pending + (let [[next-shape rect :as next-shape+rects] (first pending)] + + (if (or (not current-range) (overlaps-range? axis current-range rect)) + ;; Add shape to current row + (let [current-track (conj current-track (:id next-shape)) + current-range (join-range axis current-range rect)] + (recur (next pending) result index current-track current-range)) + + ;; New row + (recur (next pending) + (conj result {:index index + :shapes current-track + :size (size-range current-range)}) + (inc index) + #{(:id next-shape)} + (rect->range axis rect)))) + + ;; Add the still ongoing row + (conj result {:index index + :shapes current-track + :size (size-range current-range)})))) + +(defn assign-shape-cells + "Create cells for the defined tracks and assign the shapes to these cells" + [params rows cols] + + (let [assign-cell + (fn [[params auto?] row column] + (let [row-num (:index row) + column-num (:index column) + cell (ctl/cell-by-row-column params row-num column-num) + shape (first (set/intersection (:shapes row) (:shapes column))) + auto? (and auto? (some? shape))] + + [(cond-> params + (some? shape) + (assoc-in [:layout-grid-cells (:id cell) :shapes] [shape]) + + (not auto?) + (assoc-in [:layout-grid-cells (:id cell) :position] :manual)) + auto?])) + + [params _] + (->> rows + (reduce + (fn [result row] + (->> cols + (reduce + #(assign-cell %1 row %2) + result))) + [params true]))] + params)) + +(defn calculate-params + "Given the shapes calculate its grid parameters (horizontal vs vertical, gaps, etc)" + ([objects shapes] + (calculate-params objects shapes nil)) + + ([_objects shapes parent] + (if (empty? shapes) + (-> {:layout-grid-columns [ctl/default-track-value ctl/default-track-value] + :layout-grid-rows [ctl/default-track-value ctl/default-track-value]} + (ctl/create-cells [1 1 2 2])) + + (let [shapes (->> shapes (remove :hidden)) + all-shapes-rect (gco/shapes->rect shapes) + shapes+bounds + (->> shapes + (map #(vector % (grc/points->rect (get % :points))))) + + shapes-by-x + (->> shapes+bounds + (sort-by (comp :x second))) + + shapes-by-y + (->> shapes+bounds + (sort-by (comp :y second))) + + cols (calculate-tracks :x shapes-by-x) + rows (calculate-tracks :y shapes-by-y) + + num-cols (count cols) + num-rows (count rows) + + total-cols-width (->> cols (reduce #(+ %1 (:size %2)) 0)) + total-rows-height (->> rows (reduce #(+ %1 (:size %2)) 0)) + + column-gap + (if (= num-cols 1) + 0 + (/ (- (:width all-shapes-rect) total-cols-width) (dec num-cols))) + + row-gap + (if (= num-rows 1) + 0 + (/ (- (:height all-shapes-rect) total-rows-height) (dec num-rows))) + + layout-grid-rows (mapv (constantly ctl/default-track-value) rows) + layout-grid-columns (mapv (constantly ctl/default-track-value) cols) + + parent-childs-vector (gpt/to-vec (gpo/origin (:points parent)) (gpt/point all-shapes-rect)) + p-left (:x parent-childs-vector) + p-top (:y parent-childs-vector)] + + (-> {:layout-grid-columns layout-grid-columns + :layout-grid-rows layout-grid-rows + :layout-gap {:row-gap row-gap + :column-gap column-gap} + :layout-padding {:p1 p-top :p2 p-left :p3 p-top :p4 p-left} + :layout-grid-dir (if (> num-cols num-rows) :row :column)} + (ctl/create-cells [1 1 num-cols num-rows]) + (assign-shape-cells rows cols)))))) diff --git a/common/src/app/common/geom/shapes/grid_layout/positions.cljc b/common/src/app/common/geom/shapes/grid_layout/positions.cljc index 3f81928b48..6ab0c0bb75 100644 --- a/common/src/app/common/geom/shapes/grid_layout/positions.cljc +++ b/common/src/app/common/geom/shapes/grid_layout/positions.cljc @@ -6,11 +6,246 @@ (ns app.common.geom.shapes.grid-layout.positions (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.line :as gl] [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.grid-layout.layout-data :as ld] [app.common.geom.shapes.points :as gpo] - [app.common.types.modifiers :as ctm])) + [app.common.geom.shapes.transforms :as gtr] + [app.common.math :as mth] + [app.common.types.modifiers :as ctm] + [app.common.types.shape.layout :as ctl])) + +(defn cell-bounds + "Retrieves the points that define the bounds for given cell" + [{:keys [origin row-tracks column-tracks layout-bounds column-gap row-gap] :as layout-data} {:keys [row column row-span column-span] :as cell}] + + (let [hv #(gpo/start-hv layout-bounds %) + vv #(gpo/start-vv layout-bounds %) + + span-column-tracks (d/safe-subvec column-tracks (dec column) (+ (dec column) column-span)) + span-row-tracks (d/safe-subvec row-tracks (dec row) (+ (dec row) row-span))] + + (when (and span-column-tracks span-row-tracks) + (let [p1 + (gpt/add + origin + (gpt/add + (gpt/to-vec origin (dm/get-in span-column-tracks [0 :start-p])) + (gpt/to-vec origin (dm/get-in span-row-tracks [0 :start-p])))) + + p2 + (as-> p1 $ + (reduce (fn [p track] (gpt/add p (hv (:size track)))) $ span-column-tracks) + (gpt/add $ (hv (* column-gap (dec (count span-column-tracks)))))) + + p3 + (as-> p2 $ + (reduce (fn [p track] (gpt/add p (vv (:size track)))) $ span-row-tracks) + (gpt/add $ (vv (* row-gap (dec (count span-row-tracks)))))) + + p4 + (as-> p1 $ + (reduce (fn [p track] (gpt/add p (vv (:size track)))) $ span-row-tracks) + (gpt/add $ (vv (* row-gap (dec (count span-row-tracks))))))] + [p1 p2 p3 p4])))) + +(defn calc-fill-width-data + "Calculates the size and modifiers for the width of an auto-fill child" + [_parent + transform + transform-inverse + child + child-origin child-width + cell-bounds] + + (let [target-width (max (- (gpo/width-points cell-bounds) (ctl/child-width-margin child)) 0.01) + max-width (max (ctl/child-max-width child) 0.01) + target-width (mth/clamp target-width (ctl/child-min-width child) max-width) + fill-scale (/ target-width child-width)] + {:width target-width + :modifiers (ctm/resize-modifiers (gpt/point fill-scale 1) child-origin transform transform-inverse)})) + +(defn calc-fill-height-data + "Calculates the size and modifiers for the height of an auto-fill child" + [_parent + transform transform-inverse + child + child-origin child-height + cell-bounds] + (let [target-height (max (- (gpo/height-points cell-bounds) (ctl/child-height-margin child)) 0.01) + max-height (max (ctl/child-max-height child) 0.01) + target-height (mth/clamp target-height (ctl/child-min-height child) max-height) + fill-scale (/ target-height child-height)] + {:height target-height + :modifiers (ctm/resize-modifiers (gpt/point 1 fill-scale) child-origin transform transform-inverse)})) + +(defn fill-modifiers + [parent parent-bounds child child-bounds layout-data cell-data] + (let [child-origin (gpo/origin child-bounds) + child-width (gpo/width-points child-bounds) + child-height (gpo/height-points child-bounds) + + cell-bounds (cell-bounds layout-data cell-data) + + [_ transform transform-inverse] + (when (or (ctl/fill-width? child) (ctl/fill-height? child)) + (gtr/calculate-geometry @parent-bounds)) + + fill-width + (when (ctl/fill-width? child) + (calc-fill-width-data parent transform transform-inverse child child-origin child-width cell-bounds)) + + fill-height + (when (ctl/fill-height? child) + (calc-fill-height-data parent transform transform-inverse child child-origin child-height cell-bounds)) + + child-width (or (:width fill-width) child-width) + child-height (or (:height fill-height) child-height)] + + [child-width + child-height + (-> (ctm/empty) + (cond-> fill-width (ctm/add-modifiers (:modifiers fill-width))) + (cond-> fill-height (ctm/add-modifiers (:modifiers fill-height))))])) + +(defn child-position-delta + [parent child child-bounds child-width child-height layout-data cell-data] + (let [cell-bounds (cell-bounds layout-data cell-data) + child-origin (gpo/origin child-bounds) + + align (:layout-align-items parent) + justify (:layout-justify-items parent) + align-self (:align-self cell-data) + justify-self (:justify-self cell-data) + + align-self (when (and align-self (not= align-self :auto)) align-self) + justify-self (when (and justify-self (not= justify-self :auto)) justify-self) + + align (or align-self align) + justify (or justify-self justify) + + origin-h (gpo/project-point cell-bounds :h child-origin) + origin-v (gpo/project-point cell-bounds :v child-origin) + hv (partial gpo/start-hv cell-bounds) + vv (partial gpo/start-vv cell-bounds) + + [top-m right-m bottom-m left-m] (ctl/child-margins child) + + ;; Adjust alignment/justify + [from-h to-h] + (case justify + :end + [(gpt/add origin-h (hv child-width)) + (gpt/subtract (nth cell-bounds 1) (hv right-m))] + + :center + [(gpt/add origin-h (hv (/ child-width 2))) + (-> (gpo/project-point cell-bounds :h (gpo/center cell-bounds)) + (gpt/add (hv (/ left-m 2))) + (gpt/subtract (hv (/ right-m 2))))] + + [origin-h + (gpt/add (first cell-bounds) (hv left-m))]) + + [from-v to-v] + (case align + :end + [(gpt/add origin-v (vv child-height)) + (gpt/subtract (nth cell-bounds 3) (vv bottom-m))] + + :center + [(gpt/add origin-v (vv (/ child-height 2))) + (-> (gpo/project-point cell-bounds :v (gpo/center cell-bounds)) + (gpt/add (vv top-m)) + (gpt/subtract (vv bottom-m)))] + + [origin-v + (gpt/add (first cell-bounds) (vv top-m))])] + + (-> (gpt/point) + (gpt/add (gpt/to-vec from-h to-h)) + (gpt/add (gpt/to-vec from-v to-v))))) (defn child-modifiers - [_parent _transformed-parent-bounds _child child-bounds cell-data] - (ctm/move-modifiers - (gpt/subtract (:start-p cell-data) (gpo/origin child-bounds)))) + [parent parent-bounds child child-bounds layout-data cell-data] + + (let [[child-width child-height fill-modifiers] + (fill-modifiers parent parent-bounds child child-bounds layout-data cell-data) + + position-delta (child-position-delta parent child child-bounds child-width child-height layout-data cell-data)] + + (cond-> (ctm/empty) + (not (ctl/position-absolute? child)) + (-> (ctm/add-modifiers fill-modifiers) + (ctm/move position-delta))))) + +(defn get-position-grid-coord + [{:keys [layout-bounds row-tracks column-tracks]} position] + + (let [hv #(gpo/start-hv layout-bounds %) + vv #(gpo/start-vv layout-bounds %) + + make-is-inside-track + (fn [type] + (let [[vfn ofn] (if (= type :column) [vv hv] [hv vv])] + (fn is-inside-track? [{:keys [start-p size] :as track}] + (let [unit-v (vfn 1) + end-p (gpt/add start-p (ofn size))] + (gl/is-inside-lines? [start-p unit-v] [end-p unit-v] position))))) + + make-min-distance-track + (fn [type] + (let [[vfn ofn] (if (= type :column) [vv hv] [hv vv])] + (fn [[selected selected-dist] [cur-idx {:keys [start-p size] :as track}]] + (let [unit-v (vfn 1) + end-p (gpt/add start-p (ofn size)) + dist-1 (mth/abs (gl/line-value [start-p unit-v] position)) + dist-2 (mth/abs (gl/line-value [end-p unit-v] position))] + + (if (or (< dist-1 selected-dist) (< dist-2 selected-dist)) + [[cur-idx track] (min dist-1 dist-2)] + [selected selected-dist]))))) + + ;; Check if it's inside a track + [col-idx column] + (->> (d/enumerate column-tracks) + (d/seek (comp (make-is-inside-track :column) second))) + + [row-idx row] + (->> (d/enumerate row-tracks) + (d/seek (comp (make-is-inside-track :row) second))) + + ;; If not inside we find the closest start/end line + [col-idx column] + (if (some? column) + [col-idx column] + (->> (d/enumerate column-tracks) + (reduce (make-min-distance-track :column) [[nil nil] ##Inf]) + (first))) + + [row-idx row] + (if (some? row) + [row-idx row] + (->> (d/enumerate row-tracks) + (reduce (make-min-distance-track :row) [[nil nil] ##Inf]) + (first)))] + + (when (and (some? column) (some? row)) + [(inc row-idx) (inc col-idx)]))) + +(defn get-drop-cell + [frame-id objects position] + + (let [frame (get objects frame-id) + children (->> (cfh/get-immediate-children objects (:id frame)) + (remove :hidden) + (map #(vector (gpo/parent-coords-bounds (:points %) (:points frame)) %))) + + bounds (d/lazy-map (keys objects) #(gco/shape->points (get objects %))) + layout-data (ld/calc-layout-data frame (:points frame) children bounds objects)] + + (get-position-grid-coord layout-data position))) diff --git a/common/src/app/common/geom/shapes/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc index 12b64f8978..6601315ca7 100644 --- a/common/src/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -7,11 +7,13 @@ (ns app.common.geom.shapes.intersect (:require [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.path :as gpp] - [app.common.geom.shapes.rect :as gpr] [app.common.geom.shapes.text :as gte] [app.common.math :as mth])) @@ -32,10 +34,10 @@ (defn on-segment? "Given three colinear points p, q, r checks if q lies on segment pr" [{qx :x qy :y} {px :x py :y} {rx :x ry :y}] - (and (<= qx (max px rx)) - (>= qx (min px rx)) - (<= qy (max py ry)) - (>= qy (min py ry)))) + (and (<= qx (mth/max px rx)) + (>= qx (mth/min px rx)) + (<= qy (mth/max py ry)) + (>= qy (mth/min py ry)))) ;; Based on solution described here ;; https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/ @@ -53,16 +55,16 @@ (and (not= o1 o2) (not= o3 o4)) ;; p1, q1 and p2 colinear and p2 lies on p1q1 - (and (= o1 :coplanar) (on-segment? p2 p1 q1)) + (and (= o1 :coplanar) ^boolean (on-segment? p2 p1 q1)) ;; p1, q1 and q2 colinear and q2 lies on p1q1 - (and (= o2 :coplanar) (on-segment? q2 p1 q1)) + (and (= o2 :coplanar) ^boolean (on-segment? q2 p1 q1)) ;; p2, q2 and p1 colinear and p1 lies on p2q2 - (and (= o3 :coplanar) (on-segment? p1 p2 q2)) + (and (= o3 :coplanar) ^boolean (on-segment? p1 p2 q2)) ;; p2, q2 and p1 colinear and q1 lies on p2q2 - (and (= o4 :coplanar) (on-segment? q1 p2 q2))))) + (and (= o4 :coplanar) ^boolean (on-segment? q1 p2 q2))))) (defn points->lines "Given a set of points for a polygon will return @@ -71,12 +73,10 @@ (points->lines points true)) ([points closed?] - (map vector - points - (-> (rest points) - (vec) - (cond-> closed? - (conj (first points))))))) + (map vector points + (cond-> (rest points) + (true? closed?) + (concat (list (first points))))))) (defn intersects-lines? "Checks if two sets of lines intersect in any point" @@ -116,7 +116,7 @@ ;; Cast a ray from the point in any direction and count the intersections ;; if it's odd the point is inside the polygon (->> lines - (filter #(intersect-ray? p %)) + (filterv #(intersect-ray? p %)) (count) (odd?))) @@ -163,7 +163,7 @@ "Checks if the given rect intersects with the selrect" [rect points] - (let [rect-points (gpr/rect->points rect) + (let [rect-points (grc/rect->points rect) rect-lines (points->lines rect-points) points-lines (points->lines points)] @@ -173,25 +173,27 @@ (defn overlaps-path? "Checks if the given rect overlaps with the path in any point" - [shape rect] + [shape rect include-content?] (when (d/not-empty? (:content shape)) - (let [ ;; If paths are too complex the intersection is too expensive + (let [;; If paths are too complex the intersection is too expensive ;; we fallback to check its bounding box otherwise the performance penalty ;; is too big ;; TODO: Look for ways to optimize this operation simple? (> (count (:content shape)) 100) - rect-points (gpr/rect->points rect) + rect-points (grc/rect->points rect) rect-lines (points->lines rect-points) path-lines (if simple? (points->lines (:points shape)) (gpp/path->lines shape)) start-point (-> shape :content (first) :params (gpt/point))] - (or (is-point-inside-nonzero? (first rect-points) path-lines) - (is-point-inside-nonzero? start-point rect-lines) - (intersects-lines? rect-lines path-lines))))) + (or (intersects-lines? rect-lines path-lines) + (if include-content? + (or (is-point-inside-nonzero? (first rect-points) path-lines) + (is-point-inside-nonzero? start-point rect-lines)) + false))))) (defn is-point-inside-ellipse? "checks if a point is inside an ellipse" @@ -268,7 +270,7 @@ "Checks if the given rect overlaps with an ellipse" [shape rect] - (let [rect-points (gpr/rect->points rect) + (let [rect-points (grc/rect->points rect) rect-lines (points->lines rect-points) {:keys [x y width height]} shape @@ -289,7 +291,7 @@ [{:keys [position-data] :as shape} rect] (if (and (some? position-data) (d/not-empty? position-data)) - (let [center (gco/center-shape shape) + (let [center (gco/shape->center shape) transform-rect (fn [rect-points] @@ -297,7 +299,7 @@ (->> position-data (map (comp transform-rect - gpr/rect->points + grc/rect->points gte/position-data->rect)) (some #(overlaps-rect-points? rect %)))) (overlaps-rect-points? rect (:points shape)))) @@ -305,43 +307,59 @@ (defn overlaps? "General case to check for overlapping between shapes and a rectangle" [shape rect] - (let [stroke-width (/ (or (:stroke-width shape) 0) 2) - rect (-> rect - (update :x - stroke-width) - (update :y - stroke-width) - (update :width + (* 2 stroke-width)) - (update :height + (* 2 stroke-width)))] + (let [swidth (/ (or (:stroke-width shape) 0) 2) + rect (-> rect + (update :x - swidth) + (update :y - swidth) + (update :width + (* 2 swidth)) + (update :height + (* 2 swidth)))] (or (not shape) - (let [path? (= :path (:type shape)) - circle? (= :circle (:type shape)) - text? (= :text (:type shape))] - (cond - path? - (and (overlaps-rect-points? rect (:points shape)) - (overlaps-path? shape rect)) + (cond + (cfh/path-shape? shape) + (and (overlaps-rect-points? rect (:points shape)) + (overlaps-path? shape rect true)) - circle? - (and (overlaps-rect-points? rect (:points shape)) - (overlaps-ellipse? shape rect)) + (cfh/circle-shape? shape) + (and (overlaps-rect-points? rect (:points shape)) + (overlaps-ellipse? shape rect)) - text? - (overlaps-text? shape rect) + (cfh/text-shape? shape) + (overlaps-text? shape rect) - :else - (overlaps-rect-points? rect (:points shape))))))) + :else + (overlaps-rect-points? rect (:points shape)))))) (defn has-point-rect? [rect point] - (let [lines (gpr/rect->lines rect)] + (let [lines (grc/rect->lines rect)] (is-point-inside-evenodd? point lines))) -(defn has-point? - "Check if the shape contains a point" +(defn slow-has-point? [shape point] - (let [lines (points->lines (:points shape))] - ;; TODO: Will only work for simple shapes + (let [lines (points->lines (dm/get-prop shape :points))] (is-point-inside-evenodd? point lines))) +(defn fast-has-point? + [shape point] + (let [x1 (dm/get-prop shape :x) + y1 (dm/get-prop shape :y) + x2 (+ x1 (dm/get-prop shape :width)) + y2 (+ y1 (dm/get-prop shape :height)) + px (dm/get-prop point :x) + py (dm/get-prop point :y)] + (and (>= px x1) + (<= px x2) + (>= py y1) + (<= py y2)))) + +(defn has-point? + [shape point] + (if (or ^boolean (cfh/path-shape? shape) + ^boolean (cfh/bool-shape? shape) + ^boolean (cfh/circle-shape? shape)) + (slow-has-point? shape point) + (fast-has-point? shape point))) + (defn rect-contains-shape? [rect shape] (->> shape diff --git a/common/src/app/common/geom/shapes/min_size_layout.cljc b/common/src/app/common/geom/shapes/min_size_layout.cljc new file mode 100644 index 0000000000..0c3718ec5a --- /dev/null +++ b/common/src/app/common/geom/shapes/min_size_layout.cljc @@ -0,0 +1,92 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.shapes.min-size-layout + (:require + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.shapes.flex-layout.bounds :as fb] + [app.common.geom.shapes.flex-layout.layout-data :as fd] + [app.common.geom.shapes.grid-layout.bounds :as gb] + [app.common.geom.shapes.grid-layout.layout-data :as gd] + [app.common.geom.shapes.points :as gpo] + [app.common.types.shape.layout :as ctl])) + +(defn child-min-width + ([child child-bounds bounds objects] + (child-min-width child child-bounds bounds objects false)) + ([child child-bounds bounds objects strict?] + (cond + (and (not strict?) (ctl/fill-width? child) (ctl/flex-layout? child)) + (ctl/child-min-width child) + + (and strict? (ctl/fill-width? child) (ctl/flex-layout? child)) + (let [children (->> (cfh/get-immediate-children objects (dm/get-prop child :id)) + (remove ctl/position-absolute?))] + (max (ctl/child-min-width child) + (gpo/width-points (fb/layout-content-bounds bounds child children objects)))) + + (and (ctl/fill-width? child) + (ctl/grid-layout? child)) + (let [children + (->> (cfh/get-immediate-children objects (:id child)) + (remove ctl/position-absolute?) + (map #(vector @(get bounds (:id %)) %))) + layout-data (gd/calc-layout-data child @(get bounds (:id child)) children bounds objects true)] + (max (ctl/child-min-width child) + (gpo/width-points (gb/layout-content-bounds bounds child layout-data)))) + + (ctl/fill-width? child) + (ctl/child-min-width child) + + :else + (gpo/width-points child-bounds)))) + +(defn child-min-height + ([child child-bounds bounds objects] + (child-min-height child child-bounds bounds objects false)) + ([child child-bounds bounds objects strict?] + (cond + (and (not strict?) (ctl/fill-height? child) (ctl/flex-layout? child)) + (ctl/child-min-height child) + + (and strict? (ctl/fill-height? child) (ctl/flex-layout? child)) + (let [children (->> (cfh/get-immediate-children objects (dm/get-prop child :id)) + (remove ctl/position-absolute?))] + (max (ctl/child-min-height child) + (gpo/height-points (fb/layout-content-bounds bounds child children objects)))) + + (and (ctl/fill-height? child) (ctl/grid-layout? child)) + (let [children + (->> (cfh/get-immediate-children objects (dm/get-prop child :id)) + (remove ctl/position-absolute?) + (map (fn [child] [@(get bounds (:id child)) child]))) + layout-data (gd/calc-layout-data child (:points child) children bounds objects true) + auto-bounds (gb/layout-content-bounds bounds child layout-data)] + (max (ctl/child-min-height child) + (gpo/height-points auto-bounds))) + + (ctl/fill-height? child) + (ctl/child-min-height child) + + :else + (gpo/height-points child-bounds)))) + +#?(:cljs + (do (set! fd/-child-min-width child-min-width) + (set! fd/-child-min-height child-min-height) + (set! fb/-child-min-width child-min-width) + (set! fb/-child-min-height child-min-height) + (set! gd/-child-min-width child-min-width) + (set! gd/-child-min-height child-min-height)) + + :clj + (do (alter-var-root #'fd/-child-min-width (constantly child-min-width)) + (alter-var-root #'fd/-child-min-height (constantly child-min-height)) + (alter-var-root #'fb/-child-min-width (constantly child-min-width)) + (alter-var-root #'fb/-child-min-height (constantly child-min-height)) + (alter-var-root #'gd/-child-min-width (constantly child-min-width)) + (alter-var-root #'gd/-child-min-height (constantly child-min-height)))) diff --git a/common/src/app/common/geom/shapes/modifiers.cljc b/common/src/app/common/geom/shapes/modifiers.cljc deleted file mode 100644 index bfcaf65195..0000000000 --- a/common/src/app/common/geom/shapes/modifiers.cljc +++ /dev/null @@ -1,501 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.common.geom.shapes.modifiers - (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.geom.point :as gpt] - [app.common.geom.shapes.common :as gco] - [app.common.geom.shapes.constraints :as gct] - [app.common.geom.shapes.flex-layout :as gcfl] - [app.common.geom.shapes.grid-layout :as gcgl] - [app.common.geom.shapes.pixel-precision :as gpp] - [app.common.geom.shapes.points :as gpo] - [app.common.geom.shapes.transforms :as gtr] - [app.common.pages.helpers :as cph] - [app.common.types.modifiers :as ctm] - [app.common.types.shape.layout :as ctl] - [app.common.uuid :as uuid])) - -;;#?(:cljs -;; (defn modif->js -;; [modif-tree objects] -;; (clj->js (into {} -;; (map (fn [[k v]] -;; [(get-in objects [k :name]) v])) -;; modif-tree)))) - -(defn children-sequence - "Given an id returns a sequence of its children" - [id objects] - - (->> (tree-seq - #(d/not-empty? (dm/get-in objects [% :shapes])) - #(dm/get-in objects [% :shapes]) - id) - (map #(get objects %)))) - -(defn resolve-tree-sequence - "Given the ids that have changed search for layout roots to recalculate" - [ids objects] - (dm/assert! (or (nil? ids) (set? ids))) - - (let [get-tree-root - (fn ;; Finds the tree root for the current id - [id] - - (loop [current id - result id] - (let [shape (get objects current) - parent (get objects (:parent-id shape))] - (cond - (or (not shape) (= uuid/zero current)) - result - - ;; Frame found, but not layout we return the last layout found (or the id) - (and (= :frame (:type parent)) - (not (ctl/any-layout? parent))) - result - - ;; Layout found. We continue upward but we mark this layout - (ctl/any-layout? parent) - (recur (:id parent) (:id parent)) - - ;; If group or boolean or other type of group we continue with the last result - :else - (recur (:id parent) result))))) - - is-child? #(cph/is-child? objects %1 %2) - - calculate-common-roots - (fn ;; Given some roots retrieves the minimum number of tree roots - [result id] - (if (= id uuid/zero) - result - (let [root (get-tree-root id) - - ;; Remove the children from the current root - result - (if (cph/has-children? objects root) - (into #{} (remove #(is-child? root %)) result) - result) - - root-parents (cph/get-parent-ids objects root) - contains-parent? (some #(contains? result %) root-parents)] - (cond-> result - (not contains-parent?) - (conj root))))) - - roots (->> ids (reduce calculate-common-roots #{}))] - (concat - (when (contains? ids uuid/zero) [(get objects uuid/zero)]) - (mapcat #(children-sequence % objects) roots)))) - -(defn- set-children-modifiers - "Propagates the modifiers from a parent too its children applying constraints if necesary" - [modif-tree children objects bounds parent transformed-parent-bounds ignore-constraints] - (let [modifiers (dm/get-in modif-tree [(:id parent) :modifiers])] - ;; Move modifiers don't need to calculate constraints - (cond - (ctm/empty? modifiers) - modif-tree - - (ctm/only-move? modifiers) - (loop [modif-tree modif-tree - children (seq children)] - (if-let [current (first children)] - (recur (update-in modif-tree [current :modifiers] ctm/add-modifiers modifiers) - (rest children)) - modif-tree)) - - ;; Check the constraints, then resize - :else - (let [parent-id (:id parent) - parent-bounds (gtr/transform-bounds @(get bounds parent-id) (ctm/select-parent modifiers))] - (loop [modif-tree modif-tree - children (seq children)] - (if (empty? children) - modif-tree - (let [child-id (first children) - child (get objects child-id)] - (if (some? child) - (let [child-bounds @(get bounds child-id) - child-modifiers (gct/calc-child-modifiers parent child modifiers ignore-constraints child-bounds parent-bounds transformed-parent-bounds)] - (recur (cond-> modif-tree - (not (ctm/empty? child-modifiers)) - (update-in [child-id :modifiers] ctm/add-modifiers child-modifiers)) - (rest children))) - (recur modif-tree (rest children)))))))))) - -(defn get-group-bounds - [objects bounds modif-tree shape] - (let [shape-id (:id shape) - modifiers (-> (dm/get-in modif-tree [shape-id :modifiers]) - (ctm/select-geometry)) - - children (cph/get-immediate-children objects shape-id)] - - (cond - (and (cph/mask-shape? shape) (seq children)) - (get-group-bounds objects bounds modif-tree (-> children first)) - - (cph/group-shape? shape) - (let [;; Transform here to then calculate the bounds relative to the transform - current-bounds - (cond-> @(get bounds shape-id) - (not (ctm/empty? modifiers)) - (gtr/transform-bounds modifiers)) - - children-bounds - (->> children - (mapv #(get-group-bounds objects bounds modif-tree %)))] - (gpo/merge-parent-coords-bounds children-bounds current-bounds)) - - :else - (cond-> @(get bounds shape-id) - (not (ctm/empty? modifiers)) - (gtr/transform-bounds modifiers))))) - -(defn- set-flex-layout-modifiers - [modif-tree children objects bounds parent transformed-parent-bounds] - - (letfn [(apply-modifiers [child] - [(-> (get-group-bounds objects bounds modif-tree child) - (gpo/parent-coords-bounds @transformed-parent-bounds)) - child]) - - (set-child-modifiers [[layout-line modif-tree] [child-bounds child]] - (let [[modifiers layout-line] - (gcfl/layout-child-modifiers parent transformed-parent-bounds child child-bounds layout-line) - - modif-tree - (cond-> modif-tree - (d/not-empty? modifiers) - (update-in [(:id child) :modifiers] ctm/add-modifiers modifiers))] - - [layout-line modif-tree]))] - - (let [children (->> children - (keep (d/getf objects)) - (remove :hidden) - (remove gco/invalid-geometry?) - (map apply-modifiers)) - layout-data (gcfl/calc-layout-data parent children @transformed-parent-bounds) - children (into [] (cond-> children (not (:reverse? layout-data)) reverse)) - max-idx (dec (count children)) - layout-lines (:layout-lines layout-data)] - (loop [modif-tree modif-tree - layout-line (first layout-lines) - pending (rest layout-lines) - from-idx 0] - (if (and (some? layout-line) (<= from-idx max-idx)) - (let [to-idx (+ from-idx (:num-children layout-line)) - children (subvec children from-idx to-idx) - - [_ modif-tree] - (reduce set-child-modifiers [layout-line modif-tree] children)] - (recur modif-tree (first pending) (rest pending) to-idx)) - - modif-tree))))) - -(defn- set-grid-layout-modifiers - [modif-tree objects bounds parent transformed-parent-bounds] - - (letfn [(apply-modifiers [child] - [(-> (get-group-bounds objects bounds modif-tree child) - (gpo/parent-coords-bounds @transformed-parent-bounds)) - child]) - (set-child-modifiers [modif-tree cell-data [child-bounds child]] - (let [modifiers (gcgl/child-modifiers parent transformed-parent-bounds child child-bounds cell-data) - modif-tree - (cond-> modif-tree - (d/not-empty? modifiers) - (update-in [(:id child) :modifiers] ctm/add-modifiers modifiers))] - modif-tree))] - (let [children (->> (cph/get-immediate-children objects (:id parent)) - (remove :hidden) - (remove gco/invalid-geometry?) - (map apply-modifiers)) - grid-data (gcgl/calc-layout-data parent children @transformed-parent-bounds)] - (loop [modif-tree modif-tree - child (first children) - pending (rest children)] - (if (some? child) - (let [cell-data (gcgl/get-cell-data grid-data @transformed-parent-bounds child) - modif-tree (cond-> modif-tree - (some? cell-data) - (set-child-modifiers cell-data child))] - (recur modif-tree (first pending) (rest pending))) - modif-tree))))) - -(defn- calc-auto-modifiers - "Calculates the modifiers to adjust the bounds for auto-width/auto-height shapes" - [objects bounds parent] - (let [parent-id (:id parent) - parent-bounds (get bounds parent-id) - - set-parent-auto-width - (fn [modifiers auto-width] - (let [origin (gpo/origin @parent-bounds) - scale-width (/ auto-width (gpo/width-points @parent-bounds))] - (-> modifiers - (ctm/resize (gpt/point scale-width 1) origin (:transform parent) (:transform-inverse parent))))) - - set-parent-auto-height - (fn [modifiers auto-height] - (let [origin (gpo/origin @parent-bounds) - scale-height (/ auto-height (gpo/height-points @parent-bounds))] - (-> modifiers - (ctm/resize (gpt/point 1 scale-height) origin (:transform parent) (:transform-inverse parent))))) - - children (->> (cph/get-immediate-children objects parent-id) - (remove :hidden) - (remove gco/invalid-geometry?)) - - content-bounds - (when (and (d/not-empty? children) (or (ctl/auto-height? parent) (ctl/auto-width? parent))) - (gcfl/layout-content-bounds bounds parent children)) - - auto-width (when content-bounds (gpo/width-points content-bounds)) - auto-height (when content-bounds (gpo/height-points content-bounds))] - - (cond-> (ctm/empty) - (and (some? auto-width) (ctl/auto-width? parent)) - (set-parent-auto-width auto-width) - - (and (some? auto-height) (ctl/auto-height? parent)) - (set-parent-auto-height auto-height)))) - -(defn- propagate-modifiers-constraints - "Propagate modifiers to its children" - [objects bounds ignore-constraints modif-tree parent] - (let [parent-id (:id parent) - children (:shapes parent) - root? (= uuid/zero parent-id) - modifiers (-> (dm/get-in modif-tree [parent-id :modifiers]) - (ctm/select-geometry)) - has-modifiers? (ctm/child-modifiers? modifiers) - parent? (or (cph/group-like-shape? parent) (cph/frame-shape? parent)) - transformed-parent-bounds (delay (gtr/transform-bounds @(get bounds parent-id) modifiers))] - - (cond-> modif-tree - (and has-modifiers? parent? (not root?)) - (set-children-modifiers children objects bounds parent transformed-parent-bounds ignore-constraints)))) - -(defn- propagate-modifiers-layout - "Propagate modifiers to its children" - [objects bounds ignore-constraints [modif-tree autolayouts] parent] - (let [parent-id (:id parent) - root? (= uuid/zero parent-id) - modifiers (-> (dm/get-in modif-tree [parent-id :modifiers]) - (ctm/select-geometry)) - has-modifiers? (ctm/child-modifiers? modifiers) - flex-layout? (ctl/flex-layout? parent) - grid-layout? (ctl/grid-layout? parent) - auto? (or (ctl/auto-height? parent) (ctl/auto-width? parent)) - parent? (or (cph/group-like-shape? parent) (cph/frame-shape? parent)) - - transformed-parent-bounds (delay (gtr/transform-bounds @(get bounds parent-id) modifiers)) - - children-modifiers - (if flex-layout? - (->> (:shapes parent) - (filter #(ctl/layout-absolute? objects %))) - (:shapes parent)) - - children-layout - (when flex-layout? - (->> (:shapes parent) - (remove #(ctl/layout-absolute? objects %))))] - - [(cond-> modif-tree - (and has-modifiers? parent? (not root?)) - (set-children-modifiers children-modifiers objects bounds parent transformed-parent-bounds ignore-constraints) - - flex-layout? - (set-flex-layout-modifiers children-layout objects bounds parent transformed-parent-bounds) - - grid-layout? - (set-grid-layout-modifiers objects bounds parent transformed-parent-bounds)) - - ;; Auto-width/height can change the positions in the parent so we need to recalculate - (cond-> autolayouts auto? (conj (:id parent)))])) - -(defn- apply-structure-modifiers - [objects modif-tree] - (letfn [(update-children-structure-modifiers - [objects ids modifiers] - (reduce #(update %1 %2 ctm/apply-structure-modifiers modifiers) objects ids)) - - (apply-shape [objects [id {:keys [modifiers]}]] - (cond-> objects - (ctm/has-structure? modifiers) - (update id ctm/apply-structure-modifiers modifiers) - - (and (ctm/has-structure? modifiers) - (ctm/has-structure-child? modifiers)) - (update-children-structure-modifiers - (cph/get-children-ids objects id) - (ctm/select-child-structre-modifiers modifiers))))] - (reduce apply-shape objects modif-tree))) - -(defn merge-modif-tree - [modif-tree other-tree] - (reduce (fn [modif-tree [id {:keys [modifiers]}]] - (update-in modif-tree [id :modifiers] ctm/add-modifiers modifiers)) - modif-tree - other-tree)) - -(defn transform-bounds - ([bounds objects modif-tree] - (transform-bounds bounds objects modif-tree (->> (keys modif-tree) (map #(get objects %))))) - ([bounds objects modif-tree tree-seq] - - (loop [result bounds - shapes (reverse tree-seq)] - (if (empty? shapes) - result - - (let [shape (first shapes) - new-bounds (delay (get-group-bounds objects bounds modif-tree shape)) - result (assoc result (:id shape) new-bounds)] - (recur result (rest shapes))))))) - -(defn reflow-layout - [objects old-modif-tree bounds ignore-constraints id] - - (let [tree-seq (children-sequence id objects) - - [modif-tree _] - (reduce - #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [{} #{}] - tree-seq) - - bounds (transform-bounds bounds objects modif-tree tree-seq) - - modif-tree (merge-modif-tree old-modif-tree modif-tree)] - [modif-tree bounds])) - -(defn sizing-auto-modifiers - "Recalculates the layouts to adjust the sizing: auto new sizes" - [modif-tree sizing-auto-layouts objects bounds ignore-constraints] - (let [;; Step-1 resize the auto-width/height. Reflow the parents if they are also auto-width/height - [modif-tree bounds to-reflow] - (loop [modif-tree modif-tree - bounds bounds - sizing-auto-layouts (reverse sizing-auto-layouts) - to-reflow #{}] - (if-let [current (first sizing-auto-layouts)] - (let [parent-base (get objects current) - - [modif-tree bounds] - (if (contains? to-reflow current) - (reflow-layout objects modif-tree bounds ignore-constraints current) - [modif-tree bounds]) - - auto-resize-modifiers - (calc-auto-modifiers objects bounds parent-base) - - to-reflow - (cond-> to-reflow - (contains? to-reflow current) - (disj current))] - - (if (ctm/empty? auto-resize-modifiers) - (recur modif-tree - bounds - (rest sizing-auto-layouts) - to-reflow) - - (let [resize-modif-tree {current {:modifiers auto-resize-modifiers}} - - tree-seq (children-sequence current objects) - - [resize-modif-tree _] - (reduce - #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [resize-modif-tree #{}] - tree-seq) - - bounds (transform-bounds bounds objects resize-modif-tree tree-seq) - - modif-tree (merge-modif-tree modif-tree resize-modif-tree) - - to-reflow - (cond-> to-reflow - (and (ctl/flex-layout-descent? objects parent-base) - (not= uuid/zero (:frame-id parent-base))) - (conj (:frame-id parent-base)))] - (recur modif-tree - bounds - (rest sizing-auto-layouts) - to-reflow)))) - [modif-tree bounds to-reflow])) - - ;; Step-2: After resizing we still need to reflow the layout parents that are not auto-width/height - - tree-seq (resolve-tree-sequence to-reflow objects) - - [reflow-modif-tree _] - (reduce - #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [{} #{}] - tree-seq) - - result (merge-modif-tree modif-tree reflow-modif-tree)] - - result)) - -(defn set-objects-modifiers - ([modif-tree objects] - (set-objects-modifiers modif-tree objects nil)) - - ([modif-tree objects params] - (set-objects-modifiers nil modif-tree objects params)) - - ([old-modif-tree modif-tree objects - {:keys [ignore-constraints snap-pixel? snap-precision snap-ignore-axis] - :or {ignore-constraints false snap-pixel? false snap-precision 1 snap-ignore-axis nil}}] - - (let [objects (-> objects - (cond-> (some? old-modif-tree) - (apply-structure-modifiers old-modif-tree)) - (apply-structure-modifiers modif-tree)) - - modif-tree - (cond-> modif-tree - snap-pixel? (gpp/adjust-pixel-precision objects snap-precision snap-ignore-axis)) - - bounds (d/lazy-map (keys objects) #(gco/shape->points (get objects %))) - bounds (cond-> bounds - (some? old-modif-tree) - (transform-bounds objects old-modif-tree)) - - shapes-tree (resolve-tree-sequence (-> modif-tree keys set) objects) - - ;; Calculate the input transformation and constraints - modif-tree (reduce #(propagate-modifiers-constraints objects bounds ignore-constraints %1 %2) modif-tree shapes-tree) - bounds (transform-bounds bounds objects modif-tree shapes-tree) - - [modif-tree-layout sizing-auto-layouts] - (reduce #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [{} #{}] shapes-tree) - - modif-tree (merge-modif-tree modif-tree modif-tree-layout) - - ;; Calculate hug layouts positions - bounds (transform-bounds bounds objects modif-tree-layout shapes-tree) - - modif-tree - (-> modif-tree - (sizing-auto-modifiers sizing-auto-layouts objects bounds ignore-constraints)) - - modif-tree - (if old-modif-tree - (merge-modif-tree old-modif-tree modif-tree) - modif-tree)] - - ;;#?(:cljs - ;; (.log js/console ">result" (modif->js modif-tree objects))) - modif-tree))) diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index bce132b4bb..9295c421d9 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -7,13 +7,14 @@ (ns app.common.geom.shapes.path (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.shapes.common :as gsc] - [app.common.geom.shapes.rect :as gpr] + [app.common.geom.rect :as grc] + [app.common.geom.shapes.common :as gco] [app.common.math :as mth] - [app.common.path.commands :as upc] - [app.common.path.subpaths :as sp])) + [app.common.svg.path.command :as upc] + [app.common.svg.path.subpath :as sp])) (def ^:const curve-curve-precision 0.1) (def ^:const curve-range-precision 2) @@ -46,11 +47,14 @@ (defn content->points "Returns the points in the given content" [content] - (->> content - (map #(when (-> % :params :x) - (gpt/point (-> % :params :x) (-> % :params :y)))) - (remove nil?) - (into []))) + (letfn [(segment->point [seg] + (let [params (get seg :params) + x (get params :x) + y (get params :y)] + (when (d/num? x y) + (gpt/point x y))))] + (some->> (seq content) + (into [] (keep segment->point))))) (defn line-values [[from-p to-p] t] @@ -334,33 +338,46 @@ (->> (curve-extremities curve) (mapv #(curve-values curve %))))) [])] - (gpr/points->selrect points)))) + (grc/points->rect points)))) (defn content->selrect [content] - (let [calc-extremities - (fn [command prev] - (case (:command command) - :move-to [(command->point command)] + (let [extremities + (loop [points #{} + from-p nil + move-p nil + content (seq content)] + (if content + (let [command (first content) + to-p (command->point command) - ;; If it's a line we add the beginning point and endpoint - :line-to [(command->point prev) - (command->point command)] + [from-p move-p command-pts] + (case (:command command) + :move-to [to-p to-p (when to-p [to-p])] + :close-path [move-p move-p (when move-p [move-p])] + :line-to [to-p move-p (when (and from-p to-p) [from-p to-p])] + :curve-to [to-p move-p + (let [c1 (command->point command :c1) + c2 (command->point command :c2) + curve [from-p to-p c1 c2]] + (when (and from-p to-p c1 c2) + (into [from-p to-p] + (->> (curve-extremities curve) + (map #(curve-values curve %))))))] + [to-p move-p []])] - ;; We return the bezier extremities - :curve-to (into [(command->point prev) - (command->point command)] - (let [curve [(command->point prev) - (command->point command) - (command->point command :c1) - (command->point command :c2)]] - (->> (curve-extremities curve) - (map #(curve-values curve %))))) - [])) + (recur (apply conj points command-pts) from-p move-p (next content))) + points)) - extremities (mapcat calc-extremities - content - (concat [nil] content))] - (gpr/points->selrect extremities))) + ;; We haven't found any extremes so we turn the commands to points + extremities + (if (empty? extremities) + (->> content (keep command->point)) + extremities)] + + ;; If no points are returned we return an empty rect. + (if (d/not-empty? extremities) + (grc/points->rect extremities) + (grc/make-rect)))) (defn move-content [content move-vec] (let [dx (:x move-vec) @@ -474,8 +491,7 @@ result) last-start (if (= :move-to command) point - last-start) - ] + last-start)] (recur (first pending) (rest pending) result @@ -520,7 +536,7 @@ "Point on line" [position from-p to-p] - (let [e1 (gpt/to-vec from-p to-p ) + (let [e1 (gpt/to-vec from-p to-p) e2 (gpt/to-vec from-p position) len2 (+ (mth/sq (:x e1)) (mth/sq (:y e1))) @@ -591,7 +607,7 @@ (let [[from-p to-p :as curve] (subcurve-range curve from-t to-t) extremes (->> (curve-extremities curve) (mapv #(curve-values curve %)))] - (gpr/points->rect (into [from-p to-p] extremes)))) + (grc/points->rect (into [from-p to-p] extremes)))) (defn line-has-point? "Using the line equation we put the x value and check if matches with @@ -623,7 +639,7 @@ [point curve] (letfn [(check-range [from-t to-t] (let [r (curve-range->rect curve from-t to-t)] - (when (gpr/contains-point? r point) + (when (grc/contains-point? r point) (if (s= from-t to-t) (< (gpt/distance (curve-values curve from-t) point) 0.1) @@ -727,7 +743,7 @@ ray-t (get-line-tval ray-line curve-v)] (and (> ray-t 0) (> (mth/abs (- curve-tg-angle 180)) 0.01) - (> (mth/abs (- curve-tg-angle 0)) 0.01)) )))] + (> (mth/abs (- curve-tg-angle 0)) 0.01)))))] (->> curve-ts (mapv #(vector (curve-values curve %) (curve-windup curve %)))))) @@ -760,7 +776,7 @@ (let [r1 (curve-range->rect c1 c1-from c1-to) r2 (curve-range->rect c2 c2-from c2-to)] - (when (gpr/overlaps-rects? r1 r2) + (when (grc/overlaps-rects? r1 r2) (let [p1 (curve-values c1 c1-from) p2 (curve-values c2 c2-from)] @@ -811,7 +827,7 @@ [[from-p to-p :as curve]] (let [extremes (->> (curve-extremities curve) (mapv #(curve-values curve %)))] - (gpr/points->rect (into [from-p to-p] extremes)))) + (grc/points->rect (into [from-p to-p] extremes)))) (defn is-point-in-border? @@ -829,13 +845,11 @@ (defn close-content [content] (into [] - (comp (filter sp/is-closed?) - (mapcat :data)) + (mapcat :data) (->> content (sp/close-subpaths) (sp/get-subpaths)))) - (defn ray-overlaps? [ray-point {selrect :selrect}] (and (>= (:y ray-point) (:y1 selrect)) @@ -943,7 +957,7 @@ [content] (-> content content->selrect - gsc/center-selrect)) + grc/rect->center)) (defn content->points+selrect "Given the content of a shape, calculate its points and selrect" @@ -960,7 +974,7 @@ flip-y (gmt/scale (gpt/point 1 -1)) :always (gmt/multiply (:transform-inverse shape (gmt/matrix)))) - center (or (gsc/center-shape shape) + center (or (some-> (dm/get-prop shape :selrect) grc/rect->center) (content-center content)) base-content (transform-content @@ -969,16 +983,17 @@ ;; Calculates the new selrect with points given the old center points (-> (content->selrect base-content) - (gpr/rect->points) - (gsc/transform-points center transform)) + (grc/rect->points) + (gco/transform-points center transform)) - points-center (gsc/center-points points) + points-center (gco/points->center points) ;; Points is now the selrect but the center is different so we can create the selrect ;; through points selrect (-> points - (gsc/transform-points points-center transform-inverse) - (gpr/points->selrect))] + (gco/transform-points points-center transform-inverse) + (grc/points->rect))] + [points selrect])) (defn open-path? diff --git a/common/src/app/common/geom/shapes/pixel_precision.cljc b/common/src/app/common/geom/shapes/pixel_precision.cljc index 3402465018..c65b36f9b8 100644 --- a/common/src/app/common/geom/shapes/pixel_precision.cljc +++ b/common/src/app/common/geom/shapes/pixel_precision.cljc @@ -8,39 +8,44 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.points :as gpo] - [app.common.geom.shapes.rect :as gpr] [app.common.geom.shapes.transforms :as gtr] [app.common.math :as mth] - [app.common.pages.helpers :as cph] [app.common.types.modifiers :as ctm])) (defn size-pixel-precision [modifiers shape points precision] - (let [origin (gpo/origin points) - curr-width (gpo/width-points points) - curr-height (gpo/height-points points) + (let [origin (gpo/origin points) + curr-width (gpo/width-points points) + curr-height (gpo/height-points points) - [_ transform transform-inverse] (gtr/calculate-geometry points) + center (gco/points->center points) + selrect (gtr/calculate-selrect points center) - path? (cph/path-shape? shape) - vertical-line? (and path? (<= curr-width 0.01)) - horizontal-line? (and path? (<= curr-height 0.01)) + transform (gtr/calculate-transform points center selrect) + transform-inverse (when (some? transform) (gmt/inverse transform)) - target-width (if vertical-line? curr-width (max 1 (mth/round curr-width precision))) - target-height (if horizontal-line? curr-height (max 1 (mth/round curr-height precision))) + path? (cfh/path-shape? shape) + vertical-line? (and path? (<= curr-width 0.01)) + horizontal-line? (and path? (<= curr-height 0.01)) - ratio-width (/ target-width curr-width) - ratio-height (/ target-height curr-height) - scalev (gpt/point ratio-width ratio-height)] - (-> modifiers - (ctm/resize scalev origin transform transform-inverse {:precise? true})))) + target-width (if vertical-line? curr-width (mth/max 1 (mth/round curr-width precision))) + target-height (if horizontal-line? curr-height (mth/max 1 (mth/round curr-height precision))) + + ratio-width (/ target-width curr-width) + ratio-height (/ target-height curr-height) + scalev (gpt/point ratio-width ratio-height)] + + (ctm/resize modifiers scalev origin transform transform-inverse {:precise? true}))) (defn position-pixel-precision [modifiers _ points precision ignore-axis] - (let [bounds (gpr/bounds->rect points) + (let [bounds (grc/bounds->rect points) corner (gpt/point bounds) target-corner (cond-> corner @@ -69,7 +74,7 @@ points (if has-resize? (-> (:points shape) - (gco/transform-points (ctm/modifiers->transform modifiers)) ) + (gco/transform-points (ctm/modifiers->transform modifiers))) points)] [modifiers points])] (position-pixel-precision modifiers shape points precision ignore-axis))) diff --git a/common/src/app/common/geom/shapes/points.cljc b/common/src/app/common/geom/shapes/points.cljc index 348472bd25..83c110bb7b 100644 --- a/common/src/app/common/geom/shapes/points.cljc +++ b/common/src/app/common/geom/shapes/points.cljc @@ -8,9 +8,7 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] - [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.intersect :as gsi] - [app.common.geom.shapes.rect :as gre] [app.common.math :as mth])) (defn origin @@ -55,11 +53,13 @@ (defn width-points [[p0 p1 _ _]] - (max 0.01 (gpt/length (gpt/to-vec p0 p1)))) + (when (and (some? p0) (some? p1)) + (max 0.01 (gpt/length (gpt/to-vec p0 p1))))) (defn height-points [[p0 _ _ p3]] - (max 0.01 (gpt/length (gpt/to-vec p0 p3)))) + (when (and (some? p0) (some? p3)) + (max 0.01 (gpt/length (gpt/to-vec p0 p3))))) (defn pad-points [[p0 p1 p2 p3 :as points] pad-top pad-right pad-bottom pad-left] @@ -78,7 +78,7 @@ "Given a point and a line returns the parametric t the cross point with the line going through the other axis projected" [point [start end] other-axis-vec] - (let [line-vec (gpt/to-vec start end) + (let [line-vec (gpt/to-vec start end) pr-point (gsi/line-line-intersect point (gpt/add point other-axis-vec) start end)] (cond (not (mth/almost-zero? (:x line-vec))) @@ -91,6 +91,15 @@ :else 0))) +(defn project-point + "Project the point into the given axis: `:h` or `:v` means horizontal or vertical axis" + [[p0 p1 _ p3 :as bounds] axis point] + (let [[other-vec start end] + (if (= axis :h) + [(gpt/to-vec p0 p3) p0 p1] + [(gpt/to-vec p0 p1) p0 p3])] + (gsi/line-line-intersect point (gpt/add point other-vec) start end))) + (defn axis-aligned? "Check if the points are parallel to the coordinate axis." [[p1 p2 _ p4 :as pts]] @@ -104,62 +113,66 @@ (defn parent-coords-bounds [child-bounds [p1 p2 _ p4 :as parent-bounds]] - (if (empty? child-bounds) parent-bounds - (let [rh [p1 p2] - rv [p1 p4] + (if (and (axis-aligned? child-bounds) (axis-aligned? parent-bounds)) + child-bounds - hv (gpt/to-vec p1 p2) - vv (gpt/to-vec p1 p4) + (let [rh [p1 p2] + rv [p1 p4] - ph #(gpt/add p1 (gpt/scale hv %)) - pv #(gpt/add p1 (gpt/scale vv %)) + hv (gpt/to-vec p1 p2) + vv (gpt/to-vec p1 p4) - find-boundary-ts - (fn [[th-min th-max tv-min tv-max] current-point] - (let [cth (project-t current-point rh vv) - ctv (project-t current-point rv hv)] - [(min th-min cth) - (max th-max cth) - (min tv-min ctv) - (max tv-max ctv)])) + ph #(gpt/add p1 (gpt/scale hv %)) + pv #(gpt/add p1 (gpt/scale vv %)) - [th-min th-max tv-min tv-max] - (->> child-bounds - (filter #(and (d/num? (:x %)) (d/num? (:y %)))) - (reduce find-boundary-ts [##Inf ##-Inf ##Inf ##-Inf])) + find-boundary-ts + (fn [[th-min th-max tv-min tv-max] current-point] + (let [cth (project-t current-point rh vv) + ctv (project-t current-point rv hv)] + [(mth/min th-min cth) + (mth/max th-max cth) + (mth/min tv-min ctv) + (mth/max tv-max ctv)])) - minv-start (pv tv-min) - minv-end (gpt/add minv-start hv) - minh-start (ph th-min) - minh-end (gpt/add minh-start vv) + [th-min th-max tv-min tv-max] + (->> child-bounds + (filter #(and (d/num? (:x %)) (d/num? (:y %)))) + (reduce find-boundary-ts [##Inf ##-Inf ##Inf ##-Inf])) - maxv-start (pv tv-max) - maxv-end (gpt/add maxv-start hv) - maxh-start (ph th-max) - maxh-end (gpt/add maxh-start vv) + minv-start (pv tv-min) + minv-end (gpt/add minv-start hv) + minh-start (ph th-min) + minh-end (gpt/add minh-start vv) - i1 (gsi/line-line-intersect minv-start minv-end minh-start minh-end) - i2 (gsi/line-line-intersect minv-start minv-end maxh-start maxh-end) - i3 (gsi/line-line-intersect maxv-start maxv-end maxh-start maxh-end) - i4 (gsi/line-line-intersect maxv-start maxv-end minh-start minh-end)] + maxv-start (pv tv-max) + maxv-end (gpt/add maxv-start hv) + maxh-start (ph th-max) + maxh-end (gpt/add maxh-start vv) - [i1 i2 i3 i4]))) + i1 (gsi/line-line-intersect minv-start minv-end minh-start minh-end) + i2 (gsi/line-line-intersect minv-start minv-end maxh-start maxh-end) + i3 (gsi/line-line-intersect maxv-start maxv-end maxh-start maxh-end) + i4 (gsi/line-line-intersect maxv-start maxv-end minh-start minh-end)] + [i1 i2 i3 i4])))) (defn merge-parent-coords-bounds [bounds parent-bounds] (parent-coords-bounds (flatten bounds) parent-bounds)) -(defn points->selrect - [points] - (let [width (width-points points) - height (height-points points) - center (gco/center-points points)] - (gre/center->selrect center width height))) - (defn move [bounds vector] (->> bounds (map #(gpt/add % vector)))) + +(defn center + [bounds] + (let [width (width-points bounds) + height (height-points bounds) + half-h (start-hv bounds (/ width 2)) + half-v (start-vv bounds (/ height 2))] + (-> (origin bounds) + (gpt/add half-h) + (gpt/add half-v)))) diff --git a/common/src/app/common/geom/shapes/rect.cljc b/common/src/app/common/geom/shapes/rect.cljc index c304b99f47..adacddb73e 100644 --- a/common/src/app/common/geom/shapes/rect.cljc +++ b/common/src/app/common/geom/shapes/rect.cljc @@ -4,221 +4,4 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.geom.shapes.rect - (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.geom.point :as gpt] - [app.common.math :as mth])) - -(defn make-rect - ([p1 p2] - (let [xp1 (:x p1) - yp1 (:y p1) - xp2 (:x p2) - yp2 (:y p2) - x1 (min xp1 xp2) - y1 (min yp1 yp2) - x2 (max xp1 xp2) - y2 (max yp1 yp2)] - (make-rect x1 y1 (- x2 x1) (- y2 y1)))) - - ([x y width height] - (when (d/num? x y width height) - (let [width (max width 0.01) - height (max height 0.01)] - {:x x - :y y - :width width - :height height})))) - -(defn make-selrect - [x y width height] - (when (d/num? x y width height) - (let [width (max width 0.01) - height (max height 0.01)] - {:x x - :y y - :x1 x - :y1 y - :x2 (+ x width) - :y2 (+ y height) - :width width - :height height}))) - -(defn close-rect? - [rect1 rect2] - (and (mth/close? (:x rect1) (:x rect2)) - (mth/close? (:y rect1) (:y rect2)) - (mth/close? (:width rect1) (:width rect2)) - (mth/close? (:height rect1) (:height rect2)))) - -(defn close-selrect? - [selrect1 selrect2] - (and (mth/close? (:x selrect1) (:x selrect2)) - (mth/close? (:y selrect1) (:y selrect2)) - (mth/close? (:x1 selrect1) (:x1 selrect2)) - (mth/close? (:y1 selrect1) (:y1 selrect2)) - (mth/close? (:x2 selrect1) (:x2 selrect2)) - (mth/close? (:y2 selrect1) (:y2 selrect2)) - (mth/close? (:width selrect1) (:width selrect2)) - (mth/close? (:height selrect1) (:height selrect2)))) - -(defn rect->points [{:keys [x y width height]}] - (when (d/num? x y) - (let [width (max width 0.01) - height (max height 0.01)] - [(gpt/point x y) - (gpt/point (+ x width) y) - (gpt/point (+ x width) (+ y height)) - (gpt/point x (+ y height))]))) - -(defn rect->lines [{:keys [x y width height]}] - (when (d/num? x y) - (let [width (max width 0.01) - height (max height 0.01)] - [[(gpt/point x y) (gpt/point (+ x width) y)] - [(gpt/point (+ x width) y) (gpt/point (+ x width) (+ y height))] - [(gpt/point (+ x width) (+ y height)) (gpt/point x (+ y height))] - [(gpt/point x (+ y height)) (gpt/point x y)]]))) - -(defn points->rect - [points] - (when-let [points (seq points)] - (loop [minx ##Inf - miny ##Inf - maxx ##-Inf - maxy ##-Inf - pts points] - (if-let [pt (first pts)] - (let [x (dm/get-prop pt :x) - y (dm/get-prop pt :y)] - (recur (min minx x) - (min miny y) - (max maxx x) - (max maxy y) - (rest pts))) - (when (d/num? minx miny maxx maxy) - (make-rect minx miny (- maxx minx) (- maxy miny))))))) - -(defn bounds->rect - [[{ax :x ay :y} {bx :x by :y} {cx :x cy :y} {dx :x dy :y}]] - (let [minx (min ax bx cx dx) - miny (min ay by cy dy) - maxx (max ax bx cx dx) - maxy (max ay by cy dy)] - (when (d/num? minx miny maxx maxy) - (make-rect minx miny (- maxx minx) (- maxy miny))))) - -(defn squared-points - [points] - (when (d/not-empty? points) - (let [minx (transduce (keep :x) min ##Inf points) - miny (transduce (keep :y) min ##Inf points) - maxx (transduce (keep :x) max ##-Inf points) - maxy (transduce (keep :y) max ##-Inf points)] - (when (d/num? minx miny maxx maxy) - [(gpt/point minx miny) - (gpt/point maxx miny) - (gpt/point maxx maxy) - (gpt/point minx maxy)])))) - -(defn points->selrect [points] - (when-let [rect (points->rect points)] - (let [{:keys [x y width height]} rect] - (make-selrect x y width height)))) - -(defn rect->selrect [rect] - (-> rect rect->points points->selrect)) - -(defn join-rects [rects] - (when (d/not-empty? rects) - (let [minx (transduce (keep :x) min ##Inf rects) - miny (transduce (keep :y) min ##Inf rects) - maxx (transduce (keep #(when (and (:x %) (:width %)) (+ (:x %) (:width %)))) max ##-Inf rects) - maxy (transduce (keep #(when (and (:y %) (:height %))(+ (:y %) (:height %)))) max ##-Inf rects)] - (when (d/num? minx miny maxx maxy) - (make-rect minx miny (- maxx minx) (- maxy miny)))))) - -(defn join-selrects [selrects] - (when (d/not-empty? selrects) - (let [minx (transduce (keep :x1) min ##Inf selrects) - miny (transduce (keep :y1) min ##Inf selrects) - maxx (transduce (keep :x2) max ##-Inf selrects) - maxy (transduce (keep :y2) max ##-Inf selrects)] - (when (d/num? minx miny maxx maxy) - (make-selrect minx miny (- maxx minx) (- maxy miny)))))) - -(defn center->rect [{:keys [x y]} width height] - (when (d/num? x y width height) - (make-rect (- x (/ width 2)) - (- y (/ height 2)) - width - height))) - -(defn center->selrect [{:keys [x y]} width height] - (when (d/num? x y width height) - (make-selrect (- x (/ width 2)) - (- y (/ height 2)) - width - height))) - -(defn s= - [a b] - (mth/almost-zero? (- a b))) - -(defn overlaps-rects? - "Check for two rects to overlap. Rects won't overlap only if - one of them is fully to the left or the top" - [rect-a rect-b] - - (let [x1a (:x rect-a) - y1a (:y rect-a) - x2a (+ (:x rect-a) (:width rect-a)) - y2a (+ (:y rect-a) (:height rect-a)) - - x1b (:x rect-b) - y1b (:y rect-b) - x2b (+ (:x rect-b) (:width rect-b)) - y2b (+ (:y rect-b) (:height rect-b))] - - (and (or (> x2a x1b) (s= x2a x1b)) - (or (>= x2b x1a) (s= x2b x1a)) - (or (<= y1b y2a) (s= y1b y2a)) - (or (<= y1a y2b) (s= y1a y2b))))) - -(defn contains-point? - [rect point] - (assert (gpt/point? point)) - (let [x1 (:x rect) - y1 (:y rect) - x2 (+ (:x rect) (:width rect)) - y2 (+ (:y rect) (:height rect)) - - px (:x point) - py (:y point)] - - (and (or (> px x1) (s= px x1)) - (or (< px x2) (s= px x2)) - (or (> py y1) (s= py y1)) - (or (< py y2) (s= py y2))))) - -(defn contains-selrect? - "Check if a selrect sr2 is contained inside sr1" - [sr1 sr2] - (and (>= (:x1 sr2) (:x1 sr1)) - (<= (:x2 sr2) (:x2 sr1)) - (>= (:y1 sr2) (:y1 sr1)) - (<= (:y2 sr2) (:y2 sr1)))) - -(defn corners->selrect - ([p1 p2] - (corners->selrect (:x p1) (:y p1) (:x p2) (:y p2))) - ([xp1 yp1 xp2 yp2] - (make-selrect (min xp1 xp2) (min yp1 yp2) (abs (- xp1 xp2)) (abs (- yp1 yp2))))) - -(defn clip-selrect - [{:keys [x1 y1 x2 y2] :as sr} clip-rect] - (when (some? sr) - (let [{bx1 :x1 by1 :y1 bx2 :x2 by2 :y2 :as sr2} (rect->selrect clip-rect)] - (corners->selrect (max bx1 x1) (max by1 y1) (min bx2 x2) (min by2 y2))))) +(ns app.common.geom.shapes.rect) diff --git a/common/src/app/common/geom/shapes/strokes.cljc b/common/src/app/common/geom/shapes/strokes.cljc index e155dde7b1..905aac030c 100644 --- a/common/src/app/common/geom/shapes/strokes.cljc +++ b/common/src/app/common/geom/shapes/strokes.cljc @@ -1,3 +1,9 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + (ns app.common.geom.shapes.strokes) (defn update-stroke-width @@ -6,6 +12,6 @@ (defn update-strokes-width [shape scale] - (update shape :strokes - (fn [strokes] - (mapv #(update-stroke-width % scale) strokes)))) + (update shape :strokes + (fn [strokes] + (mapv #(update-stroke-width % scale) strokes)))) diff --git a/common/src/app/common/geom/shapes/text.cljc b/common/src/app/common/geom/shapes/text.cljc index 1b6739187f..1d08d6ab23 100644 --- a/common/src/app/common/geom/shapes/text.cljc +++ b/common/src/app/common/geom/shapes/text.cljc @@ -6,41 +6,36 @@ (ns app.common.geom.shapes.text (:require + [app.common.data.macros :as dm] + [app.common.geom.rect :as grc] [app.common.geom.shapes.common :as gco] - [app.common.geom.shapes.rect :as gpr] [app.common.geom.shapes.transforms :as gtr])) (defn position-data->rect [{:keys [x y width height]}] - {:x x - :y (- y height) - :width width - :height height}) + (grc/make-rect x (- y height) width height)) -(defn position-data-selrect +(defn shape->rect [shape] - (let [points (->> shape - :position-data - (mapcat (comp gpr/rect->points position-data->rect)))] - (if (empty? points) - (:selrect shape) - (-> points (gpr/points->selrect))))) + (let [points (->> (:position-data shape) + (mapcat (comp grc/rect->points position-data->rect)))] + (if (seq points) + (grc/points->rect points) + (dm/get-prop shape :selrect)))) -(defn position-data-bounding-box +(defn shape->bounds [shape] - (let [points (->> shape - :position-data - (mapcat (comp gpr/rect->points position-data->rect))) - transform (gtr/transform-matrix shape)] + (let [points (->> (:position-data shape) + (mapcat (comp grc/rect->points position-data->rect)))] (-> points - (gco/transform-points transform) - (gpr/points->selrect )))) + (gco/transform-points (gtr/transform-matrix shape)) + (grc/points->rect)))) (defn overlaps-position-data? "Checks if the given position data is inside the shape" [{:keys [points]} position-data] - (let [bounding-box (gpr/points->selrect points) + (let [bounding-box (grc/points->rect points) fix-rect #(assoc % :y (- (:y %) (:height %)))] (->> position-data - (some #(gpr/overlaps-rects? bounding-box (fix-rect %))) + (some #(grc/overlaps-rects? bounding-box (fix-rect %))) (boolean)))) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index 90bddd81e9..ebde6bf806 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -5,96 +5,114 @@ ;; Copyright (c) KALEIDOS INC (ns app.common.geom.shapes.transforms - #?(:clj (:import (org.la4j Matrix LinearAlgebra)) - :cljs (:import goog.math.Matrix)) (:require - #?(:clj [app.common.exceptions :as ex]) [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes.bool :as gshb] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.path :as gpa] - [app.common.geom.shapes.rect :as gpr] [app.common.math :as mth] - [app.common.pages.helpers :as cph] - [app.common.types.modifiers :as ctm] - [app.common.uuid :as uuid])) + [app.common.record :as cr] + [app.common.types.modifiers :as ctm])) #?(:clj (set! *warn-on-reflection* true)) +(defn- valid-point? + [o] + (and ^boolean (gpt/point? o) + ^boolean (d/num? (dm/get-prop o :x) + (dm/get-prop o :y)))) + ;; --- Relative Movement -(defn- move-selrect [{:keys [x y x1 y1 x2 y2 width height] :as selrect} {dx :x dy :y :as pt}] - (if (and (some? selrect) (some? pt) (d/num? dx dy)) - {:x (if (d/num? x) (+ dx x) x) - :y (if (d/num? y) (+ dy y) y) - :x1 (if (d/num? x1) (+ dx x1) x1) - :y1 (if (d/num? y1) (+ dy y1) y1) - :x2 (if (d/num? x2) (+ dx x2) x2) - :y2 (if (d/num? y2) (+ dy y2) y2) - :width width - :height height} +(defn- move-selrect + [selrect pt] + (if (and ^boolean (some? selrect) + ^boolean (valid-point? pt)) + (let [x (dm/get-prop selrect :x) + y (dm/get-prop selrect :y) + w (dm/get-prop selrect :width) + h (dm/get-prop selrect :height) + dx (dm/get-prop pt :x) + dy (dm/get-prop pt :y)] + + (grc/make-rect + (if ^boolean (d/num? x) (+ dx x) x) + (if ^boolean (d/num? y) (+ dy y) y) + w + h)) selrect)) -(defn- move-points [points move-vec] - (cond->> points - (d/num? (:x move-vec) (:y move-vec)) - (mapv #(gpt/add % move-vec)))) +(defn- move-points + [points move-vec] + (if (valid-point? move-vec) + (mapv #(gpt/add % move-vec) points) + points)) +;; FIXME: deprecated (defn move-position-data - ([position-data {:keys [x y]}] - (move-position-data position-data x y)) + [position-data delta] + (when (some? position-data) + (let [dx (dm/get-prop delta :x) + dy (dm/get-prop delta :y)] + (if (d/num? dx dy) + (mapv #(-> % + (update :x + dx) + (update :y + dy)) + position-data) + position-data)))) - ([position-data dx dy] - (when (some? position-data) - (cond->> position-data - (d/num? dx dy) - (mapv #(-> % - (update :x + dx) - (update :y + dy))))))) +(defn transform-position-data + [position-data transform] + (when (some? position-data) + (let [dx (dm/get-prop transform :e) + dy (dm/get-prop transform :f)] + (if (d/num? dx dy) + (mapv #(-> % + (update :x + dx) + (update :y + dy)) + position-data) + position-data)))) +;; FIXME: revist usage of mutability (defn move "Move the shape relatively to its current position applying the provided delta." - [{:keys [type] :as shape} {dx :x dy :y}] - (let [dx (d/check-num dx 0) - dy (d/check-num dy 0) - move-vec (gpt/point dx dy)] + [shape point] + (let [type (dm/get-prop shape :type) + dx (dm/get-prop point :x) + dy (dm/get-prop point :y) + dx (d/check-num dx 0) + dy (d/check-num dy 0) + mvec (gpt/point dx dy)] (-> shape - (update :selrect move-selrect move-vec) - (update :points move-points move-vec) - (d/update-when :x + dx) - (d/update-when :y + dy) - (d/update-when :position-data move-position-data dx dy) - (cond-> (= :bool type) (update :bool-content gpa/move-content move-vec)) - (cond-> (= :path type) (update :content gpa/move-content move-vec))))) + (update :selrect move-selrect mvec) + (update :points move-points mvec) + (d/update-when :x d/safe+ dx) + (d/update-when :y d/safe+ dy) + (d/update-when :position-data move-position-data mvec) + (cond-> (= :bool type) (update :bool-content gpa/move-content mvec)) + (cond-> (= :path type) (update :content gpa/move-content mvec))))) ;; --- Absolute Movement (defn absolute-move "Move the shape to the exactly specified position." - [shape {:keys [x y]}] - (let [dx (- (d/check-num x) (-> shape :selrect :x)) - dy (- (d/check-num y) (-> shape :selrect :y))] - (move shape (gpt/point dx dy)))) - -; ---- Geometric operations - -(defn- calculate-height - "Calculates the height of a parallelogram given by the points" - [[p1 _ _ p4]] - - (-> (gpt/to-vec p4 p1) - (gpt/length))) - -(defn- calculate-width - "Calculates the width of a parallelogram given by the points" - [[p1 p2 _ _]] - (-> (gpt/to-vec p1 p2) - (gpt/length))) + [shape pos] + (when shape + (let [x (dm/get-prop pos :x) + y (dm/get-prop pos :y) + sr (dm/get-prop shape :selrect) + px (dm/get-prop sr :x) + py (dm/get-prop sr :y) + dx (- (d/check-num x) px) + dy (- (d/check-num y) py)] + (move shape (gpt/point dx dy))))) ;; --- Transformation matrix operations @@ -105,7 +123,7 @@ (transform-matrix shape nil)) ([shape params] - (transform-matrix shape params (or (gco/center-shape shape) (gpt/point 0 0)))) + (transform-matrix shape params (or (gco/shape->center shape) (gpt/point 0 0)))) ([{:keys [flip-x flip-y transform] :as shape} {:keys [no-flip]} shape-center] (-> (gmt/matrix) @@ -122,6 +140,28 @@ (gmt/translate (gpt/negate shape-center))))) +(defn inverse-transform-matrix + ([shape] + (inverse-transform-matrix shape nil)) + + ([shape params] + (inverse-transform-matrix shape params (or (gco/shape->center shape) (gpt/point 0 0)))) + + ([{:keys [flip-x flip-y transform-inverse] :as shape} {:keys [no-flip]} shape-center] + (-> (gmt/matrix) + (gmt/translate shape-center) + + (cond-> (and flip-x no-flip) + (gmt/scale (gpt/point -1 1))) + + (cond-> (and flip-y no-flip) + (gmt/scale (gpt/point 1 -1))) + + (cond-> (some? transform-inverse) + (gmt/multiply transform-inverse)) + + (gmt/translate (gpt/negate shape-center))))) + (defn transform-str ([shape] (transform-str shape nil)) @@ -134,231 +174,216 @@ (dm/str (transform-matrix shape params)) ""))) -(defn inverse-transform-matrix - ([shape] - (let [shape-center (or (gco/center-shape shape) - (gpt/point 0 0))] - (inverse-transform-matrix shape shape-center))) - ([{:keys [flip-x flip-y] :as shape} center] - (-> (gmt/matrix) - (gmt/translate center) - (cond-> - flip-x (gmt/scale (gpt/point -1 1)) - flip-y (gmt/scale (gpt/point 1 -1))) - (gmt/multiply (:transform-inverse shape (gmt/matrix))) - (gmt/translate (gpt/negate center))))) - +;; FIXME: move to geom rect? (defn transform-rect "Transform a rectangles and changes its attributes" [rect matrix] - (let [points (-> (gpr/rect->points rect) + (let [points (-> (grc/rect->points rect) (gco/transform-points matrix))] - (gpr/points->rect points))) + (grc/points->rect points))) (defn transform-points-matrix - "Calculate the transform matrix to convert from the selrect to the points bounds - TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)" - [{:keys [x1 y1 x2 y2]} [d1 d2 _ d4]] + [selrect [d1 d2 _ d4]] ;; If the coordinates are very close to zero (but not zero) the rounding can mess with the ;; transforms. So we round to zero the values - (let [x1 (mth/round-to-zero x1) - y1 (mth/round-to-zero y1) - x2 (mth/round-to-zero x2) - y2 (mth/round-to-zero y2) - d1x (mth/round-to-zero (:x d1)) - d1y (mth/round-to-zero (:y d1)) - d2x (mth/round-to-zero (:x d2)) - d2y (mth/round-to-zero (:y d2)) - d4x (mth/round-to-zero (:x d4)) - d4y (mth/round-to-zero (:y d4))] - #?(:clj - ;; NOTE: the source matrix may not be invertible we can't - ;; calculate the transform, so on exception we return `nil` - (ex/ignoring - (let [target-points-matrix - (->> (list d1x d2x d4x - d1y d2y d4y - 1 1 1) - (into-array Double/TYPE) - (Matrix/from1DArray 3 3)) + (let [x1 (mth/round-to-zero (dm/get-prop selrect :x1)) + y1 (mth/round-to-zero (dm/get-prop selrect :y1)) + x2 (mth/round-to-zero (dm/get-prop selrect :x2)) + y2 (mth/round-to-zero (dm/get-prop selrect :y2)) - source-points-matrix - (->> (list x1 x2 x1 - y1 y1 y2 - 1 1 1) - (into-array Double/TYPE) - (Matrix/from1DArray 3 3)) + det (+ (- (* (- y1 y2) x1) + (* (- y1 y2) x2)) + (* (- y1 y1) x1))] - ;; May throw an exception if the matrix is not invertible - source-points-matrix-inv - (.. source-points-matrix - (withInverter LinearAlgebra/GAUSS_JORDAN) - (inverse)) + (when-not (zero? det) + (let [ma0 (mth/round-to-zero (dm/get-prop d1 :x)) + ma1 (mth/round-to-zero (dm/get-prop d2 :x)) + ma2 (mth/round-to-zero (dm/get-prop d4 :x)) + ma3 (mth/round-to-zero (dm/get-prop d1 :y)) + ma4 (mth/round-to-zero (dm/get-prop d2 :y)) + ma5 (mth/round-to-zero (dm/get-prop d4 :y)) - transform-jvm - (.. target-points-matrix - (multiply source-points-matrix-inv))] + mb0 (/ (- y1 y2) det) + mb1 (/ (- x1 x2) det) + mb2 (/ (- (* x2 y2) (* x1 y1)) det) + mb3 (/ (- y2 y1) det) + mb4 (/ (- x1 x1) det) + mb5 (/ (- (* x1 y1) (* x1 y2)) det) + mb6 (/ (- y1 y1) det) + mb7 (/ (- x2 x1) det) + mb8 (/ (- (* x1 y1) (* x2 y1)) det)] - (gmt/matrix (.get transform-jvm 0 0) - (.get transform-jvm 1 0) - (.get transform-jvm 0 1) - (.get transform-jvm 1 1) - (.get transform-jvm 0 2) - (.get transform-jvm 1 2)))) + (gmt/matrix (+ (* ma0 mb0) + (* ma1 mb3) + (* ma2 mb6)) + (+ (* ma3 mb0) + (* ma4 mb3) + (* ma5 mb6)) + (+ (* ma0 mb1) + (* ma1 mb4) + (* ma2 mb7)) + (+ (* ma3 mb1) + (* ma4 mb4) + (* ma5 mb7)) + (+ (* ma0 mb2) + (* ma1 mb5) + (* ma2 mb8)) + (+ (* ma3 mb2) + (* ma4 mb5) + (* ma5 mb8))))))) - :cljs - (let [target-points-matrix - (Matrix. #js [#js [d1x d2x d4x] - #js [d1y d2y d4y] - #js [ 1 1 1]]) +(defn calculate-selrect + [points center] - source-points-matrix - (Matrix. #js [#js [x1 x2 x1] - #js [y1 y1 y2] - #js [ 1 1 1]]) + (let [p1 (nth points 0) + p2 (nth points 1) + p4 (nth points 3) - ;; returns nil if not invertible - source-points-matrix-inv (.getInverse source-points-matrix) + width (mth/hypot + (- (dm/get-prop p2 :x) + (dm/get-prop p1 :x)) + (- (dm/get-prop p2 :y) + (dm/get-prop p1 :y))) - ;; TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM) - transform-js - (when source-points-matrix-inv - (.multiply target-points-matrix source-points-matrix-inv))] + height (mth/hypot + (- (dm/get-prop p1 :x) + (dm/get-prop p4 :x)) + (- (dm/get-prop p1 :y) + (dm/get-prop p4 :y)))] - (when transform-js - (gmt/matrix (.getValueAt transform-js 0 0) - (.getValueAt transform-js 1 0) - (.getValueAt transform-js 0 1) - (.getValueAt transform-js 1 1) - (.getValueAt transform-js 0 2) - (.getValueAt transform-js 1 2))))))) + (grc/center->rect center width height))) -(defn calculate-geometry - [points] - (let [width (calculate-width points) - height (calculate-height points) - center (gco/center-points points) - sr (gpr/center->selrect center width height) - - points-transform-mtx (transform-points-matrix sr points) +(defn calculate-transform + [points center selrect] + (let [transform (transform-points-matrix selrect points) ;; Calculate the transform by move the transformation to the center transform - (when points-transform-mtx - (gmt/multiply - (gmt/translate-matrix (gpt/negate center)) - points-transform-mtx - (gmt/translate-matrix center))) + (when (some? transform) + (-> (gmt/translate-matrix-neg center) + (gmt/multiply! transform) + (gmt/multiply! (gmt/translate-matrix center))))] - transform-inverse (when transform (gmt/inverse transform)) + ;; There is a rounding error when the matrix returned have float point values + ;; when the matrix is unit we return a "pure" matrix so we don't accumulate + ;; rounding problems + (when ^boolean (gmt/matrix? transform) + (if ^boolean (gmt/unit? transform) + gmt/base + transform)))) - ;; There is a rounding error when the matrix returned have float point values - ;; when the matrix is unit we return a "pure" matrix so we don't accumulate - ;; rounding problems - [transform transform-inverse] - (if (gmt/unit? transform) - [(gmt/matrix) (gmt/matrix)] - [transform transform-inverse])] +(defn calculate-geometry + [points] + (let [center (gco/points->center points) + selrect (calculate-selrect points center) + transform (calculate-transform points center selrect)] + [selrect transform (when (some? transform) (gmt/inverse transform))])) - [sr transform transform-inverse])) - -(defn- adjust-shape-flips +(defn- adjust-shape-flips! "After some tranformations the flip-x/flip-y flags can change we need to check this before adjusting the selrect" [shape points] + (let [points' (dm/get-prop shape :points) + p0' (nth points' 0) + p0 (nth points 0) - (let [points' (:points shape) + ;; FIXME: unroll and remove point allocation here + xv1 (gpt/to-vec p0' (nth points' 1)) + xv2 (gpt/to-vec p0 (nth points 1)) + dot-x (gpt/dot xv1 xv2) - xv1 (gpt/to-vec (nth points' 0) (nth points' 1)) - xv2 (gpt/to-vec (nth points 0) (nth points 1)) - dot-x (gpt/dot xv1 xv2) - - yv1 (gpt/to-vec (nth points' 0) (nth points' 3)) - yv2 (gpt/to-vec (nth points 0) (nth points 3)) - dot-y (gpt/dot yv1 yv2)] + yv1 (gpt/to-vec p0' (nth points' 3)) + yv2 (gpt/to-vec p0 (nth points 3)) + dot-y (gpt/dot yv1 yv2)] (cond-> shape (neg? dot-x) - (-> (update :flip-x not) - (update :rotation -)) + (cr/update! :flip-x not) + + (neg? dot-x) + (cr/update! :rotation -) (neg? dot-y) - (-> (update :flip-y not) - (update :rotation -))))) + (cr/update! :flip-y not) + + (neg? dot-y) + (cr/update! :rotation -)))) (defn- apply-transform-move "Given a new set of points transformed, set up the rectangle so it keeps its properties. We adjust de x,y,width,height and create a custom transform" [shape transform-mtx] - (let [bool? (= (:type shape) :bool) - path? (= (:type shape) :path) - text? (= (:type shape) :text) - {dx :x dy :y} (gpt/transform (gpt/point) transform-mtx) - points (gco/transform-points (:points shape) transform-mtx) - selrect (gco/transform-selrect (:selrect shape) transform-mtx)] + (let [type (dm/get-prop shape :type) + points (gco/transform-points (dm/get-prop shape :points) transform-mtx) + selrect (gco/transform-selrect (dm/get-prop shape :selrect) transform-mtx) + + ;; NOTE: ensure we start with a fresh copy of shape for mutabilty + shape (cr/clone shape) + + shape (if (= type :bool) + (update shape :bool-content gpa/transform-content transform-mtx) + shape) + shape (if (= type :text) + (update shape :position-data transform-position-data transform-mtx) + shape) + shape (if (= type :path) + (update shape :content gpa/transform-content transform-mtx) + (cr/assoc! shape + :x (dm/get-prop selrect :x) + :y (dm/get-prop selrect :y) + :width (dm/get-prop selrect :width) + :height (dm/get-prop selrect :height)))] (-> shape - (cond-> bool? - (update :bool-content gpa/transform-content transform-mtx)) - (cond-> path? - (update :content gpa/transform-content transform-mtx)) - (cond-> text? - (update :position-data move-position-data dx dy)) - (cond-> (not path?) - (assoc :x (:x selrect) - :y (:y selrect) - :width (:width selrect) - :height (:height selrect))) - (assoc :selrect selrect) - (assoc :points points)))) + (cr/assoc! :selrect selrect) + (cr/assoc! :points points)))) + (defn- apply-transform-generic "Given a new set of points transformed, set up the rectangle so it keeps its properties. We adjust de x,y,width,height and create a custom transform" [shape transform-mtx] + (let [points (-> (dm/get-prop shape :points) + (gco/transform-points transform-mtx)) - (let [points' (gco/shape->points shape) - points (gco/transform-points points' transform-mtx) - shape (-> shape (adjust-shape-flips points)) - bool? (= (:type shape) :bool) - path? (= (:type shape) :path) + ;; NOTE: ensure we have a fresh shallow copy of shape + shape (cr/clone shape) + shape (adjust-shape-flips! shape points) - [selrect transform transform-inverse] (calculate-geometry points) + center (gco/points->center points) + selrect (calculate-selrect points center) + transform (calculate-transform points center selrect) + inverse (when (some? transform) (gmt/inverse transform))] - base-rotation (or (:rotation shape) 0) - modif-rotation (or (get-in shape [:modifiers :rotation]) 0) - rotation (mod (+ base-rotation modif-rotation) 360)] - - (if-not (and transform transform-inverse) - ;; When we cannot calculate the transformation we leave the shape as it was + (if-not (and (some? inverse) (some? transform)) shape - (-> shape - (cond-> bool? - (update :bool-content gpa/transform-content transform-mtx)) - (cond-> path? - (update :content gpa/transform-content transform-mtx)) - (cond-> (not path?) - (assoc :x (:x selrect) - :y (:y selrect) - :width (:width selrect) - :height (:height selrect))) - (cond-> transform - (-> (assoc :transform transform) - (assoc :transform-inverse transform-inverse))) - (cond-> (not transform) - (dissoc :transform :transform-inverse)) - (cond-> (some? selrect) - (assoc :selrect selrect)) + (let [type (dm/get-prop shape :type) + rotation (mod (+ (d/nilv (:rotation shape) 0) + (d/nilv (dm/get-in shape [:modifiers :rotation]) 0)) + 360) + shape (if (= type :bool) + (update shape :bool-content gpa/transform-content transform-mtx) + shape) - (cond-> (d/not-empty? points) - (assoc :points points)) - (assoc :rotation rotation))))) + shape (if (= type :path) + (update shape :content gpa/transform-content transform-mtx) + (cr/assoc! shape + :x (dm/get-prop selrect :x) + :y (dm/get-prop selrect :y) + :width (dm/get-prop selrect :width) + :height (dm/get-prop selrect :height)))] + (-> shape + (cr/assoc! :transform transform) + (cr/assoc! :transform-inverse inverse) + (cr/assoc! :selrect selrect) + (cr/assoc! :points points) + (cr/assoc! :rotation rotation)))))) (defn- apply-transform "Given a new set of points transformed, set up the rectangle so it keeps its properties. We adjust de x,y,width,height and create a custom transform" [shape transform-mtx] - (if (gmt/move? transform-mtx) + (if ^boolean (gmt/move? transform-mtx) (apply-transform-move shape transform-mtx) (apply-transform-generic shape transform-mtx))) @@ -385,7 +410,7 @@ (let [;; Points for every shape inside the group points (->> children (mapcat :points)) - shape-center (gco/center-points points) + shape-center (gco/points->center points) ;; Fixed problem with empty groups. Should not happen (but it does) points (if (empty? points) (:points group) points) @@ -393,13 +418,18 @@ ;; Invert to get the points minus the transforms applied to the group base-points (gco/transform-points points shape-center (:transform-inverse group (gmt/matrix))) + ;; FIXME: looks redundant operation points -> rect -> points ;; Defines the new selection rect with its transformations - new-points (-> (gpr/points->selrect base-points) - (gpr/rect->points) + new-points (-> (grc/points->rect base-points) + (grc/rect->points) (gco/transform-points shape-center (:transform group (gmt/matrix)))) ;; Calculate the new selrect - new-selrect (gpr/points->selrect base-points)] + sr-transform (gmt/transform-in (gco/points->center new-points) (:transform-inverse group (gmt/matrix))) + new-selrect + (-> new-points + (gco/transform-points sr-transform) + (grc/points->rect))] ;; Updates the shape and the applytransform-rect will update the other properties (-> group @@ -440,6 +470,29 @@ (assoc :points points)) (update-group-selrect shape children)))) +(defn update-shapes-geometry + [objects ids] + (->> ids + (reduce + (fn [objects id] + (let [shape (get objects id) + children (cfh/get-immediate-children objects id) + shape + (cond + (cfh/mask-shape? shape) + (update-mask-selrect shape children) + + (cfh/bool-shape? shape) + (update-bool-selrect shape children objects) + + (cfh/group-shape? shape) + (update-group-selrect shape children) + + :else + shape)] + (assoc objects id shape))) + objects))) + (defn transform-shape ([shape] (let [modifiers (:modifiers shape)] @@ -448,21 +501,16 @@ (transform-shape modifiers)))) ([shape modifiers] - (letfn [(apply-modifiers - [shape modifiers] - (if (ctm/empty? modifiers) - shape - (let [transform (ctm/modifiers->transform modifiers)] - (cond-> shape - (and (some? transform) (not= uuid/zero (:id shape))) ;; Never transform the root frame - (apply-transform transform) + (if (and (some? shape) (some? modifiers) (not (ctm/empty? modifiers))) + (let [transform (ctm/modifiers->transform modifiers)] + (cond-> shape + (and (some? transform) + (not (cfh/root? shape))) + (apply-transform transform) - (ctm/has-structure? modifiers) - (ctm/apply-structure-modifiers modifiers)))))] - - (cond-> shape - (and (some? modifiers) (not (ctm/empty? modifiers))) - (apply-modifiers modifiers))))) + (ctm/has-structure? modifiers) + (ctm/apply-structure-modifiers modifiers))) + shape))) (defn apply-objects-modifiers ([objects modifiers] @@ -473,7 +521,6 @@ ids (seq ids)] (if (empty? ids) objects - (let [id (first ids) modifier (dm/get-in modifiers [id :modifiers])] (recur (d/update-when objects id transform-shape modifier) @@ -492,24 +539,16 @@ (defn transform-selrect [selrect modifiers] (-> selrect - (gpr/rect->points) + (grc/rect->points) (transform-bounds modifiers) - (gpr/points->selrect))) + (grc/points->rect))) (defn transform-selrect-matrix [selrect mtx] (-> selrect - (gpr/rect->points) + (grc/rect->points) (gco/transform-points mtx) - (gpr/points->selrect))) - -(defn selection-rect - "Returns a rect that contains all the shapes and is aware of the - rotation of each shape. Mainly used for multiple selection." - [shapes] - (->> shapes - (map (comp gpr/points->selrect :points transform-shape)) - (gpr/join-selrects))) + (grc/points->rect))) (declare apply-group-modifiers) @@ -520,7 +559,7 @@ (let [modifiers (cond-> (get-in modif-tree [(:id child) :modifiers]) propagate? (ctm/add-modifiers parent-modifiers)) child (transform-shape child modifiers) - parent? (cph/group-like-shape? child) + parent? (cfh/group-like-shape? child) modif-tree (cond-> modif-tree @@ -543,13 +582,13 @@ (map (d/getf objects) $) (apply-children-modifiers objects modif-tree modifiers $ propagate?))] (cond - (cph/mask-shape? group) + (cfh/mask-shape? group) (update-mask-selrect group children) - (cph/bool-shape? group) + (cfh/bool-shape? group) (transform-shape group modifiers) - (cph/group-shape? group) + (cfh/group-shape? group) (update-group-selrect group children) :else diff --git a/common/src/app/common/geom/shapes/tree_seq.cljc b/common/src/app/common/geom/shapes/tree_seq.cljc new file mode 100644 index 0000000000..8ed6b61b8d --- /dev/null +++ b/common/src/app/common/geom/shapes/tree_seq.cljc @@ -0,0 +1,102 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.shapes.tree-seq + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.types.shape.layout :as ctl] + [app.common.uuid :as uuid])) + +(defn get-children-seq + "Given an id returns a sequence of its children" + [id objects] + (->> (tree-seq + #(d/not-empty? (dm/get-in objects [% :shapes])) + (fn [id] + (let [shape (get objects id)] + (cond->> (:shapes shape) + (and (ctl/flex-layout? shape) (ctl/reverse? shape)) + (reverse)))) + id) + (map #(get objects %)))) + +;; Finds the tree root for the current id +(defn get-reflow-root + ([id objects] + (get-reflow-root id id objects)) + + ([current last-root objects] + (let [shape (get objects current)] + (if (or (not ^boolean shape) (= uuid/zero current)) + last-root + (let [parent-id (dm/get-prop shape :parent-id) + parent (get objects parent-id)] + (cond + ;; Frame found, but not layout we return the last layout found (or the id) + (and ^boolean (cfh/frame-shape? parent) + (not ^boolean (ctl/any-layout? parent))) + last-root + + ;; Auto-Layout found. We continue upward but we mark this layout + (and (ctl/any-layout? parent) (ctl/auto? parent)) + (recur parent-id parent-id objects) + + (ctl/any-layout? parent) + parent-id + + ;; If group or boolean or other type of group we continue with the last result + :else + (recur parent-id last-root objects))))))) + +;; Given some roots retrieves the minimum number of tree roots +(defn search-common-roots + [ids objects] + (let [find-root + (fn [roots id] + (if (= id uuid/zero) + roots + (let [root (get-reflow-root id objects) + ;; Remove the children from the current root + roots + (if ^boolean (cfh/has-children? objects root) + (into #{} (remove (partial cfh/is-child? objects root)) roots) + roots) + + contains-parent? + (->> (cfh/get-parent-ids objects root) + (some (partial contains? roots)))] + + (cond-> roots + (not contains-parent?) + (conj root)))))] + (reduce find-root #{} ids))) + +(defn resolve-tree + "Given the ids that have changed search for layout roots to recalculate" + [ids objects] + (dm/assert! (or (nil? ids) (set? ids))) + + (let [child-seq + (->> (search-common-roots ids objects) + (mapcat #(get-children-seq % objects)))] + + (if (contains? ids uuid/zero) + (cons (get objects uuid/zero) child-seq) + child-seq))) + +(defn resolve-subtree + "Resolves the subtree but only partialy from-to the parameters" + [from-id to-id objects] + (concat + (->> (get-children-seq from-id objects) + (d/take-until #(= (:id %) to-id))) + + ;; Add the children of `to-id` to the subtree. Rest is to remove the + ;; to-id element that is already on the previous sequence + (->> (get-children-seq to-id objects) + rest))) diff --git a/common/src/app/common/geom/snap.cljc b/common/src/app/common/geom/snap.cljc new file mode 100644 index 0000000000..a2cffe09f1 --- /dev/null +++ b/common/src/app/common/geom/snap.cljc @@ -0,0 +1,62 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.snap + (:require + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] + [app.common.geom.shapes :as gsh] + [app.common.types.shape-tree :as ctst])) + +(defn rect->snap-points + [rect] + (when (some? rect) + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + #{(gpt/point x y) + (gpt/point (+ x w) y) + (gpt/point (+ x w) (+ y h)) + (gpt/point x (+ y h)) + (grc/rect->center rect)}))) + +(defn- frame->snap-points + [frame] + (let [points (dm/get-prop frame :points) + rect (grc/points->rect points) + x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (into (rect->snap-points rect) + #{(gpt/point (+ x (/ w 2)) y) + (gpt/point (+ x w) (+ y (/ h 2))) + (gpt/point (+ x (/ w 2)) (+ y h)) + (gpt/point x (+ y (/ h 2)))}))) + +(defn shape->snap-points + [shape] + (if ^boolean (cfh/frame-shape? shape) + (frame->snap-points shape) + (->> (dm/get-prop shape :points) + (into #{(gsh/shape->center shape)})))) + +(defn guide->snap-points + [guide frame] + (cond + (and (some? frame) + (not ^boolean (ctst/rotated-frame? frame)) + (not ^boolean (cfh/is-direct-child-of-root? frame))) + #{} + + (= :x (:axis guide)) + #{(gpt/point (:position guide) 0)} + + :else + #{(gpt/point 0 (:position guide))})) diff --git a/common/src/app/common/jsrt.clj b/common/src/app/common/jsrt.clj new file mode 100644 index 0000000000..034ecfba06 --- /dev/null +++ b/common/src/app/common/jsrt.clj @@ -0,0 +1,77 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.jsrt + "A JS runtime for the JVM" + (:refer-clojure :exclude [run!]) + (:require + [clojure.java.io :as io]) + (:import + org.apache.commons.pool2.ObjectPool + org.apache.commons.pool2.PooledObject + org.apache.commons.pool2.PooledObjectFactory + org.apache.commons.pool2.impl.DefaultPooledObject + org.apache.commons.pool2.impl.SoftReferenceObjectPool + org.graalvm.polyglot.Context + org.graalvm.polyglot.Source + org.graalvm.polyglot.Value)) + +(defn resource->source + [path] + (let [resource (io/resource path)] + (.. (Source/newBuilder "js" resource) + (build)))) + +(defn pool? + [o] + (instance? ObjectPool o)) + +(defn pool + [& {:keys [init]}] + (SoftReferenceObjectPool. + (reify PooledObjectFactory + (activateObject [_ _]) + (destroyObject [_ o] + (let [context (.getObject ^PooledObject o)] + (.close ^java.lang.AutoCloseable context))) + + (destroyObject [_ o _] + (let [context (.getObject ^PooledObject o)] + (.close ^java.lang.AutoCloseable context))) + + (passivateObject [_ _]) + (validateObject [_ _] true) + + (makeObject [_] + (let [context (Context/create (into-array String ["js"]))] + (.initialize ^Context context "js") + (when (instance? Source init) + (.eval ^Context context ^Source init)) + (DefaultPooledObject. context)))))) + +(defn run! + [^ObjectPool pool f] + (let [ctx (.borrowObject pool)] + (try + (f ctx) + (finally + (.returnObject pool ctx))))) + +(defn eval! + [context data & {:keys [as] :or {as :string}}] + (let [result (.eval ^Context context "js" ^String data)] + (case as + (:string :str) (.asString ^Value result) + :long (.asLong ^Value result) + :int (.asInt ^Value result) + :float (.asFloat ^Value result) + :double (.asDouble ^Value result)))) + +(defn set! + [context attr value] + (let [bindings (.getBindings ^Context context "js")] + (.putMember ^Value bindings ^String attr ^String value) + context)) diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc index 0402343b94..d7780ef70e 100644 --- a/common/src/app/common/logging.cljc +++ b/common/src/app/common/logging.cljc @@ -239,7 +239,7 @@ #?(:clj (defn slf4j-log-handler {:no-doc true} - [_ _ _ {:keys [::logger ::level ::trace ::message] }] + [_ _ _ {:keys [::logger ::level ::trace ::message]}] (when-let [logger (enabled? logger level)] (let [message (cond-> @message (some? trace) @@ -271,7 +271,7 @@ (js/console.error n (pr-str v)) (js/console.error n v)))) - (when cause + (when (ex/exception? cause) (let [data (ex-data cause) explain (ex/explain data)] (when explain @@ -312,13 +312,19 @@ (let [cljs? (:ns &env)] `(do (~(if cljs? - `(partial console-log-handler nil nil nil) - `(partial slf4j-log-handler nil nil nil)) + `(partial console-log-handler nil nil nil) + `(partial slf4j-log-handler nil nil nil)) {::logger ~(str *ns*) ::level ~level ::message (delay ~message)}) nil))) +(defmacro log + [level & params] + `(do + (log! ::logger ~(str *ns*) ::level ~level ~@params) + nil)) + (defmacro info [& params] `(do diff --git a/common/src/app/common/math.cljc b/common/src/app/common/math.cljc index 0adc340beb..ac2f67ba8c 100644 --- a/common/src/app/common/math.cljc +++ b/common/src/app/common/math.cljc @@ -6,9 +6,24 @@ (ns app.common.math "A collection of math utils." - (:refer-clojure :exclude [abs]) + (:refer-clojure :exclude [abs min max]) #?(:cljs - (:require [goog.math :as math]))) + (:require-macros [app.common.math :refer [min max]])) + (:require + #?(:cljs [goog.math :as math]) + [clojure.core :as c])) + +(defmacro min + [& params] + (if (:ns &env) + `(js/Math.min ~@params) + `(c/min ~@params))) + +(defmacro max + [& params] + (if (:ns &env) + `(js/Math.max ~@params) + `(c/max ~@params))) (def PI #?(:cljs (.-PI js/Math) @@ -118,9 +133,10 @@ (defn ceil "Returns the smallest integer greater than or equal to a given number." + ^double [v] #?(:cljs (js/Math.ceil v) - :clj (Math/ceil v))) + :clj (Math/ceil ^double v))) (defn precision [v n] @@ -177,7 +193,7 @@ (defn round-to-zero "Given a number if it's close enough to zero round to the zero to avoid precision problems" [num] - (if (almost-zero? num) + (if (< (abs num) 1e-4) 0 num)) @@ -198,10 +214,12 @@ (defn max-abs [a b] - (max (abs a) (abs b))) + (max (abs a) + (abs b))) (defn sign "Get the sign (+1 / -1) for the number" [n] (if (neg? n) -1 1)) + diff --git a/common/src/app/common/media.cljc b/common/src/app/common/media.cljc index 064f11fb2c..a342a227f0 100644 --- a/common/src/app/common/media.cljc +++ b/common/src/app/common/media.cljc @@ -10,7 +10,7 @@ [cuerdas.core :as str])) ;; We have added ".ttf" as string to solve a problem with chrome input selector -(def valid-font-types #{"font/ttf", ".ttf", "font/woff", "application/font-woff", "font/otf"}) +(def valid-font-types #{"font/ttf" ".ttf" "font/woff", "application/font-woff" "woff" "font/otf" ".otf" "font/opentype"}) (def valid-image-types #{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"}) (def str-image-types (str/join "," valid-image-types)) (def str-font-types (str/join "," valid-font-types)) @@ -58,6 +58,7 @@ "application/zip" ".zip" "application/penpot" ".penpot" "application/pdf" ".pdf" + "text/plain" ".txt" nil)) (s/def ::id uuid?) diff --git a/common/src/app/common/pages.cljc b/common/src/app/common/pages.cljc deleted file mode 100644 index 2d52f03c47..0000000000 --- a/common/src/app/common/pages.cljc +++ /dev/null @@ -1,42 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.common.pages - "A common (clj/cljs) functions and specs for pages." - (:require - [app.common.data.macros :as dm] - [app.common.pages.changes :as changes] - [app.common.pages.common :as common] - [app.common.pages.focus :as focus] - [app.common.pages.indices :as indices] - [app.common.types.file :as ctf])) - -;; Common -(dm/export common/root) -(dm/export common/file-version) -(dm/export common/default-color) -(dm/export common/component-sync-attrs) -(dm/export common/retrieve-used-names) -(dm/export common/generate-unique-name) - -;; Focus -(dm/export focus/focus-objects) -(dm/export focus/filter-not-focus) -(dm/export focus/is-in-focus?) - -;; Indices -#_(dm/export indices/calculate-z-index) -#_(dm/export indices/update-z-index) -(dm/export indices/generate-child-all-parents-index) -(dm/export indices/generate-child-parent-index) -(dm/export indices/create-clip-index) - -;; Process changes -(dm/export changes/process-changes) - -;; Initialization -(dm/export ctf/make-file-data) -(dm/export ctf/empty-file-data) diff --git a/common/src/app/common/pages/migrations.cljc b/common/src/app/common/pages/migrations.cljc deleted file mode 100644 index 4cfee92b64..0000000000 --- a/common/src/app/common/pages/migrations.cljc +++ /dev/null @@ -1,503 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.common.pages.migrations - (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.geom.matrix :as gmt] - [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.path :as gsp] - [app.common.geom.shapes.text :as gsht] - [app.common.logging :as l] - [app.common.math :as mth] - [app.common.pages :as cp] - [app.common.pages.helpers :as cph] - [app.common.text :as txt] - [app.common.types.shape :as cts] - [app.common.uuid :as uuid] - [cuerdas.core :as str])) - -;; TODO: revisit this and rename to file-migrations - -(defmulti migrate :version) - -#?(:cljs (l/set-level! :info)) - -(defn migrate-data - ([data] (migrate-data data cp/file-version)) - ([data to-version] - (if (= (:version data) to-version) - data - (let [migrate-fn #(do - (l/trc :hint "migrate file" :id (:id %) :version-from %2 :version-to (inc %2)) - (migrate (assoc %1 :version (inc %2))))] - (reduce migrate-fn data (range (:version data 0) to-version)))))) - -(defn migrate-file - [{:keys [id data] :as file}] - (let [data (assoc data :id id)] - (-> file - (assoc ::orig-version (:version data)) - (assoc :data (migrate-data data))))) - -(defn migrated? - [{:keys [data] :as file}] - (or (::migrated file) - (> (:version data) - (::orig-version file)))) - -;; Default handler, noop -(defmethod migrate :default [data] data) - -;; -- MIGRATIONS -- - -;; Ensure that all :shape attributes on shapes are vectors. -(defmethod migrate 2 - [data] - (letfn [(update-object [object] - (d/update-when object :shapes - (fn [shapes] - (if (seq? shapes) - (into [] shapes) - shapes)))) - (update-page [page] - (update page :objects update-vals update-object))] - - (update data :pages-index update-vals update-page))) - -;; Changes paths formats -(defmethod migrate 3 - [data] - (letfn [(migrate-path [shape] - (if-not (contains? shape :content) - (let [content (gsp/segments->content (:segments shape) (:close? shape)) - selrect (gsh/content->selrect content) - points (gsh/rect->points selrect)] - (-> shape - (dissoc :segments) - (dissoc :close?) - (assoc :content content) - (assoc :selrect selrect) - (assoc :points points))) - ;; If the shape contains :content is already in the new format - shape)) - - (fix-frames-selrects [frame] - (if (= (:id frame) uuid/zero) - frame - (let [frame-rect (select-keys frame [:x :y :width :height])] - (-> frame - (assoc :selrect (gsh/rect->selrect frame-rect)) - (assoc :points (gsh/rect->points frame-rect)))))) - - (fix-empty-points [shape] - (let [shape (cond-> shape - (empty? (:selrect shape)) (cts/setup-rect-selrect))] - (cond-> shape - (empty? (:points shape)) - (assoc :points (gsh/rect->points (:selrect shape)))))) - - (update-object [object] - (cond-> object - (= :curve (:type object)) - (assoc :type :path) - - (#{:curve :path} (:type object)) - (migrate-path) - - (cph/frame-shape? object) - (fix-frames-selrects) - - (and (empty? (:points object)) (not= (:id object) uuid/zero)) - (fix-empty-points))) - - (update-page [page] - (update page :objects update-vals update-object))] - - (update data :pages-index update-vals update-page))) - -;; We did rollback version 4 migration. -;; Keep this in order to remember the next version to be 5 -(defmethod migrate 4 [data] data) - -;; Put the id of the local file in :component-file in instances of local components -(defmethod migrate 5 - [data] - (letfn [(update-object [object] - (if (and (some? (:component-id object)) - (nil? (:component-file object))) - (assoc object :component-file (:id data)) - object)) - - (update-page [page] - (update page :objects update-vals update-object))] - - (update data :pages-index update-vals update-page))) - -(defmethod migrate 6 - [data] - ;; Fixes issues with selrect/points for shapes with width/height = 0 (line-like paths)" - (letfn [(fix-line-paths [shape] - (if (= (:type shape) :path) - (let [{:keys [width height]} (gsh/points->rect (:points shape))] - (if (or (mth/almost-zero? width) (mth/almost-zero? height)) - (let [selrect (gsh/content->selrect (:content shape)) - points (gsh/rect->points selrect) - transform (gmt/matrix) - transform-inv (gmt/matrix)] - (assoc shape - :selrect selrect - :points points - :transform transform - :transform-inverse transform-inv)) - shape)) - shape)) - - (update-container [container] - (update container :objects update-vals fix-line-paths))] - - (-> data - (update :pages-index update-vals update-container) - (update :components update-vals update-container)))) - -;; Remove interactions pointing to deleted frames -(defmethod migrate 7 - [data] - (letfn [(update-object [page object] - (d/update-when object :interactions - (fn [interactions] - (filterv #(get-in page [:objects (:destination %)]) interactions)))) - - (update-page [page] - (update page :objects update-vals (partial update-object page)))] - - (update data :pages-index update-vals update-page))) - -;; Remove groups without any shape, both in pages and components - -(defmethod migrate 8 - [data] - (letfn [(clean-parents [obj deleted?] - (d/update-when obj :shapes - (fn [shapes] - (into [] (remove deleted?) shapes)))) - - (obj-is-empty? [obj] - (and (= (:type obj) :group) - (or (empty? (:shapes obj)) - (nil? (:selrect obj))))) - - (clean-objects [objects] - (loop [entries (seq objects) - deleted #{} - result objects] - (let [[id obj :as entry] (first entries)] - (if entry - (if (obj-is-empty? obj) - (recur (rest entries) - (conj deleted id) - (dissoc result id)) - (recur (rest entries) - deleted - result)) - [(count deleted) - (d/mapm #(clean-parents %2 deleted) result)])))) - - (clean-container [container] - (loop [n 0 - objects (:objects container)] - (let [[deleted objects] (clean-objects objects)] - (if (and (pos? deleted) (< n 1000)) - (recur (inc n) objects) - (assoc container :objects objects)))))] - - (-> data - (update :pages-index update-vals clean-container) - (update :components update-vals clean-container)))) - -(defmethod migrate 9 - [data] - (letfn [(find-empty-groups [objects] - (->> (vals objects) - (filter (fn [shape] - (and (= :group (:type shape)) - (or (empty? (:shapes shape)) - (every? (fn [child-id] - (not (contains? objects child-id))) - (:shapes shape)))))) - (map :id))) - - (calculate-changes [[page-id page]] - (let [objects (:objects page) - eids (find-empty-groups objects)] - - (map (fn [id] - {:type :del-obj - :page-id page-id - :id id}) - eids)))] - - (loop [data data] - (let [changes (mapcat calculate-changes (:pages-index data))] - (if (seq changes) - (recur (cp/process-changes data changes)) - data))))) - -(defmethod migrate 10 - [data] - (letfn [(update-page [page] - (d/update-in-when page [:objects uuid/zero] dissoc :points :selrect))] - (update data :pages-index update-vals update-page))) - -(defmethod migrate 11 - [data] - (letfn [(update-object [objects shape] - (if (cph/frame-shape? shape) - (d/update-when shape :shapes (fn [shapes] - (filterv (fn [id] (contains? objects id)) shapes))) - shape)) - - (update-page [page] - (update page :objects (fn [objects] - (update-vals objects (partial update-object objects)))))] - - (update data :pages-index update-vals update-page))) - -(defmethod migrate 12 - [data] - (letfn [(update-grid [grid] - (cond-> grid - (= :auto (:size grid)) - (assoc :size nil))) - - (update-page [page] - (d/update-in-when page [:options :saved-grids] update-vals update-grid))] - - (update data :pages-index update-vals update-page))) - -;; Add rx and ry to images -(defmethod migrate 13 - [data] - (letfn [(fix-radius [shape] - (if-not (or (contains? shape :rx) (contains? shape :r1)) - (-> shape - (assoc :rx 0) - (assoc :ry 0)) - shape)) - - (update-object [object] - (cond-> object - (cph/image-shape? object) - (fix-radius))) - - (update-page [page] - (update page :objects update-vals update-object))] - - (update data :pages-index update-vals update-page))) - -(defmethod migrate 14 - [data] - (letfn [(process-shape [shape] - (let [fill-color (str/upper (:fill-color shape)) - fill-opacity (:fill-opacity shape)] - (cond-> shape - (and (= 1 fill-opacity) - (or (= "#B1B2B5" fill-color) - (= "#7B7D85" fill-color))) - (dissoc :fill-color :fill-opacity)))) - - (update-container [{:keys [objects] :as container}] - (loop [objects objects - shapes (->> (vals objects) - (filter cph/image-shape?))] - (if-let [shape (first shapes)] - (let [{:keys [id frame-id] :as shape'} (process-shape shape)] - (if (identical? shape shape') - (recur objects (rest shapes)) - (recur (-> objects - (assoc id shape') - (d/update-when frame-id dissoc :thumbnail)) - (rest shapes)))) - (assoc container :objects objects))))] - - (-> data - (update :pages-index update-vals update-container) - (update :components update-vals update-container)))) - - -(defmethod migrate 15 [data] data) - -;; Add fills and strokes -(defmethod migrate 16 - [data] - (letfn [(assign-fills [shape] - (let [attrs {:fill-color (:fill-color shape) - :fill-color-gradient (:fill-color-gradient shape) - :fill-color-ref-file (:fill-color-ref-file shape) - :fill-color-ref-id (:fill-color-ref-id shape) - :fill-opacity (:fill-opacity shape)} - clean-attrs (d/without-nils attrs)] - (cond-> shape - (d/not-empty? clean-attrs) - (assoc :fills [clean-attrs])))) - - (assign-strokes [shape] - (let [attrs {:stroke-style (:stroke-style shape) - :stroke-alignment (:stroke-alignment shape) - :stroke-width (:stroke-width shape) - :stroke-color (:stroke-color shape) - :stroke-color-ref-id (:stroke-color-ref-id shape) - :stroke-color-ref-file (:stroke-color-ref-file shape) - :stroke-opacity (:stroke-opacity shape) - :stroke-color-gradient (:stroke-color-gradient shape) - :stroke-cap-start (:stroke-cap-start shape) - :stroke-cap-end (:stroke-cap-end shape)} - clean-attrs (d/without-nils attrs)] - (cond-> shape - (d/not-empty? clean-attrs) - (assoc :strokes [clean-attrs])))) - - (update-object [object] - (cond-> object - (and (not (cph/text-shape? object)) - (not (contains? object :strokes))) - (assign-strokes) - - (and (not (cph/text-shape? object)) - (not (contains? object :fills))) - (assign-fills))) - - (update-container [container] - (update container :objects update-vals update-object))] - - (-> data - (update :pages-index update-vals update-container) - (update :components update-vals update-container)))) - -(defmethod migrate 17 - [data] - (letfn [(affected-object? [object] - (and (cph/image-shape? object) - (some? (:fills object)) - (= 1 (count (:fills object))) - (some? (:fill-color object)) - (some? (:fill-opacity object)) - (let [color-old (str/upper (:fill-color object)) - color-new (str/upper (get-in object [:fills 0 :fill-color])) - opacity-old (:fill-opacity object) - opacity-new (get-in object [:fills 0 :fill-opacity])] - (and (= color-old color-new) - (or (= "#B1B2B5" color-old) - (= "#7B7D85" color-old)) - (= 1 opacity-old opacity-new))))) - - (update-object [object] - (cond-> object - (affected-object? object) - (assoc :fills []))) - - (update-container [container] - (update container :objects update-vals update-object))] - - (-> data - (update :pages-index update-vals update-container) - (update :components update-vals update-container)))) - -;;Remove position-data to solve a bug with the text positioning -(defmethod migrate 18 - [data] - (letfn [(update-object [object] - (cond-> object - (cph/text-shape? object) - (dissoc :position-data))) - - (update-container [container] - (update container :objects update-vals update-object))] - - (-> data - (update :pages-index update-vals update-container) - (update :components update-vals update-container)))) - -(defmethod migrate 19 - [data] - (letfn [(update-object [object] - (cond-> object - (and (cph/text-shape? object) - (d/not-empty? (:position-data object)) - (not (gsht/overlaps-position-data? object (:position-data object)))) - (dissoc :position-data))) - - (update-container [container] - (update container :objects update-vals update-object))] - - (-> data - (update :pages-index update-vals update-container) - (update :components update-vals update-container)))) - -(defmethod migrate 21 - [data] - (letfn [(update-object [objects object] - (let [frame-id (:frame-id object) - calculated-frame-id - (or (->> (cph/get-parent-ids objects (:id object)) - (map (d/getf objects)) - (d/seek cph/frame-shape?) - :id) - ;; If we cannot find any we let the frame-id as it was before - frame-id)] - (when (not= frame-id calculated-frame-id) - (l/trc :hint "Fix wrong frame-id" - :shape (:name object) - :id (:id object) - :current (dm/get-in objects [frame-id :name]) - :calculated (get-in objects [calculated-frame-id :name]))) - (assoc object :frame-id calculated-frame-id))) - - (update-container [container] - (update container :objects #(update-vals % (partial update-object %))))] - - (-> data - (update :pages-index update-vals update-container) - (update :components update-vals update-container)))) - -;; TODO: pending to do a migration for delete already not used fill -;; and stroke props. This should be done for >1.14.x version. - -(defmethod migrate 22 - [data] - (letfn [(valid-ref? [ref] - (or (uuid? ref) - (nil? ref))) - - (valid-node? [node] - (and (valid-ref? (:typography-ref-file node)) - (valid-ref? (:typography-ref-id node)) - (valid-ref? (:fill-color-ref-file node)) - (valid-ref? (:fill-color-ref-id node)))) - - (fix-ref [ref] - (if (valid-ref? ref) ref nil)) - - (fix-node [node] - (-> node - (d/update-when :typography-ref-file fix-ref) - (d/update-when :typography-ref-id fix-ref) - (d/update-when :fill-color-ref-file fix-ref) - (d/update-when :fill-color-ref-id fix-ref))) - - (update-object [object] - (let [invalid-node? (complement valid-node?)] - (cond-> object - (cph/text-shape? object) - (update :content #(txt/transform-nodes invalid-node? fix-node %))))) - - (update-container [container] - (update container :objects update-vals update-object))] - - (-> data - (update :pages-index update-vals update-container) - (update :components update-vals update-container)))) diff --git a/common/src/app/common/perf.cljc b/common/src/app/common/perf.cljc index 46ee65c877..8723ffc54c 100644 --- a/common/src/app/common/perf.cljc +++ b/common/src/app/common/perf.cljc @@ -44,11 +44,11 @@ "Determine a scale factor and unit for displaying a time." [measurement] (cond - (> measurement 60) [(/ 60) "min"] - (< measurement 1e-6) [1e9 "ns"] - (< measurement 1e-3) [1e6 "µs"] - (< measurement 1) [1e3 "ms"] - :else [1 "sec"])) + (> measurement 60) [(/ 60) "min"] + (< measurement 1e-6) [1e9 "ns"] + (< measurement 1e-3) [1e6 "µs"] + (< measurement 1) [1e3 "ms"] + :else [1 "sec"])) (defn format-time [value] diff --git a/common/src/app/common/pprint.cljc b/common/src/app/common/pprint.cljc index c17bc54736..e1c9ea39e5 100644 --- a/common/src/app/common/pprint.cljc +++ b/common/src/app/common/pprint.cljc @@ -7,16 +7,33 @@ (ns app.common.pprint (:refer-clojure :exclude [prn]) (:require - [fipp.edn :as fpp])) + [me.flowthing.pp :as pp])) -(defn pprint-str - [expr & {:keys [width level length] - :or {width 110 level 8 length 25}}] - (binding [*print-level* level - *print-length* length] - (with-out-str - (fpp/pprint expr {:width width})))) +(def default-level 8) +(def default-length 25) +(def default-width 120) + +#?(:clj + (defn set-defaults + [& {:keys [level width length]}] + (when length + (alter-var-root #'default-length (constantly length))) + (when width + (alter-var-root #'default-width (constantly width))) + (when level + (alter-var-root #'default-level (constantly level))) + nil)) (defn pprint + [expr & {:keys [width level length] + :or {width default-width + level default-level + length default-length}}] + (binding [*print-level* level + *print-length* length] + (pp/pprint expr {:max-width width}))) + +(defn pprint-str [expr & {:as opts}] - (println (pprint-str expr opts))) + (with-out-str + (pprint expr opts))) diff --git a/common/src/app/common/record.cljc b/common/src/app/common/record.cljc new file mode 100644 index 0000000000..f9d10df0ce --- /dev/null +++ b/common/src/app/common/record.cljc @@ -0,0 +1,450 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.record + "A collection of helpers and macros for defien a penpot customized record types." + (:refer-clojure :exclude [defrecord assoc! clone]) + #?(:cljs (:require-macros [app.common.record])) + #?(:clj + (:import java.util.Map$Entry))) + +#_:clj-kondo/ignore +(defmacro caching-hash + [coll hash-fn hash-key] + `(let [h# ~hash-key] + (if-not (nil? h#) + h# + (let [h# (~hash-fn ~coll)] + (set! ~hash-key h#) + h#)))) + +#?(:clj + (defn- property-symbol + [sym] + (symbol (str "-" (name sym))))) + +#?(:clj + (defn- generate-field-access + [this-sym val-sym fields] + (map (fn [field] + (cond + (nil? field) nil + (identical? field val-sym) val-sym + :else `(. ~this-sym ~(property-symbol field)))) + fields))) + + +(defprotocol ICustomRecordEquiv + (-equiv-with-exceptions [_ other exceptions])) + +#?(:clj + (defn emit-impl-js + [tagname base-fields] + (let [fields (conj base-fields '$meta '$extmap (with-meta '$hash {:mutable true})) + key-sym (gensym "key-") + val-sym (gensym "val-") + othr-sym (with-meta 'other {:tag tagname}) + this-sym (with-meta 'this {:tag tagname})] + ['cljs.core/IRecord + 'cljs.core/ICloneable + `(~'-clone [~this-sym] + (new ~tagname ~@(generate-field-access this-sym val-sym fields))) + + 'cljs.core/IHash + `(~'-hash [~this-sym] + (caching-hash ~this-sym + (fn [coll#] + (bit-xor + ~(hash (str tagname)) + (cljs.core/hash-unordered-coll coll#))) + (. ~this-sym ~'-$hash))) + + 'cljs.core/IEquiv + `(~'-equiv [~this-sym ~othr-sym] + (or (identical? ~this-sym ~othr-sym) + (and (some? ~othr-sym) + (identical? (.-constructor ~this-sym) + (.-constructor ~othr-sym)) + ~@(map (fn [field] + `(= (.. ~this-sym ~(property-symbol field)) + (.. ~(with-meta othr-sym {:tag tagname}) ~(property-symbol field)))) + base-fields) + + (= (. ~this-sym ~'-$extmap) + (. ~(with-meta othr-sym {:tag tagname}) ~'-$extmap))))) + + `ICustomRecordEquiv + `(~'-equiv-with-exceptions [~this-sym ~othr-sym ~'exceptions] + (or (identical? ~this-sym ~othr-sym) + (and (some? ~othr-sym) + (identical? (.-constructor ~this-sym) + (.-constructor ~othr-sym)) + (and ~@(->> base-fields + (map (fn [field] + `(= (.. ~this-sym ~(property-symbol field)) + (.. ~(with-meta othr-sym {:tag tagname}) ~(property-symbol field)))))) + (== (count (. ~this-sym ~'-$extmap)) + (count (. ~othr-sym ~'-$extmap)))) + + (reduce-kv (fn [~'_ ~'k ~'v] + (if (contains? ~'exceptions ~'k) + true + (if (= (get (. ~this-sym ~'-$extmap) ~'k ::not-exists) ~'v) + true + (reduced false)))) + true + (. ~othr-sym ~'-$extmap))))) + + + 'cljs.core/IMeta + `(~'-meta [~this-sym] (. ~this-sym ~'-$meta)) + + 'cljs.core/IWithMeta + `(~'-with-meta [~this-sym ~val-sym] + (new ~tagname ~@(->> (replace {'$meta val-sym} fields) + (generate-field-access this-sym val-sym)))) + + 'cljs.core/ILookup + `(~'-lookup [~this-sym k#] + (cljs.core/-lookup ~this-sym k# nil)) + + `(~'-lookup [~this-sym ~key-sym else#] + (case ~key-sym + ~@(mapcat (fn [f] [(keyword f) `(. ~this-sym ~(property-symbol f))]) + base-fields) + (cljs.core/get (. ~this-sym ~'-$extmap) ~key-sym else#))) + + 'cljs.core/ICounted + `(~'-count [~this-sym] + (+ ~(count base-fields) (count (. ~this-sym ~'-$extmap)))) + + 'cljs.core/ICollection + `(~'-conj [~this-sym ~val-sym] + (if (vector? ~val-sym) + (cljs.core/-assoc ~this-sym (cljs.core/-nth ~val-sym 0) (cljs.core/-nth ~val-sym 1)) + (reduce cljs.core/-conj ~this-sym ~val-sym))) + + 'cljs.core/IAssociative + `(~'-contains-key? [~this-sym ~key-sym] + ~(if (seq base-fields) + `(case ~key-sym + (~@(map keyword base-fields)) true + (contains? (. ~this-sym ~'-$extmap) ~key-sym)) + `(contains? (. ~this-sym ~'-$extmap) ~key-sym))) + + `(~'-assoc [~this-sym ~key-sym ~val-sym] + (case ~key-sym + ~@(mapcat (fn [fld] + [(keyword fld) `(new ~tagname ~@(->> (replace {fld val-sym '$hash nil} fields) + (generate-field-access this-sym val-sym)))]) + base-fields) + (new ~tagname ~@(->> (remove #{'$extmap '$hash} fields) + (generate-field-access this-sym val-sym)) + (assoc (. ~this-sym ~'-$extmap) ~key-sym ~val-sym) nil))) + + 'cljs.core/ITransientAssociative + `(~'-assoc! [~this-sym ~key-sym ~val-sym] + (let [key# (if (keyword? ~key-sym) + (.-fqn ~(with-meta key-sym {:tag `cljs.core/Keyword})) + ~key-sym)] + (case ~key-sym + ~@(mapcat + (fn [f] + [(keyword f) `(set! (. ~this-sym ~(property-symbol f)) ~val-sym)]) + base-fields) + + (set! (. ~this-sym ~'-$extmap) (cljs.core/assoc (. ~this-sym ~'-$extmap) ~key-sym ~val-sym))) + + ~this-sym)) + + 'cljs.core/IMap + `(~'-dissoc [~this-sym ~key-sym] + (case ~key-sym + (~@(map keyword base-fields)) + (cljs.core/-assoc ~this-sym ~key-sym nil) + + (let [extmap1# (. ~this-sym ~'-$extmap) + extmap2# (dissoc extmap1# ~key-sym)] + (if (identical? extmap1# extmap2#) + ~this-sym + (new ~tagname ~@(->> (remove #{'$extmap '$hash} fields) + (generate-field-access this-sym val-sym)) + (not-empty extmap2#) + nil))))) + + 'cljs.core/ISeqable + `(~'-seq [~this-sym] + (seq (concat [~@(map (fn [f] + `(cljs.core/MapEntry. + ~(keyword f) + (. ~this-sym ~(property-symbol f)) + nil)) + base-fields)] + (. ~this-sym ~'-$extmap)))) + + 'cljs.core/IIterable + `(~'-iterator [~this-sym] + (cljs.core/RecordIter. 0 ~this-sym ~(count base-fields) + [~@(map keyword base-fields)] + (if (. ~this-sym ~'-$extmap) + (cljs.core/-iterator (. ~this-sym ~'-$extmap)) + (cljs.core/nil-iter)))) + + 'cljs.core/IKVReduce + `(~'-kv-reduce [~this-sym f# init#] + (reduce (fn [ret# [~key-sym v#]] (f# ret# ~key-sym v#)) init# ~this-sym))]))) + +#?(:clj + (defn emit-impl-jvm + [tagname base-fields] + (let [fields (conj base-fields '$meta '$extmap (with-meta '$hash {:unsynchronized-mutable true})) + key-sym 'key + val-sym 'val + this-sym (with-meta 'this {:tag tagname})] + + ['clojure.lang.IRecord + 'clojure.lang.IPersistentMap + `(~'equiv [~this-sym ~val-sym] + (and (some? ~val-sym) + (instance? ~tagname ~val-sym) + ~@(map (fn [field] + `(= (.. ~this-sym ~(property-symbol field)) + (.. ~(with-meta val-sym {:tag tagname}) ~(property-symbol field)))) + base-fields) + (= (. ~this-sym ~'-$extmap) + (. ~(with-meta val-sym {:tag tagname}) ~'-$extmap)))) + + `(~'entryAt [~this-sym ~key-sym] + (let [v# (.valAt ~this-sym ~key-sym ::not-found)] + (when (not= v# ::not-found) + (clojure.lang.MapEntry. ~key-sym v#)))) + + `(~'valAt [~this-sym ~key-sym] + (.valAt ~this-sym ~key-sym nil)) + + `(~'valAt [~this-sym ~key-sym ~'not-found] + (case ~key-sym + ~@(mapcat (fn [f] [(keyword f) `(. ~this-sym ~(property-symbol f))]) base-fields) + (clojure.core/get (. ~this-sym ~'-$extmap) ~key-sym ~'not-found))) + + `(~'count [~this-sym] + (+ ~(count base-fields) (count (. ~this-sym ~'-$extmap)))) + + + `(~'empty [~this-sym] + (new ~tagname ~@(->> (remove #{'$extmap '$hash} fields) + (generate-field-access this-sym nil)) + nil nil)) + + `(~'cons [~this-sym ~val-sym] + (if (instance? java.util.Map$Entry ~val-sym) + (let [^Map$Entry e# ~val-sym] + (.assoc ~this-sym (.getKey e#) (.getValue e#))) + (if (instance? clojure.lang.IPersistentVector ~val-sym) + (if (= 2 (count ~val-sym)) + (.assoc ~this-sym (nth ~val-sym 0) (nth ~val-sym 1)) + (throw (IllegalArgumentException. + "Vector arg to map conj must be a pair"))) + (reduce (fn [^clojure.lang.IPersistentMap m# + ^java.util.Map$Entry e#] + (.assoc m# (.getKey e#) (.getValue e#))) + ~this-sym + ~val-sym)))) + + `(~'assoc [~this-sym ~key-sym ~val-sym] + (case ~key-sym + ~@(mapcat (fn [fld] + [(keyword fld) `(new ~tagname ~@(->> (replace {fld val-sym '$hash nil} fields) + (generate-field-access this-sym val-sym)))]) + base-fields) + (new ~tagname ~@(->> (remove #{'$extmap '$hash} fields) + (generate-field-access this-sym val-sym)) + (assoc (. ~this-sym ~'-$extmap) ~key-sym ~val-sym) + nil))) + + `(~'without [~this-sym ~key-sym] + (case ~key-sym + (~@(map keyword base-fields)) + (.assoc ~this-sym ~key-sym nil) + + (if-let [extmap1# (. ~this-sym ~'-$extmap)] + (let [extmap2# (.without ^clojure.lang.IPersistentMap extmap1# ~key-sym)] + (if (identical? extmap1# extmap2#) + ~this-sym + (new ~tagname ~@(->> (remove #{'$extmap '$hash} fields) + (generate-field-access this-sym val-sym)) + (not-empty extmap2#) + nil))) + ~this-sym))) + + `(~'seq [~this-sym] + (seq (concat [~@(map (fn [f] + `(clojure.lang.MapEntry/create + ~(keyword f) + (. ~this-sym ~(property-symbol f)))) + base-fields)] + (. ~this-sym ~'-$extmap)))) + + `(~'iterator [~this-sym] + (clojure.lang.SeqIterator. (.seq ~this-sym))) + + 'clojure.lang.IFn + `(~'invoke [~this-sym ~key-sym] + (.valAt ~this-sym ~key-sym)) + + `(~'invoke [~this-sym ~key-sym ~'not-found] + (.valAt ~this-sym ~key-sym ~'not-found)) + + 'java.util.Map + `(~'size [~this-sym] + (clojure.core/count ~this-sym)) + + `(~'containsKey [~this-sym ~key-sym] + ~(if (seq base-fields) + `(case ~key-sym + (~@(map keyword base-fields)) true + (contains? (. ~this-sym ~'-$extmap) ~key-sym)) + `(contains? (. ~this-sym ~'-$extmap) ~key-sym))) + + `(~'isEmpty [~this-sym] + (zero? (count ~this-sym))) + + `(~'keySet [~this-sym] + (throw (UnsupportedOperationException. "not implemented"))) + + `(~'entrySet [~this-sym] + (throw (UnsupportedOperationException. "not implemented"))) + + `(~'get [~this-sym ~key-sym] + (.valAt ~this-sym ~key-sym)) + + `(~'containsValue [~this-sym ~val-sym] + (throw (UnsupportedOperationException. "not implemented"))) + + `(~'values [~this-sym] + (map val (.seq ~this-sym))) + + 'java.lang.Object + `(~'equals [~this-sym other#] + (.equiv ~this-sym other#)) + + `(~'hashCode [~this-sym] + (clojure.lang.APersistentMap/mapHash ~this-sym)) + + 'clojure.lang.IHashEq + `(~'hasheq [~this-sym] + (clojure.core/hash-unordered-coll ~this-sym)) + + 'clojure.lang.IObj + `(~'meta [~this-sym] + (. ~this-sym ~'-$meta)) + + `(~'withMeta [~this-sym ~val-sym] + (new ~tagname ~@(->> (replace {'$meta val-sym} fields) + (generate-field-access this-sym val-sym))))]))) + +(defmacro defrecord + [rsym fields & impls] + (let [param (gensym "param-") + ks (map keyword fields) + fields' (mapv #(with-meta % nil) fields) + nsname (if (:ns &env) + (-> &env :ns :name) + (str *ns*)) + ident (str "#" nsname "." (name rsym))] + + `(do + (deftype ~rsym ~(into fields ['$meta '$extmap '$hash]) + ~@(if (:ns &env) + (emit-impl-js rsym fields') + (emit-impl-jvm rsym fields')) + ~@impls + + ~@(when (:ns &env) + ['cljs.core/IPrintWithWriter + `(~'-pr-writer [~'this writer# opts#] + (let [pr-pair# (fn [keyval#] + (cljs.core/pr-sequential-writer writer# (~'js* "cljs.core.pr_writer") + "" " " "" opts# keyval#))] + (cljs.core/pr-sequential-writer + writer# pr-pair# ~(str ident "{") ", " "}" opts# + (concat [~@(for [f fields'] + `(vector ~(keyword f) (. ~'this ~(property-symbol f))))] + (. ~'this ~'-$extmap)))))])) + + ~@(when-not (:ns &env) + [`(defmethod print-method ~rsym [o# ^java.io.Writer w#] + (.write w# ~(str "#" nsname "." (name rsym))) + (print-method (into {} o#) w#))]) + + (defn ~(with-meta (symbol (str "pos->" rsym)) + (assoc (meta rsym) :factory :positional)) + [~@fields'] + (new ~rsym ~@(conj fields nil nil nil))) + + (defn ~(with-meta (symbol (str 'map-> rsym)) + (assoc (meta rsym) :factory :map)) + [~param] + (let [exclude# #{~@ks} + extmap# (reduce-kv (fn [acc# k# v#] + (if (contains? exclude# k#) + acc# + (assoc acc# k# v#))) + {} + ~param)] + (new ~rsym + ~@(for [k ks] + `(get ~param ~k)) + nil + (not-empty extmap#) + nil))) + ~rsym))) + +(defmacro clone + [ssym] + (if (:ns &env) + `(cljs.core/clone ~ssym) + ssym)) + +(defmacro assoc! + "A record specific update operation" + [ssym & pairs] + (if (:ns &env) + (let [pairs (partition-all 2 pairs)] + `(-> ~ssym + ~@(map (fn [[ks vs]] + `(cljs.core/-assoc! ~ks ~vs)) + pairs))) + `(assoc ~ssym ~@pairs))) + +(defmacro update! + "A record specific update operation" + [ssym ksym f & params] + (if (:ns &env) + (let [ssym (with-meta ssym {:tag 'js})] + `(cljs.core/assoc! ~ssym ~ksym (~f (. ~ssym ~(property-symbol ksym)) ~@params))) + `(update ~ssym ~ksym ~f ~@params))) + +(defmacro define-properties! + [rsym & properties] + (let [rsym (with-meta rsym {:tag 'js})] + `(do + ~@(for [params properties + :let [pname (get params :name) + get-fn (get params :get) + set-fn (get params :set)]] + `(.defineProperty js/Object + (.-prototype ~rsym) + ~pname + (cljs.core/js-obj + "enumerable" true + "configurable" true + ~@(concat + (when get-fn + ["get" get-fn]) + (when set-fn + ["set" set-fn])))))))) + diff --git a/common/src/app/common/schema.cljc b/common/src/app/common/schema.cljc index 494b1df2a7..b1e743f643 100644 --- a/common/src/app/common/schema.cljc +++ b/common/src/app/common/schema.cljc @@ -8,13 +8,16 @@ (:refer-clojure :exclude [deref merge parse-uuid]) #?(:cljs (:require-macros [app.common.schema :refer [ignoring]])) (:require + [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.pprint :as pp] [app.common.schema.generators :as sg] [app.common.schema.openapi :as-alias oapi] [app.common.schema.registry :as sr] + [app.common.time :as tm] [app.common.uri :as u] [app.common.uuid :as uuid] - [clojure.test.check.generators :as tgen] + [clojure.core :as c] [cuerdas.core :as str] [malli.core :as m] [malli.dev.pretty :as mdp] @@ -24,25 +27,44 @@ [malli.transform :as mt] [malli.util :as mu])) -(defn validate - [s value] - (m/validate s value {:registry sr/default-registry})) +(defprotocol ILazySchema + (-get-schema [_]) + (-get-validator [_]) + (-get-explainer [_]) + (-get-decoder [_]) + (-get-encoder [_]) + (-validate [_ o]) + (-explain [_ o]) + (-decode [_ o])) -(defn explain - [s value] - (m/explain s value {:registry sr/default-registry})) - -(defn explain-data - [s value] - (mu/explain-data s value {:registry sr/default-registry})) +(def default-options + {:registry sr/default-registry}) (defn schema? [o] (m/schema? o)) +(defn lazy-schema? + [s] + (satisfies? ILazySchema s)) + (defn schema [s] - (m/schema s {:registry sr/default-registry})) + (if (lazy-schema? s) + (-get-schema s) + (m/schema s default-options))) + +(defn validate + [s value] + (if (lazy-schema? s) + (-validate s value) + (m/validate s value default-options))) + +(defn explain + [s value] + (if (lazy-schema? s) + (-explain s value) + (m/explain s value default-options))) (defn humanize [exp] @@ -56,7 +78,7 @@ (defn form [s] - (m/form s {:registry sr/default-registry})) + (m/form s default-options)) (defn merge [& items] @@ -101,51 +123,79 @@ :encoders (mt/-string-encoders)} {:name :collections :decoders coders - :encoders coders} - - ))) + :encoders coders}))) (defn validator [s] - (-> s schema m/validator)) + (if (lazy-schema? s) + (-get-validator s) + (-> s schema m/validator))) (defn explainer [s] - (-> s schema m/explainer)) - -(defn lazy-validator - [s] - (let [vfn (delay (validator s))] - (fn [v] (@vfn v)))) - -(defn lazy-explainer - [s] - (let [vfn (delay (explainer s))] - (fn [v] (@vfn v)))) + (if (lazy-schema? s) + (-get-explainer s) + (-> s schema m/explainer))) (defn encode ([s val transformer] - (m/encode s val {:registry sr/default-registry} transformer)) + (m/encode s val default-options transformer)) ([s val options transformer] (m/encode s val options transformer))) (defn decode ([s val transformer] - (m/decode s val {:registry sr/default-registry} transformer)) + (m/decode s val default-options transformer)) ([s val options transformer] (m/decode s val options transformer))) -(defn decoder +(defn encoder + ([s] + (if (lazy-schema? s) + (-get-decoder s) + (encoder s default-options default-transformer))) ([s transformer] - (m/decoder s {:registry sr/default-registry} transformer)) + (m/encoder s default-options transformer)) + ([s options transformer] + (m/encoder s options transformer))) + +(defn decoder + ([s] + (if (lazy-schema? s) + (-get-decoder s) + (decoder s default-options default-transformer))) + ([s transformer] + (m/decoder s default-options transformer)) ([s options transformer] (m/decoder s options transformer))) -(defn humanize-data - [explain-data] - (-> explain-data - (update :schema form) - (update :errors (fn [errors] (map #(update % :schema form) errors))))) +(defn lazy-validator + [s] + (let [vfn (delay (validator (if (delay? s) (deref s) s)))] + (fn [v] (@vfn v)))) + +(defn lazy-explainer + [s] + (let [vfn (delay (explainer (if (delay? s) (deref s) s)))] + (fn [v] (@vfn v)))) + +(defn lazy-decoder + [s transformer] + (let [vfn (delay (decoder (if (delay? s) (deref s) s) transformer))] + (fn [v] (@vfn v)))) + +(defn humanize-explain + [{:keys [schema errors value]} & {:keys [length level]}] + (let [errors (mapv #(update % :schema form) errors)] + (with-out-str + (println "Schema: ") + (println (pp/pprint-str (form schema) {:width 100 :level 15 :length 20})) + (println "Errors:") + (println (pp/pprint-str errors {:width 100 :level 15 :length 20})) + (println "Value:") + (println (pp/pprint-str value {:width 160 + :level (d/nilv level 8) + :length (d/nilv length 12)}))))) (defn pretty-explain [s d] @@ -183,46 +233,86 @@ ([s] (lookup sr/default-registry s)) ([registry s] (schema (mr/schema registry s)))) -(defn pred-fn +(defn fast-check! + "A fast path for checking process, assumes the ILazySchema protocol + implemented on the provided `s` schema. Sould not be used directly." + [s value] + (when-not ^boolean (-validate s value) + (let [hint (d/nilv dm/*assert-context* "check error") + explain (-explain s value)] + (throw (ex-info hint {:type :assertion + :code :data-validation + :hint hint + ::explain explain})))) + true) + +(declare define) + +(defn check-fn + "Create a predefined check function" [s] - (let [s (schema s) - v-fn (lazy-validator s) - e-fn (lazy-explainer s)] - (fn [v] - (let [result (v-fn v)] - (when (and (not result) (true? dm/*assert-context*)) - (let [hint (str "schema assert: " (pr-str (form s))) - exp (e-fn v)] - (throw (ex-info hint {:type :assertion - :code :data-validation - :hint hint - ::explain exp})))) - result)))) + (let [schema (if (lazy-schema? s) s (define s))] + (partial fast-check! schema))) + +(defn check! + "A helper intended to be used on assertions for validate/check the + schema over provided data. Raises an assertion exception, should be + used together with `dm/assert!` or `dm/verify!`." + [s value] + (if (lazy-schema? s) + (fast-check! s value) + (do + (when-not ^boolean (m/validate s value default-options) + (let [hint (d/nilv dm/*assert-context* "check error") + explain (explain s value)] + (throw (ex-info hint {:type :assertion + :code :data-validation + :hint hint + ::explain explain})))) + true))) -(defn valid? - [s v] - (let [result (validate s v)] - (when (and (not result) (true? dm/*assert-context*)) - (let [hint (str "schema assert: " (pr-str (form s))) - exp (explain s v)] - (throw (ex-info hint {:type :assertion - :code :data-validation - :hint hint - ::explain exp})))) - result)) +(defn fast-validate! + "A fast path for validation process, assumes the ILazySchema protocol + implemented on the provided `s` schema. Sould not be used directly." + ([s value] (fast-validate! s value nil)) + ([s value options] + (when-not ^boolean (-validate s value) + (let [explain (-explain s value) + options (into {:type :validation + :code :data-validation + ::explain explain} + options) + hint (get options :hint "schema validation error")] + (throw (ex-info hint options)))))) -(defn assert-fn +(defn validate-fn + "Create a predefined validate function" [s] - (let [f (pred-fn s)] - (fn [v] - (dm/assert! (f v))))) + (let [schema (if (lazy-schema? s) s (define s))] + (partial fast-validate! schema))) -(defmacro verify-fn - [s] - (let [f (pred-fn s)] - (fn [v] - (dm/verify! (f v))))) +(defn validate! + "A generic validation function for predefined schemas." + ([s value] (validate! s value nil)) + ([s value options] + (if (lazy-schema? s) + (fast-validate! s value options) + (when-not ^boolean (m/validate s value default-options) + (let [explain (explain s value) + options (into {:type :validation + :code :data-validation + ::explain explain} + options) + hint (get options :hint "schema validation error")] + (throw (ex-info hint options))))))) + +(defn conform! + [schema value] + (assert (lazy-schema? schema) "expected `schema` to satisfy ILazySchema protocol") + (let [params (-decode schema value)] + (fast-validate! schema params nil) + params)) (defn register! [type s] (let [s (if (map? s) (simple-schema s) s)] @@ -232,22 +322,84 @@ (register! type s) nil) -;; --- GENERATORS +(defn define! [id s] + (register! id s) + nil) -;; FIXME: replace with sg/subseq -(defn gen-set-from-choices - [choices] - (->> tgen/nat - (tgen/fmap (fn [i] - (into #{} - (map (fn [_] (rand-nth choices))) - (range i)))))) +(defn define + "Create ans instance of ILazySchema" + [s & {:keys [transformer] :as options}] + (let [schema (delay (schema s)) + validator (delay (m/validator @schema)) + explainer (delay (m/explainer @schema)) + options (c/merge default-options (dissoc options :transformer)) + transformer (or transformer default-transformer) + decoder (delay (m/decoder @schema options transformer)) + encoder (delay (m/encoder @schema options transformer))] + + (reify + m/AST + (-to-ast [_ options] (m/-to-ast @schema options)) + + m/EntrySchema + (-entries [_] (m/-entries @schema)) + (-entry-parser [_] (m/-entry-parser @schema)) + + m/Cached + (-cache [_] (m/-cache @schema)) + + m/LensSchema + (-keep [_] (m/-keep @schema)) + (-get [_ key default] (m/-get @schema key default)) + (-set [_ key value] (m/-set @schema key value)) + + m/Schema + (-validator [_] + (m/-validator @schema)) + (-explainer [_ path] + (m/-explainer @schema path)) + (-parser [_] + (m/-parser @schema)) + (-unparser [_] + (m/-unparser @schema)) + (-transformer [_ transformer method options] + (m/-transformer @schema transformer method options)) + (-walk [_ walker path options] + (m/-walk @schema walker path options)) + (-properties [_] + (m/-properties @schema)) + (-options [_] + (m/-options @schema)) + (-children [_] + (m/-children @schema)) + (-parent [_] + (m/-parent @schema)) + (-form [_] + (m/-form @schema)) + + ILazySchema + (-get-schema [_] + @schema) + (-get-validator [_] + @validator) + (-get-explainer [_] + @explainer) + (-get-encoder [_] + @encoder) + (-get-decoder [_] + @decoder) + (-validate [_ o] + (@validator o)) + (-explain [_ o] + (@explainer o)) + (-decode [_ o] + (@decoder o))))) ;; --- BUILTIN SCHEMAS -(def! :merge (mu/-merge)) -(def! :union (mu/-union)) +(define! :merge (mu/-merge)) +(define! :union (mu/-union)) (def uuid-rx #"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") @@ -258,7 +410,7 @@ (some->> (re-matches uuid-rx s) uuid/uuid) s)) -(def! ::uuid +(define! ::uuid {:type ::uuid :pred uuid? :type-properties @@ -279,7 +431,7 @@ s)) ;; FIXME: add proper email generator -(def! ::email +(define! ::email {:type ::email :pred (fn [s] (and (string? s) @@ -300,7 +452,7 @@ (remove str/empty?) (remove str/blank?))) -(def! ::set-of-strings +(define! ::set-of-strings {:type ::set-of-strings :pred #(and (set? %) (every? string? %)) :type-properties @@ -316,7 +468,23 @@ (let [v (if (string? v) (str/split v #"[\s,]+") v)] (into #{} non-empty-strings-xf v)))}}) -(def! ::set-of-emails +(define! ::set-of-keywords + {:type ::set-of-keywords + :pred #(and (set? %) (every? keyword? %)) + :type-properties + {:title "set[string]" + :description "Set of Strings" + :error/message "should be a set of strings" + :gen/gen (-> :keyword sg/generator sg/set) + ::oapi/type "array" + ::oapi/format "set" + ::oapi/items {:type "string" :format "keyword"} + ::oapi/unique-items true + ::oapi/decode (fn [v] + (let [v (if (string? v) (str/split v #"[\s,]+") v)] + (into #{} (comp non-empty-strings-xf (map keyword)) v)))}}) + +(define! ::set-of-emails {:type ::set-of-emails :pred #(and (set? %) (every? string? %)) :type-properties @@ -332,7 +500,7 @@ (let [v (if (string? v) (str/split v #"[\s,]+") v)] (into #{} (keep parse-email) v)))}}) -(def! ::set-of-uuid +(define! ::set-of-uuid {:type ::set-of-uuid :pred #(and (set? %) (every? uuid? %)) :type-properties @@ -348,7 +516,7 @@ (let [v (if (string? v) (str/split v #"[\s,]+") v)] (into #{} (keep parse-uuid) v)))}}) -(def! ::coll-of-uuid +(define! ::coll-of-uuid {:type ::set-of-uuid :pred (partial every? uuid?) :type-properties @@ -364,7 +532,7 @@ (let [v (if (string? v) (str/split v #"[\s,]+") v)] (into [] (keep parse-uuid) v)))}}) -(def! ::one-of +(define! ::one-of {:type ::one-of :min 1 :max 1 @@ -387,7 +555,7 @@ ;; Integer/MIN_VALUE (def min-safe-int -2147483648) -(def! ::safe-int +(define! ::safe-int {:type ::safe-int :pred #(and (int? %) (>= max-safe-int %) (>= % min-safe-int)) :type-properties @@ -402,7 +570,7 @@ (parse-long s) s))}}) -(def! ::safe-number +(define! ::safe-number {:type ::safe-number :pred #(and (number? %) (>= max-safe-int %) (>= % min-safe-int)) :type-properties @@ -418,7 +586,7 @@ (parse-double s) s))}}) -(def! ::safe-double +(define! ::safe-double {:type ::safe-double :pred #(and (double? %) (>= max-safe-int %) (>= % min-safe-int)) :type-properties @@ -433,7 +601,7 @@ (parse-double s) s))}}) -(def! ::contains-any +(define! ::contains-any {:type ::contains-any :min 1 :max 1 @@ -451,21 +619,22 @@ {:title "contains" :description "contains predicate"}}))}) -(def! ::inst +(define! ::inst {:type ::inst :pred inst? :type-properties {:title "inst" :description "Satisfies Inst protocol" :error/message "expected to be number in safe range" - :gen/gen (sg/small-int) + :gen/gen (->> (sg/small-int) + (sg/fmap (fn [v] (tm/instant v)))) ::oapi/type "number" ::oapi/format "int64"}}) -(def! ::fn +(define! ::fn [:schema fn?]) -(def! ::word-string +(define! ::word-string {:type ::word-string :pred #(and (string? %) (not (str/blank? %))) :property-pred (m/-min-max-pred count) @@ -477,7 +646,7 @@ ::oapi/type "string" ::oapi/format "string"}}) -(def! ::uri +(define! ::uri {:type ::uri :pred u/uri? :type-properties @@ -491,20 +660,23 @@ ;; ---- PREDICATES -(def safe-int? - (pred-fn ::safe-int)) +(def valid-safe-number? + (lazy-validator ::safe-number)) -(def set-of-strings? - (pred-fn ::set-of-strings)) +(def check-safe-int! + (check-fn ::safe-int)) -(def set-of-emails? - (pred-fn ::set-of-emails)) +(def check-set-of-strings! + (check-fn ::set-of-strings)) -(def set-of-uuid? - (pred-fn ::set-of-uuid)) +(def check-email! + (check-fn ::email)) -(def coll-of-uuid? - (pred-fn ::coll-of-uuid)) +(def check-coll-of-uuid! + (check-fn ::coll-of-uuid)) -(def email? - (pred-fn ::email)) +(def check-set-of-uuid! + (check-fn ::set-of-uuid)) + +(def check-set-of-emails! + (check-fn ::set-of-emails)) diff --git a/common/src/app/common/schema/desc_js_like.cljc b/common/src/app/common/schema/desc_js_like.cljc index 58ff05d0ae..28f19e1963 100644 --- a/common/src/app/common/schema/desc_js_like.cljc +++ b/common/src/app/common/schema/desc_js_like.cljc @@ -169,15 +169,14 @@ (map (fn [[k _ s]] (str (pad " " level) (str/camel k) (when (contains? optional k) "?") - ": " s ))) + ": " s))) (str/join ",\n")) header (cond-> (if (zero? level) (str "type " title) (str title)) closed? (str "!") - (some? title) (str " ") - )] + (some? title) (str " "))] (str header "{\n" entries "\n" (pad "}" level)))))) diff --git a/common/src/app/common/schema/generators.cljc b/common/src/app/common/schema/generators.cljc index f9955b49d8..83e00bfd87 100644 --- a/common/src/app/common/schema/generators.cljc +++ b/common/src/app/common/schema/generators.cljc @@ -5,12 +5,13 @@ ;; Copyright (c) KALEIDOS INC (ns app.common.schema.generators - (:refer-clojure :exclude [set subseq uuid for]) + (:refer-clojure :exclude [set subseq uuid for filter map let]) #?(:cljs (:require-macros [app.common.schema.generators])) (:require [app.common.schema.registry :as sr] [app.common.uri :as u] [app.common.uuid :as uuid] + [clojure.core :as c] [clojure.test.check :as tc] [clojure.test.check.generators :as tg] [clojure.test.check.properties :as tp] @@ -36,9 +37,13 @@ [& params] `(tp/for-all ~@params)) +(defmacro let + [& params] + `(tg/let ~@params)) + (defn check! [p & {:keys [num] :or {num 20} :as options}] - (tc/quick-check num p (assoc options :reporter-fn default-reporter-fn))) + (tc/quick-check num p (assoc options :reporter-fn default-reporter-fn :max-size 50))) (defn sample ([g] @@ -58,6 +63,10 @@ ([s opts] (mg/generator s (assoc opts :registry sr/default-registry)))) +(defn filter + [pred gen] + (tg/such-that pred gen 100)) + (defn small-double [& {:keys [min max] :or {min -100 max 100}}] (tg/double* {:min min, :max max, :infinite? false, :NaN? false})) @@ -69,20 +78,19 @@ (defn word-string [] (->> (tg/such-that #(re-matches #"\w+" %) - tg/string-alphanumeric - 50) + tg/string-alphanumeric + 50) (tg/such-that (complement str/blank?)))) (defn uri [] (tg/let [scheme (tg/elements ["http" "https"]) - domain (as-> (word-string) $ - (tg/such-that (fn [x] (> (count x) 5)) $ 100) - (tg/fmap str/lower $)) - ext (tg/elements ["net" "com" "org" "app" "io"])] + domain (as-> (word-string) $ + (tg/such-that (fn [x] (> (count x) 5)) $ 100) + (tg/fmap str/lower $)) + ext (tg/elements ["net" "com" "org" "app" "io"])] (u/uri (str scheme "://" domain "." ext)))) -;; FIXME: revisit (defn uuid [] (->> tg/small-integer @@ -98,11 +106,11 @@ ([dest elements] (->> (apply tg/tuple (repeat (count elements) tg/boolean)) (tg/fmap (fn [bools] - (into dest - (comp - (filter first) - (map second)) - (map list bools elements))))))) + (into dest + (comp + (c/filter first) + (c/map second)) + (c/map list bools elements))))))) (defn set [g] @@ -120,6 +128,10 @@ [f g] (tg/fmap f g)) +(defn mcat + [f g] + (tg/bind g f)) + (defn tuple [& opts] (apply tg/tuple opts)) diff --git a/frontend/src/app/util/svg.cljs b/common/src/app/common/svg.cljc similarity index 67% rename from frontend/src/app/util/svg.cljs rename to common/src/app/common/svg.cljc index e724136a53..1b50a9dded 100644 --- a/frontend/src/app/util/svg.cljs +++ b/common/src/app/common/svg.cljc @@ -4,28 +4,49 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.util.svg +(ns app.common.svg (:require + #?(:clj [clojure.xml :as xml] + :cljs [tubax.core :as tubax]) + #?(:cljs ["./svg/optimizer.js" :as svgo]) [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.uuid :as uuid] - [app.util.strings :as ustr] - [cuerdas.core :as str])) + [cuerdas.core :as str]) + #?(:clj + (:import + clojure.lang.XMLHandler + java.io.InputStream + javax.xml.XMLConstants + javax.xml.parsers.SAXParserFactory + org.apache.commons.io.IOUtils))) + ;; Regex for XML ids per Spec ;; https://www.w3.org/TR/2008/REC-xml-20081126/#sec-common-syn -(defonce xml-id-regex #"#([:A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF][\.\-\:0-9\xB7A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0300-\u036F\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF]*)") +(def xml-id-regex #"#([:A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF][\.\-\:0-9\xB7A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0300-\u036F\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF]*)") -(defonce matrices-regex #"(matrix|translate|scale|rotate|skewX|skewY)\(([^\)]*)\)") -(defonce number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?") +(def matrices-regex #"(matrix|translate|scale|rotate|skewX|skewY)\(([^\)]*)\)") +(def number-regex #"[+-]?\d*(\.\d+)?([eE][+-]?\d+)?") -(defonce tags-to-remove #{:linearGradient :radialGradient :metadata :mask :clipPath :filter :title}) +(def tags-to-remove #{:linearGradient :radialGradient :metadata :mask :clipPath :filter :title}) + +(defn- camelize + [s] + (when (string? s) + (let [vendor? (str/starts-with? s "-") + result #?(:cljs (js* "~{}.replace(\":\", \"-\").replace(/-./g, x=>x[1].toUpperCase())", s) + :clj (str/camel s))] + (if ^boolean vendor? + (str/capital result) + result)))) ;; https://www.w3.org/TR/SVG11/eltindex.html -(defonce svg-tags-list +(def svg-tags #{:a :altGlyph :altGlyphDef @@ -104,11 +125,10 @@ :tspan :use :view - :vkern - }) + :vkern}) ;; https://www.w3.org/TR/SVG11/attindex.html -(defonce svg-attr-list +(def svg-attrs #{:accent-height :accumulate :additive @@ -202,26 +222,6 @@ :name :numOctaves :offset - ;; We don't support events - ;;:onabort - ;;:onactivate - ;;:onbegin - ;;:onclick - ;;:onend - ;;:onerror - ;;:onfocusin - ;;:onfocusout - ;;:onload - ;;:onmousedown - ;;:onmousemove - ;;:onmouseout - ;;:onmouseover - ;;:onmouseup - ;;:onrepeat - ;;:onresize - ;;:onscroll - ;;:onunload - ;;:onzoom :operator :order :orient @@ -326,7 +326,8 @@ :z :zoomAndPan}) -(defonce svg-present-list +(def svg-presentation-attrs + "A set of presentation SVG attributes as per SVG spec." #{:alignment-baseline :baseline-shift :clip-path @@ -388,59 +389,59 @@ :writing-mode :mask-type}) -(defonce inheritable-props - [:style - :clip-rule - :color - :color-interpolation - :color-interpolation-filters - :color-profile - :color-rendering - :cursor - :direction - :dominant-baseline - :fill - :fill-opacity - :fill-rule - :font - :font-family - :font-size - :font-size-adjust - :font-stretch - :font-style - :font-variant - :font-weight - :glyph-orientation-horizontal - :glyph-orientation-vertical - :image-rendering - :letter-spacing - :marker - :marker-end - :marker-mid - :marker-start - :paint-order - :pointer-events - :shape-rendering - :stroke - :stroke-dasharray - :stroke-dashoffset - :stroke-linecap - :stroke-linejoin - :stroke-miterlimit - :stroke-opacity - :stroke-width - :text-anchor - :text-rendering - :transform - :visibility - :word-spacing - :writing-mode]) +(def inheritable-props + #{:style + :clip-rule + :color + :color-interpolation + :color-interpolation-filters + :color-profile + :color-rendering + :cursor + :direction + :dominant-baseline + :fill + :fill-opacity + :fill-rule + :font + :font-family + :font-size + :font-size-adjust + :font-stretch + :font-style + :font-variant + :font-weight + :glyph-orientation-horizontal + :glyph-orientation-vertical + :image-rendering + :letter-spacing + :marker + :marker-end + :marker-mid + :marker-start + :paint-order + :pointer-events + :shape-rendering + :stroke + :stroke-dasharray + :stroke-dashoffset + :stroke-linecap + :stroke-linejoin + :stroke-miterlimit + :stroke-opacity + :stroke-width + :text-anchor + :text-rendering + :transform + :visibility + :word-spacing + :writing-mode}) -(defonce gradient-tags +(def gradient-tags #{:linearGradient :radialGradient}) -(defonce filter-tags +(def filter-tags #{:filter :feBlend :feColorMatrix @@ -466,7 +467,7 @@ :tspan}) ;; By spec: https://www.w3.org/TR/SVG11/single-page.html#struct-GElement -(defonce svg-group-safe-tags +(def svg-group-safe-tags #{:animate :animateColor :animateMotion @@ -507,14 +508,33 @@ :text :view}) -;; Props not supported by react we need to keep them lowercase -(defonce non-react-props - #{:mask-type}) +(defn prop-key + "Convert an attr key to a react compatible prop key. Returns nil if key is empty or invalid" + [k] + (let [kn (cond + (string? k) k + (keyword? k) (name k))] + (case kn + ("" nil) nil + "class" :className + "for" :htmlFor + (let [kn1 (subs kn 0 1)] + (if (= kn1 (str/upper kn1)) + (-> kn camelize str/capital keyword) + (-> kn camelize keyword)))))) + +(def svg-props + "A set of all attrs (including the presentation) converted to + camelCase for make it React compatible." + (let [xf (map prop-key)] + (-> #{} + (into xf svg-attrs) + (into xf svg-presentation-attrs)))) ;; Defaults for some tags per spec https://www.w3.org/TR/SVG11/single-page.html ;; they are basically the defaults that can be percents and we need to replace because ;; otherwise won't work as expected in the workspace -(defonce svg-tag-defaults +(def svg-tag-defaults (let [filter-default {:units :filterUnits :default "objectBoundingBox" "objectBoundingBox" {} @@ -546,78 +566,66 @@ [num-str] (cond (str/starts-with? num-str ".") - (str "0" num-str) + (dm/str "0" num-str) (str/starts-with? num-str "-.") - (str "-0" (subs num-str 1)) + (dm/str "-0" (subs num-str 1)) :else num-str)) +(defn parse-style + [style] + (reduce (fn [res item] + (let [[k v] (-> (str/trim item) (str/split ":" 2)) + k (keyword k)] + (if (contains? res k) + res + (assoc res (keyword k) v)))) + {} + (str/split style ";"))) + +;; FIXME: rename to `format-style` or directly use parse-style on code... (defn format-styles - "Transforms attributes to their react equivalent" + "Transform string based styles found on attrs map to key-value map." [attrs] - (letfn [(format-styles [style-str] - (if (string? style-str) - (->> (str/split style-str ";") - (map str/trim) - (map #(str/split % ":")) - (group-by first) - (map (fn [[key val]] - (vector (keyword key) (second (first val))))) - (into {})) - style-str))] + (if (contains? attrs :style) + (update attrs :style + (fn [style] + (if (string? style) + (parse-style style) + style))) + attrs)) - (cond-> attrs - (contains? attrs :style) - (update :style format-styles)))) - -(defn clean-attrs - "Transforms attributes to their react equivalent" +(defn attrs->props + "Transforms and cleans svg attributes to react compatible props" ([attrs] - (clean-attrs attrs true)) + (attrs->props attrs true)) ([attrs whitelist?] - (letfn [(known-property? [[key _]] - (or (not whitelist?) - (contains? svg-attr-list key) - (contains? svg-present-list key))) + (reduce-kv (fn [res k v] + (let [k (prop-key k)] + (cond + (nil? k) + res - (transform-att [key] - (if (contains? non-react-props key) - key - (-> (d/name key) - (ustr/camelize) - (keyword)))) + (nil? v) + res - (format-styles [style-str] - (->> (str/split style-str ";") - (map str/trim) - (map #(str/split % ":")) - (group-by first) - (map (fn [[key val]] - (vector - (transform-att key) - (second (first val))))) - (into {}))) + (= k :style) + (let [v (if (string? v) (parse-style v) v) + v (not-empty (attrs->props v false))] + (if v + (assoc res k v) + res)) - (clean-att [[att val]] - (let [att (keyword att)] - (cond - (= att :class) [:className val] - (and (= att :style) (string? val)) [att (format-styles val)] - (and (= att :style) (map? val)) [att (clean-attrs val false)] - :else [(transform-att att) val])))] - - ;; Removed this warning because slows a lot rendering with big svgs - #_(let [filtered-props (->> attrs (remove known-property?) (map first))] - (when (seq filtered-props) - (.warn js/console "Unknown properties: " (str/join ", " filtered-props )))) - - (into {} - (comp (filter known-property?) - (map clean-att)) - attrs)))) + :else + (if (or (not whitelist?) (contains? svg-props k)) + (let [v (if (string? v) (str/trim v) v)] + (assoc res k v)) + res)))) + {} + attrs))) (defn update-attr-ids "Replaces the ids inside a property" @@ -636,31 +644,34 @@ (let [to-replace (replace-fn it)] (str/replace result (str "#" it) (str "#" to-replace))))] (reduce replace-id val (extract-ids val)))))] + (d/mapm update-ids attrs))) (defn replace-attrs-ids "Replaces the ids inside a property" [attrs ids-mapping] - (if (and ids-mapping (seq ids-mapping)) - (update-attr-ids attrs (fn [id] (get ids-mapping id id))) - ;; Ids-mapping is null - attrs)) + (if (empty? ids-mapping) + attrs + (update-attr-ids attrs (fn [id] (get ids-mapping id id))))) -(defn generate-id-mapping [content] +(defn generate-id-mapping + [content] (letfn [(visit-node [result node] - (let [element-id (get-in node [:attrs :id]) - result (cond-> result - element-id (assoc element-id (str (uuid/next))))] + (let [element-id (dm/get-in node [:attrs :id]) + result (if (some? element-id) + (assoc result element-id (dm/str (uuid/next))) + result)] (reduce visit-node result (:content node))))] (visit-node {} content))) -(defn extract-defs [{:keys [attrs] :as node}] +(defn extract-defs + [{:keys [attrs] :as node}] (if-not (map? node) [{} node] (let [remove-node? (fn [{:keys [tag]}] (and (some? tag) (or (contains? tags-to-remove tag) - (not (contains? svg-tags-list tag))))) + (not (contains? svg-tags tag))))) rec-result (->> (:content node) (map extract-defs)) node (assoc node :content (->> rec-result (map second) (filterv (comp not remove-node?)))) @@ -670,21 +681,24 @@ node-defs (->> rec-result (map first) (reduce merge current-node-defs))] - [ node-defs node ]))) + [node-defs node]))) -(defn find-attr-references [attrs] +(defn find-attr-references + [attrs] (->> attrs (mapcat (fn [[_ attr-value]] (if (string? attr-value) (extract-ids attr-value) (find-attr-references attr-value)))))) -(defn find-node-references [node] +(defn find-node-references + [node] (let [current (->> (find-attr-references (:attrs node)) (into #{})) children (->> (:content node) (map find-node-references) (flatten) (into #{}))] (vec (into current children)))) -(defn find-def-references [defs references] +(defn find-def-references + [defs references] (loop [result (into #{} references) checked? #{} to-check (first references) @@ -709,7 +723,8 @@ (first pending) (rest pending)))))) -(defn svg-transform-matrix [shape] +(defn svg-transform-matrix + [shape] (if (:svg-viewbox shape) (let [{svg-x :x svg-y :y @@ -741,35 +756,39 @@ ;; Transforms spec: ;; https://www.w3.org/TR/SVG11/single-page.html#coords-TransformAttribute - -(defn format-translate-params [params] +(defn- format-translate-params + [params] (assert (or (= (count params) 1) (= (count params) 2))) (if (= (count params) 1) [(gpt/point (nth params 0) 0)] [(gpt/point (nth params 0) (nth params 1))])) -(defn format-scale-params [params] +(defn- format-scale-params + [params] (assert (or (= (count params) 1) (= (count params) 2))) (if (= (count params) 1) [(gpt/point (nth params 0))] [(gpt/point (nth params 0) (nth params 1))])) -(defn format-rotate-params [params] +(defn- format-rotate-params + [params] (assert (or (= (count params) 1) (= (count params) 3)) (str "??" (count params))) (if (= (count params) 1) [(nth params 0) (gpt/point 0 0)] [(nth params 0) (gpt/point (nth params 1) (nth params 2))])) -(defn format-skew-x-params [params] +(defn- format-skew-x-params + [params] (assert (= (count params) 1)) [(nth params 0) 0]) -(defn format-skew-y-params [params] +(defn- format-skew-y-params + [params] (assert (= (count params) 1)) [0 (nth params 0)]) -(defn to-matrix [{:keys [type params]}] - (assert (#{"matrix" "translate" "scale" "rotate" "skewX" "skewY"} type)) +(defn- to-matrix + [type params] (case type "matrix" (apply gmt/matrix params) "translate" (apply gmt/translate-matrix (format-translate-params params)) @@ -778,25 +797,34 @@ "skewX" (apply gmt/skew-matrix (format-skew-x-params params)) "skewY" (apply gmt/skew-matrix (format-skew-y-params params)))) -(defn parse-transform [transform-attr] - (if transform-attr - (let [process-matrix - (fn [[_ type params]] - (let [params (->> (re-seq number-regex params) - (filter #(-> % first seq)) - (map (comp d/parse-double first)))] - {:type type :params params})) +(def ^:private + xf-parse-numbers + (comp + (map first) + (keep not-empty) + (map d/parse-double))) + +(defn parse-numbers + [data] + (->> (re-seq number-regex data) + (into [] xf-parse-numbers))) + +(defn parse-transform + [transform] + (if (string? transform) + (->> (re-seq matrices-regex transform) + (map (fn [[_ type params]] + (let [params (parse-numbers params)] + (to-matrix type params)))) + (reduce gmt/multiply (gmt/matrix))) - matrices (->> (re-seq matrices-regex transform-attr) - (map process-matrix) - (map to-matrix))] - (reduce gmt/multiply (gmt/matrix) matrices)) (gmt/matrix))) (defn format-move [[x y]] (str "M" x " " y)) (defn format-line [[x y]] (str "L" x " " y)) -(defn points->path [points-str] +(defn points->path + [points-str] (let [points (->> points-str (re-seq number-regex) (filter (comp not empty? first)) @@ -847,19 +875,30 @@ transform (update :transform append-transform)))) -(defn inherit-attributes [group-attrs {:keys [attrs] :as node}] +(defn inherit-attributes + [group-attrs {:keys [attrs] :as node}] (if (map? node) - (let [attrs (-> (format-styles attrs) - (add-transform (:transform group-attrs))) - attrs (d/deep-merge (select-keys group-attrs inheritable-props) attrs)] + (let [attrs (-> (format-styles attrs) + (add-transform (:transform group-attrs))) + group-attrs (format-styles group-attrs) + + ;; Don't inherit a property that is already in the style attribute + inherit-style (-> (:style group-attrs) (d/without-keys (keys attrs))) + inheritable-props (->> inheritable-props (remove #(contains? (:styles attrs) %))) + group-attrs (-> group-attrs (assoc :style inherit-style)) + + attrs (-> (select-keys group-attrs inheritable-props) + (d/deep-merge attrs) + (d/without-nils))] (assoc node :attrs attrs)) node)) (defn map-nodes [mapfn node] (let [update-content - (fn [content] (cond->> content - (vector? content) - (mapv (partial map-nodes mapfn))))] + (fn [content] + (cond->> content + (vector? content) + (mapv (partial map-nodes mapfn))))] (cond-> node (map? node) @@ -884,7 +923,8 @@ value))) (defn fix-default-values - "Gives values to some SVG elements which defaults won't work when imported into the platform" + "Gives values to some SVG elements which defaults won't work when + imported into the platform" [svg-data] (let [add-defaults (fn [{:keys [tag attrs] :as node}] @@ -933,8 +973,7 @@ is-other? #{:r :stroke-width}] (if is-percent? - ;; JS parseFloat removes the % symbol - (let [attr-num (d/parse-double attr-val)] + (let [attr-num (d/parse-double (str/rtrim attr-val "%"))] (str (cond (is-x? attr-key) (fix-coord :x :width attr-num) (is-y? attr-key) (fix-coord :y :height attr-num) @@ -947,33 +986,77 @@ (fix-percent-attrs-viewbox [attrs] (d/mapm fix-percent-attr-viewbox attrs)) - (fix-percent-attr-numeric [_ attr-val] - (let [is-percent? (str/ends-with? attr-val "%")] - (if is-percent? - (str (let [attr-num (d/parse-double attr-val)] - (/ attr-num 100))) - attr-val))) + (fix-percent-attr-numeric-val [val] + (let [val (d/parse-double (str/rtrim val "%"))] + (str (/ val 100)))) - (fix-percent-attrs-numeric [attrs] - (d/mapm fix-percent-attr-numeric attrs)) + (fix-percent-attr-numeric [attrs key val] + (cond + (= key :style) + attrs + + (= key :unicode) + attrs + + (str/starts-with? (d/name key) "data-") + attrs + + (str/ends-with? val "%") + (assoc attrs key (fix-percent-attr-numeric-val val)) + + :else + attrs)) (fix-percent-values [node] (let [units (or (get-in node [:attrs :filterUnits]) (get-in node [:attrs :gradientUnits]) (get-in node [:attrs :patternUnits]) (get-in node [:attrs :clipUnits]))] + (cond-> node - (= "objectBoundingBox" units) - (update :attrs fix-percent-attrs-numeric) + (or (= "objectBoundingBox" units) (nil? units)) + (update :attrs #(reduce-kv fix-percent-attr-numeric % %)) (not= "objectBoundingBox" units) (update :attrs fix-percent-attrs-viewbox))))] - (->> svg-data (map-nodes fix-percent-values))))) + (map-nodes fix-percent-values svg-data)))) (defn collect-images [svg-data] (let [redfn (fn [acc {:keys [tag attrs]}] (cond-> acc (= :image tag) - (conj (or (:href attrs) (:xlink:href attrs)))))] - (reduce-nodes redfn [] svg-data ))) + (conj {:href (or (:href attrs) (:xlink:href attrs)) + :width (d/parse-integer (:width attrs) 0) + :height (d/parse-integer (:height attrs) 0)})))] + (reduce-nodes redfn [] svg-data))) + +#?(:clj + (defn- secure-parser-factory + [^InputStream input ^XMLHandler handler] + (.. (doto (SAXParserFactory/newInstance) + (.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true) + (.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true)) + (newSAXParser) + (parse input handler)))) + +(defn strip-doctype + [data] + (cond-> data + (str/includes? data "]*>" ""))) + + +(defn parse + [text] + #?(:cljs (tubax/xml->clj text) + :clj (let [text (strip-doctype text)] + (dm/with-open [istream (IOUtils/toInputStream text "UTF-8")] + (xml/parse istream secure-parser-factory))))) + +;; FIXME pass correct plugin set +#?(:cljs + (defn optimize + ([input] (optimize input nil)) + ([input options] + (svgo/optimize input (clj->js options))))) diff --git a/common/src/app/common/svg/optimizer.js b/common/src/app/common/svg/optimizer.js new file mode 100644 index 0000000000..ccd19b6970 --- /dev/null +++ b/common/src/app/common/svg/optimizer.js @@ -0,0 +1,40591 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.penpotSvgo = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i maxAllowed) { + // Text/cdata nodes can get big, and since they're buffered, + // we can get here under normal conditions. + // Avoid issues by emitting the text node now, + // so at least it won't get any bigger. + switch (buffers[i]) { + case 'textNode': + closeText(parser) + break + + case 'cdata': + emitNode(parser, 'oncdata', parser.cdata) + parser.cdata = '' + break + + case 'script': + emitNode(parser, 'onscript', parser.script) + parser.script = '' + break + + default: + error(parser, 'Max buffer length exceeded: ' + buffers[i]) + } + } + maxActual = Math.max(maxActual, len) + } + // schedule the next check for the earliest possible buffer overrun. + var m = sax.MAX_BUFFER_LENGTH - maxActual + parser.bufferCheckPosition = m + parser.position + } + + function clearBuffers (parser) { + for (var i = 0, l = buffers.length; i < l; i++) { + parser[buffers[i]] = '' + } + } + + function flushBuffers (parser) { + closeText(parser) + if (parser.cdata !== '') { + emitNode(parser, 'oncdata', parser.cdata) + parser.cdata = '' + } + if (parser.script !== '') { + emitNode(parser, 'onscript', parser.script) + parser.script = '' + } + } + + SAXParser.prototype = { + end: function () { end(this) }, + write: write, + resume: function () { this.error = null; return this }, + close: function () { return this.write(null) }, + flush: function () { flushBuffers(this) } + } + + // this really needs to be replaced with character classes. + // XML allows all manner of ridiculous numbers and digits. + var CDATA = '[CDATA[' + var DOCTYPE = 'DOCTYPE' + var XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace' + var XMLNS_NAMESPACE = 'http://www.w3.org/2000/xmlns/' + var rootNS = { xml: XML_NAMESPACE, xmlns: XMLNS_NAMESPACE } + + // http://www.w3.org/TR/REC-xml/#NT-NameStartChar + // This implementation works on strings, a single character at a time + // as such, it cannot ever support astral-plane characters (10000-EFFFF) + // without a significant breaking change to either this parser, or the + // JavaScript language. Implementation of an emoji-capable xml parser + // is left as an exercise for the reader. + var nameStart = /[:_A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/ + + var nameBody = /[:_A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u00B7\u0300-\u036F\u203F-\u2040.\d-]/ + + var entityStart = /[#:_A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/ + var entityBody = /[#:_A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u00B7\u0300-\u036F\u203F-\u2040.\d-]/ + + function isWhitespace (c) { + return c === ' ' || c === '\n' || c === '\r' || c === '\t' + } + + function isQuote (c) { + return c === '"' || c === '\'' + } + + function isAttribEnd (c) { + return c === '>' || isWhitespace(c) + } + + function isMatch (regex, c) { + return regex.test(c) + } + + function notMatch (regex, c) { + return !isMatch(regex, c) + } + + var S = 0 + sax.STATE = { + BEGIN: S++, // leading byte order mark or whitespace + BEGIN_WHITESPACE: S++, // leading whitespace + TEXT: S++, // general stuff + TEXT_ENTITY: S++, // & and such. + OPEN_WAKA: S++, // < + SGML_DECL: S++, // + SCRIPT: S++, // @@ -33,10 +36,11 @@ {{/manifest}} + - {{>../public/images/sprites/symbol/icons.svg}} - {{>../public/images/sprites/symbol/cursors.svg}} + {{> ../public/images/sprites/symbol/icons.svg }} + {{> ../public/images/sprites/symbol/cursors.svg }}
{{# manifest}} diff --git a/frontend/resources/templates/preview-body.mustache b/frontend/resources/templates/preview-body.mustache new file mode 100644 index 0000000000..fc26837165 --- /dev/null +++ b/frontend/resources/templates/preview-body.mustache @@ -0,0 +1,2 @@ +{{>../public/images/sprites/symbol/icons.svg}} + diff --git a/frontend/resources/templates/thumbnail-renderer.mustache b/frontend/resources/templates/rasterizer.mustache similarity index 84% rename from frontend/resources/templates/thumbnail-renderer.mustache rename to frontend/resources/templates/rasterizer.mustache index 261cd05c08..46372c48df 100644 --- a/frontend/resources/templates/thumbnail-renderer.mustache +++ b/frontend/resources/templates/rasterizer.mustache @@ -2,7 +2,7 @@ - Penpot - Thumbnail Renderer + Penpot - Rasterizer - + {{/manifest}} diff --git a/frontend/resources/templates/render.mustache b/frontend/resources/templates/render.mustache index 5221030ae1..cbaad75147 100644 --- a/frontend/resources/templates/render.mustache +++ b/frontend/resources/templates/render.mustache @@ -7,7 +7,6 @@ diff --git a/frontend/scripts/_helpers.js b/frontend/scripts/_helpers.js new file mode 100644 index 0000000000..cb2d36ac96 --- /dev/null +++ b/frontend/scripts/_helpers.js @@ -0,0 +1,408 @@ +import proc from "node:child_process"; +import fs from "node:fs/promises"; +import ph from "node:path"; +import os from "node:os"; +import url from "node:url"; + +import * as marked from "marked"; +import SVGSpriter from "svg-sprite"; +import Watcher from "watcher"; +import gettext from "gettext-parser"; +import l from "lodash"; +import log from "fancy-log"; +import mustache from "mustache"; +import pLimit from "p-limit"; +import ppt from "pretty-time"; +import wpool from "workerpool"; + +function getCoreCount() { + return os.cpus().length; +} + +// const __filename = url.fileURLToPath(import.meta.url); +export const dirname = url.fileURLToPath(new URL(".", import.meta.url)); + +export function startWorker() { + return wpool.pool(dirname + "/_worker.js", { + maxWorkers: getCoreCount() + }); +} + +async function findFiles(basePath, predicate, options={}) { + predicate = predicate ?? function() { return true; } + + let files = await fs.readdir(basePath, {recursive: options.recursive ?? false}) + files = files.map((path) => ph.join(basePath, path)); + + return files; +} + +function syncDirs(originPath, destPath) { + const command = `rsync -ar --delete ${originPath} ${destPath}`; + + return new Promise((resolve, reject) => { + proc.exec(command, (cause, stdout) => { + if (cause) { reject(cause); } + else { resolve(); } + }); + }); +} + +export function isSassFile(path) { + return path.endsWith(".scss"); +} + +export function isSvgFile(path) { + return path.endsWith(".svg"); +} + +export function isJsFile(path) { + return path.endsWith(".js"); +} + +export async function compileSass(worker, path, options) { + path = ph.resolve(path); + + log.info("compile:", path); + return worker.exec("compileSass", [path, options]); +} + +export async function compileSassAll(worker) { + const limitFn = pLimit(4); + const sourceDir = "src"; + + let files = await fs.readdir(sourceDir, { recursive: true }) + files = files.filter((path) => path.endsWith(".scss")); + files = files.map((path) => ph.join(sourceDir, path)); + // files = files.slice(0, 10); + + const procs = [ + compileSass(worker, "resources/styles/main-default.scss", {}), + compileSass(worker, "resources/styles/debug.scss", {}) + ]; + + for (let path of files) { + const proc = limitFn(() => compileSass(worker, path, {modules: true})); + procs.push(proc); + } + + const result = await Promise.all(procs); + + return result.reduce((acc, item, index) => { + acc.index[item.outputPath] = item.css; + acc.items.push(item.outputPath); + return acc; + }, {index:{}, items: []}); +} + +function compare(a, b) { + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } else { + return 0; + } +} + +export function concatSass(data) { + const output = [] + + for (let path of data.items) { + output.push(data.index[path]); + } + + return output.join("\n"); +} + +export async function watch(baseDir, predicate, callback) { + predicate = predicate ?? (() => true); + + + const watcher = new Watcher(baseDir, { + persistent: true, + recursive: true + }); + + watcher.on("change", (path) => { + if (predicate(path)) { + callback(path); + } + }); +} + +async function readShadowManifest() { + try { + const manifestPath = "resources/public/js/manifest.json" + let content = await fs.readFile(manifestPath, { encoding: "utf8" }); + content = JSON.parse(content); + + const index = { + config: "js/config.js?ts=" + Date.now(), + polyfills: "js/polyfills.js?ts=" + Date.now(), + }; + + for (let item of content) { + index[item.name] = "js/" + item["output-name"]; + } + + return index; + } catch (cause) { + // log.error("error on reading manifest (using default)", cause); + return { + config: "js/config.js", + polyfills: "js/polyfills.js", + main: "js/main.js", + shared: "js/shared.js", + worker: "js/worker.js", + rasterizer: "js/rasterizer.js", + }; + } +} + +async function renderTemplate(path, context={}, partials={}) { + const content = await fs.readFile(path, {encoding: "utf-8"}); + + const ts = Math.floor(new Date()); + + context = Object.assign({}, context, { + ts: ts, + isDebug: process.env.NODE_ENV !== "production" + }); + + return mustache.render(content, context, partials); +} + +const renderer = { + link(href, title, text) { + return `${text}`; + }, +}; + +marked.use({ renderer }); + +async function readTranslations() { + const langs = [ + "ar", + "ca", + "de", + "el", + "en", + "eu", + "it", + "es", + "fa", + "fr", + "he", + "nb_NO", + "pl", + "pt_BR", + "ro", + "id", + "ru", + "tr", + "zh_CN", + "zh_Hant", + "hr", + "gl", + "pt_PT", + "cs", + "fo", + "ko", + "lv", + "nl", + // this happens when file does not matches correct + // iso code for the language. + ["ja_jp", "jpn_JP"], + // ["fi", "fin_FI"], + ["uk", "ukr_UA"], + "ha" + ]; + const result = {}; + + for (let lang of langs) { + let filename = `${lang}.po`; + if (l.isArray(lang)) { + filename = `${lang[1]}.po`; + lang = lang[0]; + } + + const content = await fs.readFile(`./translations/${filename}`, { encoding: "utf-8" }); + + lang = lang.toLowerCase(); + + const data = gettext.po.parse(content, "utf-8"); + const trdata = data.translations[""]; + + for (let key of Object.keys(trdata)) { + if (key === "") continue; + const comments = trdata[key].comments || {}; + + if (l.isNil(result[key])) { + result[key] = {}; + } + + const isMarkdown = l.includes(comments.flag, "markdown"); + + const msgs = trdata[key].msgstr; + if (msgs.length === 1) { + let message = msgs[0]; + if (isMarkdown) { + message = marked.parseInline(message); + } + + result[key][lang] = message; + } else { + result[key][lang] = msgs.map((item) => { + if (isMarkdown) { + return marked.parseInline(item); + } else { + return item; + } + }); + } + // if (key === "modals.delete-font.title") { + // console.dir(trdata[key], {depth:10}); + // console.dir(result[key], {depth:10}); + // } + } + } + + return JSON.stringify(result); +} + +async function generateSvgSprite(files, prefix) { + const spriter = new SVGSpriter({ + mode: { + symbol: { inline: true } + } + }); + + for (let path of files) { + const name = `${prefix}${ph.basename(path)}` + const content = await fs.readFile(path, {encoding: "utf-8"}); + spriter.add(name, name, content); + } + + const { result } = await spriter.compileAsync(); + const resource = result.symbol.sprite; + return resource.contents; +} + +async function generateSvgSprites() { + await fs.mkdir("resources/public/images/sprites/symbol/", { recursive: true }); + + const icons = await findFiles("resources/images/icons/", isSvgFile); + const iconsSprite = await generateSvgSprite(icons, "icon-"); + await fs.writeFile("resources/public/images/sprites/symbol/icons.svg", iconsSprite); + + const cursors = await findFiles("resources/images/cursors/", isSvgFile); + const cursorsSprite = await generateSvgSprite(icons, "cursor-"); + await fs.writeFile("resources/public/images/sprites/symbol/cursors.svg", cursorsSprite); +} + +async function generateTemplates() { + await fs.mkdir("./resources/public/", { recursive: true }); + + const translations = await readTranslations(); + const manifest = await readShadowManifest(); + let content; + + const iconsSprite = await fs.readFile("resources/public/images/sprites/symbol/icons.svg", "utf8"); + const cursorsSprite = await fs.readFile("resources/public/images/sprites/symbol/cursors.svg", "utf8"); + const partials = { + "../public/images/sprites/symbol/icons.svg": iconsSprite, + "../public/images/sprites/symbol/cursors.svg": cursorsSprite, + }; + + content = await renderTemplate("resources/templates/index.mustache", { + manifest: manifest, + translations: JSON.stringify(translations), + }, partials); + + await fs.writeFile("./resources/public/index.html", content); + + content = await renderTemplate("resources/templates/preview-body.mustache", { + manifest: manifest, + translations: JSON.stringify(translations), + }); + + await fs.writeFile("./.storybook/preview-body.html", content); + + content = await renderTemplate("resources/templates/render.mustache", { + manifest: manifest, + translations: JSON.stringify(translations), + }); + + await fs.writeFile("./resources/public/render.html", content); + + content = await renderTemplate("resources/templates/rasterizer.mustache", { + manifest: manifest, + translations: JSON.stringify(translations), + }); + + await fs.writeFile("./resources/public/rasterizer.html", content); +} + +export async function compileStyles() { + const worker = startWorker(); + const start = process.hrtime(); + + log.info("init: compile styles") + let result = await compileSassAll(worker); + result = concatSass(result); + + await fs.mkdir("./resources/public/css", { recursive: true }); + await fs.writeFile("./resources/public/css/main.css", result); + + const end = process.hrtime(start); + log.info("done: compile styles", `(${ppt(end)})`); + worker.terminate(); +} + +export async function compileSvgSprites() { + const start = process.hrtime(); + log.info("init: compile svgsprite") + await generateSvgSprites(); + const end = process.hrtime(start); + log.info("done: compile svgsprite", `(${ppt(end)})`); +} + +export async function compileTemplates() { + const start = process.hrtime(); + log.info("init: compile templates") + await generateTemplates(); + const end = process.hrtime(start); + log.info("done: compile templates", `(${ppt(end)})`); +} + +export async function compilePolyfills() { + const start = process.hrtime(); + log.info("init: compile polyfills") + + + const files = await findFiles("resources/polyfills/", isJsFile); + let result = []; + for (let path of files) { + const content = await fs.readFile(path, {encoding:"utf-8"}); + result.push(content); + } + + await fs.mkdir("./resources/public/js", { recursive: true }); + fs.writeFile("resources/public/js/polyfills.js", result.join("\n")); + + const end = process.hrtime(start); + log.info("done: compile polyfills", `(${ppt(end)})`); +} + +export async function copyAssets() { + const start = process.hrtime(); + log.info("init: copy assets") + + await syncDirs("resources/images/", "resources/public/images/"); + await syncDirs("resources/fonts/", "resources/public/fonts/"); + + const end = process.hrtime(start); + log.info("done: copy assets", `(${ppt(end)})`); +} + diff --git a/frontend/scripts/_worker.js b/frontend/scripts/_worker.js new file mode 100644 index 0000000000..eab272fbf0 --- /dev/null +++ b/frontend/scripts/_worker.js @@ -0,0 +1,97 @@ +import proc from "node:child_process"; +import fs from "node:fs/promises"; +import ph from "node:path"; +import url from "node:url"; +import * as sass from "sass-embedded"; +import log from "fancy-log"; + +import wpool from "workerpool"; +import postcss from "postcss"; +import modulesProcessor from "postcss-modules"; +import autoprefixerProcessor from "autoprefixer"; + +const compiler = await sass.initAsyncCompiler(); + +async function compileFile(path) { + const dir = ph.dirname(path); + const name = ph.basename(path, ".scss"); + const dest = `${dir}${ph.sep}${name}.css`; + + + return new Promise(async (resolve, reject) => { + try { + const result = await compiler.compileAsync(path, { + loadPaths: ["node_modules/animate.css", "resources/styles/common/", "resources/styles"], + sourceMap: false + }); + // console.dir(result); + resolve({ + inputPath: path, + outputPath: dest, + css: result.css + }); + } catch (cause) { + // console.error(cause); + reject(cause); + } + }); +} + +function configureModulesProcessor(options) { + const ROOT_NAME = "app"; + + return modulesProcessor({ + getJSON: (cssFileName, json, outputFileName) => { + // We do nothing because we don't want the generated JSON files + }, + // Calculates the whole css-module selector name. + // Should be the same as the one in the file `/src/app/main/style.clj` + generateScopedName: (selector, filename, css) => { + const dir = ph.dirname(filename); + const name = ph.basename(filename, ".css"); + const parts = dir.split("/"); + const rootIdx = parts.findIndex((s) => s === ROOT_NAME); + return parts.slice(rootIdx + 1).join("_") + "_" + name + "__" + selector; + }, + }); +} + +function configureProcessor(options={}) { + const processors = []; + + if (options.modules) { + processors.push(configureModulesProcessor(options)); + } + processors.push(autoprefixerProcessor); + + return postcss(processors); +} + +async function postProcessFile(data, options) { + const proc = configureProcessor(options); + + // We compile to the same path (all in memory) + const result = await proc.process(data.css, { + from: data.outputPath, + to: data.outputPath, + map: false, + }); + + return Object.assign(data, { + css: result.css + }); +} + +async function compile(path, options) { + let result = await compileFile(path); + return await postProcessFile(result, options); +} + +wpool.worker({ + compileSass: compile +}, { + onTerminate: async (code) => { + // log.info("worker: terminate"); + await compiler.dispose(); + } +}); diff --git a/frontend/scripts/build b/frontend/scripts/build index 434a261e81..2b462db6a5 100755 --- a/frontend/scripts/build +++ b/frontend/scripts/build @@ -1,18 +1,27 @@ #!/usr/bin/env bash +# NOTE: this script should be called from the parent directory to +# properly work. set -ex -CURRENT_VERSION=$1; -BUILD_DATE=$(date -R); -CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)}; -EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS; +export CURRENT_VERSION=$1; +export BUILD_DATE=$(date -R); +export CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)}; +export EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS; + +# Some cljs reacts on this environment variable for define more +# performant code on macros (example: rumext) +export NODE_ENV=production; yarn install || exit 1; -npx gulp clean || exit 1; -clojure -J-Xms100M -J-Xmx800M -J-XX:+UseSerialGC -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}\"}" $EXTRA_PARAMS || exit 1 -npx gulp build || exit 1; -npx gulp dist:clean || exit 1; -npx gulp dist:copy || exit 1; +rm -rf resources/public; +rm -rf target/dist; + +clojure -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}\"}" $EXTRA_PARAMS || exit 1 + +yarn run compile || exit 1; +mkdir -p target/dist; +rsync -avr resources/public/ target/dist/ sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/index.html; sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/index.html; diff --git a/frontend/scripts/compile.js b/frontend/scripts/compile.js new file mode 100644 index 0000000000..e04d070013 --- /dev/null +++ b/frontend/scripts/compile.js @@ -0,0 +1,10 @@ +import fs from "node:fs/promises"; +import ppt from "pretty-time"; +import log from "fancy-log"; +import * as h from "./_helpers.js"; + +await h.compileStyles(); +await h.copyAssets() +await h.compileSvgSprites() +await h.compileTemplates(); +await h.compilePolyfills(); diff --git a/frontend/scripts/compress-png b/frontend/scripts/compress-png deleted file mode 100755 index b18a64b96a..0000000000 --- a/frontend/scripts/compress-png +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -# This script automates compressing PNG images using the lossless Zopfli -# Compression Algorithm. The process is slow but can produce significantly -# better compression and, thus, smaller file sizes. -# -# This script is meant to be run manually, for example, before making a new -# release. -# -# Requirements -# -# zopflipng - https://github.com/google/zopfli -# Debian/Ubuntu: sudo apt install zopfli -# Fedora: sudo dnf install zopfli -# macOS: brew install zopfli -# -# Usage -# -# This script takes a single positional argument which is the path where to -# search for PNG files. By default, the target path is the current working -# directory. Run from the root of the repository to compress all PNG images. Run -# from the `frontend` subdirectory to compress all PNG images within that -# directory. Alternatively, run from any directory and pass an explicit path to -# `compress-png` to limit the script to that path/directory. - -set -o errexit -set -o nounset -set -o pipefail - -readonly TARGET="${1:-.}" -readonly ABS_TARGET="$(command -v realpath &>/dev/null && realpath "$TARGET")" - -function png_total_size() { - find "$TARGET" -type f -iname '*.png' -exec du -ch {} + | tail -1 -} - -echo "Compressing PNGs in ${ABS_TARGET:-$TARGET}" - -echo "Before" -png_total_size - -readonly opts=( - # More iterations means slower, potentially better compression. - #--iterations=500 - -m - # Try all filter strategies (slow). - #--filters=01234mepb - # According to docs, remove colors behind alpha channel 0. No visual - # difference, removes hidden information. - --lossy_transparent - # Avoid information loss that could affect how images are rendered, see - # https://github.com/penpot/penpot/issues/1533#issuecomment-1030005203 - # https://github.com/google/zopfli/issues/113 - --keepchunks=cHRM,gAMA,pHYs,iCCP,sRGB,oFFs,sTER - # Since we have git behind our back, overwrite PNG files in-place (only - # when result is smaller). - -y -) -time find "$TARGET" -type f -iname '*.png' -exec zopflipng "${opts[@]}" {} {} \; - -echo "After" -png_total_size diff --git a/frontend/scripts/find-mf-use-fn.sh b/frontend/scripts/find-mf-use-fn.sh new file mode 100755 index 0000000000..b408037f40 --- /dev/null +++ b/frontend/scripts/find-mf-use-fn.sh @@ -0,0 +1,31 @@ +#!/bin/bash +echo -e "\x1B[0;41mmf/use-fn\x1B[0m\n" + +# +# Get count of expressions +# +FN_COUNT=$(egrep -rn ":on-.*?\s+\(fn" src/app/main/ui | wc -l) +PARTIAL_COUNT=$(egrep -rn ":on-.*?\s+\(partial" src/app/main/ui | wc -l) +AFN_COUNT=$(egrep -rn ":on-.*?\s+#\(" src/app/main/ui | wc -l) + +# +# Show counts +# +echo -e ":on-.*? (fn \x1B[0;31m" $FN_COUNT "\x1B[0m" +echo -e ":on-.*? (partial \x1B[0;31m" $PARTIAL_COUNT "\x1B[0m" +echo -e ":on-.*? #(\x1B[0;31m" $AFN_COUNT "\x1B[0m\n" + +echo -e "total: \x1B[0;31m" $((FN_COUNT + PARTIAL_COUNT + AFN_COUNT)) "\x1B[0m\n" + +# Show summary or show file list +if [[ $1 == "-s" ]]; then + # + # Files with handlers that don't use mf/use-fn + # + egrep -rn ":on-.*?\s+#?\((fn|partial)" src/app/main/ui | egrep -o "src/app/.*?\.cljs:" | uniq +else + # + # List files with lines + # + egrep -rn ":on-.*?\s+#?\((fn|partial)" src/app/main/ui | egrep -o "src/app/.*?\.cljs:([0-9]+)" +fi diff --git a/frontend/scripts/jvm-repl b/frontend/scripts/jvm-repl deleted file mode 100755 index b59aaaca89..0000000000 --- a/frontend/scripts/jvm-repl +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -# A repl useful for debug macros. - -export OPTIONS="\ - -J-XX:-OmitStackTraceInFastThrow \ - -J-Xms50m -J-Xmx512m \ - -M:dev:jvm-repl"; - -set -ex; -exec clojure $OPTIONS; diff --git a/frontend/scripts/watch.js b/frontend/scripts/watch.js new file mode 100644 index 0000000000..80dda26b52 --- /dev/null +++ b/frontend/scripts/watch.js @@ -0,0 +1,74 @@ +import proc from "node:child_process"; +import fs from "node:fs/promises"; +import ph from "node:path"; + +import log from "fancy-log"; +import * as h from "./_helpers.js"; +import ppt from "pretty-time"; + +const worker = h.startWorker(); +let sass = null; + +async function compileSassAll() { + const start = process.hrtime(); + log.info("init: compile styles") + + sass = await h.compileSassAll(worker); + let output = await h.concatSass(sass); + await fs.writeFile("./resources/public/css/main.css", output); + + const end = process.hrtime(start); + log.info("done: compile styles", `(${ppt(end)})`); +} + +async function compileSass(path) { + const start = process.hrtime(); + log.info("changed:", path); + const result = await h.compileSass(worker, path, {modules:true}); + sass.index[result.outputPath] = result.css; + + const output = h.concatSass(sass); + + await fs.writeFile("./resources/public/css/main.css", output); + + const end = process.hrtime(start); + log.info("done:", `(${ppt(end)})`); +} + +await compileSassAll(); +await h.copyAssets() +await h.compileSvgSprites() +await h.compileTemplates(); +await h.compilePolyfills(); + +log.info("watch: scss src (~)") + +h.watch("src", h.isSassFile, async function (path) { + if (path.includes("common")) { + await compileSassAll(path); + } else { + await compileSass(path); + } +}); + +log.info("watch: scss: resources (~)") +h.watch("resources/styles", h.isSassFile, async function (path) { + log.info("changed:", path); + await compileSassAll() +}); + +log.info("watch: templates (~)") +h.watch("resources/templates", null, async function (path) { + log.info("changed:", path); + await h.compileTemplates(); +}); + +log.info("watch: assets (~)") +h.watch(["resources/images", "resources/fonts"], null, async function (path) { + log.info("changed:", path); + await h.compileSvgSprites(); + await h.copyAssets(); + await h.compileTemplates(); +}); + +worker.terminate(); diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index ea6faa935f..9014481973 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -1,7 +1,6 @@ {:deps {:aliases [:dev]} :http {:port 3448} :nrepl {:port 3447 :host "0.0.0.0"} - :jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"] :dev-http {8888 "classpath:public"} :builds @@ -10,17 +9,44 @@ :output-dir "resources/public/js/" :asset-path "/js" :devtools {:browser-inject :main - :watch-dir "resources/public"} + :watch-dir "resources/public" + :reload-strategy :full} :build-options {:manifest-name "manifest.json"} - + :module-loader true :modules - {:shared {:entries []} + {:shared + {:entries []} :main {:entries [app.main] :depends-on #{:shared} :init-fn app.main/init} + :util-highlight + {:entries [app.util.code-highlight] + :depends-on #{:main}} + + :main-auth + {:entries [app.main.ui.auth + app.main.ui.auth.verify-token] + :depends-on #{:main}} + + :main-viewer + {:entries [app.main.ui.viewer] + :depends-on #{:main :main-auth}} + + :main-workspace + {:entries [app.main.ui.workspace] + :depends-on #{:main}} + + :main-dashboard + {:entries [app.main.ui.dashboard] + :depends-on #{:main}} + + :main-settings + {:entries [app.main.ui.settings] + :depends-on #{:main}} + :render {:entries [app.render] :depends-on #{:shared} @@ -31,10 +57,10 @@ :web-worker true :depends-on #{:shared}} - :thumbnail-renderer - {:entries [app.thumbnail-renderer] + :rasterizer + {:entries [app.rasterizer] :depends-on #{:shared} - :init-fn app.thumbnail-renderer/init}} + :init-fn app.rasterizer/init}} :compiler-options {:output-feature-set :es2020 @@ -47,14 +73,41 @@ :compiler-options {:fn-invoke-direct true :optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced] + :output-wrapper true + :rename-prefix-namespace "PENPOT" :source-map true :elide-asserts true :anon-fn-naming-policy :off :source-map-detail-level :all}}} - :lib-penpot + ;; FIXME: maybe rename to :components ? (there are nothing storybook + ;; related, is just an ESM export of components that will be used + ;; initially on storybook but not limited to storybook) + :storybook {:target :esm - :output-dir "resources/public/libs" + :output-dir "target/storybook/" + :js-options {:js-provider :import} + + :modules + {:base + {:entries []} + + :icons + {:exports {default app.main.ui.icons/default} + :depends-on #{:base}} + + :components + {:exports {:default app.main.ui.components/default} + :depends-on #{:base}}} + + :compiler-options + {:output-feature-set :es2020 + :output-wrapper false + :warnings {:fn-deprecated false}}} + + :lib-penpot + {:target :esm + :output-dir "resources/public/libs" :modules {:penpot {:exports {:renderPage app.libs.render/render-page-export @@ -92,7 +145,7 @@ :test {:target :node-test - :output-to "target/tests.js" + :output-to "target/tests.cjs" :output-dir "target/test/" :ns-regexp "^frontend-tests.*-test$" :autorun true diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 2222b50184..c95f72e1a0 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -11,6 +11,7 @@ [app.common.uri :as u] [app.common.version :as v] [app.util.avatars :as avatars] + [app.util.extends] [app.util.globals :refer [global location]] [app.util.navigator :as nav] [app.util.object :as obj] @@ -21,7 +22,7 @@ ;; --- Auxiliar Functions (def valid-browsers - #{:chrome :firefox :safari :edge :other}) + #{:chrome :firefox :safari :safari-16 :safari-17 :edge :other}) (def valid-platforms #{:windows :linux :macos :other}) @@ -32,13 +33,17 @@ check-chrome? (fn [] (str/includes? user-agent "chrom")) check-firefox? (fn [] (str/includes? user-agent "firefox")) check-edge? (fn [] (str/includes? user-agent "edg")) - check-safari? (fn [] (str/includes? user-agent "safari"))] + check-safari? (fn [] (str/includes? user-agent "safari")) + check-safari-16? (fn [] (and (check-safari?) (str/includes? user-agent "version/16"))) + check-safari-17? (fn [] (and (check-safari?) (str/includes? user-agent "version/17")))] (cond - (check-edge?) :edge - (check-chrome?) :chrome - (check-firefox?) :firefox - (check-safari?) :safari - :else :other))) + (check-edge?) :edge + (check-chrome?) :chrome + (check-firefox?) :firefox + (check-safari-16?) :safari-16 + (check-safari-17?) :safari-17 + (check-safari?) :safari + :else :other))) (defn- parse-platform [] @@ -59,7 +64,10 @@ :webworker)) (def default-flags - [:enable-newsletter-subscription + [:enable-onboarding + :enable-onboarding-team + :enable-onboarding-questions + :enable-onboarding-newsletter :enable-dashboard-templates-section :enable-google-fonts-provider]) @@ -82,7 +90,7 @@ date))) -;; --- Globar Config Vars +;; --- Global Config Vars (def default-theme "default") (def default-language "en") @@ -97,8 +105,10 @@ (def browser (parse-browser)) (def platform (parse-platform)) -(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" nil)) -(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" nil)) +(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" "https://penpot.app/terms")) +(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" "https://penpot.app/privacy")) +(def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) +(def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) (defn- normalize-uri [uri-str] @@ -113,8 +123,8 @@ (normalize-uri (or (obj/get global "penpotPublicURI") (obj/get location "origin")))) -(def thumbnail-renderer-uri - (or (some-> (obj/get global "penpotThumbnailRendererURI") normalize-uri) +(def rasterizer-uri + (or (some-> (obj/get global "penpotRasterizerURI") normalize-uri) public-uri)) (def worker-uri @@ -124,16 +134,18 @@ (defn ^boolean check-browser? [candidate] (dm/assert! (contains? valid-browsers candidate)) - (= candidate browser)) + (if (= candidate :safari) + (contains? #{:safari :safari-16 :safari-17} browser) + (= candidate browser))) (defn ^boolean check-platform? [candidate] (dm/assert! (contains? valid-platforms candidate)) (= candidate platform)) (defn resolve-profile-photo-url - [{:keys [photo-id fullname name] :as profile}] + [{:keys [photo-id fullname name color] :as profile}] (if (nil? photo-id) - (avatars/generate {:name (or fullname name)}) + (avatars/generate {:name (or fullname name) :color color}) (dm/str (u/join public-uri "assets/by-id/" photo-id)))) (defn resolve-team-photo-url diff --git a/frontend/src/app/libs/file_builder.cljs b/frontend/src/app/libs/file_builder.cljs index f30014832c..4d3b21adce 100644 --- a/frontend/src/app/libs/file_builder.cljs +++ b/frontend/src/app/libs/file_builder.cljs @@ -7,7 +7,7 @@ (ns app.libs.file-builder (:require [app.common.data :as d] - [app.common.file-builder :as fb] + [app.common.files.builder :as fb] [app.common.media :as cm] [app.common.types.components-list :as ctkl] [app.common.uuid :as uuid] @@ -16,7 +16,7 @@ [app.util.webapi :as wapi] [app.util.zip :as uz] [app.worker.export :as e] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str])) (defn parse-data [data] @@ -55,7 +55,7 @@ (->> (rx/from (vals media)) (rx/map #(assoc % :file-id file-id)) - (rx/flat-map + (rx/merge-map (fn [media] (let [file-path (str/concat file-id "/media/" (:id media) (cm/mtype->extension (:mtype media))) blob (data-uri->blob (:uri media))] @@ -79,38 +79,38 @@ render-stream (->> files-stream - (rx/flat-map vals) - (rx/flat-map e/process-pages) + (rx/merge-map vals) + (rx/merge-map e/process-pages) (rx/observe-on :async) - (rx/flat-map e/get-page-data) + (rx/merge-map e/get-page-data) (rx/share)) colors-stream (->> files-stream - (rx/flat-map vals) + (rx/merge-map vals) (rx/map #(vector (:id %) (get-in % [:data :colors]))) (rx/filter #(d/not-empty? (second %))) (rx/map e/parse-library-color)) typographies-stream (->> files-stream - (rx/flat-map vals) + (rx/merge-map vals) (rx/map #(vector (:id %) (get-in % [:data :typographies]))) (rx/filter #(d/not-empty? (second %))) (rx/map e/parse-library-typographies)) media-stream (->> files-stream - (rx/flat-map vals) + (rx/merge-map vals) (rx/map #(vector (:id %) (get-in % [:data :media]))) (rx/filter #(d/not-empty? (second %))) - (rx/flat-map parse-library-media)) + (rx/merge-map parse-library-media)) components-stream (->> files-stream - (rx/flat-map vals) + (rx/merge-map vals) (rx/filter #(d/not-empty? (ctkl/components-seq (:data %)))) - (rx/flat-map e/parse-library-components)) + (rx/merge-map e/parse-library-components)) pages-stream (->> render-stream @@ -132,9 +132,9 @@ typographies-stream) (rx/reduce conj []) (rx/with-latest-from files-stream) - (rx/flat-map (fn [[data _]] - (->> (uz/compress-files data) - (rx/map #(vector file %))))))))) + (rx/merge-map (fn [[data _]] + (->> (uz/compress-files data) + (rx/map #(vector file %))))))))) (deftype File [^:mutable file] Object @@ -263,4 +263,4 @@ (File. (fb/create-file name))) (defn exports [] - #js { :createFile create-file-export }) + #js {:createFile create-file-export}) diff --git a/frontend/src/app/libs/render.cljs b/frontend/src/app/libs/render.cljs index 93f0e54059..26e6cfe5d6 100644 --- a/frontend/src/app/libs/render.cljs +++ b/frontend/src/app/libs/render.cljs @@ -8,7 +8,7 @@ (:require [app.common.uuid :as uuid] [app.main.render :as r] - [beicon.core :as rx] + [beicon.v2.core :as rx] [promesa.core :as p])) (defn render-page-export @@ -22,7 +22,7 @@ (fn [resolve reject] (->> (r/render-page data) (rx/take 1) - (rx/subs resolve reject))) ))) + (rx/subs! resolve reject)))))) (defn exports [] #js {:renderPage render-page-export}) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 50690807d2..868d4b9ea8 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -15,8 +15,8 @@ [app.main.data.websocket :as ws] [app.main.errors] [app.main.features :as feat] + [app.main.rasterizer :as thr] [app.main.store :as st] - [app.main.thumbnail-renderer :as tr] [app.main.ui :as ui] [app.main.ui.alert] [app.main.ui.confirm] @@ -28,10 +28,10 @@ [app.util.dom :as dom] [app.util.i18n :as i18n] [app.util.theme :as theme] - [beicon.core :as rx] + [beicon.v2.core :as rx] [debug] [features] - [potok.core :as ptk] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) (log/setup! {:app :info}) @@ -45,10 +45,18 @@ (declare reinit) +(defonce app-root + (let [el (dom/get-element "app")] + (mf/create-root el))) + +(defonce modal-root + (let [el (dom/get-element "modal")] + (mf/create-root el))) + (defn init-ui [] - (mf/mount (mf/element ui/app) (dom/get-element "app")) - (mf/mount (mf/element modal) (dom/get-element "modal"))) + (mf/render! app-root (mf/element ui/app)) + (mf/render! modal-root (mf/element modal))) (defn- initialize-profile "Event used mainly on application bootstrap; it fetches the profile @@ -104,16 +112,22 @@ (i18n/init! cf/translations) (theme/init! cf/themes) (cur/init-styles) - (tr/init!) + (thr/init!) (init-ui) (st/emit! (initialize))) (defn ^:export reinit - [] - (mf/unmount (dom/get-element "app")) - (mf/unmount (dom/get-element "modal")) - (st/emit! (ev/initialize)) - (init-ui)) + ([] + (reinit false)) + ([hard?] + ;; The hard flag will force to unmount the whole UI and will redraw every component + (when hard? + (mf/unmount! app-root) + (mf/unmount! modal-root) + (set! app-root (mf/create-root (dom/get-element "app"))) + (set! modal-root (mf/create-root (dom/get-element "modal")))) + (st/emit! (ev/initialize)) + (init-ui))) (defn ^:dev/after-load after-load [] diff --git a/frontend/src/app/main/broadcast.cljs b/frontend/src/app/main/broadcast.cljs index b83f2fa91e..33e12f12a6 100644 --- a/frontend/src/app/main/broadcast.cljs +++ b/frontend/src/app/main/broadcast.cljs @@ -9,8 +9,8 @@ (:require [app.common.exceptions :as ex] [app.common.transit :as t] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (defrecord BroadcastMessage [id type data] cljs.core/IDeref diff --git a/frontend/src/app/main/constants.cljs b/frontend/src/app/main/constants.cljs index acae0096ea..31bd4dfd63 100644 --- a/frontend/src/app/main/constants.cljs +++ b/frontend/src/app/main/constants.cljs @@ -19,9 +19,9 @@ "Default data for page metadata." {:grid-x-axis grid-x-axis :grid-y-axis grid-y-axis - :grid-color "var(--color-gray-20)" + :grid-color "var(--df-secondary)" :grid-alignment true - :background "var(--color-white)"}) + :background "var(--app-white)"}) (def size-presets [{:name "APPLE"} diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs index b9fd50a43f..0a441068f1 100644 --- a/frontend/src/app/main/data/comments.cljs +++ b/frontend/src/app/main/data/comments.cljs @@ -12,43 +12,46 @@ [app.common.schema :as sm] [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] + [app.main.data.events :as ev] [app.main.data.workspace.state-helpers :as wsh] [app.main.repo :as rp] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) -(def schema:comment-thread - [:map {:title "CommentThread"} - [:id ::sm/uuid] - [:page-id ::sm/uuid] - [:file-id ::sm/uuid] - [:project-id ::sm/uuid] - [:owner-id ::sm/uuid] - [:page-name :string] - [:file-name :string] - [:seqn :int] - [:content :string] - [:participants ::sm/set-of-uuid] - [:created-at ::sm/inst] - [:modified-at ::sm/inst] - [:position ::gpt/point] - [:count-unread-comments {:optional true} :int] - [:count-comments {:optional true} :int]]) +(def ^:private schema:comment-thread + (sm/define + [:map {:title "CommentThread"} + [:id ::sm/uuid] + [:page-id ::sm/uuid] + [:file-id ::sm/uuid] + [:project-id ::sm/uuid] + [:owner-id ::sm/uuid] + [:page-name :string] + [:file-name :string] + [:seqn :int] + [:content :string] + [:participants ::sm/set-of-uuid] + [:created-at ::sm/inst] + [:modified-at ::sm/inst] + [:position ::gpt/point] + [:count-unread-comments {:optional true} :int] + [:count-comments {:optional true} :int]])) -(def schema:comment - [:map {:title "CommentThread"} - [:id ::sm/uuid] - [:thread-id ::sm/uuid] - [:owner-id ::sm/uuid] - [:created-at ::sm/inst] - [:modified-at ::sm/inst] - [:content :string]]) +(def ^:private schema:comment + (sm/define + [:map {:title "Comment"} + [:id ::sm/uuid] + [:thread-id ::sm/uuid] + [:owner-id ::sm/uuid] + [:created-at ::sm/inst] + [:modified-at ::sm/inst] + [:content :string]])) -(def comment-thread? - (sm/pred-fn schema:comment-thread)) +(def check-comment-thread! + (sm/check-fn schema:comment-thread)) -(def comment? - (sm/pred-fn schema:comment)) +(def check-comment! + (sm/check-fn schema:comment)) (declare create-draft-thread) (declare retrieve-comment-threads) @@ -59,31 +62,45 @@ (ptk/reify ::created-thread-on-workspace ptk/UpdateEvent (update [_ state] - (-> state - (update :comment-threads assoc id (dissoc thread :comment)) - (update-in [:workspace-data :pages-index page-id :options :comment-threads-position] assoc id (select-keys thread [:position :frame-id])) - (update :comments-local assoc :open id) - (update :comments-local dissoc :draft) - (update :workspace-drawing dissoc :comment) - (update-in [:comments id] assoc (:id comment) comment))))) + (let [position (select-keys thread [:position :frame-id])] + (-> state + (update :comment-threads assoc id (dissoc thread :comment)) + (update-in [:workspace-data :pages-index page-id :options :comment-threads-position] assoc id position) + (update :comments-local assoc :open id) + (update :comments-local assoc :options nil) + (update :comments-local dissoc :draft) + (update :workspace-drawing dissoc :comment) + (update-in [:comments id] assoc (:id comment) comment)))) + + ptk/WatchEvent + (watch [_ _ _] + (rx/of (ptk/data-event ::ev/event + {::ev/name "create-comment-thread" + ::ev/origin "workspace" + :id id + :content-size (count (:content comment))}))))) -(def schema:create-thread-on-workspace - [:map - [:page-id ::sm/uuid] - [:file-id ::sm/uuid] - [:position ::gpt/point] - [:content :string]]) + +(def ^:private + schema:create-thread-on-workspace + (sm/define + [:map {:title "created-thread-on-workspace"} + [:page-id ::sm/uuid] + [:file-id ::sm/uuid] + [:position ::gpt/point] + [:content :string]])) (defn create-thread-on-workspace [params] - (dm/assert! (sm/valid? schema:create-thread-on-workspace params)) + (dm/assert! (sm/check! schema:create-thread-on-workspace params)) + (ptk/reify ::create-thread-on-workspace ptk/WatchEvent (watch [_ state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - frame-id (ctst/frame-id-by-position objects (:position params)) + frame-id (ctst/get-frame-id-by-position objects (:position params)) params (assoc params :frame-id frame-id)] (->> (rp/cmd! :create-comment-thread params) (rx/mapcat #(rp/cmd! :get-comment-thread {:file-id (:file-id %) :id (:id %)})) @@ -99,25 +116,39 @@ (ptk/reify ::created-thread-on-viewer ptk/UpdateEvent (update [_ state] - (-> state - (update :comment-threads assoc id (dissoc thread :comment)) - (update-in [:viewer :pages page-id :options :comment-threads-position] assoc id (select-keys thread [:position :frame-id])) - (update :comments-local assoc :open id) - (update :comments-local dissoc :draft) - (update :workspace-drawing dissoc :comment) - (update-in [:comments id] assoc (:id comment) comment))))) + (let [position (select-keys thread [:position :frame-id])] + (-> state + (update :comment-threads assoc id (dissoc thread :comment)) + (update-in [:viewer :pages page-id :options :comment-threads-position] assoc id position) + (update :comments-local assoc :open id) + (update :comments-local assoc :options nil) + (update :comments-local dissoc :draft) + (update :workspace-drawing dissoc :comment) + (update-in [:comments id] assoc (:id comment) comment)))) -(def schema:create-thread-on-viewer - [:map - [:page-id ::sm/uuid] - [:file-id ::sm/uuid] - [:frame-id ::sm/uuid] - [:position ::gpt/point] - [:content :string]]) + ptk/WatchEvent + (watch [_ _ _] + (rx/of (ptk/data-event ::ev/event + {::ev/name "create-comment-thread" + ::ev/origin "viewer" + :id id + :content-size (count (:content comment))}))))) + +(def ^:private + schema:create-thread-on-viewer + (sm/define + [:map {:title "created-thread-on-viewer"} + [:page-id ::sm/uuid] + [:file-id ::sm/uuid] + [:frame-id ::sm/uuid] + [:position ::gpt/point] + [:content :string]])) (defn create-thread-on-viewer [params] - (dm/assert! (sm/valid? schema:create-thread-on-viewer params)) + (dm/assert! + (sm/check! schema:create-thread-on-viewer params)) + (ptk/reify ::create-thread-on-viewer ptk/WatchEvent (watch [_ state _] @@ -146,7 +177,11 @@ (defn update-comment-thread [{:keys [id is-resolved] :as thread}] - (dm/assert! (comment-thread? thread)) + + (dm/assert! + "expected valid comment thread" + (check-comment-thread! thread)) + (ptk/reify ::update-comment-thread IDeref (-deref [_] {:is-resolved is-resolved}) @@ -157,54 +192,79 @@ ptk/WatchEvent (watch [_ state _] - (let [share-id (-> state :viewer-local :share-id)] - (->> (rp/cmd! :update-comment-thread {:id id :is-resolved is-resolved :share-id share-id}) + (let [share-id (-> state :viewer-local :share-id)] + (->> (rp/cmd! :update-comment-thread {:id id :is-resolved is-resolved :share-id share-id}) + (rx/catch (fn [{:keys [type code] :as cause}] + (if (and (= type :restriction) + (= code :max-quote-reached)) + (rx/throw cause) + (rx/throw {:type :comment-error})))) + (rx/ignore)))))) + +(defn add-comment + [thread content] + + (dm/assert! + "expected valid comment thread" + (check-comment-thread! thread)) + + (dm/assert! + "expected valid content" + (string? content)) + + (ptk/reify ::create-comment + ev/Event + (-data [_] + {:thread-id (:id thread) + :file-id (:file-id thread) + :content-size (count content)}) + + ptk/WatchEvent + (watch [_ state _] + (let [share-id (-> state :viewer-local :share-id) + created (fn [comment state] + (update-in state [:comments (:id thread)] assoc (:id comment) comment))] + (rx/concat + (->> (rp/cmd! :create-comment {:thread-id (:id thread) :content content :share-id share-id}) + (rx/map (fn [comment] (partial created comment))) (rx/catch (fn [{:keys [type code] :as cause}] (if (and (= type :restriction) (= code :max-quote-reached)) (rx/throw cause) - (rx/throw {:type :comment-error})))) - (rx/ignore)))))) - -(defn add-comment - [thread content] - (dm/assert! (comment-thread? thread)) - (dm/assert! (string? content)) - - (letfn [(created [comment state] - (update-in state [:comments (:id thread)] assoc (:id comment) comment))] - (ptk/reify ::create-comment - ptk/WatchEvent - (watch [_ state _] - (let [share-id (-> state :viewer-local :share-id)] - (rx/concat - (->> (rp/cmd! :create-comment {:thread-id (:id thread) :content content :share-id share-id}) - (rx/map #(partial created %)) - (rx/catch (fn [{:keys [type code] :as cause}] - (if (and (= type :restriction) - (= code :max-quote-reached)) - (rx/throw cause) - (rx/throw {:type :comment-error}))))) - (rx/of (refresh-comment-thread thread)))))))) + (rx/throw {:type :comment-error}))))) + (rx/of (refresh-comment-thread thread))))))) (defn update-comment [{:keys [id content thread-id] :as comment}] - (dm/assert! (comment? comment)) + (dm/assert! + "expected valid comment" + (check-comment! comment)) + (ptk/reify ::update-comment + ev/Event + (-data [_] + {:thread-id thread-id + :id id + :content-size (count content)}) + ptk/UpdateEvent (update [_ state] - (d/update-in-when state [:comments thread-id id] assoc :content content)) + (-> state + (d/update-in-when [:comments thread-id id] assoc :content content))) ptk/WatchEvent (watch [_ state _] - (let [share-id (-> state :viewer-local :share-id)] + (let [file-id (:current-file-id state) + share-id (-> state :viewer-local :share-id)] (->> (rp/cmd! :update-comment {:id id :content content :share-id share-id}) (rx/catch #(rx/throw {:type :comment-error})) - (rx/ignore)))))) + (rx/map #(retrieve-comment-threads file-id))))))) (defn delete-comment-thread-on-workspace [{:keys [id] :as thread}] - (dm/assert! (comment-thread? thread)) + (dm/assert! + "expected valid comment thread" + (check-comment-thread! thread)) (ptk/reify ::delete-comment-thread-on-workspace ptk/UpdateEvent (update [_ state] @@ -216,13 +276,20 @@ ptk/WatchEvent (watch [_ _ _] - (->> (rp/cmd! :delete-comment-thread {:id id}) - (rx/catch #(rx/throw {:type :comment-error})) - (rx/ignore))))) + (rx/concat + (->> (rp/cmd! :delete-comment-thread {:id id}) + (rx/catch #(rx/throw {:type :comment-error})) + (rx/ignore)) + (rx/of (ptk/data-event ::ev/event + {::ev/name "delete-comment-thread" + ::ev/origin "workspace" + :id id})))))) (defn delete-comment-thread-on-viewer [{:keys [id] :as thread}] - (dm/assert! (comment-thread? thread)) + (dm/assert! + "expected valid comment thread" + (check-comment-thread! thread)) (ptk/reify ::delete-comment-thread-on-viewer ptk/UpdateEvent (update [_ state] @@ -235,17 +302,29 @@ ptk/WatchEvent (watch [_ state _] (let [share-id (-> state :viewer-local :share-id)] - (->> (rp/cmd! :delete-comment-thread {:id id :share-id share-id}) - (rx/catch #(rx/throw {:type :comment-error})) - (rx/ignore)))))) - + (rx/concat + (->> (rp/cmd! :delete-comment-thread {:id id :share-id share-id}) + (rx/catch #(rx/throw {:type :comment-error})) + (rx/ignore)) + (rx/of (ptk/data-event ::ev/event + {::ev/name "delete-comment-thread" + ::ev/origin "viewer" + :id id}))))))) (defn delete-comment [{:keys [id thread-id] :as comment}] - (dm/assert! (comment? comment)) + (dm/assert! + "expected valid comment" + (check-comment! comment)) (ptk/reify ::delete-comment + ev/Event + (-data [_] + {:thread-id thread-id}) + ptk/UpdateEvent (update [_ state] - (d/update-in-when state [:comments thread-id] dissoc id)) + (-> state + (d/update-in-when [:comments thread-id] dissoc id) + (d/update-in-when [:comment-threads thread-id :count-comments] dec))) ptk/WatchEvent (watch [_ state _] @@ -256,7 +335,9 @@ (defn refresh-comment-thread [{:keys [id file-id] :as thread}] - (dm/assert! (comment-thread? thread)) + (dm/assert! + "expected valid comment thread" + (check-comment-thread! thread)) (letfn [(fetched [thread state] (assoc-in state [:comment-threads id] thread))] (ptk/reify ::refresh-comment-thread @@ -278,14 +359,14 @@ (-> (assoc-in (conj path :position) (:position comment-thread)) (assoc-in (conj path :frame-id) (:frame-id comment-thread)))))) - (fetched [[users comments] state] - (let [pages (-> (get-in state [:workspace-data :pages]) - set) - comments (filter #(contains? pages (:page-id %)) comments) - state (-> state - (assoc :comment-threads (d/index-by :id comments)) - (update :current-file-comments-users merge (d/index-by :id users)))] - (reduce set-comment-threds state comments)))] + (fetched [[users comments] state] + (let [pages (-> (get-in state [:workspace-data :pages]) + set) + comments (filter #(contains? pages (:page-id %)) comments) + state (-> state + (assoc :comment-threads (d/index-by :id comments)) + (update :current-file-comments-users merge (d/index-by :id users)))] + (reduce set-comment-threds state comments)))] (ptk/reify ::retrieve-comment-threads ptk/WatchEvent @@ -338,12 +419,19 @@ (defn open-thread [{:keys [id] :as thread}] - (dm/assert! (comment-thread? thread)) + (dm/assert! + "expected valid comment thread" + (check-comment-thread! thread)) (ptk/reify ::open-comment-thread + ev/Event + (-data [_] + {:thread-id id}) + ptk/UpdateEvent (update [_ state] (-> state (update :comments-local assoc :open id) + (update :comments-local assoc :options nil) (update :workspace-drawing dissoc :comment))))) (defn close-thread @@ -352,7 +440,7 @@ ptk/UpdateEvent (update [_ state] (-> state - (update :comments-local dissoc :open :draft) + (update :comments-local dissoc :open :draft :options) (update :workspace-drawing dissoc :comment))))) (defn update-filters @@ -379,15 +467,18 @@ (update [_ state] (update state :comments-local merge params)))) -(def schema:create-draft - [:map - [:page-id ::sm/uuid] - [:file-id ::sm/uuid] - [:position ::gpt/point]]) +(def ^:private + schema:create-draft + (sm/define + [:map {:title "create-draft"} + [:page-id ::sm/uuid] + [:file-id ::sm/uuid] + [:position ::gpt/point]])) (defn create-draft [params] - (dm/assert! (sm/valid? schema:create-draft params)) + (dm/assert! + (sm/check! schema:create-draft params)) (ptk/reify ::create-draft ptk/UpdateEvent (update [_ state] @@ -404,6 +495,19 @@ (d/update-in-when [:workspace-drawing :comment] merge data) (d/update-in-when [:comments-local :draft] merge data))))) +(defn toggle-comment-options + [comment] + (ptk/reify ::toggle-comment-options + ptk/UpdateEvent + (update [_ state] + (update-in state [:comments-local :options] #(if (= (:id comment) %) nil (:id comment)))))) + +(defn hide-comment-options + [] + (ptk/reify ::hide-comment-options + ptk/UpdateEvent + (update [_ state] + (update-in state [:comments-local :options] (constantly nil))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Helpers @@ -452,28 +556,34 @@ (filter #(contains? (:participants %) (:id profile)))))) (defn update-comment-thread-frame - ([thread ] + ([thread] (update-comment-thread-frame thread uuid/zero)) - ([thread frame-id] - (dm/assert! (comment-thread? thread)) - (ptk/reify ::update-comment-thread-frame - ptk/UpdateEvent - (update [_ state] - (let [thread-id (:id thread)] - (assoc-in state [:comment-threads thread-id :frame-id] frame-id))) + ([thread frame-id] + (dm/assert! + "expected valid comment thread" + (check-comment-thread! thread)) - ptk/WatchEvent - (watch [_ _ _] - (let [thread-id (:id thread)] - (->> (rp/cmd! :update-comment-thread-frame {:id thread-id :frame-id frame-id}) - (rx/catch #(rx/throw {:type :comment-error :code :update-comment-thread-frame})) - (rx/ignore))))))) + (ptk/reify ::update-comment-thread-frame + ptk/UpdateEvent + (update [_ state] + (let [thread-id (:id thread)] + (assoc-in state [:comment-threads thread-id :frame-id] frame-id))) + + ptk/WatchEvent + (watch [_ _ _] + (let [thread-id (:id thread)] + (->> (rp/cmd! :update-comment-thread-frame {:id thread-id :frame-id frame-id}) + (rx/catch #(rx/throw {:type :comment-error :code :update-comment-thread-frame})) + (rx/ignore))))))) (defn detach-comment-thread "Detach comment threads that are inside a frame when that frame is deleted" [ids] - (dm/assert! (sm/coll-of-uuid? ids)) + (dm/assert! + "expected a valid coll of uuid's" + (sm/check-coll-of-uuid! ids)) + (ptk/reify ::detach-comment-thread ptk/WatchEvent (watch [_ state _] diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index b8106252ab..4bab615e94 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -7,12 +7,15 @@ (ns app.main.data.common "A general purpose events." (:require + [app.common.types.components-list :as ctkl] [app.config :as cf] [app.main.data.messages :as msg] + [app.main.data.modal :as modal] + [app.main.features :as features] [app.main.repo :as rp] [app.util.i18n :refer [tr]] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SHARE LINK @@ -67,6 +70,7 @@ (rx/of (msg/dialog :content (tr "notifications.by-code.upgrade-version") :controls :inline-actions + :notification-type :inline :type level :actions [{:label "Refresh" :callback force-reload!}] :tag :notification))) @@ -76,3 +80,64 @@ :controls :close :type level :tag :notification)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SHARED LIBRARY +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn show-shared-dialog + [file-id add-shared] + (ptk/reify ::show-shared-dialog + ptk/WatchEvent + (watch [_ state _] + (let [features (features/get-team-enabled-features state) + data (:workspace-data state) + file (:workspace-file state)] + (->> (if (and data file) + (rx/of {:name (:name file) + :components-count (count (ctkl/components-seq data)) + :graphics-count (count (:media data)) + :colors-count (count (:colors data)) + :typography-count (count (:typographies data))}) + (rp/cmd! :get-file-summary {:id file-id :features features})) + (rx/map (fn [summary] + (let [count (+ (:components-count summary) + (:graphics-count summary) + (:colors-count summary) + (:typography-count summary))] + (modal/show + {:type :confirm + :title (tr "modals.add-shared-confirm.message" (:name summary)) + :message (if (zero? count) (tr "modals.add-shared-confirm-empty.hint") (tr "modals.add-shared-confirm.hint")) + :cancel-label (if (zero? count) (tr "labels.cancel") :omit) + :accept-label (tr "modals.add-shared-confirm.accept") + :accept-style :primary + :on-accept add-shared}))))))))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Exportations +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn export-files + [files binary?] + (ptk/reify ::request-file-export + ptk/WatchEvent + (watch [_ state _] + (let [features (features/get-team-enabled-features state) + team-id (:current-team-id state)] + (->> (rx/from files) + (rx/mapcat + (fn [file] + (->> (rp/cmd! :has-file-libraries {:file-id (:id file)}) + (rx/map #(assoc file :has-libraries? %))))) + (rx/reduce conj []) + (rx/map (fn [files] + (modal/show + {:type :export + :features features + :team-id team-id + :has-libraries? (->> files (some :has-libraries?)) + :files files + :binary? binary?})))))))) + diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 39e3c0c3ae..bde91339da 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -8,7 +8,9 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages :as cp] + [app.common.features :as cfeat] + [app.common.files.helpers :as cfh] + [app.common.logging :as log] [app.common.schema :as sm] [app.common.uri :as u] [app.common.uuid :as uuid] @@ -24,11 +26,15 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] + [app.util.sse :as sse] [app.util.time :as dt] [app.util.timers :as tm] [app.util.webapi :as wapi] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [clojure.set :as set] + [potok.v2.core :as ptk])) + +(log/set-level! :warn) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Initialization @@ -38,16 +44,15 @@ (declare fetch-team-members) (defn initialize - [{:keys [id] :as params}] + [{:keys [id]}] (dm/assert! (uuid? id)) (ptk/reify ::initialize ptk/UpdateEvent (update [_ state] - (du/set-current-team! id) (let [prev-team-id (:current-team-id state)] (cond-> state (not= prev-team-id id) - (-> (assoc :current-team-id id) + (-> (dissoc :current-team-id) (dissoc :dashboard-files) (dissoc :dashboard-projects) (dissoc :dashboard-shared-files) @@ -58,25 +63,36 @@ ptk/WatchEvent (watch [_ state stream] - (rx/merge - ;;fetch teams must be first in case the team doesn't exist - (ptk/watch (du/fetch-teams) state stream) - (ptk/watch (df/load-team-fonts id) state stream) - (ptk/watch (fetch-projects) state stream) - (ptk/watch (fetch-team-members) state stream) - (ptk/watch (du/fetch-users {:team-id id}) state stream) - - (let [stoper (rx/filter (ptk/type? ::finalize) stream) + (let [stopper (rx/filter (ptk/type? ::finalize) stream) profile-id (:profile-id state)] - (->> stream - (rx/filter (ptk/type? ::dws/message)) - (rx/map deref) - (rx/filter (fn [{:keys [subs-id type] :as msg}] - (and (or (= subs-id uuid/zero) - (= subs-id profile-id)) - (= :notification type)))) - (rx/map handle-notification) - (rx/take-until stoper))))))) + + (->> (rx/merge + ;; fetch teams must be first in case the team doesn't exist + (ptk/watch (du/fetch-teams) state stream) + (ptk/watch (df/load-team-fonts id) state stream) + (ptk/watch (fetch-projects id) state stream) + (ptk/watch (fetch-team-members id) state stream) + (ptk/watch (du/fetch-users {:team-id id}) state stream) + + (->> stream + (rx/filter (ptk/type? ::dws/message)) + (rx/map deref) + (rx/filter (fn [{:keys [subs-id type] :as msg}] + (and (or (= subs-id uuid/zero) + (= subs-id profile-id)) + (= :notification type)))) + (rx/map handle-notification)) + + ;; Once the teams are fecthed, initialize features related + ;; to currently active team + (->> stream + (rx/filter (ptk/type? ::du/teams-fetched)) + (rx/observe-on :async) + (rx/mapcat deref) + (rx/filter #(= id (:id %))) + (rx/map du/set-current-team))) + + (rx/take-until stopper)))))) (defn finalize [params] @@ -96,13 +112,12 @@ (assoc state :dashboard-team-members (d/index-by :id members))))) (defn fetch-team-members - [] + [team-id] (ptk/reify ::fetch-team-members ptk/WatchEvent - (watch [_ state _] - (let [team-id (:current-team-id state)] - (->> (rp/cmd! :get-team-members {:team-id team-id}) - (rx/map team-members-fetched)))))) + (watch [_ _ _] + (->> (rp/cmd! :get-team-members {:team-id team-id}) + (rx/map team-members-fetched))))) ;; --- EVENT: fetch-team-stats @@ -114,13 +129,12 @@ (assoc state :dashboard-team-stats stats)))) (defn fetch-team-stats - [] + [team-id] (ptk/reify ::fetch-team-stats ptk/WatchEvent - (watch [_ state _] - (let [team-id (:current-team-id state)] - (->> (rp/cmd! :get-team-stats {:team-id team-id}) - (rx/map team-stats-fetched)))))) + (watch [_ _ _] + (->> (rp/cmd! :get-team-stats {:team-id team-id}) + (rx/map team-stats-fetched))))) ;; --- EVENT: fetch-team-invitations @@ -169,13 +183,12 @@ (assoc state :dashboard-projects projects))))) (defn fetch-projects - [] + [team-id] (ptk/reify ::fetch-projects ptk/WatchEvent - (watch [_ state _] - (let [team-id (:current-team-id state)] - (->> (rp/cmd! :get-projects {:team-id team-id}) - (rx/map projects-fetched)))))) + (watch [_ _ _] + (->> (rp/cmd! :get-projects {:team-id team-id}) + (rx/map projects-fetched))))) ;; --- EVENT: search @@ -247,13 +260,14 @@ (update :dashboard-files d/merge files)))))) (defn fetch-shared-files - [] - (ptk/reify ::fetch-shared-files - ptk/WatchEvent - (watch [_ state _] - (let [team-id (:current-team-id state)] - (->> (rp/cmd! :get-team-shared-files {:team-id team-id}) - (rx/map shared-files-fetched)))))) + ([] (fetch-shared-files nil)) + ([team-id] + (ptk/reify ::fetch-shared-files + ptk/WatchEvent + (watch [_ state _] + (let [team-id (or team-id (:current-team-id state))] + (->> (rp/cmd! :get-team-shared-files {:team-id team-id}) + (rx/map shared-files-fetched))))))) ;; --- EVENT: recent-files @@ -292,8 +306,8 @@ (ptk/reify ::fetch-builtin-templates ptk/WatchEvent (watch [_ _ _] - (->> (rp/cmd! :get-builtin-templates) - (rx/map builtin-templates-fetched))))) + (->> (rp/cmd! :get-builtin-templates) + (rx/map builtin-templates-fetched))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Selection @@ -306,7 +320,9 @@ (update [_ state] (update state :dashboard-local assoc :selected-files #{} - :selected-project nil)))) + :selected-project nil + :menu-open false + :menu-pos nil)))) (defn toggle-file-select [{:keys [id project-id] :as file}] @@ -325,6 +341,53 @@ (assoc :selected-project project-id)))) state))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Show grid menu +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn show-file-menu-with-position + [file-id pos] + (ptk/reify ::show-file-menu-with-position + ptk/UpdateEvent + (update [_ state] + (update state :dashboard-local + assoc :menu-open true + :menu-pos pos + :file-id file-id)))) + +(defn show-file-menu + [] + (ptk/reify ::show-file-menu + ptk/UpdateEvent + (update [_ state] + (update state :dashboard-local + assoc :menu-open true)))) + +(defn hide-file-menu + [] + (ptk/reify ::hide-file-menu + ptk/UpdateEvent + (update [_ state] + (update state :dashboard-local + assoc :menu-open false)))) + +(defn start-edit-file-name + [file-id] + (ptk/reify ::start-edit-file-menu + ptk/UpdateEvent + (update [_ state] + (update state :dashboard-local + assoc :edition true + :file-id file-id)))) + +(defn stop-edit-file-name + [] + (ptk/reify ::stop-edit-file-name + ptk/UpdateEvent + (update [_ state] + (update state :dashboard-local + assoc :edition false)))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Modification ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -342,11 +405,12 @@ (dm/assert! (string? name)) (ptk/reify ::create-team ptk/WatchEvent - (watch [_ _ _] + (watch [_ state _] (let [{:keys [on-success on-error] :or {on-success identity - on-error rx/throw}} (meta params)] - (->> (rp/cmd! :create-team {:name name}) + on-error rx/throw}} (meta params) + features (features/get-enabled-features state)] + (->> (rp/cmd! :create-team {:name name :features features}) (rx/tap on-success) (rx/map team-created) (rx/catch on-error)))))) @@ -357,13 +421,15 @@ [{:keys [name emails role] :as params}] (ptk/reify ::create-team-with-invitations ptk/WatchEvent - (watch [_ _ _] + (watch [_ state _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) - params {:name name - :emails #{emails} - :role role}] + features (features/get-enabled-features state)] + params {:name name + :emails #{emails} + :role role + :features features} (->> (rp/cmd! :create-team-with-invitations params) (rx/tap on-success) (rx/map team-created) @@ -402,8 +468,12 @@ (rx/map di/validate-file) (rx/map prepare) (rx/mapcat #(rp/cmd! :update-team-photo %)) - (rx/do on-success) - (rx/map du/fetch-teams) + (rx/tap on-success) + (rx/mapcat (fn [_] + (rx/of (du/fetch-teams) + (ptk/data-event ::ev/event + {::ev/name "update-team-photo" + :team-id team-id})))) (rx/catch on-error)))))) (defn update-team-member-role @@ -417,8 +487,13 @@ params (assoc params :team-id team-id)] (->> (rp/cmd! :update-team-member-role params) (rx/mapcat (fn [_] - (rx/of (fetch-team-members) - (du/fetch-teams))))))))) + (rx/of (fetch-team-members team-id) + (du/fetch-teams) + (ptk/data-event ::ev/event + {::ev/name "update-team-member-role" + :team-id team-id + :role role + :member-id member-id}))))))))) (defn delete-team-member [{:keys [member-id] :as params}] @@ -430,8 +505,12 @@ params (assoc params :team-id team-id)] (->> (rp/cmd! :delete-team-member params) (rx/mapcat (fn [_] - (rx/of (fetch-team-members) - (du/fetch-teams))))))))) + (rx/of (fetch-team-members team-id) + (du/fetch-teams) + (ptk/data-event ::ev/event + {::ev/name "delete-team-member" + :team-id team-id + :member-id member-id}))))))))) (defn leave-team [{:keys [reassign-to] :as params}] @@ -450,17 +529,28 @@ (assoc :reassign-to reassign-to))] (->> (rp/cmd! :leave-team params) (rx/tap #(tm/schedule on-success)) + (rx/map (fn [_] + (ptk/data-event ::ev/event + {::ev/name "leave-team" + :reassign-to reassign-to + :team-id team-id}))) (rx/catch on-error)))))) (defn invite-team-members [{:keys [emails role team-id resend?] :as params}] (dm/assert! (keyword? role)) (dm/assert! (uuid? team-id)) - (dm/assert! (sm/set-of-emails? emails)) + + (dm/assert! + "expected a valid set of emails" + (sm/check-set-of-emails! emails)) (ptk/reify ::invite-team-members - IDeref - (-deref [_] {:role role :team-id team-id :resend? resend?}) + ev/Event + (-data [_] + {:role role + :team-id team-id + :resend resend?}) ptk/WatchEvent (watch [_ _ _] @@ -475,7 +565,10 @@ (defn copy-invitation-link [{:keys [email team-id] :as params}] - (dm/assert! (sm/email? email)) + (dm/assert! + "expected a valid email" + (sm/check-email! email)) + (dm/assert! (uuid? team-id)) (ptk/reify ::copy-invitation-link @@ -503,7 +596,10 @@ (defn update-team-invitation-role [{:keys [email team-id role] :as params}] - (dm/assert! (sm/email? email)) + (dm/assert! + "expected a valid email" + (sm/check-email! email)) + (dm/assert! (uuid? team-id)) (dm/assert! (keyword? role)) ;; FIXME validate role @@ -522,7 +618,7 @@ (defn delete-team-invitation [{:keys [email team-id] :as params}] - (dm/assert! (sm/email? email)) + (dm/assert! (sm/check-email! email)) (dm/assert! (uuid? team-id)) (ptk/reify ::delete-team-invitation ptk/WatchEvent @@ -626,8 +722,8 @@ ptk/WatchEvent (watch [_ state _] (let [projects (get state :dashboard-projects) - unames (cp/retrieve-used-names projects) - name (cp/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1")) + unames (cfh/get-used-names projects) + name (cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1")) team-id (:current-team-id state) params {:name name :team-id team-id} @@ -652,6 +748,11 @@ [{:keys [id name] :as params}] (dm/assert! (uuid? id)) (ptk/reify ::duplicate-project + ev/Event + (-data [_] + {:project-id id + :name name}) + ptk/WatchEvent (watch [_ _ _] (let [{:keys [on-success on-error] @@ -669,10 +770,12 @@ [{:keys [id team-id] :as params}] (dm/assert! (uuid? id)) (dm/assert! (uuid? team-id)) + (ptk/reify ::move-project - IDeref - (-deref [_] - {:id id :team-id team-id}) + ev/Event + (-data [_] + {:id id + :team-id team-id}) ptk/WatchEvent (watch [_ _ _] @@ -759,9 +862,11 @@ (defn rename-file [{:keys [id name] :as params}] (ptk/reify ::rename-file - IDeref - (-deref [_] - {::ev/origin "dashboard" :id id :name name}) + ev/Event + (-data [_] + {::ev/origin "dashboard" + :id id + :name name}) ptk/UpdateEvent (update [_ state] @@ -781,9 +886,11 @@ (defn set-file-shared [{:keys [id is-shared] :as params}] (ptk/reify ::set-file-shared - IDeref - (-deref [_] - {::ev/origin "dashboard" :id id :shared is-shared}) + ev/Event + (-data [_] + {::ev/origin "dashboard" + :id id + :shared is-shared}) ptk/UpdateEvent (update [_ state] @@ -791,7 +898,7 @@ (d/update-in-when [:dashboard-files id :is-shared] (constantly is-shared)) (d/update-in-when [:dashboard-recent-files id :is-shared] (constantly is-shared)) (cond-> - (not is-shared) + (not is-shared) (d/update-when :dashboard-shared-files dissoc id)))) ptk/WatchEvent @@ -805,9 +912,15 @@ (ptk/reify ::set-file-thumbnail ptk/UpdateEvent (update [_ state] - (-> state - (d/update-in-when [:dashboard-files file-id] assoc :thumbnail-uri thumbnail-uri) - (d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-uri thumbnail-uri))))) + (letfn [(update-search-files [files] + (->> files + (mapv #(cond-> % + (= file-id (:id %)) + (assoc :thumbnail-uri thumbnail-uri)))))] + (-> state + (d/update-in-when [:dashboard-files file-id] assoc :thumbnail-uri thumbnail-uri) + (d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-uri thumbnail-uri) + (d/update-when :dashboard-search-result update-search-files)))))) ;; --- EVENT: create-file @@ -831,9 +944,8 @@ [{:keys [project-id] :as params}] (dm/assert! (uuid? project-id)) (ptk/reify ::create-file - - IDeref - (-deref [_] {:project-id project-id}) + ev/Event + (-data [_] {:project-id project-id}) ptk/WatchEvent (watch [it state _] @@ -842,11 +954,10 @@ on-error rx/throw}} (meta params) files (get state :dashboard-files) - unames (cp/retrieve-used-names files) - name (cp/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")) - features (cond-> #{} - (features/active-feature? state :components-v2) - (conj "components/v2")) + unames (cfh/get-used-names files) + name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")) + features (-> (features/get-team-enabled-features state) + (set/difference cfeat/frontend-only-features)) params (-> params (assoc :name name) (assoc :features features))] @@ -880,12 +991,15 @@ (defn move-files [{:keys [ids project-id] :as params}] - (dm/assert! (sm/set-of-uuid? ids)) (dm/assert! (uuid? project-id)) + (dm/assert! + "expected a valid set of uuids" + (sm/check-set-of-uuid! ids)) + (ptk/reify ::move-files - IDeref - (-deref [_] + ev/Event + (-data [_] {:num-files (count ids) :project-id project-id}) @@ -909,14 +1023,14 @@ (rx/tap on-success) (rx/catch on-error)))))) - ;; --- EVENT: clone-template + (defn clone-template [{:keys [template-id project-id] :as params}] (dm/assert! (uuid? project-id)) (ptk/reify ::clone-template - IDeref - (-deref [_] + ev/Event + (-data [_] {:template-id template-id :project-id project-id}) @@ -925,7 +1039,17 @@ (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params)] - (->> (rp/cmd! :clone-template {:project-id project-id :template-id template-id}) + (->> (rp/cmd! ::sse/clone-template {:project-id project-id + :template-id template-id}) + (rx/tap (fn [event] + (let [payload (sse/get-payload event) + type (sse/get-type event)] + (if (= type "progress") + (log/dbg :hint "clone-template: progress" :section (:section payload) :name (:name payload)) + (log/dbg :hint "clone-template: end"))))) + + (rx/filter sse/end-of-stream?) + (rx/map sse/get-payload) (rx/tap on-success) (rx/catch on-error)))))) @@ -966,9 +1090,9 @@ (let [team-id (:current-team-id state)] (if (empty? term) (do - (dom/focus! (dom/get-element "search-input")) - (rx/of (rt/nav :dashboard-search - {:team-id team-id}))) + (dom/focus! (dom/get-element "search-input")) + (rx/of (rt/nav :dashboard-search + {:team-id team-id}))) (rx/of (rt/nav :dashboard-search {:team-id team-id} {:search-term term}))))) @@ -1053,11 +1177,11 @@ in-project? (contains? pparams :project-id) name (if in-project? (let [files (get state :dashboard-files) - unames (cp/retrieve-used-names files)] - (cp/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1"))) + unames (cfh/get-used-names files)] + (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1"))) (let [projects (get state :dashboard-projects) - unames (cp/retrieve-used-names projects)] - (cp/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1")))) + unames (cfh/get-used-names projects)] + (cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1")))) params (if in-project? {:project-id (:project-id pparams) :name name} diff --git a/frontend/src/app/main/data/dashboard/shortcuts.cljs b/frontend/src/app/main/data/dashboard/shortcuts.cljs index 54140e6502..98d987c113 100644 --- a/frontend/src/app/main/data/dashboard/shortcuts.cljs +++ b/frontend/src/app/main/data/dashboard/shortcuts.cljs @@ -7,7 +7,9 @@ (ns app.main.data.dashboard.shortcuts (:require [app.main.data.dashboard :as dd] + [app.main.data.events :as ev] [app.main.data.shortcuts :as ds] + [app.main.data.users :as du] [app.main.store :as st])) (def shortcuts @@ -25,11 +27,19 @@ :command "g l" :subsections [:navigation-dashboard] :fn #(st/emit! (dd/go-to-libs))} - + :create-new-project {:tooltip "+" :command "+" :subsections [:general-dashboard] - :fn #(st/emit! (dd/create-element))}}) + :fn #(st/emit! (dd/create-element))} + + :toggle-theme {:tooltip (ds/alt "M") + :command (ds/a-mod "m") + :subsections [:general-dashboard] + :fn #(st/emit! (with-meta (du/toggle-theme) + {::ev/origin "dashboard:shortcuts"}))}}) + + (defn get-tooltip [shortcut] (assert (contains? shortcuts shortcut) (str shortcut)) diff --git a/frontend/src/app/main/data/events.cljs b/frontend/src/app/main/data/events.cljs index 86d330859f..ec217339c8 100644 --- a/frontend/src/app/main/data/events.cljs +++ b/frontend/src/app/main/data/events.cljs @@ -17,9 +17,10 @@ [app.util.object :as obj] [app.util.storage :refer [storage]] [app.util.time :as dt] - [beicon.core :as rx] + [beicon.v2.core :as rx] + [beicon.v2.operators :as rxo] [lambdaisland.uri :as u] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (l/set-level! :info) @@ -74,131 +75,65 @@ ;; --- EVENT TRANSLATION -(derive :app.main.data.comments/create-comment ::generic-action) -(derive :app.main.data.comments/create-comment-thread ::generic-action) -(derive :app.main.data.comments/delete-comment ::generic-action) -(derive :app.main.data.comments/delete-comment-thread ::generic-action) -(derive :app.main.data.comments/open-comment-thread ::generic-action) -(derive :app.main.data.comments/update-comment ::generic-action) -(derive :app.main.data.comments/update-comment-thread ::generic-action) -(derive :app.main.data.comments/update-comment-thread-status ::generic-action) -(derive :app.main.data.dashboard/delete-team-member ::generic-action) -(derive :app.main.data.dashboard/duplicate-project ::generic-action) -(derive :app.main.data.dashboard/create-file ::generic-action) -(derive :app.main.data.dashboard/file-created ::generic-action) -(derive :app.main.data.dashboard/invite-team-members ::generic-action) -(derive :app.main.data.dashboard/leave-team ::generic-action) -(derive :app.main.data.dashboard/move-files ::generic-action) -(derive :app.main.data.dashboard/move-project ::generic-action) -(derive :app.main.data.dashboard/project-created ::generic-action) -(derive :app.main.data.dashboard/rename-file ::generic-action) -(derive :app.main.data.dashboard/set-file-shared ::generic-action) -(derive :app.main.data.dashboard/update-team-member-role ::generic-action) -(derive :app.main.data.dashboard/update-team-photo ::generic-action) -(derive :app.main.data.dashboard/clone-template ::generic-action) -(derive :app.main.data.fonts/add-font ::generic-action) -(derive :app.main.data.fonts/delete-font ::generic-action) -(derive :app.main.data.fonts/delete-font-variant ::generic-action) -(derive :app.main.data.modal/show-modal ::generic-action) -(derive :app.main.data.users/logout ::generic-action) -(derive :app.main.data.users/request-email-change ::generic-action) -(derive :app.main.data.users/update-password ::generic-action) -(derive :app.main.data.users/update-photo ::generic-action) -(derive :app.main.data.workspace.comments/open-comment-thread ::generic-action) -(derive :app.main.data.workspace.guides/update-guides ::generic-action) -(derive :app.main.data.workspace.libraries/add-color ::generic-action) -(derive :app.main.data.workspace.libraries/add-media ::generic-action) -(derive :app.main.data.workspace.libraries/add-typography ::generic-action) -(derive :app.main.data.workspace.libraries/delete-color ::generic-action) -(derive :app.main.data.workspace.libraries/delete-media ::generic-action) -(derive :app.main.data.workspace.libraries/delete-typography ::generic-action) -(derive :app.main.data.workspace.persistence/attach-library ::generic-action) -(derive :app.main.data.workspace.persistence/detach-library ::generic-action) -(derive :app.main.data.workspace.persistence/set-file-shard ::generic-action) -(derive :app.main.data.workspace.selection/toggle-focus-mode ::generic-action) -(derive :app.main.data.workspace/create-page ::generic-action) -(derive :app.main.data.workspace/set-workspace-layout ::generic-action) -(derive :app.main.data.workspace/toggle-layout-flag ::generic-action) +(defprotocol Event + (-data [_] "Get event data")) -(defmulti process-event ptk/type) -(defmethod process-event :default [_] nil) - -(defmethod process-event ::event - [event] - (let [data (deref event) - origin (::origin data)] - (when (::name data) - (d/without-nils - {:type (::type data "action") - :name (::name data) - :context (::context data) - :props (-> data - (dissoc ::name) - (dissoc ::type) - (dissoc ::origin) - (dissoc ::context) - (cond-> origin (assoc :origin origin)))})))) - -(defn- normalize-props +(defn- simplify-props "Removes complex data types from props." [data] - (into {} - (comp - (remove (fn [[_ v]] (nil? v))) - (map (fn [[k v :as kv]] - (cond - (map? v) [k :placeholder/map] - (vector? v) [k :placeholder/vec] - (set? v) [k :placeholder/set] - (coll? v) [k :placeholder/coll] - (fn? v) [k :placeholder/fn] - :else kv)))) - data)) + (reduce-kv (fn [data k v] + (cond + (map? v) (assoc data k :placeholder/map) + (vector? v) (assoc data k :placeholder/vec) + (set? v) (assoc data k :placeholder/set) + (coll? v) (assoc data k :placeholder/coll) + (fn? v) (assoc data k :placeholder/fn) + (nil? v) (dissoc data k) + :else data)) + data + data)) -(defmethod process-event ::generic-action +(defn- process-event-by-proto [event] - (let [type (ptk/type event) - mdata (meta event) - data (if (satisfies? IDeref event) - (deref event) - {})] + (let [data (d/deep-merge (-data event) (meta event)) + type (ptk/type event) + ev-name (name type) + context (-> (::context data) + (assoc :event-origin (::origin data)) + (assoc :event-namespace (namespace type)) + (assoc :event-symbol ev-name) + (d/without-nils)) + props (-> data d/without-qualified simplify-props)] - {:type "action" - :name (or (::name mdata) (name type)) - :props (-> (merge data (::props mdata)) - (normalize-props)) - :context (d/without-nils - {:event-origin (::origin mdata) - :event-namespace (namespace type) - :event-symbol (name type)})})) + {:type (::type data "action") + :name (::name data ev-name) + :context context + :props props})) -(defmethod process-event :app.util.router/navigated +(defn- process-data-event [event] - (let [match (deref event) - route (get-in match [:data :name]) - props {:route (name route) - :team-id (get-in match [:path-params :team-id]) - :file-id (get-in match [:path-params :file-id]) - :project-id (get-in match [:path-params :project-id])}] - {:name "navigate" - :type "action" - :props (normalize-props props)})) + (let [data (deref event) + name (::name data)] -(defmethod process-event :app.main.data.users/logged-in + (when (string? name) + (let [type (::type data "action") + context (-> (::context data) + (assoc :event-origin (::origin data)) + (d/without-nils)) + props (-> data d/without-qualified simplify-props)] + {:type type + :name name + :context context + :props props})))) + +(defn- process-event [event] - (let [data (deref event) - mdata (meta data) - props {:signin-source (::source mdata) - :email (:email data) - :auth-backend (:auth-backend data) - :fullname (:fullname data) - :is-muted (:is-muted data) - :default-team-id (str (:default-team-id data)) - :default-project-id (str (:default-project-id data))}] - {:name "signin" - :type "identify" - :profile-id (:id data) - :props (normalize-props props)})) + (cond + (satisfies? Event event) + (process-event-by-proto event) + + (ptk/data-event? event) + (process-data-event event))) ;; --- MAIN LOOP @@ -222,9 +157,7 @@ :body (http/transit-data {:events events})}] (->> (http/send! params) (rx/mapcat rp/handle-response) - (rx/catch (fn [_] - (l/error :hint "unexpected error on persisting audit events") - (rx/of nil))))) + (rx/catch (fn [_] (rx/of nil))))) (rx/of nil))) @@ -235,12 +168,12 @@ ptk/EffectEvent (effect [_ _ stream] (let [session (atom nil) - stoper (rx/filter (ptk/type? ::initialize) stream) + stopper (rx/filter (ptk/type? ::initialize) stream) buffer (atom #queue []) profile (->> (rx/from-atom storage {:emit-current-value? true}) (rx/map :profile) (rx/map :id) - (rx/dedupe))] + (rx/pipe (rxo/distinct-contiguous)))] (l/debug :hint "event instrumentation initialized") @@ -260,13 +193,13 @@ (rx/tap (fn [_] (l/debug :hint "events chunk persisted" :total (count chunk)))) (rx/map (constantly chunk)))))) - (rx/take-until stoper) - (rx/subs (fn [chunk] - (swap! buffer remove-from-buffer (count chunk))) - (fn [cause] - (l/error :hint "unexpected error on audit persistence" :cause cause)) - (fn [] - (l/debug :hint "audit persistence terminated")))) + (rx/take-until stopper) + (rx/subs! (fn [chunk] + (swap! buffer remove-from-buffer (count chunk))) + (fn [cause] + (l/error :hint "unexpected error on audit persistence" :cause cause)) + (fn [] + (l/debug :hint "audit persistence terminated")))) (->> stream (rx/with-latest-from profile) @@ -291,11 +224,11 @@ (swap! buffer append-to-buffer event))) (rx/switch-map #(rx/timer (inst-ms session-timeout))) - (rx/take-until stoper) - (rx/subs (fn [_] - (l/debug :hint "session reinitialized") - (reset! session nil)) - (fn [cause] - (l/error :hint "error on event batching stream" :cause cause)) - (fn [] - (l/debug :hitn "events batching stream terminated"))))))))) + (rx/take-until stopper) + (rx/subs! (fn [_] + (l/debug :hint "session reinitialized") + (reset! session nil)) + (fn [cause] + (l/error :hint "error on event batching stream" :cause cause)) + (fn [] + (l/debug :hitn "events batching stream terminated"))))))))) diff --git a/frontend/src/app/main/data/exports.cljs b/frontend/src/app/main/data/exports.cljs index 729d5d74f7..9894691a24 100644 --- a/frontend/src/app/main/data/exports.cljs +++ b/frontend/src/app/main/data/exports.cljs @@ -10,13 +10,14 @@ [app.main.data.modal :as modal] [app.main.data.workspace.persistence :as dwp] [app.main.data.workspace.state-helpers :as wsh] + [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] [app.util.dom :as dom] [app.util.time :as dt] [app.util.websocket :as ws] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (def default-timeout 5000) @@ -166,6 +167,13 @@ :wait true}] (rx/concat (rx/of ::dwp/force-persist) + + ;; Wait the persist to be succesfull + (->> (rx/from-atom refs/persistence-state {:emit-current-value? true}) + (rx/filter #(or (nil? %) (= :saved %))) + (rx/first) + (rx/timeout 400 (rx/empty))) + (->> (rp/cmd! :export params) (rx/mapcat (fn [{:keys [id filename]}] (->> (rp/cmd! :export {:cmd :get-resource :blob? true :id id}) @@ -201,7 +209,7 @@ (rx/filter #(= @resource-id (:resource-id %))) (rx/share)) - stoper + stopper (rx/filter #(or (= "ended" (:status %)) (= "error" (:status %))) progress-stream)] @@ -220,12 +228,12 @@ (initialize-export-status exports cmd resource)))) ;; We proceed to update the export state with incoming - ;; progress updates. We delay the stoper for give some time + ;; progress updates. We delay the stopper for give some time ;; to update the status with ended or errored status before ;; close the stream. (->> progress-stream (rx/map update-export-status) - (rx/take-until (rx/delay 500 stoper)) + (rx/take-until (rx/delay 500 stopper)) (rx/finalize (fn [] (swap! st/ongoing-tasks disj :export)))) @@ -238,7 +246,7 @@ (rx/take 1) (rx/delay default-timeout) (rx/map #(clear-export-state @resource-id)) - (rx/take-until (rx/delay 6000 stoper)))))))) + (rx/take-until (rx/delay 6000 stopper)))))))) (defn retry-last-export [] diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs index d3a499ebac..b7150b033a 100644 --- a/frontend/src/app/main/data/fonts.cljs +++ b/frontend/src/app/main/data/fonts.cljs @@ -12,6 +12,7 @@ [app.common.logging :as log] [app.common.media :as cm] [app.common.uuid :as uuid] + [app.main.data.events :as ev] [app.main.data.messages :as msg] [app.main.fonts :as fonts] [app.main.repo :as rp] @@ -19,9 +20,9 @@ [app.util.i18n :refer [tr]] [app.util.storage :refer [storage]] [app.util.webapi :as wa] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; General purpose events & IMPL @@ -236,12 +237,19 @@ (defn add-font [font] (ptk/reify ::add-font - IDeref - (-deref [_] (select-keys font [:font-family :font-style :font-weight])) - ptk/UpdateEvent (update [_ state] - (update state :dashboard-fonts assoc (:id font) font)))) + (update state :dashboard-fonts assoc (:id font) font)) + + ptk/WatchEvent + (watch [_ state _] + (let [team-id (:current-team-id state)] + (rx/of (ptk/data-event ::ev/event {::ev/name "add-font" + :team-id team-id + :font-id (:id font) + :font-family (:font-family font) + :font-style (:font-style font) + :font-weight (:font-weight font)})))))) (defn update-font [{:keys [id name] :as params}] @@ -271,6 +279,10 @@ [font-id] (dm/assert! (uuid? font-id)) (ptk/reify ::delete-font + ev/Event + (-data [_] + {:id font-id}) + ptk/UpdateEvent (update [_ state] (update state :dashboard-fonts @@ -280,8 +292,12 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/cmd! :delete-font {:id font-id :team-id team-id}) - (rx/ignore)))))) + (rx/concat + (->> (rp/cmd! :delete-font {:id font-id :team-id team-id}) + (rx/ignore)) + (rx/of (ptk/data-event ::ev/event {::ev/name "delete-font" + :team-id team-id + :font-id font-id}))))))) (defn delete-font-variant [id] @@ -297,8 +313,13 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/cmd! :delete-font-variant {:id id :team-id team-id}) - (rx/ignore)))))) + (rx/concat + (->> (rp/cmd! :delete-font-variant {:id id :team-id team-id}) + (rx/ignore)) + (rx/of (ptk/data-event ::ev/event {::ev/name "delete-font-variant" + :id id + :team-id team-id}))))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Workspace related events @@ -318,9 +339,12 @@ (swap! storage assoc ::recent-fonts most-recent-fonts))))) (defn load-recent-fonts - [] + [fonts] (ptk/reify ::load-recent-fonts ptk/UpdateEvent (update [_ state] - (let [saved-recent-fonts (::recent-fonts @storage)] + (let [fonts-map (d/index-by :id fonts) + saved-recent-fonts (->> (::recent-fonts @storage) + (keep #(get fonts-map (:id %))) + (into #{}))] (assoc-in state [:workspace-data :recent-fonts] saved-recent-fonts))))) diff --git a/frontend/src/app/main/data/media.cljs b/frontend/src/app/main/data/media.cljs index da1049ecf7..e78892bb1b 100644 --- a/frontend/src/app/main/data/media.cljs +++ b/frontend/src/app/main/data/media.cljs @@ -8,10 +8,10 @@ (:require [app.common.exceptions :as ex] [app.common.media :as cm] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.store :as st] [app.util.i18n :refer [tr]] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.spec.alpha :as s] [cuerdas.core :as str])) @@ -46,13 +46,14 @@ (defn notify-start-loading [] - (st/emit! (dm/show {:content (tr "media.loading") - :type :info - :timeout nil}))) + (st/emit! (msg/show {:content (tr "media.loading") + :notification-type :toast + :type :info + :timeout nil}))) (defn notify-finished-loading [] - (st/emit! dm/hide)) + (st/emit! msg/hide)) (defn process-error [error] @@ -68,4 +69,4 @@ :else (tr "errors.unexpected-error"))] - (rx/of (dm/error msg)))) + (rx/of (msg/error msg)))) diff --git a/frontend/src/app/main/data/messages.cljs b/frontend/src/app/main/data/messages.cljs index 8ace432288..024fec415a 100644 --- a/frontend/src/app/main/data/messages.cljs +++ b/frontend/src/app/main/data/messages.cljs @@ -9,42 +9,48 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.schema :as sm] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (declare hide) (declare show) (def default-animation-timeout 600) -(def default-timeout 5000) +(def default-timeout 7000) -(def schema:message - [:map {:title "Message"} - [:type [::sm/one-of #{:success :error :info :warning}]] - [:status {:optional true} - [::sm/one-of #{:visible :hide}]] - [:position {:optional true} - [::sm/one-of #{:fixed :floating :inline}]] - [:controls {:optional true} - [::sm/one-of #{:none :close :inline-actions :bottom-actions}]] - [:tag {:optional true} - [:or :string :keyword]] - [:timeout {:optional true} - [:maybe :int]] - [:actions {:optional true} - [:vector - [:map - [:label :string] - [:callback ::sm/fn]]]]]) - -(def message? - (sm/pred-fn schema:message)) +(def ^:private + schema:message + (sm/define + [:map {:title "Message"} + [:type [::sm/one-of #{:success :error :info :warning}]] + [:status {:optional true} + [::sm/one-of #{:visible :hide}]] + [:position {:optional true} + [::sm/one-of #{:fixed :floating :inline}]] + [:notification-type {:optional true} + [::sm/one-of #{:inline :context :toast}]] + [:controls {:optional true} + [::sm/one-of #{:none :close :inline-actions :bottom-actions}]] + [:tag {:optional true} + [:or :string :keyword]] + [:timeout {:optional true} + [:maybe :int]] + [:actions {:optional true} + [:vector + [:map + [:label :string] + [:callback ::sm/fn]]]] + [:links {:optional true} + [:vector + [:map + [:label :string] + [:callback ::sm/fn]]]]])) (defn show [data] (dm/assert! "expected valid message map" - (message? data)) + (sm/check! schema:message data)) (ptk/reify ::show ptk/UpdateEvent @@ -55,16 +61,16 @@ ptk/WatchEvent (watch [_ _ stream] (rx/merge - (let [stoper (rx/filter (ptk/type? ::hide) stream)] - (->> stream - (rx/filter (ptk/type? :app.util.router/navigate)) - (rx/map (constantly hide)) - (rx/take-until stoper))) - (when (:timeout data) - (let [stoper (rx/filter (ptk/type? ::show) stream)] - (->> (rx/of hide) - (rx/delay (:timeout data)) - (rx/take-until stoper)))))))) + (let [stopper (rx/filter (ptk/type? ::hide) stream)] + (->> stream + (rx/filter (ptk/type? :app.util.router/navigate)) + (rx/map (constantly hide)) + (rx/take-until stopper))) + (when (:timeout data) + (let [stopper (rx/filter (ptk/type? ::show) stream)] + (->> (rx/of hide) + (rx/delay (:timeout data)) + (rx/take-until stopper)))))))) (def hide (ptk/reify ::hide @@ -74,10 +80,10 @@ ptk/WatchEvent (watch [_ _ stream] - (let [stoper (rx/filter (ptk/type? ::show) stream)] + (let [stopper (rx/filter (ptk/type? ::show) stream)] (->> (rx/of #(dissoc % :message)) (rx/delay default-animation-timeout) - (rx/take-until stoper)))))) + (rx/take-until stopper)))))) (defn hide-tag [tag] @@ -89,18 +95,18 @@ (rx/of hide)))))) (defn error - ([content] (error content {})) - ([content {:keys [timeout] :or {timeout default-timeout}}] + ([content] (show {:content content :type :error - :position :fixed - :timeout timeout}))) + :notification-type :toast + :position :fixed}))) (defn info ([content] (info content {})) ([content {:keys [timeout] :or {timeout default-timeout}}] (show {:content content :type :info + :notification-type :toast :position :fixed :timeout timeout}))) @@ -109,6 +115,7 @@ ([content {:keys [timeout] :or {timeout default-timeout}}] (show {:content content :type :success + :notification-type :toast :position :fixed :timeout timeout}))) @@ -117,6 +124,7 @@ ([content {:keys [timeout] :or {timeout default-timeout}}] (show {:content content :type :warning + :notification-type :toast :position :fixed :timeout timeout}))) @@ -132,12 +140,14 @@ :tag tag}))) (defn info-dialog - ([content controls actions] - (info-dialog content controls actions nil)) - ([content controls actions tag] - (show {:content content + [& {:keys [content controls links actions tag] + :or {controls :none links nil tag nil}}] + (show (d/without-nils + {:content content :type :info :position :floating + :notification-type :inline :controls controls + :links links :actions actions :tag tag}))) diff --git a/frontend/src/app/main/data/modal.cljs b/frontend/src/app/main/data/modal.cljs index fe7055297e..1055014c28 100644 --- a/frontend/src/app/main/data/modal.cljs +++ b/frontend/src/app/main/data/modal.cljs @@ -8,9 +8,10 @@ (:refer-clojure :exclude [update]) (:require [app.common.uuid :as uuid] + [app.main.data.events :as ev] [app.main.store :as st] [cljs.core :as c] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (defonce components (atom {})) @@ -23,9 +24,11 @@ (show (uuid/next) type props)) ([id type props] (ptk/reify ::show-modal - IDeref - (-deref [_] - (merge (dissoc props :type) {:name type})) + ev/Event + (-data [_] + (-> props + (dissoc :type) + (assoc :name type))) ptk/UpdateEvent (update [_ state] diff --git a/frontend/src/app/main/data/preview.cljs b/frontend/src/app/main/data/preview.cljs new file mode 100644 index 0000000000..7510fb0894 --- /dev/null +++ b/frontend/src/app/main/data/preview.cljs @@ -0,0 +1,105 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.data.preview + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.types.shape-tree :as ctst] + [app.main.data.workspace.state-helpers :as wsh] + [app.main.fonts :as fonts] + [app.main.refs :as refs] + [app.util.code-beautify :as cb] + [app.util.code-gen :as cg] + [app.util.timers :as ts] + [beicon.v2.core :as rx] + [clojure.set :as set] + [cuerdas.core :as str] + [potok.v2.core :as ptk])) + +(def style-type "css") +(def markup-type "html") + + +(def page-template + " + + + + + + %s + +") + +(defn update-preview-window + [preview code width height] + (when preview + (if (aget preview "load") + (.load preview code width height) + (ts/schedule #(update-preview-window preview code width height))))) + +(defn shapes->fonts + [shapes] + (->> shapes + (filter cfh/text-shape?) + (map (comp fonts/get-content-fonts :content)) + (reduce set/union #{}))) + +(defn update-preview + [preview shape-id] + (ptk/reify ::update-preview + ptk/EffectEvent + (effect [_ state _] + (let [objects (wsh/lookup-page-objects state) + shape (get objects shape-id) + + all-children + (->> (cfh/selected-with-children objects [shape-id]) + (ctst/sort-z-index objects) + (keep (d/getf objects))) + + fonts (shapes->fonts all-children)] + + (->> (rx/from fonts) + (rx/merge-map fonts/fetch-font-css) + (rx/reduce conj []) + (rx/map #(str/join "\n" %)) + (rx/subs! + (fn [fontfaces-css] + (let [style-code + (dm/str + fontfaces-css "\n" + (-> (cg/generate-style-code objects style-type [shape] all-children) + (cb/format-code style-type))) + + markup-code + (-> (cg/generate-markup-code objects markup-type [shape]) + (cb/format-code markup-type))] + + (update-preview-window + preview + (str/format page-template style-code markup-code) + (-> shape :selrect :width) + (-> shape :selrect :height)))))))))) + +(defn open-preview-selected + [] + (ptk/reify ::open-preview-selected + ptk/WatchEvent + (watch [_ state _] + (let [shape-id (first (wsh/lookup-selected state)) + closed-preview (rx/subject) + preview (.open js/window "/#/frame-preview") + listener-fn #(rx/push! closed-preview true)] + (.addEventListener preview "beforeunload" listener-fn) + (->> (rx/from-atom (refs/all-children-objects shape-id) {:emit-current-value? true}) + (rx/take-until closed-preview) + (rx/debounce 1000) + (rx/map #(update-preview preview shape-id))))))) diff --git a/frontend/src/app/main/data/shortcuts.cljs b/frontend/src/app/main/data/shortcuts.cljs index f5cef666bf..55ea364a3a 100644 --- a/frontend/src/app/main/data/shortcuts.cljs +++ b/frontend/src/app/main/data/shortcuts.cljs @@ -13,7 +13,7 @@ [app.common.schema :as sm] [app.config :as cf] [cuerdas.core :as str] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (log/set-level! :warn) @@ -127,16 +127,17 @@ ;; --- EVENT: push -(def schema:shortcuts - [:map-of - :keyword - [:map - [:command [:or :string [:vector :any]]] - [:fn {:optional true} fn?] - [:tooltip {:optional true} :string]]]) +(def ^:private + schema:shortcuts + (sm/define + [:map-of :keyword + [:map + [:command [:or :string [:vector :any]]] + [:fn {:optional true} fn?] + [:tooltip {:optional true} :string]]])) -(def shortcuts? - (sm/pred-fn schema:shortcuts)) +(def check-shortcuts! + (sm/check-fn schema:shortcuts)) (defn- wrap-cb [key cb] @@ -169,8 +170,11 @@ (defn push-shortcuts [key shortcuts] - (dm/assert! (keyword? key)) - (dm/assert! (shortcuts? shortcuts)) + + (dm/assert! + "expected valid parameters" + (and (keyword? key) + (check-shortcuts! shortcuts))) (ptk/reify ::push-shortcuts ptk/UpdateEvent diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index ddf8d7400c..392c6e055e 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -16,26 +16,29 @@ [app.main.data.events :as ev] [app.main.data.media :as di] [app.main.data.websocket :as ws] + [app.main.features :as features] [app.main.repo :as rp] [app.util.i18n :as i18n] [app.util.router :as rt] [app.util.storage :refer [storage]] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) ;; --- SCHEMAS -(def schema:profile - [:map {:title "Profile"} - [:id ::sm/uuid] - [:created-at {:optional true} :any] - [:fullname {:optional true} :string] - [:email {:optional true} :string] - [:lang {:optional true} :string] - [:theme {:optional true} :string]]) +(def ^:private + schema:profile + (sm/define + [:map {:title "Profile"} + [:id ::sm/uuid] + [:created-at {:optional true} :any] + [:fullname {:optional true} :string] + [:email {:optional true} :string] + [:lang {:optional true} :string] + [:theme {:optional true} :string]])) -(def profile? - (sm/pred-fn schema:profile)) +(def check-profile! + (sm/check-fn schema:profile)) ;; --- HELPERS @@ -50,27 +53,28 @@ (defn set-current-team! [team-id] - (swap! storage assoc ::current-team-id team-id)) + (if (nil? team-id) + (swap! storage dissoc ::current-team-id) + (swap! storage assoc ::current-team-id team-id))) ;; --- EVENT: fetch-teams (defn teams-fetched [teams] - (let [teams (d/index-by :id teams) - ids (into #{} (keys teams))] + (ptk/reify ::teams-fetched + IDeref + (-deref [_] teams) - (ptk/reify ::teams-fetched - IDeref - (-deref [_] teams) + ptk/UpdateEvent + (update [_ state] + (assoc state :teams (d/index-by :id teams))) - ptk/UpdateEvent - (update [_ state] - (assoc state :teams teams)) + ptk/EffectEvent + (effect [_ _ _] + ;; Check if current team-id is part of available teams + ;; if not, dissoc it from storage. - ptk/EffectEvent - (effect [_ _ _] - ;; Check if current team-id is part of available teams - ;; if not, dissoc it from storage. + (let [ids (into #{} (map :id) teams)] (when-let [ctid (::current-team-id @storage)] (when-not (contains? ids ctid) (swap! storage dissoc ::current-team-id))))))) @@ -83,6 +87,23 @@ (->> (rp/cmd! :get-teams) (rx/map teams-fetched))))) +(defn set-current-team + [team] + (ptk/reify ::set-current-team + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc :team team) + (assoc :current-team-id (:id team)))) + + ptk/WatchEvent + (watch [_ _ _] + (rx/of (features/initialize (:features team #{})))) + + ptk/EffectEvent + (effect [_ _ _] + (set-current-team! (:id team))))) + ;; --- EVENT: fetch-profile (declare logout) @@ -112,8 +133,8 @@ (when profile (swap! storage assoc :profile profile) (i18n/set-locale! (:lang profile)) - (when (not= previous-email email) - (swap! storage dissoc ::current-team-id))))))) + (when (not= previous-email email) + (set-current-team! nil))))))) (defn fetch-profile [] @@ -140,8 +161,16 @@ (rt/nav' :dashboard-projects {:team-id team-id}))))] (ptk/reify ::logged-in - IDeref - (-deref [_] profile) + ev/Event + (-data [_] + {::ev/name "signing" + ::ev/type "identify" + :email (:email profile) + :auth-backend (:auth-backend profile) + :fullname (:fullname profile) + :is-muted (:is-muted profile) + :default-team-id (:default-team-id profile) + :default-project-id (:default-project-id profile)}) ptk/WatchEvent (watch [_ _ _] @@ -208,9 +237,14 @@ (ptk/reify ::login-from-token ptk/WatchEvent (watch [_ _ _] - (rx/of (logged-in - (with-meta profile - {::ev/source "login-with-token"})))))) + (->> (rx/of (logged-in (with-meta profile {::ev/source "login-with-token"}))) + ;; NOTE: we need this to be asynchronous because the effect + ;; should be called before proceed with the login process + (rx/observe-on :async))) + + ptk/EffectEvent + (effect [_ _ _] + (set-current-team! nil)))) (defn login-from-register "Event used mainly for mark current session as logged-in in after the @@ -255,12 +289,16 @@ (effect [_ _ _] ;; We prefer to keek some stuff in the storage like the current-team-id and the profile (swap! storage dissoc :redirect-url) + (set-current-team! nil) (i18n/reset-locale))))) (defn logout ([] (logout {})) ([params] (ptk/reify ::logout + ev/Event + (-data [_] {}) + ptk/WatchEvent (watch [_ _ _] (->> (rp/cmd! :logout) @@ -270,28 +308,62 @@ ;; --- Update Profile -(defn update-profile - [data] - (dm/assert! (profile? data)) - (ptk/reify ::update-profile +(defn persist-profile + [& {:as opts}] + (ptk/reify ::persist-profile ptk/WatchEvent - (watch [_ _ stream] - (let [mdata (meta data) - on-success (:on-success mdata identity) - on-error (:on-error mdata rx/throw)] - (->> (rp/cmd! :update-profile (dissoc data :props)) - (rx/mapcat - (fn [_] - (rx/merge - (->> stream - (rx/filter (ptk/type? ::profile-fetched)) - (rx/take 1) - (rx/tap on-success) - (rx/ignore)) - (rx/of (profile-fetched data))))) + (watch [_ state _] + (let [on-success (:on-success opts identity) + on-error (:on-error opts rx/throw) + profile (:profile state)] + + (->> (rp/cmd! :update-profile (dissoc profile :props)) + (rx/tap on-success) (rx/catch on-error)))))) +(defn update-profile + [data] + (dm/assert! + "expected valid profile data" + (check-profile! data)) + (ptk/reify ::update-profile + ptk/WatchEvent + (watch [_ state _] + (let [data (dissoc data :props) + profile (:profile state) + profile' (d/deep-merge profile data)] + + (rx/concat + (rx/of #(assoc % :profile profile')) + + (when (not= (:theme profile) (:theme profile')) + (rx/of (ptk/data-event ::ev/event + {::ev/name "activate-theme" + ::ev/origin "settings" + :theme (:theme profile')})))))))) + +;; --- Toggle Theme + +(defn toggle-theme + [] + (ptk/reify ::toggle-theme + ptk/UpdateEvent + (update [_ state] + (update-in state [:profile :theme] + (fn [current] + (if (= current "default") + "light" + "default")))) + + ptk/WatchEvent + (watch [it state _] + (let [profile (get state :profile) + origin (::ev/origin (meta it))] + (rx/of (ptk/data-event ::ev/event {:theme (:theme profile) + ::ev/name "activate-theme" + ::ev/origin origin}) + (persist-profile)))))) ;; --- Request Email Change @@ -299,6 +371,10 @@ [{:keys [email] :as data}] (dm/assert! ::us/email email) (ptk/reify ::request-email-change + ev/Event + (-data [_] + {:email email}) + ptk/WatchEvent (watch [_ _ _] (let [{:keys [on-error on-success] @@ -326,10 +402,17 @@ ;; Social registered users don't have old-password [:password-old {:optional true} [:maybe :string]]]) + (defn update-password [data] - (dm/assert! (sm/valid? schema:update-password data)) + (dm/assert! + "expected valid parameters" + (sm/check! schema:update-password data)) + (ptk/reify ::update-password + ev/Event + (-data [_] {}) + ptk/WatchEvent (watch [_ _ _] (let [{:keys [on-error on-success] @@ -384,7 +467,6 @@ (->> (rp/cmd! :update-profile-props {:props props}) (rx/map (constantly (fetch-profile)))))))) - ;; --- Update Photo (defn update-photo @@ -394,6 +476,9 @@ (di/blob? file)) (ptk/reify ::update-photo + ev/Event + (-data [_] {}) + ptk/WatchEvent (watch [_ _ _] (let [on-success di/notify-finished-loading @@ -409,7 +494,7 @@ (rx/map di/validate-file) (rx/map prepare) (rx/mapcat #(rp/cmd! :update-profile-photo %)) - (rx/do on-success) + (rx/tap on-success) (rx/map (constantly (fetch-profile))) (rx/catch on-error)))))) @@ -459,14 +544,19 @@ ;; --- EVENT: request-profile-recovery -(def schema:request-profile-recovery - [:map {:closed true} - [:email ::sm/email]]) +(def ^:private + schema:request-profile-recovery + (sm/define + [:map {:title "request-profile-recovery" :closed true} + [:email ::sm/email]])) -;; FIXME: check if we can use schema for proper filter (defn request-profile-recovery [data] - (dm/assert! (sm/valid? schema:request-profile-recovery data)) + + (dm/assert! + "expected valid parameters" + (sm/check! schema:request-profile-recovery data)) + (ptk/reify ::request-profile-recovery ptk/WatchEvent (watch [_ _ _] @@ -480,14 +570,19 @@ ;; --- EVENT: recover-profile (Password) -(def schema:recover-profile - [:map {:closed true} - [:password :string] - [:token :string]]) +(def ^:private + schema:recover-profile + (sm/define + [:map {:title "recover-profile" :closed true} + [:password :string] + [:token :string]])) (defn recover-profile [data] - (dm/assert! (sm/valid? schema:recover-profile data)) + (dm/assert! + "expected valid arguments" + (sm/check! schema:recover-profile data)) + (ptk/reify ::recover-profile ptk/WatchEvent (watch [_ _ _] diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 68e958aa1d..a45e75939a 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -8,21 +8,22 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.files.features :as ffeat] + [app.common.features :as cfeat] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] - [app.common.pages.helpers :as cph] [app.common.schema :as sm] [app.common.transit :as t] [app.common.types.shape-tree :as ctt] [app.common.types.shape.interactions :as ctsi] [app.main.data.comments :as dcm] + [app.main.data.events :as ev] [app.main.data.fonts :as df] [app.main.features :as features] [app.main.repo :as rp] [app.util.globals :as ug] [app.util.router :as rt] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) ;; --- Local State Initialization @@ -46,32 +47,39 @@ (declare zoom-to-fill) (declare zoom-to-fit) -(def schema:initialize - [:map - [:file-id ::sm/uuid] - [:share-id {:optional true} [:maybe ::sm/uuid]] - [:page-id {:optional true} ::sm/uuid]]) +(def ^:private + schema:initialize + (sm/define + [:map {:title "initialize"} + [:file-id ::sm/uuid] + [:share-id {:optional true} [:maybe ::sm/uuid]] + [:page-id {:optional true} ::sm/uuid]])) (defn initialize [{:keys [file-id share-id interactions-show?] :as params}] - (dm/assert! (sm/valid? schema:initialize params)) + (dm/assert! + "expected valid params" + (sm/check! schema:initialize params)) + (ptk/reify ::initialize ptk/UpdateEvent (update [_ state] (-> state (assoc :current-file-id file-id) (update :viewer-local - (fn [lstate] - (if (nil? lstate) - default-local-state - lstate))) + (fn [lstate] + (if (nil? lstate) + default-local-state + lstate))) (assoc-in [:viewer-local :share-id] share-id) (assoc-in [:viewer-local :interactions-show?] interactions-show?))) ptk/WatchEvent - (watch [_ _ _] + (watch [_ state _] (rx/of (fetch-bundle (d/without-nils params)) - (fetch-comment-threads params))) + ;; Only fetch threads for logged-in users + (when (some? (:profile state)) + (fetch-comment-threads params)))) ptk/EffectEvent (effect [_ _ _] @@ -92,28 +100,30 @@ ;; --- Data Fetching -(def schema:fetch-bundle - [:map - [:page-id ::sm/uuid] - [:file-id ::sm/uuid] - [:share-id {:optional true} ::sm/uuid]]) - -(def ^:private valid-fetch-bundle-params? - (sm/pred-fn schema:fetch-bundle)) +(def ^:private + schema:fetch-bundle + (sm/define + [:map {:title "fetch-bundle"} + [:page-id ::sm/uuid] + [:file-id ::sm/uuid] + [:share-id {:optional true} ::sm/uuid]])) (defn- fetch-bundle [{:keys [file-id share-id] :as params}] - (dm/assert! (valid-fetch-bundle-params? params)) + + (dm/assert! + "expected valid params" + (sm/check! schema:fetch-bundle params)) (ptk/reify ::fetch-bundle ptk/WatchEvent - (watch [_ state _] - (let [features (cond-> ffeat/enabled - (features/active-feature? state :components-v2) - (conj "components/v2") - - :always - (conj "storage/pointer-map")) + (watch [_ _ _] + (let [;; NOTE: in viewer we don't have access to the team when + ;; user is not logged-in, so we can't know which features + ;; are active from team, so in this case it is necesary + ;; report the whole set of supported features instead of + ;; the enabled ones. + features cfeat/supported-features params' (cond-> {:file-id file-id :features features} (uuid? share-id) (assoc :share-id share-id)) @@ -151,8 +161,9 @@ (rx/map (fn [data] (update bundle :file assoc :data data)))))) (rx/mapcat - (fn [{:keys [fonts] :as bundle}] + (fn [{:keys [fonts team] :as bundle}] (rx/of (df/fonts-fetched fonts) + (features/initialize (:features team)) (bundle-fetched (merge bundle params)))))))))) (declare go-to-frame) @@ -194,10 +205,10 @@ "fill" zoom-to-fill nil)) (rx/of - (cond - (some? frame-id) (go-to-frame (uuid frame-id)) - (some? index) (go-to-frame-by-index index) - :else (go-to-frame-auto))))))))) + (cond + (some? frame-id) (go-to-frame (uuid frame-id)) + (some? index) (go-to-frame-by-index index) + :else (go-to-frame-auto))))))))) (defn fetch-comment-threads [{:keys [file-id page-id share-id] :as params}] @@ -289,9 +300,9 @@ ptk/UpdateEvent (update [_ state] (let [srect (as-> (get-in state [:route :query-params :page-id]) % - (get-in state [:viewer :pages % :frames]) - (nth % (get-in state [:route :query-params :index])) - (get % :selrect)) + (get-in state [:viewer :pages % :frames]) + (nth % (get-in state [:route :query-params :index])) + (get % :selrect)) orig-size (get-in state [:viewer-local :viewport-size]) wdiff (/ (:width orig-size) (:width srect)) hdiff (/ (:height orig-size) (:height srect)) @@ -485,9 +496,11 @@ (go-to-frame frame-id nil)) ([frame-id animation] - (dm/assert! (uuid? frame-id)) - (dm/assert! (or (nil? animation) - (ctsi/animation? animation))) + (dm/assert! + "expected valid parameters" + (and (uuid? frame-id) + (or (nil? animation) + (ctsi/check-animation! animation)))) (ptk/reify ::go-to-frame ptk/UpdateEvent @@ -504,9 +517,9 @@ (some? animation) (assoc-in [:viewer-animations (:id frame)] - {:kind :go-to-frame - :orig-frame-id (:id frame) - :animation animation})))) + {:kind :go-to-frame + :orig-frame-id (:id frame) + :animation animation})))) ptk/WatchEvent (watch [_ state _] @@ -534,6 +547,11 @@ (defn go-to-section [section] (ptk/reify ::go-to-section + ev/Event + (-data [_] + {::ev/origin "viewer" + :section (name section)}) + ptk/UpdateEvent (update [_ state] (assoc state :viewer-overlays [])) @@ -548,7 +566,7 @@ ;; --- Overlays (defn- open-overlay* - [state frame position snap-to close-click-outside background-overlay animation] + [state frame position snap-to close-click-outside background-overlay animation fixed-source?] (cond-> state :always (update :viewer-overlays conj @@ -558,7 +576,8 @@ :snap-to snap-to :close-click-outside close-click-outside :background-overlay background-overlay - :animation animation}) + :animation animation + :fixed-source? fixed-source?}) (some? animation) (assoc-in [:viewer-animations (:id frame)] @@ -578,7 +597,7 @@ :animation animation}))) (defn open-overlay - [frame-id position snap-to close-click-outside background-overlay animation] + [frame-id position snap-to close-click-outside background-overlay animation fixed-source?] (dm/assert! (uuid? frame-id)) (dm/assert! (gpt/point? position)) (dm/assert! (or (nil? close-click-outside) @@ -586,7 +605,7 @@ (dm/assert! (or (nil? background-overlay) (boolean? background-overlay))) (dm/assert! (or (nil? animation) - (ctsi/animation? animation))) + (ctsi/check-animation! animation))) (ptk/reify ::open-overlay ptk/UpdateEvent (update [_ state] @@ -603,12 +622,13 @@ snap-to close-click-outside background-overlay - animation) + animation + fixed-source?) state))))) (defn toggle-overlay - [frame-id position snap-to close-click-outside background-overlay animation] + [frame-id position snap-to close-click-outside background-overlay animation fixed-source?] (dm/assert! (uuid? frame-id)) (dm/assert! (gpt/point? position)) (dm/assert! (or (nil? close-click-outside) @@ -616,7 +636,7 @@ (dm/assert! (or (nil? background-overlay) (boolean? background-overlay))) (dm/assert! (or (nil? animation) - (ctsi/animation? animation))) + (ctsi/check-animation! animation))) (ptk/reify ::toggle-overlay ptk/UpdateEvent @@ -634,7 +654,8 @@ snap-to close-click-outside background-overlay - animation) + animation + fixed-source?) (close-overlay* state (:id frame) (ctsi/invert-direction animation))))))) @@ -644,7 +665,7 @@ ([frame-id animation] (dm/assert! (uuid? frame-id)) (dm/assert! (or (nil? animation) - (ctsi/animation? animation))) + (ctsi/check-animation! animation))) (ptk/reify ::close-overlay ptk/UpdateEvent @@ -693,7 +714,7 @@ (conj id))] (-> state (assoc-in [:viewer-local :selected] - (cph/expand-region-selection objects selection))))))) + (cfh/expand-region-selection objects selection))))))) (defn select-all [] diff --git a/frontend/src/app/main/data/viewer/shortcuts.cljs b/frontend/src/app/main/data/viewer/shortcuts.cljs index 5cae65f678..f6ac67296c 100644 --- a/frontend/src/app/main/data/viewer/shortcuts.cljs +++ b/frontend/src/app/main/data/viewer/shortcuts.cljs @@ -37,17 +37,17 @@ :fn #(st/emit! dv/toggle-zoom-style)} :toggle-fullscreen {:tooltip (ds/shift "F") - :command "shift+f" + :command ["shift+f" "alt+enter"] :subsections [:zoom-viewer] :fn #(st/emit! dv/toggle-fullscreen)} - :next-frame {:tooltip ds/left-arrow - :command ["left" "up"] + :prev-frame {:tooltip ds/left-arrow + :command ["left" "up" "shift+enter" "pageup" "shift+space"] :subsections [:general-viewer] :fn #(st/emit! dv/select-prev-frame)} - :prev-frame {:tooltip ds/right-arrow - :command ["right" "down"] + :next-frame {:tooltip ds/right-arrow + :command ["right" "down" "enter" "pagedown" "space"] :subsections [:general-viewer] :fn #(st/emit! dv/select-next-frame)} diff --git a/frontend/src/app/main/data/websocket.cljs b/frontend/src/app/main/data/websocket.cljs index 7526ea8ca4..2ed31de565 100644 --- a/frontend/src/app/main/data/websocket.cljs +++ b/frontend/src/app/main/data/websocket.cljs @@ -11,8 +11,8 @@ [app.common.uri :as u] [app.config :as cf] [app.util.websocket :as ws] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (l/set-level! :error) @@ -44,16 +44,16 @@ (ptk/reify ::initialize ptk/WatchEvent (watch [_ state stream] - (l/trace :hint "event:initialize" :fn "watch") + (l/trace :hint "initialize" :fn "watch") (let [sid (:session-id state) uri (prepare-uri {:session-id sid}) ws (ws/create uri)] (vreset! ws-conn ws) - (let [stoper (rx/merge - (rx/filter (ptk/type? ::finalize) stream) - (rx/filter (ptk/type? ::initialize) stream))] + (let [stopper (rx/merge + (rx/filter (ptk/type? ::finalize) stream) + (rx/filter (ptk/type? ::initialize) stream))] (->> (rx/merge (rx/of #(assoc % :ws-conn ws)) @@ -64,7 +64,7 @@ (->> (ws/get-rcv-stream ws) (rx/filter ws/opened-event?) (rx/map (fn [_] (ptk/data-event ::opened {}))))) - (rx/take-until stoper))))))) + (rx/take-until stopper))))))) ;; --- Finalize Websocket diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index cbf0ef63bd..1dddf3f99a 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -9,22 +9,23 @@ [app.common.attrs :as attrs] [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.files.features :as ffeat] + [app.common.exceptions :as ex] + [app.common.features :as cfeat] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] [app.common.geom.align :as gal] [app.common.geom.point :as gpt] [app.common.geom.proportions :as gpp] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.logging :as log] - [app.common.pages :as cp] - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] + [app.common.geom.shapes.grid-layout :as gslg] + [app.common.schema :as sm] [app.common.text :as txt] [app.common.transit :as t] [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] [app.common.types.file :as ctf] - [app.common.types.pages-list :as ctpl] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] @@ -41,10 +42,9 @@ [app.main.data.workspace.changes :as dch] [app.main.data.workspace.collapse :as dwco] [app.main.data.workspace.drawing :as dwd] - [app.main.data.workspace.drawing.common :as dwdc] [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.fix-bool-contents :as fbc] - [app.main.data.workspace.fix-broken-shape-links :as fbs] + [app.main.data.workspace.fix-broken-shapes :as fbs] [app.main.data.workspace.fix-deleted-fonts :as fdf] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.guides :as dwgu] @@ -68,6 +68,7 @@ [app.main.data.workspace.viewport :as dwv] [app.main.data.workspace.zoom :as dwz] [app.main.features :as features] + [app.main.features.pointer-map :as fpmap] [app.main.repo :as rp] [app.main.streams :as ms] [app.main.worker :as uw] @@ -78,10 +79,10 @@ [app.util.router :as rt] [app.util.timers :as tm] [app.util.webapi :as wapi] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.spec.alpha :as s] [cuerdas.core :as str] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (def default-workspace-local {:zoom 1}) @@ -90,19 +91,12 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (declare ^:private workspace-initialized) -(declare ^:private remove-graphics) (declare ^:private libraries-fetched) ;; --- Initialize Workspace - (defn initialize-layout [lname] - ;; (dm/assert! - ;; "expected valid layout" - ;; (and (keyword? lname) - ;; (contains? layout/presets lname))) - (ptk/reify ::initialize-layout ptk/UpdateEvent (update [_ state] @@ -127,17 +121,13 @@ ptk/WatchEvent (watch [_ state _] - (let [file (:workspace-data state) - has-graphics? (-> file :media seq) - components-v2 (features/active-feature? state :components-v2)] - (rx/merge - (rx/of (fbc/fix-bool-contents) - (fdf/fix-deleted-fonts) - (fbs/fix-broken-shapes)) - - (if (and has-graphics? components-v2) - (rx/of (remove-graphics (:id file) (:name file))) - (rx/empty))))))) + (rx/of + (when (and (not (boolean (-> state :profile :props :v2-info-shown))) + (features/active-feature? state "components/v2")) + (modal/show :v2-info {})) + (fbc/fix-bool-contents) + (fdf/fix-deleted-fonts) + (fbs/fix-broken-shapes))))) (defn- workspace-data-loaded [data] @@ -147,60 +137,44 @@ (let [data (d/removem (comp t/pointer? val) data)] (assoc state :workspace-data data))))) -(defn- resolve-file-data - [file-id {:keys [pages-index] :as data}] - (letfn [(resolve-pointer [[key val :as kv]] - (if (t/pointer? val) - (->> (rp/cmd! :get-file-fragment {:file-id file-id :fragment-id @val}) - (rx/map #(get % :content)) - (rx/map #(vector key %))) - (rx/of kv))) - - (resolve-pointers [coll] - (->> (rx/from (seq coll)) - (rx/merge-map resolve-pointer) - (rx/reduce conj {})))] - - (->> (rx/zip (resolve-pointers data) - (resolve-pointers pages-index)) - (rx/take 1) - (rx/map (fn [[data pages-index]] - (assoc data :pages-index pages-index)))))) - (defn- bundle-fetched - [features [{:keys [id data] :as file} thumbnails project users comments-users]] + [{:keys [features file thumbnails project team team-users comments-users]}] (ptk/reify ::bundle-fetched ptk/UpdateEvent (update [_ state] (-> state + (assoc :users (d/index-by :id team-users)) (assoc :workspace-thumbnails thumbnails) (assoc :workspace-file (dissoc file :data)) (assoc :workspace-project project) - (assoc :current-team-id (:team-id project)) - (assoc :users (d/index-by :id users)) (assoc :current-file-comments-users (d/index-by :id comments-users)))) ptk/WatchEvent (watch [_ _ stream] - (let [team-id (:team-id project) - stoper (rx/filter (ptk/type? ::bundle-fetched) stream)] + (let [team-id (:id team) + file-id (:id file) + stopper (rx/filter (ptk/type? ::bundle-fetched) stream)] + (->> (rx/concat ;; Initialize notifications - (rx/of (dwn/initialize team-id id) + (rx/of (dwn/initialize team-id file-id) (dwsl/initialize)) ;; Load team fonts. We must ensure custom fonts are ;; fully loadad before mark workspace as initialized (rx/merge (->> stream - (rx/filter (ptk/type? :app.main.data.fonts/team-fonts-loaded)) + (rx/filter (ptk/type? ::df/team-fonts-loaded)) (rx/take 1) (rx/ignore)) (rx/of (df/load-team-fonts team-id)) + ;; FIXME: move to bundle fetch stages + ;; Load main file - (->> (resolve-file-data id data) + (->> (fpmap/resolve-file file) + (rx/map :data) (rx/mapcat (fn [{:keys [pages-index] :as data}] (->> (rx/from (seq pages-index)) (rx/mapcat @@ -214,20 +188,23 @@ (rx/map workspace-data-loaded)) ;; Load libraries - (->> (rp/cmd! :get-file-libraries {:file-id id}) + (->> (rp/cmd! :get-file-libraries {:file-id file-id}) (rx/mapcat identity) (rx/merge-map (fn [{:keys [id synced-at]}] (->> (rp/cmd! :get-file {:id id :features features}) (rx/map #(assoc % :synced-at synced-at))))) + (rx/merge-map fpmap/resolve-file) (rx/merge-map - (fn [{:keys [id data] :as file}] - (->> (resolve-file-data id data) - (rx/map (fn [data] (assoc file :data data)))))) + (fn [{:keys [id] :as file}] + (->> (rp/cmd! :get-file-object-thumbnails {:file-id id :tag "component"}) + (rx/map #(assoc file :thumbnails %))))) (rx/reduce conj []) (rx/map libraries-fetched))) - (rx/of (with-meta (workspace-initialized) {:file-id id}))) - (rx/take-until stoper)))))) + + (rx/of (with-meta (workspace-initialized) + {:file-id file-id}))) + (rx/take-until stopper)))))) (defn- libraries-fetched [libraries] @@ -248,7 +225,7 @@ (rx/concat (rx/timer 1000) (rx/of (dwl/notify-sync-file file-id)))))))) -(defn- fetch-thumbnail-blob-uri +(defn- datauri->blob-uri [uri] (->> (http/send! {:uri uri :response-type :blob @@ -256,47 +233,96 @@ (rx/map :body) (rx/map (fn [blob] (wapi/create-uri blob))))) -(defn- fetch-thumbnail-blobs +(defn- fetch-file-object-thumbnails [file-id] (->> (rp/cmd! :get-file-object-thumbnails {:file-id file-id}) (rx/mapcat (fn [thumbnails] - (->> (rx/from thumbnails) - (rx/mapcat (fn [[k v]] - ;; we only need to fetch the thumbnail if - ;; it is a data:uri, otherwise we can just - ;; use the value as is. - (if (.startsWith v "data:") - (->> (fetch-thumbnail-blob-uri v) - (rx/map (fn [uri] [k uri]))) - (rx/of [k v]))))))) + (->> (rx/from thumbnails) + (rx/mapcat (fn [[k v]] + ;; we only need to fetch the thumbnail if + ;; it is a data:uri, otherwise we can just + ;; use the value as is. + (if (str/starts-with? v "data:") + (->> (datauri->blob-uri v) + (rx/map (fn [uri] [k uri]))) + (rx/of [k v]))))))) (rx/reduce conj {}))) +(defn- fetch-bundle-stage-1 + [project-id file-id] + (ptk/reify ::fetch-bundle-stage-1 + ptk/WatchEvent + (watch [_ _ stream] + (->> (rp/cmd! :get-project {:id project-id}) + (rx/mapcat (fn [project] + (->> (rp/cmd! :get-team {:id (:team-id project)}) + (rx/mapcat (fn [team] + (let [bundle {:team team + :project project + :file-id file-id + :project-id project-id}] + (rx/of (du/set-current-team team) + (ptk/data-event ::bundle-stage-1 bundle)))))))) + (rx/take-until + (rx/filter (ptk/type? ::fetch-bundle) stream)))))) + +(defn- fetch-bundle-stage-2 + [{:keys [file-id project-id] :as bundle}] + (ptk/reify ::fetch-bundle-stage-2 + ptk/WatchEvent + (watch [_ state stream] + (let [features (features/get-team-enabled-features state) + + ;; WTF is this? + share-id (-> state :viewer-local :share-id)] + (->> (rx/zip (rp/cmd! :get-file {:id file-id :features features :project-id project-id}) + (fetch-file-object-thumbnails file-id) + (rp/cmd! :get-team-users {:file-id file-id}) + (rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id})) + (rx/take 1) + (rx/map (fn [[file thumbnails team-users comments-users]] + (let [bundle (-> bundle + (assoc :file file) + (assoc :features features) + (assoc :thumbnails thumbnails) + (assoc :team-users team-users) + (assoc :comments-users comments-users))] + (ptk/data-event ::bundle-stage-2 bundle)))) + (rx/take-until + (rx/filter (ptk/type? ::fetch-bundle) stream))))))) + +(declare go-to-component) + (defn- fetch-bundle + "Multi-stage file bundle fetch coordinator" [project-id file-id] (ptk/reify ::fetch-bundle ptk/WatchEvent (watch [_ state stream] - (let [features (cond-> ffeat/enabled - (features/active-feature? state :components-v2) - (conj "components/v2") + (->> (rx/merge + (rx/of (fetch-bundle-stage-1 project-id file-id)) - ;; We still put the feature here and not in the - ;; ffeat/enabled var because the pointers map is only - ;; supported on workspace bundle fetching mechanism. - :always - (conj "storage/pointer-map")) + (->> stream + (rx/filter (ptk/type? ::bundle-stage-1)) + (rx/observe-on :async) + (rx/map deref) + (rx/map fetch-bundle-stage-2)) - ;; WTF is this? - share-id (-> state :viewer-local :share-id) - stoper (rx/filter (ptk/type? ::fetch-bundle) stream)] - (->> (rx/zip (rp/cmd! :get-file {:id file-id :features features :project-id project-id}) - (fetch-thumbnail-blobs file-id) - (rp/cmd! :get-project {:id project-id}) - (rp/cmd! :get-team-users {:file-id file-id}) - (rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id})) - (rx/take 1) - (rx/map (partial bundle-fetched features)) - (rx/take-until stoper)))))) + (->> stream + (rx/filter (ptk/type? ::bundle-stage-2)) + (rx/observe-on :async) + (rx/map deref) + (rx/map bundle-fetched)) + + (when-let [component-id (get-in state [:route :query-params :component-id])] + (->> stream + (rx/filter (ptk/type? ::workspace-initialized)) + (rx/observe-on :async) + (rx/take 1) + (rx/map #(go-to-component (uuid/uuid component-id)))))) + + (rx/take-until + (rx/filter (ptk/type? ::fetch-bundle) stream)))))) (defn initialize-file [project-id file-id] @@ -329,20 +355,22 @@ (ptk/reify ::finalize-file ptk/UpdateEvent (update [_ state] - (dissoc state - :current-file-id - :current-project-id - :workspace-data - :workspace-editor-state - :workspace-file - :workspace-libraries - :workspace-ready? - :workspace-media-objects - :workspace-persistence - :workspace-presence - :workspace-project - :workspace-project - :workspace-undo)) + (-> state + (dissoc + :current-file-id + :current-project-id + :workspace-data + :workspace-editor-state + :workspace-file + :workspace-libraries + :workspace-media-objects + :workspace-persistence + :workspace-presence + :workspace-project + :workspace-ready? + :workspace-undo) + (update :workspace-global dissoc :read-only?) + (assoc-in [:workspace-global :options-mode] :design))) ptk/WatchEvent (watch [_ _ _] @@ -383,9 +411,10 @@ ;; we only need to proceed when page-index is properly loaded (when-let [pindex (-> state :workspace-data :pages-index)] (if (contains? pindex page-id) - (rx/of (preload-data-uris page-id) - (dwth/watch-state-changes) - (dwl/watch-component-changes)) + (let [file-id (:current-file-id state)] + (rx/of (preload-data-uris page-id) + (dwth/watch-state-changes file-id page-id) + (dwl/watch-component-changes))) (let [page-id (dm/get-in state [:workspace-data :pages 0])] (rx/of (go-to-page page-id)))))))) @@ -422,7 +451,7 @@ uris (into #{} xform (wsh/lookup-page-objects state page-id))] (->> (rx/from uris) - (rx/subs #(http/fetch-data-uri % false))))))) + (rx/subs! #(http/fetch-data-uri % false))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Workspace Page CRUD @@ -432,15 +461,16 @@ [{:keys [file-id]}] (let [id (uuid/next)] (ptk/reify ::create-page - IDeref - (-deref [_] - {:id id :file-id file-id}) + ev/Event + (-data [_] + {:id id + :file-id file-id}) ptk/WatchEvent (watch [it state _] (let [pages (get-in state [:workspace-data :pages-index]) - unames (cp/retrieve-used-names pages) - name (cp/generate-unique-name unames "Page 1") + unames (cfh/get-used-names pages) + name (cfh/generate-unique-name unames "Page 1") changes (-> (pcb/empty-changes it) (pcb/add-empty-page id name))] @@ -454,15 +484,15 @@ (watch [it state _] (let [id (uuid/next) pages (get-in state [:workspace-data :pages-index]) - unames (cp/retrieve-used-names pages) + unames (cfh/get-used-names pages) page (get-in state [:workspace-data :pages-index page-id]) - name (cp/generate-unique-name unames (:name page)) + name (cfh/generate-unique-name unames (:name page)) fdata (:workspace-data state) components-v2 (dm/get-in fdata [:options :components-v2]) objects (->> (:objects page) - (d/mapm (fn [_ val] (dissoc val :use-for-thumbnail?)))) + (d/mapm (fn [_ val] (dissoc val :use-for-thumbnail)))) main-instances-ids (set (keep #(when (ctk/main-instance? (val %)) (key %)) objects)) - ids-to-remove (set (apply concat (map #(cph/get-children-ids objects %) main-instances-ids))) + ids-to-remove (set (apply concat (map #(cfh/get-children-ids objects %) main-instances-ids))) add-component-copy (fn [objs id shape] @@ -525,11 +555,11 @@ (let [components-to-delete (->> page :objects vals - (filter #(true? (:main-instance? %))) + (filter #(true? (:main-instance %))) (map :component-id)) changes (reduce (fn [changes component-id] - (pcb/delete-component changes component-id)) + (pcb/delete-component changes component-id (:id page))) changes components-to-delete)] changes)) @@ -539,7 +569,7 @@ (ptk/reify ::delete-page ptk/WatchEvent (watch [it state _] - (let [components-v2 (features/active-feature? state :components-v2) + (let [components-v2 (features/active-feature? state "components/v2") file-id (:current-file-id state) file (wsh/get-file state file-id) pages (get-in state [:workspace-data :pages]) @@ -566,20 +596,21 @@ (defn rename-file [id name] {:pre [(uuid? id) (string? name)]} - (ptk/reify ::rename-file - IDeref - (-deref [_] - {::ev/origin "workspace" :id id :name name}) + (let [name (str/prune name 200)] + (ptk/reify ::rename-file + IDeref + (-deref [_] + {::ev/origin "workspace" :id id :name name}) - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-file :name] name)) + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-file :name] name)) - ptk/WatchEvent - (watch [_ _ _] - (let [params {:id id :name name}] - (->> (rp/cmd! :rename-file params) - (rx/ignore)))))) + ptk/WatchEvent + (watch [_ _ _] + (let [params {:id id :name name}] + (->> (rp/cmd! :rename-file params) + (rx/ignore))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Workspace State Manipulation @@ -590,7 +621,7 @@ (dm/export layout/toggle-layout-flag) (dm/export layout/remove-layout-flag) -;; --- Nudge +;; --- Profile (defn update-nudge [{:keys [big small] :as params}] @@ -630,14 +661,16 @@ (defn update-shape [id attrs] - (dm/assert! (uuid? id)) - (dm/assert! (cts/shape-attrs? attrs)) + (dm/assert! + "expected valid parameters" + (and (cts/check-shape-attrs! attrs) + (uuid? id))) + (ptk/reify ::update-shape ptk/WatchEvent (watch [_ _ _] (rx/of (dch/update-shapes [id] #(merge % attrs)))))) - (defn start-rename-shape "Start shape renaming process" [id] @@ -655,27 +688,33 @@ ptk/WatchEvent (watch [_ state _] (when-let [shape-id (dm/get-in state [:workspace-local :shape-for-rename])] - (let [shape (wsh/lookup-shape state shape-id)] + (let [shape (wsh/lookup-shape state shape-id) + name (str/trim name) + clean-name (cfh/clean-path name) + valid? (and (not (str/ends-with? name "/")) + (string? clean-name) + (not (str/blank? clean-name)))] (rx/concat ;; Remove rename state from workspace local state (rx/of #(update % :workspace-local dissoc :shape-for-rename)) ;; Rename the shape if string is not empty/blank - (when (and (string? name) (not (str/blank? name))) - (rx/of (update-shape shape-id {:name name}))) + (when valid? + (rx/of (update-shape shape-id {:name clean-name}))) ;; Update the component in case if shape is a main instance - (when (:main-instance? shape) + (when (and valid? (:main-instance shape)) (when-let [component-id (:component-id shape)] - (rx/of (dwl/rename-component component-id name))))))))))) - + (rx/of (dwl/rename-component component-id clean-name))))))))))) ;; --- Update Selected Shapes attrs - (defn update-selected-shapes [attrs] - (dm/assert! (cts/shape-attrs? attrs)) + (dm/assert! + "expected valid shape attrs" + (cts/check-shape-attrs! attrs)) + (ptk/reify ::update-selected-shapes ptk/WatchEvent (watch [_ state _] @@ -742,18 +781,22 @@ (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) - (ptk/data-event :layout/update selected-ids) + (ptk/data-event :layout/update {:ids selected-ids}) (dwu/commit-undo-transaction undo-id)))))) - ;; --- Change Shape Order (D&D Ordering) (defn relocate-shapes-changes [it objects parents parent-id page-id to-index ids groups-to-delete groups-to-unmask shapes-to-detach shapes-to-reroot shapes-to-deroot shapes-to-unconstraint] - (let [ordered-indexes (cph/order-by-indexed-shapes objects ids) - shapes (map (d/getf objects) ordered-indexes) - parent (get objects parent-id)] + (let [ordered-indexes (cfh/order-by-indexed-shapes objects ids) + shapes (map (d/getf objects) ordered-indexes) + parent (get objects parent-id) + component-main-parent (ctn/find-component-main objects parent false) + child-heads + (->> ordered-indexes + (mapcat #(ctn/get-child-heads objects %)) + (map :id))] (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) @@ -763,8 +806,19 @@ (pcb/update-shapes ordered-indexes ctl/remove-layout-item-data)) ;; Remove the hide in viewer flag - (cond-> (and (not= uuid/zero parent-id) (cph/frame-shape? parent)) - (pcb/update-shapes ordered-indexes #(cond-> % (cph/frame-shape? %) (assoc :hide-in-viewer true)))) + (cond-> (and (not= uuid/zero parent-id) (cfh/frame-shape? parent)) + (pcb/update-shapes ordered-indexes #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true)))) + + ;; Remove the swap slots if it is moving to a different component + (pcb/update-shapes child-heads + (fn [shape] + (cond-> shape + (not= component-main-parent (ctn/find-component-main objects shape false)) + (ctk/remove-swap-slot)))) + + ;; Add component-root property when moving a component outside a component + (cond-> (not (ctn/get-instance-root objects parent)) + (pcb/update-shapes child-heads #(assoc % :component-root true))) ;; Move the shapes (pcb/change-parent parent-id @@ -777,7 +831,7 @@ ;; Unmask groups whose mask have moved outside (pcb/update-shapes groups-to-unmask (fn [shape] - (assoc shape :masked-group? false))) + (assoc shape :masked-group false))) ;; Detach shapes moved out of their component (pcb/update-shapes shapes-to-detach ctk/detach-shape) @@ -785,12 +839,12 @@ ;; Make non root a component moved inside another one (pcb/update-shapes shapes-to-deroot (fn [shape] - (assoc shape :component-root? nil))) + (assoc shape :component-root nil))) ;; Make root a subcomponent moved outside its parent component (pcb/update-shapes shapes-to-reroot (fn [shape] - (assoc shape :component-root? true))) + (assoc shape :component-root true))) ;; Reset constraints depending on the new parent (pcb/update-shapes shapes-to-unconstraint @@ -818,6 +872,19 @@ (assoc :layout-item-v-sizing :fix)) parent))) + ;; Update grid layout + (cond-> (ctl/grid-layout? objects parent-id) + (pcb/update-shapes [parent-id] #(ctl/add-children-to-index % ids objects to-index))) + + (pcb/update-shapes parents + (fn [parent objects] + (cond-> parent + (ctl/grid-layout? parent) + (ctl/assign-cells objects))) + {:with-objects? true}) + + (pcb/reorder-grid-children parents) + ;; If parent locked, lock the added shapes (cond-> (:blocked parent) (pcb/update-shapes ordered-indexes #(assoc % :blocked true))) @@ -838,12 +905,12 @@ objects (wsh/lookup-page-objects state page-id) ;; Ignore any shape whose parent is also intended to be moved - ids (cph/clean-loops objects ids) + ids (cfh/clean-loops objects ids) ;; If we try to move a parent into a child we remove it - ids (filter #(not (cph/is-parent? objects parent-id %)) ids) + ids (filter #(not (cfh/is-parent? objects parent-id %)) ids) - all-parents (into #{parent-id} (map #(cph/get-parent-id objects %)) ids) + all-parents (into #{parent-id} (map #(cfh/get-parent-id objects %)) ids) parents (if ignore-parents? #{parent-id} all-parents) groups-to-delete @@ -862,7 +929,7 @@ (empty? (remove removed-id? (:shapes group)))) ;; Adds group to the remove and check its parent - (let [to-check (concat to-check [(cph/get-parent-id objects current-id)])] + (let [to-check (concat to-check [(cfh/get-parent-id objects current-id)])] (recur (first to-check) (rest to-check) (conj removed-id? current-id) @@ -881,7 +948,7 @@ ;; removed, and it must be converted to a normal group. (let [obj (get objects id) parent (get objects (:parent-id obj))] - (if (and (:masked-group? parent) + (if (and (:masked-group parent) (= id (first (:shapes parent))) (not= (:id parent) parent-id)) (conj group-ids (:id parent)) @@ -902,18 +969,19 @@ (let [shape (get objects id) parent (get objects parent-id) component-shape (ctn/get-component-shape objects shape) - component-shape-parent (ctn/get-component-shape objects parent) + component-shape-parent (ctn/get-component-shape objects parent {:allow-main? true}) + root-parent (ctn/get-instance-root objects parent) - detach? (and (ctk/in-component-copy-not-root? shape) + detach? (and (ctk/in-component-copy-not-head? shape) (not= (:id component-shape) (:id component-shape-parent))) deroot? (and (ctk/instance-root? shape) - component-shape-parent) + root-parent) reroot? (and (ctk/subinstance-head? shape) (not component-shape-parent)) ids-to-detach (when detach? - (cons id (cph/get-children-ids objects id)))] + (cons id (cfh/get-children-ids objects id)))] [(cond-> shapes-to-detach detach? (into ids-to-detach)) (cond-> shapes-to-deroot deroot? (conj id)) @@ -939,7 +1007,7 @@ (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) (dwco/expand-collapse parent-id) - (ptk/data-event :layout/update (concat all-parents ids)) + (ptk/data-event :layout/update {:ids (concat all-parents ids)}) (dwu/commit-undo-transaction undo-id)))))) (defn relocate-selected-shapes @@ -950,7 +1018,6 @@ (let [selected (wsh/lookup-selected state)] (rx/of (relocate-shapes selected parent-id to-index)))))) - (defn start-editing-selected [] (ptk/reify ::start-editing-selected @@ -971,22 +1038,21 @@ (d/ordered-set)))] (rx/of (dws/select-shapes shapes-to-select))) - (let [{:keys [id type shapes]} (get objects (first selected))] - (case type - :text - (rx/of (dwe/start-edition-mode id)) + (when (d/not-empty? selected) + (let [{:keys [id type shapes]} (get objects (first selected))] + (case type + :text + (rx/of (dwe/start-edition-mode id)) - (:group :bool :frame) - (let [shapes-ids (into (d/ordered-set) - (remove #(dm/get-in objects [% :hidden])) - shapes)] - (rx/of (dws/select-shapes shapes-ids))) + (:group :bool :frame) + (let [shapes-ids (into (d/ordered-set) shapes)] + (rx/of (dws/select-shapes shapes-ids))) - :svg-raw - nil + :svg-raw + nil - (rx/of (dwe/start-edition-mode id) - (dwdp/start-path-edit id))))))))) + (rx/of (dwe/start-edition-mode id) + (dwdp/start-path-edit id)))))))))) (defn select-parent-layer [] @@ -998,12 +1064,12 @@ shapes-to-select (->> selected (reduce - (fn [result shape-id] - (let [parent-id (dm/get-in objects [shape-id :parent-id])] - (if (and (some? parent-id) (not= parent-id uuid/zero)) - (conj result parent-id) - (conj result shape-id)))) - (d/ordered-set)))] + (fn [result shape-id] + (let [parent-id (dm/get-in objects [shape-id :parent-id])] + (if (and (some? parent-id) (not= parent-id uuid/zero)) + (conj result parent-id) + (conj result shape-id)))) + (d/ordered-set)))] (rx/of (dws/select-shapes shapes-to-select)))))) ;; --- Change Page Order (D&D Ordering) @@ -1033,7 +1099,7 @@ (let [object (get objects object-id) parent-id (:parent-id (get objects object-id)) parent (get objects parent-id)] - [(gal/align-to-rect object parent axis)])) + [(gal/align-to-parent object parent axis)])) (defn align-objects-list [objects selected axis] @@ -1050,8 +1116,7 @@ (ptk/reify ::align-objects ptk/WatchEvent (watch [_ state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) + (let [objects (wsh/lookup-page-objects state) selected (wsh/lookup-selected state) moved (if (= 1 (count selected)) (align-object-to-parent objects (first selected) axis) @@ -1060,7 +1125,7 @@ (when (can-align? selected objects) (rx/of (dwu/start-undo-transaction undo-id) (dwt/position-shapes moved) - (ptk/data-event :layout/update selected) + (ptk/data-event :layout/update {:ids selected}) (dwu/commit-undo-transaction undo-id))))))) (defn can-distribute? [selected] @@ -1087,7 +1152,7 @@ (when (can-distribute? selected) (rx/of (dwu/start-undo-transaction undo-id) (dwt/position-shapes moved) - (ptk/data-event :layout/update selected) + (ptk/data-event :layout/update {:ids selected}) (dwu/commit-undo-transaction undo-id))))))) ;; --- Shape Proportions @@ -1272,7 +1337,8 @@ (rx/of (go-to-page page-id)) (->> stream (rx/filter (ptk/type? ::initialize-page)) - (rx/take 1)) + (rx/take 1) + (rx/observe-on :async)) (select-and-zoom shape-id))) redirect-to-file @@ -1302,7 +1368,6 @@ (some->> (:main-instance-page component) (redirect-to-file file-id)))))))) - (defn go-to-component [component-id] (ptk/reify ::go-to-component @@ -1311,7 +1376,7 @@ ptk/WatchEvent (watch [_ state _] - (let [components-v2 (features/active-feature? state :components-v2)] + (let [components-v2 (features/active-feature? state "components/v2")] (if components-v2 (rx/of (go-to-main-instance nil component-id)) (let [project-id (get-in state [:workspace-project :id]) @@ -1326,7 +1391,7 @@ ptk/EffectEvent (effect [_ state _] - (let [components-v2 (features/active-feature? state :components-v2) + (let [components-v2 (features/active-feature? state "components/v2") wrapper-id (str "component-shape-id-" component-id)] (when-not components-v2 (tm/schedule-on-idle #(dom/scroll-into-view-if-needed! (dom/get-element wrapper-id)))))))) @@ -1336,15 +1401,19 @@ (ptk/reify ::show-component-in-assets ptk/WatchEvent (watch [_ state _] - (let [project-id (get-in state [:workspace-project :id]) - file-id (get-in state [:workspace-file :id]) - page-id (get state :current-page-id) - pparams {:file-id file-id :project-id project-id} - qparams {:page-id page-id :layout :assets}] - (rx/of (rt/nav :workspace pparams qparams) - (set-assets-section-open file-id :library true) - (set-assets-section-open file-id :components true) - (select-single-asset file-id component-id :components)))) + (let [project-id (get-in state [:workspace-project :id]) + file-id (get-in state [:workspace-file :id]) + page-id (get state :current-page-id) + pparams {:file-id file-id :project-id project-id} + qparams {:page-id page-id :layout :assets} + component-path (cfh/split-path (get-in state [:workspace-data :components component-id :path])) + paths (map (fn [i] (cfh/join-path (take (inc i) component-path))) (range (count component-path)))] + (rx/concat + (rx/from (map #(set-assets-group-open file-id :components % true) paths)) + (rx/of (rt/nav :workspace pparams qparams) + (set-assets-section-open file-id :library true) + (set-assets-section-open file-id :components true) + (select-single-asset file-id component-id :components))))) ptk/EffectEvent (effect [_ _ _] @@ -1399,6 +1468,7 @@ (rx/of ::dwp/force-persist (rt/nav :dashboard-fonts {:team-id team-id})))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Context Menu ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1418,7 +1488,7 @@ (watch [_ state _] (let [selected (wsh/lookup-selected state) objects (wsh/lookup-page-objects state) - all-selected (into [] (mapcat #(cph/get-children-with-self objects %)) selected) + all-selected (into [] (mapcat #(cfh/get-children-with-self objects %)) selected) head (get objects (first selected)) not-group-like? (and (= (count selected) 1) @@ -1426,17 +1496,17 @@ no-bool-shapes? (->> all-selected (some (comp #{:frame :text} :type)))] - (if (and (some? shape) (not (contains? selected (:id shape)))) - (rx/concat - (rx/of (dws/select-shape (:id shape))) - (rx/of (show-shape-context-menu params))) - (rx/of (show-context-menu - (-> params - (assoc - :kind :shape - :disable-booleans? (or no-bool-shapes? not-group-like?) - :disable-flatten? no-bool-shapes? - :selected (conj selected (:id shape))))))))))) + (if (and (some? shape) (not (contains? selected (:id shape)))) + (rx/concat + (rx/of (dws/select-shape (:id shape))) + (rx/of (show-shape-context-menu params))) + (rx/of (show-context-menu + (-> params + (assoc + :kind :shape + :disable-booleans? (or no-bool-shapes? not-group-like?) + :disable-flatten? no-bool-shapes? + :selected (conj selected (:id shape))))))))))) (defn show-page-item-context-menu [{:keys [position page] :as params}] @@ -1444,9 +1514,33 @@ (ptk/reify ::show-page-item-context-menu ptk/WatchEvent (watch [_ _ _] - (rx/of (show-context-menu - (-> params (assoc :kind :page :selected (:id page)))))))) + (rx/of (show-context-menu + (-> params (assoc :kind :page :selected (:id page)))))))) +(defn show-track-context-menu + [{:keys [grid-id type index] :as params}] + (ptk/reify ::show-track-context-menu + ptk/WatchEvent + (watch [_ _ _] + (rx/of (show-context-menu + (-> params (assoc :kind :grid-track + :grid-id grid-id + :type type + :index index))))))) + +(defn show-grid-cell-context-menu + [{:keys [grid-id] :as params}] + (ptk/reify ::show-grid-cell-context-menu + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + grid (get objects grid-id) + cells (->> (get-in state [:workspace-grid-edition grid-id :selected]) + (map #(get-in grid [:layout-grid-cells %])))] + (rx/of (show-context-menu + (-> params (assoc :kind :grid-cells + :grid grid + :cells cells)))))))) (def hide-context-menu (ptk/reify ::hide-context-menu ptk/UpdateEvent @@ -1454,6 +1548,8 @@ (assoc-in state [:workspace-local :context-menu] nil)))) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Clipboard ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1466,49 +1562,56 @@ ;; Narrow the objects map so it contains only relevant data for ;; selected and its parents - objects (cph/selected-subtree objects selected) + objects (cfh/selected-subtree objects selected) selected (->> (ctst/sort-z-index objects selected) (reverse) (into (d/ordered-set)))] (assoc data :selected selected))) - ;; Retrieve all ids of selected shapes with corresponding - ;; children; this is needed because each shape should be - ;; processed one by one because of async events (data url - ;; fetching). - (collect-object-ids [objects res id] - (let [obj (get objects id)] - (reduce (partial collect-object-ids objects) - (assoc res id obj) - (:shapes obj)))) + (fetch-image [entry] + (let [url (cf/resolve-file-media entry)] + (->> (http/send! {:method :get + :uri url + :response-type :blob}) + (rx/map :body) + (rx/mapcat wapi/read-file-as-data-url) + (rx/map #(assoc entry :data %))))) ;; Prepare the shape object. Mainly needed for image shapes ;; for retrieve the image data and convert it to the ;; data-url. - (prepare-object [objects parent-frame-id {:keys [type] :as obj}] - (let [obj (maybe-translate obj objects parent-frame-id)] - (if (= type :image) - (let [url (cf/resolve-file-media (:metadata obj))] - (->> (http/send! {:method :get - :uri url - :response-type :blob}) - (rx/map :body) - (rx/mapcat wapi/read-file-as-data-url) - (rx/map #(assoc obj ::data %)) - (rx/take 1))) + (prepare-object [objects parent-frame-id obj] + (let [obj (maybe-translate obj objects parent-frame-id) + ;; Texts can have different fills for pieces of the text + imgdata (concat + (->> (or (:position-data obj) [obj]) + (mapcat :fills) + (keep :fill-image)) + (->> (:strokes obj) + (keep :stroke-image)) + (when (cfh/image-shape? obj) + [(:metadata obj)]) + (when (:fill-image obj) + [(:fill-image obj)]))] + + (if (seq imgdata) + (->> (rx/from imgdata) + (rx/mapcat fetch-image) + (rx/reduce conj []) + (rx/map (fn [images] + (assoc obj ::images images)))) (rx/of obj)))) ;; Collects all the items together and split images into a ;; separated data structure for a more easy paste process. - (collect-data [res {:keys [id metadata] :as item}] - (let [res (update res :objects assoc id (dissoc item ::data))] - (if (= :image (:type item)) - (let [img-part {:id (:id metadata) - :name (:name item) - :file-data (::data item)}] - (update res :images conj img-part)) - res))) + (collect-data [result {:keys [id ::images] :as item}] + (cond-> result + :always + (update :objects assoc id (dissoc item ::images)) + + (some? images) + (update :images into images))) (maybe-translate [shape objects parent-frame-id] (if (= parent-frame-id uuid/zero) @@ -1516,122 +1619,158 @@ (let [frame (get objects parent-frame-id)] (gsh/translate-to-frame shape frame)))) + ;; When copying an instance that is nested inside another one, we need to + ;; advance the shape refs to one or more levels of remote mains. + (advance-copies [state selected data] + (let [file (wsh/get-local-file-full state) + libraries (wsh/get-libraries state) + page (wsh/lookup-page state) + heads (mapcat #(ctn/get-child-heads (:objects data) %) selected)] + (update data :objects + #(reduce (partial advance-copy file libraries page) + % + heads)))) + + (advance-copy [file libraries page objects shape] + (if (and (ctk/instance-head? shape) (not (ctk/main-instance? shape))) + (let [level-delta (ctn/get-nesting-level-delta (:objects page) shape uuid/zero)] + (if (pos? level-delta) + (reduce (partial advance-shape file libraries page level-delta) + objects + (cfh/get-children-with-self objects (:id shape))) + objects)) + objects)) + + (advance-shape [file libraries page level-delta objects shape] + (let [new-shape-ref (ctf/advance-shape-ref file page libraries shape level-delta {:include-deleted? true})] + (cond-> objects + (and (some? new-shape-ref) (not= new-shape-ref (:shape-ref shape))) + (assoc-in [(:id shape) :shape-ref] new-shape-ref)))) + (on-copy-error [error] - (js/console.error "Clipboard blocked:" error) + (js/console.error "clipboard blocked:" error) (rx/empty))] (ptk/reify ::copy-selected ptk/WatchEvent (watch [_ state _] - (let [objects (wsh/lookup-page-objects state) - selected (->> (wsh/lookup-selected state) - (cph/clean-loops objects)) - - parent-frame-id (cph/common-parent-frame objects selected) - pdata (reduce (partial collect-object-ids objects) {} selected) - initial {:type :copied-shapes - :file-id (:current-file-id state) - :selected selected - :objects {} - :images #{}} - selected_text (.. js/window getSelection toString)] - - (if (not-empty selected_text) + (let [text (wapi/get-current-selected-text)] + (if-not (str/empty? text) (try - (wapi/write-to-clipboard selected_text) + (wapi/write-to-clipboard text) (catch :default e (on-copy-error e))) - (->> (rx/from (seq (vals pdata))) - (rx/merge-map (partial prepare-object objects parent-frame-id)) - (rx/reduce collect-data initial) - (rx/map (partial sort-selected state)) - (rx/map t/encode-str) - (rx/map wapi/write-to-clipboard) - (rx/catch on-copy-error) - (rx/ignore)))))))) -(declare paste-shape) -(declare paste-text) -(declare paste-image) -(declare paste-svg) + (let [objects (wsh/lookup-page-objects state) + selected (->> (wsh/lookup-selected state) + (cfh/clean-loops objects)) + features (features/get-team-enabled-features state) -(def paste - (ptk/reify ::paste - ptk/WatchEvent - (watch [_ _ _] - (try - (let [clipboard-str (wapi/read-from-clipboard) - paste-transit-str - (->> clipboard-str - (rx/filter t/transit?) - (rx/map t/decode-str) - (rx/filter #(= :copied-shapes (:type %))) - (rx/map #(select-keys % [:selected :objects])) - (rx/map paste-shape)) + file-id (:current-file-id state) + frame-id (cfh/common-parent-frame objects selected) + version (dm/get-in state [:workspace-file :version]) - paste-plain-text-str - (->> clipboard-str - (rx/filter (comp not empty?)) - (rx/map paste-text)) + initial {:type :copied-shapes + :features features + :version version + :file-id file-id + :selected selected + :objects {} + :images #{}} - paste-image-str + shapes (->> (cfh/selected-with-children objects selected) + (keep (d/getf objects)))] + + (->> (rx/from shapes) + (rx/merge-map (partial prepare-object objects frame-id)) + (rx/reduce collect-data initial) + (rx/map (partial sort-selected state)) + (rx/map (partial advance-copies state selected)) + (rx/map #(t/encode-str % {:type :json-verbose})) + (rx/map wapi/write-to-clipboard) + (rx/catch on-copy-error) + (rx/ignore))))))))) + +(declare ^:private paste-transit) +(declare ^:private paste-text) +(declare ^:private paste-image) +(declare ^:private paste-svg-text) +(declare ^:private paste-shapes) + +(defn paste-from-clipboard + "Perform a `paste` operation using the Clipboard API." + [] + (letfn [(decode-entry [entry] + (try + [:transit (t/decode-str entry)] + (catch :default _cause + [:text entry]))) + + (process-entry [[type data]] + (case type + :text + (if (str/empty? data) + (rx/empty) + (rx/of (paste-text data))) + + :transit + (rx/of (paste-transit data)))) + + (on-error [cause] + (let [data (ex-data cause)] + (if (:not-implemented data) + (rx/of (msg/warn (tr "errors.clipboard-not-implemented"))) + (js/console.error "Clipboard error:" cause)) + (rx/empty)))] + + (ptk/reify ::paste-from-clipboard + ptk/WatchEvent + (watch [_ _ _] + (->> (rx/concat + (->> (wapi/read-from-clipboard) + (rx/map decode-entry) + (rx/mapcat process-entry)) (->> (wapi/read-image-from-clipboard) - (rx/map paste-image))] + (rx/map paste-image))) + (rx/take 1) + (rx/catch on-error)))))) - (->> (rx/concat paste-transit-str - paste-plain-text-str - paste-image-str) - (rx/take 1) - (rx/catch - (fn [err] - (js/console.error "Clipboard error:" err) - (rx/empty))))) - (catch :default e - (let [data (ex-data e)] - (if (:not-implemented data) - (rx/of (msg/warn (tr "errors.clipboard-not-implemented"))) - (js/console.error "ERROR" e)))))))) (defn paste-from-event + "Perform a `paste` operation from user emmited event." [event in-viewport?] (ptk/reify ::paste-from-event ptk/WatchEvent (watch [_ state _] - (try - (let [objects (wsh/lookup-page-objects state) - paste-data (wapi/read-from-paste-event event) - image-data (wapi/extract-images paste-data) - text-data (wapi/extract-text paste-data) - decoded-data (and (t/transit? text-data) - (t/decode-str text-data)) + (let [objects (wsh/lookup-page-objects state) + edit-id (dm/get-in state [:workspace-local :edition]) + is-editing? (and edit-id (= :text (get-in objects [edit-id :type])))] - edit-id (get-in state [:workspace-local :edition]) - is-editing-text? (and edit-id (= :text (get-in objects [edit-id :type])))] - - ;; Some paste events can be fired while we're editing a text - ;; we forbid that scenario so the default behaviour is executed - (when-not is-editing-text? + ;; Some paste events can be fired while we're editing a text + ;; we forbid that scenario so the default behaviour is executed + (if is-editing? + (rx/empty) + (let [pdata (wapi/read-from-paste-event event) + image-data (some-> pdata wapi/extract-images) + text-data (some-> pdata wapi/extract-text) + transit-data (ex/ignoring (some-> text-data t/decode-str))] (cond (and (string? text-data) - (str/includes? text-data "> (rx/from image-data) + (rx/map paste-image)) - (coll? decoded-data) - (->> (rx/of decoded-data) - (rx/filter #(= :copied-shapes (:type %))) - (rx/map #(paste-shape % in-viewport?))) + (coll? transit-data) + (rx/of (paste-transit (assoc transit-data :in-viewport in-viewport?))) (string? text-data) (rx/of (paste-text text-data)) :else - (rx/empty)))) - - (catch :default err - (js/console.error "Clipboard error:" err)))))) + (rx/empty)))))))) (defn selected-frame? [state] (let [selected (wsh/lookup-selected state) @@ -1660,17 +1799,25 @@ (= (:width (:selrect (first (vals paste-obj)))) (:width (:selrect frame-obj))))) +(def ^:private + schema:paste-data + (sm/define + [:map {:title "paste-data"} + [:type [:= :copied-shapes]] + [:features ::sm/set-of-strings] + [:version :int] + [:file-id ::sm/uuid] + [:selected ::sm/set-of-uuid] + [:objects + [:map-of ::sm/uuid :map]] + [:images [:set :map]] + [:position {:optional true} ::gpt/point]])) -(defn- paste-shape - [{selected :selected - paste-objects :objects ;; rename this because here comes only the clipboard shapes, - images :images ;; not the whole page tree of shapes. - :as data} - in-viewport?] - (letfn [;; Given a file-id and img (part generated by the - ;; copy-selected event), uploads the new media. - (upload-media [file-id imgpart] - (->> (http/send! {:uri (:file-data imgpart) +(defn- paste-transit + [{:keys [images] :as pdata}] + + (letfn [(upload-media [file-id imgpart] + (->> (http/send! {:uri (:data imgpart) :response-type :blob :method :get}) (rx/map :body) @@ -1680,55 +1827,95 @@ :file-id file-id :content blob :is-local true})) - (rx/mapcat #(rp/cmd! :upload-file-media-object %)) - (rx/map (fn [media] - (assoc media :prev-id (:id imgpart)))))) + (rx/mapcat (partial rp/cmd! :upload-file-media-object)) + (rx/map #(assoc % :prev-id (:id imgpart)))))] + + (ptk/reify ::paste-transit + ptk/WatchEvent + (watch [_ state _] + (let [file-id (:current-file-id state) + features (features/get-team-enabled-features state)] + + (sm/validate! schema:paste-data pdata + {:hint "invalid paste data" + :code :invalid-paste-data}) + + (cfeat/check-paste-features! features (:features pdata)) + (if (= file-id (:file-id pdata)) + (let [pdata (assoc pdata :images [])] + (rx/of (paste-shapes pdata))) + (->> (rx/from images) + (rx/merge-map (partial upload-media file-id)) + (rx/reduce conj []) + (rx/map #(assoc pdata :images %)) + (rx/map paste-shapes)))))))) + +(defn paste-shapes + [{in-viewport? :in-viewport :as pdata}] + (letfn [(translate-media [mdata media-idx attr-path] + (let [id (-> (get-in mdata attr-path) + (:id)) + mobj (get media-idx id)] + (if mobj + (if (empty? attr-path) + (-> mdata + (assoc :id (:id mobj)) + (assoc :path (:path mobj))) + (update-in mdata attr-path (fn [value] + (-> value + (assoc :id (:id mobj)) + (assoc :path (:path mobj)))))) + + mdata))) + + (add-obj? [chg] + (= (:type chg) :add-obj)) ;; Analyze the rchange and replace staled media and ;; references to the new uploaded media-objects. - (process-rchange [media-idx item] - (if (and (= (:type item) :add-obj) - (= :image (get-in item [:obj :type]))) - (update-in item [:obj :metadata] - (fn [{:keys [id] :as mdata}] - (if-let [mobj (get media-idx id)] - (assoc mdata - :id (:id mobj) - :path (:path mobj)) - mdata))) - item)) + (process-rchange [media-idx change] + (let [;; Texts can have different fills for pieces of the text + tr-fill-xf (map #(translate-media % media-idx [:fill-image])) + tr-stroke-xf (map #(translate-media % media-idx [:stroke-image]))] + (if (add-obj? change) + (update change :obj (fn [obj] + (-> obj + (update :fills #(into [] tr-fill-xf %)) + (update :strokes #(into [] tr-stroke-xf %)) + (d/update-when :metadata translate-media media-idx []) + (d/update-when :fill-image translate-media media-idx []) + (d/update-when :content + (fn [content] + (txt/xform-nodes tr-fill-xf content))) + (d/update-when :position-data + (fn [position-data] + (mapv (fn [pos-data] + (update pos-data :fills #(into [] tr-fill-xf %))) + position-data)))))) + change))) - (calculate-paste-position [state mouse-pos in-viewport?] + (calculate-paste-position [state pobjects selected position] (let [page-objects (wsh/lookup-page-objects state) - selected-objs (map #(get paste-objects %) selected) + selected-objs (map (d/getf pobjects) selected) first-selected-obj (first selected-objs) page-selected (wsh/lookup-selected state) - wrapper (gsh/selection-rect selected-objs) + wrapper (gsh/shapes->rect selected-objs) orig-pos (gpt/point (:x1 wrapper) (:y1 wrapper)) frame-id (first page-selected) frame-object (get page-objects frame-id) - base (cph/get-base-shape page-objects page-selected) - index (cph/get-position-on-parent page-objects (:id base)) - tree-root (get-tree-root-shapes paste-objects) + base (cfh/get-base-shape page-objects page-selected) + index (cfh/get-position-on-parent page-objects (:id base)) + tree-root (get-tree-root-shapes pobjects) only-one-root-shape? (and - (< 1 (count paste-objects)) - (= 1 (count tree-root))) - all-objects (merge page-objects paste-objects) - comps-nesting-loop? (not (->> (keys paste-objects) - (map #(cph/components-nesting-loop? all-objects % (:id base))) - (every? nil?)))] + (< 1 (count pobjects)) + (= 1 (count tree-root)))] (cond - comps-nesting-loop? - ;; Avoid placing a shape as a direct or indirect child of itself, - ;; or inside its main component if it's in a copy. - [uuid/zero uuid/zero (gpt/subtract mouse-pos orig-pos)] - (selected-frame? state) - (if (or (any-same-frame-from-selected? state (keys paste-objects)) + (if (or (any-same-frame-from-selected? state (keys pobjects)) (and only-one-root-shape? - (frame-same-size? paste-objects (first tree-root)))) + (frame-same-size? pobjects (first tree-root)))) ;; Paste next to selected frame, if selected is itself or of the same size as the copied (let [selected-frame-obj (get page-objects (first page-selected)) parent-id (:parent-id base) @@ -1736,7 +1923,7 @@ paste-y (:y selected-frame-obj) delta (gpt/subtract (gpt/point paste-x paste-y) orig-pos)] - [(:frame-id base) parent-id delta index]) + [parent-id delta index]) ;; Paste inside selected frame otherwise (let [selected-frame-obj (get page-objects (first page-selected)) @@ -1749,140 +1936,145 @@ margin-y (-> (- (:height origin-frame-object) (+ (:y wrapper) (:height wrapper))) (min (- (:height frame-object) (:height wrapper)))) - ;; Pasted objects mustn't exceed the selected frame x limit + ;; Pasted objects mustn't exceed the selected frame x limit paste-x (if (> (+ (:width wrapper) (:x1 wrapper)) (:width frame-object)) (+ (- (:x frame-object) (:x orig-pos)) (- (:width frame-object) (:width wrapper) margin-x)) (:x frame-object)) - ;; Pasted objects mustn't exceed the selected frame y limit + ;; Pasted objects mustn't exceed the selected frame y limit paste-y (if (> (+ (:height wrapper) (:y1 wrapper)) (:height frame-object)) (+ (- (:y frame-object) (:y orig-pos)) (- (:height frame-object) (:height wrapper) margin-y)) (:y frame-object)) delta (if (= origin-frame-id uuid/zero) - ;; When the origin isn't in a frame the result is pasted in the center. - (gpt/subtract (gsh/center-shape frame-object) (gsh/center-selrect wrapper)) - ;; When pasting from one frame to another frame the object position must be limited to container boundaries. If the pasted object doesn't fit we try to: - ;; - Align it to the limits on the x and y axis - ;; - Respect the distance of the object to the right and bottom in the original frame + ;; When the origin isn't in a frame the result is pasted in the center. + (gpt/subtract (gsh/shape->center frame-object) (grc/rect->center wrapper)) + ;; When pasting from one frame to another frame the object + ;; position must be limited to container boundaries. If + ;; the pasted object doesn't fit we try to: + ;; + ;; - Align it to the limits on the x and y axis + ;; - Respect the distance of the object to the right and bottom in the original frame (gpt/point paste-x paste-y))] - [frame-id frame-id delta (dec (count (:shapes selected-frame-obj )))])) + [frame-id delta (dec (count (:shapes selected-frame-obj)))])) (empty? page-selected) - (let [frame-id (ctst/top-nested-frame page-objects mouse-pos) - delta (gpt/subtract mouse-pos orig-pos)] - [frame-id frame-id delta]) + (let [frame-id (ctst/top-nested-frame page-objects position) + delta (gpt/subtract position orig-pos)] + [frame-id delta]) :else - (let [frame-id (:frame-id base) - parent-id (:parent-id base) + (let [parent-id (:parent-id base) delta (if in-viewport? - (gpt/subtract mouse-pos orig-pos) + (gpt/subtract position orig-pos) (gpt/subtract (gpt/point (:selrect base)) orig-pos))] - [frame-id parent-id delta index])))) + [parent-id delta index])))) ;; Change the indexes of the pasted shapes - (change-add-obj-index [paste-objects selected index change] - (let [index (or index -1) ;; if there is no current element selected, we want the first (inc index) to be 0 + (change-add-obj-index [objects selected index change] + (let [;; if there is no current element selected, we want + ;; the first (inc index) to be 0 + index (d/nilv index -1) set-index (fn [[result index] id] [(assoc result id index) (inc index)]) + ;; FIXME: optimize ??? map-ids (->> selected - (map #(get-in paste-objects [% :id])) + (map #(get-in objects [% :id])) (reduce set-index [{} (inc index)]) first)] - (if (and (= :add-obj (:type change)) + + (if (and (add-obj? change) (contains? map-ids (:old-id change))) (assoc change :index (get map-ids (:old-id change))) change))) - ;; Check if the shape is an instance whose master is defined in a - ;; library that is not linked to the current file - (foreign-instance? [shape paste-objects state] - (let [root (ctn/get-component-shape paste-objects shape {:allow-main? true}) - root-file-id (:component-file root)] - (and (some? root) - (not= root-file-id (:current-file-id state)) - (nil? (get-in state [:workspace-libraries root-file-id]))))) + (process-shape [file-id frame-id parent-id shape] + (cond-> shape + :always + (assoc :frame-id frame-id :parent-id parent-id) - ;; Proceed with the standard shape paste process. - (do-paste [it state mouse-pos media] - (let [libraries (wsh/get-libraries state) - file-id (:current-file-id state) - page (wsh/lookup-page state) - page-objects (:objects page) - media-idx (d/index-by :prev-id media) + (and (or (cfh/group-shape? shape) + (cfh/bool-shape? shape)) + (nil? (:shapes shape))) + (assoc :shapes []) - ;; Calculate position for the pasted elements - [frame-id parent-id delta index] (calculate-paste-position state mouse-pos in-viewport?) + (cfh/text-shape? shape) + (ctt/remove-external-typographies file-id)))] - process-shape - (fn [_ shape] - (let [parent (get page-objects parent-id) - component-shape (ctn/get-component-shape page-objects shape) - component-shape-parent (ctn/get-component-shape page-objects parent) - ;; if foreign instance, or a shape belonging to another component, detach the shape - detach? (or (foreign-instance? shape paste-objects state) - (and (ctk/in-component-copy-not-root? shape) - (not= (:id component-shape) - (:id component-shape-parent)))) - assign-shapes? (and (or (cph/group-shape? shape) - (cph/bool-shape? shape)) - (nil? (:shapes shape)))] - (-> shape - (assoc :frame-id frame-id :parent-id parent-id) - (cond-> assign-shapes? - (assoc :shapes [])) - (cond-> detach? - (-> - ;; this is used later, if the paste needs to create a new component from the detached shape - (assoc :saved-component-root? (:component-root? shape)) - ctk/detach-shape)) - ;; if is a text, remove references to external typographies - (cond-> (= (:type shape) :text) - (ctt/remove-external-typographies file-id))))) - - paste-objects (->> paste-objects (d/mapm process-shape)) - - all-objects (merge (:objects page) paste-objects) - - library-data (wsh/get-file state file-id) - - changes (-> (dws/prepare-duplicate-changes all-objects page selected delta it libraries library-data file-id) - (pcb/amend-changes (partial process-rchange media-idx)) - (pcb/amend-changes (partial change-add-obj-index paste-objects selected index))) - - ;; Adds a resize-parents operation so the groups are updated. We add all the new objects - new-objects-ids (->> changes :redo-changes (filter #(= (:type %) :add-obj)) (mapv :id)) - changes (pcb/resize-parents changes new-objects-ids) - - selected (->> changes - :redo-changes - (filter #(= (:type %) :add-obj)) - (filter #(selected (:old-id %))) - (map #(get-in % [:obj :id])) - (into (d/ordered-set))) - undo-id (js/Symbol)] - - (rx/of (dwu/start-undo-transaction undo-id) - (dch/commit-changes changes) - (dws/select-shapes selected) - (ptk/data-event :layout/update [frame-id]) - (dwu/commit-undo-transaction undo-id))))] - - (ptk/reify ::paste-shape + (ptk/reify ::paste-shapes ptk/WatchEvent (watch [it state _] - (let [file-id (:current-file-id state) - mouse-pos (deref ms/mouse-position)] - (if (= file-id (:file-id data)) - (do-paste it state mouse-pos []) - (->> (rx/from images) - (rx/merge-map (partial upload-media file-id)) - (rx/reduce conj []) - (rx/mapcat (partial do-paste it state mouse-pos))))))))) + (let [file-id (:current-file-id state) + page (wsh/lookup-page state) + media-idx (->> (:images pdata) + (d/index-by :prev-id)) + + selected (:selected pdata) + objects (:objects pdata) + + position (deref ms/mouse-position) + + ;; Calculate position for the pasted elements + [candidate-parent-id + delta + index] (calculate-paste-position state objects selected position) + + page-objects (:objects page) + + libraries (wsh/get-libraries state) + ldata (wsh/get-local-file state) + + full-libs (assoc-in libraries [(:id ldata) :data] ldata) + + [parent-id + frame-id] (ctn/find-valid-parent-and-frame-ids candidate-parent-id page-objects (vals objects) true full-libs) + + index (if (= candidate-parent-id parent-id) + index + 0) + + objects (update-vals objects (partial process-shape file-id frame-id parent-id)) + + all-objects (merge page-objects objects) + + + + drop-cell (when (ctl/grid-layout? all-objects parent-id) + (gslg/get-drop-cell frame-id all-objects position)) + + changes (-> (dws/prepare-duplicate-changes all-objects page selected delta it libraries ldata file-id) + (pcb/amend-changes (partial process-rchange media-idx)) + (pcb/amend-changes (partial change-add-obj-index objects selected index))) + + ;; Adds a resize-parents operation so the groups are + ;; updated. We add all the new objects + changes (->> (:redo-changes changes) + (filter add-obj?) + (map :id) + (pcb/resize-parents changes)) + + selected (into (d/ordered-set) + (comp + (filter add-obj?) + (filter #(contains? selected (:old-id %))) + (map :obj) + (map :id)) + (:redo-changes changes)) + + changes (cond-> changes + (some? drop-cell) + (pcb/update-shapes [parent-id] + #(ctl/add-children-to-cell % selected all-objects drop-cell))) + undo-id (js/Symbol)] + + (rx/of (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (dws/select-shapes selected) + (ptk/data-event :layout/update {:ids [frame-id]}) + (dwu/commit-undo-transaction undo-id))))))) (defn as-content [text] (let [paragraphs (->> (str/lines text) @@ -1902,12 +2094,12 @@ page-objects (wsh/lookup-page-objects state) frame-id (first page-selected) frame-object (get page-objects frame-id)] - (gsh/center-shape frame-object)) + (gsh/shape->center frame-object)) :else (deref ms/mouse-position))) -(defn paste-text +(defn- paste-text [text] (dm/assert! (string? text)) (ptk/reify ::paste-text @@ -1934,10 +2126,10 @@ (dwu/commit-undo-transaction undo-id)))))) ;; TODO: why not implement it in terms of upload-media-workspace? -(defn- paste-svg +(defn- paste-svg-text [text] (dm/assert! (string? text)) - (ptk/reify ::paste-svg + (ptk/reify ::paste-svg-text ptk/WatchEvent (watch [_ state _] (let [position (calculate-paste-position state) @@ -1947,14 +2139,14 @@ (defn- paste-image [image] - (ptk/reify ::paste-bin-impl + (ptk/reify ::paste-image ptk/WatchEvent (watch [_ state _] - (let [file-id (get-in state [:workspace-file :id]) + (let [file-id (dm/get-in state [:workspace-file :id]) position (calculate-paste-position state) - params {:file-id file-id - :blobs [image] - :position position}] + params {:file-id file-id + :blobs [image] + :position position}] (rx/of (dwm/upload-media-workspace params)))))) (defn toggle-distances-display [value] @@ -1991,145 +2183,10 @@ (rx/of (dch/commit-changes changes)))))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Remove graphics -;; TODO: this should be deprecated and removed together with components-v2 -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- initialize-remove-graphics - [total] - (ptk/reify ::initialize-remove-graphics - ptk/UpdateEvent - (update [_ state] - (assoc state :remove-graphics {:total total - :current nil - :error false - :completed false})))) - -(defn- update-remove-graphics - [current] - (ptk/reify ::update-remove-graphics - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:remove-graphics :current] current)))) - -(defn- error-in-remove-graphics - [] - (ptk/reify ::error-in-remove-graphics - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:remove-graphics :error] true)))) - -(defn clear-remove-graphics - [] - (ptk/reify ::clear-remove-graphics - ptk/UpdateEvent - (update [_ state] - (dissoc state :remove-graphics)))) - -(defn- complete-remove-graphics - [] - (ptk/reify ::complete-remove-graphics - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:remove-graphics :completed] true)) - - ptk/WatchEvent - (watch [_ state _] - (when-not (get-in state [:remove-graphics :error]) - (rx/of (modal/hide)))))) - -(defn- remove-graphic - [it file-data page [index [media-obj pos]]] - (let [process-shapes - (fn [[shape children]] - (let [page' (reduce #(ctst/add-shape (:id %2) %2 %1 uuid/zero (:parent-id %2) nil false) - page - (cons shape children)) - - shape' (ctn/get-shape page' (:id shape)) - - path (cph/merge-path-item (tr "workspace.assets.graphics") (:path media-obj)) - - [component-shape component-shapes updated-shapes] - (ctn/make-component-shape shape' (:objects page') (:id file-data) true) - - changes (-> (pcb/empty-changes it) - (pcb/set-save-undo? false) - (pcb/with-page page') - (pcb/with-objects (:objects page')) - (pcb/with-library-data file-data) - (pcb/delete-media (:id media-obj)) - (pcb/add-objects (cons shape children)) - (pcb/add-component (:id component-shape) - path - (:name media-obj) - component-shapes - updated-shapes - (:id shape) - (:id page)))] - - (dch/commit-changes changes))) - - shapes (if (= (:mtype media-obj) "image/svg+xml") - (->> (dwm/load-and-parse-svg media-obj) - (rx/mapcat (partial dwm/create-shapes-svg (:id file-data) (:objects page) pos))) - (dwm/create-shapes-img pos media-obj))] - - (->> (rx/concat - (rx/of (update-remove-graphics index)) - (rx/map process-shapes shapes)) - (rx/catch #(do - (log/error :msg (str "Error removing " (:name media-obj)) - :hint (ex-message %) - :error %) - (rx/of (error-in-remove-graphics))))))) - -(defn- remove-graphics - [file-id file-name] - (ptk/reify ::remove-graphics - ptk/WatchEvent - (watch [it state stream] - (let [file-data (wsh/get-file state file-id) - - grid-gap 50 - - [file-data' page-id start-pos] - (ctf/get-or-add-library-page file-data grid-gap) - - new-page? (nil? (ctpl/get-page file-data page-id)) - page (ctpl/get-page file-data' page-id) - media (vals (:media file-data')) - - media-points - (map #(assoc % :points (gsh/rect->points {:x 0 - :y 0 - :width (:width %) - :height (:height %)})) - media) - - shape-grid - (ctst/generate-shape-grid media-points start-pos grid-gap) - - stoper (rx/filter (ptk/type? ::finalize-file) stream)] - - (rx/concat - (rx/of (modal/show {:type :remove-graphics-dialog :file-name file-name}) - (initialize-remove-graphics (count media))) - (when new-page? - (rx/of (dch/commit-changes (-> (pcb/empty-changes it) - (pcb/set-save-undo? false) - (pcb/add-page (:id page) page))))) - (->> (rx/mapcat (partial remove-graphic it file-data' page) - (rx/from (d/enumerate (d/zip media shape-grid)))) - (rx/take-until stoper)) - (rx/of (complete-remove-graphics))))))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Read only ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - (defn set-workspace-read-only [read-only?] (ptk/reify ::set-workspace-read-only @@ -2141,12 +2198,10 @@ (watch [_ _ _] (if read-only? (rx/of :interrupt - (dwdc/clear-drawing) (remove-layout-flag :colorpalette) (remove-layout-flag :textpalette)) (rx/empty))))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Measurements ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2172,12 +2227,10 @@ (update [_ state] (assoc-in state [:workspace-global :margins-selected] margins-selected)))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Orphan Shapes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - (defn fix-orphan-shapes [] (ptk/reify ::fix-orphan-shapes @@ -2186,20 +2239,6 @@ (let [orphans (set (into [] (keys (wsh/find-orphan-shapes state))))] (rx/of (relocate-shapes orphans uuid/zero 0 true)))))) - - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Inspect -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - - -(defn set-inspect-expanded - [expanded?] - (ptk/reify ::set-inspect-expanded - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-local :inspect-expanded] expanded?)))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Sitemap ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2232,33 +2271,32 @@ (ptk/reify ::update-component-annotation ptk/WatchEvent (watch [it state _] + (let [data (get state :workspace-data) + update-fn + (fn [component] + ;; NOTE: we need to ensure the component exists, + ;; because there are small possibilities of race + ;; conditions with component deletion. + (when component + (if (nil? annotation) + (dissoc component :annotation) + (assoc component :annotation annotation)))) - (let [data (get state :workspace-data) - - update-fn - (fn [component] - ;; NOTE: we need to ensure the component exists, - ;; because there are small possibilities of race - ;; conditions with component deletion. - (when component - (if (nil? annotation) - (dissoc component :annotation) - (assoc component :annotation annotation)))) - - changes (-> (pcb/empty-changes it) - (pcb/with-library-data data) - (pcb/update-component id update-fn))] - - (rx/of (dch/commit-changes changes)))))) - + changes (-> (pcb/empty-changes it) + (pcb/with-library-data data) + (pcb/update-component id update-fn))] + (rx/concat + (rx/of (dch/commit-changes changes)) + (when (nil? annotation) + (rx/of (ptk/data-event ::ev/event {::ev/name "delete-component-annotation"})))))))) (defn set-annotations-expanded - [expanded?] + [expanded] (ptk/reify ::set-annotations-expanded ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-annotations :expanded?] expanded?)))) + (assoc-in state [:workspace-annotations :expanded] expanded)))) (defn set-annotations-id-for-create [id] @@ -2267,9 +2305,13 @@ (update [_ state] (if id (-> (assoc-in state [:workspace-annotations :id-for-create] id) - (assoc-in [:workspace-annotations :expanded?] true)) - (d/dissoc-in state [:workspace-annotations :id-for-create]))))) + (assoc-in [:workspace-annotations :expanded] true)) + (d/dissoc-in state [:workspace-annotations :id-for-create]))) + ptk/WatchEvent + (watch [_ _ _] + (when (some? id) + (rx/of (ptk/data-event ::ev/event {::ev/name "create-component-annotation"})))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Preview blend modes @@ -2289,6 +2331,41 @@ (update [_ state] (reduce #(update %1 :workspace-preview-blend dissoc %2) state ids)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Components +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn find-components-norefs + [] + (ptk/reify ::find-components-norefs + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + copies (->> objects + vals + (filter #(and (ctk/instance-head? %) (not (ctk/main-instance? %))))) + + copies-no-ref (filter #(not (:shape-ref %)) copies) + find-childs-no-ref (fn [acc-map item] + (let [id (:id item) + childs (->> (cfh/get-children objects id) + (filter #(not (:shape-ref %))))] + (if (seq childs) + (assoc acc-map id childs) + acc-map))) + childs-no-ref (reduce + find-childs-no-ref + {} + copies)] + (js/console.log "Copies no ref" (count copies-no-ref) (clj->js copies-no-ref)) + (js/console.log "Childs no ref" (count childs-no-ref) (clj->js childs-no-ref)))))) + +(defn set-shape-ref + [id shape-ref] + (ptk/reify ::set-shape-ref + ptk/WatchEvent + (watch [_ _ _] + (rx/of (update-shape (uuid/uuid id) {:shape-ref (uuid/uuid shape-ref)}))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Exports @@ -2296,6 +2373,7 @@ ;; Transform +(dm/export dwt/trigger-bounding-box-cloaking) (dm/export dwt/start-resize) (dm/export dwt/update-dimensions) (dm/export dwt/change-orientation) diff --git a/frontend/src/app/main/data/workspace/bool.cljs b/frontend/src/app/main/data/workspace/bool.cljs index 2b78712613..ac9e06dee4 100644 --- a/frontend/src/app/main/data/workspace/bool.cljs +++ b/frontend/src/app/main/data/workspace/bool.cljs @@ -7,18 +7,20 @@ (ns app.main.data.workspace.bool (:require [app.common.data :as d] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cph] [app.common.geom.shapes :as gsh] - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] - [app.common.path.shapes-to-path :as stp] + [app.common.svg.path.shapes-to-path :as stp] + [app.common.types.container :as ctn] + [app.common.types.shape :as cts] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (defn selected-shapes-idx [state] @@ -45,6 +47,7 @@ :name name :shapes (->> shapes (mapv :id))} (merge head-data) + (cts/setup-shape) (gsh/update-bool-selrect shapes objects))] [bool-shape (cph/get-position-on-parent objects (:id head))])) @@ -90,7 +93,8 @@ ordered-indexes (cph/order-by-indexed-shapes objects ids) shapes (->> ordered-indexes (map (d/getf objects)) - (remove cph/frame-shape?))] + (remove cph/frame-shape?) + (remove #(ctn/has-any-copy-parent? objects %)))] (when-not (empty? shapes) (let [[boolean-data index] (create-bool-data bool-type name (reverse shapes) objects) @@ -112,7 +116,8 @@ (let [objects (wsh/lookup-page-objects state) change-to-bool (fn [shape] (group->bool shape bool-type objects))] - (rx/of (dch/update-shapes [shape-id] change-to-bool {:reg-objects? true})))))) + (when-not (ctn/has-any-copy-parent? objects (get objects shape-id)) + (rx/of (dch/update-shapes [shape-id] change-to-bool {:reg-objects? true}))))))) (defn bool-to-group [shape-id] @@ -122,14 +127,17 @@ (let [objects (wsh/lookup-page-objects state) change-to-group (fn [shape] (bool->group shape objects))] - (rx/of (dch/update-shapes [shape-id] change-to-group {:reg-objects? true})))))) + (when-not (ctn/has-any-copy-parent? objects (get objects shape-id)) + (rx/of (dch/update-shapes [shape-id] change-to-group {:reg-objects? true}))))))) (defn change-bool-type [shape-id bool-type] (ptk/reify ::change-bool-type ptk/WatchEvent - (watch [_ _ _] - (let [change-type + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + change-type (fn [shape] (assoc shape :bool-type bool-type))] - (rx/of (dch/update-shapes [shape-id] change-type {:reg-objects? true})))))) + (when-not (ctn/has-any-copy-parent? objects (get objects shape-id)) + (rx/of (dch/update-shapes [shape-id] change-type {:reg-objects? true}))))))) diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index ceaef9e4bc..b2d5950865 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -9,20 +9,20 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.files.changes :as cpc] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cph] [app.common.logging :as log] - [app.common.pages :as cp] - [app.common.pages.changes :as cpc] - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] [app.common.schema :as sm] [app.common.types.shape-tree :as ctst] + [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.store :as st] [app.main.worker :as uw] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) ;; Change this to :info :debug or :trace to debug this module (log/set-level! :warn) @@ -52,9 +52,13 @@ (defn update-shapes ([ids update-fn] (update-shapes ids update-fn nil)) - ([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-remote? ignore-touched undo-group] - :or {reg-objects? false save-undo? true stack-undo? false ignore-remote? false ignore-touched false}}] - (dm/assert! (sm/coll-of-uuid? ids)) + ([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-remote? ignore-touched undo-group with-objects?] + :or {reg-objects? false save-undo? true stack-undo? false ignore-remote? false ignore-touched false with-objects? false}}] + + (dm/assert! + "expected a valid coll of uuid's" + (sm/check-coll-of-uuid! ids)) + (dm/assert! (fn? update-fn)) (ptk/reify ::update-shapes @@ -67,14 +71,15 @@ update-layout-ids (->> ids (map (d/getf objects)) - (filter #(some update-layout-attr? (pcb/changed-attrs % update-fn {:attrs attrs}))) + (filter #(some update-layout-attr? (pcb/changed-attrs % objects update-fn {:attrs attrs :with-objects? with-objects?}))) (map :id)) changes (reduce (fn [changes id] (let [opts {:attrs attrs :ignore-geometry? (get ignore-tree id) - :ignore-touched ignore-touched}] + :ignore-touched ignore-touched + :with-objects? with-objects?}] (pcb/update-shapes changes [id] update-fn (d/without-nils opts)))) (-> (pcb/empty-changes it page-id) (pcb/set-save-undo? save-undo?) @@ -83,6 +88,9 @@ (cond-> undo-group (pcb/set-undo-group undo-group))) ids) + grid-ids (->> ids (filter (partial ctl/grid-layout? objects))) + changes (pcb/update-shapes changes grid-ids ctl/assign-cell-positions {:with-objects? true}) + changes (pcb/reorder-grid-children changes ids) changes (add-undo-group changes state)] (rx/concat (if (seq (:redo-changes changes)) @@ -93,7 +101,7 @@ ;; Update layouts for properties marked (if (d/not-empty? update-layout-ids) - (rx/of (ptk/data-event :layout/update update-layout-ids)) + (rx/of (ptk/data-event :layout/update {:ids update-layout-ids})) (rx/empty)))))))) (defn send-update-indices @@ -177,9 +185,11 @@ [{:keys [redo-changes undo-changes origin save-undo? file-id undo-group tags stack-undo?] :or {save-undo? true stack-undo? false tags #{} undo-group (uuid/next)}}] - (let [error (volatile! nil) - page-id (:current-page-id @st/state) - frames (changed-frames redo-changes (wsh/lookup-page-objects @st/state))] + (let [error (volatile! nil) + page-id (:current-page-id @st/state) + frames (changed-frames redo-changes (wsh/lookup-page-objects @st/state)) + undo-changes (vec undo-changes) + redo-changes (vec redo-changes)] (ptk/reify ::commit-changes cljs.core/IDeref (-deref [_] @@ -210,12 +220,12 @@ (try (dm/assert! "expect valid vector of changes" - (and (cpc/changes? redo-changes) - (cpc/changes? undo-changes))) + (and (cpc/check-changes! redo-changes) + (cpc/check-changes! undo-changes))) (update-in state path (fn [file] (-> file - (cp/process-changes redo-changes false) + (cpc/process-changes redo-changes false) (ctst/update-object-indices page-id)))) (catch :default err diff --git a/frontend/src/app/main/data/workspace/collapse.cljs b/frontend/src/app/main/data/workspace/collapse.cljs index 1547e55f08..f805c238c8 100644 --- a/frontend/src/app/main/data/workspace/collapse.cljs +++ b/frontend/src/app/main/data/workspace/collapse.cljs @@ -6,9 +6,9 @@ (ns app.main.data.workspace.collapse (:require - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cfh] [app.common.uuid :as uuid] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) ;; --- Shape attrs (Layers Sidebar) @@ -19,12 +19,12 @@ (update [_ state] (let [expand-fn (fn [expanded] (merge expanded - (->> ids - (map #(cph/get-parent-ids objects %)) - flatten - (remove #(= % uuid/zero)) - (map (fn [id] {id true})) - (into {}))))] + (->> ids + (map #(cfh/get-parent-ids objects %)) + flatten + (remove #(= % uuid/zero)) + (map (fn [id] {id true})) + (into {}))))] (update-in state [:workspace-local :expanded] expand-fn))))) diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 3fdb48ac90..3a8bf6e13e 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -6,12 +6,12 @@ (ns app.main.data.workspace.colors (:require - [app.common.colors :as colors] + [app.common.colors :as cc] [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cfh] [app.common.schema :as sm] - [app.common.types.component :as ctk] + [app.common.text :as txt] [app.main.broadcast :as mbc] [app.main.data.events :as ev] [app.main.data.modal :as md] @@ -23,8 +23,8 @@ [app.main.data.workspace.undo :as dwu] [app.util.color :as uc] [app.util.storage :refer [storage]] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) ;; A set of keys that are used for shared state identifiers (def ^:const colorpicker-selected-broadcast-key ::colorpicker-selected) @@ -105,6 +105,9 @@ (contains? color :opacity) (assoc :fill-opacity (:opacity color)) + (contains? color :image) + (assoc :fill-image (:image color)) + :always (d/without-nils)) @@ -223,9 +226,15 @@ (assoc :stroke-color-gradient (:gradient attrs)) (contains? attrs :opacity) - (assoc :stroke-opacity (:opacity attrs))) + (assoc :stroke-opacity (:opacity attrs)) - attrs (merge attrs color-attrs)] + (contains? attrs :image) + (assoc :stroke-image (:image attrs))) + + attrs (-> + (merge attrs color-attrs) + (dissoc :image) + (dissoc :gradient))] (rx/of (dch/update-shapes ids @@ -239,7 +248,7 @@ (assoc :stroke-style :solid) (not (contains? new-attrs :stroke-alignment)) - (assoc :stroke-alignment :center) + (assoc :stroke-alignment :inner) :always (d/without-nils))] @@ -269,7 +278,10 @@ (defn add-shadow [ids shadow] - (dm/assert! (sm/coll-of-uuid? ids)) + (dm/assert! + "expected a valid coll of uuid's" + (sm/check-coll-of-uuid! ids)) + (ptk/reify ::add-shadow ptk/WatchEvent (watch [_ _ _] @@ -320,9 +332,9 @@ (ptk/reify ::reorder-strokes ptk/WatchEvent (watch [_ _ _] - (rx/of (dch/update-shapes - ids - #(swap-attrs % :strokes index new-index)))))) + (rx/of (dch/update-shapes + ids + #(swap-attrs % :strokes index new-index)))))) (defn picker-for-selected-shape [] @@ -341,7 +353,7 @@ ;; Stream that updates the stroke/width and stops if `esc` pressed (->> sub (rx/take-until stop?) - (rx/flat-map update-events)) + (rx/merge-map update-events)) ;; Hide the modal if the stop event is emitted (->> stop? @@ -355,7 +367,7 @@ (assoc-in [:workspace-global :picking-color?] true) (assoc ::md/modal {:id (random-uuid) :type :colorpicker - :props {:data {:color colors/black + :props {:data {:color cc/black :opacity 1} :disable-opacity false :disable-gradient false @@ -406,7 +418,7 @@ (watch [_ state _] (let [objects (wsh/lookup-page-objects state) selected (->> (wsh/lookup-selected state) - (cph/clean-loops objects)) + (cfh/clean-loops objects)) ids (loop [pending (seq selected) @@ -414,11 +426,7 @@ (if (empty? pending) result (let [cur (first pending) - ;; We treat frames that aren't components and with no fill the same as groups - group? (or (cph/group-shape? objects cur) - (and (cph/frame-shape? objects cur) - (empty? (dm/get-in objects [cur :fills])) - (not (ctk/instance-head? (get objects cur))))) + group? (cfh/group-shape? objects cur) pending (if group? @@ -431,6 +439,23 @@ (rx/of (change-stroke ids (merge uc/empty-color color) 0)) (rx/of (change-fill ids (merge uc/empty-color color) 0))))))) +(declare activate-colorpicker-color) +(declare activate-colorpicker-gradient) +(declare activate-colorpicker-image) +(declare update-colorpicker) + +(defn apply-color-from-colorpicker + [color] + (ptk/reify ::apply-color-from-colorpicker + ptk/WatchEvent + (watch [_ _ _] + (rx/of + (cond + (:image color) (activate-colorpicker-image) + (:color color) (activate-colorpicker-color) + (= :linear (get-in color [:gradient :type])) (activate-colorpicker-gradient :linear-gradient) + (= :radial (get-in color [:gradient :type])) (activate-colorpicker-gradient :radial-gradient)))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; COLORPICKER STATE MANAGEMENT @@ -438,9 +463,9 @@ (defn split-color-components [{:keys [color opacity] :as data}] - (let [value (if (uc/hex? color) color colors/black) - [r g b] (uc/hex->rgb value) - [h s v] (uc/hex->hsv value)] + (let [value (if (cc/valid-hex-color? color) color cc/black) + [r g b] (cc/hex->rgb value) + [h s v] (cc/hex->hsv value)] (merge data {:hex (or value "000000") :alpha (or opacity 1) @@ -455,7 +480,11 @@ (defn clear-color-components [data] - (dissoc data :hex :alpha :r :g :b :h :s :v)) + (dissoc data :hex :alpha :r :g :b :h :s :v :image)) + +(defn clear-image-components + [data] + (dissoc data :hex :alpha :r :g :b :h :s :v :color)) (defn- create-gradient [type] @@ -467,8 +496,14 @@ (defn get-color-from-colorpicker-state [{:keys [type current-color stops gradient] :as state}] - (if (= type :color) + (cond + (= type :color) (clear-color-components current-color) + + (= type :image) + (clear-image-components current-color) + + :else {:gradient (-> gradient (assoc :type (case type :linear-gradient :linear @@ -487,13 +522,13 @@ (on-change color))))) (defn initialize-colorpicker - [on-change] + [on-change tab] (ptk/reify ::initialize-colorpicker ptk/WatchEvent (watch [_ _ stream] - (let [stoper (rx/merge - (rx/filter (ptk/type? ::finalize-colorpicker) stream) - (rx/filter (ptk/type? ::initialize-colorpicker) stream))] + (let [stopper (rx/merge + (rx/filter (ptk/type? ::finalize-colorpicker) stream) + (rx/filter (ptk/type? ::initialize-colorpicker) stream))] (->> (rx/merge (->> stream @@ -502,7 +537,14 @@ (rx/filter (ptk/type? ::update-colorpicker-color) stream) (rx/filter (ptk/type? ::activate-colorpicker-gradient) stream)) (rx/map (constantly (colorpicker-onchange-runner on-change))) - (rx/take-until stoper)))))) + (rx/take-until stopper)))) + + ptk/UpdateEvent + (update [_ state] + (update state :colorpicker + (fn [state] + (-> state + (assoc :type tab))))))) (defn finalize-colorpicker [] @@ -522,13 +564,8 @@ (let [current-color (:current-color state)] (if (some? gradient) (let [stop (or (:editing-stop state) 0) - stops (mapv split-color-components (:stops gradient)) - type (case (:type gradient) - :linear :linear-gradient - :radial :radial-gradient - (:type state))] + stops (mapv split-color-components (:stops gradient))] (-> state - (assoc :type type) (assoc :current-color (nth stops stop)) (assoc :stops stops) (assoc :gradient (-> gradient @@ -537,7 +574,6 @@ (assoc :editing-stop stop))) (-> state - (assoc :type :color) (cond-> (or (nil? current-color) (not= (:color data) (:color current-color)) (not= (:opacity data) (:opacity current-color))) @@ -553,9 +589,11 @@ (update [_ state] (update state :colorpicker (fn [state] - (let [state (-> state + (let [type (:type state) + state (-> state (update :current-color merge changes) (update :current-color materialize-color-components) + (update :current-color #(if (not= type :image) (dissoc % :image) %)) ;; current color can be a library one I'm changing via colorpicker (d/dissoc-in [:current-color :id]) (d/dissoc-in [:current-color :file-id]))] @@ -564,7 +602,6 @@ (merge data) (materialize-color-components)))) (-> state - (assoc :type :color) (dissoc :gradient :stops :editing-stop))))))) ptk/WatchEvent (watch [_ state _] @@ -592,6 +629,17 @@ :editing-stop stop) state)))))) +(defn activate-colorpicker-color + [] + (ptk/reify ::activate-colorpicker-color + ptk/UpdateEvent + (update [_ state] + (update state :colorpicker + (fn [state] + (-> state + (assoc :type :color) + (dissoc :editing-stop :stops :gradient))))))) + (defn activate-colorpicker-gradient [type] (ptk/reify ::activate-colorpicker-gradient @@ -599,23 +647,32 @@ (update [_ state] (update state :colorpicker (fn [state] - (if (= type (:type state)) - (do - (-> state - (assoc :type :color) - (dissoc :editing-stop :stops :gradient))) - (let [gradient (create-gradient type) - color (:current-color state)] - (-> state - (assoc :type type) - (assoc :gradient gradient) - (cond-> (not (:stops state)) - (assoc :editing-stop 0 - :stops [(assoc color :offset 0) - (-> color - (assoc :alpha 0) - (assoc :offset 1) - (materialize-color-components))])))))))))) + (let [gradient (create-gradient type) + color (:current-color state)] + (-> state + (assoc :type type) + (assoc :gradient gradient) + (d/dissoc-in [:current-color :image]) + (cond-> (not (:stops state)) + (assoc :editing-stop 0 + :stops [(-> color + (assoc :offset 0) + (materialize-color-components)) + (-> color + (assoc :alpha 0) + (assoc :offset 1) + (materialize-color-components))]))))))))) + +(defn activate-colorpicker-image + [] + (ptk/reify ::activate-colorpicker-image + ptk/UpdateEvent + (update [_ state] + (update state :colorpicker + (fn [state] + (-> state + (assoc :type :image) + (dissoc :editing-stop :stops :gradient))))))) (defn select-color [position add-color] @@ -625,15 +682,15 @@ (let [selected (wsh/lookup-selected state) shapes (wsh/lookup-shapes state selected) shape (first shapes) - fills (if (cph/text-shape? shape) + fills (if (cfh/text-shape? shape) (:fills (dwt/current-text-values - {:editor-state (dm/get-in state [:workspace-editor-state (:id shape)]) - :shape shape - :attrs (conj dwt/text-fill-attrs :fills)})) + {:editor-state (dm/get-in state [:workspace-editor-state (:id shape)]) + :shape shape + :attrs (conj txt/text-fill-attrs :fills)})) (:fills shape)) fill (first fills) single? (and (= 1 (count selected)) - (= 1 (count fills))) + (= 1 (count fills))) data (if single? (d/without-nils {:color (:fill-color fill) :opacity (:fill-opacity fill) @@ -641,13 +698,13 @@ {:color "#406280" :opacity 1})] (rx/of (md/show :colorpicker - {:x (:x position) - :y (:y position) - :on-accept add-color - :data data - :position :right}) - (ptk/event ::ev/event {::ev/name "add-asset-to-library" - :asset-type "color"})))))) + {:x (:x position) + :y (:y position) + :on-accept add-color + :data data + :position :right}) + (ptk/event ::ev/event {::ev/name "add-asset-to-library" + :asset-type "color"})))))) (defn get-active-color-tab [] diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs index c7d9eb118f..246034878e 100644 --- a/frontend/src/app/main/data/workspace/comments.cljs +++ b/frontend/src/app/main/data/workspace/comments.cljs @@ -7,22 +7,24 @@ (ns app.main.data.workspace.comments (:require [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.pages.changes-builder :as pcb] [app.common.schema :as sm] [app.common.types.shape-tree :as ctst] [app.main.data.comments :as dcm] - [app.main.data.workspace.changes :as dwc] + [app.main.data.events :as ev] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwco] [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.viewport :as dwv] [app.main.repo :as rp] [app.main.streams :as ms] + [app.util.mouse :as mse] [app.util.router :as rt] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (declare handle-interrupt) (declare handle-comment-layer-click) @@ -33,21 +35,22 @@ (ptk/reify ::initialize-comments ptk/WatchEvent (watch [_ _ stream] - (let [stoper (rx/filter #(= ::finalize %) stream)] + (let [stopper (rx/filter #(= ::finalize %) stream)] (rx/merge (rx/of (dcm/retrieve-comment-threads file-id)) (->> stream - (rx/filter ms/mouse-click?) + (rx/filter mse/mouse-event?) + (rx/filter mse/mouse-click-event?) (rx/switch-map #(rx/take 1 ms/mouse-position)) (rx/with-latest-from ms/keyboard-space) - (rx/filter (fn [[_ space]] (not space)) ) + (rx/filter (fn [[_ space]] (not space))) (rx/map first) (rx/map handle-comment-layer-click) - (rx/take-until stoper)) + (rx/take-until stopper)) (->> stream (rx/filter dwco/interrupt?) (rx/map handle-interrupt) - (rx/take-until stoper))))))) + (rx/take-until stopper))))))) (defn- handle-interrupt [] @@ -81,7 +84,10 @@ (defn center-to-comment-thread [{:keys [position] :as thread}] - (dm/assert! (dcm/comment-thread? thread)) + (dm/assert! + "expected valid comment thread" + (dcm/check-comment-thread! thread)) + (ptk/reify ::center-to-comment-thread ptk/UpdateEvent (update [_ state] @@ -97,7 +103,9 @@ (defn navigate [thread] - (dm/assert! (dcm/comment-thread? thread)) + (dm/assert! + "expected valid comment thread" + (dcm/check-comment-thread! thread)) (ptk/reify ::open-comment-thread ptk/WatchEvent (watch [_ _ stream] @@ -111,43 +119,49 @@ (rx/take 1) (rx/mapcat #(rx/of (center-to-comment-thread thread) (dwd/select-for-drawing :comments) - (dcm/open-thread thread))))))))) + (with-meta (dcm/open-thread thread) + {::ev/origin "workspace"}))))))))) (defn update-comment-thread-position ([thread [new-x new-y]] (update-comment-thread-position thread [new-x new-y] nil)) ([thread [new-x new-y] frame-id] - (dm/assert! (dcm/comment-thread? thread)) - (ptk/reify ::update-comment-thread-position - ptk/WatchEvent - (watch [it state _] - (let [thread-id (:id thread) - page (wsh/lookup-page state) - page-id (:id page) - objects (wsh/lookup-page-objects state page-id) - new-frame-id (if (nil? frame-id) - (ctst/frame-id-by-position objects (gpt/point new-x new-y)) - (:frame-id thread)) - thread (assoc thread - :position (gpt/point new-x new-y) - :frame-id new-frame-id) + (dm/assert! + "expected valid comment thread" + (dcm/check-comment-thread! thread)) + (ptk/reify ::update-comment-thread-position + ptk/WatchEvent + (watch [it state _] + (let [thread-id (:id thread) + page (wsh/lookup-page state) + page-id (:id page) + objects (wsh/lookup-page-objects state page-id) + new-frame-id (if (nil? frame-id) + (ctst/get-frame-id-by-position objects (gpt/point new-x new-y)) + (:frame-id thread)) + thread (assoc thread + :position (gpt/point new-x new-y) + :frame-id new-frame-id) - changes - (-> (pcb/empty-changes it) - (pcb/with-page page) - (pcb/update-page-option :comment-threads-position assoc thread-id (select-keys thread [:position :frame-id])))] + changes + (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/update-page-option :comment-threads-position assoc thread-id (select-keys thread [:position :frame-id])))] - (rx/merge - (rx/of (dwc/commit-changes changes)) - (->> (rp/cmd! :update-comment-thread-position thread) - (rx/catch #(rx/throw {:type :update-comment-thread-position})) - (rx/ignore)))))))) + (rx/merge + (rx/of (dch/commit-changes changes)) + (->> (rp/cmd! :update-comment-thread-position thread) + (rx/catch #(rx/throw {:type :update-comment-thread-position})) + (rx/ignore)))))))) ;; Move comment threads that are inside a frame when that frame is moved" (defmethod ptk/resolve ::move-frame-comment-threads [_ ids] - (dm/assert! (sm/coll-of-uuid? ids)) + (dm/assert! + "expected a valid coll of uuid's" + (sm/check-coll-of-uuid! ids)) + (ptk/reify ::move-frame-comment-threads ptk/WatchEvent (watch [_ state _] diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index c4ea1a7e99..d140bdb6be 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -6,16 +6,23 @@ (ns app.main.data.workspace.common (:require + [app.common.data.macros :as dm] [app.common.logging :as log] + [app.common.types.shape.layout :as ctl] [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.util.router :as rt] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) ;; Change this to :info :debug or :trace to debug this module (log/set-level! :warn) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; HELPERS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (defn initialized? "Check if the state is properly initialized in a workspace. This means it has the `:current-page-id` and `:current-file-id` properly set." @@ -23,11 +30,9 @@ (and (uuid? (:current-file-id state)) (uuid? (:current-page-id state)))) -;; --- Helpers - -(defn interrupt? [e] (= e :interrupt)) - -(declare undo-to-index) +(defn interrupt? + [e] + (= e :interrupt)) (defn- assure-valid-current-page [] @@ -46,17 +51,27 @@ (rx/empty) (rx/of (rt/nav :workspace pparams qparams))))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; UNDO +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(declare undo-to-index) + +;; These functions should've been in +;; `src/app/main/data/workspace/undo.cljs` but doing that causes a +;; circular dependency with `src/app/main/data/workspace/changes.cljs` -;; These functions should've been in `src/app/main/data/workspace/undo.cljs` but doing that causes -;; a circular dependency with `src/app/main/data/workspace/changes.cljs` (def undo (ptk/reify ::undo ptk/WatchEvent (watch [it state _] - (let [edition (get-in state [:workspace-local :edition]) + (let [objects (wsh/lookup-page-objects state) + edition (get-in state [:workspace-local :edition]) drawing (get state :workspace-drawing)] + ;; Editors handle their own undo's - (when (and (nil? edition) (nil? (:object drawing))) + (when (or (and (nil? edition) (nil? (:object drawing))) + (ctl/grid-layout? objects edition)) (let [undo (:workspace-undo state) items (:items undo) index (or (:index undo) (dec (count items)))] @@ -64,14 +79,17 @@ (let [item (get items index) changes (:undo-changes item) undo-group (:undo-group item) - find-first-group-idx (fn ffgidx[index] - (let [item (get items index)] - (if (= (:undo-group item) undo-group) - (ffgidx (dec index)) - (inc index)))) - undo-group-index (when undo-group - (find-first-group-idx index))] + find-first-group-idx + (fn [index] + (if (= (dm/get-in items [index :undo-group]) undo-group) + (recur (dec index)) + (inc index))) + + undo-group-index + (when undo-group + (find-first-group-idx index))] + (if undo-group (rx/of (undo-to-index (dec undo-group-index))) (rx/of (dwu/materialize-undo changes (dec index)) @@ -85,9 +103,11 @@ (ptk/reify ::redo ptk/WatchEvent (watch [it state _] - (let [edition (get-in state [:workspace-local :edition]) + (let [objects (wsh/lookup-page-objects state) + edition (get-in state [:workspace-local :edition]) drawing (get state :workspace-drawing)] - (when (and (nil? edition) (or (empty? drawing) (= :curve (:tool drawing)))) + (when (and (or (nil? edition) (ctl/grid-layout? objects edition)) + (or (empty? drawing) (= :curve (:tool drawing)))) (let [undo (:workspace-undo state) items (:items undo) index (or (:index undo) (dec (count items)))] @@ -117,9 +137,11 @@ (ptk/reify ::undo-to-index ptk/WatchEvent (watch [it state _] - (let [edition (get-in state [:workspace-local :edition]) + (let [objects (wsh/lookup-page-objects state) + edition (get-in state [:workspace-local :edition]) drawing (get state :workspace-drawing)] - (when-not (or (some? edition) (some? (:object drawing))) + (when-not (and (or (some? edition) (some? (:object drawing))) + (not (ctl/grid-layout? objects edition))) (let [undo (:workspace-undo state) items (:items undo) index (or (:index undo) (dec (count items)))] @@ -141,3 +163,28 @@ :undo-changes [] :origin it :save-undo? false}))))))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Toolbar +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn hide-toolbar + [] + (ptk/reify ::hide-toolbar + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :hide-toolbar] true)))) + +(defn show-toolbar + [] + (ptk/reify ::show-toolbar + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :hide-toolbar] false)))) + +(defn toggle-toolbar-visibility + [] + (ptk/reify ::toggle-toolbar-visibility + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-local :hide-toolbar] not)))) diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs index e01ba8eb37..c4c3a148dd 100644 --- a/frontend/src/app/main/data/workspace/drawing.cljs +++ b/frontend/src/app/main/data/workspace/drawing.cljs @@ -7,15 +7,15 @@ (ns app.main.data.workspace.drawing "Drawing interactions." (:require - [app.common.types.shape :as cts] + [app.common.data.macros :as dm] [app.common.uuid :as uuid] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing.box :as box] [app.main.data.workspace.drawing.common :as common] [app.main.data.workspace.drawing.curve :as curve] [app.main.data.workspace.path :as path] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (declare start-drawing) (declare handle-drawing) @@ -23,44 +23,43 @@ ;; --- Select for Drawing (defn select-for-drawing - ([tool] (select-for-drawing tool nil)) - ([tool data] - (ptk/reify ::select-for-drawing - ptk/UpdateEvent - (update [_ state] - (-> state - (update :workspace-drawing assoc :tool tool :object data) - ;; When changing drawing tool disable "scale text" mode - ;; automatically, to help users that ignore how this - ;; mode works. - (update :workspace-layout disj :scale-text))) + [tool] + (ptk/reify ::select-for-drawing + ptk/UpdateEvent + (update [_ state] + (-> state + (update :workspace-drawing assoc :tool tool) + ;; When changing drawing tool disable "scale text" mode + ;; automatically, to help users that ignore how this + ;; mode works. + (update :workspace-layout disj :scale-text))) - ptk/WatchEvent - (watch [_ _ stream] - (rx/merge - (when (= tool :path) - (rx/of (start-drawing :path))) + ptk/WatchEvent + (watch [_ _ stream] + (rx/merge + (when (= tool :path) + (rx/of (start-drawing :path))) - (when (= tool :curve) - (let [stopper (->> stream (rx/filter dwc/interrupt?))] - (->> stream - (rx/filter (ptk/type? ::common/handle-finish-drawing)) - (rx/take 1) - (rx/observe-on :async) - (rx/map #(select-for-drawing tool data)) - (rx/take-until stopper)))) - - ;; NOTE: comments are a special case and they manage they - ;; own interrupt cycle.q - (when (and (not= tool :comments) - (not= tool :path)) - (let [stopper (rx/filter (ptk/type? ::clear-drawing) stream)] - (->> stream - (rx/filter dwc/interrupt?) - (rx/take 1) - (rx/map common/clear-drawing) - (rx/take-until stopper))))))))) + (when (= tool :curve) + (let [stopper (rx/filter dwc/interrupt? stream)] + (->> stream + (rx/filter (ptk/type? ::common/handle-finish-drawing)) + (rx/map (constantly tool)) + (rx/take 1) + (rx/observe-on :async) + (rx/map select-for-drawing) + (rx/take-until stopper)))) + ;; NOTE: comments are a special case and they manage they + ;; own interrupt cycle. + (when (and (not= tool :comments) + (not= tool :path)) + (let [stopper (rx/filter (ptk/type? ::clear-drawing) stream)] + (->> stream + (rx/filter dwc/interrupt?) + (rx/take 1) + (rx/map common/clear-drawing) + (rx/take-until stopper)))))))) ;; NOTE/TODO: when an exception is raised in some point of drawing the ;; draw lock is not released so the user need to refresh in order to @@ -68,7 +67,7 @@ (defn start-drawing [type] - {:pre [(keyword? type)]} + (dm/assert! (keyword? type)) (let [lock-id (uuid/next)] (ptk/reify ::start-drawing ptk/UpdateEvent @@ -77,35 +76,25 @@ ptk/WatchEvent (watch [_ state stream] - (let [lock (get-in state [:workspace-drawing :lock])] + (let [lock (dm/get-in state [:workspace-drawing :lock])] (when (= lock lock-id) (rx/merge (rx/of (handle-drawing type)) (->> stream - (rx/filter (ptk/type? ::common/handle-finish-drawing) ) + (rx/filter (ptk/type? ::common/handle-finish-drawing)) (rx/take 1) (rx/map #(fn [state] (update state :workspace-drawing dissoc :lock))))))))))) (defn handle-drawing [type] (ptk/reify ::handle-drawing - ptk/UpdateEvent - (update [_ state] - (let [data (cts/make-minimal-shape type)] - (update-in state [:workspace-drawing :object] merge data))) - ptk/WatchEvent (watch [_ _ _] (rx/of (case type - :path - (path/handle-new-shape) - - :curve - (curve/handle-drawing-curve) - - ;; default - (box/handle-drawing-box)))))) + :path (path/handle-new-shape) + :curve (curve/handle-drawing) + (box/handle-drawing type)))))) diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs index 9517047d79..a595a8c3fa 100644 --- a/frontend/src/app/main/data/workspace/drawing/box.cljs +++ b/frontend/src/app/main/data/workspace/drawing/box.cljs @@ -6,11 +6,14 @@ (ns app.main.data.workspace.drawing.box (:require + [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.flex-layout :as gsl] + [app.common.geom.shapes.flex-layout :as gslf] + [app.common.geom.shapes.grid-layout :as gslg] [app.common.math :as mth] - [app.common.pages.helpers :as cph] + [app.common.types.container :as ctn] [app.common.types.modifiers :as ctm] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] @@ -21,8 +24,10 @@ [app.main.data.workspace.state-helpers :as wsh] [app.main.snap :as snap] [app.main.streams :as ms] - [beicon.core :as rx] - [potok.core :as ptk])) + [app.util.array :as array] + [app.util.mouse :as mse] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (defn adjust-ratio [point initial] @@ -39,91 +44,96 @@ (> dy dx) (assoc :x (- (:x point) (* sx (- dy dx))))))) -(defn resize-shape [{:keys [x y width height] :as shape} initial point lock?] +(defn resize-shape [{:keys [x y width height] :as shape} initial point lock? mod?] (if (and (some? x) (some? y) (some? width) (some? height)) - (let [draw-rect (gsh/make-rect initial (cond-> point lock? (adjust-ratio initial))) - shape-rect (gsh/make-rect x y width height) + (let [draw-rect (grc/make-rect initial (cond-> point lock? (adjust-ratio initial))) + shape-rect (grc/make-rect x y width height) - scalev (gpt/point (/ (:width draw-rect) (:width shape-rect)) - (/ (:height draw-rect) (:height shape-rect))) + scalev (gpt/point (/ (:width draw-rect) + (:width shape-rect)) + (/ (:height draw-rect) + (:height shape-rect))) - movev (gpt/to-vec (gpt/point shape-rect) (gpt/point draw-rect))] + movev (gpt/to-vec (gpt/point shape-rect) + (gpt/point draw-rect))] (-> shape (assoc :click-draw? false) + (vary-meta merge {:mod? mod?}) (gsh/transform-shape (-> (ctm/empty) (ctm/resize scalev (gpt/point x y)) (ctm/move movev))))) shape)) -(defn update-drawing [state initial point lock?] - (update-in state [:workspace-drawing :object] resize-shape initial point lock?)) +(defn update-drawing [state initial point lock? mod?] + (update-in state [:workspace-drawing :object] resize-shape initial point lock? mod?)) (defn move-drawing [{:keys [x y]}] (fn [state] (update-in state [:workspace-drawing :object] gsh/absolute-move (gpt/point x y)))) -(defn handle-drawing-box [] - (ptk/reify ::handle-drawing-box +(defn handle-drawing + [type] + (ptk/reify ::handle-drawing ptk/WatchEvent (watch [_ state stream] - (let [stoper? #(or (ms/mouse-up? %) (= % :interrupt)) - stoper (rx/filter stoper? stream) - layout (get state :workspace-layout) - zoom (get-in state [:workspace-local :zoom] 1) - snap-pixel? (contains? layout :snap-pixel-grid) + (let [stopper (mse/drag-stopper stream) + layout (get state :workspace-layout) + zoom (dm/get-in state [:workspace-local :zoom] 1) - snap-precision (if (>= zoom zoom-half-pixel-precision) 0.5 1) - initial (cond-> @ms/mouse-position snap-pixel? (gpt/round-step snap-precision)) + snap-pixel? (contains? layout :snap-pixel-grid) + snap-prec (if (>= zoom zoom-half-pixel-precision) 0.5 1) + initial (cond-> @ms/mouse-position snap-pixel? (gpt/round-step snap-prec)) - page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - focus (:workspace-focus-selected state) + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + focus (:workspace-focus-selected state) - fid (ctst/top-nested-frame objects initial) + fid (->> (ctst/top-nested-frame objects initial) + (ctn/get-first-not-copy-parent objects) ;; We don't want to change the structure of component copies + :id) flex-layout? (ctl/flex-layout? objects fid) - drop-index (when flex-layout? (gsl/get-drop-index fid objects initial)) + grid-layout? (ctl/grid-layout? objects fid) - shape (get-in state [:workspace-drawing :object]) - shape (-> shape - (cts/setup-shape {:x (:x initial) - :y (:y initial) - :width 0.01 - :height 0.01}) - (cond-> (and (cph/frame-shape? shape) - (not= fid uuid/zero)) - (assoc :fills [] :hide-in-viewer true)) + drop-index (when flex-layout? (gslf/get-drop-index fid objects initial)) + drop-cell (when grid-layout? (gslg/get-drop-cell fid objects initial)) - (assoc :frame-id fid) + shape (-> (cts/setup-shape {:type type + :x (:x initial) + :y (:y initial) + :frame-id fid + :parent-id fid + :initialized? true + :click-draw? true + :hide-in-viewer (and (= type :frame) (not= fid uuid/zero))}) + (cond-> (some? drop-index) + (with-meta {:index drop-index})) + (cond-> (some? drop-cell) + (with-meta {:cell drop-cell})))] - (cond-> (some? drop-index) - (with-meta {:index drop-index})) - - (assoc :initialized? true) - (assoc :click-draw? true))] (rx/concat ;; Add shape to drawing state - (rx/of #(assoc-in state [:workspace-drawing :object] shape)) - + (rx/of #(update % :workspace-drawing assoc :object shape)) ;; Initial SNAP - (->> - (rx/concat - (->> (snap/closest-snap-point page-id [shape] objects layout zoom focus initial) - (rx/map move-drawing)) + (->> (rx/concat + (->> (snap/closest-snap-point page-id [shape] objects layout zoom focus initial) + (rx/map move-drawing)) - (->> ms/mouse-position - (rx/filter #(> (gpt/distance % initial) (/ 2 zoom))) - (rx/with-latest vector ms/mouse-position-shift) - (rx/switch-map - (fn [[point :as current]] - (->> (snap/closest-snap-point page-id [shape] objects layout zoom focus point) - (rx/map #(conj current %))))) - (rx/map - (fn [[_ shift? point]] - #(update-drawing % initial (cond-> point snap-pixel? (gpt/round-step snap-precision)) shift?))))) - (rx/take-until stoper)) + (->> ms/mouse-position + (rx/filter #(> (gpt/distance % initial) (/ 2 zoom))) + ;; Take until before the snap calculation otherwise we could cancel the snap in the worker + ;; and its a problem for fast moving drawing + (rx/take-until stopper) + (rx/with-latest-from ms/mouse-position-shift ms/mouse-position-mod) + (rx/switch-map + (fn [[point :as current]] + (->> (snap/closest-snap-point page-id [shape] objects layout zoom focus point) + (rx/map (partial array/conj current))))) + (rx/map + (fn [[_ shift? mod? point]] + #(update-drawing % initial (cond-> point snap-pixel? (gpt/round-step snap-prec)) shift? mod?)))))) (->> (rx/of (common/handle-finish-drawing)) (rx/delay 100))))))) diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs index 6aada37a90..fffc73b009 100644 --- a/frontend/src/app/main/data/workspace/drawing/common.cljs +++ b/frontend/src/app/main/data/workspace/drawing/common.cljs @@ -6,17 +6,18 @@ (ns app.main.data.workspace.drawing.common (:require + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.common.pages.helpers :as cph] [app.common.types.modifiers :as ctm] [app.common.types.shape :as cts] [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.worker :as uw] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (defn clear-drawing [] @@ -30,53 +31,59 @@ (ptk/reify ::handle-finish-drawing ptk/WatchEvent (watch [_ state _] - (let [tool (get-in state [:workspace-drawing :tool]) - shape (get-in state [:workspace-drawing :object]) - objects (wsh/lookup-page-objects state)] + (let [tool (dm/get-in state [:workspace-drawing :tool]) + shape (dm/get-in state [:workspace-drawing :object]) + objects (wsh/lookup-page-objects state) + page-id (:current-page-id state)] + (rx/concat (when (:initialized? shape) - (let [page-id (:current-page-id state) + (let [click-draw? (:click-draw? shape) + text? (cfh/text-shape? shape) + vbox (dm/get-in state [:workspace-local :vbox]) - click-draw? (:click-draw? shape) - text? (= :text (:type shape)) - - min-side (min 100 - (mth/floor (get-in state [:workspace-local :vbox :width])) - (mth/floor (get-in state [:workspace-local :vbox :height]))) + min-side (mth/min 100 + (mth/floor (dm/get-prop vbox :width)) + (mth/floor (dm/get-prop vbox :height))) shape (cond-> shape (not click-draw?) - (-> (assoc :grow-type :fixed)) + (assoc :grow-type :fixed) - (and click-draw? (not text?)) - (-> (assoc :width min-side :height min-side) - (cts/setup-rect-selrect) + (and ^boolean click-draw? (not ^boolean text?)) + (-> (assoc :width min-side) + (assoc :height min-side) + ;; NOTE: we need to recalculate the selrect and + ;; points, so we assign `nil` to it + (assoc :selrect nil) + (assoc :points nil) + (cts/setup-shape) (gsh/transform-shape (ctm/move-modifiers (- (/ min-side 2)) (- (/ min-side 2))))) (and click-draw? text?) (-> (assoc :height 17 :width 4 :grow-type :auto-width) - (cts/setup-rect-selrect)) + (cts/setup-shape)) :always (dissoc :initialized? :click-draw?))] ;; Add & select the created shape to the workspace (rx/concat - (if (= :frame (:type shape)) + (if (cfh/frame-shape? shape) (rx/of (dwu/start-undo-transaction (:id shape))) (rx/empty)) (rx/of (dwsh/add-shape shape {:no-select? (= tool :curve)})) - - (if (= :frame (:type shape)) + (if (cfh/frame-shape? shape) (rx/concat (->> (uw/ask! {:cmd :selection/query :page-id page-id :rect (:selrect shape) :include-frames? true - :full-frame? true}) - (rx/map #(cph/clean-loops objects %)) + :full-frame? true + :using-selrect? true}) + (rx/map #(cfh/clean-loops objects %)) (rx/map #(dwsh/move-shapes-into-frame (:id shape) %))) (rx/of (dwu/commit-undo-transaction (:id shape)))) (rx/empty))))) diff --git a/frontend/src/app/main/data/workspace/drawing/curve.cljs b/frontend/src/app/main/data/workspace/drawing/curve.cljs index 307f0973a8..5c0d98898e 100644 --- a/frontend/src/app/main/data/workspace/drawing/curve.cljs +++ b/frontend/src/app/main/data/workspace/drawing/curve.cljs @@ -6,96 +6,113 @@ (ns app.main.data.workspace.drawing.curve (:require + [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.flex-layout :as gsl] + [app.common.geom.shapes.flex-layout :as gslf] + [app.common.geom.shapes.grid-layout :as gslg] [app.common.geom.shapes.path :as gsp] + [app.common.types.container :as ctn] + [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] + [app.common.uuid :as uuid] [app.main.data.workspace.drawing.common :as common] [app.main.data.workspace.state-helpers :as wsh] [app.main.streams :as ms] + [app.util.mouse :as mse] [app.util.path.simplify-curve :as ups] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (def simplify-tolerance 0.3) -(defn stoper-event? [{:keys [type] :as event}] - (ms/mouse-event? event) (= type :up)) - -(defn initialize-drawing [state] - (assoc-in state [:workspace-drawing :object :initialized?] true)) - -(defn insert-point-segment [state point] - (let [segments (-> state - (get-in [:workspace-drawing :object :segments]) - (or []) - (conj point)) - content (gsp/segments->content segments) - selrect (gsh/content->selrect content) - points (gsh/rect->points selrect)] - (-> state - (update-in [:workspace-drawing :object] assoc - :segments segments - :content content - :selrect selrect - :points points)))) - -(defn setup-frame-curve [] - (ptk/reify ::setup-frame-path +(defn- insert-point + [point] + (ptk/reify ::insert-point ptk/UpdateEvent (update [_ state] + (update-in state [:workspace-drawing :object] + (fn [object] + (let [segments (-> (:segments object) + (conj point)) + content (gsp/segments->content segments) + selrect (gsh/content->selrect content) + points (grc/rect->points selrect)] + (-> object + (assoc :segments segments) + (assoc :content content) + (assoc :selrect selrect) + (assoc :points points)))))))) - (let [objects (wsh/lookup-page-objects state) - content (get-in state [:workspace-drawing :object :content] []) - start (get-in content [0 :params] nil) - position (when start (gpt/point start)) - frame-id (ctst/top-nested-frame objects position) - flex-layout? (ctl/flex-layout? objects frame-id) - drop-index (when flex-layout? (gsl/get-drop-index frame-id objects position))] - (-> state - (assoc-in [:workspace-drawing :object :frame-id] frame-id) - (cond-> (some? drop-index) - (update-in [:workspace-drawing :object] with-meta {:index drop-index}))))))) - -(defn curve-to-path [{:keys [segments] :as shape}] - (let [content (gsp/segments->content segments) - selrect (gsh/content->selrect content) - points (gsh/rect->points selrect)] - (-> shape - (dissoc :segments) - (assoc :content content) - (assoc :selrect selrect) - (assoc :points points) - - (cond-> (or (empty? points) (nil? selrect) (<= (count content) 1)) - (assoc :initialized? false))))) - -(defn finish-drawing-curve +(defn- setup-frame [] - (ptk/reify ::finish-drawing-curve + (ptk/reify ::setup-frame ptk/UpdateEvent (update [_ state] - (letfn [(update-curve [shape] - (-> shape - (update :segments #(ups/simplify % simplify-tolerance)) - (curve-to-path)))] - (-> state - (update-in [:workspace-drawing :object] update-curve)))))) + (let [objects (wsh/lookup-page-objects state) + content (dm/get-in state [:workspace-drawing :object :content] []) + start (dm/get-in content [0 :params] nil) + position (when start (gpt/point start)) + frame-id (->> (ctst/top-nested-frame objects position) + (ctn/get-first-not-copy-parent objects) ;; We don't want to change the structure of component copies + :id) + flex-layout? (ctl/flex-layout? objects frame-id) -(defn handle-drawing-curve [] - (ptk/reify ::handle-drawing-curve + grid-layout? (ctl/grid-layout? objects frame-id) + drop-index (when flex-layout? (gslf/get-drop-index frame-id objects position)) + drop-cell (when grid-layout? (gslg/get-drop-cell frame-id objects position))] + (update-in state [:workspace-drawing :object] + (fn [object] + (-> object + (assoc :frame-id frame-id) + (assoc :parent-id frame-id) + (cond-> (some? drop-index) + (with-meta {:index drop-index})) + (cond-> (some? drop-cell) + (with-meta {:cell drop-cell}))))))))) + +(defn finish-drawing + [] + (ptk/reify ::finish-drawing + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-drawing :object] + (fn [{:keys [segments] :as shape}] + (let [segments (ups/simplify segments simplify-tolerance) + content (gsp/segments->content segments) + selrect (gsh/content->selrect content) + points (grc/rect->points selrect)] + (-> shape + (dissoc :segments) + (assoc :content content) + (assoc :selrect selrect) + (assoc :points points) + (cond-> (or (empty? points) + (nil? selrect) + (<= (count content) 1)) + (assoc :initialized? false))))))))) + + +(defn handle-drawing [] + (ptk/reify ::handle-drawing ptk/WatchEvent (watch [_ _ stream] - (let [stoper (rx/filter stoper-event? stream) - mouse (rx/sample 10 ms/mouse-position)] + (let [stopper (mse/drag-stopper stream) + mouse (rx/sample 10 ms/mouse-position) + shape (cts/setup-shape {:type :path + :initialized? true + :frame-id uuid/zero + :parent-id uuid/zero + :segments []})] (rx/concat - (rx/of initialize-drawing) + (rx/of #(update % :workspace-drawing assoc :object shape)) (->> mouse - (rx/map (fn [pt] #(insert-point-segment % pt))) - (rx/take-until stoper)) - (rx/of (setup-frame-curve) - (finish-drawing-curve) - (common/handle-finish-drawing))))))) + (rx/map insert-point) + (rx/take-until stopper)) + (rx/of + (setup-frame) + (finish-drawing) + (common/handle-finish-drawing))))))) diff --git a/frontend/src/app/main/data/workspace/edition.cljs b/frontend/src/app/main/data/workspace/edition.cljs index 01be4c72ec..97edd06491 100644 --- a/frontend/src/app/main/data/workspace/edition.cljs +++ b/frontend/src/app/main/data/workspace/edition.cljs @@ -7,9 +7,10 @@ (ns app.main.data.workspace.edition (:require [app.common.data.macros :as dm] + [app.main.data.workspace.path.common :as dwpc] [app.main.data.workspace.state-helpers :as wsh] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (defn interrupt? [e] (= e :interrupt)) @@ -26,7 +27,8 @@ (if (contains? objects id) (-> state (assoc-in [:workspace-local :selected] #{id}) - (assoc-in [:workspace-local :edition] id)) + (assoc-in [:workspace-local :edition] id) + (dissoc :workspace-grid-edition)) state))) ptk/WatchEvent @@ -34,14 +36,23 @@ (->> stream (rx/filter interrupt?) (rx/take 1) - (rx/map (constantly clear-edition-mode)))))) + (rx/map clear-edition-mode))))) ;; If these event change modules review /src/app/main/data/workspace/path/undo.cljs -(def clear-edition-mode +(defn clear-edition-mode + [] (ptk/reify ::clear-edition-mode ptk/UpdateEvent (update [_ state] + (-> state + (update :workspace-local dissoc :edition) + (update :workspace-drawing dissoc :tool :object :lock) + (dissoc :workspace-grid-edition))) + + ptk/WatchEvent + (watch [_ state _] (let [id (get-in state [:workspace-local :edition])] - (-> state - (update :workspace-local dissoc :edition) - (cond-> (some? id) (update-in [:workspace-local :edit-path] dissoc id))))))) + (rx/concat + (when (some? id) + (dwpc/finish-path))))))) + diff --git a/frontend/src/app/main/data/workspace/fix_bool_contents.cljs b/frontend/src/app/main/data/workspace/fix_bool_contents.cljs index 63a54e7d79..8d0ac516ea 100644 --- a/frontend/src/app/main/data/workspace/fix_bool_contents.cljs +++ b/frontend/src/app/main/data/workspace/fix_bool_contents.cljs @@ -10,8 +10,8 @@ [app.common.geom.shapes :as gsh] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) ;; This event will update the file so the boolean data has a pre-generated path data ;; to increase performance. diff --git a/frontend/src/app/main/data/workspace/fix_broken_shape_links.cljs b/frontend/src/app/main/data/workspace/fix_broken_shapes.cljs similarity index 81% rename from frontend/src/app/main/data/workspace/fix_broken_shape_links.cljs rename to frontend/src/app/main/data/workspace/fix_broken_shapes.cljs index e547f0c033..c110de09b0 100644 --- a/frontend/src/app/main/data/workspace/fix_broken_shape_links.cljs +++ b/frontend/src/app/main/data/workspace/fix_broken_shapes.cljs @@ -4,15 +4,15 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.main.data.workspace.fix-broken-shape-links +(ns app.main.data.workspace.fix-broken-shapes (:require [app.main.data.workspace.changes :as dch] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) -(defn- generate-changes +(defn- generate-broken-link-changes [attr {:keys [objects id] :as container}] - (let [base {:type :fix-obj attr id} + (let [base {:type :fix-obj :fix :broken-children attr id} contains? (partial contains? objects) xform (comp ;; FIXME: Ensure all obj have id field (this is needed @@ -36,14 +36,14 @@ (defn fix-broken-shapes [] - (ptk/reify ::fix-broken-shape-links + (ptk/reify ::fix-broken-shapes ptk/WatchEvent (watch [it state _] (let [data (get state :workspace-data) changes (concat - (mapcat (partial generate-changes :page-id) + (mapcat (partial generate-broken-link-changes :page-id) (vals (:pages-index data))) - (mapcat (partial generate-changes :component-id) + (mapcat (partial generate-broken-link-changes :component-id) (vals (:components data))))] (if (seq changes) diff --git a/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs b/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs index 05b4a13da4..31ec4176ef 100644 --- a/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs +++ b/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs @@ -7,13 +7,13 @@ (ns app.main.data.workspace.fix-deleted-fonts (:require [app.common.data :as d] - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cfh] [app.common.text :as txt] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] [app.main.fonts :as fonts] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) ;; This event will update the file so the texts with non existing custom fonts try to be fixed. ;; This can happen when: @@ -25,8 +25,8 @@ [node] (let [fonts (deref fonts/fontsdb)] (and - (some? (:font-family node)) - (nil? (get fonts (:font-id node)))))) + (some? (:font-family node)) + (nil? (get fonts (:font-id node)))))) (defn calculate-alternative-font-id [value] @@ -39,7 +39,7 @@ (defn should-fix-deleted-font-shape? [shape] (let [text-nodes (txt/node-seq txt/is-text-node? (:content shape))] - (and (cph/text-shape? shape) (some has-invalid-font-family text-nodes)))) + (and (cfh/text-shape? shape) (some has-invalid-font-family text-nodes)))) (defn should-fix-deleted-font-component? [component] @@ -66,9 +66,9 @@ (defn fix-deleted-font-component [component] (update component - :objects - (fn [objects] - (d/mapm #(fix-deleted-font-shape %2) objects)))) + :objects + (fn [objects] + (d/mapm #(fix-deleted-font-shape %2) objects)))) (defn fix-deleted-font-typography [typography] @@ -84,8 +84,8 @@ (let [objects (wsh/lookup-page-objects state) ids (into #{} - (comp (filter should-fix-deleted-font-shape?) (map :id)) - (vals objects)) + (comp (filter should-fix-deleted-font-shape?) (map :id)) + (vals objects)) components (->> (wsh/lookup-local-components state) (vals) @@ -93,11 +93,11 @@ component-changes (into [] - (map (fn [component] - {:type :mod-component - :id (:id component) - :objects (-> (fix-deleted-font-component component) :objects)})) - components) + (map (fn [component] + {:type :mod-component + :id (:id component) + :objects (-> (fix-deleted-font-component component) :objects)})) + components) typographies (->> (get-in state [:workspace-data :typographies]) (vals) @@ -105,25 +105,25 @@ typography-changes (into [] - (map (fn [typography] - {:type :mod-typography - :typography (fix-deleted-font-typography typography)})) - typographies)] + (map (fn [typography] + {:type :mod-typography + :typography (fix-deleted-font-typography typography)})) + typographies)] (rx/concat - (rx/of (dch/update-shapes ids #(fix-deleted-font-shape %) {:reg-objects? false - :save-undo? false - :ignore-tree true})) - (if (empty? component-changes) - (rx/empty) - (rx/of (dch/commit-changes {:origin it - :redo-changes component-changes - :undo-changes [] - :save-undo? false}))) + (rx/of (dch/update-shapes ids #(fix-deleted-font-shape %) {:reg-objects? false + :save-undo? false + :ignore-tree true})) + (if (empty? component-changes) + (rx/empty) + (rx/of (dch/commit-changes {:origin it + :redo-changes component-changes + :undo-changes [] + :save-undo? false}))) - (if (empty? typography-changes) - (rx/empty) - (rx/of (dch/commit-changes {:origin it - :redo-changes typography-changes - :undo-changes [] - :save-undo? false})))))))) + (if (empty? typography-changes) + (rx/empty) + (rx/of (dch/commit-changes {:origin it + :redo-changes typography-changes + :undo-changes [] + :save-undo? false})))))))) diff --git a/frontend/src/app/main/data/workspace/grid.cljs b/frontend/src/app/main/data/workspace/grid.cljs index b9e0427fb2..50a2351071 100644 --- a/frontend/src/app/main/data/workspace/grid.cljs +++ b/frontend/src/app/main/data/workspace/grid.cljs @@ -9,11 +9,11 @@ [app.common.colors :as clr] [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages.changes-builder :as pcb] + [app.common.files.changes-builder :as pcb] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Grid @@ -76,6 +76,6 @@ (watch [it state _] (let [page (wsh/lookup-page state)] (rx/of (dch/commit-changes - (-> (pcb/empty-changes it) - (pcb/with-page page) - (pcb/set-page-option [:saved-grids type] params)))))))) + (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/set-page-option [:saved-grids type] params)))))))) diff --git a/frontend/src/app/main/data/workspace/grid_layout/editor.cljs b/frontend/src/app/main/data/workspace/grid_layout/editor.cljs index 6f9bf9a8eb..9af194119f 100644 --- a/frontend/src/app/main/data/workspace/grid_layout/editor.cljs +++ b/frontend/src/app/main/data/workspace/grid_layout/editor.cljs @@ -6,10 +6,14 @@ (ns app.main.data.workspace.grid-layout.editor (:require - [potok.core :as ptk])) + [app.common.data.macros :as dm] + [app.common.geom.rect :as grc] + [app.common.types.shape.layout :as ctl] + [app.main.data.workspace.state-helpers :as wsh] + [potok.v2.core :as ptk])) (defn hover-grid-cell - [grid-id row column add-to-set] + [grid-id cell-id add-to-set] (ptk/reify ::hover-grid-cell ptk/UpdateEvent (update [_ state] @@ -19,26 +23,101 @@ (fn [hover-set] (let [hover-set (or hover-set #{})] (if add-to-set - (conj hover-set [row column]) - (disj hover-set [row column])))))))) + (conj hover-set cell-id) + (disj hover-set cell-id)))))))) -(defn select-grid-cell - [grid-id row column] - (ptk/reify ::select-grid-cell +(defn add-to-selection + ([grid-id cell-id] + (add-to-selection grid-id cell-id false)) + ([grid-id cell-id shift?] + (ptk/reify ::add-to-selection + ptk/UpdateEvent + (update [_ state] + (if shift? + (let [objects (wsh/lookup-page-objects state) + grid (get objects grid-id) + selected (or (dm/get-in state [:workspace-grid-edition grid-id :selected]) #{}) + selected (into selected [cell-id]) + cells (->> selected (map #(dm/get-in grid [:layout-grid-cells %]))) + + {:keys [first-row last-row first-column last-column]} (ctl/cells-coordinates cells) + new-selected + (into #{} + (map :id) + (ctl/cells-in-area grid first-row last-row first-column last-column))] + (assoc-in state [:workspace-grid-edition grid-id :selected] new-selected)) + (update-in state [:workspace-grid-edition grid-id :selected] (fnil conj #{}) cell-id)))))) + +(defn set-selection + [grid-id cell-id] + (ptk/reify ::set-selection ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-grid-edition grid-id :selected] [row column])))) + (assoc-in state [:workspace-grid-edition grid-id :selected] #{cell-id})))) (defn remove-selection - [grid-id] + [grid-id cell-id] (ptk/reify ::remove-selection + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-grid-edition grid-id :selected] disj cell-id)))) + +(defn clear-selection + [grid-id] + (ptk/reify ::clear-selection ptk/UpdateEvent (update [_ state] (update-in state [:workspace-grid-edition grid-id] dissoc :selected)))) +(defn clean-selection + [grid-id] + (ptk/reify ::clean-selection + ptk/UpdateEvent + (update [_ state] + (let [objects (wsh/lookup-page-objects state) + shape (get objects grid-id)] + (update-in state [:workspace-grid-edition grid-id :selected] + (fn [selected] + (into #{} + (filter #(contains? (:layout-grid-cells shape) %)) + selected))))))) + (defn stop-grid-layout-editing [grid-id] (ptk/reify ::stop-grid-layout-editing ptk/UpdateEvent (update [_ state] (update state :workspace-grid-edition dissoc grid-id)))) + +(defn locate-board + [grid-id] + (ptk/reify ::locate-board + ptk/UpdateEvent + (update [_ state] + (let [objects (wsh/lookup-page-objects state) + srect (get-in objects [grid-id :selrect])] + (-> state + (update :workspace-local + (fn [{:keys [zoom vport] :as local}] + (let [{:keys [x y width height]} srect + x (+ x (/ width 2) (- (/ (:width vport) 2 zoom))) + y (+ y (/ height 2) (- (/ (:height vport) 2 zoom))) + srect (grc/make-rect x y width height)] + (-> local + (update :vbox merge (select-keys srect [:x :y :x1 :x2 :y1 :y2]))))))))))) + +(defn select-track-cells + [grid-id type index] + (ptk/reify ::select-track-cells + ptk/UpdateEvent + (update [_ state] + (let [objects (wsh/lookup-page-objects state) + parent (get objects grid-id) + + cells + (if (= type :column) + (ctl/cells-by-column parent index) + (ctl/cells-by-row parent index)) + + selected (into #{} (map :id) cells)] + (assoc-in state [:workspace-grid-edition grid-id :selected] selected))))) diff --git a/frontend/src/app/main/data/workspace/grid_layout/shortcuts.cljs b/frontend/src/app/main/data/workspace/grid_layout/shortcuts.cljs new file mode 100644 index 0000000000..8d76c38caa --- /dev/null +++ b/frontend/src/app/main/data/workspace/grid_layout/shortcuts.cljs @@ -0,0 +1,71 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.data.workspace.grid-layout.shortcuts + (:require + [app.main.data.shortcuts :as ds] + [app.main.data.workspace :as dw] + [app.main.data.workspace.common :as dwc] + [app.main.store :as st] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Shortcuts +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Shortcuts format https://github.com/ccampbell/mousetrap + +(defn esc-pressed [] + (ptk/reify ::esc-pressed + ptk/WatchEvent + (watch [_ state _] + ;; Not interrupt when we're editing a path + (let [edition-id (or (get-in state [:workspace-drawing :object :id]) + (get-in state [:workspace-local :edition])) + path-edit-mode (get-in state [:workspace-local :edit-path edition-id :edit-mode])] + (if-not (= :draw path-edit-mode) + (rx/of :interrupt) + (rx/empty)))))) + +(def shortcuts + {:escape {:tooltip (ds/esc) + :command ["escape" "enter" "v"] + :fn #(st/emit! (esc-pressed))} + + :undo {:tooltip (ds/meta "Z") + :command (ds/c-mod "z") + :fn #(st/emit! dwc/undo)} + + :redo {:tooltip (ds/meta "Y") + :command [(ds/c-mod "shift+z") (ds/c-mod "y")] + :fn #(st/emit! dwc/redo)} + + ;; ZOOM + + :increase-zoom {:tooltip "+" + :command "+" + :fn #(st/emit! (dw/increase-zoom nil))} + + :decrease-zoom {:tooltip "-" + :command "-" + :fn #(st/emit! (dw/decrease-zoom nil))} + + :reset-zoom {:tooltip (ds/shift "0") + :command "shift+0" + :fn #(st/emit! dw/reset-zoom)} + + :fit-all {:tooltip (ds/shift "1") + :command "shift+1" + :fn #(st/emit! dw/zoom-to-fit-all)} + + :zoom-selected {:tooltip (ds/shift "2") + :command "shift+2" + :fn #(st/emit! dw/zoom-to-selected-shape)}}) + +(defn get-tooltip [shortcut] + (assert (contains? shortcuts shortcut) (str shortcut)) + (get-in shortcuts [shortcut :tooltip])) diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index 311dc2b70d..c3a4032694 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -8,27 +8,24 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] [app.common.types.component :as ctk] - [app.common.types.pages-list :as ctpl] + [app.common.types.container :as ctn] [app.common.types.shape :as cts] - [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] - [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.selection :as dws] - [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (defn shapes-for-grouping [objects selected] (->> selected - (cph/order-by-indexed-shapes objects) + (cfh/order-by-indexed-shapes objects) reverse (map #(get objects %)))) @@ -41,8 +38,8 @@ group, one (or many) groups can become empty because they have had a single shape which is moved to the created group." [objects parent-id shapes] - (let [ids (cph/clean-loops objects (into #{} (map :id) shapes)) - parents (into #{} (map #(cph/get-parent-id objects %)) ids)] + (let [ids (cfh/clean-loops objects (into #{} (map :id) shapes)) + parents (into #{} (map #(cfh/get-parent-id objects %)) ids)] (loop [current-id (first parents) to-check (rest parents) removed-id? ids @@ -58,7 +55,7 @@ (empty? (remove removed-id? (:shapes group)))) ;; Adds group to the remove and check its parent - (let [to-check (concat to-check [(cph/get-parent-id objects current-id)]) ] + (let [to-check (concat to-check [(cfh/get-parent-id objects current-id)])] (recur (first to-check) (rest to-check) (conj removed-id? current-id) @@ -80,33 +77,55 @@ (:name (first shapes)) base-name) - selrect (gsh/selection-rect shapes) + selrect (gsh/shapes->rect shapes) group-idx (->> shapes last :id - (cph/get-position-on-parent objects) + (cfh/get-position-on-parent objects) inc) - group (-> (cts/make-minimal-group frame-id selrect gname) - (cts/setup-shape selrect) - (assoc :shapes (mapv :id shapes) - :parent-id parent-id - :frame-id frame-id - :index group-idx)) + + group (cts/setup-shape {:type :group + :name gname + :shapes (mapv :id shapes) + :selrect selrect + :x (:x selrect) + :y (:y selrect) + :width (:width selrect) + :height (:height selrect) + :parent-id parent-id + :frame-id frame-id + :index group-idx}) ;; Shapes that are in a component, but are not root, must be detached, ;; because they will be now children of a non instance group. - shapes-to-detach (filter ctk/in-component-copy-not-root? shapes) + shapes-to-detach (filter ctk/in-component-copy-not-head? shapes) ;; Look at the `get-empty-groups-after-group-creation` ;; docstring to understand the real purpose of this code ids-to-delete (get-empty-groups-after-group-creation objects parent-id shapes) + target-cell + (when (ctl/grid-layout? objects parent-id) + (ctl/get-cell-by-shape-id (get objects parent-id) (-> shapes last :id))) + + grid-parents + (into [] + (comp (map :parent-id) + (filter (partial ctl/grid-layout? objects))) + shapes) + changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) (pcb/add-object group {:index group-idx}) (pcb/update-shapes (map :id shapes) ctl/remove-layout-item-data) (pcb/change-parent (:id group) (reverse shapes)) (pcb/update-shapes (map :id shapes-to-detach) ctk/detach-shape) + (cond-> target-cell + (pcb/update-shapes + [parent-id] + (fn [parent] + (assoc-in parent [:layout-grid-cells (:id target-cell) :shapes] [(:id group)])))) + (pcb/update-shapes grid-parents ctl/assign-cells {:with-objects? true}) (pcb/remove-objects ids-to-delete))] [group changes])) @@ -114,9 +133,9 @@ (defn remove-group-changes [it page-id group objects] (let [children (->> (:shapes group) - (cph/order-by-indexed-shapes objects) + (cfh/order-by-indexed-shapes objects) (mapv #(get objects %))) - parent-id (cph/get-parent-id objects (:id group)) + parent-id (cfh/get-parent-id objects (:id group)) parent (get objects parent-id) index-in-parent @@ -129,7 +148,7 @@ ;; Shapes that are in a component (including root) must be detached, ;; because cannot be easyly synchronized back to the main component. shapes-to-detach (filter ctk/in-component-copy? - (cph/get-children-with-self objects (:id group)))] + (cfh/get-children-with-self objects (:id group)))] (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) @@ -140,11 +159,11 @@ (defn remove-frame-changes [it page-id frame objects] (let [children (->> (:shapes frame) - (cph/order-by-indexed-shapes objects) + (cfh/order-by-indexed-shapes objects) (mapv #(get objects %))) - parent-id (cph/get-parent-id objects (:id frame)) + parent-id (cfh/get-parent-id objects (:id frame)) idx-in-parent (->> (:id frame) - (cph/get-position-on-parent objects) + (cfh/get-position-on-parent objects) inc)] (-> (pcb/empty-changes it page-id) @@ -155,45 +174,6 @@ (pcb/remove-objects [(:id frame)])))) -(defn- clone-component-shapes-changes - [changes shape objects] - (let [shape-parent-id (:parent-id shape) - new-shape-id (uuid/next) - [_ new-shapes _] - (ctst/clone-object shape - shape-parent-id - objects - (fn [object _] - (cond-> object - (= new-shape-id (:parent-id object)) - (assoc :parent-id shape-parent-id))) - (fn [object _] object) - new-shape-id - false) - - new-shapes (->> new-shapes - (filter #(not= (:id %) new-shape-id)))] - (reduce - (fn [changes shape] - (pcb/add-object changes shape)) - changes - new-shapes))) - -(defn remove-component-changes - [it page-id shape objects file-data file] - (let [page (ctpl/get-page file-data page-id) - components-v2 (dm/get-in file-data [:options :components-v2]) - ;; In order to ungroup a component, we first make a clone of its shapes, - ;; and then we delete it - changes (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects) - (pcb/with-library-data file-data) - (pcb/with-page page) - (clone-component-shapes-changes shape objects) - (dwsh/delete-shapes-changes file page objects [(:id shape)] it components-v2))] - ;; TODO: Should we call detach-comment-thread ? - changes)) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; GROUPS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -204,14 +184,17 @@ (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - selected (wsh/lookup-selected state) - selected (cph/clean-loops objects selected) - shapes (shapes-for-grouping objects selected)] + selected (->> (wsh/lookup-selected state) + (cfh/clean-loops objects) + (remove #(ctn/has-any-copy-parent? objects (get objects %)))) + shapes (shapes-for-grouping objects selected) + parents (into #{} (map :parent-id) shapes)] (when-not (empty? shapes) (let [[group changes] (prepare-create-group it objects page-id shapes "Group" false)] (rx/of (dch/commit-changes changes) - (dws/select-shapes (d/ordered-set (:id group)))))))))) + (dws/select-shapes (d/ordered-set (:id group))) + (ptk/data-event :layout/update {:ids parents})))))))) (def ungroup-selected (ptk/reify ::ungroup-selected @@ -219,29 +202,32 @@ (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - file-data (get state :workspace-data) - file (wsh/get-local-file state) prepare (fn [shape-id] - (let [shape (get objects shape-id)] - (cond - (ctk/main-instance? shape) - (remove-component-changes it page-id shape objects file-data file) + (let [shape (get objects shape-id) + changes + (cond + (or (cfh/group-shape? shape) (cfh/bool-shape? shape)) + (remove-group-changes it page-id shape objects) - (or (cph/group-shape? shape) (cph/bool-shape? shape)) - (remove-group-changes it page-id shape objects) + (cfh/frame-shape? shape) + (remove-frame-changes it page-id shape objects))] - (cph/frame-shape? shape) - (remove-frame-changes it page-id shape objects)))) + (cond-> changes + (ctl/grid-layout? objects (:parent-id shape)) + (pcb/update-shapes [(:parent-id shape)] ctl/assign-cells {:with-objects? true})))) - selected (wsh/lookup-selected state) + selected (->> (wsh/lookup-selected state) + (remove #(ctn/has-any-copy-parent? objects (get objects %))) + ;; components can't be ungrouped + (remove #(ctk/instance-head? (get objects %)))) changes-list (sequence (keep prepare) selected) parents (into #{} - (comp (map #(cph/get-parent objects %)) + (comp (map #(cfh/get-parent objects %)) (keep :id)) selected) @@ -255,11 +241,12 @@ :origin it} undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) - (dch/commit-changes changes) - (ptk/data-event :layout/update parents) - (dwu/commit-undo-transaction undo-id) - (dws/select-shapes child-ids)))))) + (when-not (empty? selected) + (rx/of (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (ptk/data-event :layout/update {:ids parents}) + (dwu/commit-undo-transaction undo-id) + (dws/select-shapes child-ids))))))) (def mask-group (ptk/reify ::mask-group @@ -267,8 +254,9 @@ (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - selected (wsh/lookup-selected state) - selected (cph/clean-loops objects selected) + selected (->> (wsh/lookup-selected state) + (cfh/clean-loops objects) + (remove #(ctn/has-any-copy-parent? objects (get objects %)))) shapes (shapes-for-grouping objects selected) first-shape (first shapes)] (when-not (empty? shapes) @@ -290,7 +278,7 @@ (pcb/update-shapes [(:id group)] (fn [group] (assoc group - :masked-group? true + :masked-group true :selrect (:selrect first-shape) :points (:points first-shape) :transform (:transform first-shape) @@ -301,7 +289,7 @@ (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) (dws/select-shapes (d/ordered-set (:id group))) - (ptk/data-event :layout/update [(:id group)]) + (ptk/data-event :layout/update {:ids [(:id group)]}) (dwu/commit-undo-transaction undo-id)))))))) (def unmask-group @@ -319,7 +307,7 @@ (-> changes (pcb/update-shapes [(:id mask)] (fn [shape] - (dissoc shape :masked-group?))) + (dissoc shape :masked-group))) (pcb/resize-parents [(:id mask)]))) (-> (pcb/empty-changes it page-id) (pcb/with-objects objects)) diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs index b6e585811e..2c7c6c2c0a 100644 --- a/frontend/src/app/main/data/workspace/guides.cljs +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -7,27 +7,34 @@ (ns app.main.data.workspace.guides (:require [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.pages.changes-builder :as pcb] [app.common.types.page :as ctp] - [app.main.data.workspace.changes :as dwc] + [app.main.data.events :as ev] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) -(defn make-update-guide [guide] +(defn make-update-guide + [guide] (fn [other] (cond-> other (= (:id other) (:id guide)) (merge guide)))) -(defn update-guides [guide] +(defn update-guides + [guide] (dm/assert! "expected valid guide" - (ctp/guide? guide)) + (ctp/check-page-guide! guide)) (ptk/reify ::update-guides + ev/Event + (-data [_] + (assoc guide ::ev/name "update-guide")) + ptk/WatchEvent (watch [it state _] (let [page (wsh/lookup-page state) @@ -35,19 +42,22 @@ (-> (pcb/empty-changes it) (pcb/with-page page) (pcb/update-page-option :guides assoc (:id guide) guide))] - (rx/of (dwc/commit-changes changes)))))) + (rx/of (dch/commit-changes changes)))))) -(defn remove-guide [guide] +(defn remove-guide + [guide] (dm/assert! "expected valid guide" - (ctp/guide? guide)) + (ctp/check-page-guide! guide)) (ptk/reify ::remove-guide + ev/Event + (-data [_] guide) + ptk/UpdateEvent (update [_ state] (let [sdisj (fnil disj #{})] - (-> state - (update-in [:workspace-guides :hover] sdisj (:id guide))))) + (update-in state [:workspace-guides :hover] sdisj (:id guide)))) ptk/WatchEvent (watch [it state _] @@ -56,7 +66,7 @@ (-> (pcb/empty-changes it) (pcb/with-page page) (pcb/update-page-option :guides dissoc (:id guide)))] - (rx/of (dwc/commit-changes changes)))))) + (rx/of (dch/commit-changes changes)))))) (defn remove-guides [ids] diff --git a/frontend/src/app/main/data/workspace/highlight.cljs b/frontend/src/app/main/data/workspace/highlight.cljs index f9191ccebd..6f91445bae 100644 --- a/frontend/src/app/main/data/workspace/highlight.cljs +++ b/frontend/src/app/main/data/workspace/highlight.cljs @@ -8,7 +8,7 @@ (:require [app.common.data.macros :as dm] [clojure.set :as set] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) ;; --- Manage shape's highlight status diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs index ee38af28b7..1aad31f2f8 100644 --- a/frontend/src/app/main/data/workspace/interactions.cljs +++ b/frontend/src/app/main/data/workspace/interactions.cljs @@ -8,10 +8,9 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] - [app.common.pages :as cp] - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] [app.common.types.page :as ctp] [app.common.types.shape-tree :as ctst] [app.common.types.shape.interactions :as ctsi] @@ -20,30 +19,36 @@ [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.streams :as ms] - [beicon.core :as rx] - [potok.core :as ptk])) + [app.util.mouse :as mse] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) ;; --- Flows (defn add-flow [starting-frame] + + (dm/assert! + "expect uuid" + (uuid? starting-frame)) + (ptk/reify ::add-flow ptk/WatchEvent (watch [it state _] (let [page (wsh/lookup-page state) flows (get-in page [:options :flows] []) - unames (into #{} (map :name flows)) - name (cp/generate-unique-name unames "Flow 1") + unames (cfh/get-used-names flows) + name (cfh/generate-unique-name unames "Flow 1") new-flow {:id (uuid/next) :name name :starting-frame starting-frame}] (rx/of (dch/commit-changes - (-> (pcb/empty-changes it) - (pcb/with-page page) - (pcb/update-page-option :flows ctp/add-flow new-flow)))))))) + (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/update-page-option :flows ctp/add-flow new-flow)))))))) (defn add-flow-selected-frame [] @@ -61,9 +66,9 @@ (watch [it state _] (let [page (wsh/lookup-page state)] (rx/of (dch/commit-changes - (-> (pcb/empty-changes it) - (pcb/with-page page) - (pcb/update-page-option :flows ctp/remove-flow flow-id)))))))) + (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/update-page-option :flows ctp/remove-flow flow-id)))))))) (defn rename-flow [flow-id name] @@ -72,12 +77,12 @@ (ptk/reify ::rename-flow ptk/WatchEvent (watch [it state _] - (let [page (wsh/lookup-page state) ] + (let [page (wsh/lookup-page state)] (rx/of (dch/commit-changes - (-> (pcb/empty-changes it) - (pcb/with-page page) - (pcb/update-page-option :flows ctp/update-flow flow-id - #(ctp/rename-flow % name))))))))) + (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/update-page-option :flows ctp/update-flow flow-id + #(ctp/rename-flow % name))))))))) (defn start-rename-flow [id] @@ -100,7 +105,7 @@ "Check if some frame is origin or destination of any navigate interaction in the page" [objects frame-id] - (let [children (cph/get-children-with-self objects frame-id)] + (let [children (cfh/get-children-with-self objects frame-id)] (or (some ctsi/flow-origin? (map :interactions children)) (some #(ctsi/flow-to? % frame-id) (map :interactions (vals objects)))))) @@ -112,7 +117,7 @@ (watch [_ state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - frame (cph/get-root-frame objects (:id shape)) + frame (cfh/get-root-frame objects (:id shape)) flows (get-in state [:workspace-data :pages-index page-id @@ -120,16 +125,16 @@ :flows] []) flow (ctp/get-frame-flow flows (:id frame))] (rx/concat - (rx/of (dch/update-shapes [(:id shape)] - (fn [shape] - (let [new-interaction (-> ctsi/default-interaction - (ctsi/set-destination destination) - (assoc :position-relative-to (:id shape)))] - (update shape :interactions - ctsi/add-interaction new-interaction))))) - (when (and (not (connected-frame? objects (:id frame))) - (nil? flow)) - (rx/of (add-flow (:id frame)))))))))) + (rx/of (dch/update-shapes [(:id shape)] + (fn [shape] + (let [new-interaction (-> ctsi/default-interaction + (ctsi/set-destination destination) + (assoc :position-relative-to (:id shape)))] + (update shape :interactions + ctsi/add-interaction new-interaction))))) + (when (and (not (connected-frame? objects (:id frame))) + (nil? flow)) + (rx/of (add-flow (:id frame)))))))))) (defn remove-interaction [shape index] @@ -137,9 +142,9 @@ ptk/WatchEvent (watch [_ _ _] (rx/of (dch/update-shapes [(:id shape)] - (fn [shape] - (update shape :interactions - ctsi/remove-interaction index))))))) + (fn [shape] + (update shape :interactions + ctsi/remove-interaction index))))))) (defn update-interaction [shape index update-fn] @@ -147,9 +152,9 @@ ptk/WatchEvent (watch [_ _ _] (rx/of (dch/update-shapes [(:id shape)] - (fn [shape] - (update shape :interactions - ctsi/update-interaction index update-fn))))))) + (fn [shape] + (update shape :interactions + ctsi/update-interaction index update-fn))))))) (defn remove-all-interactions-nav-to "Remove all interactions that navigate to the given frame." @@ -188,13 +193,13 @@ (watch [_ state stream] (let [initial-pos @ms/mouse-position selected (wsh/lookup-selected state) - stopper (rx/filter ms/mouse-up? stream)] + stopper (mse/drag-stopper stream)] (when (= 1 (count selected)) (rx/concat - (->> ms/mouse-position - (rx/take-until stopper) - (rx/map #(move-edit-interaction initial-pos %))) - (rx/of (finish-edit-interaction index initial-pos)))))))) + (->> ms/mouse-position + (rx/take-until stopper) + (rx/map #(move-edit-interaction initial-pos %))) + (rx/of (finish-edit-interaction index initial-pos)))))))) (defn- get-target-frame [state position] @@ -203,10 +208,12 @@ from-id (-> state wsh/lookup-selected first) from-shape (wsh/lookup-shape state from-id) - from-frame-id (if (cph/frame-shape? from-shape) + from-frame-id (if (cfh/frame-shape? from-shape) from-id (:frame-id from-shape)) - target-frame (ctst/frame-by-position objects position)] + target-frame + (->> (ctst/get-frames-by-position objects position) + (last))] (when (and (not= (:id target-frame) uuid/zero) (not= (:id target-frame) from-frame-id)) @@ -250,33 +257,33 @@ undo-id (js/Symbol)] (rx/of - (dwu/start-undo-transaction undo-id) + (dwu/start-undo-transaction undo-id) - (when (:hide-in-viewer target-frame) + (when (:hide-in-viewer target-frame) ; If the target frame is hidden, we need to unhide it so ; users can navigate to it. - (dch/update-shapes [(:id target-frame)] - #(dissoc % :hide-in-viewer))) + (dch/update-shapes [(:id target-frame)] + #(dissoc % :hide-in-viewer))) - (cond - (or (nil? shape) + (cond + (or (nil? shape) ;; Didn't changed the position for the interaction - (= position initial-pos) + (= position initial-pos) ;; New interaction but invalid target - (and (nil? index) (nil? target-frame))) - nil + (and (nil? index) (nil? target-frame))) + nil ;; Dropped interaction in an invalid target. We remove it - (and (some? index) (nil? target-frame)) - (remove-interaction shape index) + (and (some? index) (nil? target-frame)) + (remove-interaction shape index) - (nil? index) - (add-new-interaction shape (:id target-frame)) + (nil? index) + (add-new-interaction shape (:id target-frame)) - :else - (update-interaction shape index change-interaction)) + :else + (update-interaction shape index change-interaction)) - (dwu/commit-undo-transaction undo-id)))))) + (dwu/commit-undo-transaction undo-id)))))) ;; --- Overlays @@ -296,7 +303,7 @@ (watch [_ state stream] (let [initial-pos @ms/mouse-position selected (wsh/lookup-selected state) - stopper (rx/filter ms/mouse-up? stream)] + stopper (mse/drag-stopper stream)] (when (= 1 (count selected)) (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -307,16 +314,16 @@ overlay-pos (-> shape (get-in [:interactions index]) :overlay-position) - orig-frame (cph/get-frame objects shape) + orig-frame (cfh/get-frame objects shape) frame-pos (gpt/point (:x orig-frame) (:y orig-frame)) offset (-> initial-pos (gpt/subtract overlay-pos) (gpt/subtract frame-pos))] (rx/concat - (->> ms/mouse-position - (rx/take-until stopper) - (rx/map #(move-overlay-pos % frame-pos offset))) - (rx/of (finish-move-overlay-pos index frame-pos offset))))))))) + (->> ms/mouse-position + (rx/take-until stopper) + (rx/map #(move-overlay-pos % frame-pos offset))) + (rx/of (finish-move-overlay-pos index frame-pos offset))))))))) (defn move-overlay-pos [pos frame-pos offset] @@ -329,33 +336,33 @@ (assoc-in state [:workspace-local :move-overlay-to] pos))))) (defn finish-move-overlay-pos - [index frame-pos offset] - (ptk/reify ::finish-move-overlay-pos + [index frame-pos offset] + (ptk/reify ::finish-move-overlay-pos ptk/UpdateEvent (update [_ state] (-> state (d/dissoc-in [:workspace-local :move-overlay-to]) (d/dissoc-in [:workspace-local :move-overlay-index]))) - ptk/WatchEvent - (watch [_ state _] - (let [pos @ms/mouse-position - overlay-pos (-> pos - (gpt/subtract frame-pos) - (gpt/subtract offset)) + ptk/WatchEvent + (watch [_ state _] + (let [pos @ms/mouse-position + overlay-pos (-> pos + (gpt/subtract frame-pos) + (gpt/subtract offset)) - page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - shape (->> state - wsh/lookup-selected - first - (get objects)) + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + shape (->> state + wsh/lookup-selected + first + (get objects)) - interactions (:interactions shape) + interactions (:interactions shape) - new-interactions - (update interactions index - #(ctsi/set-overlay-position % overlay-pos))] + new-interactions + (update interactions index + #(ctsi/set-overlay-position % overlay-pos))] - (rx/of (dch/update-shapes [(:id shape)] #(merge % {:interactions new-interactions}))))))) + (rx/of (dch/update-shapes [(:id shape)] #(merge % {:interactions new-interactions}))))))) diff --git a/frontend/src/app/main/data/workspace/layers.cljs b/frontend/src/app/main/data/workspace/layers.cljs index 9b6423d31e..afde3c03a0 100644 --- a/frontend/src/app/main/data/workspace/layers.cljs +++ b/frontend/src/app/main/data/workspace/layers.cljs @@ -11,9 +11,9 @@ [app.common.math :as mth] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) ;; -- Opacity ---------------------------------------------------------- diff --git a/frontend/src/app/main/data/workspace/layout.cljs b/frontend/src/app/main/data/workspace/layout.cljs index 16fdb7f281..157de23f7a 100644 --- a/frontend/src/app/main/data/workspace/layout.cljs +++ b/frontend/src/app/main/data/workspace/layout.cljs @@ -7,10 +7,12 @@ (ns app.main.data.workspace.layout "Workspace layout management events and helpers." (:require + [app.common.data :as d] [app.common.data.macros :as dm] + [app.main.data.events :as ev] [app.util.storage :refer [storage]] [clojure.set :as set] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (def valid-flags #{:sitemap @@ -20,13 +22,13 @@ :document-history :colorpalette :element-options - :rules - :display-grid - :snap-grid + :rulers + :display-guides + :snap-guides :scale-text :dynamic-alignment :display-artboard-names - :snap-guides + :snap-ruler-guides :show-pixel-grid :snap-pixel-grid}) @@ -50,12 +52,12 @@ #{:sitemap :layers :element-options - :rules - :display-grid - :snap-grid + :rulers + :display-guides + :snap-guides :dynamic-alignment :display-artboard-names - :snap-guides + :snap-ruler-guides :show-pixel-grid :snap-pixel-grid}) @@ -80,8 +82,8 @@ (defn toggle-layout-flag [flag & {:keys [force?] :as opts}] (ptk/reify ::toggle-layout-flag - IDeref - (-deref [_] {:name flag}) + ev/Event + (-data [_] {:name flag}) ptk/UpdateEvent (update [_ state] @@ -114,8 +116,16 @@ (defn set-options-mode [mode] - (dm/assert! (contains? valid-options-mode mode)) + (dm/assert! + "expected valid options mode" + (contains? valid-options-mode mode)) + (ptk/reify ::set-options-mode + ev/Event + (-data [_] + {::ev/origin "workspace:sidebar" + :mode (d/name mode)}) + ptk/UpdateEvent (update [_ state] (assoc-in state [:workspace-global :options-mode] mode)))) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 1ba30dca38..e055ea16e2 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -8,30 +8,38 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.files.features :as ffeat] + [app.common.files.changes :as ch] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] + [app.common.files.libraries-helpers :as cflh] + [app.common.files.shapes-helpers :as cfsh] [app.common.geom.point :as gpt] [app.common.logging :as log] - [app.common.pages :as cp] - [app.common.pages.changes :as ch] - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] [app.common.types.color :as ctc] [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] [app.common.types.file :as ctf] + [app.common.types.shape.layout :as ctl] [app.common.types.typography :as ctt] [app.common.uuid :as uuid] [app.main.data.events :as ev] [app.main.data.messages :as msg] + [app.main.data.modal :as modal] + [app.main.data.workspace :as-alias dw] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.libraries-helpers :as dwlh] + [app.main.data.workspace.notifications :as-alias dwn] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shapes :as dwsh] + [app.main.data.workspace.specialized-panel :as dwsp] [app.main.data.workspace.state-helpers :as wsh] + [app.main.data.workspace.thumbnails :as dwt] + [app.main.data.workspace.transforms :as dwtr] [app.main.data.workspace.undo :as dwu] [app.main.features :as features] + [app.main.features.pointer-map :as fpmap] [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] @@ -39,9 +47,9 @@ [app.util.i18n :refer [tr]] [app.util.router :as rt] [app.util.time :as dt] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) ;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default (log/set-level! :warn) @@ -69,7 +77,11 @@ extract (cond-> {:type (:type change) :raw-change change} shape - (assoc :shape (str prefix (:name shape))) + (assoc :shape (str prefix (:name shape)) + :shape-id (str (:id shape))) + (:obj change) + (assoc :obj (:name (:obj change)) + :obj-id (:id (:obj change))) (:operations change) (assoc :operations (:operations change)))] extract))] @@ -79,7 +91,7 @@ (defn extract-path-if-missing [item] - (let [[path name] (cph/parse-path-name (:name item))] + (let [[path name] (cfh/parse-path-name (:name item))] (if (and (= (:name item) name) (contains? item :path)) @@ -91,12 +103,13 @@ (let [id (uuid/next) color (-> color (assoc :id id) - (assoc :name (or (:color color) + (assoc :name (or (get-in color [:image :name]) + (:color color) (uc/gradient-type->string (get-in color [:gradient :type])))))] (dm/assert! ::ctc/color color) (ptk/reify ::add-color - IDeref - (-deref [_] color) + ev/Event + (-data [_] color) ptk/WatchEvent (watch [it _ _] @@ -107,7 +120,10 @@ (defn add-recent-color [color] - (dm/assert! (ctc/recent-color? color)) + (dm/assert! + "expected valid recent color map" + (ctc/check-recent-color! color)) + (ptk/reify ::add-recent-color ptk/WatchEvent (watch [it _ _] @@ -124,7 +140,7 @@ (defn- do-update-color [it state color file-id] (let [data (get state :workspace-data) - [path name] (cph/parse-path-name (:name color)) + [path name] (cfh/parse-path-name (:name color)) color (assoc color :path path :name name) changes (-> (pcb/empty-changes it) (pcb/with-library-data data) @@ -137,8 +153,11 @@ (defn update-color [color file-id] - (dm/assert! (ctc/color? color)) - (dm/assert! (uuid? file-id)) + + (dm/assert! + "expected valid parameters" + (and (ctc/check-color! color) + (uuid? file-id))) (ptk/reify ::update-color ptk/WatchEvent @@ -166,6 +185,9 @@ [{:keys [id] :as params}] (dm/assert! (uuid? id)) (ptk/reify ::delete-color + ev/Event + (-data [_] {:id id}) + ptk/WatchEvent (watch [it state _] (let [data (get state :workspace-data) @@ -176,8 +198,14 @@ (defn add-media [media] - (dm/assert! (ctf/media-object? media)) + (dm/assert! + "expected valid media object" + (ctf/check-media-object! media)) + (ptk/reify ::add-media + ev/Event + (-data [_] media) + ptk/WatchEvent (watch [it _ _] (let [obj (select-keys media [:id :name :width :height :mtype]) @@ -195,7 +223,7 @@ (let [new-name (str/trim new-name)] (if (str/empty? new-name) (rx/empty) - (let [[path name] (cph/parse-path-name new-name) + (let [[path name] (cfh/parse-path-name new-name) data (get state :workspace-data) object (get-in data [:media id]) new-object (assoc object :path path :name name) @@ -208,6 +236,9 @@ [{:keys [id] :as params}] (dm/assert! (uuid? id)) (ptk/reify ::delete-media + ev/Event + (-data [_] {:id id}) + ptk/WatchEvent (watch [it state _] (let [data (get state :workspace-data) @@ -220,10 +251,13 @@ ([typography] (add-typography typography true)) ([typography edit?] (let [typography (update typography :id #(or % (uuid/next)))] - (dm/assert! (ctt/typography? typography)) + (dm/assert! + "expected valid typography" + (ctt/check-typography! typography)) + (ptk/reify ::add-typography - IDeref - (-deref [_] typography) + ev/Event + (-data [_] typography) ptk/WatchEvent (watch [it _ _] @@ -232,7 +266,7 @@ (rx/of (dch/commit-changes changes) #(cond-> % edit? - (assoc-in [:workspace-global :rename-typography] (:id typography)))))))))) + (assoc-in [:workspace-global :edit-typography] (:id typography)))))))))) (defn- do-update-tipography [it state typography file-id] @@ -249,8 +283,11 @@ (defn update-typography [typography file-id] - (dm/assert! (ctt/typography? typography)) - (dm/assert! (uuid? file-id)) + + (dm/assert! + "expected valid typography and file-id" + (and (ctt/check-typography! typography) + (uuid? file-id))) (ptk/reify ::update-typography ptk/WatchEvent @@ -263,11 +300,14 @@ (dm/assert! (uuid? id)) (dm/assert! (string? new-name)) (ptk/reify ::rename-typography + ev/Event + (-data [_] {:id id :name new-name}) + ptk/WatchEvent (watch [it state _] (when (and (some? new-name) (not= "" new-name)) (let [data (get state :workspace-data) - [path name] (cph/parse-path-name new-name) + [path name] (cfh/parse-path-name new-name) object (get-in data [:typographies id]) new-object (assoc object :path path :name name)] (do-update-tipography it state new-object file-id)))))) @@ -276,6 +316,9 @@ [id] (dm/assert! (uuid? id)) (ptk/reify ::delete-typography + ev/Event + (-data [_] {:id id}) + ptk/WatchEvent (watch [it state _] (let [data (get state :workspace-data) @@ -288,23 +331,27 @@ "This is the second step of the component creation." [selected components-v2] (ptk/reify ::add-component2 - IDeref - (-deref [_] {:num-shapes (count selected)}) + ev/Event + (-data [_] + {::ev/name "add-component" + :shapes (count selected)}) ptk/WatchEvent (watch [it state _] (let [file-id (:current-file-id state) page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - shapes (dwg/shapes-for-grouping objects selected)] + shapes (dwg/shapes-for-grouping objects selected) + parents (into #{} (map :parent-id) shapes)] (when-not (empty? shapes) (let [[root _ changes] - (dwlh/generate-add-component it shapes objects page-id file-id components-v2 + (cflh/generate-add-component it shapes objects page-id file-id components-v2 dwg/prepare-create-group - dwsh/prepare-create-artboard-from-selection)] + cfsh/prepare-create-artboard-from-selection)] (when-not (empty? (:redo-changes changes)) (rx/of (dch/commit-changes changes) - (dws/select-shapes (d/ordered-set (:id root))))))))))) + (dws/select-shapes (d/ordered-set (:id root))) + (ptk/data-event :layout/update {:ids parents}))))))))) (defn add-component "Add a new component to current file library, from the currently selected shapes. @@ -315,12 +362,16 @@ (ptk/reify ::add-component ptk/WatchEvent (watch [_ state _] - (let [objects (wsh/lookup-page-objects state) - selected (->> (wsh/lookup-selected state) - (cph/clean-loops objects)) - components-v2 (features/active-feature? state :components-v2)] - (rx/of (add-component2 selected components-v2)))))) + (let [objects (wsh/lookup-page-objects state) + selected (->> (wsh/lookup-selected state) + (cfh/clean-loops objects)) + selected-objects (map #(get objects %) selected) + components-v2 (features/active-feature? state "components/v2") + ;; We don't want to change the structure of component copies + can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) selected-objects))] + (when can-make-component + (rx/of (add-component2 selected components-v2))))))) (defn add-multiple-components "Add several new components to current file library, from the currently selected shapes." @@ -328,18 +379,23 @@ (ptk/reify ::add-multiple-components ptk/WatchEvent (watch [_ state _] - (let [components-v2 (features/active-feature? state :components-v2) - objects (wsh/lookup-page-objects state) - selected (->> (wsh/lookup-selected state) - (cph/clean-loops objects)) - added-components (map - #(add-component2 [%] components-v2) - selected) + (let [components-v2 (features/active-feature? state "components/v2") + objects (wsh/lookup-page-objects state) + selected (->> (wsh/lookup-selected state) + (cfh/clean-loops objects)) + selected-objects (map #(get objects %) selected) + ;; We don't want to change the structure of component copies + can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) selected-objects)) + added-components (map (fn [id] + (with-meta (add-component2 [id] components-v2) + {:multiple true})) + selected) undo-id (js/Symbol)] - (rx/concat - (rx/of (dwu/start-undo-transaction undo-id)) - (rx/from added-components) - (rx/of (dwu/commit-undo-transaction undo-id))))))) + (when can-make-component + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id)) + (rx/from added-components) + (rx/of (dwu/commit-undo-transaction undo-id)))))))) (defn rename-component "Rename the component with the given id, in the current file library." @@ -353,8 +409,8 @@ (if (str/empty? new-name) (rx/empty) (let [data (get state :workspace-data) - [path name] (cph/parse-path-name new-name) - components-v2 (features/active-feature? state :components-v2) + [path name] (cfh/parse-path-name new-name) + components-v2 (features/active-feature? state "components/v2") update-fn (fn [component] @@ -379,15 +435,21 @@ (ptk/reify ::rename-component-and-main-instance ptk/WatchEvent (watch [_ state _] - (when-let [component (dm/get-in state [:workspace-data :components component-id])] - (let [shape-id (:main-instance-id component) - page-id (:main-instance-page component)] - (rx/concat - (rx/of (rename-component component-id name)) + (let [name (str/trim name) + clean-name (cfh/clean-path name) + valid? (and (not (str/ends-with? name "/")) + (string? clean-name) + (not (str/blank? clean-name))) + component (dm/get-in state [:workspace-data :components component-id])] + (when (and valid? component) + (let [shape-id (:main-instance-id component) + page-id (:main-instance-page component)] + (rx/concat + (rx/of (rename-component component-id clean-name)) ;; NOTE: only when components-v2 is enabled - (when (and shape-id page-id) - (rx/of (dch/update-shapes [shape-id] #(assoc % :name name) {:page-id page-id :stack-undo? true}))))))))) + (when (and shape-id page-id) + (rx/of (dch/update-shapes [shape-id] #(assoc % :name clean-name) {:page-id page-id :stack-undo? true})))))))))) (defn duplicate-component "Create a new component copied from the one with the given id." @@ -395,28 +457,29 @@ (ptk/reify ::duplicate-component ptk/WatchEvent (watch [it state _] - (let [libraries (wsh/get-libraries state) - library (get libraries library-id) - component (ctkl/get-component (:data library) component-id) - new-name (:name component) + (let [libraries (wsh/get-libraries state) + library (get libraries library-id) + component (ctkl/get-component (:data library) component-id) + new-name (:name component) - components-v2 (features/active-feature? state :components-v2) + components-v2 (features/active-feature? state "components/v2") - main-instance-page (when components-v2 - (ctf/get-component-page (:data library) component)) + main-instance-page (when components-v2 + (ctf/get-component-page (:data library) component)) - new-component (assoc component :id (uuid/next)) + new-component-id (when components-v2 + (uuid/next)) [new-component-shape new-component-shapes ; <- null in components-v2 new-main-instance-shape new-main-instance-shapes] - (dwlh/duplicate-component new-component (:data library)) + (dwlh/duplicate-component component new-component-id (:data library)) changes (-> (pcb/empty-changes it nil) (pcb/with-page main-instance-page) (pcb/with-objects (:objects main-instance-page)) (pcb/add-objects new-main-instance-shapes {:ignore-touched true}) (pcb/add-component (if components-v2 - (:id new-component) + new-component-id (:id new-component-shape)) (:path component) new-name @@ -436,16 +499,20 @@ ptk/WatchEvent (watch [it state _] (let [data (get state :workspace-data)] - (if (features/active-feature? state :components-v2) + (if (features/active-feature? state "components/v2") (let [component (ctkl/get-component data id) - page (ctf/get-component-page data component) - shape (ctf/get-component-root data component)] - (rx/of (dwsh/delete-shapes (:id page) #{(:id shape)}))) ;; Deleting main root triggers component delete - (let [changes (-> (pcb/empty-changes it) + page-id (:main-instance-page component) + root-id (:main-instance-id component)] + (rx/of + (dwt/clear-thumbnail (:current-file-id state) page-id root-id "component") + (dwsh/delete-shapes page-id #{root-id}))) ;; Deleting main root triggers component delete + (let [page-id (:current-page-id state) + changes (-> (pcb/empty-changes it) (pcb/with-library-data data) - (pcb/delete-component id))] + (pcb/delete-component id page-id))] (rx/of (dch/commit-changes changes)))))))) + (defn restore-component "Restore a deleted component, with the given id, in the given file library." [library-id component-id] @@ -473,35 +540,55 @@ (rx/of (dch/commit-changes (assoc changes :file-id library-id))))))) + +(defn restore-components + "Restore multiple deleted component definded by a map with the component id as key and the component library as value" + [components-data] + (dm/assert! (map? components-data)) + (ptk/reify ::restore-components + ptk/WatchEvent + (watch [_ _ _] + (let [undo-id (js/Symbol)] + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id)) + (rx/map #(restore-component (val %) (key %)) (rx/from components-data)) + (rx/of (dwu/commit-undo-transaction undo-id))))))) + (defn instantiate-component "Create a new shape in the current page, from the component with the given id in the given file library. Then selects the newly created instance." - [file-id component-id position] - (dm/assert! (uuid? file-id)) - (dm/assert! (uuid? component-id)) - (dm/assert! (gpt/point? position)) - (ptk/reify ::instantiate-component - ptk/WatchEvent - (watch [it state _] - (let [page (wsh/lookup-page state) - libraries (wsh/get-libraries state) + ([file-id component-id position] + (instantiate-component file-id component-id position nil)) + ([file-id component-id position {:keys [start-move? initial-point]}] + (dm/assert! (uuid? file-id)) + (dm/assert! (uuid? component-id)) + (dm/assert! (gpt/point? position)) + (ptk/reify ::instantiate-component + ptk/WatchEvent + (watch [it state _] + (let [page (wsh/lookup-page state) + libraries (wsh/get-libraries state) - changes (-> (pcb/empty-changes it (:id page)) - (pcb/with-objects (:objects page))) + objects (:objects page) + changes (-> (pcb/empty-changes it (:id page)) + (pcb/with-objects objects)) - [new-shape changes] - (dwlh/generate-instantiate-component changes - file-id - component-id - position - page - libraries) - undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) - (dch/commit-changes changes) - (ptk/data-event :layout/update [(:id new-shape)]) - (dws/select-shapes (d/ordered-set (:id new-shape))) - (dwu/commit-undo-transaction undo-id)))))) + [new-shape changes] + (dwlh/generate-instantiate-component changes + objects + file-id + component-id + position + page + libraries) + undo-id (js/Symbol)] + (rx/of (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (ptk/data-event :layout/update {:ids [(:id new-shape)]}) + (dws/select-shapes (d/ordered-set (:id new-shape))) + (when start-move? + (dwtr/start-move initial-point #{(:id new-shape)})) + (dwu/commit-undo-transaction undo-id))))))) (defn detach-component "Remove all references to components in the shape with the given id, @@ -513,64 +600,143 @@ (watch [it state _] (let [file (wsh/get-local-file state) page-id (get state :current-page-id) - container (cph/get-container file :page page-id) + container (cfh/get-container file :page page-id) + libraries (wsh/get-libraries state) changes (-> (pcb/empty-changes it) (pcb/with-container container) (pcb/with-objects (:objects container)) - (dwlh/generate-detach-instance container id))] + (dwlh/generate-detach-instance container libraries id))] (rx/of (dch/commit-changes changes)))))) +(defn detach-components + "Remove all references to components in the shapes with the given ids" + [ids] + (dm/assert! (seq ids)) + (ptk/reify ::detach-components + ptk/WatchEvent + (watch [_ _ _] + (let [undo-id (js/Symbol)] + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id)) + (rx/map #(detach-component %) (rx/from ids)) + (rx/of (dwu/commit-undo-transaction undo-id))))))) + (def detach-selected-components (ptk/reify ::detach-selected-components ptk/WatchEvent (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - file (wsh/get-local-file state) - container (cph/get-container file :page page-id) - selected (->> state - (wsh/lookup-selected) - (cph/clean-loops objects)) + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + file (wsh/get-local-file state) + container (cfh/get-container file :page page-id) + libraries (wsh/get-libraries state) + selected (->> state + (wsh/lookup-selected) + (cfh/clean-loops objects)) + selected-objects (map #(get objects %) selected) + copies (filter ctk/in-component-copy? selected-objects) + can-detach? (and (seq copies) + (every? #(not (ctn/has-any-copy-parent? objects %)) selected-objects)) + changes (when can-detach? + (reduce + (fn [changes id] + (dwlh/generate-detach-instance changes container libraries id)) + (-> (pcb/empty-changes it) + (pcb/with-container container) + (pcb/with-objects objects)) + selected))] - changes (reduce - (fn [changes id] - (dwlh/generate-detach-instance changes container id)) - (-> (pcb/empty-changes it) - (pcb/with-container container) - (pcb/with-objects objects)) - selected)] - - (rx/of (dch/commit-changes changes)))))) + (rx/of (when can-detach? + (dch/commit-changes changes))))))) (defn nav-to-component-file - [file-id] + [file-id component] (dm/assert! (uuid? file-id)) + (dm/assert! (some? component)) (ptk/reify ::nav-to-component-file ptk/WatchEvent (watch [_ state _] - (let [file (get-in state [:workspace-libraries file-id]) - path-params {:project-id (:project-id file) - :file-id (:id file)} - query-params {:page-id (first (get-in file [:data :pages])) - :layout :assets}] + (let [project-id (get-in state [:workspace-libraries file-id :project-id]) + path-params {:project-id project-id + :file-id file-id} + query-params {:page-id (:main-instance-page component) + :component-id (:id component)}] (rx/of (rt/nav-new-window* {:rname :workspace :path-params path-params :query-params query-params})))))) +(defn library-thumbnails-fetched + [thumbnails] + (ptk/reify ::library-thumbnails-fetched + ptk/UpdateEvent + (update [_ state] + (update state :workspace-thumbnails merge thumbnails)))) + +(defn fetch-library-thumbnails + [library-id] + (ptk/reify ::fetch-library-thumbnails + ptk/WatchEvent + (watch [_ _ _] + (->> (rp/cmd! :get-file-object-thumbnails {:file-id library-id :tag "component"}) + (rx/map library-thumbnails-fetched))))) + (defn ext-library-changed - [file-id modified-at revn changes] - (dm/assert! (uuid? file-id)) - (dm/assert! (ch/changes? changes)) + [library-id modified-at revn changes] + (dm/assert! (uuid? library-id)) + (dm/assert! (ch/check-changes! changes)) (ptk/reify ::ext-library-changed ptk/UpdateEvent (update [_ state] (-> state - (update-in [:workspace-libraries file-id] + (update-in [:workspace-libraries library-id] assoc :modified-at modified-at :revn revn) - (d/update-in-when [:workspace-libraries file-id :data] - cp/process-changes changes))))) + (d/update-in-when [:workspace-libraries library-id :data] + ch/process-changes changes))) + + ptk/WatchEvent + (watch [_ _ stream] + (let [stopper-s (rx/filter (ptk/type? ::ext-library-changed) stream)] + (->> + (rx/merge + (->> (rx/of library-id) + (rx/delay 5000) + (rx/map fetch-library-thumbnails))) + + (rx/take-until stopper-s)))))) + +(defn sync-head + [id] + (ptk/reify ::sync-head + ptk/WatchEvent + (watch [it state _] + (log/info :msg "SYNC-head of shape" :id (str id)) + (let [file (wsh/get-local-file state) + file-full (wsh/get-local-file-full state) + libraries (wsh/get-libraries state) + + page-id (:current-page-id state) + container (cfh/get-container file :page page-id) + objects (:objects container) + + shape-inst (ctn/get-shape container id) + parent (get objects (:parent-id shape-inst)) + head (ctn/get-component-shape container parent) + + components-v2 + (features/active-feature? state "components/v2") + + changes + (-> (pcb/empty-changes it) + (pcb/with-container container) + (pcb/with-objects (:objects container)) + (dwlh/generate-sync-shape-direct file-full libraries container (:id head) false components-v2))] + + (log/debug :msg "SYNC-head finished" :js/rchanges (log-changes + (:redo-changes changes) + file)) + (rx/of (dch/commit-changes changes)))))) (defn reset-component "Cancels all modifications in the shape with the given id, and all its children, in @@ -583,24 +749,49 @@ (watch [it state _] (log/info :msg "RESET-COMPONENT of shape" :id (str id)) (let [file (wsh/get-local-file state) + file-full (wsh/get-local-file-full state) libraries (wsh/get-libraries state) page-id (:current-page-id state) - container (cph/get-container file :page page-id) + container (cfh/get-container file :page page-id) components-v2 - (features/active-feature? state :components-v2) + (features/active-feature? state "components/v2") + + swap-slot (-> (ctn/get-shape container id) + (ctk/get-swap-slot)) + + undo-id (js/Symbol) changes (-> (pcb/empty-changes it) (pcb/with-container container) (pcb/with-objects (:objects container)) - (dwlh/generate-sync-shape-direct libraries container id true components-v2))] + (dwlh/generate-sync-shape-direct file-full libraries container id true components-v2))] (log/debug :msg "RESET-COMPONENT finished" :js/rchanges (log-changes (:redo-changes changes) file)) - (rx/of (dch/commit-changes changes)))))) + (rx/of + (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (when (some? swap-slot) + (sync-head id)) + (dwu/commit-undo-transaction undo-id)))))) + +(defn reset-components + "Cancels all modifications in the shapes with the given ids" + [ids] + (dm/assert! (seq ids)) + (ptk/reify ::reset-components + ptk/WatchEvent + (watch [_ _ _] + (let [undo-id (js/Symbol)] + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id)) + (rx/map #(reset-component %) (rx/from ids)) + (rx/of (dwu/commit-undo-transaction undo-id))))))) + (defn update-component "Modify the component linked to the shape with the given id, in the @@ -618,10 +809,12 @@ ptk/WatchEvent (watch [it state _] (log/info :msg "UPDATE-COMPONENT of shape" :id (str id) :undo-group undo-group) - (let [page-id (get state :current-page-id) - local-file (wsh/get-local-file state) - container (cph/get-container local-file :page page-id) - shape (ctn/get-shape container id)] + (let [page-id (get state :current-page-id) + local-file (wsh/get-local-file state) + full-file (wsh/get-local-file-full state) + container (cfh/get-container local-file :page page-id) + shape (ctn/get-shape container id) + components-v2 (features/active-feature? state "components/v2")] (when (ctk/instance-head? shape) (let [libraries (wsh/get-libraries state) @@ -630,7 +823,7 @@ (-> (pcb/empty-changes it) (pcb/set-undo-group undo-group) (pcb/with-container container) - (dwlh/generate-sync-shape-inverse libraries container id)) + (dwlh/generate-sync-shape-inverse full-file libraries container id components-v2)) file-id (:component-file shape) file (wsh/get-file state file-id) @@ -667,6 +860,18 @@ (dch/commit-changes (assoc nonlocal-changes :file-id file-id))))))))))) +(defn- update-component-thumbnail-sync + [state component-id file-id tag] + (let [current-file-id (:current-file-id state) + current-file? (= current-file-id file-id) + data (if current-file? + (get state :workspace-data) + (get-in state [:workspace-libraries file-id :data])) + component (ctkl/get-component data component-id) + page-id (:main-instance-page component) + root-id (:main-instance-id component)] + (dwt/request-thumbnail file-id page-id root-id tag "update-component-thumbnail-sync"))) + (defn update-component-sync ([shape-id file-id] (update-component-sync shape-id file-id nil)) ([shape-id file-id undo-group] @@ -674,14 +879,18 @@ ptk/WatchEvent (watch [_ state _] (let [current-file-id (:current-file-id state) + current-file? (= current-file-id file-id) page (wsh/lookup-page state) shape (ctn/get-shape page shape-id) + component-id (:component-id shape) undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) (update-component shape-id undo-group) (sync-file current-file-id file-id :components (:component-id shape) undo-group) - (when (not= current-file-id file-id) + (update-component-thumbnail-sync state component-id file-id "frame") + (update-component-thumbnail-sync state component-id file-id "component") + (when (not current-file?) (sync-file file-id file-id :components (:component-id shape) undo-group)) (dwu/commit-undo-transaction undo-id))))))) @@ -701,22 +910,156 @@ (sync-file file-id file-id :components component-id undo-group)) (dwu/commit-undo-transaction undo-id))))))) -(defn update-component-in-bulk - [shapes file-id] - (ptk/reify ::update-component-in-bulk +(defn update-component-thumbnail + "Update the thumbnail of the component with the given id, in the + current file and in the imported libraries." + [component-id file-id] + (ptk/reify ::update-component-thumbnail ptk/WatchEvent - (watch [_ _ _] - (let [undo-id (js/Symbol)] - (rx/concat - (rx/of (dwu/start-undo-transaction undo-id)) - (rx/map #(update-component-sync (:id %) file-id (uuid/next)) (rx/from shapes)) - (rx/of (dwu/commit-undo-transaction undo-id))))))) + (watch [_ state _] + (rx/of (update-component-thumbnail-sync state component-id file-id "component"))))) -(declare sync-file-2nd-stage) +(defn- find-shape-index + [objects id shape-id] + (let [object (get objects id)] + (when object + (let [shapes (:shapes object)] + (or (->> shapes + (map-indexed (fn [index shape] [shape index])) + (filter #(= shape-id (first %))) + first + second) + 0))))) + +(defn- add-component-for-swap + [shape file page libraries id-new-component index target-cell keep-props-values {:keys [undo-group]}] + (dm/assert! (uuid? id-new-component)) + (ptk/reify ::add-component-for-swap + ptk/WatchEvent + (watch [it _ _] + (let [objects (:objects page) + position (gpt/point (:x shape) (:y shape)) + changes (-> (pcb/empty-changes it (:id page)) + (pcb/set-undo-group undo-group) + (pcb/with-objects objects)) + position (-> position (with-meta {:cell target-cell})) + parent (get objects (:parent-id shape)) + inside-comp? (ctn/in-any-component? objects parent) + + [new-shape changes] + (dwlh/generate-instantiate-component changes + objects + (:id file) + id-new-component + position + page + libraries + nil + (:parent-id shape) + (:frame-id shape) + {:force-frame? true}) + + new-shape (cond-> new-shape + ; if the shape isn't inside a main component, it shouldn't have a swap slot + (and (nil? (ctk/get-swap-slot new-shape)) + inside-comp?) + (update :touched cfh/set-touched-group (-> (ctf/find-swap-slot shape + page + {:id (:id file) + :data file} + libraries) + (ctk/build-swap-slot-group)))) + + changes + (-> changes + ;; Restore the properties + (pcb/update-shapes [(:id new-shape)] #(d/patch-object % keep-props-values)) + + ;; We need to set the same index as the original shape + (pcb/change-parent (:parent-id shape) [new-shape] index {:component-swap true + :ignore-touched true}) + (dwlh/change-touched new-shape + shape + (ctn/make-container page :page) + {}))] + + ;; First delete so we don't break the grid layout cells + (rx/of (dch/commit-changes changes) + (dws/select-shape (:id new-shape) true)))))) + +(defn- component-swap + "Swaps a component with another one" + [shape file-id id-new-component] + (dm/assert! (uuid? id-new-component)) + (dm/assert! (uuid? file-id)) + (ptk/reify ::component-swap + ptk/WatchEvent + (watch [_ state _] + ;; First delete shapes so we have space in the layout otherwise we can have problems + ;; in the grid creating new rows/columns to make space + (let [file (wsh/get-file state file-id) + libraries (wsh/get-libraries state) + page (wsh/lookup-page state) + objects (wsh/lookup-page-objects state) + parent (get objects (:parent-id shape)) + + ;; If the target parent is a grid layout we need to pass the target cell + target-cell (when (ctl/grid-layout? parent) + (ctl/get-cell-by-shape-id parent (:id shape))) + + index (find-shape-index objects (:parent-id shape) (:id shape)) + + ;; Store the properties that need to be maintained when the component is swapped + keep-props-values (select-keys shape ctk/swap-keep-attrs) + + undo-id (js/Symbol) + undo-group (uuid/next)] + (rx/of + (dwu/start-undo-transaction undo-id) + (dwsh/delete-shapes nil (d/ordered-set (:id shape)) {:component-swap true + :undo-id undo-id + :undo-group undo-group}) + (add-component-for-swap shape file page libraries id-new-component index target-cell keep-props-values + {:undo-group undo-group}) + (ptk/data-event :layout/update {:ids [(:parent-id shape)] :undo-group undo-group}) + (dwu/commit-undo-transaction undo-id)))))) + +(defn component-multi-swap + "Swaps several components with another one" + [shapes file-id id-new-component] + (dm/assert! (seq shapes)) + (dm/assert! (uuid? id-new-component)) + (dm/assert! (uuid? file-id)) + (ptk/reify ::component-multi-swap + ev/Event + (-data [_] + {::ev/name "component-swap"}) + + ptk/WatchEvent + (watch [_ state _] + (let [undo-id (js/Symbol)] + (log/info :msg "COMPONENT-SWAP" + :file (dwlh/pretty-file file-id state) + :id-new-component id-new-component + :undo-id undo-id) + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id)) + (rx/map #(component-swap % file-id id-new-component) (rx/from shapes)) + (rx/of (dwu/commit-undo-transaction undo-id)) + (rx/of (dwsp/open-specialized-panel :component-swap))))))) (def valid-asset-types #{:colors :components :typographies}) +(defn set-updating-library + [updating?] + (ptk/reify ::set-updating-library + ptk/UpdateEvent + (update [_ state] + (if updating? + (assoc state :updating-library true) + (dissoc state :updating-library))))) + (defn sync-file "Synchronize the given file from the given library. Walk through all shapes in all pages in the file that use some color, typography or @@ -770,6 +1113,7 @@ (dwlh/generate-sync-library it file-id :colors asset-id library-id state)) (when sync-typographies? (dwlh/generate-sync-library it file-id :typographies asset-id library-id state))]) + file-changes (reduce pcb/concat-changes (-> (pcb/empty-changes it) @@ -781,16 +1125,37 @@ (when sync-typographies? (dwlh/generate-sync-file it file-id :typographies asset-id library-id state))]) - changes (pcb/concat-changes library-changes file-changes)] + changes (pcb/concat-changes library-changes file-changes) + + find-frames (fn [change] + (->> (ch/frames-changed file change) + (map #(assoc %1 :page-id (:page-id change))))) + + updated-frames (->> changes + :redo-changes + (mapcat find-frames) + distinct)] (log/debug :msg "SYNC-FILE finished" :js/rchanges (log-changes (:redo-changes changes) file)) (rx/concat - (rx/of (msg/hide-tag :sync-dialog)) + (rx/of (set-updating-library false) + (msg/hide-tag :sync-dialog)) (when (seq (:redo-changes changes)) (rx/of (dch/commit-changes (assoc changes ;; TODO a ver qué pasa con esto :file-id file-id)))) + (when-not (empty? updated-frames) + (rx/merge + (rx/of (ptk/data-event :layout/update {:ids (map :id updated-frames) :undo-group undo-group})) + (->> (rx/from updated-frames) + (rx/mapcat + (fn [shape] + (rx/of + (dwt/clear-thumbnail file-id (:page-id shape) (:id shape) "frame") + (when-not (= (:frame-id shape) uuid/zero) + (dwt/clear-thumbnail file-id (:page-id shape) (:frame-id shape) "frame")))))))) + (when (not= file-id library-id) ;; When we have just updated the library file, give some time for the ;; update to finish, before marking this file as synced. @@ -801,44 +1166,7 @@ (rx/concat (rx/timer 3000) (rp/cmd! :update-file-library-sync-status {:file-id file-id - :library-id library-id}))) - (when (and (seq (:redo-changes library-changes)) - sync-components?) - (rx/of (sync-file-2nd-stage file-id library-id asset-id undo-group)))))))))) - -(defn- sync-file-2nd-stage - "If some components have been modified, we need to launch another synchronization - to update the instances of the changed components." - ;; TODO: this does not work if there are multiple nested components. Only the - ;; first level will be updated. - ;; To solve this properly, it would be better to launch another sync-file - ;; recursively. But for this not to cause an infinite loop, we need to - ;; implement updated-at at component level, to detect what components have - ;; not changed, and then not to apply sync and terminate the loop. - [file-id library-id asset-id undo-group] - (dm/assert! (uuid? file-id)) - (dm/assert! (uuid? library-id)) - (dm/assert! (or (nil? asset-id) - (uuid? asset-id))) - (ptk/reify ::sync-file-2nd-stage - ptk/WatchEvent - (watch [it state _] - (log/info :msg "SYNC-FILE (2nd stage)" - :file (dwlh/pretty-file file-id state) - :library (dwlh/pretty-file library-id state)) - (let [file (wsh/get-file state file-id) - changes (reduce - pcb/concat-changes - (-> (pcb/empty-changes it) - (pcb/set-undo-group undo-group)) - [(dwlh/generate-sync-file it file-id :components asset-id library-id state) - (dwlh/generate-sync-library it file-id :components asset-id library-id state)])] - - (log/debug :msg "SYNC-FILE (2nd stage) finished" :js/rchanges (log-changes - (:redo-changes changes) - file)) - (when (seq (:redo-changes changes)) - (rx/of (dch/commit-changes (assoc changes :file-id file-id)))))))) + :library-id library-id})))))))))) (def ignore-sync "Mark the file as ignore syncs. All library changes before this moment will not @@ -857,9 +1185,7 @@ (defn assets-need-sync "Get a lazy sequence of all the assets of each type in the library that have been modified after the last sync of the library. The sync date may be - overriden by providing a ignore-until parameter. - - The sequence items are tuples of (page-id shape-id asset-id asset-type)." + overriden by providing a ignore-until parameter." ([library file-data] (assets-need-sync library file-data nil)) ([library file-data ignore-until] (let [sync-date (max (:synced-at library) (or ignore-until 0))] @@ -876,6 +1202,7 @@ ignore-until (dm/get-in state [:workspace-file :ignore-sync-until]) libraries-need-sync (filter #(seq (assets-need-sync % file-data ignore-until)) (vals (get state :workspace-libraries))) + do-more-info #(modal/show! :libraries-dialog {:starting-tab :updates}) do-update #(do (apply st/emit! (map (fn [library] (sync-file (:current-file-id state) (:id library))) @@ -886,13 +1213,51 @@ (when (seq libraries-need-sync) (rx/of (msg/info-dialog - (tr "workspace.updates.there-are-updates") - :inline-actions - [{:label (tr "workspace.updates.update") - :callback do-update} - {:label (tr "workspace.updates.dismiss") - :callback do-dismiss}] - :sync-dialog))))))) + :content (tr "workspace.updates.there-are-updates") + :controls :inline-actions + :links [{:label (tr "workspace.updates.more-info") + :callback do-more-info}] + :actions [{:label (tr "workspace.updates.dismiss") + :type :secondary + :callback do-dismiss} + {:label (tr "workspace.updates.update") + :type :primary + :callback do-update}] + :tag :sync-dialog))))))) + + +(defn touch-component + "Update the modified-at attribute of the component to now" + [id] + (dm/verify! (uuid? id)) + (ptk/reify ::touch-component + cljs.core/IDeref + (-deref [_] [id]) + + ptk/WatchEvent + (watch [it state _] + (let [data (get state :workspace-data) + changes (-> (pcb/empty-changes it) + (pcb/with-library-data data) + (pcb/update-component id #(assoc % :modified-at (dt/now))))] + (rx/of (dch/commit-changes {:origin it + :redo-changes (:redo-changes changes) + :undo-changes [] + :save-undo? false})))))) + +(defn component-changed + "Notify that the component with the given id has changed, so it needs to be updated + in the current file and in the copies. And also update its thumbnails." + [component-id file-id undo-group] + (ptk/reify ::component-changed + cljs.core/IDeref + (-deref [_] [component-id file-id]) + + ptk/WatchEvent + (watch [_ _ _] + (rx/of + (touch-component component-id) + (launch-component-sync component-id file-id undo-group))))) (defn watch-component-changes "Watch the state for changes that affect to any main instance. If a change is detected will throw @@ -901,52 +1266,81 @@ (ptk/reify ::watch-component-changes ptk/WatchEvent (watch [_ state stream] - (let [components-v2 (features/active-feature? state :components-v2) + (let [components-v2? (features/active-feature? state "components/v2") - stopper + stopper-s (->> stream - (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) + (rx/filter #(or (= ::dw/finalize-page (ptk/type %)) (= ::watch-component-changes (ptk/type %))))) workspace-data-s + (->> (rx/from-atom refs/workspace-data {:emit-current-value? true}) + (rx/share)) + + workspace-buffer-s (->> (rx/concat - (rx/of nil) - (rx/from-atom refs/workspace-data {:emit-current-value? true})) - ;; Need to get the file data before the change, so deleted shapes - ;; still exist, for example + (rx/take 1 workspace-data-s) + (rx/take 1 workspace-data-s) + workspace-data-s) + ;; Need to get the file data before the change, so deleted shapes + ;; still exist, for example. We initialize the buffer with three + ;; copies of the initial state (rx/buffer 3 1)) - change-s + changes-s (->> stream (rx/filter #(or (dch/commit-changes? %) - (= (ptk/type %) :app.main.data.workspace.notifications/handle-file-change))) + (ptk/type? % ::dwn/handle-file-change))) (rx/observe-on :async)) check-changes (fn [[event [old-data _mid_data _new-data]]] (when old-data - (let [{:keys [file-id changes save-undo? undo-group]} - (deref event) + (let [{:keys [file-id changes save-undo? undo-group]} (deref event) - components-changed + changed-components (when (or (nil? file-id) (= file-id (:id old-data))) - (reduce #(into %1 (ch/components-changed old-data %2)) - #{} - changes))] + (->> changes + (map (partial ch/components-changed old-data)) + (reduce into #{})))] - (when (and (d/not-empty? components-changed) save-undo?) - (log/info :msg "DETECTED COMPONENTS CHANGED" - :ids (map str components-changed) - :undo-group undo-group) - (run! st/emit! - (map #(launch-component-sync % (:id old-data) undo-group) - components-changed))))))] + (if (d/not-empty? changed-components) + (if save-undo? + (do (log/info :msg "DETECTED COMPONENTS CHANGED" + :ids (map str changed-components) + :undo-group undo-group) - (when components-v2 - (->> change-s - (rx/with-latest-from workspace-data-s) - (rx/map check-changes) - (rx/take-until stopper))))))) + (->> (rx/from changed-components) + (rx/map #(component-changed % (:id old-data) undo-group)))) + ;; even if save-undo? is false, we need to update the :modified-date of the component + ;; (for example, for undos) + (->> (rx/from changed-components) + (rx/map #(touch-component %)))) + (rx/empty))))) + + changes-s + (->> changes-s + (rx/with-latest-from workspace-buffer-s) + (rx/mapcat check-changes) + (rx/share)) + + notifier-s + (->> changes-s + (rx/debounce 5000) + (rx/tap #(log/trc :hint "buffer initialized")))] + + (when components-v2? + (->> (rx/merge + changes-s + + (->> changes-s + (rx/map deref) + (rx/buffer-until notifier-s) + (rx/mapcat #(into #{} %)) + (rx/map (fn [[component-id file-id]] + (update-component-thumbnail component-id file-id))))) + + (rx/take-until stopper-s))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Backend interactions @@ -956,9 +1350,11 @@ [id is-shared] {:pre [(uuid? id) (boolean? is-shared)]} (ptk/reify ::set-file-shared - IDeref - (-deref [_] - {::ev/origin "workspace" :id id :shared is-shared}) + ev/Event + (-data [_] + {::ev/origin "workspace" + :id id + :shared is-shared}) ptk/UpdateEvent (update [_ state] @@ -992,6 +1388,12 @@ (defn link-file-to-library [file-id library-id] (ptk/reify ::attach-library + ev/Event + (-data [_] + {::ev/name "attach-library" + :file-id file-id + :library-id library-id}) + ;; NOTE: this event implements UpdateEvent protocol for perform an ;; optimistic update state for make the UI feel more responsive. ptk/UpdateEvent @@ -1004,20 +1406,29 @@ ptk/WatchEvent (watch [_ state _] - (let [features (cond-> ffeat/enabled - (features/active-feature? state :components-v2) - (conj "components/v2"))] + (let [features (features/get-team-enabled-features state)] (rx/merge (->> (rp/cmd! :link-file-to-library {:file-id file-id :library-id library-id}) (rx/ignore)) (->> (rp/cmd! :get-file {:id library-id :features features}) + (rx/merge-map fpmap/resolve-file) (rx/map (fn [file] (fn [state] - (assoc-in state [:workspace-libraries library-id] file)))))))))) + (assoc-in state [:workspace-libraries library-id] file))))) + (->> (rp/cmd! :get-file-object-thumbnails {:file-id library-id :tag "component"}) + (rx/map (fn [thumbnails] + (fn [state] + (update state :workspace-thumbnails merge thumbnails)))))))))) (defn unlink-file-from-library [file-id library-id] (ptk/reify ::detach-library + ev/Event + (-data [_] + {::ev/name "detach-library" + :file-id file-id + :library-id library-id}) + ptk/UpdateEvent (update [_ state] (d/dissoc-in state [:workspace-libraries library-id])) diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index ebe7e1cdde..f6de7fb462 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -8,12 +8,12 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.grid-layout :as gslg] [app.common.logging :as log] - [app.common.pages :as cp] - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.common.text :as txt] [app.common.types.color :as ctc] @@ -21,9 +21,10 @@ [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] [app.common.types.shape-tree :as ctst] + [app.common.types.shape.layout :as ctl] [app.common.types.typography :as cty] - [app.common.uuid :as uuid] [app.main.data.workspace.state-helpers :as wsh] [cljs.spec.alpha :as s] [clojure.set :as set])) @@ -50,6 +51,10 @@ (declare change-touched) (declare change-remote-synced) (declare update-attrs) +(declare update-grid-main-attrs) +(declare update-grid-copy-attrs) +(declare update-flex-child-main-attrs) +(declare update-flex-child-copy-attrs) (declare reposition-shape) (declare make-change) @@ -59,113 +64,101 @@ "" (str "<" (get-in state [:workspace-libraries file-id :name]) ">"))) +(defn pretty-uuid + [uuid] + (let [uuid-str (str uuid)] + (subs uuid-str (- (count uuid-str) 6)))) + ;; ---- Components and instances creation ---- -(defn generate-add-component-changes - [changes root objects file-id page-id components-v2] - (let [name (:name root) - [path name] (cph/parse-path-name name) - - [root-shape new-shapes updated-shapes] - (if-not components-v2 - (ctn/make-component-shape root objects file-id components-v2) - (let [new-id (uuid/next)] - [(assoc root :id new-id) - nil - [(assoc root - :component-id new-id - :component-file file-id - :component-root? true - :main-instance? true)]])) - - changes (-> changes - (pcb/add-component (:id root-shape) - path - name - new-shapes - updated-shapes - (:id root) - page-id))] - [root-shape changes])) - -(defn generate-add-component - "If there is exactly one id, and it's a frame (or a group in v1), and not already a component, - use it as root. Otherwise, create a frame (v2) or group (v1) that contains all ids. Then, make a - component with it, and link all shapes to their corresponding one in the component." - [it shapes objects page-id file-id components-v2 prepare-create-group prepare-create-board] - (let [changes (pcb/empty-changes it page-id) - - [root changes] - (if (and (= (count shapes) 1) - (or (and (= (:type (first shapes)) :group) (not components-v2)) - (= (:type (first shapes)) :frame)) - (not (ctk/instance-head? (first shapes)))) - [(first shapes) (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects))] - (let [root-name (if (= 1 (count shapes)) - (:name (first shapes)) - "Component 1")] - (if-not components-v2 - (prepare-create-group it ; These functions needs to be passed as argument - objects ; to avoid a circular dependence - page-id - shapes - root-name - (not (ctk/instance-head? (first shapes)))) - (prepare-create-board changes - (uuid/next) - (:parent-id (first shapes)) - objects - (map :id shapes) - nil - root-name - true)))) - - [root-shape changes] (generate-add-component-changes changes root objects file-id page-id components-v2)] - [root (:id root-shape) changes])) - (defn duplicate-component "Clone the root shape of the component and all children. Generate new ids from all of them." - [component library-data] + [component new-component-id library-data] (let [components-v2 (dm/get-in library-data [:options :components-v2])] (if components-v2 (let [main-instance-page (ctf/get-component-page library-data component) main-instance-shape (ctf/get-component-root library-data component) + delta (gpt/point (+ (:width main-instance-shape) 50) 0) - position (gpt/point (:x main-instance-shape) (:y main-instance-shape)) + ids-map (volatile! {}) + inverted-ids-map (volatile! {}) + nested-main-heads (volatile! #{}) - component-instance-extra-data (if components-v2 {:main-instance? true} {}) + update-original-shape + (fn [original-shape new-shape] + ; Save some ids for later + (vswap! ids-map assoc (:id original-shape) (:id new-shape)) + (vswap! inverted-ids-map assoc (:id new-shape) (:id original-shape)) + (when (and (ctk/main-instance? original-shape) + (not= (:component-id original-shape) (:id component))) + (vswap! nested-main-heads conj (:id original-shape))) + original-shape) - [new-instance-shape new-instance-shapes] - (when (and (some? main-instance-page) (some? main-instance-shape)) - (ctn/make-component-instance main-instance-page - component - library-data - position - true - component-instance-extra-data))] + update-new-shape + (fn [new-shape _] + (cond-> new-shape + ; Link the new main to the new component + (= (:component-id new-shape) (:id component)) + (assoc :component-id new-component-id) + + :always + (gsh/move delta))) + + [new-instance-shape new-instance-shapes _] + (ctst/clone-shape main-instance-shape + (:parent-id main-instance-shape) + (:objects main-instance-page) + :update-new-shape update-new-shape + :update-original-shape update-original-shape) + + remap-frame + (fn [shape] + ; Remap all frame-ids internal to the component to the new shapes + (update shape :frame-id + #(get @ids-map % (:frame-id shape)))) + + convert-nested-main + (fn [shape] + ; If there is some nested main instance, convert it into a copy of + ; main nested in the original component. + (let [origin-shape-id (get @inverted-ids-map (:id shape)) + objects (:objects main-instance-page) + parent-ids (cfh/get-parent-ids-seq-with-self objects origin-shape-id)] + (cond-> shape + (@nested-main-heads origin-shape-id) + (dissoc :main-instance) + + (some @nested-main-heads parent-ids) + (assoc :shape-ref origin-shape-id)))) + + xf-shape (comp (map remap-frame) + (map convert-nested-main)) + + new-instance-shapes (into [] xf-shape new-instance-shapes)] [nil nil new-instance-shape new-instance-shapes]) (let [component-root (d/seek #(nil? (:parent-id %)) (vals (:objects component))) [new-component-shape new-component-shapes _] - (ctst/clone-object component-root - nil - (get component :objects) - identity)] + (ctst/clone-shape component-root + nil + (get component :objects))] [new-component-shape new-component-shapes nil nil])))) (defn generate-instantiate-component "Generate changes to create a new instance from a component." - ([changes file-id component-id position page libraries] - (generate-instantiate-component changes file-id component-id position page libraries nil nil)) + ([changes objects file-id component-id position page libraries] + (generate-instantiate-component changes objects file-id component-id position page libraries nil nil nil {})) - ([changes file-id component-id position page libraries old-id parent-id] + ([changes objects file-id component-id position page libraries old-id parent-id frame-id + {:keys [force-frame?] + :or {force-frame? false}}] (let [component (ctf/get-component libraries file-id component-id) + parent (when parent-id (get objects parent-id)) library (get libraries file-id) components-v2 (dm/get-in library [:data :options :components-v2]) @@ -175,14 +168,43 @@ component (:data library) position - components-v2) + components-v2 + (cond-> {} + force-frame? (assoc :force-frame-id frame-id))) first-shape (cond-> (first new-shapes) (not (nil? parent-id)) - (assoc :parent-id parent-id)) + (assoc :parent-id parent-id) + (and (not (nil? parent)) (= :frame (:type parent))) + (assoc :frame-id (:id parent)) + (and (not (nil? parent)) (not= :frame (:type parent))) + (assoc :frame-id (:frame-id parent)) + (and (not (nil? parent)) (ctn/in-any-component? objects parent)) + (dissoc :component-root) + (and (nil? parent) (not (nil? frame-id))) + (assoc :frame-id frame-id)) + ;; on copy/paste old id is used later to reorder the paster layers changes (cond-> (pcb/add-object changes first-shape {:ignore-touched true}) - (some? old-id) (pcb/amend-last-change #(assoc % :old-id old-id))) ; on copy/paste old id is used later to reorder the paster layers + (some? old-id) (pcb/amend-last-change #(assoc % :old-id old-id))) + + changes + (if (ctl/grid-layout? objects (:parent-id first-shape)) + (let [target-cell (-> position meta :cell) + [row column] + (if (some? target-cell) + [(:row target-cell) (:column target-cell)] + (gslg/get-drop-cell (:parent-id first-shape) objects position))] + (-> changes + (pcb/update-shapes + [(:parent-id first-shape)] + (fn [shape objects] + (-> shape + (ctl/push-into-cell [(:id first-shape)] row column) + (ctl/assign-cells objects))) + {:with-objects? true}) + (pcb/reorder-grid-children [(:parent-id first-shape)]))) + changes) changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) changes @@ -191,51 +213,87 @@ [new-shape changes]))) (declare generate-detach-recursive) +(declare generate-advance-nesting-level) (defn generate-detach-instance "Generate changes to remove the links between a shape and all its children with a component." - [changes container shape-id] - (log/debug :msg "Detach instance" :shape-id shape-id :container (:id container)) - (generate-detach-recursive changes container shape-id true)) + [changes container libraries shape-id] + (let [shape (ctn/get-shape container shape-id)] + (log/debug :msg "Detach instance" :shape-id shape-id :container (:id container)) + (generate-detach-recursive changes container libraries shape-id true (true? (:component-root shape))))) (defn- generate-detach-recursive - [changes container shape-id first] + [changes container libraries shape-id first component-root?] (let [shape (ctn/get-shape container shape-id)] (if (and (ctk/instance-head? shape) (not first)) - ;; Subinstances are not detached, but converted in top instances - (pcb/update-shapes changes [(:id shape)] #(assoc % :component-root? true)) + ; Subinstances are not detached + (cond-> changes + component-root? + ; If the initial shape was component-root, first level subinstances are converted in top instances + (pcb/update-shapes [shape-id] #(assoc % :component-root true)) + + :always + ; Near shape-refs need to be advanced one level + (generate-advance-nesting-level nil container libraries (:id shape))) + ;; Otherwise, detach the shape and all children (let [children-ids (:shapes shape)] - (reduce #(generate-detach-recursive %1 container %2 false) + (reduce #(generate-detach-recursive %1 container libraries %2 false component-root?) (pcb/update-shapes changes [(:id shape)] ctk/detach-shape) children-ids))))) +(defn- generate-advance-nesting-level + [changes file container libraries shape-id] + (let [children (cfh/get-children-with-self (:objects container) shape-id) + skip-near (fn [changes shape] + (let [ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true})] + (if (some? (:shape-ref ref-shape)) + (pcb/update-shapes changes [(:id shape)] #(assoc % :shape-ref (:shape-ref ref-shape))) + changes)))] + (reduce skip-near changes children))) + (defn prepare-restore-component ([library-data component-id current-page it] (let [component (ctkl/get-deleted-component library-data component-id) page (or (ctf/get-component-page library-data component) - current-page)] - (prepare-restore-component nil library-data component-id it page (gpt/point 0 0) nil nil))) + (when (some #(= (:id current-page) %) (:pages library-data)) ;; If the page doesn't belong to the library, it's not valid + current-page) + (ctpl/get-last-page library-data))] + (prepare-restore-component nil library-data component-id it page (gpt/point 0 0) nil nil nil))) - ([changes library-data component-id it page delta old-id parent-id] - (let [component (ctkl/get-deleted-component library-data component-id) + ([changes library-data component-id it page delta old-id parent-id frame-id] + (let [component (ctkl/get-deleted-component library-data component-id) + parent (get-in page [:objects parent-id]) + main-inst (get-in component [:objects (:main-instance-id component)]) + inside-component? (some? (ctn/get-instance-root (:objects page) parent)) + shapes (cfh/get-children-with-self (:objects component) (:main-instance-id component)) + shapes (map #(gsh/move % delta) shapes) - shapes (cph/get-children-with-self (:objects component) (:main-instance-id component)) - shapes (map #(gsh/move % delta) shapes) - first-shape (cond-> (first shapes) - (not (nil? parent-id)) - (assoc :parent-id parent-id)) - changes (-> (or changes (pcb/empty-changes it)) - (pcb/with-page page) - (pcb/with-objects (:objects page)) - (pcb/with-library-data library-data)) - changes (cond-> (pcb/add-object changes first-shape {:ignore-touched true}) - (some? old-id) (pcb/amend-last-change #(assoc % :old-id old-id))) ; on copy/paste old id is used later to reorder the paster layers - changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) - changes - (rest shapes))] - {:changes (pcb/restore-component changes component-id (:id page)) + first-shape (cond-> (first shapes) + (not (nil? parent-id)) + (assoc :parent-id parent-id) + (not (nil? frame-id)) + (assoc :frame-id frame-id) + (and (nil? frame-id) parent (= :frame (:type parent))) + (assoc :frame-id parent-id) + (and (nil? frame-id) parent (not= :frame (:type parent))) + (assoc :frame-id (:frame-id parent)) + inside-component? + (dissoc :component-root) + (not inside-component?) + (assoc :component-root true)) + + changes (-> (or changes (pcb/empty-changes it)) + (pcb/with-page page) + (pcb/with-objects (:objects page)) + (pcb/with-library-data library-data)) + changes (cond-> (pcb/add-object changes first-shape {:ignore-touched true}) + (some? old-id) (pcb/amend-last-change #(assoc % :old-id old-id))) ; on copy/paste old id is used later to reorder the paster layers + changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) + changes + (rest shapes))] + {:changes (pcb/restore-component changes component-id (:id page) main-inst) :shape (first shapes)}))) ;; ---- General library synchronization functions ---- @@ -260,19 +318,20 @@ (let [file (wsh/get-file state file-id) components-v2 (get-in file [:options :components-v2])] - (loop [pages (vals (get file :pages-index)) + (loop [containers (ctf/object-containers-seq file) changes (pcb/empty-changes it)] - (if-let [page (first pages)] - (recur (next pages) - (pcb/concat-changes - changes - (generate-sync-container it - asset-type - asset-id - library-id - state - (cph/make-container page :page) - components-v2))) + (if-let [container (first containers)] + (do + (recur (next containers) + (pcb/concat-changes + changes + (generate-sync-container it + asset-type + asset-id + library-id + state + container + components-v2)))) changes)))) (defn generate-sync-library @@ -301,14 +360,14 @@ (if-let [local-component (first local-components)] (recur (next local-components) (pcb/concat-changes - changes - (generate-sync-container it - asset-type - asset-id - library-id - state - (cph/make-container local-component :component) - components-v2))) + changes + (generate-sync-container it + asset-type + asset-id + library-id + state + (cfh/make-container local-component :component) + components-v2))) changes)))) (defn- generate-sync-container @@ -316,7 +375,7 @@ or a component) that use assets of the given type in the given library." [it asset-type asset-id library-id state container components-v2] - (if (cph/page? container) + (if (cfh/page? container) (log/debug :msg "Sync page in local file" :page-id (:id container)) (log/debug :msg "Sync component in local library" :component-id (:id container))) @@ -367,8 +426,9 @@ (defmethod generate-sync-shape :components [_ changes _library-id state container shape components-v2] (let [shape-id (:id shape) + file (wsh/get-local-file-full state) libraries (wsh/get-libraries state)] - (generate-sync-shape-direct changes libraries container shape-id false components-v2))) + (generate-sync-shape-direct changes file libraries container shape-id false components-v2))) (defmethod generate-sync-shape :colors [_ changes library-id state _ shape _] @@ -392,7 +452,7 @@ (if-let [typography (get typographies (:typography-ref-id node))] (merge node (dissoc typography :name :id)) (dissoc node :typography-ref-id - :typography-ref-file)))] + :typography-ref-file)))] (generate-sync-text-shape changes shape container update-node))) (defn- get-assets @@ -405,21 +465,35 @@ [changes shape container update-node] (let [old-content (:content shape) new-content (txt/transform-nodes update-node old-content) - changes' (-> changes - (update :redo-changes conj (make-change - container - {:type :mod-obj - :id (:id shape) - :operations [{:type :set - :attr :content - :val new-content}]})) - (update :undo-changes d/preconj (make-change - container - {:type :mod-obj - :id (:id shape) - :operations [{:type :set - :attr :content - :val old-content}]})))] + + redo-change + (make-change + container + {:type :mod-obj + :id (:id shape) + :operations [{:type :set + :attr :content + :val new-content} + {:type :set + :attr :position-data + :val nil}]}) + + undo-change + (make-change + container + {:type :mod-obj + :id (:id shape) + :operations [{:type :set + :attr :content + :val old-content} + {:type :set + :attr :position-data + :val nil}]}) + + changes' (-> changes + (update :redo-changes conj redo-change) + (update :undo-changes conj undo-change))] + (if (= new-content old-content) changes changes'))) @@ -499,7 +573,7 @@ ;; * IF THE INITIAL SHAPE IS THE SUBINSTANCE, the sync is done against ;; the remote component. Therefore, IShape-2-2-1 is synched with ;; Shape-1-1. Then the "touched" flags are reset, and the -;; "remote-synced?" flag is set (it will be set until the shape is +;; "remote-synced" flag is set (it will be set until the shape is ;; touched again or it's synced forced normal or inverse with the ;; near component). ;; @@ -509,42 +583,56 @@ ;; cleared. Then, the "touched" flags THAT ARE TRUE are copied to ;; Shape-2-2-1. This may cause that Shape-2-2-1 is now touched respect ;; to Shape-1-1, and so, some attributes are not copied in a subsequent -;; normal sync. Or, if "remote-synced?" flag is set in IShape-2-2-1, -;; all touched flags are cleared in Shape-2-2-1 and "remote-synced?" +;; normal sync. Or, if "remote-synced" flag is set in IShape-2-2-1, +;; all touched flags are cleared in Shape-2-2-1 and "remote-synced" ;; is removed. ;; ;; * IN AN INVERSE SYNC INITIATED IN THE SUBINSTANCE, the update is done ;; to the remote component. E.g. IShape-2-2-1 attributes are copied into -;; Shape-1-1, and then touched cleared and "remote-synced?" flag set. +;; Shape-1-1, and then touched cleared and "remote-synced" flag set. ;; ;; #### WARNING: there are two conditions that are invisible to user: ;; - When the near shape (Shape-2-2-1) is touched respect the remote ;; one (Shape-1-1), there is no asterisk displayed anywhere. ;; - When the instance shape (IShape-2-2-1) is synced with the remote -;; shape (remote-synced? = true), the user will see that this shape +;; shape (remote-synced = true), the user will see that this shape ;; is different than the one in the near component (Shape-2-2-1) ;; but it's not touched. +(defn- redirect-shaperef ;;Set the :shape-ref of a shape pointing to the :id of its remote-shape + ([container libraries shape] + (redirect-shaperef nil nil shape (ctf/find-remote-shape container libraries shape))) + ([_ _ shape remote-shape] + (if (some? (:shape-ref shape)) + (assoc shape :shape-ref (:id remote-shape)) + shape))) + (defn generate-sync-shape-direct "Generate changes to synchronize one shape that is the root of a component instance, and all its children, from the given component." - [changes libraries container shape-id reset? components-v2] - (log/debug :msg "Sync shape direct" :shape (str shape-id) :reset? reset?) - (let [shape-inst (ctn/get-shape container shape-id)] - (if (ctk/in-component-copy? shape-inst) - (let [library (dm/get-in libraries [(:component-file shape-inst) :data]) - component (or (ctkl/get-component library (:component-id shape-inst)) - (and reset? - (ctkl/get-deleted-component library (:component-id shape-inst)))) + [changes file libraries container shape-id reset? components-v2] + (log/debug :msg "Sync shape direct" :shape-inst (str shape-id) :reset? reset?) + (let [shape-inst (ctn/get-shape container shape-id) + library (dm/get-in libraries [(:component-file shape-inst) :data]) + component (ctkl/get-component library (:component-id shape-inst) true)] + (if (and (ctk/in-component-copy? shape-inst) + (or (ctf/direct-copy? shape-inst component container nil libraries) reset?)) ; In a normal sync, we don't want to sync remote mains, only direct/near + (let [redirect-shaperef (partial redirect-shaperef container libraries) - shape-main (when component - (ctf/get-ref-shape library component shape-inst)) + shape-main (when component + (if (and reset? components-v2) + ;; the reset is against the ref-shape, not against the original shape of the component + (ctf/find-ref-shape file container libraries shape-inst) + (ctf/get-ref-shape library component shape-inst))) - initial-root? (:component-root? shape-inst) + shape-inst (if (and reset? components-v2) + (redirect-shaperef shape-inst shape-main) + shape-inst) + + initial-root? (:component-root shape-inst) root-inst shape-inst - root-main (when component - (ctf/get-component-root library component))] + root-main shape-main] (if component (generate-sync-shape-direct-recursive changes @@ -552,34 +640,53 @@ shape-inst component library + file + libraries shape-main root-inst root-main reset? initial-root? + redirect-shaperef components-v2) - ; If the component is not found, because the master component has been - ; deleted or the library unlinked, do nothing in v2 or detach in v1. + ;; If the component is not found, because the master component has been + ;; deleted or the library unlinked, do nothing in v2 or detach in v1. (if components-v2 changes - (generate-detach-instance changes container shape-id)))) + (generate-detach-instance changes libraries container shape-id)))) changes))) +(defn- find-main-container + "Find the container that has the main shape." + [container-inst shape-inst shape-main library component] + (loop [shape-inst' shape-inst + component' component] + (let [container (ctf/get-component-container library component')] ; TODO: this won't work if some intermediate component is in a different library + (if (some? (ctn/get-shape container (:id shape-main))) ; for this to work we need to have access to the libraries list here + container + (let [parent (ctn/get-shape container-inst (:parent-id shape-inst')) + shape-inst' (ctn/get-head-shape (:objects container-inst) parent) + component' (or (ctkl/get-component library (:component-id shape-inst')) + (ctkl/get-deleted-component library (:component-id shape-inst')))] + (if (some? component) + (recur shape-inst' + component') + nil)))))) + (defn- generate-sync-shape-direct-recursive - [changes container shape-inst component library shape-main root-inst root-main reset? initial-root? components-v2] + [changes container shape-inst component library file libraries shape-main root-inst root-main reset? initial-root? redirect-shaperef components-v2] (log/debug :msg "Sync shape direct recursive" - :shape (str (:name shape-inst)) + :shape-inst (str (:name shape-inst) " " (pretty-uuid (:id shape-inst))) :component (:name component)) (if (nil? shape-main) ;; This should not occur, but protect against it in any case (if components-v2 changes - (generate-detach-instance changes container (:id shape-inst))) + (generate-detach-instance changes container {(:id library) library} (:id shape-inst))) (let [omit-touched? (not reset?) clear-remote-synced? (and initial-root? reset?) set-remote-synced? (and (not initial-root?) reset?) - changes (cond-> changes :always (update-attrs shape-inst @@ -589,6 +696,22 @@ container omit-touched?) + (ctl/flex-layout? shape-main) + (update-flex-child-copy-attrs shape-main + shape-inst + library + component + container + omit-touched?) + + (ctl/grid-layout? shape-main) + (update-grid-copy-attrs shape-main + shape-inst + library + component + container + omit-touched?) + reset? (change-touched shape-inst shape-main @@ -601,22 +724,29 @@ set-remote-synced? (change-remote-synced shape-inst container true)) - component-container (ctf/get-component-container library component) + component-container (find-main-container container shape-inst shape-main library component) children-inst (vec (ctn/get-direct-children container shape-inst)) children-main (vec (ctn/get-direct-children component-container shape-main)) + children-inst (if (and reset? components-v2) + (map #(redirect-shaperef %) children-inst) children-inst) + only-inst (fn [changes child-inst] + (log/trace :msg "Only inst" + :child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))) (if-not (and omit-touched? - (contains? (:touched shape-inst) - :shapes-group)) - (remove-shape changes - child-inst - container - omit-touched?) - changes)) + (contains? (:touched shape-inst) + :shapes-group)) + (remove-shape changes + child-inst + container + omit-touched?) + changes)) only-main (fn [changes child-main] + (log/trace :msg "Only main" + :child-main (str (:name child-main) " " (pretty-uuid (:id child-main)))) (if-not (and omit-touched? (contains? (:touched shape-inst) :shapes-group)) @@ -629,23 +759,40 @@ root-inst root-main omit-touched? - set-remote-synced?) + set-remote-synced? + components-v2) changes)) both (fn [changes child-inst child-main] + (log/trace :msg "Both" + :child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst))) + :child-main (str (:name child-main) " " (pretty-uuid (:id child-main)))) (generate-sync-shape-direct-recursive changes container child-inst component library + file + libraries child-main root-inst root-main reset? initial-root? + redirect-shaperef components-v2)) + swapped (fn [changes child-inst child-main] + (log/trace :msg "Match slot" + :child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst))) + :child-main (str (:name child-main) " " (pretty-uuid (:id child-main)))) + ;; For now we don't make any sync here. + changes) + moved (fn [changes child-inst child-main] + (log/trace :msg "Move" + :child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst))) + :child-main (str (:name child-main) " " (pretty-uuid (:id child-main)))) (move-shape changes child-inst @@ -657,26 +804,65 @@ (compare-children changes children-inst children-main + container + component-container + file + libraries only-inst only-main both + swapped moved - false)))) + false + reset? + components-v2)))) + + +(defn- generate-rename-component + [changes id new-name library-data components-v2] + (let [[path name] (cfh/parse-path-name new-name) + update-fn + (fn [component] + (cond-> component + :always + (assoc :path path + :name name) + + (not components-v2) + (update :objects + ;; Give the same name to the root shape + #(assoc-in % [id :name] name))))] + (-> changes + (pcb/with-library-data library-data) + (pcb/update-component id update-fn)))) (defn generate-sync-shape-inverse "Generate changes to update the component a shape is linked to, from the values in the shape and all its children." - [changes libraries container shape-id] + [changes file libraries container shape-id components-v2] (log/debug :msg "Sync shape inverse" :shape (str shape-id)) - (let [shape-inst (ctn/get-shape container shape-id) + (let [redirect-shaperef (partial redirect-shaperef container libraries) + shape-inst (ctn/get-shape container shape-id) library (dm/get-in libraries [(:component-file shape-inst) :data]) component (ctkl/get-component library (:component-id shape-inst)) - shape-main (ctf/get-ref-shape library component shape-inst) - initial-root? (:component-root? shape-inst) + shape-main (when component + (if components-v2 + (ctf/find-remote-shape container libraries shape-inst) + (ctf/get-ref-shape library component shape-inst))) + + shape-inst (if components-v2 + (redirect-shaperef shape-inst shape-main) + shape-inst) + + initial-root? (:component-root shape-inst) root-inst shape-inst - root-main (ctf/get-component-root library component)] + root-main (ctf/get-component-root library component) + + changes (cond-> changes + (and component (contains? (:touched shape-inst) :name-group)) + (generate-rename-component (:component-id shape-inst) (:name shape-inst) library components-v2))] (if component (generate-sync-shape-inverse-recursive changes @@ -684,14 +870,18 @@ shape-inst component library + file + libraries shape-main root-inst root-main - initial-root?) + initial-root? + redirect-shaperef + components-v2) changes))) (defn- generate-sync-shape-inverse-recursive - [changes container shape-inst component library shape-main root-inst root-main initial-root?] + [changes container shape-inst component library file libraries shape-main root-inst root-main initial-root? redirect-shaperef components-v2] (log/trace :msg "Sync shape inverse recursive" :shape (str (:name shape-inst)) :component (:name component)) @@ -722,6 +912,20 @@ component-container {:copy-touched? true})) + (ctl/flex-layout? shape-main) + (update-flex-child-main-attrs shape-main + shape-inst + component-container + container + omit-touched?) + + (ctl/grid-layout? shape-main) + (update-grid-main-attrs shape-main + shape-inst + component-container + container + omit-touched?) + clear-remote-synced? (change-remote-synced shape-inst container nil) @@ -733,6 +937,10 @@ children-main (mapv #(ctn/get-shape component-container %) (:shapes shape-main)) + children-inst (if components-v2 + (map #(redirect-shaperef %) children-inst) + children-inst) + only-inst (fn [changes child-inst] (add-shape-to-main changes child-inst @@ -742,7 +950,8 @@ component-container container root-inst - root-main)) + root-main + components-v2)) only-main (fn [changes child-main] (remove-shape changes @@ -756,10 +965,21 @@ child-inst component library + file + libraries child-main root-inst root-main - initial-root?)) + initial-root? + redirect-shaperef + components-v2)) + + swapped (fn [changes child-inst child-main] + (log/trace :msg "Match slot" + :child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst))) + :child-main (str (:name child-main) " " (pretty-uuid (:id child-main)))) + ;; For now we don't make any sync here. + changes) moved (fn [changes child-inst child-main] (move-shape @@ -774,11 +994,18 @@ (compare-children changes children-inst children-main + container + component-container + file + libraries only-inst only-main both + swapped moved - true) + true + true + components-v2) ;; The inverse sync may be made on a component that is inside a ;; remote library. We need to separate changes that are from @@ -790,18 +1017,21 @@ (-> changes (update :redo-changes (partial mapv check-local)) - (update :undo-changes (partial mapv check-local)))))) + (update :undo-changes (partial map check-local)))))) -; ---- Operation generation helpers ---- +;; ---- Operation generation helpers ---- (defn- compare-children - [changes children-inst children-main only-inst-cb only-main-cb both-cb moved-cb inverse?] + [changes children-inst children-main container-inst container-main file libraries only-inst-cb only-main-cb both-cb swapped-cb moved-cb inverse? reset? components-v2] + (log/trace :msg "Compare children") (loop [children-inst (seq (or children-inst [])) children-main (seq (or children-main [])) changes changes] (let [child-inst (first children-inst) child-main (first children-main)] + (log/trace :main (str (:name child-main) " " (pretty-uuid (:id child-main))) + :inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))) (cond (and (nil? child-inst) (nil? child-main)) changes @@ -813,13 +1043,20 @@ (reduce only-inst-cb changes children-inst) :else - (if (ctk/is-main-of? child-main child-inst) + (if (or (ctk/is-main-of? child-main child-inst components-v2) + (and (ctf/match-swap-slot? child-main child-inst container-inst container-main file libraries) (not reset?))) (recur (next children-inst) (next children-main) - (both-cb changes child-inst child-main)) + (if (ctk/is-main-of? child-main child-inst components-v2) + (both-cb changes child-inst child-main) + (swapped-cb changes child-inst child-main))) - (let [child-inst' (d/seek #(ctk/is-main-of? child-main %) children-inst) - child-main' (d/seek #(ctk/is-main-of? % child-inst) children-main)] + (let [child-inst' (d/seek #(or (ctk/is-main-of? child-main % components-v2) + (and (ctf/match-swap-slot? child-main % container-inst container-main file libraries) (not reset?))) + children-inst) + child-main' (d/seek #(or (ctk/is-main-of? % child-inst components-v2) + (and (ctf/match-swap-slot? % child-inst container-inst container-main file libraries) (not reset?))) + children-main)] (cond (nil? child-inst') (recur children-inst @@ -833,26 +1070,39 @@ :else (if inverse? - (recur (next children-inst) - (remove #(= (:id %) (:id child-main')) children-main) - (-> changes - (both-cb child-inst' child-main) - (moved-cb child-inst child-main'))) - (recur (remove #(= (:id %) (:id child-inst')) children-inst) - (next children-main) - (-> changes + (let [is-main? (ctk/is-main-of? child-inst child-main' components-v2)] + (recur (next children-inst) + (remove #(= (:id %) (:id child-main')) children-main) + (cond-> changes + is-main? (both-cb child-inst child-main') - (moved-cb child-inst' child-main))))))))))) + (not is-main?) + (swapped-cb child-inst child-main') + :always + (moved-cb child-inst child-main')))) + (let [is-main? (ctk/is-main-of? child-inst' child-main components-v2)] + (recur (remove #(= (:id %) (:id child-inst')) children-inst) + (next children-main) + (cond-> changes + is-main? + (both-cb child-inst' child-main) + (not is-main?) + (swapped-cb child-inst' child-main) + :always + (moved-cb child-inst' child-main)))))))))))) (defn- add-shape-to-instance - [changes component-shape index component-page container root-instance root-main omit-touched? set-remote-synced?] - (log/info :msg (str "ADD [P] " (:name component-shape))) + [changes component-shape index component-page container root-instance root-main omit-touched? set-remote-synced? components-v2] + (log/info :msg (str "ADD [P " (pretty-uuid (:id container)) "] " + (:name component-shape) + " " + (pretty-uuid (:id component-shape)))) (let [component-parent-shape (ctn/get-shape component-page (:parent-id component-shape)) - parent-shape (d/seek #(ctk/is-main-of? component-parent-shape %) - (cph/get-children-with-self (:objects container) + parent-shape (d/seek #(ctk/is-main-of? component-parent-shape % components-v2) + (cfh/get-children-with-self (:objects container) (:id root-instance))) all-parents (into [(:id parent-shape)] - (cph/get-parent-ids (:objects container) + (cfh/get-parent-ids (:objects container) (:id parent-shape))) update-new-shape (fn [new-shape original-shape] @@ -860,67 +1110,74 @@ root-main root-instance)] (cond-> new-shape - true - (assoc :frame-id (:frame-id parent-shape)) - - (nil? (:shape-ref original-shape)) - (assoc :shape-ref (:id original-shape)) + (= (:id original-shape) (:id component-shape)) + (assoc :frame-id (if (= (:type parent-shape) :frame) + (:id parent-shape) + (:frame-id parent-shape))) set-remote-synced? - (assoc :remote-synced? true)))) + (assoc :remote-synced true) + + :always + (-> (assoc :shape-ref (:id original-shape)) + (dissoc :touched))))) ; New shape, by definition, is synced to the main shape update-original-shape (fn [original-shape _new-shape] original-shape) [_ new-shapes _] - (ctst/clone-object component-shape + (ctst/clone-shape component-shape (:id parent-shape) (get component-page :objects) - update-new-shape - update-original-shape) + :update-new-shape update-new-shape + :update-original-shape update-original-shape + :dest-objects (get container :objects)) add-obj-change (fn [changes shape'] (update changes :redo-changes conj (make-change - container - (as-> {:type :add-obj - :id (:id shape') - :parent-id (:parent-id shape') - :index index - :ignore-touched true - :obj shape'} $ - (cond-> $ - (:frame-id shape') - (assoc :frame-id (:frame-id shape'))))))) + container + (as-> {:type :add-obj + :id (:id shape') + :parent-id (:parent-id shape') + :index index + :ignore-touched true + :obj shape'} $ + (cond-> $ + (:frame-id shape') + (assoc :frame-id (:frame-id shape'))))))) del-obj-change (fn [changes shape'] - (update changes :undo-changes d/preconj + (update changes :undo-changes conj (make-change - container - {:type :del-obj - :id (:id shape') - :ignore-touched true}))) + container + {:type :del-obj + :id (:id shape') + :ignore-touched true}))) changes' (reduce add-obj-change changes new-shapes) changes' (update changes' :redo-changes conj (make-change - container - {:type :reg-objects - :shapes all-parents})) + container + {:type :reg-objects + :shapes all-parents})) changes' (reduce del-obj-change changes' new-shapes)] - (if (and (cph/touched-group? parent-shape :shapes-group) omit-touched?) + (if (and (cfh/touched-group? parent-shape :shapes-group) omit-touched?) changes changes'))) (defn- add-shape-to-main - [changes shape index component component-container page root-instance root-main] - (log/info :msg (str "ADD [C] " (:name shape))) + [changes shape index component component-container page root-instance root-main components-v2] + (log/info :msg (str "ADD [C " (pretty-uuid (:id component-container)) "] " + (:name shape) + " " + (pretty-uuid (:id shape)))) (let [parent-shape (ctn/get-shape page (:parent-id shape)) - component-parent-shape (d/seek #(ctk/is-main-of? % parent-shape) - (cph/get-children-with-self (:objects component-container) + component-parent-shape (d/seek #(ctk/is-main-of? % parent-shape components-v2) + (cfh/get-children-with-self (:objects component-container) (:id root-main))) all-parents (into [(:id component-parent-shape)] - (cph/get-parent-ids (:objects component-container) + (cfh/get-parent-ids (:objects component-container) (:id component-parent-shape))) update-new-shape (fn [new-shape _original-shape] @@ -929,17 +1186,18 @@ root-main)) update-original-shape (fn [original-shape new-shape] - (if-not (:shape-ref original-shape) - (assoc original-shape - :shape-ref (:id new-shape)) - original-shape)) + (assoc original-shape + :shape-ref (:id new-shape))) [_new-shape new-shapes updated-shapes] - (ctst/clone-object shape - (:id component-parent-shape) - (get page :objects) - update-new-shape - update-original-shape) + (ctst/clone-shape shape + (:id component-parent-shape) + (get page :objects) + :update-new-shape update-new-shape + :update-original-shape update-original-shape + :frame-id (if (cfh/frame-shape? component-parent-shape) + (:id component-parent-shape) + (:frame-id component-parent-shape))) add-obj-change (fn [changes shape'] (update changes :redo-changes conj @@ -948,36 +1206,55 @@ {:type :add-obj :id (:id shape') :parent-id (:parent-id shape') + :frame-id (:frame-id shape') :index index :ignore-touched true - :obj shape'}) - - (ctn/page? component-container) - (assoc :frame-id (:frame-id shape'))))) + :obj shape'})))) mod-obj-change (fn [changes shape'] - (update changes :redo-changes conj - {:type :mod-obj - :page-id (:id page) - :id (:id shape') - :operations [{:type :set - :attr :component-id - :val (:component-id shape')} - {:type :set - :attr :component-file - :val (:component-file shape')} - {:type :set - :attr :component-root? - :val (:component-root? shape')} - {:type :set - :attr :shape-ref - :val (:shape-ref shape')} - {:type :set - :attr :touched - :val (:touched shape')}]})) + (let [shape-original (ctn/get-shape page (:id shape'))] + (-> changes + (update :redo-changes conj + {:type :mod-obj + :page-id (:id page) + :id (:id shape') + :operations [{:type :set + :attr :component-id + :val (:component-id shape')} + {:type :set + :attr :component-file + :val (:component-file shape')} + {:type :set + :attr :component-root + :val (:component-root shape')} + {:type :set + :attr :shape-ref + :val (:shape-ref shape')} + {:type :set + :attr :touched + :val (:touched shape')}]}) + (update :undo-changes conj + {:type :mod-obj + :page-id (:id page) + :id (:id shape-original) + :operations [{:type :set + :attr :component-id + :val (:component-id shape-original)} + {:type :set + :attr :component-file + :val (:component-file shape-original)} + {:type :set + :attr :component-root + :val (:component-root shape-original)} + {:type :set + :attr :shape-ref + :val (:shape-ref shape-original)} + {:type :set + :attr :touched + :val (:touched shape-original)}]})))) del-obj-change (fn [changes shape'] - (update changes :undo-changes d/preconj + (update changes :undo-changes conj {:type :del-obj :id (:id shape') :page-id (:id page) @@ -995,13 +1272,17 @@ (defn- remove-shape [changes shape container omit-touched?] (log/info :msg (str "REMOVE-SHAPE " - (if (cph/page? container) "[P] " "[C] ") - (:name shape))) + (if (cfh/page? container) "[P " "[C ") + (pretty-uuid (:id container)) "] " + (:name shape) + " " + (pretty-uuid (:id shape)))) (let [objects (get container :objects) - parents (cph/get-parent-ids objects (:id shape)) + parents (cfh/get-parent-ids objects (:id shape)) parent (first parents) - children (cph/get-children-ids objects (:id shape)) - ids (into [(:id shape)] children) + children (cfh/get-children-ids objects (:id shape)) + ids (-> (into [(:id shape)] children) + (reverse)) ;; Remove from bottom to top add-redo-change (fn [changes id] (update changes :redo-changes conj @@ -1013,12 +1294,12 @@ add-undo-change (fn [changes id] (let [shape' (get objects id)] - (update changes :undo-changes d/preconj + (update changes :undo-changes conj (make-change container (as-> {:type :add-obj :id id - :index (cph/get-position-on-parent objects id) + :index (cfh/get-position-on-parent objects id) :parent-id (:parent-id shape') :ignore-touched true :obj shape'} $ @@ -1030,23 +1311,25 @@ (update :redo-changes conj (make-change container {:type :reg-objects - :shapes (vec parents)})) - (add-undo-change (:id shape))) + :shapes (vec parents)}))) changes' (reduce add-undo-change changes' - children)] + ids)] - (if (and (cph/touched-group? parent :shapes-group) omit-touched?) + (if (and (cfh/touched-group? parent :shapes-group) omit-touched?) changes changes'))) (defn- move-shape [changes shape index-before index-after container omit-touched?] (log/info :msg (str "MOVE " - (if (cph/page? container) "[P] " "[C] ") + (if (cfh/page? container) "[P " "[C ") + (pretty-uuid (:id container)) "] " (:name shape) " " + (pretty-uuid (:id shape)) + " " index-before " -> " index-after)) @@ -1054,60 +1337,68 @@ changes' (-> changes (update :redo-changes conj (make-change - container - {:type :mov-objects - :parent-id (:parent-id shape) - :shapes [(:id shape)] - :index index-after - :ignore-touched true})) - (update :undo-changes d/preconj (make-change - container - {:type :mov-objects - :parent-id (:parent-id shape) - :shapes [(:id shape)] - :index index-before - :ignore-touched true})))] + container + {:type :mov-objects + :parent-id (:parent-id shape) + :shapes [(:id shape)] + :index index-after + :ignore-touched true + :syncing true})) + (update :undo-changes conj (make-change + container + {:type :mov-objects + :parent-id (:parent-id shape) + :shapes [(:id shape)] + :index index-before + :ignore-touched true + :syncing true})))] - (if (and (cph/touched-group? parent :shapes-group) omit-touched?) + (if (and (cfh/touched-group? parent :shapes-group) omit-touched?) changes changes'))) -(defn- change-touched +(defn change-touched [changes dest-shape origin-shape container {:keys [reset-touched? copy-touched?] :as options}] - (if (or (nil? (:shape-ref dest-shape)) - (not (or reset-touched? copy-touched?))) + (if (nil? (:shape-ref dest-shape)) changes (do (log/info :msg (str "CHANGE-TOUCHED " - (if (cph/page? container) "[P] " "[C] ") - (:name dest-shape)) + (if (cfh/page? container) "[P " "[C ") + (pretty-uuid (:id container)) "] " + (:name dest-shape) + " " + (pretty-uuid (:id dest-shape))) :options options) (let [new-touched (cond reset-touched? nil + copy-touched? - (if (:remote-synced? origin-shape) + (if (:remote-synced origin-shape) nil (set/union - (:touched dest-shape) - (:touched origin-shape))))] + (:touched dest-shape) + (:touched origin-shape))) + + :else + (:touched dest-shape))] (-> changes (update :redo-changes conj (make-change - container - {:type :mod-obj - :id (:id dest-shape) - :operations - [{:type :set-touched - :touched new-touched}]})) - (update :undo-changes d/preconj (make-change - container - {:type :mod-obj - :id (:id dest-shape) - :operations - [{:type :set-touched - :touched (:touched dest-shape)}]}))))))) + container + {:type :mod-obj + :id (:id dest-shape) + :operations + [{:type :set-touched + :touched new-touched}]})) + (update :undo-changes conj (make-change + container + {:type :mod-obj + :id (:id dest-shape) + :operations + [{:type :set-touched + :touched (:touched dest-shape)}]}))))))) (defn- change-remote-synced [changes shape container remote-synced?] @@ -1115,24 +1406,27 @@ changes (do (log/info :msg (str "CHANGE-REMOTE-SYNCED? " - (if (cph/page? container) "[P] " "[C] ") - (:name shape)) - :remote-synced? remote-synced?) + (if (cfh/page? container) "[P " "[C ") + (pretty-uuid (:id container)) "] " + (:name shape) + " " + (pretty-uuid (:id shape))) + :remote-synced remote-synced?) (-> changes (update :redo-changes conj (make-change - container - {:type :mod-obj - :id (:id shape) - :operations - [{:type :set-remote-synced - :remote-synced? remote-synced?}]})) - (update :undo-changes d/preconj (make-change - container - {:type :mod-obj - :id (:id shape) - :operations - [{:type :set-remote-synced - :remote-synced? (:remote-synced? shape)}]})))))) + container + {:type :mod-obj + :id (:id shape) + :operations + [{:type :set-remote-synced + :remote-synced remote-synced?}]})) + (update :undo-changes conj (make-change + container + {:type :mod-obj + :id (:id shape) + :operations + [{:type :set-remote-synced + :remote-synced (:remote-synced shape)}]})))))) (defn- update-attrs "The main function that implements the attribute sync algorithm. Copy @@ -1144,49 +1438,59 @@ (log/info :msg (str "SYNC " (:name origin-shape) + " " + (pretty-uuid (:id origin-shape)) " -> " - (if (cph/page? container) "[P] " "[C] ") - (:name dest-shape))) + (if (cfh/page? container) "[P " "[C ") + (pretty-uuid (:id container)) "] " + (:name dest-shape) + " " + (pretty-uuid (:id dest-shape)))) - (let [; To synchronize geometry attributes we need to make a prior - ; operation, because coordinates are absolute, but we need to - ; sync only the position relative to the origin of the component. - ; We solve this by moving the origin shape so it is aligned with - ; the dest root before syncing. - ; In case of subinstances, the comparison is always done with the - ; near component, because this is that we are syncing with. + (let [;; To synchronize geometry attributes we need to make a prior + ;; operation, because coordinates are absolute, but we need to + ;; sync only the position relative to the origin of the component. + ;; We solve this by moving the origin shape so it is aligned with + ;; the dest root before syncing. + ;; In case of subinstances, the comparison is always done with the + ;; near component, because this is that we are syncing with. origin-shape (reposition-shape origin-shape origin-root dest-root) touched (get dest-shape :touched #{})] - (loop [attrs (seq (keys cp/component-sync-attrs)) + (loop [attrs (->> (seq (keys ctk/sync-attrs)) + ;; We don't update the flex-child attrs + (remove ctk/swap-keep-attrs) + + ;; We don't do automatic update of the `layout-grid-cells` property. + (remove #(= :layout-grid-cells %))) roperations [] - uoperations []] + uoperations '()] (let [attr (first attrs)] (if (nil? attr) (if (empty? roperations) changes - (let [all-parents (cph/get-parent-ids (:objects container) + (let [all-parents (cfh/get-parent-ids (:objects container) (:id dest-shape))] (-> changes (update :redo-changes conj (make-change - container - {:type :mod-obj - :id (:id dest-shape) - :operations roperations})) + container + {:type :mod-obj + :id (:id dest-shape) + :operations roperations})) (update :redo-changes conj (make-change - container - {:type :reg-objects - :shapes all-parents})) - (update :undo-changes d/preconj (make-change - container - {:type :mod-obj - :id (:id dest-shape) - :operations uoperations})) + container + {:type :reg-objects + :shapes all-parents})) (update :undo-changes conj (make-change - container - {:type :reg-objects - :shapes all-parents}))))) + container + {:type :mod-obj + :id (:id dest-shape) + :operations (vec uoperations)})) + (update :undo-changes concat [(make-change + container + {:type :reg-objects + :shapes all-parents})])))) (let [roperation {:type :set :attr attr :val (get origin-shape attr) @@ -1196,7 +1500,7 @@ :val (get dest-shape attr) :ignore-touched true} - attr-group (get cp/component-sync-attrs attr)] + attr-group (get ctk/sync-attrs attr)] (if (or (= (get origin-shape attr) (get dest-shape attr)) (and (touched attr-group) omit-touched?)) @@ -1205,7 +1509,135 @@ uoperations) (recur (next attrs) (conj roperations roperation) - (d/preconj uoperations uoperation))))))))) + (conj uoperations uoperation))))))))) + +(defn- propagate-attrs + "Helper that puts the origin attributes (attrs) into dest but only if + not touched the group or if omit-touched? flag is true" + [dest origin attrs omit-touched?] + (let [touched (get dest :touched #{})] + (->> attrs + (reduce + (fn [dest attr] + (let [attr-group (get ctk/sync-attrs attr)] + (cond-> dest + (or (not (touched attr-group)) (not omit-touched?)) + (assoc attr (get origin attr))))) + dest)))) + +(defn- update-flex-child-copy-attrs + "Synchronizes the attributes inside the flex-child items (main->copy)" + [changes shape-main shape-copy main-container main-component copy-container omit-touched?] + + (let [new-changes + (-> (pcb/empty-changes) + (pcb/with-container copy-container) + (pcb/with-objects (:objects copy-container)) + + ;; The layout-item-sizing needs to be update when the parent is auto or fix + (pcb/update-shapes + [(:id shape-copy)] + (fn [shape-copy] + (cond-> shape-copy + (contains? #{:auto :fix} (:layout-item-h-sizing shape-main)) + (propagate-attrs shape-main #{:layout-item-h-sizing} omit-touched?) + + (contains? #{:auto :fix} (:layout-item-h-sizing shape-main)) + (propagate-attrs shape-main #{:layout-item-v-sizing} omit-touched?))) + {:ignore-touched true}) + + ;; Update the child flex properties from the parent + (pcb/update-shapes + (:shapes shape-copy) + (fn [child-copy] + (let [child-main (ctf/get-ref-shape main-container main-component child-copy)] + (-> child-copy + (propagate-attrs child-main ctk/swap-keep-attrs omit-touched?)))) + {:ignore-touched true}))] + (pcb/concat-changes changes new-changes))) + +(defn- update-flex-child-main-attrs + "Synchronizes the attributes inside the flex-child items (copy->main)" + [changes shape-main shape-copy main-container copy-container omit-touched?] + (let [new-changes + (-> (pcb/empty-changes) + (pcb/with-page main-container) + (pcb/with-objects (:objects main-container)) + + ;; The layout-item-sizing needs to be update when the parent is auto or fix + (pcb/update-shapes + [(:id shape-main)] + (fn [shape-main] + (cond-> shape-main + (contains? #{:auto :fix} (:layout-item-h-sizing shape-copy)) + (propagate-attrs shape-copy #{:layout-item-h-sizing} omit-touched?) + + (contains? #{:auto :fix} (:layout-item-h-sizing shape-copy)) + (propagate-attrs shape-copy #{:layout-item-v-sizing} omit-touched?))) + {:ignore-touched true}) + + ;; Updates the children properties from the parent + (pcb/update-shapes + (:shapes shape-main) + (fn [child-main] + (let [child-copy (ctf/get-shape-in-copy copy-container child-main shape-copy)] + (-> child-main + (propagate-attrs child-copy ctk/swap-keep-attrs omit-touched?)))) + {:ignore-touched true}))] + (pcb/concat-changes changes new-changes))) + +(defn- update-grid-copy-attrs + "Synchronizes the `layout-grid-cells` property from the main shape to the copies" + [changes shape-main shape-copy main-container main-component copy-container omit-touched?] + (let [ids-map + (into {} + (comp + (map #(dm/get-in copy-container [:objects %])) + (keep + (fn [copy-shape] + (let [main-shape (ctf/get-ref-shape main-container main-component copy-shape)] + [(:id main-shape) (:id copy-shape)])))) + (:shapes shape-copy)) + + new-changes + (-> (pcb/empty-changes) + (pcb/with-container copy-container) + (pcb/with-objects (:objects copy-container)) + (pcb/update-shapes + [(:id shape-copy)] + (fn [shape-copy] + ;; Take cells from main and remap the shapes to assign it to the copy + (let [copy-cells (:layout-grid-cells shape-copy) + main-cells (-> (ctl/remap-grid-cells shape-main ids-map) :layout-grid-cells)] + (assoc shape-copy :layout-grid-cells (ctl/merge-cells copy-cells main-cells omit-touched?)))) + {:ignore-touched true}))] + (pcb/concat-changes changes new-changes))) + +(defn- update-grid-main-attrs + "Synchronizes the `layout-grid-cells` property from the copy to the main shape" + [changes shape-main shape-copy main-container copy-container _omit-touched?] + (let [ids-map + (into {} + (comp + (map #(dm/get-in main-container [:objects %])) + (keep + (fn [main-shape] + (let [copy-shape (ctf/get-shape-in-copy copy-container main-shape shape-copy)] + [(:id copy-shape) (:id main-shape)])))) + (:shapes shape-main)) + + new-changes + (-> (pcb/empty-changes) + (pcb/with-page main-container) + (pcb/with-objects (:objects main-container)) + (pcb/update-shapes + [(:id shape-main)] + (fn [shape-main] + ;; Take cells from copy and remap the shapes to assign it to the copy + (let [new-cells (-> (ctl/remap-grid-cells shape-copy ids-map) :layout-grid-cells)] + (assoc shape-main :layout-grid-cells new-cells))) + {:ignore-touched true}))] + (pcb/concat-changes changes new-changes))) (defn- reposition-shape [shape origin-root dest-root] @@ -1220,7 +1652,6 @@ (defn- make-change [container change] - (if (cph/page? container) + (if (cfh/page? container) (assoc change :page-id (:id container)) (assoc change :component-id (:id container)))) - diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs index d7fed197da..693207d87e 100644 --- a/frontend/src/app/main/data/workspace/media.cljs +++ b/frontend/src/app/main/data/workspace/media.cljs @@ -6,17 +6,20 @@ (ns app.main.data.workspace.media (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.files.builder :as fb] + [app.common.files.changes-builder :as pcb] [app.common.logging :as log] [app.common.math :as mth] - [app.common.pages.changes-builder :as pcb] [app.common.schema :as sm] + [app.common.svg :refer [optimize]] + [app.common.svg.shapes-builder :as csvg.shapes-builder] [app.common.types.container :as ctn] [app.common.types.shape :as cts] - [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] - [app.config :as cfg] + [app.config :as cf] [app.main.data.media :as dmm] [app.main.data.messages :as msg] [app.main.data.workspace.changes :as dch] @@ -28,20 +31,29 @@ [app.main.store :as st] [app.util.http :as http] [app.util.i18n :refer [tr]] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] - [potok.core :as ptk] + [potok.v2.core :as ptk] [promesa.core :as p] [tubax.core :as tubax])) +(def ^:private svgo-config + {:multipass false + :plugins ["safeAndFastPreset"]}) + (defn svg->clj [[name text]] (try - (->> (rx/of (-> (tubax/xml->clj text) - (assoc :name name)))) - - (catch :default _err - (rx/throw {:type :svg-parser})))) + (let [text (if (contains? cf/flags :frontend-svgo) + (optimize text svgo-config) + text) + data (-> (tubax/xml->clj text) + (assoc :name name))] + (rx/of data)) + (catch :default cause + (js/console.error cause) + (rx/throw (ex/error :type :svg-parser + :hint (ex-message cause)))))) ;; TODO: rename to bitmap-image-uploaded (defn image-uploaded @@ -55,11 +67,14 @@ :height height :x (mth/round (- x (/ width 2))) :y (mth/round (- y (/ height 2))) - :metadata {:width width - :height height - :mtype mtype - :id id}}] - (rx/of (dwsh/create-and-add-shape :image x y shape)))))) + :fills [{:fill-opacity 1 + :fill-image {:name name + :width width + :height height + :mtype mtype + :id id + :keep-aspect-ratio true}}]}] + (rx/of (dwsh/create-and-add-shape :rect x y shape)))))) (defn svg-uploaded [svg-data file-id position] @@ -73,7 +88,7 @@ (rx/map #(svg/add-svg-shapes (assoc svg-data :image-data %) position)))))) (defn- process-uris - [{:keys [file-id local? name uris mtype on-image on-svg] }] + [{:keys [file-id local? name uris mtype on-image on-svg]}] (letfn [(svg-url? [url] (or (and mtype (= mtype "image/svg+xml")) (str/ends-with? url ".svg"))) @@ -98,13 +113,13 @@ (->> (rx/from uris) (rx/filter (comp not svg-url?)) (rx/mapcat upload) - (rx/do on-image)) + (rx/tap on-image)) (->> (rx/from uris) (rx/filter svg-url?) (rx/merge-map (partial fetch-svg name)) (rx/merge-map svg->clj) - (rx/do on-svg))))) + (rx/tap on-svg))))) (defn- process-blobs [{:keys [file-id local? name blobs force-media on-image on-svg]}] @@ -113,7 +128,7 @@ (= (.-type blob) "image/svg+xml"))) (prepare-blob [blob] - (let [name (or name (if (dmm/file? blob) (.-name blob) "blob"))] + (let [name (or name (if (dmm/file? blob) (fb/strip-image-extension (.-name blob)) "blob"))] {:file-id file-id :name name :is-local local? @@ -130,82 +145,86 @@ (rx/filter (comp not svg-blob?)) (rx/map prepare-blob) (rx/mapcat #(rp/cmd! :upload-file-media-object %)) - (rx/do on-image)) + (rx/tap on-image)) (->> (rx/from blobs) (rx/map dmm/validate-file) (rx/filter svg-blob?) (rx/merge-map extract-content) (rx/merge-map svg->clj) - (rx/do on-svg))))) + (rx/tap on-svg))))) -(def schema:process-media-objects - [:map - [:file-id ::sm/uuid] - [:local? :boolean] - [:name {:optional true} :string] - [:data {:optional true} :any] ; FIXME - [:uris {:optional true} [:sequential :string]] - [:mtype {:optional true} :string]]) +(defn handle-media-error [error on-error] + (if (ex/ex-info? error) + (handle-media-error (ex-data error) on-error) + (cond + (= (:code error) :invalid-svg-file) + (rx/of (msg/error (tr "errors.media-type-not-allowed"))) + + (= (:code error) :media-type-not-allowed) + (rx/of (msg/error (tr "errors.media-type-not-allowed"))) + + (= (:code error) :unable-to-access-to-url) + (rx/of (msg/error (tr "errors.media-type-not-allowed"))) + + (= (:code error) :invalid-image) + (rx/of (msg/error (tr "errors.media-type-not-allowed"))) + + (= (:code error) :media-max-file-size-reached) + (rx/of (msg/error (tr "errors.media-too-large"))) + + (= (:code error) :media-type-mismatch) + (rx/of (msg/error (tr "errors.media-type-mismatch"))) + + (= (:code error) :unable-to-optimize) + (rx/of (msg/error (:hint error))) + + (fn? on-error) + (on-error error) + + :else + (do + (.error js/console "ERROR" error) + (rx/of (msg/error (tr "errors.cannot-upload"))))))) + + +(def ^:private + schema:process-media-objects + (sm/define + [:map {:title "process-media-objects"} + [:file-id ::sm/uuid] + [:local? :boolean] + [:name {:optional true} :string] + [:data {:optional true} :any] ; FIXME + [:uris {:optional true} [:sequential :string]] + [:mtype {:optional true} :string]])) (defn- process-media-objects [{:keys [uris on-error] :as params}] (dm/assert! - (and (sm/valid? schema:process-media-objects params) + (and (sm/check! schema:process-media-objects params) (or (contains? params :blobs) (contains? params :uris)))) - (letfn [(handle-error [error] - (if (ex/ex-info? error) - (handle-error (ex-data error)) - (cond - (= (:code error) :invalid-svg-file) - (rx/of (msg/error (tr "errors.media-type-not-allowed"))) - - (= (:code error) :media-type-not-allowed) - (rx/of (msg/error (tr "errors.media-type-not-allowed"))) - - (= (:code error) :unable-to-access-to-url) - (rx/of (msg/error (tr "errors.media-type-not-allowed"))) - - (= (:code error) :invalid-image) - (rx/of (msg/error (tr "errors.media-type-not-allowed"))) - - (= (:code error) :media-max-file-size-reached) - (rx/of (msg/error (tr "errors.media-too-large"))) - - (= (:code error) :media-type-mismatch) - (rx/of (msg/error (tr "errors.media-type-mismatch"))) - - (= (:code error) :unable-to-optimize) - (rx/of (msg/error (:hint error))) - - (fn? on-error) - (on-error error) - - :else - (do - (.error js/console "ERROR" error) - (rx/of (msg/error (tr "errors.cannot-upload")))))))] - - (ptk/reify ::process-media-objects - ptk/WatchEvent - (watch [_ _ _] - (rx/concat - (rx/of (msg/show {:content (tr "media.loading") - :type :info - :timeout nil - :tag :media-loading})) - (->> (if (seq uris) + (ptk/reify ::process-media-objects + ptk/WatchEvent + (watch [_ _ _] + (rx/concat + (rx/of (msg/show {:content (tr "media.loading") + :notification-type :toast + :type :info + :timeout nil + :tag :media-loading})) + (->> (if (seq uris) ;; Media objects is a list of URL's pointing to the path - (process-uris params) + (process-uris params) ;; Media objects are blob of data to be upload - (process-blobs params)) + (process-blobs params)) ;; Every stream has its own sideeffect. We need to ignore the result - (rx/ignore) - (rx/catch handle-error) - (rx/finalize #(st/emit! (msg/hide-tag :media-loading))))))))) + (rx/ignore) + (rx/catch #(handle-media-error % on-error)) + (rx/finalize #(st/emit! (msg/hide-tag :media-loading)))))))) ;; Deprecated in components-v2 (defn upload-media-asset @@ -213,7 +232,8 @@ (let [params (assoc params :force-media true :local? false - :on-image #(st/emit! (dwl/add-media %)))] + :on-image #(st/emit! (dwl/add-media %)) + :on-svg #(st/emit! (dwl/add-media %)))] (process-media-objects params))) (defn upload-media-workspace @@ -225,21 +245,50 @@ (process-media-objects params))) + +(defn upload-fill-image + [file on-success] + (dm/assert! + "expected a valid blob for `file` param" + (dmm/blob? file)) + (ptk/reify ::upload-fill-image + ptk/WatchEvent + (watch [_ state _] + (let [on-upload-success + (fn [image] + (on-success image) + (dmm/notify-finished-loading)) + + prepare + (fn [content] + {:file-id (get-in state [:workspace-file :id]) + :name (if (dmm/file? content) (.-name content) (tr "media.image")) + :is-local false + :content content})] + + (dmm/notify-start-loading) + (->> (rx/of file) + (rx/map dmm/validate-file) + (rx/map prepare) + (rx/mapcat #(rp/cmd! :upload-file-media-object %)) + (rx/tap on-upload-success) + (rx/catch handle-media-error)))))) + ;; --- Upload File Media objects (defn load-and-parse-svg "Load the contents of a media-obj of type svg, and parse it into a clojure structure." [media-obj] - (let [path (cfg/resolve-file-media media-obj)] + (let [path (cf/resolve-file-media media-obj)] (->> (http/send! {:method :get :uri path :mode :no-cors}) (rx/map :body) (rx/map #(vector (:name media-obj) %)) (rx/merge-map svg->clj) (rx/catch ; When error downloading media-obj, skip it and continue with next one - #(log/error :msg (str "Error downloading " (:name media-obj) " from " path) - :hint (ex-message %) - :error %))))) + #(log/error :msg (str "Error downloading " (:name media-obj) " from " path) + :hint (ex-message %) + :error %))))) (defn create-shapes-svg "Convert svg elements into penpot shapes." @@ -251,54 +300,81 @@ process-svg (fn [svg-data] - (let [[shape children] - (svg/create-svg-shapes svg-data pos objects uuid/zero nil #{} false)] - [shape children]))] + (let [[root-svg-shape children] + (csvg.shapes-builder/create-svg-shapes svg-data pos objects uuid/zero nil #{} false) + + frame-shape + (cts/setup-shape + {:type :frame + :x (:x pos) + :y (:y pos) + :width (-> root-svg-shape :selrect :width) + :height (-> root-svg-shape :selrect :height) + :name (:name root-svg-shape) + :frame-id uuid/zero + :parent-id uuid/zero + :fills []}) + + root-svg-shape + (-> root-svg-shape + (assoc :frame-id (:id frame-shape) :parent-id (:id frame-shape))) + + shapes + (->> children + (filter #(= (:parent-id %) (:id root-svg-shape))) + (mapv :id)) + + root-svg-shape + (assoc root-svg-shape :shapes shapes) + + children (->> children (mapv #(assoc % :frame-id (:id frame-shape)))) + children (d/concat-vec [root-svg-shape] children)] + + [frame-shape children]))] (->> (upload-images svg-data) (rx/map process-svg)))) (defn create-shapes-img "Convert a media object that contains a bitmap image into shapes, - one shape of type :image and one group that contains it." + one shape of type :rect containing an image fill and one group that contains it." [pos {:keys [name width height id mtype] :as media-obj}] - (let [group-shape (cts/make-shape :group - {:x (:x pos) - :y (:y pos) - :width width - :height height} - {:name name - :frame-id uuid/zero - :parent-id uuid/zero}) + (let [frame-shape (cts/setup-shape + {:type :frame + :x (:x pos) + :y (:y pos) + :width width + :height height + :name name + :frame-id uuid/zero + :parent-id uuid/zero}) - img-shape (cts/make-shape :image - {:x (:x pos) - :y (:y pos) - :width width - :height height - :metadata {:id id - :width width - :height height - :mtype mtype}} - {:name name - :frame-id uuid/zero - :parent-id (:id group-shape)})] - (rx/of [group-shape [img-shape]]))) + img-shape (cts/setup-shape + {:type :rect + :x (:x pos) + :y (:y pos) + :width width + :height height + :fills [{:fill-opacity 1 + :fill-image {:name name + :id id + :width width + :height height + :mtype mtype + :keep-aspect-ratio true}}] + :name name + :frame-id (:id frame-shape) + :parent-id (:id frame-shape)})] + (rx/of [frame-shape [img-shape]]))) (defn- add-shapes-and-component [it file-data page name [shape children]] - (let [page' (reduce #(ctst/add-shape (:id %2) %2 %1 uuid/zero (:parent-id %2) nil false) - page - (cons shape children)) - - shape' (ctn/get-shape page' (:id shape)) - - [component-shape component-shapes updated-shapes] - (ctn/make-component-shape shape' (:objects page') (:id file-data) true) + (let [[component-shape component-shapes updated-shapes] + (ctn/convert-shape-in-component shape children (:id file-data)) changes (-> (pcb/empty-changes it) - (pcb/with-page page') - (pcb/with-objects (:objects page')) + (pcb/with-page page) + (pcb/with-objects (:objects page)) (pcb/with-library-data file-data) (pcb/add-objects (cons shape children)) (pcb/add-component (:id component-shape) @@ -341,14 +417,18 @@ :on-svg #(st/emit! (process-svg-component %)))] (process-media-objects params))) -(def schema:clone-media-object - [:map - [:file-id ::sm/uuid] - [:object-id ::sm/uuid]]) +(def ^:private + schema:clone-media-object + (sm/define + [:map {:title "clone-media-object"} + [:file-id ::sm/uuid] + [:object-id ::sm/uuid]])) (defn clone-media-object [{:keys [file-id object-id] :as params}] - (dm/assert! (sm/valid? schema:clone-media-object params)) + (dm/assert! + (sm/check! schema:clone-media-object params)) + (ptk/reify ::clone-media-objects ptk/WatchEvent (watch [_ _ _] @@ -361,11 +441,11 @@ (rx/concat (rx/of (msg/show {:content (tr "media.loading") + :notification-type :toast :type :info :timeout nil :tag :media-loading})) (->> (rp/cmd! :clone-file-media-object params) - (rx/do on-success) + (rx/tap on-success) (rx/catch on-error) (rx/finalize #(st/emit! (msg/hide-tag :media-loading))))))))) - diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index c7a6e7f6df..b552bee67a 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -9,22 +9,27 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.modifiers :as gm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.common.pages.common :as cpc] - [app.common.pages.helpers :as cph] + [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.modifiers :as ctm] + [app.common.types.shape-tree :as ctst] + [app.common.types.shape.attrs :refer [editable-attrs]] [app.common.types.shape.layout :as ctl] + [app.common.uuid :as uuid] [app.main.constants :refer [zoom-half-pixel-precision]] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.comments :as-alias dwcm] [app.main.data.workspace.guides :as-alias dwg] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) ;; -- temporary modifiers ------------------------------------------- @@ -41,34 +46,11 @@ ;; When the interaction is finished (e.g. user releases mouse button), the ;; apply-modifiers event is done, that consolidates all modifiers into the base ;; geometric attributes of the shapes. - - (defn- check-delta "If the shape is a component instance, check its relative position respect the root of the component, and see if it changes after applying a transformation." - [shape root transformed-shape transformed-root objects modif-tree] - (let [root - (cond - (:component-root? shape) - shape - - (nil? root) - (ctn/get-component-shape objects shape {:allow-main? true}) - - :else root) - - transformed-root - (cond - (:component-root? transformed-shape) - transformed-shape - - (nil? transformed-root) - (as-> (ctn/get-component-shape objects transformed-shape {:allow-main? true}) $ - (gsh/transform-shape (merge $ (get modif-tree (:id $))))) - - :else transformed-root) - - shape-delta + [shape root transformed-shape transformed-root] + (let [shape-delta (when root (gpt/point (- (gsh/left-bound shape) (gsh/left-bound root)) (- (gsh/top-bound shape) (gsh/top-bound root)))) @@ -78,45 +60,73 @@ (gpt/point (- (gsh/left-bound transformed-shape) (gsh/left-bound transformed-root)) (- (gsh/top-bound transformed-shape) (gsh/top-bound transformed-root)))) - distance (if (and shape-delta transformed-shape-delta) - (gpt/distance-vector shape-delta transformed-shape-delta) - (gpt/point 0 0)) + distance + (if (and shape-delta transformed-shape-delta) + (gpt/distance-vector shape-delta transformed-shape-delta) + (gpt/point 0 0)) selrect (:selrect shape) - transformed-selrect (:selrect transformed-shape) + transformed-selrect (:selrect transformed-shape)] - ;; There are cases in that the coordinates change slightly (e.g. when rounding - ;; to pixel, or when recalculating text positions in different zoom levels). - ;; To take this into account, we ignore movements smaller than 1 pixel. - ;; - ;; When the change is a resize, also has a transformation that may have the - ;; shape position unchanged. But in this case we do not want to ignore it. - ignore-geometry? (and (and (< (:x distance) 1) (< (:y distance) 1)) - (mth/close? (:width selrect) (:width transformed-selrect)) - (mth/close? (:height selrect) (:height transformed-selrect)))] - [root transformed-root ignore-geometry?])) + ;; There are cases in that the coordinates change slightly (e.g. when rounding + ;; to pixel, or when recalculating text positions in different zoom levels). + ;; To take this into account, we ignore movements smaller than 1 pixel. + ;; + ;; When the change is a resize, also has a transformation that may have the + ;; shape position unchanged. But in this case we do not want to ignore it. + (and (and (< (:x distance) 1) (< (:y distance) 1)) + (mth/close? (:width selrect) (:width transformed-selrect)) + (mth/close? (:height selrect) (:height transformed-selrect))))) -(defn- get-ignore-tree +(defn calculate-ignore-tree "Retrieves a map with the flag `ignore-geometry?` given a tree of modifiers" - ([modif-tree objects shape] - (get-ignore-tree modif-tree objects shape nil nil {})) + [modif-tree objects] - ([modif-tree objects shape root transformed-root ignore-tree] - (let [children (map (d/getf objects) (:shapes shape)) + (letfn [(get-ignore-tree + ([ignore-tree shape] + (let [shape-id (dm/get-prop shape :id) + transformed-shape (gsh/transform-shape shape (dm/get-in modif-tree [shape-id :modifiers])) - shape-id (:id shape) - transformed-shape (gsh/transform-shape shape (dm/get-in modif-tree [shape-id :modifiers])) + root + (if (:component-root shape) + shape + (ctn/get-component-shape objects shape {:allow-main? true})) - [root transformed-root ignore-geometry?] - (check-delta shape root transformed-shape transformed-root objects modif-tree) + transformed-root + (if (:component-root shape) + transformed-shape + (gsh/transform-shape root (dm/get-in modif-tree [(:id root) :modifiers])))] - ignore-tree (assoc ignore-tree shape-id ignore-geometry?) + (get-ignore-tree ignore-tree shape transformed-shape root transformed-root))) - set-child - (fn [ignore-tree child] - (get-ignore-tree modif-tree objects child root transformed-root ignore-tree))] + ([ignore-tree shape root transformed-root] + (let [shape-id (dm/get-prop shape :id) + transformed-shape (gsh/transform-shape shape (dm/get-in modif-tree [shape-id :modifiers]))] + (get-ignore-tree ignore-tree shape transformed-shape root transformed-root))) - (reduce set-child ignore-tree children)))) + ([ignore-tree shape transformed-shape root transformed-root] + (let [shape-id (dm/get-prop shape :id) + + ignore-tree + (cond-> ignore-tree + (and (some? root) (ctk/in-component-copy? shape)) + (assoc + shape-id + (check-delta shape root transformed-shape transformed-root))) + + set-child + (fn [ignore-tree child] + (get-ignore-tree ignore-tree child root transformed-root))] + + (->> (:shapes shape) + (map (d/getf objects)) + (reduce set-child ignore-tree)))))] + + ;; we check twice because we want only to search parents of components but once the + ;; tree is traversed we only want to process the objects in components + (->> (keys modif-tree) + (map #(get objects %)) + (reduce get-ignore-tree nil)))) (defn assoc-position-data [shape position-data old-shape] @@ -129,7 +139,6 @@ (d/not-empty? position-data) (assoc :position-data position-data)))) - (defn update-grow-type [shape old-shape] (let [auto-width? (= :auto-width (:grow-type shape)) @@ -175,14 +184,40 @@ (update-in modif-tree [parent-id :modifiers] ctm/remove-children [child-id]))) modif-tree))) +(defn add-grid-children-modifiers + [modifiers frame-id shapes objects [row column :as cell]] + (let [frame (get objects frame-id) + ids (set shapes) + + ;; Temporary remove the children when moving them + frame (-> frame + (update :shapes #(d/removev ids %)) + (ctl/assign-cells objects)) + + ids (->> ids + (remove #(ctl/position-absolute? objects %)) + (ctst/sort-z-index objects) + reverse) + + frame (-> frame + (update :shapes d/concat-vec ids) + (cond-> (some? cell) + (ctl/push-into-cell ids row column)) + (ctl/assign-cells objects))] + (-> modifiers + (ctm/change-property :layout-grid-rows (:layout-grid-rows frame)) + (ctm/change-property :layout-grid-columns (:layout-grid-columns frame)) + (ctm/change-property :layout-grid-cells (:layout-grid-cells frame))))) + (defn build-change-frame-modifiers - [modif-tree objects selected target-frame-id drop-index] + [modif-tree objects selected target-frame-id drop-index cell-data] (let [origin-frame-ids (->> selected (group-by #(get-in objects [% :frame-id]))) child-set (set (get-in objects [target-frame-id :shapes])) target-frame (get objects target-frame-id) target-flex-layout? (ctl/flex-layout? target-frame) + target-grid-layout? (ctl/grid-layout? target-frame) children-ids (concat (:shapes target-frame) selected) @@ -203,14 +238,16 @@ (fn [modif-tree [original-frame shapes]] (let [shapes (->> shapes (d/removev #(= target-frame-id %))) shapes (cond->> shapes - (and target-flex-layout? (= original-frame target-frame-id)) + (and (or target-grid-layout? target-flex-layout?) + (= original-frame target-frame-id)) ;; When movining inside a layout frame remove the shapes that are not immediate children (filterv #(contains? child-set %))) children-ids (->> (dm/get-in objects [original-frame :shapes]) (remove (set selected))) - - h-sizing? (ctl/change-h-sizing? original-frame objects children-ids) - v-sizing? (ctl/change-v-sizing? original-frame objects children-ids)] + h-sizing? (and (ctl/flex-layout? objects original-frame) + (ctl/change-h-sizing? original-frame objects children-ids)) + v-sizing? (and (ctl/flex-layout? objects original-frame) + (ctl/change-v-sizing? original-frame objects children-ids))] (cond-> modif-tree (not= original-frame target-frame-id) (-> (modifier-remove-from-parent objects shapes) @@ -222,23 +259,31 @@ (update-in [original-frame :modifiers] ctm/change-property :layout-item-v-sizing :fix))) (and target-flex-layout? (= original-frame target-frame-id)) - (update-in [target-frame-id :modifiers] ctm/add-children shapes drop-index))))] + (update-in [target-frame-id :modifiers] ctm/add-children shapes drop-index) + + ;; Add the object to the cell + target-grid-layout? + (update-in [target-frame-id :modifiers] add-grid-children-modifiers target-frame-id shapes objects cell-data))))] (as-> modif-tree $ (reduce update-frame-modifiers $ origin-frame-ids) (cond-> $ - (ctl/change-h-sizing? target-frame-id objects children-ids) - (update-in [target-frame-id :modifiers] ctm/change-property :layout-item-h-sizing :fix)) - (cond-> $ - (ctl/change-v-sizing? target-frame-id objects children-ids) + ;; Set fix position to target frame (horizontal) + (and (ctl/flex-layout? objects target-frame-id) + (ctl/change-h-sizing? target-frame-id objects children-ids)) + (update-in [target-frame-id :modifiers] ctm/change-property :layout-item-h-sizing :fix) + + ;; Set fix position to target frame (vertical) + (and (ctl/flex-layout? objects target-frame-id) + (ctl/change-v-sizing? target-frame-id objects children-ids)) (update-in [target-frame-id :modifiers] ctm/change-property :layout-item-v-sizing :fix))))) (defn modif->js - [modif-tree objects] - (clj->js (into {} - (map (fn [[k v]] - [(get-in objects [k :name]) v])) - modif-tree))) + [modif-tree objects] + (clj->js (into {} + (map (fn [[k v]] + [(get-in objects [k :name]) v])) + modif-tree))) (defn apply-text-modifier [shape {:keys [width height]}] @@ -260,19 +305,19 @@ (d/update-when result id apply-text-modifier text-modifier)))))) #_(defn apply-path-modifiers - [objects path-modifiers] - (letfn [(apply-path-modifier - [shape {:keys [content-modifiers]}] - (let [shape (update shape :content upc/apply-content-modifiers content-modifiers) - [points selrect] (helpers/content->points+selrect shape (:content shape))] - (assoc shape :selrect selrect :points points)))] - (loop [modifiers (seq path-modifiers) - result objects] - (if (empty? modifiers) - result - (let [[id path-modifier] (first modifiers)] - (recur (rest modifiers) - (update objects id apply-path-modifier path-modifier))))))) + [objects path-modifiers] + (letfn [(apply-path-modifier + [shape {:keys [content-modifiers]}] + (let [shape (update shape :content upc/apply-content-modifiers content-modifiers) + [points selrect] (helpers/content->points+selrect shape (:content shape))] + (assoc shape :selrect selrect :points points)))] + (loop [modifiers (seq path-modifiers) + result objects] + (if (empty? modifiers) + result + (let [[id path-modifier] (first modifiers)] + (recur (rest modifiers) + (update objects id apply-path-modifier path-modifier))))))) (defn- calculate-modifiers ([state modif-tree] @@ -294,11 +339,11 @@ (as-> objects $ (apply-text-modifiers $ (get state :workspace-text-modifier)) ;;(apply-path-modifiers $ (get-in state [:workspace-local :edit-path])) - (gsh/set-objects-modifiers modif-tree $ (merge - params - {:ignore-constraints ignore-constraints - :snap-pixel? snap-pixel? - :snap-precision snap-precision})))))) + (gm/set-objects-modifiers modif-tree $ (merge + params + {:ignore-constraints ignore-constraints + :snap-pixel? snap-pixel? + :snap-precision snap-precision})))))) (defn- calculate-update-modifiers [old-modif-tree state ignore-constraints ignore-snap-pixel modif-tree] @@ -314,7 +359,14 @@ objects (-> objects (apply-text-modifiers (get state :workspace-text-modifier)))] - (gsh/set-objects-modifiers old-modif-tree modif-tree objects {:ignore-constraints ignore-constraints :snap-pixel? snap-pixel? :snap-precision snap-precision}))) + + (gm/set-objects-modifiers + old-modif-tree + modif-tree + objects + {:ignore-constraints ignore-constraints + :snap-pixel? snap-pixel? + :snap-precision snap-precision}))) (defn update-modifiers ([modif-tree] @@ -345,21 +397,25 @@ (update [_ state] (assoc state :workspace-modifiers (calculate-modifiers state ignore-constraints ignore-snap-pixel modif-tree params)))))) -;; Rotation use different algorithm to calculate children modifiers (and do not use child constraints). +(def ^:private + xf-rotation-shape + (comp + (remove #(get % :blocked false)) + (filter #(:rotation (get editable-attrs (:type %)))) + (map :id))) + +;; Rotation use different algorithm to calculate children +;; modifiers (and do not use child constraints). (defn set-rotation-modifiers ([angle shapes] - (set-rotation-modifiers angle shapes (-> shapes gsh/selection-rect gsh/center-selrect))) + (set-rotation-modifiers angle shapes (-> shapes gsh/shapes->rect grc/rect->center))) ([angle shapes center] (ptk/reify ::set-rotation-modifiers ptk/UpdateEvent (update [_ state] - (let [objects (wsh/lookup-page-objects state) - ids - (->> shapes - (remove #(get % :blocked false)) - (filter #((cpc/editable-attrs (:type %)) :rotation)) - (map :id)) + (let [objects (wsh/lookup-page-objects state) + ids (sequence xf-rotation-shape shapes) get-modifier (fn [shape] @@ -367,7 +423,7 @@ modif-tree (-> (build-modif-tree ids objects get-modifier) - (gsh/set-objects-modifiers objects))] + (gm/set-objects-modifiers objects))] (assoc state :workspace-modifiers modif-tree)))))) @@ -383,18 +439,18 @@ ids (->> shapes (remove #(get % :blocked false)) - (filter #((cpc/editable-attrs (:type %)) :rotation)) + (filter #(contains? (get editable-attrs (:type %)) :rotation)) (map :id)) get-modifier (fn [shape] (let [delta (- angle (:rotation shape)) - center (gsh/center-shape shape)] + center (gsh/shape->center shape)] (ctm/rotation-modifiers shape center delta))) modif-tree (-> (build-modif-tree ids objects get-modifier) - (gsh/set-objects-modifiers objects))] + (gm/set-objects-modifiers objects))] (assoc state :workspace-modifiers modif-tree)))))) @@ -402,24 +458,33 @@ ([] (apply-modifiers nil)) - ([{:keys [modifiers undo-transation? stack-undo? ignore-constraints ignore-snap-pixel] + ([{:keys [modifiers undo-transation? stack-undo? ignore-constraints ignore-snap-pixel undo-group] :or {undo-transation? true stack-undo? false ignore-constraints false ignore-snap-pixel false}}] (ptk/reify ::apply-modifiers ptk/WatchEvent (watch [_ state _] (let [text-modifiers (get state :workspace-text-modifier) objects (wsh/lookup-page-objects state) - object-modifiers (if modifiers - (calculate-modifiers state ignore-constraints ignore-snap-pixel modifiers) - (get state :workspace-modifiers)) - ids (or (keys object-modifiers) []) - ids-with-children (into (vec ids) (mapcat #(cph/get-children-ids objects %)) ids) + object-modifiers + (if (some? modifiers) + (calculate-modifiers state ignore-constraints ignore-snap-pixel modifiers) + (get state :workspace-modifiers)) - shapes (map (d/getf objects) ids) - ignore-tree (->> (map #(get-ignore-tree object-modifiers objects %) shapes) - (reduce merge {})) - undo-id (js/Symbol)] + ids + (into [] + (remove #(= % uuid/zero)) + (keys object-modifiers)) + + ids-with-children + (into ids + (mapcat (partial cfh/get-children-ids objects)) + ids) + + ignore-tree + (calculate-ignore-tree object-modifiers objects) + + undo-id (js/Symbol)] (rx/concat (if undo-transation? @@ -431,7 +496,7 @@ ids (fn [shape] (let [modif (get-in object-modifiers [(:id shape) :modifiers]) - text-shape? (cph/text-shape? shape) + text-shape? (cfh/text-shape? shape) position-data (when text-shape? (dm/get-in text-modifiers [(:id shape) :position-data]))] (-> shape @@ -443,6 +508,7 @@ {:reg-objects? true :stack-undo? stack-undo? :ignore-tree ignore-tree + :undo-group undo-group ;; Attributes that can change in the transform. This way we don't have to check ;; all the attributes :attrs [:selrect @@ -481,7 +547,9 @@ :layout-gap :layout-item-margin :layout-item-margin-type - ]}) + :layout-grid-cells + :layout-grid-columns + :layout-grid-rows]}) ;; We've applied the text-modifier so we can dissoc the temporary data (fn [state] (update state :workspace-text-modifier #(apply dissoc % ids))) diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index e7ab374328..4bb3a97723 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -8,7 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages.changes :as cpc] + [app.common.files.changes :as cpc] [app.common.schema :as sm] [app.common.uuid :as uuid] [app.main.data.common :refer [handle-notification]] @@ -16,13 +16,14 @@ [app.main.data.workspace.changes :as dch] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.persistence :as dwp] - [app.main.streams :as ms] [app.util.globals :refer [global]] + [app.util.mouse :as mse] [app.util.object :as obj] + [app.util.rxops :as rxs] [app.util.time :as dt] - [beicon.core :as rx] + [beicon.v2.core :as rx] [clojure.set :as set] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (declare process-message) (declare handle-presence) @@ -37,7 +38,7 @@ (ptk/reify ::initialize ptk/WatchEvent (watch [_ state stream] - (let [stoper (rx/filter (ptk/type? ::finalize) stream) + (let [stopper (rx/filter (ptk/type? ::finalize) stream) profile-id (:profile-id state) initmsg [{:type :subscribe-file @@ -81,11 +82,12 @@ ;; Emit to all other connected users the current pointer ;; position changes. (->> stream - (rx/filter ms/pointer-event?) - (rx/sample 50) + (rx/filter mse/pointer-event?) + (rx/filter #(= :viewport (mse/get-pointer-source %))) + (rx/pipe (rxs/throttle 100)) (rx/map #(handle-pointer-send file-id (:pt %))))) - (rx/take-until stoper))] + (rx/take-until stopper))] (rx/concat stream (rx/of (dws/send endmsg))))))) @@ -123,17 +125,15 @@ ;; --- Handle: Presence (def ^:private presence-palette - #{"#02bf51" ; darkpastelgreen text white - "#00fa9a" ; mediumspringgreen text black - "#b22222" ; firebrick text white - "#ff8c00" ; darkorage text white - "#ffd700" ; gold text black - "#ba55d3" ; mediumorchid text white - "#dda0dd" ; plum text black - "#008ab8" ; blueNCS text white - "#00bfff" ; deepskyblue text white - "#ff1493" ; deeppink text white - "#ffafda" ; carnationpink text black + #{"#f49ef7" ; pink + "#75cafc" ; blue + "#fdcf79" ; gold + "#a9bdfa" ; indigo + "#faa6b7" ; red + "#cbaaff" ; purple + "#f9b489" ; orange + "#dee563" ; yellow -> default presence color + "#b1e96f" ; lemon }) (defn handle-presence @@ -144,7 +144,8 @@ (remove nil?)) used (into #{} xfm presence) avail (set/difference presence-palette used)] - (or (first avail) "var(--color-black)"))) + ;; If all colores are used we select the default one + (or (first avail) "#dee563"))) (update-color [color presence] (if (some? color) @@ -158,10 +159,7 @@ (assoc :updated-at (dt/now)) (assoc :version version) (update :color update-color presence) - (assoc :text-color (if (contains? ["#00fa9a" "#ffd700" "#dda0dd" "#ffafda"] - (update-color (:color presence) presence)) - "#000" - "#fff")))) + (assoc :text-color "#000000"))) (update-presence [presence] (-> presence @@ -187,18 +185,23 @@ :updated-at (dt/now) :page-id page-id)))))) -(def schema:handle-file-change - [:map - [:type :keyword] - [:profile-id ::sm/uuid] - [:file-id ::sm/uuid] - [:session-id ::sm/uuid] - [:revn :int] - [:changes ::cpc/changes]]) +(def ^:private + schema:handle-file-change + (sm/define + [:map {:title "handle-file-change"} + [:type :keyword] + [:profile-id ::sm/uuid] + [:file-id ::sm/uuid] + [:session-id ::sm/uuid] + [:revn :int] + [:changes ::cpc/changes]])) (defn handle-file-change [{:keys [file-id changes] :as msg}] - (dm/assert! (sm/valid? schema:handle-file-change msg)) + (dm/assert! + "expected valid arguments" + (sm/check! schema:handle-file-change msg)) + (ptk/reify ::handle-file-change IDeref (-deref [_] {:changes changes}) @@ -244,19 +247,24 @@ (when-not (empty? changes-by-pages) (rx/from (map process-page-changes changes-by-pages)))))))) -(def schema:handle-library-change - [:map - [:type :keyword] - [:profile-id ::sm/uuid] - [:file-id ::sm/uuid] - [:session-id ::sm/uuid] - [:revn :int] - [:modified-at ::sm/inst] - [:changes ::cpc/changes]]) +(def ^:private + schema:handle-library-change + (sm/define + [:map {:title "handle-library-change"} + [:type :keyword] + [:profile-id ::sm/uuid] + [:file-id ::sm/uuid] + [:session-id ::sm/uuid] + [:revn :int] + [:modified-at ::sm/inst] + [:changes ::cpc/changes]])) (defn handle-library-change [{:keys [file-id modified-at changes revn] :as msg}] - (dm/assert! (sm/valid? schema:handle-library-change msg)) + (dm/assert! + "expected valid arguments" + (sm/check! schema:handle-library-change msg)) + (ptk/reify ::handle-library-change ptk/WatchEvent (watch [_ state _] diff --git a/frontend/src/app/main/data/workspace/path/changes.cljs b/frontend/src/app/main/data/workspace/path/changes.cljs index 8c1e1f758e..dd188e72e3 100644 --- a/frontend/src/app/main/data/workspace/path/changes.cljs +++ b/frontend/src/app/main/data/workspace/path/changes.cljs @@ -7,20 +7,24 @@ (ns app.main.data.workspace.path.changes (:require [app.common.data.macros :as dm] - [app.common.pages.changes-builder :as pcb] + [app.common.files.changes-builder :as pcb] [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.path.common :refer [content?]] + [app.main.data.workspace.path.common :refer [check-path-content!]] [app.main.data.workspace.path.helpers :as helpers] [app.main.data.workspace.path.state :as st] [app.main.data.workspace.state-helpers :as wsh] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (defn generate-path-changes "Generates changes to update the new content of the shape" [it objects page-id shape old-content new-content] - (dm/assert! (content? old-content)) - (dm/assert! (content? new-content)) + + (dm/assert! + "expected valid path content" + (and (check-path-content! old-content) + (check-path-content! new-content))) + (let [shape-id (:id shape) [old-points old-selrect] diff --git a/frontend/src/app/main/data/workspace/path/common.cljs b/frontend/src/app/main/data/workspace/path/common.cljs index ee526a9404..8edd06ffe6 100644 --- a/frontend/src/app/main/data/workspace/path/common.cljs +++ b/frontend/src/app/main/data/workspace/path/common.cljs @@ -7,8 +7,9 @@ (ns app.main.data.workspace.path.common (:require [app.common.schema :as sm] + [app.common.svg.path.subpath :as ups] [app.main.data.workspace.path.state :as st] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (def valid-commands #{:move-to @@ -22,23 +23,27 @@ :elliptical-arc :close-path}) -(def schema:content - [:vector {:title "PathContent"} - [:map {:title "PathContentEntry"} - [:command [::sm/one-of valid-commands]] - ;; FIXME: remove the `?` from prop name - [:relative? {:optional true} :boolean] - [:params {:optional true} - [:map {:title "PathContentEntryParams"} - [:x :double] - [:y :double] - [:c1x {:optional true} :double] - [:c1y {:optional true} :double] - [:c2x {:optional true} :double] - [:c2y {:optional true} :double]]]]]) +;; FIXME: should this schema be defined on common.types ? -(def content? - (sm/pred-fn schema:content)) +(def ^:private + schema:path-content + (sm/define + [:vector {:title "PathContent"} + [:map {:title "PathContentEntry"} + [:command [::sm/one-of valid-commands]] + ;; FIXME: remove the `?` from prop name + [:relative? {:optional true} :boolean] + [:params {:optional true} + [:map {:title "PathContentEntryParams"} + [:x :double] + [:y :double] + [:c1x {:optional true} :double] + [:c1y {:optional true} :double] + [:c2x {:optional true} :double] + [:c2y {:optional true} :double]]]]])) + +(def check-path-content! + (sm/check-fn schema:path-content)) (defn init-path [] (ptk/reify ::init-path)) @@ -48,10 +53,11 @@ (dissoc state :last-point :prev-handler :drag-handler :preview)) (defn finish-path - [_source] + [] (ptk/reify ::finish-path ptk/UpdateEvent (update [_ state] (let [id (st/get-path-id state)] (-> state - (update-in [:workspace-local :edit-path id] clean-edit-state)))))) + (update-in [:workspace-local :edit-path id] clean-edit-state) + (update-in (st/get-path-location state :content) ups/close-subpaths)))))) diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 1620a8a41a..f536f3369d 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -9,23 +9,25 @@ [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes.flex-layout :as gsl] - [app.common.path.commands :as upc] - [app.common.path.shapes-to-path :as upsp] + [app.common.svg.path.command :as upc] + [app.common.svg.path.shapes-to-path :as upsp] + [app.common.types.container :as ctn] + [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.drawing.common :as dwdc] [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.path.changes :as changes] - [app.main.data.workspace.path.common :as common :refer [content?]] + [app.main.data.workspace.path.common :as common :refer [check-path-content!]] [app.main.data.workspace.path.helpers :as helpers] [app.main.data.workspace.path.state :as st] [app.main.data.workspace.path.streams :as streams] [app.main.data.workspace.path.undo :as undo] [app.main.data.workspace.state-helpers :as wsh] - [app.main.streams :as ms] - [beicon.core :as rx] - [potok.core :as ptk])) + [app.util.mouse :as mse] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (declare change-edit-mode) @@ -43,7 +45,8 @@ command (helpers/next-node shape position last-point prev-handler)] (assoc-in state [:workspace-local :edit-path id :preview] command))))) -(defn add-node [{:keys [x y shift?]}] +(defn add-node + [{:keys [x y shift?]}] (ptk/reify ::add-node ptk/UpdateEvent (update [_ state] @@ -119,16 +122,12 @@ (declare close-path-drag-end) -(defn close-path-drag-start [position] +(defn close-path-drag-start + [position] (ptk/reify ::close-path-drag-start ptk/WatchEvent (watch [_ state stream] - (let [stop-stream - (->> stream (rx/filter #(or (helpers/end-path-event? %) - (ms/mouse-up? %)))) - - content (st/get-path state :content) - + (let [content (st/get-path state :content) handlers (-> (upc/content->handlers content) (get position)) @@ -136,9 +135,13 @@ (first handlers)) drag-events-stream - (->> (streams/position-stream) - (rx/take-until stop-stream) - (rx/map #(drag-handler position idx prefix %)))] + (->> (streams/position-stream state) + (rx/map #(drag-handler position idx prefix %)) + (rx/take-until + (rx/merge + (mse/drag-stopper stream) + (->> stream + (rx/filter helpers/end-path-event?)))))] (rx/concat (rx/of (add-node position)) @@ -147,7 +150,7 @@ drag-events-stream (rx/of (finish-drag)) (rx/of (close-path-drag-end)))) - (rx/of (common/finish-path "close-path"))))))) + (rx/of (common/finish-path))))))) (defn close-path-drag-end [] (ptk/reify ::close-path-drag-end @@ -159,13 +162,15 @@ (defn start-path-from-point [position] (ptk/reify ::start-path-from-point ptk/WatchEvent - (watch [_ _ stream] - (let [mouse-up (->> stream (rx/filter #(or (helpers/end-path-event? %) - (ms/mouse-up? %)))) - drag-events (->> (streams/position-stream) - (rx/take-until mouse-up) - (rx/map #(drag-handler %)))] + (watch [_ state stream] + (let [stopper (rx/merge + (mse/drag-stopper stream) + (->> stream + (rx/filter helpers/end-path-event?))) + drag-events (->> (streams/position-stream state) + (rx/map #(drag-handler %)) + (rx/take-until stopper))] (rx/concat (rx/of (add-node position)) (streams/drag-stream @@ -181,14 +186,20 @@ (rx/merge-map #(rx/empty)))) (defn make-drag-stream - [stream down-event] - (let [mouse-up (->> stream (rx/filter #(or (helpers/end-path-event? %) - (ms/mouse-up? %)))) + [state stream down-event] - drag-events (->> (streams/position-stream) - (rx/take-until mouse-up) - (rx/map #(drag-handler %)))] + (dm/assert! + "should be a pointer" + (gpt/point? down-event)) + (let [stopper (rx/merge + (mse/drag-stopper stream) + (->> stream + (rx/filter helpers/end-path-event?))) + + drag-events (->> (streams/position-stream state) + (rx/map #(drag-handler %)) + (rx/take-until stopper))] (rx/concat (rx/of (add-node down-event)) (streams/drag-stream @@ -196,23 +207,25 @@ drag-events (rx/of (finish-drag))))))) -(defn handle-drawing-path +(defn handle-drawing [_id] - (ptk/reify ::handle-drawing-path + (ptk/reify ::handle-drawing ptk/UpdateEvent (update [_ state] (let [id (st/get-path-id state)] - (-> state - (assoc-in [:workspace-local :edit-path id :edit-mode] :draw)))) + (assoc-in state [:workspace-local :edit-path id :edit-mode] :draw))) ptk/WatchEvent - (watch [_ _ stream] - (let [mouse-down (->> stream (rx/filter ms/mouse-down?)) - end-path-events (->> stream (rx/filter helpers/end-path-event?)) + (watch [_ state stream] + (let [mouse-down (->> stream + (rx/filter mse/mouse-event?) + (rx/filter mse/mouse-down-event?)) + end-path-events (->> stream + (rx/filter helpers/end-path-event?)) ;; Mouse move preview mousemove-events - (->> (streams/position-stream) + (->> (streams/position-stream state) (rx/take-until end-path-events) (rx/map #(preview-next-point %))) @@ -220,42 +233,53 @@ mousedown-events (->> mouse-down (rx/take-until end-path-events) - (rx/with-latest merge (streams/position-stream)) - + ;; We just ignore the mouse event and stream down the + ;; last position event + (rx/with-latest-from #(-> %2) (streams/position-stream state)) ;; We change to the stream that emits the first event (rx/switch-map #(rx/race (make-node-events-stream stream) - (make-drag-stream stream %))))] + (make-drag-stream state stream %))))] (rx/concat (rx/of (undo/start-path-undo)) (rx/of (common/init-path)) (rx/merge mousemove-events mousedown-events) - (rx/of (common/finish-path "after-events"))))))) + (rx/of (common/finish-path))))))) - -(defn setup-frame-path [] - (ptk/reify ::setup-frame-path +(defn setup-frame [] + (ptk/reify ::setup-frame ptk/UpdateEvent (update [_ state] (let [objects (wsh/lookup-page-objects state) content (get-in state [:workspace-drawing :object :content] []) position (gpt/point (get-in content [0 :params] nil)) - frame-id (ctst/top-nested-frame objects position) + frame-id (->> (ctst/top-nested-frame objects position) + (ctn/get-first-not-copy-parent objects) ;; We don't want to change the structure of component copies + :id) flex-layout? (ctl/flex-layout? objects frame-id) drop-index (when flex-layout? (gsl/get-drop-index frame-id objects position))] - (-> state - (assoc-in [:workspace-drawing :object :frame-id] frame-id) - (cond-> (some? drop-index) - (update-in [:workspace-drawing :object] with-meta {:index drop-index}))))))) -(defn handle-new-shape-result [shape-id] + (update-in state [:workspace-drawing :object] + (fn [object] + (-> object + (assoc :frame-id frame-id) + (assoc :parent-id frame-id) + (cond-> (some? drop-index) + (with-meta {:index drop-index}))))))))) + +(defn handle-new-shape-result + [shape-id] (ptk/reify ::handle-new-shape-result ptk/UpdateEvent (update [_ state] (let [content (get-in state [:workspace-drawing :object :content] [])] - (dm/assert! (content? content)) + + (dm/assert! + "expected valid path content" + (check-path-content! content)) + (if (> (count content) 1) (assoc-in state [:workspace-drawing :object :initialized?] true) state))) @@ -263,8 +287,8 @@ ptk/WatchEvent (watch [_ state _] (let [content (get-in state [:workspace-drawing :object :content] [])] - (if (seq content) - (rx/of (setup-frame-path) + (if (and (seq content) (> (count content) 1)) + (rx/of (setup-frame) (dwdc/handle-finish-drawing) (dwe/start-edition-mode shape-id) (change-edit-mode :draw)) @@ -276,15 +300,15 @@ (ptk/reify ::handle-new-shape ptk/UpdateEvent (update [_ state] - (let [id (st/get-path-id state)] + (let [shape (cts/setup-shape {:type :path})] (-> state - (assoc-in [:workspace-local :edit-path id :snap-toggled] false)))) + (update :workspace-drawing assoc :object shape)))) ptk/WatchEvent (watch [_ state stream] - (let [shape-id (get-in state [:workspace-drawing :object :id])] + (let [shape-id (dm/get-in state [:workspace-drawing :object :id])] (rx/concat - (rx/of (handle-drawing-path shape-id)) + (rx/of (handle-drawing shape-id)) (->> stream (rx/filter (ptk/type? ::common/finish-path)) (rx/take 1) @@ -310,7 +334,7 @@ (if (= :draw edit-mode) (rx/concat (rx/of (dch/update-shapes [id] upsp/convert-to-path)) - (rx/of (handle-drawing-path id)) + (rx/of (handle-drawing id)) (->> stream (rx/filter (ptk/type? ::common/finish-path)) (rx/take 1) @@ -324,13 +348,14 @@ (let [id (st/get-path-id state) content (st/get-path state :content) old-content (get-in state [:workspace-local :edit-path id :old-content]) - mode (get-in state [:workspace-local :edit-path id :edit-mode])] - + mode (get-in state [:workspace-local :edit-path id :edit-mode]) + empty-content? (empty? content)] (cond - (not= content old-content) (rx/of (changes/save-path-content) - (start-draw-mode)) + (and (not= content old-content) (not empty-content?)) (rx/of (changes/save-path-content)) (= mode :draw) (rx/of :interrupt) - :else (rx/of (common/finish-path "changed-content"))))))) + :else (rx/of + (common/finish-path) + (dwdc/clear-drawing))))))) (defn change-edit-mode [mode] (ptk/reify ::change-edit-mode @@ -344,7 +369,7 @@ (watch [_ state _] (let [id (st/get-path-id state)] (cond - (and id (= :move mode)) (rx/of (common/finish-path "change-edit-mode")) + (and id (= :move mode)) (rx/of (common/finish-path)) (and id (= :draw mode)) (rx/of (start-draw-mode)) :else (rx/empty)))))) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 3b623ea57d..164e37acb1 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -8,12 +8,12 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes.path :as upg] - [app.common.pages.helpers :as cph] - [app.common.path.commands :as upc] - [app.common.path.shapes-to-path :as upsp] - [app.common.path.subpaths :as ups] + [app.common.svg.path.command :as upc] + [app.common.svg.path.shapes-to-path :as upsp] + [app.common.svg.path.subpath :as ups] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.path.changes :as changes] @@ -25,9 +25,10 @@ [app.main.data.workspace.path.undo :as undo] [app.main.data.workspace.state-helpers :as wsh] [app.main.streams :as ms] + [app.util.mouse :as mse] [app.util.path.tools :as upt] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (defn modify-handler [id index prefix dx dy match-opposite?] (ptk/reify ::modify-handler @@ -66,7 +67,7 @@ (let [changes (changes/generate-path-changes it objects page-id shape (:content shape) new-content)] (if (empty? new-content) (rx/of (dch/commit-changes changes) - dwe/clear-edition-mode) + (dwe/clear-edition-mode)) (rx/of (dch/commit-changes changes) (selection/update-selection point-change) (fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers :moving-nodes :moving-handler)))))))))) @@ -149,7 +150,8 @@ (ptk/reify ::drag-selected-points ptk/WatchEvent (watch [_ state stream] - (let [stopper (->> stream (rx/filter ms/mouse-up?)) + (let [stopper (mse/drag-stopper stream) + id (dm/get-in state [:workspace-local :edition]) selected-points (dm/get-in state [:workspace-local :edit-path id :selected-points] #{}) @@ -262,8 +264,6 @@ (rx/concat (rx/of (dch/update-shapes [id] upsp/convert-to-path)) (->> (streams/move-handler-stream handler point handler opposite points) - (rx/take-until (->> stream (rx/filter #(or (ms/mouse-up? %) - (streams/finish-edition? %))))) (rx/map (fn [{:keys [x y alt? shift?]}] (let [pos (cond-> (gpt/point x y) @@ -274,7 +274,13 @@ prefix (+ start-delta-x (- (:x pos) (:x handler))) (+ start-delta-y (- (:y pos) (:y handler))) - (not alt?)))))) + (not alt?))))) + (rx/take-until + (rx/merge + (mse/drag-stopper stream) + (->> stream + (rx/filter streams/finish-edition?))))) + (rx/concat (rx/of (apply-content-modifiers))))))))) (declare stop-path-edit) @@ -288,7 +294,7 @@ edit-path (dm/get-in state [:workspace-local :edit-path id]) content (st/get-path state :content) state (cond-> state - (cph/path-shape? objects id) + (cfh/path-shape? objects id) (st/set-content (ups/close-subpaths content)))] (cond-> state (or (not edit-path) (= :draw (:edit-mode edit-path))) @@ -308,8 +314,8 @@ (= (ptk/type %) ::start-path-edit)))) interrupt (->> stream (rx/filter #(= % :interrupt)) (rx/take 1))] (rx/concat - (rx/of (undo/start-path-undo)) - (rx/of (drawing/change-edit-mode mode)) + (rx/of (undo/start-path-undo) + (drawing/change-edit-mode mode)) (->> interrupt (rx/map #(stop-path-edit id)) (rx/take-until stopper))))))) @@ -322,7 +328,7 @@ ptk/WatchEvent (watch [_ _ _] - (rx/of (ptk/data-event :layout/update [id]))))) + (rx/of (ptk/data-event :layout/update {:ids [id]}))))) (defn split-segments [{:keys [from-p to-p t]}] diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index b76016484c..2facaf53a6 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -8,27 +8,30 @@ (:require [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.common.path.commands :as upc] - [app.common.path.subpaths :as ups] + [app.common.svg.path.command :as upc] [app.main.data.workspace.path.common :as common] - [app.main.streams :as ms] - [potok.core :as ptk])) + [app.util.mouse :as mse] + [potok.v2.core :as ptk])) -(defn end-path-event? [event] - (or (= (ptk/type event) ::common/finish-path) - (= (ptk/type event) :app.main.data.workspace.path.shortcuts/esc-pressed) - (= :app.main.data.workspace.common/clear-edition-mode (ptk/type event)) - (= :app.main.data.workspace/finalize-page (ptk/type event)) - (= event :interrupt) ;; ESC - (ms/mouse-double-click? event))) +(defn end-path-event? + [event] + (let [type (ptk/type event)] + (or (= type ::common/finish-path) + (= type :app.main.data.workspace.path.shortcuts/esc-pressed) + (= type :app.main.data.workspace.common/clear-edition-mode) + (= type :app.main.data.workspace/finalize-page) + (= event :interrupt) ;; ESC + (and ^boolean (mse/mouse-event? event) + ^boolean (mse/mouse-double-click-event? event))))) (defn content-center [content] (-> content gsh/content->selrect - gsh/center-selrect)) + grc/rect->center)) (defn content->points+selrect "Given the content of a shape, calculate its points and selrect" @@ -45,7 +48,7 @@ flip-y (gmt/scale (gpt/point 1 -1)) :always (gmt/multiply (:transform-inverse shape (gmt/matrix)))) - center (or (gsh/center-shape shape) + center (or (gsh/shape->center shape) (content-center content)) base-content (gsh/transform-content @@ -54,16 +57,16 @@ ;; Calculates the new selrect with points given the old center points (-> (gsh/content->selrect base-content) - (gsh/rect->points) + (grc/rect->points) (gsh/transform-points center transform)) - points-center (gsh/center-points points) + points-center (gsh/points->center points) ;; Points is now the selrect but the center is different so we can create the selrect ;; through points selrect (-> points (gsh/transform-points points-center transform-inverse) - (gsh/points->selrect))] + (grc/points->rect))] [points selrect])) (defn update-selrect @@ -113,7 +116,6 @@ (let [command (next-node shape position prev-point prev-handler)] (-> shape (update :content (fnil conj []) command) - (update :content ups/close-subpaths) (update-selrect)))) (defn angle-points [common p1 p2] @@ -127,7 +129,7 @@ (let [;; To match the angle, the angle should be matching (angle between points 180deg) angle-handlers (angle-points node handler opposite) - match-angle? (and match-angle? (<= (mth/abs (- 180 angle-handlers) ) 0.1)) + match-angle? (and match-angle? (<= (mth/abs (- 180 angle-handlers)) 0.1)) ;; To match distance the distance should be matching match-distance? (and match-distance? (mth/almost-zero? (- (gpt/distance node handler) diff --git a/frontend/src/app/main/data/workspace/path/selection.cljs b/frontend/src/app/main/data/workspace/path/selection.cljs index 170b959197..b2256b3c90 100644 --- a/frontend/src/app/main/data/workspace/path/selection.cljs +++ b/frontend/src/app/main/data/workspace/path/selection.cljs @@ -6,13 +6,15 @@ (ns app.main.data.workspace.path.selection (:require + [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.path.state :as st] [app.main.streams :as ms] - [beicon.core :as rx] - [potok.core :as ptk])) + [app.util.mouse :as mse] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (defn path-pointer-enter [position] (ptk/reify ::path-pointer-enter @@ -42,20 +44,27 @@ (let [id (st/get-path-id state)] (update-in state [:workspace-local :edit-path id :hover-handlers] disj [index prefix]))))) -(defn select-node-area [shift?] +(defn select-node-area + [shift?] (ptk/reify ::select-node-area ptk/UpdateEvent (update [_ state] - (let [selrect (get-in state [:workspace-local :selrect]) - id (get-in state [:workspace-local :edition]) - content (st/get-path state :content) - selected-point? #(gsh/has-point-rect? selrect %) - selected-points (or (get-in state [:workspace-local :edit-path id :selected-points]) #{}) - positions (into (if shift? selected-points #{}) - (comp (filter #(not (= (:command %) :close-path))) + (let [selrect (dm/get-in state [:workspace-local :selrect]) + id (dm/get-in state [:workspace-local :edition]) + content (st/get-path state :content) + + selected-point? (if (some? selrect) + (partial gsh/has-point-rect? selrect) + (constantly false)) + + selected-points (dm/get-in state [:workspace-local :edit-path id :selected-points]) + selected-points (or selected-points #{}) + + xform (comp (filter #(not (= (:command %) :close-path))) (map (comp gpt/point :params)) (filter selected-point?)) - content)] + positions (into (if shift? selected-points #{}) xform content)] + (cond-> state (some? id) (assoc-in [:workspace-local :edit-path id :selected-points] positions)))))) @@ -109,16 +118,15 @@ (ptk/reify ::handle-area-selection ptk/WatchEvent (watch [_ state stream] - (let [zoom (get-in state [:workspace-local :zoom] 1) - stop? (fn [event] (or (dwc/interrupt? event) (ms/mouse-up? event))) - stoper (->> stream (rx/filter stop?)) - from-p @ms/mouse-position] + (let [zoom (get-in state [:workspace-local :zoom] 1) + stopper (mse/drag-stopper stream) + from-p @ms/mouse-position] (rx/concat (->> ms/mouse-position - (rx/take-until stoper) - (rx/map #(gsh/points->rect [from-p %])) + (rx/map #(grc/points->rect [from-p %])) (rx/filter (partial valid-rect? zoom)) - (rx/map update-area-selection)) + (rx/map update-area-selection) + (rx/take-until stopper)) (rx/of (select-node-area shift?) (clear-area-selection)))))))) diff --git a/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs index 5dbef4e97f..c73ad3dfca 100644 --- a/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs +++ b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs @@ -6,13 +6,14 @@ (ns app.main.data.workspace.path.shapes-to-path (:require - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] - [app.common.path.shapes-to-path :as upsp] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cph] + [app.common.svg.path.shapes-to-path :as upsp] + [app.common.types.container :as ctn] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (defn convert-selected-to-path [] (ptk/reify ::convert-selected-to-path @@ -20,7 +21,8 @@ (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state) - selected (wsh/lookup-selected state) + selected (->> (wsh/lookup-selected state) + (remove #(ctn/has-any-copy-parent? objects (get objects %)))) children-ids (into #{} diff --git a/frontend/src/app/main/data/workspace/path/shortcuts.cljs b/frontend/src/app/main/data/workspace/path/shortcuts.cljs index 1b307e88b7..07fb3b1fa3 100644 --- a/frontend/src/app/main/data/workspace/path/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/path/shortcuts.cljs @@ -10,8 +10,8 @@ [app.main.data.workspace :as dw] [app.main.data.workspace.path :as drp] [app.main.store :as st] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Shortcuts @@ -22,14 +22,9 @@ (defn esc-pressed [] (ptk/reify ::esc-pressed ptk/WatchEvent - (watch [_ state _] - ;; Not interrupt when we're editing a path - (let [edition-id (or (get-in state [:workspace-drawing :object :id]) - (get-in state [:workspace-local :edition])) - path-edit-mode (get-in state [:workspace-local :edit-path edition-id :edit-mode])] - (if-not (= :draw path-edit-mode) - (rx/of :interrupt) - (rx/empty)))))) + (watch [_ _ _] + ;; Not interrupt when we're editing a path + (rx/of :interrupt)))) (def shortcuts {:move-nodes {:tooltip "M" diff --git a/frontend/src/app/main/data/workspace/path/state.cljs b/frontend/src/app/main/data/workspace/path/state.cljs index de30cf841c..77d3e42cb7 100644 --- a/frontend/src/app/main/data/workspace/path/state.cljs +++ b/frontend/src/app/main/data/workspace/path/state.cljs @@ -6,13 +6,48 @@ (ns app.main.data.workspace.path.state (:require - [app.common.path.shapes-to-path :as upsp])) + [app.common.data.macros :as dm] + [app.common.files.helpers :as cph] + [app.common.svg.path.shapes-to-path :as upsp])) + +(defn path-editing? + "Returns true if we're editing a path or creating a new one." + [{local :workspace-local + drawing :workspace-drawing}] + (let [selected (:selected local) + edition (:edition local) + + drawing-obj (:object drawing) + drawing-tool (:tool drawing) + + edit-path? (dm/get-in local [:edit-path edition]) + + shape (or drawing-obj (first selected)) + shape-id (:id shape) + + single? (= (count selected) 1) + editing? (and (some? shape-id) + (some? edition) + (= shape-id edition)) + + ;; we need to check if we're drawing a new object but we're + ;; not using the pencil tool. + draw-path? (and (some? drawing-obj) + (cph/path-shape? drawing-obj) + (not= :curve drawing-tool))] + + (or (and ^boolean single? + ^boolean editing? + (and (not (cph/text-shape? shape)) + (not (cph/frame-shape? shape)))) + draw-path? + edit-path?))) (defn get-path-id "Retrieves the currently editing path id" [state] - (or (get-in state [:workspace-local :edition]) - (get-in state [:workspace-drawing :object :id]))) + (or (dm/get-in state [:workspace-local :edition]) + (dm/get-in state [:workspace-drawing :object :id]))) (defn get-path-location [state & ks] diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index 071e6ab120..38d0efd508 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -14,9 +14,10 @@ [app.main.snap :as snap] [app.main.store :as st] [app.main.streams :as ms] - [beicon.core :as rx] + [app.util.mouse :as mse] + [beicon.v2.core :as rx] [okulary.core :as l] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (defonce drag-threshold 5) @@ -50,16 +51,18 @@ (let [zoom (get-in @st/state [:workspace-local :zoom] 1) start (-> @ms/mouse-position to-pixel-snap) - mouse-up (->> st/stream - (rx/filter #(or (finish-edition? %) - (ms/mouse-up? %)))) + + stopper (rx/merge + (mse/drag-stopper st/stream) + (->> st/stream + (rx/filter finish-edition?))) position-stream (->> ms/mouse-position - (rx/take-until mouse-up) (rx/map to-pixel-snap) (rx/filter (dragging? start zoom)) - (rx/take 1))] + (rx/take 1) + (rx/take-until stopper))] (rx/merge (->> position-stream @@ -110,7 +113,6 @@ (defn move-handler-stream [start-point node handler opposite points] - (let [zoom (get-in @st/state [:workspace-local :zoom] 1) ranges (snap/create-ranges points) d-pos (/ snap/snap-path-accuracy zoom) @@ -140,16 +142,20 @@ (let [snap (snap/get-snap-delta [handler] ranges d-pos)] (merge position (gpt/add position snap))))) position))] + (->> ms/mouse-position (rx/map to-pixel-snap) - (rx/with-latest merge (->> ms/mouse-position-shift (rx/map #(hash-map :shift? %)))) - (rx/with-latest merge (->> ms/mouse-position-alt (rx/map #(hash-map :alt? %)))) + (rx/with-latest-from + (fn [position shift? alt?] + (assoc position :shift? shift? :alt? alt?)) + ms/mouse-position-shift + ms/mouse-position-alt) (rx/with-latest-from (snap-toggled-stream)) (rx/map check-path-snap)))) (defn position-stream - [] - (let [zoom (get-in @st/state [:workspace-local :zoom] 1) + [state] + (let [zoom (get-in state [:workspace-local :zoom] 1) d-pos (/ snap/snap-path-accuracy zoom) get-content #(pst/get-path % :content) @@ -164,12 +170,14 @@ (->> ms/mouse-position (rx/map to-pixel-snap) - (rx/with-latest vector ranges-stream) - (rx/with-latest-from (snap-toggled-stream)) - (rx/map (fn [[[position ranges] snap-toggled]] + (rx/with-latest-from ranges-stream (snap-toggled-stream)) + (rx/map (fn [[position ranges snap-toggled]] (if snap-toggled (let [snap (snap/get-snap-delta [position] ranges d-pos)] (gpt/add position snap)) position))) - (rx/with-latest merge (->> ms/mouse-position-shift (rx/map #(hash-map :shift? %)))) - (rx/with-latest merge (->> ms/mouse-position-alt (rx/map #(hash-map :alt? %))))))) + (rx/with-latest-from + (fn [position shift? alt?] + (assoc position :shift? shift? :alt? alt?)) + ms/mouse-position-shift + ms/mouse-position-alt)))) diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 3a432cb871..e75b53fc3f 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -6,16 +6,16 @@ (ns app.main.data.workspace.path.tools (:require - [app.common.path.shapes-to-path :as upsp] - [app.common.path.subpaths :as ups] + [app.common.svg.path.shapes-to-path :as upsp] + [app.common.svg.path.subpath :as ups] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.state :as st] [app.main.data.workspace.state-helpers :as wsh] [app.util.path.tools :as upt] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (defn process-path-tool "Generic function that executes path transformations with the content and selected nodes" @@ -40,7 +40,7 @@ (rx/of (dch/update-shapes [id] upsp/convert-to-path)) (rx/of (dch/commit-changes changes) (when (empty? new-content) - dwe/clear-edition-mode)))))))))) + (dwe/clear-edition-mode))))))))))) (defn make-corner ([] diff --git a/frontend/src/app/main/data/workspace/path/undo.cljs b/frontend/src/app/main/data/workspace/path/undo.cljs index 2819cc48fe..bdaa7e256e 100644 --- a/frontend/src/app/main/data/workspace/path/undo.cljs +++ b/frontend/src/app/main/data/workspace/path/undo.cljs @@ -9,12 +9,14 @@ [app.common.data :as d] [app.common.data.undo-stack :as u] [app.common.uuid :as uuid] + [app.main.data.workspace.common :as dwc] [app.main.data.workspace.path.changes :as changes] + [app.main.data.workspace.path.common :as common] [app.main.data.workspace.path.state :as st] [app.main.store :as store] - [beicon.core :as rx] + [beicon.v2.core :as rx] [okulary.core :as l] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (defn undo-event? [event] @@ -65,8 +67,14 @@ undo-stack))))) ptk/WatchEvent - (watch [_ _ _] - (rx/of (changes/save-path-content {:preserve-move-to true}))))) + (watch [_ state _] + (let [id (st/get-path-id state) + undo-stack (get-in state [:workspace-local :edit-path id :undo-stack])] + (if (> (:index undo-stack) 0) + (rx/of (changes/save-path-content {:preserve-move-to true})) + (rx/of (changes/save-path-content {:preserve-move-to true}) + (common/finish-path) + (dwc/show-toolbar))))))) (defn redo-path [] (ptk/reify ::redo-path diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index e0b08734b9..c0032c6c8d 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -7,9 +7,8 @@ (ns app.main.data.workspace.persistence (:require [app.common.data.macros :as dm] + [app.common.files.changes :as cpc] [app.common.logging :as log] - [app.common.pages :as cp] - [app.common.pages.changes :as cpc] [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] @@ -17,11 +16,10 @@ [app.main.features :as features] [app.main.repo :as rp] [app.main.store :as st] - [app.util.router :as rt] [app.util.time :as dt] - [beicon.core :as rx] + [beicon.v2.core :as rx] [okulary.core :as l] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (log/set-level! :info) @@ -39,7 +37,7 @@ ptk/WatchEvent (watch [_ _ stream] (log/debug :hint "initialize persistence") - (let [stoper (rx/filter (ptk/type? ::initialize-persistence) stream) + (let [stopper (rx/filter (ptk/type? ::initialize-persistence) stream) commits (l/atom []) saving? (l/atom false) @@ -55,7 +53,7 @@ on-dirty (fn [] - ;; Enable reload stoper + ;; Enable reload stopper (swap! st/ongoing-tasks conj :workspace-change) (st/emit! (update-persistence-status {:status :pending}))) @@ -66,7 +64,7 @@ on-saved (fn [] - ;; Disable reload stoper + ;; Disable reload stopper (swap! st/ongoing-tasks disj :workspace-change) (st/emit! (update-persistence-status {:status :saved})) (reset! saving? false))] @@ -84,7 +82,7 @@ (assoc :file-id file-id)))) (rx/observe-on :async) (rx/tap #(swap! commits conj %)) - (rx/take-until (rx/delay 100 stoper)) + (rx/take-until (rx/delay 100 stopper)) (rx/finalize (fn [] (log/debug :hint "finalize persistence: changes watcher")))) @@ -117,7 +115,7 @@ (rx/tap on-saved) (rx/ignore))) (rx/empty)))) - (rx/take-until (rx/delay 100 stoper)) + (rx/take-until (rx/delay 100 stopper)) (rx/finalize (fn [] (log/debug :hint "finalize persistence: save loop")))) @@ -128,7 +126,7 @@ (rx/filter library-file?) (rx/filter (complement #(empty? (:changes %)))) (rx/map persist-synchronous-changes) - (rx/take-until (rx/delay 100 stoper)) + (rx/take-until (rx/delay 100 stopper)) (rx/finalize (fn [] (log/debug :hint "finalize persistence: synchronous save loop"))))))))) @@ -139,14 +137,9 @@ (ptk/reify ::persist-changes ptk/WatchEvent (watch [_ state _] - (let [;; this features set does not includes the ffeat/enabled - ;; because they are already available on the backend and - ;; this request provides a set of features to enable in - ;; this request. - features (cond-> #{} - (features/active-feature? state :components-v2) - (conj "components/v2")) - sid (:session-id state) + (let [sid (:session-id state) + + features (features/get-team-enabled-features state) params {:id file-id :revn file-revn :session-id sid @@ -168,8 +161,9 @@ (rx/merge (->> (rx/from frame-updates) (rx/mapcat (fn [[page-id frames]] - (->> frames (map #(vector page-id %))))) - (rx/map (fn [[page-id frame-id]] (dwt/update-thumbnail file-id page-id frame-id)))) + (->> frames (map (fn [frame-id] [file-id page-id frame-id]))))) + (rx/map (fn [data] + (ptk/data-event ::dwt/update data)))) (->> (rx/from (concat lagged commits)) (rx/merge-map @@ -182,19 +176,11 @@ (rx/of (shapes-changes-persisted-finished)))))) (rx/catch (fn [cause] - (cond - (= :authentication (:type cause)) - (rx/throw cause) - - (instance? js/TypeError cause) + (if (instance? js/TypeError cause) (->> (rx/timer 2000) (rx/map (fn [_] (persist-changes file-id file-revn changes pending-commits)))) - - :else - (rx/concat - (rx/of (rt/assign-exception cause)) - (rx/throw cause)))))))))) + (rx/throw cause))))))))) ;; Event to be thrown after the changes have been persisted (defn shapes-changes-persisted-finished @@ -207,9 +193,8 @@ (ptk/reify ::persist-synchronous-changes ptk/WatchEvent (watch [_ state _] - (let [features (cond-> #{} - (features/active-feature? state :components-v2) - (conj "components/v2")) + (let [features (features/get-team-enabled-features state) + sid (:session-id state) file (dm/get-in state [:workspace-libraries file-id]) @@ -240,10 +225,10 @@ (= (ptk/type event) ::changes-persisted)) (defn shapes-changes-persisted - [file-id {:keys [revn changes]}] + [file-id {:keys [revn changes] persisted-session-id :session-id}] (dm/assert! (uuid? file-id)) (dm/assert! (int? revn)) - (dm/assert! (cpc/changes? changes)) + (dm/assert! (cpc/check-changes! changes)) (ptk/reify ::shapes-changes-persisted ptk/UpdateEvent @@ -251,26 +236,28 @@ ;; NOTE: we don't set the file features context here because ;; there are no useful context for code that need to be executed ;; on the frontend side - - (if-let [current-file-id (:current-file-id state)] - (if (= file-id current-file-id) - (let [changes (group-by :page-id changes)] + (let [current-file-id (:current-file-id state) + current-session-id (:session-id state)] + (if (and (some? current-file-id) + ;; If the remote change is from teh current session we skip + (not= persisted-session-id current-session-id)) + (if (= file-id current-file-id) + (let [changes (group-by :page-id changes)] + (-> state + (update-in [:workspace-file :revn] max revn) + (update :workspace-data + (fn [file] + (loop [fdata file + entries (seq changes)] + (if-let [[page-id changes] (first entries)] + (recur (-> fdata + (cpc/process-changes changes) + (cond-> (some? page-id) + (ctst/update-object-indices page-id))) + (rest entries)) + fdata)))))) (-> state - (update-in [:workspace-file :revn] max revn) - (update :workspace-data (fn [file] - (loop [fdata file - entries (seq changes)] - (if-let [[page-id changes] (first entries)] - (recur (-> fdata - (cp/process-changes changes) - (ctst/update-object-indices page-id)) - (rest entries)) - fdata)))))) - (-> state - (update-in [:workspace-libraries file-id :revn] max revn) - (update-in [:workspace-libraries file-id :data] cp/process-changes changes))) - - state)))) - - + (update-in [:workspace-libraries file-id :revn] max revn) + (update-in [:workspace-libraries file-id :data] cpc/process-changes changes))) + state))))) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index dd2a881fa1..acb0d21750 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -8,33 +8,44 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] + [app.common.files.focus :as cpf] + [app.common.files.helpers :as cfh] + [app.common.files.libraries-helpers :as cflh] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.math :as mth] - [app.common.pages :as cp] - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] + [app.common.record :as cr] [app.common.types.component :as ctk] + [app.common.types.container :as ctn] [app.common.types.file :as ctf] [app.common.types.page :as ctp] + [app.common.types.shape-tree :as ctst] [app.common.types.shape.interactions :as ctsi] + [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] + [app.main.data.events :as ev] [app.main.data.modal :as md] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.collapse :as dwc] [app.main.data.workspace.libraries-helpers :as dwlh] + [app.main.data.workspace.specialized-panel :as-alias dwsp] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.zoom :as dwz] [app.main.refs :as refs] [app.main.streams :as ms] [app.main.worker :as uw] - [beicon.core :as rx] + [app.util.mouse :as mse] + [beicon.v2.core :as rx] + [beicon.v2.operators :as rxo] [clojure.set :as set] [linked.set :as lks] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) -(defn interrupt? [e] (= e :interrupt)) +(defn interrupt? + [e] + (= e :interrupt)) ;; --- Selection Rect @@ -49,37 +60,30 @@ (assoc-in state [:workspace-local :selrect] selrect)))) (defn handle-area-selection - [preserve? ignore-groups?] + [preserve?] (ptk/reify ::handle-area-selection ptk/WatchEvent (watch [_ state stream] - (let [zoom (get-in state [:workspace-local :zoom] 1) - stop? (fn [event] (or (interrupt? event) (ms/mouse-up? event))) - stoper (->> stream (rx/filter stop?)) + (let [zoom (dm/get-in state [:workspace-local :zoom] 1) + stopper (mse/drag-stopper stream) + init-position @ms/mouse-position - init-selrect - {:type :rect - :x1 (:x @ms/mouse-position) - :y1 (:y @ms/mouse-position) - :x2 (:x @ms/mouse-position) - :y2 (:y @ms/mouse-position)} + init-selrect (grc/make-rect + (dm/get-prop init-position :x) + (dm/get-prop init-position :y) + 0 0) calculate-selrect (fn [selrect [delta space?]] - (let [result - (cond-> selrect - :always - (-> (update :x2 + (:x delta)) - (update :y2 + (:y delta))) - - space? - (-> (update :x1 + (:x delta)) - (update :y1 + (:y delta))))] - (assoc result - :x (min (:x1 result) (:x2 result)) - :y (min (:y1 result) (:y2 result)) - :width (mth/abs (- (:x2 result) (:x1 result))) - :height (mth/abs (- (:y2 result) (:y1 result)))))) + (let [selrect (-> (cr/clone selrect) + (cr/update! :x2 + (:x delta)) + (cr/update! :y2 + (:y delta))) + selrect (if ^boolean space? + (-> selrect + (cr/update! :x1 + (:x delta)) + (cr/update! :y1 + (:y delta))) + selrect)] + (grc/update-rect! selrect :corners))) selrect-stream (->> ms/mouse-position @@ -88,9 +92,10 @@ (rx/filter some?) (rx/with-latest-from ms/keyboard-space) (rx/scan calculate-selrect init-selrect) - (rx/filter #(or (> (:width %) (/ 10 zoom)) - (> (:height %) (/ 10 zoom)))) - (rx/take-until stoper))] + (rx/filter #(or (> (dm/get-prop % :width) (/ 10 zoom)) + (> (dm/get-prop % :height) (/ 10 zoom)))) + (rx/take-until stopper))] + (rx/concat (if preserve? (rx/empty) @@ -102,9 +107,22 @@ (->> selrect-stream (rx/buffer-time 100) - (rx/map #(last %)) - (rx/dedupe) - (rx/map #(select-shapes-by-current-selrect preserve? ignore-groups?)))) + (rx/map last) + (rx/pipe (rxo/distinct-contiguous)) + (rx/with-latest-from ms/keyboard-mod ms/keyboard-shift) + (rx/map + (fn [[_ mod? shift?]] + (select-shapes-by-current-selrect shift? mod?)))) + + ;; The last "tick" from the mouse cannot be buffered so we are sure + ;; a selection is returned. Without this we can have empty selections on + ;; very fast movement + (->> selrect-stream + (rx/last) + (rx/with-latest-from ms/keyboard-mod ms/keyboard-shift) + (rx/map + (fn [[_ mod? shift?]] + (select-shapes-by-current-selrect shift? mod? false))))) (->> (rx/of (update-selrect nil)) ;; We need the async so the current event finishes before updating the selrect @@ -130,7 +148,10 @@ (watch [_ state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id)] - (rx/of (dwc/expand-all-parents [id] objects))))))) + (rx/of + (dwc/expand-all-parents [id] objects) + :interrupt + ::dwsp/interrupt)))))) (defn select-prev-shape ([] @@ -185,6 +206,9 @@ [id] (dm/assert! (uuid? id)) (ptk/reify ::deselect-shape + ptk/WatchEvent + (watch [_ _ _] + (rx/of ::dwsp/interrupt)) ptk/UpdateEvent (update [_ state] (-> state @@ -196,17 +220,20 @@ (shift-select-shapes id nil)) ([id objects] - (ptk/reify ::shift-select-shapes-2 + (ptk/reify ::shift-select-shapes + ptk/WatchEvent + (watch [_ _ _] + (rx/of ::dwsp/interrupt)) ptk/UpdateEvent (update [_ state] (let [objects (or objects (wsh/lookup-page-objects state)) - append-to-selection (cph/expand-region-selection objects (into #{} [(get-in state [:workspace-local :last-selected]) id])) + append-to-selection (cfh/expand-region-selection objects (into #{} [(get-in state [:workspace-local :last-selected]) id])) selection (-> state wsh/lookup-selected (conj id))] (-> state (assoc-in [:workspace-local :selected] - (set/union selection append-to-selection)) + (set/union selection append-to-selection)) (update :workspace-local assoc :last-selected id))))))) (defn select-shapes @@ -222,14 +249,16 @@ (let [objects (wsh/lookup-page-objects state) focus (:workspace-focus-selected state) ids (if (d/not-empty? focus) - (cp/filter-not-focus objects focus ids) + (cpf/filter-not-focus objects focus ids) ids)] (assoc-in state [:workspace-local :selected] ids))) ptk/WatchEvent (watch [_ state _] (let [objects (wsh/lookup-page-objects state)] - (rx/of (dwc/expand-all-parents ids objects)))))) + (rx/of + (dwc/expand-all-parents ids objects) + ::dwsp/interrupt))))) (defn select-all [] @@ -241,7 +270,7 @@ ;; mode is active focus (:workspace-focus-selected state) objects (-> (wsh/lookup-page-objects state) - (cp/focus-objects focus)) + (cpf/focus-objects focus)) lookup (d/getf objects) parents (->> (wsh/lookup-selected state) @@ -254,7 +283,7 @@ (-> parents first lookup) (lookup uuid/zero)) - toselect (->> (cph/get-immediate-children objects (:id parent)) + toselect (->> (cfh/get-immediate-children objects (:id parent)) (into (d/ordered-set) (comp (remove :hidden) (remove :blocked) (map :id))))] (rx/of (select-shapes toselect)))))) @@ -268,6 +297,9 @@ ([check-modal] (ptk/reify ::deselect-all + ptk/WatchEvent + (watch [_ _ _] + (rx/of ::dwsp/interrupt)) ptk/UpdateEvent (update [_ state] @@ -283,32 +315,39 @@ ;; --- Select Shapes (By selrect) (defn select-shapes-by-current-selrect - [preserve? ignore-groups?] - (ptk/reify ::select-shapes-by-current-selrect - ptk/WatchEvent - (watch [_ state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state) - selected (wsh/lookup-selected state) - initial-set (if preserve? - selected - lks/empty-linked-set) - selrect (get-in state [:workspace-local :selrect]) - blocked? (fn [id] (get-in objects [id :blocked] false))] - (when selrect - (rx/empty) - (->> (uw/ask-buffered! - {:cmd :selection/query - :page-id page-id - :rect selrect - :include-frames? true - :ignore-groups? ignore-groups? - :full-frame? true}) - (rx/map #(cph/clean-loops objects %)) - (rx/map #(into initial-set (comp - (filter (complement blocked?)) - (remove (partial cph/hidden-parent? objects))) %)) - (rx/map select-shapes))))))) + ([preserve? ignore-groups?] + (select-shapes-by-current-selrect preserve? ignore-groups? true)) + ([preserve? ignore-groups? buffered?] + (ptk/reify ::select-shapes-by-current-selrect + ptk/WatchEvent + (watch [_ state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state) + selected (wsh/lookup-selected state) + initial-set (if preserve? + selected + lks/empty-linked-set) + selrect (dm/get-in state [:workspace-local :selrect]) + blocked? (fn [id] (dm/get-in objects [id :blocked] false)) + + ask-worker (if buffered? uw/ask-buffered! uw/ask!)] + + (if (some? selrect) + (->> (ask-worker + {:cmd :selection/query + :page-id page-id + :rect selrect + :include-frames? true + :ignore-groups? ignore-groups? + :full-frame? true + :using-selrect? true}) + (rx/filter some?) + (rx/map #(cfh/clean-loops objects %)) + (rx/map #(into initial-set (comp + (filter (complement blocked?)) + (remove (partial cfh/hidden-parent? objects))) %)) + (rx/map select-shapes)) + (rx/empty))))))) (defn select-inside-group [group-id position] @@ -324,13 +363,11 @@ ;; We need to reverse the children because if two children ;; overlap we want to select the one that's over (and it's ;; in the later vector position - selected (->> children - reverse + selected (->> (reverse children) (d/seek #(gsh/has-point? % position)))] (when selected (rx/of (select-shape (:id selected)))))))) - ;; --- Duplicate Shapes (declare prepare-duplicate-shape-change) (declare prepare-duplicate-flows) @@ -344,13 +381,16 @@ (-> (pcb/empty-changes it) (pcb/with-page page) (pcb/with-objects all-objects))] - (prepare-duplicate-changes all-objects page ids delta it libraries library-data file-id init-changes))) + (prepare-duplicate-changes all-objects page ids delta it libraries library-data file-id init-changes))) ([all-objects page ids delta it libraries library-data file-id init-changes] (let [shapes (map (d/getf all-objects) ids) - unames (volatile! (cp/retrieve-used-names (:objects page))) + unames (volatile! (cfh/get-used-names (:objects page))) update-unames! (fn [new-name] (vswap! unames conj new-name)) - all-ids (reduce #(into %1 (cons %2 (cph/get-children-ids all-objects %2))) (d/ordered-set) ids) + all-ids (reduce #(into %1 (cons %2 (cfh/get-children-ids all-objects %2))) (d/ordered-set) ids) + + ;; We need ids-map for remapping the grid layout. But when duplicating the guides + ;; we calculate a new one because the components will have created new shapes. ids-map (into {} (map #(vector % (uuid/next))) all-ids) changes @@ -363,36 +403,52 @@ ids-map %2 delta + nil libraries library-data it file-id) - init-changes))] + init-changes)) + + ;; We need to check the changes to get the ids-map + ids-map + (into {} + (comp + (filter #(= :add-obj (:type %))) + (map #(vector (:old-id %) (-> % :obj :id)))) + (:redo-changes changes))] (-> changes (prepare-duplicate-flows shapes page ids-map) (prepare-duplicate-guides shapes page ids-map delta))))) (defn- prepare-duplicate-component-change - [changes page component-root parent-id delta libraries library-data it] + [changes objects page component-root parent-id frame-id delta libraries library-data it] (let [component-id (:component-id component-root) file-id (:component-file component-root) main-component (ctf/get-component libraries file-id component-id) moved-component (gsh/move component-root delta) pos (gpt/point (:x moved-component) (:y moved-component)) + origin-frame (get-in page [:objects frame-id]) + delta (cond-> delta + (some? origin-frame) + (gpt/subtract (-> origin-frame :selrect gpt/point))) instantiate-component #(dwlh/generate-instantiate-component changes + objects file-id (:component-id component-root) pos page libraries (:id component-root) - parent-id) + parent-id + frame-id + {}) restore-component - #(let [restore (dwlh/prepare-restore-component changes library-data (:component-id component-root) it page delta (:id component-root) parent-id)] + #(let [restore (dwlh/prepare-restore-component changes library-data (:component-id component-root) it page delta (:id component-root) parent-id frame-id)] [(:shape restore) (:changes restore)]) [_shape changes] @@ -401,79 +457,130 @@ (instantiate-component))] changes)) +;; TODO: move to common.files.shape-helpers (defn- prepare-duplicate-shape-change - ([changes objects page unames update-unames! ids-map obj delta libraries library-data it file-id] - (prepare-duplicate-shape-change changes objects page unames update-unames! ids-map obj delta libraries library-data it file-id (:frame-id obj) (:parent-id obj) false)) + ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data it file-id] + (prepare-duplicate-shape-change changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data it file-id (:frame-id obj) (:parent-id obj) false false true)) - ([changes objects page unames update-unames! ids-map obj delta libraries library-data it file-id frame-id parent-id duplicating-component?] + ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data it file-id frame-id parent-id duplicating-component? child? remove-swap-slot?] (cond (nil? obj) changes - (ctf/is-known-component? obj libraries) - (prepare-duplicate-component-change changes page obj parent-id delta libraries library-data it) + (ctf/is-main-of-known-component? obj libraries) + (prepare-duplicate-component-change changes objects page obj parent-id frame-id delta libraries library-data it) :else - (let [frame? (cph/frame-shape? obj) - group? (cph/group-shape? obj) - bool? (cph/bool-shape? obj) + (let [frame? (cfh/frame-shape? obj) + group? (cfh/group-shape? obj) + bool? (cfh/bool-shape? obj) new-id (ids-map (:id obj)) parent-id (or parent-id frame-id) + parent (get objects parent-id) name (:name obj) - is-component-root? (or (:saved-component-root? obj) (ctk/instance-root? obj)) - duplicating-component? (or duplicating-component? is-component-root?) - is-component-main? (ctk/main-instance? obj) + is-component-root? (or (:saved-component-root obj) + ;; Backward compatibility + (:saved-component-root? obj) + (ctk/instance-root? obj)) + duplicating-component? (or duplicating-component? (ctk/instance-head? obj)) + is-component-main? (ctk/main-instance? obj) + subinstance-head? (ctk/subinstance-head? obj) + + into-component? (and duplicating-component? + (ctn/in-any-component? objects parent)) + + level-delta (if (some? level-delta) + level-delta + (ctn/get-nesting-level-delta objects obj parent)) + new-shape-ref (ctf/advance-shape-ref nil page libraries obj level-delta {:include-deleted? true}) + regenerate-component (fn [changes shape] (let [components-v2 (dm/get-in library-data [:options :components-v2]) - [_ changes] (dwlh/generate-add-component-changes changes shape objects file-id (:id page) components-v2)] + [_ changes] (cflh/generate-add-component-changes changes shape objects file-id (:id page) components-v2)] changes)) - new-obj (-> obj - (assoc :id new-id - :name name - :parent-id parent-id - :frame-id frame-id) + new-obj + (-> obj + (assoc :id new-id + :name name + :parent-id parent-id + :frame-id frame-id) - (dissoc :shapes - :main-instance? - :use-for-thumbnail?) + (cond-> (and subinstance-head? remove-swap-slot?) + (ctk/remove-swap-slot)) - (cond-> - (or group? bool?) - (assoc :shapes [])) + (dissoc :shapes + :main-instance + :use-for-thumbnail) - (gsh/move delta) - (d/update-when :interactions #(ctsi/remap-interactions % ids-map objects))) + (cond-> into-component? + (dissoc :component-root)) + + (cond-> (and (ctk/instance-head? obj) + (not into-component?)) + (assoc :component-root true)) + + (cond-> (or frame? group? bool?) + (assoc :shapes [])) + + (cond-> (and (some? new-shape-ref) + (not= new-shape-ref (:shape-ref obj))) + (assoc :shape-ref new-shape-ref)) + + (gsh/move delta) + (d/update-when :interactions #(ctsi/remap-interactions % ids-map objects)) + + (cond-> (ctl/grid-layout? obj) + (ctl/remap-grid-cells ids-map))) new-obj (cond-> new-obj (not duplicating-component?) - (dissoc :shape-ref)) + (ctk/detach-shape)) - changes (-> (pcb/add-object changes new-obj {:ignore-touched duplicating-component?}) - (pcb/amend-last-change #(assoc % :old-id (:id obj)))) + ;; We want the first added object to touch it's parent, but not subsequent children + changes (-> (pcb/add-object changes new-obj {:ignore-touched (and duplicating-component? child?)}) + (pcb/amend-last-change #(assoc % :old-id (:id obj))) + (cond-> (ctl/grid-layout? objects (:parent-id obj)) + (-> (pcb/update-shapes [(:parent-id obj)] ctl/assign-cells {:with-objects? true}) + (pcb/reorder-grid-children [(:parent-id obj)])))) changes (cond-> changes (and is-component-root? is-component-main?) - (regenerate-component new-obj))] + (regenerate-component new-obj)) + + ;; This is needed for the recursive call to find the new object as parent + page' (ctst/add-shape (:id new-obj) + new-obj + {:objects objects} + (:frame-id new-obj) + (:parent-id new-obj) + nil + true)] (reduce (fn [changes child] (prepare-duplicate-shape-change changes - objects + (:objects page') page unames update-unames! ids-map child delta + level-delta libraries library-data it file-id (if frame? new-id frame-id) new-id - duplicating-component?)) + duplicating-component? + true + (and remove-swap-slot? + ;; only remove swap slot of children when the current shape + ;; is not a subinstance head + (not subinstance-head?)))) changes (map (d/getf objects) (:shapes obj))))))) @@ -487,42 +594,44 @@ (if-not (empty? frames-with-flow) (let [update-flows (fn [flows] (reduce - (fn [flows frame] - (let [name (cp/generate-unique-name @unames "Flow 1") - _ (vswap! unames conj name) - new-flow {:id (uuid/next) - :name name - :starting-frame (get ids-map (:id frame))}] - (ctp/add-flow flows new-flow))) - flows - frames-with-flow))] + (fn [flows frame] + (let [name (cfh/generate-unique-name @unames "Flow 1") + _ (vswap! unames conj name) + new-flow {:id (uuid/next) + :name name + :starting-frame (get ids-map (:id frame))}] + (ctp/add-flow flows new-flow))) + flows + frames-with-flow))] (pcb/update-page-option changes :flows update-flows)) changes))) (defn- prepare-duplicate-guides [changes shapes page ids-map delta] (let [guides (get-in page [:options :guides]) - frames (->> shapes - (filter #(= (:type %) :frame))) - new-guides (reduce - (fn [g frame] - (let [new-id (ids-map (:id frame)) - new-frame (-> frame - (gsh/move delta)) - new-guides (->> guides - (vals) - (filter #(= (:frame-id %) (:id frame))) - (map #(-> % - (assoc :id (uuid/next)) - (assoc :frame-id new-id) - (assoc :position (if (= (:axis %) :x) - (+ (:position %) (- (:x new-frame) (:x frame))) - (+ (:position %) (- (:y new-frame) (:y frame))))))))] - (cond-> g - (not-empty new-guides) - (conj (into {} (map (juxt :id identity) new-guides)))))) - guides - frames)] + frames (->> shapes (filter cfh/frame-shape?)) + + new-guides + (reduce + (fn [g frame] + (let [new-id (ids-map (:id frame)) + new-frame (-> frame (gsh/move delta)) + + new-guides + (->> guides + (vals) + (filter #(= (:frame-id %) (:id frame))) + (map #(-> % + (assoc :id (uuid/next)) + (assoc :frame-id new-id) + (assoc :position (if (= (:axis %) :x) + (+ (:position %) (- (:x new-frame) (:x frame))) + (+ (:position %) (- (:y new-frame) (:y frame))))))))] + (cond-> g + (not-empty new-guides) + (conj (into {} (map (juxt :id identity) new-guides)))))) + guides + frames)] (-> (pcb/with-page changes page) (pcb/set-page-option :guides new-guides)))) @@ -533,7 +642,7 @@ (let [;; index-map is a map that goes from parent-id => vector([id index-in-parent]) index-map (reduce (fn [index-map id] (let [parent-id (get-in objects [id :parent-id]) - parent-index (cph/get-position-on-parent objects id)] + parent-index (cfh/get-position-on-parent objects id)] (update index-map parent-id (fnil conj []) [id parent-index]))) {} ids) @@ -553,9 +662,9 @@ objects-indices (->> index-map (d/mapm fix-indices) (vals) (reduce merge))] (pcb/amend-changes - changes - (fn [change] - (assoc change :index (get objects-indices (:old-id change))))))) + changes + (fn [change] + (assoc change :index (get objects-indices (:old-id change))))))) (defn clear-memorize-duplicated [] @@ -578,16 +687,16 @@ ptk/WatchEvent (watch [_ _ stream] - (let [stoper (rx/filter (ptk/type? ::memorize-duplicated) stream)] + (let [stopper (rx/filter (ptk/type? ::memorize-duplicated) stream)] (->> (rx/timer 10000) ;; This time may be adjusted after some user testing. - (rx/take-until stoper) + (rx/take-until stopper) (rx/map clear-memorize-duplicated)))))) (defn calc-duplicate-delta [obj state objects] (let [{:keys [id-original id-duplicated]} (get-in state [:workspace-local :duplicated]) - move? (and (cph/frame-shape? obj) + move? (and (cfh/frame-shape? obj) (not (ctk/instance-head? obj)))] (if (or (and (not= id-original (:id obj)) (not= id-duplicated (:id obj))) @@ -613,52 +722,56 @@ ([move-delta?] (duplicate-selected move-delta? false)) ([move-delta? alt-duplication?] - (ptk/reify ::duplicate-selected - ptk/WatchEvent - (watch [it state _] - (when (or (not move-delta?) (nil? (get-in state [:workspace-local :transform]))) - (let [page (wsh/lookup-page state) - objects (:objects page) - selected (wsh/lookup-selected state)] - (when (seq selected) - (let [obj (get objects (first selected)) - delta (if move-delta? - (calc-duplicate-delta obj state objects) - (gpt/point 0 0)) + (ptk/reify ::duplicate-selected + ptk/WatchEvent + (watch [it state _] + (when (or (not move-delta?) (nil? (get-in state [:workspace-local :transform]))) + (let [page (wsh/lookup-page state) + objects (:objects page) + selected (->> (wsh/lookup-selected state) + (map (d/getf objects)) + (filter #(ctk/allow-duplicate? objects %)) + (map :id) + set)] + (when (seq selected) + (let [obj (get objects (first selected)) + delta (if move-delta? + (calc-duplicate-delta obj state objects) + (gpt/point 0 0)) - file-id (:current-file-id state) - libraries (wsh/get-libraries state) - library-data (wsh/get-file state file-id) + file-id (:current-file-id state) + libraries (wsh/get-libraries state) + library-data (wsh/get-file state file-id) - changes (->> (prepare-duplicate-changes objects page selected delta it libraries library-data file-id) - (duplicate-changes-update-indices objects selected)) + changes (->> (prepare-duplicate-changes objects page selected delta it libraries library-data file-id) + (duplicate-changes-update-indices objects selected)) - tags (or (:tags changes) #{}) + tags (or (:tags changes) #{}) - changes (cond-> changes alt-duplication? (assoc :tags (conj tags :alt-duplication))) + changes (cond-> changes alt-duplication? (assoc :tags (conj tags :alt-duplication))) - id-original (first selected) + id-original (first selected) - new-selected (->> changes - :redo-changes - (filter #(= (:type %) :add-obj)) - (filter #(selected (:old-id %))) - (map #(get-in % [:obj :id])) - (into (d/ordered-set))) + new-selected (->> changes + :redo-changes + (filter #(= (:type %) :add-obj)) + (filter #(selected (:old-id %))) + (map #(get-in % [:obj :id])) + (into (d/ordered-set))) - id-duplicated (first new-selected) + id-duplicated (first new-selected) - frames (into #{} - (map #(get-in objects [% :frame-id])) - selected) - undo-id (js/Symbol)] + frames (into #{} + (map #(get-in objects [% :frame-id])) + selected) + undo-id (js/Symbol)] - ;; Warning: This order is important for the focus mode. - (rx/of + ;; Warning: This order is important for the focus mode. + (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) (select-shapes new-selected) - (ptk/data-event :layout/update frames) + (ptk/data-event :layout/update {:ids frames}) (memorize-duplicated id-original id-duplicated) (dwu/commit-undo-transaction undo-id)))))))))) @@ -681,7 +794,7 @@ focus (-> (:workspace-focus-selected state) (set/union added) (set/difference removed)) - focus (cph/clean-loops objects focus)] + focus (cfh/clean-loops objects focus)] (-> state (assoc :workspace-focus-selected focus)))))) @@ -689,6 +802,9 @@ (defn toggle-focus-mode [] (ptk/reify ::toggle-focus-mode + ev/Event + (-data [_] {}) + ptk/UpdateEvent (update [_ state] (let [selected (wsh/lookup-selected state)] diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index e235137a8e..37e40cf91a 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -1,4 +1,4 @@ -; This Source Code Form is subject to the terms of the Mozilla Public +;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; @@ -8,24 +8,28 @@ (:require [app.common.colors :as clr] [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] + [app.common.files.shapes-helpers :as cfsh] [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] - [app.common.math :as mth] - [app.common.pages.helpers :as cph] + [app.common.geom.shapes.flex-layout :as flex] + [app.common.geom.shapes.grid-layout :as grid] [app.common.types.component :as ctc] [app.common.types.modifiers :as ctm] - [app.common.types.shape-tree :as ctt] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] - [app.main.data.workspace.changes :as dwc] + [app.main.data.events :as ev] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.colors :as cl] + [app.main.data.workspace.grid-layout.editor :as dwge] [app.main.data.workspace.modifiers :as dwm] [app.main.data.workspace.selection :as dwse] - [app.main.data.workspace.shapes :as dws] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (def layout-keys [:layout @@ -52,152 +56,45 @@ :layout-padding-type :simple :layout-padding {:p1 0 :p2 0 :p3 0 :p4 0}}) -(def initial-grid-layout ;; TODO +(def initial-grid-layout {:layout :grid :layout-grid-dir :row :layout-gap-type :multiple :layout-gap {:row-gap 0 :column-gap 0} :layout-align-items :start - :layout-align-content :stretch :layout-justify-items :start - :layout-justify-content :start + :layout-align-content :stretch + :layout-justify-content :stretch :layout-padding-type :simple :layout-padding {:p1 0 :p2 0 :p3 0 :p4 0} + :layout-grid-cells {} :layout-grid-rows [] :layout-grid-columns []}) (defn get-layout-initializer [type from-frame?] - (let [initial-layout-data + (let [[initial-layout-data calculate-params] (case type - :flex initial-flex-layout - :grid initial-grid-layout)] - (fn [shape] - (-> shape - (merge initial-layout-data) - (cond-> (= type :grid) ctl/assign-cells) - ;; If the original shape is not a frame we set clip content and show-viewer to false - (cond-> (not from-frame?) - (assoc :show-content true :hide-in-viewer true)))))) + :flex [initial-flex-layout flex/calculate-params] + :grid [initial-grid-layout grid/calculate-params])] + (fn [shape objects] + (let [shape + (-> shape + (merge initial-layout-data) -(defn shapes->flex-params - "Given the shapes calculate its flex parameters (horizontal vs vertical, gaps, etc)" - ([objects shapes] - (shapes->flex-params objects shapes nil)) - ([objects shapes parent] - (let [points - (->> shapes - (map :id) - (ctt/sort-z-index objects) - (map (comp gsh/center-shape (d/getf objects)))) - - start (first points) - end (reduce (fn [acc p] (gpt/add acc (gpt/to-vec start p))) points) - - angle (gpt/signed-angle-with-other - (gpt/to-vec start end) - (gpt/point 1 0)) - - angle (mod angle 360) - - t1 (min (abs (- angle 0)) (abs (- angle 360))) - t2 (abs (- angle 90)) - t3 (abs (- angle 180)) - t4 (abs (- angle 270)) - - tmin (min t1 t2 t3 t4) - - direction - (cond - (mth/close? tmin t1) :row - (mth/close? tmin t2) :column-reverse - (mth/close? tmin t3) :row-reverse - (mth/close? tmin t4) :column) - - selrects (->> shapes - (mapv :selrect)) - min-x (->> selrects - (mapv #(min (:x1 %) (:x2 %))) - (apply min)) - max-x (->> selrects - (mapv #(max (:x1 %) (:x2 %))) - (apply max)) - all-width (->> selrects - (map :width) - (reduce +)) - column-gap (if (and (> (count shapes) 1) - (or (= direction :row) (= direction :row-reverse))) - (/ (- (- max-x min-x) all-width) - (dec (count shapes))) - 0) - - min-y (->> selrects - (mapv #(min (:y1 %) (:y2 %))) - (apply min)) - max-y (->> selrects - (mapv #(max (:y1 %) (:y2 %))) - (apply max)) - all-height (->> selrects - (map :height) - (reduce +)) - row-gap (if (and (> (count shapes) 1) - (or (= direction :column) (= direction :column-reverse))) - (/ (- (- max-y min-y) all-height) - (dec (count shapes))) - 0) - - layout-gap {:row-gap (max row-gap 0) :column-gap (max column-gap 0)} - - parent-selrect (:selrect parent) - padding (when (and (not (nil? parent)) (> (count shapes) 0)) - {:p1 (min (- min-y (:y1 parent-selrect)) (- (:y2 parent-selrect) max-y)) - :p2 (min (- min-x (:x1 parent-selrect)) (- (:x2 parent-selrect) max-x))})] - - (cond-> {:layout-flex-dir direction :layout-gap layout-gap} - (not (nil? padding)) - (assoc :layout-padding {:p1 (:p1 padding) :p2 (:p2 padding) :p3 (:p1 padding) :p4 (:p2 padding)}))))) - -(defn shapes->grid-params - "Given the shapes calculate its flex parameters (horizontal vs vertical, gaps, etc)" - ([objects shapes] - (shapes->flex-params objects shapes nil)) - ([_objects _shapes _parent] - {})) - -(defn create-layout-from-id - [ids type from-frame?] - (ptk/reify ::create-layout-from-id - ptk/WatchEvent - (watch [_ state _] - (let [objects (wsh/lookup-page-objects state) - children-ids (into [] (mapcat #(get-in objects [% :shapes])) ids) - children-shapes (map (d/getf objects) children-ids) - parent (get objects (first ids)) - layout-params (when (d/not-empty? children-shapes) - (case type - :flex (shapes->flex-params objects children-shapes parent) - :grid (shapes->grid-params objects children-shapes parent))) - undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) - (dwc/update-shapes ids (get-layout-initializer type from-frame?)) - (dwc/update-shapes - ids - (fn [shape] - (-> shape - (cond-> (not from-frame?) - (assoc :layout-item-h-sizing :auto - :layout-item-v-sizing :auto)) - (merge layout-params)))) - (ptk/data-event :layout/update ids) - (dwc/update-shapes children-ids #(dissoc % :constraints-h :constraints-v)) - (dwu/commit-undo-transaction undo-id)))))) + ;; If the original shape is not a frame we set clip content and show-viewer to false + (cond-> (not from-frame?) + (assoc :show-content true :hide-in-viewer true))) + params (calculate-params objects (cfh/get-immediate-children objects (:id shape)) shape)] + (cond-> (merge shape params) + (= type :grid) (-> (ctl/assign-cells objects) ctl/reorder-grid-children)))))) ;; Never call this directly but through the data-event `:layout/update` ;; Otherwise a lot of cycle dependencies could be generated (defn- update-layout-positions - [ids] + [{:keys [ids undo-group]}] (ptk/reify ::update-layout-positions ptk/WatchEvent (watch [_ state _] @@ -206,7 +103,8 @@ (if (d/not-empty? ids) (let [modif-tree (dwm/create-modif-tree ids (ctm/reflow-modifiers))] (rx/of (dwm/apply-modifiers {:modifiers modif-tree - :stack-undo? true}))) + :stack-undo? true + :undo-group undo-group}))) (rx/empty)))))) (defn initialize @@ -225,6 +123,25 @@ [] (ptk/reify ::finalize)) +(defn create-layout-from-id + [id type from-frame?] + (dm/assert! + "expected uuid for `id`" + (uuid? id)) + + (ptk/reify ::create-layout-from-id + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + parent (get objects id) + undo-id (js/Symbol) + layout-initializer (get-layout-initializer type from-frame?)] + + (rx/of (dwu/start-undo-transaction undo-id) + (dch/update-shapes [id] layout-initializer {:with-objects? true}) + (dch/update-shapes (dm/get-prop parent :shapes) #(dissoc % :constraints-h :constraints-v)) + (ptk/data-event :layout/update {:ids [id]}) + (dwu/commit-undo-transaction undo-id)))))) (defn create-layout-from-selection [type] @@ -237,87 +154,66 @@ selected (wsh/lookup-selected state) selected-shapes (map (d/getf objects) selected) single? (= (count selected-shapes) 1) - has-group? (->> selected-shapes (d/seek cph/group-shape?)) + has-group? (->> selected-shapes (d/seek cfh/group-shape?)) is-group? (and single? has-group?) - has-mask? (->> selected-shapes (d/seek cph/mask-shape?)) + has-mask? (->> selected-shapes (d/seek cfh/mask-shape?)) is-mask? (and single? has-mask?) has-component? (some true? (map ctc/instance-root? selected-shapes)) - is-component? (and single? has-component?)] + is-component? (and single? has-component?) - (if (and (not is-component?) is-group? (not is-mask?)) - (let [new-shape-id (uuid/next) - parent-id (:parent-id (first selected-shapes)) - shapes-ids (:shapes (first selected-shapes)) - ordered-ids (into (d/ordered-set) shapes-ids) - undo-id (js/Symbol) - group-index (cph/get-index-replacement selected objects)] - (rx/of - (dwu/start-undo-transaction undo-id) - (dwse/select-shapes ordered-ids) - (dws/create-artboard-from-selection new-shape-id parent-id group-index) - (cl/remove-all-fills [new-shape-id] {:color clr/black - :opacity 1}) - (create-layout-from-id [new-shape-id] type false) - (dwc/update-shapes - [new-shape-id] - (fn [shape] - (-> shape - (assoc :layout-item-h-sizing :auto - :layout-item-v-sizing :auto)))) - ;; Set the children to fixed to remove strange interactions - (dwc/update-shapes - selected - (fn [shape] - (-> shape - (assoc :layout-item-h-sizing :fix - :layout-item-v-sizing :fix)))) + new-shape-id (uuid/next) + undo-id (js/Symbol)] - (ptk/data-event :layout/update [new-shape-id]) - (dws/delete-shapes page-id selected) - (dwu/commit-undo-transaction undo-id))) + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id)) + (if (and is-group? (not is-component?) (not is-mask?)) + ;; Create layout from a group: + ;; When creating a layout from a group we remove the group and create the layout with its children + (let [parent-id (:parent-id (first selected-shapes)) + shapes-ids (:shapes (first selected-shapes)) + ordered-ids (into (d/ordered-set) shapes-ids) + group-index (cfh/get-index-replacement selected objects)] + (rx/of + (dwse/select-shapes ordered-ids) + (dwsh/create-artboard-from-selection new-shape-id parent-id group-index (:name (first selected-shapes))) + (cl/remove-all-fills [new-shape-id] {:color clr/black :opacity 1}) + (create-layout-from-id new-shape-id type false) + (dch/update-shapes [new-shape-id] #(assoc % :layout-item-h-sizing :auto :layout-item-v-sizing :auto)) + (dch/update-shapes selected #(assoc % :layout-item-h-sizing :fix :layout-item-v-sizing :fix)) + (dwsh/delete-shapes page-id selected) + (ptk/data-event :layout/update {:ids [new-shape-id]}) + (dwu/commit-undo-transaction undo-id))) - (let [new-shape-id (uuid/next) - undo-id (js/Symbol) - flex-params (shapes->flex-params objects selected-shapes)] - (rx/of - (dwu/start-undo-transaction undo-id) - (dws/create-artboard-from-selection new-shape-id) - (cl/remove-all-fills [new-shape-id] {:color clr/black - :opacity 1}) - (create-layout-from-id [new-shape-id] type false) - (dwc/update-shapes - [new-shape-id] - (fn [shape] - (-> shape - (merge flex-params) - (assoc :layout-item-h-sizing :auto - :layout-item-v-sizing :auto)))) - ;; Set the children to fixed to remove strange interactions - (dwc/update-shapes - selected - (fn [shape] - (-> shape - (assoc :layout-item-h-sizing :fix - :layout-item-v-sizing :fix)))) + ;; Create Layout from selection + (rx/of + (dwsh/create-artboard-from-selection new-shape-id) + (cl/remove-all-fills [new-shape-id] {:color clr/black :opacity 1}) + (create-layout-from-id new-shape-id type false) + (dch/update-shapes [new-shape-id] #(assoc % :layout-item-h-sizing :auto :layout-item-v-sizing :auto)) + (dch/update-shapes selected #(assoc % :layout-item-h-sizing :fix :layout-item-v-sizing :fix)))) - (ptk/data-event :layout/update [new-shape-id]) - (dwu/commit-undo-transaction undo-id)))))))) + (rx/of (ptk/data-event :layout/update {:ids [new-shape-id]}) + (dwu/commit-undo-transaction undo-id))))))) (defn remove-layout [ids] - (ptk/reify ::remove-layout + (ptk/reify ::remove-shape-layout ptk/WatchEvent (watch [_ _ _] (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dwc/update-shapes ids #(apply dissoc % layout-keys)) - (ptk/data-event :layout/update ids) + (dch/update-shapes ids #(apply dissoc % layout-keys)) + (ptk/data-event :layout/update {:ids ids}) (dwu/commit-undo-transaction undo-id)))))) (defn create-layout [type] - (ptk/reify ::create-layout + (ptk/reify ::create-shape-layout + ev/Event + (-data [_] + {:layout (d/name type)}) + ptk/WatchEvent (watch [_ state _] (let [page-id (:current-page-id state) @@ -331,26 +227,27 @@ (rx/of (dwu/start-undo-transaction undo-id) (if (and single? is-frame?) - (create-layout-from-id [(first selected)] type true) + (create-layout-from-id (first selected) type true) (create-layout-from-selection type)) (dwu/commit-undo-transaction undo-id)))))) -(defn toggle-layout-flex - [] - (ptk/reify ::toggle-layout-flex +(defn toggle-layout + [type] + (ptk/reify ::toggle-shape-layout ptk/WatchEvent - (watch [_ state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) + (watch [it state _] + (let [objects (wsh/lookup-page-objects state) selected (wsh/lookup-selected state) selected-shapes (map (d/getf objects) selected) single? (= (count selected-shapes) 1) - has-flex-layout? (and single? (ctl/flex-layout? objects (:id (first selected-shapes))))] + has-layout? (and single? + (ctl/any-layout? objects (:id (first selected-shapes))))] (when (not= 0 (count selected)) - (if has-flex-layout? - (rx/of (remove-layout selected)) - (rx/of (create-layout :flex)))))))) + (let [event (if has-layout? + (remove-layout selected) + (create-layout type))] + (rx/of (with-meta event (meta it))))))))) (defn update-layout [ids changes] @@ -359,109 +256,184 @@ (watch [_ _ _] (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dwc/update-shapes ids #(d/deep-merge % changes)) - (ptk/data-event :layout/update ids) + (dch/update-shapes ids (d/patch-object changes)) + (ptk/data-event :layout/update {:ids ids}) (dwu/commit-undo-transaction undo-id)))))) -#_(defn update-grid-cells - [parent objects] - (let [children (cph/get-immediate-children objects (:id parent)) - layout-grid-rows (:layout-grid-rows parent) - layout-grid-columns (:layout-grid-columns parent) - num-rows (count layout-grid-columns) - num-columns (count layout-grid-columns) - layout-grid-cells (:layout-grid-cells parent) - - allocated-shapes - (into #{} (mapcat :shapes) (:layout-grid-cells parent)) - - no-cell-shapes - (->> children (:shapes parent) (remove allocated-shapes)) - - layout-grid-cells - (for [[row-idx row] (d/enumerate layout-grid-rows) - [col-idx col] (d/enumerate layout-grid-columns)] - - (let [shape (nth children (+ (* row-idx num-columns) col-idx) nil) - cell-data {:id (uuid/next) - :row (inc row-idx) - :column (inc col-idx) - :row-span 1 - :col-span 1 - :shapes (when shape [(:id shape)])}] - [(:id cell-data) cell-data]))] - (assoc parent :layout-grid-cells (into {} layout-grid-cells)))) - -#_(defn check-grid-cells-update - [ids] - (ptk/reify ::check-grid-cells-update - ptk/WatchEvent - (watch [_ state _] - (let [objects (wsh/lookup-page-objects state) - undo-id (js/Symbol)] - (rx/of (dwc/update-shapes - ids - (fn [shape] - (-> shape - (update-grid-cells objects))))))))) - (defn add-layout-track - [ids type value] - (assert (#{:row :column} type)) - (ptk/reify ::add-layout-column - ptk/WatchEvent - (watch [_ _ _] - (let [undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) - (dwc/update-shapes - ids - (fn [shape] - (case type - :row (ctl/add-grid-row shape value) - :column (ctl/add-grid-column shape value)))) - (ptk/data-event :layout/update ids) - (dwu/commit-undo-transaction undo-id)))))) + ([ids type value] + (add-layout-track ids type value nil)) + ([ids type value index] + (assert (#{:row :column} type)) + (ptk/reify ::add-layout-track + ptk/WatchEvent + (watch [_ _ _] + (let [undo-id (js/Symbol)] + (rx/of (dwu/start-undo-transaction undo-id) + (dch/update-shapes + ids + (fn [shape] + (case type + :row (ctl/add-grid-row shape value index) + :column (ctl/add-grid-column shape value index)))) + (ptk/data-event :layout/update {:ids ids}) + (dwu/commit-undo-transaction undo-id))))))) (defn remove-layout-track + [ids type index & {:keys [with-shapes?] :or {with-shapes? false}}] + (assert (#{:row :column} type)) + + (ptk/reify ::remove-layout-track + ptk/WatchEvent + (watch [_ state _] + (let [undo-id (js/Symbol)] + (let [objects (wsh/lookup-page-objects state) + + shapes-to-delete + (when with-shapes? + (->> ids + (mapcat + (fn [id] + (let [shape (get objects id)] + (if (= type :column) + (ctl/shapes-by-column shape index) + (ctl/shapes-by-row shape index))))) + (into #{})))] + (rx/of (dwu/start-undo-transaction undo-id) + (if shapes-to-delete + (dwsh/delete-shapes shapes-to-delete) + (rx/empty)) + (dch/update-shapes + ids + (fn [shape objects] + (case type + :row (ctl/remove-grid-row shape index objects) + :column (ctl/remove-grid-column shape index objects))) + {:with-objects? true}) + (ptk/data-event :layout/update {:ids ids}) + (dwu/commit-undo-transaction undo-id))))))) + +(defn duplicate-layout-track [ids type index] (assert (#{:row :column} type)) - (ptk/reify ::remove-layout-column + (ptk/reify ::duplicate-layout-track + ptk/WatchEvent + (watch [it state _] + (let [file-id (:current-file-id state) + page (wsh/lookup-page state) + objects (:objects page) + libraries (wsh/get-libraries state) + library-data (wsh/get-file state file-id) + shape-id (first ids) + base-shape (get objects shape-id) + + shapes-by-track + (if (= type :column) + (ctl/shapes-by-column base-shape index false) + (ctl/shapes-by-row base-shape index false)) + + ;; Change to set in order to use auxiliary functions + selected (set shapes-by-track) + + changes + (->> (dwse/prepare-duplicate-changes objects page selected (gpt/point 0 0) it libraries library-data file-id) + (dwse/duplicate-changes-update-indices objects selected)) + + ;; Creates a map with shape-id => duplicated-shape-id + ids-map + (->> changes + :redo-changes + (filter #(= (:type %) :add-obj)) + (filter #(selected (:old-id %))) + (map #(vector (:old-id %) (get-in % [:obj :id]))) + (into {})) + + changes + (-> changes + (pcb/update-shapes + ids + (fn [shape objects] + ;; The duplication could have altered the grid so we restore the values, we'll calculate the good ones now + (let [shape (merge shape (select-keys base-shape [:layout-grid-cells :layout-grid-columns :layout-grid-rows]))] + (case type + :row (ctl/duplicate-row shape objects index ids-map) + :column (ctl/duplicate-column shape objects index ids-map)))) + {:with-objects? true})) + + undo-id (js/Symbol)] + (rx/of (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (ptk/data-event :layout/update {:ids ids}) + (dwu/commit-undo-transaction undo-id)))))) + +(defn reorder-layout-track + [ids type from-index to-index move-content?] + (assert (#{:row :column} type)) + + (ptk/reify ::reorder-layout-track ptk/WatchEvent (watch [_ _ _] (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dwc/update-shapes + (dch/update-shapes ids (fn [shape] (case type - :row (ctl/remove-grid-row shape index) - :column (ctl/remove-grid-column shape index)))) - (ptk/data-event :layout/update ids) + :row (ctl/reorder-grid-row shape from-index to-index move-content?) + :column (ctl/reorder-grid-column shape from-index to-index move-content?)))) + (ptk/data-event :layout/update {:ids ids}) (dwu/commit-undo-transaction undo-id)))))) +(defn hover-layout-track + [ids type index hover?] + (assert (#{:row :column} type)) + + (ptk/reify ::hover-layout-track + ptk/UpdateEvent + (update [_ state] + (let [objects (wsh/lookup-page-objects state) + shape (get objects (first ids)) + + highlighted + (when hover? + (->> (if (= type :row) + (ctl/shapes-by-row shape index) + (ctl/shapes-by-column shape index)) + (set)))] + (cond-> state + hover? + (update-in [:workspace-grid-edition (first ids) :hover-track] (fnil conj #{}) [type index]) + + (not hover?) + (update-in [:workspace-grid-edition (first ids) :hover-track] (fnil disj #{}) [type index]) + + :always + (assoc-in [:workspace-local :highlighted] highlighted)))))) + (defn change-layout-track [ids type index props] (assert (#{:row :column} type)) - (ptk/reify ::change-layout-column + (ptk/reify ::change-layout-track ptk/WatchEvent (watch [_ _ _] (let [undo-id (js/Symbol) - property (case :row :layout-grid-rows - :column :layout-grid-columns)] + property (case type + :row :layout-grid-rows + :column :layout-grid-columns)] (rx/of (dwu/start-undo-transaction undo-id) - (dwc/update-shapes + (dch/update-shapes ids (fn [shape] (-> shape (update-in [property index] merge props)))) - (ptk/data-event :layout/update ids) + (ptk/data-event :layout/update {:ids ids}) (dwu/commit-undo-transaction undo-id)))))) (defn fix-child-sizing [objects parent-changes shape] - (let [parent (-> (cph/get-parent objects (:id shape)) + (let [parent (-> (cfh/get-parent objects (:id shape)) (d/deep-merge parent-changes)) auto-width? (ctl/auto-width? parent) @@ -472,7 +444,7 @@ all-children (->> parent :shapes (map (d/getf objects)) - (remove ctl/layout-absolute?))] + (remove ctl/position-absolute?))] (cond-> shape ;; If the parent is hug width and the direction column @@ -496,7 +468,7 @@ (assoc :layout-item-v-sizing :fix)))) (defn fix-parent-sizing - [objects ids-set changes parent] + [parent objects ids-set changes] (let [auto-width? (ctl/auto-width? parent) auto-height? (ctl/auto-height? parent) @@ -538,12 +510,220 @@ ptk/WatchEvent (watch [_ state _] (let [objects (wsh/lookup-page-objects state) - children-ids (->> ids (mapcat #(cph/get-children-ids objects %))) - parent-ids (->> ids (map #(cph/get-parent-id objects %))) + children-ids (->> ids (mapcat #(cfh/get-children-ids objects %))) + parent-ids (->> ids (map #(cfh/get-parent-id objects %))) undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dwc/update-shapes ids #(d/deep-merge (or % {}) changes)) - (dwc/update-shapes children-ids (partial fix-child-sizing objects changes)) - (dwc/update-shapes parent-ids (partial fix-parent-sizing objects (set ids) changes)) - (ptk/data-event :layout/update ids) + (dch/update-shapes ids (d/patch-object changes)) + (dch/update-shapes children-ids (partial fix-child-sizing objects changes)) + (dch/update-shapes + parent-ids + (fn [parent objects] + (-> parent + (fix-parent-sizing objects (set ids) changes) + (cond-> (ctl/grid-layout? parent) + (ctl/assign-cells objects)))) + {:with-objects? true}) + (ptk/data-event :layout/update {:ids ids}) (dwu/commit-undo-transaction undo-id)))))) + +(defn update-grid-cells + [layout-id ids props] + (ptk/reify ::update-grid-cells + ptk/WatchEvent + (watch [_ _ _] + (let [undo-id (js/Symbol)] + (rx/of + (dwu/start-undo-transaction undo-id) + + (dch/update-shapes + [layout-id] + (fn [shape] + (->> ids + (reduce + (fn [shape cell-id] + (d/update-in-when + shape + [:layout-grid-cells cell-id] + d/patch-object props)) + shape)))) + (ptk/data-event :layout/update {:ids [layout-id]}) + (dwu/commit-undo-transaction undo-id)))))) + +(defn change-cells-mode + [layout-id ids mode] + + (ptk/reify ::change-cells-mode + ptk/WatchEvent + (watch [_ _ _] + (let [undo-id (js/Symbol)] + (rx/of + (dwu/start-undo-transaction undo-id) + (dch/update-shapes + [layout-id] + (fn [shape objects] + (case mode + :auto + ;; change the manual cells and move to auto + (->> ids + (reduce + (fn [shape cell-id] + (let [cell (get-in shape [:layout-grid-cells cell-id])] + (cond-> shape + (or (contains? #{:area :manual} (:position cell)) + (> (:row-span cell) 1) + (> (:column-span cell) 1)) + (-> (d/update-in-when [:layout-grid-cells cell-id] assoc :shapes [] :position :auto) + (d/update-in-when [:layout-grid-cells cell-id] dissoc :area-name) + (ctl/resize-cell-area (:row cell) (:column cell) (:row cell) (:column cell) 1 1) + (ctl/assign-cells objects))))) + shape)) + + :manual + (->> ids + (reduce + (fn [shape cell-id] + (let [cell (get-in shape [:layout-grid-cells cell-id])] + (cond-> shape + (contains? #{:area :auto} (:position cell)) + (-> (d/assoc-in-when [:layout-grid-cells cell-id :position] :manual) + (d/update-in-when [:layout-grid-cells cell-id] dissoc :area-name) + (ctl/assign-cells objects))))) + shape)) + + :area + ;; Create area with the selected cells + (let [{:keys [first-row first-column last-row last-column]} + (ctl/cells-coordinates (->> ids (map #(get-in shape [:layout-grid-cells %])))) + + target-cell + (ctl/get-cell-by-position shape first-row first-column) + + shape + (-> shape + (ctl/resize-cell-area + (:row target-cell) (:column target-cell) + first-row + first-column + (inc (- last-row first-row)) + (inc (- last-column first-column))) + (ctl/assign-cells objects))] + + (-> shape + (d/update-in-when [:layout-grid-cells (:id target-cell)] assoc :position :area))))) + {:with-objects? true}) + (dwge/clean-selection layout-id) + (ptk/data-event :layout/update {:ids [layout-id]}) + (dwu/commit-undo-transaction undo-id)))))) + +(defn merge-cells + [layout-id ids] + + (ptk/reify ::merge-cells + ptk/WatchEvent + (watch [_ _ _] + (let [undo-id (js/Symbol)] + (rx/of + (dwu/start-undo-transaction undo-id) + (dch/update-shapes + [layout-id] + (fn [shape objects] + (let [cells (->> ids (map #(get-in shape [:layout-grid-cells %]))) + + {:keys [first-row first-column last-row last-column]} + (ctl/cells-coordinates cells) + + target-cell + (ctl/get-cell-by-position shape first-row first-column)] + (-> shape + (ctl/resize-cell-area + (:row target-cell) (:column target-cell) + first-row + first-column + (inc (- last-row first-row)) + (inc (- last-column first-column))) + (ctl/assign-cells objects)))) + {:with-objects? true}) + (dwge/clean-selection layout-id) + (ptk/data-event :layout/update {:ids [layout-id]}) + (dwu/commit-undo-transaction undo-id)))))) + +(defn update-grid-cell-position + [layout-id cell-id props] + + (ptk/reify ::update-grid-cell-position + ptk/WatchEvent + (watch [_ _ _] + (let [undo-id (js/Symbol)] + (rx/of + (dwu/start-undo-transaction undo-id) + (dch/update-shapes + [layout-id] + (fn [shape objects] + (let [prev-data (-> (dm/get-in shape [:layout-grid-cells cell-id]) + (select-keys [:row :column :row-span :column-span])) + + new-data (merge prev-data props)] + (-> shape + (ctl/resize-cell-area (:row prev-data) (:column prev-data) + (:row new-data) (:column new-data) + (:row-span new-data) (:column-span new-data)) + (ctl/assign-cells objects)))) + {:with-objects? true}) + (ptk/data-event :layout/update {:ids [layout-id]}) + (dwu/commit-undo-transaction undo-id)))))) + + +(defn create-cell-board + [layout-id cell-ids] + (ptk/reify ::create-cell-board + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state) + frame-id (uuid/next) + + undo-id (js/Symbol) + + shape (get objects layout-id) + cells (->> cell-ids (map #(get-in shape [:layout-grid-cells %]))) + selected (into #{} (mapcat :shapes) cells) + + {:keys [first-row first-column last-row last-column]} (ctl/cells-coordinates cells) + + target-cell (ctl/get-cell-by-position shape first-row first-column) + + [_ changes] + (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) + (cond-> (d/not-empty? selected) + (cfsh/prepare-create-artboard-from-selection + frame-id layout-id objects selected 0 nil true (:id target-cell))) + + (cond-> (empty? (seq selected)) + (cfsh/prepare-create-empty-artboard + frame-id layout-id objects 0 nil true (:id target-cell)))) + + changes + (-> changes + (pcb/update-shapes + [frame-id] + (fn [shape] + (-> shape + (assoc :layout-item-h-sizing :fill) + (assoc :layout-item-v-sizing :fill)))) + (pcb/update-shapes + [layout-id] + (fn [shape] + (let [new-row-span (inc (- last-row first-row)) + new-col-span (inc (- last-column first-column))] + (-> shape + (ctl/resize-cell-area + (:row target-cell) (:column target-cell) + first-row first-column new-row-span new-col-span))))))] + + (rx/of + (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (ptk/data-event :layout/update {:ids [layout-id]}) + (dwu/commit-undo-transaction undo-id)))))) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 6341a07375..4fabc4e80a 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -8,19 +8,15 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.geom.proportions :as gpp] - [app.common.geom.shapes :as gsh] - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] + [app.common.files.shapes-helpers :as cfsh] [app.common.schema :as sm] - [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.page :as ctp] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.types.shape.interactions :as ctsi] - [app.common.types.shape.layout :as ctl] - [app.common.uuid :as uuid] [app.main.data.comments :as dc] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.edition :as dwe] @@ -28,99 +24,31 @@ [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.features :as features] - [app.main.streams :as ms] - [beicon.core :as rx] - [potok.core :as ptk])) - -(defn get-shape-layer-position - [objects selected attrs] - - ;; Calculate the frame over which we're drawing - (let [position @ms/mouse-position - frame-id (:frame-id attrs (ctst/top-nested-frame objects position)) - shape (when-not (empty? selected) - (cph/get-base-shape objects selected))] - - ;; When no shapes has been selected or we're over a different frame - ;; we add it as the latest shape of that frame - (if (or (not shape) (not= (:frame-id shape) frame-id)) - [frame-id frame-id nil] - - ;; Otherwise, we add it to next to the selected shape - (let [index (cph/get-position-on-parent objects (:id shape)) - {:keys [frame-id parent-id]} shape] - [frame-id parent-id (inc index)])))) - -(defn make-new-shape - [attrs objects selected] - (let [default-attrs (if (= :frame (:type attrs)) - cts/default-frame-attrs - cts/default-shape-attrs) - - default-attrs (if (or (= :group (:type attrs)) - (= :bool (:type attrs))) - (assoc default-attrs :shapes []) - default-attrs) - - selected-non-frames - (into #{} (comp (map (d/getf objects)) - (remove cph/frame-shape?)) - selected) - - [frame-id parent-id index] - (get-shape-layer-position objects selected-non-frames attrs)] - - (-> (merge default-attrs attrs) - (gpp/setup-proportions) - (assoc :frame-id frame-id - :parent-id parent-id - :index index)))) - -(defn prepare-add-shape - [changes attrs objects selected] - (let [id (or (:id attrs) (uuid/next)) - name (:name attrs) - - shape (make-new-shape - (assoc attrs :id id :name name) - objects - selected) - - index (:index (meta attrs)) - - changes (-> changes - (pcb/with-objects objects) - (cond-> (some? index) - (pcb/add-object shape {:index index})) - (cond-> (nil? index) - (pcb/add-object shape)) - (cond-> (some? (:parent-id attrs)) - (pcb/change-parent (:parent-id attrs) [shape] index)) - (cond-> (ctl/grid-layout? objects (:parent-id shape)) - (pcb/update-shapes [(:parent-id shape)] ctl/assign-cells)))] - - [shape changes])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (defn add-shape - ([attrs] - (add-shape attrs {})) - ([attrs {:keys [no-select? no-update-layout?]}] - (dm/assert! (cts/shape-attrs? attrs)) + ([shape] + (add-shape shape {})) + ([shape {:keys [no-select? no-update-layout?]}] + + (dm/verify! + "expected a valid shape" + (cts/check-shape! shape)) + (ptk/reify ::add-shape ptk/WatchEvent (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - selected (wsh/lookup-selected state) - - changes (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects)) [shape changes] - (prepare-add-shape changes attrs objects selected) + (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) + (cfsh/prepare-add-shape shape objects)) changes (cond-> changes - (cph/text-shape? shape) + (cfh/text-shape? shape) (pcb/set-undo-group (:id shape))) undo-id (js/Symbol)] @@ -129,29 +57,14 @@ (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) (when-not no-update-layout? - (ptk/data-event :layout/update [(:parent-id shape)])) + (ptk/data-event :layout/update {:ids [(:parent-id shape)]})) (when-not no-select? (dws/select-shapes (d/ordered-set (:id shape)))) (dwu/commit-undo-transaction undo-id)) - (when (= :text (:type attrs)) + (when (cfh/text-shape? shape) (->> (rx/of (dwe/start-edition-mode (:id shape))) (rx/observe-on :async))))))))) -(defn prepare-move-shapes-into-frame - [changes frame-id shapes objects] - (let [ordered-indexes (cph/order-by-indexed-shapes objects shapes) - parent-id (get-in objects [frame-id :parent-id]) - ordered-indexes (->> ordered-indexes (remove #(= % parent-id))) - to-move-shapes (map (d/getf objects) ordered-indexes)] - (when (d/not-empty? to-move-shapes) - (-> changes - (cond-> (not (ctl/any-layout? objects frame-id)) - (pcb/update-shapes ordered-indexes ctl/remove-layout-item-data)) - (pcb/update-shapes ordered-indexes #(cond-> % (cph/frame-shape? %) (assoc :hide-in-viewer true))) - (pcb/change-parent frame-id to-move-shapes 0) - (cond-> (ctl/grid-layout? objects frame-id) - (pcb/update-shapes [frame-id] ctl/assign-cells)))))) - (defn move-shapes-into-frame [frame-id shapes] (ptk/reify ::move-shapes-into-frame @@ -159,13 +72,15 @@ (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - shapes (->> shapes (remove #(dm/get-in objects [% :blocked]))) + shapes (->> shapes + (remove #(dm/get-in objects [% :blocked])) + (cfh/order-by-indexed-shapes objects)) + changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects)) - changes (prepare-move-shapes-into-frame changes - frame-id - shapes - objects)] + + changes (cfsh/prepare-move-shapes-into-frame changes frame-id shapes objects)] + (if (some? changes) (rx/of (dch/commit-changes changes)) (rx/empty)))))) @@ -174,9 +89,13 @@ (declare update-shape-flags) (defn delete-shapes - ([ids] (delete-shapes nil ids)) - ([page-id ids] - (dm/assert! (sm/set-of-uuid? ids)) + ([ids] (delete-shapes nil ids {})) + ([page-id ids] (delete-shapes page-id ids {})) + ([page-id ids options] + (dm/assert! + "expected a valid set of uuid's" + (sm/check-set-of-uuid! ids)) + (ptk/reify ::delete-shapes ptk/WatchEvent (watch [it state _] @@ -186,20 +105,20 @@ page (wsh/lookup-page state page-id) objects (wsh/lookup-page-objects state page-id) - components-v2 (features/active-feature? state :components-v2) + components-v2 (features/active-feature? state "components/v2") - ids (cph/clean-loops objects ids) + ids (cfh/clean-loops objects ids) in-component-copy? (fn [shape-id] ;; Look for shapes that are inside a component copy, but are ;; not the root. In this case, they must not be deleted, ;; but hidden (to be able to recover them more easily). - (let [shape (get objects shape-id) - component-shape (ctn/get-component-shape objects shape)] - (and (ctk/in-component-copy? shape) - (not= shape component-shape) - (not (ctk/main-instance? component-shape))))) + ;; Unless we are doing a component swap, in which case we want + ;; to delete the old shape + (let [shape (get objects shape-id)] + (and (ctn/has-any-copy-parent? objects shape) + (not (:component-swap options))))) [ids-to-delete ids-to-hide] (if components-v2 @@ -218,22 +137,26 @@ ids-to-hide))))) [ids []]) - undo-id (js/Symbol)] + undo-id (or (:undo-id options) (js/Symbol))] (rx/concat (rx/of (dwu/start-undo-transaction undo-id) - (update-shape-flags ids-to-hide {:hidden true})) - (real-delete-shapes file page objects ids-to-delete it components-v2) + (update-shape-flags ids-to-hide {:hidden true :undo-group (:undo-group options)})) + (real-delete-shapes file page objects ids-to-delete it {:components-v2 components-v2 + :ignore-touched (:component-swap options) + :undo-group (:undo-group options) + :undo-id undo-id}) (rx/of (dwu/commit-undo-transaction undo-id)))))))) (defn- real-delete-shapes-changes - ([file page objects ids it components-v2] + ([file page objects ids it {:keys [undo-group] :as options}] (let [changes (-> (pcb/empty-changes it (:id page)) + (pcb/set-undo-group undo-group) (pcb/with-page page) (pcb/with-objects objects) (pcb/with-library-data file))] - (real-delete-shapes-changes changes file page objects ids it components-v2))) - ([changes file page objects ids _it components-v2] + (real-delete-shapes-changes changes file page objects ids it options))) + ([changes file page objects ids _it {:keys [components-v2 ignore-touched]}] (let [lookup (d/getf objects) groups-to-unmask (reduce (fn [group-ids id] @@ -242,7 +165,7 @@ ;; converted to a normal group. (let [obj (lookup id) parent (lookup (:parent-id obj))] - (if (and (:masked-group? parent) + (if (and (:masked-group parent) (= id (first (:shapes parent)))) (conj group-ids (:id parent)) group-ids))) @@ -259,12 +182,16 @@ interactions))) (vals objects)) - ;; If any of the deleted shapes is a frame with guides - guides (into {} - (comp (map second) - (remove #(contains? ids (:frame-id %))) - (map (juxt :id identity))) - (dm/get-in page [:options :guides])) + ids-set (set ids) + guides-to-remove + (->> (dm/get-in page [:options :guides]) + (vals) + (filter #(contains? ids-set (:frame-id %))) + (map :id)) + + guides + (->> guides-to-remove + (reduce dissoc (dm/get-in page [:options :guides]))) starting-flows (filter (fn [flow] @@ -276,14 +203,14 @@ all-parents (reduce (fn [res id] ;; All parents of any deleted shape must be resized. - (into res (cph/get-parent-ids objects id))) + (into res (cfh/get-parent-ids objects id))) (d/ordered-set) ids) all-children (->> ids ;; Children of deleted shapes must be also deleted. (reduce (fn [res id] - (into res (cph/get-children-ids objects id))) + (into res (cfh/get-children-ids objects id))) []) (reverse) (into (d/ordered-set))) @@ -293,7 +220,7 @@ (let [all-ids (into empty-parents ids) contains? (partial contains? all-ids) xform (comp (map lookup) - (filter #(or (cph/group-shape? %) (cph/bool-shape? %))) + (filter #(or (cfh/group-shape? %) (cfh/bool-shape? %))) (remove #(->> (:shapes %) (remove contains?) seq)) (map :id)) parents (into #{} xform all-parents)] @@ -310,7 +237,7 @@ (reduce (fn [components id] (let [shape (get objects id)] (if (and (= (:component-file shape) (:id file)) ;; Main instances should exist only in local file - (:main-instance? shape)) ;; but check anyway + (:main-instance shape)) ;; but check anyway (conj components (:component-id shape)) components))) [] @@ -323,18 +250,18 @@ changes (reduce (fn [changes component-id] ;; It's important to delete the component before the main instance, because we ;; need to store the instance position if we want to restore it later. - (pcb/delete-component changes component-id)) + (pcb/delete-component changes component-id (:id page))) changes components-to-delete) changes (-> changes (pcb/remove-objects all-children {:ignore-touched true}) - (pcb/remove-objects ids) + (pcb/remove-objects ids {:ignore-touched ignore-touched}) (pcb/remove-objects empty-parents) (pcb/resize-parents all-parents) (pcb/update-shapes groups-to-unmask (fn [shape] - (assoc shape :masked-group? false))) + (assoc shape :masked-group false))) (pcb/update-shapes (map :id interacting-shapes) (fn [shape] (d/update-when shape :interactions @@ -351,84 +278,68 @@ (defn delete-shapes-changes - [changes file page objects ids it components-v2] - (let [[changes _all-parents] (real-delete-shapes-changes changes file page objects ids it components-v2)] + [changes file page objects ids it components-v2 ignore-touched] + (let [[changes _all-parents] (real-delete-shapes-changes changes + file + page + objects + ids + it + {:components-v2 components-v2 + :ignore-touched ignore-touched})] changes)) - (defn- real-delete-shapes - [file page objects ids it components-v2] - (let [[changes all-parents] (real-delete-shapes-changes file page objects ids it components-v2) - undo-id (js/Symbol)] + [file page objects ids it options] + (let [[changes all-parents] (real-delete-shapes-changes file page objects ids it options) + undo-id (or (:undo-id options) (js/Symbol))] (rx/of (dwu/start-undo-transaction undo-id) (dc/detach-comment-thread ids) (dch/commit-changes changes) - (ptk/data-event :layout/update all-parents) + (ptk/data-event :layout/update {:ids all-parents :undo-group (:undo-group options)}) (dwu/commit-undo-transaction undo-id)))) (defn create-and-add-shape - [type frame-x frame-y data] + [type frame-x frame-y {:keys [width height] :as attrs}] (ptk/reify ::create-and-add-shape ptk/WatchEvent (watch [_ state _] - (let [{:keys [width height]} data + (let [vbc (wsh/viewport-center state) + x (:x attrs (- (:x vbc) (/ width 2))) + y (:y attrs (- (:y vbc) (/ height 2))) + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + frame-id (-> (wsh/lookup-page-objects state page-id) + (ctst/top-nested-frame {:x frame-x :y frame-y})) - vbc (wsh/viewport-center state) - x (:x data (- (:x vbc) (/ width 2))) - y (:y data (- (:y vbc) (/ height 2))) - page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - frame-id (-> (wsh/lookup-page-objects state page-id) - (ctst/top-nested-frame {:x frame-x :y frame-y})) - selected (wsh/lookup-selected state) - page-objects (wsh/lookup-page-objects state) - base (cph/get-base-shape page-objects selected) - selected-frame? (and (= 1 (count selected)) - (= :frame (get-in objects [(first selected) :type]))) - parent-id (if - (or selected-frame? (empty? selected)) frame-id - (:parent-id base)) + selected (wsh/lookup-selected state) + base (cfh/get-base-shape objects selected) + + parent-id (if (or (and (= 1 (count selected)) + (cfh/frame-shape? (get objects (first selected)))) + (empty? selected)) + frame-id + (:parent-id base)) + + ;; If the parent-id or the frame-id are component-copies, we need to get the first not copy parent + parent-id (:id (ctn/get-first-not-copy-parent objects parent-id)) ;; We don't want to change the structure of component copies + frame-id (:id (ctn/get-first-not-copy-parent objects frame-id)) + + + shape (cts/setup-shape + (-> attrs + (assoc :type type) + (assoc :x x) + (assoc :y y) + (assoc :frame-id frame-id) + (assoc :parent-id parent-id)))] - shape (-> (cts/make-minimal-shape type) - (merge data) - (merge {:x x :y y}) - (assoc :frame-id frame-id :parent-id parent-id) - (cts/setup-rect-selrect))] (rx/of (add-shape shape)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Artboard ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn prepare-create-artboard-from-selection - [changes id parent-id objects selected index frame-name without-fill?] - (let [selected-objs (map #(get objects %) selected) - new-index (or index - (cph/get-index-replacement selected objects))] - (when (d/not-empty? selected) - (let [srect (gsh/selection-rect selected-objs) - frame-id (get-in objects [(first selected) :frame-id]) - parent-id (or parent-id (get-in objects [(first selected) :parent-id])) - shape (-> (cts/make-minimal-shape :frame) - (merge {:x (:x srect) :y (:y srect) :width (:width srect) :height (:height srect)}) - (cond-> id - (assoc :id id)) - (cond-> frame-name - (assoc :name frame-name)) - (assoc :frame-id frame-id :parent-id parent-id) - (with-meta {:index new-index}) - (cond-> (or (not= frame-id uuid/zero) without-fill?) - (assoc :fills [] :hide-in-viewer true)) - (cts/setup-rect-selrect)) - - [shape changes] - (prepare-add-shape changes shape objects selected) - - changes - (prepare-move-shapes-into-frame changes (:id shape) selected objects)] - - [shape changes])))) - (defn create-artboard-from-selection ([] (create-artboard-from-selection nil)) @@ -437,26 +348,29 @@ ([id parent-id] (create-artboard-from-selection id parent-id nil)) ([id parent-id index] + (create-artboard-from-selection id parent-id index nil)) + ([id parent-id index name] (ptk/reify ::create-artboard-from-selection ptk/WatchEvent (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - selected (wsh/lookup-selected state) - selected (cph/clean-loops objects selected) + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + selected (->> (wsh/lookup-selected state) + (cfh/clean-loops objects) + (remove #(ctn/has-any-copy-parent? objects (get objects %)))) - changes (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects)) + changes (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects)) [frame-shape changes] - (prepare-create-artboard-from-selection changes - id - parent-id - objects - selected - index - nil - false) + (cfsh/prepare-create-artboard-from-selection changes + id + parent-id + objects + selected + index + name + false) undo-id (js/Symbol)] @@ -465,7 +379,7 @@ (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) (dws/select-shapes (d/ordered-set (:id frame-shape))) - (ptk/data-event :layout/update [(:id frame-shape)]) + (ptk/data-event :layout/update {:ids [(:id frame-shape)]}) (dwu/commit-undo-transaction undo-id)))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -473,14 +387,14 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn update-shape-flags - [ids {:keys [blocked hidden] :as flags}] + [ids {:keys [blocked hidden transforming undo-group] :as flags}] (dm/assert! "expected valid coll of uuids" (every? uuid? ids)) (dm/assert! "expected valid shape-attrs value for `flags`" - (cts/shape-attrs? flags)) + (cts/check-shape-attrs! flags)) (ptk/reify ::update-shape-flags ptk/WatchEvent @@ -489,14 +403,15 @@ (fn [obj] (cond-> obj (boolean? blocked) (assoc :blocked blocked) - (boolean? hidden) (assoc :hidden hidden))) + (boolean? hidden) (assoc :hidden hidden) + (boolean? transforming) (assoc :transforming transforming))) objects (wsh/lookup-page-objects state) ;; We have change only the hidden behaviour, to hide only the ;; selected shape, block behaviour remains the same. ids (if (boolean? blocked) - (into ids (->> ids (mapcat #(cph/get-children-ids objects %)))) + (into ids (->> ids (mapcat #(cfh/get-children-ids objects %)))) ids)] - (rx/of (dch/update-shapes ids update-fn)))))) + (rx/of (dch/update-shapes ids update-fn {:attrs #{:blocked :hidden :transforming} :undo-group undo-group})))))) (defn toggle-visibility-selected [] @@ -523,10 +438,12 @@ ptk/WatchEvent (watch [_ state _] (let [selected (wsh/lookup-selected state) - pages (-> state :workspace-data :pages-index vals)] + pages (-> state :workspace-data :pages-index vals) + undo-id (js/Symbol)] (rx/concat - ;; First: clear the `:use-for-thumbnail?` flag from all not + (rx/of (dwu/start-undo-transaction undo-id)) + ;; First: clear the `:use-for-thumbnail` flag from all not ;; selected frames. (rx/from (->> pages @@ -534,13 +451,14 @@ (fn [{:keys [objects id] :as page}] (->> (ctst/get-frames objects) (sequence - (comp (filter :use-for-thumbnail?) + (comp (filter :use-for-thumbnail) (map :id) (remove selected) (map (partial vector id))))))) (d/group-by first second) (map (fn [[page-id frame-ids]] - (dch/update-shapes frame-ids #(dissoc % :use-for-thumbnail?) {:page-id page-id}))))) + (dch/update-shapes frame-ids #(dissoc % :use-for-thumbnail) {:page-id page-id}))))) ;; And finally: toggle the flag value on all the selected shapes - (rx/of (dch/update-shapes selected #(update % :use-for-thumbnail? not)))))))) + (rx/of (dch/update-shapes selected #(update % :use-for-thumbnail not)) + (dwu/commit-undo-transaction undo-id))))))) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 81ce9781f7..b8d10264dd 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -8,7 +8,9 @@ (:require [app.main.data.events :as ev] [app.main.data.exports :as de] + [app.main.data.preview :as dp] [app.main.data.shortcuts :as ds] + [app.main.data.users :as du] [app.main.data.workspace :as dw] [app.main.data.workspace.colors :as mdc] [app.main.data.workspace.common :as dwc] @@ -54,8 +56,8 @@ :subsections [:edit] :fn #(emit-when-no-readonly dwc/redo)} - :clear-undo {:tooltip (ds/alt "Z") - :command "alt+z" + :clear-undo {:tooltip (ds/alt "Q") + :command "alt+q" :subsections [:edit] :fn #(emit-when-no-readonly dwu/reinitialize-undo)} @@ -67,8 +69,9 @@ :cut {:tooltip (ds/meta "X") :command (ds/c-mod "x") :subsections [:edit] - :fn #(emit-when-no-readonly (dw/copy-selected) - (dw/delete-selected))} + :fn #(emit-when-no-readonly + (dw/copy-selected) + (dw/delete-selected))} :paste {:tooltip (ds/meta "V") :disabled true @@ -219,8 +222,16 @@ :toggle-layout-flex {:tooltip (ds/shift "A") :command "shift+a" :subsections [:modify-layers] - :fn #(emit-when-no-readonly (dwsl/toggle-layout-flex))} + :fn #(emit-when-no-readonly + (with-meta (dwsl/toggle-layout :flex) + {::ev/origin "workspace:shortcuts"}))} + :toggle-layout-grid {:tooltip (ds/meta-shift "A") + :command (ds/c-mod "shift+a") + :subsections [:modify-layers] + :fn #(emit-when-no-readonly + (with-meta (dwsl/toggle-layout :grid) + {::ev/origin "workspace:shortcuts"}))} ;; TOOLS :draw-frame {:tooltip "B" @@ -247,7 +258,7 @@ :command "t" :subsections [:tools] :fn #(emit-when-no-readonly dwtxt/start-edit-if-selected - (dwd/select-for-drawing :text))} + (dwd/select-for-drawing :text))} :draw-path {:tooltip "P" :command "p" @@ -284,10 +295,10 @@ :subsections [:tools] :fn #(emit-when-no-readonly (dw/toggle-proportion-lock))} - :toggle-scale-text {:tooltip "K" - :command "k" - :subsections [:tools] - :fn #(emit-when-no-readonly (toggle-layout-flag :scale-text))} + :scale {:tooltip "K" + :command "k" + :subsections [:tools] + :fn #(emit-when-no-readonly (toggle-layout-flag :scale-text))} :open-color-picker {:tooltip "I" :command "i" @@ -343,29 +354,22 @@ ;; MAIN MENU - :toggle-rules {:tooltip (ds/meta-shift "R") + :toggle-rulers {:tooltip (ds/meta-shift "R") :command (ds/c-mod "shift+r") :subsections [:main-menu] - :fn #(st/emit! (toggle-layout-flag :rules))} + :fn #(st/emit! (toggle-layout-flag :rulers))} :select-all {:tooltip (ds/meta "A") :command (ds/c-mod "a") :subsections [:main-menu] :fn #(st/emit! (dw/select-all))} - :toggle-grid {:tooltip (ds/meta "'") + :toggle-guides {:tooltip (ds/meta "'") ;;https://github.com/ccampbell/mousetrap/issues/85 :command [(ds/c-mod "'") (ds/c-mod "219")] :show-command (ds/c-mod "'") :subsections [:main-menu] - :fn #(st/emit! (toggle-layout-flag :display-grid))} - - :toggle-snap-grid {:tooltip (ds/meta-shift "'") - ;;https://github.com/ccampbell/mousetrap/issues/85 - :command [(ds/c-mod "shift+'") (ds/c-mod "shift+219")] - :show-command (ds/c-mod "shift+'") - :subsections [:main-menu] - :fn #(st/emit! (toggle-layout-flag :snap-grid))} + :fn #(st/emit! (toggle-layout-flag :display-guides))} :toggle-alignment {:tooltip (ds/meta "\\") :command (ds/c-mod "\\") @@ -391,12 +395,19 @@ :command (ds/c-mod "shift+e") :subsections [:basics :main-menu] :fn #(st/emit! - (de/show-workspace-export-dialog))} + (de/show-workspace-export-dialog))} - :toggle-snap-guide {:tooltip (ds/meta-shift "G") - :command (ds/c-mod "shift+g") - :subsections [:main-menu] - :fn #(st/emit! (toggle-layout-flag :snap-guides))} + :toggle-snap-ruler-guide {:tooltip (ds/meta-shift "G") + :command (ds/c-mod "shift+g") + :subsections [:main-menu] + :fn #(st/emit! (toggle-layout-flag :snap-ruler-guides))} + + :toggle-snap-guides {:tooltip (ds/meta-shift "'") + ;;https://github.com/ccampbell/mousetrap/issues/85 + :command [(ds/c-mod "shift+'") (ds/c-mod "shift+219")] + :show-command (ds/c-mod "shift+'") + :subsections [:main-menu] + :fn #(st/emit! (toggle-layout-flag :snap-guides))} :show-shortcuts {:tooltip "?" :command "?" @@ -405,34 +416,34 @@ ;; PANELS - :toggle-layers {:tooltip (ds/alt "L") - :command (ds/a-mod "l") - :subsections [:panels] - :fn #(st/emit! (dw/go-to-layout :layers))} + :toggle-layers {:tooltip (ds/alt "L") + :command (ds/a-mod "l") + :subsections [:panels] + :fn #(st/emit! (dw/go-to-layout :layers))} - :toggle-assets {:tooltip (ds/alt "I") - :command (ds/a-mod "i") - :subsections [:panels] - :fn #(st/emit! (dw/go-to-layout :assets))} + :toggle-assets {:tooltip (ds/alt "I") + :command (ds/a-mod "i") + :subsections [:panels] + :fn #(st/emit! (dw/go-to-layout :assets))} - :toggle-history {:tooltip (ds/alt "H") - :command (ds/a-mod "h") - :subsections [:panels] - :fn #(emit-when-no-readonly (dw/go-to-layout :document-history))} + :toggle-history {:tooltip (ds/alt "H") + :command (ds/a-mod "h") + :subsections [:panels] + :fn #(emit-when-no-readonly (dw/go-to-layout :document-history))} - :toggle-colorpalette {:tooltip (ds/alt "P") - :command (ds/a-mod "p") - :subsections [:panels] - :fn #(do (r/set-resize-type! :bottom) - (emit-when-no-readonly (dw/remove-layout-flag :textpalette) - (toggle-layout-flag :colorpalette)))} + :toggle-colorpalette {:tooltip (ds/alt "P") + :command (ds/a-mod "p") + :subsections [:panels] + :fn #(do (r/set-resize-type! :bottom) + (emit-when-no-readonly (dw/remove-layout-flag :textpalette) + (toggle-layout-flag :colorpalette)))} - :toggle-textpalette {:tooltip (ds/alt "T") - :command (ds/a-mod "t") - :subsections [:panels] - :fn #(do (r/set-resize-type! :bottom) - (emit-when-no-readonly (dw/remove-layout-flag :colorpalette) - (toggle-layout-flag :textpalette)))} + :toggle-textpalette {:tooltip (ds/alt "T") + :command (ds/a-mod "t") + :subsections [:panels] + :fn #(do (r/set-resize-type! :bottom) + (emit-when-no-readonly (dw/remove-layout-flag :colorpalette) + (toggle-layout-flag :textpalette)))} :hide-ui {:tooltip "\\" :command "\\" @@ -471,10 +482,10 @@ :subsections [:zoom-workspace] :fn identity} - :zoom-lense-decrease {:tooltip (ds/alt "Z") - :command "alt+z" - :subsections [:zoom-workspace] - :fn identity} + :zoom-lense-decrease {:tooltip (ds/alt "Z") + :command "alt+z" + :subsections [:zoom-workspace] + :fn identity} ;; NAVIGATION @@ -534,8 +545,19 @@ :bool-exclude {:tooltip (ds/meta (ds/alt "E")) :command (ds/c-mod "alt+e") :subsections [:shape] - :fn #(emit-when-no-readonly (dw/create-bool :exclude))}} - ) + :fn #(emit-when-no-readonly (dw/create-bool :exclude))} + + ;; PREVIEW + :preview-frame {:tooltip (ds/meta (ds/alt ds/enter)) + :command (ds/c-mod "alt+enter") + :fn #(emit-when-no-readonly (dp/open-preview-selected))} + + ;; THEME + :toggle-theme {:tooltip (ds/alt "M") + :command (ds/a-mod "m") + :subsections [:basics] + :fn #(st/emit! (with-meta (du/toggle-theme) + {::ev/origin "workspace:shortcut"}))}}) (def opacity-shortcuts (into {} (->> diff --git a/frontend/src/app/main/data/workspace/specialized_panel.cljs b/frontend/src/app/main/data/workspace/specialized_panel.cljs new file mode 100644 index 0000000000..eb4ec08f58 --- /dev/null +++ b/frontend/src/app/main/data/workspace/specialized_panel.cljs @@ -0,0 +1,41 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.data.workspace.specialized-panel + (:require + [app.common.data :as d] + [app.main.data.workspace.common :as-alias dwc] + [app.main.data.workspace.state-helpers :as wsh] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) + +(defn interrupt? [e] (or (= e :interrupt) (= e ::interrupt))) + +(defn clear-specialized-panel + [] + (ptk/reify ::clear-specialized-panel + ptk/UpdateEvent + (update [_ state] + (dissoc state :specialized-panel)))) + + +(defn open-specialized-panel + [type] + (ptk/reify ::open-specialized-panel + ptk/UpdateEvent + (update [_ state] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + selected-ids (wsh/lookup-selected state) + selected-shapes (map (d/getf objects) selected-ids)] + (assoc state :specialized-panel {:type type :shapes selected-shapes}))) + ptk/WatchEvent + (watch [_ _ stream] + (->> (rx/merge + (rx/filter interrupt? stream) + (rx/filter (ptk/type? ::dwc/undo) stream)) + (rx/take 1) + (rx/map clear-specialized-panel))))) diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs index 9a569fbd18..b04fa88f64 100644 --- a/frontend/src/app/main/data/workspace/state_helpers.cljs +++ b/frontend/src/app/main/data/workspace/state_helpers.cljs @@ -8,10 +8,10 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] - [app.common.path.commands :as upc] + [app.common.svg.path.command :as upc] [app.common.uuid :as uuid])) (defn lookup-page @@ -34,6 +34,17 @@ ([state page-id] (dm/get-in state [:viewer :pages page-id :objects]))) +(defn lookup-library-objects + [state file-id page-id] + (dm/get-in state [:workspace-libraries file-id :data :pages-index page-id :objects])) + +(defn lookup-objects + [state file-id page-id] + (let [current-file? (= file-id (:current-file-id state))] + (if ^boolean current-file? + (lookup-page-objects state page-id) + (lookup-library-objects state file-id page-id)))) + (defn lookup-page-options ([state] (lookup-page-options state (:current-page-id state))) @@ -53,7 +64,7 @@ (and (contains? objects id) (or (not omit-blocked?) (not (get-in objects [id :blocked] false)))))] - (let [selected (->> selected (cph/clean-loops objects))] + (let [selected (->> selected (cfh/clean-loops objects))] (into (d/ordered-set) (filter selectable?) selected))))) @@ -99,6 +110,11 @@ [state] (get state :workspace-data)) +(defn get-local-file-full + [state] + (-> (get state :workspace-file) + (assoc :data (get state :workspace-data)))) + (defn get-file "Get the data content of the given file (it may be the current file or one library)." @@ -126,7 +142,7 @@ [parent-id state] (let [objects (lookup-page-objects state) modifiers (:workspace-modifiers state) - children-ids (cph/get-children-ids objects parent-id) + children-ids (cfh/get-children-ids objects parent-id) children (-> (select-keys objects children-ids) (update-vals diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index b2b9ff6294..fa159cf04c 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -6,576 +6,58 @@ (ns app.main.data.workspace.svg-upload (:require - [app.common.colors :as clr] [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.exceptions :as ex] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] - [app.common.math :as mth] - [app.common.pages :as cp] - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] - [app.common.spec :as us :refer [max-safe-int min-safe-int]] - [app.common.types.shape :as cts] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] + [app.common.svg :as csvg] + [app.common.svg.shapes-builder :as csvg.shapes-builder] [app.common.types.shape-tree :as ctst] - [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.selection :as dws] - [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.repo :as rp] - [app.util.color :as uc] - [app.util.path.parser :as upp] - [app.util.svg :as usvg] [app.util.webapi :as wapi] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) -(defonce default-rect {:x 0 :y 0 :width 1 :height 1 :rx 0 :ry 0}) -(defonce default-circle {:r 0 :cx 0 :cy 0}) -(defonce default-image {:x 0 :y 0 :width 1 :height 1 :rx 0 :ry 0}) - -(defn- assert-valid-num [attr num] - (when-not (and (d/num? num) - (<= num max-safe-int) - (>= num min-safe-int)) - (ex/raise :type :assertion - :code :expr-validation - :hint (str/ffmt "%1 attribute has invalid value: %2" (d/name attr) num))) - - ;; If the number is between 0-1 we round to 1 (same in negative form - (cond - (and (> num 0) (< num 1)) 1 - (and (< num 0) (> num -1)) -1 - :else num)) - -(defn- assert-valid-pos-num - [attr num] - (when-not (pos? num) - (ex/raise :type :assertion - :code :expr-validation - :hint (str/ffmt "%1 attribute should be positive" (d/name attr)))) - num) - -(defn- assert-valid-blend-mode - [mode] - (let [clean-value (-> mode - str/trim - str/lower - keyword)] - (when-not (contains? cts/blend-modes clean-value) - (ex/raise :type :assertion - :code :expr-validation - :hint (str/ffmt "%1 is not a valid blend mode" clean-value))) - clean-value)) - -(defn- svg-dimensions [data] - (let [width (get-in data [:attrs :width] 100) - height (get-in data [:attrs :height] 100) - viewbox (get-in data [:attrs :viewBox] (str "0 0 " width " " height)) - [x y width height] (->> (str/split viewbox #"\s+") - (map d/parse-double)) - width (if (= width 0) 1 width) - height (if (= height 0) 1 height)] - [(assert-valid-num :x x) - (assert-valid-num :y y) - (assert-valid-pos-num :width width) - (assert-valid-pos-num :height height)])) - -(defn tag->name - "Given a tag returns its layer name" - [tag] - (str "svg-" (cond (string? tag) tag - (keyword? tag) (d/name tag) - (nil? tag) "node" - :else (str tag)))) - -(defn setup-fill [shape] - (let [color-attr (str/trim (get-in shape [:svg-attrs :fill])) - color-attr (if (= color-attr "currentColor") clr/black color-attr) - color-style (str/trim (get-in shape [:svg-attrs :style :fill])) - color-style (if (= color-style "currentColor") clr/black color-style)] - (cond-> shape - ;; Color present as attribute - (uc/color? color-attr) - (-> (update :svg-attrs dissoc :fill) - (update-in [:svg-attrs :style] dissoc :fill) - (assoc-in [:fills 0 :fill-color] (uc/parse-color color-attr))) - - ;; Color present as style - (uc/color? color-style) - (-> (update-in [:svg-attrs :style] dissoc :fill) - (update :svg-attrs dissoc :fill) - (assoc-in [:fills 0 :fill-color] (uc/parse-color color-style))) - - (get-in shape [:svg-attrs :fill-opacity]) - (-> (update :svg-attrs dissoc :fill-opacity) - (update-in [:svg-attrs :style] dissoc :fill-opacity) - (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :fill-opacity]) - (d/parse-double 1)))) - - (get-in shape [:svg-attrs :style :fill-opacity]) - (-> (update-in [:svg-attrs :style] dissoc :fill-opacity) - (update :svg-attrs dissoc :fill-opacity) - (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :style :fill-opacity]) - (d/parse-double 1))))))) - - -(defn- setup-stroke - [shape] - (let [attrs (get shape :svg-attrs) - style (get attrs :style) - - stroke (or (str/trim (:stroke attrs)) - (str/trim (:stroke style))) - - color (cond - (= stroke "currentColor") clr/black - (= stroke "none") nil - :else (uc/parse-color stroke)) - - opacity (when (some? color) - (d/parse-double - (or (:stroke-opacity attrs) - (:stroke-opacity style)) - 1)) - - width (when (some? color) - (d/parse-double - (or (:stroke-width attrs) - (:stroke-width style)) - 1)) - - linecap (or (get attrs :stroke-linecap) - (get style :stroke-linecap)) - linecap (some-> linecap str/trim keyword) - - attrs (-> attrs - (dissoc :stroke) - (dissoc :stroke-width) - (dissoc :stroke-opacity) - (update :style (fn [style] - (-> style - (dissoc :stroke) - (dissoc :stroke-linecap) - (dissoc :stroke-width) - (dissoc :stroke-opacity)))))] - - (cond-> (assoc shape :svg-attrs attrs) - (some? color) - (assoc-in [:strokes 0 :stroke-color] color) - - (and (some? color) (some? opacity)) - (assoc-in [:strokes 0 :stroke-opacity] opacity) - - (and (some? color) (some? width)) - (assoc-in [:strokes 0 :stroke-width] width) - - (and (some? linecap) (= (:type shape) :path) - (or (= linecap :round) (= linecap :square))) - (assoc :stroke-cap-start linecap - :stroke-cap-end linecap) - - (d/any-key? (dm/get-in shape [:strokes 0]) - :stroke-color :stroke-opacity :stroke-width - :stroke-cap-start :stroke-cap-end) - (assoc-in [:strokes 0 :stroke-style] :svg)))) - -(defn setup-opacity [shape] - (cond-> shape - (get-in shape [:svg-attrs :opacity]) - (-> (update :svg-attrs dissoc :opacity) - (assoc :opacity (-> (get-in shape [:svg-attrs :opacity]) - (d/parse-double 1)))) - - (get-in shape [:svg-attrs :style :opacity]) - (-> (update-in [:svg-attrs :style] dissoc :opacity) - (assoc :opacity (-> (get-in shape [:svg-attrs :style :opacity]) - (d/parse-double 1)))) - - - (get-in shape [:svg-attrs :mix-blend-mode]) - (-> (update :svg-attrs dissoc :mix-blend-mode) - (assoc :blend-mode (-> (get-in shape [:svg-attrs :mix-blend-mode]) assert-valid-blend-mode))) - - (get-in shape [:svg-attrs :style :mix-blend-mode]) - (-> (update-in [:svg-attrs :style] dissoc :mix-blend-mode) - (assoc :blend-mode (-> (get-in shape [:svg-attrs :style :mix-blend-mode]) assert-valid-blend-mode))))) - -(defn create-raw-svg [name frame-id svg-data {:keys [attrs] :as data}] - (let [{:keys [x y width height offset-x offset-y]} svg-data] - (-> {:id (uuid/next) - :type :svg-raw - :name name - :frame-id frame-id - :width width - :height height - :x x - :y y - :content (cond-> data - (map? data) (update :attrs usvg/clean-attrs))} - (assoc :svg-attrs attrs) - (assoc :svg-viewbox (-> (select-keys svg-data [:width :height]) - (assoc :x offset-x :y offset-y))) - (cts/setup-rect-selrect)))) - -(defn create-svg-root [frame-id parent-id svg-data] - (let [{:keys [name x y width height offset-x offset-y]} svg-data] - (-> {:id (uuid/next) - :type :group - :name name - :frame-id frame-id - :parent-id parent-id - :width width - :height height - :x (+ x offset-x) - :y (+ y offset-y)} - (cts/setup-rect-selrect) - (assoc :svg-attrs (-> (:attrs svg-data) - (dissoc :viewBox :xmlns) - (d/without-keys usvg/inheritable-props)))))) - -(defn create-group [name frame-id svg-data {:keys [attrs]}] - (let [svg-transform (usvg/parse-transform (:transform attrs)) - {:keys [x y width height offset-x offset-y]} svg-data] - (-> {:id (uuid/next) - :type :group - :name name - :frame-id frame-id - :x (+ x offset-x) - :y (+ y offset-y) - :width width - :height height} - (assoc :svg-transform svg-transform) - (assoc :svg-attrs (d/without-keys attrs usvg/inheritable-props)) - (assoc :svg-viewbox (-> (select-keys svg-data [:width :height]) - (assoc :x offset-x :y offset-y))) - (cts/setup-rect-selrect)))) - -(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}] - (when (and (contains? attrs :d) (seq (:d attrs))) - (let [svg-transform (usvg/parse-transform (:transform attrs)) - path-content (upp/parse-path (:d attrs)) - content (cond-> path-content - svg-transform - (gsh/transform-content svg-transform)) - - selrect (gsh/content->selrect content) - points (gsh/rect->points selrect) - - origin (gpt/negate (gpt/point svg-data))] - (-> {:id (uuid/next) - :type :path - :name name - :frame-id frame-id - :content content - :selrect selrect - :points points} - (assoc :svg-viewbox (select-keys selrect [:x :y :width :height])) - (assoc :svg-attrs (dissoc attrs :d :transform)) - (assoc :svg-transform svg-transform) - (gsh/translate-to-frame origin))))) - -(defn calculate-rect-metadata [rect-data transform] - (let [points (-> (gsh/rect->points rect-data) - (gsh/transform-points transform)) - - [selrect transform transform-inverse] (gsh/calculate-geometry points)] - - {:x (:x selrect) - :y (:y selrect) - :width (:width selrect) - :height (:height selrect) - :selrect selrect - :points points - :transform transform - :transform-inverse transform-inverse})) - - -(defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}] - (let [svg-transform (usvg/parse-transform (:transform attrs)) - transform (->> svg-transform - (gmt/transform-in (gpt/point svg-data))) - - rect (->> (select-keys attrs [:x :y :width :height]) - (d/mapm #(d/parse-double %2))) - - origin (gpt/negate (gpt/point svg-data)) - - rect-data (-> (merge default-rect rect) - (update :x - (:x origin)) - (update :y - (:y origin))) - - metadata (calculate-rect-metadata rect-data transform)] - (-> {:id (uuid/next) - :type :rect - :name name - :frame-id frame-id} - (cond-> - (contains? attrs :rx) (assoc :rx (d/parse-double (:rx attrs 0))) - (contains? attrs :ry) (assoc :ry (d/parse-double (:ry attrs 0)))) - - (merge metadata) - (assoc :svg-viewbox (select-keys rect [:x :y :width :height])) - (assoc :svg-attrs (dissoc attrs :x :y :width :height :rx :ry :transform))))) - - -(defn create-circle-shape [name frame-id svg-data {:keys [attrs] :as data}] - (let [svg-transform (usvg/parse-transform (:transform attrs)) - transform (->> svg-transform - (gmt/transform-in (gpt/point svg-data))) - - circle (->> (select-keys attrs [:r :ry :rx :cx :cy]) - (d/mapm #(d/parse-double %2))) - - {:keys [cx cy]} circle - - rx (or (:r circle) (:rx circle)) - ry (or (:r circle) (:ry circle)) - - rect {:x (- cx rx) - :y (- cy ry) - :width (* 2 rx) - :height (* 2 ry)} - - origin (gpt/negate (gpt/point svg-data)) - - rect-data (-> rect - (update :x - (:x origin)) - (update :y - (:y origin))) - - metadata (calculate-rect-metadata rect-data transform)] - (-> {:id (uuid/next) - :type :circle - :name name - :frame-id frame-id} - - (merge metadata) - (assoc :svg-viewbox (select-keys rect [:x :y :width :height])) - (assoc :svg-attrs (dissoc attrs :cx :cy :r :rx :ry :transform))))) - -(defn create-image-shape [name frame-id svg-data {:keys [attrs] :as data}] - (let [svg-transform (usvg/parse-transform (:transform attrs)) - transform (->> svg-transform - (gmt/transform-in (gpt/point svg-data))) - - image-url (or (:href attrs) (:xlink:href attrs)) - image-data (get-in svg-data [:image-data image-url]) - - rect (->> (select-keys attrs [:x :y :width :height]) - (d/mapm #(d/parse-double %2))) - - origin (gpt/negate (gpt/point svg-data)) - - rect-data (-> (merge default-image rect) - (update :x - (:x origin)) - (update :y - (:y origin))) - - rect-metadata (calculate-rect-metadata rect-data transform)] - - (when (some? image-data) - (-> {:id (uuid/next) - :type :image - :name name - :frame-id frame-id - :metadata {:width (:width image-data) - :height (:height image-data) - :mtype (:mtype image-data) - :id (:id image-data)}} - - (merge rect-metadata) - (assoc :svg-viewbox (select-keys rect [:x :y :width :height])) - (assoc :svg-attrs (dissoc attrs :x :y :width :height :href :xlink:href)))))) - -(defn parse-svg-element [frame-id svg-data element-data unames] - (let [{:keys [tag attrs hidden]} element-data - attrs (usvg/format-styles attrs) - element-data (cond-> element-data (map? element-data) (assoc :attrs attrs)) - name (or (:id attrs) (tag->name tag)) - att-refs (usvg/find-attr-references attrs) - references (usvg/find-def-references (:defs svg-data) att-refs) - - href-id (-> (or (:href attrs) (:xlink:href attrs) "") - (subs 1)) - defs (:defs svg-data) - - use-tag? (and (= :use tag) (contains? defs href-id))] - - (if use-tag? - (let [;; Merge the data of the use definition with the properties passed as attributes - use-data (-> (get defs href-id) - (update :attrs #(d/deep-merge % (dissoc attrs :xlink:href :href)))) - displacement (gpt/point (d/parse-double (:x attrs "0")) (d/parse-double (:y attrs "0"))) - disp-matrix (str (gmt/translate-matrix displacement)) - element-data (-> element-data - (assoc :tag :g) - (update :attrs dissoc :x :y :width :height :href :xlink:href :transform) - (update :attrs usvg/add-transform disp-matrix) - (assoc :content [use-data]))] - (parse-svg-element frame-id svg-data element-data unames)) - - ;; SVG graphic elements - ;; :circle :ellipse :image :line :path :polygon :polyline :rect :text :use - (let [shape (-> (case tag - (:g :a :svg) (create-group name frame-id svg-data element-data) - :rect (create-rect-shape name frame-id svg-data element-data) - (:circle - :ellipse) (create-circle-shape name frame-id svg-data element-data) - :path (create-path-shape name frame-id svg-data element-data) - :polyline (create-path-shape name frame-id svg-data (-> element-data usvg/polyline->path)) - :polygon (create-path-shape name frame-id svg-data (-> element-data usvg/polygon->path)) - :line (create-path-shape name frame-id svg-data (-> element-data usvg/line->path)) - :image (create-image-shape name frame-id svg-data element-data) - #_other (create-raw-svg name frame-id svg-data element-data)))] - (when (some? shape) - (let [shape (-> shape - (assoc :fills []) - (assoc :strokes []) - (assoc :svg-defs (select-keys (:defs svg-data) references)) - (setup-fill) - (setup-stroke) - (setup-opacity)) - - shape (cond-> shape - hidden (assoc :hidden true)) - - children (cond->> (:content element-data) - (contains? usvg/parent-tags tag) - (mapv #(usvg/inherit-attributes attrs %)))] - - [shape children])))))) - -(defn create-svg-children - [objects selected frame-id parent-id svg-data [unames children] [_index svg-element]] - (let [[new-shape new-children] (parse-svg-element frame-id svg-data svg-element unames)] - (if (some? new-shape) - (let [shape-id (:id new-shape) - - new-shape' (-> (dwsh/make-new-shape new-shape objects selected) - (assoc :parent-id parent-id)) - - children (conj children new-shape') - unames (conj unames (:name new-shape')) - - reducer-fn (partial create-svg-children objects selected frame-id shape-id svg-data)] - - (reduce reducer-fn [unames children] (d/enumerate new-children))) - - [unames children]))) - -(defn data-uri->blob - [data-uri] - (let [[mtype b64-data] (str/split data-uri ";base64,") - mtype (subs mtype (inc (str/index-of mtype ":"))) - decoded (.atob js/window b64-data) - size (.-length ^js decoded) - content (js/Uint8Array. size)] - - (doseq [i (range 0 size)] - (aset content i (.charCodeAt decoded i))) - - (wapi/create-blob content mtype))) - -(defn extract-name [url] - (let [query-idx (str/last-index-of url "?") - url (if (> query-idx 0) (subs url 0 query-idx) url) - filename (->> (str/split url "/") (last)) +(defn extract-name [href] + (let [query-idx (str/last-index-of href "?") + href (if (> query-idx 0) (subs href 0 query-idx) href) + filename (->> (str/split href "/") (last)) ext-idx (str/last-index-of filename ".")] (if (> ext-idx 0) (subs filename 0 ext-idx) filename))) (defn upload-images "Extract all bitmap images inside the svg data, and upload them, associated to the file. - Return a map { }." + Return a map { }." [svg-data file-id] - (->> (rx/from (usvg/collect-images svg-data)) - (rx/map (fn [uri] - (merge - {:file-id file-id - :is-local true - :url uri} - (if (str/starts-with? uri "data:") - {:name "image" - :content (data-uri->blob uri)} - {:name (extract-name uri)})))) - (rx/mapcat (fn [uri-data] - (->> (rp/cmd! (if (contains? uri-data :content) + (->> (rx/from (csvg/collect-images svg-data)) + (rx/map (fn [{:keys [href] :as item}] + (let [item (-> item + (assoc :file-id file-id) + (assoc :is-local true) + (assoc :name "image"))] + (if (str/starts-with? href "data:") + (assoc item :content (wapi/data-uri->blob href)) + (-> item + (assoc :name (extract-name href)) + (assoc :url href)))))) + (rx/mapcat (fn [item] + ;; TODO: :create-file-media-object-from-url is + ;; deprecated and this should be resolved in + ;; frontend + (->> (rp/cmd! (if (contains? item :content) :upload-file-media-object :create-file-media-object-from-url) - uri-data) + (dissoc item :href)) ;; When the image uploaded fail we skip the shape ;; returning `nil` will afterward not create the shape. (rx/catch #(rx/of nil)) - (rx/map #(vector (:url uri-data) %))))) - (rx/reduce (fn [acc [url image]] (assoc acc url image)) {}))) - -(defn create-svg-shapes - [svg-data {:keys [x y]} objects frame-id parent-id selected center?] - (let [[vb-x vb-y vb-width vb-height] (svg-dimensions svg-data) - x (mth/round - (if center? - (- x vb-x (/ vb-width 2)) - x)) - y (mth/round - (if center? - (- y vb-y (/ vb-height 2)) - y)) - - unames (cp/retrieve-used-names objects) - - svg-name (str/replace (:name svg-data) ".svg" "") - - svg-data (-> svg-data - (assoc :x x - :y y - :offset-x vb-x - :offset-y vb-y - :width vb-width - :height vb-height - :name svg-name)) - - [def-nodes svg-data] (-> svg-data - (usvg/fix-default-values) - (usvg/fix-percents) - (usvg/extract-defs)) - - svg-data (assoc svg-data :defs def-nodes) - - root-shape (create-svg-root frame-id parent-id svg-data) - root-id (:id root-shape) - - ;; In penpot groups have the size of their children. To respect the imported - ;; svg size and empty space let's create a transparent shape as background to respect the imported size - base-background-shape {:tag :rect - :attrs {:x (str vb-x) - :y (str vb-y) - :width (str vb-width) - :height (str vb-height) - :fill "none" - :id "base-background"} - :hidden true - :content []} - - svg-data (-> svg-data - (assoc :defs def-nodes) - (assoc :content (into [base-background-shape] (:content svg-data)))) - - ;; Create the root shape - new-shape (dwsh/make-new-shape root-shape objects selected) - - root-attrs (-> (:attrs svg-data) - (usvg/format-styles)) - - [_ new-children] - (reduce (partial create-svg-children objects selected frame-id root-id svg-data) - [unames []] - (d/enumerate (->> (:content svg-data) - (mapv #(usvg/inherit-attributes root-attrs %)))))] - - [new-shape new-children])) + (rx/map #(vector (:href item) %))))) + (rx/reduce conj {}))) (defn add-svg-shapes [svg-data position] @@ -583,48 +65,47 @@ ptk/WatchEvent (watch [it state _] (try - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - frame-id (ctst/top-nested-frame objects position) - selected (wsh/lookup-selected state) - page-objects (wsh/lookup-page-objects state) - base (cph/get-base-shape page-objects selected) - selected-frame? (and (= 1 (count selected)) - (= :frame (get-in objects [(first selected) :type]))) + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + frame-id (ctst/top-nested-frame objects position) + selected (wsh/lookup-selected state) + base (cfh/get-base-shape objects selected) - parent-id - (if (or selected-frame? (empty? selected)) - frame-id - (:parent-id base)) + selected-id (first selected) + selected-frame? (and (= 1 (count selected)) + (= :frame (dm/get-in objects [selected-id :type]))) + + parent-id (if (or selected-frame? (empty? selected)) + frame-id + (:parent-id base)) [new-shape new-children] - (create-svg-shapes svg-data position objects frame-id parent-id selected true) + (csvg.shapes-builder/create-svg-shapes svg-data position objects frame-id parent-id selected true) - changes (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects) - (pcb/add-object new-shape)) + changes (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) + (pcb/add-object new-shape)) - changes - (reduce (fn [changes new-child] - (-> changes (pcb/add-object new-child))) - changes new-children) + changes (reduce (fn [changes new-child] + (pcb/add-object changes new-child)) + changes + new-children) - changes (pcb/resize-parents changes - (->> changes - :redo-changes - (filter #(= :add-obj (:type %))) - (map :id) - reverse - vec)) - undo-id (js/Symbol)] + changes (pcb/resize-parents changes + (->> (:redo-changes changes) + (filter #(= :add-obj (:type %))) + (map :id) + (reverse) + (vec))) + undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) (dws/select-shapes (d/ordered-set (:id new-shape))) - (ptk/data-event :layout/update [(:id new-shape)]) + (ptk/data-event :layout/update {:ids [(:id new-shape)]}) (dwu/commit-undo-transaction undo-id))) - (catch :default e - (.error js/console "Error SVG" e) + (catch :default cause + (js/console.log (.-stack cause)) (rx/throw {:type :svg-parser - :data e})))))) + :data cause})))))) diff --git a/frontend/src/app/main/data/workspace/text/shortcuts.cljs b/frontend/src/app/main/data/workspace/text/shortcuts.cljs index e60807eda1..0970fca8bb 100644 --- a/frontend/src/app/main/data/workspace/text/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/text/shortcuts.cljs @@ -7,6 +7,7 @@ (ns app.main.data.workspace.text.shortcuts (:require [app.common.data :as d] + [app.common.text :as txt] [app.main.data.shortcuts :as ds] [app.main.data.workspace.texts :as dwt] [app.main.data.workspace.undo :as dwu] @@ -76,11 +77,11 @@ (or (= variant-id "remove-bold") (= variant-id "toggle-bold"))) add-italic? (and (not italic?) - (or (= variant-id "add-italic") - (= variant-id "toggle-italic"))) + (or (= variant-id "add-italic") + (= variant-id "toggle-italic"))) remove-italic? (and italic? - (or (= variant-id "remove-italic") - (= variant-id "toggle-italic")))] + (or (= variant-id "remove-italic") + (= variant-id "toggle-italic")))] (cond (and add-bold? italic?) ;; it is italic, set it to bold+italic (choose-bold-italic) @@ -116,19 +117,18 @@ (d/merge (dwt/current-root-values {:shape shape - :attrs dwt/root-attrs}) + :attrs txt/root-attrs}) (dwt/current-paragraph-values {:editor-state editor-state :shape shape - :attrs dwt/paragraph-attrs}) + :attrs txt/paragraph-attrs}) (dwt/current-text-values {:editor-state editor-state :shape shape - :attrs dwt/text-attrs})))) + :attrs txt/text-node-attrs})))) (defn- update-attrs [shape props] - (let [ - text-values (calculate-text-values shape) + (let [text-values (calculate-text-values shape) font-size (d/parse-double (:font-size text-values)) line-height (d/parse-double (:line-height text-values)) letter-spacing (d/parse-double (:letter-spacing text-values)) @@ -166,8 +166,7 @@ all-underline? (every? #(= (:text-decoration %) "underline") text-values) all-line-through? (every? #(= (:text-decoration %) "line-through") text-values) all-bold? (every? #(is-bold? (:font-variant-id %)) text-values) - all-italic? (every? #(is-italic? (:font-variant-id %)) text-values) - ] + all-italic? (every? #(is-italic? (:font-variant-id %)) text-values)] (cond (= (:text-decoration props) "toggle-underline") (if all-underline? @@ -197,9 +196,9 @@ (blend-props text-shapes props) props)] (when (and (not read-only?) text-shapes) - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! #(update-attrs % props) text-shapes) - (st/emit! (dwu/commit-undo-transaction undo-id))))) + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! #(update-attrs % props) text-shapes) + (st/emit! (dwu/commit-undo-transaction undo-id))))) (def shortcuts {:text-align-left {:tooltip (ds/meta (ds/alt "L")) @@ -229,13 +228,13 @@ :subsections [:text-editor] :fn #(update-attrs-when-no-readonly {:text-decoration "toggle-line-through"})} - :font-size-inc {:tooltip (ds/meta-shift ds/up-arrow) - :command (ds/c-mod "shift+up") + :font-size-inc {:tooltip (ds/meta-shift ds/right-arrow) + :command (ds/c-mod "shift+right") :subsections [:text-editor] :fn #(update-attrs-when-no-readonly {:font-size-inc true})} - :font-size-dec {:tooltip (ds/meta-shift ds/down-arrow) - :command (ds/c-mod "shift+down") + :font-size-dec {:tooltip (ds/meta-shift ds/left-arrow) + :command (ds/c-mod "shift+left") :subsections [:text-editor] :fn #(update-attrs-when-no-readonly {:font-size-dec true})} diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index c17e5448cb..c42a65378f 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -9,10 +9,10 @@ [app.common.attrs :as attrs] [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.common.pages.helpers :as cph] [app.common.text :as txt] [app.common.types.modifiers :as ctm] [app.common.uuid :as uuid] @@ -29,69 +29,9 @@ [app.util.router :as rt] [app.util.text-editor :as ted] [app.util.timers :as ts] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] - [potok.core :as ptk])) - -;; -- Attrs - -(def text-typography-attrs - [:typography-ref-id - :typography-ref-file]) - -(def text-fill-attrs - [:fill-color - :fill-opacity - :fill-color-ref-id - :fill-color-ref-file - :fill-color-gradient]) - -(def text-font-attrs - [:font-id - :font-family - :font-variant-id - :font-size - :font-weight - :font-style]) - -(def text-align-attrs - [:text-align]) - -(def text-direction-attrs - [:text-direction]) - -(def text-spacing-attrs - [:line-height - :letter-spacing]) - -(def text-valign-attrs - [:vertical-align]) - -(def text-decoration-attrs - [:text-decoration]) - -(def text-transform-attrs - [:text-transform]) - -(def shape-attrs - [:grow-type]) - -(def root-attrs text-valign-attrs) - -(def paragraph-attrs - (d/concat-vec - text-align-attrs - text-direction-attrs)) - -(def text-attrs - (d/concat-vec - text-typography-attrs - text-font-attrs - text-spacing-attrs - text-decoration-attrs - text-transform-attrs)) - -(def attrs (d/concat-set shape-attrs root-attrs paragraph-attrs text-attrs)) + [potok.v2.core :as ptk])) ;; -- Editor @@ -112,6 +52,14 @@ (when-let [editor (:workspace-editor state)] (ts/schedule #(.focus ^js editor)))))) +(defn gen-name + [editor] + (when (some? editor) + (let [result + (-> (ted/get-editor-current-plain-text editor) + (txt/generate-shape-name))] + (when (not= result "") result)))) + (defn update-editor-state [{:keys [id] :as shape} editor-state] (ptk/reify ::update-editor-state @@ -122,7 +70,7 @@ (update state :workspace-editor-state dissoc id))))) (defn finalize-editor-state - [id] + [id update-name?] (ptk/reify ::finalize-editor-state ptk/WatchEvent (watch [_ state _] @@ -132,8 +80,8 @@ editor-state (get-in state [:workspace-editor-state id]) content (-> editor-state (ted/get-editor-current-content)) - text (-> (ted/get-editor-current-plain-text editor-state) - (txt/generate-shape-name)) + name (gen-name editor-state) + new-shape? (nil? (:content shape))] (if (ted/content-has-text? content) (let [content (d/merge (ted/export-content content) @@ -153,8 +101,8 @@ (assoc :content content) (cond-> position-data (assoc :position-data position-data)) - (cond-> new-shape? - (assoc :name text)) + (cond-> (and update-name? (some? name)) + (assoc :name name)) (cond-> (or (some? width) (some? height)) (gsh/transform-shape (ctm/change-size shape width height)))))) {:undo-group (when new-shape? id)}))))) @@ -164,29 +112,31 @@ (dwsh/delete-shapes #{id}))))))))) (defn initialize-editor-state - [{:keys [id content] :as shape} decorator] + [{:keys [id name content] :as shape} decorator] (ptk/reify ::initialize-editor-state ptk/UpdateEvent (update [_ state] - (let [text-state (some->> content ted/import-content) - attrs (d/merge txt/default-text-attrs - (get-in state [:workspace-global :default-font])) - editor (cond-> (ted/create-editor-state text-state decorator) - (and (nil? content) (some? attrs)) - (ted/update-editor-current-block-data attrs))] + (let [text-state (some->> content ted/import-content) + attrs (d/merge txt/default-text-attrs + (get-in state [:workspace-global :default-font])) + editor (cond-> (ted/create-editor-state text-state decorator) + (and (nil? content) (some? attrs)) + (ted/update-editor-current-block-data attrs))] (-> state (assoc-in [:workspace-editor-state id] editor)))) ptk/WatchEvent - (watch [_ _ stream] + (watch [_ state stream] ;; We need to finalize editor on two main events: (1) when user ;; explicitly navigates to other section or page; (2) when user ;; leaves the editor. - (->> (rx/merge - (rx/filter (ptk/type? ::rt/navigate) stream) - (rx/filter #(= ::finalize-editor-state %) stream)) - (rx/take 1) - (rx/map #(finalize-editor-state id)))))) + (let [editor (dm/get-in state [:workspace-editor-state id]) + update-name? (or (nil? content) (= name (gen-name editor)))] + (->> (rx/merge + (rx/filter (ptk/type? ::rt/navigate) stream) + (rx/filter #(= ::finalize-editor-state %) stream)) + (rx/take 1) + (rx/map #(finalize-editor-state id update-name?))))))) (defn select-all "Select all content of the current editor. When not editor found this @@ -277,8 +227,8 @@ (update-text-content shape txt/is-root-node? d/txt-merge attrs) (assoc shape :content (d/txt-merge {:type "root"} attrs)))) - shape-ids (cond (cph/text-shape? shape) [id] - (cph/group-shape? shape) (cph/get-children-ids objects id))] + shape-ids (cond (cfh/text-shape? shape) [id] + (cfh/group-shape? shape) (cfh/get-children-ids objects id))] (rx/of (dch/update-shapes shape-ids update-fn)))))) @@ -304,8 +254,8 @@ update-fn #(update-text-content % txt/is-paragraph-node? merge-fn attrs) shape-ids (cond - (cph/text-shape? shape) [id] - (cph/group-shape? shape) (cph/get-children-ids objects id))] + (cfh/text-shape? shape) [id] + (cfh/group-shape? shape) (cfh/get-children-ids objects id))] (rx/of (dch/update-shapes shape-ids update-fn)))))))) @@ -325,8 +275,8 @@ (or (txt/is-text-node? node) (txt/is-paragraph-node? node))) shape-ids (cond - (cph/text-shape? shape) [id] - (cph/group-shape? shape) (cph/get-children-ids objects id))] + (cfh/text-shape? shape) [id] + (cfh/group-shape? shape) (cfh/get-children-ids objects id))] (rx/of (dch/update-shapes shape-ids #(update-text-content % update-node? d/txt-merge attrs)))))))) @@ -339,8 +289,7 @@ (and (d/not-empty? color-attrs) (nil? (:fills node))) (-> (dissoc :fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient) - (assoc :fills [color-attrs]))) - )) + (assoc :fills [color-attrs]))))) (defn migrate-content [content] @@ -363,8 +312,8 @@ shape-ids (cond - (cph/text-shape? shape) [id] - (cph/group-shape? shape) (cph/get-children-ids objects id)) + (cfh/text-shape? shape) [id] + (cfh/group-shape? shape) (cfh/get-children-ids objects id)) update-content (fn [content] @@ -427,13 +376,13 @@ shape))] - (let [ids (->> (keys props) (filter changed-text?))] + (let [ids (into #{} (filter changed-text?) (keys props))] (rx/of (dwu/start-undo-transaction undo-id) (dch/update-shapes ids update-fn {:reg-objects? true :stack-undo? true :ignore-remote? true :ignore-touched true}) - (ptk/data-event :layout/update ids) + (ptk/data-event :layout/update {:ids ids}) (dwu/commit-undo-transaction undo-id)))))))) (defn resize-text @@ -612,17 +561,17 @@ ptk/WatchEvent (watch [_ _ _] (rx/concat - (let [attrs (select-keys attrs root-attrs)] + (let [attrs (select-keys attrs txt/root-attrs)] (if-not (empty? attrs) (rx/of (update-root-attrs {:id id :attrs attrs})) (rx/empty))) - (let [attrs (select-keys attrs paragraph-attrs)] + (let [attrs (select-keys attrs txt/paragraph-attrs)] (if-not (empty? attrs) (rx/of (update-paragraph-attrs {:id id :attrs attrs})) (rx/empty))) - (let [attrs (select-keys attrs text-attrs)] + (let [attrs (select-keys attrs txt/text-node-attrs)] (if-not (empty? attrs) (rx/of (update-text-attrs {:id id :attrs attrs})) (rx/empty))))))) @@ -652,12 +601,16 @@ attrs (-> typography (assoc :typography-ref-file file-id) (assoc :typography-ref-id (:id typography)) - (dissoc :id :name))] + (dissoc :id :name)) + undo-id (js/Symbol)] - (->> (rx/from (seq selected)) - (rx/map (fn [id] - (let [editor (get editor-state id)] - (update-text-attrs {:id id :editor editor :attrs attrs}))))))))) + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id)) + (->> (rx/from (seq selected)) + (rx/map (fn [id] + (let [editor (get editor-state id)] + (update-text-attrs {:id id :editor editor :attrs attrs}))))) + (rx/of (dwu/commit-undo-transaction undo-id))))))) (defn generate-typography-name [{:keys [font-id font-variant-id] :as typography}] @@ -677,14 +630,14 @@ objects (wsh/lookup-page-objects state) xform (comp (keep (d/getf objects)) - (filter cph/text-shape?)) + (filter cfh/text-shape?)) shapes (into [] xform selected) shape (first shapes) values (current-text-values {:editor-state (dm/get-in state [:workspace-editor-state (:id shape)]) :shape shape - :attrs text-attrs}) + :attrs txt/text-node-attrs}) multiple? (or (> 1 (count shapes)) (d/seek (partial = :multiple) @@ -692,9 +645,9 @@ values (-> (d/without-nils values) (select-keys - (d/concat-vec text-font-attrs - text-spacing-attrs - text-transform-attrs))) + (d/concat-vec txt/text-font-attrs + txt/text-spacing-attrs + txt/text-transform-attrs))) typ-id (uuid/next) typ (-> (if multiple? diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index 2019b340bd..e2cb1cacc9 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -7,123 +7,180 @@ (ns app.main.data.workspace.thumbnails (:require [app.common.data.macros :as dm] - [app.common.pages.helpers :as cph] - [app.common.uuid :as uuid] + [app.common.files.helpers :as cfh] + [app.common.logging :as l] + [app.common.thumbnails :as thc] [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.notifications :as-alias wnt] [app.main.data.workspace.state-helpers :as wsh] + [app.main.rasterizer :as thr] [app.main.refs :as refs] + [app.main.render :as render] [app.main.repo :as rp] [app.main.store :as st] - [app.main.worker :as uw] - [app.util.dom :as dom] [app.util.http :as http] + [app.util.queue :as q] + [app.util.time :as tp] [app.util.timers :as tm] [app.util.webapi :as wapi] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) -(defn force-render-stream - "Stream that will inform the frame-wrapper to mount into memory" - [id] - (->> st/stream - (rx/filter (ptk/type? ::force-render)) - (rx/map deref) - (rx/filter #(= % id)) - (rx/take 1))) +(l/set-level! :info) +(declare update-thumbnail) + +(defn resolve-request + "Resolves the request to generate a thumbnail for the given ids." + [item] + (let [file-id (unchecked-get item "file-id") + page-id (unchecked-get item "page-id") + shape-id (unchecked-get item "shape-id") + tag (unchecked-get item "tag")] + (st/emit! (update-thumbnail file-id page-id shape-id tag)))) + +;; Defines the thumbnail queue +(defonce queue + (q/create resolve-request (/ 1000 30))) + +(defn create-request + "Creates a request to generate a thumbnail for the given ids." + [file-id page-id shape-id tag] + #js {:file-id file-id :page-id page-id :shape-id shape-id :tag tag}) + +(defn find-request + "Returns true if the given item matches the given ids." + [file-id page-id shape-id tag item] + (and (= file-id (unchecked-get item "file-id")) + (= page-id (unchecked-get item "page-id")) + (= shape-id (unchecked-get item "shape-id")) + (= tag (unchecked-get item "tag")))) + +(defn request-thumbnail + "Enqueues a request to generate a thumbnail for the given ids." + ([file-id page-id shape-id tag] + (request-thumbnail file-id page-id shape-id tag "unknown")) + ([file-id page-id shape-id tag requester] + (ptk/reify ::request-thumbnail + ptk/EffectEvent + (effect [_ _ _] + (l/dbg :hint "request thumbnail" :requester requester :file-id file-id :page-id page-id :shape-id shape-id :tag tag) + (q/enqueue-unique + queue + (create-request file-id page-id shape-id tag) + (partial find-request file-id page-id shape-id tag)))))) + +;; This function first renders the HTML calling `render/render-frame` that +;; returns HTML as a string, then we send that data to the iframe rasterizer +;; that returns the image as a Blob. Finally we create a URI for that blob. (defn get-thumbnail - [object-id] - ;; Look for the thumbnail canvas to send the data to the backend - (let [node (dom/query (dm/fmt "image.thumbnail-canvas[data-object-id='%'][data-ready='true']" object-id)) - stopper (->> st/stream - (rx/filter (ptk/type? :app.main.data.workspace/finalize-page)) - (rx/take 1))] - ;; renders #svg image - (if (some? node) - (->> (rx/from (wapi/create-image-bitmap-with-workaround node)) - (rx/switch-map #(uw/ask! {:cmd :thumbnails/render-offscreen-canvas} %)) - (rx/map :result)) + "Returns the thumbnail for the given ids" + [state file-id page-id frame-id tag & {:keys [object-id]}] - ;; Not found, we retry after delay - (->> (rx/timer 250) - (rx/merge-map (partial get-thumbnail object-id)) - (rx/take-until stopper))))) + (let [object-id (or object-id (thc/fmt-object-id file-id page-id frame-id tag)) + tp (tp/tpoint-ms) + objects (wsh/lookup-objects state file-id page-id) + shape (get objects frame-id)] + + (->> (render/render-frame objects shape object-id) + (rx/take 1) + (rx/filter some?) + (rx/mapcat thr/render) + (rx/map (fn [blob] (wapi/create-uri blob))) + (rx/tap #(l/dbg :hint "thumbnail rendered" + :elapsed (dm/str (tp) "ms")))))) (defn clear-thumbnail - [page-id frame-id] - (ptk/reify ::clear-thumbnail - ptk/UpdateEvent - (update [_ state] - (let [object-id (dm/str page-id frame-id)] - (when-let [uri (dm/get-in state [:workspace-thumbnails object-id])] - (tm/schedule-on-idle (partial wapi/revoke-uri uri))) - (update state :workspace-thumbnails dissoc object-id))))) + ([file-id page-id frame-id tag] + (clear-thumbnail (thc/fmt-object-id file-id page-id frame-id tag))) + ([object-id] + (let [emit-rpc? (volatile! false)] + (ptk/reify ::clear-thumbnail + cljs.core/IDeref + (-deref [_] object-id) -(defn set-workspace-thumbnail + ptk/UpdateEvent + (update [_ state] + (let [uri (dm/get-in state [:workspace-thumbnails object-id])] + (if (some? uri) + (do + (l/dbg :hint "clear thumbnail" :object-id object-id) + (vreset! emit-rpc? true) + (tm/schedule-on-idle (partial wapi/revoke-uri uri)) + (update state :workspace-thumbnails dissoc object-id)) + + state))))))) + +(defn- assoc-thumbnail [object-id uri] (let [prev-uri* (volatile! nil)] - (ptk/reify ::set-workspace-thumbnail + (ptk/reify ::assoc-thumbnail ptk/UpdateEvent (update [_ state] (let [prev-uri (dm/get-in state [:workspace-thumbnails object-id])] (some->> prev-uri (vreset! prev-uri*)) + (l/trc :hint "assoc thumbnail" :object-id object-id :uri uri) (update state :workspace-thumbnails assoc object-id uri))) ptk/EffectEvent (effect [_ _ _] - (tm/schedule-on-idle #(some-> ^boolean @prev-uri* wapi/revoke-uri)))))) + (tm/schedule-on-idle + (fn [] + (when-let [uri (deref prev-uri*)] + (wapi/revoke-uri uri)))))))) (defn duplicate-thumbnail [old-id new-id] (ptk/reify ::duplicate-thumbnail ptk/UpdateEvent (update [_ state] - (let [page-id (:current-page-id state) - thumbnail (dm/get-in state [:workspace-thumbnails (dm/str page-id old-id)])] - (update state :workspace-thumbnails assoc (dm/str page-id new-id) thumbnail))))) + (let [old-id (dm/str old-id) + new-id (dm/str new-id) + thumbnail (dm/get-in state [:workspace-thumbnails old-id])] + (update state :workspace-thumbnails assoc new-id thumbnail))))) (defn update-thumbnail - "Updates the thumbnail information for the given frame `id`" - ([page-id frame-id] - (update-thumbnail nil page-id frame-id)) + "Updates the thumbnail information for the given `id`" - ([file-id page-id frame-id] - (ptk/reify ::update-thumbnail - ptk/WatchEvent - (watch [_ state _] - (let [object-id (dm/str page-id frame-id) - file-id (or file-id (:current-file-id state))] + [file-id page-id frame-id tag] + (let [object-id (thc/fmt-object-id file-id page-id frame-id tag)] + (ptk/reify ::update-thumbnail + cljs.core/IDeref + (-deref [_] object-id) - (rx/concat - ;; Delete the thumbnail first so if we interrupt we can regenerate after - (->> (rp/cmd! :delete-file-object-thumbnail {:file-id file-id :object-id object-id}) - (rx/catch rx/empty)) + ptk/WatchEvent + (watch [_ state stream] + (l/dbg :hint "update thumbnail" :object-id object-id :tag tag) + ;; Send the update to the back-end + (->> (get-thumbnail state file-id page-id frame-id tag) + (rx/mapcat (fn [uri] + (rx/merge + (rx/of (assoc-thumbnail object-id uri)) + (->> (http/send! {:uri uri :response-type :blob :method :get}) + (rx/map :body) + (rx/mapcat (fn [blob] + ;; Send the data to backend + (let [params {:file-id file-id + :object-id object-id + :media blob + :tag (or tag "frame")}] + (rp/cmd! :create-file-object-thumbnail params)))) + (rx/catch rx/empty) + (rx/ignore))))) + (rx/catch (fn [cause] + (.error js/console cause) + (rx/empty))) - ;; Send the update to the back-end - (->> (get-thumbnail object-id) - (rx/filter (fn [data] (and (some? data) (some? file-id)))) - (rx/merge-map - (fn [uri] - (rx/merge - (rx/of (set-workspace-thumbnail object-id uri)) + ;; We cancel all the stream if user starts editing while + ;; thumbnail is generating + (rx/take-until + (->> stream + (rx/filter (ptk/type? ::clear-thumbnail)) + (rx/filter #(= (deref %) object-id))))))))) - (->> (http/send! {:uri uri :response-type :blob :method :get}) - (rx/map :body) - (rx/mapcat (fn [blob] - ;; Send the data to backend - (let [params {:file-id file-id - :object-id object-id - :media blob}] - (rp/cmd! :create-file-object-thumbnail params)))) - (rx/catch rx/empty) - (rx/ignore))))) - - (rx/catch #(do (.error js/console %) - (rx/empty)))))))))) - -(defn- extract-frame-changes +(defn- extract-root-frame-changes "Process a changes set in a commit to extract the frames that are changing" - [[event [old-data new-data]]] + [page-id [event [old-data new-data]]] (let [changes (-> event deref :changes) extract-ids @@ -136,39 +193,42 @@ [])) get-frame-id - (fn [[page-id id]] - (let [old-objects (wsh/lookup-data-objects old-data page-id) - new-objects (wsh/lookup-data-objects new-data page-id) + (fn [[_ id]] + (let [old-objects (wsh/lookup-data-objects old-data page-id) + new-objects (wsh/lookup-data-objects new-data page-id) - new-shape (get new-objects id) - old-shape (get old-objects id) + new-shape (get new-objects id) + old-shape (get old-objects id) - old-frame-id (if (cph/frame-shape? old-shape) id (:frame-id old-shape)) - new-frame-id (if (cph/frame-shape? new-shape) id (:frame-id new-shape))] + old-frame-id (if (cfh/frame-shape? old-shape) id (:frame-id old-shape)) + new-frame-id (if (cfh/frame-shape? new-shape) id (:frame-id new-shape))] (cond-> #{} - (and old-frame-id (not= uuid/zero old-frame-id)) - (conj [page-id old-frame-id]) + (cfh/root-frame? old-objects old-frame-id) + (conj old-frame-id) - (and new-frame-id (not= uuid/zero new-frame-id)) - (conj [page-id new-frame-id]))))] + (cfh/root-frame? new-objects new-frame-id) + (conj new-frame-id))))] (into #{} (comp (mapcat extract-ids) + (filter (fn [[page-id']] (= page-id page-id'))) (mapcat get-frame-id)) changes))) (defn watch-state-changes "Watch the state for changes inside frames. If a change is detected will force a rendering of the frame data so the thumbnail can be updated." - [] + [file-id page-id] (ptk/reify ::watch-state-changes ptk/WatchEvent (watch [_ _ stream] - (let [stopper - (->> stream - (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) - (= ::watch-state-changes (ptk/type %))))) + (let [stopper-s (rx/filter + (fn [event] + (as-> (ptk/type event) type + (or (= :app.main.data.workspace/finalize-page type) + (= ::watch-state-changes type)))) + stream) workspace-data-s (->> (rx/concat @@ -176,26 +236,63 @@ (rx/from-atom refs/workspace-data {:emit-current-value? true})) ;; We need to keep the old-objects so we can check the frame for the ;; deleted objects - (rx/buffer 2 1)) + (rx/buffer 2 1) + (rx/share)) - change-s + local-changes-s (->> stream - (rx/filter #(or (dch/commit-changes? %) - (= (ptk/type %) :app.main.data.workspace.notifications/handle-file-change))) - (rx/observe-on :async)) - - frame-changes-s - (->> change-s + (rx/filter dch/commit-changes?) (rx/with-latest-from workspace-data-s) - (rx/flat-map extract-frame-changes) - (rx/share))] + (rx/merge-map (partial extract-root-frame-changes page-id)) + (rx/tap #(l/trc :hint "incoming change" :origin "local" :frame-id (dm/str %)))) + + notification-changes-s + (->> stream + (rx/filter (ptk/type? ::wnt/handle-file-change)) + (rx/observe-on :async) + (rx/with-latest-from workspace-data-s) + (rx/merge-map (partial extract-root-frame-changes page-id)) + (rx/tap #(l/trc :hint "incoming change" :origin "notifications" :frame-id (dm/str %)))) + + persistence-changes-s + (->> stream + (rx/filter (ptk/type? ::update)) + (rx/map deref) + (rx/filter (fn [[file-id page-id]] + (and (= file-id file-id) + (= page-id page-id)))) + (rx/map (fn [[_ _ frame-id]] frame-id)) + (rx/tap #(l/trc :hint "incoming change" :origin "persistence" :frame-id (dm/str %)))) + + all-changes-s + (->> (rx/merge + ;; LOCAL CHANGES + local-changes-s + ;; NOTIFICATIONS CHANGES + notification-changes-s + ;; PERSISTENCE CHANGES + persistence-changes-s) + + (rx/share)) + + ;; BUFFER NOTIFIER (window of 5s of inactivity) + notifier-s + (->> all-changes-s + (rx/debounce 1000) + (rx/tap #(l/trc :hint "buffer initialized")))] (->> (rx/merge - (->> frame-changes-s - (rx/filter (fn [[page-id _]] (not= page-id (:current-page-id @st/state)))) - (rx/map (fn [[page-id frame-id]] (clear-thumbnail page-id frame-id)))) + ;; Perform instant thumbnail cleaning of affected frames + ;; and interrupt any ongoing update-thumbnail process + ;; related to current frame-id + (->> all-changes-s + (rx/map #(clear-thumbnail file-id page-id % "frame"))) - (->> frame-changes-s - (rx/filter (fn [[page-id _]] (= page-id (:current-page-id @st/state)))) - (rx/map (fn [[_ frame-id]] (ptk/data-event ::force-render frame-id))))) - (rx/take-until stopper)))))) + ;; Generate thumbnails in batchs, once user becomes + ;; inactive for some instant + (->> all-changes-s + (rx/buffer-until notifier-s) + (rx/mapcat #(into #{} %)) + (rx/map #(request-thumbnail file-id page-id % "frame" "watch-state-changes")))) + + (rx/take-until stopper-s)))))) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 35a86f8048..7cea9cde9d 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -9,14 +9,16 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] [app.common.geom.matrix :as gmt] + [app.common.geom.modifiers :as gm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.flex-layout :as gslf] [app.common.geom.shapes.grid-layout :as gslg] [app.common.math :as mth] - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.modifiers :as ctm] @@ -26,30 +28,36 @@ [app.main.data.workspace.collapse :as dwc] [app.main.data.workspace.modifiers :as dwm] [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.snap :as snap] [app.main.streams :as ms] + [app.util.array :as array] [app.util.dom :as dom] - [beicon.core :as rx] - [potok.core :as ptk])) + [app.util.keyboard :as kbd] + [app.util.mouse :as mse] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) ;; -- Helpers -------------------------------------------------------- ;; For each of the 8 handlers gives the multiplier for resize ;; for example, right will only grow in the x coordinate and left ;; will grow in the inverse of the x coordinate -(def ^:private handler-multipliers - {:right [ 1 0] - :bottom [ 0 1] - :left [-1 0] - :top [ 0 -1] - :top-right [ 1 -1] - :top-left [-1 -1] - :bottom-right [ 1 1] - :bottom-left [-1 1]}) +(defn get-handler-multiplier + [handler] + (case handler + :right (gpt/point 1 0) + :bottom (gpt/point 0 1) + :left (gpt/point -1 0) + :top (gpt/point 0 -1) + :top-right (gpt/point 1 -1) + :top-left (gpt/point -1 -1) + :bottom-right (gpt/point 1 1) + :bottom-left (gpt/point -1 1))) -(defn- handler-resize-origin +(defn- get-handler-resize-origin "Given a handler, return the coordinate origin for resizes. This is the opposite of the handler so for right we want the left side as origin of the resize. @@ -58,39 +66,66 @@ mx, my => middle x/y ex, ey => end x/y " - [{sx :x sy :y :keys [width height]} handler] - (let [mx (+ sx (/ width 2)) - my (+ sy (/ height 2)) - ex (+ sx width) - ey (+ sy height) - - [x y] (case handler - :right [sx my] - :bottom [mx sy] - :left [ex my] - :top [mx ey] - :top-right [sx ey] - :top-left [ex ey] - :bottom-right [sx sy] - :bottom-left [ex sy])] - (gpt/point x y))) + [selrect handler] + (let [sx (dm/get-prop selrect :x) + sy (dm/get-prop selrect :y) + width (dm/get-prop selrect :width) + height (dm/get-prop selrect :height) + mx (+ sx (/ width 2)) + my (+ sy (/ height 2)) + ex (+ sx width) + ey (+ sy height)] + (case handler + :right (gpt/point sx my) + :bottom (gpt/point mx sy) + :left (gpt/point ex my) + :top (gpt/point mx ey) + :top-right (gpt/point sx ey) + :top-left (gpt/point ex ey) + :bottom-right (gpt/point sx sy) + :bottom-left (gpt/point ex sy)))) (defn- fix-init-point "Fix the initial point so the resizes are accurate" [initial handler shape] - (let [{:keys [x y width height]} (:selrect shape)] - (cond-> initial - (contains? #{:left :top-left :bottom-left} handler) - (assoc :x x) + (let [selrect (dm/get-prop shape :selrect) + x (dm/get-prop selrect :x) + y (dm/get-prop selrect :y) + width (dm/get-prop selrect :width) + height (dm/get-prop selrect :height)] - (contains? #{:right :top-right :bottom-right} handler) - (assoc :x (+ x width)) + (case handler + :left + (assoc initial :x x) - (contains? #{:top :top-right :top-left} handler) - (assoc :y y) + :top + (assoc initial :y y) - (contains? #{:bottom :bottom-right :bottom-left} handler) - (assoc :y (+ y height))))) + :top-left + (-> initial + (assoc :x x) + (assoc :y y)) + + :bottom-left + (-> initial + (assoc :x x) + (assoc :y (+ y height))) + + :right + (assoc initial :x (+ x width)) + + :top-right + (-> initial + (assoc :x (+ x width)) + (assoc :y y)) + + :bottom-right + (-> initial + (assoc :x (+ x width)) + (assoc :y (+ y height))) + + :bottom + (assoc initial :y (+ y height))))) (defn finish-transform [] (ptk/reify ::finish-transform @@ -98,18 +133,18 @@ (update [_ state] (update state :workspace-local dissoc :transform :duplicate-move-started? false)))) - ;; -- Resize -------------------------------------------------------- (defn start-resize "Enter mouse resize mode, until mouse button is released." [handler ids shape] - (letfn [(resize - [shape initial layout [point lock? center? point-snap]] - (let [{:keys [width height]} (:selrect shape) - {:keys [rotation]} shape + (letfn [(resize [shape initial layout [point lock? center? point-snap]] + (let [selrect (dm/get-prop shape :selrect) + width (dm/get-prop selrect :width) + height (dm/get-prop selrect :height) + rotation (dm/get-prop shape :rotation) - shape-center (gsh/center-shape shape) + shape-center (gsh/shape->center shape) shape-transform (:transform shape) shape-transform-inverse (:transform-inverse shape) @@ -123,78 +158,84 @@ shapev (-> (gpt/point width height)) - scale-text (:scale-text layout) + scale-text (contains? layout :scale-text) ;; Force lock if the scale text mode is active - lock? (or lock? scale-text) + lock? (or ^boolean lock? + ^boolean scale-text) - ;; Vector modifiers depending on the handler - handler-mult (let [[x y] (handler-multipliers handler)] (gpt/point x y)) - - ;; Difference between the origin point in the coordinate system of the rotation + ;; Difference between the origin point in the + ;; coordinate system of the rotation deltav (-> (gpt/to-vec initial point) - (gpt/multiply handler-mult)) + ;; Vector modifiers depending on the handler + (gpt/multiply (get-handler-multiplier handler))) ;; Resize vector scalev (-> (gpt/divide (gpt/add shapev deltav) shapev) (gpt/no-zeros)) - scalev (if lock? + scalev (if ^boolean lock? (let [v (cond - (#{:right :left} handler) (:x scalev) - (#{:top :bottom} handler) (:y scalev) - :else (max (:x scalev) (:y scalev)))] - (gpt/point v v)) + (or (= handler :right) + (= handler :left)) + (dm/get-prop scalev :x) + (or (= handler :top) + (= handler :bottom)) + (dm/get-prop scalev :y) + + :else + (mth/max (dm/get-prop scalev :x) + (dm/get-prop scalev :y)))] + (gpt/point v v)) scalev) ;; Resize origin point given the selected handler - handler-origin (handler-resize-origin (:selrect shape) handler) - + selrect (dm/get-prop shape :selrect) + handler-origin (get-handler-resize-origin selrect handler) ;; If we want resize from center, displace the shape ;; so it is still centered after resize. - displacement - (when center? - (-> shape-center - (gpt/subtract handler-origin) - (gpt/multiply scalev) - (gpt/add handler-origin) - (gpt/subtract shape-center) - (gpt/multiply (gpt/point -1 -1)) - (gpt/transform shape-transform))) + displacement (when ^boolean center? + (-> shape-center + (gpt/subtract handler-origin) + (gpt/multiply scalev) + (gpt/add handler-origin) + (gpt/subtract shape-center) + (gpt/multiply (gpt/point -1 -1)) + (gpt/transform shape-transform))) - resize-origin - (cond-> (gmt/transform-point-center handler-origin shape-center shape-transform) - (some? displacement) - (gpt/add displacement)) + resize-origin (gmt/transform-point-center handler-origin shape-center shape-transform) + resize-origin (if (some? displacement) + (gpt/add resize-origin displacement) + resize-origin) ;; When the horizontal/vertical scale a flex children with auto/fill ;; we change it too fixed set-fix-width? - (not (mth/close? (:x scalev) 1)) + (not (mth/close? (dm/get-prop scalev :x) 1)) set-fix-height? - (not (mth/close? (:y scalev) 1)) + (not (mth/close? (dm/get-prop scalev :y) 1)) - modifiers - (-> (ctm/empty) + modifiers (cond-> (ctm/empty) + (some? displacement) + (ctm/move displacement) - (cond-> displacement - (ctm/move displacement)) + :always + (ctm/resize scalev resize-origin shape-transform shape-transform-inverse) - (ctm/resize scalev resize-origin shape-transform shape-transform-inverse) + ^boolean set-fix-width? + (ctm/change-property :layout-item-h-sizing :fix) - (cond-> set-fix-width? - (ctm/change-property :layout-item-h-sizing :fix)) + ^boolean set-fix-height? + (ctm/change-property :layout-item-v-sizing :fix) - (cond-> set-fix-height? - (ctm/change-property :layout-item-v-sizing :fix)) - - (cond-> scale-text - (ctm/scale-content (:x scalev)))) + ^boolean scale-text + (ctm/scale-content (dm/get-prop scalev :x))) modif-tree (dwm/create-modif-tree ids modifiers)] + (rx/of (dwm/set-modifiers modif-tree scale-text)))) ;; Unifies the instantaneous proportion lock modifier @@ -202,7 +243,10 @@ ;; lock flag that can be activated on element options. (normalize-proportion-lock [[point shift? alt?]] (let [proportion-lock? (:proportion-lock shape)] - [point (or proportion-lock? shift?) alt?]))] + [point + (or ^boolean proportion-lock? + ^boolean shift?) + alt?]))] (reify ptk/UpdateEvent (update [_ state] @@ -212,13 +256,14 @@ ptk/WatchEvent (watch [_ state stream] (let [initial-position @ms/mouse-position - stopper (rx/filter ms/mouse-up? stream) + + stopper (mse/drag-stopper stream) layout (:workspace-layout state) page-id (:current-page-id state) focus (:workspace-focus-selected state) - zoom (get-in state [:workspace-local :zoom] 1) + zoom (dm/get-in state [:workspace-local :zoom] 1) objects (wsh/lookup-page-objects state page-id) - resizing-shapes (map #(get objects %) ids)] + shapes (map (d/getf objects) ids)] (rx/concat (->> ms/mouse-position @@ -226,13 +271,33 @@ (rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt) (rx/map normalize-proportion-lock) (rx/switch-map (fn [[point _ _ :as current]] - (->> (snap/closest-snap-point page-id resizing-shapes objects layout zoom focus point) + (->> (snap/closest-snap-point page-id shapes objects layout zoom focus point) (rx/map #(conj current %))))) (rx/mapcat (partial resize shape initial-position layout)) (rx/take-until stopper)) (rx/of (dwm/apply-modifiers) (finish-transform)))))))) +(defn trigger-bounding-box-cloaking + "Trigger the bounding box cloaking (with default timer of 1sec) + + Used to hide bounding-box of shape after changes in sidebar->measures." + [ids] + (dm/assert! + "expected valid coll of uuids" + (every? uuid? ids)) + + (ptk/reify ::trigger-bounding-box-cloaking + ptk/WatchEvent + (watch [_ _ stream] + (rx/concat + (rx/of (dwsh/update-shape-flags ids {:transforming true})) + (->> (rx/timer 1000) + (rx/map (fn [] + (dwsh/update-shape-flags ids {:transforming false}))) + (rx/take-until + (rx/filter (ptk/type? ::trigger-bounding-box-cloaking) stream))))))) + (defn update-dimensions "Change size of shapes, from the sideber options form. Will ignore pixel snap used in the options side panel" @@ -253,7 +318,7 @@ modif-tree (-> (dwm/build-modif-tree ids objects get-modifier) - (gsh/set-objects-modifiers objects))] + (gm/set-objects-modifiers objects))] (assoc state :workspace-modifiers modif-tree))) @@ -282,7 +347,7 @@ modif-tree (-> (dwm/build-modif-tree ids objects get-modifier) - (gsh/set-objects-modifiers objects))] + (gm/set-objects-modifiers objects))] (assoc state :workspace-modifiers modif-tree))) @@ -303,9 +368,9 @@ ptk/WatchEvent (watch [_ _ stream] - (let [stoper (rx/filter ms/mouse-up? stream) - group (gsh/selection-rect shapes) - group-center (gsh/center-selrect group) + (let [stopper (mse/drag-stopper stream) + group (gsh/shapes->rect shapes) + group-center (grc/rect->center group) initial-angle (gpt/angle @ms/mouse-position group-center) calculate-angle @@ -324,13 +389,12 @@ angle))] (rx/concat (->> ms/mouse-position - (rx/with-latest vector ms/mouse-position-mod) - (rx/with-latest vector ms/mouse-position-shift) + (rx/with-latest-from ms/mouse-position-mod ms/mouse-position-shift) (rx/map - (fn [[[pos mod?] shift?]] + (fn [[pos mod? shift?]] (let [delta-angle (calculate-angle pos mod? shift?)] (dwm/set-rotation-modifiers delta-angle shapes group-center)))) - (rx/take-until stoper)) + (rx/take-until stopper)) (rx/of (dwm/apply-modifiers) (finish-transform))))))) @@ -345,8 +409,8 @@ objects (wsh/lookup-page-objects state page-id) shapes (->> ids (map #(get objects %)))] (rx/concat - (rx/of (dwm/set-delta-rotation-modifiers rotation shapes)) - (rx/of (dwm/apply-modifiers))))))) + (rx/of (dwm/set-delta-rotation-modifiers rotation shapes)) + (rx/of (dwm/apply-modifiers))))))) ;; -- Move ---------------------------------------------------------- @@ -367,7 +431,7 @@ (watch [_ state stream] (let [initial (deref ms/mouse-position) - stopper (rx/filter ms/mouse-up? stream) + stopper (mse/drag-stopper stream) zoom (get-in state [:workspace-local :zoom] 1) ;; We toggle the selection so we don't have to wait for the event @@ -383,7 +447,7 @@ (rx/map #(gpt/length %)) (rx/filter #(> % (/ 10 zoom))) (rx/take 1) - (rx/with-latest vector ms/mouse-position-alt) + (rx/with-latest-from ms/mouse-position-alt) (rx/mapcat (fn [[_ alt?]] (rx/concat @@ -424,7 +488,7 @@ (when-let [node (dom/get-element-by-class "ghost-outline")] (dom/set-property! node "transform" (gmt/translate-matrix move-vector)))))) -(defn- start-move +(defn start-move ([from-position] (start-move from-position nil)) ([from-position ids] (ptk/reify ::start-move @@ -439,21 +503,28 @@ objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state {:omit-blocked? true}) ids (if (nil? ids) selected ids) - shapes (mapv #(get objects %) ids) + shapes (into [] + (comp (map (d/getf objects)) + (remove #(let [parent (get objects (:parent-id %))] + (and (ctk/in-component-copy? parent) + (ctl/any-layout? parent))))) + ids) + duplicate-move-started? (get-in state [:workspace-local :duplicate-move-started?] false) - stopper (rx/filter ms/mouse-up? stream) + + stopper (mse/drag-stopper stream) layout (get state :workspace-layout) zoom (get-in state [:workspace-local :zoom] 1) focus (:workspace-focus-selected state) exclude-frames (into #{} - (filter (partial cph/frame-shape? objects)) - (cph/selected-with-children objects selected)) + (filter (partial cfh/frame-shape? objects)) + (cfh/selected-with-children objects selected)) exclude-frames-siblings (into exclude-frames - (comp (mapcat (partial cph/get-siblings-ids objects)) + (comp (mapcat (partial cfh/get-siblings-ids objects)) (filter (partial ctl/any-layout-immediate-child-id? objects))) selected) @@ -464,43 +535,44 @@ ;; We send the nil first so the stream is not waiting for the first value (rx/of nil) (->> position + ;; FIXME: performance throttle (rx/throttle 20) (rx/switch-map (fn [pos] (->> (snap/closest-snap-move page-id shapes objects layout zoom focus pos) - (rx/map #(vector pos %)))))))] + (rx/map #(array pos %)))))))] (if (empty? shapes) (rx/of (finish-transform)) (let [move-stream (->> position ;; We ask for the snap position but we continue even if the result is not available - (rx/with-latest vector snap-delta) + (rx/with-latest-from snap-delta) ;; We try to use the previous snap so we don't have to wait for the result of the new (rx/map snap/correct-snap-point) - (rx/with-latest vector ms/mouse-position-mod) + (rx/with-latest-from ms/mouse-position-mod) (rx/map (fn [[move-vector mod?]] - (let [position (gpt/add from-position move-vector) - exclude-frames (if mod? exclude-frames exclude-frames-siblings) - target-frame (ctst/top-nested-frame objects position exclude-frames) - flex-layout? (ctl/flex-layout? objects target-frame) - grid-layout? (ctl/grid-layout? objects target-frame) - drop-index (cond - flex-layout? (gslf/get-drop-index target-frame objects position) - grid-layout? (gslg/get-drop-index target-frame objects position))] - [move-vector target-frame drop-index]))) + (let [position (gpt/add from-position move-vector) + exclude-frames (if mod? exclude-frames exclude-frames-siblings) + target-frame (ctst/top-nested-frame objects position exclude-frames) + [target-frame _] (ctn/find-valid-parent-and-frame-ids target-frame objects shapes) + flex-layout? (ctl/flex-layout? objects target-frame) + grid-layout? (ctl/grid-layout? objects target-frame) + drop-index (when flex-layout? (gslf/get-drop-index target-frame objects position)) + cell-data (when (and grid-layout? (not mod?)) (gslg/get-drop-cell target-frame objects position))] + (array move-vector target-frame drop-index cell-data)))) (rx/take-until stopper))] (rx/merge ;; Temporary modifiers stream (->> move-stream - (rx/with-latest-from ms/mouse-position-shift) + (rx/with-latest-from array/conj ms/mouse-position-shift) (rx/map - (fn [[[move-vector target-frame drop-index] shift?]] + (fn [[move-vector target-frame drop-index cell-data shift?]] (let [x-disp? (> (mth/abs (:x move-vector)) (mth/abs (:y move-vector))) [move-vector snap-ignore-axis] (cond @@ -514,19 +586,19 @@ [move-vector nil])] (-> (dwm/create-modif-tree ids (ctm/move-modifiers move-vector)) - (dwm/build-change-frame-modifiers objects selected target-frame drop-index) + (dwm/build-change-frame-modifiers objects selected target-frame drop-index cell-data) (dwm/set-modifiers false false {:snap-ignore-axis snap-ignore-axis})))))) (->> move-stream - (rx/with-latest-from ms/mouse-position-alt) - (rx/filter (fn [[_ alt?]] alt?)) - (rx/take 1) - (rx/mapcat - (fn [[_ alt?]] - (if (and (not duplicate-move-started?) alt?) - (rx/of (start-move-duplicate from-position) - (dws/duplicate-selected false true)) - (rx/empty))))) + (rx/with-latest-from ms/mouse-position-alt) + (rx/filter (fn [[_ alt?]] alt?)) + (rx/take 1) + (rx/mapcat + (fn [[_ alt?]] + (if (and (not duplicate-move-started?) alt?) + (rx/of (start-move-duplicate from-position) + (dws/duplicate-selected false true)) + (rx/empty))))) (->> move-stream (rx/map (comp set-ghost-displacement first))) @@ -535,11 +607,11 @@ (->> move-stream (rx/last) (rx/mapcat - (fn [[_ target-frame drop-index]] + (fn [[_ target-frame drop-index drop-cell]] (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (move-shapes-to-frame ids target-frame drop-index) (dwm/apply-modifiers {:undo-transation? false}) + (move-shapes-to-frame ids target-frame drop-index drop-cell) (finish-transform) (dwu/commit-undo-transaction undo-id)))))))))))))) @@ -555,63 +627,89 @@ objects (wsh/lookup-page-objects state) page-id (:current-page-id state) - get-new-position + get-move-to-index (fn [parent-id position] (let [parent (get objects parent-id)] - (cond - (ctl/flex-layout? parent) - (if (or - (and (ctl/reverse? parent) - (or (= direction :left) - (= direction :up))) - (and (not (ctl/reverse? parent)) - (or (= direction :right) - (= direction :down)))) - (dec position) - (+ position 2)) + (if (or (and (ctl/reverse? parent) + (or (= direction :left) + (= direction :up))) + (and (not (ctl/reverse? parent)) + (or (= direction :right) + (= direction :down)))) + (dec position) + (+ position 2)))) - ;; TODO: GRID - (ctl/grid-layout? parent) - nil - ))) + move-flex-children + (fn [changes parent-id children] + (->> children + ;; Add the position to move the children + (map (fn [id] + (let [position (cfh/get-position-on-parent objects id)] + [id (get-move-to-index parent-id position)]))) + (sort-by second >) + (reduce (fn [changes [child-id index]] + (pcb/change-parent changes parent-id [(get objects child-id)] index)) + changes))) - add-children-position - (fn [[parent-id children]] - (let [children+position + move-grid-children + (fn [changes parent-id children] + (let [parent (get objects parent-id) + + key-prop (case direction + (:up :down) :row + (:right :left) :column) + key-comp (case direction + (:up :left) < + (:down :right) >) + + {:keys [layout-grid-cells]} (->> children - (keep #(let [new-position (get-new-position - parent-id - (cph/get-position-on-parent objects %))] - (when new-position - (vector % new-position)))) - (sort-by second >))] - [parent-id children+position])) - - change-parents-and-position - (->> selected - (group-by #(dm/get-in objects [% :parent-id])) - (map add-children-position) - (into {})) + (remove #(ctk/in-component-copy-not-head? (get objects %))) + (keep #(ctl/get-cell-by-shape-id parent %)) + (sort-by key-prop key-comp) + (reduce (fn [parent {:keys [id row column row-span column-span]}] + (let [[next-row next-column] + (case direction + :up [(dec row) column] + :right [row (+ column column-span)] + :down [(+ row row-span) column] + :left [row (dec column)]) + next-cell (ctl/get-cell-by-position parent next-row next-column)] + (cond-> parent + (some? next-cell) + (ctl/swap-shapes id (:id next-cell))))) + parent))] + (-> changes + (pcb/update-shapes + [(:id parent)] + (fn [shape] + (-> shape + (assoc :layout-grid-cells layout-grid-cells) + ;; We want the previous objects value + (ctl/assign-cells objects)))) + (pcb/reorder-grid-children [(:id parent)])))) changes - (->> change-parents-and-position + (->> selected + (group-by #(dm/get-in objects [% :parent-id])) (reduce (fn [changes [parent-id children]] - (->> children - (reduce - (fn [changes [child-id index]] - (pcb/change-parent changes parent-id - [(get objects child-id)] - index)) - changes))) + (cond-> changes + (ctl/flex-layout? objects parent-id) + (move-flex-children parent-id children) + + (ctl/grid-layout? objects parent-id) + (move-grid-children parent-id children))) + (-> (pcb/empty-changes it page-id) (pcb/with-objects objects)))) + undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) - (ptk/data-event :layout/update selected) + (ptk/data-event :layout/update {:ids selected}) (dwu/commit-undo-transaction undo-id)))))) (defn nudge-selected-shapes @@ -646,7 +744,8 @@ (rx/switch-map #(rx/merge (rx/timer 1000) (->> stream - (rx/filter ms/key-up?) + (rx/filter kbd/keyboard-event?) + (rx/filter kbd/key-up-event?) (rx/delay 250)))) (rx/take 1)) @@ -679,7 +778,7 @@ selected (wsh/lookup-selected state {:omit-blocked? true}) selected-shapes (->> selected (map (d/getf objects)))] (if (every? #(and (ctl/any-layout-immediate-child? objects %) - (not (ctl/layout-absolute? %))) + (not (ctl/position-absolute? %))) selected-shapes) (rx/of (reorder-selected-layout-child direction)) (rx/of (nudge-selected-shapes direction shift?))))))) @@ -692,17 +791,18 @@ (ptk/reify ::update-position ptk/WatchEvent (watch [_ state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - shape (get objects id) + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + shape (get objects id) - bbox (-> shape :points gsh/points->selrect) + ;; FIXME: performance rect + bbox (-> shape :points grc/points->rect) - cpos (gpt/point (:x bbox) (:y bbox)) - pos (gpt/point (or (:x position) (:x bbox)) - (or (:y position) (:y bbox))) + cpos (gpt/point (:x bbox) (:y bbox)) + pos (gpt/point (or (:x position) (:x bbox)) + (or (:y position) (:y bbox))) - delta (gpt/subtract pos cpos) + delta (gpt/subtract pos cpos) modif-tree (dwm/create-modif-tree [id] (ctm/move-modifiers delta))] @@ -733,7 +833,7 @@ :ignore-snap-pixel true})))))) (defn- move-shapes-to-frame - [ids frame-id drop-index] + [ids frame-id drop-index [row column :as cell]] (ptk/reify ::move-shapes-to-frame ptk/WatchEvent (watch [it state _] @@ -743,7 +843,13 @@ frame (get objects frame-id) layout? (:layout frame) - shapes (->> ids (cph/clean-loops objects) (keep lookup)) + component-main-frame (ctn/find-component-main objects frame false) + + shapes (->> ids + (cfh/clean-loops objects) + (keep lookup) + ;;remove shapes inside copies, because we can't change the structure of copies + (remove #(ctk/in-component-copy? (get objects (:parent-id %))))) moving-shapes (cond->> shapes @@ -754,12 +860,12 @@ (remove #(and (= (:frame-id %) frame-id) (not= (:parent-id %) frame-id)))) - ordered-indexes (cph/order-by-indexed-shapes objects (map :id moving-shapes)) + ordered-indexes (cfh/order-by-indexed-shapes objects (map :id moving-shapes)) moving-shapes (map (d/getf objects) ordered-indexes) all-parents (reduce (fn [res id] - (into res (cph/get-parent-ids objects id))) + (into res (cfh/get-parent-ids objects id))) (d/ordered-set) ids) @@ -768,7 +874,7 @@ (let [all-ids (into empty-parents ids) contains? (partial contains? all-ids) xform (comp (map lookup) - (filter cph/group-shape?) + (filter cfh/group-shape?) (remove #(->> (:shapes %) (remove contains?) seq)) (map :id)) parents (into #{} xform all-parents)] @@ -786,7 +892,7 @@ moving-shapes (->> moving-shapes (remove (fn [shape] - (and (ctl/layout-absolute? shape) + (and (ctl/position-absolute? shape) (= frame-id (:parent-id shape)))))) frame-component @@ -794,11 +900,11 @@ shape-ids-to-detach (reduce (fn [result shape] - (if (and (some? shape) (ctk/in-component-copy-not-root? shape)) + (if (and (some? shape) (ctk/in-component-copy-not-head? shape)) (let [shape-component (ctn/get-component-shape objects shape)] (if (= (:id frame-component) (:id shape-component)) result - (into result (cph/get-children-ids-with-self objects (:id shape))))) + (into result (cfh/get-children-ids-with-self objects (:id shape))))) result)) #{} moving-shapes) @@ -806,15 +912,48 @@ moving-shapes-ids (map :id moving-shapes) + moving-shapes-children-ids + (->> moving-shapes-ids + (mapcat #(cfh/get-children-ids-with-self objects %))) + + child-heads + (->> moving-shapes-ids + (mapcat #(ctn/get-child-heads objects %)) + (map :id)) + changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) ;; Remove layout-item properties when moving a shape outside a layout (cond-> (not (ctl/any-layout? objects frame-id)) (pcb/update-shapes moving-shapes-ids ctl/remove-layout-item-data)) - (pcb/update-shapes moving-shapes-ids #(cond-> % (cph/frame-shape? %) (assoc :hide-in-viewer true))) + ;; Remove the swap slots if it is moving to a different component + (pcb/update-shapes child-heads + (fn [shape] + (cond-> shape + (not= component-main-frame (ctn/find-component-main objects shape false)) + (ctk/remove-swap-slot)))) + ;; Remove component-root property when moving a shape inside a component + (cond-> (ctn/get-instance-root objects frame) + (pcb/update-shapes moving-shapes-children-ids #(dissoc % :component-root))) + ;; Add component-root property when moving a component outside a component + (cond-> (not (ctn/get-instance-root objects frame)) + (pcb/update-shapes child-heads #(assoc % :component-root true))) + (pcb/update-shapes moving-shapes-ids #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true))) (pcb/update-shapes shape-ids-to-detach ctk/detach-shape) (pcb/change-parent frame-id moving-shapes drop-index) + (cond-> (ctl/grid-layout? objects frame-id) + (-> (pcb/update-shapes + [frame-id] + (fn [frame objects] + (-> frame + ;; Assign the cell when pushing into a specific grid cell + (cond-> (some? cell) + (-> (ctl/push-into-cell moving-shapes-ids row column) + (ctl/assign-cells objects))) + (ctl/assign-cell-positions objects))) + {:with-objects? true}) + (pcb/reorder-grid-children [frame-id]))) (pcb/remove-objects empty-parents))] (when (and (some? frame-id) (d/not-empty? changes)) @@ -841,10 +980,10 @@ (let [objects (wsh/lookup-page-objects state) selected (wsh/lookup-selected state {:omit-blocked? true}) shapes (map #(get objects %) selected) - selrect (gsh/selection-rect shapes) - center (gsh/center-selrect selrect) + selrect (gsh/shapes->rect shapes) + center (grc/rect->center selrect) modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point -1.0 1.0) center))] - (rx/of (dwm/apply-modifiers {:modifiers modifiers})))))) + (rx/of (dwm/apply-modifiers {:modifiers modifiers :ignore-snap-pixel true})))))) (defn flip-vertical-selected [] (ptk/reify ::flip-vertical-selected @@ -853,7 +992,7 @@ (let [objects (wsh/lookup-page-objects state) selected (wsh/lookup-selected state {:omit-blocked? true}) shapes (map #(get objects %) selected) - selrect (gsh/selection-rect shapes) - center (gsh/center-selrect selrect) + selrect (gsh/shapes->rect shapes) + center (grc/rect->center selrect) modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point 1.0 -1.0) center))] - (rx/of (dwm/apply-modifiers {:modifiers modifiers})))))) + (rx/of (dwm/apply-modifiers {:modifiers modifiers :ignore-snap-pixel true})))))) diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs index f41f972873..809c9f6a52 100644 --- a/frontend/src/app/main/data/workspace/undo.cljs +++ b/frontend/src/app/main/data/workspace/undo.cljs @@ -8,12 +8,12 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.changes :as cpc] [app.common.logging :as log] - [app.common.pages.changes :as cpc] [app.common.schema :as sm] [app.util.time :as dt] - [beicon.core :as rx] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (def discard-transaction-time-millis (* 20 1000)) @@ -24,13 +24,15 @@ ;; Undo / Redo ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def schema:undo-entry - [:map - [:undo-changes [:vector ::cpc/change]] - [:redo-changes [:vector ::cpc/change]]]) +(def ^:private + schema:undo-entry + (sm/define + [:map {:title "undo-entry"} + [:undo-changes [:vector ::cpc/change]] + [:redo-changes [:vector ::cpc/change]]])) -(def undo-entry? - (sm/pred-fn schema:undo-entry)) +(def check-undo-entry! + (sm/check-fn schema:undo-entry)) (def MAX-UNDO-SIZE 50) @@ -62,13 +64,13 @@ items (conj-undo-entry items entry)] (-> state (update :workspace-undo assoc :items items - :index (min (inc index) - (dec MAX-UNDO-SIZE))))) + :index (min (inc index) + (dec MAX-UNDO-SIZE))))) state)) (defn- stack-undo-entry [state {:keys [undo-changes redo-changes] :as entry}] - (let [index (get-in state [:workspace-undo :index] -1)] + (let [index (get-in state [:workspace-undo :index] -1)] (if (>= index 0) (update-in state [:workspace-undo :items index] (fn [item] @@ -84,29 +86,33 @@ (update-in [:workspace-undo :transaction :redo-changes] #(into % redo-changes)) (cond-> (nil? (get-in state [:workspace-undo :transaction :undo-group])) - (assoc-in [:workspace-undo :transaction :undo-group] undo-group)) + (assoc-in [:workspace-undo :transaction :undo-group] undo-group)) (assoc-in [:workspace-undo :transaction :tags] tags))) (defn append-undo [entry stack?] - (dm/assert! (boolean? stack?)) - (dm/assert! (undo-entry? entry)) + (dm/assert! + "expected valid undo entry" + (check-undo-entry! entry)) + + (dm/assert! + (boolean? stack?)) (ptk/reify ::append-undo ptk/UpdateEvent (update [_ state] - (cond - (and (get-in state [:workspace-undo :transaction]) - (or (not stack?) - (d/not-empty? (get-in state [:workspace-undo :transaction :undo-changes])) - (d/not-empty? (get-in state [:workspace-undo :transaction :redo-changes])))) - (accumulate-undo-entry state entry) + (cond + (and (get-in state [:workspace-undo :transaction]) + (or (not stack?) + (d/not-empty? (get-in state [:workspace-undo :transaction :undo-changes])) + (d/not-empty? (get-in state [:workspace-undo :transaction :redo-changes])))) + (accumulate-undo-entry state entry) - stack? - (stack-undo-entry state entry) + stack? + (stack-undo-entry state entry) - :else - (add-undo-entry state entry))))) + :else + (add-undo-entry state entry))))) (def empty-tx {:undo-changes [] :redo-changes []}) diff --git a/frontend/src/app/main/data/workspace/viewport.cljs b/frontend/src/app/main/data/workspace/viewport.cljs index 0629b33d93..55eadd321a 100644 --- a/frontend/src/app/main/data/workspace/viewport.cljs +++ b/frontend/src/app/main/data/workspace/viewport.cljs @@ -8,19 +8,24 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.align :as gal] [app.common.geom.point :as gpt] + [app.common.geom.rect :as gpr] [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.common.pages.helpers :as cph] [app.main.data.workspace.state-helpers :as wsh] - [app.main.streams :as ms] - [beicon.core :as rx] - [potok.core :as ptk])) + [app.util.mouse :as mse] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (defn initialize-viewport [{:keys [width height] :as size}] + (dm/assert! + "expected `size` to be a rect instance" + (gpr/rect? size)) + (letfn [(update* [{:keys [vport] :as local}] (let [wprop (/ (:width vport) width) hprop (/ (:height vport) height)] @@ -29,14 +34,16 @@ (update :vbox (fn [vbox] (-> vbox (update :width #(/ % wprop)) - (update :height #(/ % hprop)))))))) + (update :height #(/ % hprop)) + (gpr/update-rect :size))))))) (initialize [state local] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - shapes (cph/get-immediate-children objects) - srect (gsh/selection-rect shapes) - local (assoc local :vport size :zoom 1 :zoom-inverse 1)] + shapes (cfh/get-immediate-children objects) + srect (gsh/shapes->rect shapes) + local (assoc local :vport size :zoom 1 :zoom-inverse 1 :hide-toolbar false)] + (cond (or (not (d/num? (:width srect))) (not (d/num? (:height srect)))) @@ -46,15 +53,24 @@ (> (:height srect) height)) (let [srect (gal/adjust-to-viewport size srect {:padding 40}) zoom (/ (:width size) (:width srect))] + (-> local (assoc :zoom zoom) (assoc :zoom-inverse (/ 1 zoom)) - (update :vbox merge srect))) + (update :vbox (fn [vbox] + (-> (merge vbox srect) + (gpr/make-rect)))))) :else - (assoc local :vbox (assoc size - :x (+ (:x srect) (/ (- (:width srect) width) 2)) - :y (+ (:y srect) (/ (- (:height srect) height) 2))))))) + (let [vx (+ (:x srect) + (/ (- (:width srect) width) 2)) + vy (+ (:y srect) + (/ (- (:height srect) height) 2)) + vbox (-> size + (assoc :x vx) + (assoc :y vy) + (gpr/update-rect :position))] + (assoc local :vbox vbox))))) (setup [state local] (if (and (:vbox local) (:vport local)) @@ -139,7 +155,7 @@ (rx/concat (rx/of #(-> % (assoc-in [:workspace-local :panning] true))) (->> stream - (rx/filter ms/pointer-event?) + (rx/filter mse/pointer-event?) (rx/filter #(= :delta (:source %))) (rx/map :pt) (rx/take-until stopper) diff --git a/frontend/src/app/main/data/workspace/zoom.cljs b/frontend/src/app/main/data/workspace/zoom.cljs index fce940e6f3..379776ede7 100644 --- a/frontend/src/app/main/data/workspace/zoom.cljs +++ b/frontend/src/app/main/data/workspace/zoom.cljs @@ -3,26 +3,29 @@ ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) KALEIDOS INC + (ns app.main.data.workspace.zoom (:require + [app.common.files.helpers :as cfh] [app.common.geom.align :as gal] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] [app.main.data.workspace.state-helpers :as wsh] [app.main.streams :as ms] - [beicon.core :as rx] - [potok.core :as ptk])) + [app.util.mouse :as mse] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) (defn- impl-update-zoom [{:keys [vbox] :as local} center zoom] (let [new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom) old-zoom (:zoom local) - center (if center center (gsh/center-rect vbox)) - scale (/ old-zoom new-zoom) - mtx (gmt/scale-matrix (gpt/point scale) center) - vbox' (gsh/transform-rect vbox mtx)] + center (if center center (grc/rect->center vbox)) + scale (/ old-zoom new-zoom) + mtx (gmt/scale-matrix (gpt/point scale) center) + vbox' (gsh/transform-rect vbox mtx)] (-> local (assoc :zoom new-zoom) (assoc :zoom-inverse (/ 1 new-zoom)) @@ -73,8 +76,8 @@ (update [_ state] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - shapes (cph/get-immediate-children objects) - srect (gsh/selection-rect shapes)] + shapes (cfh/get-immediate-children objects) + srect (gsh/shapes->rect shapes)] (if (empty? shapes) state (update state :workspace-local @@ -97,7 +100,7 @@ objects (wsh/lookup-page-objects state page-id) srect (->> selected (map #(get objects %)) - (gsh/selection-rect))] + (gsh/shapes->rect))] (update state :workspace-local (fn [{:keys [vport] :as local}] (let [srect (gal/adjust-to-viewport vport srect {:padding 40}) @@ -116,7 +119,7 @@ (rx/concat (rx/of #(-> % (assoc-in [:workspace-local :zooming] true))) (->> stream - (rx/filter ms/pointer-event?) + (rx/filter mse/pointer-event?) (rx/filter #(= :delta (:source %))) (rx/map :pt) (rx/take-until stopper) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 7bd0726456..542b41bce7 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -7,8 +7,9 @@ (ns app.main.errors "Generic error handling" (:require + [app.common.exceptions :as ex] [app.common.pprint :as pp] - [app.common.schema :as sm] + [app.common.schema :as-alias sm] [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.data.users :as du] @@ -19,22 +20,22 @@ [app.util.storage :refer [storage]] [app.util.timers :as ts] [cuerdas.core :as str] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (defn- print-data! [data] (-> data (dissoc ::sm/explain) - (dissoc :hint) + (dissoc :explain) (dissoc ::trace) (dissoc ::instance) (pp/pprint {:width 70}))) (defn- print-explain! [data] - (when-let [explain (::sm/explain data)] - (-> (sm/humanize-data explain) - (pp/pprint {:width 70})))) + (when-let [explain (or (ex/explain data) + (:explain data))] + (js/console.log explain))) (defn- print-trace! [data] @@ -49,6 +50,29 @@ (finally (js/console.groupEnd message)))) +(defn print-cause! + [message cause] + (print-group! message (fn [] + (print-data! cause) + (print-explain! cause) + (print-trace! cause)))) + +(defn print-error! + [cause] + (cond + (map? cause) + (print-cause! (:hint cause "Unexpected Error") cause) + + (ex/error? cause) + (print-cause! (ex-message cause) (ex-data cause)) + + :else + (let [trace (.-stack cause)] + (print-cause! (ex-message cause) + {:hint (ex-message cause) + ::trace trace + ::instance cause})))) + (defn on-error "A general purpose error handler." [error] @@ -66,8 +90,7 @@ (defmethod ptk/handle-error :default [error] - (when-let [cause (::instance error)] - (ts/schedule #(st/emit! (rt/assign-exception cause)))) + (st/async-emit! (rt/assign-exception error)) (print-group! "Unhandled Error" (fn [] (print-trace! error) @@ -89,16 +112,24 @@ ;; the user perspective a error flash message should be visualized but ;; user can continue operate on the application. Can happen in backend ;; and frontend. -(defmethod ptk/handle-error :validation - [error] - (ts/schedule - #(st/emit! (msg/show {:content "Validation error" - :type :error - :timeout 3000}))) +(defmethod ptk/handle-error :validation + [{:keys [code] :as error}] (print-group! "Validation Error" (fn [] - (print-data! error)))) + (print-data! error) + (print-explain! error))) + (cond + (= code :invalid-paste-data) + (let [message (tr "errors.paste-data-validation")] + (st/async-emit! + (msg/show {:content message + :notification-type :toast + :type :error + :timeout 3000}))) + + :else + (st/async-emit! (rt/assign-exception error)))) ;; This is a pure frontend error that can be caused by an active @@ -108,6 +139,7 @@ [error] (ts/schedule #(st/emit! (msg/show {:content "Internal Assertion Error" + :notification-type :toast :type :error :timeout 3000}))) @@ -123,6 +155,7 @@ (ts/schedule #(st/emit! (msg/show {:content "Something wrong has happened (on worker)." + :notification-type :toast :type :error :timeout 3000}))) @@ -136,6 +169,7 @@ [_] (ts/schedule #(st/emit! (msg/show {:content "SVG is invalid or malformed" + :notification-type :toast :type :error :timeout 3000})))) @@ -144,6 +178,7 @@ [_] (ts/schedule #(st/emit! (msg/show {:content "There was an error with the comment" + :notification-type :toast :type :error :timeout 3000})))) @@ -162,33 +197,56 @@ (ts/schedule #(st/emit! (rt/assign-exception error)))) + +(defn- redirect-to-dashboard + [] + (let [team-id (:current-team-id @st/state) + project-id (:current-project-id @st/state)] + (if (and project-id team-id) + (st/emit! (rt/nav :dashboard-files {:team-id team-id :project-id project-id})) + (set! (.-href glob/location) "")))) + (defmethod ptk/handle-error :restriction [{:keys [code] :as error}] (cond - (= :feature-mismatch code) - (let [message (tr "errors.feature-mismatch" (:feature error)) - team-id (:current-team-id @st/state) - project-id (:current-project-id @st/state) - on-accept #(if (and project-id team-id) - (st/emit! (rt/nav :dashboard-files {:team-id team-id :project-id project-id})) - (set! (.-href glob/location) ""))] + (= :migration-in-progress code) + (let [message (tr "errors.migration-in-progress" (:feature error)) + on-accept (constantly nil)] (st/emit! (modal/show {:type :alert :message message :on-accept on-accept}))) - (= :features-not-supported code) - (let [message (tr "errors.feature-not-supported" (:feature error)) - team-id (:current-team-id @st/state) - project-id (:current-project-id @st/state) - on-accept #(if (and project-id team-id) - (st/emit! (rt/nav :dashboard-files {:team-id team-id :project-id project-id})) - (set! (.-href glob/location) ""))] + (= :team-feature-mismatch code) + (let [message (tr "errors.team-feature-mismatch" (:feature error)) + on-accept (constantly nil)] (st/emit! (modal/show {:type :alert :message message :on-accept on-accept}))) + (= :file-feature-mismatch code) + (let [message (tr "errors.file-feature-mismatch" (:feature error))] + (st/emit! (modal/show {:type :alert :message message :on-accept redirect-to-dashboard}))) + + (= :feature-mismatch code) + (let [message (tr "errors.feature-mismatch" (:feature error))] + (st/emit! (modal/show {:type :alert :message message :on-accept redirect-to-dashboard}))) + + (= :feature-not-supported code) + (let [message (tr "errors.feature-not-supported" (:feature error))] + (st/emit! (modal/show {:type :alert :message message :on-accept redirect-to-dashboard}))) + + (= :file-version-not-supported code) + (let [message (tr "errors.version-not-supported")] + (st/emit! (modal/show {:type :alert :message message :on-accept redirect-to-dashboard}))) + (= :max-quote-reached code) (let [message (tr "errors.max-quote-reached" (:target error))] (st/emit! (modal/show {:type :alert :message message}))) + (or (= :paste-feature-not-enabled code) + (= :missing-features-in-paste-content code) + (= :paste-feature-not-supported code)) + (let [message (tr "errors.feature-not-supported" (:feature error))] + (st/emit! (modal/show {:type :alert :message message}))) + :else - (ptk/handle-error {:type :server-error :data error}))) + (print-cause! "Restriction Error" error))) ;; This happens when the backed server fails to process the ;; request. This can be caused by an internal assertion or any other @@ -196,15 +254,25 @@ (defmethod ptk/handle-error :server-error [error] - (ts/schedule - #(st/emit! - (msg/show {:content "Something wrong has happened (on backend)." - :type :error - :timeout 3000}))) - + (st/async-emit! (rt/assign-exception error)) (print-group! "Server Error" (fn [] - (print-data! error)))) + (print-data! (dissoc error :data)) + + (when-let [werror (:data error)] + (cond + (= :assertion (:type werror)) + (print-group! "Assertion Error" + (fn [] + (print-data! werror) + (print-explain! werror))) + + :else + (print-group! "Unexpected" + (fn [] + (print-data! werror) + (print-explain! werror)))))))) + (defonce uncaught-error-handler (letfn [(is-ignorable-exception? [cause] diff --git a/frontend/src/app/main/features.cljs b/frontend/src/app/main/features.cljs index 4b0735c79e..51b30ed177 100644 --- a/frontend/src/app/main/features.cljs +++ b/frontend/src/app/main/features.cljs @@ -5,103 +5,118 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.features + "A thin, frontend centric abstraction layer and collection of + helpers for `app.common.features` namespace." (:require - [app.common.data :as d] + [app.common.features :as cfeat] [app.common.logging :as log] [app.config :as cf] [app.main.store :as st] - [app.util.timers :as tm] - [beicon.core :as rx] + [beicon.v2.core :as rx] + [clojure.set :as set] [cuerdas.core :as str] [okulary.core :as l] - [potok.core :as ptk] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) -(log/set-level! :warn) +(log/set-level! :trace) -(def available-features - #{:auto-layout :components-v2 :new-css-system}) +(def global-enabled-features + (cfeat/get-enabled-features cf/flags)) -(defn- toggle-feature +(defn get-enabled-features + [state] + (-> (get state :features/runtime #{}) + (set/intersection cfeat/no-migration-features) + (set/union global-enabled-features))) + +(defn get-team-enabled-features + [state] + (-> global-enabled-features + (set/union (:features/runtime state #{})) + (set/intersection cfeat/no-migration-features) + (set/union cfeat/default-enabled-features) + (set/union (:features/team state #{})))) + +(def features-ref + (l/derived get-team-enabled-features st/state =)) + +(defn active-feature? + "Given a state and feature, check if feature is enabled" + [state feature] + (assert (contains? cfeat/supported-features feature) "not supported feature") + (or (contains? (get state :features/runtime) feature) + (if (contains? cfeat/no-migration-features feature) + (or (contains? global-enabled-features feature) + (contains? (get state :features/team) feature)) + (contains? (get state :features/team state) feature)))) + +(defn use-feature + "A react hook that checks if feature is currently enabled" + [feature] + (assert (contains? cfeat/supported-features feature) "Not supported feature") + (let [enabled-features (mf/deref features-ref)] + (contains? enabled-features feature))) + +(defn toggle-feature + "An event constructor for runtime feature toggle. + + Warning: if a feature is active globally or by team, it can't be + disabled." [feature] (ptk/reify ::toggle-feature ptk/UpdateEvent (update [_ state] - (let [features (or (:features state) #{})] - (if (contains? features feature) - (do - (log/debug :hint "feature disabled" :feature (d/name feature)) - (assoc state :features (disj features feature))) - (do - (log/debug :hint "feature enabled" :feature (d/name feature)) - (assoc state :features (conj features feature)))))))) + (assert (contains? cfeat/supported-features feature) "not supported feature") + (update state :features/runtime (fn [features] + (if (contains? features feature) + (do + (log/trc :hint "feature disabled" :feature feature) + (disj features feature)) + (do + (log/trc :hint "feature enabled" :feature feature) + (conj features feature)))))))) -(defn- enable-feature +(defn enable-feature [feature] (ptk/reify ::enable-feature ptk/UpdateEvent (update [_ state] - (let [features (or (:features state) #{})] - (if (contains? features feature) - state - (do - (log/debug :hint "feature enabled" :feature (d/name feature)) - (assoc state :features (conj features feature)))))))) - -(defn toggle-feature! - [feature] - (assert (contains? available-features feature) "Not supported feature") - (tm/schedule-on-idle #(st/emit! (toggle-feature feature)))) - -(defn enable-feature! - [feature] - (assert (contains? available-features feature) "Not supported feature") - (tm/schedule-on-idle #(st/emit! (enable-feature feature)))) - -(defn active-feature? - ([feature] - (active-feature? @st/state feature)) - ([state feature] - (assert (contains? available-features feature) "Not supported feature") - (contains? (get state :features) feature))) - -(def features - (l/derived :features st/state)) - -(defn active-feature - [feature] - (l/derived #(contains? % feature) features)) - -(defn use-feature - [feature] - (assert (contains? available-features feature) "Not supported feature") - (let [active-feature-ref (mf/use-memo (mf/deps feature) #(active-feature feature)) - active-feature? (mf/deref active-feature-ref)] - active-feature?)) + (assert (contains? cfeat/supported-features feature) "not supported feature") + (if (active-feature? state feature) + state + (do + (log/trc :hint "feature enabled" :feature feature) + (update state :features/runtime (fnil conj #{}) feature)))))) (defn initialize - [] - (ptk/reify ::initialize - ptk/WatchEvent - (watch [_ _ _] - (log/trace :hint "event:initialize" :fn "features") - (rx/concat - ;; Enable all features set on the configuration - (->> (rx/from cf/flags) - (rx/map name) - (rx/map (fn [flag] - (when (str/starts-with? flag "frontend-feature-") - (subs flag 17)))) - (rx/filter some?) - (rx/map keyword) - (rx/map enable-feature)) + ([] (initialize #{})) + ([team-features] + (assert (set? team-features) "expected a set of features") + (assert (every? string? team-features) "expected a set of strings") + + (ptk/reify ::initialize + ptk/UpdateEvent + (update [_ state] + (let [runtime-features (get state :features/runtime #{}) + team-features (into #{} + cfeat/xf-supported-features + team-features)] + (-> state + (assoc :features/runtime runtime-features) + (assoc :features/team team-features)))) + + ptk/WatchEvent + (watch [_ _ _] + (when *assert* + (->> (rx/from cfeat/no-migration-features) + (rx/filter #(not (contains? cfeat/backend-only-features %))) + (rx/observe-on :async) + (rx/map enable-feature)))) + + ptk/EffectEvent + (effect [_ state _] + (log/trc :hint "initialized features" + :team (str/join "," (:features/team state)) + :runtime (str/join "," (:features/runtime state))))))) - ;; Enable the rest of available configuration if we are on development - ;; environemnt (aka devenv). - (when *assert* - ;; By default, all features disabled, except in development - ;; environment, that are enabled except components-v2 - (->> (rx/from available-features) - (rx/filter #(not= % :components-v2)) - (rx/filter #(not= % :new-css-system)) - (rx/map enable-feature))))))) diff --git a/frontend/src/app/main/features/pointer_map.cljs b/frontend/src/app/main/features/pointer_map.cljs new file mode 100644 index 0000000000..993427e554 --- /dev/null +++ b/frontend/src/app/main/features/pointer_map.cljs @@ -0,0 +1,33 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.features.pointer-map + "A frontend specific helpers for work with pointer-map feature" + (:require + [app.common.transit :as t] + [app.main.repo :as rp] + [beicon.v2.core :as rx])) + +(defn resolve-file + [{:keys [id data] :as file}] + (letfn [(resolve-pointer [[key val :as kv]] + (if (t/pointer? val) + (->> (rp/cmd! :get-file-fragment {:file-id id :fragment-id @val}) + (rx/map #(get % :content)) + (rx/map #(vector key %))) + (rx/of kv))) + + (resolve-pointers [coll] + (->> (rx/from (seq coll)) + (rx/merge-map resolve-pointer) + (rx/reduce conj {})))] + + (->> (rx/zip (resolve-pointers data) + (resolve-pointers (:pages-index data))) + (rx/take 1) + (rx/map (fn [[data pages-index]] + (let [data (assoc data :pages-index pages-index)] + (assoc file :data data))))))) diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index 57fd19c155..a172011245 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -17,8 +17,7 @@ [app.util.globals :as globals] [app.util.http :as http] [app.util.object :as obj] - [beicon.core :as rx] - [clojure.set :as set] + [beicon.v2.core :as rx] [cuerdas.core :as str] [lambdaisland.uri :as u] [okulary.core :as l] @@ -144,7 +143,7 @@ (->> (fetch-gfont-css url) (rx/map process-gfont-css) (rx/tap #(on-loaded id)) - (rx/subs (partial add-font-css! id))) + (rx/subs! (partial add-font-css! id))) nil))) ;; --- LOADER: CUSTOM @@ -196,38 +195,40 @@ (if-not (exists? js/window) ;; If we are in the worker environment, we just mark it as loaded ;; without really loading it. - (do - (swap! loaded-hints conj {:font-id font-id :font-variant-id variant-id}) - (p/resolved font-id)) + (do + (swap! loaded-hints conj {:font-id font-id :font-variant-id variant-id}) + (p/resolved font-id)) - (let [font (get @fontsdb font-id)] - (cond - (nil? font) - (p/resolved font-id) + (let [font (get @fontsdb font-id)] + (cond + (nil? font) + (p/resolved font-id) ;; Font already loaded, we just continue - (contains? @loaded font-id) - (p/resolved font-id) + (contains? @loaded font-id) + (p/resolved font-id) ;; Font is currently downloading. We attach the caller to the promise - (contains? @loading font-id) - (p/resolved (get @loading font-id)) + (contains? @loading font-id) + (get @loading font-id) ;; First caller, we create the promise and then wait - :else - (let [on-load (fn [resolve] - (swap! loaded conj font-id) - (swap! loading dissoc font-id) - (resolve font-id)) + :else + (let [on-load (fn [resolve] + (swap! loaded conj font-id) + (swap! loading dissoc font-id) + (resolve font-id)) - load-p (p/create - (fn [resolve _] - (-> font - (assoc ::on-loaded (partial on-load resolve)) - (load-font))))] + load-p (-> (p/create + (fn [resolve _] + (-> font + (assoc ::on-loaded (partial on-load resolve)) + (load-font)))) + ;; We need to wait for the font to be loaded + (p/then (partial p/delay 120)))] - (swap! loading assoc font-id load-p) - load-p)))))) + (swap! loading assoc font-id load-p) + load-p)))))) (defn ready [cb] @@ -246,16 +247,42 @@ (get-default-variant font))) ;; Font embedding functions +(defn get-node-fonts + "Extracts the fonts used by some node" + [node] + (let [nodes (.from js/Array (dom/query-all node "[style*=font]")) + result (.reduce nodes (fn [obj node] + (let [style (.-style node) + font-family (.-fontFamily style) + [_ font] (first + (filter (fn [[_ {:keys [id family]}]] + (or (= family font-family) + (= id font-family))) + @fontsdb)) + font-id (:id font) + font-variant (get-variant font (.-fontVariant style)) + font-variant-id (:id font-variant)] + (obj/set! + obj + (dm/str font-id ":" font-variant-id) + {:font-id font-id + :font-variant-id font-variant-id}))) + #js {})] + (.values js/Object result))) (defn get-content-fonts "Extracts the fonts used by the content of a text shape" - [{font-id :font-id children :children :as content}] - (let [current-font - (if (some? font-id) - #{(select-keys content [:font-id :font-variant-id])} - #{(select-keys txt/default-text-attrs [:font-id :font-variant-id])}) - children-font (->> children (mapv get-content-fonts))] - (reduce set/union (conj children-font current-font)))) + [content] + (->> (txt/node-seq content) + (filter txt/is-text-node?) + (reduce + (fn [result {:keys [font-id] :as node}] + (let [current-font + (if (some? font-id) + (select-keys node [:font-id :font-variant-id]) + (select-keys txt/default-text-attrs [:font-id :font-variant-id]))] + (conj result current-font))) + #{}))) (defn fetch-font-css "Given a font and the variant-id, retrieves the fontface CSS" @@ -297,3 +324,25 @@ (->> (rx/from font-refs) (rx/mapcat fetch-font-css) (rx/reduce (fn [acc css] (dm/str acc "\n" css)) ""))) + +(defonce font-styles (js/Map.)) + +(defn get-font-style-id + [{:keys [font-id font-variant-id] + :or {font-variant-id "regular"}}] + (dm/fmt "%:%" font-id font-variant-id)) + +(defn get-font-styles-by-font-ref + [font-ref] + (let [id (get-font-style-id font-ref)] + (if (.has font-styles id) + (rx/of (.get font-styles id)) + (->> (rx/of font-ref) + (rx/mapcat fetch-font-css) + (rx/tap (fn [css] (.set font-styles id css))))))) + +(defn render-font-styles-cached + [font-refs] + (->> (rx/from font-refs) + (rx/merge-map get-font-styles-by-font-ref) + (rx/reduce (fn [acc css] (dm/str acc "\n" css)) ""))) diff --git a/frontend/src/app/main/rasterizer.cljs b/frontend/src/app/main/rasterizer.cljs new file mode 100644 index 0000000000..6fcb4dc8a8 --- /dev/null +++ b/frontend/src/app/main/rasterizer.cljs @@ -0,0 +1,140 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.rasterizer + "A main entry point for the rasterizer API interface. + + This ns is responsible to provide an API for create rasterizer + iframes and interact with them using asyncrhonous + messages." + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.logging :as log] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.util.dom :as dom] + [app.util.http :as http] + [beicon.v2.core :as rx] + [cuerdas.core :as str])) + +(defonce ready? false) +(defonce queue #js []) +(defonce instance nil) +(defonce msgbus (rx/subject)) +(defonce origin + (dm/str (assoc cf/rasterizer-uri :path "/rasterizer.html"))) + +(declare send-message!) + +(defn- process-queued-messages! + [] + (loop [message (.shift ^js queue)] + (when (some? message) + (send-message! message) + (recur (.shift ^js queue))))) + +(defn- on-message + "Handles a message from the rasterizer." + [event] + (let [evorigin (unchecked-get event "origin") + evdata (unchecked-get event "data")] + + (when (and (object? evdata) (str/starts-with? origin evorigin)) + (let [scope (unchecked-get evdata "scope") + type (unchecked-get evdata "type")] + (when (= "penpot/rasterizer" scope) + (when (= type "ready") + (set! ready? true) + (process-queued-messages!)) + (rx/push! msgbus evdata)))))) + +(defn- send-message! + "Sends a message to the rasterizer." + [message] + (let [window (.-contentWindow ^js instance)] + (.postMessage ^js window message origin))) + +(defn- queue-message! + "Queues a message to be sent to the thumbnail renderer when it's ready." + [message] + (.push ^js queue message)) + +(defn- replace-uris + "Replaces URIs for rasterizer ones in styles" + [styles] + (let [public-uri (str cf/public-uri) + rasterizer-uri (str cf/rasterizer-uri)] + (if-not (= public-uri rasterizer-uri) + (str/replace styles public-uri rasterizer-uri) + styles))) + +(defn render + "Renders an SVG" + [{:keys [data styles width result] :as params}] + (let [styles (replace-uris (d/nilv styles "")) + result (d/nilv result "blob") + id (dm/str (uuid/next)) + payload #js {:data data :styles styles :width width :result result} + message #js {:id id + :scope "penpot/rasterizer" + :payload payload}] + + (if ^boolean ready? + (send-message! message) + (queue-message! message)) + + (->> msgbus + (rx/filter #(= id (unchecked-get % "id"))) + (rx/mapcat (fn [msg] + (case (unchecked-get msg "type") + "success" (rx/of (unchecked-get msg "payload")) + "failure" (rx/throw (js/Error. (unchecked-get msg "payload")))))) + (rx/take 1)))) + +(defn render-node + "Renders an SVG using a node" + [{:keys [node styles width result] :as params}] + (let [width (d/nilv width (dom/get-attribute node "width")) + styles (d/nilv styles "") + data (dom/node->xml node) + result (d/nilv result "blob")] + (render {:data data :styles styles :width width :result result}))) + +(defn init! + "Initializes the rasterizer." + [] + (let [iframe (dom/create-element "iframe")] + (dom/set-attribute! iframe "src" origin) + (dom/set-attribute! iframe "hidden" true) + (.addEventListener js/window "message" on-message) + (->> (http/fetch {:method :head + :uri cf/rasterizer-uri + :mode :no-cors}) + (rx/map (fn [response] + (let [allowed? (not (.-redirected response))] + (when-not allowed? + (log/err :hint "rasterizer iframe blocked by adblocker" :origin origin)) + allowed?))) + (rx/catch (fn [cause] + (log/err :hint "rasterizer iframe blocked by adblocker" :origin origin :cause cause) + (rx/of false))) + + (rx/subs! (fn [allowed?] + (if allowed? + (do + (dom/append-child! js/document.body iframe) + (set! instance iframe)) + + (let [new-origin (dm/str (assoc cf/public-uri :path "/rasterizer.html"))] + (log/warn :hint "fallback to main domain" :origin new-origin) + + (dom/set-attribute! iframe "src" new-origin) + (dom/append-child! js/document.body iframe) + + (set! origin new-origin) + (set! cf/rasterizer-uri cf/public-uri) + (set! instance iframe)))))))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 7f7e4bc751..969bb43a65 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -9,7 +9,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cph] [app.common.types.shape-tree :as ctt] [app.common.types.shape.layout :as ctl] [app.main.data.workspace.state-helpers :as wsh] @@ -82,16 +82,29 @@ (dm/get-in state [:dashboard-local :selected-project])) st/state)) +(defn- dashboard-extract-selected + [files selected] + (let [get-file #(get files %) + sim-file #(select-keys % [:id :name :project-id :is-shared]) + xform (comp (keep get-file) + (map sim-file))] + (->> (into #{} xform selected) + (d/index-by :id)))) + +(def dashboard-selected-search + (l/derived (fn [state] + ;; we need to this because :dashboard-search-result is a list + ;; of maps and we need a map of maps (using :id as key). + (let [files (d/index-by :id (:dashboard-search-result state))] + (->> (dm/get-in state [:dashboard-local :selected-files]) + (dashboard-extract-selected files)))) + st/state)) + (def dashboard-selected-files (l/derived (fn [state] - (let [get-file #(dm/get-in state [:dashboard-files %]) - sim-file #(select-keys % [:id :name :project-id :is-shared]) - selected (get-in state [:dashboard-local :selected-files]) - xform (comp (map get-file) - (map sim-file))] - (->> (into #{} xform selected) - (d/index-by :id)))) - st/state =)) + (->> (dm/get-in state [:dashboard-local :selected-files]) + (dashboard-extract-selected (:dashboard-files state)))) + st/state)) ;; ---- Workspace refs @@ -126,6 +139,9 @@ [id] (l/derived #(contains? % id) selected-shapes)) +(def highlighted-shapes + (l/derived :highlighted workspace-local)) + (def export-in-progress? (l/derived :export-in-progress? export)) @@ -193,6 +209,9 @@ (def snap-pixel? (l/derived #(contains? % :snap-pixel-grid) workspace-layout)) +(def rulers? + (l/derived #(contains? % :rulers) workspace-layout)) + (def workspace-file "A ref to a striped vision of file (without data)." (l/derived (fn [state] @@ -311,8 +330,8 @@ [id] (l/derived (fn [objects] - (let [children-ids (get-in objects [id :shapes])] - (into [] (keep (d/getf objects)) children-ids))) + (->> (dm/get-in objects [id :shapes]) + (into [] (keep (d/getf objects))))) workspace-page-objects =)) (defn all-children-objects @@ -416,10 +435,6 @@ ids))) st/state =)) -;; Remove this when deprecating components-v2 -(def remove-graphics - (l/derived :remove-graphics st/state)) - ;; ---- Viewer refs (defn lookup-viewer-objects-by-id @@ -472,15 +487,15 @@ (dm/get-in state [:viewer-local :zoom-type])) st/state)) -(def thumbnail-data - (l/derived #(get % :workspace-thumbnails {}) st/state)) +(def workspace-thumbnails + (l/derived :workspace-thumbnails st/state)) -(defn thumbnail-frame-data - [page-id frame-id] +(defn workspace-thumbnail-by-id + [object-id] (l/derived - (fn [thumbnails] - (get thumbnails (dm/str page-id frame-id))) - thumbnail-data)) + (fn [state] + (dm/get-in state [:workspace-thumbnails object-id])) + st/state)) (def workspace-text-modifier (l/derived :workspace-text-modifier st/state)) @@ -488,24 +503,43 @@ (defn workspace-text-modifier-by-id [id] (l/derived #(get % id) workspace-text-modifier =)) -(defn is-flex-layout-child? +(defn is-layout-child? [ids] (l/derived (fn [objects] (->> ids (map (d/getf objects)) - (some (partial ctl/flex-layout-immediate-child? objects)))) + (some (partial ctl/any-layout-immediate-child? objects)))) workspace-page-objects)) -(defn all-flex-layout-child? +(defn all-layout-child? + [ids] + (l/derived + (fn [objects] + (->> ids + (map (d/getf objects)) + (every? (partial ctl/any-layout-immediate-child? objects)))) + workspace-page-objects =)) + +(defn flex-layout-child? [ids] (l/derived (fn [objects] (->> ids (map (d/getf objects)) (every? (partial ctl/flex-layout-immediate-child? objects)))) - workspace-page-objects)) + workspace-page-objects =)) +(defn grid-layout-child? + [ids] + (l/derived + (fn [objects] + (->> ids + (map (d/getf objects)) + (every? (partial ctl/grid-layout-immediate-child? objects)))) + workspace-page-objects =)) + +;; FIXME: move to viewer.inspect.code (defn get-flex-child-viewer [ids page-id] (l/derived @@ -517,7 +551,7 @@ ids))) st/state =)) - +;; FIXME: move to viewer.inspect.code (defn get-viewer-objects ([] (let [route (deref route) @@ -541,9 +575,6 @@ [id] (l/derived #(get % id) workspace-grid-edition)) -(def workspace-annotations - (l/derived #(get % :workspace-annotations {}) st/state)) - (def current-file-id (l/derived :current-file-id st/state)) @@ -552,3 +583,12 @@ (defn workspace-preview-blend-by-id [id] (l/derived (l/key id) workspace-preview-blend =)) + +(def specialized-panel + (l/derived :specialized-panel st/state)) + +(def updating-library + (l/derived :updating-library st/state)) + +(def persistence-state + (l/derived (comp :status :workspace-persistence) st/state)) diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 19334c81bc..16c961b5dd 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -14,15 +14,19 @@ (:require ["react-dom/server" :as rds] [app.common.colors :as clr] + [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.bounds :as gsb] + [app.common.logging :as l] [app.common.math :as mth] - [app.common.pages.helpers :as cph] [app.common.types.file :as ctf] [app.common.types.modifiers :as ctm] [app.common.types.shape-tree :as ctst] + [app.common.types.shape.layout :as ctl] [app.config :as cfg] [app.main.fonts :as fonts] [app.main.ui.context :as muc] @@ -31,6 +35,7 @@ [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.export :as export] [app.main.ui.shapes.frame :as frame] + [app.main.ui.shapes.grid-layout-viewer :refer [grid-layout-viewer]] [app.main.ui.shapes.group :as group] [app.main.ui.shapes.image :as image] [app.main.ui.shapes.path :as path] @@ -39,11 +44,12 @@ [app.main.ui.shapes.svg-raw :as svg-raw] [app.main.ui.shapes.text :as text] [app.main.ui.shapes.text.fontfaces :as ff] + [app.util.dom :as dom] [app.util.http :as http] - [app.util.object :as obj] [app.util.strings :as ust] + [app.util.thumbnails :as th] [app.util.timers :as ts] - [beicon.core :as rx] + [beicon.v2.core :as rx] [clojure.set :as set] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -61,16 +67,25 @@ :fill color}]) (defn- calculate-dimensions - [objects] - (let [bounds - (->> (ctst/get-root-objects objects) - (map (partial gsb/get-object-bounds objects)) - (gsh/join-rects))] - (-> bounds - (update :x mth/finite 0) - (update :y mth/finite 0) - (update :width mth/finite 100000) - (update :height mth/finite 100000)))) + [objects aspect-ratio] + (let [root-objects (ctst/get-root-objects objects)] + (if (empty? root-objects) + ;; Empty page, we create an arbitrary rect for the thumbnail + (-> (grc/make-rect {:x 0 :y 0 :width 100 :height 100}) + (grc/update-rect :position) + (grc/fix-aspect-ratio aspect-ratio)) + + (let [bounds + (->> root-objects + (map (partial gsb/get-object-bounds objects)) + (grc/join-rects))] + (-> bounds + (update :x mth/finite 0) + (update :y mth/finite 0) + (update :width mth/finite 100000) + (update :height mth/finite 100000) + (grc/update-rect :position) + (grc/fix-aspect-ratio aspect-ratio)))))) (declare shape-wrapper-factory) @@ -79,11 +94,11 @@ (let [shape-wrapper (shape-wrapper-factory objects) frame-shape (frame/frame-shape shape-wrapper)] (mf/fnc frame-wrapper - [{:keys [shape] :as props}] - - (let [render-thumbnails? (mf/use-ctx muc/render-thumbnails) - childs (mapv #(get objects %) (:shapes shape))] - (if (and render-thumbnails? (some? (:thumbnail shape))) + {::mf/wrap-props false} + [{:keys [shape]}] + (let [thumbnails? (mf/use-ctx muc/render-thumbnails) + childs (mapv (d/getf objects) (:shapes shape))] + (if (and thumbnails? (some? (:thumbnail shape))) [:& frame/frame-thumbnail {:shape shape :bounds (:children-bounds shape)}] [:& frame-shape {:shape shape :childs childs}]))))) @@ -105,7 +120,7 @@ (mf/fnc bool-wrapper [{:keys [shape] :as props}] (let [childs (mf/with-memo [(:id shape) objects] - (->> (cph/get-children-ids objects (:id shape)) + (->> (cfh/get-children-ids objects (:id shape)) (select-keys objects)))] [:& bool-shape {:shape shape :childs childs}])))) @@ -163,8 +178,8 @@ (defn adapt-root-frame [objects object] - (let [shapes (cph/get-immediate-children objects) - srect (gsh/selection-rect shapes) + (let [shapes (cfh/get-immediate-children objects) + srect (gsh/shapes->rect shapes) object (merge object (select-keys srect [:x :y :width :height]))] (assoc object :fill-color "#f0f0f0"))) @@ -172,7 +187,7 @@ [objects object-id] (let [object (get objects object-id) object (cond->> object - (cph/root? object) + (cfh/root? object) (adapt-root-frame objects)) ;; Replace the previous object with the new one @@ -181,7 +196,7 @@ vector (-> (gpt/point (:x object) (:y object)) (gpt/negate)) - mod-ids (cons object-id (cph/get-children-ids objects object-id)) + mod-ids (cons object-id (cfh/get-children-ids objects object-id)) updt-fn #(update %1 %2 gsh/transform-shape (ctm/move-modifiers vector))] @@ -189,11 +204,11 @@ (mf/defc page-svg {::mf/wrap [mf/memo]} - [{:keys [data thumbnails? render-embed? include-metadata?] :as props - :or {render-embed? false include-metadata? false}}] + [{:keys [data use-thumbnails embed include-metadata aspect-ratio] :as props + :or {embed false include-metadata false}}] (let [objects (:objects data) - shapes (cph/get-immediate-children objects) - dim (calculate-dimensions objects) + shapes (cfh/get-immediate-children objects) + dim (calculate-dimensions objects aspect-ratio) vbox (format-viewbox dim) bgcolor (dm/get-in data [:options :background] default-color) @@ -202,25 +217,25 @@ (mf/deps objects) #(shape-wrapper-factory objects))] - [:& (mf/provider muc/render-thumbnails) {:value thumbnails?} - [:& (mf/provider embed/context) {:value render-embed?} - [:& (mf/provider export/include-metadata-ctx) {:value include-metadata?} + [:& (mf/provider muc/render-thumbnails) {:value use-thumbnails} + [:& (mf/provider embed/context) {:value embed} + [:& (mf/provider export/include-metadata-ctx) {:value include-metadata} [:svg {:view-box vbox :version "1.1" :xmlns "http://www.w3.org/2000/svg" :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns") + :xmlns:penpot (when include-metadata "https://penpot.app/xmlns") :style {:width "100%" :height "100%" :background bgcolor} :fill "none"} - (when include-metadata? + (when include-metadata [:& export/export-page {:id (:id data) :options (:options data)}]) (let [shapes (->> shapes - (remove cph/frame-shape?) - (mapcat #(cph/get-children-with-self objects (:id %)))) + (remove cfh/frame-shape?) + (mapcat #(cfh/get-children-with-self objects (:id %)))) fonts (ff/shapes->fonts shapes)] [:& ff/fontfaces-style {:fonts fonts}]) @@ -228,16 +243,34 @@ [:& shape-wrapper {:shape item :key (:id item)}])]]]])) +(mf/defc frame-imposter + {::mf/wrap-props false} + [{:keys [objects frame vbox x y width height background]}] + (let [shape-wrapper (shape-wrapper-factory objects)] + [:& (mf/provider muc/render-thumbnails) {:value false} + [:svg {:view-box vbox + :width (ust/format-precision width viewbox-decimal-precision) + :height (ust/format-precision height viewbox-decimal-precision) + :version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :fill "none"} + (when (some? background) + [:rect {:x x :y y :width width :height height :fill background}]) + [:& shape-wrapper {:shape frame}]]])) ;; Component that serves for render frame thumbnails, mainly used in ;; the viewer and inspector (mf/defc frame-svg {::mf/wrap [mf/memo]} - [{:keys [objects frame zoom show-thumbnails?] :or {zoom 1} :as props}] - (let [frame-id (:id frame) - include-metadata? (mf/use-ctx export/include-metadata-ctx) + [{:keys [objects frame zoom use-thumbnails aspect-ratio background-color] :or {zoom 1} :as props}] + (let [frame-id (:id frame) - bounds (gsb/get-object-bounds objects frame) + bgcolor (d/nilv background-color default-color) + include-metadata (mf/use-ctx export/include-metadata-ctx) + + bounds (-> (gsb/get-object-bounds objects frame) + (grc/fix-aspect-ratio aspect-ratio)) ;; Bounds without shadows/blur will be the bounds of the thumbnail bounds2 (gsb/get-object-bounds objects (dissoc frame :shadow :blur)) @@ -246,7 +279,7 @@ vector (gpt/negate delta-bounds) children-ids - (cph/get-children-ids objects frame-id) + (cfh/get-children-ids objects frame-id) objects (mf/with-memo [frame-id objects vector] @@ -277,71 +310,138 @@ height (* (:height bounds) zoom) vbox (format-viewbox {:width (:width bounds 0) :height (:height bounds 0)})] - [:& (mf/provider muc/render-thumbnails) {:value show-thumbnails?} + [:& (mf/provider muc/render-thumbnails) {:value use-thumbnails} [:svg {:view-box vbox :width (ust/format-precision width viewbox-decimal-precision) :height (ust/format-precision height viewbox-decimal-precision) :version "1.1" :xmlns "http://www.w3.org/2000/svg" :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns") + :xmlns:penpot (when include-metadata "https://penpot.app/xmlns") + :style {:background bgcolor} :fill "none"} [:& shape-wrapper {:shape frame}]]])) +(mf/defc empty-grids + {::mf/wrap-props false} + [{:keys [root-shape-id objects]}] + (let [empty-grids + (->> (cons root-shape-id (cfh/get-children-ids objects root-shape-id)) + (filter #(ctl/grid-layout? objects %)) + (map #(get objects %)) + (filter #(empty? (:shapes %))))] + (for [grid empty-grids] + [:& grid-layout-viewer {:shape grid :objects objects}]))) + ;; Component for rendering a thumbnail of a single componenent. Mainly ;; used to render thumbnails on assets panel. (mf/defc component-svg {::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} - [{:keys [objects root-shape zoom] :or {zoom 1} :as props}] + [{:keys [objects root-shape show-grids? zoom class] :or {zoom 1} :as props}] (when root-shape - (let [root-shape-id (:id root-shape) - include-metadata? (mf/use-ctx export/include-metadata-ctx) + (let [root-shape-id (:id root-shape) + include-metadata (mf/use-ctx export/include-metadata-ctx) - vector - (mf/use-memo - (mf/deps (:x root-shape) (:y root-shape)) - (fn [] - (-> (gpt/point (:x root-shape) (:y root-shape)) - (gpt/negate)))) + vector + (mf/use-memo + (mf/deps (:x root-shape) (:y root-shape)) + (fn [] + (-> (gpt/point (:x root-shape) (:y root-shape)) + (gpt/negate)))) - objects - (mf/use-memo - (mf/deps vector objects root-shape-id) - (fn [] - (let [children-ids (cons root-shape-id (cph/get-children-ids objects root-shape-id)) - update-fn #(update %1 %2 gsh/transform-shape (ctm/move-modifiers vector))] - (reduce update-fn objects children-ids)))) + objects + (mf/use-memo + (mf/deps vector objects root-shape-id) + (fn [] + (let [children-ids (cons root-shape-id (cfh/get-children-ids objects root-shape-id)) + update-fn #(update %1 %2 gsh/transform-shape (ctm/move-modifiers vector))] + (reduce update-fn objects children-ids)))) - root-shape' (get objects root-shape-id) - width (* (:width root-shape') zoom) - height (* (:height root-shape') zoom) - vbox (format-viewbox {:width (:width root-shape' 0) - :height (:height root-shape' 0)}) - root-shape-wrapper - (mf/use-memo - (mf/deps objects root-shape') - (fn [] - (case (:type root-shape') - :group (group-wrapper-factory objects) - :frame (frame-wrapper-factory objects))))] + root-shape' (get objects root-shape-id) + width (* (:width root-shape') zoom) + height (* (:height root-shape') zoom) + vbox (format-viewbox {:width (:width root-shape' 0) + :height (:height root-shape' 0)}) + root-shape-wrapper + (mf/use-memo + (mf/deps objects root-shape') + (fn [] + (case (:type root-shape') + :group (group-wrapper-factory objects) + :frame (frame-wrapper-factory objects))))] - [:svg {:view-box vbox - :width (ust/format-precision width viewbox-decimal-precision) - :height (ust/format-precision height viewbox-decimal-precision) - :version "1.1" - :xmlns "http://www.w3.org/2000/svg" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns") - :fill "none"} + [:svg {:view-box vbox + :width (ust/format-precision width viewbox-decimal-precision) + :height (ust/format-precision height viewbox-decimal-precision) + :version "1.1" + :class class + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns:penpot (when include-metadata "https://penpot.app/xmlns") + :fill "none"} - [:> shape-container {:shape root-shape'} - [:& (mf/provider muc/is-component?) {:value true} - [:& root-shape-wrapper {:shape root-shape' :view-box vbox}]]]]))) + [:* + [:> shape-container {:shape root-shape'} + [:& (mf/provider muc/is-component?) {:value true} + [:& root-shape-wrapper {:shape root-shape' :view-box vbox}]]] + + (when show-grids? + [:& empty-grids {:root-shape-id root-shape-id :objects objects}])]]))) + +(mf/defc component-svg-thumbnail + {::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} + [{:keys [thumbnail-uri on-error show-grids? class + objects root-shape zoom] :or {zoom 1} :as props}] + + (when root-shape + (let [root-shape-id (:id root-shape) + + vector + (mf/use-memo + (mf/deps (:x root-shape) (:y root-shape)) + (fn [] + (-> (gpt/point (:x root-shape) (:y root-shape)) + (gpt/negate)))) + + objects + (mf/use-memo + (mf/deps vector objects root-shape-id) + (fn [] + (let [children-ids (cons root-shape-id (cfh/get-children-ids objects root-shape-id)) + update-fn #(update %1 %2 gsh/transform-shape (ctm/move-modifiers vector))] + (reduce update-fn objects children-ids)))) + + root-shape' (get objects root-shape-id) + + width (:width root-shape' 0) + height (:height root-shape' 0) + width-zoom (* (:width root-shape') zoom) + height-zoom (* (:height root-shape') zoom) + vbox (format-viewbox {:width width :height height})] + + [:svg {:view-box vbox + :width (ust/format-precision width-zoom viewbox-decimal-precision) + :height (ust/format-precision height-zoom viewbox-decimal-precision) + :version "1.1" + :class class + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :fill "none"} + [:image {:x 0 + :y 0 + :width width + :height height + :href thumbnail-uri + :on-error on-error + :loading "lazy" + :decoding "async"}] + (when show-grids? + [:& empty-grids {:root-shape-id root-shape-id :objects objects}])]))) (mf/defc object-svg {::mf/wrap [mf/memo]} - [{:keys [objects object-id render-embed?] - :or {render-embed? false} + [{:keys [objects object-id embed] + :or {embed false} :as props}] (let [object (get objects object-id) object (cond-> object @@ -349,7 +449,7 @@ (assoc :fills [])) - {:keys [width height] :as bounds} (gsb/get-object-bounds objects object) + {:keys [width height] :as bounds} (gsb/get-object-bounds objects object {:ignore-margin? false}) vbox (format-viewbox bounds) fonts (ff/shape->fonts object objects) @@ -358,7 +458,7 @@ (shape-wrapper-factory objects))] [:& (mf/provider export/include-metadata-ctx) {:value false} - [:& (mf/provider embed/context) {:value render-embed?} + [:& (mf/provider embed/context) {:value embed} [:svg {:id (dm/str "screenshot-" object-id) :view-box vbox :width (ust/format-precision width viewbox-decimal-precision) @@ -409,40 +509,37 @@ (mf/deps objects) (fn [] (frame-wrapper-factory objects)))] - [:> "symbol" #js {:id (str root-id) - :viewBox vbox - "penpot:path" path - "penpot:main-instance-id" main-instance-id - "penpot:main-instance-page" main-instance-page - "penpot:main-instance-x" main-instance-x - "penpot:main-instance-y" main-instance-y} - [:title name] - [:> shape-container {:shape root-shape} - (case (:type root-shape) - :group [:& group-wrapper {:shape root-shape :view-box vbox}] - :frame [:& frame-wrapper {:shape root-shape :view-box vbox}])]])) + (when root-shape + [:> "symbol" #js {:id (str (:id component)) + :viewBox vbox + "penpot:path" path + "penpot:main-instance-id" main-instance-id + "penpot:main-instance-page" main-instance-page + "penpot:main-instance-x" main-instance-x + "penpot:main-instance-y" main-instance-y} + [:title name] + [:> shape-container {:shape root-shape} + (case (:type root-shape) + :group [:& group-wrapper {:shape root-shape :view-box vbox}] + :frame [:& frame-wrapper {:shape root-shape :view-box vbox}])]]))) -(mf/defc components-sprite-svg +(mf/defc components-svg {::mf/wrap-props false} - [props] - (let [data (obj/get props "data") - children (obj/get props "children") - render-embed? (obj/get props "render-embed?") - include-metadata? (obj/get props "include-metadata?") - source (keyword (obj/get props "source" "components"))] - [:& (mf/provider embed/context) {:value render-embed?} - [:& (mf/provider export/include-metadata-ctx) {:value include-metadata?} + [{:keys [data children embed include-metadata source]}] + (let [source (keyword (d/nilv source "components"))] + [:& (mf/provider embed/context) {:value embed} + [:& (mf/provider export/include-metadata-ctx) {:value include-metadata} [:svg {:version "1.1" :xmlns "http://www.w3.org/2000/svg" :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns") + :xmlns:penpot (when include-metadata "https://penpot.app/xmlns") :style {:display (when-not (some? children) "none")} :fill "none"} [:defs (for [[id component] (source data)] (let [component (ctf/load-component-objects data component)] [:& component-symbol {:key (dm/str id) :component component}]))] - + children]]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -467,7 +564,7 @@ (mapcat get-image-data))] (->> (rx/from images) (rx/map #(cfg/resolve-file-media %)) - (rx/flat-map http/fetch-data-uri)))) + (rx/merge-map http/fetch-data-uri)))) (defn populate-fonts-cache [objects] (let [texts (->> objects @@ -478,10 +575,10 @@ (->> (rx/from texts) (rx/map fonts/get-content-fonts) (rx/reduce set/union #{}) - (rx/flat-map identity) - (rx/flat-map fonts/fetch-font-css) - (rx/flat-map fonts/extract-fontface-urls) - (rx/flat-map http/fetch-data-uri)))) + (rx/merge-map identity) + (rx/merge-map fonts/fetch-font-css) + (rx/merge-map fonts/extract-fontface-urls) + (rx/merge-map http/fetch-data-uri)))) (defn render-page [data] @@ -494,7 +591,7 @@ (->> (rx/of data) (rx/map (fn [data] - (let [elem (mf/element page-svg #js {:data data :render-embed? true :include-metadata? true})] + (let [elem (mf/element page-svg #js {:data data :embed true :include-metadata true})] (rds/renderToStaticMarkup elem))))))) (defn render-components @@ -514,7 +611,60 @@ (->> (rx/of data) (rx/map (fn [data] - (let [elem (mf/element components-sprite-svg - #js {:data data :render-embed? true :include-metadata? true + (let [elem (mf/element components-svg + #js {:data data + :embed true + :include-metadata true :source (name source)})] (rds/renderToStaticMarkup elem)))))))) + +(defn render-frame + ([objects shape object-id] + (render-frame objects shape object-id nil)) + ([objects shape object-id options] + (if (some? shape) + (let [fonts (ff/shape->fonts shape objects) + + bounds (gsb/get-object-bounds objects shape {:ignore-margin? false}) + + background (when (str/ends-with? object-id "component") + (or (:background options) (dom/get-css-variable "--assets-component-background-color") "#fff")) + + x (dm/get-prop bounds :x) + y (dm/get-prop bounds :y) + width (dm/get-prop bounds :width) + height (dm/get-prop bounds :height) + + viewbox (str/ffmt "% % % %" x y width height) + + [fixed-width fixed-height] (th/get-relative-size width height) + [component-width component-height] (th/get-proportional-size width height 140 140) + + data (with-redefs [cfg/public-uri cfg/rasterizer-uri] + (rds/renderToStaticMarkup + (mf/element frame-imposter + #js {:objects objects + :frame shape + :vbox viewbox + :background background + :x x + :y y + :width width + :height height}))) + component? (str/ends-with? object-id "/component")] + + (->> (fonts/render-font-styles-cached fonts) + (rx/catch (fn [cause] + (l/err :hint "unexpected error on rendering imposter" + :cause cause) + (rx/empty))) + (rx/map (fn [styles] + {:id object-id + :data data + :width (if component? component-width fixed-width) + :height (if component? component-height fixed-height) + :styles styles})))) + + (do + (l/warn :msg "imposter shape is nil") + (rx/empty))))) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 348b606dcd..ed71b827a5 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -7,10 +7,12 @@ (ns app.main.repo (:require [app.common.data :as d] + [app.common.transit :as t] [app.common.uri :as u] [app.config :as cf] [app.util.http :as http] - [beicon.core :as rx] + [app.util.sse :as sse] + [beicon.v2.core :as rx] [cuerdas.core :as str])) (defn handle-response @@ -21,44 +23,53 @@ (rx/of nil) (= 502 status) - (rx/throw {:type :bad-gateway}) + (rx/throw (ex-info "http error" {:type :bad-gateway})) (= 503 status) - (rx/throw {:type :service-unavailable}) + (rx/throw (ex-info "http error" {:type :service-unavailable})) (= 0 (:status response)) - (rx/throw {:type :offline}) + (rx/throw (ex-info "http error" {:type :offline})) (= 200 status) (rx/of body) (= 413 status) - (rx/throw {:type :validation - :code :request-body-too-large}) + (rx/throw (ex-info "http error" + {:type :validation + :code :request-body-too-large})) (and (>= status 400) (map? body)) - (rx/throw body) + (rx/throw (ex-info "http error" body)) :else - (rx/throw {:type :unexpected-error + (rx/throw + (ex-info "http error" + {:type :unexpected-error :status status - :data body}))) + :data body})))) (def default-options {:update-file {:query-params [:id]} :get-raw-file {:rename-to :get-file :raw-transit? true} - :upsert-file-object-thumbnail {:query-params [:file-id :object-id]} - :create-file-object-thumbnail {:query-params [:file-id :object-id] - :form-data? true} + + :create-file-object-thumbnail + {:query-params [:file-id :object-id :tag] + :form-data? true} :create-file-thumbnail {:query-params [:file-id :revn] :form-data? true} + ::sse/clone-template + {:response-type ::sse/stream} + + ::sse/import-binfile + {:response-type ::sse/stream + :form-data? true} + :export-binfile {:response-type :blob} - :import-binfile {:form-data? true} - :retrieve-list-of-builtin-templates {:query-params :all} - }) + :retrieve-list-of-builtin-templates {:query-params :all}}) (defn- send! "A simple helper for a common case of sending and receiving transit @@ -84,9 +95,9 @@ :else :post) request {:method method - :uri (u/join cf/public-uri "api/rpc/command/" (name id)) + :uri (u/join cf/public-uri "api/rpc/command/" nid) :credentials "include" - :headers {"accept" "application/transit+json"} + :headers {"accept" "application/transit+json,text/event-stream,*/*"} :body (when (= method :post) (if form-data? (http/form-data params) @@ -96,11 +107,21 @@ (if query-params (select-keys params query-params) nil)) - :response-type (or response-type :text)}] - (->> (http/send! request) - (rx/map decode-fn) - (rx/mapcat handle-response)))) + :response-type + (if (= response-type ::sse/stream) + :stream + (or response-type :text))} + + result (->> (http/send! request) + (rx/map decode-fn) + (rx/mapcat handle-response))] + + (cond->> result + (= ::sse/stream response-type) + (rx/mapcat (fn [body] + (-> (sse/create-stream body) + (sse/read-stream t/decode-str))))))) (defmulti cmd! (fn [id _] id)) diff --git a/frontend/src/app/main/snap.cljs b/frontend/src/app/main/snap.cljs index 27480b0e80..d5ec5a6cfd 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -7,17 +7,19 @@ (ns app.main.snap (:require [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.focus :as cpf] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] + [app.common.geom.snap :as sp] [app.common.math :as mth] - [app.common.pages :as cp] - [app.common.pages.helpers :as cph] [app.common.uuid :refer [zero]] [app.main.refs :as refs] [app.main.worker :as uw] - [app.util.geom.snap-points :as sp] [app.util.range-tree :as rt] - [beicon.core :as rx] + [beicon.v2.core :as rx] [clojure.set :as set])) (def ^:const snap-accuracy 10) @@ -38,14 +40,14 @@ (fn [{:keys [type id frame-id]}] (cond (= type :layout) - (or (not (contains? layout :display-grid)) - (not (contains? layout :snap-grid)) + (or (not (contains? layout :display-guides)) + (not (contains? layout :snap-guides)) (and (d/not-empty? focus) (not (contains? focus id)))) (= type :guide) - (or (not (contains? layout :rules)) - (not (contains? layout :snap-guides)) + (or (not (contains? layout :rulers)) + (not (contains? layout :snap-ruler-guides)) (and (d/not-empty? focus) (not (contains? focus frame-id)))) @@ -53,7 +55,7 @@ (or (contains? filter-shapes id) (not (contains? layout :dynamic-alignment)) (and (d/not-empty? focus) - (not (cp/is-in-focus? objects focus id))))))) + (not (cpf/is-in-focus? objects focus id))))))) (defn- calculate-distance [query-result point coord] (->> query-result @@ -80,13 +82,14 @@ (defn get-snap-points [page-id frame-id remove-snap? zoom point coord] (let [value (get point coord) - vbox @refs/vbox] + vbox @refs/vbox + ranges [[(- value (/ 0.5 zoom)) (+ value (/ 0.5 zoom))]]] (->> (uw/ask! {:cmd :snaps/range-query :page-id page-id :frame-id frame-id :axis coord :bounds vbox - :ranges [[(- value (/ 0.5 zoom)) (+ value (/ 0.5 zoom))]]}) + :ranges ranges}) (rx/take 1) (rx/map (remove-from-snap-points remove-snap?))))) @@ -179,7 +182,7 @@ range-tree (- cd snap-distance-accuracy) (+ cd snap-distance-accuracy)) - (map #(- (first %) cd )))))))) + (map #(- (first %) cd)))))))) get-middle-snaps (fn [lt-dist gt-dist] @@ -208,8 +211,9 @@ (defn search-snap-distance [selrect coord shapes-lt shapes-gt zoom] (->> (rx/combine-latest shapes-lt shapes-gt) - (rx/map (fn [[shapes-lt shapes-gt]] - (calculate-snap coord selrect shapes-lt shapes-gt zoom))))) + (rx/map + (fn [[shapes-lt shapes-gt]] + (calculate-snap coord selrect shapes-lt shapes-gt zoom))))) (defn select-shapes-area [page-id frame-id selected objects area] @@ -218,7 +222,7 @@ :frame-id frame-id :include-frames? true :rect area}) - (rx/map #(cph/clean-loops objects %)) + (rx/map #(cfh/clean-loops objects %)) (rx/map #(set/difference % selected)) (rx/map #(map (d/getf objects) %)))) @@ -226,17 +230,17 @@ [page-id shapes objects zoom movev] (let [frame-id (snap-frame-id shapes) frame (get objects frame-id) - selrect (->> shapes (map #(gsh/move % movev)) gsh/selection-rect)] + selrect (->> shapes (map #(gsh/move % movev)) gsh/shapes->rect)] (->> (rx/of (vector frame selrect)) (rx/merge-map (fn [[frame selrect]] - (let [vbox (gsh/rect->selrect @refs/vbox) + (let [vbox (deref refs/vbox) + frame-id (->> shapes first :frame-id) + frame-sr (when-not (cfh/root? frame) (dm/get-prop frame :selrect)) + bounds (d/nilv (grc/clip-rect frame-sr vbox) vbox) selected (into #{} (map :id shapes)) - areas (->> (gsh/selrect->areas - (or (gsh/clip-selrect (:selrect frame) vbox) - vbox) - selrect) + areas (->> (gsh/get-areas bounds selrect) (d/mapm #(select-shapes-area page-id frame-id selected objects %2))) snap-x (search-snap-distance selrect :x (:left areas) (:right areas) zoom) snap-y (search-snap-distance selrect :y (:top areas) (:bottom areas) zoom)] @@ -272,8 +276,8 @@ snap-points (->> shapes - (gsh/selection-rect) - (sp/selrect-snap-points) + (gsh/shapes->rect) + (sp/rect->snap-points) ;; Move the points in the translation vector (map #(gpt/add % movev)))] diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index 04f7153008..7b02335e76 100644 --- a/frontend/src/app/main/store.cljs +++ b/frontend/src/app/main/store.cljs @@ -8,9 +8,11 @@ (:require [app.common.logging :as log] [app.util.object :as obj] - [beicon.core :as rx] + [app.util.timers :as tm] + [beicon.v2.core :as rx] + [beicon.v2.operators :as rxo] [okulary.core :as l] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (log/set-level! :info) @@ -41,7 +43,7 @@ (when (and *debug-events* (ptk/event? e) (not (debug-exclude-events (ptk/type e)))) - (.log js/console (str "[stream]: " (ptk/repr-event e)) ))))) + (.log js/console (str "[stream]: " (ptk/repr-event e))))))) (defonce state (ptk/store {:resolve ptk/resolve @@ -56,25 +58,25 @@ (defonce last-events (let [buffer (atom []) - allowed #{:app.main.data.workspace/initialize-page - :app.main.data.workspace/finalize-page - :app.main.data.workspace/initialize-file - :app.main.data.workspace/finalize-file}] + omitset #{:potok.v2.core/undefined + :app.main.data.workspace.persistence/update-persistence-status + :app.main.data.websocket/send-message + :app.main.data.workspace.notifications/handle-pointer-send + :app.util.router/assign-exception}] (->> (rx/merge (->> stream (rx/filter (ptk/type? :app.main.data.workspace.changes/commit-changes)) - (rx/map #(-> % deref :hint-origin str)) - (rx/dedupe)) - (->> stream - (rx/map ptk/type) - (rx/filter #(contains? allowed %)) - (rx/map str))) + (rx/map #(-> % deref :hint-origin))) + (rx/map ptk/type stream)) + (rx/filter #(not (contains? omitset %))) + (rx/map str) + (rx/pipe (rxo/distinct-contiguous)) (rx/scan (fn [buffer event] (cond-> (conj buffer event) - (> (count buffer) 20) + (> (count buffer) 50) (pop))) #queue []) - (rx/subs #(reset! buffer (vec %)))) + (rx/subs! #(reset! buffer (vec %)))) buffer)) (defn emit! @@ -86,6 +88,10 @@ (apply ptk/emit! state (cons event events)) nil)) +(defn async-emit! + [& params] + (tm/schedule #(apply emit! params))) + (defonce ongoing-tasks (l/atom #{})) (add-watch ongoing-tasks ::ongoing-tasks diff --git a/frontend/src/app/main/streams.cljs b/frontend/src/app/main/streams.cljs index a4dc59d699..d6ebf074b7 100644 --- a/frontend/src/app/main/streams.cljs +++ b/frontend/src/app/main/streams.cljs @@ -11,96 +11,46 @@ [app.main.store :as st] [app.util.globals :as globals] [app.util.keyboard :as kbd] - [beicon.core :as rx])) + [app.util.mouse :as mse] + [beicon.v2.core :as rx] + [beicon.v2.operators :as rxo])) ;; --- User Events -(defrecord KeyboardEvent [type key shift ctrl alt meta editing]) - -(defn keyboard-event? - [v] - (instance? KeyboardEvent v)) - -(defn key-up? - [v] - (and (keyboard-event? v) - (= :up (:type v)))) - -(defn key-down? - [v] - (and (keyboard-event? v) - (= :down (:type v)))) - -(defrecord MouseEvent [type ctrl shift alt meta]) - -(defn mouse-event? - [v] - (instance? MouseEvent v)) - -(defn mouse-down? - [v] - (and (mouse-event? v) - (= :down (:type v)))) - -(defn mouse-up? - [v] - (and (mouse-event? v) - (= :up (:type v)))) - -(defn mouse-click? - [v] - (and (mouse-event? v) - (= :click (:type v)))) - -(defn mouse-double-click? - [v] - (and (mouse-event? v) - (= :double-click (:type v)))) - -(defrecord PointerEvent [source pt ctrl shift alt meta]) - -(defn pointer-event? - [v] - (instance? PointerEvent v)) - -(defrecord ScrollEvent [point]) - -(defn scroll-event? - [v] - (instance? ScrollEvent v)) - (defn interaction-event? [event] - (or (keyboard-event? event) - (mouse-event? event))) + (or ^boolean (kbd/keyboard-event? event) + ^boolean (mse/mouse-event? event))) ;; --- Derived streams +(defonce ^:private pointer + (->> st/stream + (rx/filter mse/pointer-event?) + (rx/share))) + (defonce mouse-position (let [sub (rx/behavior-subject nil) - ob (->> st/stream - (rx/filter pointer-event?) - (rx/filter #(= :viewport (:source %))) - (rx/map :pt))] - (rx/subscribe-with ob sub) + ob (->> pointer + (rx/filter #(= :viewport (mse/get-pointer-source %))) + (rx/map mse/get-pointer-position))] + (rx/sub! ob sub) sub)) (defonce mouse-position-ctrl (let [sub (rx/behavior-subject nil) - ob (->> st/stream - (rx/filter pointer-event?) - (rx/map :ctrl) - (rx/dedupe))] - (rx/subscribe-with ob sub) + ob (->> pointer + (rx/map mse/get-pointer-ctrl-mod) + (rx/pipe (rxo/distinct-contiguous)))] + (rx/sub! ob sub) sub)) (defonce mouse-position-meta (let [sub (rx/behavior-subject nil) - ob (->> st/stream - (rx/filter pointer-event?) - (rx/map :meta) - (rx/dedupe))] - (rx/subscribe-with ob sub) + ob (->> pointer + (rx/map mse/get-pointer-meta-mod) + (rx/pipe (rxo/distinct-contiguous)))] + (rx/sub! ob sub) sub)) (defonce mouse-position-mod @@ -110,73 +60,84 @@ (defonce mouse-position-shift (let [sub (rx/behavior-subject nil) - ob (->> st/stream - (rx/filter pointer-event?) - (rx/map :shift) - (rx/dedupe))] - (rx/subscribe-with ob sub) + ob (->> pointer + (rx/map mse/get-pointer-shift-mod) + (rx/pipe (rxo/distinct-contiguous)))] + (rx/sub! ob sub) sub)) (defonce mouse-position-alt (let [sub (rx/behavior-subject nil) - ob (->> st/stream - (rx/filter pointer-event?) - (rx/map :alt) - (rx/dedupe))] - (rx/subscribe-with ob sub) + ob (->> pointer + (rx/map mse/get-pointer-alt-mod) + (rx/pipe (rxo/distinct-contiguous)))] + (rx/sub! ob sub) sub)) - -(defonce window-blur +(defonce ^:private window-blur (->> (rx/from-event globals/window "blur") + (rx/map (constantly false)) + (rx/share))) + +(defonce keyboard + (->> st/stream + (rx/filter kbd/keyboard-event?) (rx/share))) (defonce keyboard-alt (let [sub (rx/behavior-subject nil) - ob (->> (rx/merge - (->> st/stream - (rx/filter keyboard-event?) - (rx/filter kbd/alt-key?) - (rx/map #(= :down (:type %)))) - ;; Fix a situation caused by using `ctrl+alt` kind of shortcuts, - ;; that makes keyboard-alt stream registering the key pressed but - ;; on blurring the window (unfocus) the key down is never arrived. - (->> window-blur - (rx/map (constantly false)))) - (rx/dedupe))] - (rx/subscribe-with ob sub) + ob (->> keyboard + (rx/filter kbd/alt-key?) + (rx/map kbd/key-down-event?) + ;; Fix a situation caused by using `ctrl+alt` kind of + ;; shortcuts, that makes keyboard-alt stream + ;; registering the key pressed but on blurring the + ;; window (unfocus) the key down is never arrived. + (rx/merge window-blur) + (rx/pipe (rxo/distinct-contiguous)))] + (rx/sub! ob sub) sub)) (defonce keyboard-ctrl (let [sub (rx/behavior-subject nil) - ob (->> (rx/merge - (->> st/stream - (rx/filter keyboard-event?) - (rx/filter kbd/ctrl-key?) - (rx/map #(= :down (:type %)))) - ;; Fix a situation caused by using `ctrl+alt` kind of shortcuts, - ;; that makes keyboard-alt stream registering the key pressed but - ;; on blurring the window (unfocus) the key down is never arrived. - (->> window-blur - (rx/map (constantly false)))) - (rx/dedupe))] - (rx/subscribe-with ob sub) + ob (->> keyboard + (rx/filter kbd/ctrl-key?) + (rx/map kbd/key-down-event?) + ;; Fix a situation caused by using `ctrl+alt` kind of + ;; shortcuts, that makes keyboard-alt stream + ;; registering the key pressed but on blurring the + ;; window (unfocus) the key down is never arrived. + (rx/merge window-blur) + (rx/pipe (rxo/distinct-contiguous)))] + (rx/sub! ob sub) + sub)) + +(defonce keyboard-shift + (let [sub (rx/behavior-subject nil) + ob (->> keyboard + (rx/filter kbd/shift-key?) + (rx/map kbd/key-down-event?) + ;; Fix a situation caused by using `ctrl+alt` kind of + ;; shortcuts, that makes keyboard-alt stream + ;; registering the key pressed but on blurring the + ;; window (unfocus) the key down is never arrived. + (rx/merge window-blur) + (rx/pipe (rxo/distinct-contiguous)))] + (rx/sub! ob sub) sub)) (defonce keyboard-meta (let [sub (rx/behavior-subject nil) - ob (->> (rx/merge - (->> st/stream - (rx/filter keyboard-event?) - (rx/filter kbd/meta-key?) - (rx/map #(= :down (:type %)))) - ;; Fix a situation caused by using `ctrl+alt` kind of shortcuts, - ;; that makes keyboard-alt stream registering the key pressed but - ;; on blurring the window (unfocus) the key down is never arrived. - (->> window-blur - (rx/map (constantly false)))) - (rx/dedupe))] - (rx/subscribe-with ob sub) + ob (->> keyboard + (rx/filter kbd/meta-key?) + (rx/map kbd/key-down-event?) + ;; Fix a situation caused by using `ctrl+alt` kind of + ;; shortcuts, that makes keyboard-alt stream + ;; registering the key pressed but on blurring the + ;; window (unfocus) the key down is never arrived. + (rx/merge window-blur) + (rx/pipe (rxo/distinct-contiguous)))] + (rx/sub! ob sub) sub)) (defonce keyboard-mod @@ -186,33 +147,15 @@ (defonce keyboard-space (let [sub (rx/behavior-subject nil) - ob (->> st/stream - (rx/filter keyboard-event?) + ob (->> keyboard (rx/filter kbd/space?) - (rx/filter (comp not kbd/editing?)) - (rx/map #(= :down (:type %))) - (rx/dedupe))] - (rx/subscribe-with ob sub) - sub)) - -(defonce keyboard-z - (let [sub (rx/behavior-subject nil) - ob (->> st/stream - (rx/filter keyboard-event?) - (rx/filter kbd/z?) - (rx/filter (comp not kbd/editing?)) - (rx/map #(= :down (:type %))) - (rx/dedupe))] - (rx/subscribe-with ob sub) - sub)) - -(defonce keyboard-shift - (let [sub (rx/behavior-subject nil) - ob (->> st/stream - (rx/filter keyboard-event?) - (rx/filter kbd/shift-key?) - (rx/filter (comp not kbd/editing?)) - (rx/map #(= :down (:type %))) - (rx/dedupe))] - (rx/subscribe-with ob sub) + (rx/filter (complement kbd/editing-event?)) + (rx/map kbd/key-down-event?) + ;; Fix a situation caused by using `ctrl+alt` kind of + ;; shortcuts, that makes keyboard-alt stream + ;; registering the key pressed but on blurring the + ;; window (unfocus) the key down is never arrived. + (rx/merge window-blur) + (rx/pipe (rxo/distinct-contiguous)))] + (rx/sub! ob sub) sub)) diff --git a/frontend/src/app/main/style.clj b/frontend/src/app/main/style.clj index 80cb4f7c23..a1870314af 100644 --- a/frontend/src/app/main/style.clj +++ b/frontend/src/app/main/style.clj @@ -7,25 +7,143 @@ (ns app.main.style "A fonts loading macros." (:require - [app.common.data :as d] - [clojure.data.json :as json])) + [app.common.exceptions :as ex] + [clojure.core :as c] + [clojure.data.json :as json] + [clojure.java.io :as io] + [cuerdas.core :as str] + [rumext.v2.compiler :as mfu])) + +;; Should match with the `ROOT_NAME` constant in gulpfile.js +(def ROOT-NAME "app") + +(def ^:dynamic *css-prefix* nil) + +(defn get-prefix + ;; Calculates the css-modules prefix given the filename + ;; should be the same as the calculation inside the `gulpfile.js` + [fname] + (let [file (io/file fname) + parts + (->> (str/split (.getParent file) #"/") + (drop-while #(not= % ROOT-NAME)) + (rest) + (str/join "_"))] + (str parts "_" (subs (.getName file) 0 (- (count (.getName file)) 5)) "__"))) + +(def ^:private xform-css + (keep (fn [k] + (cond + (keyword? k) + (let [knm (name k) + kns (namespace k)] + (case kns + "global" knm + (str *css-prefix* knm))) + + (string? k) + k)))) + +(defmacro css* + "Just coerces all params to strings and concats them with + space. Used mainly to set a set of classes together." + [& selectors] + (->> selectors + (map name) + (interpose " ") + (apply str))) + +(defn- read-json-file + [path] + (or (ex/ignoring (-> (slurp (io/resource path)) + (json/read-str :key-fn keyword))) + {})) (defmacro css - [selector] - (let [;; Get the associated styles will be module.cljs => module.css.json - filename (:file (meta *ns*)) - styles-file (str "./src/" (subs filename 0 (- (count filename) 4)) "css.json") - data (-> (slurp styles-file) - (json/read-str)) - result (get data (d/name selector))] - `~result)) + "Uses a css-modules defined data for real class lookup, then concat + all classes with space in the same way as `css*`." + [& selectors] + (let [fname (-> *ns* meta :file) + prefix (get-prefix fname)] + (if (symbol? (first selectors)) + `(if ~(with-meta (first selectors) {:tag 'boolean}) + (css* ~@(binding [*css-prefix* prefix] + (into [] xform-css (rest selectors)))) + (css* ~@(rest selectors))) + `(css* ~@(binding [*css-prefix* prefix] + (into [] xform-css selectors)))))) (defmacro styles [] - (let [;; Get the associated styles will be module.cljs => module.css.json - filename (:file (meta *ns*)) - styles-file (str "./src/" (subs filename 0 (- (count filename) 4)) "css.json") - data (-> (slurp styles-file) - (json/read-str)) - data (into {} (map (fn [[k v]] [(keyword k) v])) data)] - `~data)) \ No newline at end of file + ;; Get the associated styles will be module.cljs => module.css.json + (let [fname (-> *ns* meta :file) + path (str (subs fname 0 (- (count fname) 4)) "css.json")] + (read-json-file path))) + +(def ^:private xform-css-case + (comp + (partition-all 2) + (keep (fn [[k v]] + (let [cls (cond + (keyword? k) + (let [knm (name k) + kns (namespace k)] + (case kns + "global" knm + (str *css-prefix* knm))) + + (string? k) + k)] + (when cls + (cond + (true? v) cls + (false? v) nil + :else `(if ~v ~cls "")))))) + (interpose " "))) + +;; A macro that simplifies setting up classes using css-modules and enhaces the +;; migration process from the old approach. +;; +;; Using this as example: +;; +;; (stl/css-case new-css-system +;; :left-settings-bar true +;; :global/two-row (<= size 300)) +;; +;; The first argument to the `css-case` macro is optional an if you don't +;; provide it, it will behave in the same ways as if the `new-css-system` has +;; value of `true`. +;; +;; The non-namespaces keywords passed are treated conditionally on the +;; `new-css-system` value. If is `true`, it will perform a lookup on modules for +;; corresponding (hashed) class-name; if no class name is found, the keyword +;; will be stringigied and used as-is (with no changes). If the `new-css-system` +;; is false, it will perform the same operation as if no class is found on +;; modules (leaving it as string with no modification). +;; +;; Later, we have two modifiers (namespaces): `global` which specifies +;; explicitly that no modules lookup should be performed; and `old-css` which +;; only puts the class if `new-css-system` is `false`. +;; +;; NOTE: the same behavior applies to the `css` macro + +(defmacro css-case + [& params] + (let [fname (-> *ns* meta :file) + prefix (get-prefix fname)] + + (if (symbol? (first params)) + `(if ~(with-meta (first params) {:tag 'boolean}) + ~(binding [*css-prefix* prefix] + (-> (into [] xform-css-case (rest params)) + (mfu/compile-concat :safe? false))) + ~(-> (into [] xform-css-case (rest params)) + (mfu/compile-concat :safe? false))) + `~(binding [*css-prefix* prefix] + (-> (into [] xform-css-case params) + (mfu/compile-concat :safe? false)))))) + +(defmacro css-case* + [& params] + (-> (into [] xform-css-case params) + (mfu/compile-concat :safe? false))) diff --git a/frontend/src/app/main/thumbnail_renderer.cljs b/frontend/src/app/main/thumbnail_renderer.cljs deleted file mode 100644 index 485bcfdad5..0000000000 --- a/frontend/src/app/main/thumbnail_renderer.cljs +++ /dev/null @@ -1,93 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.thumbnail-renderer - "A main entry point for the thumbnail renderer API interface. - - This ns is responsible to provide an API for create thumbnail - renderer iframes and interact with them using asyncrhonous - messages." - (:require - [app.common.data.macros :as dm] - [app.common.uuid :as uuid] - [app.config :as cf] - [app.util.dom :as dom] - [beicon.core :as rx] - [cuerdas.core :as str])) - -(defonce ready? false) -(defonce queue #js []) -(defonce instance nil) -(defonce msgbus (rx/subject)) -(defonce origin - (dm/str (assoc cf/thumbnail-renderer-uri :path "/thumbnail-renderer.html"))) - -(declare send-message!) - -(defn- process-queued-messages! - [] - (loop [message (.shift ^js queue)] - (when (some? message) - (send-message! message) - (recur (.shift ^js queue))))) - -(defn- on-message - "Handles a message from the thumbnail renderer." - [event] - (let [evorigin (unchecked-get event "origin") - evdata (unchecked-get event "data")] - - (when (and (object? evdata) (str/starts-with? origin evorigin)) - (let [scope (unchecked-get evdata "scope") - type (unchecked-get evdata "type")] - (when (= "penpot/thumbnail-renderer" scope) - (when (= type "ready") - (set! ready? true) - (process-queued-messages!)) - (rx/push! msgbus evdata)))))) - -(defn- send-message! - "Sends a message to the thumbnail renderer." - [message] - (let [window (.-contentWindow ^js instance)] - (.postMessage ^js window message origin))) - -(defn- queue-message! - "Queues a message to be sent to the thumbnail renderer when it's ready." - [message] - (.push ^js queue message)) - -(defn render - "Renders a thumbnail." - [{:keys [data styles width] :as params}] - (let [id (dm/str (uuid/next)) - payload #js {:data data :styles styles :width width} - message #js {:id id - :scope "penpot/thumbnail-renderer" - :payload payload}] - - (if ^boolean ready? - (send-message! message) - (queue-message! message)) - - (->> msgbus - (rx/filter #(= id (unchecked-get % "id"))) - (rx/mapcat (fn [msg] - (case (unchecked-get msg "type") - "success" (rx/of (unchecked-get msg "payload")) - "failure" (rx/throw (js/Error. (unchecked-get msg "payload")))))) - (rx/take 1)))) - -(defn init! - "Initializes the thumbnail renderer." - [] - (let [iframe (dom/create-element "iframe")] - (dom/set-attribute! iframe "src" origin) - (dom/set-attribute! iframe "hidden" true) - (dom/append-child! js/document.body iframe) - (.addEventListener js/window "message" on-message) - (set! instance iframe) - )) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 048c6cc9a6..9c7e3110c3 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -9,25 +9,38 @@ [app.config :as cf] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.auth :refer [auth]] - [app.main.ui.auth.verify-token :refer [verify-token]] [app.main.ui.context :as ctx] [app.main.ui.cursors :as c] - [app.main.ui.dashboard :refer [dashboard]] [app.main.ui.debug.components-preview :as cm] + [app.main.ui.frame-preview :as frame-preview] [app.main.ui.icons :as i] [app.main.ui.messages :as msgs] - [app.main.ui.onboarding] - [app.main.ui.onboarding.questions] - [app.main.ui.releases] - [app.main.ui.settings :as settings] + [app.main.ui.onboarding :refer [onboarding-modal]] + [app.main.ui.releases :refer [release-notes-modal]] [app.main.ui.static :as static] - [app.main.ui.viewer :as viewer] - [app.main.ui.workspace :as workspace] [app.util.dom :as dom] + [app.util.i18n :refer [tr]] [app.util.router :as rt] [rumext.v2 :as mf])) +(def auth-page + (mf/lazy-component app.main.ui.auth/auth)) + +(def verify-token-page + (mf/lazy-component app.main.ui.auth.verify-token/verify-token)) + +(def viewer-page + (mf/lazy-component app.main.ui.viewer/viewer)) + +(def dashboard-page + (mf/lazy-component app.main.ui.dashboard/dashboard)) + +(def settings-page + (mf/lazy-component app.main.ui.settings/settings)) + +(def workspace-page + (mf/lazy-component app.main.ui.workspace/workspace)) + (mf/defc on-main-error [{:keys [error] :as props}] (mf/with-effect @@ -35,7 +48,8 @@ [:span "Internal application error"]) (mf/defc main-page - {::mf/wrap [#(mf/catch % {:fallback on-main-error})]} + {::mf/wrap [#(mf/catch % {:fallback on-main-error})] + ::mf/props :obj} [{:keys [route profile]}] (let [{:keys [data params]} route] [:& (mf/provider ctx/current-route) {:value route} @@ -46,17 +60,17 @@ :auth-register-success :auth-recovery-request :auth-recovery) - [:& auth {:route route}] + [:? [:& auth-page {:route route}]] :auth-verify-token - [:& verify-token {:route route}] + [:? [:& verify-token-page {:route route}]] (:settings-profile :settings-password :settings-options :settings-feedback :settings-access-tokens) - [:& settings/settings {:route route}] + [:? [:& settings-page {:route route}]] :debug-icons-preview (when *assert* @@ -66,11 +80,6 @@ [:h1 "Icons"] [:& i/debug-icons-preview]]) - :debug-components-preview - [:div.debug-preview - [:h1 "Components preview"] - [:& cm/components-preview]] - (:dashboard-search :dashboard-projects :dashboard-files @@ -81,57 +90,71 @@ :dashboard-team-invitations :dashboard-team-webhooks :dashboard-team-settings) - - [:* - #_[:div.modal-wrapper - #_[:& app.main.ui.releases/release-notes-modal {:version "1.16"}] - #_[:& app.main.ui.onboarding/onboarding-templates-modal] - #_[:& app.main.ui.onboarding/onboarding-modal] - #_[:& app.main.ui.onboarding/onboarding-team-modal]] - (when-let [props (some-> profile (get :props {}))] + [:? + #_[:& app.main.ui.releases/release-notes-modal {:version "1.19"}] + #_[:& app.main.ui.onboarding/onboarding-templates-modal] + #_[:& app.main.ui.onboarding/onboarding-modal] + #_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal] + (when-let [props (get profile :props)] (cond - (and (not (:onboarding-questions-answered props false)) - (not (:onboarding-viewed props false))) - [:& app.main.ui.onboarding.questions/questions] + (and (not (:onboarding-viewed props)) + (contains? cf/flags :onboarding)) + [:& onboarding-modal {}] - (not (:onboarding-viewed props)) - [:& app.main.ui.onboarding/onboarding-modal {}] - - (and (:onboarding-viewed props) + (and (contains? cf/flags :onboarding) + (:onboarding-viewed props) (not= (:release-notes-viewed props) (:main cf/version)) (not= "0.0" (:main cf/version))) - [:& app.main.ui.releases/release-notes-modal {:version (:main cf/version)}])) + [:& release-notes-modal {:version (:main cf/version)}])) - [:& dashboard {:route route :profile profile}]] + [:& dashboard-page {:route route :profile profile}]] :viewer (let [{:keys [query-params path-params]} route - {:keys [index share-id section page-id interactions-mode frame-id] :or {section :interactions interactions-mode :show-on-click}} query-params + {:keys [index share-id section page-id interactions-mode frame-id] + :or {section :interactions interactions-mode :show-on-click}} query-params {:keys [file-id]} path-params] - (if (:token query-params) - [:& viewer/breaking-change-notice] - [:& viewer/viewer-page {:page-id page-id - :file-id file-id - :section section - :index index - :share-id share-id - :interactions-mode (keyword interactions-mode) - :interactions-show? (case (keyword interactions-mode) - :hide false - :show true - :show-on-click false) - :frame-id frame-id}])) + [:? {} + (if (:token query-params) + [:> static/error-container {} + [:div.image i/detach] + [:div.main-message (tr "viewer.breaking-change.message")] + [:div.desc-message (tr "viewer.breaking-change.description")]] + + [:& viewer-page + {:page-id page-id + :file-id file-id + :section section + :index index + :share-id share-id + :interactions-mode (keyword interactions-mode) + :interactions-show? (case (keyword interactions-mode) + :hide false + :show true + :show-on-click false) + :frame-id frame-id}])]) :workspace (let [project-id (some-> params :path :project-id uuid) file-id (some-> params :path :file-id uuid) page-id (some-> params :query :page-id uuid) layout (some-> params :query :layout keyword)] - [:& workspace/workspace {:project-id project-id - :file-id file-id - :page-id page-id - :layout-name layout - :key file-id}]) + [:? {} + [:& workspace-page {:project-id project-id + :file-id file-id + :page-id page-id + :layout-name layout + :key file-id}]]) + + + :debug-components-preview + [:div.debug-preview + [:h1 "Components preview"] + [:& cm/components-preview]] + + :frame-preview + [:& frame-preview/frame-preview] + nil)])) (mf/defc app @@ -149,6 +172,6 @@ (if edata [:& static/exception-page {:data edata}] [:* - [:& msgs/notifications] + [:& msgs/notifications-hub] (when route [:& main-page {:route route :profile profile}])])]])) diff --git a/frontend/src/app/main/ui/alert.cljs b/frontend/src/app/main/ui/alert.cljs index 1acb30eb88..821b97cee1 100644 --- a/frontend/src/app/main/ui/alert.cljs +++ b/frontend/src/app/main/ui/alert.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.alert + (:require-macros [app.main.style :as stl]) (:require [app.main.data.modal :as modal] [app.main.store :as st] @@ -25,6 +26,7 @@ hint accept-label accept-style] :as props}] + (let [on-accept (or on-accept identity) message (or message (tr "ds.alert-title")) accept-label (or accept-label (tr "ds.alert-ok")) @@ -47,29 +49,26 @@ (on-accept props)))] (->> (events/listen js/document "keydown" on-keydown) (partial events/unlistenByKey)))) + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} title] + [:button {:class (stl/css :modal-close-btn) + :on-click accept-fn} i/close]] - [:div.modal-overlay - [:div.modal-container.alert-dialog - [:div.modal-header - [:div.modal-header-title - [:h2 title]] - [:div.modal-close-button - {:on-click accept-fn} i/close]] - - [:div.modal-content + [:div {:class (stl/css :modal-content)} (when (and (string? message) (not= message "")) - [:h3 message]) + [:h3 {:class (stl/css :modal-msg)} message]) (when (and (string? scd-message) (not= scd-message "")) - [:h3 scd-message]) + [:h3 {:class (stl/css :modal-scd-msg)} scd-message]) (when (string? hint) - [:p hint])] + [:p {:class (stl/css :modal-hint)} hint])] - [:div.modal-footer - [:div.action-buttons - [:input.accept-button - {:class (dom/classnames - :danger (= accept-style :danger) - :primary (= accept-style :primary)) - :type "button" - :value accept-label - :on-click accept-fn}]]]]])) + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:input {:class (stl/css-case :accept-btn true + :danger (= accept-style :danger) + :primary (= accept-style :primary)) + :type "button" + :value accept-label + :on-click accept-fn}]]]]])) diff --git a/frontend/src/app/main/ui/alert.scss b/frontend/src/app/main/ui/alert.scss new file mode 100644 index 0000000000..33e202118d --- /dev/null +++ b/frontend/src/app/main/ui/alert.scss @@ -0,0 +1,62 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; + &.transparent { + background-color: transparent; + } +} +.modal-container { + @extend .modal-container-base; +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include headlineMediumTypography; + color: var(--modal-title-foreground-color); +} + +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content { + @include bodyLargeTypography; + margin-bottom: $s-24; +} + +.modal-hint { + @include bodyLargeTypography; +} + +.action-buttons { + @extend .modal-action-btns; +} + +.cancel-button { + @extend .modal-cancel-btn; +} + +.accept-btn { + @extend .modal-accept-btn; + &.danger { + @extend .modal-danger-btn; + } +} + +.modal-scd-msg, +.modal-subtitle, +.modal-msg { + @include bodyLargeTypography; + color: var(--modal-text-foreground-color); + line-height: 1.5; +} diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs index 92bfbcd792..218fc21ce1 100644 --- a/frontend/src/app/main/ui/auth.cljs +++ b/frontend/src/app/main/ui/auth.cljs @@ -5,7 +5,9 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.auth + (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.config :as cf] [app.main.ui.auth.login :refer [login-page]] [app.main.ui.auth.recovery :refer [recovery-page]] @@ -23,32 +25,35 @@ show-privacy? (some? cf/privacy-policy-uri)] (when show-all? - [:div.terms-login + [:div {:class (stl/css :terms-login)} (when show-terms? - [:a {:href cf/terms-of-service-uri :target "_blank"} (tr "auth.terms-of-service")]) + [:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :auth-link)} + (tr "auth.terms-of-service")]) (when show-all? - [:span (tr "labels.and")]) + [:span {:class (stl/css :and-text)} + (dm/str " " (tr "labels.and") " ")]) (when show-privacy? - [:a {:href cf/privacy-policy-uri :target "_blank"} (tr "auth.privacy-policy")])]))) + [:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :auth-link)} + (tr "auth.privacy-policy")])]))) (mf/defc auth - [{:keys [route] :as props}] - (let [section (get-in route [:data :name]) + {::mf/props :obj} + [{:keys [route]}] + (let [section (dm/get-in route [:data :name]) params (:query-params route)] - (mf/use-effect - #(dom/set-html-title (tr "title.default"))) + (mf/with-effect [] + (dom/set-html-title (tr "title.default"))) - [:main.auth - [:section.auth-sidebar - [:a.logo {:href "#/"} - [:span {:aria-hidden true} i/logo] - [:span.hidden-name "Home"]] - [:span.tagline (tr "auth.sidebar-tagline")]] + [:main {:class (stl/css :auth-section)} + [:a {:href "#/" :class (stl/css :logo-btn)} i/logo] + [:div {:class (stl/css :login-illustration)} + i/login-illustration] + + [:section {:class (stl/css :auth-content)} - [:section.auth-content (case section :auth-register [:& register-page {:params params}] @@ -68,5 +73,5 @@ :auth-recovery [:& recovery-page {:params params}]) - [:& terms-login {}]]])) - + (when (= section :auth-register) + [:& terms-login])]])) diff --git a/frontend/src/app/main/ui/auth.scss b/frontend/src/app/main/ui/auth.scss new file mode 100644 index 0000000000..81e418e9c6 --- /dev/null +++ b/frontend/src/app/main/ui/auth.scss @@ -0,0 +1,91 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.auth-section { + position: relative; + align-items: center; + background: var(--panel-background-color); + display: grid; + gap: $s-32; + grid-template-columns: repeat(5, 1fr); + height: 100%; + padding: $s-32; + width: 100%; + overflow: auto; + + @media (max-width: 992px) { + display: flex; + justify-content: center; + } +} + +.login-illustration { + display: flex; + justify-content: center; + grid-column: 1 / 4; + width: 40vw; + justify-self: center; + + svg { + width: 100%; + fill: $df-primary; + height: auto; + } + + @media (max-width: 992px) { + display: none; + } +} + +.auth-content { + grid-column: 4 / 6; + display: grid; + grid-template-rows: 1fr auto; + gap: $s-24; + height: fit-content; + max-width: $s-412; + padding-block-end: $s-8; + position: relative; + width: 100%; +} + +.logo-btn { + position: absolute; + top: $s-20; + left: $s-20; + display: flex; + justify-content: flex-start; + width: $s-120; + margin-block-end: $s-52; + + svg { + width: $s-120; + height: $s-40; + fill: var(--main-icon-foreground); + } +} + +.terms-login { + @include bodySmallTypography; + display: flex; + gap: $s-4; + justify-content: center; + width: 100%; +} + +.and-text { + border-bottom: $s-1 solid transparent; + color: var(--title-foreground-color); +} + +.auth-link { + color: var(--link-foreground-color); + &:hover { + text-decoration: underline; + } +} diff --git a/frontend/src/app/main/ui/auth/common.scss b/frontend/src/app/main/ui/auth/common.scss new file mode 100644 index 0000000000..0075c7f144 --- /dev/null +++ b/frontend/src/app/main/ui/auth/common.scss @@ -0,0 +1,151 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.auth-form-wrapper { + width: 100%; + padding-block-end: 0; + display: grid; + gap: $s-24; + form { + display: flex; + flex-direction: column; + gap: $s-12; + } +} + +.separator { + border-color: var(--modal-separator-backogrund-color); + margin: 0; +} + +.auth-title { + @include bigTitleTipography; + color: var(--title-foreground-color-hover); +} + +.auth-subtitle { + @include smallTitleTipography; + color: var(--title-foreground-color); +} + +.auth-tagline { + @include smallTitleTipography; + margin: 0; + color: var(--title-foreground-color); +} + +.form-field { + --input-width: 100%; + --input-height: #{$s-40}; + --input-min-width: 100%; +} + +.buttons-stack { + display: grid; + gap: $s-8; +} + +.login-button, +.login-ldap-button { + @extend .button-primary; + @include uppercaseTitleTipography; + height: $s-40; + width: 100%; +} + +.demo-account, +.go-back { + display: flex; + flex-direction: column; + gap: $s-12; + padding: 0; + border-block-start: none; +} + +.demo-account-link, +.go-back-link { + @extend .button-secondary; + @include uppercaseTitleTipography; + height: $s-40; +} + +.links { + display: grid; + gap: $s-24; +} + +.register, +.account, +.recovery-request { + display: flex; + justify-content: center; + gap: $s-8; + padding: 0; +} + +.register-text, +.account-text, +.recovery-text { + @include smallTitleTipography; + text-align: right; + color: var(--title-foreground-color); +} + +.register-link, +.account-link, +.recovery-link, +.forgot-pass-link { + @include smallTitleTipography; + text-align: left; + background-color: transparent; + border: none; + display: inline; + color: var(--link-foreground-color); + + &:hover { + text-decoration: underline; + } +} + +.forgot-password { + display: flex; + justify-content: flex-end; +} + +.submit-btn, +.register-btn, +.recover-btn { + @extend .button-primary; + @include uppercaseTitleTipography; + height: $s-40; + width: 100%; +} + +.login-btn { + @include smallTitleTipography; + display: flex; + align-items: center; + gap: $s-6; + width: 100%; + border-radius: $br-8; + background-color: var(--button-secondary-background-color-rest); + color: var(--button-foreground-color-focus); + span { + padding-block-start: $s-2; + } + + &:hover { + color: var(--button-foreground-color-focus); + background-color: var(--button-secondary-background-color-hover); + } +} + +.auth-buttons { + display: flex; + gap: $s-8; +} diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index cdb907ded2..79a8c599a5 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -5,12 +5,13 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.auth.login + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.logging :as log] [app.common.spec :as us] [app.config :as cf] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.users :as du] [app.main.repo :as rp] [app.main.store :as st] @@ -18,12 +19,12 @@ [app.main.ui.components.forms :as fm] [app.main.ui.components.link :as lk] [app.main.ui.icons :as i] - [app.main.ui.messages :as msgs] + [app.main.ui.notifications.context-notification :refer [context-notification]] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.keyboard :as k] [app.util.router :as rt] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.spec.alpha :as s] [rumext.v2 :as mf])) @@ -34,23 +35,30 @@ :login-with-gitlab :login-with-oidc])) +(mf/defc demo-warning + {::mf/props :obj} + [] + [:& context-notification + {:type :warning + :content (tr "auth.demo-warning")}]) + (defn- login-with-oidc [event provider params] (dom/prevent-default event) (->> (rp/cmd! :login-with-oidc (assoc params :provider provider)) - (rx/subs (fn [{:keys [redirect-uri] :as rsp}] - (if redirect-uri - (.replace js/location redirect-uri) - (log/error :hint "unexpected response from OIDC method" - :resp (pr-str rsp)))) - (fn [{:keys [type code] :as error}] - (cond - (and (= type :restriction) - (= code :provider-not-configured)) - (st/emit! (dm/error (tr "errors.auth-provider-not-configured"))) + (rx/subs! (fn [{:keys [redirect-uri] :as rsp}] + (if redirect-uri + (.replace js/location redirect-uri) + (log/error :hint "unexpected response from OIDC method" + :resp (pr-str rsp)))) + (fn [{:keys [type code] :as error}] + (cond + (and (= type :restriction) + (= code :provider-not-configured)) + (st/emit! (msg/error (tr "errors.auth-provider-not-configured"))) - :else - (st/emit! (dm/error (tr "errors.generic")))))))) + :else + (st/emit! (msg/error (tr "errors.generic")))))))) (defn- login-with-ldap [event params] @@ -58,21 +66,21 @@ (dom/stop-propagation event) (let [{:keys [on-error]} (meta params)] (->> (rp/cmd! :login-with-ldap params) - (rx/subs (fn [profile] - (if-let [token (:invitation-token profile)] - (st/emit! (rt/nav :auth-verify-token {} {:token token})) - (st/emit! (du/login-from-token {:profile profile})))) - (fn [{:keys [type code] :as error}] - (cond - (and (= type :restriction) - (= code :ldap-not-initialized)) - (st/emit! (dm/error (tr "errors.ldap-disabled"))) + (rx/subs! (fn [profile] + (if-let [token (:invitation-token profile)] + (st/emit! (rt/nav :auth-verify-token {} {:token token})) + (st/emit! (du/login-from-token {:profile profile})))) + (fn [{:keys [type code] :as error}] + (cond + (and (= type :restriction) + (= code :ldap-not-initialized)) + (st/emit! (msg/error (tr "errors.ldap-disabled"))) - (fn? on-error) - (on-error error) + (fn? on-error) + (on-error error) - :else - (st/emit! (dm/error (tr "errors.generic"))))))))) + :else + (st/emit! (msg/error (tr "errors.generic"))))))))) (s/def ::email ::us/email) (s/def ::password ::us/not-empty-string) @@ -91,35 +99,35 @@ (assoc :message (tr "errors.email-invalid")))))) (mf/defc login-form - [{:keys [params on-success-callback] :as props}] + [{:keys [params on-success-callback origin] :as props}] (let [initial (mf/use-memo (mf/deps params) (constantly params)) - error (mf/use-state false) form (fm/use-form :spec ::login-form :validators [handle-error-messages] :initial initial) on-error - (fn [cause] - (cond - (and (= :restriction (:type cause)) - (= :profile-blocked (:code cause))) - (reset! error (tr "errors.profile-blocked")) + (fn [err] + (let [cause (ex-data err)] + (cond + (and (= :restriction (:type cause)) + (= :profile-blocked (:code cause))) + (reset! error (tr "errors.profile-blocked")) - (and (= :restriction (:type cause)) - (= :admin-only-profile (:code cause))) - (reset! error (tr "errors.profile-blocked")) + (and (= :restriction (:type cause)) + (= :admin-only-profile (:code cause))) + (reset! error (tr "errors.profile-blocked")) - (and (= :validation (:type cause)) - (= :wrong-credentials (:code cause))) - (reset! error (tr "errors.wrong-credentials")) + (and (= :validation (:type cause)) + (= :wrong-credentials (:code cause))) + (reset! error (tr "errors.wrong-credentials")) - (and (= :validation (:type cause)) - (= :account-without-password (:code cause))) - (reset! error (tr "errors.wrong-credentials")) + (and (= :validation (:type cause)) + (= :account-without-password (:code cause))) + (reset! error (tr "errors.wrong-credentials")) - :else - (reset! error (tr "errors.generic")))) + :else + (reset! error (tr "errors.generic"))))) on-success-default (fn [data] @@ -149,133 +157,156 @@ (let [params (:clean-data @form)] (login-with-ldap event (with-meta params {:on-error on-error - :on-success on-success})))))] + :on-success on-success}))))) + + on-recovery-request + (mf/use-fn + #(st/emit! (rt/nav :auth-recovery-request)))] + [:* (when-let [message @error] - [:& msgs/inline-banner + [:& context-notification {:type :warning :content message - :on-close #(reset! error nil) :data-test "login-banner" :role "alert"}]) - [:& fm/form {:on-submit on-submit :form form} - [:div.fields-row + [:& fm/form {:on-submit on-submit + :class (stl/css :login-form) + :form form} + [:div {:class (stl/css :fields-row)} [:& fm/input {:name :email :type "email" - :help-icon i/at - :label (tr "auth.email")}]] + :label (tr "auth.email") + :class (stl/css :form-field)}]] - [:div.fields-row + [:div {:class (stl/css :fields-row)} [:& fm/input {:type "password" :name :password - :help-icon i/eye - :label (tr "auth.password")}]] + :label (tr "auth.password") + :class (stl/css :form-field)}]] - [:div.buttons-stack + (when (and (not= origin :viewer) + (or (contains? cf/flags :login) + (contains? cf/flags :login-with-password))) + [:div {:class (stl/css :fields-row :forgot-password)} + [:& lk/link {:action on-recovery-request + :class (stl/css :forgot-pass-link) + :data-test "forgot-password"} + (tr "auth.forgot-password")]]) + + [:div {:class (stl/css :buttons-stack)} (when (or (contains? cf/flags :login) (contains? cf/flags :login-with-password)) - [:& fm/submit-button + [:> fm/submit-button* {:label (tr "auth.login-submit") - :data-test "login-submit"}]) + :data-test "login-submit" + :class (stl/css :login-button)}]) (when (contains? cf/flags :login-with-ldap) - [:& fm/submit-button + [:> fm/submit-button* {:label (tr "auth.login-with-ldap-submit") + :class (stl/css :login-ldap-button) :on-click on-submit-ldap}])]]])) (mf/defc login-buttons [{:keys [params] :as props}] - [:div.auth-buttons - (when (contains? cf/flags :login-with-google) - [:& bl/button-link {:action #(login-with-oidc % :google params) - :icon i/brand-google - :name (tr "auth.login-with-google-submit") - :klass "btn-google-auth"}]) + (let [login-with-google (mf/use-fn (mf/deps params) #(login-with-oidc % :google params)) + login-with-github (mf/use-fn (mf/deps params) #(login-with-oidc % :github params)) + login-with-gitlab (mf/use-fn (mf/deps params) #(login-with-oidc % :gitlab params)) + login-with-oidc (mf/use-fn (mf/deps params) #(login-with-oidc % :oidc params))] - (when (contains? cf/flags :login-with-github) - [:& bl/button-link {:action #(login-with-oidc % :github params) - :icon i/brand-github - :name (tr "auth.login-with-github-submit") - :klass "btn-github-auth"}]) + [:div {:class (stl/css :auth-buttons)} + (when (contains? cf/flags :login-with-google) + [:& bl/button-link {:on-click login-with-google + :icon i/brand-google + :label (tr "auth.login-with-google-submit") + :class (stl/css :login-btn :btn-google-auth)}]) - (when (contains? cf/flags :login-with-gitlab) - [:& bl/button-link {:action #(login-with-oidc % :gitlab params) - :icon i/brand-gitlab - :name (tr "auth.login-with-gitlab-submit") - :klass "btn-gitlab-auth"}]) + (when (contains? cf/flags :login-with-github) + [:& bl/button-link {:on-click login-with-github + :icon i/brand-github + :label (tr "auth.login-with-github-submit") + :class (stl/css :login-btn :btn-github-auth)}]) - (when (contains? cf/flags :login-with-oidc) - [:& bl/button-link {:action #(login-with-oidc % :oidc params) - :icon i/brand-openid - :name (tr "auth.login-with-oidc-submit") - :klass "btn-github-auth"}])]) + (when (contains? cf/flags :login-with-gitlab) + [:& bl/button-link {:on-click login-with-gitlab + :icon i/brand-gitlab + :label (tr "auth.login-with-gitlab-submit") + :class (stl/css :login-btn :btn-gitlab-auth)}]) + + (when (contains? cf/flags :login-with-oidc) + [:& bl/button-link {:on-click login-with-oidc + :icon i/brand-openid + :label (tr "auth.login-with-oidc-submit") + :class (stl/css :login-btn :btn-oidc-auth)}])])) (mf/defc login-button-oidc [{:keys [params] :as props}] - (when (contains? cf/flags :login-with-oidc) - [:div.link-entry.link-oidc - [:a {:tab-index "0" - :on-key-down (fn [event] - (when (k/enter? event) - (login-with-oidc event :oidc params))) - :on-click #(login-with-oidc % :oidc params)} - (tr "auth.login-with-oidc-submit")]])) + (let [login-oidc + (mf/use-fn + (mf/deps params) + (fn [event] + (login-with-oidc event :oidc params))) + + handle-key-down + (mf/use-fn + (fn [event] + (when (k/enter? event) + (login-oidc event))))] + (when (contains? cf/flags :login-with-oidc) + [:button {:tab-index "0" + :class (stl/css :link-entry :link-oidc) + :on-key-down handle-key-down + :on-click login-oidc} + (tr "auth.login-with-oidc-submit")]))) (mf/defc login-methods - [{:keys [params on-success-callback] :as props}] + [{:keys [params on-success-callback origin] :as props}] [:* (when show-alt-login-buttons? [:* - [:span.separator - [:span.line] - [:span.text (tr "labels.continue-with")] - [:span.line]] - [:& login-buttons {:params params}] (when (or (contains? cf/flags :login) (contains? cf/flags :login-with-password) (contains? cf/flags :login-with-ldap)) - [:span.separator - [:span.line] - [:span.text (tr "labels.or")] - [:span.line]])]) + [:hr {:class (stl/css :separator)}])]) (when (or (contains? cf/flags :login) (contains? cf/flags :login-with-password) (contains? cf/flags :login-with-ldap)) - [:& login-form {:params params :on-success-callback on-success-callback}])]) + [:& login-form {:params params :on-success-callback on-success-callback :origin origin}])]) (mf/defc login-page [{:keys [params] :as props}] - [:div.generic-form.login-form - [:div.form-container - [:h1 {:data-test "login-title"} (tr "auth.login-title")] + (let [go-register + (mf/use-fn + #(st/emit! (rt/nav :auth-register {} params)))] - [:& login-methods {:params params}] + [:div {:class (stl/css :auth-form-wrapper)} + [:h1 {:class (stl/css :auth-title) + :data-test "login-title"} (tr "auth.login-account-title")] - [:div.links - (when (or (contains? cf/flags :login) - (contains? cf/flags :login-with-password)) - [:div.link-entry - [:& lk/link {:action #(st/emit! (rt/nav :auth-recovery-request)) - :data-test "forgot-password"} - (tr "auth.forgot-password")]]) + [:p {:class (stl/css :auth-tagline)} + (tr "auth.login-tagline")] - (when (contains? cf/flags :registration) - [:div.link-entry - [:span (tr "auth.register") " "] - [:& lk/link {:action #(st/emit! (rt/nav :auth-register {} params)) - :data-test "register-submit"} - (tr "auth.register-submit")]])] + (when (contains? cf/flags :demo-warning) + [:& demo-warning]) + + [:& login-methods {:params params}] + + [:hr {:class (stl/css :separator)}] + + [:div {:class (stl/css :links)} + (when (contains? cf/flags :registration) + [:div {:class (stl/css :register)} + [:span {:class (stl/css :register-text)} + (tr "auth.register") " "] + [:& lk/link {:action go-register + :class (stl/css :register-link) + :data-test "register-submit"} + (tr "auth.register-submit")]])]])) - (when (contains? cf/flags :demo-users) - [:div.links.demo - [:div.link-entry - [:span (tr "auth.create-demo-profile") " "] - [:& lk/link {:action #(st/emit! (du/create-demo-profile)) - :data-test "demo-account-link"} - (tr "auth.create-demo-account")]]])]]) diff --git a/frontend/src/app/main/ui/auth/login.scss b/frontend/src/app/main/ui/auth/login.scss new file mode 100644 index 0000000000..b0002114f9 --- /dev/null +++ b/frontend/src/app/main/ui/auth/login.scss @@ -0,0 +1,7 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "./common.scss"; diff --git a/frontend/src/app/main/ui/auth/recovery.cljs b/frontend/src/app/main/ui/auth/recovery.cljs index 397edc4d9a..85657eef6b 100644 --- a/frontend/src/app/main/ui/auth/recovery.cljs +++ b/frontend/src/app/main/ui/auth/recovery.cljs @@ -5,9 +5,10 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.auth.recovery + (:require-macros [app.main.style :as stl]) (:require [app.common.spec :as us] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.users :as du] [app.main.store :as st] [app.main.ui.components.forms :as fm] @@ -38,11 +39,11 @@ (defn- on-error [_form _error] - (st/emit! (dm/error (tr "auth.notifications.invalid-token-error")))) + (st/emit! (msg/error (tr "auth.notifications.invalid-token-error")))) (defn- on-success [_] - (st/emit! (dm/info (tr "auth.notifications.password-changed-successfully")) + (st/emit! (msg/info (tr "auth.notifications.password-changed-successfully")) (rt/nav :auth-login))) (defn- on-submit @@ -61,32 +62,38 @@ (fm/validate-not-empty :password-2 (tr "auth.password-not-empty"))] :initial params)] [:& fm/form {:on-submit on-submit + :class (stl/css :recovery-form) :form form} - [:div.fields-row + [:div {:class (stl/css :fields-row)} [:& fm/input {:type "password" :name :password-1 - :label (tr "auth.new-password")}]] + :show-success? true + :label (tr "auth.new-password") + :class (stl/css :form-field)}]] - [:div.fields-row + [:div {:class (stl/css :fields-row)} [:& fm/input {:type "password" :name :password-2 - :label (tr "auth.confirm-password")}]] + :show-success? true + :label (tr "auth.confirm-password") + :class (stl/css :form-field)}]] - [:& fm/submit-button - {:label (tr "auth.recovery-submit")}]])) + [:> fm/submit-button* + {:label (tr "auth.recovery-submit") + :class (stl/css :submit-btn)}]])) ;; --- Recovery Request Page (mf/defc recovery-page [{:keys [params] :as props}] - [:section.generic-form - [:div.form-container - [:h1 "Forgot your password?"] - [:div.subtitle "Please enter your new password"] - [:& recovery-form {:params params}] - - [:div.links - [:div.link-entry - [:a {:on-click #(st/emit! (rt/nav :auth-login))} - (tr "profile.recovery.go-to-login")]]]]]) + [:div {:class (stl/css :auth-form-wrapper)} + [:h1 {:class (stl/css :auth-title)} "Forgot your password?"] + [:div {:class (stl/css :auth-subtitle)} "Please enter your new password"] + [:hr {:class (stl/css :separator)}] + [:& recovery-form {:params params}] + [:div {:class (stl/css :links)} + [:div {:class (stl/css :go-back)} + [:a {:on-click #(st/emit! (rt/nav :auth-login)) + :class (stl/css :go-back-link)} + (tr "profile.recovery.go-to-login")]]]]) diff --git a/frontend/src/app/main/ui/auth/recovery.scss b/frontend/src/app/main/ui/auth/recovery.scss new file mode 100644 index 0000000000..743034faad --- /dev/null +++ b/frontend/src/app/main/ui/auth/recovery.scss @@ -0,0 +1,12 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; +@use "./common.scss"; + +.submit-btn { + margin-top: $s-16; +} diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs index 37e92b2264..b2d116daf6 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.cljs +++ b/frontend/src/app/main/ui/auth/recovery_request.cljs @@ -5,18 +5,18 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.auth.recovery-request + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.spec :as us] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.users :as du] [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.main.ui.components.link :as lk] - [app.main.ui.icons :as i] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.spec.alpha :as s] [rumext.v2 :as mf])) @@ -32,12 +32,12 @@ (mf/defc recovery-form [{:keys [on-success-callback] :as props}] - (let [form (fm/use-form :spec ::recovery-request-form + (let [form (fm/use-form :spec ::recovery-request-form :validators [handle-error-messages] :initial {}) submitted (mf/use-state false) - default-success-finish #(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))) + default-success-finish #(st/emit! (msg/info (tr "auth.notifications.recovery-token-sent"))) on-success (mf/use-callback @@ -49,19 +49,20 @@ on-error (mf/use-callback - (fn [data {:keys [code] :as error}] + (fn [data cause] (reset! submitted false) - (case code - :profile-not-verified - (rx/of (dm/error (tr "auth.notifications.profile-not-verified") {:timeout nil})) + (let [code (-> cause ex-data :code)] + (case code + :profile-not-verified + (rx/of (msg/error (tr "auth.notifications.profile-not-verified"))) - :profile-is-muted - (rx/of (dm/error (tr "errors.profile-is-muted"))) + :profile-is-muted + (rx/of (msg/error (tr "errors.profile-is-muted"))) - :email-has-permanent-bounces - (rx/of (dm/error (tr "errors.email-has-permanent-bounces" (:email data)))) + :email-has-permanent-bounces + (rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email data)))) - (rx/throw error)))) + (rx/throw cause))))) on-submit (mf/use-callback @@ -75,16 +76,18 @@ (st/emit! (du/request-profile-recovery params)))))] [:& fm/form {:on-submit on-submit + :class (stl/css :recovery-request-form) :form form} - [:div.fields-row + [:div {:class (stl/css :fields-row)} [:& fm/input {:name :email :label (tr "auth.email") - :help-icon i/at - :type "text"}]] + :type "text" + :class (stl/css :form-field)}]] - [:& fm/submit-button + [:> fm/submit-button* {:label (tr "auth.recovery-request-submit") - :data-test "recovery-resquest-submit"}]])) + :data-test "recovery-resquest-submit" + :class (stl/css :recover-btn)}]])) ;; --- Recovery Request Page @@ -93,13 +96,15 @@ [{:keys [params on-success-callback go-back-callback] :as props}] (let [default-go-back #(st/emit! (rt/nav :auth-login)) go-back (or go-back-callback default-go-back)] - [:section.generic-form - [:div.form-container - [:h1 (tr "auth.recovery-request-title")] - [:div.subtitle (tr "auth.recovery-request-subtitle")] - [:& recovery-form {:params params :on-success-callback on-success-callback}] - [:div.links - [:div.link-entry - [:& lk/link {:action go-back - :data-test "go-back-link"} - (tr "labels.go-back")]]]]])) + [:div {:class (stl/css :auth-form-wrapper)} + [:h1 {:class (stl/css :auth-title)} (tr "auth.recovery-request-title")] + [:div {:class (stl/css :auth-subtitle)} (tr "auth.recovery-request-subtitle")] + [:hr {:class (stl/css :separator)}] + + [:& recovery-form {:params params :on-success-callback on-success-callback}] + [:hr {:class (stl/css :separator)}] + [:div {:class (stl/css :go-back)} + [:& lk/link {:action go-back + :class (stl/css :go-back-link) + :data-test "go-back-link"} + (tr "labels.go-back")]]])) diff --git a/frontend/src/app/main/ui/auth/recovery_request.scss b/frontend/src/app/main/ui/auth/recovery_request.scss new file mode 100644 index 0000000000..e78e21b6de --- /dev/null +++ b/frontend/src/app/main/ui/auth/recovery_request.scss @@ -0,0 +1,12 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; +@use "./common.scss"; + +.fields-row { + margin-bottom: $s-8; +} diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 01986eccbd..633ac1177c 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -5,11 +5,12 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.auth.register + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.spec :as us] [app.config :as cf] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.users :as du] [app.main.repo :as rp] [app.main.store :as st] @@ -17,19 +18,12 @@ [app.main.ui.components.forms :as fm] [app.main.ui.components.link :as lk] [app.main.ui.icons :as i] - [app.main.ui.messages :as msgs] - [app.util.i18n :refer [tr]] + [app.util.i18n :refer [tr tr-html]] [app.util.router :as rt] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.spec.alpha :as s] [rumext.v2 :as mf])) -(mf/defc demo-warning - [_] - [:& msgs/inline-banner - {:type :warning - :content (tr "auth.demo-warning")}]) - ;; --- PAGE: Register (defn- validate @@ -56,35 +50,35 @@ :opt-un [::invitation-token])) (defn- handle-prepare-register-error - [form {:keys [type code] :as cause}] - (condp = [type code] - [:restriction :registration-disabled] - (st/emit! (dm/error (tr "errors.registration-disabled"))) + [form cause] + (let [{:keys [type code]} (ex-data cause)] + (condp = [type code] + [:restriction :registration-disabled] + (st/emit! (msg/error (tr "errors.registration-disabled"))) - [:restriction :profile-blocked] - (st/emit! (dm/error (tr "errors.profile-blocked"))) + [:restriction :profile-blocked] + (st/emit! (msg/error (tr "errors.profile-blocked"))) - [:validation :email-has-permanent-bounces] - (let [email (get @form [:data :email])] - (st/emit! (dm/error (tr "errors.email-has-permanent-bounces" email)))) + [:validation :email-has-permanent-bounces] + (let [email (get @form [:data :email])] + (st/emit! (msg/error (tr "errors.email-has-permanent-bounces" email)))) - [:validation :email-already-exists] - (swap! form assoc-in [:errors :email] - {:message "errors.email-already-exists"}) + [:validation :email-already-exists] + (swap! form assoc-in [:errors :email] + {:message "errors.email-already-exists"}) - [:validation :email-as-password] - (swap! form assoc-in [:errors :password] - {:message "errors.email-as-password"}) + [:validation :email-as-password] + (swap! form assoc-in [:errors :password] + {:message "errors.email-as-password"}) - (st/emit! (dm/error (tr "errors.generic"))))) + (st/emit! (msg/error (tr "errors.generic")))))) (defn- handle-prepare-register-success [params] (st/emit! (rt/nav :auth-register-validate {} params))) - (mf/defc register-form - [{:keys [params on-success-callback] :as props}] + [{:keys [params on-success-callback]}] (let [initial (mf/use-memo (mf/deps params) (constantly params)) form (fm/use-form :spec ::register-form :validators [validate @@ -98,84 +92,79 @@ (on-success-callback p))) on-submit - (mf/use-callback + (mf/use-fn (fn [form _event] (reset! submitted? true) (let [cdata (:clean-data @form)] (->> (rp/cmd! :prepare-register-profile cdata) (rx/map #(merge % params)) (rx/finalize #(reset! submitted? false)) - (rx/subs + (rx/subs! on-success (partial handle-prepare-register-error form))))))] - [:& fm/form {:on-submit on-submit - :form form} - [:div.fields-row - [:& fm/input {:type "email" + [:& fm/form {:on-submit on-submit :form form} + [:div {:class (stl/css :fields-row)} + [:& fm/input {:type "text" :name :email - :help-icon i/at :label (tr "auth.email") - :data-test "email-input"}]] - [:div.fields-row + :data-test "email-input" + :show-success? true + :class (stl/css :form-field)}]] + [:div {:class (stl/css :fields-row)} [:& fm/input {:name :password :hint (tr "auth.password-length-hint") :label (tr "auth.password") - :type "password"}]] + :show-success? true + :type "password" + :class (stl/css :form-field)}]] - [:& fm/submit-button + [:> fm/submit-button* {:label (tr "auth.register-submit") :disabled @submitted? - :data-test "register-form-submit"}]])) + :data-test "register-form-submit" + :class (stl/css :register-btn)}]])) (mf/defc register-methods - [{:keys [params on-success-callback] :as props}] + {::mf/props :obj} + [{:keys [params on-success-callback]}] [:* (when login/show-alt-login-buttons? - [:* - [:span.separator - [:span.line] - [:span.text (tr "labels.continue-with")] - [:span.line]] - - [:& login/login-buttons {:params params}] - - (when (or (contains? cf/flags :login) - (contains? cf/flags :login-with-ldap)) - [:span.separator - [:span.line] - [:span.text (tr "labels.or")] - [:span.line]])]) - + [:& login/login-buttons {:params params}]) + [:hr {:class (stl/css :separator)}] [:& register-form {:params params :on-success-callback on-success-callback}]]) (mf/defc register-page - [{:keys [params] :as props}] - [:div.form-container - - [:h1 {:data-test "registration-title"} (tr "auth.register-title")] - [:div.subtitle (tr "auth.register-subtitle")] + {::mf/props :obj} + [{:keys [params]}] + [:div {:class (stl/css :auth-form-wrapper)} + [:h1 {:class (stl/css :auth-title) + :data-test "registration-title"} (tr "auth.register-title")] + [:p {:class (stl/css :auth-tagline)} + (tr "auth.login-tagline")] (when (contains? cf/flags :demo-warning) - [:& demo-warning]) + [:& login/demo-warning]) [:& register-methods {:params params}] - [:div.links - [:div.link-entry - [:span (tr "auth.already-have-account") " "] - + [:div {:class (stl/css :links)} + [:div {:class (stl/css :account)} + [:span {:class (stl/css :account-text)} (tr "auth.already-have-account") " "] [:& lk/link {:action #(st/emit! (rt/nav :auth-login {} params)) + :class (stl/css :account-link) :data-test "login-here-link"} (tr "auth.login-here")]] (when (contains? cf/flags :demo-users) - [:div.link-entry - [:span (tr "auth.create-demo-profile") " "] - [:& lk/link {:action #(st/emit! (du/create-demo-profile))} - (tr "auth.create-demo-account")]])]]) + [:* + [:hr {:class (stl/css :separator)}] + [:div {:class (stl/css :demo-account)} + [:& lk/link {:action #(st/emit! (du/create-demo-profile)) + :class (stl/css :demo-account-link)} + (tr "auth.create-demo-account")]]])]]) ;; --- PAGE: register validation @@ -188,7 +177,7 @@ (do (println (:explain error)) - (st/emit! (dm/error (tr "errors.generic")))))) + (st/emit! (msg/error (tr "errors.generic")))))) (defn- handle-register-success [data] @@ -218,7 +207,7 @@ ::accept-newsletter-subscription]))) (mf/defc register-validate-form - [{:keys [params on-success-callback] :as props}] + [{:keys [params on-success-callback]}] (let [form (fm/use-form :spec ::register-validate-form :validators [(fm/validate-not-empty :fullname (tr "auth.name.not-all-space")) (fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long"))] @@ -231,57 +220,67 @@ (on-success-callback (:email p)))) on-submit - (mf/use-callback + (mf/use-fn (fn [form _event] (reset! submitted? true) (let [params (:clean-data @form)] (->> (rp/cmd! :register-profile params) (rx/finalize #(reset! submitted? false)) - (rx/subs on-success - (partial handle-register-error form))))))] + (rx/subs! on-success + (partial handle-register-error form))))))] - [:& fm/form {:on-submit on-submit - :form form} - [:div.fields-row + [:& fm/form {:on-submit on-submit :form form + :class (stl/css :register-validate-form)} + [:div {:class (stl/css :fields-row)} [:& fm/input {:name :fullname :label (tr "auth.fullname") - :type "text"}]] + :type "text" + :show-success? true + :class (stl/css :form-field)}]] (when (contains? cf/flags :terms-and-privacy-checkbox) - [:div.fields-row.input-visible.accept-terms-and-privacy-wrapper - [:& fm/input {:name :accept-terms-and-privacy - :class "check-primary" - :type "checkbox"} - [:span - (tr "auth.terms-privacy-agreement")]] - [:div.auth-links - [:a {:href "https://penpot.app/terms" :target "_blank"} (tr "auth.terms-of-service")] - [:span ",\u00A0"] - [:a {:href "https://penpot.app/privacy" :target "_blank"} (tr "auth.privacy-policy")]]]) + (let [terms-label + (mf/html + [:& tr-html + {:tag-name "div" + :label "auth.terms-privacy-agreement-md" + :params [cf/terms-of-service-uri cf/privacy-policy-uri]}])] + [:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)} + [:& fm/input {:name :accept-terms-and-privacy + :class "check-primary" + :type "checkbox" + :default-checked false + :label terms-label}]])) - [:& fm/submit-button + [:> fm/submit-button* {:label (tr "auth.register-submit") - :disabled @submitted?}]])) + :disabled @submitted? + :class (stl/css :register-btn)}]])) (mf/defc register-validate-page - [{:keys [params] :as props}] - [:div.form-container - [:h1 {:data-test "register-title"} (tr "auth.register-title")] - [:div.subtitle (tr "auth.register-subtitle")] + [{:keys [params]}] + [:div {:class (stl/css :auth-form-wrapper)} + [:h1 {:class (stl/css :auth-title) + :data-test "register-title"} (tr "auth.register-title")] + [:div {:class (stl/css :auth-subtitle)} (tr "auth.register-subtitle")] + + [:hr {:class (stl/css :separator)}] [:& register-validate-form {:params params}] - [:div.links - [:div.link-entry - [:& lk/link {:action #(st/emit! (rt/nav :auth-register {} {}))} + [:div {:class (stl/css :links)} + [:div {:class (stl/css :go-back)} + [:& lk/link {:action #(st/emit! (rt/nav :auth-register {} {})) + :class (stl/css :go-back-link)} (tr "labels.go-back")]]]]) (mf/defc register-success-page - [{:keys [params] :as props}] - [:div.form-container - [:div.notification-icon i/icon-verify] - [:div.notification-text (tr "auth.verification-email-sent")] - [:div.notification-text-email (:email params "")] - [:div.notification-text (tr "auth.check-your-email")]]) + [{:keys [params]}] + [:div {:class (stl/css :auth-form-wrapper :register-success)} + [:div {:class (stl/css :notification-icon)} i/icon-verify] + [:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")] + [:div {:class (stl/css :notification-text-email)} (:email params "")] + [:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]]) + diff --git a/frontend/src/app/main/ui/auth/register.scss b/frontend/src/app/main/ui/auth/register.scss new file mode 100644 index 0000000000..9cbc004574 --- /dev/null +++ b/frontend/src/app/main/ui/auth/register.scss @@ -0,0 +1,38 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; +@use "./common.scss"; + +.accept-terms-and-privacy-wrapper { + margin: $s-16 0; + :global(a) { + color: $df-secondary; + font-weight: $fw700; + } +} + +.register-success { + padding-bottom: $s-32; +} + +.notification-icon { + fill: var(--main-icon-foreground); + display: flex; + justify-content: center; + margin-bottom: $s-32; + svg { + width: $s-92; + height: $s-92; + } +} + +.notification-text-email, +.notification-text { + font-size: $fs-16; + color: var(--notification-foreground-color-default); + margin-bottom: $s-16; +} diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs index 7534d37316..a914bd46a2 100644 --- a/frontend/src/app/main/ui/auth/verify_token.cljs +++ b/frontend/src/app/main/ui/auth/verify_token.cljs @@ -5,8 +5,9 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.auth.verify-token + (:require-macros [app.main.style :as stl]) (:require - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.users :as du] [app.main.repo :as rp] [app.main.store :as st] @@ -16,7 +17,7 @@ [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [app.util.timers :as ts] - [beicon.core :as rx] + [beicon.v2.core :as rx] [rumext.v2 :as mf])) (defmulti handle-token (fn [token] (:iss token))) @@ -24,13 +25,13 @@ (defmethod handle-token :verify-email [data] (let [msg (tr "dashboard.notifications.email-verified-successfully")] - (ts/schedule 100 #(st/emit! (dm/success msg))) + (ts/schedule 1000 #(st/emit! (msg/success msg))) (st/emit! (du/login-from-token data)))) (defmethod handle-token :change-email [_data] (let [msg (tr "dashboard.notifications.email-changed-successfully")] - (ts/schedule 100 #(st/emit! (dm/success msg))) + (ts/schedule 100 #(st/emit! (msg/success msg))) (st/emit! (rt/nav :settings-profile) (du/fetch-profile)))) @@ -43,7 +44,7 @@ (case (:state tdata) :created (st/emit! - (dm/success (tr "auth.notifications.team-invitation-accepted")) + (msg/success (tr "auth.notifications.team-invitation-accepted")) (du/fetch-profile) (rt/nav :dashboard-projects {:team-id (:team-id tdata)})) @@ -56,7 +57,7 @@ [_tdata] (st/emit! (rt/nav :auth-login) - (dm/warn (tr "errors.unexpected-token")))) + (msg/warn (tr "errors.unexpected-token")))) (mf/defc verify-token [{:keys [route] :as props}] @@ -66,7 +67,7 @@ (mf/with-effect [] (dom/set-html-title (tr "title.default")) (->> (rp/cmd! :verify-token {:token token}) - (rx/subs + (rx/subs! (fn [tdata] (handle-token tdata)) (fn [{:keys [type code] :as error}] @@ -78,23 +79,20 @@ (= :email-already-exists code) (let [msg (tr "errors.email-already-exists")] - (ts/schedule 100 #(st/emit! (dm/error msg))) + (ts/schedule 100 #(st/emit! (msg/error msg))) (st/emit! (rt/nav :auth-login))) (= :email-already-validated code) (let [msg (tr "errors.email-already-validated")] - (ts/schedule 100 #(st/emit! (dm/warn msg))) + (ts/schedule 100 #(st/emit! (msg/warn msg))) (st/emit! (rt/nav :auth-login))) :else (let [msg (tr "errors.generic")] - (ts/schedule 100 #(st/emit! (dm/error msg))) + (ts/schedule 100 #(st/emit! (msg/error msg))) (st/emit! (rt/nav :auth-login)))))))) (if @bad-token - [:> static/static-header {} - [:div.image i/unchain] - [:div.main-message (tr "errors.invite-invalid")] - [:div.desc-message (tr "errors.invite-invalid.info")]] - [:div.verify-token + [:> static/invalid-token {}] + [:div {:class (stl/css :verify-token)} i/loader-pencil]))) diff --git a/frontend/src/app/main/ui/auth/verify_token.scss b/frontend/src/app/main/ui/auth/verify_token.scss new file mode 100644 index 0000000000..df815d4f4b --- /dev/null +++ b/frontend/src/app/main/ui/auth/verify_token.scss @@ -0,0 +1,11 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "./common.scss"; + +.verify-token { + @extend .loader-base; +} diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index fe4651d8ba..31cfd9a600 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.comments + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] @@ -26,28 +27,22 @@ [okulary.core :as l] [rumext.v2 :as mf])) +(def comments-local-options (l/derived :options refs/comments-local)) + (mf/defc resizing-textarea - {::mf/wrap-props false - ::mf/forward-ref true} - [props ref] + {::mf/wrap-props false} + [props] (let [value (d/nilv (unchecked-get props "value") "") on-focus (unchecked-get props "on-focus") on-blur (unchecked-get props "on-blur") placeholder (unchecked-get props "placeholder") on-change (unchecked-get props "on-change") on-esc (unchecked-get props "on-esc") + on-ctrl-enter (unchecked-get props "on-ctrl-enter") autofocus? (unchecked-get props "autofocus") select-on-focus? (unchecked-get props "select-on-focus") local-ref (mf/use-ref) - ref (or ref local-ref) - - on-key-down - (mf/use-fn - (fn [event] - (when (and (kbd/esc? event) - (fn? on-esc)) - (on-esc event)))) on-change* (mf/use-fn @@ -56,6 +51,17 @@ (let [content (dom/get-target-val event)] (on-change content)))) + on-key-down + (mf/use-fn + (mf/deps on-esc on-ctrl-enter on-change*) + (fn [event] + (cond + (and (kbd/esc? event) (fn? on-esc)) (on-esc event) + (and (kbd/mod? event) (kbd/enter? event) (fn? on-ctrl-enter)) + (do + (on-change* event) + (on-ctrl-enter event))))) + on-focus* (mf/use-fn (mf/deps select-on-focus? on-focus) @@ -69,30 +75,30 @@ ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect (.addEventListener target "mouseup" dom/prevent-default #js {:once true})))))] - - (mf/use-layout-effect nil (fn [] - (let [node (mf/ref-val ref)] + (let [node (mf/ref-val local-ref)] (set! (.-height (.-style node)) "0") (set! (.-height (.-style node)) (str (+ 2 (.-scrollHeight node)) "px"))))) - [:textarea - {:ref ref - :auto-focus autofocus? - :on-key-down on-key-down - :on-focus on-focus* - :on-blur on-blur - :value value - :placeholder placeholder - :on-change on-change*}])) + [:textarea {:ref local-ref + :auto-focus autofocus? + :on-key-down on-key-down + :on-focus on-focus* + :on-blur on-blur + :value value + :placeholder placeholder + :on-change on-change*}])) (mf/defc reply-form [{:keys [thread] :as props}] (let [show-buttons? (mf/use-state false) content (mf/use-state "") + disabled? (or (fm/all-spaces? @content) + (str/empty-or-nil? @content)) + on-focus (mf/use-fn #(reset! show-buttons? true)) @@ -116,25 +122,28 @@ (fn [] (st/emit! (dcm/add-comment thread @content)) (on-cancel)))] - - [:div.reply-form + [:div {:class (stl/css :reply-form)} [:& resizing-textarea {:value @content :placeholder "Reply" :on-blur on-blur :on-focus on-focus + :select-on-focus? false + :on-ctrl-enter on-submit :on-change on-change}] (when (or @show-buttons? (seq @content)) - [:div.buttons - [:input.btn-primary - {:type "button" - :value "Post" - :on-click on-submit - :disabled (or (fm/all-spaces? @content) - (str/empty-or-nil? @content))}] + [:div {:class (stl/css :buttons-wrapper)} [:input.btn-secondary {:type "button" + :class (stl/css :cancel-btn) :value "Cancel" - :on-click on-cancel}]])])) + :on-click on-cancel}] + [:input + {:type "button" + :class (stl/css-case :post-btn true + :global/disabled disabled?) + :value "Post" + :on-click on-submit + :disabled disabled?}]])])) (mf/defc draft-thread [{:keys [draft zoom on-cancel on-submit position-modifier]}] @@ -146,6 +155,9 @@ pos-x (* (:x position) zoom) pos-y (* (:y position) zoom) + disabled? (or (fm/all-spaces? content) + (str/empty-or-nil? content)) + on-esc (mf/use-fn (mf/deps draft) @@ -167,32 +179,37 @@ (partial on-submit draft))] [:* - [:div.thread-bubble - {:style {:top (str pos-y "px") + [:div + {:class (stl/css :floating-thread-bubble) + :style {:top (str pos-y "px") :left (str pos-x "px")} :on-click dom/stop-propagation} - [:span "?"]] - [:div.thread-content - {:style {:top (str (- pos-y 14) "px") - :left (str (+ pos-x 14) "px")} - :on-click dom/stop-propagation} - [:div.reply-form + "?"] + [:div {:class (stl/css :thread-content) + :style {:top (str (- pos-y 24) "px") + :left (str (+ pos-x 28) "px")} + :on-click dom/stop-propagation} + [:div {:class (stl/css :reply-form)} [:& resizing-textarea {:placeholder (tr "labels.write-new-comment") :value (or content "") :autofocus true + :select-on-focus? false :on-esc on-esc - :on-change on-change}] - [:div.buttons - [:input.btn-primary - {:on-click on-submit - :type "button" - :value "Post" - :disabled (or (fm/all-spaces? content) - (str/empty-or-nil? content))}] - [:input.btn-secondary - {:on-click on-esc - :type "button" - :value "Cancel"}]]]]])) + :on-change on-change + :on-ctrl-enter on-submit}] + [:div {:class (stl/css :buttons-wrapper)} + + [:input {:on-click on-esc + :class (stl/css :cancel-btn) + :type "button" + :value "Cancel"}] + + [:input {:on-click on-submit + :type "button" + :value "Post" + :class (stl/css-case :post-btn true + :global/disabled disabled?) + :disabled disabled?}]]]]])) (mf/defc edit-form [{:keys [content on-submit on-cancel] :as props}] @@ -205,39 +222,56 @@ on-submit* (mf/use-fn (mf/deps @content) - (fn [] (on-submit @content)))] + (fn [] (on-submit @content))) + disabled? (or (fm/all-spaces? @content) + (str/empty-or-nil? @content))] - [:div.reply-form.edit-form + [:div {:class (stl/css :edit-form)} [:& resizing-textarea {:value @content :autofocus true :select-on-focus true + :select-on-focus? false + :on-ctrl-enter on-submit* :on-change on-change}] - [:div.buttons - [:input.btn-primary {:type "button" - :value "Post" - :on-click on-submit* - :disabled (or (fm/all-spaces? @content) - (str/empty-or-nil? @content))}] - [:input.btn-secondary {:type "button" :value "Cancel" :on-click on-cancel}]]])) + [:div {:class (stl/css :buttons-wrapper)} + [:input {:type "button" + :value "Cancel" + :class (stl/css :cancel-btn) + :on-click on-cancel}] + [:input {:type "button" + :class (stl/css-case :post-btn true + :global/disabled disabled?) + :value "Post" + :on-click on-submit* + :disabled disabled?}]]])) (mf/defc comment-item [{:keys [comment thread users origin] :as props}] (let [owner (get users (:owner-id comment)) profile (mf/deref refs/profile) - options (mf/use-state false) + options (mf/deref comments-local-options) edition? (mf/use-state false) - on-show-options - (mf/use-fn #(reset! options true)) + on-toggle-options + (mf/use-fn + (mf/deps options) + (fn [event] + (dom/stop-propagation event) + (st/emit! (dcm/toggle-comment-options comment)))) on-hide-options - (mf/use-fn #(reset! options false)) + (mf/use-fn + (mf/deps options) + (fn [event] + (dom/stop-propagation event) + (st/emit! (dcm/hide-comment-options)))) on-edit-clicked (mf/use-fn + (mf/deps options) (fn [] - (reset! options false) + (st/emit! (dcm/hide-comment-options)) (reset! edition? true))) on-delete-comment @@ -253,16 +287,15 @@ (dcm/delete-comment-thread-on-viewer thread) (dcm/delete-comment-thread-on-workspace thread)))) - on-delete-thread (mf/use-fn (mf/deps thread) #(st/emit! (modal/show - {:type :confirm - :title (tr "modals.delete-comment-thread.title") - :message (tr "modals.delete-comment-thread.message") - :accept-label (tr "modals.delete-comment-thread.accept") - :on-accept delete-thread}))) + {:type :confirm + :title (tr "modals.delete-comment-thread.title") + :message (tr "modals.delete-comment-thread.message") + :accept-label (tr "modals.delete-comment-thread.accept") + :on-accept delete-thread}))) on-submit (mf/use-fn @@ -281,59 +314,87 @@ (dom/stop-propagation event) (st/emit! (dcm/update-comment-thread (update thread :is-resolved not)))))] - [:div.comment-container - [:div.comment - [:div.author - [:div.avatar + [:div {:class (stl/css :comment-container)} + [:div {:class (stl/css :comment)} + [:div {:class (stl/css :author)} + [:div {:class (stl/css :avatar)} [:img {:src (cfg/resolve-profile-photo-url owner)}]] - [:div.name - [:div.fullname (:fullname owner)] - [:div.timeago (dt/timeago (:modified-at comment))]] + [:div {:class (stl/css :name)} + [:div {:class (stl/css :fullname)} (:fullname owner)] + [:div {:class (stl/css :timeago)} (dt/timeago (:modified-at comment))]] (when (some? thread) - [:div.options-resolve {:on-click toggle-resolved} - (if (:is-resolved thread) - [:span i/checkbox-checked] - [:span i/checkbox-unchecked])]) + [:div {:class (stl/css :options-resolve-wrapper) + :on-click toggle-resolved} + [:span {:class (stl/css-case :options-resolve true + :global/checked (:is-resolved thread))} i/tick]]) (when (= (:id profile) (:id owner)) - [:div.options - [:div.options-icon {:on-click on-show-options} i/actions]])] + [:div {:class (stl/css :options) + :on-click on-toggle-options} + i/menu])] - [:div.content + [:div {:class (stl/css :content)} (if @edition? [:& edit-form {:content (:content comment) :on-submit on-submit :on-cancel on-cancel}] - [:span.text (:content comment)])]] + [:span {:class (stl/css :text)} (:content comment)])]] - [:& dropdown {:show @options + [:& dropdown {:show (= options (:id comment)) :on-close on-hide-options} - [:ul.dropdown.comment-options-dropdown - [:li {:on-click on-edit-clicked} (tr "labels.edit")] + [:ul {:class (stl/css :comment-options-dropdown)} + [:li {:class (stl/css :context-menu-option) + :on-click on-edit-clicked} + (tr "labels.edit")] (if thread - [:li {:on-click on-delete-thread} (tr "labels.delete-comment-thread")] - [:li {:on-click on-delete-comment} (tr "labels.delete-comment")])]]])) + [:li {:class (stl/css :context-menu-option) + :on-click on-delete-thread} + (tr "labels.delete-comment-thread")] + [:li {:class (stl/css :context-menu-option) + :on-click on-delete-comment} + (tr "labels.delete-comment")])]]])) (defn make-comments-ref [thread-id] (l/derived (l/in [:comments thread-id]) st/state)) +(defn- offset-position [position viewport zoom bubble-margin] + (let [viewport (or viewport {:offset-x 0 :offset-y 0 :width 0 :height 0}) + base-x (+ (* (:x position) zoom) (:offset-x viewport)) + base-y (+ (* (:y position) zoom) (:offset-y viewport)) + w (:width viewport) + h (:height viewport) + comment-width 284 ;; TODO: this is the width set via CSS in an outer container… + ;; We should probably do this in a different way. + orientation-left? (>= (+ base-x comment-width (:x bubble-margin)) w) + orientation-top? (>= base-y (/ h 2)) + h-dir (if orientation-left? :left :right) + v-dir (if orientation-top? :top :bottom) + x (:x position) + y (:y position)] + {:x x :y y :h-dir h-dir :v-dir v-dir})) + (mf/defc thread-comments {::mf/wrap [mf/memo]} - [{:keys [thread zoom users origin position-modifier]}] + [{:keys [thread zoom users origin position-modifier viewport]}] (let [ref (mf/use-ref) - - thread-id (:id thread) thread-pos (:position thread) - pos (cond-> thread-pos + base-pos (cond-> thread-pos (some? position-modifier) (gpt/transform position-modifier)) - pos-x (+ (* (:x pos) zoom) 14) - pos-y (- (* (:y pos) zoom) 14) + max-height (when (some? viewport) (int (* (:height viewport) 0.75))) + ;; We should probably look for a better way of doing this. + bubble-margin {:x 24 :y 0} + pos (offset-position base-pos viewport zoom bubble-margin) + + margin-x (* (:x bubble-margin) (if (= (:h-dir pos) :left) -1 1)) + margin-y (* (:y bubble-margin) (if (= (:v-dir pos) :top) -1 1)) + pos-x (+ (* (:x pos) zoom) margin-x) + pos-y (- (* (:y pos) zoom) margin-y) comments-ref (mf/with-memo [thread-id] (make-comments-ref thread-id)) @@ -356,19 +417,22 @@ (dom/scroll-into-view-if-needed! node))) (when (some? comment) - [:div.thread-content - {:style {:top (str pos-y "px") - :left (str pos-x "px")} - :on-click dom/stop-propagation} + [:div {:class (stl/css-case :thread-content true + :thread-content-left (= (:h-dir pos) :left) + :thread-content-top (= (:v-dir pos) :top)) + :id (str "thread-" thread-id) + :style {:left (str pos-x "px") + :top (str pos-y "px") + :max-height max-height} + :on-click dom/stop-propagation} - [:div.comments + [:div {:class (stl/css :comments)} [:& comment-item {:comment comment :users users :thread thread :origin origin}] (for [item (rest comments)] [:* {:key (dm/str (:id item))} - [:hr] [:& comment-item {:comment item :users users :origin origin}]]) @@ -487,18 +551,17 @@ (dom/stop-propagation event) (when (= origin :viewer) (on-click thread))))] - - [:div.thread-bubble - {:style {:top (str pos-y "px") - :left (str pos-x "px")} - :on-pointer-down on-pointer-down* - :on-pointer-up on-pointer-up* - :on-pointer-move on-pointer-move* - :on-click on-click* - :on-lost-pointer-capture on-lost-pointer-capture - :class (dom/classnames - :resolved (:is-resolved thread) - :unread (pos? (:count-unread-comments thread)))} + [:div {:style {:top (str pos-y "px") + :left (str pos-x "px")} + :on-pointer-down on-pointer-down* + :on-pointer-up on-pointer-up* + :on-pointer-move on-pointer-move* + :on-click on-click* + :on-lost-pointer-capture on-lost-pointer-capture + :class (stl/css-case + :floating-thread-bubble true + :resolved (:is-resolved thread) + :unread (pos? (:count-unread-comments thread)))} [:span (:seqn thread)]])) (mf/defc comment-thread @@ -513,45 +576,49 @@ (when (fn? on-click) (on-click item))))] - [:div.comment {:on-click on-click*} - [:div.author - [:div.thread-bubble - {:class (dom/classnames - :resolved (:is-resolved item) - :unread (pos? (:count-unread-comments item)))} + [:div {:class (stl/css :comment) + :on-click on-click*} + [:div {:class (stl/css :author)} + [:div {:class (stl/css-case :thread-bubble true + :resolved (:is-resolved item) + :unread (pos? (:count-unread-comments item)))} (:seqn item)] - [:div.avatar + [:div {:class (stl/css :avatar)} [:img {:src (cfg/resolve-profile-photo-url owner)}]] - [:div.name - [:div.fullname (:fullname owner) ", "] - [:div.timeago (dt/timeago (:modified-at item))]]] - [:div.content - [:span.text (:content item)]] - [:div.content.replies + [:div {:class (stl/css :name)} + [:div {:class (stl/css :fullname)} (:fullname owner)] + [:div {:class (stl/css :timeago)} (dt/timeago (:modified-at item))]]] + [:div {:class (stl/css :content)} + (:content item)] + [:div {:class (stl/css :replies)} (let [unread (:count-unread-comments item ::none) total (:count-comments item 1)] [:* (when (> total 1) (if (= total 2) - [:span.total-replies "1 reply"] - [:span.total-replies (str (dec total) " replies")])) + [:span {:class (stl/css :total-replies)} "1 reply"] + [:span {:class (stl/css :total-replies)} (str (dec total) " replies")])) (when (and (> total 1) (> unread 0)) (if (= unread 1) - [:span.new-replies "1 new reply"] - [:span.new-replies (str unread " new replies")]))])]])) + [:span {:class (stl/css :new-replies)} "1 new reply"] + [:span {:class (stl/css :new-replies)} (str unread " new replies")]))])]])) (mf/defc comment-thread-group [{:keys [group users on-thread-click]}] - [:div.thread-group + [:div {:class (stl/css :thread-group)} (if (:file-name group) - [:div.section-title - [:span.label.filename (:file-name group) ", "] - [:span.label (:page-name group)]] - [:div.section-title - [:span.icon i/file-html] - [:span.label (:page-name group)]]) - [:div.threads + [:div {:class (stl/css :section-title) + :title (str (:file-name group) ", " (:page-name group))} + [:span {:class (stl/css :file-name)} (:file-name group) ", "] + [:span {:class (stl/css :page-name)} (:page-name group)]] + + [:div {:class (stl/css :section-title) + :title (:page-name group)} + [:span {:class (stl/css :icon)} i/document] + [:span {:class (stl/css :page-name)} (:page-name group)]]) + + [:div {:class (stl/css :threads)} (for [item (:items group)] [:& comment-thread {:item item diff --git a/frontend/src/app/main/ui/comments.scss b/frontend/src/app/main/ui/comments.scss new file mode 100644 index 0000000000..3c6e569ea6 --- /dev/null +++ b/frontend/src/app/main/ui/comments.scss @@ -0,0 +1,269 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +// Comment-thread-group +.thread-group { + padding: 0 $s-12; + cursor: pointer; + border-radius: $br-8; + padding: $s-8 $s-16; + + &:hover { + background: var(--comment-thread-background-color-hover); + } +} + +.section-title { + display: grid; + grid-template-columns: auto auto; + @include bodySmallTypography; + height: $s-32; + display: flex; + align-items: center; + margin-bottom: $s-8; +} + +.file-name { + @include textEllipsis; + color: var(--comment-subtitle-color); +} + +.page-name { + @include textEllipsis; + color: var(--comment-subtitle-color); +} + +.icon { + display: flex; + align-items: center; + padding: 0 $s-6 0 $s-4; + width: $s-24; + height: $s-32; + margin-left: $s-6; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } +} + +.threads { + display: flex; + flex-direction: column; + gap: $s-24; +} + +// Comment-thread +.comment { + @include bodySmallTypography; + display: flex; + flex-direction: column; + gap: $s-12; +} + +.author { + display: flex; + gap: $s-8; +} + +.thread-bubble { + @extend .comment-bubbles; + &.resolved { + @extend .resolved-comment-bubble; + } + &.unread { + @extend .unread-comment-bubble; + } +} + +.avatar { + height: $s-32; + width: $s-32; + border-radius: $br-circle; + img { + border-radius: $br-circle; + } +} + +.name { + flex-grow: 1; + .fullname { + @include textEllipsis; + color: var(--comment-title-color); + } + .timeago { + @include textEllipsis; + color: var(--comment-subtitle-color); + } +} + +.content { + position: relative; + @include bodySmallTypography; + color: var(--color-foreground-primary); + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + white-space: pre-wrap; +} + +.replies { + display: flex; + gap: $s-8; +} + +.total-replies { + color: var(--color-foreground-secondary); +} +.new-replies { + color: var(--color-accent-primary); +} +// Thread-bubble + +.floating-thread-bubble { + @extend .comment-bubbles; + position: absolute; + cursor: pointer; + pointer-events: auto; + transform: translate(calc(-1 * $s-16), calc(-1 * $s-16)); + + &.resolved { + @extend .resolved-comment-bubble; + } + &.unread { + @extend .unread-comment-bubble; + } +} + +// thread-content +.thread-content { + position: absolute; + overflow-y: scroll; + scrollbar-gutter: stable; + width: $s-284; + padding: $s-12; + padding-inline-end: 0; + + pointer-events: auto; + user-select: text; + border-radius: $br-8; + border: $s-2 solid var(--modal-border-color); + background-color: var(--comment-modal-background-color); + --translate-x: 0%; + --translate-y: 0%; + transform: translate(var(--translate-x), var(--translate-y)); + .comments { + display: flex; + flex-direction: column; + gap: $s-24; + } +} + +.thread-content-left { + --translate-x: -100%; +} +.thread-content-top { + --translate-y: -100%; +} + +// comment-item + +.comment-container { + position: relative; + .comment { + @include bodySmallTypography; + .author { + display: flex; + gap: $s-8; + .avatar { + height: $s-32; + width: $s-32; + border-radius: $br-circle; + img { + border-radius: $br-circle; + } + } + .name { + flex-grow: 1; + .fullname { + @include textEllipsis; + color: var(--comment-title-color); + } + .timeago { + @include textEllipsis; + color: var(--comment-subtitle-color); + } + } + .options-resolve-wrapper { + @include flexCenter; + width: $s-16; + height: $s-32; + .options-resolve { + @extend .checkbox-icon; + cursor: pointer; + } + } + .options { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } + } + } + } + .comment-options-dropdown { + @extend .dropdown-wrapper; + position: absolute; + width: fit-content; + max-width: $s-200; + right: 0; + left: unset; + .context-menu-option { + @extend .dropdown-element-base; + } + } +} + +// edit-form & reply-form + +.edit-form, +.reply-form { + textarea { + @extend .input-element; + line-height: 1.45; + height: 100%; + width: 100%; + max-width: $s-260; + margin-bottom: $s-8; + padding: $s-8; + color: var(--input-foreground-color-active); + resize: vertical; + &:focus { + border: $s-1 solid var(--input-border-color-active); + outline: none; + } + } + .buttons-wrapper { + display: flex; + justify-content: flex-end; + gap: $s-4; + .post-btn { + @extend .button-primary; + height: $s-32; + width: $s-92; + margin-bottom: 0; + } + .cancel-btn { + @extend .button-secondary; + height: $s-32; + width: $s-92; + margin-bottom: 0; + } + } +} diff --git a/frontend/src/app/main/ui/components.cljs b/frontend/src/app/main/ui/components.cljs new file mode 100644 index 0000000000..6119114fc6 --- /dev/null +++ b/frontend/src/app/main/ui/components.cljs @@ -0,0 +1,20 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.components + (:require + [app.main.ui.components.buttons.simple-button :as sb] + [rumext.v2 :as mf])) + +(mf/defc story-wrapper + {::mf/wrap-props false} + [{:keys [children]}] + [:.default children]) + +(def default + "A export used for storybook" + #js {:SimpleButton sb/simple-button + :StoryWrapper story-wrapper}) diff --git a/frontend/src/app/main/ui/components/button_link.cljs b/frontend/src/app/main/ui/components/button_link.cljs index a600e7524e..fadfbd3478 100644 --- a/frontend/src/app/main/ui/components/button_link.cljs +++ b/frontend/src/app/main/ui/components/button_link.cljs @@ -9,13 +9,19 @@ [app.util.keyboard :as kbd] [rumext.v2 :as mf])) -(mf/defc button-link [{:keys [action icon name klass]}] - [:a.btn-primary.btn-large.button-link - {:class klass - :tab-index "0" - :on-click action - :on-key-down (fn [event] - (when (kbd/enter? event) - (action event)))} - [:span.logo icon] - name]) +(mf/defc button-link + {::mf/wrap-props false} + [{:keys [on-click icon label class]}] + (let [on-key-down (mf/use-fn + (mf/deps on-click) + (fn [event] + (when (kbd/enter? event) + (when (fn? on-click) + (on-click event)))))] + [:a.btn-primary.btn-large.button-link + {:class class + :tab-index "0" + :on-click on-click + :on-key-down on-key-down} + [:span.logo icon] + label])) diff --git a/frontend/src/app/main/ui/components/buttons/simple_button.cljs b/frontend/src/app/main/ui/components/buttons/simple_button.cljs new file mode 100644 index 0000000000..fb4bdd9952 --- /dev/null +++ b/frontend/src/app/main/ui/components/buttons/simple_button.cljs @@ -0,0 +1,10 @@ +(ns app.main.ui.components.buttons.simple-button + (:require-macros [app.main.style :as stl]) + (:require + [rumext.v2 :as mf])) + +(mf/defc simple-button + {::mf/wrap-props false} + [{:keys [on-click children]}] + [:button {:on-click on-click :class (stl/css :button)} children]) + diff --git a/frontend/src/app/main/ui/components/buttons/simple_button.mdx b/frontend/src/app/main/ui/components/buttons/simple_button.mdx new file mode 100644 index 0000000000..6c93cc3a21 --- /dev/null +++ b/frontend/src/app/main/ui/components/buttons/simple_button.mdx @@ -0,0 +1,16 @@ +import { Canvas, Meta } from '@storybook/blocks'; +import * as SimpleButtonStories from "./simple_button.stories" + + + +# Lorem ipsum + +This is an example of **markdown** docs within storybook, for the component ``. + +Here's how we can render a simple button: + + + +Simple buttons can also have **icons**: + + \ No newline at end of file diff --git a/frontend/src/app/main/ui/components/buttons/simple_button.scss b/frontend/src/app/main/ui/components/buttons/simple_button.scss new file mode 100644 index 0000000000..e1d162fbc1 --- /dev/null +++ b/frontend/src/app/main/ui/components/buttons/simple_button.scss @@ -0,0 +1,13 @@ +.button { + font-family: monospace; + + display: flex; + align-items: center; + column-gap: 0.5rem; + + svg { + width: 16px; + height: 16px; + stroke: #000; + } +} diff --git a/frontend/src/app/main/ui/components/buttons/simple_button.stories.jsx b/frontend/src/app/main/ui/components/buttons/simple_button.stories.jsx new file mode 100644 index 0000000000..33142e12c4 --- /dev/null +++ b/frontend/src/app/main/ui/components/buttons/simple_button.stories.jsx @@ -0,0 +1,30 @@ +import * as React from "react"; + +import Components from "@target/components"; +import Icons from "@target/icons"; + +export default { + title: 'Buttons/Simple Button', + component: Components.SimpleButton, +}; + +export const Default = { + render: () => ( + + + Simple Button + + + ), +}; + +export const WithIcon = { + render: () => ( + + + {Icons.AddRefactor} + Simple Button + + + ), +} diff --git a/frontend/src/app/main/ui/components/buttons/simple_button.test.mjs b/frontend/src/app/main/ui/components/buttons/simple_button.test.mjs new file mode 100644 index 0000000000..9d1c6c9ac2 --- /dev/null +++ b/frontend/src/app/main/ui/components/buttons/simple_button.test.mjs @@ -0,0 +1,10 @@ +import { expect, test } from 'vitest' + +test('use jsdom in this test file', () => { + const element = document.createElement('div') + expect(element).not.toBeNull() +}) + +test('adds 1 + 2 to equal 3', () => { + expect(1 +2).toBe(3) +}); diff --git a/frontend/src/app/main/ui/components/code_block.cljs b/frontend/src/app/main/ui/components/code_block.cljs index 2f8ded61e0..b766772344 100644 --- a/frontend/src/app/main/ui/components/code_block.cljs +++ b/frontend/src/app/main/ui/components/code_block.cljs @@ -5,16 +5,27 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.components.code-block + (:require-macros [app.main.style :as stl]) (:require - ["highlight.js" :as hljs] - [rumext.v2 :as mf])) + [app.common.data.macros :as dm] + [cuerdas.core :as str] + [promesa.core :as p] + [rumext.v2 :as mf] + [shadow.lazy :as lazy])) -(mf/defc code-block [{:keys [code type]}] - (let [block-ref (mf/use-ref)] - (mf/use-effect - (mf/deps code type block-ref) - (fn [] - (hljs/highlightElement (mf/ref-val block-ref)))) - [:pre.code-display {:class type - :ref block-ref} code])) +(def highlight-fn + (lazy/loadable app.util.code-highlight/highlight!)) + +(mf/defc code-block + {::mf/wrap-props false} + [{:keys [code type]}] + (let [block-ref (mf/use-ref) + code (str/trim code)] + + (mf/with-effect [code type] + (when-let [node (mf/ref-val block-ref)] + (p/let [highlight-fn (lazy/load highlight-fn)] + (highlight-fn node)))) + + [:pre {:class (dm/str type " " (stl/css :code-display)) :ref block-ref} code])) diff --git a/frontend/src/app/main/ui/components/code_block.scss b/frontend/src/app/main/ui/components/code_block.scss new file mode 100644 index 0000000000..49d63fda19 --- /dev/null +++ b/frontend/src/app/main/ui/components/code_block.scss @@ -0,0 +1,16 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.code-display { + user-select: text; + border-radius: $br-8; + margin-top: $s-8; + padding: $s-12; + background-color: var(--menu-background-color); + overflow: auto; +} diff --git a/frontend/src/app/main/ui/components/color_bullet.cljs b/frontend/src/app/main/ui/components/color_bullet.cljs index 9d973db79c..1d4b9aacc7 100644 --- a/frontend/src/app/main/ui/components/color_bullet.cljs +++ b/frontend/src/app/main/ui/components/color_bullet.cljs @@ -5,42 +5,110 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.components.color-bullet + (:require-macros [app.main.style :as stl]) (:require + [app.config :as cfg] [app.util.color :as uc] - [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [cuerdas.core :as str] [rumext.v2 :as mf])) +(defn- color-title + [color-item] + (let [name (:name color-item) + gradient (:gradient color-item) + image (:image color-item) + color (:color color-item)] + + (if (some? name) + (cond + (some? color) + (str/ffmt "% (%)" name color) + + (some? gradient) + (str/ffmt "% (%)" name (uc/gradient-type->string (:type gradient))) + + (some? image) + (str/ffmt "% (%)" name (tr "media.image")) + + :else + name) + + (cond + (some? color) + color + + (some? gradient) + (uc/gradient-type->string (:type gradient)) + + (some? image) + (tr "media.image"))))) + (mf/defc color-bullet - {::mf/wrap [mf/memo]} - [{:keys [color on-click]}] - (let [on-click (mf/use-fn - (mf/deps color on-click) - (fn [event] - (when (fn? on-click) - (^function on-click color event))))] + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [{:keys [color on-click mini? area]}] + (let [read-only? (nil? on-click) + on-click + (mf/use-fn + (mf/deps color on-click) + (fn [event] + (when (fn? on-click) + (^function on-click color event))))] (if (uc/multiple? color) - [:div.color-bullet.multiple {:on-click on-click}] - + [:div {:class (stl/css :color-bullet :multiple) + :on-click on-click + :title (color-title color)}] ;; No multiple selection - (let [color (if (string? color) {:color color :opacity 1} color)] - [:div.color-bullet - {:class (dom/classnames :is-library-color (some? (:id color)) - :is-not-library-color (nil? (:id color)) - :is-gradient (some? (:gradient color))) + (let [color (if (string? color) {:color color :opacity 1} color) + id (:id color) + gradient (:gradient color) + opacity (:opacity color) + image (:image color)] + [:div + {:class (stl/css-case + :color-bullet true + :mini mini? + :is-library-color (some? id) + :is-not-library-color (nil? id) + :is-gradient (some? gradient) + :is-transparent (and opacity (> 1 opacity)) + :grid-area area + :read-only read-only?) + :data-readonly (str read-only?) :on-click on-click - :title (uc/get-color-name color)} - (if (:gradient color) - [:div.color-bullet-wrapper {:style {:background (uc/color->background color)}}] - [:div.color-bullet-wrapper - [:div.color-bullet-left {:style {:background (uc/color->background (assoc color :opacity 1))}}] - [:div.color-bullet-right {:style {:background (uc/color->background color)}}]])])))) + :title (color-title color)} -(mf/defc color-name [{:keys [color size on-click on-double-click]}] - (let [color (if (string? color) {:color color :opacity 1} color) - {:keys [name color gradient]} color - color-str (or name color (uc/gradient-type->string (:type gradient)))] - (when (or (not size) (= size :big)) - [:span.color-text {:on-click #(when on-click (on-click %)) - :on-double-click #(when on-double-click (on-double-click %)) - :title name} color-str]))) + (cond + (some? gradient) + [:div {:class (stl/css :color-bullet-wrapper) + :style {:background (uc/color->background color)}}] + + (some? image) + (let [uri (cfg/resolve-file-media image)] + [:div {:class (stl/css :color-bullet-wrapper) + :style {:background-image (str/ffmt "url(%)" uri)}}]) + + :else + [:div {:class (stl/css :color-bullet-wrapper)} + [:div {:class (stl/css :color-bullet-left) + :style {:background (uc/color->background (assoc color :opacity 1))}}] + [:div {:class (stl/css :color-bullet-right) + :style {:background (uc/color->background color)}}]])])))) + +(mf/defc color-name + {::mf/wrap-props false} + [{:keys [color size on-click on-double-click origin]}] + (let [{:keys [name color gradient]} (if (string? color) {:color color :opacity 1} color)] + (when (or (not size) (> size 64)) + [:span {:class (stl/css-case + :color-text (and (= origin :palette) (< size 72)) + :small-text (and (= origin :palette) (>= size 64) (< size 72)) + :big-text (and (= origin :palette) (>= size 72)) + :gradient (some? gradient) + :color-row-name (not= origin :palette)) + :title name + :on-click on-click + :on-double-click on-double-click} + (or name color (uc/gradient-type->string (:type gradient)))]))) diff --git a/frontend/src/app/main/ui/components/color_bullet_new.scss b/frontend/src/app/main/ui/components/color_bullet.scss similarity index 57% rename from frontend/src/app/main/ui/components/color_bullet_new.scss rename to frontend/src/app/main/ui/components/color_bullet.scss index 66e21258a5..3a8e0e202a 100644 --- a/frontend/src/app/main/ui/components/color_bullet_new.scss +++ b/frontend/src/app/main/ui/components/color_bullet.scss @@ -10,19 +10,27 @@ position: relative; display: flex; flex-direction: row; - width: var(--bullet-size); - height: var(--bullet-size); - margin-top: $s-4; + width: var(--bullet-size, $s-24); + height: var(--bullet-size, $s-24); + min-width: var(--bullet-size, $s-24); + min-height: var(--bullet-size, $s-24); border: $s-2 solid var(--color-bullet-border-color); border-radius: $br-circle; + &.grid-area { + grid-area: color; + } &.mini { + width: var(--bullet-size, $s-16); + height: var(--bullet-size, $s-16); + min-width: var(--bullet-size, $s-16); + min-height: var(--bullet-size, $s-16); + margin-top: 0; border: 1px solid var(--color-bullet-border-color); } &.is-not-library-color { overflow: hidden; border-radius: $br-8; - & .color-bullet-wrapper { clip-path: none; } @@ -31,13 +39,13 @@ } } &.is-gradient { - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAExJREFUSIljvHnz5n8GLEBNTQ2bMMOtW7ewiuNSz4RVlIpg1IKBt4Dx////WFMRqakFl/qhH0SjFhAELNRKLaNl0Qi2YLQsGrWAcgAA0gAgQPhT2rAAAAAASUVORK5CYII=") left center; background-color: var(--color-bullet-background-color); transform: rotate(-90deg); } &.is-transparent { - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAExJREFUSIljvHnz5n8GLEBNTQ2bMMOtW7ewiuNSz4RVlIpg1IKBt4Dx////WFMRqakFl/qhH0SjFhAELNRKLaNl0Qi2YLQsGrWAcgAA0gAgQPhT2rAAAAAASUVORK5CYII=") left center; background-color: var(--color-bullet-background-color); } @@ -47,17 +55,23 @@ height: 100%; width: 100%; clip-path: circle(50%); + background-size: contain; + background-repeat: no-repeat; + background-position: center; } .color-bullet-wrapper > * { width: 100%; height: 100%; background-color: var(--color-bullet-background-color); } + &:hover:not(.read-only) { + border: $s-2 solid var(--color-bullet-border-color-selected); + } } .color-text { @include twoLineTextEllipsis; - @include titleTipography; + @include bodySmallTypography; width: $s-80; text-align: center; margin-top: $s-2; @@ -69,6 +83,18 @@ } } +.big-text { + @include inspectValue; + @include twoLineTextEllipsis; + color: var(--palette-text-color); + height: $s-28; + text-align: center; +} + .no-text { display: none; } + +.color-row-name { + color: var(--menu-foreground-color); +} diff --git a/frontend/src/app/main/ui/components/color_bullet_new.cljs b/frontend/src/app/main/ui/components/color_bullet_new.cljs deleted file mode 100644 index 2a64e1f409..0000000000 --- a/frontend/src/app/main/ui/components/color_bullet_new.cljs +++ /dev/null @@ -1,54 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.components.color-bullet-new - (:require-macros [app.main.style :refer [css]]) - (:require - [app.util.color :as uc] - [app.util.dom :as dom] - [rumext.v2 :as mf])) - - -(mf/defc color-bullet - {::mf/wrap [mf/memo]} - [{:keys [color on-click mini?]}] - (let [on-click (mf/use-fn - (mf/deps color on-click) - (fn [event] - (when (fn? on-click) - (^function on-click color event))))] - (if (uc/multiple? color) - [:div {:on-click on-click - :class (dom/classnames (css :color-bullet) true - (css :multiple) true)}] - ;; No multiple selection - (let [color (if (string? color) {:color color :opacity 1} color)] - [:div - {:class (dom/classnames (css :color-bullet) true - (css :mini) mini? - (css :is-library-color) (some? (:id color)) - (css :is-not-library-color) (nil? (:id color)) - (css :is-gradient) (some? (:gradient color)) - (css :is-transparent) (and (:opacity color) (> 1 (:opacity color)))) - :on-click on-click} - (if (:gradient color) - [:div {:class (dom/classnames (css :color-bullet-wrapper) true) - :style {:background (uc/color->background color)}}] - [:div {:class (dom/classnames (css :color-bullet-wrapper) true)} - [:div {:class (dom/classnames (css :color-bullet-left) true) - :style {:background (uc/color->background (assoc color :opacity 1))}}] - [:div {:class (dom/classnames (css :color-bullet-right) true) - :style {:background (uc/color->background color)}}]])])))) -(mf/defc color-name [{:keys [color size on-click on-double-click]}] - (let [color (if (string? color) {:color color :opacity 1} color) - {:keys [name color gradient]} color - color-str (or name color (uc/gradient-type->string (:type gradient))) - text-small (and (>= size 64) (< size 72))] - (when (or (not size) (> size 64)) - [:span {:class (dom/classnames (css :color-text) true - (css :small-text) text-small) - :on-click #(when on-click (on-click %)) - :on-double-click #(when on-double-click (on-double-click %))} color-str]))) diff --git a/frontend/src/app/main/ui/components/color_bullet_new.css.json b/frontend/src/app/main/ui/components/color_bullet_new.css.json deleted file mode 100644 index 04e955d8e4..0000000000 --- a/frontend/src/app/main/ui/components/color_bullet_new.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"components_color_bullet_new_button-primary_pDkQg","button-secondary":"components_color_bullet_new_button-secondary_y3A8V","button-icon":"components_color_bullet_new_button-icon_uAC1e","button-icon-small":"components_color_bullet_new_button-icon-small_rz5pc","color-bullet":"components_color_bullet_new_color-bullet_b1w8U","mini":"components_color_bullet_new_mini_B261Z","is-not-library-color":"components_color_bullet_new_is-not-library-color_PSveA","color-bullet-wrapper":"components_color_bullet_new_color-bullet-wrapper_clt4r","is-gradient":"components_color_bullet_new_is-gradient_6RdV2","is-transparent":"components_color_bullet_new_is-transparent_g0iwn","color-text":"components_color_bullet_new_color-text_HM6mp","small-text":"components_color_bullet_new_small-text_Y4OeK","no-text":"components_color_bullet_new_no-text_pbTQf"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/components/color_input.cljs b/frontend/src/app/main/ui/components/color_input.cljs index 2882ce8da4..4130939b74 100644 --- a/frontend/src/app/main/ui/components/color_input.cljs +++ b/frontend/src/app/main/ui/components/color_input.cljs @@ -6,24 +6,24 @@ (ns app.main.ui.components.color-input (:require - [app.util.color :as uc] + [app.common.colors :as cc] + [app.common.data :as d] [app.util.dom :as dom] [app.util.globals :as globals] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.object :as obj] [goog.events :as events] - [rumext.v2 :as mf]) - (:import goog.events.EventType)) + [rumext.v2 :as mf])) (defn clean-color [value] (-> value - (uc/expand-hex) - (uc/parse-color) - (uc/prepend-hash))) + (cc/expand-hex) + (cc/parse) + (cc/prepend-hash))) -(mf/defc color-input +(mf/defc color-input* {::mf/wrap-props false ::mf/forward-ref true} [props external-ref] @@ -31,7 +31,8 @@ on-change (obj/get props "onChange") on-blur (obj/get props "onBlur") on-focus (obj/get props "onFocus") - select-on-focus? (obj/get props "data-select-on-focus" true) + select-on-focus? (d/nilv (unchecked-get props "selectOnFocus") true) + class (d/nilv (unchecked-get props "className") "color-input") ;; We need a ref pointing to the input dom element, but the user ;; of this component may provide one (that is forwarded here). @@ -44,7 +45,7 @@ dirty-ref (mf/use-ref false) parse-value - (mf/use-callback + (mf/use-fn (mf/deps ref) (fn [] (let [input-node (mf/ref-val ref)] @@ -57,24 +58,24 @@ nil))))) update-input - (mf/use-callback + (mf/use-fn (mf/deps ref) (fn [new-value] (let [input-node (mf/ref-val ref)] - (dom/set-value! input-node (uc/remove-hash new-value))))) + (dom/set-value! input-node (cc/remove-hash new-value))))) apply-value - (mf/use-callback + (mf/use-fn (mf/deps on-change update-input) (fn [new-value] (mf/set-ref-val! dirty-ref false) - (when (and new-value (not= (uc/remove-hash new-value) value)) + (when (and new-value (not= (cc/remove-hash new-value) value)) (when on-change (on-change new-value)) (update-input new-value)))) handle-key-down - (mf/use-callback + (mf/use-fn (mf/deps apply-value update-input) (fn [event] (mf/set-ref-val! dirty-ref true) @@ -92,7 +93,7 @@ (dom/blur! input-node))))) handle-blur - (mf/use-callback + (mf/use-fn (mf/deps parse-value apply-value update-input) (fn [_] (let [new-value (parse-value)] @@ -103,7 +104,7 @@ (update-input value))))) on-click - (mf/use-callback + (mf/use-fn (fn [event] (let [target (dom/get-target event)] (when (some? ref) @@ -112,24 +113,27 @@ (dom/blur! current))))))) on-mouse-up - (mf/use-callback - (fn [event] - (dom/prevent-default event))) + (mf/use-fn + (fn [event] + (dom/prevent-default event))) handle-focus - (mf/use-callback - (fn [event] - (let [target (dom/get-target event)] - (when on-focus - (on-focus event)) + (mf/use-fn + (fn [event] + (let [target (dom/get-target event)] + (when on-focus + (on-focus event)) - (when select-on-focus? - (-> event (dom/get-target) (.select)) + (when select-on-focus? + (-> event (dom/get-target) (.select)) ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect - (.addEventListener target "mouseup" on-mouse-up #js {"once" true}))))) + (.addEventListener target "mouseup" on-mouse-up #js {"once" true}))))) - props (-> props - (obj/without ["value" "onChange" "onFocus"]) + props (-> (obj/clone props) + (obj/unset! "selectOnFocus") + (obj/set! "value" mf/undefined) + (obj/set! "onChange" mf/undefined) + (obj/set! "className" class) (obj/set! "type" "text") (obj/set! "ref" ref) ;; (obj/set! "list" list-id) @@ -157,8 +161,8 @@ (mf/use-layout-effect (fn [] - (let [keys [(events/listen globals/window EventType.POINTERDOWN on-click) - (events/listen globals/window EventType.CLICK on-click)]] + (let [keys [(events/listen globals/window "pointerdown" on-click) + (events/listen globals/window "click" on-click)]] #(doseq [key keys] (events/unlistenByKey key))))) @@ -166,7 +170,7 @@ [:> :input props] ;; FIXME: this causes some weird interactions because of using apply-value ;; [:datalist {:id list-id} - ;; (for [color-name uc/color-names] + ;; (for [color-name cc/color-names] ;; [:option color-name])] ])) diff --git a/frontend/src/app/main/ui/components/context_menu.cljs b/frontend/src/app/main/ui/components/context_menu.cljs deleted file mode 100644 index 4bf05203e6..0000000000 --- a/frontend/src/app/main/ui/components/context_menu.cljs +++ /dev/null @@ -1,131 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.components.context-menu - (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.main.refs :as refs] - [app.main.ui.components.dropdown :refer [dropdown']] - [app.main.ui.icons :as i] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [tr]] - [app.util.object :as obj] - [goog.object :as gobj] - [rumext.v2 :as mf])) - -(mf/defc context-menu - {::mf/wrap-props false} - [props] - (assert (fn? (gobj/get props "on-close")) "missing `on-close` prop") - (assert (boolean? (gobj/get props "show")) "missing `show` prop") - (assert (vector? (gobj/get props "options")) "missing `options` prop") - - (let [open? (gobj/get props "show") - on-close (gobj/get props "on-close") - options (gobj/get props "options") - is-selectable (gobj/get props "selectable") - selected (gobj/get props "selected") - top (gobj/get props "top" 0) - left (gobj/get props "left" 0) - fixed? (gobj/get props "fixed?" false) - min-width? (gobj/get props "min-width?" false) - route (mf/deref refs/route) - in-dashboard? (= :dashboard-projects (:name (:data route))) - - local (mf/use-state {:offset-y 0 - :offset-x 0 - :levels nil}) - - on-local-close - (mf/use-callback - (fn [] - (swap! local assoc :levels [{:parent-option nil - :options options}]) - (on-close))) - - check-menu-offscreen - (mf/use-callback - (mf/deps top (:offset-y @local) left (:offset-x @local)) - (fn [node] - (when (some? node) - (let [bounding_rect (dom/get-bounding-rect node) - window_size (dom/get-window-size) - {node-height :height node-width :width} bounding_rect - {window-height :height window-width :width} window_size - target-offset-y (if (> (+ top node-height) window-height) - (- node-height) - 0) - target-offset-x (if (> (+ left node-width) window-width) - (- node-width) - 0)] - - (when (or (not= target-offset-y (:offset-y @local)) (not= target-offset-x (:offset-x @local))) - (swap! local assoc :offset-y target-offset-y :offset-x target-offset-x)))))) - - enter-submenu - (mf/use-callback - (mf/deps options) - (fn [option-name sub-options] - (fn [event] - (dom/stop-propagation event) - (swap! local update :levels - conj {:parent-option option-name - :options sub-options})))) - - exit-submenu - (mf/use-callback - (fn [event] - (dom/stop-propagation event) - (swap! local update :levels pop))) - - props (obj/merge props #js {:on-close on-local-close})] - - (mf/use-effect - (mf/deps options) - #(swap! local assoc :levels [{:parent-option nil - :options options}])) - - (when (and open? (some? (:levels @local))) - [:> dropdown' props - [:div.context-menu {:class (dom/classnames :is-open open? - :fixed fixed? - :is-selectable is-selectable) - :style {:top (+ top (:offset-y @local)) - :left (+ left (:offset-x @local))}} - (let [level (-> @local :levels peek)] - [:ul.context-menu-items {:class (dom/classnames :min-width min-width?) - :ref check-menu-offscreen} - (when-let [parent-option (:parent-option level)] - [:* - [:li.context-menu-item - [:a.context-menu-action.submenu-back - {:data-no-close true - :on-click exit-submenu} - [:span i/arrow-slide] - parent-option]] - [:li.separator]]) - (for [[index [option-name option-handler sub-options data-test]] (d/enumerate (:options level))] - (when option-name - (if (= option-name :separator) - [:li.separator {:key (dm/str "context-item-" index)}] - [:li.context-menu-item - {:class (dom/classnames :is-selected (and selected (= option-name selected))) - :key (dm/str "context-item-" index)} - (if-not sub-options - [:a.context-menu-action {:on-click #(do (dom/stop-propagation %) - (on-close) - (option-handler %)) - :data-test data-test} - (if (and in-dashboard? (= option-name "Default")) - (tr "dashboard.default-team-name") - option-name)] - [:a.context-menu-action.submenu - {:data-no-close true - :on-click (enter-submenu option-name sub-options) - :data-test data-test} - option-name - [:span i/arrow-slide]])])))])]]))) diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.cljs b/frontend/src/app/main/ui/components/context_menu_a11y.cljs index 7bdd05ff57..0787b69e7c 100644 --- a/frontend/src/app/main/ui/components/context_menu_a11y.cljs +++ b/frontend/src/app/main/ui/components/context_menu_a11y.cljs @@ -5,13 +5,12 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.components.context-menu-a11y - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.main.refs :as refs] [app.main.ui.components.dropdown :refer [dropdown']] - [app.main.ui.context :as ctx] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -66,14 +65,14 @@ left (gobj/get props "left" 0) fixed? (gobj/get props "fixed?" false) min-width? (gobj/get props "min-width?" false) - workspace? (gobj/get props "workspace?" false) origin (gobj/get props "origin") route (mf/deref refs/route) - new-css-system (mf/use-ctx ctx/new-css-system) in-dashboard? (= :dashboard-projects (:name (:data route))) local (mf/use-state {:offset-y 0 :offset-x 0 :levels nil}) + width (gobj/get props "width" "initial") + on-local-close (mf/use-callback @@ -194,53 +193,38 @@ (when (and open? (some? (:levels @local))) [:> dropdown' props - (let [level (-> @local :levels peek) original-options (:options level) parent-original (:parent-option level)] - [:div {:class (if (and new-css-system workspace?) - (dom/classnames (css :is-selectable) is-selectable - (css :context-menu) true - (css :is-open) open? - (css :fixed) fixed?) - (dom/classnames :is-selectable is-selectable - :context-menu true - :is-open open? - :fixed fixed?)) + [:div {:class (stl/css-case :is-selectable is-selectable + :context-menu true + :is-open open? + :fixed fixed?) :style {:top (+ top (:offset-y @local)) :left (+ left (:offset-x @local))} :on-key-down (on-key-down original-options parent-original)} (let [level (-> @local :levels peek)] - [:ul {:class (if (and new-css-system workspace?) - (dom/classnames (css :min-width) min-width? - (css :context-menu-items) true) - (dom/classnames :min-width min-width? - :context-menu-items true)) + [:ul {:class (stl/css-case :min-width min-width? + :context-menu-items true) + :style {:width width} :role "menu" :ref check-menu-offscreen} (when-let [parent-option (:parent-option level)] [:* [:& context-menu-a11y-item {:id "go-back-sub-option" - :class (dom/classnames (css :context-menu-item) (and new-css-system workspace?)) + :class (stl/css :context-menu-item) :tab-index "0" :on-key-down (fn [event] (dom/prevent-default event))} - [:div {:class (if (and new-css-system workspace?) - (dom/classnames (css :context-menu-action) true - (css :submenu-back) true) - (dom/classnames :context-menu-action true - :submenu-back true)) - :data-no-close true - :on-click exit-submenu} - [:span {:class (dom/classnames (css :submenu-icon-back) (and new-css-system workspace?))} - (if (and new-css-system workspace?) - i/arrow-refactor - i/arrow-slide)] + [:button {:class (stl/css :context-menu-action :submenu-back) + :data-no-close true + :on-click exit-submenu} + [:span {:class (stl/css :submenu-icon-back)} i/arrow] parent-option]] - [:li {:class (if (and new-css-system workspace?) - (dom/classnames (css :separator) true) - (dom/classnames :separator true))}]]) + + [:li {:class (stl/css :separator)}]]) + (for [[index option] (d/enumerate (:options level))] (let [option-name (:option-name option) id (:id option) @@ -250,45 +234,37 @@ (when option-name (if (= option-name :separator) [:li {:key (dm/str "context-item-" index) - :class (if (and new-css-system workspace?) - (dom/classnames (css :separator) true) - (dom/classnames :separator true))}] + :class (stl/css :separator)}] [:& context-menu-a11y-item {:id id :key id - :class (if (and new-css-system workspace?) - (dom/classnames (css :is-selected) (and selected (= option-name selected)) - (css :context-menu-item) true) - (dom/classnames :is-selected (and selected (= option-name selected)))) + :class (stl/css-case + :is-selected (and selected (= option-name selected)) + :selected (and selected (= data-test selected)) + :context-menu-item true) :key-index (dm/str "context-item-" index) :tab-index "0" :on-key-down (fn [event] (dom/prevent-default event))} (if-not sub-options - [:a {:class (if (and new-css-system workspace?) - (dom/classnames (css :context-menu-action) true) - (dom/classnames :context-menu-action true)) + [:a {:class (stl/css :context-menu-action) :on-click #(do (dom/stop-propagation %) (on-close) (option-handler %)) :data-test data-test} (if (and in-dashboard? (= option-name "Default")) (tr "dashboard.default-team-name") - option-name)] - [:a {:class (if (and new-css-system workspace?) - (dom/classnames (css :context-menu-action) true - (css :submenu) true) - (dom/classnames :context-menu-action true - :submenu true)) + option-name) + + (when (and selected (= data-test selected)) + [:span {:class (stl/css :selected-icon)} i/tick])] + + [:a {:class (stl/css :context-menu-action :submenu) :data-no-close true :on-click (enter-submenu option-name sub-options) :data-test data-test} option-name - [:span {:class (dom/classnames (css :submenu-icon) (and new-css-system workspace?))} - (if (and new-css-system workspace?) - i/arrow-refactor - i/arrow-slide)]])]))))])])]))) - + [:span {:class (stl/css :submenu-icon)} i/arrow]])]))))])])]))) (mf/defc context-menu-a11y {::mf/wrap-props false} diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.css.json b/frontend/src/app/main/ui/components/context_menu_a11y.css.json deleted file mode 100644 index 480fdf318b..0000000000 --- a/frontend/src/app/main/ui/components/context_menu_a11y.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"components_context_menu_a11y_button-primary_FTrG6","button-secondary":"components_context_menu_a11y_button-secondary_tIeiM","button-icon":"components_context_menu_a11y_button-icon_eOLGl","button-icon-small":"components_context_menu_a11y_button-icon-small_bQvvB","context-menu":"components_context_menu_a11y_context-menu_bS2vM","context-menu-items":"components_context_menu_a11y_context-menu-items_lQC7H","context-menu-item":"components_context_menu_a11y_context-menu-item_E2GpJ","context-menu-action":"components_context_menu_a11y_context-menu-action_E53yg","submenu-back":"components_context_menu_a11y_submenu-back_AboXg","submenu-icon-back":"components_context_menu_a11y_submenu-icon-back_gy-B6","submenu":"components_context_menu_a11y_submenu_MuyM8","submenu-icon":"components_context_menu_a11y_submenu-icon_tWTVU","is-open":"components_context_menu_a11y_is-open_FbqIp","fixed":"components_context_menu_a11y_fixed_iJxPr","separator":"components_context_menu_a11y_separator_DrZoB","min-width":"components_context_menu_a11y_min-width_w-ron","is-selected":"components_context_menu_a11y_is-selected_UPMXx","is-selectable":"components_context_menu_a11y_is-selectable_n7sdb"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.scss b/frontend/src/app/main/ui/components/context_menu_a11y.scss index f62c2f3dc5..ea8a89cc5b 100644 --- a/frontend/src/app/main/ui/components/context_menu_a11y.scss +++ b/frontend/src/app/main/ui/components/context_menu_a11y.scss @@ -10,7 +10,7 @@ position: relative; visibility: hidden; opacity: $op-0; - z-index: $z-index-2; + z-index: $z-index-4; &.is-open { position: relative; @@ -23,6 +23,7 @@ } .context-menu-items { + @include menuShadow; position: absolute; top: $s-12; left: calc(-1 * $s-6); @@ -31,8 +32,8 @@ margin: $s-0; padding: $s-4; border-radius: $br-8; + border: $s-2 solid var(--panel-border-color); background-color: var(--menu-background-color); - box-shadow: 0px 0px $s-12 0px var(--menu-shadow-color); overflow: auto; & .separator { height: $s-12; @@ -45,7 +46,7 @@ .context-menu-item { display: flex; .context-menu-action { - @include titleTipography; + @include bodySmallTypography; display: flex; align-items: center; justify-content: flex-start; @@ -72,6 +73,9 @@ display: flex; align-items: center; font-weight: $fw700; + background: none; + border: none; + cursor: pointer; .submenu-icon-back svg { @extend .button-icon-small; stroke: var(--menu-foreground-color); @@ -82,12 +86,12 @@ &:hover .context-menu-action { background-color: var(--menu-background-color-hover); text-decoration: none; - color: var(--menu-foreground-color-hover); + color: var(--menu-foreground-color); &.submenu .submenu-icon svg { - stroke: var(--menu-foreground-color-hover); + stroke: var(--menu-foreground-color); } &.submenu-back .submenu-icon-back svg { - stroke: var(--menu-foreground-color-hover); + stroke: var(--menu-foreground-color); } } &:focus { @@ -109,6 +113,21 @@ } } } + &.selected { + .context-menu-action { + justify-content: space-between; + color: var(--menu-foreground-color-focus); + } + .selected-icon { + @extend .button-tag; + border-radius: $br-8; + height: 100%; + svg { + @extend .button-icon-small; + stroke: var(--menu-foreground-color-focus); + } + } + } } .is-selected .context-menu-action { padding-left: $s-28; diff --git a/frontend/src/app/main/ui/components/copy_button.cljs b/frontend/src/app/main/ui/components/copy_button.cljs index 571c37551e..912fcb9f9a 100644 --- a/frontend/src/app/main/ui/components/copy_button.cljs +++ b/frontend/src/app/main/ui/components/copy_button.cljs @@ -5,29 +5,43 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.components.copy-button + (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] + [app.main.data.events :as-alias ev] [app.main.ui.icons :as i] - [app.util.timers :as timers] + [app.util.dom :as dom] + [app.util.timers :as tm] [app.util.webapi :as wapi] - [beicon.core :as rx] [rumext.v2 :as mf])) -(mf/defc copy-button [{:keys [data on-copied]}] - (let [just-copied (mf/use-state false)] - (mf/use-effect - (mf/deps @just-copied) - (fn [] - (when @just-copied - (when (fn? on-copied) - (on-copied)) - (let [sub (timers/schedule 1000 #(reset! just-copied false))] - ;; On unmount we dispose the timer - #(rx/-dispose sub))))) +(mf/defc copy-button + {::mf/props :obj} + [{:keys [data on-copied children class]}] + (let [active* (mf/use-state false) + active? (deref active*) - [:button.copy-button - {:on-click #(when-not @just-copied - (reset! just-copied true) - (wapi/write-to-clipboard data))} - (if @just-copied - i/tick - i/copy)])) + class (dm/str class " " + (stl/css-case + :copy-button (not (some? children)) + :copy-wrapper (some? children))) + + on-click + (mf/use-fn + (mf/deps data) + (fn [event] + (when-not (dom/get-boolean-data event "active") + (reset! active* true) + (tm/schedule 1000 #(reset! active* false)) + (when (fn? on-copied) (on-copied event)) + (wapi/write-to-clipboard + (if (fn? data) (data) data)))))] + + [:button {:class class + :data-active (dm/str active?) + :on-click on-click} + children + [:span {:class (stl/css :icon-btn)} + (if active? + i/tick + i/clipboard)]])) diff --git a/frontend/src/app/main/ui/components/copy_button.scss b/frontend/src/app/main/ui/components/copy_button.scss new file mode 100644 index 0000000000..ec8e362bb5 --- /dev/null +++ b/frontend/src/app/main/ui/components/copy_button.scss @@ -0,0 +1,86 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.copy-button { + @include buttonStyle; + width: 100%; + height: $s-32; + border: $s-1 solid transparent; + border-radius: $br-8; + background-color: transparent; + box-sizing: border-box; + .icon-btn { + @include flexCenter; + height: $s-32; + min-width: $s-28; + width: $s-28; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } + + &:hover { + background-color: var(--color-background-tertiary); + color: var(--color-foreground-primary); + border: $s-1 solid var(--color-background-tertiary); + .icon-btn { + svg { + stroke: var(--button-tertiary-foreground-color-active); + } + } + } + &:focus, + &:focus-visible { + outline: none; + border: $s-1 solid var(--button-tertiary-border-color-focus); + background-color: transparent; + color: var(--button-tertiary-foreground-color-focus); + .icon-btn svg { + stroke: var(--button-tertiary-foreground-color-active); + } + } +} + +.copy-wrapper { + @include buttonStyle; + @include copyWrapperBase; + width: 100%; + height: fit-content; + text-align: left; + border: $s-1 solid transparent; + .icon-btn { + @include flexCenter; + position: absolute; + top: 0; + right: 0; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon-small; + stroke: var(--button-tertiary-foreground-color-focus); + display: none; + } + } + &:hover { + background-color: var(--button-tertiary-background-color-focus); + color: var(--button-tertiary-foreground-color-focus); + border: $s-1 solid var(--button-tertiary-background-color-focus); + .icon-btn svg { + display: flex; + } + } + + &:focus, + &:focus-visible { + outline: none; + border: $s-1 solid var(--button-tertiary-border-color-focus); + background-color: transparent; + color: var(--button-tertiary-foreground-color-focus); + } +} diff --git a/frontend/src/app/main/ui/components/dropdown.cljs b/frontend/src/app/main/ui/components/dropdown.cljs index 8be2f498d7..f27662421f 100644 --- a/frontend/src/app/main/ui/components/dropdown.cljs +++ b/frontend/src/app/main/ui/components/dropdown.cljs @@ -10,6 +10,7 @@ [app.util.dom :as dom] [app.util.globals :as globals] [app.util.keyboard :as kbd] + [app.util.timers :as tm] [goog.events :as events] [goog.object :as gobj] [rumext.v2 :as mf]) @@ -18,24 +19,26 @@ (mf/defc dropdown' {::mf/wrap-props false} [props] - (let [children (gobj/get props "children") - on-close (gobj/get props "on-close") - ref (gobj/get props "container") + (let [children (gobj/get props "children") + on-close (gobj/get props "on-close") + container-ref (gobj/get props "container") + listening-ref (mf/use-ref nil) on-click (fn [event] - (let [target (dom/get-target event) + (when (mf/ref-val listening-ref) + (let [target (dom/get-target event) - ;; MacOS ctrl+click sends two events: context-menu and click. - ;; In order to not have two handlings we ignore ctrl+click for this platform - mac-ctrl-click? (and (cfg/check-platform? :macos) (kbd/ctrl? event))] - (when (and (not mac-ctrl-click?) - (not (.-data-no-close ^js target))) - (if ref - (let [parent (mf/ref-val ref)] - (when-not (or (not parent) (.contains parent target)) - (on-close))) - (on-close))))) + ;; MacOS ctrl+click sends two events: context-menu and click. + ;; In order to not have two handlings we ignore ctrl+click for this platform + mac-ctrl-click? (and (cfg/check-platform? :macos) (kbd/ctrl? event))] + (when (and (not mac-ctrl-click?) + (not (.-data-no-close ^js target))) + (if container-ref + (let [parent (mf/ref-val container-ref)] + (when-not (or (not parent) (.contains parent target)) + (on-close))) + (on-close)))))) on-keyup (fn [event] @@ -47,8 +50,8 @@ (let [keys [(events/listen globals/document EventType.CLICK on-click) (events/listen globals/document EventType.CONTEXTMENU on-click) (events/listen globals/document EventType.KEYUP on-keyup)]] - #(doseq [key keys] - (events/unlistenByKey key))))] + (tm/schedule #(mf/set-ref-val! listening-ref true)) + #(run! events/unlistenByKey keys)))] (mf/use-effect on-mount) children)) diff --git a/frontend/src/app/main/ui/components/dropdown_menu.cljs b/frontend/src/app/main/ui/components/dropdown_menu.cljs index 75e32b60b1..156a1b651f 100644 --- a/frontend/src/app/main/ui/components/dropdown_menu.cljs +++ b/frontend/src/app/main/ui/components/dropdown_menu.cljs @@ -11,31 +11,18 @@ [app.util.dom :as dom] [app.util.globals :as globals] [app.util.keyboard :as kbd] + [app.util.object :as obj] [goog.events :as events] [goog.object :as gobj] [rumext.v2 :as mf]) (:import goog.events.EventType)) -(mf/defc dropdown-menu-item +(mf/defc dropdown-menu-item* {::mf/wrap-props false} [props] - - (let [children (gobj/get props "children") - on-click (gobj/get props "on-click") - on-key-down (gobj/get props "on-key-down") - id (gobj/get props "id") - klass (gobj/get props "klass") - key (gobj/get props "unique-key") - data-test (gobj/get props "data-test")] - [:li {:id id - :class klass - :tab-index "0" - :on-key-down on-key-down - :on-click on-click - :key key - :role "menuitem" - :data-test data-test} - children])) + (let [props (-> (obj/clone props) + (obj/set! "role" "menuitem"))] + [:> :li props])) (mf/defc dropdown-menu' {::mf/wrap-props false} @@ -45,7 +32,7 @@ ref (gobj/get props "container") ids (gobj/get props "ids") list-class (gobj/get props "list-class") - ids (filter some? ids) + ids (filter some? ids) on-click (fn [event] (let [target (dom/get-target event) @@ -82,7 +69,7 @@ actual-index (d/index-of ids actual-id) previous-id (if (= 0 actual-index) (last ids) - (nth ids (- actual-index 1)))] + (get ids (- actual-index 1) (last ids)))] (dom/focus! (dom/get-element previous-id)))) (when (kbd/down-arrow? event) @@ -91,25 +78,22 @@ actual-index (d/index-of ids actual-id) next-id (if (= (- len 1) actual-index) (first ids) - (nth ids (+ 1 actual-index)))] - (dom/focus! (dom/get-element next-id)))) + (get ids (+ 1 actual-index) (first ids))) + node-item (dom/get-element next-id)] + (dom/focus! node-item))) (when (kbd/tab? event) - (on-close)))) + (on-close))))] - on-mount - (fn [] - (let [keys [(events/listen globals/document EventType.CLICK on-click) - (events/listen globals/document EventType.CONTEXTMENU on-click) - (events/listen globals/document EventType.KEYUP on-keyup) - (events/listen globals/document EventType.KEYDOWN on-key-down)]] - #(doseq [key keys] - (events/unlistenByKey key))))] + (mf/with-effect [] + (let [keys [(events/listen globals/document EventType.CLICK on-click) + (events/listen globals/document EventType.CONTEXTMENU on-click) + (events/listen globals/document EventType.KEYUP on-keyup) + (events/listen globals/document EventType.KEYDOWN on-key-down)]] + #(doseq [key keys] + (events/unlistenByKey key)))) - (mf/use-effect on-mount) - [:ul {:class list-class - :role "menu"} - children])) + [:ul {:class list-class :role "menu"} children])) (mf/defc dropdown-menu {::mf/wrap-props false} @@ -117,5 +101,10 @@ (assert (fn? (gobj/get props "on-close")) "missing `on-close` prop") (assert (boolean? (gobj/get props "show")) "missing `show` prop") - (when (gobj/get props "show") - (mf/element dropdown-menu' props))) + (let [ids (obj/get props "ids") + ids (d/nilv ids (->> (obj/get props "children") + (keep #(obj/get-in % ["props" "id"]))))] + (when (gobj/get props "show") + (mf/element + dropdown-menu' + (mf/spread-props props {:ids ids}))))) diff --git a/frontend/src/app/main/ui/components/editable_label.cljs b/frontend/src/app/main/ui/components/editable_label.cljs index de24d8848c..32f8b3eb0a 100644 --- a/frontend/src/app/main/ui/components/editable_label.cljs +++ b/frontend/src/app/main/ui/components/editable_label.cljs @@ -5,7 +5,9 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.components.editable-label + (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.keyboard :as kbd] @@ -13,46 +15,91 @@ [rumext.v2 :as mf])) (mf/defc editable-label - [{:keys [value on-change on-cancel editing? disable-dbl-click? class-name] :as props}] - (let [display-value (get props :display-value value) - tooltip (get props :tooltip) - input (mf/use-ref nil) - state (mf/use-state (:editing false)) - is-editing (:editing @state) - start-editing (fn [] - (swap! state assoc :editing true) - (timers/schedule 100 #(dom/focus! (mf/ref-val input)))) - stop-editing (fn [] (swap! state assoc :editing false)) - accept-editing (fn [] - (when (:editing @state) - (let [value (-> (mf/ref-val input) dom/get-value)] - (on-change value) - (stop-editing)))) - cancel-editing (fn [] - (stop-editing) - (when on-cancel (on-cancel))) - on-dbl-click (fn [_] (when (not disable-dbl-click?) (start-editing))) - on-key-up (fn [e] - (cond - (kbd/esc? e) - (cancel-editing) + {::mf/wrap-props false} + [props] + (let [value (unchecked-get props "value") + on-change (unchecked-get props "on-change") + on-cancel (unchecked-get props "on-cancel") + editing? (unchecked-get props "editing") + dbl-click? (unchecked-get props "disable-dbl-click") + class (unchecked-get props "class") + tooltip (unchecked-get props "tooltip") + display-value (unchecked-get props "display-value") - (kbd/enter? e) - (accept-editing)))] - (mf/use-effect - (mf/deps editing?) - (fn [] - (when (and editing? (not (:editing @state))) - (start-editing)))) + final-class (dm/str class " " (stl/css :editable-label)) + input-ref (mf/use-ref nil) + internal-editing* (mf/use-state false) + internal-editing? (deref internal-editing*) - (if is-editing - [:div.editable-label {:class class-name} - [:input.editable-label-input {:ref input - :default-value value - :on-key-up on-key-up - :on-blur cancel-editing}] - [:span.editable-label-close {:on-click cancel-editing} i/close]] - [:span.editable-label {:class class-name - :title tooltip - :on-double-click on-dbl-click} display-value]))) + start-edition + (mf/use-fn + (fn [] + (reset! internal-editing* true) + (timers/schedule 100 (fn [] + (when-let [node (mf/ref-val input-ref)] + (dom/focus! node)))))) + + stop-edition + (mf/use-fn #(reset! internal-editing* false)) + + accept-edition + (mf/use-fn + (mf/deps internal-editing? on-change stop-edition) + (fn [] + (when internal-editing? + (let [value (dom/get-value (mf/ref-val input-ref))] + (when (fn? on-change) + (on-change value)) + + (stop-edition))))) + + cancel-edition + (mf/use-fn + (mf/deps stop-edition on-cancel) + (fn [] + (stop-edition) + (when (fn? on-cancel) + (on-cancel)))) + + + on-dbl-click + (mf/use-fn + (mf/deps dbl-click? start-edition) + (fn [_] + (when-not dbl-click? + (start-edition)))) + + on-key-up + (mf/use-fn + (mf/deps cancel-edition accept-edition) + (fn [event] + (cond + (kbd/esc? event) + (cancel-edition) + + (kbd/enter? event) + (accept-edition))))] + + (mf/with-effect [editing? internal-editing? start-edition] + (when (and editing? (not internal-editing?)) + (start-edition))) + + (if ^boolean internal-editing? + [:div {:class final-class} + [:input + {:class (stl/css :editable-label-input) + :ref input-ref + :default-value value + :on-key-up on-key-up + :on-double-click on-dbl-click + :on-blur cancel-edition}] + + [:span {:class (stl/css :editable-label-close) + :on-click cancel-edition} + i/delete-text]] + + [:span {:class final-class + :title tooltip + :on-double-click on-dbl-click} + display-value]))) diff --git a/frontend/src/app/main/ui/components/editable_label.scss b/frontend/src/app/main/ui/components/editable_label.scss new file mode 100644 index 0000000000..1f72eaf7ab --- /dev/null +++ b/frontend/src/app/main/ui/components/editable_label.scss @@ -0,0 +1,41 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.editable-label-input { + @include textEllipsis; + @include bodySmallTypography; + @include removeInputStyle; + flex-grow: 1; + height: $s-28; + max-width: calc(var(--parent-size) - (var(--depth) * var(--layer-indentation-size))); + margin: 0; + padding-left: $s-6; + border-radius: $br-4; + border: $s-1 solid var(--input-border-color-active); + color: var(--input-foreground-color-active); +} + +.editable-label { + display: flex; + + &.is-hidden { + display: none; + } +} + +.editable-label-close { + cursor: pointer; + + svg { + @extend .button-icon; + height: $s-12; + width: $s-12; + stroke: var(--icon-foreground); + margin: 0; + } +} diff --git a/frontend/src/app/main/ui/components/editable_select.cljs b/frontend/src/app/main/ui/components/editable_select.cljs index 32c43aff24..614fd7002c 100644 --- a/frontend/src/app/main/ui/components/editable_select.cljs +++ b/frontend/src/app/main/ui/components/editable_select.cljs @@ -5,12 +5,14 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.components.editable-select + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.math :as mth] [app.common.uuid :as uuid] [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.keyboard :as kbd] @@ -18,13 +20,17 @@ [rumext.v2 :as mf])) (mf/defc editable-select - [{:keys [value type options class on-change placeholder on-blur] :as params}] - (let [state (mf/use-state {:id (uuid/next) - :is-open? false - :current-value value - :top nil - :left nil - :bottom nil}) + [{:keys [value type options class on-change placeholder on-blur input-class] :as params}] + (let [state* (mf/use-state {:id (uuid/next) + :is-open? false + :current-value value + :top nil + :left nil + :bottom nil}) + state (deref state*) + is-open? (:is-open? state) + current-value (:current-value state) + element-id (:id state) min-val (get params :min) max-val (get params :max) @@ -32,13 +38,26 @@ emit-blur? (mf/use-ref nil) font-size-wrapper-ref (mf/use-ref) - open-dropdown #(swap! state assoc :is-open? true) - close-dropdown #(swap! state assoc :is-open? false) - select-item (fn [value] - (fn [_] - (swap! state assoc :current-value value) - (when on-change (on-change value)) - (when on-blur (on-blur)))) + toggle-dropdown + (mf/use-fn + (mf/deps state) + #(swap! state* update :is-open? not)) + + close-dropdown + (fn [event] + (dom/stop-propagation event) + (swap! state* assoc :is-open? false)) + + select-item + (mf/use-fn + (mf/deps on-change on-blur) + (fn [event] + (let [value (-> (dom/get-current-target event) + (dom/get-data "value") + (d/read-string))] + (swap! state* assoc :current-value value) + (when on-change (on-change value)) + (when on-blur (on-blur))))) as-key-value (fn [item] (if (map? item) [(:value item) (:label item)] [item item])) labels-map (into {} (map as-key-value) options) @@ -46,7 +65,7 @@ set-value (fn [value] - (swap! state assoc :current-value value) + (swap! state* assoc :current-value value) (when on-change (on-change value))) ;; TODO: why this method supposes that all editable select @@ -69,14 +88,14 @@ {:keys [left top height]} bounds bottom (when (< (- window-height top) 300) (- window-height top)) top (when (>= (- window-height top) 300) (+ top height))] - (swap! state + (swap! state* assoc :left left :top top :bottom bottom)))))) handle-key-down - (mf/use-callback + (mf/use-fn (mf/deps set-value) (fn [event] (when (= type "number") @@ -107,12 +126,12 @@ (set-value new-value))))))) handle-focus - (mf/use-callback + (mf/use-fn (fn [] (mf/set-ref-val! emit-blur? false))) handle-blur - (mf/use-callback + (mf/use-fn (fn [] (mf/set-ref-val! emit-blur? true) (timers/schedule @@ -121,56 +140,63 @@ (when (and on-blur (mf/ref-val emit-blur?)) (on-blur))))))] (mf/use-effect - (mf/deps value (:current-value @state)) - #(when (not= (str value) (:current-value @state)) - (reset! state {:current-value value}))) + (mf/deps value current-value) + #(when (not= (str value) current-value) + (reset! state* {:current-value value}))) - (mf/with-effect [(:is-open? @state)] + (mf/with-effect [is-open?] (let [wrapper-node (mf/ref-val font-size-wrapper-ref) node (dom/get-element-by-class "checked-element is-selected" wrapper-node) nodes (dom/get-elements-by-class "checked-element-value" wrapper-node) closest (fn [a b] (first (sort-by #(mth/abs (- % b)) a))) closest-value (str (closest options value))] - (when (:is-open? @state) + (when is-open? (if (some? node) (dom/scroll-into-view-if-needed! node) (some->> nodes (d/seek #(= closest-value (dom/get-inner-text %))) (dom/scroll-into-view-if-needed!))))) - (mf/set-ref-val! emit-blur? (not (:is-open? @state)))) + (mf/set-ref-val! emit-blur? (not is-open?))) - [:div.editable-select {:class class - :ref on-node-load} + + [:div {:class (dm/str class " " (stl/css :editable-select)) + :ref on-node-load} (if (= type "number") - [:> numeric-input {:value (or (some-> @state :current-value value->label) "") - :on-change set-value - :on-focus handle-focus - :on-blur handle-blur - :placeholder placeholder}] - [:input.input-text {:value (or (some-> @state :current-value value->label) "") - :on-change handle-change-input - :on-key-down handle-key-down + [:> numeric-input* {:value (or (some-> current-value value->label) "") + :className input-class + :on-change set-value :on-focus handle-focus :on-blur handle-blur - :placeholder placeholder - :type type}]) - [:span.dropdown-button {:on-click open-dropdown} i/arrow-down] + :placeholder placeholder}] + [:input {:value (or (some-> current-value value->label) "") + :class input-class + :on-change handle-change-input + :on-key-down handle-key-down + :on-focus handle-focus + :on-blur handle-blur + :placeholder placeholder + :type type}]) - [:& dropdown {:show (get @state :is-open? false) + [:span {:class (stl/css :dropdown-button) + :on-click toggle-dropdown} + i/arrow] + + [:& dropdown {:show (or is-open? false) :on-close close-dropdown} - [:ul.custom-select-dropdown {:style {:position "fixed" - :top (:top @state) - :left (:left @state) - :bottom (:bottom @state) - :ref font-size-wrapper-ref}} + [:ul {:class (stl/css :custom-select-dropdown) + :ref font-size-wrapper-ref} (for [[index item] (map-indexed vector options)] (if (= :separator item) - [:hr {:key (str (:id @state) "-" index)}] + [:li {:class (stl/css :separator) + :key (dm/str element-id "-" index)}] (let [[value label] (as-key-value item)] - [:li.checked-element - {:key (str (:id @state) "-" index) - :class (when (= (str value) (-> @state :current-value)) "is-selected") - :on-click (select-item value)} - [:span.check-icon i/tick] - [:span.checked-element-value label]])))]]])) + [:li + {:key (str element-id "-" index) + :class (stl/css-case :dropdown-element true + :is-selected (= (dm/str value) current-value)) + :data-value value + :on-click select-item} + [:span {:class (stl/css :label)} label] + [:span {:class (stl/css :check-icon)} + i/tick]])))]]])) diff --git a/frontend/src/app/main/ui/components/editable_select.scss b/frontend/src/app/main/ui/components/editable_select.scss new file mode 100644 index 0000000000..6edb393932 --- /dev/null +++ b/frontend/src/app/main/ui/components/editable_select.scss @@ -0,0 +1,70 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.editable-select { + @extend .asset-element; + margin: 0; + padding: 0; + border: $s-1 solid var(--input-border-color); + position: relative; + display: flex; + height: $s-32; + width: 100%; + padding: $s-8; + border-radius: $br-8; + cursor: pointer; + .dropdown-button { + @include flexCenter; + svg { + @extend .button-icon-small; + transform: rotate(90deg); + stroke: var(--icon-foreground); + } + } + + .custom-select-dropdown { + @extend .dropdown-wrapper; + max-height: $s-320; + .separator { + margin: 0; + height: $s-12; + } + .dropdown-element { + @extend .dropdown-element-base; + color: var(--menu-foreground-color-rest); + .label { + flex-grow: 1; + width: 100%; + } + + .check-icon { + @include flexCenter; + svg { + @extend .button-icon-small; + visibility: hidden; + stroke: var(--icon-foreground); + } + } + + &.is-selected { + color: var(--menu-foreground-color); + .check-icon svg { + stroke: var(--menu-foreground-color); + visibility: visible; + } + } + &:hover { + background-color: var(--menu-background-color-hover); + color: var(--menu-foreground-color-hover); + .check-icon svg { + stroke: var(--menu-foreground-color-hover); + } + } + } + } +} diff --git a/frontend/src/app/main/ui/components/file_uploader.cljs b/frontend/src/app/main/ui/components/file_uploader.cljs index 71af00d4ed..35429e09ee 100644 --- a/frontend/src/app/main/ui/components/file_uploader.cljs +++ b/frontend/src/app/main/ui/components/file_uploader.cljs @@ -30,15 +30,14 @@ (when label-text [:label {:for input-id :class-name label-class} label-text]) - [:input - {:style {:display "none" - :width 0} - :id input-id - :multiple multi - :accept accept - :type "file" - :ref input-ref - :on-change on-files-selected - :data-test data-test - :aria-label "uploader"}]])) + [:input {:style {:display "none" + :width 0} + :id input-id + :multiple multi + :accept accept + :type "file" + :ref input-ref + :on-change on-files-selected + :data-test data-test + :aria-label "uploader"}]])) diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index 9c415a4e5f..9aec3f3022 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -5,9 +5,11 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.components.forms + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.main.ui.components.select :as cs] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.util.dom :as dom] @@ -24,7 +26,7 @@ (def use-form fm/use-form) (mf/defc input - [{:keys [label help-icon disabled form hint trim children data-test on-change-value] :as props}] + [{:keys [label help-icon disabled form hint trim children data-test on-change-value placeholder show-success?] :as props}] (let [input-type (get props :type "text") input-name (get props :name) more-classes (get props :class) @@ -40,37 +42,27 @@ is-text? (or (= @type' "password") (= @type' "text") (= @type' "email")) + placeholder (when is-text? (or placeholder label)) touched? (get-in @form [:touched input-name]) error (get-in @form [:errors input-name]) + value (get-in @form [:data input-name] "") help-icon' (cond (and (= input-type "password") (= @type' "password")) - i/eye + i/shown (and (= input-type "password") (= @type' "text")) - i/eye-closed + i/hide :else help-icon) on-change-value (or on-change-value (constantly nil)) - klass (str more-classes " " - (dom/classnames - :focus @focus? - :valid (and touched? (not error)) - :invalid (and touched? error) - :disabled disabled - :empty (and is-text? (str/empty? value)) - :with-icon (not (nil? help-icon')) - :custom-input is-text? - :input-radio is-radio? - :input-checkbox is-checkbox?)) - swap-text-password (fn [] (swap! type' (fn [input-type] @@ -87,9 +79,7 @@ on-blur (fn [_] - (reset! focus? false) - (when-not (get-in @form [:touched input-name]) - (swap! form assoc-in [:touched input-name] true))) + (reset! focus? false)) on-click (fn [_] @@ -97,46 +87,79 @@ (swap! form assoc-in [:touched input-name] true))) props (-> props - (dissoc :help-icon :form :trim :children) + (dissoc :help-icon :form :trim :children :show-success? :auto-focus? :label) (assoc :id (name input-name) :value value :auto-focus auto-focus? :on-click (when (or is-radio? is-checkbox?) on-click) :on-focus on-focus :on-blur on-blur - :placeholder label + :placeholder placeholder :on-change on-change :type @type' :tab-index "0") (cond-> (and value is-checkbox?) (assoc :default-checked value)) (cond-> (and touched? (:message error)) (assoc "aria-invalid" "true" "aria-describedby" (dm/str "error-" input-name))) - (obj/clj->props))] + (obj/clj->props)) - [:div - {:class klass} + checked? (and is-checkbox? (= value true)) + show-valid? (and show-success? touched? (not error)) + show-invalid? (and touched? error)] + + [:div {:class (dm/str more-classes " " + (stl/css-case + :input-wrapper true + :valid show-valid? + :invalid show-invalid? + :checkbox is-checkbox? + :disabled disabled))} [:* - [:> :input props] (cond (some? label) - [:label {:for (name input-name)} label] + [:label {:class (stl/css-case :input-with-label (not is-checkbox?) + :input-label is-text? + :radio-label is-radio? + :checkbox-label is-checkbox?) + :tab-index "-1" + :for (name input-name)} label + + (when is-checkbox? + [:span {:class (stl/css-case :global/checked checked?)} (when checked? i/status-tick)]) + + (if is-checkbox? + [:> :input props] + + [:div {:class (stl/css :input-and-icon)} + [:> :input props] + (when help-icon' + [:span {:class (stl/css :help-icon) + :on-click (when (= "password" input-type) + swap-text-password)} + help-icon']) + + (when show-valid? + [:span {:class (stl/css :valid-icon)} + i/tick]) + + (when show-invalid? + [:span {:class (stl/css :invalid-icon)} + i/close])])] (some? children) - [:label {:for (name input-name)} children]) + [:label {:for (name input-name)} + [:> :input props] + children]) - (when help-icon' - [:div.help-icon - {:style {:cursor "pointer"} - :on-click (when (= "password" input-type) - swap-text-password)} - help-icon']) (cond (and touched? (:message error)) - [:span.error {:id (dm/str "error-" input-name) - :data-test (clojure.string/join [data-test "-error"])} (tr (:message error))] + [:div {:id (dm/str "error-" input-name) + :class (stl/css :error) + :data-test (clojure.string/join [data-test "-error"])} + (tr (:message error))] (string? hint) - [:span.hint hint])]])) + [:div {:class (stl/css :hint)} hint])]])) (mf/defc textarea [{:keys [label disabled form hint trim] :as props}] @@ -180,112 +203,144 @@ :on-change on-change) (obj/clj->props))] - [:div.custom-input - {:class klass} - [:* - [:label label] - [:> :textarea props] - (cond - (and touched? (:message error)) - [:span.error (tr (:message error))] + [:div {:class (dm/str klass " " (stl/css :textarea-wrapper))} + [:label {:class (stl/css :textarea-label)} label] + [:> :textarea props] + (cond + (and touched? (:message error)) + [:span {:class (stl/css :error)} (tr (:message error))] - (string? hint) - [:span.hint hint])]])) + (string? hint) + [:span {:class (stl/css :hint)} hint])])) (mf/defc select - [{:keys [options disabled label form default data-test] :as props + [{:keys [options disabled form default dropdown-class] :as props :or {default ""}}] (let [input-name (get props :name) form (or form (mf/use-ctx form-ctx)) value (or (get-in @form [:data input-name]) default) - cvalue (d/seek #(= value (:value %)) options) - focus? (mf/use-state false) - on-change + + handle-change (fn [event] - (let [target (dom/get-target event) - value (dom/get-value target)] - (fm/on-input-change form input-name value))) + (let [value (if (string? event) event (dom/get-target-val event))] + (fm/on-input-change form input-name value)))] - on-focus - (fn [_] - (reset! focus? true)) - - on-blur - (fn [_] - (reset! focus? false))] - - [:div.custom-select - [:select {:value value - :on-change on-change - :on-focus on-focus - :on-blur on-blur - :disabled disabled - :data-test data-test} - (for [item options] - [:> :option (clj->js (cond-> {:key (:value item) :value (:value item)} - (:disabled item) (assoc :disabled "disabled") - (:hidden item) (assoc :style {:display "none"}))) - (:label item)])] - - [:div.input-container {:class (dom/classnames :disabled disabled :focus @focus?)} - [:div.main-content - [:label label] - [:span.value (:label cvalue "")]] - - [:div.icon - i/arrow-slide]]])) + [:div {:class (stl/css :select-wrapper)} + [:& cs/select + {:default-value value + :disabled disabled + :options options + :dropdown-class dropdown-class + :on-change handle-change}]])) (mf/defc radio-buttons - [{:keys [name options form trim on-change-value] :as props}] - (let [form (or form (mf/use-ctx form-ctx)) - value (get-in @form [:data name] "") - on-change-value (or on-change-value (constantly nil)) - on-change (fn [event] - (let [value (-> event dom/get-target dom/get-value)] - (swap! form assoc-in [:touched name] true) - (fm/on-input-change form name value trim) - (on-change-value name value)))] - [:div.custom-radio - (for [item options] - (let [id (str/ffmt "%-%" name (:value item)) - image (:image item)] - [:div.input-radio {:key id :class (when image "with-image")} - [:input {:on-change on-change - :type "radio" - :id id - :name name - :value (:value item) - :checked (= value (:value item))}] - [:label {:for id - :style {:background-image (when image (str/ffmt "url(%)" image))} - :class (when image "with-image")} - (:label item)]]))])) + {::mf/wrap-props false} + [props] + (let [form (or (unchecked-get props "form") + (mf/use-ctx form-ctx)) + name (unchecked-get props "name") + image (unchecked-get props "image") -(mf/defc submit-button - [{:keys [label form on-click disabled data-test] :as props}] - (let [form (or form (mf/use-ctx form-ctx))] - [:input.btn-primary.btn-large - {:name "submit" - :class (when (or (not (:valid @form)) (true? disabled)) "btn-disabled") - :disabled (or (not (:valid @form)) (true? disabled)) - :tab-index "0" - :on-click on-click - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-click))) - :value label - :data-test data-test - :type "submit"}])) + current-value (or (dm/get-in @form [:data name] "") + (unchecked-get props "value")) + on-change (unchecked-get props "on-change") + options (unchecked-get props "options") + trim? (unchecked-get props "trim") + class (unchecked-get props "class") + encode-fn (d/nilv (unchecked-get props "encode-fn") identity) + decode-fn (d/nilv (unchecked-get props "decode-fn") identity) + + on-change' + (mf/use-fn + (mf/deps on-change form name) + (fn [event] + (let [value (-> event dom/get-target dom/get-value decode-fn)] + (when (some? form) + (swap! form assoc-in [:touched name] true) + (fm/on-input-change form name value trim?)) + + (when (fn? on-change) + (on-change name value)))))] + [:div {:class (if image + class + (dm/str class " " (stl/css :custom-radio)))} + (for [{:keys [image icon value label area]} options] + (let [image? (some? image) + icon? (some? icon) + value' (encode-fn value) + checked? (= value current-value) + key (str/ffmt "%-%" (d/name name) (d/name value'))] + [:label {:for key + :key key + :style {:grid-area area} + :class (stl/css-case :radio-label true + :global/checked checked? + :with-image (or image? icon?))} + (cond + image? + [:span {:style {:background-image (str/ffmt "url(%)" image)} + :class (stl/css :image-inside)}] + icon? + [:span {:class (stl/css :icon-inside)} icon] + + :else + [:span {:class (stl/css-case :radio-icon true + :global/checked checked?)} + (when checked? [:span {:class (stl/css :radio-dot)}])]) + + label + [:input {:on-change on-change' + :type "radio" + :class (stl/css :radio-input) + :id key + :name name + :value value' + :checked checked?}]]))])) + +(mf/defc submit-button* + {::mf/wrap-props false} + [{:keys [on-click children label form class name disabled] :as props}] + (let [form (or form (mf/use-ctx form-ctx)) + + disabled? (or (and (some? form) (not (:valid @form))) + (true? disabled)) + + class (d/nilv class (stl/css :button-submit)) + + name (d/nilv name "submit") + + on-key-down + (mf/use-fn + (mf/deps on-click) + (fn [event] + (when (and (kbd/enter? event) (fn? on-click)) + (on-click event)))) + + props + (mf/spread-props props {:children mf/undefined + :disabled disabled? + :on-key-down on-key-down + :name name + :labek mf/undefined + :class class + :type "submit"})] + + [:> "button" props + (if (some? children) + children + [:span label])])) (mf/defc form - [{:keys [on-submit form children class] :as props}] - (let [on-submit (or on-submit (constantly nil))] + {::mf/wrap-props false} + [{:keys [on-submit form children class]}] + (let [on-submit' (mf/use-fn + (mf/deps on-submit) + (fn [event] + (dom/prevent-default event) + (when (fn? on-submit) + (on-submit form event))))] [:& (mf/provider form-ctx) {:value form} - [:form {:class class - :on-submit (fn [event] - (dom/prevent-default event) - (on-submit form event))} - children]])) + [:form {:class class :on-submit on-submit'} children]])) (defn- conj-dedup "A helper that adds item into a vector and removes possible @@ -309,18 +364,21 @@ empty? (and (str/empty? @value) (zero? (count @items))) - klass (str (get props :class) " " - (dom/classnames - :focus @focus? - :valid (and touched? (not error)) - :invalid (and touched? error) - :empty empty? - :custom-multi-input true - :custom-input true)) + klass (str (get props :class) " " + (stl/css-case + :focus @focus? + :valid (and touched? (not error)) + :invalid (and touched? error) + :empty empty? + :custom-multi-input true)) - in-klass (str class " " - (dom/classnames - :no-padding (pos? (count @items)))) + in-klass (str class " " + (stl/css-case + :inside-input true + :no-padding (pos? (count @items)) + :invalid (and (some? valid-item-fn) + touched? + (not (valid-item-fn @value))))) on-focus (mf/use-fn #(reset! focus? true)) @@ -342,27 +400,38 @@ (mf/use-fn (mf/deps @value) (fn [event] - (cond - (or (kbd/enter? event) - (kbd/comma? event)) - (do - (dom/prevent-default event) - (dom/stop-propagation event) - (let [val (cond-> @value trim str/trim)] + (let [val (cond-> @value trim str/trim)] + (cond + (or (kbd/enter? event) (kbd/comma? event) (kbd/space? event)) + (do + (dom/prevent-default event) + (dom/stop-propagation event) + + ;; Once enter/comma is pressed we mark it as touched + (swap! form assoc-in [:touched input-name] true) + + ;; Empty values means "submit" the form (whent some items have been added (when (and (kbd/enter? event) (str/empty? @value) (not-empty @items)) (on-submit form)) - (when (not (str/empty? @value)) - (reset! value "") - (swap! items conj-dedup {:text val - :valid (valid-item-fn val) - :caution (caution-item-fn val)})))) - (and (kbd/backspace? event) - (str/empty? @value)) - (do - (dom/prevent-default event) - (dom/stop-propagation event) - (swap! items (fn [items] (if (c/empty? items) items (pop items)))))))) + ;; If we have a string in the input we add it only if valid + (when (and (valid-item-fn val) (not (str/empty? @value))) + (reset! value "") + + ;; Once added the form is back as "untouched" + (swap! form assoc-in [:touched input-name] false) + + ;; This split will allow users to copy comma/space separated values of emails + (doseq [val (str/split val #",|\s+")] + (swap! items conj-dedup {:text (str/trim val) + :valid (valid-item-fn val) + :caution (caution-item-fn val)})))) + + (and (kbd/backspace? event) (str/empty? @value)) + (do + (dom/prevent-default event) + (dom/stop-propagation event) + (swap! items (fn [items] (if (c/empty? items) items (pop items))))))))) on-blur (mf/use-fn @@ -402,15 +471,18 @@ [:label {:for (name input-name)} label] (when-let [items (seq @items)] - [:div.selected-items + [:div {:class (stl/css :selected-items)} (for [item items] - [:div.selected-item {:key (:text item) - :tab-index "0" - :on-key-down (partial manage-key-down item)} - [:span.around {:class (dom/classnames "invalid" (not (:valid item)) - "caution" (:caution item))} - [:span.text (:text item)] - [:span.icon {:on-click #(remove-item! item)} i/cross]]])])])) + [:div {:class (stl/css :selected-item) + :key (:text item) + :tab-index "0" + :on-key-down (partial manage-key-down item)} + [:span {:class (stl/css-case :around true + :invalid (not (:valid item)) + :caution (:caution item))} + [:span {:class (stl/css :text)} (:text item)] + [:button {:class (stl/css :icon) + :on-click #(remove-item! item)} i/close]]])])])) ;; --- Validators @@ -427,7 +499,7 @@ (> (count value) length)) (defn validate-length - [field length errors-msg ] + [field length errors-msg] (fn [errors data] (cond-> errors (max-length? (get data field) length) @@ -446,6 +518,6 @@ (let [value (get data field)] (cond-> errors (and - (all-spaces? value) - (> (count value) 0)) + (all-spaces? value) + (> (count value) 0)) (assoc field {:message error-msg}))))) diff --git a/frontend/src/app/main/ui/components/forms.scss b/frontend/src/app/main/ui/components/forms.scss new file mode 100644 index 0000000000..d29571542a --- /dev/null +++ b/frontend/src/app/main/ui/components/forms.scss @@ -0,0 +1,450 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +// INPUT +.input-wrapper { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + &.valid { + input { + border: $s-1 solid var(--input-border-color-success); + @extend .disabled-input; + &:hover, + &:focus { + border: $s-1 solid var(--input-border-color-success); + } + } + } + &.invalid { + input { + border: $s-1 solid var(--input-border-color-error); + @extend .disabled-input; + &:hover, + &:focus { + border: $s-1 solid var(--input-border-color-error); + } + } + } + &.valid .help-icon, + &.invalid .help-icon { + right: $s-40; + } +} + +.input-with-label { + @include flexColumn; + gap: $s-8; + @include bodySmallTypography; + justify-content: flex-start; + align-items: flex-start; + height: 100%; + width: 100%; + padding: 0; + cursor: pointer; + color: var(--modal-title-foreground-color); + text-transform: uppercase; + input { + @extend .input-element; + color: var(--input-foreground-color-active); + margin-top: 0; + width: 100%; + height: 100%; + padding: 0 $s-8; + + &:focus { + outline: none; + border: $s-1 solid var(--input-border-color-focus); + border-radius: $br-8; + } + } + // Input autofill + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + input:-webkit-autofill:active { + -webkit-text-fill-color: var(--input-foreground-color-active); + -webkit-box-shadow: inset 0 0 20px 20px var(--input-background-color); + border: $s-1 solid var(--input-border-color); + -webkit-background-clip: text; + transition: background-color 5000s ease-in-out 0s; + caret-color: var(--input-foreground-color-active); + } +} + +.input-and-icon { + position: relative; + width: var(--input-width, calc(100% - $s-1)); + min-width: var(--input-min-width); + height: var(--input-height, $s-32); +} + +.help-icon { + cursor: pointer; + position: absolute; + right: $s-16; + top: calc(50% - $s-8); + svg { + @extend .button-icon-small; + stroke: $df-secondary; + width: $s-16; + height: $s-16; + } +} + +.invalid-icon { + width: $s-16; + height: $s-16; + background: var(--input-border-color-error); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + position: absolute; + right: $s-16; + top: calc(50% - $s-8); + svg { + width: $s-12; + height: $s-12; + stroke: var(--input-background-color); + } +} + +.valid-icon { + width: $s-16; + height: $s-16; + background: var(--input-border-color-success); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + position: absolute; + right: $s-16; + top: calc(50% - $s-8); + svg { + width: $s-12; + height: $s-12; + fill: var(--input-border-color-success); + stroke: var(--input-background-color); + } +} + +.error { + color: var(--input-border-color-error); + width: 100%; + font-size: $fs-14; +} + +.hint { + @include bodySmallTypography; + width: 99%; + margin-block-start: $s-8; + color: var(--modal-text-foreground-color); +} + +.checkbox { + @extend .input-checkbox; + .checkbox-label { + @include bodySmallTypography; + display: flex; + align-items: center; + flex-direction: row-reverse; + gap: $s-6; + min-height: $s-32; + cursor: pointer; + span { + @extend .checkbox-icon; + } + input { + display: none !important; + } + &:hover { + span { + border-color: var(--input-checkbox-border-color-hover); + } + } + } +} + +// SELECT +.custom-select { + @extend .select-wrapper; + height: $s-32; + .input-container { + @include flexRow; + height: $s-32; + width: 100%; + border-radius: $br-8; + border: $s-1 solid var(--input-border-color); + color: var(--input-foreground-color-active); + background-color: var(--input-background-color); + .main-content { + @include flexColumn; + @include bodySmallTypography; + position: relative; + justify-content: center; + flex-grow: 1; + height: 100%; + padding: $s-8; + + .label { + color: var(--input-foreground-color); + } + .value { + width: 100%; + padding: 0px; + margin: 0px; + border: 0px; + color: var(--input-foreground-color-active); + } + } + .icon { + @include flexCenter; + height: $s-32; + width: $s-24; + pointer-events: none; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + transform: rotate(90deg); + } + } + + &.disabled { + background-color: var(--input-background-color-disabled); + border: $s-1 solid var(--input-border-color-disabled); + color: var(--input-foreground-color-disabled); + } + &.focus { + outline: none; + color: var(--input-foreground-color-active); + background-color: var(--input-background-color-active); + border: $s-1 solid var(--input-border-color-active); + } + } + + select { + @extend .menu-dropdown; + @include bodySmallTypography; + box-sizing: border-box; + position: absolute; + top: 0; + left: 0; + min-height: $s-32; + height: auto; + width: calc(100% - 1px); + padding: 0 $s-12; + margin: 0; + border: none; + opacity: 0; + z-index: $z-index-10; + background-color: transparent; + cursor: pointer; + option { + @include bodySmallTypography; + color: var(--title-foreground-color-hover); + background-color: var(--menu-background-color); + appearance: none; + height: $s-32; + } + } +} + +// SUBMIT-BUTTON +.button-submit { + @extend .button-primary; +} + +:disabled { + @extend .button-disabled; + min-height: $s-32; +} + +// MULTI INPUT +.custom-multi-input { + display: flex; + flex-direction: column; + position: relative; + min-height: $s-40; + max-height: $s-180; + width: 100%; + overflow-y: hidden; + .inside-input { + @include removeInputStyle; + @include bodySmallTypography; + @include textEllipsis; + width: 100%; + max-width: calc(100% - $s-1); + min-height: $s-32; + padding-top: 0; + height: $s-32; + padding: $s-8; + margin: 0; + border-radius: $br-8; + color: var(--input-foreground-color-active); + background-color: var(--input-background-color); + border: $s-1 solid var(--input-border-color-active); + &:focus { + outline: none; + border: $s-1 solid var(--input-border-color-focus); + } + &.invalid { + border: $s-1 solid var(--input-border-color-error); + &:hover, + &:focus { + border: $s-1 solid var(--input-border-color-error); + } + } + } + label { + display: none; + } + .selected-items { + display: flex; + flex-wrap: wrap; + gap: $s-4; + max-height: $s-136; + padding: $s-4 0; + overflow-y: scroll; + .selected-item { + .around { + @include flexRow; + height: $s-24; + width: fit-content; + padding-left: $s-6; + border-radius: $br-6; + background-color: var(--pill-background-color); + border: $s-1 solid var(--pill-background-color); + box-sizing: border-box; + .text { + @include bodySmallTypography; + padding-right: $s-8; + color: var(--pill-foreground-color); + } + + .icon { + @include flexCenter; + @include buttonStyle; + height: $s-32; + width: $s-24; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } + &.invalid { + background-color: var(--status-widget-background-color-error); + .text { + color: var(--alert-text-foreground-color-error); + } + .icon svg { + stroke: var(--alert-text-foreground-color-error); + } + } + } + } + } +} + +// RADIO BUTTONS +.custom-radio { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: $s-16; +} + +.radio-label { + @include bodySmallTypography; + @include flexRow; + align-items: flex-start; + gap: $s-8; + min-height: $s-32; + height: fit-content; + border-radius: $br-8; + padding: $s-8; + color: var(--input-foreground-color); + border: $s-1 solid transparent; + &:focus, + &:focus-within { + outline: none; + border: $s-1 solid var(--input-border-color-active); + } +} + +.radio-dot { + height: $s-8; + width: $s-8; + border-radius: $br-circle; + background-color: var(--color-background-tertiary); +} + +.radio-input { + width: 0; + margin: 0; +} + +.radio-icon { + @extend .checkbox-icon; + border-radius: $br-circle; +} + +.radio-label.with-image { + @include smallTitleTipography; + display: grid; + grid-template-rows: auto auto 0px; + justify-items: center; + gap: 0; + height: $s-116; + width: $s-92; + border-radius: $br-8; + margin: 0; + border: 1px solid var(--color-background-tertiary); + cursor: pointer; + &:global(.checked) { + border: 1px solid var(--color-accent-primary); + } + &:focus, + &:focus-within { + outline: none; + border: $s-1 solid var(--input-border-color-active); + } +} + +.image-inside { + width: $s-60; + height: $s-48; + background-size: $s-48; + background-repeat: no-repeat; + background-position: center; +} + +.icon-inside { + width: $s-60; + height: $s-48; + svg { + width: $s-60; + height: $s-48; + stroke: var(--icon-foreground); + fill: none; + } +} + +//TEXTAREA + +.textarea-label { + @include uppercaseTitleTipography; + color: var(--modal-title-foreground-color); + text-transform: uppercase; + margin-bottom: $s-8; +} + +.textarea-wrapper { + display: grid; + grid-template-rows: auto 1fr; +} diff --git a/frontend/src/app/main/ui/components/link.cljs b/frontend/src/app/main/ui/components/link.cljs index 3e845628e2..4c48681bba 100644 --- a/frontend/src/app/main/ui/components/link.cljs +++ b/frontend/src/app/main/ui/components/link.cljs @@ -6,16 +6,19 @@ (ns app.main.ui.components.link (:require + [app.common.data :as d] [app.util.keyboard :as kbd] [rumext.v2 :as mf])) -(mf/defc link [{:keys [action klass data-test keyboard-action children]}] - (let [keyboard-action (or keyboard-action action)] +(mf/defc link + {::mf/wrap-props false} + [{:keys [action class data-test keyboard-action children]}] + (let [keyboard-action (d/nilv keyboard-action action)] [:a {:on-click action - :class klass + :class class :on-key-down (fn [event] - (when (kbd/enter? event) + (when ^boolean (kbd/enter? event) (keyboard-action event))) :tab-index "0" :data-test data-test} - [:* children]])) + children])) diff --git a/frontend/src/app/main/ui/components/link_button.cljs b/frontend/src/app/main/ui/components/link_button.cljs new file mode 100644 index 0000000000..eb8c5db8e6 --- /dev/null +++ b/frontend/src/app/main/ui/components/link_button.cljs @@ -0,0 +1,27 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.components.link-button + (:require + [app.util.keyboard :as kbd] + [rumext.v2 :as mf])) + +(mf/defc link-button + {::mf/wrap-props false} + [{:keys [on-click class value data-test]}] + (let [on-key-down (mf/use-fn + (mf/deps on-click) + (fn [event] + (when (kbd/enter? event) + (when (fn? on-click) + (on-click event)))))] + [:input {:type "button" + :class class + :value value + :tab-index "0" + :on-click on-click + :on-key-down on-key-down + :data-test data-test}])) diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index b446920dfd..134a01440b 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -7,197 +7,207 @@ (ns app.main.ui.components.numeric-input (:require [app.common.data :as d] - [app.common.spec :as us] + [app.common.schema :as sm] [app.main.ui.formats :as fmt] + [app.main.ui.hooks :as h] [app.util.dom :as dom] [app.util.globals :as globals] [app.util.keyboard :as kbd] [app.util.object :as obj] - [app.util.simple-math :as sm] + [app.util.simple-math :as smt] + [cljs.core :as c] [cuerdas.core :as str] [goog.events :as events] - [rumext.v2 :as mf]) - (:import goog.events.EventType)) + [rumext.v2 :as mf])) -(mf/defc numeric-input +(mf/defc numeric-input* {::mf/wrap-props false ::mf/forward-ref true} [props external-ref] - (let [value-str (obj/get props "value") - min-val-str (obj/get props "min") - max-val-str (obj/get props "max") - step-val-str (obj/get props "step") - wrap-value? (obj/get props "data-wrap") - on-change (obj/get props "onChange") - on-blur (obj/get props "onBlur") - on-focus (obj/get props "onFocus") - title (obj/get props "title") - default-val (obj/get props "default") - nillable (obj/get props "nillable") - select-on-focus? (obj/get props "data-select-on-focus" true) + (let [value-str (unchecked-get props "value") + min-value (unchecked-get props "min") + max-value (unchecked-get props "max") + step-value (unchecked-get props "step") + wrap-value? (unchecked-get props "data-wrap") + on-change (unchecked-get props "onChange") + on-blur (unchecked-get props "onBlur") + on-focus (unchecked-get props "onFocus") + + title (unchecked-get props "title") + default (unchecked-get props "default") + nillable? (unchecked-get props "nillable") + class (d/nilv (unchecked-get props "className") "input-text") + + min-value (d/parse-double min-value) + max-value (d/parse-double max-value) + step-value (d/parse-double step-value 1) + default (d/parse-double default (when-not nillable? 0)) + + select-on-focus? (d/nilv (unchecked-get props "selectOnFocus") true) ;; We need a ref pointing to the input dom element, but the user ;; of this component may provide one (that is forwarded here). ;; So we use the external ref if provided, and the local one if not. - local-ref (mf/use-ref) - ref (or external-ref local-ref) - - ;; We need to store the handle-blur ref so we can call it on unmount - handle-blur-ref (mf/use-ref nil) - dirty-ref (mf/use-ref false) + local-ref (mf/use-ref) + ref (or external-ref local-ref) ;; This `value` represents the previous value and is used as ;; initil value for the simple math expression evaluation. - value (d/parse-double value-str default-val) + value (when (not= :multiple value-str) (d/parse-double value-str default)) - min-val (cond - (number? min-val-str) - min-val-str + ;; We need to store the handle-blur ref so we can call it on unmount + dirty-ref (mf/use-ref false) - (string? min-val-str) - (d/parse-double min-val-str)) - - max-val (cond - (number? max-val-str) - max-val-str - - (string? max-val-str) - (d/parse-double max-val-str)) - - step-val (cond - (number? step-val-str) - step-val-str - - (string? step-val-str) - (d/parse-double step-val-str) - - :else 1) + ;; Last value input by the user we need to store to save on unmount + last-value* (mf/use-var value) parse-value - (mf/use-callback - (mf/deps ref min-val max-val value nillable default-val) + (mf/use-fn + (mf/deps min-value max-value value nillable? default) (fn [] - (let [input-node (mf/ref-val ref) - new-value (-> (dom/get-value input-node) - (str/strip-suffix ".") - (sm/expr-eval value))] - (cond - (d/num? new-value) - (-> new-value - (cljs.core/max (/ us/min-safe-int 2)) - (cljs.core/min (/ us/max-safe-int 2)) - (cond-> - (d/num? min-val) - (cljs.core/max min-val) + (when-let [node (mf/ref-val ref)] + (let [new-value (-> (dom/get-value node) + (str/strip-suffix ".") + (smt/expr-eval value))] + (cond + (d/num? new-value) + (-> new-value + (d/max (/ sm/min-safe-int 2)) + (d/min (/ sm/max-safe-int 2)) + (cond-> (d/num? min-value) + (d/max min-value)) + (cond-> (d/num? max-value) + (d/min max-value))) - (d/num? max-val) - (cljs.core/min max-val))) + nillable? + default - nillable - default-val - - :else value)))) + :else value))))) update-input - (mf/use-callback - (mf/deps ref) + (mf/use-fn (fn [new-value] - (let [input-node (mf/ref-val ref)] - (dom/set-value! input-node (fmt/format-number new-value))))) + (when-let [node (mf/ref-val ref)] + (dom/set-value! node (fmt/format-number new-value))))) apply-value - (mf/use-callback + (mf/use-fn (mf/deps on-change update-input value) - (fn [new-value event] + (fn [event new-value] (mf/set-ref-val! dirty-ref false) (when (and (not= new-value value) (fn? on-change)) + ;; FIXME: on-change very slow, makes the handler laggy (on-change new-value event)) (update-input new-value))) set-delta - (mf/use-callback - (mf/deps wrap-value? min-val max-val parse-value apply-value) + (mf/use-fn + (mf/deps wrap-value? min-value max-value parse-value apply-value) (fn [event up? down?] - (let [current-value (parse-value)] + (let [current-value (parse-value) + current-value + (cond + (and (not current-value) down? max-value) + max-value + + (and (not current-value) up? min-value) + min-value + + (not current-value) + (d/nilv default 0) + + :else + current-value)] (when current-value (let [increment (cond (kbd/shift? event) - (if up? (* step-val 10) (* step-val -10)) + (if up? (* step-value 10) (* step-value -10)) (kbd/alt? event) - (if up? (* step-val 0.1) (* step-val -0.1)) + (if up? (* step-value 0.1) (* step-value -0.1)) :else - (if up? step-val (- step-val))) + (if up? step-value (- step-value))) new-value (+ current-value increment) new-value (cond - (and wrap-value? (d/num? max-val min-val) - (> new-value max-val) up?) - (-> new-value (- max-val) (+ min-val) (- step-val)) + (and wrap-value? (d/num? max-value min-value) + (> new-value max-value) up?) + (-> new-value (- max-value) (+ min-value) (- step-value)) - (and wrap-value? (d/num? max-val min-val) - (< new-value min-val) down?) - (-> new-value (- min-val) (+ max-val) (+ step-val)) + (and wrap-value? (d/num? max-value min-value) + (< new-value min-value) down?) + (-> new-value (- min-value) (+ max-value) (+ step-value)) - (and (d/num? min-val) (< new-value min-val)) - min-val + (and (d/num? min-value) (< new-value min-value)) + min-value - (and (d/num? max-val) (> new-value max-val)) - max-val + (and (d/num? max-value) (> new-value max-value)) + max-value :else new-value)] - (apply-value new-value event)))))) + (apply-value event new-value)))))) handle-key-down - (mf/use-callback - (mf/deps set-delta apply-value update-input) + (mf/use-fn + (mf/deps set-delta apply-value update-input parse-value) (fn [event] (mf/set-ref-val! dirty-ref true) (let [up? (kbd/up-arrow? event) down? (kbd/down-arrow? event) enter? (kbd/enter? event) esc? (kbd/esc? event) - input-node (mf/ref-val ref)] + node (mf/ref-val ref)] (when (or up? down?) (set-delta event up? down?)) + (reset! last-value* (parse-value)) (when enter? - (dom/blur! input-node)) + (dom/blur! node)) (when esc? (update-input value-str) - (dom/blur! input-node))))) + (dom/blur! node))))) + + handle-change + (mf/use-fn + (mf/deps parse-value) + (fn [] + ;; Store the last value inputed + (reset! last-value* (parse-value)))) handle-mouse-wheel - (mf/use-callback + (mf/use-fn (mf/deps set-delta) (fn [event] - (let [input-node (mf/ref-val ref)] - (when (dom/active? input-node) - (let [event (.getBrowserEvent ^js event)] - (dom/prevent-default event) - (dom/stop-propagation event) - (set-delta event (< (.-deltaY event) 0) (> (.-deltaY event) 0))))))) + (when-let [node (mf/ref-val ref)] + (when (dom/active? node) + (dom/prevent-default event) + (dom/stop-propagation event) + (let [{:keys [y]} (dom/get-delta-position event)] + (set-delta event (< y 0) (> y 0))))))) handle-blur - (mf/use-callback + (mf/use-fn (mf/deps parse-value apply-value update-input on-blur) (fn [event] - (let [new-value (or (parse-value) default-val)] - (if (or nillable new-value) - (apply-value new-value event) - (update-input new-value))) - (when on-blur (on-blur event)))) + (when (mf/ref-val dirty-ref) + (let [new-value (or @last-value* default)] + (if (or nillable? new-value) + (apply-value event new-value) + (update-input new-value))) + (when (fn? on-blur) + (on-blur event))))) + + handle-unmount (h/use-ref-callback handle-blur) on-click - (mf/use-callback + (mf/use-fn (fn [event] - (let [target (dom/get-target event)] - (when (some? ref) - (let [current (mf/ref-val ref)] - (when (and (some? current) (not (.contains current target))) - (dom/blur! current))))))) + (let [target (dom/get-target event) + node (mf/ref-val ref)] + (when (and (some? node) (not (dom/child? node target))) + (dom/blur! node))))) handle-focus (mf/use-callback @@ -211,9 +221,12 @@ ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect (.addEventListener target "mouseup" dom/prevent-default #js {:once true}))))) - props (-> props - (obj/without ["value" "onChange" "nillable" "onFocus"]) - (obj/set! "className" "input-text") + props (-> (obj/clone props) + (obj/unset! "selectOnFocus") + (obj/unset! "nillable") + (obj/set! "value" mf/undefined) + (obj/set! "onChange" handle-change) + (obj/set! "className" class) (obj/set! "type" "text") (obj/set! "ref" ref) (obj/set! "defaultValue" (fmt/format-number value)) @@ -222,50 +235,20 @@ (obj/set! "onBlur" handle-blur) (obj/set! "onFocus" handle-focus))] - (mf/use-effect - (mf/deps value) - (fn [] - (when-let [input-node (mf/ref-val ref)] - (dom/set-value! input-node (fmt/format-number value))))) + (mf/with-effect [value] + (when-let [input-node (mf/ref-val ref)] + (dom/set-value! input-node (fmt/format-number value)))) - (mf/use-effect - (mf/deps handle-blur) - (fn [] - (mf/set-ref-val! handle-blur-ref {:fn handle-blur}))) + (mf/with-effect [handle-unmount] handle-unmount) - (mf/use-layout-effect - (fn [] - #(when (mf/ref-val dirty-ref) - (let [handle-blur (:fn (mf/ref-val handle-blur-ref))] - (handle-blur))))) + (mf/with-layout-effect [] + (let [keys [(events/listen globals/window "pointerdown" on-click) + (events/listen globals/window "click" on-click)]] + #(run! events/unlistenByKey keys))) - (mf/use-layout-effect - (mf/deps handle-mouse-wheel) - (fn [] - (let [keys [(events/listen (mf/ref-val ref) EventType.WHEEL handle-mouse-wheel #js {:passive false})]] - #(doseq [key keys] - (events/unlistenByKey key))))) - - (mf/use-layout-effect - (fn [] - (let [keys [(events/listen globals/window EventType.POINTERDOWN on-click) - (events/listen globals/window EventType.CLICK on-click)]] - #(doseq [key keys] - (events/unlistenByKey key))))) - - (mf/use-layout-effect - (mf/deps handle-mouse-wheel) - (fn [] - (let [keys [(events/listen (mf/ref-val ref) EventType.WHEEL handle-mouse-wheel #js {:passive false})]] - #(doseq [key keys] - (events/unlistenByKey key))))) - - - (mf/use-layout-effect - (mf/deps handle-mouse-wheel) - (fn [] - (let [keys [(events/listen (mf/ref-val ref) EventType.WHEEL handle-mouse-wheel #js {:passive false})]] - #(doseq [key keys] - (events/unlistenByKey key))))) + (mf/with-layout-effect [handle-mouse-wheel] + (when-let [node (mf/ref-val ref)] + (let [key (events/listen node "wheel" handle-mouse-wheel #js {:passive false})] + #(events/unlistenByKey key)))) [:> :input props])) diff --git a/frontend/src/app/main/ui/components/radio_buttons.cljs b/frontend/src/app/main/ui/components/radio_buttons.cljs new file mode 100644 index 0000000000..0d7cce2948 --- /dev/null +++ b/frontend/src/app/main/ui/components/radio_buttons.cljs @@ -0,0 +1,98 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.components.radio-buttons + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.main.ui.formats :as fmt] + [app.util.dom :as dom] + [rumext.v2 :as mf])) + +(def context + (mf/create-context nil)) + +(mf/defc radio-button + {::mf/props :obj} + [{:keys [icon id value disabled title icon-class type]}] + (let [context (mf/use-ctx context) + allow-empty (unchecked-get context "allow-empty") + type (if ^boolean type + type + (if ^boolean allow-empty + "checkbox" + "radio")) + + on-change (unchecked-get context "on-change") + selected (unchecked-get context "selected") + name (unchecked-get context "name") + + encode-fn (unchecked-get context "encode-fn") + checked? (= selected value) + + value (encode-fn value)] + + + [:label {:html-for id + :title title + :class (stl/css-case + :radio-icon true + :checked checked? + :disabled disabled)} + + (if (some? icon) + [:span {:class icon-class} icon] + [:span {:class (stl/css :title-name)} value]) + + [:input {:id id + :on-change on-change + :type type + :name name + :disabled disabled + :value value + :checked checked?}]])) + +(mf/defc radio-buttons + {::mf/props :obj} + [{:keys [children on-change selected class wide encode-fn decode-fn allow-empty] :as props}] + (let [encode-fn (d/nilv encode-fn identity) + decode-fn (d/nilv decode-fn identity) + nitems (if (array? children) + (alength children) + 1) + + width (mf/with-memo [nitems] + (if (= wide true) + "unset" + (fmt/format-pixels + (+ (* 4 (- nitems 1)) + (* 28 nitems))))) + + on-change' + (mf/use-fn + (mf/deps selected on-change) + (fn [event] + (let [input (dom/get-target event) + value (dom/get-target-val event) + + ;; Only allow null values when the "allow-empty" prop is true + value (when (or (not allow-empty) + (not= value selected)) value)] + (when (fn? on-change) + (on-change (decode-fn value) event)) + (dom/blur! input)))) + + context-value + (mf/spread props + :on-change on-change' + :encode-fn encode-fn + :decode-fn decode-fn)] + + [:& (mf/provider context) {:value context-value} + [:div {:class (dm/str class " " (stl/css :radio-btn-wrapper)) + :style {:width width}} + children]])) diff --git a/frontend/src/app/main/ui/components/radio_buttons.scss b/frontend/src/app/main/ui/components/radio_buttons.scss new file mode 100644 index 0000000000..2000065688 --- /dev/null +++ b/frontend/src/app/main/ui/components/radio_buttons.scss @@ -0,0 +1,78 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.radio-btn-wrapper { + @include flexCenter; + border-radius: $br-8; + height: $s-32; + background-color: var(--input-background-color); +} + +.radio-icon { + --radio-icon-border-color: var(--radio-btn-border-color); + + @include buttonStyle; + @include flexCenter; + @include focusRadio; + height: $s-32; + flex-grow: 1; + border-radius: $s-8; + box-sizing: border-box; + border: $br-2 solid var(--radio-icon-border-color); + + input { + display: none; + } + svg { + @extend .button-icon; + stroke: var(--radio-btn-foreground-color); + } + .title-name { + @include uppercaseTitleTipography; + color: var(--radio-btn-foreground-color); + } + &:hover { + svg { + stroke: var(--radio-btn-foreground-color-selected); + } + } +} + +.checked { + --radio-icon-border-color: var(--radio-btn-border-color-selected); + + background-color: var(--radio-btn-background-color-selected); + svg { + stroke: var(--radio-btn-foreground-color-selected); + } + .title-name { + color: var(--radio-btn-foreground-color-selected); + } +} + +.disabled { + cursor: default; + background-color: transparent; + border: $s-2 solid transparent; + svg { + stroke: var(--button-foreground-color-disabled); + } + .title-name { + color: var(--button-foreground-color-disabled); + } + &:hover { + background-color: transparent; + border: $s-2 solid transparent; + svg { + stroke: var(--button-foreground-color-disabled); + } + .title-name { + color: var(--button-foreground-color-disabled); + } + } +} diff --git a/frontend/src/app/main/ui/components/search_bar.cljs b/frontend/src/app/main/ui/components/search_bar.cljs new file mode 100644 index 0000000000..0fb73303c8 --- /dev/null +++ b/frontend/src/app/main/ui/components/search_bar.cljs @@ -0,0 +1,65 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.components.search-bar + (:require-macros [app.main.style :as stl]) + (:require + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.keyboard :as kbd] + [rumext.v2 :as mf])) + +(mf/defc search-bar + {::mf/wrap-props false} + [props] + (let [children (unchecked-get props "children") + on-change (unchecked-get props "on-change") + value (unchecked-get props "value") + on-clear (unchecked-get props "clear-action") + placeholder (unchecked-get props "placeholder") + icon (unchecked-get props "icon") + autofocus (unchecked-get props "auto-focus") + id (unchecked-get props "id") + + + handle-change + (mf/use-fn + (mf/deps on-change) + (fn [event] + (let [value (dom/get-target-val event)] + (on-change value event)))) + + handle-clear + (mf/use-fn + (mf/deps on-clear on-change) + (fn [event] + (if on-clear + (on-clear event) + (on-change "" event)))) + + handle-key-down + (mf/use-fn + (fn [event] + (let [enter? (kbd/enter? event) + esc? (kbd/esc? event) + node (dom/get-target event)] + (when ^boolean enter? (dom/blur! node)) + (when ^boolean esc? (dom/blur! node)))))] + [:span {:class (stl/css-case :search-box true + :has-children (some? children))} + children + [:div {:class (stl/css :search-input-wrapper)} + icon + [:input {:id id + :on-change handle-change + :value value + :auto-focus autofocus + :placeholder placeholder + :on-key-down handle-key-down}] + (when (not= "" value) + [:button {:class (stl/css :clear) + :on-click handle-clear} + i/delete-text])]])) diff --git a/frontend/src/app/main/ui/components/search_bar.scss b/frontend/src/app/main/ui/components/search_bar.scss new file mode 100644 index 0000000000..b67317ba21 --- /dev/null +++ b/frontend/src/app/main/ui/components/search_bar.scss @@ -0,0 +1,69 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.search-box { + display: flex; + gap: $s-2; + height: $s-32; + width: 100%; + border-radius: $br-8; + background-color: var(--search-bar-background-color); +} + +.search-input-wrapper { + @include flexCenter; + height: $s-32; + width: 100%; + border: $s-1 solid var(--search-bar-input-border-color); + border-radius: $br-8; + background-color: var(--search-bar-input-background-color); + input { + width: 100%; + height: 100%; + margin: 0 $s-8 0 $s-4; + border: 0; + background-color: var(--input-background-color); + font-size: $fs-12; + color: var(--input-foreground-color); + border-radius: $br-8; + &:focus { + outline: none; + } + } + &:hover { + border: $s-1 solid var(--input-border-color-hover); + background-color: var(--input-background-color-hover); + input { + background-color: var(--input-background-color-hover); + } + } + &:focus-within { + background-color: var(--input-background-color-active); + color: var(--input-foreground-color-active); + border: $s-1 solid var(--input-border-color-focus); + input { + background-color: var(--input-background-color-active); + } + } +} + +.clear { + @extend .button-tag; + border-radius: $br-8; + height: 100%; + svg { + @extend .button-icon-small; + color: transparent; + stroke: var(--icon-foreground); + } +} + +.search-box.has-children .search-input-wrapper { + border-radius: $br-2 $br-8 $br-8 $br-2; + margin-left: 0; +} diff --git a/frontend/src/app/main/ui/components/select.cljs b/frontend/src/app/main/ui/components/select.cljs index cdb88fd037..0187499e6d 100644 --- a/frontend/src/app/main/ui/components/select.cljs +++ b/frontend/src/app/main/ui/components/select.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.components.select + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] @@ -14,14 +15,15 @@ [app.util.dom :as dom] [rumext.v2 :as mf])) + (defn- as-key-value [item] (if (map? item) - [(:value item) (:label item)] - [item item])) + [(:value item) (:label item) (:icon item)] + [item item item])) (mf/defc select - [{:keys [default-value options class is-open? on-change on-pointer-enter-option on-pointer-leave-option]}] + [{:keys [default-value options class dropdown-class is-open? on-change on-pointer-enter-option on-pointer-leave-option disabled]}] (let [label-index (mf/with-memo [options] (into {} (map as-key-value) options)) @@ -36,7 +38,17 @@ current-label (get label-index current-value) is-open? (:is-open? state) - open-dropdown (mf/use-fn #(swap! state* assoc :is-open? true)) + dropdown-element* (mf/use-ref nil) + dropdown-direction* (mf/use-state "down") + dropdown-direction-change* (mf/use-ref 0) + + open-dropdown + (mf/use-fn + (mf/deps disabled) + (fn [] + (when-not disabled + (swap! state* assoc :is-open? true)))) + close-dropdown (mf/use-fn #(swap! state* assoc :is-open? false)) select-item @@ -74,21 +86,44 @@ (mf/with-effect [default-value] (swap! state* assoc :current-value default-value)) - [:div.custom-select {:on-click open-dropdown :class class} - [:span current-label] - [:span.dropdown-button i/arrow-down] - [:& dropdown {:show is-open? :on-close close-dropdown} - [:ul.custom-select-dropdown - (for [[index item] (d/enumerate options)] - (if (= :separator item) - [:hr {:key (dm/str current-id "-" index)}] - (let [[value label] (as-key-value item)] - [:li.checked-element - {:key (dm/str current-id "-" index) - :class (when (= value current-value) "is-selected") - :data-value (pr-str value) - :on-pointer-enter highlight-item - :on-pointer-leave unhighlight-item - :on-click select-item} - [:span.check-icon i/tick] - [:span label]])))]]])) + (mf/with-effect [is-open? dropdown-element*] + (let [dropdown-element (mf/ref-val dropdown-element*)] + (when (and (= 0 (mf/ref-val dropdown-direction-change*)) dropdown-element) + (let [is-outside? (dom/is-element-outside? dropdown-element)] + (reset! dropdown-direction* (if is-outside? "up" "down")) + (mf/set-ref-val! dropdown-direction-change* (inc (mf/ref-val dropdown-direction-change*))))))) + + (let [selected-option (first (filter #(= (:value %) default-value) options)) + current-icon (:icon selected-option) + current-icon-ref (i/key->icon current-icon)] + [:div {:on-click open-dropdown + :class (dm/str (stl/css-case :custom-select true + :disabled disabled + :icon (some? current-icon-ref)) + " " class)} + (when (and current-icon current-icon-ref) + [:span {:class (stl/css :current-icon)} current-icon-ref]) + [:span {:class (stl/css :current-label)} current-label] + [:span {:class (stl/css :dropdown-button)} i/arrow] + [:& dropdown {:show is-open? :on-close close-dropdown} + [:ul {:ref dropdown-element* :data-direction @dropdown-direction* + :class (dm/str dropdown-class " " (stl/css :custom-select-dropdown))} + (for [[index item] (d/enumerate options)] + (if (= :separator item) + [:li {:class (dom/classnames (stl/css :separator) true) + :key (dm/str current-id "-" index)}] + (let [[value label icon] (as-key-value item) + icon-ref (i/key->icon icon)] + [:li + {:key (dm/str current-id "-" index) + :class (stl/css-case + :checked-element true + :disabled (:disabled item) + :is-selected (= value current-value)) + :data-value (pr-str value) + :on-pointer-enter highlight-item + :on-pointer-leave unhighlight-item + :on-click select-item} + (when (and icon icon-ref) [:span {:class (stl/css :icon)} icon-ref]) + [:span {:class (stl/css :label)} label] + [:span {:class (stl/css :check-icon)} i/tick]])))]]]))) diff --git a/frontend/src/app/main/ui/components/select.scss b/frontend/src/app/main/ui/components/select.scss new file mode 100644 index 0000000000..68af4ae365 --- /dev/null +++ b/frontend/src/app/main/ui/components/select.scss @@ -0,0 +1,129 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.custom-select { + --border-color: var(--menu-background-color); + --bg-color: var(--menu-background-color); + --icon-color: var(--icon-foreground); + --text-color: var(--menu-foreground-color); + @extend .new-scrollbar; + @include bodySmallTypography; + position: relative; + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + height: $s-32; + width: 100%; + margin: 0; + padding: $s-8; + border-radius: $br-8; + background-color: var(--bg-color); + border: $s-1 solid var(--border-color); + color: var(--text-color); + cursor: pointer; + + &.icon { + grid-template-columns: auto 1fr auto; + } + + &:hover { + --bg-color: var(--menu-background-color-hover); + --border-color: var(--menu-background-color); + --icon-color: var(--menu-foreground-color-hover); + } + + &:focus { + --bg-color: var(--menu-background-color-focus); + --border-color: var(--menu-background-focus); + } +} + +.disabled { + --bg-color: var(--menu-background-color-disabled); + --border-color: var(--menu-border-color-disabled); + --icon-color: var(--menu-foreground-color-disabled); + --text-color: var(--menu-foreground-color-disabled); + pointer-events: none; + cursor: default; +} + +.dropdown-button { + @include flexCenter; + svg { + @extend .button-icon-small; + transform: rotate(90deg); + stroke: var(--icon-color); + } +} + +.current-icon { + @include flexCenter; + width: $s-24; + padding-right: $s-4; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } +} + +.custom-select-dropdown { + @extend .dropdown-wrapper; + .separator { + margin: 0; + height: $s-12; + border-block-start: $s-1 solid var(--dropdown-separator-color); + } +} + +.custom-select-dropdown[data-direction="up"] { + bottom: $s-32; + top: auto; +} + +.checked-element { + @extend .dropdown-element-base; + .icon { + @include flexCenter; + height: $s-24; + width: $s-24; + padding-right: $s-4; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + } + + .label { + flex-grow: 1; + width: 100%; + } + + .check-icon { + @include flexCenter; + svg { + @extend .button-icon-small; + visibility: hidden; + stroke: var(--icon-foreground); + } + } + + &.is-selected { + color: var(--menu-foreground-color); + .check-icon svg { + stroke: var(--menu-foreground-color); + visibility: visible; + } + } + &.disabled { + display: none; + } +} + +.current-label { + @include textEllipsis; +} diff --git a/frontend/src/app/main/ui/components/shape_icon.cljs b/frontend/src/app/main/ui/components/shape_icon.cljs index 811cd8ed74..060213681f 100644 --- a/frontend/src/app/main/ui/components/shape_icon.cljs +++ b/frontend/src/app/main/ui/components/shape_icon.cljs @@ -7,13 +7,14 @@ (ns app.main.ui.components.shape-icon (:require [app.common.types.component :as ctk] + [app.common.types.shape :as cts] [app.common.types.shape.layout :as ctl] [app.main.ui.icons :as i] [rumext.v2 :as mf])) - (mf/defc element-icon - [{:keys [shape main-instance?] :as props}] + {::mf/wrap-props false} + [{:keys [shape main-instance?]}] (if (ctk/instance-head? shape) (if main-instance? i/component @@ -21,28 +22,44 @@ (case (:type shape) :frame (cond (and (ctl/flex-layout? shape) (ctl/col? shape)) - i/layout-columns + i/flex-horizontal (and (ctl/flex-layout? shape) (ctl/row? shape)) - i/layout-rows + i/flex-vertical - ;; TODO: GRID ICON + (ctl/grid-layout? shape) + i/flex-grid :else - i/artboard) - :image i/image - :line i/line - :circle i/circle - :path i/curve - :rect i/box + i/board) + ;; TODO -> THUMBNAIL ICON + :image i/img + :line (if (cts/has-images? shape) i/img i/path) + :circle (if (cts/has-images? shape) i/img i/elipse) + :path (if (cts/has-images? shape) i/img i/path) + :rect (if (cts/has-images? shape) i/img i/rectangle) :text i/text - :group (if (:masked-group? shape) + :group (if (:masked-group shape) i/mask - i/folder) + i/group) :bool (case (:bool-type shape) - :difference i/bool-difference - :exclude i/bool-exclude - :intersection i/bool-intersection - #_:default i/bool-union) - :svg-raw i/file-svg + :difference i/boolean-difference + :exclude i/boolean-exclude + :intersection i/boolean-intersection + #_:default i/boolean-union) + :svg-raw i/img + nil))) + + +(mf/defc element-icon-by-type + [{:keys [type main-instance?] :as props}] + (if main-instance? + i/component + (case type + :frame i/board + :image i/img + :shape i/path + :text i/text + :mask i/mask + :group i/group nil))) diff --git a/frontend/src/app/main/ui/components/shape_icon_refactor.cljs b/frontend/src/app/main/ui/components/shape_icon_refactor.cljs deleted file mode 100644 index 7068849596..0000000000 --- a/frontend/src/app/main/ui/components/shape_icon_refactor.cljs +++ /dev/null @@ -1,63 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.components.shape-icon-refactor - (:require - [app.common.types.component :as ctk] - [app.common.types.shape.layout :as ctl] - [app.main.ui.icons :as i] - [rumext.v2 :as mf])) - - -(mf/defc element-icon-refactor - [{:keys [shape main-instance?] :as props}] - (if (ctk/instance-head? shape) - (if main-instance? - i/component-refactor - i/copy-refactor) - (case (:type shape) - :frame (cond - (and (ctl/flex-layout? shape) (ctl/col? shape)) - i/flex-vertical-refactor - - (and (ctl/flex-layout? shape) (ctl/row? shape)) - i/flex-horizontal-refactor - - ;; TODO: GRID ICON - - :else - i/board-refactor) - ;; TODO -> THUMBNAIL ICON - :image i/img-refactor - :line i/path-refactor - :circle i/elipse-refactor - :path i/path-refactor - :rect i/rectangle-refactor - :text i/text-refactor - :group (if (:masked-group? shape) - i/mask-refactor - i/group-refactor) - :bool (case (:bool-type shape) - :difference i/boolean-difference-refactor - :exclude i/boolean-exclude-refactor - :intersection i/boolean-intersection-refactor - #_:default i/boolean-union-refactor) - :svg-raw i/file-svg - nil))) - - -(mf/defc element-icon-refactor-by-type - [{:keys [type main-instance?] :as props}] - (if main-instance? - i/component-refactor - (case type - :frame i/board-refactor - :image i/img-refactor - :shape i/path-refactor - :text i/text-refactor - :mask i/mask-refactor - :group i/group-refactor - nil))) \ No newline at end of file diff --git a/frontend/src/app/main/ui/components/tab_container.cljs b/frontend/src/app/main/ui/components/tab_container.cljs index 5bd44abecd..4dae9d52b3 100644 --- a/frontend/src/app/main/ui/components/tab_container.cljs +++ b/frontend/src/app/main/ui/components/tab_container.cljs @@ -5,11 +5,12 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.components.tab-container - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] - [app.main.ui.context :as ctx] + [app.common.data.macros :as dm] [app.main.ui.icons :as i] + [app.util.array :as array] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [cuerdas.core :as str] @@ -17,73 +18,51 @@ (mf/defc tab-element {::mf/wrap-props false} - [props] - (let [children (unchecked-get props "children") - new-css-system (mf/use-ctx ctx/new-css-system)] - [:div {:class (if new-css-system - (dom/classnames (css :tab-element) true) - (dom/classnames :tab-element true))} - children])) + [{:keys [children]}] + children) (mf/defc tab-container {::mf/wrap-props false} - [props] - (let [children (->> - (unchecked-get props "children") - (filter some?)) - selected (unchecked-get props "selected") - on-change (unchecked-get props "on-change-tab") - collapsable? (unchecked-get props "collapsable?") - handle-collapse (unchecked-get props "handle-collapse") + [{:keys [children selected on-change-tab collapsable handle-collapse header-class content-class]}] + (let [children (-> (array/normalize-to-array children) + (array/without-nils)) - state (mf/use-state #(or selected (-> children first .-props .-id))) - selected (or selected @state) - new-css-system (mf/use-ctx ctx/new-css-system) + selected* (mf/use-state #(or selected (-> children first .-props .-id))) + selected (or selected @selected*) - select-fn - (mf/use-fn - (mf/deps on-change) - (fn [event] - (let [id (d/read-string (.. event -target -dataset -id))] - (reset! state id) - (when (fn? on-change) (on-change id)))))] + on-click (mf/use-fn + (mf/deps on-change-tab) + (fn [event] + (let [id (-> event + (dom/get-current-target) + (dom/get-data "id") + (keyword))] + (reset! selected* id) + (when (fn? on-change-tab) + (on-change-tab id)))))] - [:div {:class (if new-css-system - (dom/classnames (css :tab-container) true) - (dom/classnames :tab-container true))} - [:div {:class (if new-css-system - (dom/classnames (css :tab-container-tabs) true) - (dom/classnames :tab-container-tabs true))} - (when (and new-css-system collapsable?) + [:section {:class (stl/css :tab-container)} + [:header {:class (dm/str header-class " " (stl/css :tab-container-tabs))} + (when ^boolean collapsable [:button {:on-click handle-collapse - :class (dom/classnames (css :collapse-sidebar) true) + :class (stl/css :collapse-sidebar) :aria-label (tr "workspace.sidebar.collapse")} - i/arrow-refactor]) - (if new-css-system - [:div {:class (dom/classnames (css :tab-container-tab-wrapper) new-css-system)} - (for [tab children] - (let [props (.-props tab) - id (.-id props) - title (.-title props)] - [:div - {:key (str/concat "tab-" (d/name id)) - :data-id (pr-str id) - :on-click select-fn - :class (dom/classnames (css :tab-container-tab-title) true - (css :current) (= selected id))} - title]))] - (for [tab children] - (let [props (.-props tab) - id (.-id props) - title (.-title props)] - [:div.tab-container-tab-title - {:key (str/concat "tab-" (d/name id)) - :data-id (pr-str id) - :on-click select-fn - :class (when (= selected id) "current")} - title])))] - [:div {:class (if new-css-system - (dom/classnames (css :tab-container-content) true) - (dom/classnames :tab-container-content true))} - (d/seek #(= selected (-> % .-props .-id)) children)]])) + i/arrow]) + [:div {:class (stl/css :tab-container-tab-wrapper)} + (for [tab children] + (let [props (.-props tab) + id (.-id props) + title (.-title props) + sid (d/name id)] + [:div {:key (str/concat "tab-" sid) + :data-id sid + :on-click on-click + :class (stl/css-case + :tab-container-tab-title true + :current (= selected id))} + title]))]] + + [:div {:class (dm/str content-class " " (stl/css :tab-container-content))} + (d/seek #(= selected (-> % .-props .-id)) + children)]])) diff --git a/frontend/src/app/main/ui/components/tab_container.css.json b/frontend/src/app/main/ui/components/tab_container.css.json deleted file mode 100644 index 6a5fd88cd8..0000000000 --- a/frontend/src/app/main/ui/components/tab_container.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"components_tab_container_button-primary_ibiAz","button-secondary":"components_tab_container_button-secondary_wZR80","button-icon":"components_tab_container_button-icon_2NhVr","button-icon-small":"components_tab_container_button-icon-small_yU7na","tab-container":"components_tab_container_tab-container_P6HRr","tab-container-content":"components_tab_container_tab-container-content_yfM9F","tab-element":"components_tab_container_tab-element_gBIwV","tab-container-tabs":"components_tab_container_tab-container-tabs_6gXOY","tab-container-tab-wrapper":"components_tab_container_tab-container-tab-wrapper_-ngrN","tab-container-tab-title":"components_tab_container_tab-container-tab-title_IN1Dx","current":"components_tab_container_current_jrovp","collapse-sidebar":"components_tab_container_collapse-sidebar_e5hFv","collapsed":"components_tab_container_collapsed_lfkjK"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/components/tab_container.scss b/frontend/src/app/main/ui/components/tab_container.scss index 23e941e3a2..7c9b08b522 100644 --- a/frontend/src/app/main/ui/components/tab_container.scss +++ b/frontend/src/app/main/ui/components/tab_container.scss @@ -7,68 +7,72 @@ .tab-container { display: grid; - grid-template-rows: auto 1fr; - grid-template-columns: 100%; + grid-template-rows: $s-32 1fr; height: 100%; - - .tab-container-content { - overflow-y: auto; - overflow-x: hidden; - } - - .tab-element { - height: 100%; - } } + .tab-container-tabs { display: flex; align-items: center; flex-direction: row; gap: $s-2; - height: $s-32; - margin: $s-4 $s-4 0 $s-4; - padding: $s-2 $s-2 $s-2 0; border-radius: $br-8; - background: var(--color-background-secondary); + background: var(--tabs-background-color); cursor: pointer; - font-size: $fs12; + font-size: $fs-12; + height: 100%; .tab-container-tab-wrapper { @include flexCenter; flex-direction: row; height: 100%; width: 100%; - gap: $s-2; .tab-container-tab-title { @include flexCenter; - @include tabTitleTipography; - height: $s-28; + @include headlineSmallTypography; + height: 100%; width: 100%; + padding: 0 $s-8; margin: 0; - border-radius: $br-5; + border-radius: $br-8; background-color: transparent; color: var(--tab-foreground-color); + white-space: nowrap; + border: $s-2 solid var(--tab-border-color); + svg { + @extend .button-icon; + stroke: var(--tab-foreground-color); + } &.current, &.current:hover { background: var(--tab-background-color-selected); + border-color: var(--tab-border-color-selected); color: var(--tab-foreground-color-selected); + svg { + stroke: var(--tab-foreground-color-selected); + } } &:hover { color: var(--tab-foreground-color-hover); + svg { + stroke: var(--tab-foreground-color-hover); + } } } } + .collapse-sidebar { @include flexCenter; @include buttonStyle; height: 100%; width: $s-24; - padding: 0; + min-width: $s-24; + padding: 0 $s-6; border-radius: $br-5; svg { @include flexCenter; - height: 12px; - width: 16px; + height: $s-16; + width: $s-16; stroke: var(--icon-foreground); transform: rotate(180deg); fill: none; @@ -83,7 +87,15 @@ &.collapsed { svg { transform: rotate(0deg); + padding: 0 0 0 $s-6; } } } } + +.tab-container-content { + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; +} diff --git a/frontend/src/app/main/ui/components/tabs_container.cljs b/frontend/src/app/main/ui/components/tabs_container.cljs deleted file mode 100644 index 2275d06feb..0000000000 --- a/frontend/src/app/main/ui/components/tabs_container.cljs +++ /dev/null @@ -1,53 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.components.tabs-container - (:require - [app.common.data :as d] - [cuerdas.core :as str] - [rumext.v2 :as mf])) - -(mf/defc tabs-element - {::mf/wrap-props false} - [props] - (let [children (unchecked-get props "children")] - [:div.tab-element - [:div.tab-element-content children]])) - -(mf/defc tabs-container - {::mf/wrap-props false} - [props] - (let [children (->> - (unchecked-get props "children") - (filter some?)) - selected (unchecked-get props "selected") - on-change (unchecked-get props "on-change-tab") - - state (mf/use-state #(or selected (-> children first .-props .-id))) - selected (or selected @state) - - select-fn - (mf/use-fn - (mf/deps on-change) - (fn [event] - (let [id (d/read-string (.. event -target -dataset -id))] - (reset! state id) - (when (fn? on-change) (on-change id)))))] - - [:div.tab-container - [:div.tab-container-tabs - (for [tab children] - (let [props (.-props tab) - id (.-id props) - title (.-title props)] - [:div.tab-container-tab-title - {:key (str/concat "tab-" (d/name id)) - :data-id (pr-str id) - :on-click select-fn - :class (when (= selected id) "current")} - title]))] - [:div.tab-container-content - (d/seek #(= selected (-> % .-props .-id)) children)]])) diff --git a/frontend/src/app/main/ui/components/title_bar.cljs b/frontend/src/app/main/ui/components/title_bar.cljs new file mode 100644 index 0000000000..04e6211338 --- /dev/null +++ b/frontend/src/app/main/ui/components/title_bar.cljs @@ -0,0 +1,58 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.components.title-bar + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data.macros :as dm] + [app.main.ui.icons :as i] + [rumext.v2 :as mf])) + +(def ^:private chevron-icon + (i/icon-xref :arrow (stl/css :chevron-icon))) + +(mf/defc title-bar + {::mf/props :obj} + [{:keys [class collapsable collapsed title children + btn-children all-clickable add-icon-gap + on-collapsed on-btn-click]}] + (let [klass (stl/css-case :title-bar true + :all-clickable all-clickable) + klass (dm/str klass " " class)] + [:div {:class klass} + (if ^boolean collapsable + [:div {:class (stl/css :title-wrapper)} + (if ^boolean all-clickable + [:button {:class (stl/css :toggle-btn) + :on-click on-collapsed} + [:span {:class (stl/css-case + :collapsabled-icon true + :collapsed collapsed)} + chevron-icon] + [:div {:class (stl/css :title)} title]] + [:* + [:button {:class (stl/css-case + :collapsabled-icon true + :collapsed collapsed) + :on-click on-collapsed} + chevron-icon] + [:div {:class (stl/css :title)} + title]])] + [:div {:class (stl/css-case + :title-only true + :title-only-icon-gap add-icon-gap)} + title]) + children + (when (some? on-btn-click) + [:button {:class (stl/css :title-button) + :on-click on-btn-click} + btn-children])])) + +(mf/defc inspect-title-bar + {::mf/props :obj} + [{:keys [class title]}] + [:div {:class (dm/str (stl/css :title-bar) " " class)} + [:div {:class (stl/css :title-only :inspect-title)} title]]) diff --git a/frontend/src/app/main/ui/components/title_bar.scss b/frontend/src/app/main/ui/components/title_bar.scss new file mode 100644 index 0000000000..20e25e233b --- /dev/null +++ b/frontend/src/app/main/ui/components/title_bar.scss @@ -0,0 +1,120 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.title-bar { + display: flex; + align-items: center; + justify-content: space-between; + height: $s-32; + width: 100%; + min-height: $s-32; + background-color: var(--title-background-color); + color: var(--title-foreground-color); +} + +.title, +.title-only, +.inspect-title { + @include headlineSmallTypography; + display: grid; + align-items: center; + justify-content: flex-start; + grid-auto-flow: column; + height: 100%; + min-height: $s-32; + + overflow: hidden; +} + +.title-only { + --title-bar-title-margin: #{$s-8}; + margin-inline-start: var(--title-bar-title-margin); +} + +.inspect-title { + color: var(--title-foreground-color-hover); +} + +.title-wrapper { + display: flex; + align-items: center; + flex-grow: 1; + padding: 0; + color: var(--title-foreground-color); + stroke: var(--title-foreground-color); + overflow: hidden; + + &:hover { + color: var(--title-foreground-color-hover); + .title { + stroke: var(--title-foreground-color-hover); + } + } +} + +.title-button { + @extend .button-tertiary; + height: $s-32; + width: calc($s-24 + $s-4); + padding: 0; + border-radius: $br-8; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.title-only-icon-gap { + --title-bar-title-margin: #{$s-12}; +} + +.toggle-btn { + @include buttonStyle; + display: flex; + align-items: center; + flex-grow: 1; + padding: 0; + color: var(--title-foreground-color); + stroke: var(--title-foreground-color); + overflow: hidden; + + --chevron-icon-color: var(--icon-foreground); + + &:hover { + --chevron-icon-color: var(--title-foreground-color-hover); + + color: var(--title-foreground-color-hover); + .title { + color: var(--title-foreground-color-hover); + stroke: var(--title-foreground-color-hover); + } + } +} + +.collapsabled-icon { + @include buttonStyle; + @include flexCenter; + height: $s-24; + border-radius: $br-8; + + --chevron-icon-rotation: 90deg; + + &.collapsed { + --chevron-icon-rotation: 0deg; + } + + &:hover { + --chevron-icon-color: var(--title-foreground-color-hover); + } +} + +.chevron-icon { + @extend .button-icon-small; + transform: rotate(var(--chevron-icon-rotation)); + stroke: var(--chevron-icon-color); +} diff --git a/frontend/src/app/main/ui/confirm.cljs b/frontend/src/app/main/ui/confirm.cljs index be4d4dc569..66fd00e925 100644 --- a/frontend/src/app/main/ui/confirm.cljs +++ b/frontend/src/app/main/ui/confirm.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.confirm + (:require-macros [app.main.style :as stl]) (:require [app.main.data.modal :as modal] [app.main.store :as st] @@ -63,42 +64,46 @@ (->> (events/listen js/document EventType.KEYDOWN on-keydown) (partial events/unlistenByKey)))) - [:div.modal-overlay - [:div.modal-container.confirm-dialog - [:div.modal-header - [:div.modal-header-title - [:h2 title]] - [:div.modal-close-button - {:on-click cancel-fn} i/close]] - [:div.modal-content + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} title] + [:button {:class (stl/css :modal-close-btn) + :on-click cancel-fn} i/close]] + + [:div {:class (stl/css :modal-content)} (when (and (string? message) (not= message "")) - [:h3 message]) + [:h3 {:class (stl/css :modal-msg)} message]) (when (and (string? scd-message) (not= scd-message "")) - [:h3 scd-message]) + [:h3 {:class (stl/css :modal-scd-msg)} scd-message]) (when (string? hint) - [:p hint]) + [:p {:class (stl/css :modal-hint)} hint]) (when (> (count items) 0) [:* - [:p (tr "ds.component-subtitle")] - [:ul.component-list + [:p {:class (stl/css :modal-subtitle)} + (tr "ds.component-subtitle")] + [:ul {:class (stl/css :component-list)} (for [item items] - [:li.modal-item-element - [:span.modal-component-icon i/component] - [:span (:name item)]])]])] + [:li {:class (stl/css :modal-item-element)} + [:span {:class (stl/css :modal-component-icon)} + i/component] + [:span {:class (stl/css :modal-component-name)} + (:name item)]])]])] - [:div.modal-footer - [:div.action-buttons + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} (when-not (= cancel-label :omit) - [:input.cancel-button - {:type "button" + [:input + {:class (stl/css :cancel-button) + :type "button" :value cancel-label :on-click cancel-fn}]) - [:input.accept-button - {:class (dom/classnames - :danger (= accept-style :danger) - :primary (= accept-style :primary)) + [:input + {:class (stl/css-case :accept-btn true + :danger (= accept-style :danger) + :primary (= accept-style :primary)) :type "button" :value accept-label :on-click accept-fn}]]]]])) diff --git a/frontend/src/app/main/ui/confirm.scss b/frontend/src/app/main/ui/confirm.scss new file mode 100644 index 0000000000..0b4b08202b --- /dev/null +++ b/frontend/src/app/main/ui/confirm.scss @@ -0,0 +1,79 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; + &.transparent { + background-color: transparent; + } +} + +.modal-container { + @extend .modal-container-base; +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include headlineMediumTypography; + color: var(--modal-title-foreground-color); +} + +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content { + @include bodyLargeTypography; + margin-bottom: $s-24; +} + +.modal-item-element { + @include flexRow; +} + +.modal-component-icon { + @include flexCenter; + height: $s-16; + width: $s-16; + svg { + @extend .button-icon-small; + stroke: var(--color); + } +} +.modal-component-name { + @include bodyLargeTypography; +} + +.modal-hint { + @extend .modal-hint-base; +} + +.action-buttons { + @extend .modal-action-btns; +} + +.cancel-button { + @extend .modal-cancel-btn; +} + +.accept-btn { + @extend .modal-accept-btn; + &.danger { + @extend .modal-danger-btn; + } +} + +.modal-scd-msg, +.modal-subtitle, +.modal-msg { + @include bodyLargeTypography; + color: var(--modal-text-foreground-color); +} diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index c0d1aa00ed..bfe0fa6ee6 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -16,16 +16,17 @@ (def current-project-id (mf/create-context nil)) (def current-page-id (mf/create-context nil)) (def current-file-id (mf/create-context nil)) +(def current-vbox (mf/create-context nil)) (def active-frames (mf/create-context nil)) (def render-thumbnails (mf/create-context nil)) (def libraries (mf/create-context nil)) (def components-v2 (mf/create-context nil)) -(def new-css-system (mf/create-context nil)) (def current-scroll (mf/create-context nil)) (def current-zoom (mf/create-context nil)) (def workspace-read-only? (mf/create-context nil)) (def is-component? (mf/create-context false)) +(def sidebar (mf/create-context nil)) diff --git a/frontend/src/app/main/ui/cursors.clj b/frontend/src/app/main/ui/cursors.clj index 9b5eb5b0c2..6b76807380 100644 --- a/frontend/src/app/main/ui/cursors.clj +++ b/frontend/src/app/main/ui/cursors.clj @@ -7,6 +7,7 @@ (ns app.main.ui.cursors (:require [app.common.uri :as u] + [clojure.core :as c] [clojure.java.io :as io] [cuerdas.core :as str])) @@ -71,6 +72,20 @@ (defmacro cursor-fn "Creates a dynamic cursor that can be rotated in runtime" [id initial] - (let [cursor (encode-svg-cursor id "{{rotation}}" default-hotspot-x default-hotspot-y default-height)] + (let [[cp1 cp2] (-> (encode-svg-cursor id "$$$" + default-hotspot-x + default-hotspot-y + default-height) + (str/split #"\$\$\$"))] `(fn [rot#] - (str/replace ~cursor "{{rotation}}" (+ ~initial rot#))))) + (str/concat ~cp1 (+ ~initial rot#) ~cp2)))) + +(defmacro collect-cursors + [] + (let [ns-info (:ns &env)] + `(cljs.core/js-obj + ~@(->> (:defs ns-info) + (map val) + (filter (fn [entry] (-> entry :meta :cursor))) + (mapcat (fn [{:keys [name] :as entry}] + [(-> name c/name str/camel str/capital) name])))))) diff --git a/frontend/src/app/main/ui/cursors.cljs b/frontend/src/app/main/ui/cursors.cljs index 93d2128fd0..8b41528167 100644 --- a/frontend/src/app/main/ui/cursors.cljs +++ b/frontend/src/app/main/ui/cursors.cljs @@ -5,71 +5,76 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.cursors - (:require-macros [app.main.ui.cursors :refer [cursor-ref cursor-fn]]) + (:require-macros [app.main.ui.cursors :refer [cursor-ref cursor-fn collect-cursors]]) (:require [app.util.timers :as ts] [cuerdas.core :as str] [rumext.v2 :as mf])) ;; Static cursors -(def comments (cursor-ref :comments 0 2 20)) -(def create-artboard (cursor-ref :create-artboard)) -(def create-ellipse (cursor-ref :create-ellipse)) -(def create-polygon (cursor-ref :create-polygon)) -(def create-rectangle (cursor-ref :create-rectangle)) -(def create-shape (cursor-ref :create-shape)) -(def duplicate (cursor-ref :duplicate 0 0 0)) -(def hand (cursor-ref :hand)) -(def move-pointer (cursor-ref :move-pointer)) -(def pen (cursor-ref :pen 0 0 0)) -(def pen-node (cursor-ref :pen-node 0 0 10 36)) -(def pencil (cursor-ref :pencil 0 0 24)) -(def picker (cursor-ref :picker 0 0 24)) -(def pointer-inner (cursor-ref :pointer-inner 0 0 0)) -(def pointer-move (cursor-ref :pointer-move 0 0 10 42)) -(def pointer-node (cursor-ref :pointer-node 0 0 10 32)) -(def resize-alt (cursor-ref :resize-alt)) -(def zoom (cursor-ref :zoom)) -(def zoom-in (cursor-ref :zoom-in)) -(def zoom-out (cursor-ref :zoom-out)) +(def ^:cursor comments (cursor-ref :comments 0 2 20)) +(def ^:cursor create-artboard (cursor-ref :create-artboard)) +(def ^:cursor create-ellipse (cursor-ref :create-ellipse)) +(def ^:cursor create-polygon (cursor-ref :create-polygon)) +(def ^:cursor create-rectangle (cursor-ref :create-rectangle)) +(def ^:cursor create-shape (cursor-ref :create-shape)) +(def ^:cursor duplicate (cursor-ref :duplicate 0 0 0)) +(def ^:cursor hand (cursor-ref :hand)) +(def ^:cursor move-pointer (cursor-ref :move-pointer)) +(def ^:cursor pen (cursor-ref :pen 0 0 0)) +(def ^:cursor pen-node (cursor-ref :pen-node 0 0 10 36)) +(def ^:cursor pencil (cursor-ref :pencil 0 0 24)) +(def ^:cursor picker (cursor-ref :picker 0 0 24)) +(def ^:cursor pointer-inner (cursor-ref :pointer-inner 0 0 0)) +(def ^:cursor pointer-move (cursor-ref :pointer-move 0 0 10 42)) +(def ^:cursor pointer-node (cursor-ref :pointer-node 0 0 10 32)) +(def ^:cursor resize-alt (cursor-ref :resize-alt)) +(def ^:cursor zoom (cursor-ref :zoom)) +(def ^:cursor zoom-in (cursor-ref :zoom-in)) +(def ^:cursor zoom-out (cursor-ref :zoom-out)) ;; Dynamic cursors -(def resize-ew (cursor-fn :resize-h 0)) -(def resize-nesw (cursor-fn :resize-h 45)) -(def resize-ns (cursor-fn :resize-h 90)) -(def resize-nwse (cursor-fn :resize-h 135)) -(def rotate (cursor-fn :rotate 90)) -(def text (cursor-fn :text 0)) +(def ^:cursor resize-ew (cursor-fn :resize-h 0)) +(def ^:cursor resize-nesw (cursor-fn :resize-h 45)) +(def ^:cursor resize-ns (cursor-fn :resize-h 90)) +(def ^:cursor resize-nwse (cursor-fn :resize-h 135)) +(def ^:cursor rotate (cursor-fn :rotate 90)) +(def ^:cursor text (cursor-fn :text 0)) ;; Text -(def scale-ew (cursor-fn :scale-h 0)) -(def scale-nesw (cursor-fn :scale-h 45)) -(def scale-ns (cursor-fn :scale-h 90)) -(def scale-nwse (cursor-fn :scale-h 135)) +(def ^:cursor scale-ew (cursor-fn :scale-h 0)) +(def ^:cursor scale-nesw (cursor-fn :scale-h 45)) +(def ^:cursor scale-ns (cursor-fn :scale-h 90)) +(def ^:cursor scale-nwse (cursor-fn :scale-h 135)) + +(def ^:cursor resize-ew-2 (cursor-fn :resize-h-2 0)) +(def ^:cursor resize-ns-2 (cursor-fn :resize-h-2 90)) + +(def default + "A collection of all icons" + (collect-cursors)) -;; -(def resize-ew-2 (cursor-fn :resize-h-2 0)) -(def resize-ns-2 (cursor-fn :resize-h-2 90)) - (mf/defc debug-preview {::mf/wrap-props false} [] - (let [rotation (mf/use-state 0)] - (mf/use-effect (fn [] (ts/interval 100 #(reset! rotation inc)))) + (let [rotation (mf/use-state 0) + entries (->> (seq (js/Object.entries default)) + (sort-by first))] + + (mf/with-effect [] + (ts/interval 100 #(reset! rotation inc))) [:section.debug-icons-preview - (for [[key val] (sort-by first (ns-publics 'app.main.ui.cursors))] - (when (not= key 'debug-icons-preview) - (let [value (deref val) - value (if (fn? value) (value @rotation) value)] - [:div.cursor-item {:key key} - [:div {:style {:width "100px" - :height "100px" - :background-image (-> value (str/replace #"(url\(.*\)).*" "$1")) - :background-size "contain" - :background-repeat "no-repeat" - :background-position "center" - :cursor value}}] + (for [[key value] entries] + (let [value (if (fn? value) (value @rotation) value)] + [:div.cursor-item {:key key} + [:div {:style {:width "100px" + :height "100px" + :background-image (-> value (str/replace #"(url\(.*\)).*" "$1")) + :background-size "contain" + :background-repeat "no-repeat" + :background-position "center" + :cursor value}}] - [:span {:style {:white-space "nowrap" - :margin-right "1rem"}} (pr-str key)]])))])) + [:span {:style {:white-space "nowrap" + :margin-right "1rem"}} (pr-str key)]]))])) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index b0ae43974b..4adec8d159 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.spec :as us] @@ -14,7 +15,6 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as ctx] - [app.main.ui.dashboard.export] [app.main.ui.dashboard.files :refer [files-section]] [app.main.ui.dashboard.fonts :refer [fonts-page font-providers-page]] [app.main.ui.dashboard.import] @@ -29,6 +29,7 @@ [app.util.keyboard :as kbd] [app.util.object :as obj] [goog.events :as events] + [okulary.core :as l] [rumext.v2 :as mf])) (defn ^boolean uuid-str? @@ -42,7 +43,7 @@ team-id (get-in route [:params :path :team-id]) project-id (get-in route [:params :path :project-id])] (cond-> - {:search-term search-term} + {:search-term search-term} (uuid-str? team-id) (assoc :team-id (uuid team-id)) @@ -57,13 +58,15 @@ project-id (:id project) team-id (:id team) + dashboard-local (mf/deref refs/dashboard-local) + file-menu-open? (:menu-open dashboard-local) + default-project-id (mf/with-memo [projects] (->> (vals projects) (d/seek :is-default) (:id))) - on-resize (mf/use-fn (fn [_] @@ -81,7 +84,10 @@ (mf/use-effect on-resize) - [:div.dashboard-content {:on-click clear-selected-fn :ref container} + + [:div {:class (stl/css :dashboard-content) + :style {:pointer-events (when file-menu-open? "none")} + :on-click clear-selected-fn :ref container} (case section :dashboard-projects [:* @@ -136,27 +142,32 @@ nil)])) +(def dashboard-initialized + (l/derived :current-team-id st/state)) + (mf/defc dashboard [{:keys [route profile] :as props}] - (let [section (get-in route [:data :name]) - params (parse-params route) + (let [section (get-in route [:data :name]) + params (parse-params route) - project-id (:project-id params) - team-id (:team-id params) - search-term (:search-term params) + project-id (:project-id params) + team-id (:team-id params) + search-term (:search-term params) - teams (mf/deref refs/teams) - team (get teams team-id) + teams (mf/deref refs/teams) + team (get teams team-id) - projects (mf/deref refs/dashboard-projects) - project (get projects project-id)] + projects (mf/deref refs/dashboard-projects) + project (get projects project-id) + + initialized? (mf/deref dashboard-initialized)] (hooks/use-shortcuts ::dashboard sc/shortcuts) - (mf/with-effect [profile team-id] + (mf/with-effect [team-id] (st/emit! (dd/initialize {:id team-id})) (fn [] - (dd/finalize {:id team-id}))) + (st/emit! (dd/finalize {:id team-id})))) (mf/with-effect [] (let [key (events/listen goog/global "keydown" @@ -169,15 +180,16 @@ [:& (mf/provider ctx/current-team-id) {:value team-id} [:& (mf/provider ctx/current-project-id) {:value project-id} - ;; NOTE: dashboard events and other related functions assumes - ;; that the team is a implicit context variable that is - ;; available using react context or accessing - ;; the :current-team-id on the state. We set the key to the - ;; team-id because we want to completely refresh all the - ;; components on team change. Many components assumes that the - ;; team is already set so don't put the team into mf/deps. - (when team - [:main.dashboard-layout {:key (:id team)} + ;; NOTE: dashboard events and other related functions assumes + ;; that the team is a implicit context variable that is + ;; available using react context or accessing + ;; the :current-team-id on the state. We set the key to the + ;; team-id because we want to completely refresh all the + ;; components on team change. Many components assumes that the + ;; team is already set so don't put the team into mf/deps. + (when (and team initialized?) + [:main {:class (stl/css :dashboard) + :key (:id team)} [:& sidebar {:team team :projects projects @@ -185,7 +197,7 @@ :profile profile :section section :search-term search-term}] - (when (and team (seq projects)) + (when (and team profile (seq projects)) [:& dashboard-content {:projects projects :profile profile @@ -193,3 +205,4 @@ :section section :search-term search-term :team team}])])]])) + diff --git a/frontend/src/app/main/ui/dashboard.scss b/frontend/src/app/main/ui/dashboard.scss new file mode 100644 index 0000000000..bad41ea111 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard.scss @@ -0,0 +1,31 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "refactor/common-refactor.scss" as *; + +.dashboard { + @extend .new-scrollbar; + background-color: var(--app-background); + display: grid; + grid-template-columns: $s-40 $s-256 1fr; + grid-template-rows: $s-52 1fr; + height: 100vh; + + :global(svg#loader-pencil) { + fill: $df-secondary; + width: $s-32; + } +} + +.dashboard-content { + display: grid; + grid-template-rows: $s-64 1fr; + position: relative; + grid-row: 1 / span 2; + padding: $s-16 $s-16 0 0; + overflow: hidden; + width: 100%; +} diff --git a/frontend/src/app/main/ui/dashboard/change_owner.cljs b/frontend/src/app/main/ui/dashboard/change_owner.cljs index 607f115e16..b3a8e04d2d 100644 --- a/frontend/src/app/main/ui/dashboard/change_owner.cljs +++ b/frontend/src/app/main/ui/dashboard/change_owner.cljs @@ -5,15 +5,16 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.change-owner + (:require-macros [app.main.style :as stl]) (:require - [app.common.spec :as us] + [app.common.spec :as us] [app.main.data.modal :as modal] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.main.ui.icons :as i] [app.util.i18n :as i18n :refer [tr]] - [cljs.spec.alpha :as s] + [cljs.spec.alpha :as s] [rumext.v2 :as mf])) (s/def ::member-id ::us/uuid) @@ -28,10 +29,13 @@ members-map (mf/deref refs/dashboard-team-members) members (vals members-map) - options (into [{:value "" - :label (tr "modals.leave-and-reassign.select-member-to-promote")}] - (filter #(not= (:label %) (:fullname profile)) - (map #(hash-map :label (:name %) :value (str (:id %))) members))) + options + (into [{:value "" + :label (tr "modals.leave-and-reassign.select-member-to-promote")}] + (comp + (filter #(not= (:email %) (:email profile))) + (map #(hash-map :label (:name %) :value (str (:id %))))) + members) on-cancel #(st/emit! (modal/hide)) on-accept @@ -39,34 +43,37 @@ (let [member-id (get-in @form [:clean-data :member-id])] (accept member-id)))] - [:div.modal-overlay - [:div.modal-container.confirm-dialog - [:div.modal-header - [:div.modal-header-title - [:h2 (tr "modals.leave-and-reassign.title")]] - [:div.modal-close-button - {:on-click on-cancel} i/close]] + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} (tr "modals.leave-and-reassign.title")] + [:button {:class (stl/css :modal-close-btn) + :on-click on-cancel} i/close]] - [:div.modal-content.generic-form - [:p (tr "modals.leave-and-reassign.hint1" (:name team))] + [:div {:class (stl/css :modal-content)} + [:p {:class (stl/css :modal-msg)} + (tr "modals.leave-and-reassign.hint1" (:name team))] (if (empty? members) - [:p (tr "modals.leave-and-reassign.forbidden")] + [:p {:class (stl/css :modal-msg)} + (tr "modals.leave-and-reassign.forbidden")] [:* [:& fm/form {:form form} [:& fm/select {:name :member-id :options options}]]])] - [:div.modal-footer - [:div.action-buttons - [:input.cancel-button - {:type "button" - :value (tr "labels.cancel") - :on-click on-cancel}] + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:input {:class (stl/css :cancel-button) + :type "button" + :value (tr "labels.cancel") + :on-click on-cancel}] [:input.accept-button {:type "button" - :class (if (:valid @form) "danger" "btn-disabled") + :class (stl/css-case :accept-btn true + :danger (:valid @form) + :global/disabled (not (:valid @form))) :disabled (not (:valid @form)) :value (tr "modals.leave-and-reassign.promote-and-leave") :on-click on-accept}]]]]])) diff --git a/frontend/src/app/main/ui/dashboard/change_owner.scss b/frontend/src/app/main/ui/dashboard/change_owner.scss new file mode 100644 index 0000000000..0b150c1c5e --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/change_owner.scss @@ -0,0 +1,56 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + @extend .modal-container-base; +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include uppercaseTitleTipography; + color: var(--modal-title-foreground-color); +} + +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content { + @include bodySmallTypography; + margin-bottom: $s-24; +} + +.input-wrapper { + @extend .input-with-label; +} + +.action-buttons { + @extend .modal-action-btns; +} + +.cancel-button { + @extend .modal-cancel-btn; +} + +.accept-btn { + @extend .modal-accept-btn; + &.danger { + @extend .modal-danger-btn; + } +} + +.modal-msg { + color: var(--modal-text-foreground-color); +} diff --git a/frontend/src/app/main/ui/dashboard/comments.cljs b/frontend/src/app/main/ui/dashboard/comments.cljs index dc92961ddf..f200d98f93 100644 --- a/frontend/src/app/main/ui/dashboard/comments.cljs +++ b/frontend/src/app/main/ui/dashboard/comments.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.comments + (:require-macros [app.main.style :as stl]) (:require [app.main.data.comments :as dcm] [app.main.data.events :as ev] @@ -14,18 +15,54 @@ [app.main.ui.comments :as cmt] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.icons :as i] - [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] - [potok.core :as ptk] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) + +(def ^:private close-icon + (i/icon-xref :close (stl/css :close-icon))) + +(def ^:private comments-icon-svg + (i/icon-xref :comments (stl/css :comments-icon))) + + +(def ^:private comments-icon-small + (i/icon-xref :comments (stl/css :comments-icon-small))) + +(mf/defc comments-icon + [{:keys [profile show? on-show-comments]}] + + (let [threads-map (mf/deref refs/comment-threads) + + tgroups + (->> (vals threads-map) + (sort-by :modified-at) + (reverse) + (dcm/apply-filters {} profile) + (dcm/group-threads-by-file-and-page)) + + handle-keydown + (mf/use-callback + (mf/deps on-show-comments) + (fn [event] + (when (kbd/enter? event) + (on-show-comments event))))] + + [:div {:class (stl/css :dashboard-comments-section)} + [:button {:tab-index "0" + :on-click on-show-comments + :on-key-down handle-keydown + :data-test "open-comments" + :class (stl/css-case :comment-button true + :open show? + :unread (boolean (seq tgroups)))} + comments-icon-small]])) + (mf/defc comments-section - [{:keys [profile team]}] - (let [show-dropdown? (mf/use-state false) - show-dropdown (mf/use-fn #(reset! show-dropdown? true)) - hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) - threads-map (mf/deref refs/comment-threads) + [{:keys [profile team show? on-hide-comments]}] + (let [threads-map (mf/deref refs/comment-threads) users (mf/deref refs/current-team-comments-users) team-id (:id team) @@ -35,6 +72,13 @@ (dcm/apply-filters {} profile) (dcm/group-threads-by-file-and-page)) + handle-keydown + (mf/use-callback + (mf/deps on-hide-comments) + (fn [event] + (when (kbd/enter? event) + (on-hide-comments event)))) + on-navigate (mf/use-callback (fn [thread] @@ -42,61 +86,43 @@ (with-meta {::ev/origin "dashboard"})))))] (mf/use-effect - (mf/deps team-id) - (fn [] - (st/emit! (dcm/retrieve-unread-comment-threads team-id)))) - - (mf/use-effect - (mf/deps @show-dropdown?) + (mf/deps team-id) (fn [] - (when @show-dropdown? + (st/emit! (dcm/retrieve-unread-comment-threads team-id)))) + + (mf/use-effect + (mf/deps show?) + (fn [] + (when show? (st/emit! (ptk/event ::ev/event {::ev/name "open-comment-notifications" ::ev/origin "dashboard"}))))) - [:div.dashboard-comments-section - [:div.button - {:tab-index "0" - :on-click show-dropdown - :on-key-down (fn [event] - (when (kbd/enter? event) - (show-dropdown event))) - :data-test "open-comments" - :class (dom/classnames :open @show-dropdown? - :unread (boolean (seq tgroups)))} - i/chat] - - [:& dropdown {:show @show-dropdown? :on-close hide-dropdown} - [:div.dropdown.comments-section.comment-threads-section. - [:div.header - [:h3 (tr "labels.comments")] - [:span.close {:tab-index (if @show-dropdown? - "0" - "-1") - :on-click hide-dropdown - :on-key-down (fn [event] - (when (kbd/enter? event) - (hide-dropdown event)))} i/close]] - - [:hr] + [:div {:class (stl/css :dashboard-comments-section)} + [:& dropdown {:show show? :on-close on-hide-comments} + [:div {:class (stl/css :dropdown :comments-section :comment-threads-section)} + [:div {:class (stl/css :header)} + [:h3 {:class (stl/css :header-title)} (tr "labels.comments")] + [:button {:class (stl/css :close-btn) + :tab-index (if show? "0" "-1") + :on-click on-hide-comments + :on-key-down handle-keydown} + close-icon]] (if (seq tgroups) - [:div.thread-groups + [:div {:class (stl/css :thread-groups)} [:& cmt/comment-thread-group {:group (first tgroups) :on-thread-click on-navigate :show-file-name true :users users}] (for [tgroup (rest tgroups)] - [:* - [:hr] + [:& cmt/comment-thread-group + {:group tgroup + :on-thread-click on-navigate + :show-file-name true + :users users + :key (:page-id tgroup)}])] - [:& cmt/comment-thread-group - {:group tgroup - :on-thread-click on-navigate - :show-file-name true - :users users - :key (:page-id tgroup)}]])] - - [:div.thread-groups-placeholder - i/chat + [:div {:class (stl/css :thread-groups-placeholder)} + comments-icon-svg (tr "labels.no-comments-available")])]]])) diff --git a/frontend/src/app/main/ui/dashboard/comments.scss b/frontend/src/app/main/ui/dashboard/comments.scss new file mode 100644 index 0000000000..b6e872e52c --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/comments.scss @@ -0,0 +1,112 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.dashboard-comments-section { + @include flexCenter; + position: relative; + border-radius: $br-8; +} + +.thread-groups { + height: calc(100% - $s-32); + overflow-y: scroll; + max-height: $s-440; + overflow: auto; +} + +.thread-group { + display: flex; + flex-direction: column; + font-size: $fs-12; +} + +.thread-groups-placeholder { + align-items: center; + display: flex; + flex-direction: column; + font-size: $fs-12; + padding: $s-24; + text-align: center; + color: $df-secondary; +} + +.comments-icon { + @extend .button-icon; + stroke: var(--icon-foreground); + height: $s-24; + width: $s-24; + margin-bottom: $s-24; +} + +.comment-button { + @include buttonStyle; + @include flexCenter; + border-radius: $br-8; + height: $s-32; + width: $s-32; + --comment-icon-small-foreground-color: var(--icon-foreground); + + &.unread, + &.open { + --comment-icon-small-foreground-color: var(--icon-foreground-selected); + } + + &:hover { + background-color: $db-quaternary; + --comment-icon-small-foreground-color: var(--icon-foreground-active); + } +} + +.comments-icon-small { + @extend .button-icon; + stroke: var(--comment-icon-small-foreground-color); +} + +.dropdown { + @include menuShadow; + background-color: $db-tertiary; + border-radius: $br-8; + border: $s-1 solid transparent; + bottom: $s-4; + height: 40vh; + max-height: $s-480; + min-height: $s-200; + position: absolute; + width: 100%; + z-index: $z-index-4; + + hr { + margin: 0; + border-color: $df-secondary; + } +} + +.header { + display: flex; + height: $s-40; + align-items: center; + padding: 0 $s-12; +} + +.header-title { + color: $df-secondary; + font-size: $fs-11; + line-height: 1.28; + flex-grow: 1; + text-transform: uppercase; +} + +.close-btn { + @include buttonStyle; + @include flexCenter; +} + +.close-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} diff --git a/frontend/src/app/main/ui/dashboard/export.cljs b/frontend/src/app/main/ui/dashboard/export.cljs deleted file mode 100644 index 509f6ae60e..0000000000 --- a/frontend/src/app/main/ui/dashboard/export.cljs +++ /dev/null @@ -1,166 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.dashboard.export - (:require - [app.common.data :as d] - [app.main.data.modal :as modal] - [app.main.features :as features] - [app.main.store :as st] - [app.main.ui.icons :as i] - [app.main.worker :as uw] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [tr]] - [beicon.core :as rx] - [rumext.v2 :as mf])) - -(def ^:const options [:all :merge :detach]) - -(mf/defc export-entry - [{:keys [file]}] - - [:div.file-entry - {:class (dom/classnames - :loading (:loading? file) - :success (:export-success? file) - :error (:export-error? file))} - [:div.file-name - [:div.file-icon - (cond (:export-success? file) i/tick - (:export-error? file) i/close - (:loading? file) i/loader-pencil)] - - [:div.file-name-label (:name file)]]]) - -(defn mark-file-error [files file-id] - (->> files - (mapv #(cond-> % - (= file-id (:id %)) - (assoc :export-error? true - :loading? false))))) - -(defn mark-file-success [files file-id] - (->> files - (mapv #(cond-> % - (= file-id (:id %)) - (assoc :export-success? true - :loading? false))))) - -(mf/defc export-dialog - {::mf/register modal/components - ::mf/register-as :export} - [{:keys [team-id files has-libraries? binary?]}] - (let [state (mf/use-state {:status :prepare - :files (->> files (mapv #(assoc % :loading? true)))}) - selected-option (mf/use-state :all) - - components-v2 (features/use-feature :components-v2) - - start-export - (fn [] - (swap! state assoc :status :exporting) - (->> (uw/ask-many! - {:cmd (if binary? :export-binary-file :export-standard-file) - :team-id team-id - :export-type @selected-option - :files files - :components-v2 components-v2}) - (rx/delay-emit 1000) - (rx/subs - (fn [msg] - (when (= :error (:type msg)) - (swap! state update :files mark-file-error (:file-id msg))) - - (when (= :finish (:type msg)) - (swap! state update :files mark-file-success (:file-id msg)) - (dom/trigger-download-uri (:filename msg) (:mtype msg) (:uri msg))))))) - - cancel-fn - (mf/use-callback - (fn [event] - (dom/prevent-default event) - (st/emit! (modal/hide)))) - - accept-fn - (mf/use-callback - (mf/deps @selected-option) - (fn [event] - (dom/prevent-default event) - (start-export))) - - on-change-handler - (mf/use-callback - (fn [_ type] - (reset! selected-option type)))] - - (mf/use-effect - (fn [] - (when-not has-libraries? - ;; Start download automatically - (start-export)))) - - [:div.modal-overlay - [:div.modal-container.export-dialog - [:div.modal-header - [:div.modal-header-title - [:h2 (tr "dashboard.export.title")]] - - [:div.modal-close-button - {:on-click cancel-fn} i/close]] - - (cond - (= (:status @state) :prepare) - [:* - [:div.modal-content - [:p.explain (tr "dashboard.export.explain")] - [:p.detail (tr "dashboard.export.detail")] - - (for [type [:all :merge :detach]] - (let [selected? (= @selected-option type)] - [:div.export-option {:class (when selected? "selected")} - [:label.option-container - ;; Execution time translation strings: - ;; dashboard.export.options.all.message - ;; dashboard.export.options.all.title - ;; dashboard.export.options.detach.message - ;; dashboard.export.options.detach.title - ;; dashboard.export.options.merge.message - ;; dashboard.export.options.merge.title - [:h3 (tr (str "dashboard.export.options." (d/name type) ".title"))] - [:p (tr (str "dashboard.export.options." (d/name type) ".message"))] - [:input {:type "radio" - :checked selected? - :on-change #(on-change-handler % type) - :name "export-option"}] - [:span {:class "option-radio-check"}]]]))] - - [:div.modal-footer - [:div.action-buttons - [:input.cancel-button - {:type "button" - :value (tr "labels.cancel") - :on-click cancel-fn}] - - [:input.accept-button - {:class "primary" - :type "button" - :value (tr "labels.continue") - :on-click accept-fn}]]]] - - (= (:status @state) :exporting) - [:* - [:div.modal-content - (for [file (:files @state)] - [:& export-entry {:file file}])] - - [:div.modal-footer - [:div.action-buttons - [:input.accept-button - {:class "primary" - :type "button" - :value (tr "labels.close") - :disabled (->> @state :files (some :loading?)) - :on-click cancel-fn}]]]])]])) diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 23726875ea..4be78a0e2b 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -6,9 +6,10 @@ (ns app.main.ui.dashboard.file-menu (:require + [app.main.data.common :as dcm] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.repo :as rp] [app.main.store :as st] @@ -17,8 +18,8 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] - [beicon.core :as rx] - [potok.core :as ptk] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) (defn get-project-name @@ -53,12 +54,14 @@ projects)) (mf/defc file-menu - [{:keys [files show? on-edit on-menu-close top left navigate? origin parent-id] :as props}] + {::mf/wrap-props false} + [{:keys [files show? on-edit on-menu-close top left navigate? origin parent-id]}] (assert (seq files) "missing `files` prop") (assert (boolean? show?) "missing `show?` prop") (assert (fn? on-edit) "missing `on-edit` prop") (assert (fn? on-menu-close) "missing `on-menu-close` prop") (assert (boolean? navigate?) "missing `navigate?` prop") + (let [is-lib-page? (= :libraries origin) is-search-page? (= :search origin) top (or top 0) @@ -74,6 +77,7 @@ other-teams (remove #(= (:id %) current-team-id) (vals @teams)) current-projects (remove #(= (:id %) (:project-id file)) (:projects current-team)) + on-new-tab (fn [_] (let [path-params {:project-id (:project-id file) @@ -84,17 +88,17 @@ on-duplicate (fn [_] (apply st/emit! (map dd/duplicate-file files)) - (st/emit! (dm/success (tr "dashboard.success-duplicate-file" (i18n/c (count files)))))) + (st/emit! (msg/success (tr "dashboard.success-duplicate-file" (i18n/c (count files)))))) - delete-fn + on-delete-accept (fn [_] (apply st/emit! (map dd/delete-file files)) - (st/emit! (dm/success (tr "dashboard.success-delete-file" (i18n/c (count files)))))) + (st/emit! (msg/success (tr "dashboard.success-delete-file" (i18n/c (count files)))) + (dd/clear-selected-files))) on-delete (fn [event] (dom/stop-propagation event) - (let [num-shared (filter #(:is-shared %) files)] (if (< 0 (count num-shared)) @@ -102,7 +106,7 @@ {:type :delete-shared-libraries :origin :delete :ids (into #{} (map :id) files) - :on-accept delete-fn + :on-accept on-delete-accept :count-libraries (count num-shared)})) (if multi? @@ -111,32 +115,47 @@ :title (tr "modals.delete-file-multi-confirm.title" file-count) :message (tr "modals.delete-file-multi-confirm.message" file-count) :accept-label (tr "modals.delete-file-multi-confirm.accept" file-count) - :on-accept delete-fn})) + :on-accept on-delete-accept})) (st/emit! (modal/show {:type :confirm :title (tr "modals.delete-file-confirm.title") :message (tr "modals.delete-file-confirm.message") :accept-label (tr "modals.delete-file-confirm.accept") - :on-accept delete-fn})))))) + :on-accept on-delete-accept})))))) on-move-success (fn [team-id project-id] (if multi? - (st/emit! (dm/success (tr "dashboard.success-move-files"))) - (st/emit! (dm/success (tr "dashboard.success-move-file")))) + (st/emit! (msg/success (tr "dashboard.success-move-files"))) + (st/emit! (msg/success (tr "dashboard.success-move-file")))) (if (or navigate? (not= team-id current-team-id)) (st/emit! (dd/go-to-files team-id project-id)) (st/emit! (dd/fetch-recent-files team-id) (dd/clear-selected-files)))) + on-move-accept + (fn [params team-id project-id] + (st/emit! (dd/move-files + (with-meta params + {:on-success #(on-move-success team-id project-id)})))) + on-move (fn [team-id project-id] (let [params {:ids (into #{} (map :id) files) :project-id project-id}] (fn [] - (st/emit! (dd/move-files - (with-meta params - {:on-success #(on-move-success team-id project-id)})))))) + + (let [num-shared (filter #(:is-shared %) files)] + (if (and (< 0 (count num-shared)) + (not= team-id current-team-id)) + (st/emit! (modal/show + {:type :delete-shared-libraries + :origin :move + :ids (into #{} (map :id) files) + :on-accept #(on-move-accept params team-id project-id) + :count-libraries (count num-shared)})) + + (on-move-accept params team-id project-id)))))) add-shared #(st/emit! (dd/set-file-shared (assoc file :is-shared true))) @@ -147,19 +166,10 @@ (fn [_] (run! #(st/emit! (dd/set-file-shared (assoc % :is-shared false))) files))) - on-add-shared (fn [event] (dom/stop-propagation event) - (st/emit! (modal/show - {:type :confirm - :message "" - :title (tr "modals.add-shared-confirm.message" (:name file)) - :hint (tr "modals.add-shared-confirm.hint") - :cancel-label :omit - :accept-label (tr "modals.add-shared-confirm.accept") - :accept-style :primary - :on-accept add-shared}))) + (st/emit! (dcm/show-shared-dialog (:id file) add-shared))) on-del-shared (fn [event] @@ -173,38 +183,26 @@ :count-libraries file-count}))) on-export-files - (fn [event-name binary?] - (st/emit! (ptk/event ::ev/event {::ev/name event-name - ::ev/origin "dashboard" - :num-files (count files)})) - - (->> (rx/from files) - (rx/flat-map - (fn [file] - (->> (rp/cmd! :has-file-libraries {:file-id (:id file)}) - (rx/map #(assoc file :has-libraries? %))))) - (rx/reduce conj []) - (rx/subs - (fn [files] - (st/emit! - (modal/show - {:type :export - :team-id current-team-id - :has-libraries? (->> files (some :has-libraries?)) - :files files - :binary? binary?})))))) + (mf/use-fn + (mf/deps files) + (fn [binary?] + (let [evname (if binary? + "export-binary-files" + "export-standard-files")] + (st/emit! (ptk/event ::ev/event {::ev/name evname + ::ev/origin "dashboard" + :num-files (count files)}) + (dcm/export-files files binary?))))) on-export-binary-files - (mf/use-callback - (mf/deps files current-team-id) - (fn [_] - (on-export-files "export-binary-files" true))) + (mf/use-fn + (mf/deps on-export-files) + (partial on-export-files true)) on-export-standard-files - (mf/use-callback - (mf/deps files current-team-id) - (fn [_] - (on-export-files "export-standard-files" false))) + (mf/use-fn + (mf/deps on-export-files) + (partial on-export-files false)) ;; NOTE: this is used for detect if component is still mounted mounted-ref (mf/use-ref true)] @@ -215,8 +213,8 @@ (when show? (->> (rp/cmd! :get-all-projects) (rx/map group-by-team) - (rx/subs #(when (mf/ref-val mounted-ref) - (reset! teams %))))))) + (rx/subs! #(when (mf/ref-val mounted-ref) + (reset! teams %))))))) (when current-team (let [sub-options (concat (vec (for [project current-projects] diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index 1ad4e9ada8..afee2564dc 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -5,25 +5,28 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.files + (:require-macros [app.main.style :as stl]) (:require - [app.common.math :as mth] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.dashboard.grid :refer [grid]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] + [app.main.ui.dashboard.pin-button :refer [pin-button*]] [app.main.ui.dashboard.project-menu :refer [project-menu]] + [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.router :as rt] - [app.util.webapi :as wapi] - [beicon.core :as rx] [cuerdas.core :as str] [rumext.v2 :as mf])) +(def ^:private menu-icon + (i/icon-xref :menu (stl/css :menu-icon))) + (mf/defc header [{:keys [project create-fn] :as props}] (let [local (mf/use-state @@ -63,20 +66,21 @@ (dd/clear-selected-files))))] - [:header.dashboard-header + [:header {:class (stl/css :dashboard-header)} (if (:is-default project) - [:div.dashboard-title#dashboard-drafts-title + [:div#dashboard-drafts-title {:class (stl/css :dashboard-title)} [:h1 (tr "labels.drafts")]] (if (:edition @local) - [:& inline-edition {:content (:name project) - :on-end (fn [name] - (let [name (str/trim name)] - (when-not (str/empty? name) - (st/emit! (-> (dd/rename-project (assoc project :name name)) - (with-meta {::ev/origin "project"})))) - (swap! local assoc :edition false)))}] - [:div.dashboard-title + [:& inline-edition + {:content (:name project) + :on-end (fn [name] + (let [name (str/trim name)] + (when-not (str/empty? name) + (st/emit! (-> (dd/rename-project (assoc project :name name)) + (with-meta {::ev/origin "project"})))) + (swap! local assoc :edition false)))}] + [:div {:class (stl/css :dashboard-title)} [:h1 {:on-double-click on-edit :data-test "project-title" :id (:id project)} @@ -90,52 +94,38 @@ :on-menu-close on-menu-close :on-import on-import}] - [:div.dashboard-header-actions - [:a.btn-secondary.btn-small - {:tab-index "0" - :on-click on-create-click - :data-test "new-file" - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-create-click event)))} + [:div {:class (stl/css :dashboard-header-actions)} + [:a {:class (stl/css :btn-secondary :btn-small :new-file) + :tab-index "0" + :on-click on-create-click + :data-test "new-file" + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-create-click event)))} (tr "dashboard.new-file")] (when-not (:is-default project) - [:button.icon.pin-icon.tooltip.tooltip-bottom - {:tab-index "0" - :class (when (:is-pinned project) "active") + [:> pin-button* + {:tab-index 0 + :is-pinned (:is-pinned project) :on-click toggle-pin - :alt (tr "dashboard.pin-unpin") - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-pin event)))} - (if (:is-pinned project) - i/pin-fill - i/pin)]) + :on-key-down (fn [event] (when (kbd/enter? event) (toggle-pin event)))}]) - [:div.icon.tooltip.tooltip-bottom-left - {:tab-index "0" - :on-click on-menu-click - :alt (tr "dashboard.options") - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-menu-click event)))} - i/actions]]])) + [:div {:class (stl/css :icon) + :tab-index "0" + :on-click on-menu-click + :title (tr "dashboard.options") + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event)))} + menu-icon]]])) (mf/defc files-section [{:keys [project team] :as props}] (let [files-map (mf/deref refs/dashboard-files) project-id (:id project) - width (mf/use-state nil) - rowref (mf/use-ref) - itemsize (if (>= @width 1030) - 280 - 230) - ratio (if (some? @width) (/ @width itemsize) 0) - nitems (mth/floor ratio) - limit (min 10 nitems) - limit (max 1 limit) + [rowref limit] (hooks/use-dynamic-grid-item-width) files (mf/with-memo [project-id files-map] (->> (vals files-map) @@ -160,21 +150,6 @@ (st/emit! (-> (dd/create-file (with-meta params mdata)) (with-meta {::ev/origin origin}))))))] - (mf/with-effect [] - (let [node (mf/ref-val rowref) - mnt? (volatile! true) - sub (->> (wapi/observe-resize node) - (rx/observe-on :af) - (rx/subs (fn [entries] - (let [row (first entries) - row-rect (.-contentRect ^js row) - row-width (.-width ^js row-rect)] - (when @mnt? - (reset! width row-width))))))] - (fn [] - (vreset! mnt? false) - (rx/dispose! sub)))) - (mf/with-effect [project] (when project (let [pname (if (:is-default project) @@ -190,7 +165,8 @@ [:& header {:team team :project project :create-fn create-file}] - [:section.dashboard-container.no-bg {:ref rowref} + [:section {:class (stl/css :dashboard-container :no-bg) + :ref rowref} [:& grid {:project project :files files :origin :files diff --git a/frontend/src/app/main/ui/dashboard/files.scss b/frontend/src/app/main/ui/dashboard/files.scss new file mode 100644 index 0000000000..98cf1733e6 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/files.scss @@ -0,0 +1,37 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; +@use "common/refactor/common-dashboard"; + +.dashboard-container { + flex: 1 0 0; + margin-right: $s-16; + overflow-y: auto; + width: 100%; + border-top: $s-1 solid $db-quaternary; + + &.dashboard-projects { + user-select: none; + } + &.dashboard-shared { + width: calc(100vw - $s-320); + margin-right: $s-52; + } + + &.search { + margin-top: $s-12; + } +} + +.new-file { + margin-inline-end: $s-8; +} + +.menu-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs index f7236a88ca..be6cd908f6 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.cljs +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -5,293 +5,393 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.fonts + (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.common.media :as cm] [app.main.data.fonts :as df] [app.main.data.modal :as modal] [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] - [app.main.ui.components.context-menu :refer [context-menu]] + [app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.icons :as i] + [app.main.ui.notifications.context-notification :refer [context-notification]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] [rumext.v2 :as mf])) -(defn- use-set-page-title +(defn- use-page-title [team section] - (mf/use-effect - (mf/deps team) - (fn [] - (when team - (let [tname (if (:is-default team) - (tr "dashboard.your-penpot") - (:name team))] - (case section - :fonts (dom/set-html-title (tr "title.dashboard.fonts" tname)) - :providers (dom/set-html-title (tr "title.dashboard.font-providers" tname)))))))) + (mf/with-effect [team] + (when team + (let [tname (if (:is-default team) + (tr "dashboard.your-penpot") + (:name team))] + (case section + :fonts (dom/set-html-title (tr "title.dashboard.fonts" tname)) + :providers (dom/set-html-title (tr "title.dashboard.font-providers" tname))))))) + +(defn- bad-font-family-tmp? + [font] + (and (contains? font :font-family-tmp) + (str/blank? (:font-family-tmp font)))) (mf/defc header - {::mf/wrap [mf/memo]} - [{:keys [section team] :as props}] - ;; (let [go-fonts - ;; (mf/use-callback - ;; (mf/deps team) - ;; #(st/emit! (rt/nav :dashboard-fonts {:team-id (:id team)}))) - - ;; go-providers - ;; (mf/use-callback - ;; (mf/deps team) - ;; #(st/emit! (rt/nav :dashboard-font-providers {:team-id (:id team)})))] - - (use-set-page-title team section) - - [:header.dashboard-header - [:div.dashboard-title#dashboard-fonts-title - [:h1 (tr "labels.fonts")]] - [:nav - #_[:ul - [:li {:class (when (= section :fonts) "active")} - [:a {:on-click go-fonts} (tr "labels.custom-fonts")]] - [:li {:class (when (= section :providers) "active")} - [:a {:on-click go-providers} (tr "labels.font-providers")]]]] - - [:div]]) + {::mf/props :obj + ::mf/memo true + ::mf/private true} + [{:keys [section team]}] + (use-page-title team section) + [:header {:class (stl/css :dashboard-header)} + [:div#dashboard-fonts-title {:class (stl/css :dashboard-title)} + [:h1 (tr "labels.fonts")]]]) (mf/defc font-variant-display-name + {::mf/props :obj + ::mf/private true} [{:keys [variant]}] [:* [:span (cm/font-weight->name (:font-weight variant))] (when (not= "normal" (:font-style variant)) [:span " " (str/capital (:font-style variant))])]) -(mf/defc fonts-upload +(mf/defc uploaded-fonts + {::mf/props :obj + ::mf/private true} [{:keys [team installed-fonts] :as props}] - (let [fonts (mf/use-state {}) - input-ref (mf/use-ref) + (let [fonts* (mf/use-state {}) + fonts (deref fonts*) + font-vals (mf/with-memo [fonts] + (->> fonts + (into [] (map val)) + (not-empty))) - uploading (mf/use-state #{}) + team-id (:id team) + + input-ref (mf/use-ref) + + uploading* (mf/use-state #{}) + uploading (deref uploading*) + + disable-upload-all? + (some bad-font-family-tmp? fonts) + + problematic-fonts? + (some :height-warning? (vals fonts)) on-click - (mf/use-callback #(dom/click (mf/ref-val input-ref))) + (mf/use-fn #(dom/click (mf/ref-val input-ref))) on-selected - (mf/use-callback - (mf/deps team installed-fonts) + (mf/use-fn + (mf/deps team-id installed-fonts) (fn [blobs] - (->> (df/process-upload blobs (:id team)) - (rx/subs (fn [result] - (swap! fonts df/merge-and-group-fonts installed-fonts result)) - (fn [error] - (js/console.error "error" error)))))) + (->> (df/process-upload blobs team-id) + (rx/subs! (fn [result] + (swap! fonts* df/merge-and-group-fonts installed-fonts result)) + (fn [error] + (js/console.error "error" error)))))) - on-upload - (mf/use-callback - (mf/deps team) - (fn [item] - (swap! uploading conj (:id item)) + on-upload* + (mf/use-fn + (fn [{:keys [id] :as item}] + (swap! uploading* conj id) (->> (rp/cmd! :create-font-variant item) (rx/delay-at-least 2000) - (rx/subs (fn [font] - (swap! fonts dissoc (:id item)) - (swap! uploading disj (:id item)) - (st/emit! (df/add-font font))) - (fn [error] - (js/console.log "error" error)))))) + (rx/subs! (fn [font] + (swap! fonts* dissoc id) + (swap! uploading* disj id) + (st/emit! (df/add-font font))) + (fn [error] + (js/console.log "error" error)))))) - on-upload-all - (fn [items] - (run! on-upload items)) + on-upload + (mf/use-fn + (mf/deps fonts on-upload*) + (fn [event] + (let [id (-> (dom/get-current-target event) + (dom/get-data "id") + (parse-uuid)) + item (get fonts id)] + (on-upload* item)))) on-blur-name - (fn [id event] - (let [name (dom/get-target-val event)] - (swap! fonts df/rename-and-regroup id name installed-fonts))) + (mf/use-fn + (mf/deps installed-fonts) + (fn [event] + (let [target (dom/get-current-target event) + id (-> target + (dom/get-data "id") + (parse-uuid)) + name (dom/get-value target)] + (when-not (str/blank? name) + (swap! fonts* df/rename-and-regroup id name installed-fonts))))) + + on-change-name + (mf/use-fn + (fn [event] + (let [target (dom/get-current-target event) + id (-> target + (dom/get-data "id") + (parse-uuid)) + name (dom/get-value target)] + (swap! fonts* update id assoc :font-family-tmp name)))) on-delete - (mf/use-callback + (mf/use-fn (mf/deps team) - (fn [{:keys [id] :as item}] - (swap! fonts dissoc id))) + (fn [event] + (let [id (-> (dom/get-current-target event) + (dom/get-data "id") + (parse-uuid))] + (swap! fonts* dissoc id)))) - on-dismiss-all - (fn [items] - (run! on-delete items)) + on-upload-all + (mf/use-fn + (mf/deps font-vals) + (fn [_] + (run! on-upload* font-vals))) - problematic-fonts? (some :height-warning? (vals @fonts))] + on-dismis-all + (mf/use-fn + (mf/deps fonts) + (fn [_] + (run! #(swap! fonts* dissoc (:id %)) (vals fonts))))] - [:div.dashboard-fonts-upload - [:div.dashboard-fonts-hero - [:div.desc + [:div {:class (stl/css :dashboard-fonts-upload)} + [:div {:class (stl/css :dashboard-fonts-hero)} + [:div {:class (stl/css :desc)} [:h2 (tr "labels.upload-custom-fonts")] [:& i18n/tr-html {:label "dashboard.fonts.hero-text1"}] - [:div.banner - [:div.icon i/msg-info] - [:div.content - [:& i18n/tr-html {:tag-name "span" - :label "dashboard.fonts.hero-text2"}]]] + [:button {:class (stl/css :btn-primary) + :on-click on-click + :tab-index "0"} + [:span (tr "labels.add-custom-font")] + [:& file-uploader {:input-id "font-upload" + :accept cm/str-font-types + :multi true + :ref input-ref + :on-selected on-selected}]] + + [:& context-notification {:content (tr "dashboard.fonts.hero-text2") + :type :default + :is-html true}] (when problematic-fonts? - [:div.banner.warning - [:div.icon i/msg-warning] - [:div.content - [:& i18n/tr-html {:tag-name "span" - :label "dashboard.fonts.warning-text"}]]])] - - [:button.btn-primary - {:on-click on-click - :tab-index "0"} - [:span (tr "labels.add-custom-font")] - [:& file-uploader {:input-id "font-upload" - :accept cm/str-font-types - :multi true - :ref input-ref - :on-selected on-selected}]]] + [:& context-notification {:content (tr "dashboard.fonts.warning-text") + :type :warning + :is-html true}])]] [:* - (when (some? (vals @fonts)) - [:div.font-item.table-row - [:span (tr "dashboard.fonts.fonts-added" (i18n/c (count (vals @fonts))))] - [:div.table-field.options - [:div.btn-primary - {:on-click #(on-upload-all (vals @fonts)) :data-test "upload-all"} + (when (seq fonts) + [:div {:class (stl/css :font-item :table-row)} + [:span (tr "dashboard.fonts.fonts-added" (i18n/c (count fonts)))] + [:div {:class (stl/css :table-field :options)} + [:button {:class (stl/css-case + :btn-primary true + :disabled disable-upload-all?) + :on-click on-upload-all + :data-test "upload-all" + :disabled disable-upload-all?} [:span (tr "dashboard.fonts.upload-all")]] - [:div.btn-secondary - {:on-click #(on-dismiss-all (vals @fonts)) :data-test "dismiss-all"} + [:button {:class (stl/css :btn-secondary) + :on-click on-dismis-all + :data-test "dismiss-all"} [:span (tr "dashboard.fonts.dismiss-all")]]]]) - (for [item (sort-by :font-family (vals @fonts))] - (let [uploading? (contains? @uploading (:id item))] - [:div.font-item.table-row {:key (:id item)} - [:div.table-field.family + (for [{:keys [id] :as item} (sort-by :font-family font-vals)] + (let [uploading? (contains? uploading id) + disable-upload? (or uploading? (bad-font-family-tmp? item))] + [:div {:class (stl/css :font-item :table-row) + :key (dm/str id)} + [:div {:class (stl/css :table-field :family)} [:input {:type "text" - :on-blur #(on-blur-name (:id item) %) + :data-id (dm/str id) + :on-blur on-blur-name + :on-change on-change-name :default-value (:font-family item)}]] - [:div.table-field.variants - [:span.label + [:div {:class (stl/css :table-field :variants)} + [:span {:class (stl/css :label)} [:& font-variant-display-name {:variant item}]]] - [:div.table-field.filenames - (for [item (:names item)] - [:span item])] - [:div.table-field.options + [:div {:class (stl/css :table-field :filenames)} + (for [item (:names item)] + [:span {:key (dm/str "name-" item)} item])] + + [:div {:class (stl/css :table-field :options)} (when (:height-warning? item) - [:span.icon.failure i/msg-warning]) - [:button.btn-primary.upload-button - {:on-click #(on-upload item) - :class (dom/classnames :disabled uploading?) - :disabled uploading?} - (if uploading? + [:span {:class (stl/css :icon :failure)} + i/msg-neutral]) + + [:button {:on-click on-upload + :data-id (dm/str id) + :class (stl/css-case + :btn-primary true + :upload-button true + :disabled disable-upload?) + :disabled disable-upload?} + (if ^boolean uploading? (tr "labels.uploading") (tr "labels.upload"))] - [:span.icon.close {:on-click #(on-delete item)} i/close]]]))]])) + [:span {:class (stl/css :icon :close) + :data-id (dm/str id) + :on-click on-delete} + i/close]]]))]])) + +(mf/defc installed-font-context-menu + {::mf/props :obj + ::mf/private true} + [{:keys [is-open on-close on-edit on-delete]}] + (let [options (mf/with-memo [on-edit on-delete] + [{:option-name (tr "labels.edit") + :id "font-edit" + :option-handler on-edit} + {:option-name (tr "labels.delete") + :id "font-delete" + :option-handler on-delete}])] + [:& context-menu-a11y + {:on-close on-close + :show is-open + :fixed? false + :min-width? true + :top -15 + :left -115 + :options options + :workspace? false}])) (mf/defc installed-font - [{:keys [font-id variants] :as props}] - (let [font (first variants) + {::mf/props :obj + ::mf/private true + ::mf/memo true} + [{:keys [font-id variants]}] + (let [font (first variants) - variants (sort-by (fn [item] - [(:font-weight item) - (if (= "normal" (:font-style item)) 1 2)]) - variants) + menu-open* (mf/use-state false) + menu-open? (deref menu-open*) + edition* (mf/use-state false) + edition? (deref edition*) - open-menu? (mf/use-state false) - edit? (mf/use-state false) - state (mf/use-var (:font-family font)) + state* (mf/use-state (:font-family font)) + font-family (deref state*) + + variants + (mf/with-memo [variants] + (sort-by (fn [item] + [(:font-weight item) + (if (= "normal" (:font-style item)) 1 2)]) + variants)) on-change - (fn [event] - (reset! state (dom/get-target-val event))) + (mf/use-fn + (fn [event] + (reset! state* (dom/get-target-val event)))) + + on-edit + (mf/use-fn #(reset! edition* true)) + + on-menu-open + (mf/use-fn #(reset! menu-open* true)) + + on-menu-close + (mf/use-fn #(reset! menu-open* false)) on-save - (fn [_] - (let [font-family @state] - (when-not (str/blank? font-family) - (st/emit! (df/update-font - {:id font-id - :name font-family}))) - (reset! edit? false))) + (mf/use-fn + (mf/deps font-family) + (fn [_] + (reset! edition* false) + (when-not (str/blank? font-family) + (st/emit! (df/update-font {:id font-id :name font-family}))))) on-key-down - (fn [event] - (when (kbd/enter? event) - (on-save event))) + (mf/use-fn + (mf/deps on-save) + (fn [event] + (when (kbd/enter? event) + (on-save event)))) on-cancel - (fn [_] - (reset! edit? false) - (reset! state (:font-family font))) + (mf/use-fn + (fn [_] + (reset! edition* false) + (reset! state* (:font-family font)))) - delete-font-fn - (fn [] (st/emit! (df/delete-font font-id))) - - delete-variant-fn - (fn [id] (st/emit! (df/delete-font-variant id))) - - on-delete - (fn [] - (st/emit! (modal/show - {:type :confirm - :title (tr "modals.delete-font.title") - :message (tr "modals.delete-font.message") - :accept-label (tr "labels.delete") - :on-accept (fn [_props] (delete-font-fn))}))) + on-delete-font + (mf/use-fn + (mf/deps font-id) + (fn [] + (let [options {:type :confirm + :title (tr "modals.delete-font.title") + :message (tr "modals.delete-font.message") + :accept-label (tr "labels.delete") + :on-accept (fn [_props] + (st/emit! (df/delete-font font-id)))}] + (st/emit! (modal/show options))))) on-delete-variant - (fn [id] - (st/emit! (modal/show - {:type :confirm - :title (tr "modals.delete-font-variant.title") - :message (tr "modals.delete-font-variant.message") - :accept-label (tr "labels.delete") - :on-accept (fn [_props] - (delete-variant-fn id))})))] + (mf/use-fn + (fn [event] + (let [id (-> (dom/get-current-target event) + (dom/get-data "id") + (parse-uuid)) + options {:type :confirm + :title (tr "modals.delete-font-variant.title") + :message (tr "modals.delete-font-variant.message") + :accept-label (tr "labels.delete") + :on-accept (fn [_props] + (st/emit! (df/delete-font-variant id)))}] + (st/emit! (modal/show options)))))] - [:div.font-item.table-row - [:div.table-field.family - (if @edit? + [:div {:class (stl/css :font-item :table-row)} + [:div {:class (stl/css :table-field :family)} + (if ^boolean edition? [:input {:type "text" - :default-value @state + :auto-focus true + :default-value font-family :on-key-down on-key-down :on-change on-change}] [:span (:font-family font)])] - [:div.table-field.variants - (for [item variants] - [:div.variant - [:span.label + [:div {:class (stl/css :table-field :variants)} + (for [{:keys [id] :as item} variants] + [:div {:class (stl/css :variant) + :key (dm/str id)} + [:span {:class (stl/css :label)} [:& font-variant-display-name {:variant item}]] - [:span.icon.close - {:on-click #(on-delete-variant (:id item))} - i/plus]])] + [:span + {:class (stl/css :icon :close) + :data-id (dm/str id) + :on-click on-delete-variant} + i/add]])] - [:div] - - (if @edit? - [:div.table-field.options - [:button.btn-primary - {:disabled (str/blank? @state) + (if ^boolean edition? + [:div {:class (stl/css :table-field :options)} + [:button + {:disabled (str/blank? font-family) :on-click on-save - :class (dom/classnames :btn-disabled (str/blank? @state))} + :class (stl/css-case :btn-primary true + :btn-disabled (str/blank? font-family))} (tr "labels.save")] - [:span.icon.close {:on-click on-cancel} i/close]] + [:button {:class (stl/css :icon :close) + :on-click on-cancel} + i/close]] - [:div.table-field.options - [:span.icon {:on-click #(reset! open-menu? true)} i/actions] - [:& context-menu - {:on-close #(reset! open-menu? false) - :show @open-menu? - :fixed? false - :top -15 - :left -115 - :options [[(tr "labels.edit") #(reset! edit? true) nil "font-edit"] - [(tr "labels.delete") on-delete nil "font-delete"]]}]])])) + [:div {:class (stl/css :table-field :options)} + [:span {:class (stl/css :icon) + :on-click on-menu-open} + i/menu] + [:& installed-font-context-menu + {:on-close on-menu-close + :is-open menu-open? + :on-delete on-delete-font + :on-edit on-edit}]])])) (mf/defc installed-fonts [{:keys [fonts] :as props}] @@ -301,18 +401,17 @@ #(str/includes? (str/lower (:font-family %)) @sterm) on-change - (mf/use-callback + (mf/use-fn (fn [event] (let [val (dom/get-target-val event)] (reset! sterm (str/lower val)))))] - [:div.dashboard-installed-fonts + [:div {:class (stl/css :dashboard-installed-fonts)} [:h3 (tr "labels.installed-fonts")] - [:div.installed-fonts-header - [:div.table-field.family (tr "labels.font-family")] - [:div.table-field.variants (tr "labels.font-variants")] - [:div] - [:div.table-field.search-input + [:div {:class (stl/css :installed-fonts-header)} + [:div {:class (stl/css :table-field :family)} (tr "labels.font-family")] + [:div {:class (stl/css :table-field :variants)} (tr "labels.font-variants")] + [:div {:class (stl/css :table-field :search-input)} [:input {:placeholder (tr "labels.search-font") :default-value "" :on-change on-change}]]] @@ -322,32 +421,34 @@ (for [[font-id variants] (->> (vals fonts) (filter matches?) (group-by :font-id))] - [:& installed-font {:key (str font-id) + [:& installed-font {:key (dm/str font-id "-installed") :font-id font-id :variants variants}]) (nil? fonts) - [:div.fonts-placeholder - [:div.icon i/loader] - [:div.label (tr "dashboard.loading-fonts")]] + [:div {:class (stl/css :fonts-placeholder)} + [:div {:class (stl/css :icon)} i/loader] + [:div {:class (stl/css :label)} (tr "dashboard.loading-fonts")]] :else - [:div.fonts-placeholder - [:div.icon i/text] - [:div.label (tr "dashboard.fonts.empty-placeholder")]])])) + [:div {:class (stl/css :fonts-placeholder)} + [:div {:class (stl/css :icon)} i/text] + [:div {:class (stl/css :label)} (tr "dashboard.fonts.empty-placeholder")]])])) (mf/defc fonts-page [{:keys [team] :as props}] (let [fonts (mf/deref refs/dashboard-fonts)] [:* [:& header {:team team :section :fonts}] - [:section.dashboard-container.dashboard-fonts - [:& fonts-upload {:team team :installed-fonts fonts}] + [:section {:class (stl/css :dashboard-container :dashboard-fonts)} + [:& uploaded-fonts {:team team :installed-fonts fonts}] [:& installed-fonts {:team team :fonts fonts}]]])) (mf/defc font-providers-page [{:keys [team] :as props}] [:* [:& header {:team team :section :providers}] - [:section.dashboard-container + [:section {:class (stl/css :dashboard-container)} [:span "font providers"]]]) + + diff --git a/frontend/src/app/main/ui/dashboard/fonts.scss b/frontend/src/app/main/ui/dashboard/fonts.scss new file mode 100644 index 0000000000..e520e01a40 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/fonts.scss @@ -0,0 +1,286 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; +@use "common/refactor/common-dashboard"; + +.dashboard-fonts { + border-top: $s-1 solid $db-quaternary; + display: flex; + flex-direction: column; + padding-left: $s-120; + overflow-y: auto; + padding-bottom: $s-120; + + .btn-primary { + font-size: $fs-11; + height: $s-32; + min-width: $s-100; + } +} + +.dashboard-installed-fonts { + max-width: $s-1000; + width: 100%; + display: flex; + margin-top: $s-24; + flex-direction: column; + + h3 { + font-size: $fs-14; + color: $df-secondary; + margin: $s-4; + } + + .font-item { + color: $db-secondary; + } +} + +.installed-fonts-header { + align-items: center; + color: $df-secondary; + display: flex; + font-size: $fs-12; + height: $s-40; + padding-left: $s-24; + text-transform: uppercase; + + > .family { + min-width: $s-200; + width: $s-200; + } + + > .variants { + padding-left: $s-12; + } +} + +.search-input { + display: flex; + flex-grow: 1; + justify-content: flex-end; + + input { + background-color: $db-tertiary; + border-color: transparent; + border-radius: $br-8; + border: $s-1 solid transparent; + color: $df-primary; + font-size: $fs-14; + height: $s-32; + margin: 0; + padding: 0 $s-8; + width: $s-152; + + &:focus { + outline: $s-1 solid $da-primary; + } + &::placeholder { + color: $df-secondary; + } + } +} + +.font-item { + align-items: center; + background-color: $db-tertiary; + border-radius: $br-4; + color: $df-secondary; + display: flex; + font-size: $fs-14; + justify-content: space-between; + margin-top: $s-4; + max-width: $s-1000; + padding: $s-12 $s-24; + width: 100%; + + input { + border: $s-1 solid transparent; + margin: 0; + padding: $s-8; + + background-color: $db-tertiary; + border-radius: $br-8; + color: $df-primary; + font-size: $fs-14; + + &:focus { + outline: $s-1 solid $da-primary; + } + } + + > .family { + min-width: $s-200; + width: $s-200; + } + + > .filenames { + min-width: $s-200; + } + + > .variants { + font-size: $fs-14; + display: flex; + flex-wrap: wrap; + flex-grow: 1; + padding-left: $s-16; + + .variant { + display: flex; + justify-content: space-between; + align-items: center; + padding: $s-8 $s-12; + cursor: pointer; + + .icon { + display: flex; + height: $s-16; + width: $s-16; + margin-left: $s-6; + align-items: center; + svg { + fill: none; + width: $s-12; + height: $s-12; + transform: rotate(45deg); + } + } + + &:hover { + .icon svg { + stroke: $df-secondary; + } + } + } + } + + .table-field { + color: $df-primary; + .variant { + background-color: $db-quaternary; + border-radius: $br-8; + margin-right: $s-4; + padding-right: $s-4; + } + } + + .filenames { + display: flex; + flex-direction: column; + font-size: $fs-12; + } + + .options { + display: flex; + justify-content: flex-end; + min-width: $s-180; + + .icon { + width: $s-24; + cursor: pointer; + display: flex; + margin-left: $s-12; + justify-content: center; + align-items: center; + svg { + width: $s-16; + height: $s-16; + stroke: $df-secondary; + fill: none; + } + + &.failure { + margin-right: $s-12; + svg { + stroke: var(--element-foreground-warning); + } + } + + &.close { + background: none; + border: none; + svg { + stroke: $df-secondary; + } + } + } + } +} + +.dashboard-fonts-upload { + max-width: $s-1000; + width: 100%; + display: flex; + flex-direction: column; + + .upload-button { + width: $s-100; + } + + .btn-secondary { + margin-left: $s-12; + } +} + +.dashboard-fonts-hero { + font-size: $fs-14; + padding: $s-32 0; + margin-top: $s-80; + display: flex; + justify-content: space-between; + + .btn-primary { + height: $s-40; + width: 100%; + } + + .desc { + display: flex; + flex-direction: column; + gap: $s-24; + color: $db-secondary; + width: $s-500; + + h2 { + color: $df-primary; + font-weight: 400; + } + p { + color: $df-secondary; + font-size: $fs-16; + } + } + + .btn-primary { + flex-shrink: 0; + } +} + +.fonts-placeholder { + align-items: center; + border-radius: $br-8; + border: $s-1 solid $db-quaternary; + display: flex; + flex-direction: column; + height: $s-160; + justify-content: center; + margin-top: $s-16; + max-width: $s-1000; + width: 100%; + + .icon svg { + stroke: $df-secondary; + fill: none; + width: $s-32; + height: $s-32; + } + + .label { + color: $df-secondary; + font-size: $fs-14; + } +} diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 10e3d2646a..dfffc9c669 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -5,20 +5,21 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.grid + (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.files.features :as ffeat] [app.common.geom.point :as gpt] [app.common.logging :as log] [app.main.data.dashboard :as dd] [app.main.data.messages :as msg] [app.main.features :as features] [app.main.fonts :as fonts] + [app.main.rasterizer :as thr] [app.main.refs :as refs] - [app.main.render :refer [component-svg]] + [app.main.render :as render] [app.main.repo :as rp] [app.main.store :as st] - [app.main.thumbnail-renderer :as thr] [app.main.ui.components.color-bullet :as bc] [app.main.ui.dashboard.file-menu :refer [file-menu]] [app.main.ui.dashboard.import :refer [use-import-file]] @@ -34,7 +35,7 @@ [app.util.keyboard :as kbd] [app.util.time :as dt] [app.util.timers :as ts] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -48,25 +49,25 @@ (->> (rp/cmd! :create-file-thumbnail params) (rx/map :uri)))) +(defn render-thumbnail + [file-id revn] + (->> (wrk/ask! {:cmd :thumbnails/generate-for-file + :revn revn + :file-id file-id + :features (features/get-team-enabled-features @st/state)}) + (rx/mapcat (fn [{:keys [fonts] :as result}] + (->> (fonts/render-font-styles fonts) + (rx/map (fn [styles] + (assoc result + :styles styles + :width 252)))))))) + (defn- ask-for-thumbnail "Creates some hooks to handle the files thumbnails cache" [file-id revn] - (let [features (cond-> ffeat/enabled - (features/active-feature? :components-v2) - (conj "components/v2"))] - - (->> (wrk/ask! {:cmd :thumbnails/generate-for-file - :revn revn - :file-id file-id - :features features}) - (rx/mapcat (fn [{:keys [fonts] :as result}] - (->> (fonts/render-font-styles fonts) - (rx/map (fn [styles] - (assoc result - :styles styles - :width 250)))))) - (rx/mapcat thr/render) - (rx/mapcat (partial persist-thumbnail file-id revn))))) + (->> (render-thumbnail file-id revn) + (rx/mapcat thr/render) + (rx/mapcat (partial persist-thumbnail file-id revn)))) (mf/defc grid-item-thumbnail {::mf/wrap-props false} @@ -77,24 +78,30 @@ (mf/with-effect [file-id revn visible? thumbnail-uri] (when (and visible? (not thumbnail-uri)) (->> (ask-for-thumbnail file-id revn) - (rx/subs (fn [url] - (st/emit! (dd/set-file-thumbnail file-id url))) - (fn [cause] - (log/error :hint "unable to render thumbnail" - :file-if file-id - :revn revn - :message (ex-message cause))))))) + (rx/subs! (fn [url] + (st/emit! (dd/set-file-thumbnail file-id url))) + (fn [cause] + (log/error :hint "unable to render thumbnail" + :file-if file-id + :revn revn + :message (ex-message cause))))))) - [:div.grid-item-th - {:style {:background-color background-color} - :ref container} + [:div {:class (stl/css :grid-item-th) + :style {:background-color background-color} + :ref container} (when visible? (if thumbnail-uri - [:img.grid-item-thumbnail-image {:src thumbnail-uri}] + [:img {:class (stl/css :grid-item-thumbnail-image) + :src thumbnail-uri + :loading "lazy" + :decoding "async"}] i/loader-pencil))])) ;; --- Grid Item Library +(def ^:private menu-icon + (i/icon-xref :menu (stl/css :menu-icon))) + (mf/defc grid-item-library {::mf/wrap [mf/memo]} [{:keys [file] :as props}] @@ -104,7 +111,7 @@ (let [font-ids (map :font-id (get-in file [:library-summary :typographies :sample] []))] (run! fonts/ensure-loaded! font-ids)))) - [:div.grid-item-th.library + [:div {:class (stl/css :grid-item-th :library)} (if (nil? file) i/loader-pencil (let [summary (:library-summary file) @@ -112,85 +119,93 @@ colors (:colors summary) typographies (:typographies summary)] [:* - (when (and (zero? (:count components)) (zero? (:count colors)) (zero? (:count typographies))) [:* - [:div.asset-section - [:div.asset-title + [:div {:class (stl/css :asset-section)} + [:div {:class (stl/css :asset-title)} [:span (tr "workspace.assets.components")] - [:span.num-assets (str "\u00A0(") 0 ")"]]] ;; Unicode 00A0 is non-breaking space - [:div.asset-section - [:div.asset-title - [:span (tr "workspace.assets.colors")] - [:span.num-assets (str "\u00A0(") 0 ")"]]] ;; Unicode 00A0 is non-breaking space - [:div.asset-section - [:div.asset-title - [:span (tr "workspace.assets.typography")] - [:span.num-assets (str "\u00A0(") 0 ")"]]]]) ;; Unicode 00A0 is non-breaking space + [:span {:class (stl/css :num-assets)} (str "\u00A0(") 0 ")"]]] ;; Unicode 00A0 is non-breaking space + [:div {:class (stl/css :asset-section)} + [:div {:class (stl/css :asset-title)} + [:span (tr "workspace.assets.colors")] + [:span {:class (stl/css :num-assets)} (str "\u00A0(") 0 ")"]]] ;; Unicode 00A0 is non-breaking space + [:div {:class (stl/css :asset-section)} + [:div {:class (stl/css :asset-title)} + [:span (tr "workspace.assets.typography")] + [:span {:class (stl/css :num-assets)} (str "\u00A0(") 0 ")"]]]]) ;; Unicode 00A0 is non-breaking space (when (pos? (:count components)) - [:div.asset-section - [:div.asset-title + [:div {:class (stl/css :asset-section)} + [:div {:class (stl/css :asset-title)} [:span (tr "workspace.assets.components")] - [:span.num-assets (str "\u00A0(") (:count components) ")"]] ;; Unicode 00A0 is non-breaking space - [:div.asset-list + [:span {:class (stl/css :num-assets)} (str "\u00A0(") (:count components) ")"]] ;; Unicode 00A0 is non-breaking space + [:div {:class (stl/css :asset-list)} (for [component (:sample components)] (let [root-id (or (:main-instance-id component) (:id component))] ;; Check for components-v2 in library - [:div.asset-list-item {:key (str "assets-component-" (:id component))} - [:& component-svg {:root-shape (get-in component [:objects root-id]) - :objects (:objects component)}] ;; Components in the summary come loaded with objects, even in v2 - [:div.name-block - [:span.item-name {:title (:name component)} + [:div {:class (stl/css :asset-list-item) + :key (str "assets-component-" (:id component))} + [:& render/component-svg {:root-shape (get-in component [:objects root-id]) + :objects (:objects component)}] ;; Components in the summary come loaded with objects, even in v2 + [:div {:class (stl/css :name-block)} + [:span {:class (stl/css :item-name) + :title (:name component)} (:name component)]]])) (when (> (:count components) (count (:sample components))) - [:div.asset-list-item - [:div.name-block - [:span.item-name "(...)"]]])]]) + [:div {:class (stl/css :asset-list-item)} + [:div {:class (stl/css :name-block)} + [:span {:class (stl/css :item-name)} "(...)"]]])]]) (when (pos? (:count colors)) - [:div.asset-section - [:div.asset-title + [:div {:class (stl/css :asset-section)} + [:div {:class (stl/css :asset-title)} [:span (tr "workspace.assets.colors")] - [:span.num-assets (str "\u00A0(") (:count colors) ")"]] ;; Unicode 00A0 is non-breaking space - [:div.asset-list + [:span {:class (stl/css :num-assets)} (str "\u00A0(") (:count colors) ")"]] ;; Unicode 00A0 is non-breaking space + [:div {:class (stl/css :asset-list)} (for [color (:sample colors)] (let [default-name (cond (:gradient color) (uc/gradient-type->string (get-in color [:gradient :type])) (:color color) (:color color) :else (:value color))] - [:div.asset-list-item {:key (str "assets-color-" (:id color))} + [:div {:class (stl/css :asset-list-item :color-item) + :key (str "assets-color-" (:id color))} [:& bc/color-bullet {:color {:color (:color color) - :opacity (:opacity color)}}] - [:div.name-block - [:span.color-name (:name color)] + :id (:id color) + :opacity (:opacity color)} + :mini? true}] + [:div {:class (stl/css :name-block)} + [:span {:class (stl/css :color-name)} (:name color)] (when-not (= (:name color) default-name) - [:span.color-value (:color color)])]])) + [:span {:class (stl/css :color-value)} (:color color)])]])) + (when (> (:count colors) (count (:sample colors))) - [:div.asset-list-item - [:div.name-block - [:span.item-name "(...)"]]])]]) + [:div {:class (stl/css :asset-list-item)} + [:div {:class (stl/css :name-block)} + [:span {:class (stl/css :item-name)} "(...)"]]])]]) (when (pos? (:count typographies)) - [:div.asset-section - [:div.asset-title + [:div {:class (stl/css :asset-section)} + [:div {:class (stl/css :asset-title)} [:span (tr "workspace.assets.typography")] - [:span.num-assets (str "\u00A0(") (:count typographies) ")"]] ;; Unicode 00A0 is non-breaking space - [:div.asset-list + [:span {:class (stl/css :num-assets)} (str "\u00A0(") (:count typographies) ")"]] ;; Unicode 00A0 is non-breaking space + [:div {:class (stl/css :asset-list)} (for [typography (:sample typographies)] - [:div.asset-list-item {:key (str "assets-typography-" (:id typography))} - [:div.typography-sample - {:style {:font-family (:font-family typography) - :font-weight (:font-weight typography) - :font-style (:font-style typography)}} + [:div {:class (stl/css :asset-list-item) + :key (str "assets-typography-" (:id typography))} + [:div {:class (stl/css :typography-sample) + :style {:font-family (:font-family typography) + :font-weight (:font-weight typography) + :font-style (:font-style typography)}} (tr "workspace.assets.typography.sample")] - [:div.name-block - [:span.item-name {:title (:name typography)} + [:div {:class (stl/css :name-block)} + [:span {:class (stl/css :item-name) + :title (:name typography)} (:name typography)]]]) + (when (> (:count typographies) (count (:sample typographies))) - [:div.asset-list-item - [:div.name-block - [:span.item-name "(...)"]]])]])]))]) + [:div {:class (stl/css :asset-list-item)} + [:div {:class (stl/css :name-block)} + [:span {:class (stl/css :item-name)} "(...)"]]])]])]))]) ;; --- Grid Item @@ -199,43 +214,48 @@ (let [locale (mf/deref i18n/locale) time (dt/timeago modified-at {:locale locale})] - [:span.date - time])) + [:span {:class (stl/css :date)} time])) (defn create-counter-element [_element file-count] (let [counter-el (dom/create-element "div")] - (dom/set-property! counter-el "class" "drag-counter") + (dom/set-property! counter-el "class" (stl/css :drag-counter)) (dom/set-text! counter-el (str file-count)) counter-el)) (mf/defc grid-item {:wrap [mf/memo]} - [{:keys [file navigate? origin library-view?] :as props}] + [{:keys [file origin library-view?] :as props}] (let [file-id (:id file) - local (mf/use-state {:menu-open false - :menu-pos nil - :edition false}) - selected-files (mf/deref refs/dashboard-selected-files) + + ;; FIXME: this breaks react hooks rule, hooks should never to + ;; be in a conditional code + selected-files (if (= origin :search) + (mf/deref refs/dashboard-selected-search) + (mf/deref refs/dashboard-selected-files)) + dashboard-local (mf/deref refs/dashboard-local) + file-menu-open? (:menu-open dashboard-local) + + selected? (contains? selected-files file-id) + node-ref (mf/use-ref) menu-ref (mf/use-ref) - selected? (contains? selected-files file-id) - on-menu-close (mf/use-fn - #(swap! local assoc :menu-open false)) + (fn [_] + (st/emit! (dd/hide-file-menu)))) on-select - (fn [event] - (when (and (or (not selected?) (> (count selected-files) 1)) - (not (:menu-open @local))) - (dom/stop-propagation event) - (let [shift? (kbd/shift? event)] - (when-not shift? - (st/emit! (dd/clear-selected-files))) - (st/emit! (dd/toggle-file-select file))))) + (mf/use-fn + (fn [event] + (when (or (not selected?) (> (count selected-files) 1)) + (dom/stop-propagation event) + (let [shift? (kbd/shift? event)] + (when-not shift? + (st/emit! (dd/clear-selected-files))) + (st/emit! (dd/toggle-file-select file)))))) on-navigate (mf/use-fn @@ -250,15 +270,17 @@ (mf/use-fn (mf/deps selected-files) (fn [event] + (st/emit! (dd/hide-file-menu)) (let [offset (dom/get-offset-position (.-nativeEvent event)) select-current? (not (contains? selected-files (:id file))) item-el (mf/ref-val node-ref) - counter-el (create-counter-element item-el - (if select-current? - 1 - (count selected-files)))] + counter-el (create-counter-element + item-el + (if select-current? + 1 + (count selected-files)))] (when select-current? (st/emit! (dd/clear-selected-files)) (st/emit! (dd/toggle-file-select file))) @@ -278,6 +300,7 @@ (mf/use-fn (mf/deps file selected?) (fn [event] + (dom/stop-propagation event) (dom/prevent-default event) (when-not selected? (when-not (kbd/shift? event) @@ -292,9 +315,7 @@ x (:left points)] (gpt/point x y)) client-position)] - (swap! local assoc - :menu-open true - :menu-pos position)))) + (st/emit! (dd/show-file-menu-with-position file-id position))))) edit (mf/use-fn @@ -303,42 +324,42 @@ (let [name (str/trim name)] (when (not= name "") (st/emit! (dd/rename-file (assoc file :name name))))) - (swap! local assoc :edition false))) + (st/emit! (dd/stop-edit-file-name)))) on-edit (mf/use-fn (mf/deps file) (fn [event] (dom/stop-propagation event) - (swap! local assoc - :edition true - :menu-open false)))] + (st/emit! (dd/start-edit-file-name file-id)))) - (mf/with-effect [selected? local] - (when (and (not selected?) (:menu-open @local)) - (swap! local assoc :menu-open false))) + handle-key-down + (mf/use-callback + (mf/deps on-navigate on-select) + (fn [event] + (dom/stop-propagation event) + (when (kbd/enter? event) + (on-navigate event)) + (when (kbd/shift? event) + (when (or (kbd/down-arrow? event) (kbd/left-arrow? event) (kbd/up-arrow? event) (kbd/right-arrow? event)) + (on-select event)) ;; TODO Fix this + )))] - [:li.grid-item.project-th {:class (dom/classnames :library library-view?)} + [:li + {:class (stl/css-case :grid-item true :project-th true :library library-view?)} [:button - {:tab-index "0" - :class (dom/classnames :selected selected? - :library library-view?) + {:class (stl/css-case :selected selected? :library library-view?) :ref node-ref + :title (:name file) :draggable true :on-click on-select - :on-key-down (fn [event] - (dom/stop-propagation event) - (when (kbd/enter? event) - (on-navigate event)) - (when (kbd/shift? event) - (when (or (kbd/down-arrow? event) (kbd/left-arrow? event) (kbd/up-arrow? event) (kbd/right-arrow? event)) - (on-select event)) ;; TODO Fix this - )) + :on-key-down handle-key-down :on-double-click on-navigate :on-drag-start on-drag-start :on-context-menu on-menu-click} - [:div.overlay] + [:div {:class (stl/css :overlay)}] + (if library-view? [:& grid-item-library {:file file}] [:& grid-item-thumbnail @@ -348,18 +369,20 @@ :background-color (dm/get-in file [:data :options :background])}]) (when (and (:is-shared file) (not library-view?)) - [:div.item-badge i/library]) - [:div.info-wrapper - [:div.item-info - (if (:edition @local) + [:div {:class (stl/css :item-badge)} i/library]) + + [:div {:class (stl/css :info-wrapper)} + [:div {:class (stl/css :item-info)} + (if (and (= file-id (:file-id dashboard-local)) (:edition dashboard-local)) [:& inline-edition {:content (:name file) :on-end edit}] [:h3 (:name file)]) [:& grid-item-metadata {:modified-at (:modified-at file)}]] - [:div.project-th-actions {:class (dom/classnames - :force-display (:menu-open @local))} - [:div.project-th-icon.menu - {:tab-index "0" + + [:div {:class (stl/css-case :project-th-actions true :force-display (:menu-open dashboard-local))} + [:div + {:class (stl/css :project-th-icon :menu) + :tab-index "0" :ref menu-ref :id (str file-id "-action-menu") :on-click on-menu-click @@ -367,18 +390,20 @@ (when (kbd/enter? event) (dom/stop-propagation event) (on-menu-click event)))} - i/actions - (when selected? - [:& file-menu {:files (vals selected-files) - :show? (:menu-open @local) - :left (+ 24 (:x (:menu-pos @local))) - :top (:y (:menu-pos @local)) - :navigate? navigate? - :on-edit on-edit - :on-menu-close on-menu-close - :origin origin - :dashboard-local dashboard-local - :parent-id (str file-id "-action-menu")}])]]]]])) + menu-icon + (when (and selected? file-menu-open?) + ;; When the menu is open we disable events in the dashboard. We need to force pointer events + ;; so the menu can be handled + [:div {:style {:pointer-events "all"}} + [:& file-menu {:files (vals selected-files) + :show? (:menu-open dashboard-local) + :left (+ 24 (:x (:menu-pos dashboard-local))) + :top (:y (:menu-pos dashboard-local)) + :navigate? true + :on-edit on-edit + :on-menu-close on-menu-close + :origin origin + :parent-id (str file-id "-action-menu")}]])]]]]])) (mf/defc grid [{:keys [files project origin limit library-view? create-fn] :as props}] @@ -398,8 +423,9 @@ on-drag-enter (mf/use-fn (fn [e] - (when (or (dnd/has-type? e "Files") - (dnd/has-type? e "application/x-moz-file")) + (when (and (not (dnd/has-type? e "penpot/files")) + (or (dnd/has-type? e "Files") + (dnd/has-type? e "application/x-moz-file"))) (dom/prevent-default e) (reset! dragging? true)))) @@ -419,36 +445,37 @@ on-drop (mf/use-fn (fn [e] - (when (or (dnd/has-type? e "Files") - (dnd/has-type? e "application/x-moz-file")) + (when (and (not (dnd/has-type? e "penpot/files")) + (or (dnd/has-type? e "Files") + (dnd/has-type? e "application/x-moz-file"))) (dom/prevent-default e) (reset! dragging? false) (import-files (.-files (.-dataTransfer e))))))] - [:div.dashboard-grid - {:on-drag-enter on-drag-enter - :on-drag-over on-drag-over - :on-drag-leave on-drag-leave - :on-drop on-drop - :ref node-ref} + [:div {:class (stl/css :dashboard-grid) + :on-drag-enter on-drag-enter + :on-drag-over on-drag-over + :on-drag-leave on-drag-leave + :on-drop on-drop + :ref node-ref} + (cond (nil? files) [:& loading-placeholder] (seq files) - [:ul.grid-row - {:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}} + (for [[index slice] (d/enumerate (partition-all limit files))] - (when @dragging? - [:li.grid-item]) - - (for [item files] - [:& grid-item - {:file item - :key (:id item) - :navigate? true - :origin origin - :library-view? library-view?}])] + [:ul {:class (stl/css :grid-row) :key (dm/str index)} + (when @dragging? + [:li {:class (stl/css :grid-item)}]) + (for [item slice] + [:& grid-item + {:file item + :key (:id item) + :navigate? true + :origin origin + :library-view? library-view?}])]) :else [:& empty-placeholder @@ -460,11 +487,13 @@ [{:keys [files selected-files dragging? limit] :as props}] (let [elements limit limit (if dragging? (dec limit) limit)] - [:ul.grid-row.no-wrap - {:style {:grid-template-columns (dm/str "repeat(" elements ", 1fr)")}} + [:ul + {:class (stl/css :grid-row :no-wrap) + :style {:grid-template-columns (dm/str "repeat(" elements ", 1fr)")}} (when dragging? - [:li.grid-item.dragged]) + [:li {:class (stl/css :grid-item :dragged)}]) + (for [item (take limit files)] [:& grid-item {:id (:id item) @@ -500,12 +529,12 @@ (do (dom/prevent-default e) (when-not (or (dnd/from-child? e) - (dnd/broken-event? e)) + (dnd/broken-event? e)) (when (not= selected-project project-id) (reset! dragging? true)))) (or (dnd/has-type? e "Files") - (dnd/has-type? e "application/x-moz-file")) + (dnd/has-type? e "application/x-moz-file")) (do (dom/prevent-default e) (reset! dragging? true))))) @@ -545,16 +574,17 @@ (st/emit! (dd/move-files (with-meta data mdata)))))) (or (dnd/has-type? e "Files") - (dnd/has-type? e "application/x-moz-file")) + (dnd/has-type? e "application/x-moz-file")) (do (dom/prevent-default e) (reset! dragging? false) (import-files (.-files (.-dataTransfer e)))))))] - [:div.dashboard-grid {:on-drag-enter on-drag-enter - :on-drag-over on-drag-over - :on-drag-leave on-drag-leave - :on-drop on-drop} + [:div {:class (stl/css :dashboard-grid) + :on-drag-enter on-drag-enter + :on-drag-over on-drag-over + :on-drag-leave on-drag-leave + :on-drop on-drop} (cond (nil? files) [:& loading-placeholder] @@ -571,4 +601,3 @@ {:dragging? @dragging? :limit limit :create-fn create-fn}])])) - diff --git a/frontend/src/app/main/ui/dashboard/grid.scss b/frontend/src/app/main/ui/dashboard/grid.scss new file mode 100644 index 0000000000..72f95e4ec2 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/grid.scss @@ -0,0 +1,380 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +$thumbnail-default-width: $s-252; // Default width +$thumbnail-default-height: $s-168; // Default width + +.dashboard-grid { + font-size: $fs-14; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + padding: 0 $s-16; +} + +.grid-row { + display: grid; + grid-auto-flow: column; + grid-auto-columns: calc($s-12 + var(--th-width, #{$thumbnail-default-width})); + width: 100%; + gap: $s-24; +} + +.grid-item { + align-items: center; + cursor: pointer; + display: flex; + flex-direction: column; + margin: $s-12 0; + position: relative; + text-align: center; + + a, + button { + width: 100%; + font-weight: $fw400; + } + button { + background-color: transparent; + border: none; + padding: 0 $s-6; + } + + .grid-item-th { + border-radius: $br-8; + text-align: initial; + width: var(--th-width, #{$thumbnail-default-width}); + height: var(--th-height, #{$thumbnail-default-height}); + background-size: cover; + overflow: hidden; + + img { + object-fit: contain; + } + } + + &.dragged { + border-radius: $br-4; + outline: $br-4 solid $da-primary; + text-align: initial; + width: calc(var(--th-width) + $s-12); + height: var(--th-height, #{$thumbnail-default-height}); + } + + &.overlay { + border-radius: $br-4; + border: $s-2 solid $da-tertiary; + height: 100%; + opacity: 0; + pointer-events: none; + position: absolute; + width: 100%; + z-index: $z-index-1; + } + + &:hover .overlay { + display: block; + opacity: 1; + } + + .info-wrapper { + display: grid; + grid-template-columns: 1fr auto; + cursor: pointer; + max-width: var(--th-width, $thumbnail-default-width); + } + + .item-info { + display: grid; + padding: $s-8; + text-align: left; + width: 100%; + font-size: $fs-12; + + h3 { + border: $s-1 solid transparent; + color: $df-primary; + font-size: $fs-16; + font-weight: $fw400; + height: $s-28; + line-height: 1.92; + max-width: $s-260; + overflow: hidden; + padding-right: $s-8; + padding: 0; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + + @media #{$bp-max-1366} { + max-width: $s-232; + } + } + + .date { + color: $df-secondary; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + white-space: nowrap; + max-width: $s-260; + &::first-letter { + text-transform: capitalize; + } + @media #{$bp-max-1366} { + max-width: $s-232; + } + } + } + + .item-badge { + background-color: $da-primary; + border: none; + border-radius: $br-6; + position: absolute; + top: $s-12; + right: $s-12; + height: $s-32; + width: $s-32; + display: flex; + align-items: center; + justify-content: center; + + svg { + stroke: $db-secondary; + fill: none; + height: $s-16; + width: $s-16; + } + } + + &.add-file { + border: $s-1 dashed $df-secondary; + justify-content: center; + box-shadow: none; + + span { + color: $db-primary; + font-size: $fs-14; + } + + &:hover { + background-color: $df-primary; + border: $s-2 solid $da-tertiary; + } + } +} + +.drag-counter { + position: absolute; + top: $s-4; + left: $s-4; + width: $s-32; + height: $s-32; + background-color: $da-tertiary; + border-radius: $br-circle; + color: $db-secondary; + font-size: $fs-16; + display: flex; + justify-content: center; + align-items: center; +} + +// PROJECTS, ELEMENTS & ICONS GRID +.project-th { + background-color: transparent; + border-radius: $br-8; + padding-top: $s-6; + + &:hover, + &:focus, + &:focus-within { + background-color: $db-tertiary; + .project-th-actions { + opacity: 1; + } + a { + text-decoration: none; + } + } + + .selected { + .grid-item-th { + outline: $s-4 solid $da-tertiary; + } + } +} + +.project-th-actions { + align-items: center; + display: flex; + height: 100%; + justify-content: center; + opacity: 0; + right: $s-6; + width: $s-32; + + span { + color: $db-secondary; + } +} + +.project-th-icon { + align-items: center; + display: flex; + margin-right: $s-8; + margin-top: 0; +} + +.menu { + align-items: flex-end; + display: flex; + flex-direction: column; + height: $s-32; + justify-content: center; + margin-right: 0; + margin-top: $s-20; + width: 100%; + --menu-icon-color: var(--button-tertiary-foreground-color-rest); + + &:hover, + &:focus { + --menu-icon-color: var(--button-tertiary-foreground-color-hover); + } +} + +.menu-icon { + stroke: var(--menu-icon-color); + fill: none; + margin-right: 0; + height: $s-16; + width: $s-16; +} + +.project-th-actions.force-display { + opacity: 1; +} + +.grid-item-th { + border-radius: $br-4; + cursor: pointer; + height: 100%; + overflow: hidden; + position: relative; + width: 100%; + display: flex; + justify-content: center; + flex-direction: row; + + .img-th { + height: auto; + width: 100%; + } + + svg { + height: 100%; + width: 100%; + } + + :global(svg#loader-pencil) { + stroke: $db-quaternary; + width: calc(var(--th-width, #{$thumbnail-default-width}) * 0.25); + } +} + +// LIBRARY VIEW +.library { + height: $s-580; +} + +.grid-item.project-th.library { + height: $s-612; +} + +.grid-item-th.library { + background-color: $db-tertiary; + flex-direction: column; + height: 90%; + justify-content: flex-start; + max-height: $s-580; + padding: $s-32; + + .asset-section { + font-size: $fs-12; + color: $df-secondary; + + &:not(:first-child) { + margin-top: $s-16; + } + } + + .asset-title { + display: flex; + font-size: $fs-12; + text-transform: uppercase; + + .num-assets { + color: $df-secondary; + } + } + + .asset-list-item { + align-items: center; + border-radius: $br-4; + border: $s-1 solid transparent; + color: $df-primary; + display: flex; + font-size: $fs-12; + margin-top: $s-4; + padding: $s-2; + position: relative; + + .name-block { + color: $df-secondary; + width: calc(100% - $s-24 - $s-8); + } + + .item-name { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + svg { + background-color: var(--color-canvas); + border-radius: $br-4; + border: $s-2 solid transparent; + height: $s-24; + margin-right: $s-8; + width: $s-24; + } + + .color-name { + color: $df-primary; + } + + .color-value { + color: $df-secondary; + margin-left: $s-4; + text-transform: uppercase; + } + + .typography-sample { + height: $s-20; + margin-right: $s-4; + width: $s-20; + } + } +} + +.color-item { + display: grid; + grid-template-columns: auto 1fr; + gap: $s-8; +} diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index bb26e8d0a8..14f0318e3b 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.import + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] @@ -13,17 +14,20 @@ [app.main.data.events :as ev] [app.main.data.messages :as msg] [app.main.data.modal :as modal] + [app.main.errors :as errors] + [app.main.features :as features] [app.main.store :as st] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.icons :as i] + [app.main.ui.notifications.context-notification :refer [context-notification]] [app.main.worker :as uw] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.webapi :as wapi] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] - [potok.core :as ptk] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) (log/set-level! :debug) @@ -32,25 +36,26 @@ (defn use-import-file [project-id on-finish-import] - (mf/use-callback + (mf/use-fn (mf/deps project-id on-finish-import) - (fn [files] - (when files - (let [files (->> files - (mapv - (fn [file] - {:name (.-name file) - :uri (wapi/create-uri file)})))] + (fn [entries] + (let [entries (->> entries + (mapv (fn [file] + {:name (.-name file) + :uri (wapi/create-uri file)})) + (not-empty))] + (when entries (st/emit! (modal/show {:type :import :project-id project-id - :files files + :entries entries :on-finish-import on-finish-import}))))))) (mf/defc import-form - {::mf/forward-ref true} - [{:keys [project-id on-finish-import]} external-ref] + {::mf/forward-ref true + ::mf/props :obj} + [{:keys [project-id on-finish-import]} external-ref] (let [on-file-selected (use-import-file project-id on-finish-import)] [:form.import-file {:aria-hidden "true"} [:& file-uploader {:accept ".penpot,.zip" @@ -58,68 +63,72 @@ :ref external-ref :on-selected on-file-selected}]])) -(defn update-file [files file-id new-name] - (->> files - (mapv - (fn [file] +(defn- update-entry-name + [entries file-id new-name] + (mapv (fn [entry] (let [new-name (str/trim new-name)] - (cond-> file - (and (= (:file-id file) file-id) + (cond-> entry + (and (= (:file-id entry) file-id) (not= "" new-name)) - (assoc :name new-name))))))) + (assoc :name new-name)))) + entries)) -(defn remove-file [files file-id] - (->> files - (mapv - (fn [file] - (cond-> file - (= (:file-id file) file-id) - (assoc :deleted? true)))))) +(defn- remove-entry + [entries file-id] + (mapv (fn [entry] + (cond-> entry + (= (:file-id entry) file-id) + (assoc :deleted true))) + entries)) -(defn set-analyze-error - [files uri] - (->> files - (mapv (fn [file] - (cond-> file - (= uri (:uri file)) - (assoc :status :analyze-error)))))) +(defn- update-with-analyze-error + [entries uri error] + (->> entries + (mapv (fn [entry] + (cond-> entry + (= uri (:uri entry)) + (-> (assoc :status :analyze-error) + (assoc :error error))))))) -(defn set-analyze-result [files uri type data] - (let [existing-files? (into #{} (->> files (map :file-id) (filter some?))) - replace-file - (fn [file] - (if (and (= uri (:uri file)) - (= (:status file) :analyzing)) - (->> (:files data) - (remove (comp existing-files? first)) - (mapv (fn [[file-id file-data]] - (-> file-data - (assoc :file-id file-id - :status :ready - :uri uri - :type type))))) - [file]))] - (into [] (mapcat replace-file) files))) +(defn- update-with-analyze-result + [entries uri type result] + (let [existing-entries? (into #{} (keep :file-id) entries) + replace-entry + (fn [entry] + (if (and (= uri (:uri entry)) + (= (:status entry) :analyzing)) + (->> (:files result) + (remove (comp existing-entries? first)) + (map (fn [[file-id file-data]] + (-> file-data + (assoc :file-id file-id) + (assoc :status :ready) + (assoc :uri uri) + (assoc :type type))))) + [entry]))] + (into [] (mapcat replace-entry) entries))) -(defn mark-files-importing [files] - (->> files +(defn- mark-entries-importing + [entries] + (->> entries (filter #(= :ready (:status %))) (mapv #(assoc % :status :importing)))) -(defn update-status [files file-id status progress errors] - (->> files - (mapv (fn [file] - (cond-> file - (and (= file-id (:file-id file)) (not= status :import-progress)) - (assoc :status status) +(defn- update-entry-status + [entries file-id status progress errors] + (mapv (fn [entry] + (cond-> entry + (and (= file-id (:file-id entry)) (not= status :import-progress)) + (assoc :status status) - (and (= file-id (:file-id file)) (= status :import-progress)) - (assoc :progress progress) + (and (= file-id (:file-id entry)) (= status :import-progress)) + (assoc :progress progress) - (= file-id (:file-id file)) - (assoc :errors errors)))))) + (= file-id (:file-id entry)) + (assoc :errors errors))) + entries)) -(defn parse-progress-message +(defn- parse-progress-message [message] (case (:type message) :upload-data @@ -145,57 +154,120 @@ (str message))) +(defn- has-status-importing? + [item] + (= (:status item) :importing)) + +(defn- has-status-analyzing? + [item] + (= (:status item) :analyzing)) + +(defn- has-status-analyze-error? + [item] + (= (:status item) :analyzing)) + +(defn- has-status-success? + [item] + (and (= (:status item) :import-finish) + (empty? (:errors item)))) + +(defn- has-status-error? + [item] + (and (= (:status item) :import-finish) + (d/not-empty? (:errors item)))) + +(defn- has-status-ready? + [item] + (and (= :ready (:status item)) + (not (:deleted item)))) + +(defn- analyze-entries + [state entries] + (->> (uw/ask-many! + {:cmd :analyze-import + :files entries + :features @features/features-ref}) + (rx/mapcat #(rx/delay emit-delay (rx/of %))) + (rx/filter some?) + (rx/subs! + (fn [{:keys [uri data error type] :as msg}] + (if (some? error) + (swap! state update-with-analyze-error uri error) + (swap! state update-with-analyze-result uri type data)))))) + +(defn- import-files! + [state project-id entries] + (st/emit! (ptk/data-event ::ev/event {::ev/name "import-files" + :num-files (count entries)})) + (->> (uw/ask-many! + {:cmd :import-files + :project-id project-id + :files entries + :features @features/features-ref}) + (rx/subs! + (fn [{:keys [file-id status message errors] :as msg}] + (swap! state update-entry-status file-id status message errors))))) + (mf/defc import-entry - [{:keys [state file editing? can-be-deleted?]}] + {::mf/props :obj + ::mf/memo true + ::mf/private true} + [{:keys [entries entry edition can-be-deleted on-edit on-change on-delete]}] + (let [status (:status entry) + loading? (or (= :analyzing status) + (= :importing status)) + analyze-error? (= :analyze-error status) + import-finish? (= :import-finish status) + import-error? (= :import-error status) + import-warn? (d/not-empty? (:errors entry)) + ready? (= :ready status) + is-shared? (:shared entry) + progress (:progress entry) - (let [loading? (or (= :analyzing (:status file)) - (= :importing (:status file))) - analyze-error? (= :analyze-error (:status file)) - import-finish? (= :import-finish (:status file)) - import-error? (= :import-error (:status file)) - import-warn? (d/not-empty? (:errors file)) - ready? (= :ready (:status file)) - is-shared? (:shared file) - progress (:progress file) + file-id (:file-id entry) + editing? (and (some? file-id) (= edition file-id)) - handle-edit-key-press - (mf/use-callback - (fn [e] - (when (or (kbd/enter? e) (kbd/esc? e)) - (dom/prevent-default e) - (dom/stop-propagation e) - (dom/blur! (dom/get-target e))))) + on-edit-key-press + (mf/use-fn + (fn [event] + (when (or (kbd/enter? event) + (kbd/esc? event)) + (dom/prevent-default event) + (dom/stop-propagation event) + (dom/blur! (dom/get-target event))))) - handle-edit-blur - (mf/use-callback - (mf/deps file) - (fn [e] - (let [value (dom/get-target-val e)] - (swap! state #(-> (assoc % :editing nil) - (update :files update-file (:file-id file) value)))))) + on-edit-blur + (mf/use-fn + (mf/deps file-id on-change) + (fn [event] + (let [value (dom/get-target-val event)] + (on-change file-id value event)))) - handle-edit-entry - (mf/use-callback - (mf/deps file) - (fn [] - (swap! state assoc :editing (:file-id file)))) + on-edit' + (mf/use-fn + (mf/deps file-id on-change) + (fn [event] + (when (fn? on-edit) + (on-edit file-id event)))) - handle-remove-entry - (mf/use-callback - (mf/deps file) - (fn [] - (swap! state update :files remove-file (:file-id file))))] + on-delete' + (mf/use-fn + (mf/deps file-id on-delete) + (fn [event] + (when (fn? on-delete) + (on-delete file-id event))))] - [:div.file-entry - {:class (dom/classnames - :loading loading? - :success (and import-finish? (not import-warn?) (not import-error?)) - :warning (and import-finish? import-warn? (not import-error?)) - :error (or import-error? analyze-error?) - :editable (and ready? (not editing?)))} + [:div {:class (stl/css-case + :file-entry true + :loading loading? + :success (and import-finish? (not import-warn?) (not import-error?)) + :warning (and import-finish? import-warn? (not import-error?)) + :error (or import-error? analyze-error?) + :editable (and ready? (not editing?)))} - [:div.file-name - [:div.file-icon + [:div {:class (stl/css :file-name)} + [:div {:class (stl/css-case :file-icon true + :icon-fill ready?)} (cond loading? i/loader-pencil ready? i/logo-icon import-warn? i/msg-warning @@ -204,220 +276,254 @@ analyze-error? i/close)] (if editing? - [:div.file-name-edit + [:div {:class (stl/css :file-name-edit)} [:input {:type "text" :auto-focus true - :default-value (:name file) - :on-key-press handle-edit-key-press - :on-blur handle-edit-blur}]] + :default-value (:name entry) + :on-key-press on-edit-key-press + :on-blur on-edit-blur}]] - [:div.file-name-label (:name file) (when is-shared? i/library)]) + [:div {:class (stl/css :file-name-label)} + (:name entry) + (when ^boolean is-shared? + [:span {:class (stl/css :icon)} + i/library])]) - [:div.edit-entry-buttons - (when (= "application/zip" (:type file)) - [:button {:on-click handle-edit-entry} i/pencil]) - (when can-be-deleted? - [:button {:on-click handle-remove-entry} i/trash])]] + [:div {:class (stl/css :edit-entry-buttons)} + (when (and (= "application/zip" (:type entry)) + (= status :ready)) + [:button {:on-click on-edit'} i/curve]) + (when can-be-deleted + [:button {:on-click on-delete'} i/delete])]] (cond analyze-error? - [:div.error-message - (tr "dashboard.import.analyze-error")] + [:div {:class (stl/css :error-message)} + (if (some? (:error entry)) + (tr (:error entry)) + (tr "dashboard.import.analyze-error"))] import-error? - [:div.error-message + [:div {:class (stl/css :error-message)} (tr "dashboard.import.import-error")] (and (not import-finish?) (some? progress)) - [:div.progress-message (parse-progress-message progress)]) + [:div {:class (stl/css :progress-message)} (parse-progress-message progress)]) - [:div.linked-libraries - (for [library-id (:libraries file)] - (let [library-data (->> @state :files (d/seek #(= library-id (:file-id %)))) - error? (or (:deleted? library-data) (:import-error library-data))] + [:div {:class (stl/css :linked-libraries)} + (for [library-id (:libraries entry)] + (let [library-data (d/seek #(= library-id (:file-id %)) entries) + error? (or (:deleted library-data) + (:import-error library-data))] (when (some? library-data) - [:div.linked-library-tag {:class (when error? "error")} - (if error? i/unchain i/chain) (:name library-data)])))]])) + [:div {:class (stl/css :linked-library) + :key (dm/str library-id)} + (:name library-data) + [:span {:class (stl/css-case + :linked-library-tag true + :error error?)} + i/detach]])))]])) (mf/defc import-dialog {::mf/register modal/components - ::mf/register-as :import} - [{:keys [project-id files template on-finish-import]}] - (let [state (mf/use-state - {:status :analyzing - :editing nil - :importing-templates 0 - :files (->> files - (mapv #(assoc % :status :analyzing)))}) + ::mf/register-as :import + ::mf/props :obj} - analyze-import - (mf/use-callback - (fn [files] - (->> (uw/ask-many! - {:cmd :analyze-import - :files files}) - (rx/delay-emit emit-delay) - (rx/subs - (fn [{:keys [uri data error type] :as msg}] - (log/debug :uri uri :data data :error error) - (if (some? error) - (swap! state update :files set-analyze-error uri) - (swap! state update :files set-analyze-result uri type data))))))) + [{:keys [project-id entries template on-finish-import]}] - import-files - (mf/use-callback - (fn [project-id files] - (st/emit! (ptk/event ::ev/event {::ev/name "import-files" - :num-files (count files)})) - (->> (uw/ask-many! - {:cmd :import-files - :project-id project-id - :files files}) - (rx/subs - (fn [{:keys [file-id status message errors] :as msg}] - (swap! state update :files update-status file-id status message errors)))))) + (mf/with-effect [] + ;; dispose uris when the component is umount + (fn [] (run! wapi/revoke-uri (map :uri entries)))) - handle-cancel - (mf/use-callback - (mf/deps (:editing @state)) + (let [entries* (mf/use-state + (fn [] (mapv #(assoc % :status :analyzing) entries))) + entries (deref entries*) + + status* (mf/use-state :analyzing) + status (deref status*) + + edition* (mf/use-state nil) + edition (deref edition*) + + template-finished* (mf/use-state nil) + template-finished (deref template-finished*) + + on-template-cloned-success + (mf/use-fn + (fn [] + (reset! status* :importing) + (reset! template-finished* true) + (st/emit! (dd/fetch-recent-files)))) + + on-template-cloned-error + (mf/use-fn + (fn [cause] + (reset! status* :error) + (reset! template-finished* true) + (errors/print-error! cause) + (rx/of (modal/hide) + (msg/error (tr "dashboard.libraries-and-templates.import-error"))))) + + continue-entries + (mf/use-fn + (mf/deps entries) + (fn [] + (let [entries (filterv has-status-ready? entries)] + (swap! status* (constantly :importing)) + (swap! entries* mark-entries-importing) + (import-files! entries* project-id entries)))) + + continue-template + (mf/use-fn + (mf/deps on-template-cloned-success + on-template-cloned-error + template) + (fn [] + (let [mdata {:on-success on-template-cloned-success + :on-error on-template-cloned-error} + params {:project-id project-id :template-id (:id template)}] + (swap! status* (constantly :importing)) + (st/emit! (dd/clone-template (with-meta params mdata)))))) + + on-edit + (mf/use-fn + (fn [file-id _event] + (swap! edition* (constantly file-id)))) + + on-entry-change + (mf/use-fn + (fn [file-id value] + (swap! edition* (constantly nil)) + (swap! entries* update-entry-name file-id value))) + + on-entry-delete + (mf/use-fn + (fn [file-id] + (swap! entries* remove-entry file-id))) + + on-cancel + (mf/use-fn + (mf/deps edition) (fn [event] - (when (nil? (:editing @state)) + (when (nil? edition) (dom/prevent-default event) (st/emit! (modal/hide))))) - on-template-cloned-success - (fn [] - (swap! state - (fn [state] - (-> state - (assoc :status :importing :importing-templates 0)))) - (st/emit! (dd/fetch-recent-files))) - - on-template-cloned-error - (fn [] - (st/emit! - (modal/hide) - (msg/error (tr "dashboard.libraries-and-templates.import-error")))) - - continue-files - (fn [] - (let [files (->> @state :files (filterv #(and (= :ready (:status %)) (not (:deleted? %)))))] - (import-files project-id files)) - - (swap! state - (fn [state] - (-> state - (assoc :status :importing) - (update :files mark-files-importing))))) - - continue-template - (fn [] - (let [mdata {:on-success on-template-cloned-success :on-error on-template-cloned-error} - params {:project-id project-id :template-id (:id template)}] - (swap! state - (fn [state] - (-> state - (assoc :status :importing :importing-templates 1)))) - (st/emit! (dd/clone-template (with-meta params mdata))))) - - - handle-continue - (mf/use-callback - (mf/deps project-id (:files @state)) + on-continue + (mf/use-fn + (mf/deps template + continue-template + continue-entries) (fn [event] (dom/prevent-default event) (if (some? template) (continue-template) - (continue-files)))) + (continue-entries)))) - handle-accept - (mf/use-callback + on-accept + (mf/use-fn + (mf/deps on-finish-import) (fn [event] (dom/prevent-default event) (st/emit! (modal/hide)) - (when on-finish-import (on-finish-import)))) + (when (fn? on-finish-import) + (on-finish-import)))) - files (->> (:files @state) (filterv (comp not :deleted?))) + entries (filterv (comp not :deleted) entries) + num-importing (+ (count (filterv has-status-importing? entries)) + (if (some? template) 1 0)) - num-importing (+ - (->> files (filter #(= (:status %) :importing)) count) - (:importing-templates @state)) + success-num (if (some? template) + 1 + (count (filterv has-status-success? entries))) + errors? (if (some? template) + (= status :error) + (or (some has-status-error? entries) + (zero? (count entries)))) - warning-files (->> files (filter #(and (= (:status %) :import-finish) (d/not-empty? (:errors %)))) count) - success-files (->> files (filter #(and (= (:status %) :import-finish) (empty? (:errors %)))) count) - pending-analysis? (> (->> files (filter #(= (:status %) :analyzing)) count) 0) - pending-import? (> num-importing 0) - - valid-files? (or (some? template) - (> (+ (->> files (filterv (fn [x] (not= (:status x) :analyze-error))) count)) 0))] + pending-analysis? (some has-status-analyzing? entries) + pending-import? (and (or (nil? template) + (not template-finished)) + (pos? num-importing)) - (mf/use-effect - (fn [] - (let [sub (analyze-import files)] - #(rx/dispose! sub)))) + valid-all-entries? (or (some? template) + (not (some has-status-analyze-error? entries))) - (mf/use-effect - (fn [] - ;; dispose uris when the component is umount - #(doseq [file files] - (wapi/revoke-uri (:uri file))))) + template-status + (cond + (and (= :importing status) pending-import?) + :importing - [:div.modal-overlay - [:div.modal-container.import-dialog - [:div.modal-header - [:div.modal-header-title - [:h2 (tr "dashboard.import")]] + (and (= :importing status) (not ^boolean pending-import?)) + :import-finish - [:div.modal-close-button - {:on-click handle-cancel} i/close]] + :else + :ready)] - [:div.modal-content - (when (and (= :importing (:status @state)) (not pending-import?)) - (if (> warning-files 0) - [:div.feedback-banner.warning - [:div.icon i/msg-warning] - [:div.message (tr "dashboard.import.import-warning" warning-files success-files)]] + ;; Run analyze operation on component mount + (mf/with-effect [] + (let [sub (analyze-entries entries* entries)] + (partial rx/dispose! sub))) - [:div.feedback-banner - [:div.icon i/checkbox-checked] - [:div.message (tr "dashboard.import.import-message" (i18n/c (if (some? template) 1 success-files)))]])) + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} (tr "dashboard.import")] - (for [file files] - (let [editing? (and (some? (:file-id file)) - (= (:file-id file) (:editing @state)))] - [:& import-entry {:state state - :key (dm/str (:uri file)) - :file file - :editing? editing? - :can-be-deleted? (> (count files) 1)}])) + [:button {:class (stl/css :modal-close-btn) + :on-click on-cancel} i/close]] + + [:div {:class (stl/css :modal-content)} + (when (and (= :analyzing status) errors?) + [:& context-notification + {:type :warning + :content (tr "dashboard.import.import-warning")}]) + + (when (and (= :importing status) (not ^boolean pending-import?)) + (cond + errors? + [:& context-notification + {:type :warning + :content (tr "dashboard.import.import-warning")}] + + :else + [:& context-notification + {:type :success + :content (tr "dashboard.import.import-message" (i18n/c success-num))}])) + + (for [entry entries] + [:& import-entry {:edition edition + :key (dm/str (:uri entry)) + :entry entry + :entries entries + :on-edit on-edit + :on-change on-entry-change + :on-delete on-entry-delete + :can-be-deleted (> (count entries) 1)}]) (when (some? template) - [:& import-entry {:state state - :file (assoc template :status (if (= 1 (:importing-templates @state)) :importing :ready)) - :editing? false - :can-be-deleted? false}])] + [:& import-entry {:entry (assoc template :status template-status) + :can-be-deleted false}])] - [:div.modal-footer - [:div.action-buttons - (when (= :analyzing (:status @state)) - [:input.cancel-button - {:type "button" - :value (tr "labels.cancel") - :on-click handle-cancel}]) + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + (when (= :analyzing status) + [:input {:class (stl/css :cancel-button) + :type "button" + :value (tr "labels.cancel") + :on-click on-cancel}]) - (when (= :analyzing (:status @state)) - [:input.accept-button - {:class "primary" - :type "button" - :value (tr "labels.continue") - :disabled (or pending-analysis? (not valid-files?)) - :on-click handle-continue}]) + (when (and (= :analyzing status) (not errors?)) + [:input {:class (stl/css :accept-btn) + :type "button" + :value (tr "labels.continue") + :disabled (or pending-analysis? (not valid-all-entries?)) + :on-click on-continue}]) - (when (= :importing (:status @state)) - [:input.accept-button - {:class "primary" - :type "button" - :value (tr "labels.accept") - :disabled (or pending-import? (not valid-files?)) - :on-click handle-accept}])]]]])) + (when (and (= :importing status) (not errors?)) + [:input {:class (stl/css :accept-btn) + :type "button" + :value (tr "labels.accept") + :disabled (or pending-import? (not valid-all-entries?)) + :on-click on-accept}])]]]])) diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss new file mode 100644 index 0000000000..7708be6efd --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/import.scss @@ -0,0 +1,198 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + @extend .modal-container-base; + display: flex; + flex-direction: column; +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include uppercaseTitleTipography; + color: var(--modal-title-foreground-color); +} + +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content { + @include bodySmallTypography; + flex: 1; + overflow-y: auto; + overflow-x: hidden; + display: grid; + grid-template-columns: 1fr; + gap: $s-16; + margin-bottom: $s-24; + min-height: 40px; +} + +.action-buttons { + @extend .modal-action-btns; +} + +.cancel-button { + @extend .modal-cancel-btn; +} +.accept-btn { + @extend .modal-accept-btn; + &.danger { + @extend .modal-danger-btn; + } +} + +.modal-scd-msg, +.modal-subtitle, +.modal-msg { + @include bodySmallTypography; + color: var(--modal-text-foreground-color); + line-height: 1.5; +} + +.file-entry { + .file-name { + @include flexRow; + margin-bottom: $s-8; + .file-icon { + @include flexCenter; + height: $s-24; + width: $s-16; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + &.icon-fill svg { + fill: var(--icon-foreground); + } + } + .file-name-edit { + @extend .input-element; + flex-grow: 1; + } + .file-name-label { + @include bodySmallTypography; + display: flex; + align-items: center; + gap: $s-12; + flex-grow: 1; + .icon { + @include flexCenter; + height: $s-16; + width: $s-16; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } + } + .edit-entry-buttons { + @include flexRow; + button { + @extend .button-tertiary; + width: $s-28; + height: $s-32; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + } + } + } + .error-message, + .progress-message { + height: $s-32; + color: var(--modal-text-foreground-color); + } + + .linked-library { + display: flex; + align-items: center; + gap: $s-12; + color: var(--modal-text-foreground-color); + .linked-library-tag { + @include flexCenter; + height: $s-24; + width: $s-24; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + &.error { + svg { + stroke: var(--element-foreground-error); + } + } + } + } + + &.loading { + .file-name { + color: var(--modal-text-foreground-color); + .file-icon { + :global(#loader-pencil) { + color: var(--modal-text-foreground-color); + stroke: var(--modal-text-foreground-color); + fill: var(--modal-text-foreground-color); + } + } + } + } + &.warning { + .file-name { + color: var(--element-foreground-warning); + .file-icon svg { + stroke: var(--element-foreground-warning); + } + .file-icon.icon-fill svg { + fill: var(--element-foreground-warning); + } + } + } + &.success { + .file-name { + color: var(--modal-text-foreground-color); + .file-icon svg { + stroke: var(--modal-text-foreground-color); + } + .file-icon.icon-fill svg { + fill: var(--modal-text-foreground-color); + } + } + } + &.error { + .file-name { + color: var(--modal-text-foreground-color); + .file-icon svg { + stroke: var(--modal-text-foreground-color); + } + .file-icon.icon-fill svg { + fill: var(--modal-text-foreground-color); + } + } + } + &.editable { + .file-name { + color: var(--modal-text-foreground-color); + .file-icon svg { + stroke: var(--modal-text-foreground-color); + } + .file-icon.icon-fill svg { + fill: var(--modal-text-foreground-color); + } + } + } +} diff --git a/frontend/src/app/main/ui/dashboard/inline_edition.cljs b/frontend/src/app/main/ui/dashboard/inline_edition.cljs index 13449aece4..0ceda89a6d 100644 --- a/frontend/src/app/main/ui/dashboard/inline_edition.cljs +++ b/frontend/src/app/main/ui/dashboard/inline_edition.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.inline-edition + (:require-macros [app.main.style :as stl]) (:require [app.main.ui.icons :as i] [app.util.dom :as dom] @@ -59,13 +60,14 @@ (dom/focus! node) (dom/select-text! node)))) - [:div.edit-wrapper - [:input.element-title {:value @name - :ref input-ref - :on-click on-click - :on-change on-input - :on-key-down on-keyup - :on-blur on-blur}] - [:span.close {:on-click on-cancel} i/close]])) - + [:div {:class (stl/css :edit-wrapper)} + [:input {:class (stl/css :element-title) + :value @name + :ref input-ref + :on-click on-click + :on-change on-input + :on-key-down on-keyup + :on-blur on-blur}] + [:span {:class (stl/css :close) + :on-click on-cancel} i/close]])) diff --git a/frontend/src/app/main/ui/dashboard/inline_edition.scss b/frontend/src/app/main/ui/dashboard/inline_edition.scss new file mode 100644 index 0000000000..b2d0276cd5 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/inline_edition.scss @@ -0,0 +1,53 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.edit-wrapper { + border-radius: $br-4; + display: flex; + padding-right: $s-24; + position: relative; + margin-right: $s-24; +} + +input.element-title { + background-color: var(--input-background-color-active); + border-radius: $br-8; + color: $df-primary; + font-size: $fs-16; + height: $s-32; + margin: 0; + border: none; + padding: $s-6; + width: 100%; + + &:focus-visible { + border: $s-1 solid $da-primary; + outline: none; + } +} + +.close { + cursor: pointer; + position: absolute; + + top: $s-1; + right: calc(-1 * $s-8); + + svg { + fill: $df-secondary; + height: $s-16; + transform: rotate(45deg) translateY(7px); + width: $s-16; + margin: 0; + } + &:hover { + svg { + fill: var(--element-foreground-warning); + } + } +} diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index c52ab6ce25..dd543c154e 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -5,18 +5,17 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.libraries + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] - [app.common.math :as mth] [app.main.data.dashboard :as dd] [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.dashboard.grid :refer [grid]] + [app.main.ui.hooks :as hooks] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.webapi :as wapi] - [beicon.core :as rx] [rumext.v2 :as mf])) (mf/defc libraries-page @@ -33,18 +32,9 @@ (sort-by :modified-at) (reverse)))) - components-v2 (features/use-feature :components-v2) + components-v2 (features/use-feature "components/v2") - width (mf/use-state nil) - rowref (mf/use-ref) - - itemsize (if components-v2 - 350 - (if (>= @width 1030) 280 230)) - ratio (if (some? @width) (/ @width itemsize) 0) - nitems (mth/floor ratio) - limit (min 10 nitems) - limit (max 1 limit)] + [rowref limit] (hooks/use-dynamic-grid-item-width 350)] (mf/with-effect [team] (when team @@ -54,29 +44,14 @@ (dom/set-html-title (tr "title.dashboard.shared-libraries" tname))))) (mf/with-effect [] - (st/emit! (dd/fetch-shared-files) + (st/emit! (dd/fetch-shared-files (:id team)) (dd/clear-selected-files))) - (mf/with-effect [] - (let [node (mf/ref-val rowref) - mnt? (volatile! true) - sub (->> (wapi/observe-resize node) - (rx/observe-on :af) - (rx/subs (fn [entries] - (let [row (first entries) - row-rect (.-contentRect ^js row) - row-width (.-width ^js row-rect)] - (when @mnt? - (reset! width row-width))))))] - (fn [] - (vreset! mnt? false) - (rx/dispose! sub)))) - [:* - [:header.dashboard-header {:ref rowref} - [:div.dashboard-title#dashboard-libraries-title + [:header {:class (stl/css :dashboard-header)} + [:div#dashboard-libraries-title {:class (stl/css :dashboard-title)} [:h1 (tr "dashboard.libraries-title")]]] - [:section.dashboard-container.no-bg.dashboard-shared + [:section {:class (stl/css :dashboard-container :no-bg :dashboard-shared) :ref rowref} [:& grid {:files files :project default-project :origin :libraries diff --git a/frontend/src/app/main/ui/dashboard/libraries.scss b/frontend/src/app/main/ui/dashboard/libraries.scss new file mode 100644 index 0000000000..69660e4f0a --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/libraries.scss @@ -0,0 +1,24 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; +@use "common/refactor/common-dashboard"; + +.dashboard-container { + flex: 1 0 0; + margin-right: $s-16; + overflow-y: auto; + width: 100%; + border-top: $s-1 solid $db-quaternary; + + &.dashboard-projects { + user-select: none; + } + + &.search { + margin-top: $s-12; + } +} diff --git a/frontend/src/app/main/ui/dashboard/pin_button.cljs b/frontend/src/app/main/ui/dashboard/pin_button.cljs new file mode 100644 index 0000000000..14ee57332e --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/pin_button.cljs @@ -0,0 +1,32 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.dashboard.pin-button + (:require-macros + [app.common.data.macros :as dm] + [app.main.style :as stl] + [app.main.ui.icons :refer [icon-xref]]) + (:require + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [tr]] + [app.util.object :as obj] + [rumext.v2 :as mf])) + +(def ^:private pin-icon + (icon-xref :pin (stl/css :icon))) + +(mf/defc pin-button* + {::mf/props :obj} + [{:keys [aria-label is-pinned class] :as props}] + (let [aria-label (or aria-label (tr "dashboard.pin-unpin")) + class (dm/str (or class "") " " (stl/css-case :button true :button-active is-pinned)) + + props (-> (obj/clone props) + (obj/unset! "isPinned") + (obj/set! "className" class) + (obj/set! "aria-label" aria-label))] + + [:> "button" props pin-icon])) diff --git a/frontend/src/app/main/ui/dashboard/pin_button.scss b/frontend/src/app/main/ui/dashboard/pin_button.scss new file mode 100644 index 0000000000..9b00a1307c --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/pin_button.scss @@ -0,0 +1,35 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.button { + --pin-button-icon-color: var(--button-icon-foreground-color); + --pin-button-bg-color: none; + --pin-button-border-color: none; + + width: $s-32; + height: $s-32; + background: var(--pin-button-bg-color); + border: $s-2 solid var(--pin-button-border-color); + border-radius: $br-8; + display: grid; + place-content: center; + cursor: pointer; +} + +.button-active { + --pin-button-icon-color: var(--button-icon-foreground-color-selected); + --pin-button-bg-color: var(--button-icon-background-color-selected); + --pin-button-border-color: var(--button-icon-border-color-selected); +} + +.icon { + width: $s-16; + height: $s-16; + fill: none; + stroke: var(--pin-button-icon-color); +} diff --git a/frontend/src/app/main/ui/dashboard/placeholder.cljs b/frontend/src/app/main/ui/dashboard/placeholder.cljs index 9b22d63303..8f5daa04e9 100644 --- a/frontend/src/app/main/ui/dashboard/placeholder.cljs +++ b/frontend/src/app/main/ui/dashboard/placeholder.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.placeholder + (:require-macros [app.main.style :as stl]) (:require [app.main.ui.icons :as i] [app.util.i18n :as i18n :refer [tr]] @@ -19,23 +20,26 @@ (create-fn "dashboard:empty-folder-placeholder")))] (cond (true? dragging?) - [:ul.grid-row.no-wrap - {:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}} - [:li.grid-item]] + [:ul + {:class (stl/css :grid-row :no-wrap) + :style {:grid-template-columns (str "repeat(" limit ", 1fr)")}} + [:li {:class (stl/css :grid-item :grid-empty-placeholder :dragged)}]] (= :libraries origin) - [:div.grid-empty-placeholder.libs {:data-test "empty-placeholder"} - [:div.text + [:div {:class (stl/css :grid-empty-placeholder :libs) + :data-test "empty-placeholder"} + [:div {:class (stl/css :text)} [:& i18n/tr-html {:label "dashboard.empty-placeholder-drafts"}]]] :else - [:div.grid-empty-placeholder - {:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}} - [:button.create-new {:on-click on-click} (tr "dashboard.new-file")]]))) + [:div + {:class (stl/css :grid-empty-placeholder)} + [:button {:class (stl/css :create-new) + :on-click on-click} + i/add]]))) (mf/defc loading-placeholder [] - [:div.grid-empty-placeholder.loader - [:div.icon i/loader] - [:div.text (tr "dashboard.loading-files")]]) - + [:div {:class (stl/css :grid-empty-placeholder :loader)} + [:div {:class (stl/css :icon)} i/loader] + [:div {:class (stl/css :text)} (tr "dashboard.loading-files")]]) diff --git a/frontend/src/app/main/ui/dashboard/placeholder.scss b/frontend/src/app/main/ui/dashboard/placeholder.scss new file mode 100644 index 0000000000..f2a37fbf07 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/placeholder.scss @@ -0,0 +1,93 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; +@use "./grid.scss" as g; + +.grid-empty-placeholder { + border-radius: $br-12; + display: grid; + padding: $s-12 0; + + &.loader { + justify-items: center; + } + + .icon { + display: flex; + align-items: center; + justify-content: center; + svg { + width: $s-64; + height: $s-64; + stroke: $df-secondary; + fill: none; + } + } + + &.libs { + background-image: url(/images/ph-left.svg), url(/images/ph-right.svg); + background-position: + 15% bottom, + 85% top; + background-repeat: no-repeat; + align-items: center; + border: $s-1 solid $db-quaternary; + border-radius: $br-4; + display: flex; + flex-direction: column; + height: $s-200; + margin: $s-16; + padding: $s-48; + justify-content: center; + + .text { + a { + color: $df-primary; + } + p { + max-width: $s-360; + text-align: center; + font-size: $fs-16; + } + } + } + + .create-new { + background-color: $db-tertiary; + border-radius: $br-8; + color: $df-primary; + cursor: pointer; + height: $s-160; + margin: $s-8; + text-transform: uppercase; + border: $s-2 solid transparent; + width: var(--th-width, #{g.$thumbnail-default-width}); + height: var(--th-height, #{g.$thumbnail-default-height}); + + svg { + width: $s-32; + height: $s-32; + stroke: $df-secondary; + } + + &:hover { + border: $s-2 solid $da-tertiary; + background-color: $db-quaternary; + color: $da-primary; + + svg { + stroke: $da-tertiary; + } + } + } + + .text { + margin-top: $s-12; + color: $df-secondary; + font-size: $fs-16; + } +} diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs index c85cc543b0..bd725a905a 100644 --- a/frontend/src/app/main/ui/dashboard/project_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs @@ -6,8 +6,6 @@ (ns app.main.ui.dashboard.project-menu (:require - [app.common.data.macros :as dm] - [app.common.schema :as sm] [app.main.data.dashboard :as dd] [app.main.data.messages :as msg] [app.main.data.modal :as modal] @@ -21,19 +19,8 @@ [app.util.router :as rt] [rumext.v2 :as mf])) -(def schema:project-menu - [:map {:title "UIProjectMenu"} - [:project some?] - [:show? :boolean] - [:on-menu-close {:optional true} ::sm/fn] - [:on-error {:optional true} ::sm/fn] - [:top {:optional true} [:maybe :double]] - [:left {:optional true} [:maybe :double]] - [:on-import {:optional true} ::sm/fn]]) - (mf/defc project-menu [{:keys [project show? on-edit on-menu-close top left on-import] :as props}] - (dm/assert! (sm/valid? schema:project-menu props)) (let [top (or top 0) left (or left 0) @@ -95,40 +82,40 @@ (when (fn? on-import) (on-import)))) options [(when-not (:is-default project) - {:option-name (tr "labels.rename") - :id "project-menu-rename" - :option-handler on-edit - :data-test "project-rename"}) - (when-not (:is-default project) - {:option-name (tr "dashboard.duplicate") - :id "project-menu-duplicated" - :option-handler on-duplicate - :data-test "project-duplicate"}) - (when-not (:is-default project) - {:option-name (tr "dashboard.pin-unpin") - :id "project-menu-pin" - :option-handler toggle-pin}) + {:option-name (tr "labels.rename") + :id "project-menu-rename" + :option-handler on-edit + :data-test "project-rename"}) + (when-not (:is-default project) + {:option-name (tr "dashboard.duplicate") + :id "project-menu-duplicated" + :option-handler on-duplicate + :data-test "project-duplicate"}) + (when-not (:is-default project) + {:option-name (tr "dashboard.pin-unpin") + :id "project-menu-pin" + :option-handler toggle-pin}) - (when (and (seq teams) (not (:is-default project))) - {:option-name (tr "dashboard.move-to") - :id "project-menu-move-to" - :sub-options (for [team teams] - {:option-name (:name team) - :id (:name team) - :option-handler (on-move (:id team))}) - :data-test "project-move-to"}) - (when (some? on-import) - {:option-name (tr "dashboard.import") - :id "project-menu-import" - :option-handler on-import-files - :data-test "file-import"}) - (when-not (:is-default project) - {:option-name :separator}) - (when-not (:is-default project) - {:option-name (tr "labels.delete") - :id "project-menu-delete" - :option-handler on-delete - :data-test "project-delete"})]] + (when (and (seq teams) (not (:is-default project))) + {:option-name (tr "dashboard.move-to") + :id "project-menu-move-to" + :sub-options (for [team teams] + {:option-name (:name team) + :id (:name team) + :option-handler (on-move (:id team))}) + :data-test "project-move-to"}) + (when (some? on-import) + {:option-name (tr "dashboard.import") + :id "project-menu-import" + :option-handler on-import-files + :data-test "file-import"}) + (when-not (:is-default project) + {:option-name :separator}) + (when-not (:is-default project) + {:option-name (tr "labels.delete") + :id "project-menu-delete" + :option-handler on-delete + :data-test "project-delete"})]] [:* [:& udi/import-form {:ref file-input diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index 3c58700aee..fc0a51f97c 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -5,44 +5,57 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.projects + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.geom.point :as gpt] - [app.common.math :as mth] [app.config :as cf] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.data.users :as du] + [app.main.errors :as errors] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.dashboard.grid :refer [line-grid]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] + [app.main.ui.dashboard.pin-button :refer [pin-button*]] [app.main.ui.dashboard.project-menu :refer [project-menu]] + [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.router :as rt] [app.util.time :as dt] - [app.util.webapi :as wapi] - [beicon.core :as rx] [cuerdas.core :as str] [okulary.core :as l] - [potok.core :as ptk] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) +(def ^:private show-more-icon + (i/icon-xref :arrow (stl/css :show-more-icon))) + +(def ^:private close-icon + (i/icon-xref :close (stl/css :close-icon))) + +(def ^:private add-icon + (i/icon-xref :add (stl/css :add-icon))) + +(def ^:private menu-icon + (i/icon-xref :menu (stl/css :menu-icon))) + (mf/defc header {::mf/wrap [mf/memo]} [] (let [on-click (mf/use-fn #(st/emit! (dd/create-project)))] - [:header.dashboard-header - [:div.dashboard-title#dashboard-projects-title + [:header {:class (stl/css :dashboard-header)} + [:div#dashboard-projects-title {:class (stl/css :dashboard-title)} [:h1 (tr "dashboard.projects-title")]] - [:button.btn-secondary.btn-small - {:on-click on-click - :data-test "new-project-button"} + [:button {:class (stl/css :btn-secondary :btn-small) + :on-click on-click + :data-test "new-project-button"} (tr "dashboard.new-project")]])) (mf/defc team-hero @@ -64,21 +77,25 @@ (dom/prevent-default event) (close-fn)))] - [:div.team-hero - [:img {:src "images/deco-team-banner.png" :border "0" - :role "presentation"}] - [:div.text - [:div.title (tr "dasboard.team-hero.title")] - [:div.info + [:div {:class (stl/css :team-hero)} + [:div {:class (stl/css :img-wrapper)} + [:img {:src "images/deco-team-banner.png" + :border "0" + :role "presentation"}]] + [:div {:class (stl/css :text)} + [:div {:class (stl/css :title)} (tr "dasboard.team-hero.title")] + [:div {:class (stl/css :info)} [:span (tr "dasboard.team-hero.text")] - [:a {:on-click on-nav-members-click} (tr "dasboard.team-hero.management")]]] - [:button.btn-primary.invite - {:on-click on-invite-click} - (tr "onboarding.choice.team-up.invite-members")] - [:button.close - {:on-click on-close-click - :aria-label (tr "labels.close")} - [:span i/close]]])) + [:a {:on-click on-nav-members-click} (tr "dasboard.team-hero.management")]] + [:button + {:class (stl/css :btn-primary :invite) + :on-click on-invite-click} + (tr "onboarding.choice.team-up.invite-members")]] + + [:button {:class (stl/css :close) + :on-click on-close-click + :aria-label (tr "labels.close")} + close-icon]])) (def builtin-templates (l/derived :builtin-templates st/state)) @@ -101,35 +118,38 @@ on-template-cloned-error (mf/use-fn - (fn [] - (swap! state #(assoc % :status :waiting)) - (st/emit! - (msg/error (tr "dashboard.libraries-and-templates.import-error"))))) + (fn [cause] + (swap! state assoc :status :error) + (errors/print-error! cause) + (st/emit! (msg/error (tr "dashboard.libraries-and-templates.import-error"))))) download-tutorial (mf/use-fn (mf/deps template default-project-id) (fn [] - (let [mdata {:on-success on-template-cloned-success :on-error on-template-cloned-error} - params {:project-id default-project-id :template-id (:id template)}] + (let [mdata {:on-success on-template-cloned-success + :on-error on-template-cloned-error} + params {:project-id default-project-id + :template-id (:id template)}] (swap! state #(assoc % :status :importing)) (st/emit! (with-meta (dd/clone-template (with-meta params mdata)) {::ev/origin "get-started-hero-block"})))))] - [:article.tutorial - [:div.thumbnail] - [:div.text - [:h2.title (tr "dasboard.tutorial-hero.title")] - [:p.info (tr "dasboard.tutorial-hero.info")] - [:button.btn-primary.action {:on-click download-tutorial} + [:article {:class (stl/css :tutorial)} + [:div {:class (stl/css :thumbnail)}] + [:div {:class (stl/css :text)} + [:h2 {:class (stl/css :title)} (tr "dasboard.tutorial-hero.title")] + [:p {:class (stl/css :info)} (tr "dasboard.tutorial-hero.info")] + [:button {:class (stl/css :btn-primary :action) + :on-click download-tutorial} (case (:status @state) :waiting (tr "dasboard.tutorial-hero.start") :importing [:span.loader i/loader-pencil] :success "")]] - [:button.close - {:on-click close-tutorial - :aria-label (tr "labels.close")} - [:span.icon i/close]]])) + [:button {:class (stl/css :close) + :on-click close-tutorial + :aria-label (tr "labels.close")} + close-icon]])) (mf/defc interface-walkthrough {::mf/wrap [mf/memo]} @@ -139,20 +159,20 @@ (st/emit! (ptk/event ::ev/event {::ev/name "show-walkthrough" ::ev/origin "get-started-hero-block" :section "dashboard"})))] - [:article.walkthrough - [:div.thumbnail] - [:div.text - [:h2.title (tr "dasboard.walkthrough-hero.title")] - [:p.info (tr "dasboard.walkthrough-hero.info")] - [:a.btn-primary.action - {:href " https://design.penpot.app/walkthrough" - :target "_blank" - :on-click handle-walkthrough-link} + [:article {:class (stl/css :walkthrough)} + [:div {:class (stl/css :thumbnail)}] + [:div {:class (stl/css :text)} + [:h2 {:class (stl/css :title)} (tr "dasboard.walkthrough-hero.title")] + [:p {:class (stl/css :info)} (tr "dasboard.walkthrough-hero.info")] + [:a {:class (stl/css :btn-primary :action) + :href " https://design.penpot.app/walkthrough" + :target "_blank" + :on-click handle-walkthrough-link} (tr "dasboard.walkthrough-hero.start")]] - [:button.close - {:on-click close-walkthrough - :aria-label (tr "labels.close")} - [:span.icon i/close]]])) + [:button {:class (stl/css :close) + :on-click close-walkthrough + :aria-label (tr "labels.close")} + close-icon]])) (mf/defc project-item [{:keys [project first? team files] :as props}] @@ -168,17 +188,7 @@ :menu-pos nil :edition? (= (:id project) edit-id)}) - width (mf/use-state nil) - rowref (mf/use-ref) - itemsize (if (>= @width 1030) - 280 - 230) - - ratio (if (some? @width) (/ @width itemsize) 0) - nitems (mth/floor ratio) - limit (min 10 nitems) - limit (max 1 limit) - + [rowref limit] (hooks/use-dynamic-grid-item-width) on-nav (mf/use-fn (mf/deps project-id team-id) @@ -189,9 +199,9 @@ toggle-pin (mf/use-fn (mf/deps project) - (fn [event] - (dom/stop-propagation event) - (st/emit! (dd/toggle-project-pin project)))) + (fn [event] + (dom/stop-propagation event) + (st/emit! (dd/toggle-project-pin project)))) on-menu-click (mf/use-fn @@ -255,102 +265,98 @@ (fn [] (st/emit! (dd/fetch-files {:project-id project-id}) (dd/fetch-recent-files (:id team)) - (dd/fetch-projects) - (dd/clear-selected-files))))] + (dd/fetch-projects (:id team)) + (dd/clear-selected-files)))) - (mf/with-effect - (let [node (mf/ref-val rowref) - mnt? (volatile! true) - sub (->> (wapi/observe-resize node) - (rx/observe-on :af) - (rx/subs (fn [entries] - (let [row (first entries) - row-rect (.-contentRect ^js row) - row-width (.-width ^js row-rect)] - (when @mnt? - (reset! width row-width))))))] - (fn [] - (vreset! mnt? false) - (rx/dispose! sub)))) + handle-create-click + (mf/use-callback + (mf/deps on-create-click) + (fn [event] + (when (kbd/enter? event) + (on-create-click event)))) - [:article.dashboard-project-row - {:class (when first? "first")} - [:header.project {:ref rowref} - [:div.project-name-wrapper + + handle-menu-click + (mf/use-callback + (mf/deps on-menu-click) + (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (on-menu-click event)))) + title-width (/ 100 limit)] + + [:article {:class (stl/css-case :dashboard-project-row true :first first?)} + [:header {:class (stl/css :project)} + [:div {:class (stl/css :project-name-wrapper)} (if (:edition? @local) [:& inline-edition {:content (:name project) :on-end on-edit}] [:h2 {:on-click on-nav + :style {:max-width (str title-width "%")} + :class (stl/css :project-name) + :title (if (:is-default project) + (tr "labels.drafts") + (:name project)) :on-context-menu on-menu-click} (if (:is-default project) (tr "labels.drafts") (:name project))]) - [:& project-menu - {:project project - :show? (:menu-open @local) - :left (+ 24 (:x (:menu-pos @local))) - :top (:y (:menu-pos @local)) - :on-edit on-edit-open - :on-menu-close on-menu-close - :on-import on-import}] + [:div {:class (stl/css :info-wrapper)} + [:& project-menu + {:project project + :show? (:menu-open @local) + :left (+ 24 (:x (:menu-pos @local))) + :top (:y (:menu-pos @local)) + :on-edit on-edit-open + :on-menu-close on-menu-close + :on-import on-import}] - [:span.info (str (tr "labels.num-of-files" (i18n/c file-count)))] - (let [time (-> (:modified-at project) - (dt/timeago {:locale locale}))] - [:span.recent-files-row-title-info (str ", " time)]) - [:div.project-actions - (when-not (:is-default project) - [:button.pin-icon.tooltip.tooltip-bottom - {:class (when (:is-pinned project) "active") - :on-click toggle-pin - :alt (tr "dashboard.pin-unpin") - :aria-label (tr "dashboard.pin-unpin") - :tab-index "0"} - (if (:is-pinned project) - i/pin-fill - i/pin)]) + [:span {:class (stl/css :info)} (str (tr "labels.num-of-files" (i18n/c file-count)))] - [:button.btn-secondary.btn-small.tooltip.tooltip-bottom - {:on-click on-create-click - :alt (tr "dashboard.new-file") - :aria-label (tr "dashboard.new-file") - :data-test "project-new-file" - :tab-index "0" - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-create-click event)))} - i/close] + (let [time (-> (:modified-at project) + (dt/timeago {:locale locale}))] + [:span {:class (stl/css :recent-files-row-title-info)} (str ", " time)]) - [:button.btn-secondary.btn-small.tooltip.tooltip-bottom - {:on-click on-menu-click - :alt (tr "dashboard.options") - :aria-label (tr "dashboard.options") - :data-test "project-options" - :tab-index "0" - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (on-menu-click event)))} - i/actions]]]] + [:div {:class (stl/css-case :project-actions true + :pinned-project (:is-pinned project))} + (when-not (:is-default project) + [:> pin-button* {:class (stl/css :pin-button) :is-pinned (:is-pinned project) :on-click toggle-pin :tab-index 0}]) - [:& line-grid - {:project project - :team team - :files files - :create-fn create-file - :limit limit}] + [:button {:class (stl/css :add-file-btn) + :on-click on-create-click + :title (tr "dashboard.new-file") + :aria-label (tr "dashboard.new-file") + :data-test "project-new-file" + :on-key-down handle-create-click} + add-icon] + + [:button {:class (stl/css :options-btn) + :on-click on-menu-click + :title (tr "dashboard.options") + :aria-label (tr "dashboard.options") + :data-test "project-options" + :on-key-down handle-menu-click} + menu-icon]]]]] + + [:div {:class (stl/css :grid-container) :ref rowref} + [:& line-grid + {:project project + :team team + :files files + :create-fn create-file + :limit limit}]] (when (and (> limit 0) (> file-count limit)) - [:button.show-more {:on-click on-nav - :tab-index "0" - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-nav)))} - [:div.placeholder-label - (tr "dashboard.show-all-files")] - [:div.placeholder-icon i/arrow-down]])])) + [:button {:class (stl/css :show-more) + :on-click on-nav + :tab-index "0" + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-nav)))} + [:span {:class (stl/css :placeholder-label)} (tr "dashboard.show-all-files")] + show-more-icon])])) (def recent-files-ref @@ -367,11 +373,12 @@ you-admin? (get-in team [:permissions :is-admin]) can-invite? (or you-owner? you-admin?) team-hero? (and can-invite? - (:team-hero? props true) - (not (:is-default team))) + (:team-hero? props true) + (not (:is-default team))) tutorial-viewed? (:viewed-tutorial? props true) walkthrough-viewed? (:viewed-walkthrough? props true) + is-my-penpot (= (:default-team-id profile) (:id team)) team-id (:id team) @@ -379,24 +386,32 @@ (mf/use-fn (fn [] (st/emit! (du/update-profile-props {:team-hero? false}) - (ptk/event ::ev/event {::ev/name "dont-show-team-up-hero" - ::ev/origin "dashboard"})))) + (ptk/data-event ::ev/event {::ev/name "dont-show-team-up-hero" + ::ev/origin "dashboard"})))) + close-tutorial (mf/use-fn (fn [] (st/emit! (du/update-profile-props {:viewed-tutorial? true}) - (ptk/event ::ev/event {::ev/name "dont-show" - ::ev/origin "get-started-hero-block" - :type "tutorial" - :section "dashboard"})))) + (ptk/data-event ::ev/event {::ev/name "dont-show-tutorial" + ::ev/origin "get-started-hero" + :type "tutorial" + :section "dashboard"})))) + close-walkthrough (mf/use-fn (fn [] (st/emit! (du/update-profile-props {:viewed-walkthrough? true}) - (ptk/event ::ev/event {::ev/name "dont-show" - ::ev/origin "get-started-hero-block" - :type "walkthrough" - :section "dashboard"}))))] + (ptk/data-event ::ev/event {::ev/name "dont-show-walkthrough" + ::ev/origin "get-started-hero" + :type "walkthrough" + :section "dashboard"})))) + + show-hero? (and is-my-penpot + (or (not tutorial-viewed?) + (not walkthrough-viewed?))) + + show-team-hero? (and (not is-my-penpot) team-hero?)] (mf/with-effect [team] (let [tname (if (:is-default team) @@ -411,32 +426,35 @@ (when (seq projects) [:* [:& header] + [:div {:class (stl/css :projects-container)} + [:* + (when team-hero? + [:& team-hero {:team team :close-fn close-banner}]) - (when team-hero? - [:& team-hero {:team team :close-fn close-banner}]) + (when (and (contains? cf/flags :dashboard-templates-section) + show-hero?) + [:div {:class (stl/css :hero-projects)} + (when (and (not tutorial-viewed?) (:is-default team)) + [:& tutorial-project + {:close-tutorial close-tutorial + :default-project-id default-project-id}]) - (when (and (contains? cf/flags :dashboard-templates-section) - (or (not tutorial-viewed?) - (not walkthrough-viewed?))) - [:div.hero-projects - (when (and (not tutorial-viewed?) (:is-default team)) - [:& tutorial-project - {:close-tutorial close-tutorial - :default-project-id default-project-id}]) - - (when (and (not walkthrough-viewed?) (:is-default team)) - [:& interface-walkthrough - {:close-walkthrough close-walkthrough}])]) - - [:div.dashboard-container.no-bg.dashboard-projects - (for [{:keys [id] :as project} projects] - (let [files (when recent-map - (->> (vals recent-map) - (filterv #(= id (:project-id %))) - (sort-by :modified-at #(compare %2 %1))))] - [:& project-item {:project project - :team team - :files files - :first? (= project (first projects)) - :key id}]))]]))) + (when (and (not walkthrough-viewed?) (:is-default team)) + [:& interface-walkthrough + {:close-walkthrough close-walkthrough}])]) + [:div {:class (stl/css-case :dashboard-container true + :no-bg true + :dashboard-projects true + :with-hero show-hero? + :with-team-hero show-team-hero?)} + (for [{:keys [id] :as project} projects] + (let [files (when recent-map + (->> (vals recent-map) + (filterv #(= id (:project-id %))) + (sort-by :modified-at #(compare %2 %1))))] + [:& project-item {:project project + :team team + :files files + :first? (= project (first projects)) + :key id}]))]]]]))) diff --git a/frontend/src/app/main/ui/dashboard/projects.scss b/frontend/src/app/main/ui/dashboard/projects.scss new file mode 100644 index 0000000000..eb73b2e891 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/projects.scss @@ -0,0 +1,326 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; +@use "common/refactor/common-dashboard"; + +.dashboard-container { + flex: 1 0 0; + width: 100%; + margin-right: $s-16; + border-top: $s-1 solid var(--panel-border-color); + overflow-y: auto; +} + +.dashboard-projects { + user-select: none; + height: calc(100vh - $s-64); +} + +.with-hero, +.with-team-hero { + height: calc(100vh - $s-280); +} + +.dashboard-shared { + width: calc(100vw - $s-320); + margin-right: $s-52; +} + +.search { + margin-top: $s-12; +} + +.dashboard-project-row { + --actions-opacity: 0; + margin-bottom: $s-24; + position: relative; + + &:hover, + &:focus, + &:focus-within { + --actions-opacity: 1; + } +} + +.pinned-project { + --actions-opacity: 1; +} + +.projects-container { + display: grid; + grid-auto-rows: min-content; +} + +.project { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: $s-8; + width: 99%; + max-height: $s-40; + padding: $s-8 $s-8 $s-8 $s-16; + margin-top: $s-16; + border-radius: $br-4; +} + +.project-name-wrapper { + display: flex; + align-items: center; + justify-content: flex-start; + width: 100%; + min-height: $s-32; + margin-left: $s-8; +} + +.project-name { + @include bodyLargeTypography; + @include textEllipsis; + width: fit-content; + margin-right: $s-12; + line-height: 0.8; + color: var(--title-foreground-color-hover); + cursor: pointer; +} + +.info-wrapper { + display: flex; + align-items: center; + gap: $s-8; +} + +.info, +.recent-files-row-title-info { + @include bodyMediumTypography; + color: var(--title-foreground-color); + @media (max-width: 760px) { + display: none; + } +} + +.project-actions { + display: flex; + opacity: var(--actions-opacity); + margin-left: $s-32; +} + +.add-file-btn, +.options-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-32; + margin: 0 $s-8; + padding: $s-8; +} + +.add-icon, +.menu-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} + +.grid-container { + width: 100%; + padding: 0 $s-4; +} + +.show-more { + --show-more-color: var(--button-secondary-foreground-color-rest); + @include buttonStyle; + @include bodyMediumTypography; + position: absolute; + top: $s-8; + right: $s-52; + display: flex; + align-items: center; + justify-content: space-between; + column-gap: $s-12; + color: var(--show-more-color); + + &:hover { + --show-more-color: var(--button-secondary-foreground-color-active); + } +} + +.show-more-icon { + height: $s-16; + width: $s-16; + fill: none; + stroke: var(--show-more-color); +} + +// Team hero +.team-hero { + background-color: $db-tertiary; + border-radius: $br-8; + border: none; + display: flex; + margin: $s-16; + padding: $s-8; + position: relative; + + img { + border-radius: $br-4; + height: $s-200; + width: auto; + + @media (max-width: 1200px) { + display: none; + width: 0; + } + } +} + +.text { + display: flex; + flex-direction: column; + align-items: flex-start; + flex-grow: 1; + padding: $s-20 $s-20; +} + +.title { + font-size: $fs-24; + color: $df-primary; + font-weight: $fw400; +} + +.info { + flex: 1; + font-size: $fs-16; + span { + color: $df-secondary; + display: block; + } + a { + color: $da-primary; + } + padding: $s-8 0; +} + +.close { + --close-icon-foreground-color: var(--icon-foreground); + position: absolute; + top: $s-20; + right: $s-24; + width: $s-24; + background-color: transparent; + border: none; + cursor: pointer; + &:hover { + --close-icon-foreground-color: var(--button-icon-foreground-color-selected); + } +} + +.close-icon { + @extend .button-icon; + stroke: var(--close-icon-foreground-color); +} + +.invite { + height: $s-32; + width: $s-180; +} + +.img-wrapper { + display: flex; + align-items: center; + justify-content: center; + width: $s-200; + height: $s-200; + overflow: hidden; + border-radius: $br-4; + @media (max-width: 1200px) { + display: none; + width: 0; + } +} + +.hero-projects { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: $s-32; + margin: 0 $s-16 $s-16 $s-20; + + @media (max-width: 1366px) { + grid-template-columns: 1fr; + } + + .tutorial, + .walkthrough { + display: grid; + grid-template-columns: auto 1fr; + position: relative; + border-radius: $br-8; + min-height: $s-216; + background-color: $db-tertiary; + padding: $s-8; + + .thumbnail { + width: $s-200; + height: $s-200; + border-radius: $br-6; + padding: $s-32; + display: block; + background-color: var(--color-canvas); + } + + img { + border-radius: $br-4; + margin-bottom: 0; + width: $s-232; + } + + .text { + padding: $s-32; + display: flex; + flex-direction: column; + } + + .title { + color: $df-primary; + font-size: $fs-24; + font-weight: $fw400; + margin-bottom: $s-8; + } + .info { + flex: 1; + color: $df-secondary; + margin-bottom: $s-20; + font-size: $fs-16; + } + .invite { + height: $s-32; + } + .action { + width: $s-180; + height: $s-40; + } + } + .walkthrough { + .thumbnail { + background-image: url("/images/walkthrough-cover.png"); + background-position: center; + background-repeat: no-repeat; + background-size: cover; + } + } + .tutorial { + .thumbnail { + background-image: url("/images/hands-on-tutorial.png"); + background-position: center; + background-repeat: no-repeat; + background-size: cover; + } + .loader { + display: flex; + svg#loader-pencil { + width: $s-32; + } + } + } +} diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs index bcbcfd710f..3b4d090996 100644 --- a/frontend/src/app/main/ui/dashboard/search.cljs +++ b/frontend/src/app/main/ui/dashboard/search.cljs @@ -5,84 +5,59 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.search + (:require-macros [app.main.style :as stl]) (:require - [app.common.math :as mth] [app.main.data.dashboard :as dd] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.dashboard.grid :refer [grid]] + [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.webapi :as wapi] - [beicon.core :as rx] [rumext.v2 :as mf])) (mf/defc search-page [{:keys [team search-term] :as props}] - - (mf/use-effect - (mf/deps team) - (fn [] - (when team - (let [tname (if (:is-default team) - (tr "dashboard.your-penpot") - (:name team))] - (dom/set-html-title (tr "title.dashboard.search" tname)))))) - - (mf/use-effect - (mf/deps search-term) - (fn [] - (st/emit! (dd/search {:search-term search-term}) - (dd/clear-selected-files)))) - (let [result (mf/deref refs/dashboard-search-result) - width (mf/use-state nil) - rowref (mf/use-ref) - itemsize (if (>= @width 1030) - 280 - 230) + [rowref limit] (hooks/use-dynamic-grid-item-width)] - ratio (if (some? @width) (/ @width itemsize) 0) - nitems (mth/floor ratio) - limit (min 10 nitems) - limit (max 1 limit)] (mf/use-effect + (mf/deps team) (fn [] - (let [node (mf/ref-val rowref) - mnt? (volatile! true) - sub (->> (wapi/observe-resize node) - (rx/observe-on :af) - (rx/subs (fn [entries] - (let [row (first entries) - row-rect (.-contentRect ^js row) - row-width (.-width ^js row-rect)] - (when @mnt? - (reset! width row-width))))))] - (fn [] - (vreset! mnt? false) - (rx/dispose! sub))))) + (when team + (let [tname (if (:is-default team) + (tr "dashboard.your-penpot") + (:name team))] + (dom/set-html-title (tr "title.dashboard.search" tname)))))) + + (mf/use-effect + (mf/deps search-term) + (fn [] + (st/emit! (dd/search {:search-term search-term}) + (dd/clear-selected-files)))) [:* - [:header.dashboard-header - [:div.dashboard-title#dashboard-search-title + [:header {:class (stl/css :dashboard-header)} + [:div#dashboard-search-title {:class (stl/css :dashboard-title)} [:h1 (tr "dashboard.title-search")]]] - [:section.dashboard-container.search.no-bg {:ref rowref} + [:section {:class (stl/css :dashboard-container :search :no-bg) + :ref rowref} (cond (empty? search-term) - [:div.grid-empty-placeholder.search - [:div.icon i/search] - [:div.text (tr "dashboard.type-something")]] + [:div {:class (stl/css :grid-empty-placeholder :search)} + [:div {:class (stl/css :icon)} i/search] + [:div {:class (stl/css :text)} (tr "dashboard.type-something")]] (nil? result) - [:div.grid-empty-placeholder.search - [:div.icon i/search] - [:div.text (tr "dashboard.searching-for" search-term)]] + [:div {:class (stl/css :grid-empty-placeholder :search)} + [:div {:class (stl/css :icon)} i/search] + [:div {:class (stl/css :text)} (tr "dashboard.searching-for" search-term)]] (empty? result) - [:div.grid-empty-placeholder.search - [:div.icon i/search] - [:div.text (tr "dashboard.no-matches-for" search-term)]] + [:div {:class (stl/css :grid-empty-placeholder :search)} + [:div {:class (stl/css :icon)} i/search] + [:div {:class (stl/css :text)} (tr "dashboard.no-matches-for" search-term)]] :else [:& grid {:files result diff --git a/frontend/src/app/main/ui/dashboard/search.scss b/frontend/src/app/main/ui/dashboard/search.scss new file mode 100644 index 0000000000..7c20671c21 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/search.scss @@ -0,0 +1,49 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; +@use "common/refactor/common-dashboard"; +@use "./placeholder.scss"; + +.dashboard-container { + flex: 1 0 0; + margin-right: $s-16; + overflow-y: auto; + width: 100%; + border-top: $s-1 solid $db-quaternary; + + &.dashboard-projects { + user-select: none; + } + &.dashboard-shared { + width: calc(100vw - $s-320); + margin-right: $s-52; + } + + &.search { + margin-top: $s-12; + } +} + +.grid-empty-placeholder.search { + align-items: center; + display: flex; + justify-content: center; + flex-direction: column; + height: $s-200; + background: transparent; + border: $s-1 solid $db-quaternary; + border-radius: $br-8; + + .text { + color: $df-primary; + } + .icon svg { + stroke: $df-secondary; + width: $s-32; + height: $s-32; + } +} diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 571b97fca8..96d4c9df08 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.sidebar + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] @@ -17,13 +18,13 @@ [app.main.data.users :as du] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item]] + [app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]] [app.main.ui.components.link :refer [link]] - [app.main.ui.dashboard.comments :refer [comments-section]] + [app.main.ui.dashboard.comments :refer [comments-icon comments-section]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.project-menu :refer [project-menu]] [app.main.ui.dashboard.team-form] - [app.main.ui.icons :as i] + [app.main.ui.icons :as i :refer [icon-xref]] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] [app.util.i18n :as i18n :refer [tr]] @@ -31,12 +32,40 @@ [app.util.object :as obj] [app.util.router :as rt] [app.util.timers :as ts] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.spec.alpha :as s] + [cuerdas.core :as str] [goog.functions :as f] - [potok.core :as ptk] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) +(def ^:private clear-search-icon + (icon-xref :delete-text (stl/css :clear-search-icon))) + +(def ^:private search-icon + (icon-xref :search (stl/css :search-icon))) + +(def ^:private tick-icon + (icon-xref :tick (stl/css :tick-icon))) + +(def ^:private logo-icon + (icon-xref :logo (stl/css :logo-icon))) + +(def ^:private add-icon + (icon-xref :add (stl/css :add-icon))) + +(def ^:private arrow-icon + (icon-xref :arrow (stl/css :arrow-icon))) + +(def ^:private menu-icon + (icon-xref :menu (stl/css :menu-icon))) + +(def ^:private pin-icon + (icon-xref :pin (stl/css :pin-icon))) + +(def ^:private exit-icon + (icon-xref :exit (stl/css :exit-icon))) + (mf/defc sidebar-project [{:keys [item selected?] :as props}] (let [dstate (mf/deref refs/dashboard-local) @@ -44,20 +73,21 @@ selected-project (:selected-project dstate) edit-id (:project-for-edit dstate) - local (mf/use-state + local* (mf/use-state {:menu-open false :menu-pos nil :edition? (= (:id item) edit-id) :dragging? false}) + local @local* on-click - (mf/use-callback + (mf/use-fn (mf/deps item) (fn [] (st/emit! (dd/go-to-files (:id item))))) on-key-down - (mf/use-callback + (mf/use-fn (mf/deps item) (fn [event] (when (kbd/enter? event) @@ -71,61 +101,62 @@ (dom/set-attribute! project-title "tabindex" "-1"))))))))) on-menu-click - (mf/use-callback + (mf/use-fn (fn [event] (let [position (dom/get-client-position event)] (dom/prevent-default event) - (swap! local assoc + (swap! local* assoc :menu-open true :menu-pos position)))) on-menu-close - (mf/use-callback #(swap! local assoc :menu-open false)) + (mf/use-fn #(swap! local* assoc :menu-open false)) on-edit-open - (mf/use-callback #(swap! local assoc :edition? true)) + (mf/use-fn #(swap! local* assoc :edition? true)) on-edit - (mf/use-callback + (mf/use-fn (mf/deps item) (fn [name] - (st/emit! (-> (dd/rename-project (assoc item :name name)) - (with-meta {::ev/origin "dashboard:sidebar"}))) - (swap! local assoc :edition? false))) + (when-not (str/blank? name) + (st/emit! (-> (dd/rename-project (assoc item :name name)) + (with-meta {::ev/origin "dashboard:sidebar"})))) + (swap! local* assoc :edition? false))) on-drag-enter - (mf/use-callback + (mf/use-fn (mf/deps selected-project) (fn [e] (when (dnd/has-type? e "penpot/files") (dom/prevent-default e) (when-not (dnd/from-child? e) (when (not= selected-project (:id item)) - (swap! local assoc :dragging? true)))))) + (swap! local* assoc :dragging? true)))))) on-drag-over - (mf/use-callback + (mf/use-fn (fn [e] (when (dnd/has-type? e "penpot/files") (dom/prevent-default e)))) on-drag-leave - (mf/use-callback + (mf/use-fn (fn [e] (when-not (dnd/from-child? e) - (swap! local assoc :dragging? false)))) + (swap! local* assoc :dragging? false)))) on-drop-success - (mf/use-callback + (mf/use-fn (mf/deps (:id item)) #(st/emit! (msg/success (tr "dashboard.success-move-file")) (dd/go-to-files (:id item)))) on-drop - (mf/use-callback + (mf/use-fn (mf/deps item selected-files) (fn [_] - (swap! local assoc :dragging? false) + (swap! local* assoc :dragging? false) (when (not= selected-project (:id item)) (let [data {:ids selected-files :project-id (:id item)} @@ -134,8 +165,10 @@ [:* [:li {:tab-index "0" - :class (if selected? "current" - (when (:dragging? @local) "dragging")) + :class (stl/css-case :project-element true + :sidebar-nav-item true + :current selected? + :dragging (:dragging? local)) :on-click on-click :on-key-down on-key-down :on-double-click on-edit-open @@ -144,14 +177,14 @@ :on-drag-over on-drag-over :on-drag-leave on-drag-leave :on-drop on-drop} - (if (:edition? @local) + (if (:edition? local) [:& inline-edition {:content (:name item) :on-end on-edit}] - [:span.element-title (:name item)])] + [:span {:class (stl/css :element-title)} (:name item)])] [:& project-menu {:project item - :show? (:menu-open @local) - :left (:x (:menu-pos @local)) - :top (:y (:menu-pos @local)) + :show? (:menu-open local) + :left (:x (:menu-pos local)) + :top (:y (:menu-pos local)) :on-edit on-edit-open :on-menu-close on-menu-close}]])) @@ -162,19 +195,19 @@ emit! (mf/use-memo #(f/debounce st/emit! 500)) on-search-blur - (mf/use-callback + (mf/use-fn (fn [_] (reset! focused? false))) on-search-change - (mf/use-callback + (mf/use-fn (mf/deps team-id) (fn [event] (let [value (dom/get-target-val event)] (emit! (dd/go-to-search value))))) on-clear-click - (mf/use-callback + (mf/use-fn (mf/deps team-id) (fn [e] (let [search-input (dom/get-element "search-input")] @@ -185,7 +218,7 @@ (dom/stop-propagation e)))) on-key-press - (mf/use-callback + (mf/use-fn (fn [e] (when (kbd/enter? e) (ts/schedule-on-idle @@ -196,84 +229,111 @@ (dom/focus! search-title) (dom/set-attribute! search-title "tabindex" "-1"))))) (dom/prevent-default e) - (dom/stop-propagation e))))] + (dom/stop-propagation e)))) - [:form.sidebar-search - [:input.input-text - {:key "images-search-box" - :id "search-input" - :type "text" - :aria-label (tr "dashboard.search-placeholder") - :placeholder (tr "dashboard.search-placeholder") - :default-value search-term - :auto-complete "off" - ;; :on-focus on-search-focus - :on-blur on-search-blur - :on-change on-search-change - :on-key-press on-key-press - :ref #(when % (set! (.-value %) search-term))}] + handle-clear-search + (mf/use-fn + (mf/deps on-clear-click) + (fn [event] + (when (kbd/enter? event) + (on-clear-click event))))] + + [:form {:class (stl/css :sidebar-search)} + [:input {:class (stl/css :input-text) + :key "images-search-box" + :id "search-input" + :type "text" + :aria-label (tr "dashboard.search-placeholder") + :placeholder (tr "dashboard.search-placeholder") + :default-value search-term + :auto-complete "off" + ;; :on-focus on-search-focus + :on-blur on-search-blur + :on-change on-search-change + :on-key-press on-key-press + :ref #(when % (set! (.-value %) search-term))}] (if (or @focused? (seq search-term)) - [:div.clear-search - {:tab-index "0" - :on-click on-clear-click - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-clear-click event)))} - i/close] + [:button {:class (stl/css :search-btn :clear-search-btn) + :tab-index "0" + :on-click on-clear-click + :on-key-down handle-clear-search} + clear-search-icon] - [:div.search - {:on-click on-clear-click} - i/search])])) + [:button {:class (stl/css :search-btn) + :on-click on-clear-click} + search-icon])])) (mf/defc teams-selector-dropdown-items + {::mf/wrap-props false} [{:keys [team profile teams] :as props}] (let [on-create-clicked - (mf/use-callback + (mf/use-fn #(st/emit! (modal/show :team-form {}))) team-selected - (mf/use-callback - (fn [team-id] - (st/emit! (dd/go-to-projects team-id))))] + (mf/use-fn + (fn [event] + (let [team-id (-> (dom/get-current-target event) + (dom/get-data "value"))] + (st/emit! (dd/go-to-projects team-id))))) + + handle-select-default + (mf/use-fn + (mf/deps profile team-selected) + (fn [event] + (when (kbd/enter? event) + (team-selected (:default-team-id profile) event)))) + + handle-select-team + (mf/use-fn + (mf/deps team-selected) + (fn [event] + (when (kbd/enter? event) + (team-selected event)))) + + handle-creation-key-down + (mf/use-fn + (mf/deps on-create-clicked) + (fn [event] + (when (kbd/enter? event) + (on-create-clicked event))))] [:* - [:& dropdown-menu-item {:on-click (partial team-selected (:default-team-id profile)) - :on-key-down (fn [event] - (when (kbd/enter? event) - (team-selected (:default-team-id profile) event))) - :id "teams-selector-default-team" - :unique-key "default-team" - :klass "team-name"} - [:span.team-icon i/logo-icon] - [:span.team-text (tr "dashboard.your-penpot")] + [:> dropdown-menu-item* {:on-click team-selected + :data-value (:default-team-id profile) + :on-key-down handle-select-default + :id "teams-selector-default-team" + :class (stl/css :team-dropdown-item)} + [:span {:class (stl/css :penpot-icon)} i/logo-icon] + + [:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")] (when (= (:default-team-id profile) (:id team)) - [:span.icon i/tick])] + tick-icon)] (for [team-item (remove :is-default (vals teams))] - [:& dropdown-menu-item {:on-click (partial team-selected (:id team-item)) - :on-key-down (fn [event] - (when (kbd/enter? event) - (team-selected (:id team-item) event))) - :id (str "teams-selector-" (:id team-item)) - :klass "team-name" - :unique-key (dm/str (:id team-item))} - [:span.team-icon - [:img {:src (cf/resolve-team-photo-url team-item) - :alt (:name team-item)}]] - [:span.team-text {:title (:name team-item)} (:name team-item)] + [:> dropdown-menu-item* {:on-click team-selected + :data-value (:id team-item) + :on-key-down handle-select-team + :id (str "teams-selector-" (:id team-item)) + :class (stl/css :team-dropdown-item) + :key (str "teams-selector-" (:id team-item))} + [:img {:src (cf/resolve-team-photo-url team-item) + :class (stl/css :team-picture) + :alt (:name team-item)}] + [:span {:class (stl/css :team-text) + :title (:name team-item)} (:name team-item)] (when (= (:id team-item) (:id team)) - [:span.icon i/tick])]) - [:hr {:role "separator"}] - [:& dropdown-menu-item {:on-click on-create-clicked - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-create-clicked event))) - :id "teams-selector-create-team" - :klass "team-name action" - :unique-key "teams-selector-create-team"} - [:span.team-icon.new-team i/close] - [:span.team-text (tr "dashboard.create-new-team")]]])) + tick-icon)]) + + [:hr {:role "separator" + :class (stl/css :team-separator)}] + [:> dropdown-menu-item* {:on-click on-create-clicked + :on-key-down handle-creation-key-down + :id "teams-selector-create-team" + :class (stl/css :team-dropdown-item :action)} + [:span {:class (stl/css :icon-wrapper)} add-icon] + [:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]]])) (s/def ::member-id ::us/uuid) (s/def ::leave-modal-form @@ -311,144 +371,203 @@ (rx/throw error))) leave-fn - (fn [member-id] - (let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))] - (st/emit! (dd/leave-team (with-meta params - {:on-success on-success - :on-error on-error}))))) + (mf/use-fn + (mf/deps on-success on-error) + (fn [member-id] + (let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))] + (st/emit! (dd/leave-team (with-meta params + {:on-success on-success + :on-error on-error})))))) delete-fn - (fn [] - (st/emit! (dd/delete-team (with-meta team {:on-success on-success - :on-error on-error})))) + (mf/use-fn + (mf/deps team on-success on-error) + (fn [] + (st/emit! (dd/delete-team (with-meta team {:on-success on-success + :on-error on-error}))))) on-rename-clicked - (fn [] - (st/emit! (modal/show :team-form {:team team}))) + (mf/use-fn + (mf/deps team) + (fn [] + (st/emit! (modal/show :team-form {:team team})))) on-leave-clicked - #(st/emit! (modal/show - {:type :confirm - :title (tr "modals.leave-confirm.title") - :message (tr "modals.leave-confirm.message") - :accept-label (tr "modals.leave-confirm.accept") - :on-accept leave-fn})) + (mf/use-fn + (mf/deps leave-fn) + #(st/emit! (modal/show + {:type :confirm + :title (tr "modals.leave-confirm.title") + :message (tr "modals.leave-confirm.message") + :accept-label (tr "modals.leave-confirm.accept") + :on-accept leave-fn}))) on-leave-as-owner-clicked - (fn [] - (st/emit! (dd/fetch-team-members) - (modal/show - {:type :leave-and-reassign - :profile profile - :team team - :accept leave-fn}))) + (mf/use-fn + (mf/deps team profile leave-fn) + (fn [] + (st/emit! (dd/fetch-team-members (:id team)) + (modal/show + {:type :leave-and-reassign + :profile profile + :team team + :accept leave-fn})))) leave-and-close - #(st/emit! (modal/show - {:type :confirm - :title (tr "modals.leave-confirm.title") - :message (tr "modals.leave-and-close-confirm.message" (:name team)) - :scd-message (tr "modals.leave-and-close-confirm.hint") - :accept-label (tr "modals.leave-confirm.accept") - :on-accept delete-fn})) + (mf/use-fn + (mf/deps team delete-fn) + #(st/emit! (modal/show + {:type :confirm + :title (tr "modals.leave-confirm.title") + :message (tr "modals.leave-and-close-confirm.message" (:name team)) + :scd-message (tr "modals.leave-and-close-confirm.hint") + :accept-label (tr "modals.leave-confirm.accept") + :on-accept delete-fn}))) on-delete-clicked - #(st/emit! - (modal/show - {:type :confirm - :title (tr "modals.delete-team-confirm.title") - :message (tr "modals.delete-team-confirm.message") - :accept-label (tr "modals.delete-team-confirm.accept") - :on-accept delete-fn}))] + (mf/use-fn + (mf/deps delete-fn) + #(st/emit! + (modal/show + {:type :confirm + :title (tr "modals.delete-team-confirm.title") + :message (tr "modals.delete-team-confirm.message") + :accept-label (tr "modals.delete-team-confirm.accept") + :on-accept delete-fn}))) + + handle-members + (mf/use-fn + (mf/deps go-members) + (fn [event] + (when (kbd/enter? event) + (go-members)))) + + handle-invitations + (mf/use-fn + (mf/deps go-invitations) + (fn [event] + (when (kbd/enter? event) + (go-invitations)))) + + handle-webhooks + (mf/use-fn + (mf/deps go-webhooks) + (fn [event] + (when (kbd/enter? event) + (go-webhooks)))) + + handle-settings + (mf/use-fn + (mf/deps go-settings) + (fn [event] + (when (kbd/enter? event) + (go-settings)))) + + + handle-rename + (mf/use-fn + (mf/deps on-rename-clicked) + (fn [event] + (when (kbd/enter? event) + (on-rename-clicked)))) + + + handle-leave-and-close + (mf/use-fn + (mf/deps leave-and-close) + (fn [event] + (when (kbd/enter? event) + (leave-and-close)))) + + handle-leave-as-owner-clicked + (mf/use-fn + (mf/deps on-leave-as-owner-clicked) + (fn [event] + (when (kbd/enter? event) + (on-leave-as-owner-clicked)))) + + + handle-on-leave-clicked + (mf/use-fn + (mf/deps on-leave-clicked) + (fn [event] + (when (kbd/enter? event) + (on-leave-clicked)))) + + handle-on-delete-clicked + (mf/use-fn + (mf/deps on-delete-clicked) + (fn [event] + (when (kbd/enter? event) + (on-delete-clicked))))] [:* - [:& dropdown-menu-item {:on-click go-members - :on-key-down (fn [event] - (when (kbd/enter? event) - (go-members))) - :id "teams-options-members" - :unique-key "teams-options-members" - :data-test "team-members"} + [:> dropdown-menu-item* {:on-click go-members + :on-key-down handle-members + :className (stl/css :team-options-item) + :id "teams-options-members" + :data-test "team-members"} (tr "labels.members")] - [:& dropdown-menu-item {:on-click go-invitations - :on-key-down (fn [event] - (when (kbd/enter? event) - (go-invitations))) - :id "teams-options-invitations" - :unique-key "teams-options-invitations" - :data-test "team-invitations"} + [:> dropdown-menu-item* {:on-click go-invitations + :on-key-down handle-invitations + :className (stl/css :team-options-item) + :id "teams-options-invitations" + :data-test "team-invitations"} (tr "labels.invitations")] (when (contains? cf/flags :webhooks) - [:& dropdown-menu-item {:on-click go-webhooks - :on-key-down (fn [event] - (when (kbd/enter? event) - (go-webhooks))) - :id "teams-options-webhooks" - :unique-key "teams-options-webhooks"} + [:> dropdown-menu-item* {:on-click go-webhooks + :on-key-down handle-webhooks + :className (stl/css :team-options-item) + :id "teams-options-webhooks"} (tr "labels.webhooks")]) - [:& dropdown-menu-item {:on-click go-settings - :on-key-down (fn [event] - (when (kbd/enter? event) - (go-settings))) - :id "teams-options-settings" - :unique-key "teams-options-settings" - :data-test "team-settings"} + [:> dropdown-menu-item* {:on-click go-settings + :on-key-down handle-settings + :className (stl/css :team-options-item) + :id "teams-options-settings" + :data-test "team-settings"} (tr "labels.settings")] - [:hr] + [:hr {:class (stl/css :team-option-separator)}] (when can-rename? - [:& dropdown-menu-item {:on-click on-rename-clicked - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-rename-clicked))) - :id "teams-options-rename" - :unique-key "teams-options-rename" - :data-test "rename-team"} + [:> dropdown-menu-item* {:on-click on-rename-clicked + :on-key-down handle-rename + :id "teams-options-rename" + :className (stl/css :team-options-item) + :data-test "rename-team"} (tr "labels.rename")]) (cond (= (count members) 1) - [:& dropdown-menu-item {:on-click leave-and-close - :on-key-down (fn [event] - (when (kbd/enter? event) - (leave-and-close))) - :id "teams-options-leave-team" - :unique-key "teams-options-leave-team"} + [:> dropdown-menu-item* {:on-click leave-and-close + :on-key-down handle-leave-and-close + :className (stl/css :team-options-item) + :id "teams-options-leave-team"} (tr "dashboard.leave-team")] (get-in team [:permissions :is-owner]) - [:& dropdown-menu-item {:on-click on-leave-as-owner-clicked - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-leave-as-owner-clicked))) - :id "teams-options-leave-team" - :unique-key "teams-options-leave-team" - :data-test "leave-team"} + [:> dropdown-menu-item* {:on-click on-leave-as-owner-clicked + :on-key-down handle-leave-as-owner-clicked + :id "teams-options-leave-team" + :className (stl/css :team-options-item) + :data-test "leave-team"} (tr "dashboard.leave-team")] (> (count members) 1) - [:& dropdown-menu-item {:on-click on-leave-clicked - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-leave-clicked))) - :id "teams-options-leave-team" - :unique-key "teams-options-leave-team"} + [:> dropdown-menu-item* {:on-click on-leave-clicked + :on-key-down handle-on-leave-clicked + :className (stl/css :team-options-item) + :id "teams-options-leave-team"} (tr "dashboard.leave-team")]) - (when (get-in team [:permissions :is-owner]) - [:& dropdown-menu-item {:on-click on-delete-clicked - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-delete-clicked))) - :id "teams-options-delete-team" - :unique-key "teams-options-delete-team" - :klass "warning" - :data-test "delete-team"} + [:> dropdown-menu-item* {:on-click on-delete-clicked + :on-key-down handle-on-delete-clicked + :id "teams-options-delete-team" + :className (stl/css :team-options-item :warning) + :data-test "delete-team"} (tr "dashboard.delete-team")])])) - (mf/defc sidebar-team-switch [{:keys [team profile] :as props}] (let [teams (mf/deref refs/teams) @@ -467,62 +586,93 @@ "teams-options-rename") "teams-options-leave-team" (when (get-in team [:permissions :is-owner]) - "teams-options-delete-team")]] + "teams-options-delete-team")] + + handle-show-team-click + (fn [event] + (dom/stop-propagation event) + (swap! show-teams-ddwn? not) + (reset! show-team-opts-ddwn? false)) + + handle-show-team-keydown + (fn [event] + (when (or (kbd/space? event) (kbd/enter? event)) + (dom/prevent-default event) + (reset! show-teams-ddwn? true) + (reset! show-team-opts-ddwn? false) + (ts/schedule-on-idle + (fn [] + (let [first-element (dom/get-element (first ids))] + (when first-element + (dom/focus! first-element))))))) + + close-team-opts-ddwn + (mf/use-fn + #(reset! show-team-opts-ddwn? false)) + + handle-show-opts-click + (fn [event] + (dom/stop-propagation event) + (swap! show-team-opts-ddwn? not) + (reset! show-teams-ddwn? false)) + + handle-show-opts-keydown + (fn [event] + (when (or (kbd/space? event) (kbd/enter? event)) + (dom/prevent-default event) + (reset! show-team-opts-ddwn? true) + (reset! show-teams-ddwn? false) + (ts/schedule-on-idle + (fn [] + (let [first-element (dom/get-element (first options-ids))] + (when first-element + (dom/focus! first-element))))))) + + handle-close-team + (fn [] + (reset! show-teams-ddwn? false))] + + [:div {:class (stl/css :sidebar-team-switch)} + [:div {:class (stl/css :switch-content)} + [:button {:class (stl/css :current-team) + :on-click handle-show-team-click + :on-key-down handle-show-team-keydown} - [:div.sidebar-team-switch - [:div.switch-content - [:button.current-team {:tab-index "0" - :on-click #(reset! show-teams-ddwn? true) - :on-key-down (fn [event] - (when (or (kbd/space? event) (kbd/enter? event)) - (dom/prevent-default event) - (reset! show-teams-ddwn? true) - (ts/schedule-on-idle - (fn [] - (let [first-element (dom/get-element (first ids))] - (when first-element - (dom/focus! first-element)))))))} (if (:is-default team) - [:div.team-name - [:span.team-icon i/logo-icon] - [:span.team-text (tr "dashboard.default-team-name")]] - [:div.team-name - [:span.team-icon - [:img {:src (cf/resolve-team-photo-url team) - :alt (:name team)}]] - [:span.team-text {:title (:name team)} (:name team)]]) + [:div {:class (stl/css :team-name)} + [:span {:class (stl/css :penpot-icon)} i/logo-icon] + [:span {:class (stl/css :team-text)} (tr "dashboard.default-team-name")]] - [:span.switch-icon - i/arrow-down]] + [:div {:class (stl/css :team-name)} + [:img {:src (cf/resolve-team-photo-url team) + :class (stl/css :team-picture) + :alt (:name team)}] + [:span {:class (stl/css :team-text) :title (:name team)} (:name team)]]) + + arrow-icon] (when-not (:is-default team) - [:button.switch-options {:on-click #(reset! show-team-opts-ddwn? true) - :tab-index "0" - :on-key-down (fn [event] - (when (or (kbd/space? event) (kbd/enter? event)) - (dom/prevent-default event) - (reset! show-team-opts-ddwn? true) - (ts/schedule-on-idle - (fn [] - (let [first-element (dom/get-element (first options-ids))] - (when first-element - (dom/focus! first-element)))))))} - i/actions])] + [:button {:class (stl/css :switch-options) + :on-click handle-show-opts-click + :tab-index "0" + :on-key-down handle-show-opts-keydown} + menu-icon])] ;; Teams Dropdown + [:& dropdown-menu {:show @show-teams-ddwn? - :on-close #(reset! show-teams-ddwn? false) + :on-close handle-close-team :ids ids - :list-class "dropdown teams-dropdown"} + :list-class (stl/css :dropdown :teams-dropdown)} [:& teams-selector-dropdown-items {:ids ids :team team :profile profile :teams teams}]] [:& dropdown-menu {:show @show-team-opts-ddwn? - :on-close #(reset! show-team-opts-ddwn? false) + :on-close close-team-opts-ddwn :ids options-ids - :list-class "dropdown options-dropdown"} + :list-class (stl/css :dropdown :options-dropdown)} [:& team-options-dropdown {:team team :profile profile}]]])) @@ -540,12 +690,12 @@ (= (:id project) default-project-id)) go-projects - (mf/use-callback + (mf/use-fn (mf/deps team) #(st/emit! (rt/nav :dashboard-projects {:team-id (:id team)}))) go-projects-with-key - (mf/use-callback + (mf/use-fn (mf/deps team) #(st/emit! (rt/nav :dashboard-projects {:team-id (:id team)}) (ts/schedule-on-idle @@ -557,12 +707,12 @@ (dom/set-attribute! projects-title "tabindex" "-1"))))))) go-fonts - (mf/use-callback + (mf/use-fn (mf/deps team) #(st/emit! (rt/nav :dashboard-fonts {:team-id (:id team)}))) go-fonts-with-key - (mf/use-callback + (mf/use-fn (mf/deps team) #(st/emit! (rt/nav :dashboard-fonts {:team-id (:id team)}) (ts/schedule-on-idle @@ -573,7 +723,7 @@ (dom/focus! font-title) (dom/set-attribute! font-title "tabindex" "-1"))))))) go-drafts - (mf/use-callback + (mf/use-fn (mf/deps team default-project-id) (fn [] (st/emit! (rt/nav :dashboard-files @@ -581,7 +731,7 @@ :project-id default-project-id})))) go-drafts-with-key - (mf/use-callback + (mf/use-fn (mf/deps team default-project-id) #(st/emit! (rt/nav :dashboard-files {:team-id (:id team) :project-id default-project-id}) @@ -594,12 +744,12 @@ (dom/set-attribute! drafts-title "tabindex" "-1"))))))) go-libs - (mf/use-callback + (mf/use-fn (mf/deps team) #(st/emit! (rt/nav :dashboard-libraries {:team-id (:id team)}))) go-libs-with-key - (mf/use-callback + (mf/use-fn (mf/deps team) #(st/emit! (rt/nav :dashboard-libraries {:team-id (:id team)}) (ts/schedule-on-idle @@ -614,45 +764,53 @@ (remove :is-default) (filter :is-pinned))] - [:div.sidebar-content + [:div {:class (stl/css :sidebar-content)} [:& sidebar-team-switch {:team team :profile profile}] - [:hr] + [:& sidebar-search {:search-term search-term :team-id (:id team)}] - [:div.sidebar-content-section - [:ul.sidebar-nav.no-overflow - [:li.recent-projects - {:class-name (when projects? "current")} + + [:div {:class (stl/css :sidebar-content-section)} + [:ul {:class (stl/css :sidebar-nav)} + [:li {:class (stl/css-case :recent-projects true + :sidebar-nav-item true + :current projects?)} [:& link {:action go-projects + :class (stl/css :sidebar-link) :keyboard-action go-projects-with-key} - [:span.element-title (tr "labels.projects")]]] + [:span {:class (stl/css :element-title)} (tr "labels.projects")]]] - [:li {:class-name (when drafts? "current")} + [:li {:class (stl/css-case :current drafts? + :sidebar-nav-item true)} [:& link {:action go-drafts + :class (stl/css :sidebar-link) :keyboard-action go-drafts-with-key} - [:span.element-title (tr "labels.drafts")]]] + [:span {:class (stl/css :element-title)} (tr "labels.drafts")]]] - [:li {:class-name (when libs? "current")} + [:li {:class (stl/css-case :current libs? + :sidebar-nav-item true)} [:& link {:action go-libs + :class (stl/css :sidebar-link) :keyboard-action go-libs-with-key} - [:span.element-title (tr "labels.shared-libraries")]]]]] + [:span {:class (stl/css :element-title)} (tr "labels.shared-libraries")]]]]] - [:hr] - - [:div.sidebar-content-section - [:ul.sidebar-nav.no-overflow - [:li {:class-name (when fonts? "current")} + [:div {:class (stl/css :sidebar-content-section)} + [:ul {:class (stl/css :sidebar-nav)} + [:li {:class (stl/css-case :sidebar-nav-item true + :current fonts?)} [:& link {:action go-fonts + :class (stl/css :sidebar-link) :keyboard-action go-fonts-with-key :data-test "fonts"} - [:span.element-title (tr "labels.fonts")]]]]] + [:span {:class (stl/css :element-title)} (tr "labels.fonts")]]]]] - [:hr] - [:div.sidebar-content-section {:data-test "pinned-projects"} + + [:div {:class (stl/css :sidebar-content-section) + :data-test "pinned-projects"} (if (seq pinned-projects) - [:ul.sidebar-nav + [:ul {:class (stl/css :sidebar-nav :pinned-projects)} (for [item pinned-projects] [:& sidebar-project {:item item @@ -660,141 +818,228 @@ :id (:id item) :team-id (:id team) :selected? (= (:id item) (:id project))}])] - [:div.sidebar-empty-placeholder - [:span.icon i/pin] - [:span.text (tr "dashboard.no-projects-placeholder")]])]])) - + [:div {:class (stl/css :sidebar-empty-placeholder)} + pin-icon + [:span {:class (stl/css :empty-text)} (tr "dashboard.no-projects-placeholder")]])]])) (mf/defc profile-section [{:keys [profile team] :as props}] - (let [show (mf/use-state false) + (let [show* (mf/use-state false) + show (deref show*) photo (cf/resolve-profile-photo-url profile) on-click - (mf/use-callback + (mf/use-fn (fn [section event] (dom/stop-propagation event) + (reset! show* false) (if (keyword? section) (st/emit! (rt/nav section)) (st/emit! section)))) show-release-notes - (mf/use-callback + (mf/use-fn (fn [event] (let [version (:main cf/version)] (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) (if (and (kbd/alt? event) (kbd/mod? event)) (st/emit! (modal/show {:type :onboarding})) - (st/emit! (modal/show {:type :release-notes :version version}))))))] + (st/emit! (modal/show {:type :release-notes :version version})))))) - [:div.profile-section - [:div.profile {:tab-index "0" - :on-click #(reset! show true) - :on-key-down (fn [event] - (when (kbd/enter? event) - (reset! show true))) - :data-test "profile-btn"} - [:img {:src photo - :alt (:fullname profile)}] - [:span (:fullname profile)]] + show-comments* (mf/use-state false) + show-comments? @show-comments* - [:& dropdown-menu {:on-close #(reset! show false) - :show @show} - [:ul.dropdown - [:li {:tab-index (if show - "0" - "-1") - :on-click (partial on-click :settings-profile) - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-click :settings-profile event))) + handle-hide-comments + (mf/use-fn + (fn [] + (reset! show-comments* false))) + + handle-show-comments + (mf/use-fn + (fn [] + (reset! show-comments* true))) + + handle-click + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (swap! show* not))) + + handle-key-down + (mf/use-fn + (fn [event] + (when (kbd/enter? event) + (reset! show* true)))) + + handle-close + (fn [event] + (dom/stop-propagation event) + (reset! show* false)) + + handle-key-down-profile + (mf/use-fn + (mf/deps on-click) + (fn [event] + (when (kbd/enter? event) + (on-click :settings-profile event)))) + + handle-click-url + (mf/use-fn + (fn [event] + (let [url (-> (dom/get-current-target event) + (dom/get-data "url"))] + (dom/open-new-window url)))) + + handle-keydown-url + (mf/use-fn + (fn [event] + (let [url (-> (dom/get-current-target event) + (dom/get-data "url"))] + (when (kbd/enter? event) + (dom/open-new-window url))))) + + handle-show-release-notes + (mf/use-fn + (mf/deps show-release-notes) + (fn [event] + (when (kbd/enter? event) + (show-release-notes)))) + + handle-feedback-click + (mf/use-fn + (mf/deps on-click) + #(on-click :settings-feedback %)) + + handle-feedback-keydown + (mf/use-fn + (mf/deps on-click) + (fn [event] + (when (kbd/enter? event) + (on-click :settings-feedback event)))) + + handle-logout-click + (mf/use-fn + (mf/deps on-click) + #(on-click (du/logout) %)) + + handle-logout-keydown + (mf/use-fn + (mf/deps on-click) + (fn [event] + (when (kbd/enter? event) + (on-click (du/logout) event)))) + + handle-set-profile + (mf/use-fn + (mf/deps on-click) + (fn [event] + (on-click :settings-profile event)))] + + [:* + (when (and team profile) + [:& comments-section + {:profile profile + :team team + :show? show-comments? + :on-show-comments handle-show-comments + :on-hide-comments handle-hide-comments}]) + + [:div {:class (stl/css :profile-section)} + [:div {:class (stl/css :profile) + :tab-index "0" + :on-click handle-click + :on-key-down handle-key-down + :data-test "profile-btn"} + [:img {:src photo + :class (stl/css :profile-img) + :alt (:fullname profile)}] + [:span {:class (stl/css :profile-fullname)} (:fullname profile)]] + + [:& dropdown-menu {:on-close handle-close :show show :list-class (stl/css :profile-dropdown)} + [:li {:tab-index (if show "0" "-1") + :class (stl/css :profile-dropdown-item) + :on-click handle-set-profile + :on-key-down handle-key-down-profile :data-test "profile-profile-opt"} - [:span.text (tr "labels.your-account")]] - [:li.separator {:tab-index (if show - "0" - "-1") - :on-click #(dom/open-new-window "https://help.penpot.app") - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/open-new-window "https://help.penpot.app"))) - :data-test "help-center-profile-opt"} - [:span.text (tr "labels.help-center")]] - [:li {:tab-index (if show - "0" - "-1") - :on-click #(dom/open-new-window "https://community.penpot.app") - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/open-new-window "https://community.penpot.app")))} - [:span.text (tr "labels.community")]] - [:li {:tab-index (if show - "0" - "-1") - :on-click #(dom/open-new-window "https://www.youtube.com/c/Penpot") - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/open-new-window "https://www.youtube.com/c/Penpot")))} - [:span.text (tr "labels.tutorials")]] - [:li {:tab-index (if show - "0" - "-1") - :on-click show-release-notes - :on-key-down (fn [event] - (when (kbd/enter? event) - (show-release-notes)))} - [:span (tr "labels.release-notes")]] + (tr "labels.your-account")] - [:li.separator {:tab-index (if show - "0" - "-1") - :on-click #(dom/open-new-window "https://penpot.app/libraries-templates") - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/open-new-window "https://penpot.app/libraries-templates"))) - :data-test "libraries-templates-profile-opt"} - [:span.text (tr "labels.libraries-and-templates")]] - [:li {:tab-index (if show - "0" - "-1") - :on-click #(dom/open-new-window "https://github.com/penpot/penpot") - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/open-new-window "https://github.com/penpot/penpot")))} - [:span (tr "labels.github-repo")]] - [:li {:tab-index (if show - "0" - "-1") - :on-click #(dom/open-new-window "https://penpot.app/terms") - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/open-new-window "https://penpot.app/terms")))} - [:span (tr "auth.terms-of-service")]] + [:li {:class (stl/css :profile-separator)}] + + [:li {:class (stl/css :profile-dropdown-item) + :tab-index (if show "0" "-1") + :data-url "https://help.penpot.app" + :on-click handle-click-url + :on-key-down handle-keydown-url + :data-test "help-center-profile-opt"} + (tr "labels.help-center")] + + [:li {:tab-index (if show "0" "-1") + :class (stl/css :profile-dropdown-item) + :data-url "https://community.penpot.app" + :on-click handle-click-url + :on-key-down handle-keydown-url} + (tr "labels.community")] + + [:li {:tab-index (if show "0" "-1") + :class (stl/css :profile-dropdown-item) + :data-url "https://www.youtube.com/c/Penpot" + :on-click handle-click-url + :on-key-down handle-keydown-url} + (tr "labels.tutorials")] + + [:li {:tab-index (if show "0" "-1") + :class (stl/css :profile-dropdown-item) + :on-click show-release-notes + :on-key-down handle-show-release-notes} + (tr "labels.release-notes")] + + [:li {:class (stl/css :profile-separator)}] + + [:li {:class (stl/css :profile-dropdown-item) + :tab-index (if show "0" "-1") + :data-url "https://penpot.app/libraries-templates" + :on-click handle-click-url + :on-key-down handle-keydown-url + :data-test "libraries-templates-profile-opt"} + (tr "labels.libraries-and-templates")] + + [:li {:tab-index (if show "0" "-1") + :class (stl/css :profile-dropdown-item) + :data-url "https://github.com/penpot/penpot" + :on-click handle-click-url + :on-key-down handle-keydown-url} + (tr "labels.github-repo")] + + [:li {:tab-index (if show "0" "-1") + :class (stl/css :profile-dropdown-item) + :data-url "https://penpot.app/terms" + :on-click handle-click-url + :on-key-down handle-keydown-url} + (tr "auth.terms-of-service")] + + [:li {:class (stl/css :profile-separator)}] (when (contains? cf/flags :user-feedback) - [:li.separator {:tab-index (if show - "0" - "-1") - :on-click (partial on-click :settings-feedback) - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-click :settings-feedback event))) - :data-test "feedback-profile-opt"} - [:span.text (tr "labels.give-feedback")]]) + [:li {:class (stl/css :profile-dropdown-item) + :tab-index (if show "0" "-1") + :on-click handle-feedback-click + :on-key-down handle-feedback-keydown + :data-test "feedback-profile-opt"} + (tr "labels.give-feedback")]) - [:li.separator {:tab-index (if show - "0" - "-1") - :on-click #(on-click (du/logout) %) - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-click (du/logout) event))) - :data-test "logout-profile-opt"} - [:span.icon i/exit] - [:span.text (tr "labels.logout")]]]] + [:li {:class (stl/css :profile-dropdown-item :item-with-icon) + :tab-index (if show "0" "-1") + :on-click handle-logout-click + :on-key-down handle-logout-keydown + :data-test "logout-profile-opt"} + exit-icon + (tr "labels.logout")]] - (when (and team profile) - [:& comments-section {:profile profile - :team team}])])) + (when (and team profile) + [:& comments-icon + {:profile profile + :show? show-comments? + :on-show-comments handle-show-comments}])]])) (mf/defc sidebar {::mf/wrap-props false @@ -802,8 +1047,9 @@ [props] (let [team (obj/get props "team") profile (obj/get props "profile")] - [:nav.dashboard-sidebar + [:nav {:class (stl/css :dashboard-sidebar)} [:> sidebar-content props] [:& profile-section {:profile profile :team team}]])) + diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss new file mode 100644 index 0000000000..f939c8d442 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -0,0 +1,382 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; +@use "common/refactor/common-dashboard"; + +// SIDEBAR COMPONENT +.dashboard-sidebar { + grid-row: 1 / span 2; + grid-column: 1 / span 2; + display: grid; + grid-template-rows: 1fr auto; + height: 100%; + width: 100%; + padding: $s-16 0 0 0; + margin: 0 $s-16 0 0; + border-right: $s-1 solid var(--panel-border-color); + background-color: var(--panel-background-color); + z-index: $z-index-1; +} + +//SIDEBAR CONTENT COMPONENT +.sidebar-content { + display: grid; + grid-template-rows: auto auto auto auto 1fr; + gap: $s-24; + height: 100%; + padding: 0; + overflow-y: auto; +} + +// SIDEBAR TEAM SWITCH +.sidebar-team-switch { + position: relative; + margin: $s-4 $s-16; +} + +.switch-content { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + height: $s-48; + width: 100%; + border-radius: $br-8; + border: $s-1 solid var(--menu-background-color); + background-color: var(--menu-background-color); +} + +.current-team { + @include buttonStyle; + display: grid; + align-items: center; + grid-template-columns: 1fr auto; + gap: $s-8; + height: 100%; + padding: 0 $s-12; +} + +.team-name { + display: grid; + align-items: center; + grid-template-columns: auto 1fr; + gap: $s-12; + height: $s-40; +} + +.team-text { + @include textEllipsis; + @include smallTitleTipography; + width: $s-144; + text-align: left; + color: var(--menu-foreground-color-hover); +} + +// This icon still use the old svg +.penpot-icon { + @include flexCenter; + svg { + fill: var(--icon-foreground); + width: $s-24; + height: $s-24; + } +} + +.team-picture { + @include flexCenter; + border-radius: 50%; + height: $s-24; + width: $s-24; +} + +.arrow-icon { + @extend .button-icon; + transform: rotate(90deg); + stroke: var(--icon-foreground); +} + +.switch-options { + @include buttonStyle; + @include flexCenter; + max-width: $s-24; + min-width: $s-28; + height: 100%; + border-left: $s-1 solid var(--panel-background-color); + background-color: transparent; +} + +.menu-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} + +// DROPDOWNS + +.teams-dropdown { + @extend .menu-dropdown; + left: 0; + top: $s-52; + height: fit-content; + max-height: $s-480; + min-width: $s-248; + width: 100%; + overflow-x: hidden; + overflow-y: auto; +} + +.team-dropdown-item { + @extend .menu-item-base; + display: grid; + grid-template-columns: $s-24 1fr auto; + gap: $s-8; + height: $s-40; +} + +.action { + --sidebar-action-icon-color: var(--icon-foreground); + --sidebar-icon-backgroun-color: var(--color-background-secondary); + &:hover { + --sidebar-action-icon-color: var(--color-background-secondary); + --sidebar-icon-backgroun-color: var(--color-accent-primary); + } +} + +.icon-wrapper { + @include flexCenter; + width: $s-24; + height: $s-24; + margin-right: $s-12; + border-radius: 50%; + background-color: var(--sidebar-icon-backgroun-color); +} + +.add-icon { + @extend .button-icon; + width: $s-24; + height: $s-24; + stroke: var(--sidebar-action-icon-color); +} + +.team-separator { + border-top: $s-1 solid var(--dropdown-separator-color); + margin: 0; +} + +.tick-icon { + @extend .button-icon-small; + stroke: var(--icon-foreground); +} + +.options-dropdown { + @extend .menu-dropdown; + right: $s-2; + top: $s-52; + max-height: $s-480; + &:not(.teams-dropdown) { + min-width: $s-160; + } +} + +.team-options-item { + @extend .menu-item-base; + height: $s-40; +} + +.team-option-separator { + height: $s-1; + margin: 0; + border-top: $s-1 solid var(--dropdown-separator-color); +} + +// Sections +.sidebar-nav { + margin: 0; + user-select: none; + overflow: none; +} + +.pinned-projects { + overflow-y: auto; +} + +.sidebar-nav-item { + cursor: pointer; + &:hover { + background-color: var(--sidebar-element-background-color-hover); + span { + color: var(--sidebar-element-foreground-color-hover); + } + } + + &.current { + background-color: var(--sidebar-element-background-color-selected); + .element-title { + color: var(--sidebar-element-foreground-color-selected); + } + } +} + +.recent-projects svg { + stroke: var(--main-icon-foreground); +} + +.sidebar-link { + display: block; + padding: $s-8 $s-8 $s-8 $s-24; + font-weight: $fw400; + width: 100%; + &:hover { + text-decoration: none; + } +} + +.project-element { + padding: $s-8 $s-8 $s-8 $s-24; +} + +.element-title { + @include textEllipsis; + width: $s-256; + color: var(--sidebar-element-foreground-color); + font-size: $fs-14; +} + +// Pinned projects + +.sidebar-empty-placeholder { + padding: $s-12; + color: var(--empty-message-foreground-color); + display: flex; + align-items: center; +} + +.pin-icon { + @extend .button-icon-small; + stroke: var(--icon-foreground); + margin: 0 $s-12; +} + +.empty-text { + font-size: $fs-12; +} + +// Search + +.sidebar-search { + position: relative; + display: grid; + grid-template-columns: 1fr; + align-items: center; + border: $s-1 solid transparent; + margin: 0 $s-16; + border-radius: $br-8; + background-color: var(--search-bar-input-background-color); +} + +.input-text { + @include smallTitleTipography; + height: $s-40; + width: 100%; + padding: $s-6 $s-12; + margin: 0; + border: transparent; + border-radius: $br-8; + background: transparent; + color: var(--search-bar-foreground-color); + + &:focus, + &:focus-within, + &:focus-visible { + outline: none; + border: $s-1 solid var(--search-bar-input-border-color-focus); + } + ::placeholder { + color: var(--search-bar-placeholder-foreground-color); + } +} + +.search-btn { + @include buttonStyle; + @include flexCenter; + position: absolute; + right: 0; + height: $s-24; + width: $s-32; + padding: 0 $s-8; +} + +.search-icon, +.clear-search-btn { + @extend .button-icon; + --sidebar-search-foreground-color: var(--search-bar-icon-foreground-color); + stroke: var(--sidebar-search-foreground-color); +} + +.clear-search-btn:hover { + --sidebar-search-foreground-color: var(--search-bar-icon-foreground-color-hover); +} + +// Profile +.profile-section { + position: relative; + display: grid; + grid-template-columns: 1fr auto; + padding: $s-12 $s-16; + border-top: $s-1 solid var(--panel-border-color); + background-color: var(--profile-section-background-color); + cursor: pointer; +} + +.profile { + display: grid; + grid-template-columns: auto 1fr; + gap: $s-8; + cursor: pointer; +} + +.profile-fullname { + @include smallTitleTipography; + @include textEllipsis; + align-self: center; + max-width: $s-160; + color: var(--profile-foreground-color); +} + +.profile-img { + height: $s-40; + width: $s-40; + border-radius: $br-circle; +} + +.profile-dropdown { + @extend .menu-dropdown; + left: $s-16; + bottom: $s-72; + min-width: $s-252; + // TODO ADD animation fadeInUp +} + +.profile-dropdown-item { + @extend .menu-item-base; + @include smallTitleTipography; + height: $s-40; + padding: $s-8 $s-16; +} + +.profile-separator { + height: $s-6; +} + +.item-with-icon { + display: grid; + grid-template-columns: auto 1fr; + gap: $s-8; +} + +.exit-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 43f16aac7d..76652e98ae 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.team + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] @@ -23,13 +24,40 @@ [app.main.ui.dashboard.change-owner] [app.main.ui.dashboard.team-form] [app.main.ui.icons :as i] + [app.main.ui.notifications.badge :refer [badge-notification]] + [app.main.ui.notifications.context-notification :refer [context-notification]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.spec.alpha :as s] [cuerdas.core :as str] [rumext.v2 :as mf])) + +(def ^:private arrow-icon + (i/icon-xref :arrow (stl/css :arrow-icon))) + +(def ^:private menu-icon + (i/icon-xref :menu (stl/css :menu-icon))) + +(def ^:private warning-icon + (i/icon-xref :msg-warning (stl/css :warning-icon))) + +(def ^:private success-icon + (i/icon-xref :msg-success (stl/css :success-icon))) + +(def ^:private image-icon + (i/icon-xref :img (stl/css :image-icon))) + +(def ^:private user-icon + (i/icon-xref :user (stl/css :user-icon))) + +(def ^:private document-icon + (i/icon-xref :document (stl/css :document-icon))) + +(def ^:private group-icon + (i/icon-xref :group (stl/css :group-icon))) + (mf/defc header {::mf/wrap [mf/memo] ::mf/wrap-props false} @@ -53,32 +81,33 @@ :team team :origin :team}))))] - [:header.dashboard-header.team - [:div.dashboard-title + [:header {:class (stl/css :dashboard-header :team)} + [:div {:class (stl/css :dashboard-title)} [:h1 (cond members-section? (tr "labels.members") settings-section? (tr "labels.settings") invitations-section? (tr "labels.invitations") webhooks-section? (tr "labels.webhooks") :else nil)]] - [:nav.dashboard-header-menu - [:ul.dashboard-header-options - [:li {:class (when members-section? "active")} + [:nav {:class (stl/css :dashboard-header-menu)} + [:ul {:class (stl/css :dashboard-header-options)} + [:li {:class (when members-section? (stl/css :active))} [:a {:on-click on-nav-members} (tr "labels.members")]] - [:li {:class (when invitations-section? "active")} + [:li {:class (when invitations-section? (stl/css :active))} [:a {:on-click on-nav-invitations} (tr "labels.invitations")]] (when (contains? cfg/flags :webhooks) - [:li {:class (when webhooks-section? "active")} + [:li {:class (when webhooks-section? (stl/css :active))} [:a {:on-click on-nav-webhooks} (tr "labels.webhooks")]]) - [:li {:class (when settings-section? "active")} + [:li {:class (when settings-section? (stl/css :active))} [:a {:on-click on-nav-settings} (tr "labels.settings")]]]] - [:div.dashboard-buttons + [:div {:class (stl/css :dashboard-buttons)} (if (and (or invitations-section? members-section?) (:is-admin permissions)) - [:a.btn-secondary.btn-small - {:on-click on-invite-member + [:a + {:class (stl/css :btn-secondary :btn-small) + :on-click on-invite-member :data-test "invite-member"} (tr "dashboard.invite-profile")] - [:div.blank-space])]])) + [:div {:class (stl/css :blank-space)}])]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; INVITATIONS MODAL @@ -153,29 +182,29 @@ (with-meta {::ev/origin origin})) (dd/fetch-team-invitations))))] - [:div.modal.dashboard-invite-modal.form-container - {:class (dom/classnames - :hero (= origin :hero))} + + [:div {:class (stl/css-case :modal-team-container true + :hero (= origin :hero))} [:& fm/form {:on-submit on-submit :form form} - [:div.title - [:span.text (tr "modals.invite-team-member.title")]] + [:div {:class (stl/css :modal-title)} + (tr "modals.invite-team-member.title")] (when-not (= "" @error-text) - [:div.error - [:span.icon i/msg-error] - [:span.text @error-text]]) + [:& context-notification {:content @error-text + :type :error}]) (when (some current-data-emails current-members-emails) - [:div.warning - [:span.icon i/msg-warning] - [:span.text (tr "modals.invite-member.repeated-invitation")]]) + [:& context-notification {:content (tr "modals.invite-member.repeated-invitation") + :type :warning}]) - [:div.form-row - [:p.label (tr "onboarding.choice.team-up.roles")] + [:div {:class (stl/css :role-select)} + [:p {:class (stl/css :role-title)} + (tr "onboarding.choice.team-up.roles")] [:& fm/select {:name :role :options roles}]] - [:div.form-row + [:div {:class (stl/css :invitation-row)} [:& fm/multi-input {:type "email" + :class (stl/css :email-input) :name :emails :auto-focus? true :trim true @@ -184,10 +213,12 @@ :label (tr "modals.invite-member.emails") :on-submit on-submit}]] - [:div.action-buttons - [:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept") - :disabled (and (boolean (some current-data-emails current-members-emails)) - (empty? (remove current-members-emails current-data-emails)))}]]]])) + [:div {:class (stl/css :action-buttons)} + [:> fm/submit-button* + {:label (tr "modals.invite-member-confirm.accept") + :class (stl/css :accept-btn) + :disabled (and (boolean (some current-data-emails current-members-emails)) + (empty? (remove current-members-emails current-data-emails)))}]]]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MEMBERS SECTION @@ -198,13 +229,13 @@ [{:keys [member profile]}] (let [is-you? (= (:id profile) (:id member))] [:* - [:div.member-image - [:img {:src (cfg/resolve-profile-photo-url member)}]] - [:div.member-info - [:div.member-name (:name member) + [:img {:class (stl/css :member-image) + :src (cfg/resolve-profile-photo-url member)}] + [:div {:class (stl/css :member-info)} + [:div {:class (stl/css :member-name)} (:name member) (when is-you? - [:span.you (tr "labels.you")])] - [:div.member-email (:email member)]]])) + [:span {:class (stl/css :you)} (tr "labels.you")])] + [:div {:class (stl/css :member-email)} (:email member)]]])) (mf/defc rol-info {::mf/wrap-props false} @@ -231,21 +262,28 @@ on-hide (mf/use-fn #(reset! show? false))] [:* (if (and can-change-rol? not-superior? (not (and is-you? you-owner?))) - [:div.rol-selector.has-priv {:on-click on-show} - [:span.rol-label (tr role)] - [:span.icon i/arrow-down]] - [:div.rol-selector - [:span.rol-label (tr role)]]) + [:div {:class (stl/css :rol-selector :has-priv) + :on-click on-show} + [:span {:class (stl/css :rol-label)} (tr role)] + arrow-icon] + [:div {:class (stl/css :rol-selector)} + [:span {:class (stl/css :rol-label)} (tr role)]]) [:& dropdown {:show @show? :on-close on-hide} - [:ul.dropdown.options-dropdown - [:li {:on-click on-set-admin} (tr "labels.admin")] - [:li {:on-click on-set-editor} (tr "labels.editor")] - ;; Temporarily disabled viewer role - ;; https://tree.taiga.io/project/penpot/issue/1083 - ;; [:li {:on-click set-viewer} (tr "labels.viewer")] + [:ul {:class (stl/css :roles-dropdown)} + [:li {:on-click on-set-admin + :class (stl/css :rol-dropdown-item)} + (tr "labels.admin")] + [:li {:on-click on-set-editor + :class (stl/css :rol-dropdown-item)} + (tr "labels.editor")] + ;; Temporarily disabled viewer role + ;; https://tree.taiga.io/project/penpot/issue/1083 + ;; [:li {:on-click set-viewer} (tr "labels.viewer")] (when you-owner? - [:li {:on-click (partial on-set-owner member)} (tr "labels.owner")])]]])) + [:li {:on-click (partial on-set-owner member) + :class (:stl/css :rol-dropdown-item)} + (tr "labels.owner")])]]])) (mf/defc member-actions {::mf/wrap-props false} @@ -262,14 +300,20 @@ [:* (when (or is-you? (and can-delete? (not (and is-owner? (not owner?))))) - [:span.icon {:on-click on-show} [i/actions]]) + [:button {:class (stl/css :menu-btn) + :on-click on-show} + menu-icon]) [:& dropdown {:show @show? :on-close on-hide} - [:ul.dropdown.actions-dropdown + [:ul {:class (stl/css :actions-dropdown)} (when is-you? - [:li {:on-click on-leave} (tr "dashboard.leave-team")]) + [:li {:on-click on-leave + :class (stl/css :action-dropdown-item) + :key "is-you-option"} (tr "dashboard.leave-team")]) (when (and can-delete? (not is-you?) (not (and is-owner? (not owner?)))) - [:li {:on-click on-delete} (tr "labels.remove-member")])]]])) + [:li {:on-click on-delete + :class (stl/css :action-dropdown-item) + :key "is-not-you-option"} (tr "labels.remove-member")])]]])) (defn- set-role! [member-id role] (let [params {:member-id member-id :role role}] @@ -354,7 +398,7 @@ (mf/use-fn (mf/deps profile team on-leave-accepted) (fn [] - (st/emit! (dd/fetch-team-members) + (st/emit! (dd/fetch-team-members (:id team)) (modal/show {:type :leave-and-reassign :profile profile @@ -389,11 +433,11 @@ (= true owner?) on-change-owner-and-leave :else on-leave)] - [:div.table-row - [:div.table-field.name + [:div {:class (stl/css :table-row)} + [:div {:class (stl/css :table-field :field-name)} [:& member-info {:member member :profile profile}]] - [:div.table-field.roles + [:div {:class (stl/css :table-field :field-roles)} [:& rol-info {:member member :team team :on-set-admin on-set-admin @@ -401,7 +445,7 @@ :on-set-owner on-set-owner :profile profile}]] - [:div.table-field.actions + [:div {:class (stl/css :table-field :field-actions)} [:& member-actions {:member member :profile profile :team team @@ -419,12 +463,12 @@ (->> (vals members-map) (d/seek :is-owner)))] - [:div.dashboard-table.team-members - [:div.table-header - [:div.table-field.name (tr "labels.member")] - [:div.table-field.role (tr "labels.role")]] + [:div {:class (stl/css :dashboard-table :team-members)} + [:div {:class (stl/css :table-header)} + [:div {:class (stl/css :table-field :title-field-name)} (tr "labels.member")] + [:div {:class (stl/css :table-field :title-field-role)} (tr "labels.role")]] - [:div.table-rows + [:div {:class (stl/css :table-rows)} [:& team-member {:member owner :team team @@ -451,12 +495,12 @@ (tr "dashboard.your-penpot") (:name team))))) - (mf/with-effect [] - (st/emit! (dd/fetch-team-members))) + (mf/with-effect [team] + (st/emit! (dd/fetch-team-members (:id team)))) [:* [:& header {:section :dashboard-team-members :team team}] - [:section.dashboard-container.dashboard-team-members + [:section {:class (stl/css :dashboard-container :dashboard-team-members)} [:& team-members {:profile profile :team team @@ -490,28 +534,23 @@ [:* (if (and can-invite? (= status :pending)) - [:div.rol-selector.has-priv {:on-click on-show} - [:span.rol-label label] - [:span.icon i/arrow-down]] - [:div.rol-selector - [:span.rol-label label]]) + [:div {:class (stl/css :rol-selector :has-priv) + :on-click on-show} + [:span {:class (stl/css :rol-label)} label] + arrow-icon] + [:div {:class (stl/css :rol-selector)} + [:span {:class (stl/css :rol-label)} label]]) [:& dropdown {:show @show? :on-close on-hide} - [:ul.dropdown.options-dropdown - [:li {:data-role "admin" :on-click on-change'} (tr "labels.admin")] - [:li {:data-role "editor" :on-click on-change'} (tr "labels.editor")]]]])) - -(mf/defc invitation-status-badge - {::mf/wrap-props false} - [{:keys [status]}] - [:div.status-badge - {:class (dom/classnames - :expired (= status :expired) - :pending (= status :pending))} - [:span.status-label - (if (= status :expired) - (tr "labels.expired-invitation") - (tr "labels.pending-invitation"))]]) + [:ul {:class (stl/css :roles-dropdown)} + [:li {:data-role "admin" + :class (stl/css :rol-dropdown-item) + :on-click on-change'} + (tr "labels.admin")] + [:li {:data-role "editor" + :class (stl/css :rol-dropdown-item) + :on-click on-change'} + (tr "labels.editor")]]]])) (mf/defc invitation-actions {::mf/wrap-props false} @@ -592,12 +631,21 @@ on-show (mf/use-fn #(reset! show? true))] [:* - [:span.icon {:on-click on-show} [i/actions]] + [:button {:class (stl/css :menu-btn) + :on-click on-show} + menu-icon] + [:& dropdown {:show @show? :on-close on-hide} - [:ul.dropdown.actions-dropdown - [:li {:on-click on-copy} (tr "labels.copy-invitation-link")] - [:li {:on-click on-resend} (tr "labels.resend-invitation")] - [:li {:on-click on-delete} (tr "labels.delete-invitation")]]]])) + [:ul {:class (stl/css :actions-dropdown :invitations-dropdown)} + [:li {:on-click on-copy + :class (stl/css :action-dropdown-item)} + (tr "labels.copy-invitation-link")] + [:li {:on-click on-resend + :class (stl/css :action-dropdown-item)} + (tr "labels.resend-invitation")] + [:li {:on-click on-delete + :class (stl/css :action-dropdown-item)} + (tr "labels.delete-invitation")]]]])) (mf/defc invitation-row {::mf/wrap [mf/memo] @@ -608,6 +656,10 @@ email (:email invitation) role (:role invitation) status (if expired? :expired :pending) + type (if expired? :warning :default) + badge-content (if (= status :expired) + (tr "labels.expired-invitation") + (tr "labels.pending-invitation")) on-change-role (mf/use-fn @@ -617,20 +669,20 @@ mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}] (st/emit! (dd/update-team-invitation-role (with-meta params mdata))))))] - [:div.table-row - [:div.table-field.mail email] + [:div {:class (stl/css :table-row :table-row-invitations)} + [:div {:class (stl/css :table-field :field-email)} email] - [:div.table-field.roles + [:div {:class (stl/css :table-field :field-roles)} [:& invitation-role-selector {:can-invite? can-invite? :role role :status status :on-change on-change-role}]] - [:div.table-field.status - [:& invitation-status-badge {:status status}]] + [:div {:class (stl/css :table-field :field-status)} + [:& badge-notification {:type type :content badge-content}]] - [:div.table-field.actions + [:div {:class (stl/css :table-field :field-actions)} (when can-invite? [:& invitation-actions {:invitation invitation @@ -638,7 +690,7 @@ (mf/defc empty-invitation-table [{:keys [can-invite?] :as props}] - [:div.empty-invitations + [:div {:class (stl/css :empty-invitations)} [:span (tr "labels.no-invitations")] (when can-invite? [:& i18n/tr-html {:label "labels.no-invitations-hint" @@ -651,14 +703,14 @@ can-invite? (or owner? admin?) team-id (:id team)] - [:div.dashboard-table.invitations - [:div.table-header - [:div.table-field.name (tr "labels.invitations")] - [:div.table-field.role (tr "labels.role")] - [:div.table-field.status (tr "labels.status")]] + [:div {:class (stl/css :invitations)} + [:div {:class (stl/css :table-header)} + [:div {:class (stl/css :title-field-name)} (tr "labels.invitations")] + [:div {:class (stl/css :title-field-role)} (tr "labels.role")] + [:div {:class (stl/css :title-field-status)} (tr "labels.status")]] (if (empty? invitations) [:& empty-invitation-table {:can-invite? can-invite?}] - [:div.table-rows + [:div {:class (stl/css :table-rows)} (for [invitation invitations] [:& invitation-row {:key (:email invitation) @@ -683,9 +735,12 @@ [:* [:& header {:section :dashboard-team-invitations :team team}] - [:section.dashboard-container.dashboard-team-invitations - [:& invitation-section {:team team - :invitations invitations}]]])) + [:section {:class (stl/css :dashboard-team-invitations)} + ;; TODO: We should consider adding a "loading state" here + ;; with an (if (nil? invitations) [:& loading-state] [:& invitations]) + (when-not (nil? invitations) + [:& invitation-section {:team team + :invitations invitations}])]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; WEBHOOKS SECTION @@ -724,24 +779,25 @@ on-error (mf/use-fn - (fn [form {:keys [type code hint] :as error}] - (if (and (= type :validation) - (= code :webhook-validation)) - (let [message (cond - (= hint "unknown") - (tr "errors.webhooks.unexpected") - (= hint "invalid-uri") - (tr "errors.webhooks.invalid-uri") - (= hint "ssl-validation-error") - (tr "errors.webhooks.ssl-validation") - (= hint "timeout") - (tr "errors.webhooks.timeout") - (= hint "connection-error") - (tr "errors.webhooks.connection") - (str/starts-with? hint "unexpected-status") - (tr "errors.webhooks.unexpected-status" (extract-status hint)))] - (swap! form assoc-in [:errors :uri] {:message message})) - (rx/throw error)))) + (fn [form error] + (let [{:keys [type code hint]} (ex-data error)] + (if (and (= type :validation) + (= code :webhook-validation)) + (let [message (cond + (= hint "unknown") + (tr "errors.webhooks.unexpected") + (= hint "invalid-uri") + (tr "errors.webhooks.invalid-uri") + (= hint "ssl-validation-error") + (tr "errors.webhooks.ssl-validation") + (= hint "timeout") + (tr "errors.webhooks.timeout") + (= hint "connection-error") + (tr "errors.webhooks.connection") + (str/starts-with? hint "unexpected-status") + (tr "errors.webhooks.unexpected-status" (extract-status hint)))] + (swap! form assoc-in [:errors :uri] {:message message})) + (rx/throw error))))) on-create-submit (mf/use-fn @@ -770,74 +826,63 @@ (let [data (:clean-data @form)] (if (:id data) (on-update-submit form) - (on-create-submit form)))))] + (on-create-submit form))))) - [:div.modal-overlay - [:div.modal-container.webhooks-modal + on-modal-close #(st/emit! (modal/hide))] + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} [:& fm/form {:form form :on-submit on-submit} + [:div {:class (stl/css :modal-header)} + (if webhook + [:h2 {:class (stl/css :modal-title)} (tr "modals.edit-webhook.title")] + [:h2 {:class (stl/css :modal-title)} (tr "modals.create-webhook.title")]) - [:div.modal-header - [:div.modal-header-title - (if webhook - [:h2 (tr "modals.edit-webhook.title")] - [:h2 (tr "modals.create-webhook.title")])] + [:button {:class (stl/css :modal-close-btn) + :on-click on-modal-close} i/close]] - [:div.modal-close-button - {:on-click #(st/emit! (modal/hide))} i/close]] + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :fields-row)} + [:& fm/input {:type "text" + :auto-focus? true + :form form + :name :uri + :label (tr "modals.create-webhook.url.label") + :placeholder (tr "modals.create-webhook.url.placeholder")}]] + [:div {:class (stl/css :fields-row)} + [:div {:class (stl/css :select-title)} (tr "dashboard.webhooks.content-type")] + [:& fm/select {:options valid-webhook-mtypes + :default "application/json" + :name :mtype}]] + [:div {:class (stl/css :fields-row)} + [:& fm/input {:type "checkbox" + :class (stl/css :custom-input-checkbox) + :form form + :name :is-active + :label (tr "dashboard.webhooks.active")}] + [:div {:class (stl/css :hint)} (tr "dashboard.webhooks.active.explain")]]] - [:div.modal-content.generic-form - [:div.fields-container - [:div.fields-row - [:& fm/input {:type "text" - :auto-focus? true - :form form - :name :uri - :label (tr "modals.create-webhook.url.label") - :placeholder (tr "modals.create-webhook.url.placeholder")}]] - - [:div.fields-row - [:& fm/select {:options valid-webhook-mtypes - :label (tr "dashboard.webhooks.content-type") - :default "application/json" - :name :mtype}]]] - [:div.fields-row - [:div.input-checkbox.check-primary - [:& fm/input {:type "checkbox" - :form form - :name :is-active - :label (tr "dashboard.webhooks.active")}]] - [:div.explain (tr "dashboard.webhooks.active.explain")]]] - - [:div.modal-footer - [:div.action-buttons - [:input.cancel-button - {:type "button" - :value (tr "labels.cancel") - :on-click #(modal/hide!)}] - [:& fm/submit-button + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:input {:class (stl/css :cancel-button) + :type "button" + :value (tr "labels.cancel") + :on-click #(modal/hide!)}] + [:> fm/submit-button* {:label (if webhook (tr "modals.edit-webhook.submit-label") (tr "modals.create-webhook.submit-label"))}]]]]]])) - (mf/defc webhooks-hero {::mf/wrap-props false} [] - [:div.banner - [:div.title (tr "labels.webhooks") - [:div.description (tr "dashboard.webhooks.description")]] - [:div.create-container - [:div.create (tr "dashboard.webhooks.create")]]] - - [:div.webhooks-hero-container - [:div.webhooks-hero - [:div.desc - [:h2 (tr "labels.webhooks")] - [:& i18n/tr-html {:label "dashboard.webhooks.description"}]] - - [:div.btn-primary - {:on-click #(st/emit! (modal/show :webhook {}))} - [:span (tr "dashboard.webhooks.create")]]]]) + [:div {:class (stl/css :webhooks-hero-container)} + [:h2 {:class (stl/css :hero-title)} + (tr "labels.webhooks")] + [:& i18n/tr-html {:class (stl/css :hero-desc) + :label "dashboard.webhooks.description"}] + [:button {:class (stl/css :hero-btn) + :on-click #(st/emit! (modal/show :webhook {}))} + (tr "dashboard.webhooks.create")]]) (mf/defc webhook-actions {::mf/wrap-props false} @@ -846,23 +891,26 @@ on-show (mf/use-fn #(reset! show? true)) on-hide (mf/use-fn #(reset! show? false))] + [:* - [:span.icon {:on-click on-show} [i/actions]] + [:button {:class (stl/css :menu-btn) + :on-click on-show} + menu-icon] [:& dropdown {:show @show? :on-close on-hide} - [:ul.dropdown.actions-dropdown - [:li {:on-click on-edit} (tr "labels.edit")] - [:li {:on-click on-delete} (tr "labels.delete")]]]])) + [:ul {:class (stl/css :webhook-actions-dropdown)} + [:li {:on-click on-edit + :class (stl/css :webhook-dropdown-item)} (tr "labels.edit")] + [:li {:on-click on-delete + :class (stl/css :webhook-dropdown-item)} (tr "labels.delete")]]]])) (mf/defc last-delivery-icon {::mf/wrap-props false} [{:keys [success? text]}] - [:div.last-delivery-icon - [:div.tooltip - [:div.label text] - [:div.arrow-down]] + [:div {:class (stl/css :last-delivery-icon) + :title text} (if success? - [:span.icon.success i/msg-success] - [:span.icon.failure i/msg-warning])]) + success-icon + warning-icon)]) (mf/defc webhook-item {::mf/wrap [mf/memo]} @@ -906,19 +954,20 @@ (str/starts-with? error-code "unexpected-status") (dm/str " " (tr "errors.webhooks.unexpected-status" (extract-status error-code))))))] - [:div.table-row - [:div.table-field.last-delivery - [:div.icon-container - [:& last-delivery-icon - {:success? (nil? error-code) - :text last-delivery-text}]]] - [:div.table-field.uri + + [:div {:class (stl/css :table-row :webhook-row)} + [:div {:class (stl/css :table-field :last-delivery) + :title last-delivery-text} + (if (nil? error-code) + success-icon + warning-icon)] + [:div {:class (stl/css :table-field :uri)} [:div (dm/str (:uri webhook))]] - [:div.table-field.active + [:div {:class (stl/css :table-field :active)} [:div (if (:is-active webhook) (tr "labels.active") (tr "labels.inactive"))]] - [:div.table-field.actions + [:div {:class (stl/css :table-field :actions)} [:& webhook-actions {:on-edit on-edit :on-delete on-delete}]]])) @@ -926,10 +975,9 @@ (mf/defc webhooks-list {::mf/wrap-props false} [{:keys [webhooks]}] - [:div.dashboard-table - [:div.table-rows - (for [webhook webhooks] - [:& webhook-item {:webhook webhook :key (:id webhook)}])]]) + [:div {:class (stl/css :table-rows :webhook-table)} + (for [webhook webhooks] + [:& webhook-item {:webhook webhook :key (:id webhook)}])]) (mf/defc team-webhooks-page {::mf/wrap-props false} @@ -948,11 +996,11 @@ [:* [:& header {:team team :section :dashboard-team-webhooks}] - [:section.dashboard-container.dashboard-team-webhooks - [:div + [:section {:class (stl/css :dashboard-container :dashboard-team-webhooks)} + [:* [:& webhooks-hero] (if (empty? webhooks) - [:div.webhooks-empty + [:div {:class (stl/css :webhooks-empty)} [:div (tr "dashboard.webhooks.empty.no-webhooks")] [:div (tr "dashboard.webhooks.empty.add-one")]] [:& webhooks-list {:webhooks webhooks}])]]])) @@ -991,42 +1039,58 @@ (:name team))))) - (mf/with-effect [] - (st/emit! (dd/fetch-team-members) - (dd/fetch-team-stats))) + (mf/with-effect [team] + (let [team-id (:id team)] + (st/emit! (dd/fetch-team-members team-id) + (dd/fetch-team-stats team-id)))) [:* [:& header {:section :dashboard-team-settings :team team}] - [:section.dashboard-container.dashboard-team-settings - [:div.team-settings - [:div.horizontal-blocks - [:div.block.info-block - [:div.label (tr "dashboard.team-info")] - [:div.name (:name team)] - [:div.icon - (when can-edit? - [:span.update-overlay {:on-click on-image-click} i/image]) - [:img {:src (cfg/resolve-team-photo-url team)}] - (when can-edit? - [:& file-uploader {:accept "image/jpeg,image/png" - :multi false - :ref finput - :on-selected on-file-selected}])]] + [:section {:class (stl/css :dashboard-team-settings)} + [:div {:class (stl/css :block :info-block)} + [:div {:class (stl/css :block-label)} + (tr "dashboard.team-info")] + [:div {:class (stl/css :block-text)} + (:name team)] + [:div {:class (stl/css :team-icon)} + (when can-edit? + [:button {:class (stl/css :update-overlay) + :on-click on-image-click} + image-icon]) + [:img {:class (stl/css :team-image) + :src (cfg/resolve-team-photo-url team)}] + (when can-edit? + [:& file-uploader {:accept "image/jpeg,image/png" + :multi false + :ref finput + :on-selected on-file-selected}])]] - [:div.block.owner-block - [:div.label (tr "dashboard.team-members")] - [:div.owner - [:span.icon [:img {:src (cfg/resolve-profile-photo-url owner)}]] - [:span.text (str (:name owner) " (" (tr "labels.owner") ")")]] - [:div.summary - [:span.icon i/user] - [:span.text (tr "dashboard.num-of-members" (count members-map))]]] + [:div {:class (stl/css :block)} + [:div {:class (stl/css :block-label)} + (tr "dashboard.team-members")] + + [:div {:class (stl/css :block-content)} + [:img {:class (stl/css :owner-icon) + :src (cfg/resolve-profile-photo-url owner)}] + [:span {:class (stl/css :block-text)} + (str (:name owner) " (" (tr "labels.owner") ")")]] + + [:div {:class (stl/css :block-content)} + user-icon + [:span {:class (stl/css :block-text)} + (tr "dashboard.num-of-members" (count members-map))]]] + + [:div {:class (stl/css :block)} + [:div {:class (stl/css :block-label)} + (tr "dashboard.team-projects")] + + [:div {:class (stl/css :block-content)} + group-icon + [:span {:class (stl/css :block-text)} + (tr "labels.num-of-projects" (i18n/c (dec (:projects stats))))]] + + [:div {:class (stl/css :block-content)} + document-icon + [:span {:class (stl/css :block-text)} + (tr "labels.num-of-files" (i18n/c (:files stats)))]]]]])) - [:div.block.stats-block - [:div.label (tr "dashboard.team-projects")] - [:div.projects - [:span.icon i/folder] - [:span.text (tr "labels.num-of-projects" (i18n/c (dec (:projects stats))))]] - [:div.files - [:span.icon i/file-html] - [:span.text (tr "labels.num-of-files" (i18n/c (:files stats)))]]]]]]])) diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss new file mode 100644 index 0000000000..e79a69f63e --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -0,0 +1,544 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; +@use "common/refactor/common-dashboard"; + +// Dashboard team settings +.dashboard-team-settings { + display: grid; + grid-template-rows: auto auto 1fr; + justify-items: center; + gap: $s-24; + width: 100%; + border-top: $s-1 solid var(--panel-border-color); + overflow-y: auto; +} + +.block { + display: grid; + grid-auto-rows: min-content; + gap: $s-8; + max-width: $s-1000; + width: $s-1000; +} + +.info-block { + position: relative; + padding-top: $s-180; +} + +.block-label { + @include headlineSmallTypography; + color: var(--title-foreground-color); +} + +.block-text { + color: var(--title-foreground-color-hover); +} + +.block-content { + display: grid; + grid-template-columns: $s-32 1fr; + align-items: center; + gap: $s-12; +} + +.owner-icon { + width: $s-32; + height: $s-32; + border-radius: 50%; +} + +.user-icon, +.document-icon, +.group-icon { + @extend .button-icon; + margin: 0 auto; + stroke: var(--icon-foreground); +} + +.team-icon { + --update-button-opacity: 0; + position: absolute; + top: 0; + left: 0; + height: $s-120; + width: $s-120; + padding: $s-16; + + &:hover { + --update-button-opacity: 1; + } +} + +.team-image { + border-radius: 50%; + width: $s-120; + height: $s-120; +} + +.update-overlay { + opacity: var(--update-button-opacity); + @include buttonStyle; + @include flexCenter; + position: absolute; + top: $s-16; + left: $s-16; + height: 100%; + width: 100%; + z-index: $z-index-modal; + border-radius: $br-circle; + background-color: $da-primary; +} + +.image-icon { + @extend .button-icon; + min-width: $s-24; + min-height: $s-24; + stroke: var(--icon-foreground-hover); +} + +// TEAM MEMBERS PAGE +.dashboard-team-members { + display: grid; + justify-items: center; + width: 100%; + height: 100%; + padding-top: $s-20; + border-top: $s-1 solid var(--panel-border-color); + overflow-y: auto; + scrollbar-gutter: stable; +} + +.team-members { + display: grid; + grid-template-rows: auto 1fr; + height: fit-content; + max-width: $s-1000; + width: $s-1000; +} + +.table-header { + @include headlineSmallTypography; + display: grid; + align-items: center; + grid-template-columns: 43% 1fr $s-108 $s-12; + height: $s-40; + width: 100%; + max-width: $s-1000; + padding: 0 $s-16; + user-select: none; + color: var(--title-foreground-color); +} + +.table-rows { + display: grid; + grid-auto-rows: $s-64; + gap: $s-16; + width: 100%; + height: 100%; + max-width: $s-1000; + margin-top: $s-16; + color: var(--title-foreground-color); +} + +.table-row { + display: grid; + grid-template-columns: 43% 1fr auto; + align-items: center; + height: $s-64; + width: 100%; + padding: 0 $s-16; + border-radius: $br-8; + background-color: var(--dashboard-list-background-color); + color: var(--dashboard-list-foreground-color); +} + +.title-field-name { + width: 43%; + min-width: $s-300; +} + +.title-field-roles { + position: relative; + cursor: default; +} + +.field-name { + display: grid; + grid-template-columns: auto 1fr; + gap: $s-16; + width: 43%; + min-width: $s-300; +} + +.field-roles { + position: relative; + cursor: default; +} + +.field-actions { + position: relative; +} + +// MEMBER INFO +.member-image { + height: $s-32; + width: $s-32; + border-radius: $br-circle; +} + +.member-info { + display: grid; + grid-template-rows: 1fr 1fr; + width: 100%; +} + +.member-name, +.member-email { + @include textEllipsis; + @include bodyLargeTypography; +} + +.member-email { + @include bodySmallTypography; + color: var(--dashboard-list-text-foreground-color); +} + +.you { + color: var(--dashboard-list-text-foreground-color); + margin-left: $s-6; +} + +// ROL INFO +.rol-selector { + position: relative; + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + height: $s-32; + min-width: $s-160; + width: fit-content; + padding: $s-4 $s-8; + border-radius: $br-8; + border-color: var(--menu-background-color-hover); + background-color: var(--menu-background-color-hover); + font-size: $fs-14; +} + +.has-priv { + cursor: pointer; +} + +.rol-label { + user-select: none; +} + +.roles-dropdown { + @extend .menu-dropdown; + bottom: calc(-1 * $s-76); + width: fit-content; + min-width: $s-160; +} + +.rol-dropdown-item { + @extend .menu-item-base; +} + +// MEMBER ACTIONS +.menu-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} + +.menu-btn { + @include buttonStyle; +} + +.actions-dropdown { + @extend .menu-dropdown; + bottom: calc(-1 * $s-32); + right: 0; + left: unset; + width: fit-content; + min-width: $s-160; +} + +.action-dropdown-item { + @extend .menu-item-base; +} + +// TEAM INVITATION PAGE +.dashboard-team-invitations { + display: grid; + justify-items: center; + width: 100%; + height: 100%; + padding-top: $s-20; + border-top: $s-1 solid var(--panel-border-color); + overflow-y: auto; + scrollbar-gutter: stable; +} + +.invitations { + display: grid; + grid-template-rows: auto 1fr; + height: fit-content; + max-width: $s-1000; + width: $s-1000; +} + +.table-row-invitations { + grid-template-columns: 43% 1fr $s-108 $s-12; + align-items: center; +} + +.empty-invitations { + display: grid; + place-items: center; + align-content: center; + height: $s-156; + max-width: $s-1000; + width: 100%; + margin-top: $s-16; + border: $s-1 solid var(--panel-border-color); + border-radius: $br-8; + color: var(--dashboard-list-text-foreground-color); +} + +.title-field-status { + position: relative; + cursor: default; +} + +.field-email { + @include textEllipsis; + @include bodyLargeTypography; + display: grid; + align-items: center; +} + +.invitations-dropdown { + bottom: calc(-1 * $s-112); + right: calc(-1 * $s-20); +} + +// WEBHOOKS SECTION +.dashboard-team-webhooks { + display: grid; + grid-template-rows: auto 1fr; + justify-items: center; + gap: $s-24; + width: 100%; + height: 100%; + padding-top: $s-16; + border-top: $s-1 solid var(--panel-border-color); + overflow-y: auto; +} + +.webhooks-hero-container { + display: grid; + gap: $s-32; + max-width: $s-1000; + width: $s-1000; +} + +.webhooks-empty { + display: grid; + place-items: center; + align-content: center; + height: $s-156; + max-width: $s-1000; + width: 100%; + padding: $s-32; + border: $s-1 solid var(--panel-border-color); + border-radius: $br-8; + color: var(--dashboard-list-text-foreground-color); +} + +.webhooks-hero { + font-size: $fs-14; + display: grid; + grid-template-rows: auto 1fr auto; + gap: $s-32; + margin-top: $s-32; + margin: 0; + padding: $s-32; + padding: 0; + width: $s-468; +} + +.hero-title { + @include bigTitleTipography; + color: var(--dashboard-list-foreground-color); +} + +.hero-desc { + color: $df-secondary; + margin-bottom: 0; + font-size: $fs-16; + max-width: $s-512; +} + +.hero-btn { + @extend .button-primary; + height: $s-32; + max-width: $s-512; +} + +.webhook-table { + height: fit-content; +} + +.webhook-row { + display: grid; + align-items: center; + grid-template-columns: auto 1fr auto auto; + gap: $s-16; +} + +.actions { + position: relative; +} + +.webhook-actions-dropdown { + @extend .menu-dropdown; + right: calc(-1 * $s-16); + bottom: calc(-1 * $s-40); + width: fit-content; + min-width: $s-160; +} + +.webhook-dropdown-item { + @extend .menu-item-base; +} + +.success-icon { + @extend .button-icon; + stroke: var(--alert-icon-foreground-color-success); +} + +.warning-icon { + @extend .button-icon; + stroke: var(--alert-icon-foreground-color-warning); +} + +// INVITE MEMBERS MODAL +.modal-team-container { + @extend .modal-container-base; + @include menuShadow; + position: fixed; + top: $s-72; + right: $s-12; + left: unset; + width: $s-400; + padding: $s-32; + background-color: var(--modal-background-color); + &.hero { + top: $s-216; + right: $s-32; + } +} + +.modal-title { + @include headlineMediumTypography; + height: $s-32; + color: var(--modal-title-foreground-color); +} + +.role-select { + @include flexColumn; + row-gap: $s-8; +} + +.arrow-icon { + @extend .button-icon; + stroke: var(--icon-foreground); + transform: rotate(90deg); +} + +.role-title { + @include bodyLargeTypography; + margin: 0; + color: var(--modal-title-foreground-color); +} + +.invitation-row { + margin-top: $s-8; + margin-bottom: $s-24; +} + +.action-buttons { + display: flex; + justify-content: flex-end; +} + +.accept-btn { + @extend .modal-accept-btn; +} + +// WEBHOOKS MODAL + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + @extend .modal-container-base; +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include uppercaseTitleTipography; + color: var(--modal-title-foreground-color); +} + +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content { + @include flexColumn; + gap: $s-24; + @include bodySmallTypography; + margin-bottom: $s-24; +} + +.fields-row { + @include flexColumn; +} + +.select-title { + @include bodySmallTypography; + color: var(--modal-title-foreground-color); +} + +.custom-input-checkbox { + align-items: flex-start; +} + +.hint { + color: var(--modal-text-foreground-color); +} + +.action-buttons { + @extend .modal-action-btns; + button { + @extend .modal-accept-btn; + } + .cancel-button { + @extend .modal-cancel-btn; + } +} + +.email-input { + @extend .input-base; + height: auto; +} diff --git a/frontend/src/app/main/ui/dashboard/team_form.cljs b/frontend/src/app/main/ui/dashboard/team_form.cljs index e09501b60b..7a37ec9c6c 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.cljs +++ b/frontend/src/app/main/ui/dashboard/team_form.cljs @@ -5,17 +5,20 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.team-form + (:require-macros [app.main.style :as stl]) (:require [app.common.spec :as us] [app.main.data.dashboard :as dd] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.main.ui.icons :as i] + [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] [app.util.router :as rt] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.spec.alpha :as s] [rumext.v2 :as mf])) @@ -26,22 +29,22 @@ (defn- on-create-success [_form response] (let [msg "Team created successfully"] - (st/emit! (dm/success msg) + (st/emit! (msg/success msg) (modal/hide) (rt/nav :dashboard-projects {:team-id (:id response)})))) (defn- on-update-success [_form _response] (let [msg "Team created successfully"] - (st/emit! (dm/success msg) + (st/emit! (msg/success msg) (modal/hide)))) (defn- on-error [form _response] (let [id (get-in @form [:clean-data :id])] (if id - (rx/of (dm/error "Error on updating team.")) - (rx/of (dm/error "Error on creating team."))))) + (rx/of (msg/error "Error on updating team.")) + (rx/of (msg/error "Error on creating team."))))) (defn- on-create-submit [form] @@ -72,32 +75,50 @@ form (fm/use-form :spec ::team-form :validators [(fm/validate-not-empty :name (tr "auth.name.not-all-space")) (fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))] - :initial initial)] - [:div.modal-overlay - [:div.modal-container.team-form-modal - [:& fm/form {:form form :on-submit on-submit} + :initial initial) + handle-keydown + (mf/use-callback + (mf/deps) + (fn [e] + (when (kbd/enter? e) + (dom/prevent-default e) + (dom/stop-propagation e) + (on-submit form e)))) - [:div.modal-header - [:div.modal-header-title - (if team - [:h2 (tr "labels.rename-team")] - [:h2 (tr "labels.create-team")])] + on-close #(st/emit! (modal/hide))] - [:div.modal-close-button - {:on-click #(st/emit! (modal/hide))} i/close]] + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:& fm/form {:form form + :on-submit on-submit + :class (stl/css :team-form)} - [:div.modal-content.generic-form + [:div {:class (stl/css :modal-header)} + (if team + [:h2 {:class (stl/css :modal-title)} + (tr "labels.rename-team")] + [:h2 {:class (stl/css :modal-title)} + (tr "labels.create-team")]) + + [:button {:class (stl/css :modal-close-btn) + :on-click on-close} i/close]] + + [:div {:class (stl/css :modal-content)} [:& fm/input {:type "text" :auto-focus? true + :class (stl/css :group-name-input) :form form :name :name - :label (tr "labels.create-team.placeholder")}]] + :placeholder "E.g. Design" + :label (tr "labels.create-team.placeholder") + :on-key-down handle-keydown}]] - [:div.modal-footer - [:div.action-buttons - [:& fm/submit-button + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:> fm/submit-button* {:label (if team (tr "labels.update-team") - (tr "labels.create-team"))}]]]]]])) + (tr "labels.create-team")) + :class (stl/css :accept-btn)}]]]]]])) diff --git a/frontend/src/app/main/ui/dashboard/team_form.scss b/frontend/src/app/main/ui/dashboard/team_form.scss new file mode 100644 index 0000000000..d94cb4c285 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/team_form.scss @@ -0,0 +1,68 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + @extend .modal-container-base; +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include uppercaseTitleTipography; + color: var(--modal-title-foreground-color); +} + +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content { + margin-bottom: $s-24; +} + +.team-form { + min-width: $s-400; +} + +.group-name-input { + @extend .input-element-label; + margin-bottom: $s-8; + label { + @include flexColumn; + @include bodySmallTypography; + align-items: flex-start; + width: 100%; + border: none; + background-color: transparent; + height: 100%; + + input { + @include bodySmallTypography; + } + } +} + +.action-buttons { + @extend .modal-action-btns; +} + +.cancel-button { + @extend .modal-cancel-btn; +} +.accept-btn { + @extend .modal-accept-btn; + &.danger { + @extend .modal-danger-btn; + } +} diff --git a/frontend/src/app/main/ui/dashboard/templates.cljs b/frontend/src/app/main/ui/dashboard/templates.cljs index fcc05a9f7c..f7a42454fc 100644 --- a/frontend/src/app/main/ui/dashboard/templates.cljs +++ b/frontend/src/app/main/ui/dashboard/templates.cljs @@ -5,9 +5,9 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.dashboard.templates + (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] - [app.common.math :as mth] [app.config :as cf] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] @@ -21,9 +21,15 @@ [app.util.keyboard :as kbd] [app.util.router :as rt] [okulary.core :as l] - [potok.core :as ptk] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) +(def ^:private arrow-icon + (i/icon-xref :arrow (stl/css :arrow-icon))) + +(def ^:private download-icon + (i/icon-xref :download (stl/css :download-icon))) + (def builtin-templates (l/derived :builtin-templates st/state)) @@ -73,12 +79,18 @@ (dom/prevent-default event) (on-click event))))] - [:div.title + [:div {:class (stl/css :title)} [:button {:tab-index "0" + :class (stl/css :title-btn) :on-click on-click :on-key-down on-key-down} - [:span (tr "dashboard.libraries-and-templates")] - [:span.icon (if ^boolean collapsed i/arrow-up i/arrow-down)]]])) + [:span {:class (stl/css :title-text)} + (tr "dashboard.libraries-and-templates")] + (if ^boolean collapsed + [:span {:class (stl/css :title-icon :title-icon-collapsed)} + arrow-icon] + [:span {:class (stl/css :title-icon)} + arrow-icon])]])) (mf/defc card-item {::mf/wrap-props false} @@ -100,18 +112,22 @@ (dom/stop-propagation event) (on-import item event))))] - [:a.card-container - {:tab-index (if (or (not is-visible) collapsed) "-1" "0") - :id id - :data-index index - :on-click on-click - :on-key-down on-key-down} - [:div.template-card - [:div.img-container + [:a {:class (stl/css :card-container) + :tab-index (if (or (not is-visible) collapsed) "-1" "0") + :id id + :data-index index + :on-click on-click + :on-mouse-down dom/prevent-default + :on-key-down on-key-down} + [:div {:class (stl/css :template-card)} + [:div {:class (stl/css :img-container)} [:img {:src (dm/str thb) - :alt (:name item)}]] - [:div.card-name [:span (:name item)] - [:span.icon i/download]]]])) + :alt (:name item) + :loading "lazy" + :decoding "async"}]] + [:div {:class (stl/css :card-name)} + [:span {:class (stl/css :card-text)} (:name item)] + download-icon]]])) (mf/defc card-item-link {::mf/wrap-props false} @@ -134,22 +150,22 @@ (dom/stop-propagation event) (on-click event))))] - [:div.card-container - [:div.template-card - [:div.img-container + [:div {:class (stl/css :card-container)} + [:div {:class (stl/css :template-card)} + [:div {:class (stl/css :img-container)} [:a {:id id :tab-index (if (or (not is-visible) collapsed) "-1" "0") :href "https://penpot.app/libraries-templates.html" :target "_blank" :on-click on-click :on-key-down on-key-down} - [:div.template-link - [:div.template-link-title (tr "dashboard.libraries-and-templates")] - [:div.template-link-text (tr "dashboard.libraries-and-templates.explore")]]]]]])) + [:div {:class (stl/css :template-link)} + [:div {:class (stl/css :template-link-title)} (tr "dashboard.libraries-and-templates")] + [:div {:class (stl/css :template-link-text)} (tr "dashboard.libraries-and-templates.explore")]]]]]])) (mf/defc templates-section {::mf/wrap-props false} - [{:keys [default-project-id profile project-id team-id content-width]}] + [{:keys [default-project-id profile project-id team-id]}] (let [templates (mf/deref builtin-templates) templates (mf/with-memo [templates] (filterv #(not= (:id %) "tutorial-for-beginners") templates)) @@ -164,83 +180,65 @@ props (:props profile) collapsed (:builtin-templates-collapsed-status props false) - card-offset* (mf/use-state 0) - card-offset (deref card-offset*) + can-move (mf/use-state {:left false :right true}) - card-width 275 total (count templates) - container-size (* (+ 2 total) card-width) ;; We need space for total plus the libraries&templates link - more-cards (> (+ card-offset (* (+ 1 total) card-width)) content-width) - card-count (mth/floor (/ content-width 275)) - left-moves (/ card-offset -275) - first-card left-moves - last-card (+ (- card-count 1) left-moves) content-ref (mf/use-ref) - on-move-left + move-left (fn [] (dom/scroll-by! (mf/ref-val content-ref) -300 0)) + move-right (fn [] (dom/scroll-by! (mf/ref-val content-ref) 300 0)) + + update-can-move + (fn [scroll-left scroll-available client-width] + (reset! can-move {:left (> scroll-left 0) + :right (> scroll-available client-width)})) + + on-scroll (mf/use-fn - (mf/deps card-offset card-width) - (fn [_event] - (when-not (zero? card-offset) - (dom/animate! (mf/ref-val content-ref) - [#js {:left (dm/str card-offset "px")} - #js {:left (dm/str (+ card-offset card-width) "px")}] - #js {:duration 200 :easing "linear"}) - (reset! card-offset* (+ card-offset card-width))))) + (fn [e] + (let [scroll (dom/get-target-scroll e) + scroll-left (:scroll-left scroll) + scroll-available (- (:scroll-width scroll) scroll-left) + client-rect (dom/get-client-size (dom/get-target e))] + (update-can-move scroll-left scroll-available (unchecked-get client-rect "width"))))) + + on-move-left + (mf/use-fn #(move-left)) on-move-left-key-down - (mf/use-fn - (mf/deps on-move-left first-card) - (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (on-move-left event) - (when-let [node (dom/get-element (dm/str "card-container-" first-card))] - (dom/focus! node))))) + (mf/use-fn #(move-left)) on-move-right - (mf/use-fn - (mf/deps more-cards card-offset card-width) - (fn [_event] - (when more-cards - (swap! card-offset* inc) - (dom/animate! (mf/ref-val content-ref) - [#js {:left (dm/str card-offset "px")} - #js {:left (dm/str (- card-offset card-width) "px")}] - #js {:duration 200 :easing "linear"}) - (reset! card-offset* (- card-offset card-width))))) + (mf/use-fn #(move-right)) on-move-right-key-down - (mf/use-fn - (mf/deps on-move-right last-card) - (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (on-move-right event) - (when-let [node (dom/get-element (dm/str "card-container-" last-card))] - (dom/focus! node))))) + (mf/use-fn #(move-right)) on-import-template (mf/use-fn (mf/deps default-project-id project-id section templates team-id) (fn [template _event] - (import-template! template team-id project-id default-project-id section))) + (import-template! template team-id project-id default-project-id section)))] - ] + (mf/with-effect [content-ref templates] + (let [content (mf/ref-val content-ref)] + (when (and (some? content) (some? templates)) + (dom/scroll-to content #js {:behavior "instant" :left 0 :top 0}) + (.dispatchEvent content (js/Event. "scroll"))))) (mf/with-effect [profile collapsed] (when (and profile (not collapsed)) (st/emit! (dd/fetch-builtin-templates)))) - [:div.dashboard-templates-section - {:class (when ^boolean collapsed "collapsed")} + [:div {:class (stl/css-case :dashboard-templates-section true + :collapsed collapsed)} [:& title {:collapsed collapsed}] - [:div.content {:ref content-ref - :style {:left card-offset - :width (dm/str container-size "px")}} + [:div {:class (stl/css :content) + :on-scroll on-scroll + :ref content-ref} (for [index (range (count templates))] [:& card-item @@ -248,28 +246,26 @@ :item (nth templates index) :index index :key index - :is-visible (and (>= index first-card) - (<= index last-card)) + :is-visible true :collapsed collapsed}]) [:& card-item-link - {:is-visible (and (>= total first-card) (<= total last-card)) + {:is-visible true :collapsed collapsed :section section :total total}]] - (when (< card-offset 0) - [:button.button.left - {:tab-index (if ^boolean collapsed "-1" "0") - :on-click on-move-left - :on-key-down on-move-left-key-down} - i/go-prev]) - - (when more-cards - [:button.button.right - {:tab-index (if collapsed "-1" "0") - :on-click on-move-right - :aria-label (tr "labels.next") - :on-key-down on-move-right-key-down} - i/go-next])])) + (when (:left @can-move) + [:button {:class (stl/css :move-button :move-left) + :tab-index (if ^boolean collapsed "-1" "0") + :on-click on-move-left + :on-key-down on-move-left-key-down} + arrow-icon]) + (when (:right @can-move) + [:button {:class (stl/css :move-button :move-right) + :tab-index (if collapsed "-1" "0") + :on-click on-move-right + :aria-label (tr "labels.next") + :on-key-down on-move-right-key-down} + arrow-icon])])) diff --git a/frontend/src/app/main/ui/dashboard/templates.scss b/frontend/src/app/main/ui/dashboard/templates.scss new file mode 100644 index 0000000000..76cf2f4559 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/templates.scss @@ -0,0 +1,199 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.dashboard-templates-section { + position: absolute; + display: flex; + flex-direction: column; + justify-content: flex-end; + bottom: 0; + width: 100%; + height: $s-228; + transition: bottom 300ms; + pointer-events: none; + &.collapsed { + bottom: calc(-1 * $s-228); + transition: bottom 300ms; + } +} + +.title { + pointer-events: all; + width: fit-content; + top: calc(-1 * $s-56); + text-align: right; + height: $s-56; + position: absolute; + right: calc(-1 * $s-24); +} + +.title-btn { + border: none; + cursor: pointer; + height: $s-56; + display: inline-flex; + align-items: center; + border-top-left-radius: $br-10; + border-top-right-radius: $br-10; + margin-right: $s-32; + position: relative; + z-index: $z-index-1; + background-color: $db-quaternary; +} + +.title-text { + display: inline-block; + vertical-align: middle; + line-height: 1.2; + font-size: $fs-16; + margin-left: $s-16; + margin-right: $s-8; + color: $df-primary; + font-weight: $fw400; +} + +.title-icon { + display: inline-block; + vertical-align: middle; + margin-left: $s-16; + margin-right: $s-8; + color: $df-primary; + margin-left: $s-16; + margin-right: $s-16; + transform: rotate(90deg); +} + +.title-icon-collapsed { + transform: rotate(-90deg); +} + +.arrow-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} + +.move-button { + position: absolute; + top: $s-136; + border: $s-2 solid $df-secondary; + border-radius: 50%; + text-align: center; + width: $s-36; + height: $s-36; + cursor: pointer; + background-color: $df-primary; + display: flex; + align-items: center; + justify-content: center; + pointer-events: all; + + &:hover { + border: $s-2 solid $da-tertiary; + } +} + +.move-left { + left: 0; + margin-left: $s-44; + transform: rotate(180deg); +} + +.move-right { + right: 0; + margin-right: $s-44; +} + +.content { + display: grid; + grid-template-columns: repeat(auto-fill, minmax($s-276, $s-276)); + grid-auto-flow: column; + pointer-events: all; + height: $s-228; + margin-left: $s-6; + border-top-left-radius: $s-8; + background-color: $db-quaternary; + overflow: scroll hidden; + scroll-behavior: smooth; + scroll-snap-type: x mandatory; + scroll-snap-stop: always; +} + +.card-container { + width: $s-276; + margin-top: $s-20; + text-align: center; + vertical-align: top; + background-color: transparent; + border: none; + padding: 0; + scroll-snap-align: start; +} + +.template-card { + display: inline-block; + width: $s-256; + font-size: $fs-16; + cursor: pointer; + color: $df-primary; + padding: $s-3 $s-6 $s-16 $s-6; + border-radius: $br-8; + + &:hover { + background-color: $db-tertiary; + } +} + +.img-container { + width: 100%; + height: $s-136; + margin-bottom: $s-16; + border-radius: $br-5; + display: flex; + justify-content: center; + flex-direction: column; + + img { + border-radius: $br-4; + } +} + +.card-name { + padding: 0 $s-6; + display: flex; + justify-content: space-between; + height: $s-24; + align-items: center; +} + +.card-text { + font-weight: $fw500; + font-size: $fs-16; +} + +.download-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} + +.template-link { + border: $s-2 solid transparent; + margin: $s-32; + padding: $s-32 0; +} + +.template-link-title { + font-size: $fs-14; + color: $df-primary; + font-weight: $fw400; +} + +.template-link-text { + font-size: $fs-12; + margin-top: $s-8; + color: $df-secondary; +} diff --git a/frontend/src/app/main/ui/debug/components_preview.cljs b/frontend/src/app/main/ui/debug/components_preview.cljs index 2b950df042..9fd0788b77 100644 --- a/frontend/src/app/main/ui/debug/components_preview.cljs +++ b/frontend/src/app/main/ui/debug/components_preview.cljs @@ -5,13 +5,28 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.debug.components-preview - (:require-macros [app.main.style :refer [css styles]]) - (:require [app.common.data :as d] - [app.main.data.users :as du] - [app.main.refs :as refs] - [app.main.store :as st] - [app.util.dom :as dom] - [rumext.v2 :as mf])) + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.main.data.users :as du] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.components.search-bar :refer [search-bar]] + [app.main.ui.components.tab-container :refer [tab-container tab-element]] + [app.main.ui.components.title-bar :refer [title-bar]] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [rumext.v2 :as mf])) + +(mf/defc component-wrapper + {::mf/wrap-props false} + [props] + (let [children (unchecked-get props "children") + title (unchecked-get props "title")] + [:div {:class (stl/css :component)} + [:h4 {:class (stl/css :component-name)} title] + children])) (mf/defc components-preview {::mf/wrap-props false} @@ -24,20 +39,46 @@ (let [theme (dom/event->value event) data (assoc initial :theme theme)] (st/emit! (du/update-profile data)))) + colors ["var(--color-background-primary)" + "var(--color-background-secondary)" + "var(--color-background-tertiary)" + "var(--color-background-quaternary)" + "var(--color-foreground-primary)" + "var(--color-foreground-secondary)" + "var(--color-accent-primary)" + "var(--color-accent-primary-muted)" + "var(--color-accent-secondary)" + "var(--color-accent-tertiary)"] - colors [:bg-primary - :bg-secondary - :bg-tertiary - :bg-cuaternary - :fg-primary - :fg-secondary - :acc - :acc-muted - :acc-secondary - :acc-tertiary]] + ;; COMPONENTS FNs + state* (mf/use-state {:collapsed? true + :tab-selected :first + :input-value "" + :radio-selected "first"}) + state (deref state*) + + collapsed? (:collapsed? state) + toggle-collapsed + (mf/use-fn #(swap! state* update :collapsed? not)) + + tab-selected (:tab-selected state) + set-tab (mf/use-fn #(swap! state* assoc :tab-selected %)) + + input-value (:input-value state) + radio-selected (:radio-selected state) + + set-radio-selected (mf/use-fn #(swap! state* assoc :radio-selected %)) + + update-search + (mf/use-fn + (fn [value _event] + (swap! state* assoc :input-value value))) + + + on-btn-click (mf/use-fn #(prn "eyy"))] [:section.debug-components-preview - [:div {:class (css :themes-row)} + [:div {:class (stl/css :themes-row)} [:h2 "Themes"] [:select {:label "Select theme color" :name :theme @@ -46,12 +87,184 @@ :on-change on-change} [:option {:label "Penpot Dark (default)" :value "default"}] [:option {:label "Penpot Light" :value "light"}]] - [:div {:class (css :wrapper)} - (let [css (styles)] - (for [color colors] - [:div {:class (dom/classnames (get css color) true - (get css :rect) true)} - (d/name color)]))]] - [:div {:class (css :components-row)} - [:h2 {:class (css :title)} "Components"] - [:div {:class (css :component-wrapper)}]]])) \ No newline at end of file + [:div {:class (stl/css :wrapper)} + (for [color colors] + [:div {:class (stl/css :color-wrapper)} + [:span (d/name color)] + [:div {:key color + :style {:background color} + :class (stl/css :rect)}]])]] + + [:div {:class (stl/css :components-row)} + [:h2 {:class (stl/css :title)} "Components"] + [:div {:class (stl/css :components-wrapper)} + [:div {:class (stl/css :components-group)} + [:h3 "Titles"] + [:& component-wrapper + {:title "Title"} + [:& title-bar {:collapsable false + :title "Title"}]] + [:& component-wrapper + {:title "Title and action button"} + [:& title-bar {:collapsable false + :title "Title" + :on-btn-click on-btn-click + :btn-children i/add}]] + [:& component-wrapper + {:title "Collapsed title and action button"} + [:& title-bar {:collapsable true + :collapsed collapsed? + :on-collapsed toggle-collapsed + :title "Title" + :on-btn-click on-btn-click + :btn-children i/add}]] + [:& component-wrapper + {:title "Collapsed title and children"} + [:& title-bar {:collapsable true + :collapsed collapsed? + :on-collapsed toggle-collapsed + :title "Title"} + [:& tab-container {:on-change-tab set-tab + :selected tab-selected} + [:& tab-element {:id :first + :title "A tab"}] + [:& tab-element {:id :second + :title "B tab"}]]]]] + + [:div {:class (stl/css :components-group)} + [:h3 "Tabs component"] + [:& component-wrapper + {:title "2 tab component"} + [:& tab-container {:on-change-tab set-tab + :selected tab-selected} + [:& tab-element {:id :first :title "First tab"} + [:div "This is first tab content"]] + + [:& tab-element {:id :second :title "Second tab"} + [:div "This is second tab content"]]]] + [:& component-wrapper + {:title "3 tab component"} + [:& tab-container {:on-change-tab set-tab + :selected tab-selected} + [:& tab-element {:id :first :title "First tab"} + [:div "This is first tab content"]] + + [:& tab-element {:id :second + :title "Second tab"} + [:div "This is second tab content"]] + [:& tab-element {:id :third + :title "Third tab"} + [:div "This is third tab content"]]]]] + + [:div {:class (stl/css :components-group)} + [:h3 "Search bar"] + [:& component-wrapper + {:title "Search bar only"} + [:& search-bar {:on-change update-search + :value input-value + :placeholder "Test value"}]] + [:& component-wrapper + {:title "Search and button"} + [:& search-bar {:on-change update-search + :value input-value + :placeholder "Test value"} + [:button {:class (stl/css :button-secondary) + :on-click on-btn-click} + "X"]]]] + + [:div {:class (stl/css :components-group)} + [:h3 "Radio buttons"] + [:& component-wrapper + {:title "Two radio buttons (toggle)"} + [:& radio-buttons {:selected radio-selected + :on-change set-radio-selected + :name "listing-style"} + [:& radio-button {:icon i/view-as-list + :value "first" + :id :list}] + [:& radio-button {:icon i/flex-grid + :value "second" + :id :grid}]]] + [:& component-wrapper + {:title "Three radio buttons"} + [:& radio-buttons {:selected radio-selected + :on-change set-radio-selected + :name "listing-style"} + [:& radio-button {:icon i/view-as-list + :value "first" + :id :first}] + [:& radio-button {:icon i/flex-grid + :value "second" + :id :second}] + + [:& radio-button {:icon i/add + :value "third" + :id :third}]]] + + [:& component-wrapper + {:title "Four radio buttons"} + [:& radio-buttons {:selected radio-selected + :on-change set-radio-selected + :name "listing-style"} + [:& radio-button {:icon i/view-as-list + :value "first" + :id :first}] + [:& radio-button {:icon i/flex-grid + :value "second" + :id :second}] + + [:& radio-button {:icon i/add + :value "third" + :id :third}] + + [:& radio-button {:icon i/board + :value "forth" + :id :forth}]]]] + [:div {:class (stl/css :components-group)} + [:h3 "Buttons"] + [:& component-wrapper + {:title "Button primary"} + [:button {:class (stl/css :button-primary)} + "Primary"]] + [:& component-wrapper + {:title "Button primary with icon"} + [:button {:class (stl/css :button-primary)} + i/add]] + + [:& component-wrapper + {:title "Button secondary"} + [:button {:class (stl/css :button-secondary)} + "secondary"]] + [:& component-wrapper + {:title "Button secondary with icon"} + [:button {:class (stl/css :button-secondary)} + i/add]] + + [:& component-wrapper + {:title "Button tertiary"} + [:button {:class (stl/css :button-tertiary)} + "tertiary"]] + [:& component-wrapper + {:title "Button tertiary with icon"} + [:button {:class (stl/css :button-tertiary)} + i/add]]] + [:div {:class (stl/css :components-group)} + [:h3 "Inputs"] + [:& component-wrapper + {:title "Only input"} + [:div {:class (stl/css :input-wrapper)} + [:input {:class (stl/css :basic-input) + :placeholder "----"}]]] + [:& component-wrapper + {:title "Input with label"} + [:div {:class (stl/css :input-wrapper)} + [:span {:class (stl/css :input-label)} "label"] + [:input {:class (stl/css :basic-input) + :placeholder "----"}]]] + [:& component-wrapper + {:title "Input with icon"} + [:div {:class (stl/css :input-wrapper)} + [:span {:class (stl/css :input-label)} + i/add] + [:input {:class (stl/css :basic-input) + :placeholder "----"}]]]]]]])) diff --git a/frontend/src/app/main/ui/debug/components_preview.css.json b/frontend/src/app/main/ui/debug/components_preview.css.json deleted file mode 100644 index 9b9b623867..0000000000 --- a/frontend/src/app/main/ui/debug/components_preview.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"debug_components_preview_button-primary_Q2m40","button-secondary":"debug_components_preview_button-secondary_yPp3n","button-icon":"debug_components_preview_button-icon_J36A6","button-icon-small":"debug_components_preview_button-icon-small_Pf3jb","themes-row":"debug_components_preview_themes-row_wEU8d","wrapper":"debug_components_preview_wrapper_535-4","rect":"debug_components_preview_rect_jomnq","bg-primary":"debug_components_preview_bg-primary_Rt4oW","bg-secondary":"debug_components_preview_bg-secondary_rcmll","bg-tertiary":"debug_components_preview_bg-tertiary_7rITE","bg-cuaternary":"debug_components_preview_bg-cuaternary_UEBPN","fg-primary":"debug_components_preview_fg-primary_naliT","fg-secondary":"debug_components_preview_fg-secondary_zT9IX","acc":"debug_components_preview_acc_h3Bia","acc-muted":"debug_components_preview_acc-muted_uingh","acc-secondary":"debug_components_preview_acc-secondary_oHH6y","acc-tertiary":"debug_components_preview_acc-tertiary_SwBjy","components-row":"debug_components_preview_components-row_N3f-J","title":"debug_components_preview_title_TVtzz","component-wrapper":"debug_components_preview_component-wrapper_yC9G1"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/debug/components_preview.scss b/frontend/src/app/main/ui/debug/components_preview.scss index 4d1cf79e33..eb1d83acd1 100644 --- a/frontend/src/app/main/ui/debug/components_preview.scss +++ b/frontend/src/app/main/ui/debug/components_preview.scss @@ -12,64 +12,27 @@ color: var(--color-foreground-primary); background: var(--color-background-secondary); .wrapper { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: $s-40; background-color: var(--color-background-primary); width: 100%; padding: $s-20; - display: flex; - justify-content: center; - gap: $s-20; - flex-wrap: wrap; .rect { display: flex; justify-content: center; align-items: center; - border: 1px solid var(--color-foreground-primary); + border: $s-1 solid var(--color-foreground-primary); padding: $s-20; height: $s-96; min-width: $s-152; } - .bg-primary { - background: var(--color-background-primary); - color: var(--color-foreground-primary); - } - .bg-secondary { - background: var(--color-background-secondary); - color: var(--color-foreground-primary); - } - .bg-tertiary { - background: var(--color-background-tertiary); - color: var(--color-foreground-primary); - } - .bg-cuaternary { - background: var(--color-background-quaternary); - color: var(--color-foreground-primary); - } - .fg-primary { - background: var(--color-foreground-primary); - color: var(--color-background-primary); - } - .fg-secondary { - background: var(--color-foreground-secondary); - color: var(--color-background-primary); - } - .acc { - background: var(--color-accent-primary); - color: var(--color-background-primary); - } - .acc-muted { - background: var(--color-accent-primary-muted); - color: var(--color-foreground-primary); - } - .acc-secondary { - background: var(--color-accent-secondary); - color: var(--color-background-primary); - } - .acc-tertiary { - background: var(--color-accent-tertiary); - color: var(--color-background-primary); - } } } +.color-wrapper { + display: grid; + grid-template-rows: auto $s-96; +} .components-row { color: var(--color-foreground-primary); @@ -79,7 +42,57 @@ .title { padding: $s-20; } - .component-wrapper { + .components-wrapper { padding: $s-20; + display: flex; + flex-wrap: wrap; + gap: $s-20; + .components-group { + @include flexCenter; + justify-content: flex-start; + flex-direction: column; + border-radius: $s-8; + h3 { + @include bodySmallTypography; + font-size: $fs-24; + width: 100%; + } + .component { + display: flex; + flex-direction: column; + gap: $s-8; + width: $s-240; + max-height: $s-80; + margin-bottom: $s-16; + .component-name { + @include uppercaseTitleTipography; + font-weight: bold; + } + } + } + .button-primary { + @extend .button-primary; + height: $s-32; + svg { + @extend .button-icon; + } + } + .button-secondary { + @extend .button-secondary; + height: $s-32; + svg { + @extend .button-icon; + } + } + .button-tertiary { + @extend .button-tertiary; + height: $s-32; + svg { + @extend .button-icon; + } + } + .input-wrapper { + @extend .input-element; + } } } diff --git a/frontend/src/app/main/ui/delete_shared.cljs b/frontend/src/app/main/ui/delete_shared.cljs index 7d47a89548..ec21009371 100644 --- a/frontend/src/app/main/ui/delete_shared.cljs +++ b/frontend/src/app/main/ui/delete_shared.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.delete-shared + (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] [app.main.data.modal :as modal] @@ -14,7 +15,7 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as k] - [beicon.core :as rx] + [beicon.v2.core :as rx] [goog.events :as events] [rumext.v2 :as mf])) @@ -25,7 +26,7 @@ ::mf/register-as :delete-shared-libraries ::mf/wrap-props false} [{:keys [ids on-accept on-cancel accept-style origin count-libraries]}] - (let [references* (mf/use-state {}) + (let [references* (mf/use-state nil) references (deref references*) on-accept (or on-accept noop) @@ -34,39 +35,28 @@ cancel-label (tr "labels.cancel") accept-style (or accept-style :danger) - is-delete? (= origin :delete) count-files (count (keys references)) - title (if ^boolean is-delete? - (tr "modals.delete-shared-confirm.title" (i18n/c count-libraries)) - (tr "modals.unpublish-shared-confirm.title" (i18n/c count-libraries))) + title (case origin + :delete (tr "modals.delete-shared-confirm.title" (i18n/c count-libraries)) + :unpublish (tr "modals.unpublish-shared-confirm.title" (i18n/c count-libraries)) + :move (tr "modals.move-shared-confirm.title" (i18n/c count-libraries))) - subtitle (if ^boolean is-delete? - (tr "modals.delete-shared-confirm.message" (i18n/c count-libraries)) - (tr "modals.unpublish-shared-confirm.message" (i18n/c count-libraries))) + subtitle (case origin + :delete (tr "modals.delete-shared-confirm.message" (i18n/c count-libraries)) + :unpublish (tr "modals.unpublish-shared-confirm.message" (i18n/c count-libraries)) + :move (tr "modals.move-shared-confirm.message" (i18n/c count-libraries))) - accept-label (if ^boolean is-delete? - (tr "modals.delete-shared-confirm.accept" (i18n/c count-libraries)) - (tr "modals.unpublish-shared-confirm.accept" (i18n/c count-libraries))) + accept-label (case origin + :delete (tr "modals.delete-shared-confirm.accept" (i18n/c count-libraries)) + :unpublish (tr "modals.unpublish-shared-confirm.accept" (i18n/c count-libraries)) + :move (tr "modals.move-shared-confirm.accept" (i18n/c count-libraries))) - no-files-msg (if ^boolean is-delete? - (tr "modals.delete-shared-confirm.no-files-message" (i18n/c count-libraries)) - (tr "modals.unpublish-shared-confirm.no-files-message" (i18n/c count-libraries))) + no-files-msg (tr "modals.delete-shared-confirm.activated.no-files-message" (i18n/c count-libraries)) - scd-msg (if ^boolean is-delete? - (if (= count-files 1) - (tr "modals.delete-shared-confirm.scd-message" (i18n/c count-libraries)) - (tr "modals.delete-shared-confirm.scd-message-many" (i18n/c count-libraries))) - (if (= count-files 1) - (tr "modals.unpublish-shared-confirm.scd-message" (i18n/c count-libraries)) - (tr "modals.unpublish-shared-confirm.scd-message-many" (i18n/c count-libraries)))) - hint (if ^boolean is-delete? - (if (= count-files 1) - (tr "modals.delete-shared-confirm.hint" (i18n/c count-libraries)) - (tr "modals.delete-shared-confirm.hint-many" (i18n/c count-libraries))) - (if (= count-files 1) - (tr "modals.unpublish-shared-confirm.hint" (i18n/c count-libraries)) - (tr "modals.unpublish-shared-confirm.hint-many" (i18n/c count-libraries)))) + scd-msg (tr "modals.delete-shared-confirm.activated.scd-message" (i18n/c count-libraries)) + + hint (tr "modals.delete-unpublish-shared-confirm.activated.hint" (i18n/c count-files)) accept-fn (mf/use-fn @@ -86,12 +76,12 @@ (mf/with-effect [ids] (->> (rx/from ids) - (rx/map #(array-map :file-id %)) - (rx/mapcat #(rp/cmd! :get-library-file-references %)) + (rx/filter some?) + (rx/mapcat #(rp/cmd! :get-library-file-references {:file-id %})) (rx/mapcat identity) (rx/map (juxt :id :name)) (rx/reduce conj []) - (rx/subs #(reset! references* %)))) + (rx/subs! #(reset! references* %)))) (mf/with-effect [accept-fn] (letfn [(on-keydown [event] @@ -102,45 +92,43 @@ (let [key (events/listen js/document "keydown" on-keydown)] (partial events/unlistenByKey key)))) - [:div.modal-overlay - [:div.modal-container.confirm-dialog - [:div.modal-header - [:div.modal-header-title - [:h2 title]] - [:div.modal-close-button - {:on-click cancel-fn} i/close]] + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} title] + [:button {:class (stl/css :modal-close-btn) + :on-click cancel-fn} i/close]] - [:div.modal-content.delete-shared + [:div {:class (stl/css :modal-content)} (when (and (string? subtitle) (not= subtitle "")) - [:h3 subtitle]) + [:h3 {:class (stl/css :modal-subtitle)} subtitle]) (when (not= 0 count-libraries) (if (pos? (count references)) [:* [:div (when (and (string? scd-msg) (not= scd-msg "")) - [:h3 scd-msg]) - [:ul.file-list + [:h3 {:class (stl/css :modal-scd-msg)} scd-msg]) + [:ul {:class (stl/css :element-list)} (for [[file-id file-name] references] - [:li.modal-item-element - {:key (dm/str file-id)} + [:li {:class (stl/css :list-item) + :key (dm/str file-id)} [:span "- " file-name]])]] (when (and (string? hint) (not= hint "")) - [:h3 hint])] + [:h3 {:class (stl/css :modal-hint)} hint])] [:* - [:h3 no-files-msg]]))] + [:h3 {:class (stl/css :modal-msg)} no-files-msg]]))] - [:div.modal-footer - [:div.action-buttons + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} (when-not (= cancel-label :omit) - [:input.cancel-button - {:type "button" - :value cancel-label - :on-click cancel-fn}]) + [:input {:class (stl/css :cancel-button) + :type "button" + :value cancel-label + :on-click cancel-fn}]) - [:input.accept-button - {:class (dom/classnames - :danger (= accept-style :danger) - :primary (= accept-style :primary)) - :type "button" - :value accept-label - :on-click accept-fn}]]]]])) + [:input {:class (stl/css-case :accept-btn true + :danger (= accept-style :danger) + :primary (= accept-style :primary)) + :type "button" + :value accept-label + :on-click accept-fn}]]]]])) diff --git a/frontend/src/app/main/ui/delete_shared.scss b/frontend/src/app/main/ui/delete_shared.scss new file mode 100644 index 0000000000..dfd7741f33 --- /dev/null +++ b/frontend/src/app/main/ui/delete_shared.scss @@ -0,0 +1,68 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; + &.transparent { + background-color: transparent; + } +} + +.modal-container { + @extend .modal-container-base; +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include headlineMediumTypography; + color: var(--modal-title-foreground-color); +} + +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content { + @include bodySmallTypography; + margin-bottom: $s-24; +} + +.modal-hint { + @extend .modal-hint-base; +} + +.element-list { + @include bodyLargeTypography; + color: var(--modal-text-foreground-color); +} + +.action-buttons { + @extend .modal-action-btns; +} + +.cancel-button { + @extend .modal-cancel-btn; +} + +.accept-btn { + @extend .modal-accept-btn; + &.danger { + @extend .modal-danger-btn; + } +} + +.modal-scd-msg, +.modal-subtitle, +.modal-msg { + @include bodyLargeTypography; + color: var(--modal-text-foreground-color); + line-height: 1.5; +} diff --git a/frontend/src/app/main/ui/export.cljs b/frontend/src/app/main/ui/export.cljs index 9837a05ff0..c13b074a4e 100644 --- a/frontend/src/app/main/ui/export.cljs +++ b/frontend/src/app/main/ui/export.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.export "Assets exportation common components." + (:require-macros [app.main.style :as stl]) (:require [app.common.colors :as clr] [app.common.data :as d] @@ -16,12 +17,23 @@ [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.workspace.shapes :refer [shape-wrapper]] + [app.main.worker :as uw] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr c]] [app.util.strings :as ust] + [beicon.v2.core :as rx] [cuerdas.core :as str] [rumext.v2 :as mf])) +(def ^:private neutral-icon + (i/icon-xref :msg-neutral (stl/css :icon))) + +(def ^:private error-icon + (i/icon-xref :delete-text (stl/css :icon))) + +(def ^:private close-icon + (i/icon-xref :close (stl/css :close-icon))) + (mf/defc export-multiple-dialog [{:keys [exports title cmd no-selection]}] (let [lstate (mf/deref refs/export) @@ -52,98 +64,124 @@ :cmd cmd}))) on-toggle-enabled - (fn [index] - (swap! exports update-in [index :enabled] not)) + (mf/use-fn + (mf/deps exports) + (fn [event] + (let [index (-> (dom/get-current-target event) + (dom/get-data "value") + (d/parse-integer))] + (when (some? index) + (swap! exports update-in [index :enabled] not))))) change-all (fn [_] (swap! exports (fn [exports] (mapv #(assoc % :enabled (not all-checked?)) exports))))] - [:div.modal-overlay - [:div.modal-container.export-multiple-dialog - {:class (when (empty? all-exports) "empty")} - [:div.modal-header - [:div.modal-header-title - [:h2 title]] + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css-case :modal-container true + :empty (empty? all-exports))} - [:div.modal-close-button - {:on-click cancel-fn} i/close]] + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} title] + [:button {:class (stl/css :modal-close-btn) + :on-click cancel-fn} + i/close]] [:* - [:div.modal-content + [:div {:class (stl/css :modal-content)} (if (> (count all-exports) 0) [:* - [:div.header - [:div.field.check {:on-click change-all} - (cond - all-checked? [:span.checked i/checkbox-checked] - all-unchecked? [:span.unchecked i/checkbox-unchecked] - :else [:span.intermediate i/checkbox-intermediate])] - [:div.field.title (tr "dashboard.export-multiple.selected" - (c (count enabled-exports)) - (c (count all-exports)))]] + [:div {:class (stl/css :selection-header)} + [:button {:class (stl/css :selection-btn) + :on-click change-all} + [:span {:class (stl/css :checkbox-wrapper)} + (cond + all-checked? [:span {:class (stl/css-case :checkobox-tick true + :global/checked true)} + i/tick] + all-unchecked? [:span {:class (stl/css-case :checkobox-tick true + :global/uncheked true)}] + :else [:span {:class (stl/css-case :checkobox-tick true + :global/intermediate true)} + i/remove-icon])]] + [:div {:class (stl/css :selection-title)} + (tr "dashboard.export-multiple.selected" + (c (count enabled-exports)) + (c (count all-exports)))]] + [:div {:class (stl/css :selection-wrapper)} + [:div {:class (stl/css-case :selection-list true + :selection-shadow (> (count all-exports) 8))} + (for [[index {:keys [shape suffix] :as export}] (d/enumerate @exports)] + (let [{:keys [x y width height]} (:selrect shape)] + [:div {:class (stl/css :selection-row) + :key (:id shape)} + [:button {:class (stl/css :selection-btn) + :data-value (str index) + :on-click on-toggle-enabled} + [:span {:class (stl/css :checkbox-wrapper)} + (if (:enabled export) + [:span {:class (stl/css-case :checkobox-tick true + :global/checked true)} + i/tick] + [:span {:class (stl/css-case :checkobox-tick true + :global/uncheked true)}])] - [:div.body - (for [[index {:keys [shape suffix] :as export}] (d/enumerate @exports)] - (let [{:keys [x y width height]} (:selrect shape)] - [:div.row - [:div.field.check {:on-click #(on-toggle-enabled index)} - (if (:enabled export) - [:span.checked i/checkbox-checked] - [:span.unchecked i/checkbox-unchecked])] + [:div {:class (stl/css :image-wrapper)} + (if (some? (:thumbnail shape)) + [:img {:src (:thumbnail shape)}] + [:svg {:view-box (dm/str x " " y " " width " " height) + :width 24 + :height 20 + :version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + ;; Fix Chromium bug about color of html texts + ;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5 + :style {:-webkit-print-color-adjust :exact} + :fill "none"} - [:div.field.image - (if (some? (:thumbnail shape)) - [:img {:src (:thumbnail shape)}] - [:svg {:view-box (dm/str x " " y " " width " " height) - :width 24 - :height 20 - :version "1.1" - :xmlns "http://www.w3.org/2000/svg" - :xmlnsXlink "http://www.w3.org/1999/xlink" - ;; Fix Chromium bug about color of html texts - ;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5 - :style {:-webkit-print-color-adjust :exact} - :fill "none"} + [:& shape-wrapper {:shape shape}]])] - [:& shape-wrapper {:shape shape}]])] + [:div {:class (stl/css :selection-name)} + (cond-> (:name shape) suffix (str suffix))] + (when (:scale export) + [:div {:class (stl/css :selection-scale)} + (dm/str (ust/format-precision (* width (:scale export)) 2) "x" + (ust/format-precision (* height (:scale export)) 2))]) - [:div.field.name (cond-> (:name shape) suffix (str suffix))] - (when (:scale export) - [:div.field.scale (dm/str (ust/format-precision (* width (:scale export)) 2) "x" - (ust/format-precision (* height (:scale export)) 2) "px ")]) + (when (:type export) + [:div {:class (stl/css :selection-extension)} + (-> export :type d/name str/upper)])]]))]]] - (when (:type export) - [:div.field.extension (-> export :type d/name str/upper)])]))] + [:& no-selection])] - [:div.modal-footer - [:div.action-buttons - [:input.cancel-button - {:type "button" - :value (tr "labels.cancel") - :on-click cancel-fn}] + (when (> (count all-exports) 0) + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:input {:class (stl/css :cancel-button) + :type "button" + :value (tr "labels.cancel") + :on-click cancel-fn}] - [:input.accept-button.primary - {:class (dom/classnames - :btn-disabled (or in-progress? all-unchecked?)) - :disabled (or in-progress? all-unchecked?) - :type "button" - :value (if in-progress? - (tr "workspace.options.exporting-object") - (tr "labels.export")) - :on-click (when-not in-progress? accept-fn)}]]]] - - [:& no-selection])]]]])) + [:input {:class (stl/css-case :accept-btn true + :btn-disabled (or in-progress? all-unchecked?)) + :disabled (or in-progress? all-unchecked?) + :type "button" + :value (if in-progress? + (tr "workspace.options.exporting-object") + (tr "labels.export")) + :on-click (when-not in-progress? accept-fn)}]]])]]])) (mf/defc shapes-no-selection [] - [:div.no-selection - [:img {:src "images/export-no-shapes.png" :border "0"}] - [:p (tr "dashboard.export-shapes.no-elements")] - [:p (tr "dashboard.export-shapes.how-to")] - [:p [:a {:target "_blank" - :href "https://help.penpot.app/user-guide/exporting/ "} - (tr "dashboard.export-shapes.how-to-link")]]]) + [:div {:class (stl/css :no-selection)} + [:p {:class (stl/css :modal-msg)} + (tr "dashboard.export-shapes.no-elements")] + [:p {:class (stl/css :modal-scd-msg)} (tr "dashboard.export-shapes.how-to")] + [:a {:target "_blank" + :class (stl/css :modal-link) + :href "https://help.penpot.app/user-guide/exporting/ "} + (tr "dashboard.export-shapes.how-to-link")]]) (mf/defc export-shapes-dialog {::mf/register modal/components @@ -169,25 +207,34 @@ (mf/defc export-progress-widget {::mf/wrap [mf/memo]} [] - (let [state (mf/deref refs/export) - error? (:error state) - healthy? (:healthy? state) - detail-visible? (:detail-visible state) - widget-visible? (:widget-visible state) - progress (:progress state) - exports (:exports state) - total (count exports) - complete? (= progress total) - circ (* 2 Math/PI 12) - pct (- circ (* circ (/ progress total))) + (let [state (mf/deref refs/export) + profile (mf/deref refs/profile) + theme (or (:theme profile) "default") + is-default-theme? (= "default" theme) + error? (:error state) + healthy? (:healthy? state) + detail-visible? (:detail-visible state) + widget-visible? (:widget-visible state) + progress (:progress state) + exports (:exports state) + total (count exports) + complete? (= progress total) + circ (* 2 Math/PI 12) + pct (- circ (* circ (/ progress total))) pwidth (if error? 280 (/ (* progress 280) total)) color (cond - error? clr/danger - healthy? clr/primary - (not healthy?) clr/warning) + error? clr/new-danger + healthy? (if is-default-theme? + clr/new-primary + clr/new-primary-light) + (not healthy?) clr/new-warning) + + background-clr (if is-default-theme? + clr/background-quaternary + clr/background-quaternary-light) title (cond error? (tr "workspace.options.exporting-object-error") complete? (tr "workspace.options.exporting-complete") @@ -202,46 +249,232 @@ [:* (when widget-visible? - [:div.export-progress-widget {:on-click toggle-detail-visibility} - [:svg {:width "32" :height "32"} - [:circle {:r "12" - :cx "16" - :cy "16" + [:div {:class (stl/css :export-progress-widget) + :on-click toggle-detail-visibility} + [:svg {:width "24" :height "24"} + [:circle {:r "10" + :cx "12" + :cy "12" :fill "transparent" - :stroke clr/gray-40 + :stroke background-clr :stroke-width "4"}] - [:circle {:r "12" - :cx "16" - :cy "16" + [:circle {:r "10" + :cx "12" + :cy "12" :fill "transparent" :stroke color :stroke-width "4" :stroke-dasharray (dm/str circ " " circ) :stroke-dashoffset pct - :transform "rotate(-90 16,16)" + :transform "rotate(-90 12,12)" :style {:transition "stroke-dashoffset 1s ease-in-out"}}]]]) (when detail-visible? - [:div.export-progress-modal-overlay - [:div.export-progress-modal-container - [:div.export-progress-modal-header - [:p.export-progress-modal-title title] - (if error? - [:button.btn-secondary.retry {:on-click retry-last-export} (tr "workspace.options.retry")] - [:p.progress (dm/str progress " / " total)]) + [:div {:class (stl/css-case :export-progress-modal true + :has-error error?)} + (if error? + error-icon + neutral-icon) - [:button.modal-close-button {:on-click toggle-detail-visibility} i/close]] + [:p {:class (stl/css :export-progress-title)} + title + (if error? + [:button {:class (stl/css :retry-btn) + :on-click retry-last-export} + (tr "workspace.options.retry")] - [:svg.progress-bar {:height 8 :width 280} - [:g - [:path {:d "M0 0 L280 0" - :stroke clr/gray-10 - :stroke-width 30}] - [:path {:d (dm/str "M0 0 L280 0") - :stroke color - :stroke-width 30 - :fill "transparent" - :stroke-dasharray 280 - :stroke-dashoffset (- 280 pwidth) - :style {:transition "stroke-dashoffset 1s ease-in-out"}}]]]]])])) + [:p {:class (stl/css :progress)} + (dm/str progress " / " total)])] + [:button {:class (stl/css :progress-close-button) + :on-click toggle-detail-visibility} + close-icon] + + (when-not error? + [:svg {:class (stl/css :progress-bar) + :height 4 + :width 280} + [:g + [:path {:d "M0 0 L280 0" + :stroke background-clr + :stroke-width 30}] + [:path {:d (dm/str "M0 0 L280 0") + :stroke color + :stroke-width 30 + :fill "transparent" + :stroke-dasharray 280 + :stroke-dashoffset (- 280 pwidth) + :style {:transition "stroke-dashoffset 1s ease-in-out"}}]]])])])) + +(def ^:const options [:all :merge :detach]) + +(mf/defc export-entry + {::mf/wrap-props false} + [{:keys [file]}] + [:div {:class (stl/css-case :file-entry true + :loading (:loading? file) + :success (:export-success? file) + :error (:export-error? file))} + + [:div {:class (stl/css :file-name)} + [:span {:class (stl/css :file-icon)} + (cond (:export-success? file) i/tick + (:export-error? file) i/close + (:loading? file) i/loader-pencil)] + + [:div {:class (stl/css :file-name-label)} + (:name file)]]]) + +(defn- mark-file-error + [files file-id] + (mapv #(cond-> % + (= file-id (:id %)) + (assoc :export-error? true + :loading? false)) + files)) + +(defn- mark-file-success + [files file-id] + (mapv #(cond-> % + (= file-id (:id %)) + (assoc :export-success? true + :loading? false)) + files)) + +(def export-types + [:all :merge :detach]) + +(mf/defc export-dialog + {::mf/register modal/components + ::mf/register-as :export + ::mf/wrap-props false} + [{:keys [team-id files has-libraries? binary? features]}] + (let [state* (mf/use-state + #(let [files (mapv (fn [file] (assoc file :loading? true)) files)] + {:status :prepare + :selected :all + :files files})) + + state (deref state*) + selected (:selected state) + status (:status state) + + + + start-export + (mf/use-fn + (mf/deps team-id selected files features) + (fn [] + (swap! state* assoc :status :exporting) + (->> (uw/ask-many! + {:cmd (if binary? :export-binary-file :export-standard-file) + :team-id team-id + :features features + :export-type selected + :files files}) + (rx/mapcat #(->> (rx/of %) + (rx/delay 1000))) + (rx/subs! + (fn [msg] + (cond + (= :error (:type msg)) + (swap! state* update :files mark-file-error (:file-id msg)) + + (= :finish (:type msg)) + (do + (swap! state* update :files mark-file-success (:file-id msg)) + (dom/trigger-download-uri (:filename msg) (:mtype msg) (:uri msg))))))))) + + on-cancel + (mf/use-fn + (fn [event] + (dom/prevent-default event) + (st/emit! (modal/hide)))) + + on-accept + (mf/use-fn + (mf/deps start-export) + (fn [event] + (dom/prevent-default event) + (start-export))) + + on-change + (mf/use-fn + (fn [event] + (let [type (-> (dom/get-target event) + (dom/get-data "type") + (keyword))] + (swap! state* assoc :selected type))))] + + (mf/with-effect [has-libraries?] + ;; Start download automatically when no libraries + (when-not has-libraries? + (start-export))) + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} + (tr "dashboard.export.title")] + [:button {:class (stl/css :modal-close-btn) + :on-click on-cancel} i/close]] + + (cond + (= status :prepare) + [:* + [:div {:class (stl/css :modal-content)} + [:p {:class (stl/css :modal-msg)} (tr "dashboard.export.explain")] + [:p {:class (stl/css :modal-scd-msg)} (tr "dashboard.export.detail")] + + (for [type export-types] + [:div {:class (stl/css :export-option true) + :key (name type)} + [:label {:for (str "export-" type) + :class (stl/css-case :global/checked (= selected type))} + ;; Execution time translation strings: + ;; dashboard.export.options.all.message + ;; dashboard.export.options.all.title + ;; dashboard.export.options.detach.message + ;; dashboard.export.options.detach.title + ;; dashboard.export.options.merge.message + ;; dashboard.export.options.merge.title + [:span {:class (stl/css-case :global/checked (= selected type))} + (when (= selected type) + i/status-tick)] + [:div {:class (stl/css :option-content)} + [:h3 {:class (stl/css :modal-subtitle)} (tr (dm/str "dashboard.export.options." (d/name type) ".title"))] + [:p {:class (stl/css :modal-msg)} (tr (dm/str "dashboard.export.options." (d/name type) ".message"))]] + + [:input {:type "radio" + :class (stl/css :option-input) + :id (str "export-" type) + :checked (= selected type) + :name "export-option" + :data-type (name type) + :on-change on-change}]]])] + + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:input {:class (stl/css :cancel-button) + :type "button" + :value (tr "labels.cancel") + :on-click on-cancel}] + + [:input {:class (stl/css :accept-btn) + :type "button" + :value (tr "labels.continue") + :on-click on-accept}]]]] + + (= status :exporting) + [:* + [:div {:class (stl/css :modal-content)} + (for [file (:files state)] + [:& export-entry {:file file :key (dm/str (:id file))}])] + + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:input {:class (stl/css :accept-btn) + :type "button" + :value (tr "labels.close") + :disabled (->> state :files (some :loading?)) + :on-click on-cancel}]]]])]])) diff --git a/frontend/src/app/main/ui/export.scss b/frontend/src/app/main/ui/export.scss new file mode 100644 index 0000000000..479d714a39 --- /dev/null +++ b/frontend/src/app/main/ui/export.scss @@ -0,0 +1,335 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +// PROGRESS WIDGET +.export-progress-widget { + @include flexCenter; + width: $s-28; + height: $s-28; +} + +// PROGRESS MODAL +.export-progress-modal { + --export-modal-bg-color: var(--alert-background-color-default); + --export-modal-fg-color: var(--alert-text-foreground-color-default); + --export-modal-icon-color: var(--alert-icon-foreground-color-default); + --export-modal-border-color: var(--alert-border-color-default); + position: absolute; + right: $s-16; + top: $s-48; + display: grid; + grid-template-columns: $s-24 1fr $s-24; + grid-template-areas: + "icon text close" + "bar bar bar"; + gap: $s-4 $s-8; + padding-block-start: $s-8; + background-color: var(--export-modal-bg-color); + border: $s-1 solid var(--export-modal-border-color); + border-radius: $br-8; + z-index: $z-index-modal; + overflow: hidden; +} + +.has-error { + --export-modal-bg-color: var(--alert-background-color-error); + --export-modal-fg-color: var(--alert-text-foreground-color-error); + --export-modal-icon-color: var(--alert-icon-foreground-color-error); + --export-modal-border-color: var(--alert-border-color-error); + grid-template-areas: "icon text close"; + gap: $s-8; + padding-block: $s-8; +} + +.icon { + @extend .button-icon; + grid-area: icon; + align-self: center; + margin-inline-start: $s-8; + stroke: var(--export-modal-icon-color); +} + +.export-progress-title { + @include bodyMediumTypography; + display: grid; + grid-template-columns: auto 1fr; + gap: $s-8; + grid-area: text; + align-self: center; + padding: 0; + margin: 0; + color: var(--export-modal-fg-color); +} + +.progress { + @include bodyMediumTypography; + padding-left: $s-8; + margin: 0; + align-self: center; + color: var(--modal-text-foreground-color); +} + +.retry-btn { + @include buttonStyle; + @include bodySmallTypography; + display: inline; + text-align: left; + color: var(--modal-link-foreground-color); + margin: 0; + padding: 0; +} + +.progress-close-button { + @include buttonStyle; + padding: 0; + margin-inline-end: $s-8; +} + +.close-icon { + @extend .button-icon; + stroke: var(--export-modal-icon-color); +} + +.progress-bar { + margin-top: 0; + grid-area: bar; +} + +// EXPORT MODAL +.modal-overlay { + @extend .modal-overlay-base; + &.transparent { + background-color: transparent; + } +} + +.modal-container { + @extend .modal-container-base; + max-height: calc(10 * $s-80); +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include headlineMediumTypography; + color: var(--modal-title-foreground-color); +} + +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content, +.no-selection { + @include bodySmallTypography; + margin-bottom: $s-24; + .modal-hint { + @include bodySmallTypography; + color: var(--modal-text-foreground-color); + } + .modal-link { + @include bodyLargeTypography; + text-decoration: none; + cursor: pointer; + color: var(--modal-link-foreground-color); + } + .selection-header { + @include flexRow; + height: $s-32; + margin-bottom: $s-4; + .selection-btn { + @include buttonStyle; + @extend .input-checkbox; + @include flexCenter; + height: $s-24; + width: $s-24; + padding: 0; + margin-left: $s-16; + span { + @extend .checkbox-icon; + } + } + .selection-title { + @include bodyLargeTypography; + color: var(--modal-text-foreground-color); + } + } + .selection-wrapper { + position: relative; + width: 100%; + height: fit-content; + } + .selection-shadow { + width: 100%; + height: 100%; + &:after { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 50px; + background: linear-gradient(to top, rgba(24, 24, 26, 1) 0%, rgba(24, 24, 26, 0) 100%); + content: ""; + pointer-events: none; + } + } + .selection-list { + @include flexColumn; + max-height: $s-400; + overflow-y: auto; + padding-bottom: $s-12; + .selection-row { + @include flexRow; + background-color: var(--entry-background-color); + min-height: $s-40; + border-radius: $br-8; + .selection-btn { + @include buttonStyle; + display: grid; + grid-template-columns: min-content auto 1fr auto auto; + align-items: center; + width: 100%; + height: 10%; + gap: $s-8; + padding: 0 $s-16; + .checkbox-wrapper { + @extend .input-checkbox; + @include flexCenter; + height: $s-24; + width: $s-24; + padding: 0; + .checkobox-tick { + @extend .checkbox-icon; + } + } + .selection-name { + @include bodyLargeTypography; + @include textEllipsis; + flex-grow: 1; + color: var(--modal-text-foreground-color); + text-align: start; + } + .selection-scale { + @include bodyLargeTypography; + @include textEllipsis; + min-width: $s-108; + padding: $s-12; + color: var(--modal-text-foreground-color); + } + .selection-extension { + @include bodyLargeTypography; + @include textEllipsis; + min-width: $s-72; + padding: $s-12; + color: var(--modal-text-foreground-color); + } + } + .image-wrapper { + @include flexCenter; + min-height: $s-32; + min-width: $s-32; + background-color: var(--app-white); + border-radius: $br-6; + margin: auto 0; + img, + svg { + object-fit: contain; + max-height: $s-40; + } + } + } + } +} + +.action-buttons { + @extend .modal-action-btns; +} +.cancel-button { + @extend .modal-cancel-btn; +} +.accept-btn { + @extend .modal-accept-btn; + &.danger { + @extend .modal-danger-btn; + } +} + +.modal-scd-msg, +.modal-subtitle, +.modal-msg { + @include bodyLargeTypography; + color: var(--modal-text-foreground-color); +} + +.export-option { + @extend .input-checkbox; + width: 100%; + align-items: flex-start; + label { + align-items: flex-start; + .modal-subtitle { + @include bodyLargeTypography; + color: var(--modal-title-foreground-color); + } + } + span { + margin-top: $s-8; + } +} + +.option-content { + @include flexColumn; + @include bodyLargeTypography; +} + +.file-entry { + .file-name { + @include flexRow; + .file-icon { + @include flexCenter; + height: $s-16; + width: $s-16; + + svg { + @extend .button-icon-small; + stroke: var(--input-foreground); + } + } + .file-name-label { + @include bodyLargeTypography; + } + } + &.loading { + .file-name { + color: var(--modal-text-foreground-color); + .file-icon svg:global(#loader-pencil) { + color: var(--modal-text-foreground-color); + stroke: var(--modal-text-foreground-color); + fill: var(--modal-text-foreground-color); + } + } + } + &.error { + .file-name { + color: var(--modal-text-foreground-color); + .file-icon svg { + stroke: var(--modal-text-foreground-color); + } + } + } + &.success { + .file-name { + color: var(--modal-text-foreground-color); + .file-icon svg { + stroke: var(--modal-text-foreground-color); + } + } + } +} diff --git a/frontend/src/app/main/ui/flex_controls.cljs b/frontend/src/app/main/ui/flex_controls.cljs new file mode 100644 index 0000000000..a21c6b9d90 --- /dev/null +++ b/frontend/src/app/main/ui/flex_controls.cljs @@ -0,0 +1,16 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.flex-controls + (:require + [app.common.data.macros :as dm] + [app.main.ui.flex-controls.gap :as fcg] + [app.main.ui.flex-controls.margin :as fcm] + [app.main.ui.flex-controls.padding :as fcp])) + +(dm/export fcg/gap-control) +(dm/export fcm/margin-control) +(dm/export fcp/padding-control) diff --git a/frontend/src/app/main/ui/flex_controls/common.cljs b/frontend/src/app/main/ui/flex_controls/common.cljs new file mode 100644 index 0000000000..032760688f --- /dev/null +++ b/frontend/src/app/main/ui/flex_controls/common.cljs @@ -0,0 +1,35 @@ +(ns app.main.ui.flex-controls.common + (:require + [app.main.ui.formats :as fmt] + [rumext.v2 :as mf])) + +;; ------------------------------------------------ +;; CONSTANTS +;; ------------------------------------------------ + +(def font-size 11) +(def distance-color "var(--da-quaternary)") +(def distance-text-color "var(--app-white)") +(def warning-color "var(--status-color-warning-500)") +(def flex-display-pill-width 40) +(def flex-display-pill-height 20) +(def flex-display-pill-border-radius 4) + +(mf/defc flex-display-pill + [{:keys [x y width height font-size border-radius value color]}] + [:g.distance-pill + [:rect {:x x + :y y + :width width + :height height + :rx border-radius + :ry border-radius + :style {:fill color}}] + + [:text {:x (+ x (/ width 2)) + :y (+ y (/ height 2)) + :text-anchor "middle" + :dominant-baseline "central" + :style {:fill distance-text-color + :font-size font-size}} + (fmt/format-number (or value 0))]]) diff --git a/frontend/src/app/main/ui/flex_controls/gap.cljs b/frontend/src/app/main/ui/flex_controls/gap.cljs new file mode 100644 index 0000000000..e04024d862 --- /dev/null +++ b/frontend/src/app/main/ui/flex_controls/gap.cljs @@ -0,0 +1,311 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.flex-controls.gap + (:require + [app.common.data :as d] + [app.common.files.helpers :as cfh] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.flex-layout :as gsl] + [app.common.geom.shapes.points :as gpo] + [app.common.types.modifiers :as ctm] + [app.common.types.shape.layout :as ctl] + [app.main.data.workspace.modifiers :as dwm] + [app.main.data.workspace.state-helpers :as wsh] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.css-cursors :as cur] + [app.main.ui.flex-controls.common :as fcc] + [app.main.ui.workspace.viewport.viewport-ref :refer [point->viewport]] + [app.util.dom :as dom] + [rumext.v2 :as mf])) + +(mf/defc gap-display + [{:keys [frame-id zoom gap-type gap on-pointer-enter on-pointer-leave + rect-data hover? selected? mouse-pos hover-value + on-move-selected on-context-menu]}] + (let [resizing (mf/use-var nil) + start (mf/use-var nil) + original-value (mf/use-var 0) + negate? (:resize-negate? rect-data) + axis (:resize-axis rect-data) + + on-pointer-down + (mf/use-fn + (mf/deps frame-id gap-type gap) + (fn [event] + (dom/capture-pointer event) + (reset! resizing gap-type) + (reset! start (dom/get-client-position event)) + (reset! original-value (:initial-value rect-data)))) + + on-lost-pointer-capture + (mf/use-fn + (mf/deps frame-id gap-type gap) + (fn [event] + (dom/release-pointer event) + (reset! resizing nil) + (reset! start nil) + (reset! original-value 0) + (st/emit! (dwm/apply-modifiers)))) + + on-pointer-move + (mf/use-fn + (mf/deps frame-id gap-type gap) + (fn [event] + (let [pos (dom/get-client-position event)] + (reset! mouse-pos (point->viewport pos)) + (when (= @resizing gap-type) + (let [delta (-> (gpt/to-vec @start pos) + (cond-> negate? gpt/negate) + (get axis)) + val (int (max (+ @original-value (/ delta zoom)) 0)) + layout-gap (assoc gap gap-type val) + modifiers (dwm/create-modif-tree [frame-id] (ctm/change-property (ctm/empty) :layout-gap layout-gap))] + + (reset! hover-value val) + (st/emit! (dwm/set-modifiers modifiers)))))))] + + [:g.gap-rect + [:rect.info-area + {:x (:x rect-data) + :y (:y rect-data) + :width (:width rect-data) + :height (:height rect-data) + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-pointer-move on-pointer-move + :on-pointer-down on-move-selected + :on-context-menu on-context-menu + + :style {:fill (if (or hover? selected?) fcc/distance-color "none") + :opacity (if selected? 0.5 0.25)}}] + + (let [handle-width + (if (= axis :x) + (/ 2 zoom) + (min (* (:width rect-data) 0.5) (/ 20 zoom))) + + handle-height + (if (= axis :y) + (/ 2 zoom) + (min (* (:height rect-data) 0.5) (/ 30 zoom)))] + [:rect.handle + {:x (+ (:x rect-data) (/ (- (:width rect-data) handle-width) 2)) + :y (+ (:y rect-data) (/ (- (:height rect-data) handle-height) 2)) + :width handle-width + :height handle-height + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move + :on-context-menu on-context-menu + :class (when (or hover? selected?) + (if (= (:resize-axis rect-data) :x) (cur/get-dynamic "resize-ew" 0) (cur/get-dynamic "resize-ew" 90))) + :style {:fill (if (or hover? selected?) fcc/distance-color "none") + :opacity (if selected? 0 1)}}])])) + +(mf/defc gap-rects + [{:keys [frame zoom on-move-selected on-context-menu]}] + (let [frame-id (:id frame) + saved-dir (:layout-flex-dir frame) + is-col? (or (= :column saved-dir) (= :column-reverse saved-dir)) + flip-x (:flip-x frame) + flip-y (:flip-y frame) + pill-width (/ fcc/flex-display-pill-width zoom) + pill-height (/ fcc/flex-display-pill-height zoom) + workspace-modifiers (mf/deref refs/workspace-modifiers) + gap-selected (mf/deref refs/workspace-gap-selected) + hover (mf/use-state nil) + hover-value (mf/use-state 0) + mouse-pos (mf/use-state nil) + padding (:layout-padding frame) + gap (:layout-gap frame) + {:keys [width height x1 y1]} (:selrect frame) + on-pointer-enter (fn [hover-type val] + (reset! hover hover-type) + (reset! hover-value val)) + + on-pointer-leave #(reset! hover nil) + negate {:column-gap (if flip-x true false) + :row-gap (if flip-y true false)} + + objects (wsh/lookup-page-objects @st/state) + children (->> (cfh/get-immediate-children objects frame-id) + (remove ctl/position-absolute?)) + + children-to-display (if (or (= :row-reverse saved-dir) + (= :column-reverse saved-dir)) + (drop-last children) + (rest children)) + children-to-display (->> children-to-display + (map #(gsh/transform-shape % (get-in workspace-modifiers [(:id %) :modifiers])))) + + wrap-blocks + (let [block-children (->> children + (map #(vector (gpo/parent-coords-bounds (:points %) (:points frame)) %))) + bounds (d/lazy-map (keys objects) #(gsh/shape->points (get objects %))) + + layout-data (gsl/calc-layout-data frame (:points frame) block-children bounds objects) + layout-bounds (:layout-bounds layout-data) + xv #(gpo/start-hv layout-bounds %) + yv #(gpo/start-vv layout-bounds %)] + (for [{:keys [start-p line-width line-height layout-gap-row layout-gap-col num-children]} (:layout-lines layout-data)] + (let [line-width (if is-col? line-width (+ line-width (* (dec num-children) layout-gap-row))) + line-height (if is-col? (+ line-height (* (dec num-children) layout-gap-col)) line-height) + end-p (-> start-p (gpt/add (xv line-width)) (gpt/add (yv line-height)))] + {:x1 (min (:x start-p) (:x end-p)) + :y1 (min (:y start-p) (:y end-p)) + :x2 (max (:x start-p) (:x end-p)) + :y2 (max (:y start-p) (:y end-p))}))) + + block-contains + (fn [x y block] + (if is-col? + (<= (:x1 block) x (:x2 block)) + (<= (:y1 block) y (:y2 block)))) + + get-container-block + (fn [shape] + (let [selrect (:selrect shape) + x (/ (+ (:x1 selrect) (:x2 selrect)) 2) + y (/ (+ (:y1 selrect) (:y2 selrect)) 2)] + (->> wrap-blocks + (filter #(block-contains x y %)) + first))) + + create-cgdd + (fn [shape] + (let [block (get-container-block shape) + x (if flip-x + (- (:x1 (:selrect shape)) + (get-in shape [:layout-item-margin :m2]) + (:column-gap gap)) + (+ (:x2 (:selrect shape)) (get-in shape [:layout-item-margin :m2]))) + y (:y1 block) + h (- (:y2 block) (:y1 block))] + {:x x + :y y + :height h + :width (:column-gap gap) + :initial-value (:column-gap gap) + :resize-type :left + :resize-axis :x + :resize-negate? (:column-gap negate) + :gap-type (if is-col? :row-gap :column-gap)})) + + create-cgdd-block + (fn [block] + (let [x (if flip-x + (- (:x1 block) (:column-gap gap)) + (:x2 block)) + y (if flip-y + (+ y1 (:p3 padding)) + (+ y1 (:p1 padding))) + h (- height (+ (:p1 padding) (:p3 padding)))] + {:x x + :y y + :width (:column-gap gap) + :height h + :initial-value (:column-gap gap) + :resize-type :left + :resize-axis :x + :resize-negate? (:column-gap negate) + :gap-type (if is-col? :column-gap :row-gap)})) + + create-rgdd + (fn [shape] + (let [block (get-container-block shape) + x (:x1 block) + y (if flip-y + (- (:y1 (:selrect shape)) + (get-in shape [:layout-item-margin :m3]) + (:row-gap gap)) + (+ (:y2 (:selrect shape)) (get-in shape [:layout-item-margin :m3]))) + w (- (:x2 block) (:x1 block))] + {:x x + :y y + :width w + :height (:row-gap gap) + :initial-value (:row-gap gap) + :resize-type :bottom + :resize-axis :y + :resize-negate? (:row-gap negate) + :gap-type (if is-col? :row-gap :column-gap)})) + + create-rgdd-block + (fn [block] + (let [x (if flip-x + (+ x1 (:p2 padding)) + (+ x1 (:p4 padding))) + y (if flip-y + (- (:y1 block) (:row-gap gap)) + (:y2 block)) + w (- width (+ (:p2 padding) (:p4 padding)))] + {:x x + :y y + :width w + :height (:row-gap gap) + :initial-value (:row-gap gap) + :resize-type :bottom + :resize-axis :y + :resize-negate? (:row-gap negate) + :gap-type (if is-col? :column-gap :row-gap)})) + + display-blocks (if is-col? + (->> (drop-last wrap-blocks) + (map create-cgdd-block)) + (->> (drop-last wrap-blocks) + (map create-rgdd-block))) + + display-children (if is-col? + (->> children-to-display + (map create-rgdd)) + (->> children-to-display + (map create-cgdd)))] + + [:g.gaps {:pointer-events "visible"} + (for [[index display-item] (d/enumerate (concat display-blocks display-children))] + (let [gap-type (:gap-type display-item)] + [:& gap-display {:key (str frame-id index) + :frame-id frame-id + :zoom zoom + :gap-type gap-type + :gap gap + :on-pointer-enter (partial on-pointer-enter gap-type (get gap gap-type)) + :on-pointer-leave on-pointer-leave + :on-move-selected on-move-selected + :on-context-menu on-context-menu + :rect-data display-item + :hover? (= @hover gap-type) + :selected? (= gap-selected gap-type) + :mouse-pos mouse-pos + :hover-value hover-value}])) + + (when @hover + [:& fcc/flex-display-pill + {:height pill-height + :width pill-width + :font-size (/ fcc/font-size zoom) + :border-radius (/ fcc/flex-display-pill-border-radius zoom) + :color fcc/distance-color + :x (:x @mouse-pos) + :y (- (:y @mouse-pos) pill-width) + :value @hover-value}])])) + + +(mf/defc gap-control + [{:keys [frame zoom on-move-selected on-context-menu]}] + (when frame + [:g.measurement-gaps {:pointer-events "none"} + [:g.hover-shapes + [:& gap-rects + {:frame frame + :zoom zoom + :on-move-selected on-move-selected + :on-context-menu on-context-menu}]]])) diff --git a/frontend/src/app/main/ui/flex_controls/margin.cljs b/frontend/src/app/main/ui/flex_controls/margin.cljs new file mode 100644 index 0000000000..dee2900b39 --- /dev/null +++ b/frontend/src/app/main/ui/flex_controls/margin.cljs @@ -0,0 +1,185 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.flex-controls.margin + (:require + [app.common.geom.point :as gpt] + [app.common.types.modifiers :as ctm] + [app.main.data.workspace.modifiers :as dwm] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.css-cursors :as cur] + [app.main.ui.flex-controls.common :as fcc] + [app.main.ui.workspace.viewport.viewport-ref :refer [point->viewport]] + [app.util.dom :as dom] + [rumext.v2 :as mf])) + +(mf/defc margin-display [{:keys [shape-id zoom hover-all? hover-v? hover-h? margin-num margin on-pointer-enter on-pointer-leave + rect-data hover? selected? mouse-pos hover-value]}] + (let [resizing? (mf/use-var false) + start (mf/use-var nil) + original-value (mf/use-var 0) + negate? (true? (:resize-negate? rect-data)) + axis (:resize-axis rect-data) + + on-pointer-down + (mf/use-fn + (mf/deps shape-id margin-num margin) + (fn [event] + (dom/capture-pointer event) + (reset! resizing? true) + (reset! start (dom/get-client-position event)) + (reset! original-value (:initial-value rect-data)))) + + on-lost-pointer-capture + (mf/use-fn + (mf/deps shape-id margin-num margin) + (fn [event] + (dom/release-pointer event) + (reset! resizing? false) + (reset! start nil) + (reset! original-value 0) + (st/emit! (dwm/apply-modifiers)))) + + on-pointer-move + (mf/use-fn + (mf/deps shape-id margin-num margin hover-all? hover-v? hover-h?) + (fn [event] + (let [pos (dom/get-client-position event)] + (reset! mouse-pos (point->viewport pos)) + (when @resizing? + (let [delta (-> (gpt/to-vec @start pos) + (cond-> negate? gpt/negate) + (get axis)) + val (int (max (+ @original-value (/ delta zoom)) 0)) + layout-item-margin (cond + hover-all? (assoc margin :m1 val :m2 val :m3 val :m4 val) + hover-v? (assoc margin :m1 val :m3 val) + hover-h? (assoc margin :m2 val :m4 val) + :else (assoc margin margin-num val)) + layout-item-margin-type (if (= (:m1 margin) (:m2 margin) (:m3 margin) (:m4 margin)) :simple :multiple) + modifiers (dwm/create-modif-tree [shape-id] + (-> (ctm/empty) + (ctm/change-property :layout-item-margin layout-item-margin) + (ctm/change-property :layout-item-margin-type layout-item-margin-type)))] + (reset! hover-value val) + (st/emit! (dwm/set-modifiers modifiers)))))))] + + [:rect.margin-rect + {:x (:x rect-data) + :y (:y rect-data) + :width (max 0 (:width rect-data)) + :height (max 0 (:height rect-data)) + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move + :class (when (or hover? selected?) + (if (= (:resize-axis rect-data) :x) (cur/get-dynamic "resize-ew" 0) (cur/get-dynamic "resize-ew" 90))) + :style {:fill (if (or hover? selected?) fcc/warning-color "none") + :opacity (if selected? 0.5 0.25)}}])) + + +(mf/defc margin-rects [{:keys [shape frame zoom alt? shift?]}] + (let [shape-id (:id shape) + pill-width (/ fcc/flex-display-pill-width zoom) + pill-height (/ fcc/flex-display-pill-height zoom) + margins-selected (mf/deref refs/workspace-margins-selected) + hover-value (mf/use-state 0) + mouse-pos (mf/use-state nil) + hover (mf/use-state nil) + hover-all? (and (not (nil? @hover)) alt?) + hover-v? (and (or (= @hover :m1) (= @hover :m3)) shift?) + hover-h? (and (or (= @hover :m2) (= @hover :m4)) shift?) + margin (:layout-item-margin shape) + {:keys [width height x1 x2 y1 y2]} (:selrect shape) + on-pointer-enter (fn [hover-type val] + (reset! hover hover-type) + (reset! hover-value val)) + on-pointer-leave #(reset! hover nil) + hover? #(or hover-all? + (and (or (= % :m1) (= % :m3)) hover-v?) + (and (or (= % :m2) (= % :m4)) hover-h?) + (= @hover %)) + margin-display-data {:m1 {:key (str shape-id "-m1") + :x x1 + :y (if (:flip-y frame) y2 (- y1 (:m1 margin))) + :width width + :height (:m1 margin) + :initial-value (:m1 margin) + :resize-type :top + :resize-axis :y + :resize-negate? (:flip-y frame)} + :m2 {:key (str shape-id "-m2") + :x (if (:flip-x frame) (- x1 (:m2 margin)) x2) + :y y1 + :width (:m2 margin) + :height height + :initial-value (:m2 margin) + :resize-type :left + :resize-axis :x + :resize-negate? (:flip-x frame)} + :m3 {:key (str shape-id "-m3") + :x x1 + :y (if (:flip-y frame) (- y1 (:m3 margin)) y2) + :width width + :height (:m3 margin) + :initial-value (:m3 margin) + :resize-type :top + :resize-axis :y + :resize-negate? (:flip-y frame)} + :m4 {:key (str shape-id "-m4") + :x (if (:flip-x frame) x2 (- x1 (:m4 margin))) + :y y1 + :width (:m4 margin) + :height height + :initial-value (:m4 margin) + :resize-type :left + :resize-axis :x + :resize-negate? (:flip-x frame)}}] + + [:g.margins {:pointer-events "visible"} + (for [[margin-num rect-data] margin-display-data] + [:& margin-display + {:key (:key rect-data) + :shape-id shape-id + :zoom zoom + :hover-all? hover-all? + :hover-v? hover-v? + :hover-h? hover-h? + :margin-num margin-num + :margin margin + :on-pointer-enter (partial on-pointer-enter margin-num (get margin margin-num)) + :on-pointer-leave on-pointer-leave + :rect-data rect-data + :hover? (hover? margin-num) + :selected? (get margins-selected margin-num) + :mouse-pos mouse-pos + :hover-value hover-value}]) + + (when @hover + [:& fcc/flex-display-pill + {:height pill-height + :width pill-width + :font-size (/ fcc/font-size zoom) + :border-radius (/ fcc/flex-display-pill-border-radius zoom) + :color fcc/warning-color + :x (:x @mouse-pos) + :y (- (:y @mouse-pos) pill-width) + :value @hover-value}])])) + +(mf/defc margin-control + [{:keys [shape parent zoom alt? shift?]}] + (when shape + [:g.measurement-gaps {:pointer-events "none"} + [:g.hover-shapes + [:& margin-rects + {:shape shape + :frame parent + :zoom zoom + :alt? alt? + :shift? shift?}]]])) diff --git a/frontend/src/app/main/ui/flex_controls/padding.cljs b/frontend/src/app/main/ui/flex_controls/padding.cljs new file mode 100644 index 0000000000..96e0c07d8f --- /dev/null +++ b/frontend/src/app/main/ui/flex_controls/padding.cljs @@ -0,0 +1,222 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.flex-controls.padding + (:require + [app.common.geom.point :as gpt] + [app.common.types.modifiers :as ctm] + [app.main.data.workspace.modifiers :as dwm] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.css-cursors :as cur] + [app.main.ui.flex-controls.common :as fcc] + [app.main.ui.workspace.viewport.viewport-ref :refer [point->viewport]] + [app.util.dom :as dom] + [rumext.v2 :as mf])) + +(mf/defc padding-display + [{:keys [frame-id zoom hover-all? hover-v? hover-h? padding-num padding on-pointer-enter on-pointer-leave + rect-data hover? selected? mouse-pos hover-value on-move-selected on-context-menu]}] + (let [resizing? (mf/use-var false) + start (mf/use-var nil) + original-value (mf/use-var 0) + negate? (true? (:resize-negate? rect-data)) + axis (:resize-axis rect-data) + + on-pointer-down + (mf/use-fn + (mf/deps frame-id rect-data padding-num) + (fn [event] + (dom/capture-pointer event) + (reset! resizing? true) + (reset! start (dom/get-client-position event)) + (reset! original-value (:initial-value rect-data)))) + + on-lost-pointer-capture + (mf/use-fn + (mf/deps frame-id padding-num padding) + (fn [event] + (dom/release-pointer event) + (reset! resizing? false) + (reset! start nil) + (reset! original-value 0) + (st/emit! (dwm/apply-modifiers)))) + + on-pointer-move + (mf/use-fn + (mf/deps frame-id padding-num padding hover-all? hover-v? hover-h?) + (fn [event] + (let [pos (dom/get-client-position event)] + (reset! mouse-pos (point->viewport pos)) + (when @resizing? + (let [delta (-> (gpt/to-vec @start pos) + (cond-> negate? gpt/negate) + (get axis)) + val (int (max (+ @original-value (/ delta zoom)) 0)) + layout-padding (cond + hover-all? (assoc padding :p1 val :p2 val :p3 val :p4 val) + hover-v? (assoc padding :p1 val :p3 val) + hover-h? (assoc padding :p2 val :p4 val) + :else (assoc padding padding-num val)) + + + layout-padding-type (if (= (:p1 padding) (:p2 padding) (:p3 padding) (:p4 padding)) :simple :multiple) + modifiers (dwm/create-modif-tree [frame-id] + (-> (ctm/empty) + (ctm/change-property :layout-padding layout-padding) + (ctm/change-property :layout-padding-type layout-padding-type)))] + (reset! hover-value val) + (st/emit! (dwm/set-modifiers modifiers)))))))] + + [:g.padding-rect + [:rect.info-area + {:x (:x rect-data) + :y (:y rect-data) + :width (max 0 (:width rect-data)) + :height (max 0 (:height rect-data)) + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-pointer-move on-pointer-move + :on-pointer-down on-move-selected + :on-context-menu on-context-menu + :style {:fill (if (or hover? selected?) fcc/distance-color "none") + :opacity (if selected? 0.5 0.25)}}] + + (let [handle-width + (if (= axis :x) + (/ 2 zoom) + (min (* (:width rect-data) 0.5) (/ 20 zoom))) + + handle-height + (if (= axis :y) + (/ 2 zoom) + (min (* (:height rect-data) 0.5) (/ 30 zoom)))] + [:rect.handle + {:x (+ (:x rect-data) (/ (- (:width rect-data) handle-width) 2)) + :y (+ (:y rect-data) (/ (- (:height rect-data) handle-height) 2)) + :width handle-width + :height handle-height + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move + :on-context-menu on-context-menu + :class (when (or hover? selected?) + (if (= (:resize-axis rect-data) :x) (cur/get-dynamic "resize-ew" 0) (cur/get-dynamic "resize-ew" 90))) + :style {:fill (if (or hover? selected?) fcc/distance-color "none") + :opacity (if selected? 0 1)}}])])) + +(mf/defc padding-rects + [{:keys [frame zoom alt? shift? on-move-selected on-context-menu]}] + (let [frame-id (:id frame) + paddings-selected (mf/deref refs/workspace-paddings-selected) + hover-value (mf/use-state 0) + mouse-pos (mf/use-state nil) + hover (mf/use-state nil) + hover-all? (and (not (nil? @hover)) alt?) + hover-v? (and (or (= @hover :p1) (= @hover :p3)) shift?) + hover-h? (and (or (= @hover :p2) (= @hover :p4)) shift?) + padding (:layout-padding frame) + {:keys [width height x1 x2 y1 y2]} (:selrect frame) + on-pointer-enter (fn [hover-type val] + (reset! hover hover-type) + (reset! hover-value val)) + on-pointer-leave #(reset! hover nil) + pill-width (/ fcc/flex-display-pill-width zoom) + pill-height (/ fcc/flex-display-pill-height zoom) + hover? #(or hover-all? + (and (or (= % :p1) (= % :p3)) hover-v?) + (and (or (= % :p2) (= % :p4)) hover-h?) + (= @hover %)) + negate {:p1 (if (:flip-y frame) true false) + :p2 (if (:flip-x frame) true false) + :p3 (if (:flip-y frame) true false) + :p4 (if (:flip-x frame) true false)} + negate (cond-> negate + (not= :auto (:layout-item-h-sizing frame)) (assoc :p2 (not (:p2 negate))) + (not= :auto (:layout-item-v-sizing frame)) (assoc :p3 (not (:p3 negate)))) + + padding-rect-data {:p1 {:key (str frame-id "-p1") + :x x1 + :y (if (:flip-y frame) (- y2 (:p1 padding)) y1) + :width width + :height (:p1 padding) + :initial-value (:p1 padding) + :resize-type (if (:flip-y frame) :bottom :top) + :resize-axis :y + :resize-negate? (:p1 negate)} + :p2 {:key (str frame-id "-p2") + :x (if (:flip-x frame) x1 (- x2 (:p2 padding))) + :y y1 + :width (:p2 padding) + :height height + :initial-value (:p2 padding) + :resize-type :left + :resize-axis :x + :resize-negate? (:p2 negate)} + :p3 {:key (str frame-id "-p3") + :x x1 + :y (if (:flip-y frame) y1 (- y2 (:p3 padding))) + :width width + :height (:p3 padding) + :initial-value (:p3 padding) + :resize-type :bottom + :resize-axis :y + :resize-negate? (:p3 negate)} + :p4 {:key (str frame-id "-p4") + :x (if (:flip-x frame) (- x2 (:p4 padding)) x1) + :y y1 + :width (:p4 padding) + :height height + :initial-value (:p4 padding) + :resize-type (if (:flip-x frame) :right :left) + :resize-axis :x + :resize-negate? (:p4 negate)}}] + + [:g.paddings {:pointer-events "visible"} + (for [[padding-num rect-data] padding-rect-data] + [:& padding-display + {:key (:key rect-data) + :frame-id frame-id + :zoom zoom + :hover-all? hover-all? + :hover-v? hover-v? + :hover-h? hover-h? + :padding padding + :mouse-pos mouse-pos + :hover-value hover-value + :padding-num padding-num + :on-pointer-enter (partial on-pointer-enter padding-num (get padding padding-num)) + :on-pointer-leave on-pointer-leave + :on-move-selected on-move-selected + :on-context-menu on-context-menu + :hover? (hover? padding-num) + :selected? (get paddings-selected padding-num) + :rect-data rect-data}]) + (when @hover + [:& fcc/flex-display-pill + {:height pill-height + :width pill-width + :font-size (/ fcc/font-size zoom) + :border-radius (/ fcc/flex-display-pill-border-radius zoom) + :color fcc/distance-color + :x (:x @mouse-pos) + :y (- (:y @mouse-pos) pill-width) + :value @hover-value}])])) + +(mf/defc padding-control + [{:keys [frame zoom alt? shift? on-move-selected on-context-menu]}] + (when frame + [:g.measurement-gaps {:pointer-events "none"} + [:g.hover-shapes + [:& padding-rects + {:frame frame + :zoom zoom + :alt? alt? + :shift? shift? + :on-move-selected on-move-selected + :on-context-menu on-context-menu}]]])) diff --git a/frontend/src/app/main/ui/formats.cljs b/frontend/src/app/main/ui/formats.cljs index 76fac5eda0..eb9f2fc52e 100644 --- a/frontend/src/app/main/ui/formats.cljs +++ b/frontend/src/app/main/ui/formats.cljs @@ -16,32 +16,45 @@ (format-percent value nil)) ([value {:keys [precision] :or {precision 2}}] - (when (d/num? value) - (let [percent-val (mth/precision (* value 100) precision)] - (dm/str percent-val "%"))))) + (let [value (if (string? value) (d/parse-double value) value)] + (when (d/num? value) + (let [percent-val (mth/precision (* value 100) precision)] + (dm/str percent-val "%")))))) + +(defn format-frs + ([value] + (format-frs value nil)) + ([value {:keys [precision] :or {precision 2}}] + (let [value (if (string? value) (d/parse-double value) value)] + (when (d/num? value) + (let [value (mth/precision value precision)] + (dm/str value "fr")))))) (defn format-number ([value] (format-number value nil)) ([value {:keys [precision] :or {precision 2}}] - (when (d/num? value) - (let [value (mth/precision value precision)] - (dm/str value))))) + (let [value (if (string? value) (d/parse-double value) value)] + (when (d/num? value) + (let [value (mth/precision value precision)] + (dm/str value)))))) (defn format-pixels ([value] (format-pixels value nil)) ([value {:keys [precision] :or {precision 2}}] - (when (d/num? value) - (let [value (mth/precision value precision)] - (dm/str value "px"))))) + (let [value (if (string? value) (d/parse-double value) value)] + (when (d/num? value) + (let [value (mth/precision value precision)] + (dm/str value "px")))))) (defn format-int [value] - (when (d/num? value) - (let [value (mth/precision value 0)] - (dm/str value)))) + (let [value (if (string? value) (d/parse-double value) value)] + (when (d/num? value) + (let [value (mth/precision value 0)] + (dm/str value))))) (defn format-padding-margin-shorthand [values] @@ -97,3 +110,15 @@ (if (= row-gap column-gap) (str/fmt "%spx" (format-number row-gap)) (str/fmt "%spx %spx" (format-number row-gap) (format-number column-gap))))) + +(defn format-matrix + ([mtx] + (format-matrix mtx 2)) + ([{:keys [a b c d e f]} precision] + (dm/fmt "matrix(%, %, %, %, %, %)" + (mth/to-fixed a precision) + (mth/to-fixed b precision) + (mth/to-fixed c precision) + (mth/to-fixed d precision) + (mth/to-fixed e precision) + (mth/to-fixed f precision)))) diff --git a/frontend/src/app/main/ui/frame_preview.cljs b/frontend/src/app/main/ui/frame_preview.cljs new file mode 100644 index 0000000000..bff3da3abd --- /dev/null +++ b/frontend/src/app/main/ui/frame_preview.cljs @@ -0,0 +1,70 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.frame-preview + (:require + [app.common.data :as d] + [rumext.v2 :as mf])) + +(mf/defc frame-preview + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [] + + (let [iframe-ref (mf/use-ref nil) + last-data* (mf/use-state nil) + + zoom-ref (mf/use-ref nil) + zoom* (mf/use-state 1) + zoom @zoom* + + + handle-load + (mf/use-callback + (fn [data width height] + (reset! last-data* data) + (let [iframe-dom (mf/ref-val iframe-ref)] + (when iframe-dom + (-> iframe-dom (aset "width" (+ width 64))) + (-> iframe-dom (aset "height" (+ height 64))) + (-> iframe-dom .-contentWindow .-document .open) + (-> iframe-dom .-contentWindow .-document (.write data)) + (-> iframe-dom .-contentWindow .-document .close))))) + + load-ref + (mf/use-callback + (fn [iframe-dom] + (.log js/console "load-ref" iframe-dom) + (mf/set-ref-val! iframe-ref iframe-dom) + (when (and iframe-dom @last-data*) + (-> iframe-dom .-contentWindow .-document .open) + (-> iframe-dom .-contentWindow .-document (.write @last-data*)) + (-> iframe-dom .-contentWindow .-document .close)))) + + change-zoom + (mf/use-callback + (fn [] + (let [zoom-level (d/parse-integer (.-value (mf/ref-val zoom-ref)))] + (reset! zoom* (/ zoom-level 100)))))] + + (mf/use-effect + (fn [] + (aset js/window "load" handle-load) + #(js-delete js/window "load"))) + + [:div {:style {:display "flex" :width "100%" :height "100%" :flex-direction "column" :overflow "auto" :align-items "center"}} + [:input {:id "zoom-input" + :ref zoom-ref + :type "range" :min 1 :max 400 :default-value 100 + :on-change change-zoom + :style {:max-width "500px"}}] + + [:div {:style {:width "100%" :height "100%" :overflow "auto"}} + [:iframe {:ref load-ref + :frame-border "0" + :scrolling "no" + :style {:transform-origin "top left" + :transform (str "scale(" zoom ")")}}]]])) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index 8d08610405..bb05d2b1cc 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -1,3 +1,4 @@ + ;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. @@ -7,7 +8,8 @@ (ns app.main.ui.hooks "A collection of general purpose react hooks." (:require - [app.common.pages :as cp] + [app.common.files.focus :as cpf] + [app.common.math :as mth] [app.main.broadcast :as mbc] [app.main.data.shortcuts :as dsc] [app.main.refs :as refs] @@ -16,17 +18,26 @@ [app.util.dom.dnd :as dnd] [app.util.storage :refer [storage]] [app.util.timers :as ts] - [beicon.core :as rx] + [app.util.webapi :as wapi] + [beicon.v2.core :as rx] + [beicon.v2.operators :as rxo] [goog.functions :as f] [rumext.v2 :as mf])) +(def ^:private render-id 0) + +(defn use-render-id + "Get a stable, DOM usable identifier across all react rerenders" + [] + (mf/useMemo #(js* "\"render-\" + (++~{})" render-id) #js [])) + (defn use-rxsub [ob] (let [[state reset-state!] (mf/useState #(if (satisfies? IDeref ob) @ob nil))] (mf/useEffect (fn [] - (let [sub (rx/subscribe ob #(reset-state! %))] - #(rx/cancel! sub))) + (let [sub (rx/sub! ob #(reset-state! %))] + #(rx/dispose! sub))) #js [ob]) state)) @@ -39,13 +50,6 @@ (fn [] (st/emit! (dsc/pop-shortcuts key)))))) -(defn invisible-image - [] - (let [img (js/Image.) - imd "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="] - (set! (.-src img) imd) - img)) - (defn- set-timer [state ms func] (assoc state :timer (ts/schedule ms func))) @@ -96,7 +100,7 @@ cleanup (fn [] - (some-> (:subscr @state) rx/unsub!) + (some-> (:subscr @state) rx/dispose!) (swap! state (fn [state] (-> state (cancel-timer) @@ -111,11 +115,13 @@ on-drag-start (fn [event] (if (or disabled (not draggable?)) - (dom/prevent-default event) + (do + (dom/stop-propagation event) + (dom/prevent-default event)) (do (dom/stop-propagation event) (dnd/set-data! event data-type data) - (dnd/set-drag-image! event (invisible-image)) + (dnd/set-drag-image! event (dnd/invisible-image)) (dnd/set-allowed-effect! event "move") (when (fn? on-drag) (on-drag data))))) @@ -159,7 +165,7 @@ (cleanup) (rx/push! global-drag-end nil) (when (fn? on-drop) - (on-drop side drop-data)))) + (on-drop side drop-data event)))) on-drag-end (fn [event] @@ -171,7 +177,9 @@ on-mount (fn [] (let [dom (mf/ref-val ref)] - (.setAttribute dom "draggable" draggable?) + ;; In firefox it needs to be draggable for problems with event handling. + ;; It will stop the drag operation in on-drag-start + (.setAttribute dom "draggable" (and draggable? (not disabled))) ;; Register all events in the (default) bubble mode, so that they ;; are captured by the most leaf item. The handler will stop @@ -201,11 +209,15 @@ ([stream on-subscribe] (use-stream stream (mf/deps) on-subscribe)) ([stream deps on-subscribe] + (use-stream stream deps on-subscribe nil)) + ([stream deps on-subscribe on-dispose] (mf/use-effect deps (fn [] - (let [sub (->> stream (rx/subs on-subscribe))] - #(rx/dispose! sub)))))) + (let [sub (->> stream (rx/subs! on-subscribe))] + #(do + (rx/dispose! sub) + (when on-dispose (on-dispose)))))))) ;; https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state (defn use-previous @@ -226,6 +238,13 @@ (reset! ptr value)) ptr)) +(defn use-update-ref + [value] + (let [ref (mf/use-ref value)] + (mf/with-effect [value] + (mf/set-ref-val! ref value)) + ref)) + (defn use-ref-callback "Returns a stable callback pointer what calls the interned callback. The interned callback will be automatically updated on @@ -248,16 +267,15 @@ (mf/set-ref-val! ref val)) (mf/ref-val ref))) +;; FIXME: rename to use-focus-objects (defn with-focus-objects ([objects] (let [focus (mf/deref refs/workspace-focus-selected)] (with-focus-objects objects focus))) ([objects focus] - (let [objects (mf/use-memo - (mf/deps focus objects) - #(cp/focus-objects objects focus))] - objects))) + (mf/with-memo [focus objects] + (cpf/focus-objects objects focus)))) (defn use-debounce [ms value] @@ -296,11 +314,14 @@ (fn [entries _] (run! (partial rx/push! intersection-subject) (seq entries))) #js {:rootMargin "0px" - :threshold 1.0}))) + :threshold #js [0 1.0]}))) (defn use-visible [ref & {:keys [once?]}] - (let [[state update-state!] (mf/useState false)] + (let [state (mf/useState false) + update-state! (aget state 1) + state (aget state 0)] + (mf/with-effect [once?] (let [node (mf/ref-val ref) stream (->> intersection-subject @@ -308,16 +329,17 @@ (let [target (unchecked-get entry "target")] (identical? target node)))) (rx/map (fn [entry] - (let [ratio (unchecked-get entry "intersectionRatio") - intersecting? (unchecked-get entry "isIntersecting")] - (or intersecting? (> ratio 0.5))))) - (rx/dedupe)) - stream (if once? - (->> stream - (rx/filter identity) - (rx/take 1)) - stream) - subs (rx/subscribe stream update-state!)] + (let [ratio (unchecked-get entry "intersectionRatio") + intersecting? (unchecked-get entry "isIntersecting") + intersecting? (or ^boolean intersecting? + ^boolean (> ratio 0.5))] + (when (and (true? intersecting?) (true? once?)) + (.unobserve ^js @intersection-observer node)) + + intersecting?))) + + (rx/pipe (rxo/distinct-contiguous))) + subs (rx/sub! stream update-state!)] (.observe ^js @intersection-observer node) (fn [] (.unobserve ^js @intersection-observer node) @@ -325,3 +347,49 @@ state)) +(defn use-dynamic-grid-item-width + ([] (use-dynamic-grid-item-width nil)) + ([itemsize] + (let [width* (mf/use-state nil) + width (deref width*) + + rowref (mf/use-ref) + + itemsize (cond + (some? itemsize) itemsize + (>= width 1030) 280 + :else 230) + + ratio (if (some? width) (/ width itemsize) 0) + nitems (mth/floor ratio) + limit (mth/min 10 nitems) + limit (mth/max 1 limit) + + th-size (when width + (mth/floor (- (/ (- width 32 (* (dec limit) 24)) limit) 12))) + + ;; Need an even value + th-size (if (odd? (int th-size)) (- th-size 1) th-size)] + + (mf/with-effect + [th-size] + (when th-size + (let [node (mf/ref-val rowref)] + (.setProperty (.-style node) "--th-width" (str th-size "px")) + (.setProperty (.-style node) "--th-height" (str (mth/ceil (* th-size (/ 2 3))) "px"))))) + + (mf/with-effect [] + (let [node (mf/ref-val rowref) + mnt? (volatile! true) + sub (->> (wapi/observe-resize node) + (rx/subs! (fn [entries] + (let [row (first entries) + row-rect (.-contentRect ^js row) + row-width (.-width ^js row-rect)] + (when @mnt? + (reset! width* row-width))))))] + (fn [] + (vreset! mnt? false) + (rx/dispose! sub)))) + + [rowref limit]))) diff --git a/frontend/src/app/main/ui/hooks/resize.cljs b/frontend/src/app/main/ui/hooks/resize.cljs index 2811805a06..148cdf773d 100644 --- a/frontend/src/app/main/ui/hooks/resize.cljs +++ b/frontend/src/app/main/ui/hooks/resize.cljs @@ -9,6 +9,7 @@ [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.logging :as log] + [app.common.math :as mth] [app.main.ui.context :as ctx] [app.main.ui.hooks :as hooks] [app.util.dom :as dom] @@ -23,54 +24,72 @@ (set! last-resize-type type)) (defn use-resize-hook - [key initial min-val max-val axis negate? resize-type] + ([key initial min-val max-val axis negate? resize-type] + (use-resize-hook key initial min-val max-val axis negate? resize-type nil)) - (let [current-file-id (mf/use-ctx ctx/current-file-id) - size-state (mf/use-state (or (get-in @storage [::saved-resize current-file-id key]) initial)) - parent-ref (mf/use-ref nil) + ([key initial min-val max-val axis negate? resize-type on-change-size] + (let [current-file-id (mf/use-ctx ctx/current-file-id) + size-state (mf/use-state (or (get-in @storage [::saved-resize current-file-id key]) initial)) + parent-ref (mf/use-ref nil) - dragging-ref (mf/use-ref false) - start-size-ref (mf/use-ref nil) - start-ref (mf/use-ref nil) + dragging-ref (mf/use-ref false) + start-size-ref (mf/use-ref nil) + start-ref (mf/use-ref nil) - on-pointer-down - (mf/use-callback - (mf/deps @size-state) - (fn [event] - (dom/capture-pointer event) - (mf/set-ref-val! start-size-ref @size-state) - (mf/set-ref-val! dragging-ref true) - (mf/set-ref-val! start-ref (dom/get-client-position event)) - (set! last-resize-type resize-type))) + on-pointer-down + (mf/use-callback + (mf/deps @size-state) + (fn [event] + (dom/capture-pointer event) + (mf/set-ref-val! start-size-ref @size-state) + (mf/set-ref-val! dragging-ref true) + (mf/set-ref-val! start-ref (dom/get-client-position event)) + (set! last-resize-type resize-type))) - on-lost-pointer-capture - (mf/use-callback - (fn [event] - (dom/release-pointer event) - (mf/set-ref-val! start-size-ref nil) - (mf/set-ref-val! dragging-ref false) - (mf/set-ref-val! start-ref nil) - (set! last-resize-type nil))) + on-lost-pointer-capture + (mf/use-callback + (fn [event] + (dom/release-pointer event) + (mf/set-ref-val! start-size-ref nil) + (mf/set-ref-val! dragging-ref false) + (mf/set-ref-val! start-ref nil) + (set! last-resize-type nil))) - on-pointer-move - (mf/use-callback - (mf/deps min-val max-val negate?) - (fn [event] - (when (mf/ref-val dragging-ref) - (let [start (mf/ref-val start-ref) - pos (dom/get-client-position event) - delta (-> (gpt/to-vec start pos) - (cond-> negate? gpt/negate) - (get axis)) - start-size (mf/ref-val start-size-ref) - new-size (-> (+ start-size delta) (max min-val) (min max-val))] - (reset! size-state new-size) - (swap! storage assoc-in [::saved-resize current-file-id key] new-size)))))] - {:on-pointer-down on-pointer-down - :on-lost-pointer-capture on-lost-pointer-capture - :on-pointer-move on-pointer-move - :parent-ref parent-ref - :size @size-state})) + on-pointer-move + (mf/use-callback + (mf/deps min-val max-val negate?) + (fn [event] + (when (mf/ref-val dragging-ref) + (let [start (mf/ref-val start-ref) + pos (dom/get-client-position event) + delta (-> (gpt/to-vec start pos) + (cond-> negate? gpt/negate) + (get axis)) + start-size (mf/ref-val start-size-ref) + new-size (-> (+ start-size delta) (max min-val) (min max-val))] + (reset! size-state new-size) + (swap! storage assoc-in [::saved-resize current-file-id key] new-size) + (when on-change-size (on-change-size new-size)))))) + + set-size + (mf/use-callback + (mf/deps on-change-size) + (fn [new-size] + (let [new-size (mth/clamp new-size min-val max-val)] + (reset! size-state new-size) + (swap! storage assoc-in [::saved-resize current-file-id key] new-size) + (when on-change-size (on-change-size new-size)))))] + + (mf/use-effect + (fn [] + (when on-change-size (on-change-size @size-state)))) + + {:on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move + :parent-ref parent-ref + :set-size set-size + :size @size-state}))) (defn use-resize-observer [callback] diff --git a/frontend/src/app/main/ui/icons.clj b/frontend/src/app/main/ui/icons.clj index 668ac50b78..600dc7e02f 100644 --- a/frontend/src/app/main/ui/icons.clj +++ b/frontend/src/app/main/ui/icons.clj @@ -5,13 +5,27 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.icons - (:require [rumext.v2])) + (:require + [clojure.core :as c] + [cuerdas.core :as str] + [rumext.v2])) + +(def exceptions #{:penpot-logo-icon}) (defmacro icon-xref - [id] + [id & [class]] (let [href (str "#icon-" (name id)) - class (str "icon-" (name id))] + class (or class (str "icon-" (name id)))] `(rumext.v2/html [:svg {:width 500 :height 500 :class ~class} [:use {:href ~href}]]))) +(defmacro collect-icons + [] + (let [ns-info (:ns &env)] + `(cljs.core/js-obj + ~@(->> (:defs ns-info) + (map val) + (filter (fn [entry] (-> entry :meta :icon))) + (mapcat (fn [{:keys [name] :as entry}] + [(-> name c/name str/camel str/capital) name])))))) diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index 7e338c7f5c..002ef71a85 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -6,376 +6,260 @@ (ns app.main.ui.icons (:refer-clojure :exclude [import mask]) - (:require-macros [app.main.ui.icons :refer [icon-xref]]) - (:require [rumext.v2 :as mf])) + (:require-macros [app.main.ui.icons :refer [icon-xref collect-icons]]) + (:require + [app.common.data :as d] + [cuerdas.core :as str] + [rumext.v2 :as mf])) ;; Keep the list of icons sorted +(def ^:icon icon-verify (icon-xref :icon-verify)) +(def ^:icon loader (icon-xref :loader)) +(def ^:icon logo (icon-xref :penpot-logo)) +(def ^:icon logo-icon (icon-xref :penpot-logo-icon)) +(def ^:icon logo-error-screen (icon-xref :logo-error-screen)) +(def ^:icon login-illustration (icon-xref :login-illustration)) -(def action (icon-xref :action)) -(def actions (icon-xref :actions)) -(def align-bottom (icon-xref :align-bottom)) -(def align-content-column-around (icon-xref :align-content-column-around)) -(def align-content-column-evenly (icon-xref :align-content-column-evenly)) -(def align-content-column-between (icon-xref :align-content-column-between)) -(def align-content-column-center (icon-xref :align-content-column-center)) -(def align-content-column-end (icon-xref :align-content-column-end)) -(def align-content-column-start (icon-xref :align-content-column-start)) -(def align-content-row-around (icon-xref :align-content-row-around)) -(def align-content-row-evenly (icon-xref :align-content-row-evenly)) -(def align-content-row-between (icon-xref :align-content-row-between)) -(def align-content-row-center (icon-xref :align-content-row-center)) -(def align-content-row-end (icon-xref :align-content-row-end)) -(def align-content-row-start (icon-xref :align-content-row-start)) -(def align-items-column-baseline (icon-xref :align-items-column-baseline)) -(def align-items-column-center (icon-xref :align-items-column-center)) -(def align-items-column-end (icon-xref :align-items-column-end)) -(def align-items-column-start (icon-xref :align-items-column-start)) -(def align-items-column-strech (icon-xref :align-items-column-strech)) -(def align-items-row-baseline (icon-xref :align-items-row-baseline)) -(def align-items-row-center (icon-xref :align-items-row-center)) -(def align-items-row-end (icon-xref :align-items-row-end)) -(def align-items-row-start (icon-xref :align-items-row-start)) -(def align-items-row-strech (icon-xref :align-items-row-strech)) -(def align-self-column-baseline (icon-xref :align-self-column-baseline)) -(def align-self-column-center (icon-xref :align-self-column-center)) -(def align-self-column-top (icon-xref :align-self-column-top)) -(def align-self-column-bottom (icon-xref :align-self-column-bottom)) -(def align-self-column-strech (icon-xref :align-self-column-strech)) -(def align-self-row-baseline (icon-xref :align-self-row-baseline)) -(def align-self-row-center (icon-xref :align-self-row-center)) -(def align-self-row-left (icon-xref :align-self-row-left)) -(def align-self-row-right (icon-xref :align-self-row-right)) -(def align-self-row-strech (icon-xref :align-self-row-strech)) -(def align-middle (icon-xref :align-middle)) -(def align-top (icon-xref :align-top)) -(def alignment (icon-xref :alignment)) -(def animate-down (icon-xref :animate-down)) -(def animate-left (icon-xref :animate-left)) -(def animate-right (icon-xref :animate-right)) -(def animate-up (icon-xref :animate-up)) -(def arrow-down (icon-xref :arrow-down)) -(def arrow-end (icon-xref :arrow-end)) -(def arrow-slide (icon-xref :arrow-slide)) -(def arrow-up (icon-xref :arrow-up)) -(def artboard (icon-xref :artboard)) -(def at (icon-xref :at)) -(def auto-direction (icon-xref :auto-direction)) -(def auto-fill (icon-xref :auto-fill)) -(def auto-fix (icon-xref :auto-fix)) -(def auto-fix-layout (icon-xref :auto-fix-layout)) -(def auto-gap (icon-xref :auto-gap)) -(def auto-height (icon-xref :auto-height)) -(def auto-hug (icon-xref :auto-hug)) -(def auto-margin-side (icon-xref :auto-margin-side)) -(def auto-margin-both-sides (icon-xref :auto-margin-both-sides)) -(def auto-margin (icon-xref :auto-margin)) -(def auto-padding (icon-xref :auto-padding)) -(def auto-padding-side (icon-xref :auto-padding-side)) -(def auto-padding-both-sides (icon-xref :auto-padding-both-sides)) -(def auto-width (icon-xref :auto-width)) -(def auto-wrap (icon-xref :auto-wrap)) -(def bool-difference (icon-xref :boolean-difference)) -(def bool-exclude (icon-xref :boolean-exclude)) -(def bool-flatten (icon-xref :boolean-flatten)) -(def bool-intersection (icon-xref :boolean-intersection)) -(def bool-union (icon-xref :boolean-union)) -(def box (icon-xref :box)) -(def bug (icon-xref :bug)) -(def chain (icon-xref :chain)) -(def chat (icon-xref :chat)) -(def checkbox-checked (icon-xref :checkbox-checked)) -(def checkbox-unchecked (icon-xref :checkbox-unchecked)) -(def checkbox-intermediate (icon-xref :checkbox-intermediate)) -(def circle (icon-xref :circle)) -(def close (icon-xref :close)) -(def code (icon-xref :code)) -(def component (icon-xref :component)) -(def component-copy (icon-xref :component-copy)) -(def copy (icon-xref :copy)) -(def curve (icon-xref :curve)) -(def cross (icon-xref :cross)) -(def download (icon-xref :download)) -(def easing-linear (icon-xref :easing-linear)) -(def easing-ease (icon-xref :easing-ease)) -(def easing-ease-in (icon-xref :easing-ease-in)) -(def easing-ease-out (icon-xref :easing-ease-out)) -(def easing-ease-in-out (icon-xref :easing-ease-in-out)) -(def exclude (icon-xref :exclude)) -(def exit (icon-xref :exit)) -(def export (icon-xref :export)) -(def eye (icon-xref :eye)) -(def eye-closed (icon-xref :eye-closed)) -(def file-html (icon-xref :file-html)) -(def file-svg (icon-xref :file-svg)) -(def fill (icon-xref :fill)) -(def folder (icon-xref :folder)) -(def folder-zip (icon-xref :folder-zip)) -(def full-screen (icon-xref :full-screen)) -(def full-screen-off (icon-xref :full-screen-off)) -(def grid (icon-xref :grid)) -(def grid-justify-content-column-around (icon-xref :grid-justify-content-column-around)) -(def grid-justify-content-column-between (icon-xref :grid-justify-content-column-between)) -(def grid-justify-content-column-center (icon-xref :grid-justify-content-column-center)) -(def grid-justify-content-column-end (icon-xref :grid-justify-content-column-end)) -(def grid-justify-content-column-start (icon-xref :grid-justify-content-column-start)) -(def grid-justify-content-row-around (icon-xref :grid-justify-content-row-around)) -(def grid-justify-content-row-between (icon-xref :grid-justify-content-row-between)) -(def grid-justify-content-row-center (icon-xref :grid-justify-content-row-center)) -(def grid-justify-content-row-end (icon-xref :grid-justify-content-row-end)) -(def grid-justify-content-row-start (icon-xref :grid-justify-content-row-start)) -(def grid-layout-mode (icon-xref :grid-layout-mode)) -(def grid-snap (icon-xref :grid-snap)) -(def go-next (icon-xref :go-next)) -(def go-prev (icon-xref :go-prev)) -(def help (icon-xref :help)) -(def icon-empty (icon-xref :icon-empty)) -(def icon-filter (icon-xref :filter)) -(def icon-list (icon-xref :icon-list)) -(def icon-lock (icon-xref :icon-lock)) -(def icon-set (icon-xref :icon-set)) -(def icon-verify (icon-xref :icon-verify)) -(def image (icon-xref :image)) -(def import (icon-xref :import)) -(def infocard (icon-xref :infocard)) -(def interaction (icon-xref :interaction)) -(def justify-content-column-around (icon-xref :justify-content-column-around)) -(def justify-content-column-evenly (icon-xref :justify-content-column-evenly)) -(def justify-content-column-between (icon-xref :justify-content-column-between)) -(def justify-content-column-center (icon-xref :justify-content-column-center)) -(def justify-content-column-end (icon-xref :justify-content-column-end)) -(def justify-content-column-start (icon-xref :justify-content-column-start)) -(def justify-content-row-around (icon-xref :justify-content-row-around)) -(def justify-content-row-evenly (icon-xref :justify-content-row-evenly)) -(def justify-content-row-between (icon-xref :justify-content-row-between)) -(def justify-content-row-center (icon-xref :justify-content-row-center)) -(def justify-content-row-end (icon-xref :justify-content-row-end)) -(def justify-content-row-start (icon-xref :justify-content-row-start)) -(def icon-key (icon-xref :icon-key)) -(def layers (icon-xref :layers)) -(def layout-columns (icon-xref :layout-columns)) -(def layout-rows (icon-xref :layout-rows)) -(def letter-spacing (icon-xref :letter-spacing)) -(def libraries (icon-xref :libraries)) -(def library (icon-xref :library)) -(def line (icon-xref :line)) -(def line-height (icon-xref :line-height)) -(def listing-enum (icon-xref :listing-enum)) -(def listing-thumbs (icon-xref :listing-thumbs)) -(def loader (icon-xref :loader)) -(def lock (icon-xref :lock)) -(def logo (icon-xref :penpot-logo)) -(def logo-icon (icon-xref :penpot-logo-icon)) -(def logout (icon-xref :logout)) -(def lowercase (icon-xref :lowercase)) -(def mail (icon-xref :mail)) -(def mask (icon-xref :mask)) -(def minus (icon-xref :minus)) -(def move (icon-xref :move)) -(def msg-error (icon-xref :msg-error)) -(def msg-info (icon-xref :msg-info)) -(def msg-success (icon-xref :msg-success)) -(def msg-warning (icon-xref :msg-warning)) -(def navigate (icon-xref :navigate)) -(def nodes-add (icon-xref :nodes-add)) -(def nodes-corner (icon-xref :nodes-corner)) -(def nodes-curve (icon-xref :nodes-curve)) -(def nodes-join (icon-xref :nodes-join)) -(def nodes-merge (icon-xref :nodes-merge)) -(def nodes-remove (icon-xref :nodes-remove)) -(def nodes-separate (icon-xref :nodes-separate)) -(def nodes-snap (icon-xref :nodes-snap)) -(def organize (icon-xref :organize)) -(def palette (icon-xref :palette)) -(def pen (icon-xref :pen)) -(def pencil (icon-xref :pencil)) -(def picker (icon-xref :picker)) -(def picker-harmony (icon-xref :picker-harmony)) -(def picker-hsv (icon-xref :picker-hsv)) -(def picker-ramp (icon-xref :picker-ramp)) -(def pin (icon-xref :pin)) -(def pin-fill (icon-xref :pin-fill)) -(def play (icon-xref :play)) -(def plus (icon-xref :plus)) -(def pointer-inner (icon-xref :pointer-inner)) -(def position-absolute (icon-xref :position-absolute)) -(def position-bottom-center (icon-xref :position-bottom-center)) -(def position-bottom-left (icon-xref :position-bottom-left)) -(def position-bottom-right (icon-xref :position-bottom-right)) -(def position-center (icon-xref :position-center)) -(def position-top-center (icon-xref :position-top-center)) -(def position-top-left (icon-xref :position-top-left)) -(def position-top-right (icon-xref :position-top-right)) -(def radius (icon-xref :radius)) -(def radius-1 (icon-xref :radius-1)) -(def radius-4 (icon-xref :radius-4)) -(def recent (icon-xref :recent)) -(def redo (icon-xref :redo)) -(def reset (icon-xref :reset)) -(def rotate (icon-xref :rotate)) -(def ruler (icon-xref :ruler)) -(def ruler-tool (icon-xref :ruler-tool)) -(def search (icon-xref :search)) -(def set-thumbnail (icon-xref :set-thumbnail)) -(def shape-halign-center (icon-xref :shape-halign-center)) -(def shape-halign-left (icon-xref :shape-halign-left)) -(def shape-halign-right (icon-xref :shape-halign-right)) -(def shape-hdistribute (icon-xref :shape-hdistribute)) -(def shape-valign-bottom (icon-xref :shape-valign-bottom)) -(def shape-valign-center (icon-xref :shape-valign-center)) -(def shape-valign-top (icon-xref :shape-valign-top)) -(def shape-vdistribute (icon-xref :shape-vdistribute)) -(def shortcut (icon-xref :shortcut)) -(def size-horiz (icon-xref :size-horiz)) -(def size-vert (icon-xref :size-vert)) -(def sort-ascending (icon-xref :sort-ascending)) -(def sort-descending (icon-xref :sort-descending)) -(def space-around (icon-xref :space-around)) -(def space-between (icon-xref :space-between)) -(def strikethrough (icon-xref :strikethrough)) -(def stroke (icon-xref :stroke)) -(def switch (icon-xref :switch)) -(def text (icon-xref :text)) -(def text-align-center (icon-xref :text-align-center)) -(def text-align-justify (icon-xref :text-align-justify)) -(def text-align-left (icon-xref :text-align-left)) -(def text-align-right (icon-xref :text-align-right)) -(def text-direction-ltr (icon-xref :text-direction-ltr)) -(def text-direction-rtl (icon-xref :text-direction-rtl)) -(def tick (icon-xref :tick)) -(def titlecase (icon-xref :titlecase)) -(def toggle (icon-xref :toggle)) -(def trash (icon-xref :trash)) -(def tree (icon-xref :tree)) -(def unchain (icon-xref :unchain)) -(def underline (icon-xref :underline)) -(def undo (icon-xref :undo)) -(def ungroup (icon-xref :ungroup)) -(def unlock (icon-xref :unlock)) -(def uppercase (icon-xref :uppercase)) -(def user (icon-xref :user)) +(def ^:icon brand-openid (icon-xref :brand-openid)) +(def ^:icon brand-github (icon-xref :brand-github)) +(def ^:icon brand-gitlab (icon-xref :brand-gitlab)) +(def ^:icon brand-google (icon-xref :brand-google)) -(def brand-openid (icon-xref :brand-openid)) -(def brand-github (icon-xref :brand-github)) -(def brand-gitlab (icon-xref :brand-gitlab)) -(def brand-google (icon-xref :brand-google)) +(def ^:icon absolute (icon-xref :absolute)) +(def ^:icon add (icon-xref :add)) +(def ^:icon align-bottom (icon-xref :align-bottom)) +(def ^:icon align-content-column-around (icon-xref :align-content-column-around)) +(def ^:icon align-content-column-between (icon-xref :align-content-column-between)) +(def ^:icon align-content-column-center (icon-xref :align-content-column-center)) +(def ^:icon align-content-column-end (icon-xref :align-content-column-end)) +(def ^:icon align-content-column-evenly (icon-xref :align-content-column-evenly)) +(def ^:icon align-content-column-start (icon-xref :align-content-column-start)) +(def ^:icon align-content-column-stretch (icon-xref :align-content-column-stretch)) +(def ^:icon align-content-row-around (icon-xref :align-content-row-around)) +(def ^:icon align-content-row-between (icon-xref :align-content-row-between)) +(def ^:icon align-content-row-center (icon-xref :align-content-row-center)) +(def ^:icon align-content-row-end (icon-xref :align-content-row-end)) +(def ^:icon align-content-row-evenly (icon-xref :align-content-row-evenly)) +(def ^:icon align-content-row-start (icon-xref :align-content-row-start)) +(def ^:icon align-content-row-stretch (icon-xref :align-content-row-stretch)) +(def ^:icon align-horizontal-center (icon-xref :align-horizontal-center)) +(def ^:icon align-items-column-center (icon-xref :align-items-column-center)) +(def ^:icon align-items-column-end (icon-xref :align-items-column-end)) +(def ^:icon align-items-column-start (icon-xref :align-items-column-start)) +(def ^:icon align-items-row-center (icon-xref :align-items-row-center)) +(def ^:icon align-items-row-end (icon-xref :align-items-row-end)) +(def ^:icon align-items-row-start (icon-xref :align-items-row-start)) +(def ^:icon align-left (icon-xref :align-left)) +(def ^:icon align-right (icon-xref :align-right)) +(def ^:icon align-self-column-bottom (icon-xref :align-self-column-bottom)) +(def ^:icon align-self-column-center (icon-xref :align-self-column-center)) +(def ^:icon align-self-column-stretch (icon-xref :align-self-column-stretch)) +(def ^:icon align-self-column-top (icon-xref :align-self-column-top)) +(def ^:icon align-self-row-center (icon-xref :align-self-row-center)) +(def ^:icon align-self-row-left (icon-xref :align-self-row-left)) +(def ^:icon align-self-row-right (icon-xref :align-self-row-right)) +(def ^:icon align-self-row-stretch (icon-xref :align-self-row-stretch)) +(def ^:icon align-top (icon-xref :align-top)) +(def ^:icon align-vertical-center (icon-xref :align-vertical-center)) +(def ^:icon arrow (icon-xref :arrow)) +(def ^:icon asc-sort (icon-xref :asc-sort)) +(def ^:icon board (icon-xref :board)) +(def ^:icon boards-thumbnail (icon-xref :boards-thumbnail)) +(def ^:icon boolean-difference (icon-xref :boolean-difference)) +(def ^:icon boolean-exclude (icon-xref :boolean-exclude)) +(def ^:icon boolean-flatten (icon-xref :boolean-flatten)) +(def ^:icon boolean-intersection (icon-xref :boolean-intersection)) +(def ^:icon boolean-union (icon-xref :boolean-union)) +(def ^:icon bug (icon-xref :bug)) +(def ^:icon clip-content (icon-xref :clip-content)) +(def ^:icon clipboard (icon-xref :clipboard)) +(def ^:icon close-small (icon-xref :close-small)) +(def ^:icon close (icon-xref :close)) +(def ^:icon code (icon-xref :code)) +(def ^:icon column-reverse (icon-xref :column-reverse)) +(def ^:icon column (icon-xref :column)) +(def ^:icon comments (icon-xref :comments)) +(def ^:icon component-copy (icon-xref :component-copy)) +(def ^:icon component (icon-xref :component)) +(def ^:icon constraint-horizontal (icon-xref :constraint-horizontal)) +(def ^:icon constraint-vertical (icon-xref :constraint-vertical)) +(def ^:icon corner-bottom-left (icon-xref :corner-bottom-left)) +(def ^:icon corner-bottom-right (icon-xref :corner-bottom-right)) +(def ^:icon corner-bottom (icon-xref :corner-bottom)) +(def ^:icon corner-center (icon-xref :corner-center)) +(def ^:icon corner-radius (icon-xref :corner-radius)) +(def ^:icon corner-top (icon-xref :corner-top)) +(def ^:icon corner-top-left (icon-xref :corner-top-left)) +(def ^:icon corner-top-right (icon-xref :corner-top-right)) +(def ^:icon curve (icon-xref :curve)) +(def ^:icon delete-text (icon-xref :delete-text)) +(def ^:icon delete (icon-xref :delete)) +(def ^:icon desc-sort (icon-xref :desc-sort)) +(def ^:icon detach (icon-xref :detach)) +(def ^:icon detached (icon-xref :detached)) +(def ^:icon distribute-horizontally (icon-xref :distribute-horizontally)) +(def ^:icon distribute-vertical-spacing (icon-xref :distribute-vertical-spacing)) +(def ^:icon document (icon-xref :document)) +(def ^:icon download (icon-xref :download)) +(def ^:icon drop-icon (icon-xref :drop)) +(def ^:icon easing-ease-in-out (icon-xref :easing-ease-in-out)) +(def ^:icon easing-ease-in (icon-xref :easing-ease-in)) +(def ^:icon easing-ease-out (icon-xref :easing-ease-out)) +(def ^:icon easing-ease (icon-xref :easing-ease)) +(def ^:icon easing-linear (icon-xref :easing-linear)) +(def ^:icon effects (icon-xref :effects)) +(def ^:icon elipse (icon-xref :elipse)) +(def ^:icon exit (icon-xref :exit)) +(def ^:icon expand (icon-xref :expand)) +(def ^:icon feedback (icon-xref :feedback)) +(def ^:icon fill-content (icon-xref :fill-content)) +(def ^:icon filter-icon (icon-xref :filter)) +(def ^:icon fixed-width (icon-xref :fixed-width)) +(def ^:icon flex-grid (icon-xref :flex-grid)) +(def ^:icon flex-horizontal (icon-xref :flex-horizontal)) +(def ^:icon flex-vertical (icon-xref :flex-vertical)) +(def ^:icon flex (icon-xref :flex)) +(def ^:icon flip-horizontal (icon-xref :flip-horizontal)) +(def ^:icon flip-vertical (icon-xref :flip-vertical)) +(def ^:icon gap-horizontal (icon-xref :gap-horizontal)) +(def ^:icon gap-vertical (icon-xref :gap-vertical)) +(def ^:icon graphics (icon-xref :graphics)) +(def ^:icon grid-column (icon-xref :grid-column)) +(def ^:icon grid-columns (icon-xref :grid-columns)) +(def ^:icon grid-gutter (icon-xref :grid-gutter)) +(def ^:icon grid-margin (icon-xref :grid-margin)) +(def ^:icon grid (icon-xref :grid)) +(def ^:icon grid-row (icon-xref :grid-row)) +(def ^:icon grid-rows (icon-xref :grid-rows)) +(def ^:icon grid-square (icon-xref :grid-square)) +(def ^:icon group (icon-xref :group)) +(def ^:icon gutter-horizontal (icon-xref :gutter-horizontal)) +(def ^:icon gutter-vertical (icon-xref :gutter-vertical)) +(def ^:icon help (icon-xref :help)) +(def ^:icon hide (icon-xref :hide)) +(def ^:icon history (icon-xref :history)) +(def ^:icon hsva (icon-xref :hsva)) +(def ^:icon hug-content (icon-xref :hug-content)) +(def ^:icon icon (icon-xref :icon)) +(def ^:icon img (icon-xref :img)) +(def ^:icon interaction (icon-xref :interaction)) +(def ^:icon join-nodes (icon-xref :join-nodes)) +(def ^:icon justify-content-column-around (icon-xref :justify-content-column-around)) +(def ^:icon justify-content-column-between (icon-xref :justify-content-column-between)) +(def ^:icon justify-content-column-center (icon-xref :justify-content-column-center)) +(def ^:icon justify-content-column-end (icon-xref :justify-content-column-end)) +(def ^:icon justify-content-column-evenly (icon-xref :justify-content-column-evenly)) +(def ^:icon justify-content-column-start (icon-xref :justify-content-column-start)) +(def ^:icon justify-content-row-around (icon-xref :justify-content-row-around)) +(def ^:icon justify-content-row-between (icon-xref :justify-content-row-between)) +(def ^:icon justify-content-row-center (icon-xref :justify-content-row-center)) +(def ^:icon justify-content-row-end (icon-xref :justify-content-row-end)) +(def ^:icon justify-content-row-evenly (icon-xref :justify-content-row-evenly)) +(def ^:icon justify-content-row-start (icon-xref :justify-content-row-start)) +(def ^:icon layers (icon-xref :layers)) +(def ^:icon library (icon-xref :library)) +(def ^:icon locate (icon-xref :locate)) +(def ^:icon lock (icon-xref :lock)) +(def ^:icon margin-bottom (icon-xref :margin-bottom)) +(def ^:icon margin-left (icon-xref :margin-left)) +(def ^:icon margin-left-right (icon-xref :margin-left-right)) +(def ^:icon margin-right (icon-xref :margin-right)) +(def ^:icon margin-top-bottom (icon-xref :margin-top-bottom)) +(def ^:icon margin-top (icon-xref :margin-top)) +(def ^:icon margin (icon-xref :margin)) +(def ^:icon mask (icon-xref :mask)) +(def ^:icon masked (icon-xref :masked)) +(def ^:icon menu (icon-xref :menu)) +(def ^:icon merge-nodes (icon-xref :merge-nodes)) +(def ^:icon move (icon-xref :move)) +(def ^:icon msg-error (icon-xref :msg-error)) +(def ^:icon msg-neutral (icon-xref :msg-neutral)) +(def ^:icon msg-success (icon-xref :msg-success)) +(def ^:icon msg-warning (icon-xref :msg-warning)) +(def ^:icon open-link (icon-xref :open-link)) +(def ^:icon padding-bottom (icon-xref :padding-bottom)) +(def ^:icon padding-extended (icon-xref :padding-extended)) +(def ^:icon padding-left-right (icon-xref :padding-left-right)) +(def ^:icon padding-left (icon-xref :padding-left)) +(def ^:icon padding-right (icon-xref :padding-right)) +(def ^:icon padding-top-bottom (icon-xref :padding-top-bottom)) +(def ^:icon padding-top (icon-xref :padding-top)) +(def ^:icon path (icon-xref :path)) +(def ^:icon pentool (icon-xref :pentool)) +(def ^:icon picker (icon-xref :picker)) +(def ^:icon pin (icon-xref :pin)) +(def ^:icon play (icon-xref :play)) +(def ^:icon rectangle (icon-xref :rectangle)) +(def ^:icon reload (icon-xref :reload)) +(def ^:icon remove-icon (icon-xref :remove)) +(def ^:icon rgba-complementary (icon-xref :rgba-complementary)) +(def ^:icon rgba (icon-xref :rgba)) +(def ^:icon rotation (icon-xref :rotation)) +(def ^:icon row-reverse (icon-xref :row-reverse)) +(def ^:icon row (icon-xref :row)) +(def ^:icon search (icon-xref :search)) +(def ^:icon separate-nodes (icon-xref :separate-nodes)) +(def ^:icon shown (icon-xref :shown)) +(def ^:icon size-horizontal (icon-xref :size-horizontal)) +(def ^:icon size-vertical (icon-xref :size-vertical)) +(def ^:icon snap-nodes (icon-xref :snap-nodes)) +(def ^:icon status-alert (icon-xref :status-alert)) +(def ^:icon status-tick (icon-xref :status-tick)) +(def ^:icon status-update (icon-xref :status-update)) +(def ^:icon status-wrong (icon-xref :status-wrong)) +(def ^:icon stroke-arrow (icon-xref :stroke-arrow)) +(def ^:icon stroke-circle (icon-xref :stroke-circle)) +(def ^:icon stroke-diamond (icon-xref :stroke-diamond)) +(def ^:icon stroke-rectangle (icon-xref :stroke-rectangle)) +(def ^:icon stroke-rounded (icon-xref :stroke-rounded)) +(def ^:icon stroke-size (icon-xref :stroke-size)) +(def ^:icon stroke-squared (icon-xref :stroke-squared)) +(def ^:icon stroke-triangle (icon-xref :stroke-triangle)) +(def ^:icon svg (icon-xref :svg)) +(def ^:icon swatches (icon-xref :swatches)) +(def ^:icon switch (icon-xref :switch)) +(def ^:icon text-align-center (icon-xref :text-align-center)) +(def ^:icon text-align-left (icon-xref :text-align-left)) +(def ^:icon text-align-right (icon-xref :text-align-right)) +(def ^:icon text-auto-height (icon-xref :text-auto-height)) +(def ^:icon text-auto-width (icon-xref :text-auto-width)) +(def ^:icon text-bottom (icon-xref :text-bottom)) +(def ^:icon text-fixed (icon-xref :text-fixed)) +(def ^:icon text-justify (icon-xref :text-justify)) +(def ^:icon text-letterspacing (icon-xref :text-letterspacing)) +(def ^:icon text-lineheight (icon-xref :text-lineheight)) +(def ^:icon text-lowercase (icon-xref :text-lowercase)) +(def ^:icon text-ltr (icon-xref :text-ltr)) +(def ^:icon text-middle (icon-xref :text-middle)) +(def ^:icon text-mixed (icon-xref :text-mixed)) +(def ^:icon text-palette (icon-xref :text-palette)) +(def ^:icon text-paragraph (icon-xref :text-paragraph)) +(def ^:icon text-rtl (icon-xref :text-rtl)) +(def ^:icon text-stroked (icon-xref :text-stroked)) +(def ^:icon text-top (icon-xref :text-top)) +(def ^:icon text-underlined (icon-xref :text-underlined)) +(def ^:icon text-uppercase (icon-xref :text-uppercase)) +(def ^:icon text (icon-xref :text)) +(def ^:icon thumbnail (icon-xref :thumbnail)) +(def ^:icon tick (icon-xref :tick)) +(def ^:icon to-corner (icon-xref :to-corner)) +(def ^:icon to-curve (icon-xref :to-curve)) +(def ^:icon tree (icon-xref :tree)) +(def ^:icon unlock (icon-xref :unlock)) +(def ^:icon user (icon-xref :user)) +(def ^:icon v2-icon-1 (icon-xref :v2-icon-1)) +(def ^:icon v2-icon-2 (icon-xref :v2-icon-2)) +(def ^:icon v2-icon-3 (icon-xref :v2-icon-3)) +(def ^:icon v2-icon-4 (icon-xref :v2-icon-4)) +(def ^:icon vertical-align-items-center (icon-xref :vertical-align-items-center)) +(def ^:icon vertical-align-items-end (icon-xref :vertical-align-items-end)) +(def ^:icon vertical-align-items-start (icon-xref :vertical-align-items-start)) +(def ^:icon view-as-icons (icon-xref :view-as-icons)) +(def ^:icon view-as-list (icon-xref :view-as-list)) +(def ^:icon wrap (icon-xref :wrap)) -(def add-refactor (icon-xref :add-refactor)) -(def arrow-refactor (icon-xref :arrow-refactor)) -(def absolute-refactor (icon-xref :absolute-refactor)) -(def align-bottom-refactor (icon-xref :align-bottom-refactor)) -(def align-content-center-refactor (icon-xref :align-content-center-refactor)) -(def align-content-end-refactor (icon-xref :align-content-end-refactor)) -(def align-content-space-around-refactor (icon-xref :align-content-space-around-refactor)) -(def align-content-space-between-refactor (icon-xref :align-content-space-between-refactor)) -(def align-content-space-evenly-refactor (icon-xref :align-content-space-evenly-refactor)) -(def align-content-start-refactor (icon-xref :align-content-start-refactor)) -(def align-horizontal-center-refactor (icon-xref :align-horizontal-center-refactor)) -(def align-vertical-center-refactor (icon-xref :align-vertical-center-refactor)) -(def align-items-center-refactor (icon-xref :align-items-center-refactor)) -(def align-items-end-refactor (icon-xref :align-items-end-refactor)) -(def align-items-start-refactor (icon-xref :align-items-start-refactor)) -(def align-left-refactor (icon-xref :align-left-refactor)) -(def align-right-refactor (icon-xref :align-right-refactor)) -(def align-top-refactor (icon-xref :align-top-refactor)) -(def board-refactor (icon-xref :board-refactor)) -(def boards-thumbnail-refactor (icon-xref :boards-thumbnail-refactor)) -(def boolean-difference-refactor (icon-xref :boolean-difference-refactor)) -(def boolean-exclude-refactor (icon-xref :boolean-exclude-refactor)) -(def boolean-flatten-refactor (icon-xref :boolean-flatten-refactor)) -(def boolean-intersection-refactor (icon-xref :boolean-intersection-refactor)) -(def boolean-union-refactor (icon-xref :boolean-union-refactor)) -(def close-refactor (icon-xref :close-refactor)) -(def close-small-refactor (icon-xref :close-small-refactor)) -(def component-refactor (icon-xref :component-refactor)) -(def copy-refactor (icon-xref :copy-refactor)) -(def column-refactor (icon-xref :column-refactor)) -(def column-reverse-refactor (icon-xref :column-reverse-refactor)) -(def constraint-horizontal-refactor (icon-xref :constraint-horizontal-refactor)) -(def constraint-vertical-refactor (icon-xref :constraint-vertical-refactor)) -(def corner-radius-refactor (icon-xref :corner-radius-refactor)) -(def curve-refactor (icon-xref :curve-refactor)) -(def distribute-vertical-sapcing-refactor (icon-xref :distribute-vertical-spacing-refactor)) -(def delete-refactor (icon-xref :delete-refactor)) -(def delete-text-refactor (icon-xref :delete-text-refactor)) -(def document-refactor (icon-xref :document-refactor)) -(def drop-refactor (icon-xref :drop-refactor)) -(def effects-refactor (icon-xref :effects-refactor)) -(def elipse-refactor (icon-xref :elipse-refactor)) -(def filter-refactor (icon-xref :filter-refactor)) -(def flex-refactor (icon-xref :flex-refactor)) -(def flex-horizontal-refactor (icon-xref :flex-horizontal-refactor)) -(def flex-grid-refactor (icon-xref :flex-grid-refactor)) -(def flex-vertical-refactor (icon-xref :flex-vertical-refactor)) -(def flip-horizontal-refactor (icon-xref :flip-horizontal-refactor)) -(def grid-column-refactor (icon-xref :grid-column-refactor)) -(def grid-columns-refactor (icon-xref :grid-columns-refactor)) -(def grid-gutter-refactor (icon-xref :grid-gutter-refactor)) -(def grid-margin-refactor (icon-xref :grid-margin-refactor)) -(def grid-row-refactor (icon-xref :grid-row-refactor)) -(def grid-rows-refactor (icon-xref :grid-rows-refactor)) -(def grid-square-refactor (icon-xref :grid-square-refactor)) -(def grid-refactor (icon-xref :grid-refactor)) -(def group-refactor (icon-xref :group-refactor)) -(def gutter-horizontal-refactor (icon-xref :gutter-horizontal-refactor)) -(def gutter-vertical-refactor (icon-xref :gutter-vertical-refactor)) -(def hide-refactor (icon-xref :hide-refactor)) -(def img-refactor (icon-xref :img-refactor)) -(def icon-refactor (icon-xref :icon-refactor)) -(def justify-content-center-refactor (icon-xref :justify-content-center-refactor)) -(def justify-content-end-refactor (icon-xref :justify-content-end-refactor)) -(def justify-content-start-refactor (icon-xref :justify-content-start-refactor)) -(def justify-content-space-between-refactor (icon-xref :justify-content-space-between-refactor)) -(def justify-content-space-around-refactor (icon-xref :justify-content-space-around-refactor)) -(def justify-content-space-evenly-refactor (icon-xref :justify-content-space-evenly-refactor)) -(def lock-refactor (icon-xref :lock-refactor)) -(def library-refactor (icon-xref :library-refactor)) -(def margin-bottom-refactor (icon-xref :margin-bottom-refactor)) -(def margin-left-refactor (icon-xref :margin-left-refactor)) -(def margin-left-right-refactor (icon-xref :margin-left-right-refactor)) -(def margin-right-refactor (icon-xref :margin-right-refactor)) -(def margin-top-refactor (icon-xref :margin-top-refactor)) -(def margin-top-bottom-refactor (icon-xref :margin-top-bottom-refactor)) -(def mask-refactor (icon-xref :mask-refactor)) -(def masked-refactor (icon-xref :masked-refactor)) -(def menu-refactor (icon-xref :menu-refactor)) -(def move-refactor (icon-xref :move-refactor)) -(def open-link-refactor (icon-xref :open-link-refactor)) -(def padding-bottom-refactor (icon-xref :padding-bottom-refactor)) -(def padding-top-refactor (icon-xref :padding-top-refactor)) -(def padding-top-bottom-refactor (icon-xref :padding-top-bottom-refactor)) -(def padding-left-refactor (icon-xref :padding-left-refactor)) -(def padding-left-right-refactor (icon-xref :padding-left-right-refactor)) -(def padding-right-refactor (icon-xref :padding-right-refactor)) -(def padding-extended-refactor (icon-xref :padding-extended-refactor)) -(def path-refactor (icon-xref :path-refactor)) -(def pentool-refactor (icon-xref :pentool-refactor)) -(def rectangle-refactor (icon-xref :rectangle-refactor)) -(def remove-refactor (icon-xref :remove-refactor)) -(def rotation-refactor (icon-xref :rotation-refactor)) -(def row-reverse-refactor (icon-xref :row-reverse-refactor)) -(def search-refactor (icon-xref :search-refactor)) -(def size-horizontal-refactor (icon-xref :size-horizontal-refactor)) -(def size-vertical-refactor (icon-xref :size-vertical-refactor)) -(def shown-refactor (icon-xref :shown-refactor)) -(def stroke-size-refactor (icon-xref :stroke-size-refactor)) -(def svg-refactor (icon-xref :svg-refactor)) -(def swatches-refactor (icon-xref :swatches-refactor)) -(def text-align-center-refactor (icon-xref :text-align-center-refactor)) -(def text-align-left-refactor (icon-xref :text-align-left-refactor)) -(def text-align-right-refactor (icon-xref :text-align-right-refactor)) -(def text-auto-height-refactor (icon-xref :text-auto-height-refactor)) -(def text-auto-width-refactor (icon-xref :text-auto-width-refactor)) -(def text-paragraph-refactor (icon-xref :text-paragraph-refactor)) -(def text-refactor (icon-xref :text-refactor)) -(def text-palette-refactor (icon-xref :text-palette-refactor)) -(def tick-refactor (icon-xref :tick-refactor)) -(def unlock-refactor (icon-xref :unlock-refactor)) -(def vertical-align-items-center-refactor (icon-xref :vertical-align-items-center-refactor)) -(def vertical-align-items-end-refactor (icon-xref :vertical-align-items-end-refactor)) -(def vertical-align-items-start-refactor (icon-xref :vertical-align-items-start-refactor)) -(def view-as-icons-refactor (icon-xref :view-as-icons-refactor)) -(def wrap-refactor (icon-xref :wrap-refactor)) -(def loader-pencil + +(def ^:icon loader-pencil (mf/html [:svg {:viewBox "0 0 677.34762 182.15429" @@ -392,12 +276,24 @@ :d "M134.482 157.147v25l518.57.008.002-25-518.572-.008z"}]]])) +(def default + "A collection of all icons" + (collect-icons)) + (mf/defc debug-icons-preview {::mf/wrap-props false} [] - [:section.debug-icons-preview - (for [[key val] (sort-by first (ns-publics 'app.main.ui.icons))] - (when (not= key 'debug-icons-preview) - [:div.icon-item {:key key} - (deref val) - [:span (pr-str key)]]))]) + (let [entries (->> (seq (js/Object.entries default)) + (sort-by first))] + [:section.debug-icons-preview + [:h2 "icons"] + (for [[key val] entries] + [:div.icon-item-old {:key key + :title key} + val + [:span key]])])) + +(defn key->icon + [icon-key] + (when icon-key + (unchecked-get default (-> icon-key d/name str/camel str/capital)))) diff --git a/frontend/src/app/main/ui/loader.cljs b/frontend/src/app/main/ui/loader.cljs index 393de79d6b..43a7901815 100644 --- a/frontend/src/app/main/ui/loader.cljs +++ b/frontend/src/app/main/ui/loader.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.loader + (:require-macros [app.main.style :as stl]) (:require [app.main.store :as st] [app.main.ui.icons :as i] @@ -15,4 +16,5 @@ (mf/defc loader [] (when (mf/deref st/loader) - [:div.loader-content i/loader-pencil])) + [:div {:class (stl/css :loader-content)} + i/loader-pencil])) diff --git a/frontend/src/app/main/ui/loader.scss b/frontend/src/app/main/ui/loader.scss new file mode 100644 index 0000000000..71121f51d9 --- /dev/null +++ b/frontend/src/app/main/ui/loader.scss @@ -0,0 +1,11 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.loader-content { + @extend .loader-base; +} diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs index 49342608c4..370830fdb8 100644 --- a/frontend/src/app/main/ui/measurements.cljs +++ b/frontend/src/app/main/ui/measurements.cljs @@ -9,21 +9,11 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.flex-layout :as gsl] - [app.common.geom.shapes.points :as gpo] [app.common.math :as mth] - [app.common.pages.helpers :as cph] - [app.common.types.modifiers :as ctm] [app.common.uuid :as uuid] - [app.main.data.workspace.modifiers :as dwm] - [app.main.data.workspace.state-helpers :as wsh] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.css-cursors :as cur] [app.main.ui.formats :as fmt] - [app.main.ui.workspace.viewport.viewport-ref :refer [point->viewport]] - [app.util.dom :as dom] [rumext.v2 :as mf])) ;; ------------------------------------------------ @@ -33,31 +23,26 @@ (def font-size 11) (def selection-rect-width 1) -(def select-color "var(--color-select)") +(def select-color "var(--color-accent-tertiary)") (def select-guide-width 1) (def select-guide-dasharray 5) -(def hover-color "var(--color-distance)") -(def hover-color-text "var(--color-white)") -(def hover-guide-width 1) +(def hover-color "var(--color-accent-quaternary)") -(def size-display-color "var(--color-white)") +(def size-display-color "var(--app-white)") (def size-display-opacity 0.7) -(def size-display-text-color "var(--color-black)") +(def size-display-text-color "var(--app-black)") (def size-display-width-min 50) (def size-display-width-max 75) (def size-display-height 16) -(def distance-color "var(--color-distance)") -(def distance-text-color "var(--color-white)") +(def distance-color "var(--color-accent-quaternary)") +(def distance-text-color "var(--app-white)") (def distance-border-radius 2) (def distance-pill-width 50) (def distance-pill-height 16) (def distance-line-stroke 1) -(def warning-color "var(--color-warning)") -(def flex-display-pill-width 40) -(def flex-display-pill-height 20) -(def flex-display-pill-border-radius 4) + ;; ------------------------------------------------ ;; HELPERS @@ -94,18 +79,18 @@ (or (and (neg? ss) (pos? se)) (and (pos? ss) (neg? ee)) (and (neg? ss) (> ss se))) - (conj [ from-s (+ from-s ss) ]) + (conj [from-s (+ from-s ss)]) (and (neg? se) (<= ss se)) - (conj [ from-s (+ from-s se) ]) + (conj [from-s (+ from-s se)]) (and (pos? es) (<= es ee)) - (conj [ from-e (+ from-e es) ]) + (conj [from-e (+ from-e es)]) (or (and (pos? ee) (neg? es)) (and (neg? ee) (pos? ss)) (and (pos? ee) (< ee es))) - (conj [ from-e (+ from-e ee) ])))) + (conj [from-e (+ from-e ee)])))) ;; ------------------------------------------------ ;; COMPONENTS @@ -248,8 +233,8 @@ (mf/defc measurement [{:keys [bounds frame selected-shapes hover-shape zoom]}] (let [selected-ids (into #{} (map :id) selected-shapes) - selected-selrect (gsh/selection-rect selected-shapes) - hover-selrect (-> hover-shape :points gsh/points->selrect) + selected-selrect (gsh/shapes->rect selected-shapes) + hover-selrect (-> hover-shape :points grc/points->rect) bounds-selrect (bound->selrect bounds) hover-selected-shape? (not (contains? selected-ids (:id hover-shape)))] @@ -262,7 +247,7 @@ (if (or (not hover-shape) (not hover-selected-shape?)) (when (and frame (not= uuid/zero (:id frame))) - (let [frame-bb (-> (:points frame) (gsh/points->selrect))] + (let [frame-bb (-> (:points frame) (grc/points->rect))] [:g.hover-shapes [:& selection-rect {:type :hover :selrect frame-bb :zoom zoom}] [:& distance-display {:from frame-bb @@ -275,600 +260,3 @@ [:& size-display {:selrect hover-selrect :zoom zoom}] [:& distance-display {:from hover-selrect :to selected-selrect :zoom zoom :bounds bounds-selrect}]])]))) - - -(mf/defc flex-display-pill [{:keys [x y width height font-size border-radius value color]}] - [:g.distance-pill - [:rect {:x x - :y y - :width width - :height height - :rx border-radius - :ry border-radius - :style {:fill color}}] - - [:text {:x (+ x (/ width 2)) - :y (+ y (/ height 2)) - :text-anchor "middle" - :dominant-baseline "central" - :style {:fill distance-text-color - :font-size font-size}} - (fmt/format-number (or value 0))]]) - - -(mf/defc padding-display [{:keys [frame-id zoom hover-all? hover-v? hover-h? padding-num padding on-pointer-enter on-pointer-leave - rect-data hover? selected? mouse-pos hover-value]}] - (let [resizing? (mf/use-var false) - start (mf/use-var nil) - original-value (mf/use-var 0) - negate? (true? (:resize-negate? rect-data)) - axis (:resize-axis rect-data) - - on-pointer-down - (mf/use-callback - (mf/deps frame-id rect-data padding-num) - (fn [event] - (dom/capture-pointer event) - (reset! resizing? true) - (reset! start (dom/get-client-position event)) - (reset! original-value (:initial-value rect-data)))) - - on-lost-pointer-capture - (mf/use-callback - (mf/deps frame-id padding-num padding) - (fn [event] - (dom/release-pointer event) - (reset! resizing? false) - (reset! start nil) - (reset! original-value 0) - (st/emit! (dwm/apply-modifiers)))) - - on-pointer-move - (mf/use-callback - (mf/deps frame-id padding-num padding hover-all? hover-v? hover-h?) - (fn [event] - (let [pos (dom/get-client-position event)] - (reset! mouse-pos (point->viewport pos)) - (when @resizing? - (let [delta (-> (gpt/to-vec @start pos) - (cond-> negate? gpt/negate) - (get axis)) - val (int (max (+ @original-value (/ delta zoom)) 0)) - layout-padding (cond - hover-all? (assoc padding :p1 val :p2 val :p3 val :p4 val) - hover-v? (assoc padding :p1 val :p3 val) - hover-h? (assoc padding :p2 val :p4 val) - :else (assoc padding padding-num val)) - - - layout-padding-type (if (= (:p1 padding) (:p2 padding) (:p3 padding) (:p4 padding)) :simple :multiple) - modifiers (dwm/create-modif-tree [frame-id] - (-> (ctm/empty) - (ctm/change-property :layout-padding layout-padding) - (ctm/change-property :layout-padding-type layout-padding-type)))] - (reset! hover-value val) - (st/emit! (dwm/set-modifiers modifiers)))))))] - - [:rect.padding-rect {:x (:x rect-data) - :y (:y rect-data) - :width (max 0 (:width rect-data)) - :height (max 0 (:height rect-data)) - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave - :on-pointer-down on-pointer-down - :on-lost-pointer-capture on-lost-pointer-capture - :on-pointer-move on-pointer-move - :class (when (or hover? selected?) - (if (= (:resize-axis rect-data) :x) (cur/get-dynamic "resize-ew" 0) (cur/get-dynamic "resize-ew" 90))) - :style {:fill (if (or hover? selected?) distance-color "none") - :opacity (if selected? 0.5 0.25)}}])) - -(mf/defc padding-rects [{:keys [frame zoom alt? shift?]}] - (let [frame-id (:id frame) - paddings-selected (mf/deref refs/workspace-paddings-selected) - hover-value (mf/use-var 0) - mouse-pos (mf/use-var nil) - hover (mf/use-var nil) - hover-all? (and (not (nil? @hover)) alt?) - hover-v? (and (or (= @hover :p1) (= @hover :p3)) shift?) - hover-h? (and (or (= @hover :p2) (= @hover :p4)) shift?) - padding (:layout-padding frame) - {:keys [width height x1 x2 y1 y2]} (:selrect frame) - on-pointer-enter (fn [hover-type val] - (reset! hover hover-type) - (reset! hover-value val)) - on-pointer-leave #(reset! hover nil) - pill-width (/ flex-display-pill-width zoom) - pill-height (/ flex-display-pill-height zoom) - hover? #(or hover-all? - (and (or (= % :p1) (= % :p3)) hover-v?) - (and (or (= % :p2) (= % :p4)) hover-h?) - (= @hover %)) - negate {:p1 (if (:flip-y frame) true false) - :p2 (if (:flip-x frame) true false) - :p3 (if (:flip-y frame) true false) - :p4 (if (:flip-x frame) true false)} - negate (cond-> negate - (not= :auto (:layout-item-h-sizing frame)) (assoc :p2 (not (:p2 negate))) - (not= :auto (:layout-item-v-sizing frame)) (assoc :p3 (not (:p3 negate)))) - - padding-rect-data {:p1 {:key (str frame-id "-p1") - :x x1 - :y (if (:flip-y frame) (- y2 (:p1 padding)) y1) - :width width - :height (:p1 padding) - :initial-value (:p1 padding) - :resize-type (if (:flip-y frame) :bottom :top) - :resize-axis :y - :resize-negate? (:p1 negate)} - :p2 {:key (str frame-id "-p2") - :x (if (:flip-x frame) x1 (- x2 (:p2 padding))) - :y y1 - :width (:p2 padding) - :height height - :initial-value (:p2 padding) - :resize-type :left - :resize-axis :x - :resize-negate? (:p2 negate)} - :p3 {:key (str frame-id "-p3") - :x x1 - :y (if (:flip-y frame) y1 (- y2 (:p3 padding))) - :width width - :height (:p3 padding) - :initial-value (:p3 padding) - :resize-type :bottom - :resize-axis :y - :resize-negate? (:p3 negate)} - :p4 {:key (str frame-id "-p4") - :x (if (:flip-x frame) (- x2 (:p4 padding)) x1) - :y y1 - :width (:p4 padding) - :height height - :initial-value (:p4 padding) - :resize-type (if (:flip-x frame) :right :left) - :resize-axis :x - :resize-negate? (:p4 negate)}}] - - [:g.paddings {:pointer-events "visible"} - (for [[padding-num rect-data] padding-rect-data] - [:& padding-display {:key (:key rect-data) - :frame-id frame-id - :zoom zoom - :hover-all? hover-all? - :hover-v? hover-v? - :hover-h? hover-h? - :padding padding - :mouse-pos mouse-pos - :hover-value hover-value - :padding-num padding-num - :on-pointer-enter (partial on-pointer-enter padding-num (get padding padding-num)) - :on-pointer-leave on-pointer-leave - :hover? (hover? padding-num) - :selected? (get paddings-selected padding-num) - :rect-data rect-data}]) - (when @hover - [:& flex-display-pill {:height pill-height - :width pill-width - :font-size (/ font-size zoom) - :border-radius (/ flex-display-pill-border-radius zoom) - :color distance-color - :x (:x @mouse-pos) - :y (- (:y @mouse-pos) pill-width) - :value @hover-value}])])) - -(mf/defc margin-display [{:keys [shape-id zoom hover-all? hover-v? hover-h? margin-num margin on-pointer-enter on-pointer-leave - rect-data hover? selected? mouse-pos hover-value]}] - (let [resizing? (mf/use-var false) - start (mf/use-var nil) - original-value (mf/use-var 0) - negate? (true? (:resize-negate? rect-data)) - axis (:resize-axis rect-data) - - on-pointer-down - (mf/use-callback - (mf/deps shape-id margin-num margin) - (fn [event] - (dom/capture-pointer event) - (reset! resizing? true) - (reset! start (dom/get-client-position event)) - (reset! original-value (:initial-value rect-data)))) - - on-lost-pointer-capture - (mf/use-callback - (mf/deps shape-id margin-num margin) - (fn [event] - (dom/release-pointer event) - (reset! resizing? false) - (reset! start nil) - (reset! original-value 0) - (st/emit! (dwm/apply-modifiers)))) - - on-pointer-move - (mf/use-callback - (mf/deps shape-id margin-num margin hover-all? hover-v? hover-h?) - (fn [event] - (let [pos (dom/get-client-position event)] - (reset! mouse-pos (point->viewport pos)) - (when @resizing? - (let [delta (-> (gpt/to-vec @start pos) - (cond-> negate? gpt/negate) - (get axis)) - val (int (max (+ @original-value (/ delta zoom)) 0)) - layout-item-margin (cond - hover-all? (assoc margin :m1 val :m2 val :m3 val :m4 val) - hover-v? (assoc margin :m1 val :m3 val) - hover-h? (assoc margin :m2 val :m4 val) - :else (assoc margin margin-num val)) - layout-item-margin-type (if (= (:m1 margin) (:m2 margin) (:m3 margin) (:m4 margin)) :simple :multiple) - modifiers (dwm/create-modif-tree [shape-id] - (-> (ctm/empty) - (ctm/change-property :layout-item-margin layout-item-margin) - (ctm/change-property :layout-item-margin-type layout-item-margin-type)))] - (reset! hover-value val) - (st/emit! (dwm/set-modifiers modifiers)))))))] - - [:rect.margin-rect {:x (:x rect-data) - :y (:y rect-data) - :width (:width rect-data) - :height (:height rect-data) - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave - :on-pointer-down on-pointer-down - :on-lost-pointer-capture on-lost-pointer-capture - :on-pointer-move on-pointer-move - :class (when (or hover? selected?) - (if (= (:resize-axis rect-data) :x) (cur/get-dynamic "resize-ew" 0) (cur/get-dynamic "resize-ew" 90))) - :style {:fill (if (or hover? selected?) warning-color "none") - :opacity (if selected? 0.5 0.25)}}])) - -(mf/defc margin-rects [{:keys [shape frame zoom alt? shift?]}] - (let [shape-id (:id shape) - pill-width (/ flex-display-pill-width zoom) - pill-height (/ flex-display-pill-height zoom) - margins-selected (mf/deref refs/workspace-margins-selected) - hover-value (mf/use-var 0) - mouse-pos (mf/use-var nil) - hover (mf/use-var nil) - hover-all? (and (not (nil? @hover)) alt?) - hover-v? (and (or (= @hover :m1) (= @hover :m3)) shift?) - hover-h? (and (or (= @hover :m2) (= @hover :m4)) shift?) - margin (:layout-item-margin shape) - {:keys [width height x1 x2 y1 y2]} (:selrect shape) - on-pointer-enter (fn [hover-type val] - (reset! hover hover-type) - (reset! hover-value val)) - on-pointer-leave #(reset! hover nil) - hover? #(or hover-all? - (and (or (= % :m1) (= % :m3)) hover-v?) - (and (or (= % :m2) (= % :m4)) hover-h?) - (= @hover %)) - margin-display-data {:m1 {:key (str shape-id "-m1") - :x x1 - :y (if (:flip-y frame) y2 (- y1 (:m1 margin))) - :width width - :height (:m1 margin) - :initial-value (:m1 margin) - :resize-type :top - :resize-axis :y - :resize-negate? (:flip-y frame)} - :m2 {:key (str shape-id "-m2") - :x (if (:flip-x frame) (- x1 (:m2 margin)) x2) - :y y1 - :width (:m2 margin) - :height height - :initial-value (:m2 margin) - :resize-type :left - :resize-axis :x - :resize-negate? (:flip-x frame)} - :m3 {:key (str shape-id "-m3") - :x x1 - :y (if (:flip-y frame) (- y1 (:m3 margin)) y2) - :width width - :height (:m3 margin) - :initial-value (:m3 margin) - :resize-type :top - :resize-axis :y - :resize-negate? (:flip-y frame)} - :m4 {:key (str shape-id "-m4") - :x (if (:flip-x frame) x2 (- x1 (:m4 margin))) - :y y1 - :width (:m4 margin) - :height height - :initial-value (:m4 margin) - :resize-type :left - :resize-axis :x - :resize-negate? (:flip-x frame)}}] - - [:g.margins {:pointer-events "visible"} - (for [[margin-num rect-data] margin-display-data] - [:& margin-display - {:key (:key rect-data) - :shape-id shape-id - :zoom zoom - :hover-all? hover-all? - :hover-v? hover-v? - :hover-h? hover-h? - :margin-num margin-num - :margin margin - :on-pointer-enter (partial on-pointer-enter margin-num (get margin margin-num)) - :on-pointer-leave on-pointer-leave - :rect-data rect-data - :hover? (hover? margin-num) - :selected? (get margins-selected margin-num) - :mouse-pos mouse-pos - :hover-value hover-value}]) - - (when @hover - [:& flex-display-pill {:height pill-height - :width pill-width - :font-size (/ font-size zoom) - :border-radius (/ flex-display-pill-border-radius zoom) - :color warning-color - :x (:x @mouse-pos) - :y (- (:y @mouse-pos) pill-width) - :value @hover-value}])])) - -(mf/defc gap-display [{:keys [frame-id zoom gap-type gap on-pointer-enter on-pointer-leave - rect-data hover? selected? mouse-pos hover-value]}] - (let [resizing (mf/use-var nil) - start (mf/use-var nil) - original-value (mf/use-var 0) - negate? (:resize-negate? rect-data) - axis (:resize-axis rect-data) - - on-pointer-down - (mf/use-callback - (mf/deps frame-id gap-type gap) - (fn [event] - (dom/capture-pointer event) - (reset! resizing gap-type) - (reset! start (dom/get-client-position event)) - (reset! original-value (:initial-value rect-data)))) - - on-lost-pointer-capture - (mf/use-callback - (mf/deps frame-id gap-type gap) - (fn [event] - (dom/release-pointer event) - (reset! resizing nil) - (reset! start nil) - (reset! original-value 0) - (st/emit! (dwm/apply-modifiers)))) - - on-pointer-move - (mf/use-callback - (mf/deps frame-id gap-type gap) - (fn [event] - (let [pos (dom/get-client-position event)] - (reset! mouse-pos (point->viewport pos)) - (when (= @resizing gap-type) - (let [delta (-> (gpt/to-vec @start pos) - (cond-> negate? gpt/negate) - (get axis)) - val (int (max (+ @original-value (/ delta zoom)) 0)) - layout-gap (assoc gap gap-type val) - modifiers (dwm/create-modif-tree [frame-id] (ctm/change-property (ctm/empty) :layout-gap layout-gap))] - - (reset! hover-value val) - (st/emit! (dwm/set-modifiers modifiers)))))))] - - [:rect.gap-rect {:x (:x rect-data) - :y (:y rect-data) - :width (:width rect-data) - :height (:height rect-data) - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave - :on-pointer-down on-pointer-down - :on-lost-pointer-capture on-lost-pointer-capture - :on-pointer-move on-pointer-move - :class (when (or hover? selected?) - (if (= (:resize-axis rect-data) :x) (cur/get-dynamic "resize-ew" 0) (cur/get-dynamic "resize-ew" 90))) - :style {:fill (if (or hover? selected?) distance-color "none") - :opacity (if selected? 0.5 0.25)}}])) - -(mf/defc gap-rects [{:keys [frame zoom]}] - (let [frame-id (:id frame) - saved-dir (:layout-flex-dir frame) - is-col? (or (= :column saved-dir) (= :column-reverse saved-dir)) - flip-x (:flip-x frame) - flip-y (:flip-y frame) - pill-width (/ flex-display-pill-width zoom) - pill-height (/ flex-display-pill-height zoom) - workspace-modifiers (mf/deref refs/workspace-modifiers) - gap-selected (mf/deref refs/workspace-gap-selected) - hover (mf/use-var nil) - hover-value (mf/use-var 0) - mouse-pos (mf/use-var nil) - padding (:layout-padding frame) - gap (:layout-gap frame) - {:keys [width height x1 y1]} (:selrect frame) - on-pointer-enter (fn [hover-type val] - (reset! hover hover-type) - (reset! hover-value val)) - - on-pointer-leave #(reset! hover nil) - negate {:column-gap (if flip-x true false) - :row-gap (if flip-y true false)} - - objects (wsh/lookup-page-objects @st/state) - children (->> (cph/get-immediate-children objects frame-id) - (remove :layout-item-absolute) - (remove :hidden)) - - children-to-display (if (or (= :row-reverse saved-dir) - (= :column-reverse saved-dir)) - (drop-last children) - (rest children)) - children-to-display (->> children-to-display - (map #(gsh/transform-shape % (get-in workspace-modifiers [(:id %) :modifiers])))) - - wrap-blocks - (let [block-children (->> children - (map #(vector (gpo/parent-coords-bounds (:points %) (:points frame)) %))) - layout-data (gsl/calc-layout-data frame block-children (:points frame)) - layout-bounds (:layout-bounds layout-data) - xv #(gpo/start-hv layout-bounds %) - yv #(gpo/start-vv layout-bounds %)] - (for [{:keys [start-p line-width line-height layout-gap-row layout-gap-col num-children]} (:layout-lines layout-data)] - (let [line-width (if is-col? line-width (+ line-width (* (dec num-children) layout-gap-row))) - line-height (if is-col? (+ line-height (* (dec num-children) layout-gap-col)) line-height) - end-p (-> start-p (gpt/add (xv line-width)) (gpt/add (yv line-height)))] - {:x1 (min (:x start-p) (:x end-p)) - :y1 (min (:y start-p) (:y end-p)) - :x2 (max (:x start-p) (:x end-p)) - :y2 (max (:y start-p) (:y end-p))}))) - - block-contains - (fn [x y block] - (if is-col? - (<= (:x1 block) x (:x2 block)) - (<= (:y1 block) y (:y2 block)))) - - get-container-block - (fn [shape] - (let [selrect (:selrect shape) - x (/ (+ (:x1 selrect) (:x2 selrect)) 2) - y (/ (+ (:y1 selrect) (:y2 selrect)) 2)] - (->> wrap-blocks - (filter #(block-contains x y %)) - first))) - - create-cgdd - (fn [shape] - (let [block (get-container-block shape) - x (if flip-x - (- (:x1 (:selrect shape)) - (get-in shape [:layout-item-margin :m2]) - (:column-gap gap)) - (+ (:x2 (:selrect shape)) (get-in shape [:layout-item-margin :m2]))) - y (:y1 block) - h (- (:y2 block) (:y1 block))] - {:x x - :y y - :height h - :width (:column-gap gap) - :initial-value (:column-gap gap) - :resize-type :left - :resize-axis :x - :resize-negate? (:column-gap negate) - :gap-type (if is-col? :row-gap :column-gap)})) - - create-cgdd-block - (fn [block] - (let [x (if flip-x - (- (:x1 block) (:column-gap gap)) - (:x2 block)) - y (if flip-y - (+ y1 (:p3 padding)) - (+ y1 (:p1 padding))) - h (- height (+ (:p1 padding) (:p3 padding)))] - {:x x - :y y - :width (:column-gap gap) - :height h - :initial-value (:column-gap gap) - :resize-type :left - :resize-axis :x - :resize-negate? (:column-gap negate) - :gap-type (if is-col? :column-gap :row-gap)})) - - create-rgdd - (fn [shape] - (let [block (get-container-block shape) - x (:x1 block) - y (if flip-y - (- (:y1 (:selrect shape)) - (get-in shape [:layout-item-margin :m3]) - (:row-gap gap)) - (+ (:y2 (:selrect shape)) (get-in shape [:layout-item-margin :m3]))) - w (- (:x2 block) (:x1 block))] - {:x x - :y y - :width w - :height (:row-gap gap) - :initial-value (:row-gap gap) - :resize-type :bottom - :resize-axis :y - :resize-negate? (:row-gap negate) - :gap-type (if is-col? :row-gap :column-gap)})) - - create-rgdd-block - (fn [block] - (let [x (if flip-x - (+ x1 (:p2 padding)) - (+ x1 (:p4 padding))) - y (if flip-y - (- (:y1 block) (:row-gap gap)) - (:y2 block)) - w (- width (+ (:p2 padding) (:p4 padding)))] - {:x x - :y y - :width w - :height (:row-gap gap) - :initial-value (:row-gap gap) - :resize-type :bottom - :resize-axis :y - :resize-negate? (:row-gap negate) - :gap-type (if is-col? :column-gap :row-gap)})) - - display-blocks (if is-col? - (->> (drop-last wrap-blocks) - (map create-cgdd-block)) - (->> (drop-last wrap-blocks) - (map create-rgdd-block))) - - display-children (if is-col? - (->> children-to-display - (map create-rgdd)) - (->> children-to-display - (map create-cgdd)))] - - [:g.gaps {:pointer-events "visible"} - [:* - (for [[index display-item] (d/enumerate (concat display-blocks display-children))] - (let [gap-type (:gap-type display-item)] - [:& gap-display {:key (str frame-id index) - :frame-id frame-id - :zoom zoom - :gap-type gap-type - :gap gap - :on-pointer-enter (partial on-pointer-enter gap-type (get gap gap-type)) - :on-pointer-leave on-pointer-leave - :rect-data display-item - :hover? (= @hover gap-type) - :selected? (= gap-selected gap-type) - :mouse-pos mouse-pos - :hover-value hover-value}]))] - - (when @hover - [:& flex-display-pill {:height pill-height - :width pill-width - :font-size (/ font-size zoom) - :border-radius (/ flex-display-pill-border-radius zoom) - :color distance-color - :x (:x @mouse-pos) - :y (- (:y @mouse-pos) pill-width) - :value @hover-value}])])) - -(mf/defc padding - [{:keys [frame zoom alt? shift?]}] - (when frame - [:g.measurement-gaps {:pointer-events "none"} - [:g.hover-shapes - [:& padding-rects {:frame frame :zoom zoom :alt? alt? :shift? shift?}]]])) - -(mf/defc gap - [{:keys [frame zoom]}] - (when frame - [:g.measurement-gaps {:pointer-events "none"} - [:g.hover-shapes - [:& gap-rects {:frame frame :zoom zoom}]]])) - -(mf/defc margin - [{:keys [shape parent zoom alt? shift?]}] - (when shape - [:g.measurement-gaps {:pointer-events "none"} - [:g.hover-shapes - [:& margin-rects {:shape shape :frame parent :zoom zoom :alt? alt? :shift? shift?}]]])) - - diff --git a/frontend/src/app/main/ui/messages.cljs b/frontend/src/app/main/ui/messages.cljs index ae7e53930e..00df9c3f3d 100644 --- a/frontend/src/app/main/ui/messages.cljs +++ b/frontend/src/app/main/ui/messages.cljs @@ -6,73 +6,44 @@ (ns app.main.ui.messages (:require - [app.common.uuid :as uuid] - [app.main.data.messages :as dm] + [app.main.data.messages :as dmsg] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.icons :as i] - [app.util.dom :as dom] + [app.main.ui.notifications.context-notification :refer [context-notification]] + [app.main.ui.notifications.inline-notification :refer [inline-notification]] + [app.main.ui.notifications.toast-notification :refer [toast-notification]] [rumext.v2 :as mf])) -(mf/defc banner - [{:keys [type position status controls content actions on-close data-test role] :as props}] - [:div.banner {:class (dom/classnames - :warning (= type :warning) - :error (= type :error) - :success (= type :success) - :info (= type :info) - :fixed (= position :fixed) - :floating (= position :floating) - :inline (= position :inline) - :hide (= status :hide))} - [:div.wrapper - [:div.icon (case type - :warning i/msg-warning - :error i/msg-error - :success i/msg-success - :info i/msg-info - i/msg-error)] - [:div.content {:class (dom/classnames - :inline-actions (= controls :inline-actions) - :bottom-actions (= controls :bottom-actions)) - :data-test data-test - :role role} - content - (when (or (= controls :bottom-actions) (= controls :inline-actions)) - [:div.actions - (for [action actions] - [:div.btn-secondary.btn-small {:key (uuid/next) - :on-click (:callback action)} - (:label action)])])] - (when (= controls :close) - [:div.btn-close {:on-click on-close} i/close])]]) - -(mf/defc notifications +(mf/defc notifications-hub [] (let [message (mf/deref refs/message) - on-close #(st/emit! dm/hide)] + + on-close #(st/emit! dmsg/hide) + + toast-message {:type (or (:type message) :info) + :links (:links message) + :on-close on-close + :content (:content message)} + + inline-message {:actions (:actions message) + :links (:links message) + :content (:content message)} + + context-message {:type (or (:type message) :info) + :links (:links message) + :content (:content message)} + + is-context-msg (and (nil? (:timeout message)) (nil? (:actions message))) + is-toast-msg (or (= :toast (:notification-type message)) (some? (:timeout message))) + is-inline-msg (or (= :inline (:notification-type message)) (and (some? (:position message)) (= :floating (:position message))))] + (when message - [:& banner (assoc message - :position (or (:position message) :fixed) - :controls (if (some? (:controls message)) - (:controls message) - :close) - :on-close on-close)]))) - -(mf/defc inline-banner - {::mf/wrap [mf/memo]} - [{:keys [type content on-close actions data-test role] :as props}] - [:& banner {:type type - :position :inline - :status :visible - :controls (if (some? on-close) - :close - (if (some? actions) - :bottom-actions - :none)) - :content content - :on-close on-close - :actions actions - :data-test data-test - :role role}]) - + (cond + is-toast-msg + [:& toast-notification toast-message] + is-inline-msg + [:& inline-notification inline-message] + is-context-msg + [:& context-notification context-message] + :else + [:& toast-notification toast-message])))) diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs index 94831f9517..0c59faa26d 100644 --- a/frontend/src/app/main/ui/modal.cljs +++ b/frontend/src/app/main/ui/modal.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.modal + (:require-macros [app.main.style :as stl]) (:require [app.main.data.modal :as dm] [app.main.store :as st] @@ -45,9 +46,9 @@ {::mf/wrap-props false ::mf/wrap [mf/memo]} [props] - (let [data (unchecked-get props "data") - wrapper-ref (mf/use-ref nil) - components (mf/deref dm/components) + (let [data (unchecked-get props "data") + wrapper-ref (mf/use-ref nil) + components (mf/deref dm/components) allow-click-outside (:allow-click-outside data) @@ -66,21 +67,22 @@ (events/listen js/document EventType.KEYDOWN handle-keydown) ;; Changing to js/document breaks the color picker - (events/listen (dom/get-root) EventType.POINTERDOWN handle-click-outside) + (events/listen (dom/get-root) EventType.POINTERDOWN handle-click-outside) (events/listen js/document EventType.CONTEXTMENU handle-click-outside)]] #(doseq [key keys] (events/unlistenByKey key))))) (when-let [component (get components (:type data))] - [:div.modal-wrapper {:ref wrapper-ref} + [:div {:ref wrapper-ref + :class (stl/css :modal-wrapper)} (mf/element component (:props data))]))) - (def modal-ref (l/derived ::dm/modal st/state)) (mf/defc modal + {::mf/wrap-props false} [] (let [modal (mf/deref modal-ref)] (when modal diff --git a/frontend/src/app/main/ui/modal.scss b/frontend/src/app/main/ui/modal.scss new file mode 100644 index 0000000000..6dd5b4eda0 --- /dev/null +++ b/frontend/src/app/main/ui/modal.scss @@ -0,0 +1,9 @@ +@import "refactor/common-refactor.scss"; + +:global(:root) { + --s-4: 0.25rem; +} + +.modal-wrapper { + @extend .new-scrollbar; +} diff --git a/frontend/src/app/main/ui/notifications/badge.cljs b/frontend/src/app/main/ui/notifications/badge.cljs new file mode 100644 index 0000000000..e6e6c449eb --- /dev/null +++ b/frontend/src/app/main/ui/notifications/badge.cljs @@ -0,0 +1,27 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.notifications.badge + (:require-macros [app.main.style :as stl]) + (:require + [rumext.v2 :as mf])) + +(mf/defc badge-notification + "They are persistent, informative and non-actionable. + They are small messages in specific areas off the app" + + {::mf/props :obj} + [{:keys [type content size is-focus] :as props}] + + [:aside {:class (stl/css-case :badge-notification true + :warning (= type :warning) + :error (= type :error) + :success (= type :success) + :info (= type :info) + :small (= size :small) + :focus is-focus)} + content]) + diff --git a/frontend/src/app/main/ui/notifications/badge.scss b/frontend/src/app/main/ui/notifications/badge.scss new file mode 100644 index 0000000000..b98a5ee88b --- /dev/null +++ b/frontend/src/app/main/ui/notifications/badge.scss @@ -0,0 +1,64 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.badge-notification { + @include smallTitleTipography; + --badge-notification-bg-color: var(--alert-background-color-default); + --badge-notification-fg-color: var(--alert-text-foreground-color-default); + --badge-notification-border-color: var(--alert-border-color-default); + box-sizing: border-box; + display: grid; + place-items: center; + grid-template-columns: 1fr; + min-height: $s-32; + height: fit-content; + min-width: $s-80; + width: fit-content; + padding: 0; + margin: 0; + border: $s-1 solid var(--badge-notification-border-color); + border-radius: $br-8; + background-color: var(--badge-notification-bg-color); + color: var(--badge-notification-fg-color); +} + +.small { + @include bodySmallTypography; + min-height: $s-20; + border-radius: $br-6; +} + +.warning { + --badge-notification-bg-color: var(--alert-background-color-warning); + --badge-notification-fg-color: var(--alert-text-foreground-color-warning); + --badge-notification-border-color: var(--alert-border-color-warning); +} + +.success { + --badge-notification-bg-color: var(--alert-background-color-success); + --badge-notification-fg-color: var(--alert-text-foreground-color-success); + --badge-notification-border-color: var(--alert-border-color-success); +} + +.info { + --badge-notification-bg-color: var(--alert-background-color-info); + --badge-notification-fg-color: var(--alert-text-foreground-color-info); + --badge-notification-border-color: var(--alert-border-color-info); +} + +.error { + --badge-notification-bg-color: var(--alert-background-color-error); + --badge-notification-fg-color: var(--alert-text-foreground-color-error); + --badge-notification-border-color: var(--alert-border-color-error); +} + +.focus { + --badge-notification-bg-color: transparent; + --badge-notification-fg-color: var(--alert-text-foreground-color-focus); + --badge-notification-border-color: var(--alert-border-color-focus); +} diff --git a/frontend/src/app/main/ui/notifications/context_notification.cljs b/frontend/src/app/main/ui/notifications/context_notification.cljs new file mode 100644 index 0000000000..19a68ad265 --- /dev/null +++ b/frontend/src/app/main/ui/notifications/context_notification.cljs @@ -0,0 +1,68 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.notifications.context-notification + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.main.ui.components.link-button :as lb] + [app.main.ui.icons :as i] + [rumext.v2 :as mf])) + +(def ^:private neutral-icon + (i/icon-xref :msg-neutral (stl/css :icon))) + +(def ^:private error-icon + (i/icon-xref :delete-text (stl/css :icon))) + +(def ^:private success-icon + (i/icon-xref :status-tick (stl/css :icon))) + +(def ^:private info-icon + (i/icon-xref :help (stl/css :icon))) + +(defn get-icon-by-type + [type] + (case type + :warning neutral-icon + :error error-icon + :success success-icon + :info info-icon + neutral-icon)) + +(mf/defc context-notification + "They are persistent, informative and non-actionable. + They are contextual messages in specific areas off the app" + + {::mf/props :obj} + [{:keys [type content links is-html] :as props}] + + [:aside {:class (stl/css-case :context-notification true + :contain-html is-html + :warning (= type :warning) + :error (= type :error) + :success (= type :success) + :info (= type :info))} + + (get-icon-by-type type) + + ;; The content can arrive in markdown format, in these cases + ;; we will use the prop is-html to true to indicate it and + ;; that the html injection is performed and the necessary css classes are applied. + [:div {:class (stl/css :context-text) + :dangerouslySetInnerHTML (when is-html #js {:__html content})} + (when-not is-html + [:* + content + (when (some? links) + (for [[index link] (d/enumerate links)] + ;; TODO Review this component + [:& lb/link-button {:class (stl/css :link) + :on-click (:callback link) + :value (:label link) + :key (dm/str "link-" index)}]))])]]) + diff --git a/frontend/src/app/main/ui/notifications/context_notification.scss b/frontend/src/app/main/ui/notifications/context_notification.scss new file mode 100644 index 0000000000..69374ac8c7 --- /dev/null +++ b/frontend/src/app/main/ui/notifications/context_notification.scss @@ -0,0 +1,88 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.context-notification { + --context-notification-bg-color: var(--alert-background-color-default); + --context-notification-fg-color: var(--alert-text-foreground-color-default); + --context-notification-icon-color: var(--alert-icon-foreground-color-default); + --context-notification-border-color: var(--alert-border-color-default); + box-sizing: border-box; + display: grid; + grid-template-columns: $s-16 1fr; + gap: $s-8; + min-height: $s-32; + height: fit-content; + width: 100%; + padding: $s-8; + border: $s-1 solid var(--context-notification-border-color); + border-radius: $br-8; + background-color: var(--context-notification-bg-color); +} + +.warning { + --context-notification-bg-color: var(--alert-background-color-warning); + --context-notification-fg-color: var(--alert-text-foreground-color-warning); + --context-notification-icon-color: var(--alert-icon-foreground-color-warning); + --context-notification-border-color: var(--alert-border-color-warning); +} + +.success { + --context-notification-bg-color: var(--alert-background-color-success); + --context-notification-fg-color: var(--alert-text-foreground-color-success); + --context-notification-icon-color: var(--alert-icon-foreground-color-success); + --context-notification-border-color: var(--alert-border-color-success); +} + +.info { + --context-notification-bg-color: var(--alert-background-color-info); + --context-notification-fg-color: var(--alert-text-foreground-color-info); + --context-notification-icon-color: var(--alert-icon-foreground-color-info); + --context-notification-border-color: var(--alert-border-color-info); +} + +.default { + --context-notification-bg-color: var(--alert-background-color-default); + --context-notification-fg-color: var(--alert-text-foreground-color-default); + --context-notification-icon-color: var(--alert-icon-foreground-color-default); + --context-notification-border-color: var(--alert-border-color-default); +} + +.error { + --context-notification-bg-color: var(--alert-background-color-error); + --context-notification-fg-color: var(--alert-text-foreground-color-error); + --context-notification-icon-color: var(--alert-icon-foreground-color-error); + --context-notification-border-color: var(--alert-border-color-error); +} + +.icon { + @extend .button-icon; + align-self: self-start; + stroke: var(--context-notification-icon-color); +} + +.context-text { + @include bodySmallTypography; + align-self: center; + color: var(--context-notification-fg-color); + margin: auto 0; +} + +// The second rule only applies when the element receives embedded +// links within the content by means of the dangerouslySetInnerHTML. +// Only in this case the contain-html class will be used. + +.link, +.contain-html .context-text a { + @include bodySmallTypography; + align-self: center; + display: inline; + text-align: left; + height: $s-16; + margin: 0; + color: var(--modal-link-foreground-color); +} diff --git a/frontend/src/app/main/ui/notifications/inline_notification.cljs b/frontend/src/app/main/ui/notifications/inline_notification.cljs new file mode 100644 index 0000000000..85bd77e982 --- /dev/null +++ b/frontend/src/app/main/ui/notifications/inline_notification.cljs @@ -0,0 +1,45 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.notifications.inline-notification + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.uuid :as uuid] + [app.main.ui.components.link-button :as lb] + [rumext.v2 :as mf])) + + + +(mf/defc inline-notification + "They are persistent messages and report a special situation + of the application and require user interaction to disappear." + + {::mf/props :obj} + [{:keys [content actions links] :as props}] + [:aside {:class (stl/css :inline-notification)} + [:div {:class (stl/css :inline-text)} + + content + + (when (some? links) + [:nav {:class (stl/css :link-nav)} + (for [[index link] (d/enumerate links)] + [:& lb/link-button {:key (dm/str "link-" index) + :class (stl/css :link) + :on-click (:callback link) + :value (:label link)}])])] + + [:div {:class (stl/css :actions)} + (for [action actions] + [:button {:key (uuid/next) + :class (stl/css-case :action-btn true + :primary (= :primary (:type action)) + :secondary (= :secondary (:type action)) + :danger (= :danger (:type action))) + :on-click (:callback action)} + (:label action)])]]) diff --git a/frontend/src/app/main/ui/notifications/inline_notification.scss b/frontend/src/app/main/ui/notifications/inline_notification.scss new file mode 100644 index 0000000000..0bbb0d0c01 --- /dev/null +++ b/frontend/src/app/main/ui/notifications/inline_notification.scss @@ -0,0 +1,78 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.inline-notification { + --inline-notification-bg-color: var(--alert-background-color-default); + --inline-notification-fg-color: var(--alert-text-foreground-color-default); + --inline-notification-border-color: var(--alert-border-color-default); + @include alertShadow; + position: absolute; + top: $s-72; + left: 0; + right: 0; + display: grid; + grid-template-columns: 1fr auto; + gap: $s-24; + min-height: $s-48; + min-width: $s-640; + width: fit-content; + max-width: $s-960; + padding: $s-8; + margin-inline: auto; + border: $s-1 solid var(--inline-notification-border-color); + border-radius: $br-8; + z-index: $z-index-modal; + background-color: var(--inline-notification-bg-color); + color: var(--inline-notification-fg-color); +} + +.inline-text { + @include bodySmallTypography; + align-self: center; +} + +.link-nav { + display: inline; +} + +.link { + @include bodySmallTypography; + margin: 0; + height: 100%; + color: var(--modal-link-foreground-color); +} + +.actions { + display: grid; + grid-template-columns: none; + grid-auto-flow: column; + align-self: center; + gap: $s-8; +} + +.action-btn { + @extend .button-secondary; + @include uppercaseTitleTipography; + min-height: $s-32; + min-width: $s-32; + width: fit-content; + padding: $s-8 $s-24; + border: $s-1 solid transparent; +} + +.action-btn.primary { + @extend .button-primary; +} + +.action-btn.secondary { + @extend .button-secondary; +} + +.action-btn.danger { + @extend .modal-danger-btn; +} diff --git a/frontend/src/app/main/ui/notifications/toast_notification.cljs b/frontend/src/app/main/ui/notifications/toast_notification.cljs new file mode 100644 index 0000000000..c4583c9019 --- /dev/null +++ b/frontend/src/app/main/ui/notifications/toast_notification.cljs @@ -0,0 +1,73 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.notifications.toast-notification + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.main.ui.components.link-button :as lb] + [app.main.ui.icons :as i] + [rumext.v2 :as mf])) + +(def ^:private neutral-icon + (i/icon-xref :msg-neutral (stl/css :icon))) + +(def ^:private error-icon + (i/icon-xref :delete-text (stl/css :icon))) + +(def ^:private success-icon + (i/icon-xref :status-tick (stl/css :icon))) + +(def ^:private info-icon + (i/icon-xref :help (stl/css :icon))) + +(def ^:private close-icon + (i/icon-xref :close (stl/css :close-icon))) + +(defn get-icon-by-type + [type] + (case type + :warning neutral-icon + :error error-icon + :success success-icon + :info info-icon + neutral-icon)) + +(mf/defc toast-notification + "These are ephemeral elements that disappear when + the close button is pressed, + the page is refreshed, + the page is navigated to another page or + after 7 seconds, which is enough time to be read, + except for error messages that require user interaction." + + {::mf/props :obj} + [{:keys [type content on-close links] :as props}] + + [:aside {:class (stl/css-case :toast-notification true + :warning (= type :warning) + :error (= type :error) + :success (= type :success) + :info (= type :info))} + + (get-icon-by-type type) + + [:div {:class (stl/css :text)} + content + (when (some? links) + [:nav {:class (stl/css :link-nav)} + (for [[index link] (d/enumerate links)] + [:& lb/link-button {:key (dm/str "link-" index) + :class (stl/css :link) + :on-click (:callback link) + :value (:label link)}])])] + + + + [:button {:class (stl/css :btn-close) + :on-click on-close} + close-icon]]) diff --git a/frontend/src/app/main/ui/notifications/toast_notification.scss b/frontend/src/app/main/ui/notifications/toast_notification.scss new file mode 100644 index 0000000000..6626fc1190 --- /dev/null +++ b/frontend/src/app/main/ui/notifications/toast_notification.scss @@ -0,0 +1,100 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.toast-notification { + --toast-notification-bg-color: var(--alert-background-color-default); + --toast-notification-fg-color: var(--alert-text-foreground-color-default); + --toast-notification-icon-color: var(--alert-icon-foreground-color-default); + --toast-notification-border-color: var(--alert-border-color-default); + @include alertShadow; + position: fixed; + top: $s-16; + right: $s-16; + display: grid; + grid-template-columns: $s-16 1fr auto; + gap: $s-8; + min-height: $s-32; + min-width: $s-228; + max-width: $s-400; + padding: $s-8; + border: $s-1 solid var(--toast-notification-border-color); + background-color: var(--toast-notification-bg-color); + border-radius: $br-8; + color: var(--toast-notification-fg-color); + z-index: $z-index-alert; +} + +.warning { + --toast-notification-bg-color: var(--alert-background-color-warning); + --toast-notification-fg-color: var(--alert-text-foreground-color-warning); + --toast-notification-icon-color: var(--alert-icon-foreground-color-warning); + --toast-notification-border-color: var(--alert-border-color-warning); +} + +.success { + --toast-notification-bg-color: var(--alert-background-color-success); + --toast-notification-fg-color: var(--alert-text-foreground-color-success); + --toast-notification-icon-color: var(--alert-icon-foreground-color-success); + --toast-notification-border-color: var(--alert-border-color-success); +} + +.info { + --toast-notification-bg-color: var(--alert-background-color-info); + --toast-notification-fg-color: var(--alert-text-foreground-color-info); + --toast-notification-icon-color: var(--alert-icon-foreground-color-info); + --toast-notification-border-color: var(--alert-border-color-info); +} + +.default { + --toast-notification-bg-color: var(--alert-background-color-default); + --toast-notification-fg-color: var(--alert-text-foreground-color-default); + --toast-notification-icon-color: var(--alert-icon-foreground-color-default); + --toast-notification-border-color: var(--alert-border-color-default); +} + +.error { + --toast-notification-bg-color: var(--alert-background-color-error); + --toast-notification-fg-color: var(--alert-text-foreground-color-error); + --toast-notification-icon-color: var(--alert-icon-foreground-color-error); + --toast-notification-border-color: var(--alert-border-color-error); +} + +.link-nav { + display: inline; +} + +.link { + @include bodySmallTypography; + color: var(--modal-link-foreground-color); + margin: 0; +} + +.icon { + @extend .button-icon; + align-self: flex-start; + stroke: var(--toast-notification-icon-color); +} + +.text { + @include bodySmallTypography; + align-self: center; +} + +.btn-close { + @include buttonStyle; + align-self: flex-start; + width: $s-16; + margin: 0; + padding: 0; + background-color: transparent; +} + +.close-icon { + @extend .button-icon; + stroke: var(--toast-notification-icon-color); +} diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs index f84f91f3f3..3788ac3f54 100644 --- a/frontend/src/app/main/ui/onboarding.cljs +++ b/frontend/src/app/main/ui/onboarding.cljs @@ -5,7 +5,9 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.onboarding + (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.config :as cf] [app.main.data.events :as ev] [app.main.data.modal :as modal] @@ -17,7 +19,7 @@ [app.main.ui.onboarding.templates] [app.util.i18n :as i18n :refer [tr]] [app.util.timers :as tm] - [potok.core :as ptk] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) ;; --- ONBOARDING LIGHTBOX @@ -34,34 +36,41 @@ (fn [] (send-event "onboarding-step1-continue") (next))] - [:div.modal-container.onboarding.onboarding-v2 - [:div.modal-left.welcome - [:img {:src "images/onboarding-welcome.png" :border "0" :alt (tr "onboarding.welcome.alt")}]] - [:div.modal-right - [:div.release-container [:span.release "Version " (:main cf/version)]] - [:div.right-content - [:div.modal-title - [:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.welcome.title")]] + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-left)} + [:img {:src "images/welcomeilustration.svg" + :border "0" + :alt (tr "onboarding.welcome.alt")}]] + [:div {:class (stl/css :modal-right)} + [:div {:class (stl/css :release)} + "Version " (:main cf/version)] + [:h1 {:class (stl/css :modal-title) + :data-test "onboarding-welcome"} + (tr "onboarding-v2.welcome.title")] + [:p {:class (stl/css :modal-text)} + (tr "onboarding-v2.welcome.desc1")] - [:div.modal-content - [:p (tr "onboarding-v2.welcome.desc1")] - [:div.welcome-card - [:img {:src "images/community.svg" :border "0"}] - [:div - [:div.title [:a {:href "https://community.penpot.app/" :target "_blank" :on-click #(send-event "onboarding-community-link")} (tr "onboarding-v2.welcome.desc2.title")]] - [:div.description (tr "onboarding-v2.welcome.desc2")]]] + [:div {:class (stl/css :text-wrapper)} + [:div {:class (stl/css :property-title)} + [:a {:href "https://community.penpot.app/" + :target "_blank" + :on-click #(send-event "onboarding-community-link")} + (tr "onboarding-v2.welcome.desc2.title")]] + [:div {:class (stl/css :property-description)} + (tr "onboarding-v2.welcome.desc2")]] - [:div.welcome-card - [:img {:src "images/contributing.svg" :border "0"}] - [:div - [:div.title [:a {:href "https://help.penpot.app/contributing-guide/" :target "_blank" :on-click #(send-event "onboarding-contributing-link")} (tr "onboarding-v2.welcome.desc3.title")]] - [:div.description (tr "onboarding-v2.welcome.desc3")]]]]] - [:div.modal-navigation - [:button.btn-secondary {:on-click go-next :data-test "onboarding-next-btn"} (tr "labels.continue")]] - [:img.deco.square {:src "images/deco-square.svg" :border "0"}] - [:img.deco.circle {:src "images/deco-circle.svg" :border "0"}] - [:img.deco.line1 {:src "images/deco-line1.svg" :border "0"}] - [:img.deco.line2 {:src "images/deco-line2.svg" :border "0"}]]])) + [:div {:class (stl/css :text-wrapper)} + [:div {:class (stl/css :property-title)} + [:a {:href "https://help.penpot.app/contributing-guide/" + :target "_blank" :on-click #(send-event "onboarding-contributing-link")} + (tr "onboarding-v2.welcome.desc3.title")]] + [:div {:class (stl/css :property-description)} + (tr "onboarding-v2.welcome.desc3")]] + + [:button {:on-click go-next + :class (stl/css :accept-btn) + :data-test "onboarding-next-btn"} + (tr "labels.continue")]]])) (mf/defc onboarding-before-start [{:keys [next] :as props}] @@ -69,35 +78,46 @@ (fn [] (send-event "onboarding-step2-continue") (next))] - [:div.modal-container.onboarding.onboarding-v2 - [:div.modal-left.welcome - [:img {:src "images/onboarding-people.png" :border "0" :alt (tr "onboarding.welcome.alt")}]] - [:div.modal-right - [:div.release-container [:span.release "Version " (:main cf/version)]] - [:div.right-content - [:div.modal-title - [:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.before-start.title")]] + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-left)} + [:img {:src "images/beforeyoustartilustration.svg" + :border "0" + :alt (tr "onboarding.welcome.alt")}]] + [:div {:class (stl/css :modal-right)} + [:div {:class (stl/css :release)} + "Version " (:main cf/version)] - [:div.modal-content - [:p (tr "onboarding-v2.before-start.desc1")] - [:div.welcome-card - [:img {:src "images/user-guide.svg" :border "0"}] - [:div - [:div.title [:a {:href "https://help.penpot.app/user-guide/" :target "_blank" :on-click #(send-event "onboarding-user-guide-link")} (tr "onboarding-v2.before-start.desc2.title")]] - [:div.description (tr "onboarding-v2.before-start.desc2")]]] + [:h1 {:class (stl/css :modal-title) + :data-test "onboarding-welcome"} + (tr "onboarding-v2.before-start.title")] + [:p {:class (stl/css :modal-text)} + (tr "onboarding-v2.before-start.desc1")] - [:div.welcome-card - [:img {:src "images/video-tutorials.svg" :border "0"}] - [:div - [:div.title [:a {:href "https://www.youtube.com/c/Penpot" :target "_blank" :on-click #(send-event "onboarding-video-tutorials-link")} (tr "onboarding-v2.before-start.desc3.title")]] - [:div.description (tr "onboarding-v2.before-start.desc3")]]]]] - [:div.modal-navigation - [:button.btn-secondary {:on-click go-next :data-test "onboarding-next-btn"} (tr "labels.continue")]] - [:img.deco.square {:src "images/deco-square.svg" :border "0"}] - [:img.deco.circle {:src "images/deco-circle.svg" :border "0"}] - [:img.deco.line1 {:src "images/deco-line1.svg" :border "0"}] - [:img.deco.line2 {:src "images/deco-line2.svg" :border "0"}]]])) + [:div {:class (stl/css :text-wrapper)} + [:div {:class (stl/css :property-title)} + [:a {:class (stl/css :modal-link) + :href "https://help.penpot.app/user-guide/" + :target "_blank" + :on-click #(send-event "onboarding-user-guide-link")} + (tr "onboarding-v2.before-start.desc2.title")]] + [:div {:class (stl/css :property-description)} + (tr "onboarding-v2.before-start.desc2")]] + [:div {:class (stl/css :text-wrapper)} + [:div {:class (stl/css :property-title)} + [:a {:class (stl/css :modal-link) + :href "https://www.youtube.com/c/Penpot" + :target "_blank" + :on-click #(send-event "onboarding-video-tutorials-link")} + (tr "onboarding-v2.before-start.desc3.title")]] + [:div {:class (stl/css :property-description)} + (tr "onboarding-v2.before-start.desc3")]] + + + [:button {:on-click go-next + :class (stl/css :accept-btn) + :data-test "onboarding-next-btn"} + (tr "labels.continue")]]])) (mf/defc onboarding-modal {::mf/register modal/components @@ -111,11 +131,18 @@ skip (mf/use-fn - #(st/emit! (modal/hide) - (if (contains? cf/flags :newsletter-subscription) - (modal/show {:type :onboarding-newsletter-modal}) - (modal/show {:type :onboarding-team})) - (du/mark-onboarding-as-viewed)))] + (fn [] + (st/emit! (modal/hide) + (du/mark-onboarding-as-viewed)) + (cond + (contains? cf/flags :onboarding-questions) + (modal/show! {:type :onboarding-questions}) + + (contains? cf/flags :onboarding-newsletter) + (modal/show! {:type :onboarding-newsletter}) + + (contains? cf/flags :onboarding-team) + (modal/show! {:type :onboarding-team}))))] (mf/with-effect [@slide] (when (not= :start @slide) @@ -125,8 +152,8 @@ (reset! klass nil) (tm/dispose! sem)))) - [:div.modal-overlay - [:div.animated {:class @klass} + [:div {:class (stl/css :modal-overlay)} + [:div.animated {:class (dm/str @klass " " (stl/css :animated))} (case @slide :start [:& onboarding-welcome {:next #(navigate :opensource)}] :opensource [:& onboarding-before-start {:next skip}])]])) diff --git a/frontend/src/app/main/ui/onboarding.scss b/frontend/src/app/main/ui/onboarding.scss new file mode 100644 index 0000000000..8cd674ba32 --- /dev/null +++ b/frontend/src/app/main/ui/onboarding.scss @@ -0,0 +1,86 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + @extend .modal-container-base; + position: relative; + display: grid; + grid-template-columns: auto auto; + gap: $s-32; + padding-inline: $s-100; + padding-block-start: $s-100; + padding-block-end: $s-72; + margin: 0; + width: $s-960; + height: $s-632; + max-width: $s-960; + max-height: $s-632; +} + +.modal-left { + width: $s-240; + margin-block-end: $s-64; + img { + width: $s-240; + height: 100%; + border-radius: $br-8 0 0 $br-8; + } +} + +.modal-right { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: $s-40 auto auto auto $s-32; + gap: $s-24; + position: relative; +} + +.release { + @include bodySmallTypography; + position: absolute; + top: calc(-1 * $s-28); + right: 0; + padding: $s-8; + color: var(--modal-text-foreground-color); +} + +.modal-title { + @include bigTitleTipography; + color: var(--modal-title-foreground-color); +} + +.modal-text, +.property-description { + @include bodyLargeTypography; + margin: 0; + color: var(--modal-text-foreground-color); +} + +.modal-link { + @include bodyLargeTypography; + color: var(--modal-link-foreground-color); + margin: 0; +} + +.text-wrapper { + @include flexColumn; +} + +.property-title a { + @include medTitleTipography; + color: var(--modal-title-foreground-color); +} + +.accept-btn { + @extend .modal-accept-btn; + justify-self: flex-end; +} diff --git a/frontend/src/app/main/ui/onboarding/newsletter.cljs b/frontend/src/app/main/ui/onboarding/newsletter.cljs index 33b4658400..e0336641ed 100644 --- a/frontend/src/app/main/ui/onboarding/newsletter.cljs +++ b/frontend/src/app/main/ui/onboarding/newsletter.cljs @@ -5,21 +5,23 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.onboarding.newsletter + (:require-macros [app.main.style :as stl]) (:require - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.data.users :as du] [app.main.store :as st] + [app.main.ui.icons :as i] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) -(mf/defc onboarding-newsletter-modal +(mf/defc onboarding-newsletter {::mf/register modal/components - ::mf/register-as :onboarding-newsletter-modal} + ::mf/register-as :onboarding-newsletter} [] (let [message (tr "onboarding.newsletter.acceptance-message") newsletter-updates (mf/use-state false) - newsletter-news (mf/use-state false) + newsletter-news (mf/use-state false) toggle (mf/use-callback (fn [option] @@ -31,31 +33,53 @@ (mf/deps @newsletter-updates @newsletter-news) (fn [] (st/emit! (when (or @newsletter-updates @newsletter-news) - (dm/success message)) + (msg/success message)) (modal/show {:type :onboarding-team}) (du/update-profile-props {:newsletter-updates @newsletter-updates :newsletter-news @newsletter-news}))))] - [:div.modal-overlay - [:div.modal-container.onboarding.newsletter.animated.fadeInDown - [:div.modal-top - [:h1.newsletter-title {:data-test "onboarding-newsletter-title"} (tr "onboarding.newsletter.title")] - [:p (tr "onboarding-v2.newsletter.desc")]] - [:div.modal-bottom - [:div.newsletter-options - [:div.input-checkbox.check-primary - [:input {:type "checkbox" - :id "newsletter-updates" - :on-change #(toggle newsletter-updates)}] - [:label {:for "newsletter-updates"} (tr "onboarding-v2.newsletter.updates")]] - [:div.input-checkbox.check-primary - [:input {:type "checkbox" - :id "newsletter-news" - :on-change #(toggle newsletter-news)}] - [:label {:for "newsletter-news"} (tr "onboarding-v2.newsletter.news")]]] - [:p (tr "onboarding-v2.newsletter.privacy1") [:a {:target "_blank" :href "https://penpot.app/privacy"} (tr "onboarding.newsletter.policy")]] - [:p (tr "onboarding-v2.newsletter.privacy2")]] - [:div.modal-footer - [:button.btn-primary {:on-click accept} (tr "labels.continue")]] - [:img.deco.top {:src "images/deco-newsletter.png" :border "0"}] - [:img.deco.newsletter-left {:src "images/deco-news-left.png" :border "0"}] - [:img.deco.newsletter-right {:src "images/deco-news-right.png" :border "0"}]]])) + [:div {:class (stl/css :modal-overlay)} + [:div.animated.fadeInDown {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-left)} + [:img {:src "images/deco-newsletter.png" + :border "0"}]] + + [:div {:class (stl/css :modal-right)} + [:h2 {:class (stl/css :modal-title) + :data-test "onboarding-newsletter-title"} + (tr "onboarding.newsletter.title")] + [:p {:class (stl/css :modal-text)} + (tr "onboarding-v2.newsletter.desc")] + + + [:div {:class (stl/css :newsletter-options)} + [:div {:class (stl/css :input-wrapper)} + [:label {:for "newsletter-updates"} + [:span {:class (stl/css-case :global/checked @newsletter-updates)} + (when @newsletter-updates + i/status-tick)] + (tr "onboarding-v2.newsletter.updates") + [:input {:type "checkbox" + :id "newsletter-updates" + :on-change #(toggle newsletter-updates)}]]] + + [:div {:class (stl/css :input-wrapper)} + [:label {:for "newsletter-news"} + [:span {:class (stl/css-case :global/checked @newsletter-news)} + (when @newsletter-news + i/status-tick)] + (tr "onboarding-v2.newsletter.news") + [:input {:type "checkbox" + :id "newsletter-news" + :on-change #(toggle newsletter-news)}]]]] + + [:p {:class (stl/css :modal-text)} + (tr "onboarding-v2.newsletter.privacy1") + [:a {:class (stl/css :modal-link) + :target "_blank" + :href "https://penpot.app/privacy"} + (tr "onboarding.newsletter.policy")]] + [:p {:class (stl/css :modal-text)} + (tr "onboarding-v2.newsletter.privacy2")] + + [:button {:on-click accept + :class (stl/css :accept-btn)} (tr "labels.continue")]]]])) diff --git a/frontend/src/app/main/ui/onboarding/newsletter.scss b/frontend/src/app/main/ui/onboarding/newsletter.scss new file mode 100644 index 0000000000..20b4230df3 --- /dev/null +++ b/frontend/src/app/main/ui/onboarding/newsletter.scss @@ -0,0 +1,76 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + @extend .modal-container-base; + position: relative; + display: grid; + grid-template-columns: auto auto; + gap: $s-32; + padding-inline: $s-100; + padding-block-start: $s-100; + padding-block-end: $s-72; + margin: 0; + width: $s-960; + height: $s-632; + max-width: $s-960; + max-height: $s-632; +} + +.modal-left { + width: $s-172; + margin-block-end: $s-64; + img { + width: $s-172; + border-radius: $br-8 0 0 $br-8; + } +} + +.modal-right { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: $s-40 auto auto auto auto $s-32; + gap: $s-24; + position: relative; +} + +.modal-title { + @include bigTitleTipography; + color: var(--modal-title-foreground-color); +} + +.modal-text { + @include bodyLargeTypography; + color: var(--modal-text-foreground-color); + margin: 0; +} + +.newsletter-options { + display: grid; + gap: $s-16; + margin-inline-start: $s-16; +} + +.input-wrapper { + @extend .input-checkbox; +} + +.modal-link { + @include bodyLargeTypography; + color: var(--modal-link-foreground-color); + margin: 0; +} + +.accept-btn { + @extend .modal-accept-btn; + justify-self: flex-end; +} diff --git a/frontend/src/app/main/ui/onboarding/questions.cljs b/frontend/src/app/main/ui/onboarding/questions.cljs index d5cd52a95d..ae9f5d4274 100644 --- a/frontend/src/app/main/ui/onboarding/questions.cljs +++ b/frontend/src/app/main/ui/onboarding/questions.cljs @@ -6,84 +6,135 @@ (ns app.main.ui.onboarding.questions "External form for onboarding questions." + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.config :as cf] + [app.main.data.modal :as modal] [app.main.data.users :as du] [app.main.store :as st] [app.main.ui.components.forms :as fm] + [app.main.ui.icons :as i] [app.util.i18n :as i18n :refer [tr]] [cljs.spec.alpha :as s] [cuerdas.core :as str] [rumext.v2 :as mf])) (mf/defc step-container - [{:keys [form step on-next on-prev children] :as props}] - [:& fm/form {:form form :on-submit on-next} - [:div.step-header - [:div.step-number (str/ffmt "%/4" step)]] + [{:keys [form step on-next on-prev children class] :as props}] + + [:& fm/form {:form form :on-submit on-next :class (dm/str class " " (stl/css :form-wrapper))} + [:div {:class (stl/css :paginator)} (str/ffmt "%/4" step)] children - [:div.buttons - [:div.step-next - [:& fm/submit-button - {:label (if (< step 4) (tr "questions.next") (tr "questions.start")) - :class "step-next"}]] + + [:div {:class (stl/css :action-buttons)} (when on-prev - [:div.step-prev - [:button {:on-click on-prev} (tr "questions.previous")]])]]) + [:button {:class (stl/css :prev-button) + :on-click on-prev} (tr "questions.previous")]) + + [:> fm/submit-button* + {:label (if (< step 4) (tr "questions.next") (tr "questions.start")) + :class (stl/css :next-button)}]]]) (s/def ::questions-form-step-1 (s/keys :req-un [::planning])) (mf/defc step-1 [{:keys [on-next form] :as props}] - [:& step-container {:form form :step 1 :on-next on-next} - [:img.header-image {:src "images/form/use-for-1.png" :alt (tr "questions.lets-get-started")}] - [:h1 (tr "questions.lets-get-started")] - [:p.intro (tr "questions.your-feedback-will-help-us")] - [:h3 (tr "questions.questions-how-are-you-planning-to-use-penpot")] - [:& fm/select {:options [{:label (tr "questions.select-option") :value "" :key "questions-how-are-you-planning-to-use-penpot" :disabled true} - {:label (tr "questions.discover-more-about-penpot") :value "discover-more-about-penpot" :key "discover-more-about-penpot"} - {:label (tr "questions.test-penpot-to-see-if-its-a-fit-for-team") :value "test-penpot-to-see-if-its-a-fit-for-team" :key "test-penpot-to-see-if-its-a-fit-for-team"} - {:label (tr "questions.start-to-work-on-my-project") :value "start-to-work-on-my-project" :key "start-to-work-on-my-project"} - {:label (tr "questions.get-the-code-from-my-team-project") :value "get-the-code-from-my-team-project" :key "get-the-code-from-my-team-project"} - {:label (tr "questions.leave-feedback-for-my-team-project") :value "leave-feedback-for-my-team-project" :key "leave-feedback-for-my-team-project"} - {:label (tr "questions.work-in-concept-ideas") :value "work-in-concept-ideas" :key "work-in-concept-ideas"} - {:label (tr "questions.try-out-before-using-penpot-on-premise") :value "try-out-before-using-penpot-on-premise" :key "try-out-before-using-penpot-on-premise"}] - :default "" - :name :planning}]]) + [:& step-container {:form form :step 1 :on-next on-next :class (stl/css :step-1)} + [:img {:class (stl/css :header-image) + :src "images/form/use-for-1.png" :alt (tr "questions.lets-get-started")}] + [:h1 {:class (stl/css :modal-title)} (tr "questions.lets-get-started")] + [:p {:class (stl/css :modal-text)} (tr "questions.your-feedback-will-help-us")] + + [:div {:class (stl/css :modal-question)} + [:h3 {:class (stl/css :modal-subtitle)} (tr "questions.questions-how-are-you-planning-to-use-penpot")] + [:& fm/select + {:options [{:label (tr "questions.select-option") + :value "" :key "questions-how-are-you-planning-to-use-penpot" + :disabled true} + {:label (tr "questions.discover-more-about-penpot") + :value "discover-more-about-penpot" + :key "discover-more-about-penpot"} + {:label (tr "questions.test-penpot-to-see-if-its-a-fit-for-team") + :value "test-penpot-to-see-if-its-a-fit-for-team" + :key "test-penpot-to-see-if-its-a-fit-for-team"} + {:label (tr "questions.start-to-work-on-my-project") + :value "start-to-work-on-my-project" + :key "start-to-work-on-my-project"} + {:label (tr "questions.get-the-code-from-my-team-project") + :value "get-the-code-from-my-team-project" + :key "get-the-code-from-my-team-project"} + {:label (tr "questions.leave-feedback-for-my-team-project") + :value "leave-feedback-for-my-team-project" + :key "leave-feedback-for-my-team-project"} + {:label (tr "questions.work-in-concept-ideas") + :value "work-in-concept-ideas" + :key "work-in-concept-ideas"} + {:label (tr "questions.try-out-before-using-penpot-on-premise") + :value "try-out-before-using-penpot-on-premise" + :key "try-out-before-using-penpot-on-premise"}] + :default "" + :name :planning + :dropdown-class (stl/css :question-dropdown)}]]]) (s/def ::questions-form-step-2 - (s/keys :req-un [::experience-branding-illustrations-marketing-pieces ::experience-interface-design-visual-assets-design-systems ::experience-interface-wireframes-user-journeys-flows-navigation-trees])) + (s/keys :req-un [::experience-branding-illustrations-marketing-pieces + ::experience-interface-design-visual-assets-design-systems + ::experience-interface-wireframes-user-journeys-flows-navigation-trees])) (mf/defc step-2 [{:keys [on-next on-prev form] :as props}] - [:& step-container {:form form :step 2 :on-next on-next :on-prev on-prev} - [:h3 (tr "questions.describe-your-experience-working-on")] + [:& step-container {:form form :step 2 :on-next on-next :on-prev on-prev :class (stl/css :step-2)} + [:h1 {:class (stl/css :modal-title)} + (tr "questions.describe-your-experience-working-on")] - [:div.section (tr "branding-illustrations-marketing-pieces")] - [:& fm/radio-buttons {:options [{:label (tr "questions.none") :value "none"} - {:label (tr "questions.some") :value "some"} - {:label (tr "questions.a-lot") :value "a-lot"}] - :name :experience-branding-illustrations-marketing-pieces}] + [:div {:class (stl/css-case :modal-question true + :question-centered true)} + [:div {:class (stl/css-case :modal-subtitle true + :centered true)} + (tr "branding-illustrations-marketing-pieces")] + [:& fm/radio-buttons {:options [{:label (tr "questions.none") :value "none"} + {:label (tr "questions.some") :value "some"} + {:label (tr "questions.a-lot") :value "a-lot"}] + :name :experience-branding-illustrations-marketing-pieces + :class (stl/css :radio-btns)}]] - [:div.section (tr "questions.interface-design-visual-assets-design-systems")] - [:& fm/radio-buttons {:options [{:label (tr "questions.none") :value "none"} - {:label (tr "questions.some") :value "some"} - {:label (tr "questions.a-lot") :value "a-lot"}] - :name :experience-interface-design-visual-assets-design-systems}] + [:div {:class (stl/css-case :modal-question true + :question-centered true)} + [:div {:class (stl/css-case :modal-subtitle true + :centered true)} + (tr "questions.interface-design-visual-assets-design-systems")] + [:& fm/radio-buttons {:options [{:label (tr "questions.none") :value "none"} + {:label (tr "questions.some") :value "some"} + {:label (tr "questions.a-lot") :value "a-lot"}] + :name :experience-interface-design-visual-assets-design-systems + :class (stl/css :radio-btns)}]] - [:div.section (tr "questions.wireframes-user-journeys-flows-navigation-trees")] - [:& fm/radio-buttons {:options [{:label (tr "questions.none") :value "none"} - {:label (tr "questions.some") :value "some"} - {:label (tr "questions.a-lot") :value "a-lot"}] - :name :experience-interface-wireframes-user-journeys-flows-navigation-trees}]]) + [:div {:class (stl/css-case :modal-question true + :question-centered true)} + [:div {:class (stl/css-case :modal-subtitle true + :centered true)} + (tr "questions.wireframes-user-journeys-flows-navigation-trees")] + [:& fm/radio-buttons {:options [{:label (tr "questions.none") :value "none"} + {:label (tr "questions.some") :value "some"} + {:label (tr "questions.a-lot") :value "a-lot"}] + :name :experience-interface-wireframes-user-journeys-flows-navigation-trees + :class (stl/css :radio-btns)}]]]) (s/def ::questions-form-step-3 (s/keys :req-un [::experience-design-tool] - :opt-un[::experience-design-tool-other])) + :opt-un [::experience-design-tool-other])) + +(defn- step-1-form-validator + [errors data] + (let [planning (-> (:planning data) (str/trim))] + (cond-> errors + (= planning "") + (assoc :planning {:code "missing"})))) (defn- step-3-form-validator [errors data] @@ -104,24 +155,31 @@ (swap! form d/dissoc-in [:data :experience-design-tool-other]) (swap! form d/dissoc-in [:errors :experience-design-tool-other])))))] - [:& step-container {:form form :step 3 :on-next on-next :on-prev on-prev} - [:h3 (tr "question.design-tool-more-experienced-with")] - [:& fm/radio-buttons {:options [{:label (tr "questions.figma") :value "figma" :image "images/form/figma.png"} - {:label (tr "questions.sketch") :value "sketch" :image "images/form/sketch.png"} - {:label (tr "questions.adobe-xd") :value "adobe-xd" :image "images/form/adobe-xd.png"} - {:label (tr "questions.canva") :value "canva" :image "images/form/canva.png"} - {:label (tr "questions.invision") :value "invision" :image "images/form/invision.png"} - {:label (tr "questions.never-used-a-tool") :value "never-used-a-tool" :image "images/form/never-used.png"} - {:label (tr "questions.other") :value "other"}] - :name :experience-design-tool - :on-change-value on-design-tool-change}] - [:div.other - [:label (tr "questions.other")] - [:& fm/input {:name :experience-design-tool-other :label (tr "questions.other") :disabled (not= experience-design-tool "other")}]]])) + [:& step-container {:form form :step 3 :on-next on-next :on-prev on-prev :class (stl/css :step-3)} + [:h1 {:class (stl/css :modal-title)} + (tr "question.design-tool-more-experienced-with")] + [:div {:class (stl/css :radio-wrapper)} + [:& fm/radio-buttons {:options [{:label (tr "questions.figma") :value "figma" :image "images/form/figma.png" :area "image1"} + {:label (tr "questions.sketch") :value "sketch" :image "images/form/sketch.png" :area "image2"} + {:label (tr "questions.adobe-xd") :value "adobe-xd" :image "images/form/adobe-xd.png" :area "image3"} + {:label (tr "questions.canva") :value "canva" :image "images/form/canva.png" :area "image4"} + {:label (tr "questions.invision") :value "invision" :image "images/form/invision.png" :area "image5"} + {:label (tr "questions.never-used-one") :area "image6" :value "never-used-a-tool" :icon i/curve} + {:label (tr "questions.other") :value "other" :area "other"}] + :name :experience-design-tool + :image true + :class (stl/css :image-radio) + :on-change on-design-tool-change}] + + [:& fm/input {:name :experience-design-tool-other + :class (stl/css :input-spacing) + :placeholder (tr "questions.other") + :label "" + :disabled (not= experience-design-tool "other")}]]])) (s/def ::questions-form-step-4 (s/keys :req-un [::team-size ::role] - :opt-un [::role-other])) + :opt-un [::role-other])) (defn- step-4-form-validator [errors data] @@ -142,34 +200,43 @@ (swap! form d/dissoc-in [:data :role-other]) (swap! form d/dissoc-in [:errors :role-other])))))] - [:& step-container {:form form :step 4 :on-next on-next :on-prev on-prev} - [:h3 (tr "questions.role")] - [:& fm/radio-buttons {:options [{:label (tr "questions.designer") :value "designer"} - {:label (tr "questions.developer") :value "developer"} - {:label (tr "questions.manager") :value "manager"} - {:label (tr "questions.founder") :value "founder"} - {:label (tr "questions.marketing") :value "marketing"} - {:label (tr "questions.student-teacher") :value "student-teacher"} - {:label (tr "questions.other") :value "other"}] - :name :role - :on-change-value on-role-change}] - [:div.other - [:label (tr "questions.other")] - [:& fm/input {:name :role-other :label (tr "questions.other") :disabled (not= role "other")}]] + [:& step-container {:form form :step 4 :on-next on-next :on-prev on-prev :class (stl/css :step-4)} + [:h1 {:class (stl/css :modal-title)} (tr "questions.role")] + [:div {:class (stl/css :radio-wrapper)} + [:& fm/radio-buttons {:options [{:label (tr "questions.designer") :value "designer"} + {:label (tr "questions.developer") :value "developer"} + {:label (tr "questions.manager") :value "manager"} + {:label (tr "questions.founder") :value "founder"} + {:label (tr "questions.marketing") :value "marketing"} + {:label (tr "questions.student-teacher") :value "student-teacher"} + {:label (tr "questions.other") :value "other"}] + :name :role + :on-change on-role-change}] + [:& fm/input {:name :role-other + :class (stl/css :input-spacing) + :label "" + :placeholder (tr "questions.other") + :disabled (not= role "other")}]] - [:h3 (tr "questions.team-size")] - [:& fm/select {:options [{:label (tr "questions.select-option") :value "" :key "team-size" :disabled true} - {:label (tr "questions.more-than-50") :value "more-than-50" :key "more-than-50"} - {:label (tr "questions.31-50") :value "31-50" :key "31-50"} - {:label (tr "questions.11-30") :value "11-30" :key "11-30"} - {:label (tr "questions.2-10") :value "2-10" :key "2-10"} - {:label (tr "questions.freelancer") :value "freelancer" :key "freelancer"} - {:label (tr "questions.personal-project") :value "personal-project" :key "personal-project"}] - :default "" - :name :team-size}]])) + [:div {:class (stl/css :modal-question)} + [:h3 {:class (stl/css :modal-subtitle)} (tr "questions.team-size")] + [:& fm/select {:options [{:label (tr "questions.select-option") :value "" :key "team-size" :disabled true} + {:label (tr "questions.more-than-50") :value "more-than-50" :key "more-than-50"} + {:label (tr "questions.31-50") :value "31-50" :key "31-50"} + {:label (tr "questions.11-30") :value "11-30" :key "11-30"} + {:label (tr "questions.2-10") :value "2-10" :key "2-10"} + {:label (tr "questions.freelancer") :value "freelancer" :key "freelancer"} + {:label (tr "questions.personal-project") :value "personal-project" :key "personal-project"}] + :default "" + :name :team-size}]]])) -(mf/defc questions - [{:keys []}] +;; NOTE: we don't register it on registry modal because we reference +;; this modal directly on the ui namespace. + +(mf/defc questions-modal + {::mf/register modal/components + ::mf/register-as :onboarding-questions} + [] (let [container (mf/use-ref) step (mf/use-state 1) clean-data (mf/use-state {}) @@ -177,48 +244,56 @@ ;; Forms are initialized here because we can go back and forth between the steps ;; and we want to keep the filled info step-1-form (fm/use-form - :initial {} - :spec ::questions-form-step-1) + :initial {} + :validators [step-1-form-validator] + :spec ::questions-form-step-1) step-2-form (fm/use-form - :initial {} - :spec ::questions-form-step-2) + :initial {} + :spec ::questions-form-step-2) step-3-form (fm/use-form - :initial {} - :validators [step-3-form-validator] - :spec ::questions-form-step-3) + :initial {} + :validators [step-3-form-validator] + :spec ::questions-form-step-3) step-4-form (fm/use-form - :initial {} - :validators [step-4-form-validator] - :spec ::questions-form-step-4) + :initial {} + :validators [step-4-form-validator] + :spec ::questions-form-step-4) on-next (mf/use-fn - (fn [form] - (swap! step inc) - (swap! clean-data merge (:clean-data @form)))) + (fn [form] + (swap! step inc) + (swap! clean-data merge (:clean-data @form)))) on-prev (mf/use-fn - (fn [] - (swap! step dec))) + (fn [] + (swap! step dec))) on-submit (mf/use-fn - (mf/deps @clean-data) - (fn [form] - (let [questionnaire (merge @clean-data (:clean-data @form))] - (reset! clean-data questionnaire) - (st/emit! (du/mark-questions-as-answered questionnaire)))))] + (mf/deps @clean-data) + (fn [form] + (let [questionnaire (merge @clean-data (:clean-data @form))] + (reset! clean-data questionnaire) + (st/emit! (du/mark-questions-as-answered questionnaire)) - [:div.modal-wrapper.questions-form - [:div.modal-overlay - [:div.modal-container.onboarding.onboarding-v2 {:ref container} - [:img.deco.left {:src "images/deco-left.png" :border 0}] - [:img.deco.right {:src "images/deco-right.png" :border 0}] - [:div.signup-questions - (case @step - 1 [:& step-1 {:on-next on-next :on-prev on-prev :form step-1-form}] - 2 [:& step-2 {:on-next on-next :on-prev on-prev :form step-2-form}] - 3 [:& step-3 {:on-next on-next :on-prev on-prev :form step-3-form}] - 4 [:& step-4 {:on-next on-submit :on-prev on-prev :form step-4-form}])]]]])) + (cond + (contains? cf/flags :onboarding-newsletter) + (modal/show! {:type :onboarding-newsletter}) + + (contains? cf/flags :onboarding-team) + (modal/show! {:type :onboarding-team}) + + :else + (modal/hide!)))))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container) + :ref container} + (case @step + 1 [:& step-1 {:on-next on-next :on-prev on-prev :form step-1-form}] + 2 [:& step-2 {:on-next on-next :on-prev on-prev :form step-2-form}] + 3 [:& step-3 {:on-next on-next :on-prev on-prev :form step-3-form}] + 4 [:& step-4 {:on-next on-submit :on-prev on-prev :form step-4-form}])]])) diff --git a/frontend/src/app/main/ui/onboarding/questions.scss b/frontend/src/app/main/ui/onboarding/questions.scss new file mode 100644 index 0000000000..1496215ebc --- /dev/null +++ b/frontend/src/app/main/ui/onboarding/questions.scss @@ -0,0 +1,138 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + max-width: $s-744; + max-height: fit-content; + width: $s-744; + padding-inline: $s-100; + padding-block-start: $s-40; + padding-block-end: $s-72; + border-radius: $br-8; + border: $s-2 solid var(--modal-border-color); + background-color: var(--modal-background-color); +} + +.form-wrapper { + display: grid; + grid-template-columns: 1fr; + gap: $s-24; +} + +// STEP CONTAINER +.paginator { + @include smallTitleTipography; + height: $s-20; + text-align: right; + color: var(--modal-text-foreground-color); +} + +.action-buttons { + @extend .modal-action-btns; +} +.next-button { + @extend .modal-accept-btn; +} + +.prev-button { + @extend .modal-cancel-btn; +} + +// STEP 1 + +// .step-1 { +// max-height: $s-468; +// height: $s-468; +// } + +.header-image { + height: $s-112; + width: auto; + margin-inline-start: auto; +} + +.modal-title { + @include bigTitleTipography; + color: var(--modal-title-foreground-color); + min-height: $s-32; + margin-block: auto; +} + +.modal-subtitle { + @include bodyLargeTypography; + color: var(--modal-title-foreground-color); + margin: 0; + padding: 0; +} + +.modal-text { + @include bodyLargeTypography; + color: var(--modal-text-foreground-color); + margin: 0; +} + +// STEP-2 + +.step-2 { + grid-template-rows: $s-20 auto auto auto auto $s-32; +} + +.modal-question { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: $s-16 $s-32; + gap: $s-16; + height: fit-content; +} + +.question-centered { + width: $s-424; + grid-template-rows: auto $s-32; + margin: 0 auto; +} + +.radio-wrapper { + display: grid; + grid-template-columns: 1fr; + gap: $s-8; +} + +// STEP-3 +.step-3 { + grid-template-rows: $s-20 auto auto $s-32; +} + +.image-radio { + display: grid; + grid-template-rows: 1fr 1fr $s-32; + grid-template-columns: $s-88 $s-92 $s-92 $s-92 $s-88; + grid-template-areas: + ". image1 image2 image3 ." + ". image4 image5 image6 ." + "other other other other other"; + row-gap: $s-16; + column-gap: $s-24; +} + +.input-spacing { + height: $s-32; + width: calc(100% - $s-24); + margin-inline-start: $s-24; + margin-block-end: $s-8; +} + +// STEP-4 + +.step-4 { + grid-template-rows: $s-20 auto auto auto $s-32; + row-gap: $s-16; +} diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs index e37244969f..a3a007c38a 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.cljs +++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs @@ -5,11 +5,13 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.onboarding.team-choice + (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dmc] [app.common.spec :as us] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.refs :as refs] [app.main.store :as st] @@ -19,34 +21,44 @@ [app.util.router :as rt] [app.util.timers :as tm] [cljs.spec.alpha :as s] - [potok.core :as ptk] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) (s/def ::name ::us/not-empty-string) (s/def ::team-form (s/keys :req-un [::name])) -(mf/defc team-modal-right +(mf/defc team-modal-left [] - [:div.team-right - [:h2.subtitle (tr "onboarding.team-modal.create-team")] - [:p.info (tr "onboarding.team-modal.create-team-desc")] - [:ul.team-features - [:li.feature - [:span.icon i/file-html] - [:p.feature-txt (tr "onboarding.team-modal.create-team-feature-1")]] - [:li.feature - [:span.icon i/pointer-inner] - [:p.feature-txt (tr "onboarding.team-modal.create-team-feature-2")]] - [:li.feature - [:span.icon i/tree] - [:p.feature-txt (tr "onboarding.team-modal.create-team-feature-3")]] - [:li.feature - [:span.icon i/user] - [:p.feature-txt (tr "onboarding.team-modal.create-team-feature-4")]] - [:li.feature - [:span.icon i/tick] - [:p.feature-txt (tr "onboarding.team-modal.create-team-feature-5")]]]]) + [:div {:class (stl/css :modal-left)} + [:h1 {:class (stl/css :modal-title)} + (tr "onboarding-v2.welcome.title")] + + [:h2 {:class (stl/css :modal-subtitle)} + (tr "onboarding.team-modal.team-definition")] + [:p {:class (stl/css :modal-text)} + (tr "onboarding.team-modal.create-team-desc")] + [:ul {:class (stl/css :team-features)} + [:li {:class (stl/css :feature)} + [:span {:class (stl/css :icon)} i/document] + [:p {:class (stl/css :modal-desc)} + (tr "onboarding.team-modal.create-team-feature-1")]] + [:li {:class (stl/css :feature)} + [:span {:class (stl/css :icon)} i/move] + [:p {:class (stl/css :modal-desc)} + (tr "onboarding.team-modal.create-team-feature-2")]] + [:li {:class (stl/css :feature)} + [:span {:class (stl/css :icon)} i/tree] + [:p {:class (stl/css :modal-desc)} + (tr "onboarding.team-modal.create-team-feature-3")]] + [:li {:class (stl/css :feature)} + [:span {:class (stl/css :icon)} i/user] + [:p {:class (stl/css :modal-desc)} + (tr "onboarding.team-modal.create-team-feature-4")]] + [:li {:class (stl/css :feature)} + [:span {:class (stl/css :icon)} i/tick] + [:p {:class (stl/css :modal-desc)} + (tr "onboarding.team-modal.create-team-feature-5")]]]]) (mf/defc onboarding-team-modal {::mf/register modal/components @@ -57,7 +69,7 @@ :validators [(fm/validate-not-empty :name (tr "auth.name.not-all-space")) (fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))]) on-submit - (mf/use-callback + (mf/use-fn (fn [form _] (let [tname (get-in @form [:clean-data :name])] (st/emit! (modal/show {:type :onboarding-team-invitations :name tname}) @@ -73,36 +85,48 @@ :step 1})))) teams (mf/deref refs/teams)] - (if (< (count teams) 2) - [:div.modal-overlay - [:div.modal-container.onboarding-team.animated.fadeIn - [:div.team-left - [:h2.title (tr "onboarding.team-modal.create-team")] - [:p.info (tr "onboarding.choice.team-up.create-team-desc")] - [:& fm/form {:form form - :on-submit on-submit} - [:& fm/input {:type "text" - :name :name - :label (tr "onboarding.choice.team-up.create-team-placeholder")}] + (mf/with-effect [teams] + (when (> (count teams) 1) + (st/emit! (modal/hide)))) - [:& fm/submit-button - {:label (tr "onboarding.choice.team-up.continue-creating-team")}]] + (when (< (count teams) 2) + [:div {:class (stl/css :modal-overlay)} + [:div.animated.fadeIn {:class (stl/css :modal-container)} + [:& team-modal-left] + [:div {:class (stl/css :separator)}] + [:div {:class (stl/css :modal-right)} + [:div {:class (stl/css :first-block)} + [:h2 {:class (stl/css :modal-subtitle)} + (tr "onboarding.team-modal.create-team")] + [:p {:class (stl/css :modal-text)} + (tr "onboarding.choice.team-up.create-team-desc")] + [:& fm/form {:form form + :class (stl/css :modal-form) + :on-submit on-submit} - [:h2.title (tr "onboarding.choice.team-up.start-without-a-team")] - [:p.info (tr "onboarding.choice.team-up.start-without-a-team-description")] + [:& fm/input {:type "text" + :class (stl/css :team-name-input) + :name :name + :placeholder "Team name" + :label (tr "onboarding.choice.team-up.create-team-placeholder")}] - [:div - [:button.btn-primary.btn-large {:on-click on-skip} (tr "onboarding.choice.team-up.continue-without-a-team")]]] - [:& team-modal-right] - [:div.paginator "1/2"] + [:div {:class (stl/css :action-buttons)} + [:> fm/submit-button* + {:class (stl/css :accept-button) + :label (tr "onboarding.choice.team-up.continue-creating-team")}]]]] + [:div {:class (stl/css :second-block)} + [:h2 {:class (stl/css :modal-subtitle)} + (tr "onboarding.choice.team-up.start-without-a-team")] + [:p {:class (stl/css :modal-text)} + (tr "onboarding.choice.team-up.start-without-a-team-description")] - [:img.deco.square {:src "images/deco-square.svg" :border "0"}] - [:img.deco.circle {:src "images/deco-circle.svg" :border "0"}] - [:img.deco.line1 {:src "images/deco-line1.svg" :border "0"}] - [:img.deco.line2 {:src "images/deco-line2.svg" :border "0"}]]] + [:div {:class (stl/css :action-buttons)} + [:button {:class (stl/css :accept-button) + :on-click on-skip} + (tr "onboarding.choice.team-up.continue-without-a-team")]]]] - (st/emit! (modal/hide))))) + [:div {:class (stl/css :paginator)} "1/2"]]]))) (defn get-available-roles [] @@ -119,8 +143,9 @@ (mf/defc onboarding-team-invitations-modal {::mf/register modal/components - ::mf/register-as :onboarding-team-invitations} - [{:keys [name] :as props}] + ::mf/register-as :onboarding-team-invitations + ::mf/props :obj} + [{:keys [name]}] (let [initial (mf/use-memo (constantly {:role "editor" :name name})) @@ -128,11 +153,11 @@ :initial initial) params (:clean-data @form) emails (:emails params) - + roles (mf/use-memo #(get-available-roles)) on-success - (mf/use-callback + (mf/use-fn (fn [_form response] (let [team-id (:id response)] (st/emit! @@ -142,13 +167,13 @@ (modal/hide)))))) on-error - (mf/use-callback + (mf/use-fn (fn [_form _response] - (st/emit! (dm/error "Error on creating team.")))) + (st/emit! (msg/error "Error on creating team.")))) ;; The SKIP branch only creates the team, without invitations on-invite-later - (mf/use-callback + (mf/use-fn (fn [_] (let [mdata {:on-success (partial on-success form) :on-error (partial on-error form)} @@ -161,13 +186,13 @@ ;; The SUBMIT branch creates the team with the invitations on-invite-now - (mf/use-callback - (fn [_] + (mf/use-fn + (fn [form] (let [mdata {:on-success (partial on-success form) :on-error (partial on-error form)} params (:clean-data @form) emails (:emails params)] - + (st/emit! (if (> (count emails) 0) ;; If the user is only inviting to itself we don't call to create-team-with-invitations (dd/create-team-with-invitations (with-meta params mdata)) @@ -178,59 +203,60 @@ :role (:role params) :name name :step 2}))))) - + on-submit - (mf/use-callback - (fn [_] + (mf/use-fn + (fn [form] (let [params (:clean-data @form) emails (:emails params)] (if (> (count emails) 0) (on-invite-now form) - (on-invite-later form)))))] + (on-invite-later form)) + (modal/hide!))))] - [:div.modal-overlay - [:div.modal-container.onboarding-team-members.animated.fadeIn - [:div.team-left - [:h2.title (tr "onboarding.choice.team-up.invite-members")] - [:p.info (tr "onboarding.choice.team-up.invite-members-info")] + [:div {:class (stl/css :modal-overlay)} + [:div.animated.fadeIn {:class (stl/css :modal-container)} + [:& team-modal-left] + [:div {:class (stl/css :separator)}] + [:div {:class (stl/css :modal-right-invitations)} + [:h2 {:class (stl/css :modal-subtitle)} (tr "onboarding.choice.team-up.invite-members")] + [:p {:class (stl/css :modal-text)} (tr "onboarding.choice.team-up.invite-members-info")] [:& fm/form {:form form + :class (stl/css :modal-form-invitations) :on-submit on-submit} - [:div.invite-row - [:div.role-wrapper - [:span.rol (tr "onboarding.choice.team-up.roles")] - [:& fm/select {:name :role :options roles}]] + [:div {:class (stl/css :role-select)} + [:p {:class (stl/css :role-title)} (tr "onboarding.choice.team-up.roles")] + [:& fm/select {:name :role :options roles}]] + [:div {:class (stl/css :invitation-row)} [:& fm/multi-input {:type "email" :name :emails :auto-focus? true :trim true :valid-item-fn us/parse-email :caution-item-fn #{} - :on-submit on-submit - :label (tr "modals.invite-member.emails")}]] + :label (tr "modals.invite-member.emails") + :on-submit on-submit}]] - [:div.buttons - [:button.btn-secondary.btn-large - {:on-click #(st/emit! (modal/show {:type :onboarding-team}) - (ptk/event ::ev/event {::ev/name "invite-members-back" - ::ev/origin "onboarding" - :name name - :step 2}))} + [:div {:class (stl/css :action-buttons)} + [:button {:class (stl/css :back-button) + :on-click #(st/emit! (modal/show {:type :onboarding-team}) + (ptk/event ::ev/event {::ev/name "invite-members-back" + ::ev/origin "onboarding" + :name name + :step 2}))} (tr "labels.back")] - [:& fm/submit-button - {:label - (if (> (count emails) 0) - (tr "onboarding.choice.team-up.create-team-and-send-invites") - (tr "onboarding.choice.team-up.create-team-without-inviting"))}]] - [:div.skip-action - (tr "onboarding.choice.team-up.create-team-and-send-invites-description")]]] - [:& team-modal-right] - [:div.paginator "2/2"] - [:img.deco.square {:src "images/deco-square.svg" :border "0"}] - [:img.deco.circle {:src "images/deco-circle.svg" :border "0"}] - [:img.deco.line1 {:src "images/deco-line1.svg" :border "0"}] - [:img.deco.line2 {:src "images/deco-line2.svg" :border "0"}]]])) + [:> fm/submit-button* + {:class (stl/css :accept-button) + :label (if (> (count emails) 0) + (tr "onboarding.choice.team-up.create-team-and-invite") + (tr "onboarding.choice.team-up.create-team-without-invite"))}]] + [:div {:class (stl/css :modal-hint)} + (dmc/str "(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")")]]] + + + [:div {:class (stl/css :paginator)} "2/2"]]])) diff --git a/frontend/src/app/main/ui/onboarding/team_choice.scss b/frontend/src/app/main/ui/onboarding/team_choice.scss new file mode 100644 index 0000000000..25437a67cc --- /dev/null +++ b/frontend/src/app/main/ui/onboarding/team_choice.scss @@ -0,0 +1,191 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + position: relative; + display: grid; + grid-template-columns: 1fr $s-32 1fr; + gap: $s-24; + width: $s-908; + height: $s-632; + padding-inline: $s-100; + padding-block-start: $s-40; + padding-block-end: $s-72; + border-radius: $br-8; + background-color: var(--modal-background-color); + border: $s-2 solid var(--modal-border-color); +} + +.paginator { + @include bodySmallTypography; + position: absolute; + top: $s-40; + right: $s-100; + padding: $s-4; + border-radius: $br-6; + color: var(--color-foreground-secondary); +} + +// MODAL LEFT +.modal-left { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: $s-32 auto auto 1fr; + gap: $s-16; + max-height: $s-512; + padding-block-start: $s-44; +} + +.modal-title { + @include bigTitleTipography; + color: var(--modal-title-foreground-color); + margin-bottom: $s-8; +} + +.modal-subtitle { + @include medTitleTipography; + color: var(--modal-title-foreground-color); +} + +.modal-text { + @include bodyLargeTypography; + color: var(--modal-text-foreground-color); + margin: 0; +} + +.modal-desc { + @include smallTitleTipography; + margin: 0; + color: var(--modal-title-foreground-color); +} + +.team-features { + @include flexColumn; + gap: $s-16; + margin: 0; +} + +.feature { + @include flexRow; + gap: $s-16; +} + +.icon { + @include flexCenter; + height: $s-32; + width: $s-32; + border-radius: $br-circle; + border: $s-1 solid var(--color-accent-primary); + svg { + @extend .button-icon; + stroke: var(--color-accent-primary); + } +} + +.action-buttons { + @extend .modal-action-btns; + justify-content: flex-end; +} + +.accept-button { + @extend .modal-accept-btn; +} + +.back-button { + @extend .modal-cancel-btn; +} + +// SEPARATOR +.separator { + width: $s-8; + height: $s-420; + border-radius: $br-8; + margin-block-start: $s-92; + opacity: 42%; + background-color: var(--modal-separator-backogrund-color); +} + +// MODAL RIGHT TEAM +.modal-right { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr auto; + gap: $s-24; + max-height: $s-512; + margin-block-start: $s-92; +} + +.first-block, +.second-block { + @include flexColumn; + gap: $s-16; +} + +.modal-form { + display: grid; + grid-template-columns: 1fr; + gap: $s-16; +} + +.team-name-input { + @extend .input-element-label; + label { + @include flexColumn; + @include bodySmallTypography; + align-items: flex-start; + width: 100%; + border: none; + background-color: transparent; + height: 100%; + + input { + @include bodySmallTypography; + margin-top: $s-8; + } + } +} + +// MODAL RIGHT INVITATIONS + +.modal-right-invitations { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto auto 1fr; + gap: $s-16; + max-height: $s-512; + margin-block-start: $s-92; +} + +.modal-form-invitations { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto 1fr auto auto; + margin-block-end: $s-72; + gap: $s-8; +} + +.role-title { + @include uppercaseTitleTipography; + margin-block-end: $s-8; + color: var(--modal-title-foreground-color); +} + +.invitation-row { + margin: 0; + color: var(--modal-title-foreground-color); +} + +.modal-hint { + @include bodySmallTypography; + color: var(--modal-text-foreground-color); + text-align: right; +} diff --git a/frontend/src/app/main/ui/onboarding/templates.cljs b/frontend/src/app/main/ui/onboarding/templates.cljs index 01ed353455..af0abb2b1d 100644 --- a/frontend/src/app/main/ui/onboarding/templates.cljs +++ b/frontend/src/app/main/ui/onboarding/templates.cljs @@ -16,7 +16,7 @@ [app.util.http :as http] [app.util.i18n :as i18n :refer [tr]] [app.util.webapi :as wapi] - [beicon.core :as rx] + [beicon.v2.core :as rx] [rumext.v2 :as mf])) (mf/defc template-item @@ -39,13 +39,12 @@ (fn [] (reset! downloading? true) (->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors}) - (rx/subs (fn [{:keys [body] :as response}] - (open-import-modal {:name name :uri (wapi/create-uri body)})) - (fn [error] - (js/console.log "error" error)) - (fn [] - (reset! downloading? false))))) - ] + (rx/subs! (fn [{:keys [body] :as response}] + (open-import-modal {:name name :uri (wapi/create-uri body)})) + (fn [error] + (js/console.log "error" error)) + (fn [] + (reset! downloading? false)))))] [:div.template-item [:div.template-item-content diff --git a/frontend/src/app/main/ui/releases.cljs b/frontend/src/app/main/ui/releases.cljs index 5c0e088729..562c1eab2e 100644 --- a/frontend/src/app/main/ui/releases.cljs +++ b/frontend/src/app/main/ui/releases.cljs @@ -26,6 +26,7 @@ [app.main.ui.releases.v1-7] [app.main.ui.releases.v1-8] [app.main.ui.releases.v1-9] + [app.main.ui.releases.v2-0] [app.util.object :as obj] [app.util.timers :as tm] [rumext.v2 :as mf])) @@ -33,40 +34,41 @@ ;;; --- RELEASE NOTES MODAL (mf/defc release-notes - [{:keys [version] :as props}] - (let [slide (mf/use-state :start) - klass (mf/use-state "fadeInDown") + {::mf/props :obj} + [{:keys [version]}] + (let [slide* (mf/use-state :start) + slide (deref slide*) + + klass* (mf/use-state "fadeInDown") + klass (deref klass*) navigate - (mf/use-callback #(reset! slide %)) + (mf/use-fn #(reset! slide* %)) next - (mf/use-callback + (mf/use-fn (mf/deps slide) (fn [] - (if (= @slide :start) + (if (= slide :start) (navigate 0) - (navigate (inc @slide))))) + (navigate (inc slide))))) finish - (mf/use-callback + (mf/use-fn + (mf/deps version) #(st/emit! (modal/hide) (du/mark-onboarding-as-viewed {:version version})))] - (mf/use-effect - (mf/deps) - (fn [] - #(st/emit! (du/mark-onboarding-as-viewed {:version version})))) + (mf/with-effect [] + #(st/emit! (du/mark-onboarding-as-viewed {:version version}))) - (mf/use-layout-effect - (mf/deps @slide) - (fn [] - (when (not= :start @slide) - (reset! klass "fadeIn")) - (let [sem (tm/schedule 300 #(reset! klass nil))] - (fn [] - (reset! klass nil) - (tm/dispose! sem))))) + (mf/with-effect [slide] + (when (not= :start slide) + (reset! klass* "fadeIn")) + (let [sem (tm/schedule 300 #(reset! klass* nil))] + (fn [] + (reset! klass* nil) + (tm/dispose! sem)))) (rc/render-release-notes {:next next @@ -89,4 +91,4 @@ (defmethod rc/render-release-notes "0.0" [params] - (rc/render-release-notes (assoc params :version "1.18"))) + (rc/render-release-notes (assoc params :version "2.0"))) diff --git a/frontend/src/app/main/ui/releases.scss b/frontend/src/app/main/ui/releases.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/main/ui/releases/common.cljs b/frontend/src/app/main/ui/releases/common.cljs index a20f2e56d6..4e3ce7cc5e 100644 --- a/frontend/src/app/main/ui/releases/common.cljs +++ b/frontend/src/app/main/ui/releases/common.cljs @@ -5,15 +5,16 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.releases.common + (:require-macros [app.main.style :as stl]) (:require - [app.util.dom :as dom] [rumext.v2 :as mf])) (defmulti render-release-notes :version) (mf/defc navigation-bullets [{:keys [slide navigate total]}] - [:ul.step-dots + [:ul {:class (stl/css :step-dots)} (for [i (range total)] - [:li {:class (dom/classnames :current (= slide i)) + [:li {:class (stl/css-case :dot true + :current (= slide i)) :on-click #(navigate i)}])]) diff --git a/frontend/src/app/main/ui/releases/common.scss b/frontend/src/app/main/ui/releases/common.scss new file mode 100644 index 0000000000..d2f7a7f117 --- /dev/null +++ b/frontend/src/app/main/ui/releases/common.scss @@ -0,0 +1,32 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.step-dots { + display: grid; + grid-template-columns: none; + grid-auto-flow: column; + gap: $s-8; + height: fit-content; + width: fit-content; + margin: 0; + padding: 0; + align-self: center; + justify-self: flex-start; +} + +.dot { + height: $s-12; + width: $s-12; + border-radius: $br-circle; + background-color: var(--modal-navigator-foreground-color-rest); + cursor: pointer; +} + +.current { + background-color: var(--modal-navigator-foreground-color-active); +} diff --git a/frontend/src/app/main/ui/releases/v1_10.cljs b/frontend/src/app/main/ui/releases/v1_10.cljs index a837bd8061..6f7c0e887d 100644 --- a/frontend/src/app/main/ui/releases/v1_10.cljs +++ b/frontend/src/app/main/ui/releases/v1_10.cljs @@ -13,7 +13,7 @@ [{:keys [klass finish version]}] (mf/html [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/beta-on.jpg" :border "0" :alt "Penpot is now BETA"}]] diff --git a/frontend/src/app/main/ui/releases/v1_11.cljs b/frontend/src/app/main/ui/releases/v1_11.cljs index 017337bffc..395cd72ee7 100644 --- a/frontend/src/app/main/ui/releases/v1_11.cljs +++ b/frontend/src/app/main/ui/releases/v1_11.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.11" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Beta release 1.11"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.11-animations.gif" :border "0" :alt "Animations"}]] @@ -46,13 +46,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 3}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.11-bg-export.gif" :border "0" :alt "Ignore background on export"}]] @@ -65,13 +65,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 3}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.11-zoom-widget.gif" :border "0" :alt "New zoom widget"}]] @@ -84,6 +84,6 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 3}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_12.cljs b/frontend/src/app/main/ui/releases/v1_12.cljs index 77edc383d5..65d7e2a41d 100644 --- a/frontend/src/app/main/ui/releases/v1_12.cljs +++ b/frontend/src/app/main/ui/releases/v1_12.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.12" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Beta release 1.12"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.12-ui.gif" :border "0" :alt "Adjustable UI"}]] @@ -46,13 +46,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.12-guides.gif" :border "0" :alt "Guides"}]] @@ -65,13 +65,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.12-scrollbars.gif" :border "0" :alt "Scrollbars"}]] @@ -83,13 +83,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 3 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.12-nudge.gif" :border "0" :alt "Nudge amount"}]] @@ -102,6 +102,6 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_13.cljs b/frontend/src/app/main/ui/releases/v1_13.cljs index 48b8c79082..39ad2c79ac 100644 --- a/frontend/src/app/main/ui/releases/v1_13.cljs +++ b/frontend/src/app/main/ui/releases/v1_13.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.13" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Beta release 1.13"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.13-multi-export.gif" :border "0" :alt "Multiple exports"}]] @@ -46,13 +46,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.13-multiple-fills.gif" :border "0" :alt "Multiple fills and strokes"}]] @@ -65,13 +65,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.13-members.gif" :border "0" :alt "Members area redesign"}]] @@ -84,13 +84,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 3 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.13-focus.gif" :border "0" :alt "Focus mode"}]] @@ -103,6 +103,6 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_14.cljs b/frontend/src/app/main/ui/releases/v1_14.cljs index f63568d04b..334e993f62 100644 --- a/frontend/src/app/main/ui/releases/v1_14.cljs +++ b/frontend/src/app/main/ui/releases/v1_14.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.14" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Beta release 1.14"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.14-shortcuts.gif" :border "0" :alt "Shortcuts panel"}]] @@ -46,13 +46,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.14-color-group.gif" :border "0" :alt "Colors selection"}]] @@ -65,13 +65,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.14-fix-on-scroll.gif" :border "0" :alt "Fix elements at scroll"}]] @@ -84,13 +84,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 3 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.14-group-assets.gif" :border "0" :alt "Group library assets with drag & drop"}]] @@ -103,6 +103,6 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_15.cljs b/frontend/src/app/main/ui/releases/v1_15.cljs index 42f92457af..9cdb26b079 100644 --- a/frontend/src/app/main/ui/releases/v1_15.cljs +++ b/frontend/src/app/main/ui/releases/v1_15.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.15" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Beta release 1.15"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.15-nested-boards.gif" :border "0" :alt "Nested boards"}]] @@ -46,13 +46,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.15-share.gif" :border "0" :alt "Share prototype options"}]] @@ -65,13 +65,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.15-comments.gif" :border "0" :alt "Comments positioning"}]] @@ -84,13 +84,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 3 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.15-view-mode.gif" :border "0" :alt "View Mode improvements"}]] @@ -103,6 +103,6 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_16.cljs b/frontend/src/app/main/ui/releases/v1_16.cljs index 7502deaa12..537507abb6 100644 --- a/frontend/src/app/main/ui/releases/v1_16.cljs +++ b/frontend/src/app/main/ui/releases/v1_16.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.16" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Beta release 1.16"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.16-dashboard.gif" :border "0" :alt "Dashboard refreshed look & feel"}]] @@ -46,13 +46,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.16-slider.gif" :border "0" :alt "Libraries & templates module"}]] @@ -60,18 +60,18 @@ [:div.modal-title [:h2 "Libraries & templates module"]] [:div.modal-content - [:p "This new module will allow you to import a curated selection of the files that are available at the Libraries & Templates page directly from your projects dashboard."] + [:p "This new module will allow you to import a curated selection of the files that are available at the Libraries & Templates page directly from your projects dashboard."] [:p "You no longer need to to download most of them to the computer before importing."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.16-onboarding.gif" :border "0" :alt "Improved onboarding"}]] @@ -84,13 +84,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 3 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.16-click-zoom.gif" :border "0" :alt "Zoom to shape with double click"}]] @@ -103,6 +103,6 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_17.cljs b/frontend/src/app/main/ui/releases/v1_17.cljs index 1c9bd519af..1965748c6a 100644 --- a/frontend/src/app/main/ui/releases/v1_17.cljs +++ b/frontend/src/app/main/ui/releases/v1_17.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.17" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/onboarding-version.jpg" :border "0" :alt "What's new release 1.17"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.17-flex-layout.gif" :border "0" :alt "Flex-Layout"}]] @@ -46,13 +46,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.17-inspect.gif" :border "0" :alt "Inspect at the workspace"}]] @@ -60,18 +60,18 @@ [:div.modal-title [:h2 "Inspect at the workspace"]] [:div.modal-content - [:p "Now you can inspect designs to get measures, properties and production-ready code right at the workspace, so designers and developers can share the same space while working."] + [:p "Now you can inspect designs to get measures, properties and production-ready code right at the workspace, so designers and developers can share the same space while working."] [:p "Also, inspect mode provides a safer view-only mode and other improvements."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.17-webhook.gif" :border "0" :alt "Webhooks"}]] @@ -84,13 +84,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 3 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.17-ally.gif" :border "0" :alt "Accessibility improvements"}]] @@ -103,6 +103,6 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_18.cljs b/frontend/src/app/main/ui/releases/v1_18.cljs index e6fbcae68a..cb6d73458c 100644 --- a/frontend/src/app/main/ui/releases/v1_18.cljs +++ b/frontend/src/app/main/ui/releases/v1_18.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.18" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/onboarding-version.jpg" :border "0" :alt "What's new release 1.18"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.18-spacing.gif" :border "0" :alt "Spacing management"}]] @@ -46,13 +46,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.18-absolute.gif" :border "0" :alt "Position absolute feature"}]] @@ -60,18 +60,18 @@ [:div.modal-title [:h2 "Absolute position elements in Flex layout"]] [:div.modal-content - [:p "Sometimes you need to freely position an element in a specific place regardless of the size of the layout where it belongs."] + [:p "Sometimes you need to freely position an element in a specific place regardless of the size of the layout where it belongs."] [:p "Now you can exclude elements from the Flex layout flow using absolute position."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.18-z-index.gif" :border "0" :alt "Z-index feature"}]] @@ -84,13 +84,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 3 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.18-scale.gif" :border "0" :alt "Scale content proportionally"}]] @@ -103,6 +103,6 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_19.cljs b/frontend/src/app/main/ui/releases/v1_19.cljs index 0a8db0eec2..8543a0c45b 100644 --- a/frontend/src/app/main/ui/releases/v1_19.cljs +++ b/frontend/src/app/main/ui/releases/v1_19.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.19" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/onboarding-version.jpg" @@ -42,7 +42,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.19-contributions.png" @@ -73,13 +73,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 2}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/1.19-tokens.gif" @@ -100,7 +100,7 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 2}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_4.cljs b/frontend/src/app/main/ui/releases/v1_4.cljs index 50a1801772..bc80923258 100644 --- a/frontend/src/app/main/ui/releases/v1_4.cljs +++ b/frontend/src/app/main/ui/releases/v1_4.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.4" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Alpha release 1.4.0"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/select-files.gif" :border "0" :alt "New file selection"}]] @@ -46,13 +46,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/manage-files.gif" :border "0" :alt "Manage files"}]] @@ -65,13 +65,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/rtl.gif" :border "0" :alt "RTL support"}]] @@ -84,13 +84,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 3 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/blend-modes.gif" :border "0" :alt "Blend modes"}]] @@ -103,7 +103,7 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_5.cljs b/frontend/src/app/main/ui/releases/v1_5.cljs index e5382ab431..8d962515d7 100644 --- a/frontend/src/app/main/ui/releases/v1_5.cljs +++ b/frontend/src/app/main/ui/releases/v1_5.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.5" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Alpha release 1.5.0"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/path-tool.gif" :border "0" :alt "New path tool"}]] @@ -46,13 +46,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 3}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/assets-organiz.gif" :border "0" :alt "Manage libraries"}]] @@ -65,13 +65,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 3}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/smart-inputs.gif" :border "0" :alt "Smart inputs"}]] @@ -84,7 +84,7 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 3}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_6.cljs b/frontend/src/app/main/ui/releases/v1_6.cljs index 3eaf51443e..c6636c4550 100644 --- a/frontend/src/app/main/ui/releases/v1_6.cljs +++ b/frontend/src/app/main/ui/releases/v1_6.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.6" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Alpha release 1.6.0"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/custom-fonts.gif" :border "0" :alt "Upload/use custom fonts"}]] @@ -46,13 +46,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/scale-text.gif" :border "0" :alt "Interactively scale text"}]] @@ -65,13 +65,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/performance.gif" :border "0" :alt "Performance improvements"}]] @@ -84,13 +84,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 3 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/shapes-to-path.gif" :border "0" :alt "Shapes to path"}]] @@ -103,6 +103,6 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_7.cljs b/frontend/src/app/main/ui/releases/v1_7.cljs index a0f774dfd9..32666d5158 100644 --- a/frontend/src/app/main/ui/releases/v1_7.cljs +++ b/frontend/src/app/main/ui/releases/v1_7.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.7" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Alpha release 1.7"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/export.gif" :border "0" :alt "Export & Import"}]] @@ -49,13 +49,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/constraints.gif" :border "0" :alt "Resizing constraints"}]] @@ -71,13 +71,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/group-components.gif" :border "0" :alt "Library assets management improvements"}]] @@ -91,13 +91,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 3 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/copy-paste.gif" :border "0" :alt "Paste components from file to file"}]] @@ -109,6 +109,6 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_8.cljs b/frontend/src/app/main/ui/releases/v1_8.cljs index 3abfebb91a..dfff4bd9f2 100644 --- a/frontend/src/app/main/ui/releases/v1_8.cljs +++ b/frontend/src/app/main/ui/releases/v1_8.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.8" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Alpha release 1.8"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/share-viewer.gif" :border "0" :alt "Share options and pages at view mode"}]] @@ -46,13 +46,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/stroke-caps.gif" :border "0" :alt "Path stroke caps"}]] @@ -65,13 +65,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/navigate-history.gif" :border "0" :alt "Navigable history"}]] @@ -84,13 +84,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 3 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/export-artboards.gif" :border "0" :alt "Export artboards PDF"}]] @@ -103,6 +103,6 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_9.cljs b/frontend/src/app/main/ui/releases/v1_9.cljs index 2c617db956..e5dda41cc8 100644 --- a/frontend/src/app/main/ui/releases/v1_9.cljs +++ b/frontend/src/app/main/ui/releases/v1_9.cljs @@ -12,10 +12,10 @@ (defmethod c/render-release-notes "1.9" [{:keys [slide klass next finish navigate version]}] (mf/html - (case @slide + (case slide :start [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Alpha release 1.9"}]] @@ -33,7 +33,7 @@ 0 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/advanced-proto.gif" :border "0" :alt "Advanced interactions"}]] @@ -46,13 +46,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 1 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/flows-proto.gif" :border "0" :alt "Multiple flows"}]] @@ -65,13 +65,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 2 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/booleans.gif" :border "0" :alt "Boolean shapes"}]] @@ -84,13 +84,13 @@ [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]] 3 [:div.modal-overlay - [:div.animated {:class @klass} + [:div.animated {:class klass} [:div.modal-container.onboarding.feature [:div.modal-left [:img {:src "images/features/libraries-feature.gif" :border "0" :alt "Libraries & templates"}]] @@ -103,6 +103,6 @@ [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] [:& c/navigation-bullets - {:slide @slide + {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v2_0.cljs b/frontend/src/app/main/ui/releases/v2_0.cljs new file mode 100644 index 0000000000..0c3af30604 --- /dev/null +++ b/frontend/src/app/main/ui/releases/v2_0.cljs @@ -0,0 +1,203 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.releases.v2-0 + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data.macros :as dm] + [app.main.ui.releases.common :as c] + [rumext.v2 :as mf])) + +;; TODO: Review all copies and alt text +(defmethod c/render-release-notes "2.0" + [{:keys [slide klass next finish navigate version]}] + (mf/html + (case slide + :start + [:div {:class (stl/css :modal-overlay)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.0-intro-image.png" + :class (stl/css :start-image) + :border "0" + :alt "A graphic illustration with Penpot style"}] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "Welcome to Penpot 2.0! "] + + [:div {:class (stl/css :version-tag)} + (dm/str "Version " version)]] + + [:div {:class (stl/css :features-block)} + [:p {:class (stl/css :feature-content)} + [:spam {:class (stl/css :feature-title)} + "CSS Grid Layout: "] + "Bring your designs to life, knowing that what you create is what developers code."] + + [:p {:class (stl/css :feature-content)} + [:spam {:class (stl/css :feature-title)} + "Sleeker UI: "] + "We’ve polished Penpot to make your experience smoother and more enjoyable."] + + [:p {:class (stl/css :feature-content)} + [:spam {:class (stl/css :feature-title)} + "New Components System: "] + "Managing and using your design components got a whole lot better."] + + [:p {:class (stl/css :feature-content)} + "And that’s not all - we’ve fined tuned performance and " + "accessibility to give you a better and more fluid design experience."] + + [:p {:class (stl/css :feature-content)} + " Ready to dive in? Let 's get started!"]] + + [:div {:class (stl/css :navigation)} + [:button {:class (stl/css :next-btn) + :on-click next} "Continue"]]]]]] + + 0 + [:div {:class (stl/css :modal-overlay)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.0-css-grid.gif" + :class (stl/css :start-image) + :border "0" + :alt "Penpot's CSS Grid Layout"}] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "CSS Grid Layout - Design Meets Development"]] + + [:div {:class (stl/css :feature)} + [:p {:class (stl/css :feature-content)} + "The much-awaited Grid Layout introduces 2-dimensional" + " layout capabilities to Penpot, allowing for the creation" + " of adaptive layouts by leveraging the power of CSS properties."] + + [:p {:class (stl/css :feature-content)} + "It’s a host of new features, including columns and" + " rows management, flexible units such as FR (fractions)," + " the ability to create and name areas, and tons of new " + "and unique possibilities within a design tool."] + + [:p {:class (stl/css :feature-content)} + "Designers will learn CSS basics while working, " + "and as always with Penpot, developers can pick" + " up the design as code to take it from there."]] + + [:div {:class (stl/css :navigation)} + [:& c/navigation-bullets + {:slide slide + :navigate navigate + :total 4}] + + [:button {:on-click next + :class (stl/css :next-btn)} "Continue"]]]]]] + + 1 + [:div {:class (stl/css :modal-overlay)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.0-new-ui.gif" + :class (stl/css :start-image) + :border "0" + :alt "Penpot's UI Makeover"}] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "UI Makeover - Smoother, Sharper, and Simply More Fun"]] + + [:div {:class (stl/css :feature)} + [:p {:class (stl/css :feature-content)} + "We've completely overhauled Penpot's user interface. " + "The improvements in consistency, the introduction of " + "new microinteractions, and attention to countless details" + " will significantly enhance the productivity and enjoyment of using Penpot."] + [:p {:class (stl/css :feature-content)} + "Furthermore, we’ve made several accessibility improvements, " + "with better color contrast, keyboard navigation," + " and adherence to other best practices."]] + + [:div {:class (stl/css :navigation)} + [:& c/navigation-bullets + {:slide slide + :navigate navigate + :total 4}] + + [:button {:on-click next + :class (stl/css :next-btn)} "Continue"]]]]]] + + 2 + [:div {:class (stl/css :modal-overlay)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.0-components.gif" + :class (stl/css :start-image) + :border "0" + :alt "Penpot's new components system"}] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "New Components System"]] + [:div {:class (stl/css :feature)} + [:p {:class (stl/css :feature-content)} + "The new Penpot components system improves" + " control over instances, including their " + "inheritances and properties overrides. " + "Main components are now accessible as design" + " elements, allowing a better updating " + "workflow through instant changes synchronization."] + [:p {:class (stl/css :feature-content)} + "And that’s not all, there are new capabilities " + "such as component swapping and annotations " + "that will help you to better manage your design systems."]] + + [:div {:class (stl/css :navigation)} + [:& c/navigation-bullets + {:slide slide + :navigate navigate + :total 4}] + + [:button {:on-click next + :class (stl/css :next-btn)} "Continue"]]]]]] + + 3 + [:div {:class (stl/css :modal-overlay)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.0-html.gif" + :class (stl/css :start-image) + :border "0" + :alt " Penpot's HTML code generator"}] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "And much more"]] + [:div {:class (stl/css :feature)} + [:p {:class (stl/css :feature-content)} + "In addition to all of this, we’ve included several other requested improvements:"] + [:ul {:class (stl/css :feature-list)} + [:li "Access HTML markup code directly in inspect mode"] + [:li "Images are now treated as element fills, maintaining their aspect ratio on resize, ideal for flexible designs"] + [:li "Enjoy new color themes with options for both dark and light modes"] + [:li "Feel the speed boost! Enjoy a smoother experience with a bunch of performance improvements"]]] + + [:div {:class (stl/css :navigation)} + + [:& c/navigation-bullets + {:slide slide + :navigate navigate + :total 4}] + + [:button {:on-click finish + :class (stl/css :next-btn)} "Let's go"]]]]]]))) + diff --git a/frontend/src/app/main/ui/releases/v2_0.scss b/frontend/src/app/main/ui/releases/v2_0.scss new file mode 100644 index 0000000000..0108877b0e --- /dev/null +++ b/frontend/src/app/main/ui/releases/v2_0.scss @@ -0,0 +1,98 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + display: grid; + grid-template-columns: $s-324 1fr; + height: $s-500; + width: $s-888; + border-radius: $br-8; + background-color: var(--modal-background-color); + border: $s-2 solid var(--modal-border-color); +} + +.start-image { + width: $s-324; + border-radius: $br-8 0 0 $br-8; +} + +.modal-content { + padding: $s-40; + display: grid; + grid-template-rows: auto 1fr $s-32; + gap: $s-24; +} + +.modal-header { + display: grid; + gap: $s-8; +} + +.version-tag { + @include flexCenter; + @include headlineSmallTypography; + height: $s-32; + width: $s-96; + background-color: var(--communication-tag-background-color); + color: var(--communication-tag-foreground-color); + border-radius: $br-8; +} + +.modal-title { + @include headlineLargeTypography; + color: var(--modal-title-foreground-color); +} + +.features-block { + display: flex; + flex-direction: column; + gap: $s-16; + width: $s-440; +} + +.feature { + display: flex; + flex-direction: column; + gap: $s-8; +} + +.feature-title { + @include bodyLargeTypography; + color: var(--modal-title-foreground-color); +} + +.feature-content { + @include bodyMediumTypography; + margin: 0; + color: var(--modal-text-foreground-color); +} + +.feature-list { + @include bodyMediumTypography; + color: var(--modal-text-foreground-color); + list-style: disc; + display: grid; + gap: $s-8; +} + +.navigation { + width: 100%; + display: grid; + grid-template-areas: "bullets button"; +} + +.next-btn { + @extend .button-primary; + width: $s-100; + justify-self: flex-end; + grid-area: button; +} diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index 4944fcc85a..eeb5f156c3 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -14,9 +14,9 @@ [app.main.repo :as rp] [app.main.store :as st] [app.util.router :as rt] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.spec.alpha :as s] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (s/def ::page-id ::us/uuid) (s/def ::file-id ::us/uuid) @@ -50,6 +50,7 @@ ["/options" :settings-options] ["/access-tokens" :settings-access-tokens]] + ["/frame-preview" :frame-preview] ["/view/:file-id" {:name :viewer :conform @@ -111,16 +112,16 @@ ;; some race conditions that causes unexpected redirects on ;; invitations workflows (and probably other cases). (->> (rp/cmd! :get-profile) - (rx/subs (fn [{:keys [id] :as profile}] - (cond - (= id uuid/zero) - (st/emit! (rt/nav :auth-login)) + (rx/subs! (fn [{:keys [id] :as profile}] + (cond + (= id uuid/zero) + (st/emit! (rt/nav :auth-login)) - empty-path? - (st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)})) + empty-path? + (st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)})) - :else - (st/emit! (rt/assign-exception {:type :not-found}))))))))) + :else + (st/emit! (rt/assign-exception {:type :not-found}))))))))) (defn init-routes [] diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs index 2c890d51ea..8b024332d5 100644 --- a/frontend/src/app/main/ui/settings.cljs +++ b/frontend/src/app/main/ui/settings.cljs @@ -5,9 +5,12 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.settings + (:require-macros [app.main.style :as stl]) (:require + [app.main.data.dashboard.shortcuts :as sc] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.hooks :as hooks] [app.main.ui.settings.access-tokens :refer [access-tokens-page]] [app.main.ui.settings.change-email] [app.main.ui.settings.delete-account] @@ -23,8 +26,8 @@ (mf/defc header {::mf/wrap [mf/memo]} [] - [:header.dashboard-header - [:div.dashboard-title + [:header {:class (stl/css :dashboard-header)} + [:div {:class (stl/css :dashboard-title)} [:h1 {:data-test "account-title"} (tr "dashboard.your-account-title")]]]) (mf/defc settings @@ -33,18 +36,20 @@ profile (mf/deref refs/profile) locale (mf/deref i18n/locale)] + (hooks/use-shortcuts ::dashboard sc/shortcuts) + (mf/use-effect #(when (nil? profile) (st/emit! (rt/nav :auth-login)))) - [:section.dashboard-layout + [:section {:class (stl/css :dashboard-layout-refactor :dashboard)} [:& sidebar {:profile profile :locale locale :section section}] - [:div.dashboard-content + [:div {:class (stl/css :dashboard-content)} [:& header] - [:section.dashboard-container + [:section {:class (stl/css :dashboard-container)} (case section :settings-profile [:& profile-page {:locale locale}] @@ -60,4 +65,3 @@ :settings-access-tokens [:& access-tokens-page])]]])) - diff --git a/frontend/src/app/main/ui/settings.scss b/frontend/src/app/main/ui/settings.scss new file mode 100644 index 0000000000..6bae8499fa --- /dev/null +++ b/frontend/src/app/main/ui/settings.scss @@ -0,0 +1,273 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; +@use "common/refactor/common-dashboard"; + +.dashboard { + background-color: var(--app-background); + display: grid; + grid-template-rows: $s-48 1fr; + grid-template-columns: $s-40 $s-256 1fr; + height: 100vh; +} + +.dashboard-content { + display: flex; + flex-direction: column; + position: relative; + grid-row: 1 / span 2; + padding: $s-16 $s-16 0 0; +} + +.dashboard-container { + flex: 1 0 0; + margin-right: $s-16; + overflow-y: auto; + width: 100%; + border-top: $s-1 solid $db-quaternary; + + &.dashboard-projects { + user-select: none; + } + &.dashboard-shared { + width: calc(100vw - $s-320); + margin-right: $s-52; + } + + &.search { + margin-top: $s-12; + } +} + +.dashboard-settings { + display: flex; + width: 100%; + justify-content: center; + align-items: center; + a { + color: $df-secondary; + } +} + +.form-container { + width: $s-800; + margin: $s-48 auto $s-32 $s-120; + display: flex; + max-width: $s-368; + width: 100%; + + &.two-columns { + max-width: $s-520; + justify-content: space-between; + flex-direction: row; + } + + h2 { + margin-bottom: $s-16; + } + + form { + width: $s-468; + + .custom-input, + .custom-select { + flex-direction: column-reverse; + label { + position: relative; + text-transform: uppercase; + color: $df-primary; + font-size: $fs-11; + margin-bottom: $s-12; + margin-left: calc(-1 * $s-4); + } + input, + select { + background-color: $db-tertiary; + border-radius: $s-8; + border-color: transparent; + color: $df-primary; + padding: 0 $s-16; + &:focus { + outline: $s-1 solid $da-primary; + } + ::placeholder { + color: $df-secondary; + } + } + .help-icon { + bottom: $s-12; + top: auto; + svg { + fill: $df-secondary; + } + } + &.disabled { + input { + background-color: var(--input-background-color-disabled); + border-color: $db-quaternary; + color: $df-secondary; + } + } + .input-container { + background-color: $db-tertiary; + border-radius: $s-8; + border-color: transparent; + margin-top: $s-24; + .main-content { + label { + position: absolute; + top: calc(-1 * $s-24); + } + span { + color: $df-primary; + } + } + &:focus { + border: $s-1 solid $da-primary; + } + } + textarea { + border-radius: $s-8; + padding: $s-12 $s-16; + background-color: $db-tertiary; + color: $df-primary; + border: none; + &:focus { + outline: $s-1 solid $da-primary; + } + } + } + + .field-title { + color: $df-primary; + } + .field-title:not(:first-child) { + margin-top: $s-64; + } + + .field-text { + color: $df-secondary; + } + button, + .btn-secondary { + width: 100%; + font-size: $fs-11; + text-transform: uppercase; + background-color: $db-tertiary; + color: $df-primary; + &:hover { + color: $da-primary; + background-color: $db-quaternary; + } + } + hr { + display: none; + } + } + .links { + margin-top: $s-12; + } +} + +.profile-form { + display: flex; + flex-direction: column; + max-width: $s-368; + width: 100%; + + .newsletter-subs { + border-bottom: $s-1 solid $df-secondary; + border-top: $s-1 solid $df-secondary; + padding: $s-32 0; + margin-bottom: $s-32; + + .newsletter-title { + font-family: "worksans", sans-serif; + color: $df-secondary; + font-size: $fs-14; + } + + label { + font-family: "worksans", sans-serif; + color: $db-primary; + font-size: $fs-12; + margin-right: calc(-1 * $s-16); + margin-bottom: $s-12; + } + + .info { + font-family: "worksans", sans-serif; + color: $df-secondary; + font-size: $fs-12; + margin-bottom: $s-8; + } + + .input-checkbox label { + align-items: flex-start; + } + } +} + +.avatar-form { + display: flex; + flex-direction: column; + width: $s-120; + min-width: $s-120; + + img { + border-radius: 50%; + flex-shrink: 0; + height: $s-120; + margin-right: $s-16; + width: $s-120; + } + + .image-change-field { + position: relative; + width: $s-120; + height: $s-120; + + .update-overlay { + opacity: 0; + cursor: pointer; + position: absolute; + width: $s-120; + height: $s-120; + border-radius: 50%; + font-size: $fs-24; + color: $df-primary; + line-height: 5; + text-align: center; + background: $da-tertiary; + z-index: 14; + } + + input[type="file"] { + width: $s-120; + height: $s-120; + position: absolute; + opacity: 0; + cursor: pointer; + top: 0; + z-index: 15; + } + + &:hover { + .update-overlay { + opacity: 0.8; + } + } + } +} + +.options-form, +.password-form { + h2 { + font-size: $fs-14; + margin-bottom: $s-20; + } +} diff --git a/frontend/src/app/main/ui/settings/access_tokens.cljs b/frontend/src/app/main/ui/settings/access_tokens.cljs index 2444324edf..663c9a09d0 100644 --- a/frontend/src/app/main/ui/settings/access_tokens.cljs +++ b/frontend/src/app/main/ui/settings/access_tokens.cljs @@ -5,9 +5,10 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.settings.access-tokens + (:require-macros [app.main.style :as stl]) (:require [app.common.spec :as us] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.data.users :as du] [app.main.store :as st] @@ -24,6 +25,15 @@ [okulary.core :as l] [rumext.v2 :as mf])) +(def ^:private clipboard-icon + (i/icon-xref :clipboard (stl/css :clipboard-icon))) + +(def ^:private close-icon + (i/icon-xref :close (stl/css :close-icon))) + +(def ^:private menu-icon + (i/icon-xref :menu (stl/css :menu-icon))) + (def tokens-ref (l/derived :access-tokens st/state)) @@ -61,38 +71,38 @@ on-success (mf/use-fn - (mf/deps created) - (fn [_] - (let [message (tr "dashboard.access-tokens.create.success")] - (st/emit! (du/fetch-access-tokens) - (dm/success message) - (reset! created? true))))) + (mf/deps created) + (fn [_] + (let [message (tr "dashboard.access-tokens.create.success")] + (st/emit! (du/fetch-access-tokens) + (msg/success message) + (reset! created? true))))) on-close (mf/use-fn - (mf/deps created) - (fn [_] - (reset! created? false) - (st/emit! (modal/hide)))) + (mf/deps created) + (fn [_] + (reset! created? false) + (st/emit! (modal/hide)))) on-error (mf/use-fn - (fn [_] - (st/emit! (dm/error (tr "errors.generic")) - (modal/hide)))) + (fn [_] + (st/emit! (msg/error (tr "errors.generic")) + (modal/hide)))) on-submit (mf/use-fn - (fn [form] - (let [cdata (:clean-data @form) - mdata {:on-success (partial on-success form) - :on-error (partial on-error form)} - expiration (:expiration-date cdata) - params (cond-> {:name (:name cdata) - :perms (:perms cdata)} - (not= "never" expiration) (assoc :expiration expiration))] - (st/emit! (du/create-access-token - (with-meta params mdata)))))) + (fn [form] + (let [cdata (:clean-data @form) + mdata {:on-success (partial on-success form) + :on-error (partial on-error form)} + expiration (:expiration-date cdata) + params (cond-> {:name (:name cdata) + :perms (:perms cdata)} + (not= "never" expiration) (assoc :expiration expiration))] + (st/emit! (du/create-access-token + (with-meta params mdata)))))) copy-token (mf/use-fn @@ -100,87 +110,96 @@ (fn [event] (dom/prevent-default event) (wapi/write-to-clipboard (:token created)) - (st/emit! (dm/show {:type :info - :content (tr "dashboard.access-tokens.copied-success") - :timeout 1000}))))] + (st/emit! (msg/show {:type :info + :notification-type :toast + :content (tr "dashboard.access-tokens.copied-success") + :timeout 7000}))))] - [:div.modal-overlay - [:div.modal-container.access-tokens-modal + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} [:& fm/form {:form form :on-submit on-submit} - [:div.modal-header - [:div.modal-header-title - [:h2 (tr "modals.create-access-token.title")]] + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} (tr "modals.create-access-token.title")] - [:div.modal-close-button - {:on-click on-close} i/close]] + [:button {:class (stl/css :modal-close-btn) + :on-click on-close} + close-icon]] - [:div.modal-content.generic-form - [:div.fields-container - [:div.fields-row - [:& fm/input {:type "text" - :auto-focus? true - :form form - :name :name + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :fields-row)} + [:& fm/input {:type "text" + :auto-focus? true + :form form + :name :name + :disabled @created? + :label (tr "modals.create-access-token.name.label") + :show-success? true + :placeholder (tr "modals.create-access-token.name.placeholder")}]] + + [:div {:class (stl/css :fields-row)} + [:div {:class (stl/css :select-title)} + (tr "modals.create-access-token.expiration-date.label")] + [:& fm/select {:options [{:label (tr "dashboard.access-tokens.expiration-never") :value "never" :key "never"} + {:label (tr "dashboard.access-tokens.expiration-30-days") :value "720h" :key "720h"} + {:label (tr "dashboard.access-tokens.expiration-60-days") :value "1440h" :key "1440h"} + {:label (tr "dashboard.access-tokens.expiration-90-days") :value "2160h" :key "2160h"} + {:label (tr "dashboard.access-tokens.expiration-180-days") :value "4320h" :key "4320h"}] + :default "never" :disabled @created? - :label (tr "modals.create-access-token.name.label") - :placeholder (tr "modals.create-access-token.name.placeholder")}]] + :name :expiration-date}] + (when @created? + [:span {:class (stl/css :token-created-info)} + (if (:expires-at created) + (tr "dashboard.access-tokens.token-will-expire" (dt/format-date-locale (:expires-at created) {:locale locale})) + (tr "dashboard.access-tokens.token-will-not-expire"))])] - [:div.fields-row - [:& fm/select {:options [{:label (tr "dashboard.access-tokens.expiration-never") :value "never" :key "never"} - {:label (tr "dashboard.access-tokens.expiration-30-days") :value "720h" :key "720h"} - {:label (tr "dashboard.access-tokens.expiration-60-days") :value "1440h" :key "1440h"} - {:label (tr "dashboard.access-tokens.expiration-90-days") :value "2160h" :key "2160h"} - {:label (tr "dashboard.access-tokens.expiration-180-days") :value "4320h" :key "4320h"}] - :label (tr "modals.create-access-token.expiration-date.label") - :default "never" - :disabled @created? - :name :expiration-date}] - (when @created? - [:span.token-created-info - (if (:expires-at created) - (tr "dashboard.access-tokens.token-will-expire" (dt/format-date-locale (:expires-at created) {:locale locale})) - (tr "dashboard.access-tokens.token-will-not-expire"))])] + [:div {:class (stl/css :fields-row)} + (when @created? + [:div {:class (stl/css :custon-input-wrapper)} + [:input {:type "text" + :value (:token created "") + :class (stl/css :custom-input-token) + :placeholder (tr "modals.create-access-token.token") + :read-only true}] + [:button {:title (tr "modals.create-access-token.copy-token") + :class (stl/css :copy-btn) + :on-click copy-token} + clipboard-icon]]) + #_(when @created? + [:button {:class (stl/css :copy-btn) + :title (tr "modals.create-access-token.copy-token") + :on-click copy-token} + [:span {:class (stl/css :token-value)} (:token created "")] + [:span {:class (stl/css :icon)} + i/clipboard]])]] - [:div.fields-row.access-token-created - (when @created? - [:div.custom-input.with-icon - [:input {:type "text" - :value (:token created "") - :placeholder (tr "modals.create-access-token.token") - :read-only true}] - [:button.help-icon {:title (tr "modals.create-access-token.copy-token") - :on-click copy-token} + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} - i/copy]])]]] - - [:div.modal-footer - [:div.action-buttons (if @created? - [:input.cancel-button - {:type "button" - :value (tr "labels.close") - :on-click #(modal/hide!)}] + [:input {:class (stl/css :cancel-button) + :type "button" + :value (tr "labels.close") + :on-click #(modal/hide!)}] [:* - [:input.cancel-button - {:type "button" - :value (tr "labels.cancel") - :on-click #(modal/hide!)}] - [:& fm/submit-button - {:label (tr "modals.create-access-token.submit-label")}]])]]]]])) + [:input {:class (stl/css :cancel-button) + :type "button" + :value (tr "labels.cancel") + :on-click #(modal/hide!)}] + [:> fm/submit-button* + {:large? false :label (tr "modals.create-access-token.submit-label")}]])]]]]])) (mf/defc access-tokens-hero [] (let [on-click (mf/use-fn #(st/emit! (modal/show :access-token {})))] - [:div.access-tokens-hero-container - [:div.access-tokens-hero - [:div.desc - [:h2 (tr "dashboard.access-tokens.personal")] - [:p (tr "dashboard.access-tokens.personal.description")]] + [:div {:class (stl/css :access-tokens-hero)} + [:h2 {:class (stl/css :hero-title)} (tr "dashboard.access-tokens.personal")] + [:p {:class (stl/css :hero-desc)} (tr "dashboard.access-tokens.personal.description")] - [:button.btn-primary - {:on-click on-click} - [:span (tr "dashboard.access-tokens.create")]]]])) + [:button {:class (stl/css :hero-btn) + :on-click on-click} + (tr "dashboard.access-tokens.create")]])) (mf/defc access-token-actions [{:keys [on-delete]}] @@ -200,17 +219,22 @@ (mf/use-fn (fn [event] (dom/prevent-default event) - (swap! local assoc :menu-open true)))] + (swap! local assoc :menu-open true))) - [:div.icon - {:tab-index "0" - :ref menu-ref - :on-click on-menu-click - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (on-menu-click event)))} - i/actions + on-keydown + (mf/use-callback + (mf/deps on-menu-click) + (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (on-menu-click event))))] + + [:button {:class (stl/css :menu-btn) + :tab-index "0" + :ref menu-ref + :on-click on-menu-click + :on-key-down on-keydown} + menu-icon [:& context-menu-a11y {:on-close on-menu-close :show show? @@ -247,36 +271,35 @@ :accept-label (tr "modals.delete-acces-token.accept") :on-accept delete-fn}))))] - [:div.table-row - [:div.table-field.name + [:div {:class (stl/css :table-row)} + [:div {:class (stl/css :table-field :field-name)} (str (:name token))] - [:div.table-field.expiration-date - [:span.content {:class (when expired? "expired")} - (cond - (nil? expires-at) (tr "dashboard.access-tokens.no-expiration") - expired? (tr "dashboard.access-tokens.expired-on" expires-txt) - :else (tr "dashboard.access-tokens.expires-on" expires-txt))]] - [:div.table-field.actions + + [:div {:class (stl/css-case :expiration-date true + :expired expired?)} + (cond + (nil? expires-at) (tr "dashboard.access-tokens.no-expiration") + expired? (tr "dashboard.access-tokens.expired-on" expires-txt) + :else (tr "dashboard.access-tokens.expires-on" expires-txt))] + [:div {:class (stl/css :table-field :actions)} [:& access-token-actions {:on-delete on-delete}]]])) (mf/defc access-tokens-page [] - (mf/with-effect [] - (dom/set-html-title (tr "title.settings.access-tokens")) - (st/emit! (du/fetch-access-tokens))) - (let [tokens (mf/deref tokens-ref)] - [:div.dashboard-access-tokens - [:div - [:& access-tokens-hero] - (if (empty? tokens) - [:div.access-tokens-empty - [:div (tr "dashboard.access-tokens.empty.no-access-tokens")] - [:div (tr "dashboard.access-tokens.empty.add-one")]] - [:div.dashboard-table - [:div.table-rows - (for [token tokens] - [:& access-token-item {:token token :key (:id token)}])]])]])) + (mf/with-effect [] + (dom/set-html-title (tr "title.settings.access-tokens")) + (st/emit! (du/fetch-access-tokens))) + [:div {:class (stl/css :dashboard-access-tokens)} + [:& access-tokens-hero] + (if (empty? tokens) + [:div {:class (stl/css :access-tokens-empty)} + [:div (tr "dashboard.access-tokens.empty.no-access-tokens")] + [:div (tr "dashboard.access-tokens.empty.add-one")]] + [:div {:class (stl/css :dashboard-table)} + [:div {:class (stl/css :table-rows)} + (for [token tokens] + [:& access-token-item {:token token :key (:id token)}])]])])) diff --git a/frontend/src/app/main/ui/settings/access_tokens.scss b/frontend/src/app/main/ui/settings/access_tokens.scss new file mode 100644 index 0000000000..50239c61b0 --- /dev/null +++ b/frontend/src/app/main/ui/settings/access_tokens.scss @@ -0,0 +1,201 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +// ACCESS TOKENS PAGE +.dashboard-access-tokens { + display: grid; + grid-template-rows: auto 1fr; + margin: $s-80 auto $s-120 auto; + gap: $s-32; + width: $s-800; +} + +// hero +.access-tokens-hero { + display: grid; + grid-template-rows: auto auto 1fr; + gap: $s-32; + width: $s-500; + font-size: $fs-14; + margin: $s-16 auto 0 auto; +} + +.hero-title { + @include bigTitleTipography; + color: var(--title-foreground-color-hover); +} + +.hero-desc { + color: var(--title-foreground-color); + margin-bottom: 0; + font-size: $fs-14; +} + +.hero-btn { + @extend .button-primary; +} + +// table empty +.access-tokens-empty { + display: grid; + place-items: center; + align-content: center; + height: $s-156; + max-width: $s-1000; + width: 100%; + padding: $s-32; + border: $s-1 solid var(--panel-border-color); + border-radius: $br-8; + color: var(--dashboard-list-text-foreground-color); +} + +// Access tokens table +.dashboard-table { + height: fit-content; +} + +.table-rows { + display: grid; + grid-auto-rows: $s-64; + gap: $s-16; + width: 100%; + height: 100%; + max-width: $s-1000; + margin-top: $s-16; + color: var(--title-foreground-color); +} + +.table-row { + display: grid; + grid-template-columns: 43% 1fr auto; + align-items: center; + height: $s-64; + width: 100%; + padding: 0 $s-16; + border-radius: $br-8; + background-color: var(--dashboard-list-background-color); + color: var(--dashboard-list-foreground-color); +} + +.field-name { + @include textEllipsis; + display: grid; + width: 43%; + min-width: $s-300; +} + +.expiration-date { + @include flexCenter; + min-width: $s-76; + width: fit-content; + height: $s-24; + border-radius: $br-8; + color: var(--dashboard-list-text-foreground-color); +} + +.expired { + @include headlineSmallTypography; + padding: 0 $s-6; + color: var(--pill-foreground-color); + background-color: var(--status-widget-background-color-warning); +} + +.actions { + position: relative; +} +.menu-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} + +.menu-btn { + @include buttonStyle; +} + +// Create access token modal +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + @extend .modal-container-base; + min-width: $s-408; +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include uppercaseTitleTipography; + color: var(--modal-title-foreground-color); +} +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content { + @include flexColumn; + gap: $s-24; + @include bodySmallTypography; + margin-bottom: $s-24; +} + +.select-title { + @include bodySmallTypography; + color: var(--modal-title-foreground-color); +} + +.custon-input-wrapper { + @include flexRow; + border-radius: $br-8; + height: $s-32; + background-color: var(--input-background-color); +} + +.custom-input-token { + @extend .input-element; + margin: 0; + flex-grow: 1; + &:focus { + outline: none; + border: $s-1 solid var(--input-border-color-active); + } +} + +.token-value { + @include textEllipsis; + @include bodySmallTypography; + flex-grow: 1; +} + +.copy-btn { + @include flexCenter; + @extend .button-secondary; + height: $s-28; + width: $s-28; +} + +.clipboard-icon { + @extend .button-icon-small; +} + +.token-created-info { + color: var(--modal-text-foreground-color); +} + +.action-buttons { + @extend .modal-action-btns; + button { + @extend .modal-accept-btn; + } +} + +.cancel-button { + @extend .modal-cancel-btn; +} diff --git a/frontend/src/app/main/ui/settings/change_email.cljs b/frontend/src/app/main/ui/settings/change_email.cljs index 26521ed784..daeaf8bf87 100644 --- a/frontend/src/app/main/ui/settings/change_email.cljs +++ b/frontend/src/app/main/ui/settings/change_email.cljs @@ -5,20 +5,21 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.settings.change-email + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dma] [app.common.spec :as us] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.data.users :as du] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.main.ui.icons :as i] - [app.main.ui.messages :as msgs] + [app.main.ui.notifications.context-notification :refer [context-notification]] [app.util.i18n :as i18n :refer [tr]] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.spec.alpha :as s] [rumext.v2 :as mf])) @@ -38,19 +39,19 @@ (s/keys :req-un [::email-1 ::email-2])) (defn- on-error - [form {:keys [code] :as error}] - (case code + [form error] + (case (:code (ex-data error)) :email-already-exists (swap! form (fn [data] (let [error {:message (tr "errors.email-already-exists")}] (assoc-in data [:errors :email-1] error)))) :profile-is-muted - (rx/of (dm/error (tr "errors.profile-is-muted"))) + (rx/of (msg/error (tr "errors.profile-is-muted"))) :email-has-permanent-bounces (let [email (get @form [:data :email-1])] - (rx/of (dm/error (tr "errors.email-has-permanent-bounces" email)))) + (rx/of (msg/error (tr "errors.email-has-permanent-bounces" email)))) (rx/throw error))) @@ -60,7 +61,7 @@ (st/emit! (du/fetch-profile) (modal/hide)) (let [message (tr "notifications.validation-email-sent" (:email profile))] - (st/emit! (dm/info message) + (st/emit! (msg/info message) (modal/hide))))) (defn- on-submit @@ -85,51 +86,53 @@ (mf/use-callback (mf/deps profile) (partial on-submit profile)) - + on-email-change (mf/use-callback - (fn [_ _] - (let [different-emails-error? (= (dma/get-in @form [:errors :email-2 :code]) :different-emails) - email-1 (dma/get-in @form [:clean-data :email-1]) - email-2 (dma/get-in @form [:clean-data :email-2])] - (println "different-emails-error?" (and different-emails-error? (= email-1 email-2))) - (when (and different-emails-error? (= email-1 email-2)) - (swap! form d/dissoc-in [:errors :email-2])))))] + (fn [_ _] + (let [different-emails-error? (= (dma/get-in @form [:errors :email-2 :code]) :different-emails) + email-1 (dma/get-in @form [:clean-data :email-1]) + email-2 (dma/get-in @form [:clean-data :email-2])] + (when (and different-emails-error? (= email-1 email-2)) + (swap! form d/dissoc-in [:errors :email-2])))))] - [:div.modal-overlay - [:div.modal-container.change-email-modal.form-container + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} [:& fm/form {:form form :on-submit on-submit} - [:div.modal-header - [:div.modal-header-title - [:h2 {:data-test "change-email-title"} - (tr "modals.change-email.title")]] - [:div.modal-close-button - {:on-click on-close} i/close]] + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title) + :data-test "change-email-title"} + (tr "modals.change-email.title")] + [:button {:class (stl/css :modal-close-btn) + :on-click on-close} i/close]] - [:div.modal-content - [:& msgs/inline-banner + [:div {:class (stl/css :modal-content)} + [:& context-notification {:type :info :content (tr "modals.change-email.info" (:email profile))}] - [:div.fields-container - [:div.fields-row - [:& fm/input {:type "email" - :name :email-1 - :label (tr "modals.change-email.new-email") - :trim true - :on-change-value on-email-change}]] - [:div.fields-row - [:& fm/input {:type "email" - :name :email-2 - :label (tr "modals.change-email.confirm-email") - :trim true - :on-change-value on-email-change}]]]] + [:div {:class (stl/css :fields-row)} + [:& fm/input {:type "email" + :name :email-1 + :label (tr "modals.change-email.new-email") + :trim true + :show-success? true + :on-change-value on-email-change}]] - [:div.modal-footer - [:div.action-buttons {:data-test "change-email-submit"} - [:& fm/submit-button + [:div {:class (stl/css :fields-row)} + [:& fm/input {:type "email" + :name :email-2 + :label (tr "modals.change-email.confirm-email") + :trim true + :show-success? true + :on-change-value on-email-change}]]] + + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons) + :data-test "change-email-submit"} + [:> fm/submit-button* {:label (tr "modals.change-email.submit")}]]]]]])) diff --git a/frontend/src/app/main/ui/settings/change_email.scss b/frontend/src/app/main/ui/settings/change_email.scss new file mode 100644 index 0000000000..fef165d493 --- /dev/null +++ b/frontend/src/app/main/ui/settings/change_email.scss @@ -0,0 +1,56 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + @extend .modal-container-base; + min-width: $s-408; +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include uppercaseTitleTipography; + color: var(--modal-title-foreground-color); +} + +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content { + @include flexColumn; + @include bodySmallTypography; + gap: $s-24; + margin-bottom: $s-24; +} + +.fields-row { + @include flexColumn; +} + +.select-title { + @include bodySmallTypography; + color: var(--modal-title-foreground-color); +} + +.action-buttons { + @extend .modal-action-btns; + button { + @extend .modal-accept-btn; + } +} + +.cancel-button { + @extend .modal-cancel-btn; +} diff --git a/frontend/src/app/main/ui/settings/delete_account.cljs b/frontend/src/app/main/ui/settings/delete_account.cljs index 766f3d01d4..384907dfe6 100644 --- a/frontend/src/app/main/ui/settings/delete_account.cljs +++ b/frontend/src/app/main/ui/settings/delete_account.cljs @@ -5,22 +5,23 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.settings.delete-account + (:require-macros [app.main.style :as stl]) (:require - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.data.users :as du] [app.main.store :as st] [app.main.ui.icons :as i] - [app.main.ui.messages :as msgs] + [app.main.ui.notifications.context-notification :refer [context-notification]] [app.util.i18n :as i18n :refer [tr]] - [beicon.core :as rx] + [beicon.v2.core :as rx] [rumext.v2 :as mf])) (defn on-error [{:keys [code] :as error}] (if (= :owner-teams-with-people code) (let [msg (tr "notifications.profile-deletion-not-allowed")] - (rx/of (dm/error msg))) + (rx/of (msg/error msg))) (rx/throw error))) (mf/defc delete-account-modal @@ -34,26 +35,30 @@ (mf/use-callback #(st/emit! (modal/hide) (du/request-account-deletion - (with-meta {} {:on-error on-error}))))] + (with-meta {} {:on-error on-error}))))] - [:div.modal-overlay - [:div.modal-container.change-email-modal - [:div.modal-header - [:div.modal-header-title - [:h2 (tr "modals.delete-account.title")]] - [:div.modal-close-button - {:on-click on-close} i/close]] + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} - [:div.modal-content - [:& msgs/inline-banner + [:div {:class (stl/css :modal-header)} + + [:h2 {:class (stl/css :modal-title)} (tr "modals.delete-account.title")] + [:button {:class (stl/css :modal-close-btn) + :on-click on-close} i/close]] + + [:div {:class (stl/css :modal-content)} + [:& context-notification {:type :warning :content (tr "modals.delete-account.info")}]] - [:div.modal-footer - [:div.action-buttons - [:button.btn-warning.btn-large {:on-click on-accept - :data-test "delete-account-btn"} - (tr "modals.delete-account.confirm")] - [:button.btn-secondary.btn-large {:on-click on-close} - (tr "modals.delete-account.cancel")]]]]])) + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:button {:class (stl/css :cancel-button) + :on-click on-close} + (tr "modals.delete-account.cancel")] + [:button {:class (stl/css-case :accept-button true + :danger true) + :on-click on-accept + :data-test "delete-account-btn"} + (tr "modals.delete-account.confirm")]]]]])) diff --git a/frontend/src/app/main/ui/settings/delete_account.scss b/frontend/src/app/main/ui/settings/delete_account.scss new file mode 100644 index 0000000000..fd8e01b55e --- /dev/null +++ b/frontend/src/app/main/ui/settings/delete_account.scss @@ -0,0 +1,60 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + @extend .modal-container-base; + min-width: $s-408; +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include uppercaseTitleTipography; + color: var(--modal-title-foreground-color); +} + +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content { + @include flexColumn; + @include bodySmallTypography; + gap: $s-24; + margin-bottom: $s-24; +} + +.fields-row { + @include flexColumn; +} + +.select-title { + @include bodySmallTypography; + color: var(--modal-title-foreground-color); +} + +.action-buttons { + @extend .modal-action-btns; +} + +.cancel-button { + @extend .modal-cancel-btn; +} + +.accept-button { + @extend .modal-accept-btn; + &.danger { + @extend .modal-danger-btn; + } +} diff --git a/frontend/src/app/main/ui/settings/feedback.cljs b/frontend/src/app/main/ui/settings/feedback.cljs index d510ef7c5b..4fcc7f790c 100644 --- a/frontend/src/app/main/ui/settings/feedback.cljs +++ b/frontend/src/app/main/ui/settings/feedback.cljs @@ -6,16 +6,17 @@ (ns app.main.ui.settings.feedback "Feedback form." + (:require-macros [app.main.style :as stl]) (:require [app.common.spec :as us] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.spec.alpha :as s] [rumext.v2 :as mf])) @@ -38,7 +39,7 @@ (mf/deps profile) (fn [_] (reset! loading false) - (st/emit! (dm/success (tr "labels.feedback-sent"))) + (st/emit! (msg/success (tr "labels.feedback-sent"))) (swap! form assoc :data {} :touched {} :errors {}))) on-error @@ -47,8 +48,8 @@ (fn [{:keys [code] :as error}] (reset! loading false) (if (= code :feedback-disabled) - (st/emit! (dm/error (tr "labels.feedback-disabled"))) - (st/emit! (dm/error (tr "errors.generic")))))) + (st/emit! (msg/error (tr "labels.feedback-disabled"))) + (st/emit! (msg/error (tr "errors.generic")))))) on-submit (mf/use-callback @@ -57,53 +58,57 @@ (reset! loading true) (let [data (:clean-data @form)] (->> (rp/cmd! :send-user-feedback data) - (rx/subs on-succes on-error)))))] + (rx/subs! on-succes on-error)))))] - [:& fm/form {:class "feedback-form" + [:& fm/form {:class (stl/css :feedback-form) :on-submit on-submit :form form} - ;; --- Feedback section - [:h2 (tr "feedback.title")] - [:p (tr "feedback.subtitle")] + ;; --- Feedback section + [:h2 {:class (stl/css :field-title)} (tr "feedback.title")] + [:p {:class (stl/css :field-text)} (tr "feedback.subtitle")] - [:div.fields-row + [:div {:class (stl/css :fields-row)} [:& fm/input {:label (tr "feedback.subject") - :name :subject}]] - [:div.fields-row + :name :subject + :show-success? true}]] + [:div {:class (stl/css :fields-row :description)} [:& fm/textarea {:label (tr "feedback.description") :name :content :rows 5}]] - [:& fm/submit-button + [:> fm/submit-button* {:label (if @loading (tr "labels.sending") (tr "labels.send")) + :class (stl/css :feedback-button-link) :disabled @loading}] [:hr] - [:h2 (tr "feedback.discourse-title")] - [:p (tr "feedback.discourse-subtitle1")] + [:h2 {:class (stl/css :field-title)} (tr "feedback.discourse-title")] + [:p {:class (stl/css :field-text)} (tr "feedback.discourse-subtitle1")] - [:a.btn-secondary.btn-large - {:href "https://community.penpot.app" :target "_blank"} + [:a + {:class (stl/css :feedback-button-link) + :href "https://community.penpot.app" + :target "_blank"} (tr "feedback.discourse-go-to")] [:hr] - [:h2 (tr "feedback.twitter-title")] - [:p (tr "feedback.twitter-subtitle1")] + [:h2 {:class (stl/css :field-title)} (tr "feedback.twitter-title")] + [:p {:class (stl/css :field-text)} (tr "feedback.twitter-subtitle1")] - [:a.btn-secondary.btn-large - {:href "https://twitter.com/penpotapp" :target "_blank"} - (tr "feedback.twitter-go-to")] - - ])) + [:a + {:class (stl/css :feedback-button-link) + :href "https://twitter.com/penpotapp" + :target "_blank"} + (tr "feedback.twitter-go-to")]])) (mf/defc feedback-page [] (mf/use-effect - #(dom/set-html-title (tr "title.settings.feedback"))) + #(dom/set-html-title (tr "title.settings.feedback"))) - [:div.dashboard-settings - [:div.form-container + [:div {:class (stl/css :dashboard-settings)} + [:div {:class (stl/css :form-container)} [:& feedback-form]]]) diff --git a/frontend/src/app/main/ui/settings/feedback.scss b/frontend/src/app/main/ui/settings/feedback.scss new file mode 100644 index 0000000000..f2638a7f54 --- /dev/null +++ b/frontend/src/app/main/ui/settings/feedback.scss @@ -0,0 +1,29 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor" as *; +@use "./profile"; + +.feedback-form { + textarea { + border-radius: $br-8; + padding: $br-12; + background-color: $db-tertiary; + color: $df-primary; + border: none; + + ::placeholder { + color: $db-disabled; + } + &:focus { + outline: $s-1 solid $da-primary; + } + } +} + +.feedback-button-link { + @extend .button-primary; +} diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs index 6356a9e209..2586559bd1 100644 --- a/frontend/src/app/main/ui/settings/options.cljs +++ b/frontend/src/app/main/ui/settings/options.cljs @@ -5,11 +5,11 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.settings.options + (:require-macros [app.main.style :as stl]) (:require [app.common.spec :as us] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.users :as du] - [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.forms :as fm] @@ -25,31 +25,32 @@ (s/keys :opt-un [::lang ::theme])) (defn- on-success - [_] - (st/emit! (dm/success (tr "notifications.profile-saved")))) + [profile] + (st/emit! (msg/success (tr "notifications.profile-saved")) + (du/profile-fetched profile))) (defn- on-submit [form _event] - (let [data (:clean-data @form) - mdata {:on-success (partial on-success form)}] - (st/emit! (du/update-profile (with-meta data mdata))))) + (let [data (:clean-data @form)] + (st/emit! (du/update-profile data) + (du/persist-profile {:on-success on-success})))) (mf/defc options-form + {::mf/wrap-props false} [] (let [profile (mf/deref refs/profile) initial (mf/with-memo [profile] (update profile :lang #(or % ""))) form (fm/use-form :spec ::options-form - :initial initial) - new-css-system (features/use-feature :new-css-system)] + :initial initial)] - [:& fm/form {:class "options-form" + [:& fm/form {:class (stl/css :options-form) :on-submit on-submit :form form} - [:h2 (tr "labels.language")] + [:h3 (tr "labels.language")] - [:div.fields-row + [:div {:class (stl/css :fields-row)} [:& fm/select {:options (into [{:label "Auto (browser)" :value ""}] i18n/supported-locales) :label (tr "dashboard.select-ui-language") @@ -57,18 +58,19 @@ :name :lang :data-test "setting-lang"}]] - (when new-css-system - [:h2 (tr "dashboard.theme-change")] - [:div.fields-row - [:& fm/select {:label (tr "dashboard.select-ui-theme") - :name :theme - :default "default" - :options [{:label "Penpot Dark (default)" :value "default"} - {:label "Penpot Light" :value "light"}] - :data-test "setting-theme"}]]) - [:& fm/submit-button + [:h3 (tr "dashboard.theme-change")] + [:div {:class (stl/css :fields-row)} + [:& fm/select {:label (tr "dashboard.select-ui-theme") + :name :theme + :default "default" + :options [{:label "Penpot Dark (default)" :value "default"} + {:label "Penpot Light" :value "light"}] + :data-test "setting-theme"}]] + + [:> fm/submit-button* {:label (tr "dashboard.update-settings") - :data-test "submit-lang-change"}]])) + :data-test "submit-lang-change" + :class (stl/css :btn-primary)}]])) ;; --- Password Page @@ -77,7 +79,8 @@ (mf/use-effect #(dom/set-html-title (tr "title.settings.options"))) - [:div.dashboard-settings - [:div.form-container - {:data-test "settings-form"} + [:div {:class (stl/css :dashboard-settings)} + [:div {:class (stl/css :form-container) :data-test "settings-form"} + [:h2 (tr "labels.settings")] [:& options-form {}]]]) + diff --git a/frontend/src/app/main/ui/settings/options.scss b/frontend/src/app/main/ui/settings/options.scss new file mode 100644 index 0000000000..474e96838f --- /dev/null +++ b/frontend/src/app/main/ui/settings/options.scss @@ -0,0 +1,7 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "./profile" as *; diff --git a/frontend/src/app/main/ui/settings/password.cljs b/frontend/src/app/main/ui/settings/password.cljs index a2f2c727b1..db12f1b3c9 100644 --- a/frontend/src/app/main/ui/settings/password.cljs +++ b/frontend/src/app/main/ui/settings/password.cljs @@ -5,9 +5,10 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.settings.password + (:require-macros [app.main.style :as stl]) (:require [app.common.spec :as us] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.users :as udu] [app.main.store :as st] [app.main.ui.components.forms :as fm] @@ -18,7 +19,7 @@ (defn- on-error [form error] - (case (:code error) + (case (:code (ex-data error)) :old-password-not-match (swap! form assoc-in [:errors :password-old] {:message (tr "errors.wrong-old-password")}) @@ -27,7 +28,7 @@ {:message (tr "errors.email-as-password")}) (let [msg (tr "generic.error")] - (st/emit! (dm/error msg))))) + (st/emit! (msg/error msg))))) (defn- on-success [form] @@ -36,7 +37,7 @@ msg (tr "dashboard.notifications.password-saved")] (dom/clean-value! password-old-node) (dom/focus! password-old-node) - (st/emit! (dm/success msg)))) + (st/emit! (msg/success msg)))) (defn- on-submit [form event] @@ -76,32 +77,35 @@ (fm/validate-not-empty :password-2 (tr "auth.password-not-empty")) password-equality] :initial initial)] - [:& fm/form {:class "password-form" + [:& fm/form {:class (stl/css :password-form) :on-submit on-submit :form form} - [:h2 (t locale "dashboard.password-change")] - [:div.fields-row + + [:div {:class (stl/css :fields-row)} [:& fm/input {:type "password" :name :password-old :auto-focus? true :label (t locale "labels.old-password")}]] - [:div.fields-row + [:div {:class (stl/css :fields-row)} [:& fm/input {:type "password" :name :password-1 + :show-success? true :label (t locale "labels.new-password")}]] - [:div.fields-row + [:div {:class (stl/css :fields-row)} [:& fm/input {:type "password" :name :password-2 + :show-success? true :label (t locale "labels.confirm-password")}]] - [:& fm/submit-button - {:label (t locale "dashboard.update-settings") - :data-test "submit-password"}]])) + [:> fm/submit-button* + {:label (t locale "dashboard.password-change") + :data-test "submit-password" + :class (stl/css :update-btn)}]])) ;; --- Password Page @@ -110,6 +114,7 @@ (mf/use-effect #(dom/set-html-title (tr "title.settings.password"))) - [:section.dashboard-settings.form-container - [:div.form-container + [:section {:class (stl/css :dashboard-settings)} + [:div {:class (stl/css :form-container)} + [:h2 (t locale "dashboard.password-change")] [:& password-form {:locale locale}]]]) diff --git a/frontend/src/app/main/ui/settings/password.scss b/frontend/src/app/main/ui/settings/password.scss new file mode 100644 index 0000000000..fe44ec71b3 --- /dev/null +++ b/frontend/src/app/main/ui/settings/password.scss @@ -0,0 +1,14 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; +@use "./profile" as *; + +.update-btn { + margin-top: $s-16; + @extend .button-primary; + height: $s-36; +} diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs index cbb302b709..ac4c3ca7c2 100644 --- a/frontend/src/app/main/ui/settings/profile.cljs +++ b/frontend/src/app/main/ui/settings/profile.cljs @@ -5,17 +5,17 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.settings.profile + (:require-macros [app.main.style :as stl]) (:require [app.common.spec :as us] [app.config :as cf] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.data.users :as du] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.forms :as fm] - [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [cljs.spec.alpha :as s] @@ -27,15 +27,12 @@ (s/def ::profile-form (s/keys :req-un [::fullname ::email])) -(defn- on-success - [_] - (st/emit! (dm/success (tr "notifications.profile-saved")))) - (defn- on-submit [form _event] - (let [data (:clean-data @form) - mdata {:on-success (partial on-success form)}] - (st/emit! (du/update-profile (with-meta data mdata))))) + (let [data (:clean-data @form)] + (st/emit! (du/update-profile data) + (du/persist-profile) + (msg/success (tr "notifications.profile-saved"))))) ;; --- Profile Form @@ -45,37 +42,46 @@ form (fm/use-form :spec ::profile-form :initial profile :validators [(fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long")) - (fm/validate-not-empty :fullname (tr "auth.name.not-all-space"))])] + (fm/validate-not-empty :fullname (tr "auth.name.not-all-space"))]) + + handle-show-change-email + (mf/use-callback + #(modal/show! :change-email {})) + + handle-show-delete-account + (mf/use-callback + #(modal/show! :delete-account {}))] [:& fm/form {:on-submit on-submit :form form - :class "profile-form"} - [:div.fields-row + :class (stl/css :profile-form)} + [:div {:class (stl/css :fields-row)} [:& fm/input {:type "text" :name :fullname :label (tr "dashboard.your-name")}]] - [:div.fields-row + [:div {:class (stl/css :fields-row) + :on-click handle-show-change-email} [:& fm/input {:type "email" :name :email :disabled true - :help-icon i/at :label (tr "dashboard.your-email")}] - [:div.options + [:div {:class (stl/css :options)} [:div.change-email - [:a {:on-click #(modal/show! :change-email {})} + [:a {:on-click handle-show-change-email} (tr "dashboard.change-email")]]]] - [:& fm/submit-button + [:> fm/submit-button* {:label (tr "dashboard.save-settings") - :disabled (empty? (:touched @form))}] + :disabled (empty? (:touched @form)) + :class (stl/css :btn-primary)}] - [:div.links - [:div.link-item - [:a {:on-click #(modal/show! :delete-account {}) + [:div {:class (stl/css :links)} + [:div {:class (stl/css :link-item)} + [:a {:on-click handle-show-delete-account :data-test "remove-acount-btn"} (tr "dashboard.remove-account")]]]])) @@ -91,9 +97,10 @@ (fn [file] (st/emit! (du/update-photo file)))] - [:form.avatar-form - [:div.image-change-field - [:span.update-overlay {:on-click on-image-click} (tr "labels.update")] + [:form {:class (stl/css :avatar-form)} + [:div {:class (stl/css :image-change-field)} + [:span {:class (stl/css :update-overlay) + :on-click on-image-click} (tr "labels.update")] [:img {:src photo}] [:& file-uploader {:accept "image/jpeg,image/png" :multi false @@ -106,8 +113,9 @@ (mf/defc profile-page [] (mf/with-effect [] (dom/set-html-title (tr "title.settings.profile"))) - [:div.dashboard-settings - [:div.form-container.two-columns + [:div {:class (stl/css :dashboard-settings)} + [:div {:class (stl/css :form-container)} + [:h2 (tr "labels.profile")] [:& profile-photo-form] [:& profile-form]]]) diff --git a/frontend/src/app/main/ui/settings/profile.scss b/frontend/src/app/main/ui/settings/profile.scss new file mode 100644 index 0000000000..62b81e3df8 --- /dev/null +++ b/frontend/src/app/main/ui/settings/profile.scss @@ -0,0 +1,331 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor" as *; + +.dashboard-settings { + display: flex; + width: 100%; + justify-content: center; + align-items: center; + a:not(.button-primary) { + color: $df-secondary; + } +} + +.form-container { + display: flex; + justify-content: center; + flex-direction: column; + max-width: $s-500; + margin-bottom: $s-32; + width: $s-580; + margin: $s-80 auto $s-120 auto; + justify-content: center; + + form { + display: flex; + flex-direction: column; + + .btn-secondary { + width: 100%; + font-size: $fs-11; + text-transform: uppercase; + background-color: $db-tertiary; + color: $df-primary; + &:hover { + color: $da-primary; + background-color: $db-quaternary; + } + } + hr { + display: none; + } + } + + .fields-row { + --input-height: #{$s-40}; + margin-bottom: $s-20; + flex-direction: column; + + .options { + display: flex; + justify-content: flex-end; + font-size: $fs-14; + margin-top: $s-12; + } + } + + .field { + margin-bottom: $s-20; + } + + .field-title { + color: $df-primary; + + &:not(:first-child) { + margin-top: $s-64; + } + } + + .field-text { + color: $df-secondary; + } + + .custom-input, + .custom-select { + flex-direction: column-reverse; + label { + position: relative; + text-transform: uppercase; + color: $df-primary; + font-size: $fs-11; + margin-bottom: $s-12; + margin-left: calc(-1 * $s-4); + } + input, + select { + background-color: $db-tertiary; + border-radius: $br-8; + border-color: transparent; + color: $df-primary; + padding: 0 $s-16; + &:focus { + outline: $s-1 solid $da-primary; + } + ::placeholder { + color: $df-secondary; + } + } + .help-icon { + bottom: $s-12; + top: auto; + svg { + fill: $df-secondary; + } + } + &.disabled { + input { + background-color: var(--input-background-color-disabled); + border-color: $db-quaternary; + color: $df-secondary; + } + } + .input-container { + background-color: $db-tertiary; + border-radius: $br-8; + border-color: transparent; + margin-top: $s-24; + .main-content { + label { + position: absolute; + top: calc(-1 * $s-24); + } + span { + color: $df-primary; + } + } + &:focus { + border: $s-1 solid $da-primary; + } + } + textarea { + border-radius: $br-8; + padding: $s-12 $s-16; + background-color: $db-tertiary; + color: $df-primary; + border: none; + &:focus { + outline: $s-1 solid $da-primary; + } + } + } + + &.two-columns { + max-width: $s-520; + justify-content: space-between; + flex-direction: row; + } + + h1 { + font-size: $fs-36; + color: $db-tertiary; + margin-bottom: $s-20; + } + + .subtitle { + font-size: $fs-24; + color: $db-tertiary; + margin-bottom: $s-20; + } + + .notification-icon { + justify-content: center; + display: flex; + margin-bottom: $s-48; + + svg { + fill: $db-primary; + height: 40%; + width: 40%; + } + } + + .notification-text { + font-size: $fs-16; + color: $db-primary; + margin-bottom: $s-20; + } + + .notification-text-email { + background: $df-primary; + border-radius: $br-4; + color: $db-primary; + font-size: $fs-16; + font-weight: $fw500; + margin: $s-24 0 $s-40 0; + padding: $s-16; + text-align: center; + } + + h2 { + font-size: $fs-24; + font-weight: $fw400; + color: $df-primary; + display: flex; + align-items: center; + margin: $s-16 0; + } + + h3 { + font-size: $fs-12; + font-weight: $fw400; + color: $df-primary; + display: flex; + align-items: center; + margin: $s-8 0; + text-transform: uppercase; + } + + a:not(.button-primary) { + &:hover { + text-decoration: underline; + } + } + + p { + color: $db-primary; + } + + hr { + border-color: $df-secondary; + } + + .links { + margin-top: $s-12; + } +} + +form.avatar-form { + display: flex; + flex-direction: column; + width: $s-148; + height: $s-148; + margin: $s-16 0; + + img { + border-radius: 50%; + flex-shrink: 0; + height: 100%; + margin-right: $s-16; + width: 100%; + } +} + +.image-change-field { + position: relative; + width: 100%; + height: 100%; + + .update-overlay { + opacity: 0; + cursor: pointer; + position: absolute; + width: 100%; + height: 100%; + border-radius: 50%; + font-size: $fs-24; + color: $df-primary; + line-height: 6; + text-align: center; + background: $da-tertiary; + z-index: $z-index-modal; + } + + input[type="file"] { + width: 100%; + height: 100%; + position: absolute; + opacity: 0; + cursor: pointer; + top: 0; + z-index: $z-index-modal; + } + + &:hover { + .update-overlay { + opacity: 0.8; + } + } +} + +.profile-form { + display: flex; + flex-direction: column; + max-width: $s-368; + width: 100%; +} + +.newsletter-subs { + border-bottom: $s-1 solid $df-secondary; + border-top: $s-1 solid $df-secondary; + padding: $s-32 0; + margin-bottom: $s-32; + + .newsletter-title { + font-family: "worksans", sans-serif; + color: $df-secondary; + font-size: $fs-14; + } + + label { + font-family: "worksans", sans-serif; + color: $db-primary; + font-size: $fs-12; + margin-right: calc(-1 * $s-16); + margin-bottom: $s-12; + } + + .info { + color: $df-secondary; + font-size: $fs-12; + margin-bottom: $s-8; + } + + .input-checkbox label { + align-items: flex-start; + } +} + +.btn-secondary { + @extend .button-secondary; + height: $s-32; +} + +.btn-primary { + @extend .button-primary; + height: $s-32; +} diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs index 8c077cb61d..c878c7b4bc 100644 --- a/frontend/src/app/main/ui/settings/sidebar.cljs +++ b/frontend/src/app/main/ui/settings/sidebar.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.settings.sidebar + (:require-macros [app.main.style :as stl]) (:require [app.config :as cf] [app.main.data.events :as ev] @@ -16,105 +17,108 @@ [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.router :as rt] - [potok.core :as ptk] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) +(def ^:private arrow-icon + (i/icon-xref :arrow (stl/css :arrow-icon))) + +(def ^:private feedback-icon + (i/icon-xref :feedback (stl/css :feedback-icon))) + +(def ^:private go-settings-profile + #(st/emit! (rt/nav :settings-profile))) + +(def ^:private go-settings-feedback + #(st/emit! (rt/nav :settings-feedback))) + +(def ^:private go-settings-password + #(st/emit! (rt/nav :settings-password))) + +(def ^:private go-settings-options + #(st/emit! (rt/nav :settings-options))) + +(def ^:private go-settings-access-tokens + #(st/emit! (rt/nav :settings-access-tokens))) + +(defn- show-release-notes + [event] + (let [version (:main cf/version)] + (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) + + (if (and (kbd/alt? event) (kbd/mod? event)) + (st/emit! (modal/show {:type :onboarding})) + (st/emit! (modal/show {:type :release-notes :version version}))))) + (mf/defc sidebar-content - [{:keys [profile section] :as props}] + {::mf/props :obj} + [{:keys [profile section]}] (let [profile? (= section :settings-profile) password? (= section :settings-password) options? (= section :settings-options) feedback? (= section :settings-feedback) access-tokens? (= section :settings-access-tokens) + team-id (du/get-current-team-id profile) go-dashboard - (mf/use-callback - (mf/deps profile) - #(st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)}))) + (mf/use-fn + (mf/deps team-id) + #(st/emit! (rt/nav :dashboard-projects {:team-id team-id})))] - go-settings-profile - (mf/use-callback - (mf/deps profile) - #(st/emit! (rt/nav :settings-profile))) + [:div {:class (stl/css :sidebar-content)} + [:div {:class (stl/css :sidebar-content-section)} + [:button {:class (stl/css :back-to-dashboard) + :on-click go-dashboard} + arrow-icon + [:span {:class (stl/css :back-text)} (tr "labels.dashboard")]]] - go-settings-feedback - (mf/use-callback - (mf/deps profile) - #(st/emit! (rt/nav :settings-feedback))) + [:hr {:class (stl/css :sidebar-separator)}] - go-settings-password - (mf/use-callback - (mf/deps profile) - #(st/emit! (rt/nav :settings-password))) - - go-settings-options - (mf/use-callback - (mf/deps profile) - #(st/emit! (rt/nav :settings-options))) - - go-settings-access-tokens - (mf/use-callback - (mf/deps profile) - #(st/emit! (rt/nav :settings-access-tokens))) - - show-release-notes - (mf/use-callback - (fn [event] - (let [version (:main cf/version)] - (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) - (if (and (kbd/alt? event) (kbd/mod? event)) - (st/emit! (modal/show {:type :onboarding})) - (st/emit! (modal/show {:type :release-notes :version version}))))))] - - [:div.sidebar-content - [:div.sidebar-content-section - [:div.back-to-dashboard {:on-click go-dashboard} - [:span.icon i/arrow-down] - [:span.text (tr "labels.dashboard")]]] - [:hr] - - [:div.sidebar-content-section - [:ul.sidebar-nav.no-overflow - [:li {:class (when profile? "current") + [:div {:class (stl/css :sidebar-content-section)} + [:ul {:class (stl/css :sidebar-nav-settings)} + [:li {:class (stl/css-case :current profile? + :settings-item true) :on-click go-settings-profile} - i/user - [:span.element-title (tr "labels.profile")]] + [:span {:class (stl/css :element-title)} (tr "labels.profile")]] - [:li {:class (when password? "current") + [:li {:class (stl/css-case :current password? + :settings-item true) :on-click go-settings-password} - i/lock - [:span.element-title (tr "labels.password")]] + [:span {:class (stl/css :element-title)} (tr "labels.password")]] - [:li {:class (when options? "current") + [:li {:class (stl/css-case :current options? + :settings-item true) :on-click go-settings-options :data-test "settings-profile"} - i/tree - [:span.element-title (tr "labels.settings")]] + [:span {:class (stl/css :element-title)} (tr "labels.settings")]] (when (contains? cf/flags :access-tokens) - [:li {:class (when access-tokens? "current") + [:li {:class (stl/css-case :current access-tokens? + :settings-item true) :on-click go-settings-access-tokens :data-test "settings-access-tokens"} - i/icon-key - [:span.element-title (tr "labels.access-tokens")]]) + [:span {:class (stl/css :element-title)} (tr "labels.access-tokens")]]) - [:hr] + [:hr {:class (stl/css :sidebar-separator)}] - [:li {:on-click show-release-notes :data-test "release-notes"} - i/pencil - [:span.element-title (tr "labels.release-notes")]] + [:li {:on-click show-release-notes :data-test "release-notes" + :class (stl/css :settings-item)} + [:span {:class (stl/css :element-title)} (tr "labels.release-notes")]] (when (contains? cf/flags :user-feedback) - [:li {:class (when feedback? "current") + [:li {:class (stl/css-case :current feedback? + :settings-item true) :on-click go-settings-feedback} - i/msg-info - [:span.element-title (tr "labels.give-feedback")]])]]])) + feedback-icon + [:span {:class (stl/css :element-title)} (tr "labels.give-feedback")]])]]])) (mf/defc sidebar - {::mf/wrap [mf/memo]} + {::mf/wrap [mf/memo] + ::mf/props :obj} [{:keys [profile locale section]}] - [:div.dashboard-sidebar.settings + [:div {:class (stl/css :dashboard-sidebar :settings)} [:& sidebar-content {:profile profile :section section}] [:& profile-section {:profile profile :locale locale}]]) + diff --git a/frontend/src/app/main/ui/settings/sidebar.scss b/frontend/src/app/main/ui/settings/sidebar.scss new file mode 100644 index 0000000000..a027458927 --- /dev/null +++ b/frontend/src/app/main/ui/settings/sidebar.scss @@ -0,0 +1,91 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.dashboard-sidebar { + grid-column: 1 / span 2; + grid-row: 1 / span 2; + display: grid; + grid-template-rows: 1fr auto; + height: 100%; + padding-block-start: $s-16; + border-right: $s-1 solid var(--panel-border-color); + z-index: $z-index-1; + background-color: var(--panel-background-color); +} + +.sidebar-content { + display: grid; + grid-template-rows: auto auto 1fr; + height: 100%; + padding: 0; + overflow-y: auto; +} + +.sidebar-separator { + border-color: transparent; + margin: $s-12 $s-16; +} + +.sidebar-nav-settings { + display: grid; + grid-auto-rows: auto; + margin: 0; + overflow: unset; + user-select: none; +} + +.settings-item { + --settings-foreground-color: var(--menu-foreground-color-rest); + --settings-background-color: transparent; + display: flex; + align-items: center; + padding: $s-8 $s-8 $s-8 $s-24; + color: var(--settings-foreground-color); + background-color: var(--settings-background-color); + cursor: pointer; + + &:hover { + --settings-foreground-color: var(--sidebar-element-foreground-color-hover); + --settings-background-color: var(--sidebar-element-background-color-hover); + } + + &.current { + --settings-foreground-color: var(--sidebar-element-foreground-color-selected); + --settings-background-color: var(--sidebar-element-background-color-selected); + } +} + +.feedback-icon { + @extend .button-icon-small; + stroke: var(--settings-foreground-color); + margin-right: $s-8; +} + +.element-title { + @include textEllipsis; + @include bodyMediumTypography; +} + +.back-to-dashboard { + @include buttonStyle; + display: flex; + align-items: center; + padding: $s-12 $s-16; + font-size: $fs-14; +} + +.back-text { + color: $df-primary; +} + +.arrow-icon { + @extend .button-icon; + stroke: var(--icon-foreground); + transform: rotate(180deg); + margin-right: $s-12; +} diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 8ea32ef4ec..15f99ddf47 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -9,27 +9,27 @@ [app.common.colors :as clr] [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] + [app.common.svg :as csvg] [app.common.types.shape :refer [stroke-caps-line stroke-caps-marker]] [app.common.types.shape.radius :as ctsr] - [app.main.ui.context :as muc] [app.util.object :as obj] - [app.util.svg :as usvg] - [cuerdas.core :as str] - [rumext.v2 :as mf])) + [cuerdas.core :as str])) -(defn- stroke-type->dasharray - [width style] - (let [values (case style - :mixed [5 5 1 5] - ;; We want 0 so they are circles - :dotted [(- width) 5] - :dashed [10 10] - nil)] +(defn- calculate-dasharray + [style width] + (let [w+5 (+ 5 width) + w+1 (+ 1 width) + w+10 (+ 10 width)] + (case style + :mixed (str/concat "" w+5 "," w+5 "," w+1 "," w+5) + :dotted (str/concat "0," w+5) + :dashed (str/concat "" w+10 "," w+10) + ""))) - (->> values (map #(+ % width)) (str/join ",")))) - -(defn extract-border-radius [{:keys [x y width height] :as shape}] +(defn get-border-props + [shape] (case (ctsr/radius-mode shape) :radius-1 (let [radius (gsh/shape-corners-1 shape)] @@ -37,6 +37,10 @@ :radius-4 (let [[r1 r2 r3 r4] (gsh/shape-corners-4 shape) + x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + width (dm/get-prop shape :width) + height (dm/get-prop shape :height) top (- width r1 r2) right (- height r2 r3) bottom (- width r3 r4) @@ -53,194 +57,204 @@ "a" r1 "," r1 " 0 0 1 " r1 "," (- r1) " " "z")}))) +(defn add-border-props! + [props shape] + (obj/merge! props (get-border-props shape))) -(defn add-border-radius [attrs shape] - (obj/merge! attrs (extract-border-radius shape))) - -(defn add-fill - ([attrs fill-data render-id type] - (add-fill attrs fill-data render-id nil type)) - +(defn add-fill! ([attrs fill-data render-id index type] - (let [fill-attrs - (cond - (contains? fill-data :fill-image) - (let [fill-image-id (str "fill-image-" render-id)] - {:fill (str "url(#" fill-image-id ")")}) + (add-fill! attrs fill-data render-id index type "none")) + ([attrs fill-data render-id index type fill-default] + (let [index (if (some? index) (dm/str "-" index) "")] + (cond + (contains? fill-data :fill-image) + (let [id (dm/str "fill-image-" render-id)] + (obj/set! attrs "fill" (dm/str "url(#" id ")"))) - (and (contains? fill-data :fill-color-gradient) (some? (:fill-color-gradient fill-data))) - (let [fill-color-gradient-id (str "fill-color-gradient_" render-id (if index (str "_" index) ""))] - {:fill (str "url(#" fill-color-gradient-id ")")}) + (some? (:fill-color-gradient fill-data)) + (let [id (dm/str "fill-color-gradient-" render-id index)] + (obj/set! attrs "fill" (dm/str "url(#" id ")"))) - (contains? fill-data :fill-color) - {:fill (:fill-color fill-data)} + (contains? fill-data :fill-color) + (obj/set! attrs "fill" (:fill-color fill-data)) - :else - {:fill "none"}) + :else + (obj/set! attrs "fill" fill-default)) - fill-attrs (cond-> fill-attrs - (contains? fill-data :fill-opacity) - (assoc :fillOpacity (:fill-opacity fill-data)) + (when (contains? fill-data :fill-opacity) + (obj/set! attrs "fillOpacity" (:fill-opacity fill-data))) - ;; Old texts with only an opacity set are black by default - (and (= type :text) (nil? (:fill-color-gradient fill-data)) (nil? (:fill-color fill-data))) - (assoc :fill "black"))] + (when (and (= :text type) + (nil? (:fill-color-gradient fill-data)) + (nil? (:fill-color fill-data))) + (obj/set! attrs "fill" "black")) - (obj/merge! attrs (clj->js fill-attrs))))) + attrs))) -(defn add-stroke [attrs stroke-data render-id index] - (let [stroke-style (:stroke-style stroke-data :solid) - stroke-color-gradient-id (str "stroke-color-gradient_" render-id "_" index) - stroke-width (:stroke-width stroke-data 1)] - (if (not= stroke-style :none) - (let [stroke-attrs - (cond-> {:strokeWidth stroke-width} - (:stroke-color-gradient stroke-data) - (assoc :stroke (str/format "url(#%s)" stroke-color-gradient-id)) +(defn add-stroke! + [attrs data render-id index open-path?] + (let [style (:stroke-style data :solid)] + (when-not (= style :none) + (let [width (:stroke-width data 1) + gradient (:stroke-color-gradient data) + color (:stroke-color data) + opacity (:stroke-opacity data)] - (and (not (:stroke-color-gradient stroke-data)) - (:stroke-color stroke-data nil)) - (assoc :stroke (:stroke-color stroke-data nil)) + (obj/set! attrs "strokeWidth" width) - (and (not (:stroke-color-gradient stroke-data)) - (:stroke-opacity stroke-data nil)) - (assoc :strokeOpacity (:stroke-opacity stroke-data nil)) + (when (some? gradient) + (let [gradient-id (dm/str "stroke-color-gradient-" render-id "-" index)] + (obj/set! attrs "stroke" (str/ffmt "url(#%)" gradient-id)))) - (not= stroke-style :svg) - (assoc :strokeDasharray (stroke-type->dasharray stroke-width stroke-style)) + (when-not (some? gradient) + (when (some? color) + (obj/set! attrs "stroke" color)) + (when (some? opacity) + (obj/set! attrs "strokeOpacity" opacity))) - ;; For simple line caps we use svg stroke-line-cap attribute. This - ;; only works if all caps are the same and we are not using the tricks - ;; for inner or outer strokes. - (and (stroke-caps-line (:stroke-cap-start stroke-data)) - (= (:stroke-cap-start stroke-data) (:stroke-cap-end stroke-data)) - (not (#{:inner :outer} (:stroke-alignment stroke-data))) - (not= :dotted stroke-style)) - (assoc :strokeLinecap (:stroke-cap-start stroke-data)) + (when (not= style :svg) + (obj/set! attrs "strokeDasharray" (calculate-dasharray style width))) - (= :dotted stroke-style) - (assoc :strokeLinecap "round") + ;; For simple line caps we use svg stroke-line-cap attribute. This + ;; only works if all caps are the same and we are not using the tricks + ;; for inner or outer strokes. + (let [caps-start (:stroke-cap-start data) + caps-end (:stroke-cap-end data) + alignment (:stroke-alignment data)] + (cond + (and (contains? stroke-caps-line caps-start) + (= caps-start caps-end) + (or open-path? + (and (not= :inner alignment) + (not= :outer alignment))) + (not= :dotted style)) + (obj/set! attrs "strokeLinecap" (name caps-start)) - ;; For other cap types we use markers. - (and (or (stroke-caps-marker (:stroke-cap-start stroke-data)) - (and (stroke-caps-line (:stroke-cap-start stroke-data)) - (not= (:stroke-cap-start stroke-data) (:stroke-cap-end stroke-data)))) - (not (#{:inner :outer} (:stroke-alignment stroke-data)))) - (assoc :markerStart - (str/format "url(#marker-%s-%s)" render-id (name (:stroke-cap-start stroke-data)))) + (= :dotted style) + (obj/set! attrs "strokeLinecap" "round")) - (and (or (stroke-caps-marker (:stroke-cap-end stroke-data)) - (and (stroke-caps-line (:stroke-cap-end stroke-data)) - (not= (:stroke-cap-start stroke-data) (:stroke-cap-end stroke-data)))) - (not (#{:inner :outer} (:stroke-alignment stroke-data)))) - (assoc :markerEnd - (str/format "url(#marker-%s-%s)" render-id (name (:stroke-cap-end stroke-data)))))] + (when (or open-path? + (and (not= :inner alignment) + (not= :outer alignment))) - (obj/merge! attrs (clj->js stroke-attrs))) - attrs))) + ;; For other cap types we use markers. + (when (or (contains? stroke-caps-marker caps-start) + (and (contains? stroke-caps-line caps-start) + (not= caps-start caps-end))) + (obj/set! attrs "markerStart" (str/ffmt "url(#marker-%-%)" render-id (name caps-start)))) -(defn add-layer-props [attrs shape] - (cond-> attrs - (:opacity shape) - (obj/set! "opacity" (:opacity shape)))) + (when (or (contains? stroke-caps-marker caps-end) + (and (contains? stroke-caps-line caps-end) + (not= caps-start caps-end))) + (obj/set! attrs "markerEnd" (str/ffmt "url(#marker-%-%)" render-id (name caps-end)))))))) -(defn extract-svg-attrs - [render-id svg-defs svg-attrs] - (if (and (empty? svg-defs) (empty? svg-attrs)) - [#js {} #js {}] - (let [replace-id (fn [id] - (if (contains? svg-defs id) - (str render-id "-" id) - id)) - svg-attrs (-> svg-attrs - (usvg/clean-attrs) - (usvg/update-attr-ids replace-id) - (dissoc :id)) + attrs)) - attrs (-> svg-attrs (dissoc :style) (clj->js)) - styles (-> svg-attrs (:style {}) (clj->js))] - [attrs styles]))) +(defn get-svg-props + [shape render-id] + (let [attrs (get shape :svg-attrs {}) + defs (get shape :svg-defs {})] + (if (and (empty? defs) + (empty? attrs)) + #js {} + (-> attrs + (csvg/update-attr-ids + (fn [id] + (if (contains? defs id) + (dm/str render-id "-" id) + id))) + (dissoc :id) + (obj/map->obj))))) -(defn add-style-attrs - ([props shape] - (let [render-id (mf/use-ctx muc/render-id)] - (add-style-attrs props shape render-id))) +(defn get-fill-style + ([fill-data index render-id type] + (add-fill! #js {} fill-data render-id index type)) + ([fill-data index render-id type fill-default] + (add-fill! #js {} fill-data render-id index type fill-default))) +(defn add-fill-props! ([props shape render-id] - (let [svg-defs (:svg-defs shape {}) - svg-attrs (:svg-attrs shape {}) + (add-fill-props! props shape 0 render-id)) - [svg-attrs svg-styles] - (extract-svg-attrs render-id svg-defs svg-attrs) + ([props shape position render-id] + (let [shape-fills (get shape :fills) + shape-shadow (get shape :shadow) + shape-blur (get shape :blur) - styles (-> (obj/get props "style" (obj/create)) - (obj/merge! svg-styles) - (add-layer-props shape)) + svg-attrs (get-svg-props shape render-id) + svg-styles (obj/get svg-attrs "style") - styles (cond (or (some? (:fill-image shape)) - (= :image (:type shape)) - (> (count (:fills shape)) 1) - (some #(some? (:fill-color-gradient %)) (:fills shape))) - (obj/set! styles "fill" (str "url(#fill-0-" render-id ")")) + shape-type (dm/get-prop shape :type) - ;; imported svgs can have fill and fill-opacity attributes - (and (some? svg-styles) (obj/contains? svg-styles "fill")) - (-> styles - (obj/set! "fill" (obj/get svg-styles "fill")) - (obj/set! "fillOpacity" (obj/get svg-styles "fillOpacity"))) + style (-> (obj/get props "style") + (obj/clone) + (obj/merge! svg-styles)) - (and (some? svg-attrs) (obj/contains? svg-attrs "fill")) - (-> styles - (obj/set! "fill" (obj/get svg-attrs "fill")) - (obj/set! "fillOpacity" (obj/get svg-attrs "fillOpacity"))) + url-fill? (or ^boolean (some? (:fill-image shape)) + ^boolean (cfh/image-shape? shape) + ^boolean (> (count shape-fills) 1) + ^boolean (some? (some :fill-color-gradient shape-fills)) + ^boolean (some? (some :fill-image shape-fills))) - ;; If the shape comes from an imported SVG (we know because it has - ;; the :svg-attrs atribute), and it does not have an own fill, we - ;; set a default black fill. This will be inherited by child nodes, - ;; and is for emulating the behavior of standard SVG, in that a node - ;; that has no explicit fill has a default fill of black. - ;; This may be reset to normal if a Penpot frame shape appears below - ;; (see main.ui.shapes.frame/frame-container). - (and (contains? shape :svg-attrs) - (#{:svg-raw :group} (:type shape)) - (empty? (:fills shape))) - (-> styles - (obj/set! "fill" (or (obj/get (:wrapper-styles shape) "fill") clr/black))) + props (if (cfh/frame-shape? shape) + props + (if (or (some? (->> shape-shadow (remove :hidden) seq)) + (and (some? shape-blur) (not ^boolean (:hidden shape-blur)))) + (obj/set! props "filter" (dm/fmt "url(#filter-%)" render-id)) + props))] - (d/not-empty? (:fills shape)) - (add-fill styles (d/without-nils (get-in shape [:fills 0])) render-id 0 (:type shape)) + (cond + ;; If the shape comes from an imported SVG (we know because + ;; it has the :svg-attrs atribute), and it does not have an + ;; own fill, we set a default black fill. This will be + ;; inherited by child nodes, and is for emulating the + ;; behavior of standard SVG, in that a node that has no + ;; explicit fill has a default fill of black. This may be + ;; reset to normal if a Penpot frame shape appears below + ;; (see main.ui.shapes.frame/frame-container). + (and ^boolean (contains? shape :svg-attrs) + ^boolean (or ^boolean (= :svg-raw shape-type) + ^boolean (= :group shape-type)) + ^boolean (empty? shape-fills)) + (let [wstyle (get shape :wrapper-styles) + fill (obj/get wstyle "fill") + fill (d/nilv fill clr/black)] + (obj/set! style "fill" fill)) - :else - styles)] + ^boolean url-fill? + (do + (obj/unset! style "fill") + (obj/unset! style "fillOpacity") + (obj/set! props "fill" (dm/fmt "url(#fill-%-%)" position render-id))) + + (and ^boolean (some? svg-styles) + ^boolean (obj/contains? svg-styles "fill")) + (let [fill (obj/get svg-styles "fill") + opacity (obj/get svg-styles "fillOpacity")] + (when (some? fill) + (obj/set! style "fill" fill)) + (when (some? opacity) + (obj/set! style "fillOpacity" opacity))) + + (and ^boolean (some? svg-attrs) + ^boolean (empty? shape-fills)) + (let [fill (obj/get svg-attrs "fill") + opacity (obj/get svg-attrs "fillOpacity")] + (when (some? fill) + (obj/set! style "fill" fill)) + (when (some? opacity) + (obj/set! style "fillOpacity" opacity))) + + ^boolean (d/not-empty? shape-fills) + (let [fill (nth shape-fills 0) + svg-fill (obj/get svg-attrs "fill") + fill-default (d/nilv svg-fill "none")] + (obj/merge! style (get-fill-style fill render-id 0 shape-type fill-default))) + + (and ^boolean (cfh/path-shape? shape) + ^boolean (empty? shape-fills)) + (obj/set! style "fill" "none")) (-> props (obj/merge! svg-attrs) - (add-border-radius shape) - (obj/set! "style" styles))))) - -(defn extract-style-attrs - ([shape] - (-> (obj/create) - (add-style-attrs shape))) - ([shape render-id] - (-> (obj/create) - (add-style-attrs shape render-id)))) - -(defn extract-fill-attrs - [fill-data render-id index type] - (let [fill-styles (-> (obj/get fill-data "style" (obj/create)) - (add-fill fill-data render-id index type))] - (-> (obj/create) - (obj/set! "style" fill-styles)))) - -(defn extract-stroke-attrs - [stroke-data index render-id] - (let [stroke-styles (-> (obj/get stroke-data "style" (obj/create)) - (add-stroke stroke-data render-id index))] - (-> (obj/create) - (obj/set! "style" stroke-styles)))) - -(defn extract-border-radius-attrs - [shape] - (-> (obj/create) - (add-border-radius shape))) + (obj/set! "style" style))))) diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs index d725a00aeb..29308eebab 100644 --- a/frontend/src/app/main/ui/shapes/bool.cljs +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -8,39 +8,40 @@ (:require [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] - [app.main.ui.hooks :refer [use-equal-memo]] + [app.main.ui.hooks :as h] [app.main.ui.shapes.export :as use] [app.main.ui.shapes.path :refer [path-shape]] - [app.util.object :as obj] [rumext.v2 :as mf])) (defn bool-shape [shape-wrapper] (mf/fnc bool-shape - {::mf/wrap-props false} - [props] - (let [shape (obj/get props "shape") - childs (obj/get props "childs") - childs (use-equal-memo childs) - include-metadata? (mf/use-ctx use/include-metadata-ctx) + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + child-objs (unchecked-get props "childs") + child-objs (h/use-equal-memo child-objs) - bool-content - (mf/use-memo - (mf/deps shape childs) - (fn [] - (cond - (some? (:bool-content shape)) - (:bool-content shape) + metadata? (mf/use-ctx use/include-metadata-ctx) + content (mf/with-memo [shape child-objs] + (let [content (:bool-content shape)] + (cond + (some? content) + content - (some? childs) - (gsh/calc-bool-content shape childs))))] + (some? child-objs) + (gsh/calc-bool-content shape child-objs)))) - [:* - (when (some? bool-content) - [:& path-shape {:shape (assoc shape :content bool-content)}]) + shape (mf/with-memo [shape content] + (assoc shape :content content))] - (when include-metadata? - [:> "penpot:bool" {} - (for [item (->> (:shapes shape) (mapv #(get childs %)))] - [:& shape-wrapper {:shape item - :key (dm/str (:id item))}])])]))) + [:* + (when (some? content) + [:& path-shape {:shape shape}]) + + (when metadata? + [:> "penpot:bool" {} + (for [item (map #(get child-objs %) (:shapes shape))] + [:& shape-wrapper + {:shape item + :key (dm/str (dm/get-prop item :id))}])])]))) diff --git a/frontend/src/app/main/ui/shapes/circle.cljs b/frontend/src/app/main/ui/shapes/circle.cljs index 4501098ba0..f241a05f1e 100644 --- a/frontend/src/app/main/ui/shapes/circle.cljs +++ b/frontend/src/app/main/ui/shapes/circle.cljs @@ -6,8 +6,8 @@ (ns app.main.ui.shapes.circle (:require + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] - [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]] [app.util.object :as obj] [rumext.v2 :as mf])) @@ -16,21 +16,22 @@ {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - {:keys [x y width height]} shape - transform (gsh/transform-str shape) - cx (+ x (/ width 2)) - cy (+ y (/ height 2)) - rx (/ width 2) - ry (/ height 2) + x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + w (dm/get-prop shape :width) + h (dm/get-prop shape :height) - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:cx cx - :cy cy - :rx rx - :ry ry - :transform transform}))] + t (gsh/transform-str shape) + + cx (+ x (/ w 2)) + cy (+ y (/ h 2)) + rx (/ w 2) + ry (/ h 2) + + props (mf/with-memo [shape] + (-> #js {} + (obj/merge! #js {:cx cx :cy cy :rx rx :ry ry :transform t})))] [:& shape-custom-strokes {:shape shape} [:> :ellipse props]])) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index a7b1425de2..35d2bd7f0d 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -8,9 +8,12 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.bounds :as gsb] - [app.common.pages.helpers :as cph] + [app.common.geom.shapes.text :as gst] + [app.config :as cf] [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.gradients :as grad] @@ -18,187 +21,249 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) -(defn add-props - [props new-props] - (-> props - (obj/merge (clj->js new-props)))) - -(defn add-style - [props new-style] - (let [old-style (obj/get props "style") - style (obj/merge old-style (clj->js new-style))] - (-> props (obj/merge #js {:style style})))) +;; FIXME: this clearly should be renamed to something different, this +;; namespace has also fill related code (mf/defc inner-stroke-clip-path + {::mf/wrap-props false} [{:keys [shape render-id index]}] - (let [suffix (if index (str "-" index) "") - clip-id (str "inner-stroke-" render-id "-" (:id shape) suffix) - shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix)] - [:> "clipPath" #js {:id clip-id} - [:use {:href (str "#" shape-id)}]])) + (let [shape-id (dm/get-prop shape :id) + suffix (if (some? index) (dm/str "-" index) "") + clip-id (dm/str "inner-stroke-" render-id "-" shape-id suffix) + href (dm/str "#stroke-shape-" render-id "-" shape-id suffix)] + [:> "clipPath" {:id clip-id} + [:use {:href href}]])) (mf/defc outer-stroke-mask + {::mf/wrap-props false} [{:keys [shape stroke render-id index]}] - (let [suffix (if index (str "-" index) "") - stroke-mask-id (str "outer-stroke-" render-id "-" (:id shape) suffix) - shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix) - stroke-width (case (:stroke-alignment stroke :center) - :center (/ (:stroke-width stroke 0) 2) - :outer (:stroke-width stroke 0) - 0) - margin (gsb/shape-stroke-margin stroke stroke-width) + (let [shape-id (dm/get-prop shape :id) + suffix (if (some? index) (dm/str "-" index) "") + mask-id (dm/str "outer-stroke-" render-id "-" shape-id suffix) + shape-id (dm/str "stroke-shape-" render-id "-" shape-id suffix) + href (dm/str "#" shape-id) - selrect - (if (cph/text-shape? shape) - (gsh/position-data-selrect shape) - (gsh/points->selrect (:points shape))) + stroke-width (case (:stroke-alignment stroke :center) + :center (/ (:stroke-width stroke 0) 2) + :outer (:stroke-width stroke 0) + 0) + stroke-margin (gsb/shape-stroke-margin shape stroke-width) - bounding-box - (-> selrect - (update :x - (+ stroke-width margin)) - (update :y - (+ stroke-width margin)) - (update :width + (* 2 (+ stroke-width margin))) - (update :height + (* 2 (+ stroke-width margin))))] + ;; NOTE: for performance reasons we may can delimit a bit the + ;; dependencies to really useful shape attrs instead of using + ;; the shepe as-is. + selrect (mf/with-memo [shape] + (if (cfh/text-shape? shape) + (gst/shape->rect shape) + (grc/points->rect (:points shape)))) - [:mask {:id stroke-mask-id - :x (:x bounding-box) - :y (:y bounding-box) - :width (:width bounding-box) - :height (:height bounding-box) + x (- (dm/get-prop selrect :x) stroke-margin) + y (- (dm/get-prop selrect :y) stroke-margin) + w (+ (dm/get-prop selrect :width) (* 2 stroke-margin)) + h (+ (dm/get-prop selrect :height) (* 2 stroke-margin))] + + [:mask {:id mask-id + :x x + :y y + :width w + :height h :maskUnits "userSpaceOnUse"} - [:use {:href (str "#" shape-id) - :style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}] + [:use + {:href href + :style {:fill "none" + :stroke "white" + :strokeWidth (* stroke-width 2)}}] - [:use {:href (str "#" shape-id) - :style #js {:fill "black" - :stroke "none"}}]])) + [:use + {:href href + :style {:fill "black" + :stroke "none"}}]])) (mf/defc cap-markers + {::mf/wrap-props false} [{:keys [stroke render-id index]}] - (let [marker-id-prefix (str "marker-" render-id) + (let [id-prefix (dm/str "marker-" render-id) + + gradient (:stroke-color-gradient stroke) + image (:stroke-image stroke) cap-start (:stroke-cap-start stroke) cap-end (:stroke-cap-end stroke) - stroke-color (if (:stroke-color-gradient stroke) - (str/format "url(#%s)" (str "stroke-color-gradient_" render-id "_" index)) - (:stroke-color stroke)) + color (cond + (some? gradient) + (str/ffmt "url(#stroke-color-gradient-%s-%s)" render-id index) - stroke-opacity (when-not (:stroke-color-gradient stroke) - (:stroke-opacity stroke))] + (some? image) + (str/ffmt "url(#stroke-fill-%-%)" render-id index) + + :else + (:stroke-color stroke)) + + opacity (when-not (some? gradient) + (:stroke-opacity stroke))] [:* - (when (or (= cap-start :line-arrow) (= cap-end :line-arrow)) - [:marker {:id (str marker-id-prefix "-line-arrow") - :viewBox "0 0 3 6" - :refX "2" - :refY "3" - :markerWidth "8.5" - :markerHeight "8.5" - :orient "auto-start-reverse" - :fill stroke-color - :fillOpacity stroke-opacity} - [:path {:d "M 0.5 0.5 L 3 3 L 0.5 5.5 L 0 5 L 2 3 L 0 1 z"}]]) + (when (or (= cap-start :line-arrow) + (= cap-end :line-arrow)) + [:marker {:id (dm/str id-prefix "-line-arrow") + :viewBox "0 0 3 6" + :refX "2" + :refY "3" + :markerWidth "8.5" + :markerHeight "8.5" + :orient "auto-start-reverse" + :fill color + :fillOpacity opacity} + [:path {:d "M 0.5 0.5 L 3 3 L 0.5 5.5 L 0 5 L 2 3 L 0 1 z"}]]) - (when (or (= cap-start :triangle-arrow) (= cap-end :triangle-arrow)) - [:marker {:id (str marker-id-prefix "-triangle-arrow") - :viewBox "0 0 3 6" - :refX "2" - :refY "3" - :markerWidth "8.5" - :markerHeight "8.5" - :orient "auto-start-reverse" - :fill stroke-color - :fillOpacity stroke-opacity} - [:path {:d "M 0 0 L 3 3 L 0 6 z"}]]) + (when (or (= cap-start :triangle-arrow) + (= cap-end :triangle-arrow)) + [:marker {:id (dm/str id-prefix "-triangle-arrow") + :viewBox "0 0 3 6" + :refX "2" + :refY "3" + :markerWidth "8.5" + :markerHeight "8.5" + :orient "auto-start-reverse" + :fill color + :fillOpacity opacity} + [:path {:d "M 0 0 L 3 3 L 0 6 z"}]]) - (when (or (= cap-start :square-marker) (= cap-end :square-marker)) - [:marker {:id (str marker-id-prefix "-square-marker") - :viewBox "0 0 6 6" - :refX "3" - :refY "3" - :markerWidth "4.2426" ;; diagonal length of a 3x3 square - :markerHeight "4.2426" - :orient "auto-start-reverse" - :fill stroke-color - :fillOpacity stroke-opacity} - [:rect {:x 0 :y 0 :width 6 :height 6}]]) + (when (or (= cap-start :square-marker) + (= cap-end :square-marker)) + [:marker {:id (dm/str id-prefix "-square-marker") + :viewBox "0 0 6 6" + :refX "3" + :refY "3" + :markerWidth "4.2426" ;; diagonal length of a 3x3 square + :markerHeight "4.2426" + :orient "auto-start-reverse" + :fill color + :fillOpacity opacity} + [:rect {:x 0 :y 0 :width 6 :height 6}]]) - (when (or (= cap-start :circle-marker) (= cap-end :circle-marker)) - [:marker {:id (str marker-id-prefix "-circle-marker") - :viewBox "0 0 6 6" - :refX "3" - :refY "3" - :markerWidth "4" - :markerHeight "4" - :orient "auto-start-reverse" - :fill stroke-color - :fillOpacity stroke-opacity} - [:circle {:cx "3" :cy "3" :r "3"}]]) + (when (or (= cap-start :circle-marker) + (= cap-end :circle-marker)) + [:marker {:id (dm/str id-prefix "-circle-marker") + :viewBox "0 0 6 6" + :refX "3" + :refY "3" + :markerWidth "4" + :markerHeight "4" + :orient "auto-start-reverse" + :fill color + :fillOpacity opacity} + [:circle {:cx "3" :cy "3" :r "3"}]]) - (when (or (= cap-start :diamond-marker) (= cap-end :diamond-marker)) - [:marker {:id (str marker-id-prefix "-diamond-marker") - :viewBox "0 0 6 6" - :refX "3" - :refY "3" - :markerWidth "6" - :markerHeight "6" - :orient "auto-start-reverse" - :fill stroke-color - :fillOpacity stroke-opacity} - [:path {:d "M 3 0 L 6 3 L 3 6 L 0 3 z"}]]) + (when (or (= cap-start :diamond-marker) + (= cap-end :diamond-marker)) + [:marker {:id (dm/str id-prefix "-diamond-marker") + :viewBox "0 0 6 6" + :refX "3" + :refY "3" + :markerWidth "6" + :markerHeight "6" + :orient "auto-start-reverse" + :fill color + :fillOpacity opacity} + [:path {:d "M 3 0 L 6 3 L 3 6 L 0 3 z"}]]) ;; If the user wants line caps but different in each end, ;; simulate it with markers. - (when (and (or (= cap-start :round) (= cap-end :round)) - (not= cap-start cap-end)) - [:marker {:id (str marker-id-prefix "-round") - :viewBox "0 0 6 6" - :refX "3" - :refY "3" - :markerWidth "6" - :markerHeight "6" - :orient "auto-start-reverse" - :fill stroke-color - :fillOpacity stroke-opacity} - [:path {:d "M 3 2.5 A 0.5 0.5 0 0 1 3 3.5 "}]]) + (when (and (or (= cap-start :round) + (= cap-end :round)) + (not= cap-start cap-end)) + [:marker {:id (dm/str id-prefix "-round") + :viewBox "0 0 6 6" + :refX "3" + :refY "3" + :markerWidth "6" + :markerHeight "6" + :orient "auto-start-reverse" + :fill color + :fillOpacity opacity} + [:path {:d "M 3 2.5 A 0.5 0.5 0 0 1 3 3.5 "}]]) - (when (and (or (= cap-start :square) (= cap-end :square)) - (not= cap-start cap-end)) - [:marker {:id (str marker-id-prefix "-square") - :viewBox "0 0 6 6" - :refX "3" - :refY "3" - :markerWidth "6" - :markerHeight "6" - :orient "auto-start-reverse" - :fill stroke-color - :fillOpacity stroke-opacity} - [:rect {:x 3 :y 2.5 :width 0.5 :height 1}]])])) + (when (and (or (= cap-start :square) + (= cap-end :square)) + (not= cap-start cap-end)) + [:marker {:id (dm/str id-prefix "-square") + :viewBox "0 0 6 6" + :refX "3" + :refY "3" + :markerWidth "6" + :markerHeight "6" + :orient "auto-start-reverse" + :fill color + :fillOpacity opacity} + [:rect {:x 3 :y 2.5 :width 0.5 :height 1}]])])) (mf/defc stroke-defs + {::mf/wrap-props false} [{:keys [shape stroke render-id index]}] + (let [open-path? (and ^boolean (cfh/path-shape? shape) + ^boolean (gsh/open-path? shape)) + gradient (:stroke-color-gradient stroke) + alignment (:stroke-alignment stroke :center) + width (:stroke-width stroke 0) - (let [open-path? (and (= :path (:type shape)) (gsh/open-path? shape))] + props #js {:id (dm/str "stroke-color-gradient-" render-id "-" index) + :gradient gradient + :shape shape + :force-transform (cfh/path-shape? shape)} + stroke-image (:stroke-image stroke) + uri (when stroke-image (cf/resolve-file-media stroke-image)) + + stroke-width (case (:stroke-alignment stroke :center) + :center (/ (:stroke-width stroke 0) 2) + :outer (:stroke-width stroke 0) + 0) + margin (gsb/shape-stroke-margin stroke stroke-width) + + selrect (mf/with-memo [shape] + (if (cfh/text-shape? shape) + (gst/shape->rect shape) + (grc/points->rect (:points shape)))) + + stroke-margin (+ stroke-width margin) + + w (+ (dm/get-prop selrect :width) (* 2 stroke-margin)) + h (+ (dm/get-prop selrect :height) (* 2 stroke-margin)) + image-props #js {:href uri + :preserveAspectRatio "xMidYMid slice" + :width 1 + :height 1 + :id (dm/str "stroke-image-" render-id "-" index)}] [:* - (cond (some? (:stroke-color-gradient stroke)) - (case (:type (:stroke-color-gradient stroke)) - :linear [:> grad/linear-gradient #js {:id (str (name :stroke-color-gradient) "_" render-id "_" index) - :gradient (:stroke-color-gradient stroke) - :shape shape}] - :radial [:> grad/radial-gradient #js {:id (str (name :stroke-color-gradient) "_" render-id "_" index) - :gradient (:stroke-color-gradient stroke) - :shape shape}])) + (when (some? gradient) + (case (:type gradient) + :linear [:> grad/linear-gradient props] + :radial [:> grad/radial-gradient props])) + + (when (:stroke-image stroke) + ;; We need to make the pattern size and the image fit so it's not repeated + [:pattern {:id (dm/str "stroke-fill-" render-id "-" index) + :patternContentUnits "objectBoundingBox" + :x (- (/ stroke-margin (dm/get-prop selrect :width))) + :y (- (/ stroke-margin (dm/get-prop selrect :height))) + :width (/ w (dm/get-prop selrect :width)) + :height (/ h (dm/get-prop selrect :height)) + :viewBox "0 0 1 1" + :preserveAspectRatio "xMidYMid slice" + :patternTransform (when (cfh/path-shape? shape) (gsh/transform-str shape))} + [:> :image image-props]]) + (cond (and (not open-path?) - (= :inner (:stroke-alignment stroke :center)) - (> (:stroke-width stroke 0) 0)) + (= :inner alignment) + (> width 0)) [:& inner-stroke-clip-path {:shape shape :render-id render-id :index index}] (and (not open-path?) - (= :outer (:stroke-alignment stroke :center)) - (> (:stroke-width stroke 0) 0)) + (= :outer alignment) + (> width 0)) [:& outer-stroke-mask {:shape shape :stroke stroke :render-id render-id @@ -210,119 +275,135 @@ :render-id render-id :index index}])])) -;; Outer alignment: display the shape in two layers. One -;; without stroke (only fill), and another one only with stroke -;; at double width (transparent fill) and passed through a mask -;; that shows the whole shape, but hides the original shape -;; without stroke +;; Outer alignment: display the shape in two layers. One without +;; stroke (only fill), and another one only with stroke at double +;; width (transparent fill) and passed through a mask that shows the +;; whole shape, but hides the original shape without stroke + (mf/defc outer-stroke {::mf/wrap-props false} - [props] + [{:keys [children shape stroke index]}] + (let [shape-id (dm/get-prop shape :id) + render-id (mf/use-ctx muc/render-id) - (let [render-id (mf/use-ctx muc/render-id) - child (obj/get props "children") - base-props (obj/get child "props") - elem-name (obj/get child "type") - shape (obj/get props "shape") - stroke (obj/get props "stroke") - index (obj/get props "index") - stroke-width (:stroke-width stroke) + props (obj/get children "props") + style (obj/get props "style") - suffix (if index (str "-" index) "") - stroke-mask-id (str "outer-stroke-" render-id "-" (:id shape) suffix) - shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix)] + stroke-width (:stroke-width stroke 0) + + suffix (if (some? index) (dm/str "-" index) "") + mask-id (dm/str "outer-stroke-" render-id "-" shape-id suffix) + shape-id (dm/str "stroke-shape-" render-id "-" shape-id suffix) + href (dm/str "#" shape-id)] [:g.outer-stroke-shape [:defs [:& stroke-defs {:shape shape :stroke stroke :render-id render-id :index index}] - [:> elem-name (-> (obj/clone base-props) - (obj/set! "id" shape-id) - (obj/set! - "style" - (-> (obj/get base-props "style") - (obj/clone) - (obj/without ["fill" "fillOpacity" "stroke" "strokeWidth" "strokeOpacity" "strokeStyle" "strokeDasharray"]))))]] + (let [type (obj/get children "type") + style (-> (obj/clone style) + (obj/unset! "fill") + (obj/unset! "fillOpacity") + (obj/unset! "stroke") + (obj/unset! "strokeWidth") + (obj/unset! "strokeOpacity") + (obj/unset! "strokeStyle") + (obj/unset! "strokeDasharray")) + props (-> (obj/clone props) + (obj/set! "id" shape-id) + (obj/set! "style" style))] - [:use {:href (str "#" shape-id) - :mask (str "url(#" stroke-mask-id ")") - :style (-> (obj/get base-props "style") - (obj/clone) + [:> type props])] + + [:use {:href href + :mask (dm/str "url(#" mask-id ")") + :style (-> (obj/clone style) (obj/set! "strokeWidth" (* stroke-width 2)) - (obj/without ["fill" "fillOpacity"]) - (obj/set! "fill" "none"))}] + (obj/set! "fill" "none") + (obj/unset! "fillOpacity"))}] - [:use {:href (str "#" shape-id) - :style (-> (obj/get base-props "style") - (obj/clone) + [:use {:href href + :style (-> (obj/clone style) (obj/set! "stroke" "none"))}]])) -;; Inner alignment: display the shape with double width stroke, -;; and clip the result with the original shape without stroke. +;; Inner alignment: display the shape with double width stroke, and +;; clip the result with the original shape without stroke. + (mf/defc inner-stroke {::mf/wrap-props false} [props] - (let [render-id (mf/use-ctx muc/render-id) - child (obj/get props "children") - base-props (obj/get child "props") - elem-name (obj/get child "type") - shape (obj/get props "shape") - stroke (obj/get props "stroke") - index (obj/get props "index") - transform (obj/get base-props "transform") + (let [child (unchecked-get props "children") + shape (unchecked-get props "shape") + stroke (unchecked-get props "stroke") + index (unchecked-get props "index") + + shape-id (dm/get-prop shape :id) + render-id (mf/use-ctx muc/render-id) + + type (obj/get child "type") + + props (-> (obj/get child "props") obj/clone) + ;; FIXME: check if style need to be cloned + style (-> (obj/get props "style") obj/clone) + transform (obj/get props "transform") stroke-width (:stroke-width stroke 0) - suffix (if index (str "-" index) "") - clip-id (str "inner-stroke-" render-id "-" (:id shape) suffix) - shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix) + suffix (if (some? index) (dm/str "-" index) "") + clip-id (dm/str "inner-stroke-" render-id "-" shape-id suffix) + shape-id (dm/str "stroke-shape-" render-id "-" shape-id suffix) + clip-path (dm/str "url('#" clip-id "')") - clip-path (str "url('#" clip-id "')") - shape-props (-> base-props - (add-props {:id shape-id - :transform nil}) - (add-style {:strokeWidth (* stroke-width 2)}))] + style (obj/set! style "strokeWidth" (* stroke-width 2)) - [:g.inner-stroke-shape {:transform transform} + props (-> props + (obj/set! "id" (dm/str shape-id)) + (obj/set! "style" style) + (obj/unset! "transform"))] + + [:g.inner-stroke-shape + {:transform transform} [:defs [:& stroke-defs {:shape shape :stroke stroke :render-id render-id :index index}] - [:> elem-name shape-props]] + [:> type props]] - [:use {:href (str "#" shape-id) + [:use {:href (dm/str "#" shape-id) :clipPath clip-path}]])) -; The SVG standard does not implement yet the 'stroke-alignment' -; attribute, to define the position of the stroke relative to the -; stroke axis (inner, center, outer). Here we implement a patch to be -; able to draw the stroke in the three cases. See discussion at: -; https://stackoverflow.com/questions/7241393/can-you-control-how-an-svgs-stroke-width-is-drawn +;; The SVG standard does not implement yet the 'stroke-alignment' +;; attribute, to define the position of the stroke relative to the +;; stroke axis (inner, center, outer). Here we implement a patch to be +;; able to draw the stroke in the three cases. See discussion at: +;; https://stackoverflow.com/questions/7241393/can-you-control-how-an-svgs-stroke-width-is-drawn + (mf/defc shape-custom-stroke {::mf/wrap-props false} [props] + (let [child (unchecked-get props "children") + shape (unchecked-get props "shape") + stroke (unchecked-get props "stroke") + index (unchecked-get props "index") - (let [child (obj/get props "children") - shape (obj/get props "shape") - stroke (obj/get props "stroke") + render-id (mf/use-ctx muc/render-id) + render-id (d/nilv (unchecked-get props "render-id") render-id) - render-id (mf/use-ctx muc/render-id) - index (obj/get props "index") - stroke-width (:stroke-width stroke 0) - stroke-style (:stroke-style stroke :none) + stroke-width (:stroke-width stroke 0) + stroke-style (:stroke-style stroke :none) stroke-position (:stroke-alignment stroke :center) - has-stroke? (and (> stroke-width 0) - (not= stroke-style :none)) - closed? (or (not= :path (:type shape)) (not (gsh/open-path? shape))) - inner? (= :inner stroke-position) - outer? (= :outer stroke-position)] + + has-stroke? (and (> stroke-width 0) + (not= stroke-style :none)) + closed? (or (not ^boolean (cfh/path-shape? shape)) + (not ^boolean (gsh/open-path? shape))) + inner? (= :inner stroke-position) + outer? (= :outer stroke-position)] (cond (and has-stroke? inner? closed?) - [:& inner-stroke {:shape shape :stroke stroke :index index} - child] + [:& inner-stroke {:shape shape :stroke stroke :index index} child] (and has-stroke? outer? closed?) - [:& outer-stroke {:shape shape :stroke stroke :index index} - child] + [:& outer-stroke {:shape shape :stroke stroke :index index} child] :else [:g.stroke-shape @@ -330,145 +411,104 @@ [:& stroke-defs {:shape shape :stroke stroke :render-id render-id :index index}]] child]))) -(defn build-fill-props [shape child position render-id] - (let [url-fill? (or (some? (:fill-image shape)) - (= :image (:type shape)) - (> (count (:fills shape)) 1) - (some :fill-color-gradient (:fills shape))) +(defn- build-fill-element + [shape child position render-id] + (let [type (obj/get child "type") + props (-> (obj/get child "props") + (obj/clone)) + props (attrs/add-fill-props! props shape position render-id)] + (mf/html [:> type props]))) - props (cond-> (obj/create) - (or - ;; There are any shadows - (and (seq (->> (:shadow shape) (remove :hidden))) (not (cph/frame-shape? shape))) - ;; There is a blur - (and (:blur shape) (-> shape :blur :hidden not) (not (cph/frame-shape? shape)))) - (obj/set! "filter" (dm/fmt "url(#filter_%)" render-id))) +(defn- build-stroke-element + [child value position render-id open-path?] + (let [props (obj/get child "props") + type (obj/get child "type") - svg-defs (:svg-defs shape {}) - svg-attrs (:svg-attrs shape {}) - - [svg-attrs svg-styles] - (attrs/extract-svg-attrs render-id svg-defs svg-attrs)] - - (cond - url-fill? - (let [props (obj/set! props - "style" - (-> (obj/get child "props") - (obj/get "style") - (obj/clone) - (obj/without ["fill" "fillOpacity"])))] - (obj/set! props "fill" (dm/fmt "url(#fill-%-%)" position render-id))) - - (and (some? svg-styles) (obj/contains? svg-styles "fill")) - (let [style - (-> (obj/get child "props") - (obj/get "style") - (obj/clone) - (obj/set! "fill" (obj/get svg-styles "fill")) - (obj/set! "fillOpacity" (obj/get svg-styles "fillOpacity")))] - (-> props - (obj/set! "style" style))) - - (and (some? svg-attrs) (empty? (:fills shape))) - (let [style - (-> (obj/get child "props") - (obj/get "style") - (obj/clone)) - - style (-> style - (obj/set! "fill" (obj/get svg-attrs "fill")) - (obj/set! "fillOpacity" (obj/get svg-attrs "fillOpacity")))] - (-> props - (obj/set! "style" style))) - - (d/not-empty? (:fills shape)) - (let [fill-props - (attrs/extract-fill-attrs (get-in shape [:fills 0]) render-id 0 (:type shape)) - - style (-> (obj/get child "props") - (obj/get "style") - (obj/clone) - (obj/merge! (obj/get fill-props "style")))] - - (cond-> (obj/merge! props fill-props) - (some? style) - (obj/set! "style" style))) - - (and (= :path (:type shape)) (empty? (:fills shape))) - (let [style - (-> (obj/get child "props") - (obj/get "style") - (obj/clone) - (obj/set! "fill" "none"))] - (-> props - (obj/set! "style" style))) - - :else - (obj/create)))) - -(defn build-stroke-props [position child value render-id] - (let [props (-> (obj/get child "props") + style (-> (obj/get props "style") (obj/clone) - (obj/without ["fill" "fillOpacity"]))] - (-> props - (obj/set! - "style" - (-> (obj/get props "style") - (obj/set! "fill" "none") - (obj/set! "fillOpacity" "none"))) - (add-style (obj/get (attrs/extract-stroke-attrs value position render-id) "style"))))) + (obj/set! "fill" "none") + (obj/set! "fillOpacity" "none") + (attrs/add-stroke! value render-id position open-path?)) + + style (if (:stroke-image value) + (obj/set! style "stroke" (dm/fmt "url(#stroke-fill-%-%)" render-id position)) + style) + + props (-> (obj/clone props) + (obj/unset! "fill") + (obj/unset! "fillOpacity") + (obj/set! "style" style))] + + (mf/html [:> type props]))) (mf/defc shape-fills {::mf/wrap-props false} [props] - (let [child (obj/get props "children") - shape (obj/get props "shape") - elem-name (obj/get child "type") - position (or (obj/get props "position") 0) - render-id (or (obj/get props "render-id") (mf/use-ctx muc/render-id)) - fill-props (build-fill-props shape child position render-id)] - [:g.fills {:id (dm/fmt "fills-%" (:id shape))} - [:> elem-name (-> (obj/get child "props") - (obj/clone) - (obj/merge! fill-props))]])) + (let [child (unchecked-get props "children") + shape (unchecked-get props "shape") + + shape-id (dm/get-prop shape :id) + + position (d/nilv (unchecked-get props "position") 0) + + render-id (mf/use-ctx muc/render-id) + render-id (d/nilv (unchecked-get props "render-id") render-id)] + + [:g.fills {:id (dm/fmt "fills-%" shape-id)} + (build-fill-element shape child position render-id)])) (mf/defc shape-strokes {::mf/wrap-props false} [props] - (let [child (obj/get props "children") - shape (obj/get props "shape") + (let [child (unchecked-get props "children") + shape (unchecked-get props "shape") - elem-name (obj/get child "type") - render-id (or (obj/get props "render-id") (mf/use-ctx muc/render-id)) - stroke-id (dm/fmt "strokes-%" (:id shape)) - stroke-props (-> (obj/create) - (obj/set! "id" stroke-id) - (obj/set! "className" "strokes") - (cond-> - ;; There is a blur - (and (:blur shape) (not (cph/frame-shape? shape)) (-> shape :blur :hidden not)) - (obj/set! "filter" (dm/fmt "url(#filter_blur_%)" render-id)) + shape-id (dm/get-prop shape :id) - ;; There are any shadows and no fills - (and (empty? (:fills shape)) (not (cph/frame-shape? shape)) (seq (->> (:shadow shape) (remove :hidden)))) - (obj/set! "filter" (dm/fmt "url(#filter_%)" render-id))))] - [:* - (when - (d/not-empty? (:strokes shape)) - [:> :g stroke-props - (for [[index value] (-> (d/enumerate (:strokes shape)) reverse)] - (let [props (build-stroke-props index child value render-id)] - [:& shape-custom-stroke {:shape shape :stroke value :index index :key (dm/str index "-" stroke-id)} - [:> elem-name props]]))])])) + render-id (mf/use-ctx muc/render-id) + render-id (d/nilv (unchecked-get props "render-id") render-id) + + stroke-id (dm/fmt "strokes-%" shape-id) + + shape-blur (get shape :blur) + shape-fills (get shape :fills) + shape-shadow (get shape :shadow) + shape-strokes (not-empty (get shape :strokes)) + + svg-attrs (attrs/get-svg-props shape render-id) + + style (-> (obj/get props "style") + (obj/clone) + (obj/merge! (obj/get svg-attrs "style"))) + + props (mf/spread-props svg-attrs + {:id stroke-id + :className "strokes" + :style style}) + + open-path? (and ^boolean (cfh/path-shape? shape) + ^boolean (gsh/open-path? shape))] + (when-not ^boolean (cfh/frame-shape? shape) + (when (and (some? shape-blur) + (not ^boolean (:hidden shape-blur))) + (obj/set! props "filter" (dm/fmt "url(#filter-blur-%)" render-id))) + + (when (and (empty? shape-fills) + (some? (->> shape-shadow (remove :hidden) not-empty))) + (obj/set! props "filter" (dm/fmt "url(#filter-%)" render-id)))) + + (when (some? shape-strokes) + [:> :g props + (for [[index value] (reverse (d/enumerate shape-strokes))] + [:& shape-custom-stroke {:shape shape + :stroke value + :index index + :key (dm/str index "-" stroke-id)} + (build-stroke-element child value index render-id open-path?)])]))) (mf/defc shape-custom-strokes {::mf/wrap-props false} [props] - (let [children (obj/get props "children") - shape (obj/get props "shape") - position (obj/get props "position") - render-id (obj/get props "render-id")] - [:* - [:& shape-fills {:shape shape :position position :render-id render-id} children] - [:& shape-strokes {:shape shape :position position :render-id render-id} children]])) + [:* + [:> shape-fills props] + [:> shape-strokes props]]) diff --git a/frontend/src/app/main/ui/shapes/embed.cljs b/frontend/src/app/main/ui/shapes/embed.cljs index 8d0f04b399..218b9687b8 100644 --- a/frontend/src/app/main/ui/shapes/embed.cljs +++ b/frontend/src/app/main/ui/shapes/embed.cljs @@ -8,7 +8,7 @@ (:require [app.main.ui.hooks :as hooks] [app.util.http :as http] - [beicon.core :as rx] + [beicon.v2.core :as rx] [rumext.v2 :as mf])) (def context (mf/create-context false)) @@ -37,10 +37,10 @@ (rx/filter some?) (url-mapping) (rx/reduce conj {}) - (rx/subs (fn [data] - (when-not (= data (mf/ref-val uri-data)) - (mf/set-ref-val! uri-data data) - (reset! state inc)))))] + (rx/subs! (fn [data] + (when-not (= data (mf/ref-val uri-data)) + (mf/set-ref-val! uri-data data) + (reset! state inc)))))] #(when sub (rx/dispose! sub))))) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index 00de59a7a6..d10378e190 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -10,24 +10,28 @@ importation." (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] + [app.common.svg :as csvg] [app.main.ui.context :as muc] [app.util.json :as json] [app.util.object :as obj] - [app.util.svg :as usvg] [cuerdas.core :as str] [rumext.v2 :as mf])) -(def include-metadata-ctx (mf/create-context false)) +(def ^:private internal-counter (atom 0)) + +(def include-metadata-ctx + (mf/create-context false)) (mf/defc render-xml [{{:keys [tag attrs content] :as node} :xml}] (cond (map? node) - [:> (d/name tag) (clj->js (usvg/clean-attrs attrs)) + [:> (d/name tag) (obj/map->obj (csvg/attrs->props attrs)) (for [child content] - [:& render-xml {:xml child}])] + [:& render-xml {:xml child :key (swap! internal-counter inc)}])] (string? node) node @@ -70,9 +74,9 @@ image? (= :image (:type shape)) text? (= :text (:type shape)) path? (= :path (:type shape)) - mask? (and group? (:masked-group? shape)) + mask? (and group? (:masked-group shape)) bool? (= :bool (:type shape)) - center (gsh/center-shape shape)] + center (gsh/shape->center shape)] (-> props (add! :name) (add! :blocked) @@ -100,6 +104,9 @@ (-> (add! :show-content) (add! :hide-in-viewer))) + (cond-> (and frame? (:use-for-thumbnail shape)) + (add! :use-for-thumbnail)) + (cond-> (and (or rect? image? frame?) (some? (:r1 shape))) (-> (add! :r1) (add! :r2) @@ -137,8 +144,8 @@ (add! :typography-ref-file) (add! :component-file) (add! :component-id) - (add! :component-root?) - (add! :main-instance?) + (add! :component-root) + (add! :main-instance) (add! :shape-ref)))) (defn prefix-keys [m] @@ -165,10 +172,10 @@ (mf/defc export-flows [{:keys [flows]}] [:> "penpot:flows" #js {} - (for [{:keys [id name starting-frame]} flows] - [:> "penpot:flow" #js {:id id - :name name - :starting-frame starting-frame}])]) + (for [{:keys [id name starting-frame]} flows] + [:> "penpot:flow" #js {:id id + :name name + :starting-frame starting-frame}])]) (mf/defc export-guides [{:keys [guides]}] @@ -200,6 +207,7 @@ (for [{:keys [style hidden color offset-x offset-y blur spread]} shadow] [:> "penpot:shadow" #js {:penpot:shadow-type (d/name style) + :key (swap! internal-counter inc) :penpot:hidden (str hidden) :penpot:color (str (:color color)) :penpot:opacity (str (:opacity color)) @@ -221,6 +229,7 @@ (for [{:keys [scale suffix type]} exports] [:> "penpot:export" #js {:penpot:type (d/name type) + :key (swap! internal-counter inc) :penpot:suffix suffix :penpot:scale (str scale)}]))) @@ -247,7 +256,7 @@ [:* (when (contains? shape :svg-attrs) (let [svg-transform (get shape :svg-transform) - svg-attrs (->> shape :svg-attrs keys (mapv d/name) (str/join ",") ) + svg-attrs (->> shape :svg-attrs keys (mapv d/name) (str/join ",")) svg-defs (->> shape :svg-defs keys (mapv d/name) (str/join ","))] [:> "penpot:svg-import" #js {:penpot:svg-attrs (when-not (empty? svg-attrs) svg-attrs) @@ -262,7 +271,8 @@ :penpot:svg-viewbox-width (get-in shape [:svg-viewbox :width]) :penpot:svg-viewbox-height (get-in shape [:svg-viewbox :height])} (for [[def-id def-xml] (:svg-defs shape)] - [:> "penpot:svg-def" #js {:def-id def-id} + [:> "penpot:svg-def" #js {:def-id def-id + :key (swap! internal-counter inc)} [:& render-xml {:xml def-xml}]])])) (when (= (:type shape) :svg-raw) @@ -278,40 +288,54 @@ (clj->js))))] [:> "penpot:svg-content" props (for [leaf (->> shape :content :content (filter string?))] - [:> "penpot:svg-child" {} leaf])]))])) + [:> "penpot:svg-child" {:key (swap! internal-counter inc)} leaf])]))])) (defn- export-fills-data [{:keys [fills]}] - (when-let [fills (seq fills)] - (mf/html - [:> "penpot:fills" #js {} - (for [[index fill] (d/enumerate fills)] - [:> "penpot:fill" - #js {:penpot:fill-color (if (some? (:fill-color-gradient fill)) - (str/format "url(#%s)" (str "fill-color-gradient_" (mf/use-ctx muc/render-id) "_" index)) - (d/name (:fill-color fill))) - :penpot:fill-color-ref-file (d/name (:fill-color-ref-file fill)) - :penpot:fill-color-ref-id (d/name (:fill-color-ref-id fill)) - :penpot:fill-opacity (d/name (:fill-opacity fill))}])]))) + (when-let [fills (seq fills)] + (let [render-id (mf/use-ctx muc/render-id)] + (mf/html + [:> "penpot:fills" #js {} + (for [[index fill] (d/enumerate fills)] + (let [fill-image-id (dm/str "fill-image-" render-id "-" index)] + [:> "penpot:fill" + #js {:penpot:fill-color (cond + (some? (:fill-color-gradient fill)) + (str/format "url(#%s)" (str "fill-color-gradient-" render-id "-" index)) + + :else + (d/name (:fill-color fill))) + :key (swap! internal-counter inc) + + :penpot:fill-image-id (when (:fill-image fill) fill-image-id) + :penpot:fill-color-ref-file (d/name (:fill-color-ref-file fill)) + :penpot:fill-color-ref-id (d/name (:fill-color-ref-id fill)) + :penpot:fill-opacity (d/name (:fill-opacity fill))}]))])))) (defn- export-strokes-data [{:keys [strokes]}] (when-let [strokes (seq strokes)] - (mf/html - [:> "penpot:strokes" #js {} - (for [[index stroke] (d/enumerate strokes)] - [:> "penpot:stroke" - #js {:penpot:stroke-color (if (some? (:stroke-color-gradient stroke)) - (str/format "url(#%s)" (str "stroke-color-gradient_" (mf/use-ctx muc/render-id) "_" index)) - (d/name (:stroke-color stroke))) - :penpot:stroke-color-ref-file (d/name (:stroke-color-ref-file stroke)) - :penpot:stroke-color-ref-id (d/name (:stroke-color-ref-id stroke)) - :penpot:stroke-opacity (d/name (:stroke-opacity stroke)) - :penpot:stroke-style (d/name (:stroke-style stroke)) - :penpot:stroke-width (d/name (:stroke-width stroke)) - :penpot:stroke-alignment (d/name (:stroke-alignment stroke)) - :penpot:stroke-cap-start (d/name (:stroke-cap-start stroke)) - :penpot:stroke-cap-end (d/name (:stroke-cap-end stroke))}])]))) + (let [render-id (mf/use-ctx muc/render-id)] + (mf/html + [:> "penpot:strokes" #js {} + (for [[index stroke] (d/enumerate strokes)] + (let [stroke-image-id (dm/str "stroke-image-" render-id "-" index)] + [:> "penpot:stroke" + #js {:penpot:stroke-color (cond + (some? (:stroke-color-gradient stroke)) + (str/format "url(#%s)" (str "stroke-color-gradient-" render-id "-" index)) + :else + (d/name (:stroke-color stroke))) + :key (swap! internal-counter inc) + :penpot:stroke-image-id (when (:stroke-image stroke) stroke-image-id) + :penpot:stroke-color-ref-file (d/name (:stroke-color-ref-file stroke)) + :penpot:stroke-color-ref-id (d/name (:stroke-color-ref-id stroke)) + :penpot:stroke-opacity (d/name (:stroke-opacity stroke)) + :penpot:stroke-style (d/name (:stroke-style stroke)) + :penpot:stroke-width (d/name (:stroke-width stroke)) + :penpot:stroke-alignment (d/name (:stroke-alignment stroke)) + :penpot:stroke-cap-start (d/name (:stroke-cap-start stroke)) + :penpot:stroke-cap-end (d/name (:stroke-cap-end stroke))}]))])))) (defn- export-interactions-data [{:keys [interactions]}] (when-let [interactions (seq interactions)] @@ -327,6 +351,7 @@ :penpot:overlay-position-x ((d/nilf get-in) interaction [:overlay-position :x]) :penpot:overlay-position-y ((d/nilf get-in) interaction [:overlay-position :y]) :penpot:url (:url interaction) + :key (swap! internal-counter inc) :penpot:close-click-outside ((d/nilf str) (:close-click-outside interaction)) :penpot:background-overlay ((d/nilf str) (:background-overlay interaction)) :penpot:preserve-scroll ((d/nilf str) (:preserve-scroll interaction))}])]))) @@ -340,9 +365,14 @@ layout-wrap-type layout-padding-type layout-padding + layout-justify-items layout-justify-content layout-align-items - layout-align-content]}] + layout-align-content + layout-grid-dir + layout-grid-rows + layout-grid-columns + layout-grid-cells]}] (when layout (mf/html @@ -358,9 +388,51 @@ :penpot:layout-padding-p2 (:p2 layout-padding) :penpot:layout-padding-p3 (:p3 layout-padding) :penpot:layout-padding-p4 (:p4 layout-padding) + :penpot:layout-justify-items (d/name layout-justify-items) :penpot:layout-justify-content (d/name layout-justify-content) :penpot:layout-align-items (d/name layout-align-items) - :penpot:layout-align-content (d/name layout-align-content)}]))) + :penpot:layout-align-content (d/name layout-align-content) + :penpot:layout-grid-dir (d/name layout-grid-dir)} + + [:> "penpot:grid-rows" #js {} + (for [[idx {:keys [type value]}] (d/enumerate layout-grid-rows)] + [:> "penpot:grid-track" + #js {:penpot:index idx + :key (swap! internal-counter inc) + :penpot:type (d/name type) + :penpot:value value}])] + + [:> "penpot:grid-columns" #js {} + (for [[idx {:keys [type value]}] (d/enumerate layout-grid-columns)] + [:> "penpot:grid-track" + #js {:penpot:index idx + :key (swap! internal-counter inc) + :penpot:type (d/name type) + :penpot:value value}])] + + [:> "penpot:grid-cells" #js {} + (for [[_ {:keys [id + area-name + row + row-span + column + column-span + position + align-self + justify-self + shapes]}] layout-grid-cells] + [:> "penpot:grid-cell" + #js {:penpot:id id + :key (swap! internal-counter inc) + :penpot:area-name area-name + :penpot:row row + :penpot:row-span row-span + :penpot:column column + :penpot:column-span column-span + :penpot:position (d/name position) + :penpot:align-self (d/name align-self) + :penpot:justify-self (d/name justify-self) + :penpot:shapes (str/join " " shapes)}])]]))) (defn- export-layout-item-data [{:keys [layout-item-margin @@ -371,7 +443,9 @@ layout-item-min-h layout-item-max-w layout-item-min-w - layout-item-align-self]}] + layout-item-align-self + layout-item-absolute + layout-item-z-index]}] (when (or layout-item-margin layout-item-margin-type @@ -381,7 +455,9 @@ layout-item-min-h layout-item-max-w layout-item-min-w - layout-item-align-self) + layout-item-align-self + layout-item-absolute + layout-item-z-index) (mf/html [:> "penpot:layout-item" #js {:penpot:layout-item-margin-m1 (:m1 layout-item-margin) @@ -395,7 +471,9 @@ :penpot:layout-item-min-h layout-item-min-h :penpot:layout-item-max-w layout-item-max-w :penpot:layout-item-min-w layout-item-min-w - :penpot:layout-item-align-self (d/name layout-item-align-self)}]))) + :penpot:layout-item-align-self (d/name layout-item-align-self) + :penpot:layout-item-absolute layout-item-absolute + :penpot:layout-item-z-index layout-item-z-index}]))) (mf/defc export-data @@ -411,5 +489,5 @@ (export-strokes-data shape) (export-grid-data shape) (export-layout-container-data shape) - (export-layout-item-data shape)])) + (export-layout-item-data shape)])) diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs index de37f15609..076a662c04 100644 --- a/frontend/src/app/main/ui/shapes/fills.cljs +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -8,94 +8,154 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.config :as cfg] + [app.common.geom.shapes.text :as gst] + [app.config :as cf] [app.main.ui.shapes.attrs :as attrs] - [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.gradients :as grad] [app.util.object :as obj] [rumext.v2 :as mf])) (def no-repeat-padding 1.05) +(mf/defc internal-fills + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + render-id (unchecked-get props "render-id") + + type (dm/get-prop shape :type) + image (get shape :fill-image) + fills (get shape :fills []) + + selrect (dm/get-prop shape :selrect) + + bounds (when (cfh/text-shape? shape) + (gst/shape->rect shape)) + + metadata (get shape :metadata) + + x (dm/get-prop selrect :x) + y (dm/get-prop selrect :y) + width (dm/get-prop selrect :width) + height (dm/get-prop selrect :height) + + has-image? (or (some? metadata) + (some? image)) + + uri (cond + (some? metadata) + (cf/resolve-file-media metadata) + + (some? image) + (cf/resolve-file-media image)) + + uris (into [uri] + (comp + (keep :fill-image) + (map cf/resolve-file-media)) + fills) + + transform (gsh/transform-str shape) + + pat-props #js {:patternUnits "userSpaceOnUse" + :x x + :y y + :width width + :height height} + + pat-props (if (or (= :path type) (= :bool type)) + (obj/set! pat-props "patternTransform" transform) + pat-props)] + + (for [[obj-index obj] (d/enumerate (or (:position-data shape) [shape]))] + [:* {:key (dm/str obj-index)} + (for [[fill-index value] (reverse (d/enumerate (get obj :fills [])))] + (when (some? (:fill-color-gradient value)) + (let [gradient (:fill-color-gradient value) + + from-p (-> (gpt/point (+ x (* width (:start-x gradient))) + (+ y (* height (:start-y gradient))))) + to-p (-> (gpt/point (+ x (* width (:end-x gradient))) + (+ y (* height (:end-y gradient))))) + + gradient + (cond-> gradient + (some? bounds) + (assoc + :start-x (/ (- (:x from-p) (:x bounds)) (:width bounds)) + :start-y (/ (- (:y from-p) (:y bounds)) (:height bounds)) + :end-x (/ (- (:x to-p) (:x bounds)) (:width bounds)) + :end-y (/ (- (:y to-p) (:y bounds)) (:height bounds)))) + + props #js {:id (dm/str "fill-color-gradient-" render-id "-" fill-index) + :key (dm/str fill-index) + :gradient gradient + :shape obj}] + (case (d/name (:type gradient)) + "linear" [:> grad/linear-gradient props] + "radial" [:> grad/radial-gradient props])))) + + + (let [fill-id (dm/str "fill-" obj-index "-" render-id)] + [:> :pattern (-> (obj/clone pat-props) + (obj/set! "id" fill-id) + (cond-> (and has-image? (nil? bounds)) + (-> (obj/set! "width" (* width no-repeat-padding)) + (obj/set! "height" (* height no-repeat-padding)))) + (cond-> (some? bounds) + (-> (obj/set! "width" (:width bounds)) + (obj/set! "height" (:height bounds))))) + [:g + (for [[fill-index value] (reverse (d/enumerate (get obj :fills [])))] + (let [style (attrs/get-fill-style value fill-index render-id type) + props #js {:key (dm/str fill-index) + :width (d/nilv (:width bounds) width) + :height (d/nilv (:height bounds) height) + :style style}] + (if (:fill-image value) + (let [uri (cf/resolve-file-media (:fill-image value)) + keep-ar? (-> value :fill-image :keep-aspect-ratio) + image-props #js {:id (dm/str "fill-image-" render-id "-" fill-index) + :href (get uris uri uri) + :preserveAspectRatio (if keep-ar? "xMidYMid slice" "none") + :width width + :height height + :key (dm/str fill-index) + :opacity (:fill-opacity value)}] + [:> :image image-props]) + [:> :rect props]))) + + (when ^boolean has-image? + [:g + ;; We add this shape to add a padding so the patter won't repeat + ;; Issue: https://tree.taiga.io/project/penpot/issue/5583 + [:rect {:x 0 + :y 0 + :width (* width no-repeat-padding) + :height (* height no-repeat-padding) + :fill "none"}] + [:image {:href uri + :preserveAspectRatio "none" + :x 0 + :y 0 + :width width + :height height}]])]])]))) + (mf/defc fills {::mf/wrap-props false} [props] + (let [shape (unchecked-get props "shape") + type (dm/get-prop shape :type) + image (:fill-image shape) + fills (:fills shape [])] - (let [shape (obj/get props "shape") - render-id (obj/get props "render-id")] - - (when (or (some? (:fill-image shape)) - (#{:image :text} (:type shape)) - (> (count (:fills shape)) 1) - (some :fill-color-gradient (:fills shape))) - - (let [{:keys [x y width height]} (:selrect shape) - {:keys [metadata]} shape - - has-image? (or metadata (:fill-image shape)) - - uri (cond - metadata - (cfg/resolve-file-media metadata) - - (:fill-image shape) - (cfg/resolve-file-media (:fill-image shape))) - - embed (embed/use-data-uris [uri]) - transform (gsh/transform-str shape) - - ;; When true the image has not loaded yet - loading? (and (some? uri) (not (contains? embed uri))) - - pattern-attrs (cond-> #js {:patternUnits "userSpaceOnUse" - :x x - :y y - :height height - :width width - :data-loading loading?} - (= :path (:type shape)) - (obj/set! "patternTransform" transform)) - type (:type shape)] - - (for [[shape-index shape] (d/enumerate (or (:position-data shape) [shape]))] - [:* {:key (dm/str shape-index)} - (for [[fill-index value] (-> (d/enumerate (:fills shape [])) reverse)] - (when (some? (:fill-color-gradient value)) - (let [props #js {:id (dm/str "fill-color-gradient_" render-id "_" fill-index) - :key (dm/str fill-index) - :gradient (:fill-color-gradient value) - :shape shape}] - (case (d/name (:type (:fill-color-gradient value))) - "linear" [:> grad/linear-gradient props] - "radial" [:> grad/radial-gradient props])))) - - - (let [fill-id (dm/str "fill-" shape-index "-" render-id)] - [:> :pattern (-> (obj/clone pattern-attrs) - (obj/set! "id" fill-id) - (cond-> has-image? - (-> (obj/set! "width" (* width no-repeat-padding)) - (obj/set! "height" (* height no-repeat-padding))))) - [:g - (for [[fill-index value] (-> (d/enumerate (:fills shape [])) reverse)] - [:> :rect (-> (attrs/extract-fill-attrs value render-id fill-index type) - (obj/set! "key" (dm/str fill-index)) - (obj/set! "width" width) - (obj/set! "height" height))]) - - (when has-image? - [:g - ;; We add this shape to add a padding so the patter won't repeat - ;; Issue: https://tree.taiga.io/project/penpot/issue/5583 - [:rect {:x 0 - :y 0 - :width (* width no-repeat-padding) - :height (* height no-repeat-padding) - :fill "none"}] - [:image {:href (or (:data-uri shape) (get embed uri uri)) - :preserveAspectRatio "none" - :x 0 - :y 0 - :width width - :height height}]])]])]))))) + (when (or (some? image) + (or (= type :image) + (= type :text)) + (> (count fills) 1) + (some :fill-color-gradient fills) + (some :fill-image fills)) + [:> internal-fills props]))) diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index 5593a8b4d9..f7ff505dbc 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -6,32 +6,31 @@ (ns app.main.ui.shapes.filters (:require + [app.common.colors :as cc] [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.shapes.bounds :as gsb] [app.common.uuid :as uuid] - [app.util.color :as color] [cuerdas.core :as str] [rumext.v2 :as mf])) (defn get-filter-id [] - (str "filter_" (uuid/next))) + (dm/str "filter-" (uuid/next))) (defn filter-str [filter-id shape] - (when (or (seq (->> (:shadow shape) (remove :hidden))) (and (:blur shape) (-> shape :blur :hidden not))) - (str/fmt "url(#$0)" [filter-id]))) + (str/ffmt "url(#%)" filter-id))) (mf/defc color-matrix [{:keys [color]}] (let [{:keys [color opacity]} color - [r g b a] (color/hex->rgba color opacity) + [r g b a] (cc/hex->rgba color opacity) [r g b] [(/ r 255) (/ g 255) (/ b 255)]] [:feColorMatrix {:type "matrix" - :values (str/fmt "0 0 0 0 $0 0 0 0 0 $1 0 0 0 0 $2 0 0 0 $3 0" [r g b a])}])) + :values (str/ffmt "0 0 0 0 % 0 0 0 0 % 0 0 0 0 % 0 0 0 % 0" r g b a)}])) (mf/defc drop-shadow-filter [{:keys [filter-in filter-id params]}] @@ -46,6 +45,12 @@ :in "SourceAlpha" :result filter-id}]) + (when (< spread 0) + [:feMorphology {:radius (- spread) + :operator "erode" + :in "SourceAlpha" + :result filter-id}]) + [:feOffset {:dx offset-x :dy offset-y}] [:feGaussianBlur {:stdDeviation (/ blur 2)}] [:& color-matrix {:color color}] diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 42eccf32b3..291c27130f 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -7,43 +7,52 @@ (ns app.main.ui.shapes.frame (:require [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] + [app.common.geom.shapes.bounds :as gsb] [app.common.types.shape.layout :as ctl] [app.config :as cf] [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-fills shape-strokes]] + [app.util.debug :as dbg] [app.util.object :as obj] - [debug :refer [debug?]] [rumext.v2 :as mf])) -(defn frame-clip-id +(defn- frame-clip-id [shape render-id] - (dm/str "frame-clip-" (:id shape) "-" render-id)) + (dm/str "frame-clip-" (dm/get-prop shape :id) "-" render-id)) -(defn frame-clip-url +(defn- frame-clip-url [shape render-id] - (when (= :frame (:type shape)) - (dm/str "url(#" (frame-clip-id shape render-id) ")"))) + (dm/str "url(#" (frame-clip-id shape render-id) ")")) (mf/defc frame-clip-def - [{:keys [shape render-id]}] - (when (and (= :frame (:type shape)) (not (:show-content shape))) - (let [{:keys [x y width height]} shape - transform (gsh/transform-str shape) - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:x x - :y y - :width width - :height height - :transform transform})) - path? (some? (.-d props))] - [:clipPath.frame-clip-def {:id (frame-clip-id shape render-id) :class "frame-clip"} - (if ^boolean path? - [:> :path props] - [:> :rect props])]))) + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape")] + (when (and ^boolean (cfh/frame-shape? shape) + (not ^boolean (:show-content shape))) + + (let [render-id (unchecked-get props "render-id") + x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + w (dm/get-prop shape :width) + h (dm/get-prop shape :height) + t (gsh/transform-str shape) + + props (mf/with-memo [shape] + (-> #js {} + (attrs/add-border-props! shape) + (obj/merge! #js {:x x :y y :width w :height h :transform t}))) + + path? (some? (.-d props))] + + [:clipPath {:id (frame-clip-id shape render-id) + :class "frame-clip frame-clip-def"} + (if ^boolean path? + [:> :path props] + [:> :rect props])])))) ;; Wrapper around the frame that will handle things such as strokes and other properties ;; we wrap the proper frames and also the thumbnails @@ -51,29 +60,41 @@ {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - children (unchecked-get props "children") + (let [shape (unchecked-get props "shape") + children (unchecked-get props "children") - {:keys [x y width height show-content]} shape - transform (gsh/transform-str shape) + render-id (mf/use-ctx muc/render-id) - render-id (mf/use-ctx muc/render-id) + x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + w (dm/get-prop shape :width) + h (dm/get-prop shape :height) + opacity (dm/get-prop shape :opacity) + transform (gsh/transform-str shape) + + show-content? (get shape :show-content) + + props (mf/with-memo [shape] + (-> #js {} + (attrs/add-border-props! shape) + (obj/merge! + #js {:x x + :y y + :width w + :height h + :transform transform + :className "frame-background"}))) + path? (some? (.-d props))] - props (-> (attrs/extract-style-attrs shape render-id) - (obj/merge! - #js {:x x - :y y - :transform transform - :width width - :height height - :className "frame-background"})) - path? (some? (.-d props))] [:* - [:g {:clip-path (when (not show-content) (frame-clip-url shape render-id)) - :fill "none"} ;; A frame sets back normal fill behavior (default transparent). It may have - ;; been changed to default black if a shape coming from an imported SVG file - ;; is rendered. See main.ui.shapes.attrs/add-style-attrs. - [:& frame-clip-def {:shape shape :render-id render-id}] + [:g {:clip-path (when-not ^boolean show-content? + (frame-clip-url shape render-id)) + ;; A frame sets back normal fill behavior (default + ;; transparent). It may have been changed to default black + ;; if a shape coming from an imported SVG file is + ;; rendered. See main.ui.shapes.attrs/add-style-attrs. + :fill "none" + :opacity opacity} [:& shape-fills {:shape shape} (if ^boolean path? @@ -92,32 +113,43 @@ {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - bounds (or (unchecked-get props "bounds") - (gsh/points->selrect (:points shape))) + bounds (unchecked-get props "bounds") + + shape-id (dm/get-prop shape :id) + points (dm/get-prop shape :points) + + bounds (mf/with-memo [bounds points] + (or bounds (gsb/get-frame-bounds shape))) - shape-id (:id shape) thumb (:thumbnail shape) - debug? (debug? :thumbnails) - safari? (cf/check-browser? :safari)] + debug? (dbg/enabled? :thumbnails) + safari? (cf/check-browser? :safari) + + ;; FIXME: ensure bounds is always a rect instance and + ;; dm/get-prop for static attr access + bx (:x bounds) + by (:y bounds) + bh (:height bounds) + bw (:width bounds)] [:* [:image.frame-thumbnail {:id (dm/str "thumbnail-" shape-id) :href thumb + :x bx + :y by + :width bw + :height bh :decoding "async" - :x (:x bounds) - :y (:y bounds) - :width (:width bounds) - :height (:height bounds) :style {:filter (when (and (not ^boolean safari?) ^boolean debug?) "sepia(1)")}}] ;; Safari don't support filters so instead we add a rectangle around the thumbnail (when (and ^boolean safari? ^boolean debug?) - [:rect {:x (+ (:x bounds) 4) - :y (+ (:y bounds) 4) - :width (- (:width bounds) 8) - :height (- (:height bounds) 8) + [:rect {:x (+ bx 4) + :y (+ by 4) + :width (- bw 8) + :height (- bh 8) :stroke "red" :stroke-width 2}])])) @@ -134,14 +166,17 @@ (mf/fnc frame-shape {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - childs (unchecked-get props "childs") - childs (cond-> childs - (ctl/any-layout? shape) - (cph/sort-layout-children-z-index))] - [:> frame-container props - [:g.frame-children {:opacity (:opacity shape)} - (for [item childs] - (when (:id item) - [:& shape-wrapper {:key (dm/str (:id item)) :shape item}]))]]))) + (let [shape (unchecked-get props "shape") + childs (unchecked-get props "childs") + reverse? (and (ctl/flex-layout? shape) (ctl/reverse? shape)) + childs (cond-> childs + (ctl/any-layout? shape) + (ctl/sort-layout-children-z-index reverse?))] + + [:> frame-container props + [:g.frame-children + (for [item childs] + (let [id (dm/get-prop item :id)] + (when (some? id) + [:& shape-wrapper {:key (dm/str id) :shape item}])))]]))) diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs index 3f921cc3ae..d2a74a1193 100644 --- a/frontend/src/app/main/ui/shapes/gradients.cljs +++ b/frontend/src/app/main/ui/shapes/gradients.cljs @@ -8,110 +8,132 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.main.ui.context :as muc] [app.main.ui.shapes.export :as ed] [app.util.object :as obj] [rumext.v2 :as mf])) -(defn add-metadata [props gradient] +(defn- add-metadata! + [props gradient] (-> props (obj/set! "penpot:gradient" "true") (obj/set! "penpot:start-x" (:start-x gradient)) - (obj/set! "penpot:start-x" (:start-x gradient)) (obj/set! "penpot:start-y" (:start-y gradient)) (obj/set! "penpot:end-x" (:end-x gradient)) (obj/set! "penpot:end-y" (:end-y gradient)) (obj/set! "penpot:width" (:width gradient)))) -(mf/defc linear-gradient [{:keys [id gradient shape]}] - (let [transform (when (= :path (:type shape)) - (gsh/transform-matrix shape nil (gpt/point 0.5 0.5))) +(mf/defc linear-gradient + {::mf/wrap-props false} + [{:keys [id gradient shape force-transform]}] + (let [transform (mf/with-memo [shape] + (when force-transform + (gsh/transform-matrix shape nil (gpt/point 0.5 0.5)))) - base-props #js {:id id - :x1 (:start-x gradient) - :y1 (:start-y gradient) - :x2 (:end-x gradient) - :y2 (:end-y gradient) - :gradientTransform (dm/str transform)} + metadata? (mf/use-ctx ed/include-metadata-ctx) + props #js {:id id + :x1 (:start-x gradient) + :y1 (:start-y gradient) + :x2 (:end-x gradient) + :y2 (:end-y gradient) + :gradientTransform (dm/str transform)}] - include-metadata? (mf/use-ctx ed/include-metadata-ctx) - - props (cond-> base-props - include-metadata? - (add-metadata gradient))] + (when ^boolean metadata? + (add-metadata! props gradient)) [:> :linearGradient props (for [{:keys [offset color opacity]} (:stops gradient)] [:stop {:key (dm/str id "-stop-" offset) - :offset (or offset 0) + :offset (d/nilv offset 0) :stop-color color :stop-opacity opacity}])])) -(mf/defc radial-gradient [{:keys [id gradient shape]}] - (let [path? (= :path (:type shape)) - shape-transform (or (when path? (:transform shape)) (gmt/matrix)) - shape-transform-inv (or (when path? (:transform-inverse shape)) (gmt/matrix)) +(mf/defc radial-gradient + {::mf/wrap-props false} + [{:keys [id gradient shape]}] + (let [path? (cfh/path-shape? shape) + + transform (when ^boolean path? + (dm/get-prop shape :transform)) + transform (d/nilv transform gmt/base) + + transform-inv (when ^boolean path? + (dm/get-prop shape :transform-inverse)) + transform-inv (d/nilv transform-inv gmt/base) {:keys [start-x start-y end-x end-y] gwidth :width} gradient - gradient-vec (gpt/to-vec (gpt/point start-x start-y) - (gpt/point end-x end-y)) + gstart-pt (gpt/point start-x start-y) + gend-pt (gpt/point end-x end-y) + gradient-vec (gpt/to-vec gstart-pt gend-pt) - angle (+ (gpt/angle gradient-vec) 90) + angle (+ (gpt/angle gradient-vec) 90) - bb-shape (gsh/selection-rect [shape]) + points (dm/get-prop shape :points) + bounds (mf/with-memo [points] + (grc/points->rect points)) + selrect (dm/get-prop shape :selrect) - ;; Paths don't have a transform in SVG because we transform the points - ;; we need to compensate the difference between the original rectangle - ;; and the transformed one. This factor is that calculation. - factor (if path? - (/ (:height (:selrect shape)) (:height bb-shape)) - 1.0) + ;; Paths don't have a transform in SVG because we transform + ;; the points we need to compensate the difference between the + ;; original rectangle and the transformed one. This factor is + ;; that calculation. + factor (if ^boolean path? + (/ (dm/get-prop selrect :height) + (dm/get-prop bounds :height)) + 1.0) - transform (-> (gmt/matrix) - (gmt/translate (gpt/point start-x start-y)) - (gmt/multiply shape-transform) - (gmt/rotate angle) - (gmt/scale (gpt/point gwidth factor)) - (gmt/multiply shape-transform-inv) - (gmt/translate (gpt/negate (gpt/point start-x start-y)))) + transform (mf/with-memo [gradient transform transform-inv factor] + (-> (gmt/matrix) + (gmt/translate gstart-pt) + (gmt/multiply transform) + (gmt/rotate angle) + (gmt/scale (gpt/point gwidth factor)) + (gmt/multiply transform-inv) + (gmt/translate (gpt/negate gstart-pt)))) - gradient-radius (gpt/length gradient-vec) - base-props #js {:id id - :cx start-x - :cy start-y - :r gradient-radius - :gradientTransform transform} + metadata? (mf/use-ctx ed/include-metadata-ctx) - include-metadata? (mf/use-ctx ed/include-metadata-ctx) + props #js {:id id + :cx start-x + :cy start-y + :r (gpt/length gradient-vec) + :gradientTransform transform}] + + (when ^boolean metadata? + (add-metadata! props gradient)) - props (cond-> base-props - include-metadata? - (add-metadata gradient))] [:> :radialGradient props (for [{:keys [offset color opacity]} (:stops gradient)] [:stop {:key (dm/str id "-stop-" offset) - :offset (or offset 0) + :offset (d/nilv offset 0) :stop-color color :stop-opacity opacity}])])) (mf/defc gradient {::mf/wrap-props false} [props] - (let [attr (obj/get props "attr") - shape (obj/get props "shape") - id (obj/get props "id") - id' (mf/use-ctx muc/render-id) - id (or id (dm/str (name attr) "_" id')) + (let [attr (unchecked-get props "attr") + shape (unchecked-get props "shape") + id (unchecked-get props "id") + rid (mf/use-ctx muc/render-id) + + id (if (some? id) + id + (dm/str (name attr) "-" rid)) + gradient (get shape attr) - gradient-props #js {:id id - :gradient gradient - :shape shape}] - (when gradient - (case (d/name (:type gradient)) - "linear" [:> linear-gradient gradient-props] - "radial" [:> radial-gradient gradient-props] + props #js {:id id + :gradient gradient + :shape shape}] + + (when (some? gradient) + (case (:type gradient) + :linear [:> linear-gradient props] + :radial [:> radial-gradient props] nil)))) diff --git a/frontend/src/app/main/ui/shapes/grid_layout_viewer.cljs b/frontend/src/app/main/ui/shapes/grid_layout_viewer.cljs new file mode 100644 index 0000000000..5cd437eada --- /dev/null +++ b/frontend/src/app/main/ui/shapes/grid_layout_viewer.cljs @@ -0,0 +1,101 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.shapes.grid-layout-viewer + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.grid-layout :as gsg] + [app.common.geom.shapes.points :as gpo] + [app.common.types.shape.layout :as ctl] + [rumext.v2 :as mf])) + +(mf/defc grid-cell-area-label + {::mf/wrap-props false} + [props] + + (let [cell-origin (unchecked-get props "origin") + cell-width (unchecked-get props "width") + text (unchecked-get props "text") + + area-width (* 10 (count text)) + area-height 25 + area-x (- (+ (:x cell-origin) cell-width) area-width) + area-y (:y cell-origin) + + area-text-x (+ area-x (/ area-width 2)) + area-text-y (+ area-y (/ area-height 2))] + + [:g {:pointer-events "none"} + [:rect {:x area-x + :y area-y + :width area-width + :height area-height + :style {:fill "var(--color-accent-quaternary)" + :fill-opacity 0.3}}] + [:text {:x area-text-x + :y area-text-y + :style {:fill "var(--color-accent-quaternary)" + :font-family "worksans" + :font-weight 600 + :font-size 14 + :alignment-baseline "central" + :text-anchor "middle"}} + text]])) + +(mf/defc grid-cell + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + cell (unchecked-get props "cell") + layout-data (unchecked-get props "layout-data") + + cell-bounds (gsg/cell-bounds layout-data cell) + cell-origin (gpo/origin cell-bounds) + cell-width (gpo/width-points cell-bounds) + cell-height (gpo/height-points cell-bounds) + cell-center (gsh/points->center cell-bounds) + cell-origin (gpt/transform cell-origin (gmt/transform-in cell-center (:transform-inverse shape)))] + + [:g.cell + [:rect + {:transform (dm/str (gmt/transform-in cell-center (:transform shape))) + :x (:x cell-origin) + :y (:y cell-origin) + :width cell-width + :height cell-height + :style {:stroke "var(--color-accent-quaternary)" + :stroke-width 1.5 + :fill "none"}}] + + (when (:area-name cell) + [:& grid-cell-area-label {:origin cell-origin + :width cell-width + :text (:area-name cell)}])])) + +(mf/defc grid-layout-viewer + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + objects (unchecked-get props "objects") + bounds (d/lazy-map (keys objects) #(gsh/shape->points (get objects %))) + children + (->> (cfh/get-immediate-children objects (:id shape)) + (remove :hidden) + (map #(vector (gpo/parent-coords-bounds (:points %) (:points shape)) %))) + + layout-data (gsg/calc-layout-data shape (:points shape) children bounds objects)] + + [:g.cells + (for [cell (ctl/get-cells shape {:sort? true})] + [:& grid-cell {:key (dm/str "cell-" (:id cell)) + :shape shape + :layout-data layout-data + :cell cell}])])) diff --git a/frontend/src/app/main/ui/shapes/group.cljs b/frontend/src/app/main/ui/shapes/group.cljs index b4cab7f1a6..8b3ad40ce8 100644 --- a/frontend/src/app/main/ui/shapes/group.cljs +++ b/frontend/src/app/main/ui/shapes/group.cljs @@ -9,7 +9,6 @@ [app.common.data.macros :as dm] [app.main.ui.context :as muc] [app.main.ui.shapes.mask :refer [mask-url clip-url mask-factory]] - [app.util.object :as obj] [rumext.v2 :as mf])) (defn group-shape @@ -18,41 +17,40 @@ (mf/fnc group-shape {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - childs (unchecked-get props "childs") - objects (unchecked-get props "objects") - render-id (mf/use-ctx muc/render-id) - masked-group? (:masked-group? shape) + (let [shape (unchecked-get props "shape") + childs (unchecked-get props "childs") + render-id (mf/use-ctx muc/render-id) + masked-group? (:masked-group shape) - [mask childs] (if masked-group? - [(first childs) (rest childs)] - [nil childs]) + mask (if ^boolean masked-group? + (first childs) + nil) + childs (if ^boolean masked-group? + (rest childs) + childs) - ;; We need to separate mask and clip into two because a bug in Firefox - ;; breaks when the group has clip+mask+foreignObject - ;; Clip and mask separated will work in every platform - ; Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1734805 - [clip-wrapper clip-props] - (if masked-group? - ["g" (-> (obj/create) - (obj/set! "clipPath" (clip-url render-id mask)))] - [mf/Fragment nil]) + wrapper (if ^boolean masked-group? "g" mf/Fragment) + clip-props (if ^boolean masked-group? + #js {:clipPath (clip-url render-id mask)} + #js {}) - [mask-wrapper mask-props] - (if masked-group? - ["g" (-> (obj/create) - (obj/set! "mask" (mask-url render-id mask)))] - [mf/Fragment nil])] + mask-props (if ^boolean masked-group? + #js {:mask (mask-url render-id mask)} + #js {})] - [:> clip-wrapper clip-props - [:> mask-wrapper mask-props - (when masked-group? - [:> render-mask #js {:mask mask - :objects objects}]) + ;; We need to separate mask and clip into two because a bug in + ;; Firefox breaks when the group has clip+mask+foreignObject + ;; Clip and mask separated will work in every platform Firefox + ;; bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1734805 + [:> wrapper clip-props + [:> wrapper mask-props + (when ^boolean masked-group? + [:& render-mask {:mask mask}]) (for [item childs] - [:& shape-wrapper {:shape item - :key (dm/str (:id item))}])]])))) + [:& shape-wrapper + {:shape item + :key (dm/str (dm/get-prop item :id))}])]])))) diff --git a/frontend/src/app/main/ui/shapes/image.cljs b/frontend/src/app/main/ui/shapes/image.cljs index 42b9ddc5ab..0288b11ebe 100644 --- a/frontend/src/app/main/ui/shapes/image.cljs +++ b/frontend/src/app/main/ui/shapes/image.cljs @@ -6,7 +6,9 @@ (ns app.main.ui.shapes.image (:require + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] + [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]] [app.util.object :as obj] @@ -16,20 +18,25 @@ {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - {:keys [x y width height]} shape + (let [shape (unchecked-get props "shape") + + x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + w (dm/get-prop shape :width) + h (dm/get-prop shape :height) + + render-id (mf/use-ctx muc/render-id) transform (gsh/transform-str shape) - props (-> (attrs/extract-style-attrs shape) - (obj/merge! (attrs/extract-border-radius-attrs shape)) - (obj/merge! - #js {:x x - :y y - :transform transform - :width width - :height height})) - path? (some? (.-d props))] + + props (mf/with-memo [shape render-id] + (-> #js {} + (attrs/add-fill-props! shape render-id) + (attrs/add-border-props! shape) + (obj/merge! #js {:x x :y y :width w :height h :transform transform}))) + + path? (some? (.-d props))] [:& shape-custom-strokes {:shape shape} - (if path? + (if ^boolean path? [:> :path props] [:> :rect props])])) diff --git a/frontend/src/app/main/ui/shapes/mask.cljs b/frontend/src/app/main/ui/shapes/mask.cljs index f74e3a6640..250b7bd400 100644 --- a/frontend/src/app/main/ui/shapes/mask.cljs +++ b/frontend/src/app/main/ui/shapes/mask.cljs @@ -8,28 +8,29 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.geom.shapes :as gsh] + [app.common.files.helpers :as cfh] + [app.common.geom.rect :as grc] [app.main.ui.context :as muc] [cuerdas.core :as str] [rumext.v2 :as mf])) (defn mask-id [render-id mask] - (str render-id "-" (:id mask) "-mask")) + (dm/str render-id "-" (:id mask) "-mask")) (defn mask-url [render-id mask] - (str "url(#" (mask-id render-id mask) ")")) + (dm/str "url(#" (mask-id render-id mask) ")")) (defn clip-id [render-id mask] - (str render-id "-" (:id mask) "-clip")) + (dm/str render-id "-" (:id mask) "-clip")) (defn clip-url [render-id mask] - (str "url(#" (clip-id render-id mask) ")")) + (dm/str "url(#" (clip-id render-id mask) ")")) (defn filter-id [render-id mask] - (str render-id "-" (:id mask) "-filter")) + (dm/str render-id "-" (:id mask) "-filter")) (defn filter-url [render-id mask] - (str "url(#" (filter-id render-id mask) ")")) + (dm/str "url(#" (filter-id render-id mask) ")")) (defn set-white-fill [shape] @@ -42,17 +43,39 @@ (d/update-when :position-data #(mapv update-color %)) (assoc :stroke-color "#FFFFFF" :stroke-opacity 1)))) +(defn- point->str + [point] + (dm/str (dm/get-prop point :x) "," (dm/get-prop point :y))) + (defn mask-factory [shape-wrapper] (mf/fnc mask-shape {::mf/wrap-props false} [props] - (let [mask (unchecked-get props "mask") - render-id (mf/use-ctx muc/render-id) - svg-text? (and (= :text (:type mask)) (some? (:position-data mask))) + (let [mask (unchecked-get props "mask") + render-id (mf/use-ctx muc/render-id) + + svg-text? (and ^boolean (cfh/text-shape? mask) + ^boolean (some? (:position-data mask))) + + points (dm/get-prop mask :points) + points-str (mf/with-memo [points] + (->> (map point->str points) + (str/join " "))) + + bounds (mf/with-memo [points] + (grc/points->rect points)) + + bx (dm/get-prop bounds :x) + by (dm/get-prop bounds :y) + bw (dm/get-prop bounds :width) + bh (dm/get-prop bounds :height) + + shape (mf/with-memo [mask] + (-> mask + (dissoc :shadow :blur) + (assoc :is-mask? true)))] - mask-bb (:points mask) - mask-bb-rect (gsh/points->rect mask-bb)] [:defs [:filter {:id (filter-id render-id mask)} [:feFlood {:flood-color "white" @@ -66,26 +89,26 @@ ;; we cannot use clips instead of mask because clips can only be simple shapes [:clipPath {:class "mask-clip-path" :id (clip-id render-id mask)} - [:polyline {:points (->> mask-bb - (map #(dm/str (:x %) "," (:y %))) - (str/join " "))}]] + [:polyline {:points points-str}]] ;; When te shape is a text we pass to the shape the info and disable the filter. ;; There is a bug in Firefox with filters and texts. We change the text to white at shape level [:mask {:class "mask-shape" :id (mask-id render-id mask) - :x (:x mask-bb-rect) - :y (:y mask-bb-rect) - :width (:width mask-bb-rect) - :height (:height mask-bb-rect) + :x bx + :y by + :width bw + :height bh ;; This is necesary to prevent a race condition in the dynamic-modifiers whether the modifier ;; triggers afte the render - :data-old-x (:x mask-bb-rect) - :data-old-y (:y mask-bb-rect) - :data-old-width (:width mask-bb-rect) - :data-old-height (:height mask-bb-rect) + :data-old-x bx + :data-old-y by + :data-old-width bw + :data-old-height bh :mask-units "userSpaceOnUse"} - [:g {:filter (when-not svg-text? (filter-url render-id mask))} - [:& shape-wrapper {:shape (-> mask (dissoc :shadow :blur) (assoc :is-mask? true))}]]]]))) + + [:g {:filter (when-not ^boolean svg-text? + (filter-url render-id mask))} + [:& shape-wrapper {:shape shape}]]]]))) diff --git a/frontend/src/app/main/ui/shapes/path.cljs b/frontend/src/app/main/ui/shapes/path.cljs index 031b0458b9..f44d430428 100644 --- a/frontend/src/app/main/ui/shapes/path.cljs +++ b/frontend/src/app/main/ui/shapes/path.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.shapes.path (:require [app.common.logging :as log] - [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]] [app.util.object :as obj] [app.util.path.format :as upf] @@ -26,9 +25,9 @@ :shape-name (:name shape) :shape-id (:id shape) :cause e) - ""))) + ""))) - props (-> (attrs/extract-style-attrs shape) + props (-> #js {} (obj/set! "d" pdata))] [:& shape-custom-strokes {:shape shape} diff --git a/frontend/src/app/main/ui/shapes/rect.cljs b/frontend/src/app/main/ui/shapes/rect.cljs index 722ad5573b..64b2c6cc53 100644 --- a/frontend/src/app/main/ui/shapes/rect.cljs +++ b/frontend/src/app/main/ui/shapes/rect.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.shapes.rect (:require + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]] @@ -16,16 +17,18 @@ {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - {:keys [x y width height]} shape - transform (gsh/transform-str shape) - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:x x - :y y - :transform transform - :width width - :height height})) + x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + w (dm/get-prop shape :width) + h (dm/get-prop shape :height) + + t (gsh/transform-str shape) + + props (mf/with-memo [shape] + (-> #js {} + (attrs/add-border-props! shape) + (obj/merge! #js {:x x :y y :transform t :width w :height h}))) path? (some? (.-d props))] diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index d1f767ead6..cbda63671d 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -8,9 +8,10 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cfh] [app.main.refs :as refs] [app.main.ui.context :as muc] + [app.main.ui.hooks :as h] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.export :as ed] [app.main.ui.shapes.fills :as fills] @@ -20,28 +21,30 @@ [app.util.object :as obj] [rumext.v2 :as mf])) +;; FIXME: revisit this: (defn propagate-wrapper-styles-child [child wrapper-props] - (let [child-props-childs - (-> (obj/get child "props") - (obj/clone) - (-> (obj/get "childs"))) + (when (some? child) + (let [child-props-childs + (-> (obj/get child "props") + (obj/clone) + (-> (obj/get "childs"))) - child-props-childs - (->> child-props-childs - (map #(assoc % :wrapper-styles (obj/get wrapper-props "style")))) + child-props-childs + (->> child-props-childs + (map #(assoc % :wrapper-styles (obj/get wrapper-props "style")))) - child-props - (-> (obj/get child "props") - (obj/clone) - (obj/set! "childs" child-props-childs))] + child-props + (-> (obj/get child "props") + (obj/clone) + (obj/set! "childs" child-props-childs))] - (-> (obj/clone child) - (obj/set! "props" child-props)))) + (-> (obj/clone child) + (obj/set! "props" child-props))))) (defn propagate-wrapper-styles ([children wrapper-props] - (if (.isArray js/Array children) + (if ^boolean (obj/array? children) (->> children (map #(propagate-wrapper-styles-child % wrapper-props))) (-> children (propagate-wrapper-styles-child wrapper-props))))) @@ -54,7 +57,7 @@ children (unchecked-get props "children") pointer-events (unchecked-get props "pointer-events") disable-shadows? (unchecked-get props "disable-shadows?") - shape-id (:id shape) + shape-id (dm/get-prop shape :id) preview-blend-mode-ref (mf/with-memo [shape-id] (refs/workspace-preview-blend-by-id shape-id)) @@ -62,11 +65,13 @@ blend-mode (-> (mf/deref preview-blend-mode-ref) (or (:blend-mode shape))) - type (:type shape) - render-id (mf/use-id) - filter-id (dm/str "filter_" render-id) + type (dm/get-prop shape :type) + render-id (h/use-render-id) + filter-id (dm/str "filter-" render-id) styles (-> (obj/create) (obj/set! "pointerEvents" pointer-events) + (cond-> (not (cfh/frame-shape? shape)) + (obj/set! "opacity" (:opacity shape))) (cond-> (and blend-mode (not= blend-mode :normal)) (obj/set! "mixBlendMode" (d/name blend-mode)))) @@ -76,15 +81,17 @@ shape-without-shadows (assoc shape :shadow []) filter-str - (when (and (or (cph/group-shape? shape) - (cph/frame-shape? shape) - (cph/svg-raw-shape? shape)) - (not disable-shadows?)) + (when (and (or (cfh/group-shape? shape) + (cfh/frame-shape? shape) + (cfh/svg-raw-shape? shape)) + (not disable-shadows?)) (filters/filter-str filter-id shape)) wrapper-props (-> (obj/clone props) - (obj/without ["shape" "children" "disable-shadows?"]) + (obj/unset! "shape") + (obj/unset! "children") + (obj/unset! "disable-shadows?") (obj/set! "ref" ref) (obj/set! "id" (dm/fmt "shape-%" shape-id)) (obj/set! "style" styles)) @@ -92,7 +99,8 @@ wrapper-props (cond-> wrapper-props (= :group type) - (attrs/add-style-attrs shape render-id) + (-> (attrs/add-fill-props! shape render-id) + (attrs/add-border-props! shape)) (some? filter-str) (obj/set! "filter" filter-str)) @@ -111,8 +119,12 @@ [:defs [:& defs/svg-defs {:shape shape :render-id render-id}] [:& filters/filters {:shape shape :filter-id filter-id}] - [:& filters/filters {:shape shape-without-blur :filter-id (dm/fmt "filter_shadow_%" render-id)}] - [:& filters/filters {:shape shape-without-shadows :filter-id (dm/fmt "filter_blur_%" render-id)}] - [:& fills/fills {:shape shape :render-id render-id}] - [:& frame/frame-clip-def {:shape shape :render-id render-id}]] + [:& filters/filters {:shape shape-without-blur :filter-id (dm/fmt "filter-shadow-%" render-id)}] + [:& filters/filters {:shape shape-without-shadows :filter-id (dm/fmt "filter-blur-%" render-id)}] + [:& frame/frame-clip-def {:shape shape :render-id render-id}] + + ;; Text fills need to be defined afterwards because they are specified per text-block + (when-not (cfh/text-shape? shape) + [:& fills/fills {:shape shape :render-id render-id}])] + children]])) diff --git a/frontend/src/app/main/ui/shapes/svg_defs.cljs b/frontend/src/app/main/ui/shapes/svg_defs.cljs index 1be7aade58..f636bf205a 100644 --- a/frontend/src/app/main/ui/shapes/svg_defs.cljs +++ b/frontend/src/app/main/ui/shapes/svg_defs.cljs @@ -9,9 +9,11 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.bounds :as gsb] - [app.util.svg :as usvg] + [app.common.svg :as csvg] + [app.util.object :as obj] [rumext.v2 :as mf])) (defn add-matrix [attrs transform-key transform-matrix] @@ -22,23 +24,26 @@ (str transform-matrix " " val) (str transform-matrix))))) -(mf/defc svg-node [{:keys [type node prefix-id transform bounds]}] +(mf/defc svg-node + {::mf/wrap-props false} + [{:keys [type node prefix-id transform bounds]}] (cond (string? node) node :else (let [{:keys [tag attrs content]} node - transform-gradient? (and (contains? usvg/gradient-tags tag) + transform-gradient? (and (contains? csvg/gradient-tags tag) (= "userSpaceOnUse" (get attrs :gradientUnits "objectBoundingBox"))) transform-pattern? (and (= :pattern tag) + (= "userSpaceOnUse" (get attrs :patternContentUnits "userSpaceOnUse")) (= "userSpaceOnUse" (get attrs :patternUnits "userSpaceOnUse"))) transform-clippath? (and (= :clipPath tag) (= "userSpaceOnUse" (get attrs :clipPathUnits "userSpaceOnUse"))) - transform-filter? (and (contains? usvg/filter-tags tag) + transform-filter? (and (contains? csvg/filter-tags tag) (= "userSpaceOnUse" (get attrs :filterUnits "objectBoundingBox"))) transform-mask? (and (= :mask tag) @@ -46,8 +51,8 @@ attrs (-> attrs - (usvg/update-attr-ids prefix-id) - (usvg/clean-attrs) + (csvg/update-attr-ids prefix-id) + (csvg/attrs->props) ;; This clasname will be used to change the transform on the viewport ;; only necessary for groups because shapes have their own transform (cond-> (and (or transform-gradient? @@ -58,11 +63,11 @@ (= :group type)) (update :className #(if % (dm/str % " svg-def") "svg-def"))) (cond-> - transform-gradient? (add-matrix :gradientTransform transform) - transform-pattern? (add-matrix :patternTransform transform) - transform-clippath? (add-matrix :transform transform) - (or transform-filter? - transform-mask?) (merge bounds))) + transform-gradient? (add-matrix :gradientTransform transform) + transform-pattern? (add-matrix :patternTransform transform) + transform-clippath? (add-matrix :transform transform) + (or transform-filter? + transform-mask?) (merge bounds))) ;; Fixes race condition with dynamic modifiers forcing redraw this properties before ;; the effect triggers @@ -79,7 +84,7 @@ :transform (str transform)}] [mf/Fragment #js {}])] - [:> (name tag) (clj->js attrs) + [:> (name tag) (obj/map->obj attrs) [:> wrapper wrapper-props (for [[index node] (d/enumerate content)] [:& svg-node {:key (dm/str "node-" index) @@ -89,40 +94,42 @@ :transform transform :bounds bounds}])]]))) -(defn svg-def-bounds [svg-def shape transform] - (let [{:keys [tag]} svg-def] - (if (or (= tag :mask) (contains? usvg/filter-tags tag)) - (-> (gsh/make-rect (d/parse-double (get-in svg-def [:attrs :x])) - (d/parse-double (get-in svg-def [:attrs :y])) - (d/parse-double (get-in svg-def [:attrs :width])) - (d/parse-double (get-in svg-def [:attrs :height]))) - (gsh/transform-rect transform)) - (gsb/get-shape-filter-bounds shape)))) +(defn- get-svg-def-bounds + [{:keys [tag attrs] :as node} shape transform] + (if (or (= tag :mask) (contains? csvg/filter-tags tag)) + (some-> (grc/make-rect (d/parse-double (get attrs :x)) + (d/parse-double (get attrs :y)) + (d/parse-double (get attrs :width)) + (d/parse-double (get attrs :height))) + (gsh/transform-rect transform)) + (gsb/get-shape-filter-bounds shape))) -(mf/defc svg-defs [{:keys [shape render-id]}] - (let [svg-defs (:svg-defs shape) +(mf/defc svg-defs + {::mf/wrap-props false} + [{:keys [shape render-id]}] + (let [defs (:svg-defs shape) - transform (mf/use-memo - (mf/deps shape) - #(if (= :svg-raw (:type shape)) + transform (mf/with-memo [shape] + (if (= :svg-raw (:type shape)) (gmt/matrix) - (usvg/svg-transform-matrix shape))) + (csvg/svg-transform-matrix shape))) ;; Paths doesn't have transform so we have to transform its gradients transform (if (some? (:svg-transform shape)) (gmt/multiply transform (:svg-transform shape)) transform) - prefix-id - (fn [id] - (cond->> id - (contains? svg-defs id) (str render-id "-")))] + ;; FIXME: naming + prefix-id (mf/use-fn + (mf/deps render-id defs) + (fn [id] + (cond->> id + (contains? defs id) (str render-id "-"))))] - (when (seq svg-defs) - (for [[key svg-def] svg-defs] - [:& svg-node {:key (dm/str key) - :type (:type shape) - :node svg-def - :prefix-id prefix-id - :transform transform - :bounds (svg-def-bounds svg-def shape transform)}])))) + (for [[key node] defs] + [:& svg-node {:key (dm/str key) + :type (:type shape) + :node node + :prefix-id prefix-id + :transform transform + :bounds (get-svg-def-bounds node shape transform)}]))) diff --git a/frontend/src/app/main/ui/shapes/svg_raw.cljs b/frontend/src/app/main/ui/shapes/svg_raw.cljs index 6a744745be..49578d08d3 100644 --- a/frontend/src/app/main/ui/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/shapes/svg_raw.cljs @@ -8,82 +8,89 @@ (:require [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] - [app.main.ui.shapes.attrs :as usa] + [app.common.svg :as csvg] + [app.main.ui.context :as muc] + [app.main.ui.shapes.attrs :as attrs] [app.util.object :as obj] - [app.util.svg :as usvg] + [cuerdas.core :as str] [rumext.v2 :as mf])) ;; Graphic tags -(defonce graphic-element? +(def graphic-element #{:svg :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath :use}) ;; Context to store a re-mapping of the ids (def svg-ids-ctx (mf/create-context nil)) -(defn set-styles [attrs shape] - (let [custom-attrs (-> (usa/extract-style-attrs shape) - (obj/without ["transform"])) - - attrs (or attrs {}) - attrs (cond-> attrs - (string? (:style attrs)) usvg/clean-attrs) - style (obj/merge! (clj->js (:style attrs {})) - (obj/get custom-attrs "style"))] - (-> (clj->js attrs) - (obj/merge! custom-attrs) - (obj/set! "style" style)))) - -(defn translate-shape [attrs shape] - (let [transform (dm/str (usvg/svg-transform-matrix shape) - " " - (:transform attrs ""))] - (cond-> attrs - (and (:svg-viewbox shape) (graphic-element? (-> shape :content :tag))) - (assoc :transform transform)))) - (mf/defc svg-root {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - children (unchecked-get props "children") - {:keys [x y width height]} shape - {:keys [attrs] :as content} (:content shape) + (let [shape (unchecked-get props "shape") + children (unchecked-get props "children") - ids-mapping (mf/use-memo #(usvg/generate-id-mapping content)) + x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + w (dm/get-prop shape :width) + h (dm/get-prop shape :height) - attrs (-> (set-styles attrs shape) - (obj/set! "x" x) - (obj/set! "y" y) - (obj/set! "width" width) - (obj/set! "height" height) - (obj/set! "preserveAspectRatio" "none"))] + ids-mapping (mf/with-memo [shape] + (csvg/generate-id-mapping (:content shape))) + + render-id (mf/use-ctx muc/render-id) + + props (mf/with-memo [shape render-id] + (-> #js {} + (attrs/add-fill-props! shape render-id) + (obj/unset! "transform") + (obj/set! "x" x) + (obj/set! "y" y) + (obj/set! "width" w) + (obj/set! "height" h) + (obj/set! "preserveAspectRatio" "none")))] [:& (mf/provider svg-ids-ctx) {:value ids-mapping} [:g.svg-raw {:transform (gsh/transform-str shape)} - [:> "svg" attrs children]]])) + [:> "svg" props children]]])) (mf/defc svg-element {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - children (unchecked-get props "children") - - {:keys [content]} shape - {:keys [attrs tag]} content + (let [shape (unchecked-get props "shape") + children (unchecked-get props "children") ids-mapping (mf/use-ctx svg-ids-ctx) + render-id (mf/use-ctx muc/render-id) - attrs (mf/use-memo #(usvg/replace-attrs-ids attrs ids-mapping)) + tag (-> shape :content :tag) - attrs (translate-shape attrs shape) - element-id (get-in content [:attrs :id]) - attrs (cond-> (set-styles attrs shape) - (and element-id (contains? ids-mapping element-id)) - (obj/set! "id" (get ids-mapping element-id)))] - [:> (name tag) attrs children])) + shape + (mf/with-memo [shape ids-mapping] + (let [tag (-> shape :content :tag)] + (-> shape + (update :svg-attrs csvg/replace-attrs-ids ids-mapping) + (update :svg-attrs (fn [attrs] + (if (contains? graphic-element tag) + (assoc attrs :transform (str/ffmt "% %" + (csvg/svg-transform-matrix shape) + (:transform attrs ""))) + (dissoc attrs :transform))))))) -(defn svg-raw-shape [shape-wrapper] + props + (mf/with-memo [shape render-id] + (let [element-id (dm/get-in shape [:svg-attrs :id]) + props (attrs/add-fill-props! #js {} shape render-id)] + + (when (and (some? element-id) + (contains? ids-mapping element-id)) + (obj/set! props "id" (get ids-mapping element-id))) + + props))] + + [:> (name tag) props children])) + +(defn svg-raw-shape + [shape-wrapper] (mf/fnc svg-raw-shape {::mf/wrap-props false} [props] @@ -91,28 +98,26 @@ (let [shape (unchecked-get props "shape") childs (unchecked-get props "childs") - {:keys [content]} shape - {:keys [tag]} content + content (get shape :content) + tag (get content :tag) svg-root? (and (map? content) (= tag :svg)) svg-tag? (map? content) svg-leaf? (string? content) - valid-tag? (contains? usvg/svg-tags-list tag)] + valid-tag? (contains? csvg/svg-tags tag)] (cond - svg-root? + ^boolean svg-root? [:& svg-root {:shape shape} (for [item childs] [:& shape-wrapper {:shape item :key (dm/str (:id item))}])] - (and svg-tag? valid-tag?) + (and ^boolean svg-tag? + ^boolean valid-tag?) [:& svg-element {:shape shape} (for [item childs] [:& shape-wrapper {:shape item :key (dm/str (:id item))}])] - svg-leaf? - content - - :else nil)))) - + ^boolean svg-leaf? + content)))) diff --git a/frontend/src/app/main/ui/shapes/text/fo_text.cljs b/frontend/src/app/main/ui/shapes/text/fo_text.cljs index 994b989808..1f7836cc0c 100644 --- a/frontend/src/app/main/ui/shapes/text/fo_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/fo_text.cljs @@ -6,12 +6,11 @@ (ns app.main.ui.shapes.text.fo-text (:require - [app.common.colors :as clr] + [app.common.colors :as cc] [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] - [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.text.styles :as sts] - [app.util.color :as uc] [app.util.object :as obj] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -85,9 +84,9 @@ [colors] (assert (set? colors)) (loop [current-rgb [0 0 0]] - (let [current-hex (uc/rgb->hex current-rgb)] + (let [current-hex (cc/rgb->hex current-rgb)] (if (contains? colors current-hex) - (recur (uc/next-rgb current-rgb)) + (recur (cc/next-rgb current-rgb)) current-hex)))) (defn- fill->color @@ -122,7 +121,7 @@ (filter some?)) colors (->> color-data - (into #{clr/black} + (into #{cc/black} (comp (filter #(= :solid (:type %))) (map :hex)))) @@ -170,16 +169,16 @@ [colors color-mapping color-mapping-inverse])) (mf/defc text-shape - {::mf/wrap-props false + {::mf/props :obj ::mf/forward-ref true} - [props ref] - (let [shape (obj/get props "shape") - transform (gsh/transform-str shape) - - {:keys [id x y width height content]} shape - grow-type (obj/get props "grow-type") ;; This is only needed in workspace - ;; We add 8px to add a padding for the exporter - ;; width (+ width 8) + [{:keys [shape grow-type]} ref] + (let [transform (gsh/transform-str shape) + id (dm/get-prop shape :id) + x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + width (dm/get-prop shape :width) + height (dm/get-prop shape :height) + content (get shape :content) [colors _color-mapping color-mapping-inverse] (retrieve-colors shape)] @@ -187,17 +186,16 @@ {:x x :y y :id id - :data-colors (->> colors (str/join ",")) + :data-colors (str/join "," colors) :data-mapping (-> color-mapping-inverse clj->js js/JSON.stringify) :transform transform :width (if (#{:auto-width} grow-type) 100000 width) :height (if (#{:auto-height :auto-width} grow-type) 100000 height) - :style (-> (obj/create) (attrs/add-layer-props shape)) :ref ref} ;; We use a class here because react has a bug that won't use the appropriate selector for ;; `background-clip` [:style ".text-node { background-clip: text; - -webkit-background-clip: text;" ] + -webkit-background-clip: text; }"] [:& render-node {:index 0 :shape shape :node content}]])) diff --git a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs index 6bfcdf675c..ceca03f536 100644 --- a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs +++ b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs @@ -7,24 +7,14 @@ (ns app.main.ui.shapes.text.fontfaces (:require [app.common.data :as d] - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cfh] [app.main.fonts :as fonts] - [app.main.ui.shapes.embed :as embed] [app.util.object :as obj] - [beicon.core :as rx] + [beicon.v2.core :as rx] [clojure.set :as set] [cuerdas.core :as str] [rumext.v2 :as mf])) -(defn replace-embeds - "Replace into the font-faces of a CSS the URL's that are present in `embed-data` by its - data-uri" - [css urls embed-data] - (letfn [(replace-url [css url] - (str/replace css url (get embed-data url url)))] - (->> urls - (reduce replace-url css)))) - (defn use-fonts-css "Hook that retrieves the CSS of the fonts passed as parameter" [fonts] @@ -38,7 +28,7 @@ (->> (rx/from fonts) (rx/merge-map fonts/fetch-font-css) (rx/reduce conj []) - (rx/subs + (rx/subs! (fn [result] (let [css (str/join "\n" result)] (when-not (= (mf/ref-val fonts-css-ref) css) @@ -48,7 +38,7 @@ (mf/ref-val fonts-css-ref))) -(mf/defc fontfaces-style-render +(mf/defc fontfaces-style-html {::mf/wrap-props false ::mf/wrap [#(mf/memo' % (mf/check-props ["fonts"]))]} [props] @@ -56,43 +46,33 @@ (let [fonts (obj/get props "fonts") ;; Fetch its CSS fontfaces - fonts-css (use-fonts-css fonts) + fonts-css (use-fonts-css fonts)] - ;; Extract from the CSS the URL's to embed - fonts-urls (mf/use-memo - (mf/deps fonts-css) - #(fonts/extract-fontface-urls fonts-css)) + [:style fonts-css])) - - ;; Calculate the data-uris for these fonts - fonts-embed (embed/use-data-uris fonts-urls) - - loading? (some? (d/seek #(not (contains? fonts-embed %)) fonts-urls)) - - ;; Creates a style tag by replacing the urls with the data uri - style (replace-embeds fonts-css fonts-urls fonts-embed)] - - (cond - (d/not-empty? style) - [:style {:data-loading loading?} style] - - (d/not-empty? fonts) - [:style {:data-loading true}]))) +(mf/defc fontfaces-style-render + {::mf/wrap-props false + ::mf/wrap [#(mf/memo' % (mf/check-props ["fonts"]))]} + [props] + (let [fonts (obj/get props "fonts") + ;; Fetch its CSS fontfaces + fonts-css (use-fonts-css fonts)] + [:style fonts-css])) (defn shape->fonts [shape objects] (let [initial (cond-> #{} - (cph/text-shape? shape) + (cfh/text-shape? shape) (into (fonts/get-content-fonts (:content shape))))] - (->> (cph/get-children objects (:id shape)) - (filter cph/text-shape?) + (->> (cfh/get-children objects (:id shape)) + (filter cfh/text-shape?) (map (comp fonts/get-content-fonts :content)) (reduce set/union initial)))) (defn shapes->fonts [shapes] (->> shapes - (filter cph/text-shape?) + (filter cfh/text-shape?) (map (comp fonts/get-content-fonts :content)) (reduce set/union #{}))) diff --git a/frontend/src/app/main/ui/shapes/text/html_text.cljs b/frontend/src/app/main/ui/shapes/text/html_text.cljs index d60810e166..fd3995c38e 100644 --- a/frontend/src/app/main/ui/shapes/text/html_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/html_text.cljs @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.text :as txt] [app.main.ui.shapes.text.styles :as sts] [app.util.object :as obj] [rumext.v2 :as mf])) @@ -18,11 +19,13 @@ (let [node (obj/get props "node") parent (obj/get props "parent") shape (obj/get props "shape") + code? (obj/get props "code?") text (:text node) style (if (= text "") (sts/generate-text-styles shape parent) - (sts/generate-text-styles shape node))] - [:span.text-node {:style style} + (sts/generate-text-styles shape node)) + class (when code? (:$id node))] + [:span.text-node {:style style :class class} (if (= text "") "\u00A0" text)])) (mf/defc render-root @@ -31,19 +34,25 @@ (let [node (obj/get props "node") children (obj/get props "children") shape (obj/get props "shape") - style (sts/generate-root-styles shape node)] + code? (obj/get props "code?") + style (sts/generate-root-styles shape node code?) + class (when code? (:$id node))] [:div.root.rich-text {:style style + :class class :xmlns "http://www.w3.org/1999/xhtml"} children])) (mf/defc render-paragraph-set {::mf/wrap-props false} [props] - (let [children (obj/get props "children") + (let [node (obj/get props "node") + children (obj/get props "children") shape (obj/get props "shape") - style (sts/generate-paragraph-set-styles shape)] - [:div.paragraph-set {:style style} children])) + code? (obj/get props "code?") + style (when-not code? (sts/generate-paragraph-set-styles shape)) + class (when code? (:$id node))] + [:div.paragraph-set {:style style :class class} children])) (mf/defc render-paragraph {::mf/wrap-props false} @@ -51,15 +60,18 @@ (let [node (obj/get props "node") shape (obj/get props "shape") children (obj/get props "children") - style (sts/generate-paragraph-styles shape node) + code? (obj/get props "code?") + style (when-not code? (sts/generate-paragraph-styles shape node)) + class (when code? (:$id node)) dir (:text-direction node "auto")] - [:p.paragraph {:style style :dir dir} children])) + [:p.paragraph {:style style :dir dir :class class} children])) ;; -- Text nodes (mf/defc render-node {::mf/wrap-props false} [props] - (let [{:keys [type text children] :as parent} (obj/get props "node")] + (let [{:keys [type text children] :as parent} (obj/get props "node") + code? (obj/get props "code?")] (if (string? text) [:> render-text props] (let [component (case type @@ -74,7 +86,8 @@ (obj/set! "node" node) (obj/set! "parent" parent) (obj/set! "index" index) - (obj/set! "key" index))] + (obj/set! "key" index) + (obj/set! "code?" code?))] [:> render-node props]))]))))) (mf/defc text-shape @@ -83,23 +96,32 @@ [props ref] (let [shape (obj/get props "shape") grow-type (obj/get props "grow-type") - {:keys [id x y width height content]} shape] + code? (obj/get props "code?") + {:keys [id x y width height content]} shape + + content (if code? (txt/index-content content) content) + + style + (when-not code? + #js {:position "fixed" + :left 0 + :top 0 + :background "white" + :width (if (#{:auto-width} grow-type) 100000 width) + :height (if (#{:auto-height :auto-width} grow-type) 100000 height)})] [:div.text-node-html {:id (dm/str "html-text-node-" id) :ref ref :data-x x :data-y y - :style {:position "fixed" - :left 0 - :top 0 - :background "white" - :width (if (#{:auto-width} grow-type) 100000 width) - :height (if (#{:auto-height :auto-width} grow-type) 100000 height)}} + :style style} ;; We use a class here because react has a bug that won't use the appropriate selector for ;; `background-clip` - [:style ".text-node { background-clip: text; - -webkit-background-clip: text;" ] + (when (not code?) + [:style ".text-node { background-clip: text; + -webkit-background-clip: text; }"]) [:& render-node {:index 0 :shape shape - :node content}]])) + :node content + :code? code?}]])) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 5ad25a7dfd..c8fbf20533 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -6,26 +6,29 @@ (ns app.main.ui.shapes.text.styles (:require + [app.common.colors :as cc] [app.common.data :as d] [app.common.text :as txt] [app.common.transit :as transit] [app.main.fonts :as fonts] + [app.main.ui.formats :as fmt] [app.util.color :as uc] [app.util.object :as obj] [cuerdas.core :as str])) (defn generate-root-styles - [{:keys [width height]} node] - (let [valign (:vertical-align node "top") - base #js {:height height - :width width - :fontFamily "sourcesanspro" - :display "flex" - :whiteSpace "break-spaces"}] - (cond-> base - (= valign "top") (obj/set! "alignItems" "flex-start") - (= valign "center") (obj/set! "alignItems" "center") - (= valign "bottom") (obj/set! "alignItems" "flex-end")))) + ([props node] + (generate-root-styles props node false)) + ([{:keys [width height]} node code?] + (let [valign (:vertical-align node "top") + base #js {:height (when-not code? (fmt/format-pixels height)) + :width (when-not code? (fmt/format-pixels width)) + :display "flex" + :whiteSpace "break-spaces"}] + (cond-> base + (= valign "top") (obj/set! "alignItems" "flex-start") + (= valign "center") (obj/set! "alignItems" "center") + (= valign "bottom") (obj/set! "alignItems" "flex-end"))))) (defn generate-paragraph-set-styles [{:keys [grow-type] :as shape}] @@ -48,7 +51,8 @@ [_shape data] (let [line-height (:line-height data 1.2) text-align (:text-align data "start") - base #js {:fontSize (str (:font-size data (:font-size txt/default-text-attrs)) "px") + base #js {;; Fix a problem when exporting HTML + :fontSize 0 ;;(str (:font-size data (:font-size txt/default-text-attrs)) "px") :lineHeight (:line-height data (:line-height txt/default-text-attrs)) :margin 0}] (cond-> base @@ -72,17 +76,25 @@ font-size (:font-size data) fill-color (or (-> data :fills first :fill-color) (:fill-color data)) fill-opacity (or (-> data :fills first :fill-opacity) (:fill-opacity data)) + fill-gradient (or (-> data :fills first :fill-color-gradient) (:fill-color-gradient data)) - [r g b a] (uc/hex->rgba fill-color fill-opacity) + [r g b a] (cc/hex->rgba fill-color fill-opacity) text-color (when (and (some? fill-color) (some? fill-opacity)) (str/format "rgba(%s, %s, %s, %s)" r g b a)) + gradient? (some? fill-gradient) + + text-color (if gradient? + (uc/color->background {:gradient fill-gradient}) + text-color) + fontsdb (deref fonts/fontsdb) base #js {:textDecoration text-decoration :textTransform text-transform - :color (if show-text? text-color "transparent") - :caretColor (or text-color "black") + :color (if (and show-text? (not gradient?)) text-color "transparent") + :background (when (and show-text? gradient?) text-color) + :caretColor (if (and (not gradient?) text-color) text-color "black") :overflowWrap "initial" :lineBreak "auto" :whiteSpace "break-spaces" diff --git a/frontend/src/app/main/ui/shapes/text/svg_text.cljs b/frontend/src/app/main/ui/shapes/text/svg_text.cljs index e508f0bbfc..20c8e8edc2 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -13,6 +13,7 @@ [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]] + [app.main.ui.shapes.fills :as fills] [app.main.ui.shapes.gradients :as grad] [app.util.object :as obj] [rumext.v2 :as mf])) @@ -50,10 +51,11 @@ :y y :width width :height height} - (attrs/add-style-attrs shape render-id)) + (attrs/add-fill-props! shape render-id) + (attrs/add-border-props! shape)) get-gradient-id (fn [index] - (str render-id "_" (:id shape) "_" index))] + (str render-id "-" (:id shape) "-" index))] [:* ;; Definition of gradients for partial elements @@ -61,7 +63,7 @@ [:defs (for [[index data] (d/enumerate position-data)] (when (some? (:fill-color-gradient data)) - (let [id (dm/str "fill-color-gradient_" (get-gradient-id index))] + (let [id (dm/str "fill-color-gradient-" (get-gradient-id index))] [:& grad/gradient {:id id :key id :attr :fill-color-gradient @@ -96,8 +98,15 @@ (obj/set! "fill" (str "url(#fill-" index "-" render-id ")")))} (cond-> browser-props (obj/merge! browser-props))) - shape (assoc shape :fills (:fills data))] + shape (assoc shape :fills (:fills data)) + + ;; Need to create new render-id per text-block + render-id (dm/str render-id "-" index)] + + [:& (mf/provider muc/render-id) {:key index :value render-id} + ;; Text fills definition. Need to be defined per-text block + [:defs + [:& fills/fills {:shape shape :render-id render-id}]] - [:& (mf/provider muc/render-id) {:key index :value (str render-id "_" (:id shape) "_" index)} [:& shape-custom-strokes {:shape shape :position index :render-id render-id} [:> :text props (:text data)]]]))]])) diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index 9164f1041a..54b02f50b2 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -5,68 +5,147 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.static + (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] + [app.common.pprint :as pp] [app.main.store :as st] [app.main.ui.icons :as i] + [app.util.dom :as dom] [app.util.globals :as globals] [app.util.i18n :refer [tr]] - [app.util.object :as obj] [app.util.router :as rt] + [app.util.webapi :as wapi] [rumext.v2 :as mf])) -(mf/defc static-header +(mf/defc error-container {::mf/wrap-props false} - [props] - (let [children (obj/get props "children") - on-click (mf/use-callback #(set! (.-href globals/location) "/"))] - [:section.exception-layout - [:div.exception-header - {:on-click on-click} - i/logo] - [:div.exception-content - [:div.container children]]])) + [{:keys [children]}] + (let [on-click (mf/use-callback #(set! (.-href globals/location) "/"))] + [:section {:class (stl/css :exception-layout)} + [:button + {:class (stl/css :exception-header) + :on-click on-click} + i/logo-icon] + [:div {:class (stl/css :deco-before)} i/logo-error-screen] + + [:div {:class (stl/css :exception-content)} + [:div {:class (stl/css :container)} children]] + + [:div {:class (stl/css :deco-after)} i/logo-error-screen]])) + +(mf/defc invalid-token + [] + [:> error-container {} + [:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")] + [:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]]) (mf/defc not-found [] - [:> static-header {} - [:div.image i/icon-empty] - [:div.main-message (tr "labels.not-found.main-message")] - [:div.desc-message (tr "labels.not-found.desc-message")]]) + [:> error-container {} + [:div {:class (stl/css :main-message)} (tr "labels.not-found.main-message")] + [:div {:class (stl/css :desc-message)} (tr "labels.not-found.desc-message")]]) (mf/defc bad-gateway [] - [:> static-header {} - [:div.image i/icon-empty] - [:div.main-message (tr "labels.bad-gateway.main-message")] - [:div.desc-message (tr "labels.bad-gateway.desc-message")] - [:div.sign-info - [:a.btn-primary.btn-small - {:on-click (fn [] (st/emit! #(dissoc % :exception)))} - (tr "labels.retry")]]]) + (let [handle-retry + (mf/use-callback + (fn [] (st/emit! (rt/assign-exception nil))))] + [:> error-container {} + [:div {:class (stl/css :main-message)} (tr "labels.bad-gateway.main-message")] + [:div {:class (stl/css :desc-message)} (tr "labels.bad-gateway.desc-message")] + [:div {:class (stl/css :sign-info)} + [:button {:on-click handle-retry} (tr "labels.retry")]]])) (mf/defc service-unavailable [] - [:> static-header {} - [:div.image i/icon-empty] - [:div.main-message (tr "labels.service-unavailable.main-message")] - [:div.desc-message (tr "labels.service-unavailable.desc-message")] - [:div.sign-info - [:a.btn-primary.btn-small - {:on-click (fn [] (st/emit! #(dissoc % :exception)))} - (tr "labels.retry")]]]) + (let [on-click (mf/use-fn #(st/emit! (rt/assign-exception nil)))] + [:> error-container {} + [:div {:class (stl/css :main-message)} (tr "labels.service-unavailable.main-message")] + [:div {:class (stl/css :desc-message)} (tr "labels.service-unavailable.desc-message")] + [:div {:class (stl/css :sign-info)} + [:button {:on-click on-click} (tr "labels.retry")]]])) + + +(defn generate-report + [data] + (try + (let [team-id (:current-team-id @st/state) + profile-id (:profile-id @st/state) + + trace (:app.main.errors/trace data) + instance (:app.main.errors/instance data) + content (with-out-str + (println "Hint: " (or (:hint data) (ex-message instance) "--")) + (println "Prof ID:" (str (or profile-id "--"))) + (println "Team ID:" (str (or team-id "--"))) + + (when-let [file-id (:file-id data)] + (println "File ID:" (str file-id))) + + (println) + + (println "Data:") + (loop [data data] + (-> (d/without-qualified data) + (dissoc :explain) + (d/update-when :data (constantly "(...)")) + (pp/pprint {:level 8 :length 10})) + + (println) + + (when-let [explain (:explain data)] + (print explain)) + + (when (and (= :server-error (:type data)) + (contains? data :data)) + (recur (:data data)))) + + (println "Trace:") + (println trace) + (println) + + (println "Last events:") + (pp/pprint @st/last-events {:length 200}) + + (println))] + (wapi/create-blob content "text/plain")) + (catch :default err + (.error js/console err) + nil))) + (mf/defc internal-error - [] - [:> static-header {} - [:div.image i/icon-empty] - [:div.main-message (tr "labels.internal-error.main-message")] - [:div.desc-message (tr "labels.internal-error.desc-message")] - [:div.sign-info - [:a.btn-primary.btn-small - {:on-click (fn [] (st/emit! (rt/assign-exception nil)))} - (tr "labels.retry")]]]) + {::mf/props :obj} + [{:keys [data]}] + (let [on-click (mf/use-fn #(st/emit! (rt/assign-exception nil))) + report-uri (mf/use-ref nil) + report (mf/use-memo (mf/deps data) #(generate-report data)) + + on-download + (mf/use-fn + (fn [event] + (dom/prevent-default event) + (when-let [uri (mf/ref-val report-uri)] + (dom/trigger-download-uri "report" "text/plain" uri))))] + + (mf/with-effect [report] + (when (some? report) + (let [uri (wapi/create-uri report)] + (mf/set-ref-val! report-uri uri) + (fn [] + (wapi/revoke-uri uri))))) + + [:> error-container {} + [:div {:class (stl/css :main-message)} (tr "labels.internal-error.main-message")] + [:div {:class (stl/css :desc-message)} (tr "labels.internal-error.desc-message")] + (when (some? report) + [:a {:on-click on-download} "Download report.txt"]) + [:div {:class (stl/css :sign-info)} + [:button {:on-click on-click} (tr "labels.retry")]]])) (mf/defc exception-page + {::mf/props :obj} [{:keys [data] :as props}] (case (:type data) :not-found @@ -78,5 +157,4 @@ :service-unavailable [:& service-unavailable] - [:& internal-error])) - + [:> internal-error props])) diff --git a/frontend/src/app/main/ui/static.scss b/frontend/src/app/main/ui/static.scss new file mode 100644 index 0000000000..6fc83378ce --- /dev/null +++ b/frontend/src/app/main/ui/static.scss @@ -0,0 +1,100 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.exception-layout { + width: 100%; + height: 100%; + background-color: $db-secondary; +} + +.deco-before, +.deco-after { + position: absolute; + left: calc(50% - $s-40); + + svg { + position: absolute; + fill: $df-secondary; + height: 1537px; + width: $s-80; + } +} + +.deco-before { + height: 34vh; + top: 0; + svg { + bottom: 0; + } +} + +.deco-after { + height: 34vh; + bottom: 0; + svg { + top: 0; + } +} + +.exception-header { + padding: $s-24 $s-32; + position: fixed; + background: none; + border: none; + cursor: pointer; + svg { + fill: $df-primary; + width: $s-48; + height: auto; + } +} + +.exception-content { + display: flex; + height: 100%; + justify-content: center; + width: 100%; + + .container { + align-items: center; + display: flex; + flex-direction: column; + gap: $s-16; + height: 34vh; + justify-content: center; + margin-top: 33vh; + text-align: center; + width: $s-640; + } +} + +.main-message { + @include bigTitleTipography; + color: $df-primary; +} + +.desc-message { + @include bigTitleTipography; + color: $df-secondary; +} + +.sign-info { + text-align: center; + button { + @extend .button-primary; + text-transform: uppercase; + padding: $s-8 $s-16; + font-size: $fs-11; + } +} + +.image { + svg { + fill: $df-primary; + } +} diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index ad8e0f594a..6883a6d247 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -5,14 +5,14 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer - (:import goog.events.EventType) + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes.bounds :as gsb] - [app.common.pages.helpers :as cph] [app.common.text :as txt] [app.common.types.shape.interactions :as ctsi] [app.main.data.comments :as dcm] @@ -24,7 +24,6 @@ [app.main.ui.context :as ctx] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] - [app.main.ui.static :as static] [app.main.ui.viewer.comments :refer [comments-layer comments-sidebar]] [app.main.ui.viewer.header :as header] [app.main.ui.viewer.inspect :as inspect] @@ -37,6 +36,7 @@ [app.util.globals :as globals] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] + [app.util.object :as obj] [app.util.webapi :as wapi] [cuerdas.core :as str] [goog.events :as events] @@ -91,18 +91,30 @@ :vbox (str "0 0 " width " " height)}))) (mf/defc viewer-pagination - [{:keys [index num-frames left-bar right-bar] :as props}] - [:* - (when (pos? index) - [:div.viewer-go-prev {:class (when left-bar "left-bar")} - [:div.arrow {:on-click #(st/emit! dv/select-prev-frame)} i/go-prev]]) - (when (< (+ index 1) num-frames) - [:div.viewer-go-next {:class (when right-bar "right-bar")} - [:div.arrow {:on-click #(st/emit! dv/select-next-frame)} i/go-next]]) - [:div.viewer-bottom {:class (when left-bar "left-bar")} - [:div.reset {:on-click #(st/emit! dv/select-first-frame)} i/reset] - [:div.counter (str/join " / " [(+ index 1) num-frames])] - [:span]]]) + [{:keys [index num-frames left-bar right-bar comment-sidebar] :as props}] + (let [go-prev-frame (mf/use-fn #(st/emit! dv/select-prev-frame)) + go-next-frame (mf/use-fn #(st/emit! dv/select-next-frame)) + go-first-frame (mf/use-fn #(st/emit! dv/select-first-frame))] + [:* + (when (pos? index) + [:button {:class (stl/css-case :viewer-go-prev true + :left-bar left-bar) + :on-click go-prev-frame} + i/arrow]) + (when (< (+ index 1) num-frames) + [:button {:class (stl/css-case :viewer-go-next true + :comment-sidebar comment-sidebar + :right-bar right-bar) + :on-click go-next-frame} + i/arrow]) + [:div {:class (stl/css-case :viewer-bottom true + :left-bar left-bar)} + [:button {:on-click go-first-frame + :class (stl/css :reset-button)} + i/reload] + [:span {:class (stl/css :counter)} + (str/join " / " [(+ index 1) num-frames])] + [:span]]])) (mf/defc viewer-pagination-and-sidebar {::mf/wrap [mf/memo]} @@ -113,7 +125,7 @@ [:& viewer-pagination {:index index :num-frames (count (:frames page)) - :right-bar show-sidebar?}] + :comment-sidebar show-sidebar?}] (when show-sidebar? [:& comments-sidebar @@ -127,6 +139,7 @@ background-overlay? (:background-overlay overlay) overlay-frame (:frame overlay) overlay-position (:position overlay) + fixed-base? (:fixed-source? overlay) size (mf/with-memo [page overlay zoom] @@ -147,35 +160,57 @@ [:* (when (or close-click-outside? background-overlay?) - [:div.viewer-overlay-background - {:class (dom/classnames :visible background-overlay?) - :style {:width (:width wrapper-size) - :height (:height wrapper-size) - :position "absolute" - :left 0 - :top 0} - :on-click on-click}]) + [:div {:class (stl/css-case :viewer-overlay-background true + :visible background-overlay?) + :style {:width (:width wrapper-size) + :height (:height wrapper-size) + :position "absolute" + :left 0 + :top 0} + :on-click on-click}]) - [:div.viewport-container.viewer-overlay - {:id (dm/str "overlay-" (:id overlay-frame)) - :style {:width (:width size) - :height (:height size) - :left (* (:x overlay-position) zoom) - :top (* (:y overlay-position) zoom)}} - - [:& interactions/viewport - {:frame overlay-frame - :base-frame frame - :frame-offset overlay-position - :size size - :delta delta - :page page - :interactions-mode interactions-mode}]]])) + (if fixed-base? + [:div {:class (stl/css :viewport-container-wrapper) + :style {:position "absolute" + :left (* (:x overlay-position) zoom) + :top (* (:y overlay-position) zoom) + :width (:width size) + :height (:height size) + :z-index 2}} + [:div {:class (stl/css :viewer-overlay :viewport-container) + :id (dm/str "overlay-" (:id overlay-frame)) + :style {:width (:width size) + :height (:height size) + :position "fixed"}} + [:& interactions/viewport + {:frame overlay-frame + :base-frame frame + :frame-offset overlay-position + :size size + :delta delta + :page page + :interactions-mode interactions-mode}]]] + [:div {:class (stl/css :viewer-overlay :viewport-container) + :id (dm/str "overlay-" (:id overlay-frame)) + :style {:width (:width size) + :height (:height size) + :left (* (:x overlay-position) zoom) + :top (* (:y overlay-position) zoom)}} + [:& interactions/viewport + {:frame overlay-frame + :base-frame frame + :frame-offset overlay-position + :size size + :delta delta + :page page + :interactions-mode interactions-mode}]])])) (mf/defc viewer-wrapper + {::mf/wrap-props false} [{:keys [wrapper-size orig-frame orig-viewport-ref orig-size page file users current-viewport-ref - size frame interactions-mode overlays zoom section index] :as props}] + size frame interactions-mode overlays zoom section index]}] + [:* [:& viewer-pagination-and-sidebar {:section section @@ -185,16 +220,17 @@ :frame frame :interactions-mode interactions-mode}] - [:div.viewer-wrapper - {:style {:width (:width wrapper-size) - :height (:height wrapper-size)}} - [:div.viewer-clipper + [:div {:class (stl/css :viewer-wrapper) + :style {:width (:width wrapper-size) + :height (:height wrapper-size)}} + [:div {:class (stl/css :viewer-clipper)} + (when orig-frame - [:div.viewport-container - {:ref orig-viewport-ref - :style {:width (:width orig-size) - :height (:height orig-size) - :position "relative"}} + [:div {:class (stl/css :viewport-container) + :ref orig-viewport-ref + :style {:width (:width orig-size) + :height (:height orig-size) + :position "relative"}} [:& interactions/viewport {:frame orig-frame @@ -205,11 +241,11 @@ :users users :interactions-mode interactions-mode}]]) - [:div.viewport-container - {:ref current-viewport-ref - :style {:width (:width size) - :height (:height size) - :position "relative"}} + [:div {:class (stl/css :viewport-container) + :ref current-viewport-ref + :style {:width (:width size) + :height (:height size) + :position "relative"}} [:& interactions/viewport {:frame frame @@ -220,13 +256,14 @@ :interactions-mode interactions-mode}] (for [overlay overlays] - [:& viewer-overlay {:overlay overlay - :key (dm/str (:id overlay)) - :page page - :frame frame - :zoom zoom - :wrapper-size wrapper-size - :interactions-mode interactions-mode}])]] + [:& viewer-overlay + {:overlay overlay + :key (dm/str (:id overlay)) + :page page + :frame frame + :zoom zoom + :wrapper-size wrapper-size + :interactions-mode interactions-mode}])]] (when (= section :comments) @@ -236,12 +273,10 @@ :page page :zoom zoom}])]]) -(mf/defc viewer - [{:keys [params data]}] - - (let [{:keys [page-id share-id section index interactions-mode]} params - {:keys [file users project permissions]} data - +(mf/defc viewer-content + {::mf/wrap-props false} + [{:keys [data page-id share-id section index interactions-mode] :as props}] + (let [{:keys [file users project permissions]} data allowed (or (= section :interactions) (and (= section :comments) @@ -273,7 +308,7 @@ (hooks/use-equal-memo (->> (:objects page) (vals) - (filter cph/text-shape?))) + (filter cfh/text-shape?))) zoom (:zoom local) zoom-type (:zoom-type local) @@ -295,7 +330,8 @@ size (mf/with-memo [frame zoom] - (calculate-size (:objects page) frame zoom)) + (when frame + (calculate-size (:objects page) frame zoom))) orig-size (mf/with-memo [orig-frame zoom] @@ -307,17 +343,17 @@ (calculate-wrapper size orig-size zoom)) click-on-screen - (mf/use-callback + (mf/use-fn (fn [event] (let [origin (dom/get-target event) - over-section? (dom/class? origin "viewer-section") + over-section? (dom/get-data origin "viewer-section") layout (dom/get-element "viewer-layout") - has-force? (dom/class? layout "force-visible")] + has-force? (dom/get-data layout "force-visible")] (when over-section? - (if has-force? - (dom/remove-class! layout "force-visible") - (dom/add-class! layout "force-visible")))))) + (if (= has-force? "true") + (dom/set-data! layout "force-visible" false) + (dom/set-data! layout "force-visible" true)))))) on-click (mf/use-fn @@ -337,10 +373,10 @@ (mf/use-fn (fn [event] (let [event (.getBrowserEvent ^js event) - wrapper (dom/get-element-by-class "inspect-svg-wrapper") - section (dom/get-element-by-class "inspect-svg-container") + wrapper (dom/get-element "inspect-svg-wrapper") + section (dom/get-element "inspect-svg-container") target (.-target event)] - (when (or (dom/child? target wrapper) (dom/class? target "inspect-svg-container")) + (when (or (dom/child? target wrapper) (dom/id? target "inspect-svg-container")) (let [norm-event ^js (nw/normalize-wheel event) mod? (kbd/mod? event) shift? (kbd/shift? event) @@ -356,9 +392,13 @@ (if shift? (dom/set-h-scroll-pos! section new-scroll-pos) (dom/set-scroll-pos! section new-scroll-pos))))))))) + on-thumbnails-close + (mf/use-fn + #(st/emit! dv/close-thumbnails-panel)) + on-exit-fullscreen - (mf/use-callback + (mf/use-fn (fn [] (when (not (dom/fullscreen?)) (st/emit! (dv/exit-fullscreen)))))] @@ -380,8 +420,8 @@ (mf/with-effect [] (let [events - [(events/listen globals/window EventType.CLICK on-click) - (events/listen (mf/ref-val viewer-section-ref) EventType.WHEEL on-wheel #js {"passive" false})]] + [(events/listen globals/window "click" on-click) + (events/listen (mf/ref-val viewer-section-ref) "wheel" on-wheel #js {"passive" false})]] (doseq [event dom/fullscreen-events] (.addEventListener globals/document event on-exit-fullscreen false)) @@ -417,7 +457,9 @@ fullscreen-dom? (dom/fullscreen?)] (when (not= fullscreen? fullscreen-dom?) (if fullscreen? - (wapi/request-fullscreen wrapper) + (let [layout (dom/get-element "viewer-layout")] + (dom/set-data! layout "force-visible" false) + (wapi/request-fullscreen wrapper)) (wapi/exit-fullscreen)))))) (mf/use-effect @@ -437,7 +479,7 @@ nil) ;; Navigate animation needs to be started after navigation ;; is complete, and we have the next page index. - (let [nav-animation (d/seek #(= (:kind %) :go-to-frame) (vals current-animations))] + (let [nav-animation (d/seek #(= (:kind %) :go-to-frame) (vals current-animations))] (when nav-animation (let [orig-viewport (mf/ref-val orig-viewport-ref) current-viewport (mf/ref-val current-viewport-ref)] @@ -495,40 +537,39 @@ (run! fonts/ensure-loaded! fonts)))) [:div#viewer-layout - {:class (dom/classnames + {:class (stl/css-case :force-visible (:show-thumbnails local) :viewer-layout (not= section :inspect) - :inspect-layout (= section :inspect) - :fullscreen fullscreen?)} + :inspect-layout (= section :inspect)) + :data-fullscreen fullscreen? + :data-force-visible (:show-thumbnails local)} + + + [:div {:class (stl/css :viewer-content)} + + + [:button {:on-click on-thumbnails-close + :class (stl/css-case :thumbnails-close true + :invisible (not (:show-thumbnails local false)))}] - [:div.viewer-content - [:& header/header {:project project - :index index - :file file - :page page - :frame frame - :permissions permissions - :zoom zoom - :section section - :interactions-mode interactions-mode}] - [:div.thumbnail-close {:on-click #(st/emit! dv/close-thumbnails-panel) - :class (dom/classnames :invisible (not (:show-thumbnails local false)))}] [:& thumbnails-panel {:frames frames :show? (:show-thumbnails local false) :page page :index index :thumbnail-data (:thumbnails file)}] - [:section.viewer-section {:id "viewer-section" - :ref viewer-section-ref - :class (if fullscreen? "fullscreen" "") + + [:section#viewer-section {:ref viewer-section-ref + :data-viewer-section true + :class (stl/css-case :viewer-section true + :fulscreen fullscreen?) :on-click click-on-screen} (cond (empty? frames) - [:section.empty-state + [:section {:class (stl/css :empty-state)} [:span (tr "viewer.empty-state")]] (nil? frame) - [:section.empty-state + [:section {:class (stl/css :empty-state)} (when (some? index) [:span (tr "viewer.frame-not-found")])] @@ -546,7 +587,6 @@ :interactions-mode interactions-mode :share-id share-id}] - [:& (mf/provider ctx/current-zoom) {:value zoom} [:& viewer-wrapper {:wrapper-size wrapper-size @@ -563,28 +603,36 @@ :overlays overlays :zoom zoom :section section - :index index}]]))]]])) + :index index}]]))]] -;; --- Component: Viewer Page + [:& header/header {:project project + :index index + :file file + :page page + :frame frame + :permissions permissions + :zoom zoom + :section section + :shown-thumbnails (:show-thumbnails local) + :interactions-mode interactions-mode}]])) -(mf/defc viewer-page - [{:keys [file-id] :as props}] +;; --- Component: Viewer - (mf/with-effect [file-id] - (st/emit! (dv/initialize props)) - (fn [] - (st/emit! (dv/finalize props)))) +(mf/defc viewer + {::mf/wrap-props false} + [{:keys [file-id share-id page-id] :as props}] + (mf/with-effect [file-id page-id share-id] + (let [params {:file-id file-id + :page-id page-id + :share-id share-id}] + (st/emit! (dv/initialize params)) + (fn [] + (st/emit! (dv/finalize params))))) (if-let [data (mf/deref refs/viewer-data)] - (let [key (str (get-in data [:file :id]))] - [:& viewer {:params props :data data :key key}]) + (let [props (obj/merge props #js {:data data :key (dm/str file-id)})] + [:> viewer-content props]) - [:div.loader-content.viewer-loader + [:div {:class (stl/css :loader-content)} i/loader-pencil])) -(mf/defc breaking-change-notice - [] - [:> static/static-header {} - [:div.image i/unchain] - [:div.main-message (tr "viewer.breaking-change.message")] - [:div.desc-message (tr "viewer.breaking-change.description")]]) diff --git a/frontend/src/app/main/ui/viewer.scss b/frontend/src/app/main/ui/viewer.scss new file mode 100644 index 0000000000..14dabe4c29 --- /dev/null +++ b/frontend/src/app/main/ui/viewer.scss @@ -0,0 +1,219 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.viewer-layout { + height: 100vh; + display: grid; + grid-template-rows: $s-48 auto; + grid-template-columns: 1fr; + user-select: none; +} + +.viewer-content { + overflow: hidden; + grid-row: 2 / span 1; + display: grid; + grid-template-rows: $s-252 auto; + grid-template-columns: 1fr; + background-color: var(--viewer-background-color); +} + +.empty-state { + @include bodySmallTypography; + color: var(--empty-message-foreground-color); + display: grid; + place-items: center; + height: 100%; + width: 100%; +} + +.viewer-header { + grid-row: 1 / span 1; +} + +.inspect-layout { + display: grid; + grid-template-rows: $s-48 auto; + grid-template-columns: 1fr; + height: 100vh; + margin-top: 0; + user-select: none; +} + +.thumbnails-close { + @include buttonStyle; + grid-row: 1 / span 2; + grid-column: 1 / span 1; + z-index: $z-index-10; + background-color: var(--overlay-color); +} + +.thumbnails-close.invisible { + display: none; +} + +.viewer-section { + @extend .new-scrollbar; + grid-row: 1 / span 2; + grid-column: 1 / span 1; + display: flex; + align-items: center; + flex-wrap: nowrap; + height: calc(100vh - $s-48); + flex-flow: wrap; + overflow: auto; +} + +.inspect-layout .viewer-section { + flex-wrap: nowrap; + margin-top: 0; + height: 100%; + overflow: hidden; +} + +.viewer-go-prev, +.viewer-go-next { + @extend .button-secondary; + @include flexCenter; + position: absolute; + right: $s-8; + height: $s-64; + width: $s-32; + top: calc(50vh - $s-32); + z-index: $z-index-2; + background-color: var(--viewer-controls-background-color); + transition: transform 400ms ease 300ms; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.viewer-go-next.comment-sidebar { + right: $s-280; +} + +.viewer-go-prev { + left: $s-8; + right: unset; + svg { + transform: rotate(180deg); + } +} + +.viewer-bottom { + position: fixed; + bottom: 0; + display: flex; + align-items: flex-start; + justify-content: space-between; + width: 100%; + height: $s-40; + padding-right: 0 $s-8 $s-40 $s-8; + transition: transform 400ms ease 300ms; + z-index: $z-index-2; +} + +.reset-button { + @extend .button-secondary; + @include flexCenter; + height: $s-32; + width: $s-28; + margin-left: $s-8; + background-color: var(--viewer-controls-background-color); + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.counter { + @include flexCenter; + @include bodySmallTypography; + border-radius: $br-8; + width: $s-64; + height: $s-32; + color: var(--viewer-thumbnails-control-foreground-color); + background-color: var(--viewer-controls-background-color); +} + +.viewer-wrapper { + position: relative; + margin: 0 auto; +} + +.viewer-clipper { + display: grid; + grid-template-rows: 1fr; + grid-template-columns: 1fr; + justify-items: center; + align-items: center; + overflow: hidden; +} + +.viewer-overlay-background { + position: absolute; + top: 0; + left: 0; + + &.visible { + background-color: rgb(0, 0, 0, 0.2); + } +} + +.viewer-overlay { + position: absolute; +} + +.viewport-container { + clip-path: inset(0 0 0 0); + grid-column: 1 / 1; + grid-row: 1 / 1; + + .not-fixed { + position: absolute; + } + + .fixed { + position: fixed; + pointer-events: none; + + .frame-children g { + pointer-events: auto; + } + } +} + +.loader-content { + @extend .loader-base; +} + +/** FULLSCREEN */ +[data-fullscreen="true"] .viewer-bottom { + transform: translateY($s-40); +} + +[data-force-visible="true"] .viewer-bottom { + transform: translateY(0); +} + +[data-fullscreen="true"] .viewer-go-next { + transform: translateX($s-40); +} + +[data-fullscreen="true"] .viewer-go-prev { + transform: translateX(-$s-40); +} + +[data-force-visible="true"] .viewer-go-next { + transform: translateX(0); +} + +[data-force-visible="true"] .viewer-go-prev { + transform: translateX(0); +} diff --git a/frontend/src/app/main/ui/viewer/comments.cljs b/frontend/src/app/main/ui/viewer/comments.cljs index 116b5dcce9..c5677e0fb1 100644 --- a/frontend/src/app/main/ui/viewer/comments.cljs +++ b/frontend/src/app/main/ui/viewer/comments.cljs @@ -5,9 +5,13 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.comments + (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.main.data.comments :as dcm] [app.main.data.events :as ev] @@ -23,62 +27,97 @@ [rumext.v2 :as mf])) (mf/defc comments-menu - {::mf/wrap [mf/memo] - ::mf/wrap-props false} + {::mf/props :obj + ::mf/memo true} [] - (let [{cmode :mode cshow :show show-sidebar? :show-sidebar?} (mf/deref refs/comments-local) + (let [state (mf/deref refs/comments-local) + cmode (:mode state) + cshow (:show state) + show-sidebar? (:show-sidebar? state false) show-dropdown? (mf/use-state false) toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) update-mode - (mf/use-callback - (fn [mode] - (st/emit! (dcm/update-filters {:mode mode})))) + (mf/use-fn + (fn [event] + (let [mode (-> (dom/get-current-target event) + (dom/get-data "value") + (keyword))] + (st/emit! (dcm/update-filters {:mode mode}))))) update-show - (mf/use-callback - (fn [mode] - (st/emit! (dcm/update-filters {:show mode})))) + (mf/use-fn + (fn [event] + (let [mode (-> (dom/get-current-target event) + (dom/get-data "value") + (keyword)) + mode (if (= :pending mode) :all :pending)] + (st/emit! (dcm/update-filters {:show mode}))))) update-options - (mf/use-callback - (fn [mode] - (st/emit! (dcm/update-options {:show-sidebar? mode}))))] + (mf/use-fn + (fn [event] + (let [mode (-> (dom/get-current-target event) + (dom/get-data "value") + (parse-boolean))] + (st/emit! (dcm/update-options {:show-sidebar? (not mode)})))))] + + [:div {:class (stl/css :view-options) + :on-click toggle-dropdown} + [:span {:class (stl/css :dropdown-title)} (tr "labels.comments")] + [:span {:class (stl/css :icon-dropdown)} i/arrow] - [:div.view-options {:on-click toggle-dropdown} - [:span.label (tr "labels.comments")] - [:span.icon i/arrow-down] [:& dropdown {:show @show-dropdown? :on-close hide-dropdown} + [:ul {:class (stl/css :dropdown)} - [:ul.dropdown.with-check - [:li {:class (dom/classnames :selected (or (= :all cmode) (nil? cmode))) - :on-click #(update-mode :all)} - [:span.icon i/tick] - [:span.label (tr "labels.show-all-comments")]] + [:li {:class (stl/css-case + :dropdown-element true + :selected (or (= :all cmode) (nil? cmode))) + :data-value "all" + :on-click update-mode} + [:span {:class (stl/css :label)} (tr "labels.show-all-comments")] + (when (or (= :all cmode) (nil? cmode)) + [:span {:class (stl/css :icon)} i/tick])] - [:li {:class (dom/classnames :selected (= :yours cmode)) - :on-click #(update-mode :yours)} - [:span.icon i/tick] - [:span.label (tr "labels.show-your-comments")]] + [:li {:class (stl/css-case + :dropdown-element true + :selected (= :yours cmode)) + :data-value "yours" + :on-click update-mode} + [:span {:class (stl/css :label)} (tr "labels.show-your-comments")] + (when (= :yours cmode) + [:span {:class (stl/css :icon)} + i/tick])] - [:hr] + [:li {:class (stl/css :separator)}] - [:li {:class (dom/classnames :selected (= :pending cshow)) - :on-click #(update-show (if (= :pending cshow) :all :pending))} - [:span.icon i/tick] - [:span.label (tr "labels.hide-resolved-comments")]] + [:li {:class (stl/css-case + :dropdown-element true + :selected (= :pending cshow)) + :data-value (d/name cshow) + :on-click update-show} + [:span {:class (stl/css :label)} (tr "labels.hide-resolved-comments")] + (when (= :pending cshow) + [:span {:class (stl/css :icon)} + i/tick])] - [:hr] - [:li {:class (dom/classnames :selected show-sidebar?) - :on-click #(update-options (not show-sidebar?))} - [:span.icon i/tick] - [:span.label (tr "labels.show-comments-list")]]]]])) + [:li {:class (stl/css :separator)}] + + [:li {:class (stl/css-case + :dropdown-element true + :selected show-sidebar?) + :data-value (dm/str show-sidebar?) + :on-click update-options} + [:span {:class (stl/css :label)} (tr "labels.show-comments-list")] + (when show-sidebar? + [:span {:class (stl/css :icon)} i/tick])]]]])) -(defn- update-thread-position [positions {:keys [id] :as thread}] +(defn- update-thread-position + [positions {:keys [id] :as thread}] (if-let [data (get positions id)] (-> thread (assoc :position (:position data)) @@ -86,7 +125,8 @@ thread)) (mf/defc comments-layer - [{:keys [zoom file users frame page] :as props}] + {::mf/props :obj} + [{:keys [zoom file users frame page]}] (let [profile (mf/deref refs/profile) local (mf/deref refs/comments-local) @@ -103,7 +143,7 @@ threads-map (mf/deref refs/comment-threads) frame-corner (mf/with-memo [frame] - (-> frame :points gsh/points->selrect gpt/point)) + (-> frame :points grc/points->rect gpt/point)) modifier1 (mf/with-memo [frame-corner] (-> (gmt/matrix) @@ -158,9 +198,10 @@ (st/emit! (dcm/create-thread-on-viewer params) (dcm/close-thread)))))] - [:div.comments-section {:on-click on-click} - [:div.viewer-comments-container - [:div.threads + [:div {:class (stl/css :comments-section) + :on-click on-click} + [:div {:class (stl/css :viewer-comments-container)} + [:div {:class (stl/css :threads)} (for [item threads] [:& cmt/thread-bubble {:thread item @@ -195,6 +236,6 @@ (dcm/apply-filters local profile) (filter (fn [{:keys [position]}] (gsh/has-point? frame position))))] - [:aside.settings-bar.settings-bar-right.comments-right-sidebar - [:div.settings-bar-inside - [:& wc/comments-sidebar {:users users :threads threads :page-id (:id page)}]]])) + [:aside {:class (stl/css :comments-sidebar)} + [:div {:class (stl/css :settings-bar-inside)} + [:& wc/comments-sidebar {:from-viewer true :users users :threads threads :page-id (:id page)}]]])) diff --git a/frontend/src/app/main/ui/viewer/comments.scss b/frontend/src/app/main/ui/viewer/comments.scss new file mode 100644 index 0000000000..7472dc8d2e --- /dev/null +++ b/frontend/src/app/main/ui/viewer/comments.scss @@ -0,0 +1,119 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +// COMMENT DROPDOWN ON HEADER +.view-options { + @include bodySmallTypography; + display: flex; + align-items: center; + position: relative; + gap: $s-4; + height: $s-32; + padding: $s-8; + border-radius: $br-8; + background-color: var(--input-background-color); + cursor: pointer; +} + +.dropdown { + @extend .menu-dropdown; + right: $s-2; + top: calc($s-2 + $s-48); + width: $s-272; + padding: $s-6; +} + +.dropdown-title { + @include bodySmallTypography; + flex-grow: 1; + color: var(--input-foreground-color-active); +} + +.label { + flex-grow: 1; + color: var(--input-foreground-color); +} + +.icon, +.icon-dropdown { + @include flexCenter; + height: 100%; + width: $s-16; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } +} + +.icon-dropdown svg { + transform: rotate(90deg); +} + +.dropdown-element { + @extend .dropdown-element-base; + .icon { + @include flexCenter; + height: 100%; + width: $s-16; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } + &:hover .label { + color: var(--input-foreground-color-active); + } +} + +.dropdown-element.selected { + .label { + color: var(--input-foreground-color-active); + } + .icon svg { + stroke: var(--input-foreground-color); + } +} + +.separator { + height: $s-8; +} + +// FLOATING COMMENT +.viewer-comments-container { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + z-index: $z-index-1; +} + +.threads { + position: absolute; + top: 0px; + left: 0px; +} + +//COMMENT SIDEBAR +.comments-sidebar { + position: absolute; + right: 0; + top: $s-44; + width: $s-276; + height: calc(100vh - $s-48); + z-index: $z-index-10; + background-color: var(--panel-background-color); +} + +.settings-bar-inside { + overflow-y: auto; +} + +.comments-section { + background-color: var(--panel-background-color); +} diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 34c7e51737..6e3051cf73 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -5,9 +5,11 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.header + (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] [app.main.data.modal :as modal] + [app.main.data.shortcuts :as scd] [app.main.data.viewer :as dv] [app.main.data.viewer.shortcuts :as sc] [app.main.store :as st] @@ -32,7 +34,8 @@ (modal/show! :login-register {})) (mf/defc zoom-widget - {::mf/wrap [mf/memo]} + {::mf/memo true + ::mf/props :obj} [{:keys [zoom on-increase on-decrease @@ -41,196 +44,299 @@ on-zoom-fit on-zoom-fill] :as props}] - (let [show-dropdown? (mf/use-state false)] - [:div.zoom-widget {:on-click #(reset! show-dropdown? true)} - [:span.label (fmt/format-percent zoom)] - [:span.icon i/arrow-down] - [:& dropdown {:show @show-dropdown? - :on-close #(reset! show-dropdown? false)} - [:ul.dropdown - [:li.basic-zoom-bar - [:span.zoom-btns - [:button {:on-click (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (on-decrease))} "-"] - [:p.zoom-size (fmt/format-percent zoom)] - [:button {:on-click (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (on-increase))} "+"]] - [:button.reset-btn {:on-click on-zoom-reset} (tr "workspace.header.reset-zoom")]] - [:li.separator] - [:li {:on-click on-zoom-fit} - (tr "workspace.header.zoom-fit") [:span (sc/get-tooltip :toggle-zoom-style)]] - [:li {:on-click on-zoom-fill} - (tr "workspace.header.zoom-fill") [:span (sc/get-tooltip :toggle-zoom-style)]] - [:li {:on-click on-fullscreen} - (tr "workspace.header.zoom-full-screen") [:span (sc/get-tooltip :toggle-fullscreen)]]]]])) + (let [open* (mf/use-state false) + open? (deref open*) + open-dropdown + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! open* true))) + + close-dropdown + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! open* false))) + + on-increase + (mf/use-fn + (mf/deps on-increase) + (fn [event] + (dom/stop-propagation event) + (on-increase))) + + on-decrease + (mf/use-fn + (mf/deps on-decrease) + (fn [event] + (dom/stop-propagation event) + (on-decrease)))] + + [:div {:class (stl/css-case :zoom-widget true + :selected open?) + :on-click open-dropdown + :title (tr "workspace.header.zoom")} + [:span {:class (stl/css :label)} (fmt/format-percent zoom)] + [:& dropdown {:show open? + :on-close close-dropdown} + [:ul {:class (stl/css :dropdown)} + [:li {:class (stl/css :basic-zoom-bar)} + [:span {:class (stl/css :zoom-btns)} + [:button {:class (stl/css :zoom-btn) + :on-click on-decrease} + [:span {:class (stl/css :zoom-icon)} + i/remove-icon]] + [:p {:class (stl/css :zoom-text)} + (fmt/format-percent zoom)] + [:button {:class (stl/css :zoom-btn) + :on-click on-increase} + [:span {:class (stl/css :zoom-icon)} + i/add]]] + [:button {:class (stl/css :reset-btn) + :on-click on-zoom-reset} + (tr "workspace.header.reset-zoom")]] + + [:li {:class (stl/css :zoom-option) + :on-click on-zoom-fit} + (tr "workspace.header.zoom-fit") + [:span {:class (stl/css :shortcuts)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-zoom-style))] + [:span {:class (stl/css :shortcut-key) + :key (dm/str "zoom-fit-" sc)} sc])]] + [:li {:class (stl/css :zoom-option) + :on-click on-zoom-fill} + (tr "workspace.header.zoom-fill") + [:span {:class (stl/css :shortcuts)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-zoom-style))] + [:span {:class (stl/css :shortcut-key) + :key (dm/str "zoom-fill-" sc)} sc])]] + [:li {:class (stl/css :zoom-option) + :on-click on-fullscreen} + (tr "workspace.header.zoom-full-screen") + [:span {:class (stl/css :shortcuts)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-fullscreen))] + [:span {:class (stl/css :shortcut-key) + :key (dm/str "zoom-fullscreen-" sc)} sc])]]]]])) (mf/defc header-options [{:keys [section zoom page file index permissions interactions-mode]}] - (let [fullscreen? (mf/deref fullscreen-ref) + (let [fullscreen? (mf/deref fullscreen-ref) toggle-fullscreen - (mf/use-callback + (mf/use-fn (fn [] (st/emit! dv/toggle-fullscreen))) go-to-workspace - (mf/use-callback + (mf/use-fn (mf/deps page) (fn [] (st/emit! (dv/go-to-workspace (:id page))))) open-share-dialog - (mf/use-callback + (mf/use-fn (mf/deps page) (fn [] (modal/show! :share-link {:page page :file file}) - (modal/allow-click-outside!)))] + (modal/allow-click-outside!))) + + handle-increase + (mf/use-fn + #(st/emit! dv/increase-zoom)) + + handle-decrease + (mf/use-fn + #(st/emit! dv/decrease-zoom)) + + handle-zoom-reset + (mf/use-fn + #(st/emit! dv/reset-zoom)) + + handle-zoom-fill + (mf/use-fn + #(st/emit! dv/zoom-to-fill)) + + handle-zoom-fit + (mf/use-fn + #(st/emit! dv/zoom-to-fit))] + + [:div {:class (stl/css :options-zone)} + [:& export-progress-widget] - [:div.options-zone (case section :interactions [:* (when index [:& flows-menu {:page page :index index}]) [:& interactions-menu {:interactions-mode interactions-mode}]] :comments [:& comments-menu] + [:div {:class (stl/css :view-options)}]) - [:div.view-options]) - - [:& export-progress-widget] [:& zoom-widget {:zoom zoom - :on-increase #(st/emit! dv/increase-zoom) - :on-decrease #(st/emit! dv/decrease-zoom) - :on-zoom-reset #(st/emit! dv/reset-zoom) - :on-zoom-fill #(st/emit! dv/zoom-to-fill) - :on-zoom-fit #(st/emit! dv/zoom-to-fit) + :on-increase handle-increase + :on-decrease handle-decrease + :on-zoom-reset handle-zoom-reset + :on-zoom-fill handle-zoom-fill + :on-zoom-fit handle-zoom-fit :on-fullscreen toggle-fullscreen}] - [:span.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left - {:alt (tr "viewer.header.fullscreen") - :on-click toggle-fullscreen} - (if fullscreen? - i/full-screen-off - i/full-screen)] + (when (:can-edit permissions) + [:span {:on-click go-to-workspace + :class (stl/css :edit-btn)} + i/curve]) + + [:span {:title (tr "viewer.header.fullscreen") + :class (stl/css-case :fullscreen-btn true + :selected fullscreen?) + :on-click toggle-fullscreen} + i/expand] (when (:is-admin permissions) - [:span.btn-primary.tooltip.tooltip-bottom-left {:on-click open-share-dialog :alt (tr "labels.share-prototype")} i/export [:span (tr "labels.share-prototype")]]) - - (when (:can-edit permissions) - [:span.btn-text-dark {:on-click go-to-workspace} (tr "labels.edit-file")]) + [:button {:on-click open-share-dialog + :class (stl/css :share-btn)} + (tr "labels.share")]) (when-not (:is-logged permissions) - [:span.btn-text-dark {:on-click open-login-dialog} (tr "labels.log-or-sign")])])) + [:span {:on-click open-login-dialog + :class (stl/css :go-log-btn)} (tr "labels.log-or-sign")])])) (mf/defc header-sitemap - [{:keys [project file page frame] :as props}] + [{:keys [project file page frame toggle-thumbnails] :as props}] (let [project-name (:name project) file-name (:name file) page-name (:name page) + page-id (:id page) frame-name (:name frame) show-dropdown? (mf/use-state false) - toggle-thumbnails - (mf/use-callback - (fn [] - (st/emit! dv/toggle-thumbnails-panel))) - open-dropdown - (mf/use-callback + (mf/use-fn (fn [] (reset! show-dropdown? true))) close-dropdown - (mf/use-callback + (mf/use-fn (fn [] (reset! show-dropdown? false))) navigate-to - (mf/use-callback + (mf/use-fn (fn [page-id] (st/emit! (dv/go-to-page page-id)) (reset! show-dropdown? false)))] - [:div.sitemap-zone {:alt (tr "viewer.header.sitemap")} - [:div.breadcrumb - {:on-click open-dropdown} - [:span.project-name project-name] - [:span "/"] - [:span.file-name file-name] - [:span "/"] - - [:span.page-name page-name] - - - [:& dropdown {:show @show-dropdown? - :on-close close-dropdown} - [:ul.dropdown - (for [id (get-in file [:data :pages])] - [:li {:id (str id) - :key (str id) - :on-click (partial navigate-to id)} - (get-in file [:data :pages-index id :name])])]]] - - [:span.icon {:on-click open-dropdown} i/arrow-down] - [:div.current-frame - {:on-click toggle-thumbnails} - [:span.label "/"] - [:span.label frame-name]] - [:span.icon {:on-click toggle-thumbnails} i/arrow-down]])) - + [:div {:class (stl/css :sitemap-zone) + :title (tr "viewer.header.sitemap")} + [:span {:class (stl/css :project-name)} project-name] + [:div {:class (stl/css :sitemap-text)} + [:div {:class (stl/css :breadcrumb) + :on-click open-dropdown} + [:span {:class (stl/css :breadcrumb-text)} + (dm/str file-name " / " page-name)] + [:span {:class (stl/css :icon)} i/arrow] + [:span "/"] + [:& dropdown {:show @show-dropdown? + :on-close close-dropdown} + [:ul {:class (stl/css :dropdown-sitemap)} + (for [id (get-in file [:data :pages])] + [:li {:class (stl/css-case :dropdown-element true + :selected (= page-id id)) + :id (dm/str id) + :key (dm/str id) + :on-click (partial navigate-to id)} + [:span {:class (stl/css :label)} + (get-in file [:data :pages-index id :name])] + (when (= page-id id) + [:span {:class (stl/css :icon-check)} i/tick])])]]] + [:div {:class (stl/css :current-frame) + :id "current-frame" + :on-click toggle-thumbnails} + [:span {:class (stl/css :frame-name)} frame-name] + [:span {:class (stl/css :icon)} i/arrow]]]])) (mf/defc header - [{:keys [project file page frame zoom section permissions index interactions-mode]}] + [{:keys [project file page frame zoom section permissions index interactions-mode shown-thumbnails]}] (let [go-to-dashboard - #(st/emit! (dv/go-to-dashboard)) + (mf/use-fn + #(st/emit! (dv/go-to-dashboard))) go-to-inspect - (fn[] - (if (:is-logged permissions) - (st/emit! dv/close-thumbnails-panel (dv/go-to-section :inspect)) - (open-login-dialog))) + (mf/use-fn + (mf/deps permissions) + (fn [] + (if (:is-logged permissions) + (st/emit! dv/close-thumbnails-panel + (dv/go-to-section :inspect)) + (open-login-dialog)))) navigate - (fn [section] - (if (or (= section :interactions) (:is-logged permissions)) - (st/emit! (dv/go-to-section section)) - (open-login-dialog)))] + (mf/use-fn + (mf/deps permissions) + (fn [event] + (let [section (-> (dom/get-current-target event) + (dom/get-data "value") + (keyword))] + (if (or (= section :interactions) (:is-logged permissions)) + (st/emit! (dv/go-to-section section)) + (open-login-dialog))))) - [:header.viewer-header - [:div.nav-zone + toggle-thumbnails + (mf/use-fn + (fn [] + (st/emit! dv/toggle-thumbnails-panel))) + + + close-thumbnails + (mf/use-fn + (mf/deps shown-thumbnails) + (fn [_] + (when shown-thumbnails + (st/emit! dv/close-thumbnails-panel))))] + + + [:header {:class (stl/css-case :viewer-header true + :fullscreen (mf/deref fullscreen-ref)) + :on-click close-thumbnails} + [:div {:class (stl/css :nav-zone)} ;; If the user doesn't have permission we disable the link - [:div.main-icon {:style {:cursor (when-not (:can-edit permissions) "auto")}} - [:a {:on-click go-to-dashboard - :style {:pointer-events (when-not (:can-edit permissions) "none")}} i/logo-icon]] + [:a {:class (stl/css :home-link) + :on-click go-to-dashboard + :style {:cursor (when-not (:can-edit permissions) "auto") + :pointer-events (when-not (:can-edit permissions) "none")}} + [:span {:class (stl/css :logo-icon)} + i/logo-icon]] - [:& header-sitemap {:project project :file file :page page :frame frame :index index}]] + [:& header-sitemap {:project project + :file file + :page page + :frame frame + :toggle-thumbnails toggle-thumbnails + :index index}]] - [:div.mode-zone - [:button.mode-zone-button.tooltip.tooltip-bottom - {:on-click #(navigate :interactions) - :class (dom/classnames :active (= section :interactions)) - :alt (tr "viewer.header.interactions-section" (sc/get-tooltip :open-interactions))} + [:div {:class (stl/css :mode-zone)} + [:button {:on-click navigate + :data-value "interactions" + :class (stl/css-case :mode-zone-btn true + :selected (= section :interactions)) + :title (tr "viewer.header.interactions-section" (sc/get-tooltip :open-interactions))} i/play] (when (or (:can-edit permissions) (= (:who-comment permissions) "all")) - [:button.mode-zone-button.tooltip.tooltip-bottom - {:on-click #(navigate :comments) - :class (dom/classnames :active (= section :comments)) - :alt (tr "viewer.header.comments-section" (sc/get-tooltip :open-comments))} - i/chat]) + [:button {:on-click navigate + :data-value "comments" + :class (stl/css-case :mode-zone-btn true + :selected (= section :comments)) + :title (tr "viewer.header.comments-section" (sc/get-tooltip :open-comments))} + i/comments]) (when (or (= (:type permissions) :membership) (and (= (:type permissions) :share-link) (= (:who-inspect permissions) "all"))) - [:button.mode-zone-button.tooltip.tooltip-bottom - {:on-click go-to-inspect - :class (dom/classnames :active (= section :inspect)) - :alt (tr "viewer.header.inspect-section" (sc/get-tooltip :open-inspect))} + [:button {:on-click go-to-inspect + :class (stl/css-case :mode-zone-btn true + :selected (= section :inspect)) + :title (tr "viewer.header.inspect-section" (sc/get-tooltip :open-inspect))} i/code])] [:& header-options {:section section diff --git a/frontend/src/app/main/ui/viewer/header.scss b/frontend/src/app/main/ui/viewer/header.scss new file mode 100644 index 0000000000..c131e07b4d --- /dev/null +++ b/frontend/src/app/main/ui/viewer/header.scss @@ -0,0 +1,322 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.viewer-header { + position: absolute; + top: 0; + grid-column: 1 / span 1; + grid-row: 1 / span 1; + display: grid; + grid-template-columns: 1fr $s-92 1fr; + justify-content: space-between; + align-items: center; + height: $s-48; + width: 100vw; + padding: $s-8 $s-12; + transition: transform 400ms ease 300ms; + background-color: var(--panel-background-color); +} + +// FILE NAVIGATION + +.nav-zone { + display: flex; + justify-content: flex-start; + flex-basis: min-content; + width: 100%; + gap: $s-12; +} + +.home-link { + padding: 0; +} + +.logo-icon { + @include flexCenter; + width: $s-32; + height: $s-32; + svg { + width: $s-28; + fill: var(--icon-foreground-hover); + } +} + +.sitemap-zone { + @include flexColumn; + position: relative; + width: 100%; +} + +.project-name { + @include uppercaseTitleTipography; + color: var(--title-foreground-color); +} + +.sitemap-text { + @include flexRow; +} + +.breadcrumb { + @include bodySmallTypography; + @include flexRow; + color: var(--title-foreground-color); + cursor: pointer; +} + +.breadcrumb-text { + @include textEllipsis; + max-width: 12vw; // This is a fallback + max-width: 12cqw; // This is a unit refered to container +} + +.icon { + @include flexCenter; + height: $s-16; + width: $s-16; + svg { + @extend .button-icon-small; + transform: rotate(90deg); + stroke: var(--icon-foreground); + } +} + +.dropdown { + position: absolute; +} + +.dropdown-sitemap { + @extend .menu-dropdown; + left: 0; + top: calc($s-2 + $s-48); + width: $s-272; + padding: $s-6; +} + +.dropdown-element { + @extend .dropdown-element-base; + .icon-check { + @include flexCenter; + height: 100%; + width: $s-16; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } + &:hover .label { + color: var(--input-foreground-color-active); + } +} + +.current-frame { + @include bodySmallTypography; + @include flexRow; + flex-grow: 1; + color: var(--title-foreground-color-hover); + cursor: pointer; + .icon svg { + stroke: var(--title-foreground-color-hover); + } +} + +.frame-name { + @include textEllipsis; + max-width: 17vw; // This is a fallback + max-width: 17cqw; // This is a unit refered to container +} + +// SECTION BUTTONS +.mode-zone { + @include flexRow; + height: 100%; +} + +.mode-zone-btn { + @extend .button-tertiary; + @include flexCenter; + height: $s-32; + width: $s-28; + padding: 0; + svg { + @extend .button-icon; + } +} + +.selected { + @extend .button-icon-selected; +} + +// OPTION AREA +.options-zone { + @include flexRow; + position: relative; + justify-content: flex-end; + gap: $s-8; + z-index: $z-index-10; +} + +.view-options { + position: relative; + display: flex; + align-items: center; + cursor: pointer; +} + +.fullscreen-btn { + @extend .button-tertiary; + @include flexCenter; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.share-btn { + @extend .button-primary; + height: $s-32; + min-width: $s-72; + margin-left: $s-4; +} + +.edit-btn { + @extend .button-tertiary; + @include flexCenter; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.go-log-btn { + @extend .button-tertiary; + @include bodySmallTypography; + height: $s-32; + padding: 0 $s-8; + border-radius: $br-8; + color: var(--button-tertiary-foreground-color-rest); +} + +// ZOOM WIDGET +.zoom-widget { + @include buttonStyle; + @include flexCenter; + height: $s-28; + min-width: $s-64; + border-radius: $br-8; + .label { + @include bodySmallTypography; + color: var(--button-tertiary-foreground-color-rest); + } + + &:hover { + .label { + color: var(--button-tertiary-foreground-color-focus); + } + } + &.selected { + .label { + color: var(--button-tertiary-foreground-color-focus); + } + } +} + +.dropdown { + @extend .menu-dropdown; + right: $s-2; + top: calc($s-2 + $s-48); + width: $s-272; +} + +.basic-zoom-bar { + display: flex; + justify-content: space-between; + padding: $s-6; + cursor: auto; +} + +.zoom-btns { + display: flex; +} + +.zoom-btn { + @extend .button-tertiary; + height: $s-28; + width: $s-28; + border-radius: $br-8; + .zoom-icon { + @include flexCenter; + width: $s-24; + height: $s-32; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + } + &:hover { + .zoom-icon svg { + stroke: var(--button-tertiary-foreground-color-hover); + } + } +} + +.zoom-text { + @include flexCenter; + height: 100%; + min-width: $s-64; + padding: 0; + margin: 0 $s-2; + color: var(--modal-title-foreground-color); +} + +.reset-btn { + @extend .button-tertiary; + color: var(--button-tertiary-foreground-color-hover); + height: $s-28; + border-radius: $br-8; +} + +.zoom-option { + @extend .menu-item-base; + .shortcuts { + @extend .shortcut-base; + .shortcut-key { + @extend .shortcut-key-base; + } + } + &:hover { + color: var(--menu-foreground-color-hover); + .shortcuts { + .shortcut-key { + color: var(--menu-foreground-color-hover); + } + } + } +} + +/** FULLSCREEN */ +[data-fullscreen="true"] .viewer-header::after { + content: " "; + position: absolute; + width: 100%; + height: $s-48; + left: 0; + top: $s-48; +} + +[data-fullscreen="true"] .viewer-header { + transform: translateY(-$s-48); +} + +[data-force-visible="true"] .viewer-header, +[data-fullscreen="true"] .viewer-header:hover { + transform: translateY(0); +} diff --git a/frontend/src/app/main/ui/viewer/inspect.cljs b/frontend/src/app/main/ui/viewer/inspect.cljs index 4faa901099..110317c186 100644 --- a/frontend/src/app/main/ui/viewer/inspect.cljs +++ b/frontend/src/app/main/ui/viewer/inspect.cljs @@ -5,9 +5,13 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect + (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] + [app.common.data.macros :as dm] [app.main.data.viewer :as dv] [app.main.store :as st] + [app.main.ui.hooks.resize :refer [use-resize-hook]] [app.main.ui.viewer.inspect.left-sidebar :refer [left-sidebar]] [app.main.ui.viewer.inspect.render :refer [render-frame-svg]] [app.main.ui.viewer.inspect.right-sidebar :refer [right-sidebar]] @@ -18,25 +22,31 @@ (:import goog.events.EventType)) (defn handle-select-frame - [frame] - (fn [event] + [event] + (let [frame-id (-> (dom/get-current-target event) + (dom/get-data "value") + (d/read-string)) + origin (dom/get-target event) + over-section? (dom/class? origin "inspect-svg-container") + layout (dom/get-element "viewer-layout") + has-force? (dom/class? layout "force-visible")] + (dom/prevent-default event) (dom/stop-propagation event) - (st/emit! (dv/select-shape (:id frame))) - - (let [origin (dom/get-target event) - over-section? (dom/class? origin "inspect-svg-container") - layout (dom/get-element "viewer-layout") - has-force? (dom/class? layout "force-visible")] - - (when over-section? - (if has-force? - (dom/remove-class! layout "force-visible") - (dom/add-class! layout "force-visible")))))) + (st/emit! (dv/select-shape frame-id)) + (when over-section? + (if has-force? + (dom/remove-class! layout "force-visible") + (dom/add-class! layout "force-visible"))))) (mf/defc viewport [{:keys [local file page frame index viewer-pagination size share-id]}] (let [inspect-svg-container-ref (mf/use-ref nil) + current-section* (mf/use-state :info) + current-section (deref current-section*) + + can-be-expanded? (= current-section :code) + on-mouse-wheel (fn [event] (when (kbd/mod? event) @@ -55,7 +65,23 @@ (let [key1 (events/listen goog/global EventType.WHEEL on-mouse-wheel #js {"passive" false})] (fn [] - (events/unlistenByKey key1))))] + (events/unlistenByKey key1)))) + + {:keys [on-pointer-down on-lost-pointer-capture on-pointer-move] + set-right-size :set-size + right-size :size} + (use-resize-hook :code 276 276 768 :x true :right) + + handle-change-section + (mf/use-callback + (fn [section] + (reset! current-section* section))) + + handle-expand + (mf/use-callback + (mf/deps right-size) + (fn [] + (set-right-size (if (> right-size 276) 276 768))))] (mf/use-effect on-mount) @@ -68,13 +94,28 @@ [:& left-sidebar {:frame frame :local local :page page}] - [:div.inspect-svg-wrapper {:on-click (handle-select-frame frame)} + [:div#inspect-svg-wrapper {:class (stl/css :inspect-svg-wrapper) + :data-value (pr-str (:id frame)) + :on-click handle-select-frame} [:& viewer-pagination {:index index :num-frames (count (:frames page)) :left-bar true :right-bar true}] - [:div.inspect-svg-container {:ref inspect-svg-container-ref} + [:div#inspect-svg-container {:class (stl/css :inspect-svg-container) + :ref inspect-svg-container-ref} [:& render-frame-svg {:frame frame :page page :local local :size size}]]] - [:& right-sidebar {:frame frame - :selected (:selected local) - :page page - :file file - :share-id share-id}]])) + [:div {:class (stl/css-case :sidebar-container true + :not-expand (not can-be-expanded?) + :expanded can-be-expanded?) + + :style #js {"--width" (when can-be-expanded? (dm/str right-size "px"))}} + (when can-be-expanded? + [:div {:class (stl/css :resize-area) + :on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move}]) + [:& right-sidebar {:frame frame + :selected (:selected local) + :page page + :file file + :on-change-section handle-change-section + :on-expand handle-expand + :share-id share-id}]]])) diff --git a/frontend/src/app/main/ui/viewer/inspect.scss b/frontend/src/app/main/ui/viewer/inspect.scss new file mode 100644 index 0000000000..340003ce92 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect.scss @@ -0,0 +1,56 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +$width-settings-bar: $s-276; +$width-settings-bar-max: $s-500; + +.inspect-svg-wrapper { + @include flexCenter; + position: relative; + flex-direction: column; + flex: 1; + width: 100%; + height: 100%; + overflow: hidden; +} + +.inspect-svg-container { + display: grid; + align-items: center; + justify-content: safe center; + width: 100%; + height: 100%; + margin: 0 auto; + overflow: auto; +} + +.sidebar-container { + position: relative; + align-self: flex-start; + width: $width-settings-bar; + + background-color: var(--panel-background-color); + border-top: $s-1 solid var(--search-bar-input-border-color); +} + +.not-expand { + max-width: $width-settings-bar; +} + +.expanded { + width: var(--width, $width-settings-bar); +} + +.resize-area { + position: absolute; + left: 0; + width: $s-8; + height: 100%; + z-index: $z-index-10; + cursor: ew-resize; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/annotation.cljs b/frontend/src/app/main/ui/viewer/inspect/annotation.cljs index 401b8c111f..a0dacdfbd2 100644 --- a/frontend/src/app/main/ui/viewer/inspect/annotation.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/annotation.cljs @@ -5,15 +5,20 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.annotation + (:require-macros [app.main.style :as stl]) (:require [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.components.title-bar :refer [title-bar]] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) (mf/defc annotation [{:keys [content] :as props}] - [:div.attributes-block.inspect-annotation - [:div.attributes-block-title - [:div.attributes-block-title-text (tr "workspace.options.component.annotation")] - [:& copy-button {:data content}]] - [:div.content content]]) + [:div {:class (stl/css :attributes-block)} + [:& title-bar {:collapsable false + :title (tr "workspace.options.component.annotation") + :class (stl/css :title-spacing-annotation)} + [:& copy-button {:data content + :class (stl/css :copy-btn-title)}]] + + [:div {:class (stl/css :annotation-content)} content]]) diff --git a/frontend/src/app/main/ui/viewer/inspect/annotation.scss b/frontend/src/app/main/ui/viewer/inspect/annotation.scss new file mode 100644 index 0000000000..27d27c61c2 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/annotation.scss @@ -0,0 +1,24 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.attributes-block { + @include flexColumn; +} + +.title-spacing-annotation { + @extend .attr-title; +} + +.annotation-content { + @include bodySmallTypography; + color: var(--entry-foreground-color); +} + +.copy-btn-title { + max-width: $s-28; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes.cljs index 19fc153112..9798af6777 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes.cljs @@ -5,17 +5,17 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.attributes + (:require-macros [app.main.style :as stl]) (:require - [app.common.geom.shapes :as gsh] [app.common.types.components-list :as ctkl] [app.main.ui.hooks :as hooks] [app.main.ui.viewer.inspect.annotation :refer [annotation]] [app.main.ui.viewer.inspect.attributes.blur :refer [blur-panel]] [app.main.ui.viewer.inspect.attributes.fill :refer [fill-panel]] + [app.main.ui.viewer.inspect.attributes.geometry :refer [geometry-panel]] [app.main.ui.viewer.inspect.attributes.image :refer [image-panel]] [app.main.ui.viewer.inspect.attributes.layout :refer [layout-panel]] - [app.main.ui.viewer.inspect.attributes.layout-flex :refer [layout-flex-panel]] - [app.main.ui.viewer.inspect.attributes.layout-flex-element :refer [layout-flex-element-panel]] + [app.main.ui.viewer.inspect.attributes.layout-element :refer [layout-element-panel]] [app.main.ui.viewer.inspect.attributes.shadow :refer [shadow-panel]] [app.main.ui.viewer.inspect.attributes.stroke :refer [stroke-panel]] [app.main.ui.viewer.inspect.attributes.svg :refer [svg-panel]] @@ -24,31 +24,29 @@ [rumext.v2 :as mf])) (def type->options - {:multiple [:fill :stroke :image :text :shadow :blur :layout-flex-item] - :frame [:layout :fill :stroke :shadow :blur :layout-flex :layout-flex-item] - :group [:layout :svg :layout-flex-item] - :rect [:layout :fill :stroke :shadow :blur :svg :layout-flex-item] - :circle [:layout :fill :stroke :shadow :blur :svg :layout-flex-item] - :path [:layout :fill :stroke :shadow :blur :svg :layout-flex-item] - :image [:image :layout :fill :stroke :shadow :blur :svg :layout-flex-item] - :text [:layout :text :shadow :blur :stroke :layout-flex-item]}) + {:multiple [:fill :stroke :image :text :shadow :blur :layout-element] + :frame [:geometry :fill :stroke :shadow :blur :layout :layout-element] + :group [:geometry :svg :layout-element] + :rect [:geometry :fill :stroke :shadow :blur :svg :layout-element] + :circle [:geometry :fill :stroke :shadow :blur :svg :layout-element] + :path [:geometry :fill :stroke :shadow :blur :svg :layout-element] + :image [:image :geometry :fill :stroke :shadow :blur :svg :layout-element] + :text [:geometry :text :shadow :blur :stroke :layout-element]}) (mf/defc attributes - [{:keys [page-id file-id shapes frame from libraries share-id]}] + [{:keys [page-id file-id shapes frame from libraries share-id objects]}] (let [shapes (hooks/use-equal-memo shapes) - shapes (mf/with-memo [shapes] - (mapv #(gsh/translate-to-frame % frame) shapes)) type (if (= (count shapes) 1) (-> shapes first :type) :multiple) options (type->options type) content (when (= (count shapes) 1) (ctkl/get-component-annotation (first shapes) libraries))] - [:div.element-options + [:div {:class (stl/css :element-options)} (for [[idx option] (map-indexed vector options)] [:> (case option + :geometry geometry-panel :layout layout-panel - :layout-flex layout-flex-panel - :layout-flex-item layout-flex-element-panel + :layout-element layout-element-panel :fill fill-panel :stroke stroke-panel :shadow shadow-panel @@ -58,6 +56,7 @@ :svg svg-panel) {:key idx :shapes shapes + :objects objects :frame frame :from from}]) (when content diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes.scss b/frontend/src/app/main/ui/viewer/inspect/attributes.scss new file mode 100644 index 0000000000..54980db833 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes.scss @@ -0,0 +1,16 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-options { + display: flex; + flex-direction: column; + gap: $s-16; + width: 100%; + height: 100%; + padding-top: $s-8; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/blur.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/blur.cljs index 86d97d3606..ff2e450f98 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/blur.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/blur.cljs @@ -5,34 +5,36 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.attributes.blur + (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.main.ui.components.copy-button :refer [copy-button]] - [app.util.code-gen :as cg] + [app.main.ui.components.title-bar :refer [inspect-title-bar]] + [app.util.code-gen.style-css :as css] [app.util.i18n :refer [tr]] - [cuerdas.core :as str] [rumext.v2 :as mf])) (defn has-blur? [shape] (:blur shape)) -(defn copy-data [shape] - (cg/generate-css-props - shape - :blur - {:to-prop "filter" - :format #(str/fmt "blur(%spx)" (:value %))})) - -(mf/defc blur-panel [{:keys [shapes]}] +(mf/defc blur-panel + [{:keys [objects shapes]}] (let [shapes (->> shapes (filter has-blur?))] (when (seq shapes) - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (tr "inspect.attributes.blur")] + [:div {:class (stl/css :attributes-block)} + [:& inspect-title-bar + {:title (tr "inspect.attributes.blur") + :class (stl/css :title-spacing-blur)} (when (= (count shapes) 1) - [:& copy-button {:data (copy-data (first shapes))}])] + [:& copy-button {:data (css/get-css-property objects (first shapes) :filter) + :class (stl/css :copy-btn-title)}])] - (for [shape shapes] - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.blur.value")] - [:div.attributes-value (-> shape :blur :value) "px"] - [:& copy-button {:data (copy-data shape)}]])]))) + [:div {:class (stl/css :attributes-content)} + (for [shape shapes] + [:div {:class (stl/css :blur-row) + :key (dm/str "block-" (:id shape) "-blur")} + [:div {:class (stl/css :global/attr-label)} "Filter"] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (css/get-css-property objects shape :filter)} + [:div {:class (stl/css :button-children)} + (css/get-css-value objects shape :filter)]]]])]]))) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/blur.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/blur.scss new file mode 100644 index 0000000000..a3f2dc334e --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/blur.scss @@ -0,0 +1,27 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.attributes-block { + @include flexColumn; +} + +.title-spacing-blur { + @extend .attr-title; +} + +.blur-row { + @extend .attr-row; +} + +.button-children { + @extend .copy-button-children; +} + +.copy-btn-title { + max-width: $s-28; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs index 76e593f4c8..b5c26b07f8 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs @@ -5,19 +5,24 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.attributes.common + (:require-macros [app.main.style :as stl]) (:require + [app.common.colors :as cc] + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.media :as cm] + [app.config :as cf] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.color-bullet :refer [color-bullet color-name]] + [app.main.ui.components.color-bullet :as cb] [app.main.ui.components.copy-button :refer [copy-button]] - [app.util.color :as uc] - [app.util.dom :as dom] + [app.main.ui.components.select :refer [select]] + [app.main.ui.formats :as fmt] [app.util.i18n :refer [tr]] [cuerdas.core :as str] [okulary.core :as l] [rumext.v2 :as mf])) - (def file-colors-ref (l/derived (l/in [:viewer :file :data :colors]) st/state)) @@ -41,42 +46,104 @@ (defn- get-file-colors [] (or (mf/deref file-colors-ref) (mf/deref refs/workspace-file-colors))) +(defn get-css-rule-humanized [property] + (as-> property $ + (d/name $) + (str/split $ "-") + (str/join " " $) + (str/capital $))) + (mf/defc color-row [{:keys [color format copy-data on-change-format]}] - (let [colors-library (get-colors-library color) - file-colors (get-file-colors) + (let [colors-library (get-colors-library color) + file-colors (get-file-colors) color-library-name (get-in (or colors-library file-colors) [(:id color) :name]) - color (assoc color :color-library-name color-library-name)] - [:div.attributes-color-row - (when color-library-name - [:div.attributes-color-id - [:& color-bullet {:color color}] - [:div color-library-name]]) + color (assoc color :color-library-name color-library-name) + image (:image color)] - [:div.attributes-color-value {:class (when color-library-name "hide-color")} - [:& color-bullet {:color color}] - (if (:gradient color) - [:& color-name {:color color}] - (case format - :rgba (let [[r g b a] (uc/hex->rgba (:color color) (:opacity color))] - [:div (str/fmt "%s, %s, %s, %s" r g b a)]) - :hsla (let [[h s l a] (uc/hex->hsla (:color color) (:opacity color)) - result (uc/format-hsla [h s l a])] - [:div result]) - [:* - [:& color-name {:color color}] - (when-not (:gradient color) [:div (str (* 100 (:opacity color)) "%")])])) + (if image + (let [mtype (-> image :mtype) + name (or (:name image) (tr "media.image")) + extension (cm/mtype->extension mtype)] + [:div {:class (stl/css :attributes-image-as-color-row)} + [:div {:class (stl/css :attributes-color-row)} + [:div {:class (stl/css :bullet-wrapper) + :style #js {"--bullet-size" "16px"}} + [:& cb/color-bullet {:color color + :mini? true}]] - (when-not (and on-change-format (:gradient color)) - [:select.color-format-select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)} - [:option {:value "hex"} - (tr "inspect.attributes.color.hex")] + [:div {:class (stl/css :format-wrapper)} + [:div {:class (stl/css :image-format)} + (tr "media.image.short")]] + [:& copy-button {:data copy-data + :class (stl/css :color-row-copy-btn)} + [:div {:class (stl/css-case :color-info true + :two-line (some? color-library-name))} + [:div {:class (stl/css :first-row)} + [:span {:class (stl/css :opacity-info)} + (str (* 100 (:opacity color)) "%")]] - [:option {:value "rgba"} - (tr "inspect.attributes.color.rgba")] + (when color-library-name + [:div {:class (stl/css :second-row)} + [:div {:class (stl/css :color-name-library)} + color-library-name]])]] - [:option {:value "hsla"} - (tr "inspect.attributes.color.hsla")]])] - (when copy-data - [:& copy-button {:data copy-data}])])) + [:div {:class (stl/css :image-download)} + [:div {:class (stl/css :image-wrapper)} + [:img {:src (cf/resolve-file-media image)}]] + + [:a {:class (stl/css :download-button) + :target "_blank" + :download (cond-> name extension (str/concat extension)) + :href (cf/resolve-file-media image)} + (tr "inspect.attributes.image.download")]]]]) + + [:div {:class (stl/css :attributes-color-row)} + [:div {:class (stl/css :bullet-wrapper) + :style #js {"--bullet-size" "16px"}} + [:& cb/color-bullet {:color color + :mini? true}]] + + [:div {:class (stl/css :format-wrapper)} + (when-not (and on-change-format (or (:gradient color) image)) + [:& select + {:default-value format + :class (stl/css :select-format-wrapper) + :options [{:value :hex :label (tr "inspect.attributes.color.hex")} + {:value :rgba :label (tr "inspect.attributes.color.rgba")} + {:value :hsla :label (tr "inspect.attributes.color.hsla")}] + :on-change on-change-format}]) + (when (:gradient color) + [:div {:class (stl/css :format-info)} "rgba"])] + + [:& copy-button {:data copy-data + :class (stl/css-case :color-row-copy-btn true + :one-line (not color-library-name) + :two-line (some? color-library-name))} + [:div {:class (stl/css :first-row)} + [:div {:class (stl/css :name-opacity)} + [:span {:class (stl/css-case :color-value-wrapper true + :gradient-name (:gradient color))} + (if (:gradient color) + [:& cb/color-name {:color color :size 90}] + (case format + :hex [:& cb/color-name {:color color}] + :rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))] + (str/ffmt "%, %, %, %" r g b a)) + :hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color)) + result (cc/format-hsla [h s l a])] + [:* result])))] + + (when-not (:gradient color) + [:span {:class (stl/css :opacity-info)} + (dm/str (-> color + (:opacity) + (d/coalesce 1) + (* 100) + (fmt/format-number)) "%")])]] + + (when color-library-name + [:div {:class (stl/css :second-row)} + [:div {:class (stl/css :color-name-library)} + color-library-name]])]]))) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/common.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/common.scss new file mode 100644 index 0000000000..22be662af5 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/common.scss @@ -0,0 +1,185 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.attributes-image-as-color-row { + max-width: $s-240; +} + +.attributes-color-row { + display: grid; + grid-template-columns: $s-16 $s-72 $s-144; + gap: $s-4; +} + +.bullet-wrapper { + @include flexCenter; + height: $s-32; +} + +.format-wrapper { + width: $s-72; + height: $s-32; +} + +.image-format { + @include uppercaseTitleTipography; + height: $s-32; + padding: $s-8 0; + color: var(--menu-foreground-color-rest); +} + +.select-format-wrapper { + width: 100%; + padding: $s-8 $s-2; + background-color: transparent; + border-color: transparent; + color: var(--menu-foreground-color-rest); +} + +.format-info { + @include uppercaseTitleTipography; + display: flex; + align-items: center; + width: 100%; + height: 100%; + padding-left: $s-2; + font-size: $fs-12; + color: var(--menu-foreground-color-rest); +} + +.color-row-copy-btn { + max-width: $s-144; +} + +.color-info { + display: flex; + align-items: flex-start; + gap: $s-4; + flex-grow: 1; + max-width: $s-144; + button { + visibility: hidden; + min-width: $s-28; + } + &:hover button { + visibility: visible; + } +} +.one-line { + max-height: $s-32; +} +.two-line { + display: grid; + grid-template-rows: 1fr 1fr; +} +.color-name-wrapper { + @include bodySmallTypography; + @include flexColumn; + padding: $s-8 $s-4 $s-8 $s-8; + height: $s-32; + max-width: $s-80; + + &.gradient-color { + color: var(--menu-foreground-color); + max-width: $s-124; + } + .color-name-library { + @include bodySmallTypography; + @include textEllipsis; + text-align: left; + height: $s-16; + color: var(--menu-foreground-color-rest); + } + .color-value-wrapper { + @include bodySmallTypography; + height: $s-16; + color: var(--menu-foreground-color); + } +} + +.opacity-info { + @include bodySmallTypography; + color: var(--menu-foreground-color); + padding: $s-8 0; +} + +.first-row { + display: grid; + grid-template-columns: 1fr $s-28; + height: fit-content; + width: 100%; + padding: 0; + margin: 0; +} + +.name-opacity { + height: fit-content; + width: 100%; + line-height: $s-16; + display: grid; + grid-template-columns: 1fr auto; +} + +.color-value-wrapper { + @include textEllipsis; + @include inspectValue; + text-transform: uppercase; + &.gradient-name { + text-transform: none; + } +} + +.opacity-info { + @include inspectValue; + text-transform: uppercase; + width: 100%; +} + +.second-row { + min-height: $s-16; + padding-right: $s-8; + width: 100%; + text-align: left; + margin: 0; + padding: 0; +} + +.color-name-library { + @include inspectValue; + color: var(--menu-foreground-color-rest); +} + +.image-download { + grid-column: 1 / 4; +} + +.download-button { + @extend .button-secondary; + @include uppercaseTitleTipography; + height: $s-32; + width: 100%; + margin-top: $s-4; +} + +.image-wrapper { + background-color: var(--menu-background-color); + position: relative; + @include flexCenter; + width: $s-240; + height: $s-160; + max-height: $s-160; + max-width: $s-248; + margin: $s-8 0; + border-radius: $br-8; + + img { + height: 100%; + width: 100%; + object-fit: contain; + } +} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/fill.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/fill.cljs index 43fe7dffb3..05acf6fbf6 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/fill.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/fill.cljs @@ -5,66 +5,63 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.attributes.fill + (:require-macros [app.main.style :as stl]) (:require + [app.main.ui.components.title-bar :refer [inspect-title-bar]] [app.main.ui.viewer.inspect.attributes.common :refer [color-row]] - [app.util.code-gen :as cg] - [app.util.color :as uc] + [app.util.code-gen.style-css :as css] [app.util.i18n :refer [tr]] [rumext.v2 :as mf])) -(def fill-attributes [:fill-color :fill-color-gradient]) +(def properties [:background :background-color :background-image]) (defn shape->color [shape] {:color (:fill-color shape) :opacity (:fill-opacity shape) :gradient (:fill-color-gradient shape) :id (:fill-color-ref-id shape) - :file-id (:fill-color-ref-file shape)}) + :file-id (:fill-color-ref-file shape) + :image (:fill-image shape)}) -(defn has-color? [shape] +(defn has-fill? [shape] (and (not (contains? #{:text :group} (:type shape))) (or (:fill-color shape) (:fill-color-gradient shape) (seq (:fills shape))))) -(defn copy-data [shape] - (cg/generate-css-props - shape - fill-attributes - {:to-prop "background" - :format #(uc/color->background (shape->color shape))})) - -(defn copy-data-format [shape format] - (cg/generate-css-props - shape - fill-attributes - {:to-prop "background-color" - :format #(uc/color->format->background (shape->color shape) format)})) - -(mf/defc fill-block [{:keys [shape]}] - (let [color-format (mf/use-state :hex) - color (shape->color shape)] - - [:div.attributes-fill-block - [:& color-row {:color color - :format @color-format - :on-change-format #(reset! color-format %) - :copy-data (copy-data-format shape @color-format)}]])) +(mf/defc fill-block + {::mf/wrap-props false} + [{:keys [objects shape]}] + (let [format* (mf/use-state :hex) + format (deref format*) + color (shape->color shape) + on-change + (mf/use-fn + (fn [format] + (reset! format* format)))] + [:div {:class (stl/css :attributes-fill-block)} + [:& color-row + {:color color + :format format + :on-change-format on-change + :copy-data (css/get-shape-properties-css objects {:fills [shape]} properties)}]])) (mf/defc fill-panel + {::mf/wrap-props false} [{:keys [shapes]}] - (let [shapes (->> shapes (filter has-color?))] + (let [shapes (filter has-fill? shapes)] (when (seq shapes) - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (tr "inspect.attributes.fill")]] + [:div {:class (stl/css :attributes-block)} + [:& inspect-title-bar + {:title (tr "inspect.attributes.fill") + :class (stl/css :title-spacing-fill)}] - [:div.attributes-fill-blocks + [:div {:class (stl/css :attributes-content)} (for [shape shapes] - (if (seq (:fills shape)) - (for [value (:fills shape [])] - [:& fill-block {:key (str "fill-block-" (:id shape) value) - :shape value}]) - [:& fill-block {:key (str "fill-block-only" (:id shape)) - :shape shape}]))]]))) + (if (seq (:fills shape)) + (for [value (:fills shape [])] + [:& fill-block {:key (str "fill-block-" (:id shape) value) + :shape value}]) + [:& fill-block {:key (str "fill-block-only" (:id shape)) + :shape shape}]))]]))) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/fill.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/fill.scss new file mode 100644 index 0000000000..9515dad3ee --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/fill.scss @@ -0,0 +1,20 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.attributes-block { + @include flexColumn; +} + +.title-spacing-fill { + @extend .attr-title; +} + +.attributes-content { + display: grid; + gap: $s-4; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/geometry.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/geometry.cljs new file mode 100644 index 0000000000..8fd9c4ee70 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/geometry.cljs @@ -0,0 +1,50 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.viewer.inspect.attributes.geometry + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.components.title-bar :refer [inspect-title-bar]] + [app.main.ui.viewer.inspect.attributes.common :as cmm] + [app.util.code-gen.style-css :as css] + [app.util.i18n :refer [tr]] + [rumext.v2 :as mf])) + +(def properties [:width :height :left :top :border-radius :transform]) + +(mf/defc geometry-block + [{:keys [objects shape]}] + [:* + (for [[idx property] (d/enumerate properties)] + (when-let [value (css/get-css-value objects shape property)] + (let [property-name (cmm/get-css-rule-humanized property)] + [:div {:key (dm/str "block-" idx "-" (d/name property)) + :title property-name + :class (stl/css :geometry-row)} + [:div {:class (stl/css :global/attr-label)} property-name] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (css/get-css-property objects shape property)} + [:div {:class (stl/css :button-children)} value]]]])))]) + + +(mf/defc geometry-panel + [{:keys [objects shapes]}] + [:div {:class (stl/css :attributes-block)} + [:& inspect-title-bar + {:title (tr "inspect.attributes.size") + :class (stl/css :title-spacing-geometry)} + + (when (= (count shapes) 1) + [:& copy-button {:data (css/get-shape-properties-css objects (first shapes) properties) + :class (stl/css :copy-btn-title)}])] + + (for [shape shapes] + [:& geometry-block {:shape shape + :objects objects + :key (:id shape)}])]) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/geometry.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/geometry.scss new file mode 100644 index 0000000000..b32c0c1031 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/geometry.scss @@ -0,0 +1,27 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.attributes-block { + @include flexColumn; +} + +.title-spacing-geometry { + @extend .attr-title; +} + +.geometry-row { + @extend .attr-row; +} + +.button-children { + @extend .copy-button-children; +} + +.copy-btn-title { + max-width: $s-28; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/image.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/image.cljs index 5d25e5e9db..093b6a6578 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/image.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/image.cljs @@ -5,12 +5,13 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.attributes.image + (:require-macros [app.main.style :as stl]) (:require + [app.common.files.helpers :as cfh] [app.common.media :as cm] - [app.common.pages.helpers :as cph] [app.config :as cf] [app.main.ui.components.copy-button :refer [copy-button]] - [app.util.code-gen :as cg] + [app.util.code-gen.style-css :as css] [app.util.i18n :refer [tr]] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -19,27 +20,32 @@ (= (:type shape) :image)) (mf/defc image-panel - [{:keys [shapes]}] - (for [shape (filter cph/image-shape? shapes)] - [:div.attributes-block {:key (str "image-" (:id shape))} - [:div.attributes-image-row - [:div.attributes-image - [:img {:src (cf/resolve-file-media (-> shape :metadata))}]]] + [{:keys [objects shapes]}] + (for [shape (filter cfh/image-shape? shapes)] + [:div {:class (stl/css :attributes-block) + :key (str "image-" (:id shape))} + [:div {:class (stl/css :image-wrapper)} + [:img {:src (cf/resolve-file-media (-> shape :metadata))}]] - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.image.width")] - [:div.attributes-value (-> shape :metadata :width) "px"] - [:& copy-button {:data (cg/generate-css-props shape :width)}]] + [:div {:class (stl/css :image-row)} + [:div {:class (stl/css :global/attr-label)} + (tr "inspect.attributes.image.width")] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (css/get-css-property objects (:metadata shape) :width)} + [:div {:class (stl/css :button-children)} (css/get-css-value objects (:metadata shape) :width)]]]] - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.image.height")] - [:div.attributes-value (-> shape :metadata :height) "px"] - [:& copy-button {:data (cg/generate-css-props shape :height)}]] + [:div {:class (stl/css :image-row)} + [:div {:class (stl/css :global/attr-label)} + (tr "inspect.attributes.image.height")] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (css/get-css-property objects (:metadata shape) :height)} + [:div {:class (stl/css :button-children)} (css/get-css-value objects (:metadata shape) :height)]]]] (let [mtype (-> shape :metadata :mtype) name (:name shape) extension (cm/mtype->extension mtype)] - [:a.download-button {:target "_blank" - :download (cond-> name extension (str/concat extension)) - :href (cf/resolve-file-media (-> shape :metadata))} + [:a {:class (stl/css :download-button) + :target "_blank" + :download (cond-> name extension (str/concat extension)) + :href (cf/resolve-file-media (-> shape :metadata))} (tr "inspect.attributes.image.download")])])) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/image.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/image.scss new file mode 100644 index 0000000000..f9e128e307 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/image.scss @@ -0,0 +1,45 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.attributes-block { + @include flexColumn; + margin-bottom: $s-16; +} + +.image-wrapper { + background-color: var(--menu-background-color); + position: relative; + @include flexCenter; + width: $s-248; + height: $s-160; + max-height: $s-160; + max-width: $s-248; + margin: $s-8 0; + border-radius: $br-8; + + img { + height: 100%; + width: 100%; + object-fit: contain; + } +} + +.image-row { + @extend .attr-row; +} + +.button-children { + @extend .copy-button-children; +} + +.download-button { + @extend .button-secondary; + @include uppercaseTitleTipography; + height: $s-32; + margin-top: $s-4; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/layout.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/layout.cljs index f1d4490a2c..9af1d09272 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/layout.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/layout.cljs @@ -5,93 +5,62 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.attributes.layout + (:require-macros [app.main.style :as stl]) (:require - [app.common.types.shape.radius :as ctsr] + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.types.shape.layout :as ctl] [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.formats :as fmt] - [app.util.code-gen :as cg] - [app.util.i18n :refer [tr]] - [cuerdas.core :as str] + [app.main.ui.components.title-bar :refer [inspect-title-bar]] + [app.main.ui.viewer.inspect.attributes.common :as cmm] + [app.util.code-gen.style-css :as css] [rumext.v2 :as mf])) -(def properties [:width :height :x :y :radius :rx :r1]) - -(def params - {:to-prop {:x "left" - :y "top" - :rotation "transform" - :rx "border-radius" - :r1 "border-radius"} - :format {:rotation #(str/fmt "rotate(%sdeg)" %) - :r1 #(apply str/fmt "%spx, %spx, %spx, %spx" %) - :width #(cg/get-size :width %) - :height #(cg/get-size :height %)} - :multi {:r1 [:r1 :r2 :r3 :r4]}}) - -(defn copy-data - ([shape] - (apply copy-data shape properties)) - ([shape & properties] - (cg/generate-css-props shape properties params))) +(def properties + [:display + :flex-direction + :flex-wrap + :grid-template-rows + :grid-template-columns + :align-items + :align-content + :justify-items + :justify-content + :row-gap + :column-gap + :gap + :padding]) (mf/defc layout-block - [{:keys [shape]}] - (let [selrect (:selrect shape) - {:keys [x y width height]} selrect] - [:* - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.layout.width")] - [:div.attributes-value (fmt/format-size :width width shape)] - [:& copy-button {:data (copy-data selrect :width)}]] - - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.layout.height")] - [:div.attributes-value (fmt/format-size :height height shape)] - [:& copy-button {:data (copy-data selrect :height)}]] - - (when (not= (:x shape) 0) - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.layout.left")] - [:div.attributes-value (fmt/format-pixels x)] - [:& copy-button {:data (copy-data selrect :x)}]]) - - (when (not= (:y shape) 0) - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.layout.top")] - [:div.attributes-value (fmt/format-pixels y)] - [:& copy-button {:data (copy-data selrect :y)}]]) - - (when (ctsr/radius-1? shape) - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.layout.radius")] - [:div.attributes-value (fmt/format-pixels (:rx shape 0))] - [:& copy-button {:data (copy-data shape :rx)}]]) - - (when (ctsr/radius-4? shape) - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.layout.radius")] - [:div.attributes-value - (fmt/format-number (:r1 shape)) ", " - (fmt/format-number (:r2 shape)) ", " - (fmt/format-number (:r3 shape)) ", " - (fmt/format-pixels (:r4 shape))] - [:& copy-button {:data (copy-data shape :r1)}]]) - - (when (not= (:rotation shape 0) 0) - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.layout.rotation")] - [:div.attributes-value (fmt/format-number (:rotation shape)) "deg"] - [:& copy-button {:data (copy-data shape :rotation)}]])])) + [{:keys [objects shape]}] + (for [property properties] + (when-let [value (css/get-css-value objects shape property)] + (let [property-name (cmm/get-css-rule-humanized property)] + [:div {:class (stl/css :layout-row)} + [:div {:title property-name + :key (dm/str "layout-" (:id shape) "-" (d/name property)) + :class (stl/css :global/attr-label)} + property-name] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (css/get-css-property objects shape property)} + [:div {:class (stl/css :button-children)} value]]]])))) (mf/defc layout-panel - [{:keys [shapes]}] - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (tr "inspect.attributes.size")] - (when (= (count shapes) 1) - [:& copy-button {:data (copy-data (first shapes))}])] + [{:keys [objects shapes]}] + (let [shapes (->> shapes (filter ctl/any-layout?))] - (for [shape shapes] - [:& layout-block {:shape shape - :key (:id shape)}])]) + (when (seq shapes) + [:div {:class (stl/css :attributes-block)} + [:& inspect-title-bar + {:title "Layout" + :class (stl/css :title-spacing-layout)} + + (when (= (count shapes) 1) + [:& copy-button {:data (css/get-shape-properties-css objects (first shapes) properties) + :class (stl/css :copy-btn-title)}])] + + (for [shape shapes] + [:& layout-block {:shape shape + :objects objects + :key (:id shape)}])]))) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/layout.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/layout.scss new file mode 100644 index 0000000000..8a84e1d988 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/layout.scss @@ -0,0 +1,27 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.attributes-block { + @include flexColumn; +} + +.title-spacing-layout { + @extend .attr-title; +} + +.layout-row { + @extend .attr-row; +} + +.button-children { + @extend .copy-button-children; +} + +.copy-btn-title { + max-width: $s-28; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/layout_element.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/layout_element.cljs new file mode 100644 index 0000000000..4ba5a98a6b --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/layout_element.cljs @@ -0,0 +1,80 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.viewer.inspect.attributes.layout-element + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.types.shape.layout :as ctl] + [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.components.title-bar :refer [title-bar]] + [app.main.ui.viewer.inspect.attributes.common :as cmm] + [app.util.code-gen.style-css :as css] + [rumext.v2 :as mf])) + +(def properties + [:margin + :max-height + :min-height + :max-width + :min-width + :align-self + :justify-self + :flex-shrink + :flex + + ;; Grid cell properties + :grid-column + :grid-row]) + +(mf/defc layout-element-block + [{:keys [objects shape]}] + (for [property properties] + (when-let [value (css/get-css-value objects shape property)] + (let [property-name (cmm/get-css-rule-humanized property)] + [:div {:class (stl/css :layout-element-row) + :key (dm/str "layout-element-" (:id shape) "-" (d/name property))} + [:div {:class (stl/css :global/attr-label)} property-name] + [:div {:class (stl/css :global/attr-value)} + + [:& copy-button {:data (css/get-css-property objects shape property)} + [:div {:class (stl/css :button-children)} value]]]])))) + +(mf/defc layout-element-panel + [{:keys [objects shapes]}] + (let [shapes (->> shapes (filter #(ctl/any-layout-immediate-child? objects %))) + only-flex? (every? #(ctl/flex-layout-immediate-child? objects %) shapes) + only-grid? (every? #(ctl/grid-layout-immediate-child? objects %) shapes) + + some-layout-prop? + (->> shapes + (mapcat (fn [shape] + (keep #(css/get-css-value objects shape %) properties))) + (seq)) + + menu-title + (cond + only-flex? + "Flex element" + only-grid? + "Flex element" + :else + "Layout element")] + + (when some-layout-prop? + [:div {:class (stl/css :attributes-block)} + [:& title-bar {:collapsable false + :title menu-title + :class (stl/css :title-spacing-layout-element)} + (when (= (count shapes) 1) + [:& copy-button {:data (css/get-shape-properties-css objects (first shapes) properties) + :class (stl/css :copy-btn-title)}])] + + (for [shape shapes] + [:& layout-element-block {:shape shape + :objects objects + :key (:id shape)}])]))) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/layout_element.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/layout_element.scss new file mode 100644 index 0000000000..56b174dd58 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/layout_element.scss @@ -0,0 +1,27 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.attributes-block { + @include flexColumn; +} + +.title-spacing-layout-element { + @extend .attr-title; +} + +.layout-element-row { + @extend .attr-row; +} + +.button-children { + @extend .copy-button-children; +} + +.copy-btn-title { + max-width: $s-28; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/layout_flex.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/layout_flex.cljs deleted file mode 100644 index 4e4430f3cd..0000000000 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/layout_flex.cljs +++ /dev/null @@ -1,139 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.viewer.inspect.attributes.layout-flex - (:require - [app.common.data :as d] - [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.formats :as fm] - [app.util.code-gen :as cg] - [cuerdas.core :as str] - [rumext.v2 :as mf])) - -(def properties [:layout - :layout-flex-dir - :layout-align-items - :layout-justify-content - :layout-gap - :layout-padding - :layout-wrap-type]) - -(def align-contet-prop [:layout-align-content]) - -(def layout-flex-params - {:props [:layout - :layout-align-items - :layout-flex-dir - :layout-justify-content - :layout-gap - :layout-padding - :layout-wrap-type] - :to-prop {:layout "display" - :layout-flex-dir "flex-direction" - :layout-align-items "align-items" - :layout-justify-content "justify-content" - :layout-wrap-type "flex-wrap" - :layout-gap "gap" - :layout-padding "padding"} - :format {:layout d/name - :layout-flex-dir d/name - :layout-align-items d/name - :layout-justify-content d/name - :layout-wrap-type d/name - :layout-gap fm/format-gap - :layout-padding fm/format-padding}}) - -(def layout-align-content-params - {:props [:layout-align-content] - :to-prop {:layout-align-content "align-content"} - :format {:layout-align-content d/name}}) - -(defn copy-data - ([shape] - (let [properties-for-copy (if (:layout-align-content shape) - (into [] (concat properties align-contet-prop)) - properties)] - (apply copy-data shape properties-for-copy))) - - ([shape & properties] - (let [params (if (:layout-align-content shape) - (d/deep-merge layout-align-content-params layout-flex-params ) - layout-flex-params)] - (cg/generate-css-props shape properties params)))) - -(mf/defc manage-padding - [{:keys [padding type]}] - (let [values (fm/format-padding-margin-shorthand (vals padding))] - [:div.attributes-value - {:title (str (str/join "px " (vals values)) "px")} - (for [[k v] values] - [:span.items {:key (str type "-" k "-" v)} v "px"])])) - -(mf/defc layout-flex-block - [{:keys [shape]}] - [:* - [:div.attributes-unit-row - [:div.attributes-label "Display"] - [:div.attributes-value "Flex"] - [:& copy-button {:data (copy-data shape)}]] - - [:div.attributes-unit-row - [:div.attributes-label "Direction"] - [:div.attributes-value (str/capital (d/name (:layout-flex-dir shape)))] - [:& copy-button {:data (copy-data shape :layout-flex-dir)}]] - - [:div.attributes-unit-row - [:div.attributes-label "Align-items"] - [:div.attributes-value (str/capital (d/name (:layout-align-items shape)))] - [:& copy-button {:data (copy-data shape :layout-align-items)}]] - - [:div.attributes-unit-row - [:div.attributes-label "Justify-content"] - [:div.attributes-value (str/capital (d/name (:layout-justify-content shape)))] - [:& copy-button {:data (copy-data shape :layout-justify-content)}]] - - [:div.attributes-unit-row - [:div.attributes-label "Flex wrap"] - [:div.attributes-value (str/capital (d/name (:layout-wrap-type shape)))] - [:& copy-button {:data (copy-data shape :layout-wrap-type)}]] - - (when (= :wrap (:layout-wrap-type shape)) - [:div.attributes-unit-row - [:div.attributes-label "Align-content"] - [:div.attributes-value (str/capital (d/name (:layout-align-content shape)))] - [:& copy-button {:data (copy-data shape :layout-align-content)}]]) - - [:div.attributes-unit-row - [:div.attributes-label "Gaps"] - (if (= (:row-gap (:layout-gap shape)) (:column-gap (:layout-gap shape))) - [:div.attributes-value - [:span (-> shape :layout-gap :row-gap fm/format-pixels)]] - [:div.attributes-value - [:span.items (-> shape :layout-gap :row-gap fm/format-pixels)] - [:span (-> shape :layout-gap :column-gap fm/format-pixels)]]) - [:& copy-button {:data (copy-data shape :layout-gap)}]] - - [:div.attributes-unit-row - [:div.attributes-label "Padding"] - [:& manage-padding {:padding (:layout-padding shape) :type "padding"}] - [:& copy-button {:data (copy-data shape :layout-padding)}]]]) - -(defn has-flex? [shape] - (= :flex (:layout shape))) - -(mf/defc layout-flex-panel - [{:keys [shapes]}] - (let [shapes (->> shapes (filter has-flex?))] - (when (seq shapes) - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text "Layout"] - (when (= (count shapes) 1) - [:& copy-button {:data (copy-data (first shapes))}])] - - (for [shape shapes] - [:& layout-flex-block {:shape shape - :key (:id shape)}])]))) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/layout_flex_element.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/layout_flex_element.cljs deleted file mode 100644 index 9e23c6a4c1..0000000000 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/layout_flex_element.cljs +++ /dev/null @@ -1,155 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.viewer.inspect.attributes.layout-flex-element - (:require - [app.common.data :as d] - [app.main.refs :as refs] - [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.formats :as fmt] - [app.main.ui.viewer.inspect.code :as cd] - [app.util.code-gen :as cg] - [cuerdas.core :as str] - [rumext.v2 :as mf])) - - -(defn format-margin - [margin-values] - (let [short-hand (fmt/format-padding-margin-shorthand (vals margin-values)) - parsed-values (map #(str/fmt "%spx" %) (vals short-hand))] - (str/join " " parsed-values))) - -(def properties [:layout-item-margin ;; {:m1 0 :m2 0 :m3 0 :m4 0} - :layout-item-max-h ;; num - :layout-item-min-h ;; num - :layout-item-max-w ;; num - :layout-item-min-w ;; num - :layout-item-align-self]) ;; :start :end :center - -(def layout-flex-item-params - {:props [:layout-item-margin - :layout-item-max-h - :layout-item-min-h - :layout-item-max-w - :layout-item-min-w - :layout-item-align-self] - :to-prop {:layout-item-margin "margin" - :layout-item-align-self "align-self" - :layout-item-max-h "max-height" - :layout-item-min-h "min-height" - :layout-item-max-w "max-width" - :layout-item-min-w "min-width"} - :format {:layout-item-margin format-margin - :layout-item-align-self d/name}}) - -(defn copy-data - ([shape] - (apply copy-data shape properties)) - - ([shape & properties] - (cg/generate-css-props shape properties layout-flex-item-params))) - -(mf/defc manage-margin - [{:keys [margin type]}] - (let [values (fmt/format-padding-margin-shorthand (vals margin))] - [:div.attributes-value - (for [[k v] values] - [:span.items {:key (str type "-" k "-" v)} v "px"])])) - -(defn manage-sizing - [value type] - (let [ref-value-h {:fill "Width 100%" - :fix "Fixed width" - :auto "Fit content"} - ref-value-v {:fill "Height 100%" - :fix "Fixed height" - :auto "Fit content"}] - (if (= :h type) - (ref-value-h value) - (ref-value-v value)))) - -(mf/defc layout-element-block - [{:keys [shape]}] - (let [old-margin (:layout-item-margin shape) - new-margin {:m1 0 :m2 0 :m3 0 :m4 0} - merged-margin (merge new-margin old-margin) - shape (assoc shape :layout-item-margin merged-margin)] - - [:* - (when (:layout-item-align-self shape) - [:div.attributes-unit-row - [:div.attributes-label "Align self"] - [:div.attributes-value (str/capital (d/name (:layout-item-align-self shape)))] - [:& copy-button {:data (copy-data shape :layout-item-align-self)}]]) - - (when (:layout-item-margin shape) - [:div.attributes-unit-row - [:div.attributes-label "Margin"] - [:& manage-margin {:margin merged-margin :type "margin"}] - [:& copy-button {:data (copy-data shape :layout-item-margin)}]]) - - (when (:layout-item-h-sizing shape) - [:div.attributes-unit-row - [:div.attributes-label "Horizontal sizing"] - [:div.attributes-value (manage-sizing (:layout-item-h-sizing shape) :h)] - [:& copy-button {:data (copy-data shape :layout-item-h-sizing)}]]) - - (when (:layout-item-v-sizing shape) - [:div.attributes-unit-row - [:div.attributes-label "Vertical sizing"] - [:div.attributes-value (manage-sizing (:layout-item-v-sizing shape) :v)] - [:& copy-button {:data (copy-data shape :layout-item-v-sizing)}]]) - - (when (= :fill (:layout-item-h-sizing shape)) - [:* - (when (some? (:layout-item-max-w shape)) - [:div.attributes-unit-row - [:div.attributes-label "Max. width"] - [:div.attributes-value (fmt/format-pixels (:layout-item-max-w shape))] - [:& copy-button {:data (copy-data shape :layout-item-max-w)}]]) - - (when (some? (:layout-item-min-w shape)) - [:div.attributes-unit-row - [:div.attributes-label "Min. width"] - [:div.attributes-value (fmt/format-pixels (:layout-item-min-w shape))] - [:& copy-button {:data (copy-data shape :layout-item-min-w)}]])]) - - (when (= :fill (:layout-item-v-sizing shape)) - [:* - (when (:layout-item-max-h shape) - [:div.attributes-unit-row - [:div.attributes-label "Max. height"] - [:div.attributes-value (fmt/format-pixels (:layout-item-max-h shape))] - [:& copy-button {:data (copy-data shape :layout-item-max-h)}]]) - - (when (:layout-item-min-h shape) - [:div.attributes-unit-row - [:div.attributes-label "Min. height"] - [:div.attributes-value (fmt/format-pixels (:layout-item-min-h shape))] - [:& copy-button {:data (copy-data shape :layout-item-min-h)}]])])])) - -(mf/defc layout-flex-element-panel - [{:keys [shapes from]}] - (let [route (mf/deref refs/route) - page-id (:page-id (:query-params route)) - mod-shapes (cd/get-flex-elements page-id shapes from) - shape (first mod-shapes) - has-margin? (some? (:layout-item-margin shape)) - has-values? (or (some? (:layout-item-max-w shape)) - (some? (:layout-item-max-h shape)) - (some? (:layout-item-min-w shape)) - (some? (:layout-item-min-h shape))) - has-align? (some? (:layout-item-align-self shape)) - has-sizing? (or (some? (:layout-item-h-sizing shape)) - (some? (:layout-item-w-sizing shape))) - must-show (or has-margin? has-values? has-align? has-sizing?)] - (when (and (= (count mod-shapes) 1) must-show) - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text "Flex element"] - [:& copy-button {:data (copy-data shape)}]] - - [:& layout-element-block {:shape shape}]]))) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/shadow.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/shadow.cljs index 25f04df952..c62a85fb9a 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/shadow.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/shadow.cljs @@ -5,11 +5,14 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.attributes.shadow + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.components.title-bar :refer [inspect-title-bar]] [app.main.ui.viewer.inspect.attributes.common :refer [color-row]] - [app.util.code-gen :as cg] + [app.util.code-gen.style-css :as css] [app.util.i18n :refer [tr]] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -18,37 +21,28 @@ (:shadow shape)) (defn shape-copy-data [shape] - (cg/generate-css-props - shape - :shadow - {:to-prop "box-shadow" - :format #(str/join ", " (map cg/shadow->css (:shadow shape)))})) + (str/join ", " (map css/shadow->css (:shadow shape)))) (defn shadow-copy-data [shadow] - (cg/generate-css-props - shadow - :style - {:to-prop "box-shadow" - :format #(cg/shadow->css shadow)})) + (css/shadow->css shadow)) (mf/defc shadow-block [{:keys [shadow]}] (let [color-format (mf/use-state :hex)] - [:div.attributes-shadow-block - [:div.attributes-shadow-row - [:div.attributes-label (->> shadow :style d/name (str "workspace.options.shadow-options.") (tr))] - [:div.attributes-shadow {:title (tr "workspace.options.shadow-options.offsetx")} - [:div.attributes-value (str (:offset-x shadow) "px")]] - - [:div.attributes-shadow {:title (tr "workspace.options.shadow-options.offsety")} - [:div.attributes-value (str (:offset-y shadow) "px")]] - - [:div.attributes-shadow {:title (tr "workspace.options.shadow-options.blur")} - [:div.attributes-value (str (:blur shadow) "px")]] - - [:div.attributes-shadow {:title (tr "workspace.options.shadow-options.spread")} - [:div.attributes-value (str (:spread shadow) "px")]] - - [:& copy-button {:data (shadow-copy-data shadow)}]] + [:div {:class (stl/css :attributes-shadow-block)} + [:div {:class (stl/css :shadow-row)} + [:div {:class (stl/css :global/attr-label)} (->> shadow :style d/name (str "workspace.options.shadow-options.") (tr))] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (shadow-copy-data shadow) + :class (stl/css :color-row-copy-btn)} + [:div {:class (stl/css :button-children) + :title (dm/str (tr "workspace.options.shadow-options.offsetx") " " + (tr "workspace.options.shadow-options.offsety") " " + (tr "workspace.options.shadow-options.blur") " " + (tr "workspace.options.shadow-options.spread"))} + (str (:offset-x shadow) "px") " " + (str (:offset-y shadow) "px") " " + (str (:blur shadow) "px") " " + (str (:spread shadow) "px")]]]] [:& color-row {:color (:color shadow) :format @color-format @@ -56,13 +50,16 @@ (mf/defc shadow-panel [{:keys [shapes]}] (let [shapes (->> shapes (filter has-shadow?))] - (when (and (seq shapes) (> (count shapes) 0)) - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (tr "inspect.attributes.shadow")]] - [:div.attributes-shadow-blocks + (when (and (seq shapes) (> (count shapes) 0)) + [:div {:class (stl/css :attributes-block)} + [:& inspect-title-bar + {:title (tr "inspect.attributes.shadow") + :class (stl/css :title-spacing-shadow)}] + + [:div {:class (stl/css :attributes-content)} (for [shape shapes] (for [shadow (:shadow shape)] [:& shadow-block {:shape shape + :key (dm/str "block-" (:id shape) "-shadow") :shadow shadow}]))]]))) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/shadow.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/shadow.scss new file mode 100644 index 0000000000..1d741ab26e --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/shadow.scss @@ -0,0 +1,23 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.attributes-block { + @include flexColumn; +} + +.title-spacing-shadow { + @extend .attr-title; +} + +.shadow-row { + @extend .attr-row; +} + +.button-children { + @extend .copy-button-children; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/stroke.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/stroke.cljs index 34e1f70bc5..5d55671605 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/stroke.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/stroke.cljs @@ -5,93 +5,55 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.attributes.stroke + (:require-macros [app.main.style :as stl]) (:require - [app.common.data :as d] - [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.components.title-bar :refer [inspect-title-bar]] [app.main.ui.viewer.inspect.attributes.common :refer [color-row]] - [app.util.code-gen :as cg] - [app.util.color :as uc] + [app.util.code-gen.style-css :as css] [app.util.i18n :refer [tr]] - [cuerdas.core :as str] [rumext.v2 :as mf])) -(defn shape->color [shape] +(def properties [:border]) + +(defn stroke->color [shape] {:color (:stroke-color shape) :opacity (:stroke-opacity shape) :gradient (:stroke-color-gradient shape) :id (:stroke-color-ref-id shape) - :file-id (:stroke-color-ref-file shape)}) - -(defn format-stroke [shape] - (let [width (:stroke-width shape) - style (d/name (:stroke-style shape)) - style (if (= style "svg") "solid" style) - color (-> shape shape->color uc/color->background)] - (str/format "%spx %s %s" width style color))) + :file-id (:stroke-color-ref-file shape) + :image (:stroke-image shape)}) (defn has-stroke? [shape] - (let [stroke-style (:stroke-style shape)] - (or - (and stroke-style - (and (not= stroke-style :none) - (not= stroke-style :svg))) - (seq (:strokes shape))))) - -(defn copy-stroke-data [shape] - (cg/generate-css-props - shape - :stroke-style - {:to-prop "border" - :format #(format-stroke shape)})) - -(defn copy-color-data [shape] - (cg/generate-css-props - shape - :stroke-color - {:to-prop "border-color" - :format #(uc/color->background (shape->color shape))})) + (seq (:strokes shape))) (mf/defc stroke-block - [{:keys [shape]}] - (let [color-format (mf/use-state :hex) - color (shape->color shape)] - [:div.attributes-stroke-block - (let [{:keys [stroke-style stroke-alignment]} shape - stroke-style (if (= stroke-style :svg) :solid stroke-style) - stroke-alignment (or stroke-alignment :center)] - [:div.attributes-stroke-row - [:div.attributes-label (tr "inspect.attributes.stroke.width")] - [:div.attributes-value (:stroke-width shape) "px"] - ;; Execution time translation strings: - ;; inspect.attributes.stroke.style.dotted - ;; inspect.attributes.stroke.style.mixed - ;; inspect.attributes.stroke.style.none - ;; inspect.attributes.stroke.style.solid - [:div.attributes-value (->> stroke-style d/name (str "inspect.attributes.stroke.style.") (tr))] - ;; Execution time translation strings: - ;; inspect.attributes.stroke.alignment.center - ;; inspect.attributes.stroke.alignment.inner - ;; inspect.attributes.stroke.alignment.outer - [:div.attributes-label (->> stroke-alignment d/name (str "inspect.attributes.stroke.alignment.") (tr))] - [:& copy-button {:data (copy-stroke-data shape)}]]) - [:& color-row {:color color - :format @color-format - :copy-data (copy-color-data shape) - :on-change-format #(reset! color-format %)}]])) + {::mf/wrap-props false} + [{:keys [objects shape]}] + (let [format* (mf/use-state :hex) + format (deref format*) + color (stroke->color shape) + on-change + (mf/use-fn + (fn [format] + (reset! format* format)))] + [:div {:class (stl/css :attributes-fill-block)} + [:& color-row + {:color color + :format format + :on-change-format on-change + :copy-data (css/get-shape-properties-css objects {:strokes [shape]} properties)}]])) (mf/defc stroke-panel [{:keys [shapes]}] (let [shapes (->> shapes (filter has-stroke?))] (when (seq shapes) - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (tr "inspect.attributes.stroke")]] + [:div {:class (stl/css :attributes-block)} + [:& inspect-title-bar + {:title (tr "inspect.attributes.stroke") + :class (stl/css :title-spacing-stroke)}] - [:div.attributes-stroke-blocks + [:div {:class (stl/css :attributes-content)} (for [shape shapes] - (if (seq (:strokes shape)) - (for [value (:strokes shape [])] - [:& stroke-block {:key (str "stroke-color-" (:id shape) value) - :shape value}]) - [:& stroke-block {:key (str "stroke-color-only" (:id shape)) - :shape shape}]))]]))) + (for [value (:strokes shape)] + [:& stroke-block {:key (str "stroke-color-" (:id shape) value) + :shape value}]))]]))) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/stroke.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/stroke.scss new file mode 100644 index 0000000000..fe0f59df43 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/stroke.scss @@ -0,0 +1,32 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.attributes-block { + @include flexColumn; +} + +.title-spacing-stroke { + @extend .attr-title; +} + +.attributes-stroke-block { + @include flexColumn; +} + +.stroke-row { + @extend .attr-row; +} + +.button-children { + @extend .copy-button-children; +} + +.attributes-content { + display: grid; + gap: $s-4; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/svg.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/svg.cljs index 80731f37bb..b353ad5ba5 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/svg.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/svg.cljs @@ -5,9 +5,11 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.attributes.svg + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.components.title-bar :refer [inspect-title-bar]] [app.util.i18n :refer [tr]] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -20,31 +22,38 @@ (mf/defc svg-attr [{:keys [attr value]}] (if (map? value) [:* - [:div.attributes-block-title - [:div.attributes-block-title-text (d/name attr)] + [:div {:class (stl/css :attributes-subtitle)} + [:span (d/name attr)] [:& copy-button {:data (map->css value)}]] (for [[attr-key attr-value] value] - [:& svg-attr {:attr attr-key :value attr-value}])] + [:& svg-attr {:attr attr-key :value attr-value :key (str/join "svg-key-" attr-key)}])] - [:div.attributes-unit-row - [:div.attributes-label (d/name attr)] - [:div.attributes-value (str value)] - [:& copy-button {:data (d/name value)}]])) + (let [attr-name (as-> attr $ + (d/name $) + (str/split $ "-") + (str/join " " $) + (str/capital $))] + [:div {:class (stl/css :svg-row)} + [:div {:class (stl/css :global/attr-label)} attr-name] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (d/name value) + :class (stl/css :copy-btn-title)} + [:div {:class (stl/css :button-children)} (str value)]]]]))) (mf/defc svg-block [{:keys [shape]}] [:* (for [[attr-key attr-value] (:svg-attrs shape)] - [:& svg-attr {:attr attr-key :value attr-value}])] ) + [:& svg-attr {:attr attr-key :value attr-value :key (str/join "svg-block-key" attr-key)}])]) (mf/defc svg-panel [{:keys [shapes]}] - (let [shape (first shapes)] (when (seq (:svg-attrs shape)) - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (tr "workspace.sidebar.options.svg-attrs.title")]] + [:div {:class (stl/css :attributes-block)} + [:& inspect-title-bar + {:title (tr "workspace.sidebar.options.svg-attrs.title") + :class (stl/css :title-spacing-svg)}] [:& svg-block {:shape shape}]]))) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/svg.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/svg.scss new file mode 100644 index 0000000000..1ad6d6a8fc --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/svg.scss @@ -0,0 +1,44 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.attributes-block { + @include flexColumn; +} + +.title-spacing-svg { + @extend .attr-title; +} + +.svg-row { + @extend .attr-row; +} + +.button-children { + @extend .copy-button-children; +} + +.attributes-subtitle { + @include uppercaseTitleTipography; + display: flex; + justify-content: space-between; + height: $s-32; + span { + height: $s-32; + display: flex; + align-items: center; + } + button { + display: none; + } + + &:hover { + button { + display: block; + } + } +} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/text.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/text.cljs index d5c1c87b45..d2a3eceb85 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/text.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/text.cljs @@ -5,18 +5,19 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.attributes.text + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.text :as txt] [app.main.fonts :as fonts] + [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.components.title-bar :refer [inspect-title-bar]] + [app.main.ui.formats :as fmt] [app.main.ui.viewer.inspect.attributes.common :refer [color-row]] - [app.util.code-gen :as cg] - [app.util.color :as uc] [app.util.i18n :refer [tr]] - [app.util.strings :as ust] [cuerdas.core :as str] [okulary.core :as l] [rumext.v2 :as mf])) @@ -33,159 +34,154 @@ (get-in state [:viewer-libraries file-id :data :typographies]))] #(l/derived get-library st/state))) -(defn format-number [number] - (-> number - d/parse-double - (ust/format-precision 2))) - -(def properties [:fill-color - :fill-color-gradient - :font-family - :font-style - :font-size - :font-weight - :line-height - :letter-spacing - :text-decoration - :text-transform]) - -(defn shape->color [shape] - {:color (:fill-color shape) - :opacity (:fill-opacity shape) - :gradient (:fill-color-gradient shape) - :id (:fill-color-ref-id shape) - :file-id (:fill-color-ref-file shape)}) - -(def params - {:to-prop {:fill-color "color" - :fill-color-gradient "color"} - :format {:font-family #(dm/str "'" % "'") - :font-style #(dm/str % ) - :font-size #(dm/str (format-number %) "px") - :font-weight d/name - :line-height #(format-number %) - :letter-spacing #(dm/str (format-number %) "px") - :text-decoration d/name - :text-transform d/name - :fill-color #(-> %2 shape->color uc/color->background) - :fill-color-gradient #(-> %2 shape->color uc/color->background)}}) +(defn fill->color [{:keys [fill-color fill-opacity fill-color-gradient fill-color-ref-id fill-color-ref-file fill-image]}] + {:color fill-color + :opacity fill-opacity + :gradient fill-color-gradient + :id fill-color-ref-id + :file-id fill-color-ref-file + :image fill-image}) (defn copy-style-data - ([style] - (cg/generate-css-props style properties params)) - ([style & properties] - (cg/generate-css-props style properties params))) + [style & properties] + (->> properties + (map #(dm/str (d/name %) ": " (get style %) ";")) + (str/join "\n"))) + +(mf/defc typography-block + [{:keys [text style]}] + (let [typography-library-ref + (mf/use-memo + (mf/deps (:typography-ref-file style)) + (make-typographies-library-ref (:typography-ref-file style))) -(mf/defc typography-block [{:keys [text style]}] - (let [typography-library-ref (mf/use-memo - (mf/deps (:typography-ref-file style)) - (make-typographies-library-ref (:typography-ref-file style))) typography-library (mf/deref typography-library-ref) + file-typographies-viewer (mf/deref file-typographies-ref) + file-typographies-workspace (mf/deref refs/workspace-file-typography) - file-typographies (mf/deref file-typographies-ref) + file-library-workspace (get (mf/deref refs/workspace-libraries) (:typography-ref-file style)) + typography-external-lib (get-in file-library-workspace [:data :typographies (:typography-ref-id style)]) - color-format (mf/use-state :hex) + color-format (mf/use-state :hex) - typography (get (or typography-library file-typographies) (:typography-ref-id style))] - - [:div.attributes-text-block - (if (:typography-ref-id style) - [:div.attributes-typography-name-row - [:div.typography-entry - [:div.typography-sample - {:style {:font-family (:font-family typography) - :font-weight (:font-weight typography) - :font-style (:font-style typography)}} - (tr "workspace.assets.typography.text-styles")]] - [:div.typography-entry-name (:name typography)] - [:& copy-button {:data (copy-style-data typography)}]] - - [:div.attributes-typography-row - [:div.typography-sample - {:style {:font-family (:font-family style) - :font-weight (:font-weight style) - :font-style (:font-style style)}} - (tr "workspace.assets.typography.text-styles")] - [:& copy-button {:data (copy-style-data style)}]]) + typography (or (get (or typography-library file-typographies-viewer file-typographies-workspace) (:typography-ref-id style)) typography-external-lib)] + [:div {:class (stl/css :attributes-content)} (when (:fills style) (for [[idx fill] (map-indexed vector (:fills style))] [:& color-row {:key idx :format @color-format - :color (shape->color fill) + :color (fill->color fill) :copy-data (copy-style-data fill :fill-color :fill-color-gradient) :on-change-format #(reset! color-format %)}])) + (when (:typography-ref-id style) + [:div {:class (stl/css :text-row)} + [:div {:class (stl/css :global/attr-label)} + (tr "inspect.attributes.typography")] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (copy-style-data typography :font-family :font-weight :font-style) + :class (stl/css :copy-btn-wrapper)} + [:div {:class (stl/css :button-children)} (:name typography)]]]]) + (when (:font-id style) - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.typography.font-family")] - [:div.attributes-value (-> style :font-id fonts/get-font-data :name)] - [:& copy-button {:data (copy-style-data style :font-family)}]]) + [:div {:class (stl/css :text-row)} + [:div {:class (stl/css :global/attr-label)} (tr "inspect.attributes.typography.font-family")] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (copy-style-data style :font-family)} + [:div {:class (stl/css :button-children)} + (-> style :font-id fonts/get-font-data :name)]]]]) (when (:font-style style) - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.typography.font-style")] - [:div.attributes-value (str (:font-style style))] - [:& copy-button {:data (copy-style-data style :font-style)}]]) + [:div {:class (stl/css :text-row)} + [:div {:class (stl/css :global/attr-label)} + (tr "inspect.attributes.typography.font-style")] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (copy-style-data style :font-style)} + [:div {:class (stl/css :button-children)} + (dm/str (:font-style style))]]]]) (when (:font-size style) - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.typography.font-size")] - [:div.attributes-value (str (format-number (:font-size style))) "px"] - [:& copy-button {:data (copy-style-data style :font-size)}]]) + [:div {:class (stl/css :text-row)} + [:div {:class (stl/css :global/attr-label)} + (tr "inspect.attributes.typography.font-size")] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (copy-style-data style :font-size)} + [:div {:class (stl/css :button-children)} + (fmt/format-pixels (:font-size style))]]]]) (when (:font-weight style) - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.typography.font-weight")] - [:div.attributes-value (str (:font-weight style))] - [:& copy-button {:data (copy-style-data style :font-weight)}]]) + [:div {:class (stl/css :text-row)} + [:div {:class (stl/css :global/attr-label)} + (tr "inspect.attributes.typography.font-weight")] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (copy-style-data style :font-weight)} + [:div {:class (stl/css :button-children)} + (dm/str (:font-weight style))]]]]) (when (:line-height style) - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.typography.line-height")] - [:div.attributes-value (format-number (:line-height style))] - [:& copy-button {:data (copy-style-data style :line-height)}]]) + [:div {:class (stl/css :text-row)} + [:div {:class (stl/css :global/attr-label)} + (tr "inspect.attributes.typography.line-height")] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (copy-style-data style :line-height)} + [:div {:class (stl/css :button-children)} + (fmt/format-number (:line-height style))]]]]) (when (:letter-spacing style) - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.typography.letter-spacing")] - [:div.attributes-value (str (format-number (:letter-spacing style))) "px"] - [:& copy-button {:data (copy-style-data style :letter-spacing)}]]) + [:div {:class (stl/css :text-row)} + [:div {:class (stl/css :global/attr-label)} + (tr "inspect.attributes.typography.letter-spacing")] + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (copy-style-data style :letter-spacing)} + [:div {:class (stl/css :button-children)} + (fmt/format-pixels (:letter-spacing style))]]]]) (when (:text-decoration style) - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.typography.text-decoration")] - ;; Execution time translation strings: - ;; inspect.attributes.typography.text-decoration.none - ;; inspect.attributes.typography.text-decoration.strikethrough - ;; inspect.attributes.typography.text-decoration.underline - [:div.attributes-value (->> style :text-decoration (str "inspect.attributes.typography.text-decoration.") (tr))] - [:& copy-button {:data (copy-style-data style :text-decoration)}]]) + [:div {:class (stl/css :text-row)} + [:div {:class (stl/css :global/attr-label)} + (tr "inspect.attributes.typography.text-decoration")] + ;; Execution time translation strings: + ;; inspect.attributes.typography.text-decoration.none + ;; inspect.attributes.typography.text-decoration.strikethrough + ;; inspect.attributes.typography.text-decoration.underline + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (copy-style-data style :text-decoration)} + [:div {:class (stl/css :button-children)} + (tr (dm/str "inspect.attributes.typography.text-decoration." (:text-decoration style)))]]]]) (when (:text-transform style) - [:div.attributes-unit-row - [:div.attributes-label (tr "inspect.attributes.typography.text-transform")] - ;; Execution time translation strings: - ;; inspect.attributes.typography.text-transform.lowercase - ;; inspect.attributes.typography.text-transform.none - ;; inspect.attributes.typography.text-transform.titlecase - ;; inspect.attributes.typography.text-transform.uppercase - [:div.attributes-value (->> style :text-transform (str "inspect.attributes.typography.text-transform.") (tr))] - [:& copy-button {:data (copy-style-data style :text-transform)}]]) + [:div {:class (stl/css :text-row)} + [:div {:class (stl/css :global/attr-label)} + (tr "inspect.attributes.typography.text-transform")] + ;; Execution time translation strings: + ;; inspect.attributes.typography.text-transform.lowercase + ;; inspect.attributes.typography.text-transform.none + ;; inspect.attributes.typography.text-transform.titlecase + ;; inspect.attributes.typography.text-transform.uppercase + ;; inspect.attributes.typography.text-transform.unset + [:div {:class (stl/css :global/attr-value)} + [:& copy-button {:data (copy-style-data style :text-transform)} + [:div {:class (stl/css :button-children)} + (tr (dm/str "inspect.attributes.typography.text-transform." (:text-transform style)))]]]]) - [:div.attributes-content-row - [:pre.attributes-content (str/trim text)] - [:& copy-button {:data (str/trim text)}]]])) + [:& copy-button {:data (str/trim text) + :class (stl/css :attributes-content-row)} + [:span {:class (stl/css :content) + :style {:font-family (:font-family style) + :font-weight (:font-weight style) + :font-style (:font-style style)}} + (str/trim text)]]])) (mf/defc text-block [{:keys [shape]}] - (let [style-text-blocks (->> (keys txt/default-text-attrs) - (cg/parse-style-text-blocks (:content shape)) + (let [style-text-blocks (->> (:content shape) + (txt/content->text+styles) (remove (fn [[_ text]] (str/empty? (str/trim text)))) (mapv (fn [[style text]] (vector (merge txt/default-text-attrs style) text))))] (for [[idx [full-style text]] (map-indexed vector style-text-blocks)] - [:& typography-block {:key idx + [:& typography-block {:key idx :shape shape :style full-style :text text}]))) @@ -193,10 +189,11 @@ (mf/defc text-panel [{:keys [shapes]}] (when-let [shapes (seq (filter has-text? shapes))] - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (tr "inspect.attributes.typography")]] + [:div {:class (stl/css :attributes-block)} + [:& inspect-title-bar + {:title (tr "inspect.attributes.typography") + :class (stl/css :title-spacing-text)}] (for [shape shapes] [:& text-block {:shape shape - :key (str "text-block" (:id shape))}])])) + :key (dm/str "text-block" (:id shape))}])])) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/text.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/text.scss new file mode 100644 index 0000000000..cba9ecdbe3 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/text.scss @@ -0,0 +1,52 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.attributes-block { + @include flexColumn; +} + +.title-spacing-text { + @extend .attr-title; +} + +.attributes-content { + @include flexColumn; +} + +.text-row { + @extend .attr-row; + :global(.attr-value) { + align-items: center; + } +} + +.button-children { + @extend .copy-button-children; +} + +.attributes-content-row { + max-width: $s-240; + min-height: calc($s-2 + $s-32); + border-radius: $br-8; + border: $s-1 solid var(--menu-border-color-disabled); + margin-top: $s-4; + .content { + @include bodySmallTypography; + width: 100%; + padding: $s-4 0; + color: var(--color-foreground-secondary); + } + + &:hover { + border: $s-1 solid var(--color-background-tertiary); + background-color: var(--menu-background-color); + .content { + color: var(--menu-foreground-color-hover); + } + } +} diff --git a/frontend/src/app/main/ui/viewer/inspect/code.cljs b/frontend/src/app/main/ui/viewer/inspect/code.cljs index 2d97af2ed3..bceac31ade 100644 --- a/frontend/src/app/main/ui/viewer/inspect/code.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/code.cljs @@ -5,123 +5,331 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.code + (:require-macros [app.main.style :as stl]) (:require - ["js-beautify" :as beautify] - ["react-dom/server" :as rds] + [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] + [app.common.types.shape-tree :as ctst] + [app.config :as cfg] [app.main.data.events :as ev] + [app.main.fonts :as fonts] [app.main.refs :as refs] - [app.main.render :as render] [app.main.store :as st] [app.main.ui.components.code-block :refer [code-block]] [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.hooks :as hooks] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.hooks.resize :refer [use-resize-hook]] [app.main.ui.icons :as i] + [app.main.ui.shapes.text.fontfaces :refer [shapes->fonts]] + [app.util.code-beautify :as cb] [app.util.code-gen :as cg] + [app.util.dom :as dom] + [app.util.http :as http] + [app.util.webapi :as wapi] + [beicon.v2.core :as rx] [cuerdas.core :as str] - [potok.core :as ptk] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) -(defn generate-markup-code [objects shapes] - ;; Here we can render specific HTML code - (->> shapes - (map (fn [shape] - (dm/str - "" - (rds/renderToStaticMarkup - (mf/element - render/object-svg - #js {:objects objects - :object-id (-> shape :id)}))))) - (str/join "\n\n"))) +(def embed-images? true) +(def remove-localhost? true) -(defn format-code [code type] - (let [code (-> code - (str/replace "" "") - (str/replace "><" ">\n<"))] - (cond-> code - (= type "svg") (beautify/html #js {"indent_size" 2})))) +(def page-template + " + + + + + + %s + +") -(defn get-flex-elements [page-id shapes from] - (let [ids (mapv :id shapes) - ids (hooks/use-equal-memo ids) - get-layout-children-refs (mf/use-memo (mf/deps ids page-id from) #(if (= from :workspace) - (refs/workspace-get-flex-child ids) - (refs/get-flex-child-viewer ids page-id)))] - - (mf/deref get-layout-children-refs))) - -(defn get-objects [from] +(defn- use-objects [from] (let [page-objects-ref - (mf/use-memo - (mf/deps from) - (fn [] - (if (= from :workspace) - refs/workspace-page-objects - (refs/get-viewer-objects))))] + (mf/with-memo [from] + (if (= from :workspace) + ;; FIXME: fix naming consistency issues + refs/workspace-page-objects + (refs/get-viewer-objects)))] (mf/deref page-objects-ref))) +(defn- shapes->images + [shapes] + (->> shapes + (keep + (fn [shape] + (when-let [data (or (:metadata shape) (:fill-image shape) (-> shape :fills first :fill-image))] + [(:id shape) (cfg/resolve-file-media data)]))))) + +(defn- replace-map + [value map] + (reduce + (fn [value [old new]] + (str/replace value old new)) + value map)) + +(defn gen-all-code + [style-code markup-code images-data] + (let [markup-code (cond-> markup-code + embed-images? (replace-map images-data)) + + style-code (cond-> style-code + embed-images? (replace-map images-data))] + (str/format page-template style-code markup-code))) + (mf/defc code [{:keys [shapes frame on-expand from]}] - (let [style-type (mf/use-state "css") - markup-type (mf/use-state "svg") - shapes (->> shapes - (map #(gsh/translate-to-frame % frame))) - route (mf/deref refs/route) - page-id (:page-id (:query-params route)) - flex-items (get-flex-elements page-id shapes from) - objects (get-objects from) - shapes (->> shapes - (map #(assoc % :parent (get objects (:parent-id %)))) - (map #(assoc % :flex-items flex-items))) - style-code (-> (cg/generate-style-code @style-type shapes) - (format-code "css")) + (let [style-type* (mf/use-state "css") + markup-type* (mf/use-state "html") + fontfaces-css* (mf/use-state nil) + images-data* (mf/use-state nil) + + style-type (deref style-type*) + markup-type (deref markup-type*) + fontfaces-css (deref fontfaces-css*) + images-data (deref images-data*) + + collapsed* (mf/use-state #{}) + collapsed-css? (contains? @collapsed* :css) + collapsed-markup? (contains? @collapsed* :markup) + + objects (use-objects from) + + shapes + (mf/with-memo [shapes frame] + (mapv #(gsh/translate-to-frame % frame) shapes)) + + all-children + (mf/use-memo + (mf/deps shapes objects) + (fn [] + (->> shapes + (map :id) + (cfh/selected-with-children objects) + (ctst/sort-z-index objects) + (map (d/getf objects))))) + + fonts + (mf/with-memo [all-children] + (shapes->fonts all-children)) + + images-urls + (mf/with-memo [all-children] + (shapes->images all-children)) + + style-code + (mf/use-memo + (mf/deps fontfaces-css style-type shapes all-children cg/generate-style-code) + (fn [] + (dm/str + fontfaces-css "\n" + (-> (cg/generate-style-code objects style-type shapes all-children) + (cb/format-code style-type))))) markup-code - (-> (mf/use-memo (mf/deps shapes) #(generate-markup-code objects shapes)) - (format-code "svg")) + (mf/use-memo + (mf/deps markup-type shapes images-data) + (fn [] + (-> (cg/generate-markup-code objects markup-type shapes) + (cb/format-code markup-type)))) on-markup-copied - (mf/use-callback - (mf/deps @markup-type) + (mf/use-fn + (mf/deps markup-type from) (fn [] - (st/emit! (ptk/event ::ev/event - {::ev/name "copy-inspect-code" - :type @markup-type})))) + (let [origin (if (= :workspace from) + "workspace" + "viewer")] + (st/emit! (ptk/event ::ev/event + {::ev/name "copy-inspect-code" + ::ev/origin origin + :type markup-type}))))) on-style-copied - (mf/use-callback - (mf/deps @style-type) + (mf/use-fn + (mf/deps style-type from) (fn [] - (st/emit! (ptk/event ::ev/event - {::ev/name "copy-inspect-style" - :type @style-type}))))] + (let [origin (if (= :workspace from) + "workspace" + "viewer")] + (st/emit! (ptk/event ::ev/event + {::ev/name "copy-inspect-style" + ::ev/origin origin + :type style-type}))))) - [:div.element-options - [:div.code-block - [:div.code-row-lang "CSS" + {on-markup-pointer-down :on-pointer-down + on-markup-lost-pointer-capture :on-lost-pointer-capture + on-markup-pointer-move :on-pointer-move + markup-size :size} + (use-resize-hook :code 400 100 800 :y false :bottom) - [:button.expand-button - {:on-click on-expand} - i/full-screen] + {on-style-pointer-down :on-pointer-down + on-style-lost-pointer-capture :on-lost-pointer-capture + on-style-pointer-move :on-pointer-move + style-size :size} + (use-resize-hook :code 400 100 800 :y false :bottom) - [:& copy-button {:data style-code - :on-copied on-style-copied}]] + ;; set-style + ;; (mf/use-fn + ;; (fn [value] + ;; (reset! style-type* value))) - [:div.code-row-display - [:& code-block {:type @style-type - :code style-code}]]] + set-markup + (mf/use-fn + (mf/deps markup-type*) + (fn [value] + (reset! markup-type* value))) - [:div.code-block - [:div.code-row-lang "SVG" + handle-copy-all-code + (mf/use-fn + (mf/deps style-code markup-code images-data) + (fn [] + (wapi/write-to-clipboard (gen-all-code style-code markup-code images-data)) + (let [origin (if (= :workspace from) + "workspace" + "viewer")] + (st/emit! (ptk/event ::ev/event + {::ev/name "copy-inspect-code" + ::ev/origin origin + :type "all"}))))) - [:button.expand-button - {:on-click on-expand} - i/full-screen] + ;;handle-open-review + ;;(mf/use-fn + ;; (fn [] + ;; (st/emit! (dp/open-preview-selected)))) - [:& copy-button {:data markup-code - :on-copied on-markup-copied}]] - [:div.code-row-display - [:& code-block {:type @markup-type - :code markup-code}]]]])) + handle-collapse + (mf/use-fn + (fn [event] + (let [panel-type (-> (dom/get-current-target event) + (dom/get-data "type") + (keyword))] + (swap! collapsed* + (fn [collapsed] + (if (contains? collapsed panel-type) + (disj collapsed panel-type) + (conj collapsed panel-type))))))) + copy-css-fn + (mf/use-fn + (mf/deps style-code images-data) + #(replace-map style-code images-data)) + + copy-html-fn + (mf/use-fn + (mf/deps markup-code images-data) + #(replace-map markup-code images-data))] + + (mf/with-effect [fonts] + (->> (rx/from fonts) + (rx/merge-map fonts/fetch-font-css) + (rx/reduce conj []) + (rx/subs! + (fn [result] + (let [css (str/join "\n" result)] + (reset! fontfaces-css* css)))))) + + (mf/with-effect [images-urls] + (->> (rx/from images-urls) + (rx/merge-map + (fn [[_ uri]] + (->> (http/fetch-data-uri uri true) + (rx/catch (fn [_] (rx/of (hash-map uri uri))))))) + (rx/reduce conj {}) + (rx/subs! + (fn [result] + (reset! images-data* result))))) + + [:div {:class (stl/css :element-options)} + [:div {:class (stl/css :attributes-block)} + [:button {:class (stl/css :download-button) + :on-click handle-copy-all-code} + "Copy all code"]] + + #_[:div.attributes-block + [:button.download-button {:on-click handle-open-review} + "Preview"]] + + [:div {:class (stl/css-case :code-block true + :collapsed collapsed-css?)} + [:div {:class (stl/css :code-row-lang)} + [:button {:class (stl/css :toggle-btn) + :data-type "css" + :on-click handle-collapse} + [:span {:class (stl/css-case + :collapsabled-icon true + :rotated collapsed-css?)} + i/arrow]] + + [:div {:class (stl/css :code-lang-option)} + "CSS"] + ;; We will have a select when we have more than one option + ;; [:& select {:default-value style-type + ;; :class (stl/css :code-lang-select) + ;; :on-change set-style + ;; :options [{:label "CSS" :value "css"}]}] + + [:div {:class (stl/css :action-btns)} + [:button {:class (stl/css :expand-button) + :on-click on-expand} + i/code] + + [:& copy-button {:data copy-css-fn + :class (stl/css :css-copy-btn) + :on-copied on-style-copied}]]] + + (when-not collapsed-css? + [:div {:class (stl/css :code-row-display) + :style {:--code-height (dm/str (or style-size 400) "px")}} + [:& code-block {:type style-type + :code style-code}]]) + + [:div {:class (stl/css :resize-area) + :on-pointer-down on-style-pointer-down + :on-lost-pointer-capture on-style-lost-pointer-capture + :on-pointer-move on-style-pointer-move}]] + + [:div {:class (stl/css-case :code-block true + :collapsed collapsed-markup?)} + [:div {:class (stl/css :code-row-lang)} + [:button {:class (stl/css :toggle-btn) + :data-type "markup" + :on-click handle-collapse} + [:span {:class (stl/css-case + :collapsabled-icon true + :rotated collapsed-markup?)} + i/arrow]] + + [:& radio-buttons {:selected markup-type + :on-change set-markup + :class (stl/css :code-lang-options) + :wide true + :name "listing-style"} + [:& radio-button {:value "html" + :id :html}] + [:& radio-button {:value "svg" + :id :svg}]] + + [:div {:class (stl/css :action-btns)} + [:button {:class (stl/css :expand-button) + :on-click on-expand} + i/code] + + [:& copy-button {:data copy-html-fn + :class (stl/css :html-copy-btn) + :on-copied on-markup-copied}]]] + + (when-not collapsed-markup? + [:div {:class (stl/css :code-row-display) + :style {:--code-height (dm/str (or markup-size 400) "px")}} + [:& code-block {:type markup-type + :code markup-code}]]) + + [:div {:class (stl/css :resize-area) + :on-pointer-down on-markup-pointer-down + :on-lost-pointer-capture on-markup-lost-pointer-capture + :on-pointer-move on-markup-pointer-move}]]])) diff --git a/frontend/src/app/main/ui/viewer/inspect/code.scss b/frontend/src/app/main/ui/viewer/inspect/code.scss new file mode 100644 index 0000000000..1b7b3f9ca9 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/code.scss @@ -0,0 +1,137 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-options { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + padding-bottom: $s-16; +} + +.download-button { + @extend .button-secondary; + @include uppercaseTitleTipography; + height: $s-32; + width: 100%; + margin: $s-8 0; +} + +.code-block { + @include codeTypography; + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + padding: 0 $s-4 $s-8 0; + + pre { + border-radius: $br-8; + padding: $s-16; + overflow: auto; + height: 100%; + } + + // Overrides background setted in the theme + :global(.hljs) { + background: $db-tertiary; + } + + &.collapsed { + height: initial; + } +} + +.code-row-lang { + display: grid; + grid-template-columns: $s-12 1fr $s-60; + gap: $s-4; + width: 100%; +} + +.code-lang { + @include uppercaseTitleTipography; + display: flex; + align-items: center; +} + +.action-btns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $s-4; +} + +.expand-button, +.css-copy-btn, +.html-copy-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.code-lang-options { + max-width: $s-108; +} +.code-lang-select { + @include uppercaseTitleTipography; + width: $s-72; + border: $s-1 solid transparent; + background-color: transparent; + color: var(--menu-foreground-color-disabled); +} +.code-lang-option { + @include uppercaseTitleTipography; + width: $s-72; + height: $s-32; + padding: $s-8; + color: var(--menu-foreground-color-disabled); +} + +.code-row-display { + flex: 1; + min-height: 0; + overflow: hidden; + padding-bottom: $s-8; +} + +.toggle-btn { + @include buttonStyle; + display: flex; + align-items: center; + padding: 0; + color: var(--title-foreground-color); + stroke: var(--title-foreground-color); + .collapsabled-icon { + @include flexCenter; + height: $s-24; + border-radius: $br-8; + svg { + @extend .button-icon-small; + transform: rotate(90deg); + stroke: var(--icon-foreground); + } + &.rotated svg { + transform: rotate(0deg); + } + } + &:hover { + color: var(--title-foreground-color-hover); + stroke: var(--title-foreground-color-hover); + .title { + color: var(--title-foreground-color-hover); + stroke: var(--title-foreground-color-hover); + } + .collapsabled-icon svg { + stroke: var(--title-foreground-color-hover); + } + } +} diff --git a/frontend/src/app/main/ui/viewer/inspect/exports.cljs b/frontend/src/app/main/ui/viewer/inspect/exports.cljs index 4a34421341..392b6d8cd2 100644 --- a/frontend/src/app/main/ui/viewer/inspect/exports.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/exports.cljs @@ -5,11 +5,14 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.exports + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.main.data.exports :as de] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.select :refer [select]] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :refer [tr c]] @@ -83,33 +86,45 @@ (mf/use-callback (mf/deps shapes) (fn [index event] - (let [target (dom/get-target event) - value (dom/get-value target) - value (d/parse-double value)] - (swap! exports assoc-in [index :scale] value)))) + (let [scale (d/parse-double event)] + (swap! exports assoc-in [index :scale] scale)))) on-suffix-change (mf/use-callback (mf/deps shapes) - (fn [index event] - (let [target (dom/get-target event) - value (dom/get-value target)] + (fn [event] + (let [value (dom/get-target-val event) + index (-> (dom/get-current-target event) + (dom/get-data "value") + (d/parse-integer))] (swap! exports assoc-in [index :suffix] value)))) on-type-change (mf/use-callback (mf/deps shapes) (fn [index event] - (let [target (dom/get-target event) - value (dom/get-value target) - value (keyword value)] - (swap! exports assoc-in [index :type] value)))) + (let [type (keyword event)] + (swap! exports assoc-in [index :type] type)))) + manage-key-down (mf/use-callback (fn [event] (let [esc? (kbd/esc? event)] (when esc? - (dom/blur! (dom/get-target event))))))] + (dom/blur! (dom/get-target event)))))) + + size-options [{:value "0.5" :label "0.5x"} + {:value "0.75" :label "0.75x"} + {:value "1" :label "1x"} + {:value "1.5" :label "1.5x"} + {:value "2" :label "2x"} + {:value "4" :label "4x"} + {:value "6" :label "6x"}] + + format-options [{:value "png" :label "PNG"} + {:value "jpeg" :label "JPG"} + {:value "svg" :label "SVG"} + {:value "pdf" :label "PDF"}]] (mf/use-effect (mf/deps shapes) @@ -118,46 +133,64 @@ flatten distinct vec)))) + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-title)} + [:& title-bar {:collapsable false + :title (tr "workspace.options.export") + :class (stl/css :title-spacing-export-viewer)} + [:button {:class (stl/css :add-export) + :on-click add-export} i/add]]] - [:div.element-set.exports-options - [:div.element-set-title - [:span (tr "workspace.options.export")] - [:div.add-page {:on-click add-export} i/close]] + (cond + (= :multiple exports) + [:div {:class (stl/css :multiple-exports)} + [:div {:class (stl/css :label)} (tr "settings.multiple")] + [:div {:class (stl/css :actions)} + [:button {:class (stl/css :action-btn) + :on-click ()} + i/remove-icon]]] - (when (seq @exports) - [:div.element-set-content + (seq @exports) + [:div {:class (stl/css :element-set-content)} (for [[index export] (d/enumerate @exports)] - [:div.element-set-options-group - {:key index} - (when (scale-enabled? export) - [:select.input-select {:on-change (partial on-scale-change index) - :value (:scale export)} - [:option {:value "0.5"} "0.5x"] - [:option {:value "0.75"} "0.75x"] - [:option {:value "1"} "1x"] - [:option {:value "1.5"} "1.5x"] - [:option {:value "2"} "2x"] - [:option {:value "4"} "4x"] - [:option {:value "6"} "6x"]]) + [:div {:class (stl/css :element-group) + :key index} + [:div {:class (stl/css :input-wrapper)} + [:div {:class (stl/css :format-select)} + [:& select + {:default-value (d/name (:type export)) + :options format-options + :dropdown-class (stl/css :dropdown-upwards) + :on-change (partial on-type-change index)}]] + (when (scale-enabled? export) + [:div {:class (stl/css :size-select)} + [:& select + {:default-value (str (:scale export)) + :options size-options + :dropdown-class (stl/css :dropdown-upwards) + :on-change (partial on-scale-change index)}]]) + [:label {:class (stl/css :suffix-input) + :for "suffix-export-input"} + [:input {:class (stl/css :type-input) + :id "suffix-export-input" + :type "text" + :value (:suffix export) + :placeholder (tr "workspace.options.export.suffix") + :data-value (str index) + :on-change on-suffix-change + :on-key-down manage-key-down}]]] - [:input.input-text {:value (:suffix export) - :placeholder (tr "workspace.options.export.suffix") - :on-change (partial on-suffix-change index) - :on-key-down manage-key-down}] - [:select.input-select {:value (d/name (:type export)) - :on-change (partial on-type-change index)} - [:option {:value "png"} "PNG"] - [:option {:value "jpeg"} "JPEG"] - [:option {:value "svg"} "SVG"] - [:option {:value "pdf"} "PDF"]] - [:div.delete-icon {:on-click (partial delete-export index)} - i/minus]]) - - [:div.btn-icon-dark.download-button - {:on-click (when-not in-progress? on-download) - :class (dom/classnames :btn-disabled in-progress?) - :disabled in-progress?} - (if in-progress? - (tr "workspace.options.exporting-object") - (tr "workspace.options.export-object" (c (count shapes))))]])])) + [:button {:class (stl/css :action-btn) + :on-click (partial delete-export index)} + i/remove-icon]])]) + (when (or (= :multiple exports) (seq @exports)) + [:button + {:on-click (when-not in-progress? on-download) + :class (stl/css-case + :export-btn true + :btn-disabled in-progress?) + :disabled in-progress?} + (if in-progress? + (tr "workspace.options.exporting-object") + (tr "workspace.options.export-object" (c (count shapes))))])])) diff --git a/frontend/src/app/main/ui/viewer/inspect/exports.scss b/frontend/src/app/main/ui/viewer/inspect/exports.scss new file mode 100644 index 0000000000..95e6743739 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/exports.scss @@ -0,0 +1,103 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + padding-bottom: $s-16; + margin: 0; +} + +.element-title { + margin: 0; +} + +.title-spacing-export-viewer { + margin: 0; + color: var(--entry-foreground-color-hover); + margin-inline-start: calc(-1 * $s-8); + width: calc(100% + $s-8); +} + +.add-export { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.element-set-content { + @include flexColumn; + margin-bottom: $s-4; +} + +.multiple-exports { + @include flexRow; +} + +.label { + @extend .mixed-bar; +} + +.actions { + @include flexRow; +} + +.element-group { + display: grid; + grid-template-columns: repeat(8, 1fr); + column-gap: $s-4; + .action-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } + } +} + +.input-wrapper { + grid-column: span 7; + display: grid; + grid-template-columns: subgrid; +} + +.format-select { + grid-column: span 2; + padding: 0; + + .dropdown-upwards { + bottom: $s-36; + width: $s-80; + top: unset; + } +} + +.size-select { + grid-column: span 2; + padding: 0; + .dropdown-upwards { + bottom: $s-36; + top: unset; + width: $s-80; + } +} + +.suffix-input { + @extend .input-element; + grid-column: span 3; +} + +.export-btn { + @extend .button-secondary; + @include uppercaseTitleTipography; + height: $s-32; + width: $s-252; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/left_sidebar.cljs b/frontend/src/app/main/ui/viewer/inspect/left_sidebar.cljs index 0172a429a4..f6c234bd47 100644 --- a/frontend/src/app/main/ui/viewer/inspect/left_sidebar.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/left_sidebar.cljs @@ -5,14 +5,14 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.left-sidebar + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] - [app.common.types.shape.layout :as ctl] + [app.common.data.macros :as dm] + [app.common.types.component :as ctk] [app.main.data.viewer :as dv] [app.main.store :as st] - [app.main.ui.components.shape-icon :as si] - [app.main.ui.icons :as i] - [app.main.ui.workspace.sidebar.layer-name :refer [layer-name]] + [app.main.ui.workspace.sidebar.layer-item :refer [layer-item-inner]] [app.util.dom :as dom] [app.util.keyboard :as kbd] [okulary.core :as l] @@ -24,36 +24,43 @@ (l/derived st/state))) (mf/defc layer-item - [{:keys [item selected objects disable-collapse?] :as props}] + [{:keys [item selected objects depth component-child? hide-toggle?] :as props}] (let [id (:id item) + hidden? (:hidden item) selected? (contains? selected id) item-ref (mf/use-ref nil) + depth (+ depth 1) + component-tree? (or component-child? (ctk/instance-root? item) (ctk/instance-head? item)) - collapsed-iref (mf/use-memo - (mf/deps id) - (make-collapsed-iref id)) + collapsed-iref + (mf/use-memo + (mf/deps id) + (make-collapsed-iref id)) expanded? (not (mf/deref collapsed-iref)) - absolute? (ctl/layout-absolute? item) + toggle-collapse - (fn [event] - (dom/stop-propagation event) - (st/emit! (dv/toggle-collapse id))) + (mf/use-callback + (mf/deps id) + (fn [event] + (dom/stop-propagation event) + (st/emit! (dv/toggle-collapse id)))) select-shape - (fn [event] - (dom/prevent-default event) - (let [id (:id item)] - (cond - (kbd/mod? event) - (st/emit! (dv/toggle-selection id)) + (mf/use-callback + (mf/deps id) + (fn [event] + (dom/prevent-default event) + (cond + (kbd/mod? event) + (st/emit! (dv/toggle-selection id)) - (kbd/shift? event) - (st/emit! (dv/shift-select-to id)) + (kbd/shift? event) + (st/emit! (dv/shift-select-to id)) - :else - (st/emit! (dv/select-shape id)))))] + :else + (st/emit! (dv/select-shape id)))))] (mf/use-effect (mf/deps selected) @@ -61,29 +68,25 @@ (when (and (= (count selected) 1) selected?) (dom/scroll-into-view-if-needed! (mf/ref-val item-ref) true)))) - [:li {:ref item-ref - :class (dom/classnames - :component (not (nil? (:component-id item))) - :masked (:masked-group? item) - :selected selected?)} - - [:div.element-list-body {:class (dom/classnames :selected selected? - :icon-layer (= (:type item) :icon)) - :on-click select-shape} - [:div.icon - (when absolute? - [:div.absolute i/position-absolute]) - [:& si/element-icon {:shape item}]] - [:& layer-name {:shape item :disabled-double-click true}] - - (when (and (not disable-collapse?) (:shapes item)) - [:span.toggle-content - {:on-click toggle-collapse - :class (when expanded? "inverse")} - i/arrow-slide])] + [:& layer-item-inner + {:ref item-ref + :item item + :depth depth + :read-only? true + :highlighted? false + :selected? selected? + :component-tree? component-tree? + :hidden? hidden? + :filtered? false + :expanded? expanded? + :hide-toggle? hide-toggle? + :on-select-shape select-shape + :on-toggle-collapse toggle-collapse} (when (and (:shapes item) expanded?) - [:ul.element-children + [:div {:class (stl/css-case + :element-children true + :parent-selected selected?)} (for [[index id] (reverse (d/enumerate (:shapes item)))] (when-let [item (get objects id)] [:& layer-item @@ -91,19 +94,24 @@ :selected selected :index index :objects objects - :key (:id item)}]))])])) + :key (dm/str id) + :depth depth + :component-child? component-tree?}]))])])) (mf/defc left-sidebar [{:keys [frame page local]}] (let [selected (:selected local) objects (:objects page)] - [:aside.settings-bar.settings-bar-left - [:div.settings-bar-inside - [:ul.element-list + [:aside {:class (stl/css :settings-bar-left)} + [:div {:class (stl/css :settings-bar-inside)} + [:div {:class (stl/css :element-list)} [:& layer-item {:item frame :selected selected :index 0 :objects objects - :disable-collapse? true}]]]])) + :sortable? false + :filtered? false + :depth -2 + :hide-toggle? true}]]]])) diff --git a/frontend/src/app/main/ui/viewer/inspect/left_sidebar.scss b/frontend/src/app/main/ui/viewer/inspect/left_sidebar.scss new file mode 100644 index 0000000000..503b80907e --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/left_sidebar.scss @@ -0,0 +1,22 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.settings-bar-left { + background-color: var(--panel-background-color); + height: 100%; + width: $s-256; +} + +.settings-bar-inside { + display: grid; + grid-template-columns: 100%; + grid-template-rows: 100%; + height: calc(100% - $s-2); + overflow-y: auto; + padding-top: $s-8; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/render.cljs b/frontend/src/app/main/ui/viewer/inspect/render.cljs index 01adf3e355..0872a5c424 100644 --- a/frontend/src/app/main/ui/viewer/inspect/render.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/render.cljs @@ -7,9 +7,9 @@ (ns app.main.ui.viewer.inspect.render "The main container for a frame in inspect mode" (:require + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] [app.main.data.viewer :as dv] [app.main.store :as st] [app.main.ui.shapes.bool :as bool] @@ -33,16 +33,16 @@ (defn handle-hover-shape [shape hover?] (fn [event] - (when-not (or (cph/group-shape? shape) - (cph/root-frame? shape)) + (when-not (or (cfh/group-shape? shape) + (cfh/root-frame? shape)) (dom/prevent-default event) (dom/stop-propagation event) (st/emit! (dv/hover-shape (:id shape) hover?))))) (defn select-shape [shape] (fn [event] - (when-not (or (cph/group-shape? shape) - (cph/root-frame? shape)) + (when-not (or (cfh/group-shape? shape) + (cfh/root-frame? shape)) (dom/stop-propagation event) (dom/prevent-default event) (cond @@ -61,7 +61,7 @@ childs (unchecked-get props "childs") frame (unchecked-get props "frame") render-wrapper? (or (not= :svg-raw (:type shape)) - (svg-raw/graphic-element? (get-in shape [:content :tag])))] + (contains? svg-raw/graphic-element (get-in shape [:content :tag])))] (if render-wrapper? [:> shape-container {:shape shape @@ -119,7 +119,7 @@ {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - children (->> (cph/get-children-ids objects (:id shape)) + children (->> (cfh/get-children-ids objects (:id shape)) (select-keys objects)) props (-> (obj/create) (obj/merge! props) @@ -170,7 +170,9 @@ (mf/use-memo (mf/deps objects) #(svg-raw-container-factory objects))] (when (and shape (not (:hidden shape))) - (let [shape (gsh/translate-to-frame shape frame) + (let [shape (if frame + (gsh/translate-to-frame shape frame) + shape) opts #js {:shape shape :frame frame}] (case (:type shape) diff --git a/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs b/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs index e3d03bc3f4..c90ab718a0 100644 --- a/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs @@ -5,12 +5,13 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.inspect.right-sidebar + (:require-macros [app.main.style :as stl]) (:require - [app.main.data.workspace :as dw] + [app.common.data.macros :as dm] + [app.common.types.component :as ctk] [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.components.shape-icon :as si] - [app.main.ui.components.tabs-container :refer [tabs-container tabs-element]] + [app.main.ui.components.shape-icon :as sir] + [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.icons :as i] [app.main.ui.viewer.inspect.attributes :refer [attributes]] [app.main.ui.viewer.inspect.code :refer [code]] @@ -38,71 +39,105 @@ :data local}))))) (mf/defc right-sidebar - [{:keys [frame page file selected shapes page-id file-id share-id from] - :or {from :inspect}}] - (let [expanded (mf/use-state false) - section (mf/use-state :info #_:code) - shapes (or shapes - (resolve-shapes (:objects page) selected)) + [{:keys [frame page objects file selected shapes page-id file-id share-id from on-change-section on-expand] + :or {from :viewer}}] + (let [section (mf/use-state :info #_:code) + objects (or objects (:objects page)) + shapes (or shapes + (resolve-shapes objects selected)) + first-shape (first shapes) + page-id (or page-id (:id page)) + file-id (or file-id (:id file)) - first-shape (first shapes) - page-id (or page-id (:id page)) - file-id (or file-id (:id file)) + libraries (get-libraries from) - libraries (get-libraries from)] + file (mf/deref refs/viewer-file) + components-v2 (dm/get-in file [:data :options :components-v2]) + main-instance? (if components-v2 + (ctk/main-instance? first-shape) + true) - [:aside.settings-bar.settings-bar-right {:class (when @expanded "expanded")} - [:div.settings-bar-inside - (if (seq shapes) - [:div.tool-window - [:div.tool-window-bar.big - (if (> (count shapes) 1) - [:* - [:span.tool-window-bar-icon i/layers] - [:span.tool-window-bar-title (tr "inspect.tabs.code.selected.multiple" (count shapes))]] - [:* - [:span.tool-window-bar-icon - [:& si/element-icon {:shape first-shape}]] - ;; Execution time translation strings: - ;; inspect.tabs.code.selected.circle - ;; inspect.tabs.code.selected.component - ;; inspect.tabs.code.selected.curve - ;; inspect.tabs.code.selected.frame - ;; inspect.tabs.code.selected.group - ;; inspect.tabs.code.selected.image - ;; inspect.tabs.code.selected.mask - ;; inspect.tabs.code.selected.path - ;; inspect.tabs.code.selected.rect - ;; inspect.tabs.code.selected.svg-raw - ;; inspect.tabs.code.selected.text - [:span.tool-window-bar-title (:name first-shape)]])] - [:div.tool-window-content.inspect - [:& tabs-container {:on-change-tab #(do - (reset! expanded false) - (reset! section %) - (when (= from :workspace) - (st/emit! (dw/set-inspect-expanded false)))) - :selected @section} - [:& tabs-element {:id :info :title (tr "inspect.tabs.info")} - [:& attributes {:page-id page-id - :file-id file-id - :frame frame - :shapes shapes - :from from - :libraries libraries - :share-id share-id}]] + handle-change-tab + (mf/use-fn + (mf/deps from on-change-section) + (fn [new-section] + (reset! section new-section) + (when on-change-section + (on-change-section new-section)))) - [:& tabs-element {:id :code :title (tr "inspect.tabs.code")} - [:& code {:frame frame - :shapes shapes - :on-expand (fn [] - (when (= from :workspace) - (st/emit! (dw/set-inspect-expanded (not @expanded)))) - (swap! expanded not)) - :from from}]]]]] - [:div.empty - [:span.tool-window-bar-icon i/code] - [:div (tr "inspect.empty.select")] - [:span.tool-window-bar-icon i/help] - [:div (tr "inspect.empty.help")] - [:button.btn-primary.action {:on-click #(dom/open-new-window "https://help.penpot.app/user-guide/inspect/")} (tr "inspect.empty.more-info")]])]])) + handle-expand + (mf/use-fn + (mf/deps on-expand) + (fn [] + (when on-expand (on-expand)))) + + navigate-to-help + (mf/use-fn + (fn [] + (dom/open-new-window "https://help.penpot.app/user-guide/inspect/")))] + + (mf/use-effect + (mf/deps shapes handle-change-tab) + (fn [] + (when-not (seq shapes) + (handle-change-tab :info)))) + + [:aside {:class (stl/css-case :settings-bar-right true + :viewer-code (= from :viewer))} + (if (seq shapes) + [:div {:class (stl/css :tool-windows)} + [:div {:class (stl/css :shape-row)} + (if (> (count shapes) 1) + [:* + [:span {:class (stl/css :layers-icon)} i/layers] + [:span {:class (stl/css :layer-title)} (tr "inspect.tabs.code.selected.multiple" (count shapes))]] + [:* + [:span {:class (stl/css :shape-icon)} + [:& sir/element-icon {:shape first-shape :main-instance? main-instance?}]] + ;; Execution time translation strings: + ;; inspect.tabs.code.selected.circle + ;; inspect.tabs.code.selected.component + ;; inspect.tabs.code.selected.curve + ;; inspect.tabs.code.selected.frame + ;; inspect.tabs.code.selected.group + ;; inspect.tabs.code.selected.image + ;; inspect.tabs.code.selected.mask + ;; inspect.tabs.code.selected.path + ;; inspect.tabs.code.selected.rect + ;; inspect.tabs.code.selected.svg-raw + ;; inspect.tabs.code.selected.text + [:span {:class (stl/css :layer-title)} (:name first-shape)]])] + [:div {:class (stl/css :inspect-content)} + [:& tab-container {:on-change-tab handle-change-tab + :selected @section + :content-class (stl/css :tab-content) + :header-class (stl/css :tab-header)} + [:& tab-element {:id :info :title (tr "inspect.tabs.info")} + [:& attributes {:page-id page-id + :objects objects + :file-id file-id + :frame frame + :shapes shapes + :from from + :libraries libraries + :share-id share-id}]] + + [:& tab-element {:id :code :title (tr "inspect.tabs.code")} + [:& code {:frame frame + :shapes shapes + :on-expand handle-expand + :from from}]]]]] + [:div {:class (stl/css :empty)} + [:div {:class (stl/css :code-info)} + [:span {:class (stl/css :placeholder-icon)} + i/code] + [:span {:class (stl/css :placeholder-label)} + (tr "inspect.empty.select")]] + [:div {:class (stl/css :help-info)} + [:span {:class (stl/css :placeholder-icon)} + i/help] + [:span {:class (stl/css :placeholder-label)} + (tr "inspect.empty.help")]] + [:button {:class (stl/css :more-info-btn) + :on-click navigate-to-help} + (tr "inspect.empty.more-info")]])])) diff --git a/frontend/src/app/main/ui/viewer/inspect/right_sidebar.scss b/frontend/src/app/main/ui/viewer/inspect/right_sidebar.scss new file mode 100644 index 0000000000..48bb94620a --- /dev/null +++ b/frontend/src/app/main/ui/viewer/inspect/right_sidebar.scss @@ -0,0 +1,106 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.settings-bar-right { + min-width: $s-252; + width: 100%; + height: 100vh; + position: relative; + left: unset; + right: unset; + grid-area: right-sidebar; + overflow: hidden; + &.viewer-code { + height: calc(100vh - $s-48); + } +} + +.viewer-code { + padding-inline-start: $s-8; +} + +.tool-windows { + height: 100%; + display: flex; + flex-direction: column; + gap: $s-8; +} + +.shape-row { + display: grid; + grid-template-columns: auto 1fr; + gap: $s-8; + align-items: center; + height: $s-32; +} + +.layers-icon, +.shape-icon { + @include flexCenter; + height: $s-32; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } +} + +.layer-title { + @include bodySmallTypography; + @include textEllipsis; + height: $s-32; + padding: $s-8 0; + color: var(--assets-item-name-foreground-color-rest); +} + +.empty { + display: flex; + flex-direction: column; + align-items: center; + gap: $s-40; + padding-top: $s-24; +} + +.code-info, +.help-info { + @include flexColumn; + align-items: center; + justify-content: flex-start; + gap: $s-12; + margin-right: $s-8; +} + +.placeholder-icon { + @extend .empty-icon; +} + +.placeholder-label { + @include bodySmallTypography; + text-align: center; + width: $s-200; + color: var(--empty-message-foreground-color); +} + +.more-info-btn { + @extend .button-secondary; + @include uppercaseTitleTipography; + height: $s-32; + padding: $s-8 $s-24; +} + +.inspect-content { + flex: 1; + overflow: hidden; +} + +.tab-content { + scrollbar-gutter: stable; +} + +.tab-header { + margin-right: $s-12; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/selection_feedback.cljs b/frontend/src/app/main/ui/viewer/inspect/selection_feedback.cljs index f62ed17d57..eceaad61ac 100644 --- a/frontend/src/app/main/ui/viewer/inspect/selection_feedback.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/selection_feedback.cljs @@ -15,7 +15,7 @@ ;; CONSTANTS ;; ------------------------------------------------ -(def select-color "var(--color-select)") +(def select-color "var(--color-accent-tertiary)") (def selection-rect-width 1) (def select-guide-width 1) (def select-guide-dasharray 5) @@ -58,7 +58,7 @@ shapes (resolve-shapes objects [hover]) hover-shape (or (first shapes) frame) selected-shapes (resolve-shapes objects selected) - selrect (gsh/selection-rect selected-shapes)] + selrect (gsh/shapes->rect selected-shapes)] (when (d/not-empty? selected-shapes) [:g.selection-feedback {:pointer-events "none"} diff --git a/frontend/src/app/main/ui/viewer/interactions.cljs b/frontend/src/app/main/ui/viewer/interactions.cljs index deab4db369..9fc794f608 100644 --- a/frontend/src/app/main/ui/viewer/interactions.cljs +++ b/frontend/src/app/main/ui/viewer/interactions.cljs @@ -5,12 +5,13 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.interactions + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] [app.common.types.modifiers :as ctm] [app.common.types.page :as ctp] [app.common.uuid :as uuid] @@ -34,98 +35,138 @@ (gpt/add delta) (gpt/negate)) update-fn #(d/update-when %1 %2 gsh/transform-shape (ctm/move-modifiers vector))] - (->> (cph/get-children-ids objects frame-id) + (->> (cfh/get-children-ids objects frame-id) (into [frame-id]) (reduce update-fn objects)))) +(defn get-fixed-ids + [objects] + (let [fixed-ids (filter cfh/fixed-scroll? (vals objects)) + + ;; we have to consider the children if the fixed element is a group + fixed-children-ids + (into #{} (mapcat #(cfh/get-children-ids objects (:id %)) fixed-ids)) + + parent-children-ids + (->> fixed-ids + (mapcat #(cons (:id %) (cfh/get-parent-ids objects (:id %)))) + (remove #(= % uuid/zero))) + + fixed-ids + (concat fixed-children-ids parent-children-ids)] + fixed-ids)) + (mf/defc viewport-svg {::mf/wrap [mf/memo] ::mf/wrap-props false} [props] - (let [page (unchecked-get props "page") - frame (unchecked-get props "frame") - base (unchecked-get props "base") - offset (unchecked-get props "offset") - size (unchecked-get props "size") - delta (or (unchecked-get props "delta") (gpt/point 0 0)) + (let [page (unchecked-get props "page") + frame (unchecked-get props "frame") + base (unchecked-get props "base") + offset (unchecked-get props "offset") + size (unchecked-get props "size") + fixed? (unchecked-get props "fixed?") + delta (or (unchecked-get props "delta") (gpt/point 0 0)) + vbox (:vbox size) - vbox (:vbox size) + frame (cond-> frame fixed? (assoc :fixed-scroll true)) - fixed-ids (filter :fixed-scroll (vals (:objects page))) + objects (:objects page) + objects (cond-> objects fixed? (assoc-in [(:id frame) :fixed-scroll] true)) - ;; we have con consider the children if the fixed element is a group - fixed-children-ids (into #{} (mapcat #(cph/get-children-ids (:objects page) (:id %)) fixed-ids)) + fixed-ids (get-fixed-ids objects) - parent-children-ids (->> fixed-ids - (mapcat #(cons (:id %) (cph/get-parent-ids (:objects page) (:id %)))) - (remove #(= % uuid/zero))) + not-fixed-ids + (->> (remove (set fixed-ids) (keys objects)) + (remove #(= % uuid/zero))) - fixed-ids (concat fixed-children-ids parent-children-ids) + calculate-objects + (fn [ids] + (->> ids + (map (d/getf objects)) + (concat [frame]) + (d/index-by :id) + (prepare-objects frame size delta))) - not-fixed-ids (->> (remove (set fixed-ids) (keys (:objects page))) - (remove #(= % uuid/zero))) + objects-fixed + (mf/with-memo [fixed-ids page frame size delta] + (calculate-objects fixed-ids)) - calculate-objects (fn [ids] (->> ids - (map (d/getf (:objects page))) - (concat [frame]) - (d/index-by :id) - (prepare-objects frame size delta))) + objects-not-fixed + (mf/with-memo [not-fixed-ids page frame size delta] + (calculate-objects not-fixed-ids)) - objects-fixed (mf/with-memo [fixed-ids page frame size delta] - (calculate-objects fixed-ids)) + all-objects + (mf/with-memo [objects-fixed objects-not-fixed] + (merge objects-fixed objects-not-fixed)) - objects-not-fixed (mf/with-memo [not-fixed-ids page frame size delta] - (calculate-objects not-fixed-ids)) + wrapper-fixed + (mf/with-memo [page frame size] + (shapes/frame-container-factory (assoc objects-fixed ::fixed true) all-objects)) - all-objects (mf/with-memo [objects-fixed objects-not-fixed] - (merge objects-fixed objects-not-fixed)) - - wrapper-fixed (mf/with-memo [page frame size] - (shapes/frame-container-factory objects-fixed all-objects)) - - wrapper-not-fixed (mf/with-memo [objects-not-fixed] - (shapes/frame-container-factory objects-not-fixed all-objects)) + wrapper-not-fixed + (mf/with-memo [objects-not-fixed] + (shapes/frame-container-factory objects-not-fixed all-objects)) ;; Retrieve frames again with correct modifier frame (get all-objects (:id frame)) base (get all-objects (:id base)) - non-delay-interactions (->> (:interactions frame) - (filterv #(not= (:event-type %) :after-delay))) + non-delay-interactions + (->> (:interactions frame) + (filterv #(not= (:event-type %) :after-delay))) - fixed-frame (-> frame - (dissoc :fills) - (assoc :interactions non-delay-interactions))] + fixed-frame + (-> frame + (dissoc :fills) + (assoc :interactions non-delay-interactions))] [:& (mf/provider shapes/base-frame-ctx) {:value base} [:& (mf/provider shapes/frame-offset-ctx) {:value offset} - ;; We have two different svgs for fixed and not fixed elements so we can emulate the sticky css attribute in svg - [:svg.not-fixed {:view-box vbox - :width (:width size) - :height (:height size) - :version "1.1" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns "http://www.w3.org/2000/svg" - :fill "none"} - [:& wrapper-not-fixed {:shape frame :view-box vbox}]] - [:svg.fixed {:view-box vbox - :width (:width size) - :height (:height size) - :version "1.1" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns "http://www.w3.org/2000/svg" - :fill "none" - :style {:width (:width size) - :height (:height size)}} - [:& wrapper-fixed {:shape fixed-frame :view-box vbox}]]]])) + (if fixed? + [:svg {:class (stl/css :fixed) + :view-box vbox + :width (:width size) + :height (:height size) + :version "1.1" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns "http://www.w3.org/2000/svg" + :fill "none"} + [:& wrapper-not-fixed {:shape frame :view-box vbox}]] + + [:* + ;; We have two different svgs for fixed and not fixed elements so we can emulate the sticky css attribute in svg + [:svg {:class (stl/css :fixed) + :view-box vbox + :width (:width size) + :height (:height size) + :version "1.1" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns "http://www.w3.org/2000/svg" + :fill "none" + :style {:width (:width size) + :height (:height size) + :z-index 1}} + [:& wrapper-fixed {:shape fixed-frame :view-box vbox}]] + + [:svg {:class (stl/css :not-fixed) + :view-box vbox + :width (:width size) + :height (:height size) + :version "1.1" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns "http://www.w3.org/2000/svg" + :fill "none"} + [:& wrapper-not-fixed {:shape frame :view-box vbox}]]])]])) (mf/defc viewport {::mf/wrap [mf/memo] ::mf/wrap-props false} [props] (let [;; NOTE: with `use-equal-memo` hook we ensure that all values - ;; conserves the reference identity for avoid unnecessary dummy - ;; rerenders. + ;; conserves the reference identity for avoid unnecessary + ;; dummy rerenders. + mode (h/use-equal-memo (unchecked-get props "interactions-mode")) offset (h/use-equal-memo (unchecked-get props "frame-offset")) size (h/use-equal-memo (unchecked-get props "size")) @@ -133,7 +174,8 @@ page (unchecked-get props "page") frame (unchecked-get props "frame") - base (unchecked-get props "base-frame")] + base (unchecked-get props "base-frame") + fixed? (unchecked-get props "fixed?")] (mf/with-effect [mode] (let [on-click @@ -173,7 +215,8 @@ :base base :offset offset :size size - :delta delta}])) + :delta delta + :fixed? fixed?}])) (mf/defc flows-menu {::mf/wrap [mf/memo]} @@ -181,33 +224,44 @@ (let [flows (dm/get-in page [:options :flows]) frames (:frames page) frame (get frames index) - current-flow (mf/use-state - (ctp/get-frame-flow flows (:id frame))) + current-flow* (mf/use-state + #(ctp/get-frame-flow flows (:id frame))) - show-dropdown? (mf/use-state false) - toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) - hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) + current-flow (deref current-flow*) + + show-dropdown?* (mf/use-state false) + show-dropdown? (deref show-dropdown?*) + toggle-dropdown (mf/use-fn #(swap! show-dropdown?* not)) + hide-dropdown (mf/use-fn #(reset! show-dropdown?* false)) select-flow (mf/use-callback - (fn [flow] - (reset! current-flow flow) - (st/emit! (dv/go-to-frame (:starting-frame flow)))))] + (fn [event] + (let [flow (-> (dom/get-current-target event) + (dom/get-data "value") + (d/read-string))] + (reset! current-flow* flow) + (st/emit! (dv/go-to-frame (:starting-frame flow))))))] (when (seq flows) - [:div.view-options {:on-click toggle-dropdown} - [:span.icon i/play] - [:span.label (:name @current-flow)] - [:span.icon i/arrow-down] - [:& dropdown {:show @show-dropdown? + [:div {:on-click toggle-dropdown + :class (stl/css :view-options)} + [:span {:class (stl/css :icon)} i/play] + [:span {:class (stl/css :dropdown-title)} (:name current-flow)] + [:span {:class (stl/css :icon-dropdown)} i/arrow] + [:& dropdown {:show show-dropdown? :on-close hide-dropdown} - [:ul.dropdown.with-check + [:ul {:class (stl/css :dropdown)} (for [[index flow] (d/enumerate flows)] [:li {:key (dm/str "flow-" (:id flow) "-" index) - :class (dom/classnames :selected (= (:id flow) (:id @current-flow))) - :on-click #(select-flow flow)} - [:span.icon i/tick] - [:span.label (:name flow)]])]]]))) + :class (stl/css-case :dropdown-element true + :selected (= (:id flow) (:id current-flow))) + ;; This is not a best practise, is not very performant Do not reproduce + :data-value (pr-str flow) + :on-click select-flow} + [:span {:class (stl/css :label)} (:name flow)] + (when (= (:id flow) (:id current-flow)) + [:span {:class (stl/css :icon)} i/tick])])]]]))) (mf/defc interactions-menu [{:keys [interactions-mode]}] @@ -217,54 +271,82 @@ select-mode (mf/use-fn - (fn [event] - (let [mode (some-> (dom/get-current-target event) - (dom/get-data "mode") - (keyword))] - (dom/stop-propagation event) - (st/emit! (dv/set-interactions-mode mode)))))] - - [:div.view-options {:on-click toggle-dropdown} - [:span.label (tr "viewer.header.interactions")] - [:span.icon i/arrow-down] + (fn [event] + (let [mode (some-> (dom/get-current-target event) + (dom/get-data "mode") + (keyword))] + (dom/stop-propagation event) + (st/emit! (dv/set-interactions-mode mode)))))] + [:div {:on-click toggle-dropdown + :class (stl/css :view-options)} + [:span {:class (stl/css :dropdown-title)} (tr "viewer.header.interactions")] + [:span {:class (stl/css :icon-dropdown)} i/arrow] [:& dropdown {:show @show-dropdown? :on-close hide-dropdown} - [:ul.dropdown.with-check - [:li {:class (dom/classnames :selected (= interactions-mode :hide)) + [:ul {:class (stl/css :dropdown)} + [:li {:class (stl/css-case :dropdown-element true + :selected (= interactions-mode :hide)) :on-click select-mode - :data-mode :hide} - [:span.icon i/tick] - [:span.label (tr "viewer.header.dont-show-interactions")]] + :data-mode "hide"} - [:li {:class (dom/classnames :selected (= interactions-mode :show)) + [:span {:class (stl/css :label)} (tr "viewer.header.dont-show-interactions")] + (when (= interactions-mode :hide) + [:span {:class (stl/css :icon)} i/tick])] + + [:li {:class (stl/css-case :dropdown-element true + :selected (= interactions-mode :show)) :on-click select-mode - :data-mode :show} - [:span.icon i/tick] - [:span.label (tr "viewer.header.show-interactions")]] + :data-mode "show"} + [:span {:class (stl/css :label)} (tr "viewer.header.show-interactions")] + (when (= interactions-mode :show) + [:span {:class (stl/css :icon)} i/tick])] - [:li {:class (dom/classnames :selected (= interactions-mode :show-on-click)) + + + [:li {:class (stl/css-case :dropdown-element true + :selected (= interactions-mode :show-on-click)) :on-click select-mode - :data-mode :show-on-click} - [:span.icon i/tick] - [:span.label (tr "viewer.header.show-interactions-on-click")]]]]])) + :data-mode "show-on-click"} + [:span {:class (stl/css :label)} (tr "viewer.header.show-interactions-on-click")] + (when (= interactions-mode :show-on-click) + [:span {:class (stl/css :icon)} i/tick])]]]])) (defn animate-go-to-frame [animation current-viewport orig-viewport current-size orig-size wrapper-size] (case (:animation-type animation) + ;; Why use three keyframes instead of two? + ;; If we use two keyframes, the first frame + ;; will disappear while the second frame + ;; is still appearing. + ;; ___ ___ + ;; \/ + ;; ___/\___ + ;; ^ in here we have 50% opacity of both frames so the background + ;; is visible. + ;; + ;; This solution waits until the second frame + ;; has appeared to disappear the first one. + ;; ________ + ;; /\ + ;; _/ \___ + ;; ^ in here we have 100% opacity of the first frame and 0% opacity. :dissolve (do (dom/animate! orig-viewport [#js {:opacity "100%"} - #js {:opacity "0"}] - #js {:duration (:duration animation) - :easing (name (:easing animation))} - #(st/emit! (dv/complete-animation))) + #js {:opacity "0%"} + #js {:opacity "0%"}] + #js {:delay (/ (:duration animation) 3) + :duration (/ (* 2 (:duration animation)) 3) + :easing (name (:easing animation))}) (dom/animate! current-viewport - [#js {:opacity "0"} + [#js {:opacity "0%"} + #js {:opacity "100%"} #js {:opacity "100%"}] #js {:duration (:duration animation) - :easing (name (:easing animation))})) + :easing (name (:easing animation))} + #(st/emit! (dv/complete-animation)))) :slide (case (:way animation) diff --git a/frontend/src/app/main/ui/viewer/interactions.scss b/frontend/src/app/main/ui/viewer/interactions.scss new file mode 100644 index 0000000000..3cd9751b47 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/interactions.scss @@ -0,0 +1,92 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.view-options { + @include bodySmallTypography; + display: flex; + align-items: center; + position: relative; + gap: $s-4; + height: $s-32; + border-radius: $br-8; + background-color: var(--input-background-color); + padding: $s-8; + cursor: pointer; +} +.dropdown-title { + @include bodySmallTypography; + flex-grow: 1; + color: var(--input-foreground-color-active); +} + +.label { + flex-grow: 1; + color: var(--input-foreground-color); +} + +.dropdown { + @extend .menu-dropdown; + right: $s-2; + top: calc($s-2 + $s-48); + width: $s-272; + padding: $s-6; + max-height: calc(100vh - 3 * ($s-2 + $s-48)); + overflow: auto; +} + +.dropdown-element { + @extend .dropdown-element-base; + min-height: $s-32; + .icon { + @include flexCenter; + height: 100%; + width: $s-16; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } + &:hover .label { + color: var(--input-foreground-color-active); + } +} + +.dropdown-element.selected { + .label { + color: var(--input-foreground-color-active); + } + .icon svg { + stroke: var(--input-foreground-color); + } +} + +.icon, +.icon-dropdown { + @include flexCenter; + height: 100%; + width: $s-16; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } +} + +.icon-dropdown svg { + transform: rotate(90deg); +} + +// breakpoint 1013px + +.fixed { + position: fixed; + pointer-events: none; + + :global(.frame-children) g { + pointer-events: auto; + } +} diff --git a/frontend/src/app/main/ui/viewer/login.cljs b/frontend/src/app/main/ui/viewer/login.cljs index 7a130a7a42..1a1e692dcf 100644 --- a/frontend/src/app/main/ui/viewer/login.cljs +++ b/frontend/src/app/main/ui/viewer/login.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.login + (:require-macros [app.main.style :as stl]) (:require [app.common.logging :as log] [app.main.data.modal :as modal] @@ -28,12 +29,27 @@ (let [uri (. (. js/document -location) -href) user-email (mf/use-state "") register-token (mf/use-state "") - current-section (mf/use-state :login) - set-current-section (mf/use-fn #(reset! current-section %)) + + current-section* (mf/use-state :login) + current-section (deref current-section*) + + set-current-section + (mf/use-fn #(reset! current-section* %)) + + set-section + (mf/use-fn + (fn [event] + (let [section (-> (dom/get-current-target event) + (dom/get-data "value") + (keyword))] + (set-current-section section)))) + + go-back-to-login (mf/use-fn #(set-current-section :login)) + main-section (or - (= @current-section :login) - (= @current-section :register) - (= @current-section :register-validate)) + (= current-section :login) + (= current-section :register) + (= current-section :register-validate)) close (fn [event] (dom/prevent-default event) @@ -49,58 +65,64 @@ (fn [data] (reset! register-token (:token data)) (set-current-section :register-validate))] + (mf/with-effect [] (swap! storage assoc :redirect-url uri)) - [:div.modal-overlay - [:div.modal-container.login-register - [:div.title - [:div.modal-close-button {:on-click close :title (tr "labels.close")} - i/close] - (when main-section - [:h2 (tr "labels.continue-with-penpot")])] + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} (tr "labels.continue-with-penpot")] + [:button {:class (stl/css :modal-close-btn) + :title (tr "labels.close") + :on-click close} i/close]] - [:div.modal-bottom.auth-content - - (case @current-section + [:div {:class (stl/css :modal-content)} + (case current-section :login - [:div.generic-form.login-form - [:div.form-container - [:& login-methods {:on-success-callback success-login}] - [:div.links - [:div.link-entry - [:a {:on-click #(set-current-section :recovery-request)} - (tr "auth.forgot-password")]] - [:div.link-entry - [:span (tr "auth.register") " "] - [:a {:on-click #(set-current-section :register)} - (tr "auth.register-submit")]]]]] + [:div {:class (stl/css :form-container)} + [:& login-methods {:on-success-callback success-login :origin :viewer}] + [:div {:class (stl/css :links)} + [:div {:class (stl/css :recovery-request)} + [:a {:on-click set-section + :class (stl/css :recovery-link) + :data-value "recovery-request"} + (tr "auth.forgot-password")]] + [:div {:class (stl/css :register)} + [:span {:class (stl/css :register-text)} + (tr "auth.register") " "] + [:a {:on-click set-section + :class (stl/css :register-link) + :data-value "register"} + (tr "auth.register-submit")]]]] :register - [:div.form-container + [:div {:class (stl/css :form-container)} [:& register-methods {:on-success-callback success-register}] - [:div.links - [:div.link-entry + [:div {:class (stl/css :links)} + [:div {:class (stl/css :account)} [:span (tr "auth.already-have-account") " "] - [:a {:on-click #(set-current-section :login)} + [:a {:on-click set-section + :data-value "login"} (tr "auth.login-here")]]]] :register-validate - [:div.form-container + [:div {:class (stl/css :form-container)} [:& register-validate-form {:params {:token @register-token} :on-success-callback success-email-sent}] - [:div.links - [:div.link-entry - [:a {:on-click #(set-current-section :register)} + [:div {:class (stl/css :links)} + [:div {:class (stl/css :register)} + [:a {:on-click set-section + :data-value "register"} (tr "labels.go-back")]]]] :recovery-request - [:& recovery-request-page {:go-back-callback #(set-current-section :login) + [:& recovery-request-page {:go-back-callback go-back-to-login :on-success-callback success-email-sent}] :email-sent - [:div.form-container - [:& register-success-page {:params {:email @user-email}}]])] + [:div {:class (stl/css :form-container)} + [:& register-success-page {:params {:email @user-email}}]]) - (when main-section - [:div.modal-footer.links - [:& terms-login]])]])) + (when main-section + [:div {:class (stl/css :links)} + [:& terms-login]])]]])) diff --git a/frontend/src/app/main/ui/viewer/login.scss b/frontend/src/app/main/ui/viewer/login.scss new file mode 100644 index 0000000000..74dc3eb6e2 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/login.scss @@ -0,0 +1,73 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + @extend .modal-container-base; +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include uppercaseTitleTipography; + color: var(--modal-title-foreground-color); +} + +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content { + @include flexColumn; + @include bodySmallTypography; + gap: $s-24; + max-height: $s-400; + width: $s-368; + overflow: hidden auto; + form { + display: flex; + flex-direction: column; + margin-bottom: 1.5rem; + gap: 0.75rem; + } +} + +.form-container { + display: flex; + justify-content: center; + flex-direction: column; + max-width: $s-368; +} + +.links { + position: relative; +} + +.link-entry { + display: flex; + flex-direction: column; + gap: $s-12; + + span { + text-align: center; + font-size: $fs-14; + color: var(--modal-text-foreground-color); + margin-top: $s-12; + } + a { + @extend .button-secondary; + height: $s-40; + text-transform: uppercase; + font-size: $fs-11; + } +} diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 5c41de9211..5832f28cea 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -7,8 +7,9 @@ (ns app.main.ui.viewer.shapes "The main container for a frame in viewer mode" (:require + [app.common.data :as d] + [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] [app.common.types.shape.interactions :as ctsi] [app.main.data.viewer :as dv] [app.main.refs :as refs] @@ -39,9 +40,9 @@ (defn- find-relative-to-base-frame [shape objects overlays-ids base-frame] (cond - (cph/frame-shape? shape) shape - (or (empty? overlays-ids) (nil? shape) (cph/root? shape)) base-frame - :else (find-relative-to-base-frame (cph/get-parent objects (:id shape)) objects overlays-ids base-frame))) + (cfh/frame-shape? shape) shape + (or (empty? overlays-ids) (nil? shape) (cfh/root? shape)) base-frame + :else (find-relative-to-base-frame (cfh/get-parent objects (:id shape)) objects overlays-ids base-frame))) (defn- activate-interaction [interaction shape base-frame frame-offset objects overlays] @@ -69,6 +70,7 @@ background-overlay (:background-overlay interaction) overlays-ids (set (map :id overlays)) relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame) + fixed-base? (cfh/fixed? objects relative-to-id) [position snap-to] (ctsi/calc-overlay-position interaction shape objects @@ -82,7 +84,8 @@ snap-to close-click-outside background-overlay - (:animation interaction))))) + (:animation interaction) + fixed-base?)))) :toggle-overlay (let [dest-frame-id (:destination interaction) @@ -95,6 +98,7 @@ relative-to-shape (or (get objects relative-to-id) base-frame) overlays-ids (set (map :id overlays)) relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame) + fixed-base? (cfh/fixed? objects (:id base-frame)) [position snap-to] (ctsi/calc-overlay-position interaction shape objects @@ -111,7 +115,8 @@ snap-to close-click-outside background-overlay - (:animation interaction))))) + (:animation interaction) + fixed-base?)))) :close-overlay (let [dest-frame-id (or (:destination interaction) @@ -147,10 +152,11 @@ (if (= (:type shape) :frame) ;; manual interactions are always from "self" (:frame-id shape) (:id shape)) - (:position-relative-to interaction)) + (:position-relative-to interaction)) relative-to-shape (or (get objects relative-to-id) base-frame) overlays-ids (set (map :id overlays)) relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame) + fixed-base? (cfh/fixed? objects (:id base-frame)) [position snap-to] (ctsi/calc-overlay-position interaction shape objects @@ -167,7 +173,8 @@ snap-to close-click-outside background-overlay - (:animation interaction))))) + (:animation interaction) + fixed-base?)))) :close-overlay @@ -183,6 +190,7 @@ background-overlay (:background-overlay interaction) overlays-ids (set (map :id overlays)) relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame) + fixed-base? (cfh/fixed? objects (:id base-frame)) [position snap-to] (ctsi/calc-overlay-position interaction shape objects @@ -196,7 +204,8 @@ snap-to close-click-outside background-overlay - (:animation interaction))))) + (:animation interaction) + fixed-base?)))) nil)) (defn- on-pointer-down @@ -256,90 +265,87 @@ (mf/defc interaction [{:keys [shape interactions interactions-show?]}] - (let [{:keys [x y width height]} (:selrect shape) - frame? (= :frame (:type shape))] + (let [{:keys [x y width height]} (:selrect shape)] (when-not (empty? interactions) [:rect {:x (- x 1) :y (- y 1) :width (+ width 2) :height (+ height 2) - :fill "var(--color-primary)" - :stroke "var(--color-primary)" + :fill "var(--color-accent-tertiary)" + :stroke "var(--color-accent-tertiary)" :stroke-width (if interactions-show? 1 0) :fill-opacity (if interactions-show? 0.2 0) - :style {:pointer-events (when frame? "none")} :transform (gsh/transform-str shape)}]))) - ;; TODO: use-memo use-fn (defn generic-wrapper-factory "Wrap some svg shape and add interaction controls" [component] (mf/fnc generic-wrapper - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - childs (unchecked-get props "childs") - frame (unchecked-get props "frame") - objects (unchecked-get props "objects") - all-objects (or (unchecked-get props "all-objects") objects) - base-frame (mf/use-ctx base-frame-ctx) - frame-offset (mf/use-ctx frame-offset-ctx) - interactions-show? (mf/deref viewer-interactions-show?) - overlays (mf/deref refs/viewer-overlays) - interactions (:interactions shape) - svg-element? (and (= :svg-raw (:type shape)) - (not= :svg (get-in shape [:content :tag]))) + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + childs (unchecked-get props "childs") + frame (unchecked-get props "frame") + objects (unchecked-get props "objects") + all-objects (or (unchecked-get props "all-objects") objects) + base-frame (mf/use-ctx base-frame-ctx) + frame-offset (mf/use-ctx frame-offset-ctx) + interactions-show? (mf/deref viewer-interactions-show?) + overlays (mf/deref refs/viewer-overlays) + interactions (:interactions shape) + svg-element? (and (= :svg-raw (:type shape)) + (not= :svg (get-in shape [:content :tag]))) - ;; The objects parameter has the shapes that we must draw. It may be a subset of - ;; all-objects in some cases (e.g. if there are fixed elements). But for interactions - ;; handling we need access to all objects inside the page. + ;; The objects parameter has the shapes that we must draw. It may be a subset of + ;; all-objects in some cases (e.g. if there are fixed elements). But for interactions + ;; handling we need access to all objects inside the page. - on-pointer-down - (mf/use-fn (mf/deps shape base-frame frame-offset all-objects) - #(on-pointer-down % shape base-frame frame-offset all-objects overlays)) + on-pointer-down + (mf/use-fn (mf/deps shape base-frame frame-offset all-objects) + #(on-pointer-down % shape base-frame frame-offset all-objects overlays)) - on-pointer-up - (mf/use-fn (mf/deps shape base-frame frame-offset all-objects) - #(on-pointer-up % shape base-frame frame-offset all-objects overlays)) + on-pointer-up + (mf/use-fn (mf/deps shape base-frame frame-offset all-objects) + #(on-pointer-up % shape base-frame frame-offset all-objects overlays)) - on-pointer-enter - (mf/use-fn (mf/deps shape base-frame frame-offset all-objects) - #(on-pointer-enter % shape base-frame frame-offset all-objects overlays)) + on-pointer-enter + (mf/use-fn (mf/deps shape base-frame frame-offset all-objects) + #(on-pointer-enter % shape base-frame frame-offset all-objects overlays)) - on-pointer-leave - (mf/use-fn (mf/deps shape base-frame frame-offset all-objects) - #(on-pointer-leave % shape base-frame frame-offset all-objects overlays))] + on-pointer-leave + (mf/use-fn (mf/deps shape base-frame frame-offset all-objects) + #(on-pointer-leave % shape base-frame frame-offset all-objects overlays))] - (mf/with-effect [] - (let [sems (on-load shape base-frame frame-offset objects overlays)] - (partial run! tm/dispose! sems))) + (mf/with-effect [] + (let [sems (on-load shape base-frame frame-offset objects overlays)] + (partial run! tm/dispose! sems))) - (if-not svg-element? - [:> shape-container {:shape shape - :cursor (when (ctsi/actionable? interactions) "pointer") - :on-pointer-down on-pointer-down - :on-pointer-up on-pointer-up - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave} + (if-not svg-element? + [:> shape-container {:shape shape + :cursor (when (ctsi/actionable? interactions) "pointer") + :on-pointer-down on-pointer-down + :on-pointer-up on-pointer-up + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave} - [:& component {:shape shape - :frame frame - :childs childs - :is-child-selected? true - :objects objects}] + [:& component {:shape shape + :frame frame + :childs childs + :is-child-selected? true + :objects objects}] - [:& interaction {:shape shape - :interactions interactions - :interactions-show? interactions-show?}]] + [:& interaction {:shape shape + :interactions interactions + :interactions-show? interactions-show?}]] ;; Don't wrap svg elements inside a otherwise some can break - [:& component {:shape shape - :frame frame - :childs childs - :objects objects}])))) + [:& component {:shape shape + :frame frame + :childs childs + :objects objects}])))) (defn frame-wrapper [shape-container] @@ -382,60 +388,59 @@ (defn frame-container-factory [objects all-objects] (let [shape-container (shape-container-factory objects all-objects) - frame-wrapper (frame-wrapper shape-container)] + frame-wrapper (frame-wrapper shape-container) + lookup-xf (keep (d/getf objects))] (mf/fnc frame-container - {::mf/wrap-props false} - [props] - (let [shape (obj/get props "shape") - childs (mapv #(get objects %) (:shapes shape)) - props (obj/merge! #js {} props - #js {:shape shape - :childs childs - :objects objects - :all-objects all-objects})] - - [:> frame-wrapper props])))) + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + childs (into [] lookup-xf (:shapes shape)) + props (obj/merge props + #js {:childs childs + :objects objects + :all-objects all-objects})] + [:> frame-wrapper props])))) (defn group-container-factory [objects all-objects] (let [shape-container (shape-container-factory objects all-objects) group-wrapper (group-wrapper shape-container)] (mf/fnc group-container - {::mf/wrap-props false} - [props] - (let [childs (mapv #(get objects %) (:shapes (unchecked-get props "shape"))) - props (obj/merge! #js {} props - #js {:childs childs - :objects objects})] - (when (not-empty childs) - [:> group-wrapper props]))))) + {::mf/wrap-props false} + [props] + (let [childs (mapv #(get objects %) (:shapes (unchecked-get props "shape"))) + props (obj/merge! #js {} props + #js {:childs childs + :objects objects})] + (when (not-empty childs) + [:> group-wrapper props]))))) (defn bool-container-factory [objects all-objects] (let [shape-container (shape-container-factory objects all-objects) bool-wrapper (bool-wrapper shape-container)] (mf/fnc bool-container - {::mf/wrap-props false} - [props] - (let [childs (->> (cph/get-children-ids objects (:id (unchecked-get props "shape"))) - (select-keys objects)) - props (obj/merge! #js {} props - #js {:childs childs - :objects objects})] - [:> bool-wrapper props])))) + {::mf/wrap-props false} + [props] + (let [childs (->> (cfh/get-children-ids objects (:id (unchecked-get props "shape"))) + (select-keys objects)) + props (obj/merge! #js {} props + #js {:childs childs + :objects objects})] + [:> bool-wrapper props])))) (defn svg-raw-container-factory [objects all-objects] (let [shape-container (shape-container-factory objects all-objects) svg-raw-wrapper (svg-raw-wrapper shape-container)] (mf/fnc svg-raw-container - {::mf/wrap-props false} - [props] - (let [childs (mapv #(get objects %) (:shapes (unchecked-get props "shape"))) - props (obj/merge! #js {} props - #js {:childs childs - :objects objects})] - [:> svg-raw-wrapper props])))) + {::mf/wrap-props false} + [props] + (let [childs (mapv #(get objects %) (:shapes (unchecked-get props "shape"))) + props (obj/merge! #js {} props + #js {:childs childs + :objects objects})] + [:> svg-raw-wrapper props])))) (defn shape-container-factory [objects all-objects] @@ -445,42 +450,42 @@ image-wrapper (image-wrapper) circle-wrapper (circle-wrapper)] (mf/fnc shape-container - {::mf/wrap-props false - ::mf/wrap [mf/memo]} - [props] - (let [shape (unchecked-get props "shape") - frame (unchecked-get props "frame") + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [props] + (let [shape (unchecked-get props "shape") + frame (unchecked-get props "frame") - group-container - (mf/with-memo [objects] - (group-container-factory objects all-objects)) + group-container + (mf/with-memo [objects] + (group-container-factory objects all-objects)) - frame-container - (mf/with-memo [objects] - (frame-container-factory objects all-objects)) + frame-container + (mf/with-memo [objects] + (frame-container-factory objects all-objects)) - bool-container - (mf/with-memo [objects] - (bool-container-factory objects all-objects)) + bool-container + (mf/with-memo [objects] + (bool-container-factory objects all-objects)) - svg-raw-container - (mf/with-memo [objects] - (svg-raw-container-factory objects all-objects))] - (when (and shape (not (:hidden shape))) - (let [shape (-> shape - #_(gsh/transform-shape) - (gsh/translate-to-frame frame)) + svg-raw-container + (mf/with-memo [objects] + (svg-raw-container-factory objects all-objects))] + (when (and shape (not (:hidden shape))) + (let [shape (if frame + (gsh/translate-to-frame shape frame) + shape) - opts #js {:shape shape - :objects objects - :all-objects all-objects}] - (case (:type shape) - :frame [:> frame-container opts] - :text [:> text-wrapper opts] - :rect [:> rect-wrapper opts] - :path [:> path-wrapper opts] - :image [:> image-wrapper opts] - :circle [:> circle-wrapper opts] - :group [:> group-container {:shape shape :frame frame :objects objects}] - :bool [:> bool-container {:shape shape :frame frame :objects objects}] - :svg-raw [:> svg-raw-container {:shape shape :frame frame :objects objects}]))))))) + opts #js {:shape shape + :objects objects + :all-objects all-objects}] + (case (:type shape) + :frame [:> frame-container opts] + :text [:> text-wrapper opts] + :rect [:> rect-wrapper opts] + :path [:> path-wrapper opts] + :image [:> image-wrapper opts] + :circle [:> circle-wrapper opts] + :group [:> group-container {:shape shape :frame frame :objects objects}] + :bool [:> bool-container {:shape shape :frame frame :objects objects}] + :svg-raw [:> svg-raw-container {:shape shape :frame frame :objects objects}]))))))) diff --git a/frontend/src/app/main/ui/viewer/share_link.cljs b/frontend/src/app/main/ui/viewer/share_link.cljs index 9ae7f1b2ad..8b08ba935c 100644 --- a/frontend/src/app/main/ui/viewer/share_link.cljs +++ b/frontend/src/app/main/ui/viewer/share_link.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.share-link + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] @@ -16,12 +17,13 @@ [app.main.data.modal :as modal] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.select :refer [select]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [app.util.webapi :as wapi] - [potok.core :as ptk] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) (log/set-level! :warn) @@ -133,6 +135,7 @@ (fn [_] (wapi/write-to-clipboard current-link) (st/emit! (msg/show {:type :info + :notification-type :toast :content (tr "common.share-link.link-copied-success") :timeout 1000}))) @@ -151,135 +154,165 @@ (fn [_] (swap! perms-visible* not)) - on-who-change - (fn [type event] - (let [target (dom/get-target event) - value (dom/get-value target) - value (keyword value)] - (reset! confirm* false) - (if (= type :comment) - (swap! options* assoc :who-comment (d/name value)) - (swap! options* assoc :who-inspect (d/name value)))))] + on-inspect-change + (fn [value] + (reset! confirm* false) + (swap! options* assoc :who-inspect value)) - [:div.modal-overlay.transparent.share-modal - [:div.modal-container.share-link-dialog - [:div.modal-content.initial - [:div.title - [:h2 (tr "common.share-link.title")] - [:div.modal-close-button - {:on-click on-close - :title (tr "labels.close")} - i/close]]] - [:div.modal-content - [:div.share-link-section + on-comment-change + (fn [value] + (reset! confirm* false) + (swap! options* assoc :who-comment value))] + + [:div {:class (stl/css :share-modal)} + [:div {:class (stl/css :share-link-dialog)} + [:div {:class (stl/css :share-link-header)} + [:h2 {:class (stl/css :share-link-title)} + (tr "common.share-link.title")] + [:button {:class (stl/css :modal-close-button) + :on-click on-close + :title (tr "labels.close")} + i/close]] + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :share-link-section)} (when (and (not confirm?) (some? current-link)) - [:div.custom-input.with-icon - [:input {:type "text" + [:div {:class (stl/css :custon-input-wrapper)} + [:input {:class (stl/css :input-text) + :type "text" :value (or current-link "") :placeholder (tr "common.share-link.placeholder") :read-only true}] - [:div.help-icon {:title (tr "viewer.header.share.copy-link") - :on-click copy-link} - i/copy]]) - [:div.hint-wrapper + + [:button {:class (stl/css :copy-button) + :title (tr "viewer.header.share.copy-link") + :on-click copy-link} + i/clipboard]]) + + [:div {:class (stl/css :hint-wrapper)} (when (not ^boolean confirm?) - [:div.hint (tr "common.share-link.permissions-hint")]) + [:div {:class (stl/css :hint)} (tr "common.share-link.permissions-hint")]) (cond (true? confirm?) - [:div.confirm-dialog - [:div.description (tr "common.share-link.confirm-deletion-link-description")] - [:div.actions - [:input.btn-secondary - {:type "button" - :on-click #(reset! confirm* false) - :value (tr "labels.cancel")}] - [:input.btn-warning - {:type "button" - :on-click delete-link - :value (tr "common.share-link.destroy-link")}]]] + [:div {:class (stl/css :confirm-dialog)} + [:div {:class (stl/css :description)} + (tr "common.share-link.confirm-deletion-link-description")] + [:div {:class (stl/css :actions)} + [:input {:type "button" + :class (stl/css :button-cancel) + :on-click #(reset! confirm* false) + :value (tr "labels.cancel")}] + [:input {:type "button" + :class (stl/css :button-danger) + :on-click delete-link + :value (tr "common.share-link.destroy-link")}]]] (some? current-link) - [:input.btn-secondary + [:input {:type "button" - :class "primary" + :class (stl/css :button-danger) :on-click try-delete-link :value (tr "common.share-link.destroy-link")}] :else - [:input.btn-primary + [:input {:type "button" - :class "primary" + :class (stl/css :button-active) :on-click create-link - :value (tr "common.share-link.get-link")}])]]] - [:div.modal-content.ops-section - [:div.manage-permissions - {:on-click toggle-perms-visibility} - [:span.icon i/picker-hsv] - [:div.title (tr "common.share-link.manage-ops")]] - (when ^boolean perms-visible? - [:* - (let [all-selected? (:all-pages options) - pages (->> (get-in file [:data :pages]) - (map #(get-in file [:data :pages-index %]))) - selected (:pages options)] + :value (tr "common.share-link.get-link")}])]] + + + (when (not ^boolean confirm?) + [:div {:class (stl/css :permissions-section)} + [:button {:class (stl/css :manage-permissions) + :on-click toggle-perms-visibility} + [:span {:class (stl/css-case :icon true + :rotated perms-visible?)} + i/arrow] + (tr "common.share-link.manage-ops")] + + (when ^boolean perms-visible? [:* - [:div.view-mode - [:div.subtitle - [:span.icon i/play] - (tr "common.share-link.permissions-pages")] - [:div.items - (if (= 1 (count pages)) - [:div.input-checkbox.check-primary - [:input {:type "checkbox" - :id (dm/str "page-" current-page-id) - :data-page-id (dm/str current-page-id) - :on-change on-mark-checked-page - :checked true}] - [:label {:for (str "page-" current-page-id)} (:name current-page)] - [:span (str " " (tr "common.share-link.current-tag"))]] + (let [all-selected? (:all-pages options) + pages (->> (get-in file [:data :pages]) + (map #(get-in file [:data :pages-index %]))) + selected (:pages options)] + [:div {:class (stl/css :view-mode)} + [:div {:class (stl/css :subtitle)} + (tr "common.share-link.permissions-pages")] + [:div {:class (stl/css :items)} + (if (= 1 (count pages)) + [:div {:class (stl/css :checkbox-wrapper)} + + [:label {:for (str "page-" current-page-id) + :class (stl/css-case :global/checked true)} + + [:span {:class (stl/css :checked)} + i/status-tick] + + (:name current-page)] - [:* - [:div.row - [:div.input-checkbox.check-primary [:input {:type "checkbox" - :id "view-all" - :checked all-selected? - :name "pages-mode" - :on-change on-toggle-all}] - [:label {:for "view-all"} (tr "common.share-link.view-all")]] - [:span.count-pages (tr "common.share-link.page-shared" (i18n/c (count selected)))]] + :id (dm/str "page-" current-page-id) + :data-page-id (dm/str current-page-id) + :on-change on-mark-checked-page + :checked true}] + [:span (str " " (tr "common.share-link.current-tag"))]] - [:ul.pages-selection - (for [{:keys [id name]} pages] - [:li.input-checkbox.check-primary {:key (dm/str id)} - [:input {:type "checkbox" - :id (dm/str "page-" id) - :data-page-id (dm/str id) - :on-change on-mark-checked-page - :checked (contains? selected id)}] - (if (= current-page-id id) - [:* - [:label {:for (dm/str "page-" id)} name] - [:span.current-tag (dm/str " " (tr "common.share-link.current-tag"))]] - [:label {:for (dm/str "page-" id)} name])])]])]]]) - [:div.access-mode - [:div.subtitle - [:span.icon i/chat] - (tr "common.share-link.permissions-can-comment")] - [:div.items - [:select.input-select {:on-change (partial on-who-change :comment) - :value (:who-comment options)} - [:option {:value "team"} (tr "common.share-link.team-members")] - [:option {:value "all"} (tr "common.share-link.all-users")]]]] - [:div.inspect-mode - [:div.subtitle - [:span.icon i/code] - (tr "common.share-link.permissions-can-inspect")] - [:div.items - [:select.input-select {:on-change (partial on-who-change :inspect) - :value (:who-inspect options)} - [:option {:value "team"} (tr "common.share-link.team-members")] - [:option {:value "all"} (tr "common.share-link.all-users")]]]]])]]])) + [:* + [:div {:class (stl/css :select-all-row)} + [:div {:class (stl/css :checkbox-wrapper)} + [:label {:for "view-all" + :class (stl/css :select-all-label)} + [:span {:class (stl/css-case :global/checked all-selected?)} + (when all-selected? + i/status-tick)] + (tr "common.share-link.view-all") + [:input {:type "checkbox" + :id "view-all" + :checked all-selected? + :name "pages-mode" + :on-change on-toggle-all}]]] + + [:span {:class (stl/css :count-pages)} + (tr "common.share-link.page-shared" (i18n/c (count selected)))]] + + [:ul {:class (stl/css :pages-selection)} + (for [{:keys [id name]} pages] + [:li {:class (stl/css :checkbox-wrapper) + :key (dm/str id)} + [:label {:for (dm/str "page-" id)} + [:span {:class (stl/css-case :global/checked (contains? selected id))} + (when (contains? selected id) + i/status-tick)] + name + (when (= current-page-id id) + [:div {:class (stl/css :current-tag)} (dm/str " " (tr "common.share-link.current-tag"))]) + [:input {:type "checkbox" + :id (dm/str "page-" id) + :data-page-id (dm/str id) + :on-change on-mark-checked-page + :checked (contains? selected id)}]]])]])]]) + + [:div {:class (stl/css :access-mode)} + [:div {:class (stl/css :subtitle)} + (tr "common.share-link.permissions-can-comment")] + [:div {:class (stl/css :items)} + [:& select + {:class (stl/css :who-comment-select) + :default-value (dm/str (:who-comment options)) + :options [{:value "team" :label (tr "common.share-link.team-members")} + {:value "all" :label (tr "common.share-link.all-users")}] + :on-change on-comment-change}]]] + [:div {:class (stl/css :inspect-mode)} + [:div {:class (stl/css :subtitle)} + (tr "common.share-link.permissions-can-inspect")] + [:div {:class (stl/css :items)} + [:& select + {:class (stl/css :who-inspect-select) + :default-value (dm/str (:who-inspect options)) + :options [{:value "team" :label (tr "common.share-link.team-members")} + {:value "all" :label (tr "common.share-link.all-users")}] + :on-change on-inspect-change}]]]])])]]])) diff --git a/frontend/src/app/main/ui/viewer/share_link.scss b/frontend/src/app/main/ui/viewer/share_link.scss new file mode 100644 index 0000000000..8c32338bc5 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/share_link.scss @@ -0,0 +1,200 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.share-modal { + display: block; + position: absolute; + top: $s-52; + right: $s-12; + left: calc(100vw - $s-512); + z-index: $z-index-modal; +} + +.share-link-dialog { + @extend .modal-container-base; + min-height: unset; +} + +.share-link-header { + margin-bottom: $s-24; +} + +.share-link-title { + @include uppercaseTitleTipography; + color: var(--modal-title-foreground-color); +} + +.modal-close-button { + @extend .modal-close-btn-base; +} + +.modal-content { + @include bodySmallTypography; + @include flexColumn; + gap: $s-24; +} + +.share-link-section { + @include flexColumn; + gap: $s-8; +} + +.hint-wrapper { + @include flexRow; +} + +.hint { + flex-grow: 1; + color: var(--modal-text-foreground-color); +} + +.custon-input-wrapper { + @include flexRow; + border-radius: $br-8; + height: $s-32; + width: 100%; + background-color: var(--input-background-color); +} + +.input-text { + @extend .input-element; + color: var(--input-foreground-color-active); + padding-left: $s-8; + margin: 0; + flex-grow: 1; + &:focus { + outline: none; + border: $s-1 solid var(--input-border-color-active); + } +} + +.copy-button { + @extend .button-secondary; + @include flexRow; + gap: $s-8; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + stroke: var(--icon-foreground-hover); + } +} + +.description { + @include bodySmallTypography; + color: var(--modal-text-foreground-color); + margin-bottom: $s-24; +} + +.actions { + @include flexRow; + justify-content: flex-end; +} + +.button-active { + @extend .modal-accept-btn; +} + +.button-cancel { + @extend .modal-cancel-btn; +} + +.button-danger { + @extend .modal-danger-btn; +} + +.permissions-section { + @include flexColumn; + gap: $s-8; +} + +.manage-permissions { + @include buttonStyle; + @include uppercaseTitleTipography; + color: var(--menu-foreground-color-rest); + height: $s-32; + display: flex; + align-items: center; + padding: 0; +} + +.icon { + @include flexCenter; + margin-right: $s-6; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + &.rotated { + transform: rotate(90deg); + } +} + +.view-mode, +.access-mode, +.inspect-mode { + display: flex; + width: 100%; +} + +.view-mode { + max-height: $s-216; + overflow: hidden auto; + scrollbar-gutter: stable; +} + +.subtitle { + color: var(--modal-text-foreground-color); + display: flex; + align-items: center; + justify-content: flex-start; + width: $s-136; + height: $s-32; +} + +.items { + flex-grow: 1; + color: var(--input-foreground-color-active); +} +.select-all-row { + @include flexRow; + justify-content: space-between; + height: $s-32; + border-bottom: $s-1 solid var(--input-border-color-disabled); +} +.select-all-label { + color: var(--input-foreground-color-active); +} +.pages-selection { + margin: 0; + li { + border-bottom: $s-1 solid var(--input-border-color-disabled); + } + li:last-child { + border-bottom: none; + } +} +.count-pages, +.current-tag { + @include bodySmallTypography; + color: var(--input-foreground-color); +} + +.checkbox-wrapper { + @extend .input-checkbox; + height: $s-32; + padding: 0; + span.checked { + background-color: var(--input-checkbox-background-color-active); + border: $s-1 solid var(--input-checkbox-background-color-active); + svg { + @extend .button-icon-small; + stroke: var(--input-checkbox-foreground-color-active); + } + } +} diff --git a/frontend/src/app/main/ui/viewer/thumbnails.cljs b/frontend/src/app/main/ui/viewer/thumbnails.cljs index 0563116ecd..4a20d35fe2 100644 --- a/frontend/src/app/main/ui/viewer/thumbnails.cljs +++ b/frontend/src/app/main/ui/viewer/thumbnails.cljs @@ -5,16 +5,16 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer.thumbnails + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] [app.main.data.viewer :as dv] [app.main.render :as render] [app.main.store :as st] [app.main.ui.icons :as i] - [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.object :as obj] [app.util.timers :as ts] @@ -57,44 +57,58 @@ (mf/use-effect on-mount) (if expanded? - [:div.thumbnails-content - [:div.thumbnails-list-expanded children]] - [:div.thumbnails-content - [:div.left-scroll-handler {:on-click on-left-arrow-click} i/arrow-slide] - [:div.right-scroll-handler {:on-click on-right-arrow-click} i/arrow-slide] - [:div.thumbnails-list {:ref container :on-wheel on-scroll} - [:div.thumbnails-list-inside {:style {:right (str (* @offset 152) "px")}} + [:div {:class (stl/css :thumbnails-content)} + [:div {:class (stl/css :thumbnails-list-expanded)} children]] + + [:div {:class (stl/css :thumbnails-content)} + [:button {:class (stl/css :left-scroll-handler) + :on-click on-left-arrow-click} i/arrow] + [:button {:class (stl/css :right-scroll-handler) + :on-click on-right-arrow-click} i/arrow] + + [:div {:class (stl/css :thumbnails-list) + :ref container + :on-wheel on-scroll} + [:div {:class (stl/css :thumbnails-list-inside) + :style {:right (str (* @offset 152) "px")}} children]]]))) (mf/defc thumbnails-summary [{:keys [on-toggle-expand on-close total] :as props}] - [:div.thumbnails-summary - [:span.counter (tr "labels.num-of-frames" (i18n/c total))] - [:span.buttons - [:span.btn-expand {:on-click on-toggle-expand} i/arrow-down] - [:span.btn-close {:on-click on-close} i/close]]]) + [:div {:class (stl/css :thumbnails-summary)} + [:span {:class (stl/css :counter)} + (tr "labels.num-of-frames" (i18n/c total))] + [:span {:class (stl/css :actions)} + [:button {:class (stl/css :expand-btn) + :on-click on-toggle-expand} i/arrow] + [:button {:class (stl/css :close-btn) + :on-click on-close} i/close]]]) (mf/defc thumbnail-item {::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} [{:keys [selected? frame on-click index objects page-id thumbnail-data]}] - (let [children-ids (cph/get-children-ids objects (:id frame)) - children-bounds (gsh/selection-rect (concat [frame] (->> children-ids (keep (d/getf objects)))))] - [:div.thumbnail-item {:on-click #(on-click % index)} - [:div.thumbnail-preview - {:class (dom/classnames :selected selected?)} + (let [children-ids (cfh/get-children-ids objects (:id frame)) + children-bounds (gsh/shapes->rect (concat [frame] (->> children-ids (keep (d/getf objects)))))] + + [:button {:class (stl/css :thumbnail-item) + :on-click #(on-click % index)} + [:div {:class (stl/css-case :thumbnail-preview true + :selected selected?)} [:& render/frame-svg {:frame (-> frame (assoc :thumbnail (get thumbnail-data (dm/str page-id (:id frame)))) (assoc :children-bounds children-bounds)) :objects objects - :show-thumbnails? true}]] - [:div.thumbnail-info - [:span.name {:title (:name frame)} (:name frame)]]])) + :use-thumbnails true}]] + [:div {:class (stl/css :thumbnail-info) + :title (:name frame)} + (:name frame)]])) (mf/defc thumbnails-panel [{:keys [frames page index show? thumbnail-data] :as props}] - (let [expanded? (mf/use-state false) + (let [expanded-state (mf/use-state false) + expanded? (deref expanded-state) container (mf/use-ref) objects (:objects page) @@ -102,24 +116,27 @@ selected (mf/use-var false) on-item-click - (mf/use-callback - (mf/deps @expanded?) + (mf/use-fn + (mf/deps expanded?) (fn [_ index] (compare-and-set! selected false true) (st/emit! (dv/go-to-frame-by-index index)) - (when @expanded? - (on-close))))] + (when expanded? + (on-close)))) - [:section.viewer-thumbnails - {;; This is better as an inline-style so it won't make a reflow of every frame inside - :style {:display (when (not show?) "none")} - :class (dom/classnames :expanded @expanded?) - :ref container} + toggle-expand + (mf/use-fn + #(swap! expanded-state not))] + [:section {:class (stl/css-case :viewer-thumbnails true + :expanded expanded?) + ;; This is better as an inline-style so it won't make a reflow of every frame inside + :style {:display (when (not show?) "none")} + :ref container} - [:& thumbnails-summary {:on-toggle-expand #(swap! expanded? not) + [:& thumbnails-summary {:on-toggle-expand toggle-expand :on-close on-close :total (count frames)}] - [:& thumbnails-content {:expanded? @expanded? + [:& thumbnails-content {:expanded? expanded? :total (count frames)} (for [[i frame] (d/enumerate frames)] [:& thumbnail-item {:index i diff --git a/frontend/src/app/main/ui/viewer/thumbnails.scss b/frontend/src/app/main/ui/viewer/thumbnails.scss new file mode 100644 index 0000000000..c4de6c4153 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/thumbnails.scss @@ -0,0 +1,152 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.viewer-thumbnails { + background-color: var(--viewer-background-color); + grid-row: 1 / span 1; + grid-column: 1 / span 1; + overflow: hidden; + display: flex; + flex-direction: column; + z-index: $z-index-10; +} + +.expanded { + grid-row: 1 / span 2; + + .expand-btn svg { + transform: rotate(-90deg); + } +} + +.thumbnails-summary { + display: flex; + justify-content: space-between; + align-items: center; + height: $s-32; + margin: $s-24 $s-24 0 $s-24; +} + +.counter { + @include bodySmallTypography; + color: var(--viewer-thumbnails-control-foreground-color); +} + +.actions { + @include flexRow; + width: $s-60; +} + +.expand-btn, +.close-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.expand-btn svg { + transform: rotate(90deg); +} + +.thumbnails-content { + display: grid; + grid-template-columns: $s-40 auto $s-40; + grid-template-rows: auto; +} + +.thumbnails-list-expanded { + grid-column: 1 / span 3; + grid-row: 1 / span 1; + display: flex; + flex-wrap: wrap; + overflow: hidden; +} + +.right-scroll-handler, +.left-scroll-handler { + @extend .button-tertiary; + @include flexCenter; + grid-column: 3 / span 1; + grid-row: 1 / span 1; + width: $s-32; + height: $s-60; + margin: auto 0; + z-index: $z-index-10; + opacity: 0; + &:hover { + opacity: 1; + } + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.left-scroll-handler { + grid-column: 1 / span 1; + grid-row: 1 / span 1; + svg { + transform: rotate(180deg); + } +} + +.thumbnails-list { + grid-column: 1 / span 3; + grid-row: 1 / span 1; + display: flex; + flex-wrap: nowrap; + overflow: hidden; +} + +.thumbnails-list-inside { + display: flex; + position: relative; +} + +.thumbnail-item { + @include buttonStyle; + display: flex; + flex-direction: column; + padding: $s-16; +} + +.thumbnail-preview { + @include flexCenter; + width: $s-132; + min-height: $s-132; + height: $s-132; + padding: $s-4; + + svg { + width: 100%; + height: 100%; + } + + &.selected { + background-color: var(--viewer-thumbnail-background-color-selected); + border-radius: $br-8; + } + + &:hover { + border: $s-1 solid var(--viewer-thumbnail-border-color); + border-radius: $br-8; + } +} + +.thumbnail-info { + @include bodySmallTypography; + @include textEllipsis; + text-align: center; + color: var(--viewer-thumbnails-control-foreground-color); + padding: $s-8 0; + width: 100%; + max-width: $s-132; +} diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index a2a278d5ee..8363e438f3 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -5,7 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] [app.main.data.messages :as msg] @@ -20,25 +20,20 @@ [app.main.ui.hooks :as hooks] [app.main.ui.hooks.resize :refer [use-resize-observer]] [app.main.ui.icons :as i] - [app.main.ui.workspace.colorpalette :refer [colorpalette]] [app.main.ui.workspace.colorpicker] [app.main.ui.workspace.context-menu :refer [context-menu]] [app.main.ui.workspace.coordinates :as coordinates] - [app.main.ui.workspace.header :refer [header]] - [app.main.ui.workspace.left-toolbar :refer [left-toolbar]] [app.main.ui.workspace.libraries] [app.main.ui.workspace.nudge] [app.main.ui.workspace.palette :refer [palette]] [app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]] [app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]] [app.main.ui.workspace.sidebar.history :refer [history-toolbox]] - [app.main.ui.workspace.textpalette :refer [textpalette]] [app.main.ui.workspace.viewport :refer [viewport]] + [app.util.debug :as dbg] [app.util.dom :as dom] [app.util.globals :as globals] [app.util.i18n :as i18n :refer [tr]] - [app.util.router :as rt] - [debug :refer [debug?]] [goog.events :as events] [okulary.core :as l] [rumext.v2 :as mf])) @@ -62,7 +57,8 @@ (mf/defc workspace-content {::mf/wrap-props false} [{:keys [file layout page-id wglobal]}] - (let [selected (mf/deref refs/selected-shapes) + (let [palete-size (mf/use-state nil) + selected (mf/deref refs/selected-shapes) {:keys [vport] :as wlocal} (mf/deref refs/workspace-local) {:keys [options-mode]} wglobal @@ -70,7 +66,6 @@ colorpalette? (:colorpalette layout) textpalette? (:textpalette layout) hide-ui? (:hide-ui layout) - new-css-system (mf/use-ctx ctx/new-css-system) on-resize (mf/use-fn @@ -79,26 +74,27 @@ (when (and vport (not= size vport)) (st/emit! (dw/update-viewport-size resize-type size))))) + on-resize-palette + (mf/use-fn + (fn [size] + (reset! palete-size size))) + node-ref (use-resize-observer on-resize)] [:* - (if new-css-system - [:& palette {:layout layout}] - [:* - (when (and colorpalette? (not hide-ui?)) - [:& colorpalette]) - - (when (and textpalette? (not hide-ui?)) - [:& textpalette])]) + (when (not hide-ui?) + [:& palette {:layout layout + :on-change-palette-size on-resize-palette}]) [:section.workspace-content {:key (dm/str "workspace-" page-id) :ref node-ref} - [:section.workspace-viewport - (when (debug? :coordinates) + + [:section {:class (stl/css :workspace-viewport)} + (when (dbg/enabled? :coordinates) [:& coordinates/coordinates {:colorpalette? colorpalette?}]) - (when (debug? :history-overlay) - [:div.history-debug-overlay + (when (dbg/enabled? :history-overlay) + [:div {:class (stl/css :history-debug-overlay)} [:button {:on-click #(st/emit! dw/reinitialize-undo)} "CLEAR"] [:& history-toolbox]]) @@ -106,21 +102,27 @@ :wlocal wlocal :wglobal wglobal :selected selected - :layout layout}]]] + :layout layout + :palete-size + (when (and (or colorpalette? textpalette?) (not hide-ui?)) + @palete-size)}]]] (when-not hide-ui? [:* - [:& left-toolbar {:layout layout}] (if (:collapse-left-sidebar layout) [:& collapsed-button] - [:& left-sidebar {:layout layout}]) + [:& left-sidebar {:layout layout + :file file + :page-id page-id}]) [:& right-sidebar {:section options-mode :selected selected - :layout layout}]])])) + :layout layout + :file file + :page-id page-id}]])])) (mf/defc workspace-loader [] - [:div.workspace-loader + [:div {:class (stl/css :workspace-loader)} i/loader-pencil]) (mf/defc workspace-page @@ -170,8 +172,7 @@ (make-file-ready-ref file-id)) file-ready? (mf/deref file-ready*) - components-v2? (features/use-feature :components-v2) - new-css? (features/use-feature :new-css-system) + components-v2? (features/use-feature "components/v2") background-color (:background-color wglobal)] @@ -197,68 +198,15 @@ [:& (mf/provider ctx/current-team-id) {:value team-id} [:& (mf/provider ctx/current-page-id) {:value page-id} [:& (mf/provider ctx/components-v2) {:value components-v2?} - [:& (mf/provider ctx/new-css-system) {:value new-css?} - [:& (mf/provider ctx/workspace-read-only?) {:value read-only?} - [:section#workspace {:class (when new-css? (css :workspace)) - :style {:background-color background-color - :touch-action "none"}} - (when (not (:hide-ui layout)) - [:& header {:file file - :page-id page-id - :project project - :layout layout}]) + [:& (mf/provider ctx/workspace-read-only?) {:value read-only?} + [:section#workspace-refactor {:class (stl/css :workspace) + :style {:background-color background-color + :touch-action "none"}} + [:& context-menu] - [:& context-menu] - - (if ^boolean file-ready? - [:& workspace-page {:page-id page-id - :file file - :wglobal wglobal - :layout layout}] - [:& workspace-loader])]]]]]]]])) - -(mf/defc remove-graphics-dialog - {::mf/register modal/components - ::mf/register-as :remove-graphics-dialog} - [{:keys [] :as ctx}] - (let [remove-state (mf/deref refs/remove-graphics) - project (mf/deref refs/workspace-project) - close #(modal/hide!) - reload-file #(dom/reload-current-window) - nav-out #(st/emit! (rt/navigate :dashboard-files - {:team-id (:team-id project) - :project-id (:id project)}))] - (mf/use-effect - (fn [] - #(st/emit! (dw/clear-remove-graphics)))) - - [:div.modal-overlay - [:div.modal-container.remove-graphics-dialog - [:div.modal-header - [:div.modal-header-title - [:h2 (tr "workspace.remove-graphics.title" (:file-name ctx))]] - (if (and (:completed remove-state) (:error remove-state)) - [:div.modal-close-button - {:on-click close} i/close] - [:div.modal-close-button - {:on-click nav-out} - i/close])] - (if-not (and (:completed remove-state) (:error remove-state)) - [:div.modal-content - [:p (tr "workspace.remove-graphics.text1")] - [:p (tr "workspace.remove-graphics.text2")] - [:p.progress-message (tr "workspace.remove-graphics.progress" - (:current remove-state) - (:total remove-state))]] - [:* - [:div.modal-content - [:p.error-message [:span i/close] (tr "workspace.remove-graphics.error-msg")] - [:p (tr "workspace.remove-graphics.error-hint")]] - [:div.modal-footer - [:div.action-buttons - [:input.button-secondary {:type "button" - :value (tr "labels.close") - :on-click close}] - [:input.button-primary {:type "button" - :value (tr "labels.reload-file") - :on-click reload-file}]]]])]])) + (if ^boolean file-ready? + [:& workspace-page {:page-id page-id + :file file + :wglobal wglobal + :layout layout}] + [:& workspace-loader])]]]]]]])) diff --git a/frontend/src/app/main/ui/workspace.css.json b/frontend/src/app/main/ui/workspace.css.json deleted file mode 100644 index f37e8f34fe..0000000000 --- a/frontend/src/app/main/ui/workspace.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"ui_workspace_button-primary_FZJ-T","button-secondary":"ui_workspace_button-secondary_oDzCJ","button-icon":"ui_workspace_button-icon_L5y8h","button-icon-small":"ui_workspace_button-icon-small_Ppp3W","workspace":"ui_workspace_workspace_xutJr"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace.scss b/frontend/src/app/main/ui/workspace.scss index 251aa975a0..0cc517eae5 100644 --- a/frontend/src/app/main/ui/workspace.scss +++ b/frontend/src/app/main/ui/workspace.scss @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + @import "refactor/common-refactor.scss"; // Work Sans @@ -6,33 +12,53 @@ @include font-face("worksans", "WorkSans-Bold", bold); // Space mono -@include font-face("spacemono", "SpaceMono-Regular", normal); +@include font-face("robotomono", "RobotoMono-Regular", normal); :global(:root) { --s-4: 0.25rem; - --layer-indentation-size: calc(var(--s-4) * 5); + --layer-indentation-size: calc(var(--s-4) * 6); } .workspace { - ::-webkit-scrollbar { - background-color: transparent; - cursor: pointer; - height: $s-12; - width: $s-12; - } - ::-webkit-scrollbar-track, - ::-webkit-scrollbar-corner { - background-color: transparent; - } + @extend .new-scrollbar; + width: 100vw; + height: 100vh; + max-height: 100vh; + user-select: none; + display: grid; + grid-template-areas: "left-sidebar viewport right-sidebar"; + grid-template-rows: 1fr; + grid-template-columns: auto 1fr auto; + overflow: hidden; - ::-webkit-scrollbar-thumb { - background-color: rgba(170, 181, 186, 0.3); - background-clip: content-box; - border: $s-2 solid transparent; - border-radius: $br-8; - &:hover { - background-color: rgba(170, 181, 186, 0.7); - outline: none; + .workspace-loader { + @include flexCenter; + grid-area: viewport; + background-color: var(--loader-background); + :global(svg#loader-pencil) { + fill: var(--icon-foreground); } } } + +.workspace-content { + grid-area: viewport; +} + +.history-debug-overlay { + bottom: 0; + max-height: $s-500; + width: $s-500; + overflow-y: auto; + position: absolute; + z-index: $z-index-modal; +} + +.workspace-viewport { + overflow: hidden; + transition: none; + display: grid; + grid-template-rows: $s-20 1fr; + grid-template-columns: $s-20 1fr; + flex: 1; +} diff --git a/frontend/src/app/main/ui/workspace/color_palette.cljs b/frontend/src/app/main/ui/workspace/color_palette.cljs index b4db61613c..b48eb89dc3 100644 --- a/frontend/src/app/main/ui/workspace/color_palette.cljs +++ b/frontend/src/app/main/ui/workspace/color_palette.cljs @@ -5,17 +5,16 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.color-palette - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] [app.main.data.workspace.colors :as mdc] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.color-bullet-new :as cb] + [app.main.ui.components.color-bullet :as cb] [app.main.ui.hooks :as h] [app.main.ui.icons :as i] [app.util.color :as uc] - [app.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.object :as obj] @@ -26,19 +25,19 @@ [{:keys [color size]}] (letfn [(select-color [event] (st/emit! (mdc/apply-color-from-palette color (kbd/alt? event))))] - [:div {:class (dom/classnames (css :color-cell) true - (css :is-not-library-color) (nil? (:id color)) - (css :no-text) (<= size 64)) + [:div {:class (stl/css-case :color-cell true + :is-not-library-color (nil? (:id color)) + :no-text (<= size 64)) :title (uc/get-color-name color) :on-click select-color} [:& cb/color-bullet {:color color}] - [:& cb/color-name {:color color :size size}]])) + [:& cb/color-name {:color color :size size :origin :palette}]])) (mf/defc palette [{:keys [current-colors size width]}] (let [;; We had to do this due to a bug that leave some bugged colors - current-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %)) current-colors)) + current-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %) (:image %)) current-colors)) state (mf/use-state {:show-menu false}) offset-step (cond (<= size 64) 40 @@ -102,34 +101,34 @@ (when (not= 0 (:offset @state)) (swap! state assoc :offset 0))) - [:div {:class (dom/classnames (css :color-palette) true - (css :no-text) (< size 64)) + [:div {:class (stl/css-case :color-palette true + :no-text (< size 64)) :style #js {"--bullet-size" (dm/str bullet-size "px")}} (when show-arrows? - [:button {:class (dom/classnames (css :left-arrow) true) + [:button {:class (stl/css :left-arrow) :disabled (= offset 0) - :on-click on-left-arrow-click} i/arrow-refactor]) - [:div {:class (dom/classnames (css :color-palette-content) true) + :on-click on-left-arrow-click} i/arrow]) + [:div {:class (stl/css :color-palette-content) :ref container :on-wheel on-scroll} (if (empty? current-colors) - [:div {:class (dom/classnames (css :color-palette-empty) true) + [:div {:class (stl/css :color-palette-empty) :style {:position "absolute" :left "50%" :top "50%" :transform "translate(-50%, -50%)"}} (tr "workspace.libraries.colors.empty-palette")] - [:div {:class (dom/classnames (css :color-palette-inside) true) + [:div {:class (stl/css :color-palette-inside) :style {:position "relative" :max-width (str width "px") :right (str (* offset-step offset) "px")}} (for [[idx item] (map-indexed vector current-colors)] [:& palette-item {:color item :key idx :size size}])])] (when show-arrows? - [:button {:class (dom/classnames (css :right-arrow) true) + [:button {:class (stl/css :right-arrow) :disabled (= offset max-offset) - :on-click on-right-arrow-click} i/arrow-refactor])])) + :on-click on-right-arrow-click} i/arrow])])) (defn library->colors [shared-libs selected] (map #(merge % {:file-id selected}) diff --git a/frontend/src/app/main/ui/workspace/color_palette.css.json b/frontend/src/app/main/ui/workspace/color_palette.css.json deleted file mode 100644 index f8fedfc2bf..0000000000 --- a/frontend/src/app/main/ui/workspace/color_palette.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"workspace_color_palette_button-primary_0d2e2","button-secondary":"workspace_color_palette_button-secondary_C8qJL","button-icon":"workspace_color_palette_button-icon_-tBR6","color-palette":"workspace_color_palette_color-palette_hfJPA","left-arrow":"workspace_color_palette_left-arrow_PK7sj","right-arrow":"workspace_color_palette_right-arrow_swpS9","button-icon-small":"workspace_color_palette_button-icon-small_RrGTg","disabled":"workspace_color_palette_disabled_bz-he","color-palette-content":"workspace_color_palette_color-palette-content_okg18","color-palette-inside":"workspace_color_palette_color-palette-inside_dCIeR","color-cell":"workspace_color_palette_color-cell_ITDgl","is-not-library-color":"workspace_color_palette_is-not-library-color_EqCM6","no-text":"workspace_color_palette_no-text_QMPK0"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/color_palette.scss b/frontend/src/app/main/ui/workspace/color_palette.scss index cb3a2b46ac..4815dee3e2 100644 --- a/frontend/src/app/main/ui/workspace/color_palette.scss +++ b/frontend/src/app/main/ui/workspace/color_palette.scss @@ -9,88 +9,94 @@ .color-palette { height: 100%; display: flex; +} - .left-arrow, - .right-arrow { - @include buttonStyle; - @include flexCenter; - position: relative; +.left-arrow, +.right-arrow { + @include buttonStyle; + @include flexCenter; + position: relative; + height: 100%; + width: $s-24; + padding: 0; + z-index: $z-index-5; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + &::after { + content: ""; + position: absolute; + z-index: $z-index-1; + bottom: 0; + left: calc(-1 * $s-80); height: 100%; - width: $s-24; - padding: 0; - z-index: $z-index-4; + width: $s-80; + background-image: linear-gradient( + to left, + var(--palette-button-shadow-initial) 0%, + var(--palette-button-shadow-final) 100% + ); + pointer-events: none; + } + &:hover { svg { - @extend .button-icon; - } - &::after { - content: ""; - position: absolute; - z-index: $z-index-1; - bottom: 0; - left: calc(-1 * $s-80); - height: 100%; - width: $s-80; - background-image: linear-gradient( - to left, - var(--palette-button-shadow-initial) 0%, - var(--palette-button-shadow-final) 100% - ); - pointer-events: none; - } - &:hover { - svg { - stroke: var(--button-foreground-hover); - } - } - &:disabled { - svg { - stroke: var(--button-foreground-color-disabled); - } - &::after { - background-image: none; - } + stroke: var(--button-foreground-hover); } } - .left-arrow { - &::after { - left: $s-24; - background-image: linear-gradient( - to right, - var(--palette-button-shadow-initial) 0%, - var(--palette-button-shadow-final) 100% - ); + &:disabled { + svg { + stroke: var(--button-foreground-color-disabled); } - &.disabled ::after { + &::after { background-image: none; } - - svg { - transform: rotate(180deg); - } - } - - .color-palette-content { - display: flex; - overflow: hidden; - - .color-palette-inside { - display: flex; - gap: $s-8; - } - .color-cell { - display: grid; - grid-template-columns: 100%; - grid-template-rows: auto 1fr; - justify-items: center; - height: 100%; - width: $s-80; - &.is-not-library-color { - width: $s-64; - } - &.no-text { - @include flexCenter; - width: $s-32; - } - } } } +.left-arrow { + &::after { + left: $s-24; + background-image: linear-gradient( + to right, + var(--palette-button-shadow-initial) 0%, + var(--palette-button-shadow-final) 100% + ); + } + &.disabled ::after { + background-image: none; + } + + svg { + transform: rotate(180deg); + } +} + +.color-palette-content { + display: flex; + overflow: hidden; + + .color-palette-inside { + display: flex; + gap: $s-8; + } + .color-cell { + display: grid; + grid-template-columns: 100%; + grid-template-rows: auto 1fr; + justify-items: center; + height: 100%; + width: $s-80; + &.is-not-library-color { + width: $s-64; + } + &.no-text { + @include flexCenter; + width: $s-32; + } + } +} + +.color-palette-empty { + @include bodySmallTypography; + color: var(--palette-text-color); +} diff --git a/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.cljs b/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.cljs index 5780cbb27e..17dbb9450f 100644 --- a/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.cljs +++ b/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.cljs @@ -5,16 +5,14 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.color-palette-ctx-menu - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] [app.main.refs :as refs] - [app.main.ui.components.color-bullet-new :as cb] + [app.main.ui.components.color-bullet :as cb] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.icons :as i] - [app.util.dom :as dom] [app.util.i18n :refer [tr]] - [cuerdas.core :as str] [rumext.v2 :as mf])) (mf/defc color-palette-ctx-menu @@ -24,61 +22,71 @@ shared-libs (mf/deref refs/workspace-libraries)] [:& dropdown {:show show-menu? :on-close close-menu} - [:ul {:class (dom/classnames (css :palette-menu) true)} + [:ul {:class (stl/css :palette-menu)} (for [{:keys [data id] :as library} (vals shared-libs)] (let [colors (-> data :colors vals)] - [:li - {:class (dom/classnames (css :palette-library) true - (css :selected) (= selected id)) - :key (dm/str "library-" id) - :on-click on-select-palette - :data-palette (dm/str id)} - [:div {:class (dom/classnames (css :option-wrapper) true)} - [:div {:class (dom/classnames (css :library-name) true)} - (str (:name library) " " (str/ffmt "(%)" (count colors))) + [:li {:class (stl/css-case :palette-library true + :selected (= selected id)) + :key (dm/str "library-" id) + :on-click on-select-palette + :data-palette (dm/str id)} + [:div {:class (stl/css :option-wrapper)} + [:div {:class (stl/css :library-name)} + [:div {:class (stl/css :lib-name-wrapper)} + [:span {:class (stl/css :lib-name)} + (dm/str (:name library))] + [:span {:class (stl/css :lib-num)} + (dm/str "(" (count colors) ")")]] (when (= selected id) - [:span {:class (dom/classnames (css :icon-wrapper) true)} - i/tick-refactor])] - [:div {:class (dom/classnames (css :color-sample) true) + [:span {:class (stl/css :icon-wrapper)} + i/tick])] + [:div {:class (stl/css :color-sample) :style #js {"--bullet-size" "20px"}} (for [[i {:keys [color id gradient]}] (map-indexed vector (take 7 colors))] [:& cb/color-bullet {:key (dm/str "color-" i) :mini? true :color {:color color :id id :gradient gradient}}])]]])) - [:li {:class (dom/classnames (css :file-library) true - (css :selected) (= selected :file)) + [:li {:class (stl/css-case :file-library true + :selected (= selected :file)) :on-click on-select-palette :data-palette "file"} - [:div {:class (dom/classnames (css :option-wrapper) true)} - [:div {:class (dom/classnames (css :library-name) true)} - (dm/str - (tr "workspace.libraries.colors.file-library") - (str/ffmt " (%)" (count file-colors))) + [:div {:class (stl/css :option-wrapper)} + [:div {:class (stl/css :library-name)} + + [:div {:class (stl/css :lib-name-wrapper)} + [:span {:class (stl/css :lib-name)} + (dm/str (tr "workspace.libraries.colors.file-library"))] + [:span {:class (stl/css :lib-num)} + (dm/str "(" (count file-colors) ")")]] (when (= selected :file) - [:span {:class (dom/classnames (css :icon-wrapper) true)} - i/tick-refactor])] - [:div {:class (dom/classnames (css :color-sample) true) + [:span {:class (stl/css :icon-wrapper)} + i/tick])] + [:div {:class (stl/css :color-sample) :style #js {"--bullet-size" "20px"}} (for [[i color] (map-indexed vector (take 7 (vals file-colors)))] [:& cb/color-bullet {:key (dm/str "color-" i) :mini? true :color color}])]]] - [:li {:class (dom/classnames (css :recent-colors) true - (css :selected) (= selected :recent)) + [:li {:class (stl/css :recent-colors true + :selected (= selected :recent)) :on-click on-select-palette :data-palette "recent"} - [:div {:class (dom/classnames (css :option-wrapper) true)} - [:div {:class (dom/classnames (css :library-name) true)} - (str (tr "workspace.libraries.colors.recent-colors") - (str/format " (%s)" (count recent-colors))) + [:div {:class (stl/css :option-wrapper)} + [:div {:class (stl/css :library-name)} + [:div {:class (stl/css :lib-name-wrapper)} + [:span {:class (stl/css :lib-name)} + (dm/str (tr "workspace.libraries.colors.recent-colors"))] + [:span {:class (stl/css :lib-num)} + (dm/str "(" (count recent-colors) ")")]] + (when (= selected :recent) - [:span {:class (dom/classnames (css :icon-wrapper) true)} - i/tick-refactor])] - [:div {:class (dom/classnames (css :color-sample) true) + [:span {:class (stl/css :icon-wrapper)} + i/tick])] + [:div {:class (stl/css :color-sample) :style #js {"--bullet-size" "20px"}} (for [[idx color] (map-indexed vector (take 7 (reverse recent-colors)))] [:& cb/color-bullet {:key (str "color-" idx) diff --git a/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.css.json b/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.css.json deleted file mode 100644 index a3d1913bbc..0000000000 --- a/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"workspace_color_palette_ctx_menu_button-primary_2ka4z","button-secondary":"workspace_color_palette_ctx_menu_button-secondary_jfajf","button-icon":"workspace_color_palette_ctx_menu_button-icon_cCaY2","button-icon-small":"workspace_color_palette_ctx_menu_button-icon-small_-knT4","palette-menu":"workspace_color_palette_ctx_menu_palette-menu_Vrjfy","palette-library":"workspace_color_palette_ctx_menu_palette-library_0LFV5","selected":"workspace_color_palette_ctx_menu_selected_lfchf","icon-wrapper":"workspace_color_palette_ctx_menu_icon-wrapper_v8-ys","recent-colors":"workspace_color_palette_ctx_menu_recent-colors_Q4fss","file-library":"workspace_color_palette_ctx_menu_file-library_8qsbr","option-wrapper":"workspace_color_palette_ctx_menu_option-wrapper_st9Cq","library-name":"workspace_color_palette_ctx_menu_library-name_BL8b8","color-sample":"workspace_color_palette_ctx_menu_color-sample_jQUGL"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss b/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss index b18d453140..920d6031ba 100644 --- a/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss +++ b/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss @@ -9,10 +9,11 @@ .palette-menu { position: absolute; left: auto; - bottom: $s-0; + bottom: var(--height); + max-width: $s-480; padding: $s-4; margin: 0 0 $s-4 0; - z-index: $z-index-10; + z-index: $z-index-4; border-radius: $br-10; background-color: var(--context-menu-background-color); @@ -22,6 +23,7 @@ position: relative; display: flex; align-items: flex-start; + width: 100%; padding: $s-8; border-radius: $br-8; margin-bottom: $s-4; @@ -29,13 +31,33 @@ margin-bottom: 0; } .option-wrapper { + width: 100%; .library-name { - @include titleTipography; + @include bodySmallTypography; color: var(--context-menu-foreground-color); - display: flex; - justify-content: space-between; + display: grid; + grid-template-columns: 1fr $s-24; + .lib-name-wrapper { + display: flex; + max-width: $s-400; + .lib-name { + @include textEllipsis; + max-width: $s-380; + } + .lib-num { + margin-left: $s-4; + } + } + .icon-wrapper { + margin-left: $s-4; + @include flexCenter; + svg { + @extend .button-icon-small; + @include flexCenter; + stroke: var(--icon-foreground); + } + } } - .color-sample { display: flex; flex-direction: row; @@ -46,15 +68,16 @@ &.selected, &:hover { - .icon-wrapper { - @include flexCenter; - svg { - @include flexCenter; - @extend .button-icon-small; - } - } .option-wrapper .library-name { color: var(--context-menu-foreground-color-selected); + .icon-wrapper { + @include flexCenter; + svg { + @include flexCenter; + @extend .button-icon-small; + stroke: var(--context-menu-foreground-color-selected); + } + } } } diff --git a/frontend/src/app/main/ui/workspace/colorpalette.cljs b/frontend/src/app/main/ui/workspace/colorpalette.cljs deleted file mode 100644 index cdc29cffde..0000000000 --- a/frontend/src/app/main/ui/workspace/colorpalette.cljs +++ /dev/null @@ -1,213 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.workspace.colorpalette - (:require - [app.common.data.macros :as dm] - [app.main.data.workspace.colors :as mdc] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.components.color-bullet :as cb] - [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.hooks :as h] - [app.main.ui.hooks.resize :refer [use-resize-hook]] - [app.main.ui.icons :as i] - [app.util.dom :as dom] - [app.util.i18n :refer [tr]] - [app.util.keyboard :as kbd] - [app.util.object :as obj] - [cuerdas.core :as str] - [goog.events :as events] - [rumext.v2 :as mf])) - -;; --- Components - -(mf/defc palette-item - {::mf/wrap [mf/memo]} - [{:keys [color]}] - (letfn [(select-color [event] - (st/emit! (mdc/apply-color-from-palette color (kbd/alt? event))))] - [:div.color-cell {:on-click select-color} - [:& cb/color-bullet {:color color}] - [:& cb/color-name {:color color}]])) - -(mf/defc palette - [{:keys [current-colors recent-colors file-colors shared-libs selected on-select]}] - (let [;; We had to do this due to a bug that leave some bugged colors - current-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %)) current-colors)) - state (mf/use-state {:show-menu false}) - - width (:width @state 0) - visible (/ width 66) - - offset (:offset @state 0) - max-offset (- (count current-colors) - visible) - - container (mf/use-ref nil) - - {:keys [on-pointer-down on-lost-pointer-capture on-pointer-move parent-ref size]} - (use-resize-hook :palette 72 54 80 :y true :bottom) - - on-left-arrow-click - (mf/use-callback - (mf/deps max-offset visible) - (fn [_] - (swap! state update :offset - (fn [offset] - (if (pos? offset) - (max (- offset (/ visible 2)) 0) - offset))))) - - on-right-arrow-click - (mf/use-callback - (mf/deps max-offset visible) - (fn [_] - (swap! state update :offset - (fn [offset] - (if (< offset max-offset) - (min max-offset (+ offset (/ visible 2))) - offset))))) - - on-scroll - (mf/use-callback - (mf/deps max-offset) - (fn [event] - (let [delta (+ (.. event -nativeEvent -deltaY) (.. event -nativeEvent -deltaX))] - (if (pos? delta) - (on-right-arrow-click event) - (on-left-arrow-click event))))) - - on-resize - (mf/use-callback - (fn [_] - (let [dom (mf/ref-val container) - width (obj/get dom "clientWidth")] - (swap! state assoc :width width)))) - - on-select-palette - (mf/use-fn - (mf/deps on-select) - (fn [event] - (let [node (dom/get-current-target event) - value (dom/get-attribute node "data-palette")] - (on-select (if (or (= "file" value) (= "recent" value)) - (keyword value) - (parse-uuid value))))))] - - (mf/use-layout-effect - #(let [dom (mf/ref-val container) - width (obj/get dom "clientWidth")] - (swap! state assoc :width width))) - - (mf/with-effect [] - (let [key1 (events/listen js/window "resize" on-resize)] - #(events/unlistenByKey key1))) - - [:div.color-palette {:ref parent-ref - :class (dom/classnames :no-text (< size 72)) - :style #js {"--height" (dm/str size "px") - "--bullet-size" (dm/str (if (< size 72) (- size 15) (- size 30)) "px")}} - [:div.resize-area {:on-pointer-down on-pointer-down - :on-lost-pointer-capture on-lost-pointer-capture - :on-pointer-move on-pointer-move}] - [:& dropdown {:show (:show-menu @state) - :on-close #(swap! state assoc :show-menu false)} - [:ul.workspace-context-menu.palette-menu - (for [{:keys [data id] :as library} (vals shared-libs)] - (let [colors (->> data :colors vals (sort-by :name))] - [:li.palette-library - {:key (dm/str "library-" id) - :on-click on-select-palette - :data-palette (dm/str id)} - (when (= selected id) i/tick) - [:div.library-name (str (:name library) " " (str/ffmt "(%)" (count colors)))] - [:div.color-sample - (for [[i {:keys [color]}] (map-indexed vector (take 7 colors))] - [:& cb/color-bullet {:key (dm/str "color-" i) - :color color}])]])) - - [:li.palette-library - {:on-click on-select-palette - :data-palette "file"} - (when (= selected :file) i/tick) - [:div.library-name (dm/str - (tr "workspace.libraries.colors.file-library") - (str/ffmt " (%)" (count file-colors)))] - [:div.color-sample - (for [[i color] (map-indexed vector (take 7 (->> (vals file-colors) (sort-by :name))))] - [:& cb/color-bullet {:key (dm/str "color-" i) - :color color}])]] - - [:li.palette-library - {:on-click on-select-palette - :data-palette "recent"} - (when (= selected :recent) i/tick) - [:div.library-name (str (tr "workspace.libraries.colors.recent-colors") - (str/format " (%s)" (count recent-colors)))] - [:div.color-sample - (for [[idx color] (map-indexed vector (take 7 (reverse recent-colors)))] - [:& cb/color-bullet {:key (str "color-" idx) - :color color}])]]]] - - [:div.color-palette-actions - {:on-click #(swap! state assoc :show-menu true)} - [:div.color-palette-actions-button i/actions]] - - [:span.left-arrow {:on-click on-left-arrow-click} i/arrow-slide] - [:div.color-palette-content {:ref container :on-wheel on-scroll} - (if (empty? current-colors) - [:div.color-palette-empty {:style {:position "absolute" - :left "50%" - :top "50%" - :transform "translate(-50%, -50%)"}} - (tr "workspace.libraries.colors.empty-palette")] - [:div.color-palette-inside {:style {:position "relative" - :right (str (* 66 offset) "px")}} - (for [[idx item] (map-indexed vector current-colors)] - [:& palette-item {:color item :key idx}])])] - - [:span.right-arrow {:on-click on-right-arrow-click} i/arrow-slide]])) - -(defn library->colors [shared-libs selected] - (map #(merge % {:file-id selected}) - (-> shared-libs - (get-in [selected :data :colors]) - (vals)))) - -(mf/defc colorpalette - {::mf/wrap [mf/memo]} - [] - (let [recent-colors (mf/deref refs/workspace-recent-colors) - file-colors (mf/deref refs/workspace-file-colors) - shared-libs (mf/deref refs/workspace-libraries) - selected (h/use-shared-state mdc/colorpalette-selected-broadcast-key :recent) - - colors (mf/use-state []) - on-select (mf/use-fn #(reset! selected %))] - - (mf/with-effect [@selected] - (let [colors' (cond - (= @selected :recent) (reverse recent-colors) - (= @selected :file) (->> (vals file-colors) (sort-by :name)) - :else (->> (library->colors shared-libs @selected) (sort-by :name)))] - (reset! colors (into [] colors')))) - - (mf/with-effect [recent-colors @selected] - (when (= @selected :recent) - (reset! colors (reverse recent-colors)))) - - (mf/with-effect [file-colors @selected] - (when (= @selected :file) - (reset! colors (into [] (->> (vals file-colors) - (sort-by :name)))))) - - [:& palette {:current-colors @colors - :recent-colors recent-colors - :file-colors file-colors - :shared-libs shared-libs - :selected @selected - :on-select on-select}])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index 9b912afd5a..bfcb839dbc 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -5,13 +5,23 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.colorpicker + (:require-macros [app.main.style :as stl]) (:require + [app.common.colors :as cc] + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.config :as cfg] + [app.main.data.events :as-alias ev] [app.main.data.modal :as modal] [app.main.data.workspace.colors :as dc] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.media :as dwm] [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.file-uploader :refer [file-uploader]] + [app.main.ui.components.select :refer [select]] + [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.icons :as i] [app.main.ui.workspace.colorpicker.color-inputs :refer [color-inputs]] [app.main.ui.workspace.colorpicker.gradients :refer [gradients]] @@ -19,11 +29,11 @@ [app.main.ui.workspace.colorpicker.hsva :refer [hsva-selector]] [app.main.ui.workspace.colorpicker.libraries :refer [libraries]] [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector]] - [app.util.color :as uc] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [cuerdas.core :as str] [okulary.core :as l] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) ;; --- Refs @@ -43,30 +53,86 @@ ;; --- Color Picker Modal (mf/defc colorpicker - [{:keys [data disable-gradient disable-opacity on-change on-accept]}] - (let [state (mf/deref refs/colorpicker) - node-ref (mf/use-ref) + {::mf/props :obj} + [{:keys [data disable-gradient disable-opacity disable-image on-change on-accept]}] + (let [state (mf/deref refs/colorpicker) + node-ref (mf/use-ref) ;; TODO: I think we need to put all this picking state under ;; the same object for avoid creating adhoc refs for each ;; value - picking-color? (mf/deref picking-color?) - picked-color (mf/deref picked-color) - picked-color-select (mf/deref picked-color-select) + picking-color? (mf/deref picking-color?) + picked-color (mf/deref picked-color) + picked-color-select (mf/deref picked-color-select) - current-color (:current-color state) + current-color (:current-color state) - active-tab (mf/use-state (dc/get-active-color-tab)) - drag? (mf/use-state false) - - set-tab! + active-fill-tab (if (:image data) + :image + (if-let [gradient (:gradient data)] + (case (:type gradient) + :linear :linear-gradient + :radial :radial-gradient) + :color)) + active-color-tab (mf/use-state (dc/get-active-color-tab)) + drag? (mf/use-state false) + + fill-image-ref (mf/use-ref nil) + + selected-mode (get state :type :color) + + disabled-color-accept? (and + (= selected-mode :image) + (not (:image current-color))) + + on-fill-image-success (mf/use-fn - (fn [event] - (let [tab (-> (dom/get-current-target event) - (dom/get-data "tab") - (keyword))] - (reset! active-tab tab) - (dc/set-active-color-tab! tab)))) + (fn [image] + (st/emit! (dc/update-colorpicker-color + {:image (-> (select-keys image [:id :width :height :mtype :name]) + (assoc :keep-aspect-ratio true))} + (not @drag?))))) + + on-fill-image-click + (mf/use-fn #(dom/click (mf/ref-val fill-image-ref))) + + on-fill-image-selected + (mf/use-fn + (fn [file] + (st/emit! (dwm/upload-fill-image file on-fill-image-success)))) + + handle-change-keep-aspect-ratio + (mf/use-fn + (mf/deps current-color) + (fn [] + (let [keep-aspect-ratio? (-> current-color :image :keep-aspect-ratio not) + image (-> (:image current-color) + (assoc :keep-aspect-ratio keep-aspect-ratio?))] + + + (st/emit! + (dc/update-colorpicker-color {:image image} true) + (ptk/data-event ::ev/event {::ev/name "toggle-image-aspect-ratio" + ::ev/origin "workspace:colorpicker" + :checked keep-aspect-ratio?}))))) + + on-change-tab + (mf/use-fn + (fn [event] + (let [tab (-> (dom/get-current-target event) + (dom/get-data "tab") + (keyword))] + (reset! active-color-tab tab) + (dc/set-active-color-tab! tab)))) + + handle-change-mode + (mf/use-fn + (fn [value] + (case value + :color (st/emit! (dc/activate-colorpicker-color)) + :linear-gradient (st/emit! (dc/activate-colorpicker-gradient :linear-gradient)) + :radial-gradient (st/emit! (dc/activate-colorpicker-gradient :radial-gradient)) + :image (st/emit! (dc/activate-colorpicker-image))))) handle-change-color (mf/use-fn @@ -86,55 +152,27 @@ (fn [] (if picking-color? (do (modal/disallow-click-outside!) - (st/emit! (dc/stop-picker))) + (st/emit! (dc/stop-picker))) (do (modal/allow-click-outside!) - (st/emit! (dc/start-picker)))))) + (st/emit! (dc/start-picker)))))) handle-change-stop (mf/use-fn - (fn [offset] - (st/emit! (dc/select-colorpicker-gradient-stop offset)))) + (fn [event] + (let [offset (-> (dom/get-current-target event) + (dom/get-data "value") + (d/parse-integer))] + (st/emit! (dc/select-colorpicker-gradient-stop offset))))) on-select-library-color (mf/use-fn - (fn [state color] - (let [type-origin (:type state) - editig-stop-origin (:editing-stop state) - is-gradient? (some? (:gradient color)) - change-to (fn [new-color] - (st/emit! (dc/update-colorpicker new-color)) - (on-change new-color)) - clean-stop (fn [stops index color] - (-> (nth stops index) - (merge color) - (assoc :offset index) - (dissoc :r) - (dissoc :g) - (dissoc :b) - (dissoc :alpha) - (dissoc :s) - (dissoc :h) - (dissoc :v) - (dissoc :hex))) - set-new-gradient (fn [state color index] - (let [old-stops (:stops state) - old-gradient (:gradient state) - new-gradient (-> old-gradient - (cond-> (= index 0) (assoc :stops [(clean-stop old-stops 0 color) (nth old-stops 1)])) - (cond-> (= index 1) (assoc :stops [(nth old-stops 0) (clean-stop old-stops 1 color)])) - (dissoc :shape-id))] - (change-to {:gradient new-gradient})))] - ;; If we have any kind of gradient and: - ;; Click on a solid color -> This color is applied to the selected offset - ;; Click on a color with transparency -> The same to solid color will happend - ;; Click on any kind of gradient -> The color changes completly to new gradient - - ;; If we have a non gradient color the new color is applied without any change - (if (or (= :radial-gradient type-origin) (= :linear-gradient type-origin)) - (if is-gradient? - (change-to color) - (set-new-gradient state color editig-stop-origin)) - (change-to color))))) + (mf/deps data handle-change-color) + (fn [_ color] + (if (and (some? (:color color)) (some? (:gradient data))) + (handle-change-color {:hex (:color color) :alpha (:opacity color)}) + (do + (st/emit! (dc/apply-color-from-colorpicker color)) + (on-change color))))) on-add-library-color (mf/use-fn @@ -142,27 +180,40 @@ (fn [_] (st/emit! (dwl/add-color (dc/get-color-from-colorpicker-state state))))) - on-activate-linear-gradient - (mf/use-fn #(st/emit! (dc/activate-colorpicker-gradient :linear-gradient))) - - on-activate-radial-gradient - (mf/use-fn #(st/emit! (dc/activate-colorpicker-gradient :radial-gradient))) - on-start-drag (mf/use-fn + (mf/deps drag? node-ref) (fn [] (reset! drag? true) (st/emit! (dwu/start-undo-transaction (mf/ref-val node-ref))))) on-finish-drag (mf/use-fn + (mf/deps drag? node-ref) (fn [] (reset! drag? false) - (st/emit! (dwu/commit-undo-transaction (mf/ref-val node-ref)))))] + (st/emit! (dwu/commit-undo-transaction (mf/ref-val node-ref))))) + + on-color-accept + (mf/use-fn + (mf/deps state) + (fn [] + (on-accept (dc/get-color-from-colorpicker-state state)) + (modal/hide!))) + + options + (mf/with-memo [selected-mode disable-gradient disable-image] + (d/concat-vec + [{:value :color :label (tr "media.solid")}] + (when (not disable-gradient) + [{:value :linear-gradient :label (tr "media.linear")} + {:value :radial-gradient :label (tr "media.radial")}]) + (when (not disable-image) + [{:value :image :label (tr "media.image")}])))] ;; Initialize colorpicker state (mf/with-effect [] - (st/emit! (dc/initialize-colorpicker on-change)) + (st/emit! (dc/initialize-colorpicker on-change active-fill-tab)) (partial st/emit! (dc/finalize-colorpicker))) ;; Update colorpicker with external color changes @@ -174,9 +225,9 @@ (let [node (mf/ref-val node-ref) {:keys [r g b h v]} current-color rgb [r g b] - hue-rgb (uc/hsv->rgb [h 1.0 255]) - hsl-from (uc/hsv->hsl [h 0.0 v]) - hsl-to (uc/hsv->hsl [h 1.0 v]) + hue-rgb (cc/hsv->rgb [h 1.0 255]) + hsl-from (cc/hsv->hsl [h 0.0 v]) + hsl-to (cc/hsv->hsl [h 1.0 v]) format-hsl (fn [[h s l]] (str/fmt "hsl(%s, %s, %s)" @@ -188,156 +239,216 @@ (dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from)) (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)))) - ;; Updates color when used el pixel picker + ;; Updates color when pixel picker is used (mf/with-effect [picking-color? picked-color picked-color-select] (when (and picking-color? picked-color picked-color-select) (let [[r g b alpha] picked-color - hex (uc/rgb->hex [r g b]) - [h s v] (uc/hex->hsv hex)] + hex (cc/rgb->hex [r g b]) + [h s v] (cc/hex->hsv hex)] (handle-change-color {:hex hex :r r :g g :b b :h h :s s :v v :alpha (/ alpha 255)})))) - [:div.colorpicker {:ref node-ref - :style {:touch-action "none"}} - [:div.colorpicker-content - [:div.top-actions - [:button.picker-btn - {:class (when picking-color? "active") - :on-click handle-click-picker} - i/picker] + [:div {:class (stl/css :colorpicker) + :ref node-ref + :style {:touch-action "none"}} + [:div {:class (stl/css :top-actions)} + (when (or (not disable-gradient) (not disable-image)) + [:div {:class (stl/css :select)} + [:& select + {:default-value selected-mode + :options options + :on-change handle-change-mode}]]) + (when (not= selected-mode :image) + [:button {:class (stl/css-case :picker-btn true + :selected picking-color?) + :on-click handle-click-picker} + i/picker])] - (when (not disable-gradient) - [:div.gradients-buttons - [:button.gradient.linear-gradient - {:on-click on-activate-linear-gradient - :class (when (= :linear-gradient (:type state)) "active")}] + (when (or (= selected-mode :linear-gradient) + (= selected-mode :radial-gradient)) + [:& gradients + {:stops (:stops state) + :editing-stop (:editing-stop state) + :on-select-stop handle-change-stop}]) - [:button.gradient.radial-gradient - {:on-click on-activate-radial-gradient - :class (when (= :radial-gradient (:type state)) "active")}]])] + (if (= selected-mode :image) + (let [uri (cfg/resolve-file-media (:image current-color)) + keep-aspect-ratio? (-> current-color :image :keep-aspect-ratio)] + [:div {:class (stl/css :select-image)} + [:div {:class (stl/css :content)} + (when (:image current-color) + [:img {:src uri}])] + (when (some? (:image current-color)) + [:div {:class (stl/css :checkbox-option)} + [:label {:for "keep-aspect-ratio" + :class (stl/css-case :global/checked keep-aspect-ratio?)} + [:span {:class (stl/css-case :global/checked keep-aspect-ratio?)} + (when keep-aspect-ratio? + i/status-tick)] + (tr "media.keep-aspect-ratio") + [:input {:type "checkbox" + :id "keep-aspect-ratio" + :checked keep-aspect-ratio? + :on-change handle-change-keep-aspect-ratio}]]]) + [:button + {:class (stl/css :choose-image) + :title (tr "media.choose-image") + :aria-label (tr "media.choose-image") + :on-click on-fill-image-click} + (tr "media.choose-image") + [:& file-uploader + {:input-id "fill-image-upload" + :accept "image/jpeg,image/png" + :multi false + :ref fill-image-ref + :on-selected on-fill-image-selected}]]]) + [:* + [:div {:class (stl/css :colorpicker-tabs)} + [:& tab-container + {:on-change-tab on-change-tab + :selected @active-color-tab + :collapsable false} - (when (or (= (:type state) :linear-gradient) - (= (:type state) :radial-gradient)) - [:& gradients - {:stops (:stops state) - :editing-stop (:editing-stop state) - :on-select-stop handle-change-stop}]) + [:& tab-element {:id :ramp :title i/rgba} + (if picking-color? + [:div {:class (stl/css :picker-detail-wrapper)} + [:div {:class (stl/css :center-circle)}] + [:canvas#picker-detail {:class (stl/css :picker-detail) :width 256 :height 140}]] + [:& ramp-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}])] - [:div.colorpicker-tabs - [:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand - {:class (when (= @active-tab :ramp) "active") - :alt (tr "workspace.libraries.colors.rgba") - :on-click set-tab! - :data-tab "ramp"} i/picker-ramp] - [:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand - {:class (when (= @active-tab :harmony) "active") - :alt (tr "workspace.libraries.colors.rgb-complementary") - :on-click set-tab! - :data-tab "harmony"} i/picker-harmony] - [:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand - {:class (when (= @active-tab :hsva) "active") - :alt (tr "workspace.libraries.colors.hsv") - :on-click set-tab! - :data-tab "hsva"} i/picker-hsv]] + [:& tab-element {:id :harmony :title i/rgba-complementary} + (if picking-color? + [:div {:class (stl/css :picker-detail-wrapper)} + [:div {:class (stl/css :center-circle)}] + [:canvas#picker-detail {:class (stl/css :picker-detail) :width 256 :height 140}]] + [:& harmony-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}])] - (if picking-color? - [:div.picker-detail-wrapper - [:div.center-circle] - [:canvas#picker-detail {:width 200 :height 160}]] - (case @active-tab - :ramp - [:& ramp-selector - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}] - :harmony - [:& harmony-selector - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}] - :hsva - [:& hsva-selector - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}] - nil)) + [:& tab-element {:id :hsva :title i/hsva} + (if picking-color? + [:div {:class (stl/css :picker-detail-wrapper)} + [:div {:class (stl/css :center-circle)}] + [:canvas#picker-detail {:class (stl/css :picker-detail) :width 256 :height 140}]] + [:& hsva-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}])]]] - [:& color-inputs - {:type (if (= @active-tab :hsva) :hsv :rgb) - :disable-opacity disable-opacity - :color current-color - :on-change handle-change-color}] + [:& color-inputs + {:type (if (= @active-color-tab :hsva) :hsv :rgb) + :disable-opacity disable-opacity + :color current-color + :on-change handle-change-color}] - [:& libraries - {:state state - :current-color current-color - :disable-gradient disable-gradient - :disable-opacity disable-opacity - :on-select-color on-select-library-color - :on-add-library-color on-add-library-color}] + [:& libraries + {:state state + :current-color current-color + :disable-gradient disable-gradient + :disable-opacity disable-opacity + :disable-image disable-image + :on-select-color on-select-library-color + :on-add-library-color on-add-library-color}]]) - (when on-accept - [:div.actions - [:button.btn-primary.btn-large - {:on-click (fn [] - (on-accept (dc/get-color-from-colorpicker-state state)) - (modal/hide!))} - (tr "workspace.libraries.colors.save-color")]])]])) + (when (fn? on-accept) + [:div {:class (stl/css :actions)} + [:button {:class (stl/css-case + :accept-color true + :btn-disabled disabled-color-accept?) + :on-click on-color-accept + :disabled disabled-color-accept?} + (tr "workspace.libraries.colors.save-color")]])])) (defn calculate-position "Calculates the style properties for the given coordinates and position" [{vh :height} position x y] (let [;; picker height in pixels - h 430 - ;; Checks for overflow outside the viewport height - overflow-fix (max 0 (+ y (- 50) h (- vh)))] - (cond - (or (nil? x) (nil? y)) {:left "auto" :right "16rem" :top "4rem"} - (= position :left) {:left (str (- x 250) "px") - :top (str (- y 50 overflow-fix) "px")} - :else {:left (str (+ x 80) "px") - :top (str (- y 70 overflow-fix) "px")}))) + h 510 + ;; Checks for overflow outside the viewport height + max-y (- vh h) + rulers? (mf/deref refs/rulers?) + left-offset (if rulers? 40 18) + + x-pos 400] + + (cond + (or (nil? x) (nil? y)) + #js {:left "auto" :right "16rem" :top "4rem"} + + (= position :left) + (if (> y max-y) + #js {:left (dm/str (- x x-pos) "px") + :bottom "1rem"} + #js {:left (dm/str (- x x-pos) "px") + :top (dm/str (- y 70) "px")}) + + (= position :right) + (if (> y max-y) + #js {:left (dm/str (+ x 80) "px") + :bottom "1rem"} + #js {:left (dm/str (+ x 80) "px") + :top (dm/str (- y 70) "px")}) + + :else + (if (> y max-y) + #js {:left (dm/str (+ x left-offset) "px") + :bottom "1rem"} + #js {:left (dm/str (+ x left-offset) "px") + :top (dm/str (- y 70) "px")})))) (mf/defc colorpicker-modal {::mf/register modal/components - ::mf/register-as :colorpicker} + ::mf/register-as :colorpicker + ::mf/props :obj} [{:keys [x y data position disable-gradient disable-opacity - on-change on-close on-accept] :as props}] - (let [vport (mf/deref viewport) - dirty? (mf/use-var false) + disable-image + on-change + on-close + on-accept]}] + (let [vport (mf/deref viewport) + dirty? (mf/use-var false) last-change (mf/use-var nil) - position (or position :left) - style (calculate-position vport position x y) + position (d/nilv position :left) + style (calculate-position vport position x y) - handle-change - (fn [new-data] - (reset! dirty? (not= data new-data)) - (reset! last-change new-data) - (when on-change - (on-change new-data)))] + on-change' + (mf/use-fn + (mf/deps on-change) + (fn [new-data] + (reset! dirty? (not= data new-data)) + (reset! last-change new-data) - (mf/use-effect - (fn [] - #(when (and @dirty? @last-change on-close) - (on-close @last-change)))) + (if (fn? on-change) + (on-change new-data) + (st/emit! (dc/update-colorpicker new-data)))))] + + (mf/with-effect [] + #(when (and @dirty? @last-change on-close) + (on-close @last-change))) + + [:div {:class (stl/css :colorpicker-tooltip) + :style style} - [:div.colorpicker-tooltip - {:style (clj->js style)} [:& colorpicker {:data data :disable-gradient disable-gradient :disable-opacity disable-opacity - :on-change handle-change + :disable-image disable-image + :on-change on-change' :on-accept on-accept}]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.scss b/frontend/src/app/main/ui/workspace/colorpicker.scss new file mode 100644 index 0000000000..cc739678d5 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker.scss @@ -0,0 +1,178 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.colorpicker-tooltip { + @extend .modal-background; + left: calc(10 * $s-140); + width: auto; +} + +.colorpicker { + border-radius: $br-8; + width: $s-260; + & > * { + width: $s-260; + } +} + +.top-actions { + display: flex; + align-items: flex-start; + flex-direction: row-reverse; + justify-content: space-between; + height: $s-40; +} + +.picker-btn { + @include buttonStyle; + @include flexCenter; + border-radius: $br-8; + background-color: transparent; + border: $s-1 solid transparent; + height: $s-20; + width: $s-20; + border-radius: $br-4; + padding: 0; + margin-top: $s-4; + svg { + @extend .button-icon; + stroke: var(--button-tertiary-foreground-color-rest); + } + &:hover { + svg { + stroke: var(--button-tertiary-foreground-color-focus); + } + } + &:focus, + &:focus-visible { + outline: none; + svg { + stroke: var(--button-secondary-foreground-color-hover); + } + } + &:active { + outline: none; + border: $s-1 solid transparent; + svg { + stroke: var(--button-tertiary-foreground-color-active); + } + } + &.selected { + svg { + stroke: var(--button-tertiary-foreground-color-active); + } + } +} + +.gradient-buttons { + display: flex; + align-items: center; + gap: $s-8; +} + +.gradient-btn { + @extend .button-tertiary; + height: $s-20; + width: $s-20; + border-radius: $br-4; + border: $s-2 solid transparent; + &:hover { + border: $s-2 solid var(--colorpicker-details-color-selected); + } +} + +.linear-gradient-btn { + background: linear-gradient(180deg, var(--color-foreground-secondary), transparent); + &.selected { + background: linear-gradient(to bottom, rgba(126, 255, 245, 1) 0%, rgba(126, 255, 245, 0.2) 100%); + border: $s-2 solid var(--colorpicker-details-color-selected); + } +} + +.radial-gradient-btn { + background: radial-gradient(transparent, var(--color-foreground-secondary)); + &.selected { + background: radial-gradient(rgba(126, 255, 245, 1) 0%, rgba(126, 255, 245, 0.2) 100%); + border: $s-2 solid var(--colorpicker-details-color-selected); + } +} + +.actions { + display: flex; + gap: $s-4; +} + +.accept-color { + @include uppercaseTitleTipography; + @extend .button-secondary; + width: 100%; + height: $s-32; + margin-top: $s-8; +} + +.picker-detail-wrapper { + @include flexCenter; + position: relative; + margin: $s-12 0 $s-8 0; +} + +.center-circle { + width: $s-24; + height: $s-24; + border: $s-2 solid var(--colorpicker-details-color); + border-radius: $br-circle; + position: absolute; + left: 50%; + top: 50%; + transform: translate(calc(-1 * $s-12), calc(-1 * $s-12)); +} + +.picker-detail { + image-rendering: crisp-edges; + image-rendering: pixelated; +} + +.select { + width: $s-116; +} + +.select-image { + margin-top: $s-4; +} + +.content { + border-radius: $br-8; + display: flex; + justify-content: center; + background-image: url("/images/colorpicker-no-image.png"); + background-position: center; + background-size: auto $s-140; + height: $s-140; + margin-bottom: $s-6; + margin-right: $s-1; + img { + height: fit-content; + width: fit-content; + max-height: 100%; + max-width: 100%; + margin: auto; + } +} + +.choose-image { + @extend .button-secondary; + @include uppercaseTitleTipography; + width: 100%; + margin-top: $s-12; + height: $s-32; +} + +.checkbox-option { + @extend .input-checkbox; + margin: $s-16 0 0 0; +} diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs index dad0b71e73..53fb08dfd0 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs @@ -5,10 +5,11 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.colorpicker.color-inputs + (:require-macros [app.main.style :as stl]) (:require + [app.common.colors :as cc] [app.common.data :as d] [app.common.math :as mth] - [app.util.color :as uc] [app.util.dom :as dom] [rumext.v2 :as mf])) @@ -42,23 +43,27 @@ setup-hex-color (fn [hex] - (let [[r g b] (uc/hex->rgb hex) - [h s v] (uc/hex->hsv hex)] + (let [[r g b] (cc/hex->rgb hex) + [h s v] (cc/hex->hsv hex)] (on-change {:hex hex :h h :s s :v v :r r :g g :b b}))) on-change-hex (fn [e] (let [val (-> e dom/get-target-val parse-hex)] - (when (uc/hex? val) + (when (cc/valid-hex-color? val) (setup-hex-color val)))) on-blur-hex (fn [e] (let [val (-> e dom/get-target-val) + ;; FIXME: looks redundant, cc/parse already handles + ;; hex colors; also it performs the parse-hex twice + ;; that is completly unnecessary val (cond - (uc/color? val) (uc/parse-color val) - (uc/hex? (parse-hex val)) (parse-hex val))] + (cc/color-string? val) (cc/parse val) + (cc/valid-hex-color? (parse-hex val)) (parse-hex val))] + (when (some? val) (setup-hex-color val)))) @@ -73,15 +78,15 @@ (when (not (nil? val)) (if (#{:r :g :b} property) (let [{:keys [r g b]} (merge color (hash-map property val)) - hex (uc/rgb->hex [r g b]) - [h s v] (uc/hex->hsv hex)] + hex (cc/rgb->hex [r g b]) + [h s v] (cc/hex->hsv hex)] (on-change {:hex hex :h h :s s :v v :r r :g g :b b})) (let [{:keys [h s v]} (merge color (hash-map property val)) - hex (uc/hsv->hex [h s v]) - [r g b] (uc/hex->rgb hex)] + hex (cc/hsv->hex [h s v]) + [r g b] (cc/hex->rgb hex)] (on-change {:hex hex :h h :s s :v v :r r :g g :b b}))))))) @@ -108,84 +113,86 @@ property-val)] (dom/set-value! node new-val)))))))) - [:div.color-values - {:class (when disable-opacity "disable-opacity")} - [:input {:id "hex-value" - :ref (:hex refs) - :default-value hex - :on-change on-change-hex - :on-blur on-blur-hex}] + [:div {:class (stl/css-case :color-values true + :disable-opacity disable-opacity)} - (if (= type :rgb) - [:* - [:input {:id "red-value" - :ref (:r refs) - :type "number" - :min 0 - :max 255 - :default-value red - :on-change (on-change-property :r 255)}] + [:div {:class (stl/css :colors-row)} + (if (= type :rgb) + [:* + [:div {:class (stl/css :input-wrapper)} + [:span {:class (stl/css :input-label)} "R"] + [:input {:id "red-value" + :ref (:r refs) + :type "number" + :min 0 + :max 255 + :default-value red + :on-change (on-change-property :r 255)}]] + [:div {:class (stl/css :input-wrapper)} + [:span {:class (stl/css :input-label)} "G"] + [:input {:id "green-value" + :ref (:g refs) + :type "number" + :min 0 + :max 255 + :default-value green + :on-change (on-change-property :g 255)}]] + [:div {:class (stl/css :input-wrapper)} + [:span {:class (stl/css :input-label)} "B"] + [:input {:id "blue-value" + :ref (:b refs) + :type "number" + :min 0 + :max 255 + :default-value blue + :on-change (on-change-property :b 255)}]]] - [:input {:id "green-value" - :ref (:g refs) - :type "number" - :min 0 - :max 255 - :default-value green - :on-change (on-change-property :g 255)}] - - [:input {:id "blue-value" - :ref (:b refs) - :type "number" - :min 0 - :max 255 - :default-value blue - :on-change (on-change-property :b 255)}]] - [:* - [:input {:id "hue-value" - :ref (:h refs) - :type "number" - :min 0 - :max 360 - :default-value hue - :on-change (on-change-property :h 360)}] - - [:input {:id "saturation-value" - :ref (:s refs) - :type "number" - :min 0 - :max 100 - :step 1 - :default-value saturation - :on-change (on-change-property :s 100)}] - - [:input {:id "value-value" - :ref (:v refs) - :type "number" - :min 0 - :max 100 - :default-value value - :on-change (on-change-property :v 100)}]]) - - (when (not disable-opacity) - [:input.alpha-value {:id "alpha-value" - :ref (:alpha refs) - :type "number" - :min 0 - :step 1 - :max 100 - :default-value (if (= alpha :multiple) "" alpha) - :on-change on-change-opacity}]) - - [:label.hex-label {:for "hex-value"} "HEX"] - (if (= type :rgb) - [:* - [:label.red-label {:for "red-value"} "R"] - [:label.green-label {:for "green-value"} "G"] - [:label.blue-label {:for "blue-value"} "B"]] - [:* - [:label.red-label {:for "hue-value"} "H"] - [:label.green-label {:for "saturation-value"} "S"] - [:label.blue-label {:for "value-value"} "V"]]) - (when (not disable-opacity) - [:label.alpha-label {:for "alpha-value"} "A"])])) + [:* + [:div {:class (stl/css :input-wrapper)} + [:span {:class (stl/css :input-label)} "H"] + [:input {:id "hue-value" + :ref (:h refs) + :type "number" + :min 0 + :max 360 + :default-value hue + :on-change (on-change-property :h 360)}]] + [:div {:class (stl/css :input-wrapper)} + [:span {:class (stl/css :input-label)} "S"] + [:input {:id "saturation-value" + :ref (:s refs) + :type "number" + :min 0 + :max 100 + :step 1 + :default-value saturation + :on-change (on-change-property :s 100)}]] + [:div {:class (stl/css :input-wrapper)} + [:span {:class (stl/css :input-label)} "V"] + [:input {:id "value-value" + :ref (:v refs) + :type "number" + :min 0 + :max 100 + :default-value value + :on-change (on-change-property :v 100)}]]])] + [:div {:class (stl/css :hex-alpha-wrapper)} + [:div {:class (stl/css-case :input-wrapper true + :hex true)} + [:span {:class (stl/css :input-label)} "HEX"] + [:input {:id "hex-value" + :ref (:hex refs) + :default-value hex + :on-change on-change-hex + :on-blur on-blur-hex}]] + (when (not disable-opacity) + [:div {:class (stl/css-case :input-wrapper true)} + [:span {:class (stl/css :input-label)} "A"] + [:input {:id "alpha-value" + :ref (:alpha refs) + :type "number" + :min 0 + :step 1 + :max 100 + :default-value (if (= alpha :multiple) "" alpha) + :on-change on-change-opacity}]])]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss new file mode 100644 index 0000000000..0dfa490ed0 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss @@ -0,0 +1,38 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.color-values { + @include flexColumn; + margin-top: $s-8; + + &.disable-opacity { + grid-template-columns: 3.5rem repeat(3, 1fr); + } + .colors-row { + @include flexRow; + .input-wrapper { + @extend .input-element; + width: $s-84; + display: flex; + align-items: baseline; + } + } + .hex-alpha-wrapper { + @include flexRow; + .input-wrapper { + @extend .input-element; + width: $s-84; + &.hex { + width: $s-172; + display: flex; + align-items: baseline; + gap: $s-8; + } + } + } +} diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs index 43a4f768b6..28ea90b57e 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.colorpicker.gradients + (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] [cuerdas.core :as str] @@ -20,17 +21,21 @@ (mf/defc gradients [{:keys [stops editing-stop on-select-stop]}] - [:div.gradient-stops - [:div.gradient-background-wrapper - [:div.gradient-background {:style {:background (gradient->string stops)}}]] + [:div {:class (stl/css :gradient-stops)} + [:div {:class (stl/css :gradient-background-wrapper)} + [:div {:class (stl/css :gradient-background) + :style {:background (gradient->string stops)}}]] - [:div.gradient-stop-wrapper + [:div {:class (stl/css :gradient-stop-wrapper)} (for [{:keys [offset hex r g b alpha] :as value} stops] - [:div.gradient-stop - {:class (when (= editing-stop offset) "active") - :on-click (partial on-select-stop offset) - :style {:left (dm/str (* offset 100) "%")} - :key (dm/str offset)} + [:button {:class (stl/css-case :gradient-stop true + :selected (= editing-stop offset)) + :data-value (str offset) + :on-click on-select-stop + :style {:left (dm/str (* offset 100) "%")} + :key (dm/str offset)} - [:div.gradient-stop-color {:style {:background-color hex}}] - [:div.gradient-stop-alpha {:style {:background-color (str/ffmt "rgba(%1, %2, %3, %4)" r g b alpha)}}]])]]) + [:div {:class (stl/css :gradient-stop-color) + :style {:background-color hex}}] + [:div {:class (stl/css :gradient-stop-alpha) + :style {:background-color (str/ffmt "rgba(%1, %2, %3, %4)" r g b alpha)}}]])]]) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss b/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss new file mode 100644 index 0000000000..374edaaa76 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss @@ -0,0 +1,62 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.gradient-stops { + display: flex; + height: $s-20; + width: 100%; + margin: $s-12 0; + background-color: var(--colorpicker-handlers-color); + border-radius: $br-6; +} + +.gradient-background-wrapper { + height: 100%; + width: 100%; + border-radius: $br-6; + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAExJREFUSIljvHnz5n8GLEBNTQ2bMMOtW7ewiuNSz4RVlIpg1IKBt4Dx////WFMRqakFl/qhH0SjFhAELNRKLaNl0Qi2YLQsGrWAcgAA0gAgQPhT2rAAAAAASUVORK5CYII=") + left center; +} + +.gradient-background { + height: 100%; + width: 100%; + border-radius: $br-6; + border: $s-2 solid var(--colorpicker-details-color); +} + +.gradient-stop-wrapper { + position: absolute; + width: calc(100% - $s-40); + left: $s-20; +} + +.gradient-stop { + position: absolute; + display: grid; + grid-template-columns: 50% 50%; + padding: 0; + width: $s-16; + height: $s-24; + border-radius: $br-4; + margin-top: calc(-1 * $s-2); + margin-left: calc(-1 * $s-8); + border: $s-2 solid var(--colorpicker-handlers-color); + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII="); + background-position: left center; + background-size: 8px; + &.selected { + border: $s-2 solid var(--colorpicker-details-color-selected); + } +} + +.gradient-stop-color, +.gradient-stop-alpha { + width: 100%; + height: 100%; +} diff --git a/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs b/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs index 119faa933d..361a51f7e1 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs @@ -5,11 +5,13 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.colorpicker.harmony + (:require-macros [app.main.style :as stl]) (:require + [app.common.colors :as cc] + [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.math :as mth] [app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]] - [app.util.color :as uc] [app.util.dom :as dom] [app.util.object :as obj] [cuerdas.core :as str] @@ -57,13 +59,13 @@ (gpt/point x y))) (mf/defc harmony-selector [{:keys [color disable-opacity on-change on-start-drag on-finish-drag]}] - (let [canvas-ref (mf/use-ref nil) + (let [canvas-ref (mf/use-ref nil) + canvas-side 192 {hue :h saturation :s value :v alpha :alpha} color - canvas-side 152 - pos-current (color->point canvas-side hue saturation) + pos-current (color->point canvas-side hue saturation) pos-complement (color->point canvas-side (mod (+ hue 180) 360) saturation) - dragging? (mf/use-state false) + dragging? (mf/use-state false) calculate-pos (fn [ev] (let [{:keys [left right top bottom]} (-> ev dom/get-target dom/get-bounding-rect) @@ -75,10 +77,10 @@ py (- (* 2 py) 1) angle (mth/degrees (mth/atan2 px py)) - new-hue (mod (- angle 90 ) 360) + new-hue (mod (- angle 90) 360) new-saturation (mth/clamp (mth/distance [px py] [0 0]) 0 1) - hex (uc/hsv->hex [new-hue new-saturation value]) - [r g b] (uc/hex->rgb hex)] + hex (cc/hsv->hex [new-hue new-saturation value]) + [r g b] (cc/hex->rgb hex)] (on-change {:hex hex :r r :g g :b b :h new-hue @@ -103,49 +105,38 @@ (on-finish-drag)))) on-change-value (fn [new-value] - (let [hex (uc/hsv->hex [hue saturation new-value]) - [r g b] (uc/hex->rgb hex)] + (let [hex (cc/hsv->hex [hue saturation new-value]) + [r g b] (cc/hex->rgb hex)] (on-change {:hex hex :r r :g g :b b :v new-value}))) on-complement-click (fn [_] (let [new-hue (mod (+ hue 180) 360) - hex (uc/hsv->hex [new-hue saturation value]) - [r g b] (uc/hex->rgb hex)] + hex (cc/hsv->hex [new-hue saturation value]) + [r g b] (cc/hex->rgb hex)] (on-change {:hex hex :r r :g g :b b :h new-hue :s saturation}))) - on-change-opacity (fn [new-alpha] (on-change {:alpha new-alpha}))] + on-change-opacity (fn [new-alpha] (on-change {:alpha new-alpha})) + + ;; This colors are to display the value slider + [h1 s1 l1] (cc/hsv->hsl [hue saturation 0]) + [h2 s2 l2] (cc/hsv->hsl [hue saturation 255])] (mf/use-effect (mf/deps canvas-ref) (fn [] (when canvas-ref (create-color-wheel (mf/ref-val canvas-ref))))) - [:div.harmony-selector - [:div.hue-wheel-wrapper - [:canvas.hue-wheel - {:ref canvas-ref - :width canvas-side - :height canvas-side - :on-pointer-down handle-start-drag - :on-pointer-up handle-stop-drag - :on-lost-pointer-capture handle-stop-drag - :on-click calculate-pos - :on-pointer-move #(when @dragging? (calculate-pos %))}] - [:div.handler {:style {:pointer-events "none" - :left (:x pos-current) - :top (:y pos-current)}}] - [:div.handler.complement {:style {:left (:x pos-complement) - :top (:y pos-complement) - :cursor "pointer"} - :on-click on-complement-click}]] - [:div.handlers-wrapper - [:& slider-selector {:class "value" + [:div {:class (stl/css :harmony-selector) + :style {"--hue-from" (dm/str "hsl(" h1 ", " (* s1 100) "%, " (* l1 100) "%)") + "--hue-to" (dm/str "hsl(" h2 ", " (* s2 100) "%, " (* l2 100) "%)")}} + [:div {:class (stl/css :handlers-wrapper)} + [:& slider-selector {:type :value :vertical? true - :reverse? true + :reverse? false :value value :max-value 255 :vertical true @@ -153,11 +144,32 @@ :on-start-drag on-start-drag :on-finish-drag on-finish-drag}] (when (not disable-opacity) - [:& slider-selector {:class "opacity" - :vertical? true - :value alpha - :max-value 1 - :vertical true - :on-change on-change-opacity - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}])]])) + [[:& slider-selector {:type :opacity + :vertical? true + :value alpha + :max-value 1 + :vertical true + :on-change on-change-opacity + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}]])] + + [:div {:class (stl/css :hue-wheel-wrapper)} + [:canvas {:class (stl/css :hue-wheel) + :ref canvas-ref + :width canvas-side + :height canvas-side + :on-pointer-down handle-start-drag + :on-pointer-up handle-stop-drag + :on-lost-pointer-capture handle-stop-drag + :on-click calculate-pos + :on-pointer-move #(when @dragging? (calculate-pos %))}] + [:div {:class (stl/css :handler) + :style {:pointer-events "none" + :left (:x pos-current) + :top (:y pos-current)}}] + [:div {:class (stl/css-case :handler true + :complement true) + :style {:left (:x pos-complement) + :top (:y pos-complement) + :cursor "pointer"} + :on-click on-complement-click}]]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss b/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss new file mode 100644 index 0000000000..04bc1d46ac --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss @@ -0,0 +1,44 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.harmony-selector { + display: flex; + align-items: center; + gap: $s-8; + margin-top: $s-12; + margin-bottom: $s-8; +} + +.hue-wheel-wrapper { + @include flexCenter; + position: relative; +} + +.hue-wheel { + width: $s-196; + height: $s-196; +} + +.handler { + @extend .colorpicker-handler; + height: $s-16; + width: $s-16; + border: $s-2 solid var(--colorpicker-handlers-color); +} + +.handler.complement { + background-color: var(--colorpicker-handlers-color); + border: $s-2 solid var(--colorpicker-handlers-color); +} + +.handlers-wrapper { + @include flexRow; + height: $s-200; + width: $s-52; + flex-grow: 1; +} diff --git a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs index 6933baeb3a..cf4554b6e3 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs @@ -5,9 +5,10 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.colorpicker.hsva + (:require-macros [app.main.style :as stl]) (:require + [app.common.colors :as cc] [app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]] - [app.util.color :as uc] [rumext.v2 :as mf])) (mf/defc hsva-selector [{:keys [color disable-opacity on-change on-start-drag on-finish-drag]}] @@ -16,46 +17,50 @@ (fn [new-value] (let [change (hash-map key new-value) {:keys [h s v]} (merge color change) - hex (uc/hsv->hex [h s v]) - [r g b] (uc/hex->rgb hex)] + hex (cc/hsv->hex [h s v]) + [r g b] (cc/hex->rgb hex)] (on-change (merge change {:hex hex :r r :g g :b b}))))) on-change-opacity (fn [new-alpha] (on-change {:alpha new-alpha}))] - [:div.hsva-selector - [:span.hsva-selector-label "H"] - [:& slider-selector - {:class "hue" - :max-value 360 - :value hue - :on-change (handle-change-slider :h) - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}] - - [:span.hsva-selector-label "S"] - [:& slider-selector - {:class "saturation" - :max-value 1 - :value saturation - :on-change (handle-change-slider :s) - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}] - - [:span.hsva-selector-label "V"] - [:& slider-selector - {:class "value" - :reverse? false - :max-value 255 - :value value - :on-change (handle-change-slider :v) - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}] - + [:div {:class (stl/css :hsva-selector)} + [:div {:class (stl/css :hsva-row)} + [:span {:class (stl/css :hsva-selector-label)} "H"] + [:& slider-selector + {:class (stl/css :hsva-bar) + :type :hue + :max-value 360 + :value hue + :on-change (handle-change-slider :h) + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}]] + [:div {:class (stl/css :hsva-row)} + [:span {:class (stl/css :hsva-selector-label)} "S"] + [:& slider-selector + {:class (stl/css :hsva-bar) + :type :saturation + :max-value 1 + :value saturation + :on-change (handle-change-slider :s) + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}]] + [:div {:class (stl/css :hsva-row)} + [:span {:class (stl/css :hsva-selector-label)} "V"] + [:& slider-selector + {:class (stl/css :hsva-bar) + :type :value + :reverse? false + :max-value 255 + :value value + :on-change (handle-change-slider :v) + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}]] (when (not disable-opacity) - [:* - [:span.hsva-selector-label "A"] + [:div {:class (stl/css :hsva-row)} + [:span {:class (stl/css :hsva-selector-label)} "A"] [:& slider-selector - {:class "opacity" + {:class (stl/css :hsva-bar) + :type :opacity :max-value 1 :value alpha :on-change on-change-opacity diff --git a/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss b/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss new file mode 100644 index 0000000000..4b02ceec61 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss @@ -0,0 +1,31 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.hsva-selector { + @include flexColumn; + padding: $s-4; + grid-row-gap: $s-8; + margin-bottom: $s-8; +} + +.hsva-row { + display: flex; + align-items: center; +} + +.hsva-selector-label { + @include uppercaseTitleTipography; + display: flex; + align-items: center; + justify-content: flex-start; + width: $s-32; +} + +.hsva-bar { + width: $s-228; +} diff --git a/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs index ba6a8b28e1..14e851d31d 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs @@ -5,46 +5,72 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.colorpicker.libraries + (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.main.data.events :as ev] [app.main.data.workspace :as dw] [app.main.data.workspace.colors :as mdc] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.color-bullet :refer [color-bullet]] + [app.main.ui.components.color-bullet :as cb] + [app.main.ui.components.select :refer [select]] [app.main.ui.hooks :as h] [app.main.ui.hooks.resize :as r] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.timers :as ts] [rumext.v2 :as mf])) (mf/defc libraries - [{:keys [state on-select-color on-add-library-color disable-gradient disable-opacity]}] + [{:keys [state on-select-color on-add-library-color disable-gradient disable-opacity disable-image]}] (let [selected (h/use-shared-state mdc/colorpicker-selected-broadcast-key :recent) current-colors (mf/use-state []) shared-libs (mf/deref refs/workspace-libraries) file-colors (mf/deref refs/workspace-file-colors) recent-colors (mf/deref refs/workspace-recent-colors) - recent-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %)) recent-colors)) + recent-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %) (:image %)) recent-colors)) on-library-change (mf/use-fn (fn [event] - (let [val (dom/get-target-val event)] - (reset! selected - (if (or (= val "recent") - (= val "file")) - (keyword val) - (parse-uuid val)))))) + (reset! selected + (if (or (= event "recent") + (= event "file")) + (keyword event) + (parse-uuid event))))) check-valid-color? (fn [color] (and (or (not disable-gradient) (not (:gradient color))) - (or (not disable-opacity) (= 1 (:opacity color)))))] + (or (not disable-opacity) (= 1 (:opacity color))) + (or (not disable-image) (not (:image color))))) + + toggle-palette + (mf/use-fn + (mf/deps @selected) + (fn [] + (r/set-resize-type! :bottom) + (dom/add-class! (dom/get-element-by-class "color-palette") "fade-out-down") + (st/emit! (dw/remove-layout-flag :textpalette) + (-> (mdc/show-palette @selected) + (vary-meta assoc ::ev/origin "workspace-colorpicker"))))) + + shared-libs-options (mapv (fn [lib] {:value (d/name (:id lib)) :label (:name lib)}) (vals shared-libs)) + + + library-options [{:value "recent" :label (tr "workspace.libraries.colors.recent-colors")} + {:value "file" :label (tr "workspace.libraries.colors.file-library")}] + + options (concat library-options shared-libs-options) + + on-color-click + (mf/use-fn + (mf/deps state) + (fn [event] + (on-select-color state event)))] ;; Load library colors when the select is changed (mf/with-effect [@selected recent-colors file-colors] @@ -71,33 +97,26 @@ (let [colors (vals file-colors)] (reset! current-colors (into [] (filter check-valid-color?) colors))))) - [:div.libraries - [:select {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :on-change on-library-change - :value (name @selected)} - [:option {:value "recent"} (tr "workspace.libraries.colors.recent-colors")] - [:option {:value "file"} (tr "workspace.libraries.colors.file-library")] + [:div {:class (stl/css :libraries)} + [:div {:class (stl/css :select-wrapper)} + [:& select + {:class (stl/css :shadow-type-select) + :default-value (or (name @selected) "recent") + :options options + :on-change on-library-change}]] - (for [[_ {:keys [name id]}] shared-libs] - [:option {:key id :value id} name])] - - [:div.selected-colors + [:div {:class (stl/css :selected-colors)} (when (= @selected :file) - [:div.color-bullet.button.plus-button {:style {:background-color "var(--color-white)"} - :on-click on-add-library-color} - i/plus]) + [:button {:class (stl/css :add-color-btn) + :on-click on-add-library-color} + i/add]) - [:div.color-bullet.button {:style {:background-color "var(--color-white)"} - :on-click(fn [] - (r/set-resize-type! :bottom) - (dom/add-class! (dom/get-element-by-class "color-palette") "fade-out-down") - (ts/schedule 300 #(st/emit! (dw/remove-layout-flag :textpalette) - (-> (dw/toggle-layout-flag :colorpalette) - (vary-meta assoc ::ev/origin "workspace-colorpicker")))))} - i/palette] + [:button {:class (stl/css :palette-btn) + :on-click toggle-palette} + i/swatches] (for [[idx color] (map-indexed vector @current-colors)] - [:& color-bullet + [:& cb/color-bullet {:key (dm/str "color-" idx) :color color - :on-click (partial on-select-color state)}])]])) + :on-click on-color-click}])]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss b/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss new file mode 100644 index 0000000000..63a0f83987 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss @@ -0,0 +1,42 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.libraries { + margin-top: $s-8; + width: 100%; +} + +.selected-colors { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: $s-4; + justify-content: space-between; + overflow: auto; + margin-top: $s-8; +} + +.add-color-btn, +.palette-btn { + @extend .button-secondary; + height: $s-24; + width: $s-24; + border-radius: $br-circle; + padding: 0; + svg { + @extend .button-icon; + } +} + +.selected-colors::after { + content: ""; + flex: auto; +} + +.select-wrapper { + overflow: initial; +} diff --git a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs index 0a839324a8..970525d3c1 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs @@ -5,11 +5,12 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.colorpicker.ramp + (:require-macros [app.main.style :as stl]) (:require + [app.common.colors :as cc] [app.common.math :as mth] - [app.main.ui.components.color-bullet :refer [color-bullet]] + [app.main.ui.components.color-bullet :as cb] [app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]] - [app.util.color :as uc] [app.util.dom :as dom] [rumext.v2 :as mf])) @@ -37,17 +38,17 @@ (fn [event] (dom/release-pointer event) (reset! dragging? false) - (on-finish-drag))) - ] - [:div.value-saturation-selector - {:on-pointer-down handle-start-drag - :on-pointer-up handle-stop-drag - :on-lost-pointer-capture handle-stop-drag - :on-click calculate-pos - :on-pointer-move #(when @dragging? (calculate-pos %))} - [:div.handler {:style {:pointer-events "none" - :left (str (* 100 saturation) "%") - :top (str (* 100 (- 1 (/ value 255))) "%")}}]])) + (on-finish-drag)))] + [:div {:class (stl/css :value-saturation-selector) + :on-pointer-down handle-start-drag + :on-pointer-up handle-stop-drag + :on-lost-pointer-capture handle-stop-drag + :on-click calculate-pos + :on-pointer-move #(when @dragging? (calculate-pos %))} + [:div {:class (stl/css :handler) + :style {:pointer-events "none" + :left (str (* 100 saturation) "%") + :top (str (* 100 (- 1 (/ value 255))) "%")}}]])) (mf/defc ramp-selector [{:keys [color disable-opacity on-change on-start-drag on-finish-drag]}] @@ -56,8 +57,8 @@ on-change-value-saturation (fn [new-saturation new-value] - (let [hex (uc/hsv->hex [hue new-saturation new-value]) - [r g b] (uc/hex->rgb hex)] + (let [hex (cc/hsv->hex [hue new-saturation new-value]) + [r g b] (cc/hex->rgb hex)] (on-change {:hex hex :r r :g g :b b :s new-saturation @@ -65,15 +66,15 @@ on-change-hue (fn [new-hue] - (let [hex (uc/hsv->hex [new-hue saturation value]) - [r g b] (uc/hex->rgb hex)] + (let [hex (cc/hsv->hex [new-hue saturation value]) + [r g b] (cc/hex->rgb hex)] (on-change {:hex hex :r r :g g :b b - :h new-hue} ))) + :h new-hue}))) on-change-opacity (fn [new-opacity] - (on-change {:alpha new-opacity} ))] + (on-change {:alpha new-opacity}))] [:* [:& value-saturation-selector {:hue hue @@ -83,20 +84,23 @@ :on-start-drag on-start-drag :on-finish-drag on-finish-drag}] - [:div.shade-selector - [:& color-bullet {:color {:color hex - :opacity alpha}}] - [:& slider-selector {:class "hue" - :max-value 360 - :value hue - :on-change on-change-hue - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}] + [:div {:class (stl/css :shade-selector) + :style #js {"--bullet-size" "52px"}} + [:& cb/color-bullet {:color {:color hex + :opacity alpha} + :area true}] + [:div {:class (stl/css :sliders-wrapper)} + [:& slider-selector {:type :hue + :max-value 360 + :value hue + :on-change on-change-hue + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] - (when (not disable-opacity) - [:& slider-selector {:class "opacity" - :max-value 1 - :value alpha - :on-change on-change-opacity - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}])]])) + (when (not disable-opacity) + [:& slider-selector {:type :opacity + :max-value 1 + :value alpha + :on-change on-change-opacity + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}])]]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss b/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss new file mode 100644 index 0000000000..512739d8fd --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss @@ -0,0 +1,51 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.value-saturation-selector { + background-color: rgba(var(--hue-rgb)); + position: relative; + height: $s-140; + width: $s-256; + margin-top: $s-12; + margin-bottom: $s-12; + cursor: pointer; + + &::before { + content: ""; + position: absolute; + width: 100%; + height: 100%; + background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0)); + } + + &::after { + content: ""; + position: absolute; + width: 100%; + height: 100%; + background: linear-gradient(to top, #000, rgba(0, 0, 0, 0)); + } +} + +.handler { + @extend .colorpicker-handler; + height: $s-16; + width: $s-16; + border: $s-2 solid var(--colorpicker-handlers-color); +} + +.shade-selector { + display: flex; + gap: $s-4; + height: $s-52; + cursor: pointer; +} + +.sliders-wrapper { + @include flexColumn; +} diff --git a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs index 7b5af5bae6..1fa07db8a4 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs @@ -5,14 +5,16 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.colorpicker.slider-selector + (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.common.math :as mth] [app.util.dom :as dom] [app.util.object :as obj] [rumext.v2 :as mf])) (mf/defc slider-selector - [{:keys [value class min-value max-value vertical? reverse? on-change on-start-drag on-finish-drag]}] + [{:keys [value class min-value max-value vertical? reverse? on-change on-start-drag on-finish-drag type]}] (let [min-value (or min-value 0) max-value (or max-value 1) dragging? (mf/use-state false) @@ -49,23 +51,27 @@ value (+ min-value (* unit-value (- max-value min-value)))] (on-change value))))] - [:div.slider-selector - {:class (str (if vertical? "vertical " "") class) - :on-pointer-down handle-start-drag - :on-pointer-up handle-stop-drag - :on-lost-pointer-capture handle-stop-drag - :on-click calculate-pos - :on-pointer-move #(when @dragging? (calculate-pos %))} + [:div {:class (stl/css-case :opacity-wrapper (= type :opacity))} + [:div {:class (dm/str class (stl/css-case :vertical vertical? + :slider-selector true + :hue (= type :hue) + :opacity (= type :opacity) + :value (= type :value))) + :on-pointer-down handle-start-drag + :on-pointer-up handle-stop-drag + :on-lost-pointer-capture handle-stop-drag + :on-click calculate-pos + :on-pointer-move #(when @dragging? (calculate-pos %))} + (let [value-percent (* (/ (- value min-value) + (- max-value min-value)) 100) - (let [value-percent (* (/ (- value min-value) - (- max-value min-value)) 100) + value-percent (if reverse? + (mth/abs (- value-percent 100)) + value-percent) + value-percent-str (str value-percent "%") - value-percent (if reverse? - (mth/abs (- value-percent 100)) - value-percent) - value-percent-str (str value-percent "%") - - style-common #js {:pointerEvents "none"} - style-horizontal (obj/merge! #js {:left value-percent-str} style-common) - style-vertical (obj/merge! #js {:bottom value-percent-str} style-common)] - [:div.handler {:style (if vertical? style-vertical style-horizontal)}])])) + style-common #js {:pointerEvents "none"} + style-horizontal (obj/merge! #js {:left value-percent-str} style-common) + style-vertical (obj/merge! #js {:bottom value-percent-str} style-common)] + [:div {:class (stl/css :handler) + :style (if vertical? style-vertical style-horizontal)}])]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss new file mode 100644 index 0000000000..7de62cbef9 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss @@ -0,0 +1,118 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.slider-selector { + --gradient-direction: 90deg; + --background-repeat: left; + + &.vertical { + --gradient-direction: 0deg; + --background-repeat: top; + } + position: relative; + align-self: center; + height: $s-24; + width: $s-200; + border: $s-2 solid var(--colorpicker-details-color); + border-radius: $br-6; + background: linear-gradient( + var(--gradient-direction), + rgba(var(--color), 0) 0%, + rgba(var(--color), 1) 100% + ); + cursor: pointer; + + &.vertical { + width: $s-24; + height: $s-200; + } + + &.hue { + background: linear-gradient( + var(--gradient-direction), + #f00 0%, + #ff0 17%, + #0f0 33%, + #0ff 50%, + #00f 67%, + #f0f 83%, + #f00 100% + ); + } + + &.saturation { + background: linear-gradient( + var(--gradient-direction), + var(--saturation-grad-from) 0%, + var(--saturation-grad-to) 100% + ); + } + + &.opacity { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAExJREFUSIljvHnz5n8GLEBNTQ2bMMOtW7ewiuNSz4RVlIpg1IKBt4Dx////WFMRqakFl/qhH0SjFhAELNRKLaNl0Qi2YLQsGrWAcgAA0gAgQPhT2rAAAAAASUVORK5CYII=") + var(--background-repeat) center; + + &::after { + content: ""; + position: absolute; + border-radius: $br-6; + width: 100%; + height: 100%; + background: linear-gradient( + var(--gradient-direction), + rgba(var(--color), 0) 0%, + rgba(var(--color), 1) 100% + ); + } + } + + &.value { + background: linear-gradient(var(--gradient-direction), #000 0%, #fff 100%); + } + + .handler { + position: absolute; + left: 50%; + width: calc($s-8 + $s-2); + height: calc($s-24 + $s-1); + border-radius: $br-4; + z-index: $z-index-1; + transform: translate(-4px, -3px); + background-color: var(--colorpicker-handlers-color); + } + + &.vertical .handler { + height: calc($s-8 + $s-2); + width: calc($s-24 + $s-1); + transform: translate(-12px, 5px); + } +} + +.opacity-wrapper { + background-color: var(--colorpicker-background-color); + border-radius: $br-8; +} + +.slider-selector.hue { + grid-area: hue; +} + +.slider-selector.opacity { + grid-area: opacity; +} + +.slider-selector.value { + background: linear-gradient(var(--gradient-direction), var(--hue-from, #000) 0%, var(--hue-to, #fff) 100%); +} +.slider-selector.saturation { + background: linear-gradient( + var(--gradient-direction), + var(--saturation-grad-from) 0%, + var(--saturation-grad-to) 100% + ); +} diff --git a/frontend/src/app/main/ui/workspace/comments.cljs b/frontend/src/app/main/ui/workspace/comments.cljs index dff8689f20..4ff0e1842d 100644 --- a/frontend/src/app/main/ui/workspace/comments.cljs +++ b/frontend/src/app/main/ui/workspace/comments.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.comments + (:require-macros [app.main.style :as stl]) (:require [app.main.data.comments :as dcm] [app.main.data.events :as ev] @@ -26,55 +27,84 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (mf/defc sidebar-options - [] + {::mf/props :obj} + [{:keys [from-viewer]}] (let [{cmode :mode cshow :show} (mf/deref refs/comments-local) update-mode (mf/use-fn - (fn [mode] - (st/emit! (dcm/update-filters {:mode mode})))) + (fn [event] + (let [mode (-> (dom/get-current-target event) + (dom/get-data "value") + (keyword))] + (st/emit! (dcm/update-filters {:mode mode}))))) update-show (mf/use-fn - (fn [mode] - (st/emit! (dcm/update-filters {:show mode}))))] + (mf/deps cshow) + (fn [] + (let [mode (if (= :pending cshow) :all :pending)] + (st/emit! (dcm/update-filters {:show mode})))))] - [:ul.dropdown.with-check - [:li {:class (dom/classnames :selected (or (= :all cmode) (nil? cmode))) - :on-click #(update-mode :all)} - [:span.icon i/tick] - [:span.label (tr "labels.show-all-comments")]] + [:ul {:class (stl/css-case :comment-mode-dropdown true + :viewer-dropdown from-viewer)} + [:li {:class (stl/css-case :dropdown-item true + :selected (or (= :all cmode) (nil? cmode))) + :data-value "all" + :on-click update-mode} - [:li {:class (dom/classnames :selected (= :yours cmode)) - :on-click #(update-mode :yours)} - [:span.icon i/tick] - [:span.label (tr "labels.show-your-comments")]] - - [:hr] - - [:li {:class (dom/classnames :selected (= :pending cshow)) - :on-click #(update-show (if (= :pending cshow) :all :pending))} - [:span.icon i/tick] - [:span.label (tr "labels.hide-resolved-comments")]]])) + [:span {:class (stl/css :label)} (tr "labels.show-all-comments")] + [:span {:class (stl/css :icon)} i/tick]] + [:li {:class (stl/css-case :dropdown-item true + :selected (= :yours cmode)) + :data-value "yours" + :on-click update-mode} + [:span {:class (stl/css :label)} (tr "labels.show-your-comments")] + [:span {:class (stl/css :icon)} i/tick]] + [:li {:class (stl/css :separator)}] + [:li {:class (stl/css-case :dropdown-item true + :selected (= :pending cshow)) + :on-click update-show} + [:span {:class (stl/css :label)} (tr "labels.hide-resolved-comments")] + [:span {:class (stl/css :icon)} i/tick]]])) (mf/defc comments-sidebar - [{:keys [users threads page-id]}] + {::mf/props :obj} + [{:keys [users threads page-id from-viewer]}] (let [threads-map (mf/deref refs/threads-ref) profile (mf/deref refs/profile) users-refs (mf/deref refs/current-file-comments-users) users (or users users-refs) local (mf/deref refs/comments-local) - options? (mf/use-state false) + + state* (mf/use-state false) + options? (deref state*) + threads (if (nil? threads) (->> (vals threads-map) (sort-by :modified-at) (reverse) (dcm/apply-filters local profile)) threads) - tgroups (->> threads - (dcm/group-threads-by-page)) + + close-section + (mf/use-fn + (mf/deps from-viewer) + (fn [] + (if from-viewer + (st/emit! (dcm/update-options {:show-sidebar? false})) + (st/emit! :interrupt (dw/deselect-all true))))) + + tgroups (->> threads + (dcm/group-threads-by-page)) page-id (or page-id (mf/use-ctx ctx/current-page-id)) + toggle-mode-selector + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (swap! state* not))) + on-thread-click (mf/use-fn (mf/deps page-id) @@ -88,35 +118,43 @@ (dwcm/center-to-comment-thread thread) (-> (dcm/open-thread thread) (with-meta {::ev/origin "workspace"})))))))] + [:div {:class (stl/css-case :comments-section true + :from-viewer from-viewer)} + [:div {:class (stl/css-case :comments-section-title true + :viewer-title from-viewer)} + [:span (tr "labels.comments")] + [:button {:class (stl/css :close-button) + :on-click close-section} + i/close]] - [:div.comments-section.comment-threads-section - [:div.workspace-comment-threads-sidebar-header - [:div.label (tr "labels.comments")] - [:div.options {:on-click #(reset! options? true)} - [:div.label (case (:mode local) - (nil :all) (tr "labels.all") - :yours (tr "labels.only-yours"))] - [:div.icon i/arrow-down]] + [:button {:class (stl/css :mode-dropdown-wrapper) + :on-click toggle-mode-selector} - [:& dropdown {:show @options? - :on-close #(reset! options? false)} - [:& sidebar-options {:local local}]]] + [:span {:class (stl/css :mode-label)} (case (:mode local) + (nil :all) (tr "labels.show-all-comments") + :yours (tr "labels.show-your-comments"))] + [:div {:class (stl/css :arrow-icon)} i/arrow]] - (if (seq tgroups) - [:div.thread-groups - [:& cmt/comment-thread-group - {:group (first tgroups) - :on-thread-click on-thread-click - :users users}] - (for [tgroup (rest tgroups)] - [:* - [:hr] + [:& dropdown {:show options? + :on-close #(reset! state* false)} + [:& sidebar-options {:local local :from-viewer from-viewer}]] + + [:div {:class (stl/css :comments-section-content)} + + (if (seq tgroups) + [:div {:class (stl/css :thread-groups)} + [:& cmt/comment-thread-group + {:group (first tgroups) + :on-thread-click on-thread-click + :users users}] + (for [tgroup (rest tgroups)] [:& cmt/comment-thread-group {:group tgroup :on-thread-click on-thread-click :users users - :key (:page-id tgroup)}]])] + :key (:page-id tgroup)}])] - [:div.thread-groups-placeholder - i/chat - (tr "labels.no-comments-available")])])) + [:div {:class (stl/css :thread-group-placeholder)} + [:span {:class (stl/css :placeholder-icon)} i/comments] + [:span {:class (stl/css :placeholder-label)} + (tr "labels.no-comments-available")]])]])) diff --git a/frontend/src/app/main/ui/workspace/comments.scss b/frontend/src/app/main/ui/workspace/comments.scss new file mode 100644 index 0000000000..c1b7124f56 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/comments.scss @@ -0,0 +1,171 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.comments-section { + position: relative; + background-color: var(--panel-background-color); + display: grid; + grid-template-rows: $s-40 $s-48 1fr; +} + +.from-viewer { + padding: 0 $s-8; +} + +.comments-section-title { + @include flexCenter; + @include uppercaseTitleTipography; + position: relative; + height: $s-32; + min-height: $s-32; + margin: $s-8 $s-8 0 $s-8; + border-radius: $br-8; + background-color: var(--panel-title-background-color); + span { + @include flexCenter; + flex-grow: 1; + color: var(--title-foreground-color-hover); + } +} + +.viewer-title { + margin: 0; + margin-block-start: $s-8; +} + +.close-button { + @extend .button-tertiary; + position: absolute; + right: $s-2; + top: $s-2; + height: $s-28; + width: $s-28; + border-radius: $br-6; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.mode-dropdown-wrapper { + @include buttonStyle; + @extend .asset-element; + background-color: var(--color-background-tertiary); + display: flex; + width: 100%; + max-width: $s-256; + height: $s-32; + padding: $s-8; + border-radius: $br-8; + margin: $s-16 auto 0 auto; + cursor: pointer; + position: relative; +} + +.mode-label { + padding-right: 8px; + flex-grow: 1; + display: flex; + justify-content: flex-start; +} + +.arrow-icon { + @include flexCenter; + height: $s-24; + width: $s-24; + svg { + @extend .button-icon-small; + transform: rotate(90deg); + stroke: var(--icon-foreground); + } +} + +.comment-mode-dropdown { + @extend .dropdown-wrapper; + top: $s-92; + left: $s-12; + max-width: $s-256; + width: 100%; +} + +.viewer-dropdown { + left: $s-8; +} + +.dropdown-item { + @extend .dropdown-element-base; + justify-content: space-between; + .icon { + @include flexCenter; + height: $s-24; + width: $s-24; + svg { + @extend .button-icon-small; + stroke: transparent; + } + } + .label { + @include bodySmallTypography; + } + &:hover { + .icon svg { + stroke: transparent; + } + } + &.selected { + .label { + color: var(--menu-foreground-color); + } + .icon svg { + stroke: var(--icon-foreground-hover); + } + } +} + +.separator { + height: $s-12; +} + +.comments-section-content { + height: 100%; + overflow-y: auto; +} + +.thread-groups { + display: flex; + flex-direction: column; + gap: $s-24; +} + +.thread-group-placeholder { + @include flexColumn; + align-items: center; + justify-content: flex-start; + margin-top: $s-36; +} + +.placeholder-icon { + @include flexCenter; + height: $s-48; + width: $s-48; + border-radius: $br-circle; + background-color: var(--empty-message-background-color); + svg { + @extend .button-icon; + height: $s-28; + width: $s-28; + stroke: var(--empty-message-foreground-color); + } +} + +.placeholder-label { + @include bodySmallTypography; + text-align: center; + width: $s-184; + color: var(--empty-message-foreground-color); +} diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index c2d8995453..7f899e9470 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -6,15 +6,16 @@ (ns app.main.ui.workspace.context-menu "A workspace specific context menu (mouse right click)." - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cfh] [app.common.types.component :as ctk] - [app.common.types.components-list :as ctkl] - [app.common.types.file :as ctf] + [app.common.types.container :as ctn] [app.common.types.page :as ctp] + [app.common.types.shape.layout :as ctl] + [app.common.uuid :as uuid] [app.main.data.events :as ev] [app.main.data.modal :as modal] [app.main.data.shortcuts :as scd] @@ -29,9 +30,9 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.components.shape-icon-refactor :as sic] - [app.main.ui.context :as ctx] + [app.main.ui.components.shape-icon :as sic] [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.assets.common :as cmm] [app.util.dom :as dom] [app.util.i18n :refer [tr] :as i18n] [app.util.timers :as timers] @@ -47,10 +48,11 @@ (dom/stop-propagation event)) (mf/defc menu-entry - [{:keys [title shortcut on-click on-pointer-enter on-pointer-leave on-unmount children selected? icon] :as props}] + {::mf/props :obj} + [{:keys [title shortcut on-click on-pointer-enter on-pointer-leave + on-unmount children selected? icon disabled value]}] (let [submenu-ref (mf/use-ref nil) - hovering? (mf/use-ref false) - new-css-system (mf/use-ctx ctx/new-css-system) + hovering? (mf/use-ref false) on-pointer-enter (mf/use-callback (fn [] @@ -84,76 +86,54 @@ (constantly on-unmount)) (if icon - [:li {:class (if new-css-system - (dom/classnames (css :icon-menu-item) true) - (dom/classnames :icon-menu-item true)) + [:li {:class (stl/css :icon-menu-item) + :disabled disabled + :data-value value :ref set-dom-node :on-click on-click :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave} [:span - {:class (if new-css-system - (dom/classnames (css :icon-wrapper) true) - (dom/classnames :icon-wrapper true))} - (if selected? [:span {:class (if new-css-system - (dom/classnames (css :selected-icon) true) - (dom/classnames :selected-icon true))} - (if new-css-system - i/tick-refactor - i/tick)] - [:span {:class (if new-css-system - (dom/classnames (css :selected-icon) true) - (dom/classnames :selected-icon true))}]) - [:span {:class (if new-css-system - (dom/classnames (css :shape-icon) true) - (dom/classnames :shape-icon true))} icon]] - [:span {:class (if new-css-system - (dom/classnames (css :title) true) - (dom/classnames :title true))} title]] - [:li {:class (dom/classnames (css :context-menu-item) new-css-system) + {:class (stl/css :icon-wrapper)} + (if selected? [:span {:class (stl/css :selected-icon)} + i/tick] + [:span {:class (stl/css :selected-icon)}]) + [:span {:class (stl/css :shape-icon)} icon]] + [:span {:class (stl/css :title)} title]] + [:li {:class (stl/css :context-menu-item) + :disabled disabled :ref set-dom-node + :data-value value :on-click on-click :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave} - [:span {:class (if new-css-system - (dom/classnames (css :title) true) - (dom/classnames :title true))} title] + [:span {:class (stl/css :title)} title] (when shortcut - [:span {:class (if new-css-system - (dom/classnames (css :shortcut) true) - (dom/classnames :shortcut true))} - (if new-css-system - (for [sc (scd/split-sc shortcut)] - [:span {:class (dom/classnames (css :shortcut-key) true)} sc]) - (or shortcut ""))]) + [:span {:class (stl/css :shortcut)} + (for [[idx sc] (d/enumerate (scd/split-sc shortcut))] + [:span {:key (dm/str shortcut "-" idx) + :class (stl/css :shortcut-key)} sc])]) (when (> (count children) 1) - (if new-css-system - [:span {:class (dom/classnames (css :submenu-icon) true)} i/arrow-refactor] - [:span.submenu-icon i/arrow-slide])) + [:span {:class (stl/css :submenu-icon)} i/arrow]) (when (> (count children) 1) - [:ul - {:class (if new-css-system - (dom/classnames (css :workspace-context-submenu) true) - (dom/classnames :workspace-context-menu true)) - :ref submenu-ref - :style {:display "none" :left 250} - :on-context-menu prevent-default} + [:ul {:class (stl/css :workspace-context-submenu) + :ref submenu-ref + :style {:display "none" :left 250} + :on-context-menu prevent-default} children])]))) + (mf/defc menu-separator [] - (let [new-css-system (mf/use-ctx ctx/new-css-system)] - [:li {:class (if new-css-system - (dom/classnames (css :separator) true) - (dom/classnames :separator true))}])) + [:li {:class (stl/css :separator)}]) (mf/defc context-menu-edit [_] (let [do-copy #(st/emit! (dw/copy-selected)) do-cut #(st/emit! (dw/copy-selected) (dw/delete-selected)) - do-paste #(st/emit! dw/paste) + do-paste #(st/emit! (dw/paste-from-clipboard)) do-duplicate #(st/emit! (dw/duplicate-selected true))] [:* [:& menu-entry {:title (tr "workspace.shape.menu.copy") @@ -200,7 +180,7 @@ :on-pointer-enter (on-pointer-enter (:id object)) :on-pointer-leave (on-pointer-leave (:id object)) :on-unmount (on-unmount (:id object)) - :icon (sic/element-icon-refactor {:shape object})}])]) + :icon (sic/element-icon {:shape object})}])]) [:& menu-entry {:title (tr "workspace.shape.menu.forward") :shortcut (sc/get-tooltip :bring-forward) :on-click do-bring-forward}] @@ -233,11 +213,11 @@ (mf/defc context-menu-thumbnail [{:keys [shapes]}] (let [single? (= (count shapes) 1) - has-frame? (some cph/frame-shape? shapes) + has-frame? (some cfh/frame-shape? shapes) do-toggle-thumbnail #(st/emit! (dw/toggle-file-thumbnail-selected))] (when (and single? has-frame?) [:* - (if (every? :use-for-thumbnail? shapes) + (if (every? :use-for-thumbnail shapes) [:& menu-entry {:title (tr "workspace.shape.menu.thumbnail-remove") :on-click do-toggle-thumbnail}] [:& menu-entry {:title (tr "workspace.shape.menu.thumbnail-set") @@ -248,47 +228,54 @@ (mf/defc context-menu-group [{:keys [shapes]}] - (let [multiple? (> (count shapes) 1) - single? (= (count shapes) 1) - do-create-artboard-from-selection #(st/emit! (dwsh/create-artboard-from-selection)) + (let [multiple? (> (count shapes) 1) + single? (= (count shapes) 1) - has-frame? (->> shapes (d/seek cph/frame-shape?)) - has-group? (->> shapes (d/seek cph/group-shape?)) - has-bool? (->> shapes (d/seek cph/bool-shape?)) - has-mask? (->> shapes (d/seek :masked-group?)) + objects (deref refs/workspace-page-objects) + any-in-copy? (some true? (map #(ctn/has-any-copy-parent? objects %) shapes)) - is-group? (and single? has-group?) - is-bool? (and single? has-bool?) + ;; components can't be ungrouped + has-frame? (->> shapes (d/seek #(and (cfh/frame-shape? %) (not (ctk/instance-head? %))))) + has-group? (->> shapes (d/seek #(and (cfh/group-shape? %) (not (ctk/instance-head? %))))) + has-bool? (->> shapes (d/seek cfh/bool-shape?)) + has-mask? (->> shapes (d/seek :masked-group)) + + is-group? (and single? has-group?) + is-bool? (and single? has-bool?) do-create-group #(st/emit! dw/group-selected) do-mask-group #(st/emit! dw/mask-group) do-remove-group #(st/emit! dw/ungroup-selected) - do-unmask-group #(st/emit! dw/unmask-group)] + do-unmask-group #(st/emit! dw/unmask-group) + do-create-artboard-from-selection + #(st/emit! (dwsh/create-artboard-from-selection))] [:* - (when (or has-bool? has-group? has-mask? has-frame?) - [:& menu-entry {:title (tr "workspace.shape.menu.ungroup") - :shortcut (sc/get-tooltip :ungroup) - :on-click do-remove-group}]) + (when (not any-in-copy?) + [:* + (when (or has-bool? has-group? has-mask? has-frame?) + [:& menu-entry {:title (tr "workspace.shape.menu.ungroup") + :shortcut (sc/get-tooltip :ungroup) + :on-click do-remove-group}]) - [:& menu-entry {:title (tr "workspace.shape.menu.group") - :shortcut (sc/get-tooltip :group) - :on-click do-create-group}] + [:& menu-entry {:title (tr "workspace.shape.menu.group") + :shortcut (sc/get-tooltip :group) + :on-click do-create-group}] - (when (or multiple? (and is-group? (not has-mask?)) is-bool?) - [:& menu-entry {:title (tr "workspace.shape.menu.mask") - :shortcut (sc/get-tooltip :mask) - :on-click do-mask-group}]) + (when (or multiple? (and is-group? (not has-mask?)) is-bool?) + [:& menu-entry {:title (tr "workspace.shape.menu.mask") + :shortcut (sc/get-tooltip :mask) + :on-click do-mask-group}]) - (when has-mask? - [:& menu-entry {:title (tr "workspace.shape.menu.unmask") - :shortcut (sc/get-tooltip :unmask) - :on-click do-unmask-group}]) + (when has-mask? + [:& menu-entry {:title (tr "workspace.shape.menu.unmask") + :shortcut (sc/get-tooltip :unmask) + :on-click do-unmask-group}]) - [:& menu-entry {:title (tr "workspace.shape.menu.create-artboard-from-selection") - :shortcut (sc/get-tooltip :artboard-selection) - :on-click do-create-artboard-from-selection}] - [:& menu-separator]])) + [:& menu-entry {:title (tr "workspace.shape.menu.create-artboard-from-selection") + :shortcut (sc/get-tooltip :artboard-selection) + :on-click do-create-artboard-from-selection}] + [:& menu-separator]])])) (mf/defc context-focus-mode-menu [{:keys []}] @@ -306,10 +293,10 @@ (let [multiple? (> (count shapes) 1) single? (= (count shapes) 1) - has-group? (->> shapes (d/seek cph/group-shape?)) - has-bool? (->> shapes (d/seek cph/bool-shape?)) - has-frame? (->> shapes (d/seek cph/frame-shape?)) - has-path? (->> shapes (d/seek cph/path-shape?)) + has-group? (->> shapes (d/seek cfh/group-shape?)) + has-bool? (->> shapes (d/seek cfh/bool-shape?)) + has-frame? (->> shapes (d/seek cfh/frame-shape?)) + has-path? (->> shapes (d/seek cfh/path-shape?)) is-group? (and single? has-group?) is-bool? (and single? has-bool?) @@ -395,7 +382,7 @@ prototype? (= options-mode :prototype) single? (= (count shapes) 1) - has-frame? (->> shapes (d/seek cph/frame-shape?)) + has-frame? (->> shapes (d/seek cfh/frame-shape?)) is-frame? (and single? has-frame?)] (when (and prototype? is-frame?) @@ -406,194 +393,99 @@ [:& menu-entry {:title (tr "workspace.shape.menu.flow-start") :on-click do-add-flow}]))))) -(mf/defc context-menu-flex - [{:keys [shapes]}] - (let [single? (= (count shapes) 1) - has-frame? (->> shapes (d/seek cph/frame-shape?)) - is-frame? (and single? has-frame?) - is-flex-container? (and is-frame? (= :flex (:layout (first shapes)))) - ids (->> shapes (map :id)) - add-flex #(st/emit! (if is-frame? - (dwsl/create-layout-from-id ids :flex true) - (dwsl/create-layout-from-selection :flex))) - remove-flex #(st/emit! (dwsl/remove-layout ids))] +(mf/defc context-menu-layout + {::mf/props :obj} + [{:keys [shapes]}] + (let [single? (= (count shapes) 1) + objects (deref refs/workspace-page-objects) + any-in-copy? (some true? (map #(ctn/has-any-copy-parent? objects %) shapes)) + + has-flex? + (and single? (every? ctl/flex-layout? shapes)) + + has-grid? + (and single? (every? ctl/grid-layout? shapes)) + + on-add-layout + (mf/use-fn + (fn [event] + (let [type (-> (dom/get-current-target event) + (dom/get-data "value") + (keyword))] + (st/emit! (with-meta (dwsl/create-layout type) + {::ev/origin "workspace:context-menu"}))))) + + on-remove-layout + (mf/use-fn + (mf/deps shapes) + (fn [_event] + (let [ids (map :id shapes)] + (st/emit! (dwsl/remove-layout ids)))))] [:* - (when (not is-flex-container?) - [:div - [:& menu-separator] - [:& menu-entry {:title (tr "workspace.shape.menu.add-flex") - :shortcut (sc/get-tooltip :toggle-layout-flex) - :on-click add-flex}]]) - (when is-flex-container? - [:div - [:& menu-separator] - [:& menu-entry {:title (tr "workspace.shape.menu.remove-flex") - :shortcut (sc/get-tooltip :toggle-layout-flex) - :on-click remove-flex}]])])) + (when (not any-in-copy?) + (if (or ^boolean has-flex? + ^boolean has-grid?) + [:div + [:& menu-separator] + (if has-flex? + [:& menu-entry {:title (tr "workspace.shape.menu.remove-flex") + :shortcut (sc/get-tooltip :toggle-layout-flex) + :on-click on-remove-layout}] + [:& menu-entry {:title (tr "workspace.shape.menu.remove-grid") + :shortcut (sc/get-tooltip :toggle-layout-grid) + :on-click on-remove-layout}])] + + [:div + [:& menu-separator] + [:& menu-entry {:title (tr "workspace.shape.menu.add-flex") + :shortcut (sc/get-tooltip :toggle-layout-flex) + :value "flex" + :on-click on-add-layout}] + [:& menu-entry {:title (tr "workspace.shape.menu.add-grid") + :shortcut (sc/get-tooltip :toggle-layout-grid) + :value "grid" + :on-click on-add-layout}]]))])) (mf/defc context-menu-component [{:keys [shapes]}] - (let [single? (= (count shapes) 1) - components-v2 (features/use-feature :components-v2) - - has-component? (some true? (map #(contains? % :component-id) shapes)) - is-component? (and single? (-> shapes first :component-id some?)) - in-copy-not-root? (some true? (map #(ctk/in-component-copy-not-root? %) shapes)) - - objects (deref refs/workspace-page-objects) - touched? (and single? (cph/component-touched? objects (:id (first shapes)))) - can-update-main? (or (not components-v2) touched?) - - first-shape (first shapes) - {:keys [id component-id component-file main-instance?]} first-shape - component-shapes (filter #(contains? % :component-id) shapes) - - - current-file-id (mf/use-ctx ctx/current-file-id) - local-component? (= component-file current-file-id) - remote-components (filter #(not= (:component-file %) current-file-id) - component-shapes) - - workspace-data (deref refs/workspace-data) - workspace-libraries (deref refs/workspace-libraries) - component (if local-component? - (ctkl/get-component workspace-data component-id) - (ctf/get-component workspace-libraries component-file component-id)) - is-dangling? (nil? component) - lacks-annotation? (nil? (:annotation component)) - lib-exists? (and (not local-component?) - (some? (get workspace-libraries component-file))) - - do-add-component #(st/emit! (dwl/add-component)) - do-add-multiple-components #(st/emit! (dwl/add-multiple-components)) - do-detach-component #(st/emit! (dwl/detach-component id)) - do-detach-component-in-bulk #(st/emit! dwl/detach-selected-components) - do-reset-component #(st/emit! (dwl/reset-component id)) - do-show-component #(st/emit! (dw/go-to-component component-id)) - do-show-in-assets #(st/emit! (if components-v2 - (dw/show-component-in-assets component-id) - (dw/go-to-component component-id))) - create-annotation #(when components-v2 - (st/emit! (dw/set-annotations-id-for-create (:id first-shape)))) - - do-navigate-component-file #(st/emit! (dwl/nav-to-component-file component-file)) - do-update-component #(st/emit! (dwl/update-component-sync id component-file)) - do-update-component-in-bulk #(st/emit! (dwl/update-component-in-bulk component-shapes component-file)) - do-restore-component #(st/emit! (dwl/restore-component component-file component-id) - (dw/go-to-main-instance nil component-id)) - - do-update-remote-component - #(st/emit! (modal/show - {:type :confirm - :message "" - :title (tr "modals.update-remote-component.message") - :hint (tr "modals.update-remote-component.hint") - :cancel-label (tr "modals.update-remote-component.cancel") - :accept-label (tr "modals.update-remote-component.accept") - :accept-style :primary - :on-accept do-update-component})) - - do-update-in-bulk (fn [] - (if (empty? remote-components) - (do-update-component-in-bulk) - #(st/emit! (modal/show - {:type :confirm - :message "" - :title (tr "modals.update-remote-component-in-bulk.message") - :hint (tr "modals.update-remote-component-in-bulk.hint") - :items remote-components - :cancel-label (tr "modals.update-remote-component.cancel") - :accept-label (tr "modals.update-remote-component.accept") - :accept-style :primary - :on-accept do-update-component-in-bulk}))))] + (let [components-v2 (features/use-feature "components/v2") + single? (= (count shapes) 1) + objects (deref refs/workspace-page-objects) + can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) shapes)) + heads (filter ctk/instance-head? shapes) + components-menu-entries (cmm/generate-components-menu-entries heads components-v2) + do-add-component #(st/emit! (dwl/add-component)) + do-add-multiple-components #(st/emit! (dwl/add-multiple-components))] [:* - [:* - (when (or (not in-copy-not-root?) (and has-component? (not single?))) - [:& menu-separator]) - (when-not in-copy-not-root? - [:& menu-entry {:title (tr "workspace.shape.menu.create-component") - :shortcut (sc/get-tooltip :create-component) - :on-click do-add-component}]) - (when-not (or single? in-copy-not-root?) - [:& menu-entry {:title (tr "workspace.shape.menu.create-multiple-components") - :on-click do-add-multiple-components}]) - (when (and has-component? (not single?)) - [:* - [:& menu-entry {:title (tr "workspace.shape.menu.detach-instances-in-bulk") - :shortcut (sc/get-tooltip :detach-component) - :on-click do-detach-component-in-bulk}] - [:& menu-entry {:title (tr "workspace.shape.menu.update-components-in-bulk") - :on-click do-update-in-bulk}]])] - - (when is-component? - ;; WARNING: this menu is the same as the context menu at the sidebar. - ;; If you change it, you must change equally the file - ;; app/main/ui/workspace/sidebar/options/menus/component.cljs + (when can-make-component ;; We don't want to change the structure of component copies [:* [:& menu-separator] - (if main-instance? - [:* - [:& menu-entry {:title (tr "workspace.shape.menu.show-in-assets") - :on-click do-show-in-assets}] - (when (and components-v2 local-component? lacks-annotation?) - [:& menu-entry {:title (tr "workspace.shape.menu.create-annotation") - :on-click create-annotation}])] - (if local-component? - (if is-dangling? - [:* - [:& menu-entry {:title (tr "workspace.shape.menu.detach-instance") - :shortcut (sc/get-tooltip :detach-component) - :on-click do-detach-component}] - (when can-update-main? - [:& menu-entry {:title (tr "workspace.shape.menu.reset-overrides") - :on-click do-reset-component}]) - (when components-v2 - [:& menu-entry {:title (tr "workspace.shape.menu.restore-main") - :on-click do-restore-component}])] - [:* - [:& menu-entry {:title (tr "workspace.shape.menu.detach-instance") - :shortcut (sc/get-tooltip :detach-component) - :on-click do-detach-component}] - (when can-update-main? - [:* - [:& menu-entry {:title (tr "workspace.shape.menu.reset-overrides") - :on-click do-reset-component}] - [:& menu-entry {:title (tr "workspace.shape.menu.update-main") - :on-click do-update-component}]]) - [:& menu-entry {:title (tr "workspace.shape.menu.show-main") - :on-click do-show-component}]]) - (if is-dangling? - [:* - [:& menu-entry {:title (tr "workspace.shape.menu.detach-instance") - :shortcut (sc/get-tooltip :detach-component) - :on-click do-detach-component}] - (when can-update-main? - [:& menu-entry {:title (tr "workspace.shape.menu.reset-overrides") - :on-click do-reset-component}]) - (when (and components-v2 lib-exists?) - [:& menu-entry {:title (tr "workspace.shape.menu.restore-main") - :on-click do-restore-component}])] - [:* - [:& menu-entry {:title (tr "workspace.shape.menu.detach-instance") - :shortcut (sc/get-tooltip :detach-component) - :on-click do-detach-component}] - (when can-update-main? - [:* - [:& menu-entry {:title (tr "workspace.shape.menu.reset-overrides") - :on-click do-reset-component}] - [:& menu-entry {:title (tr "workspace.shape.menu.update-main") - :on-click do-update-remote-component}]]) - [:& menu-entry {:title (tr "workspace.shape.menu.go-main") - :on-click do-navigate-component-file}]])))]) - [:& menu-separator]])) + + [:& menu-entry {:title (tr "workspace.shape.menu.create-component") + :shortcut (sc/get-tooltip :create-component) + :on-click do-add-component}] + (when (not single?) + [:& menu-entry {:title (tr "workspace.shape.menu.create-multiple-components") + :on-click do-add-multiple-components}])]) + + (when (seq components-menu-entries) + [:* + [:& menu-separator] + (for [entry components-menu-entries :when (not (nil? entry))] + [:& menu-entry {:key (uuid/next) + :title (tr (:msg entry)) + :shortcut (when (contains? entry :shortcut) (sc/get-tooltip (:shortcut entry))) + :on-click (:action entry)}])])])) (mf/defc context-menu-delete [] (let [do-delete #(st/emit! (dw/delete-selected))] - [:& menu-entry {:title (tr "workspace.shape.menu.delete") - :shortcut (sc/get-tooltip :delete) - :on-click do-delete}])) + [:* + [:& menu-separator] + [:& menu-entry {:title (tr "workspace.shape.menu.delete") + :shortcut (sc/get-tooltip :delete) + :on-click do-delete}]])) (mf/defc shape-context-menu {::mf/wrap [mf/memo]} @@ -614,7 +506,7 @@ [:> context-menu-path props] [:> context-menu-layer-options props] [:> context-menu-prototype props] - [:> context-menu-flex props] + [:> context-menu-layout props] [:> context-menu-component props] [:> context-menu-delete props]]))) @@ -645,7 +537,7 @@ (mf/defc viewport-context-menu [] (let [focus (mf/deref refs/workspace-focus-selected) - do-paste #(st/emit! dw/paste) + do-paste #(st/emit! (dw/paste-from-clipboard)) do-hide-ui #(st/emit! (-> (dw/toggle-layout-flag :hide-ui) (vary-meta assoc ::ev/origin "workspace-context-menu"))) do-toggle-focus-mode #(st/emit! (dw/toggle-focus-mode))] @@ -662,13 +554,97 @@ :shortcut (sc/get-tooltip :toggle-focus-mode) :on-click do-toggle-focus-mode}])])) +(mf/defc grid-track-context-menu + [{:keys [mdata] :as props}] + (let [{:keys [type index grid-id]} mdata + do-delete-track + (mf/use-callback + (mf/deps grid-id type index) + (fn [] + (st/emit! (dwsl/remove-layout-track [grid-id] type index)))) + + do-add-track-before + (mf/use-callback + (mf/deps grid-id type index) + (fn [] + (st/emit! (dwsl/add-layout-track [grid-id] type ctl/default-track-value index)))) + + do-add-track-after + (mf/use-callback + (mf/deps grid-id type index) + (fn [] + (st/emit! (dwsl/add-layout-track [grid-id] type ctl/default-track-value (inc index))))) + + do-duplicate-track + (mf/use-callback + (mf/deps grid-id type index) + (fn [] + (st/emit! (dwsl/duplicate-layout-track [grid-id] type index)))) + + do-delete-track-shapes + (mf/use-callback + (mf/deps grid-id type index) + (fn [] + (st/emit! (dwsl/remove-layout-track [grid-id] type index {:with-shapes? true}))))] + + (if (= type :column) + [:* + [:& menu-entry {:title (tr "workspace.context-menu.grid-track.column.duplicate") :on-click do-duplicate-track}] + [:& menu-entry {:title (tr "workspace.context-menu.grid-track.column.add-before") :on-click do-add-track-before}] + [:& menu-entry {:title (tr "workspace.context-menu.grid-track.column.add-after") :on-click do-add-track-after}] + [:& menu-entry {:title (tr "workspace.context-menu.grid-track.column.delete") :on-click do-delete-track}] + [:& menu-entry {:title (tr "workspace.context-menu.grid-track.column.delete-shapes") :on-click do-delete-track-shapes}]] + + [:* + [:& menu-entry {:title (tr "workspace.context-menu.grid-track.row.duplicate") :on-click do-duplicate-track}] + [:& menu-entry {:title (tr "workspace.context-menu.grid-track.row.add-before") :on-click do-add-track-before}] + [:& menu-entry {:title (tr "workspace.context-menu.grid-track.row.add-after") :on-click do-add-track-after}] + [:& menu-entry {:title (tr "workspace.context-menu.grid-track.row.delete") :on-click do-delete-track}] + [:& menu-entry {:title (tr "workspace.context-menu.grid-track.row.delete-shapes") :on-click do-delete-track-shapes}]]))) + +(mf/defc grid-cells-context-menu + [{:keys [mdata] :as props}] + (let [{:keys [grid cells]} mdata + + single? (= (count cells) 1) + + can-merge? + (mf/use-memo + (mf/deps cells) + #(ctl/valid-area-cells? cells)) + + do-merge-cells + (mf/use-callback + (mf/deps grid cells) + (fn [] + (st/emit! (dwsl/merge-cells (:id grid) (map :id cells))))) + + do-create-board + (mf/use-callback + (mf/deps grid cells) + (fn [] + (st/emit! (dwsl/create-cell-board (:id grid) (map :id cells)))))] + [:* + (when (not single?) + [:& menu-entry {:title (tr "workspace.context-menu.grid-cells.merge") + :on-click do-merge-cells + :disabled (not can-merge?)}]) + + (when single? + [:& menu-entry {:title (tr "workspace.context-menu.grid-cells.area") + :on-click do-merge-cells}]) + + [:& menu-entry {:title (tr "workspace.context-menu.grid-cells.create-board") + :on-click do-create-board + :disabled (and (not single?) (not can-merge?))}]])) + + (mf/defc context-menu [] (let [mdata (mf/deref menu-ref) top (- (get-in mdata [:position :y]) 20) left (get-in mdata [:position :x]) - dropdown-ref (mf/use-ref) - new-css-system (mf/use-ctx ctx/new-css-system)] + dropdown-ref (mf/use-ref)] (mf/use-effect (mf/deps mdata) @@ -685,17 +661,15 @@ [:& dropdown {:show (boolean mdata) :on-close #(st/emit! dw/hide-context-menu)} - [:ul - {:class (if new-css-system - (dom/classnames (css :workspace-context-menu) true) - (dom/classnames :workspace-context-menu true)) - :ref dropdown-ref - :style {:top top :left left} - :on-context-menu prevent-default} - - (case (:kind mdata) - :shape [:& shape-context-menu {:mdata mdata}] - :page [:& page-item-context-menu {:mdata mdata}] - [:& viewport-context-menu {:mdata mdata}])]])) - + [:div {:class (stl/css :workspace-context-menu) + :ref dropdown-ref + :style {:top top :left left} + :on-context-menu prevent-default} + [:ul {:class (stl/css :context-list)} + (case (:kind mdata) + :shape [:& shape-context-menu {:mdata mdata}] + :page [:& page-item-context-menu {:mdata mdata}] + :grid-track [:& grid-track-context-menu {:mdata mdata}] + :grid-cells [:& grid-cells-context-menu {:mdata mdata}] + [:& viewport-context-menu {:mdata mdata}])]]])) diff --git a/frontend/src/app/main/ui/workspace/context_menu.css.json b/frontend/src/app/main/ui/workspace/context_menu.css.json deleted file mode 100644 index a41facee1a..0000000000 --- a/frontend/src/app/main/ui/workspace/context_menu.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"workspace_context_menu_button-primary_d6q-P","button-secondary":"workspace_context_menu_button-secondary_bIdqe","button-icon":"workspace_context_menu_button-icon_tXvxe","button-icon-small":"workspace_context_menu_button-icon-small_c0rVU","workspace-context-menu":"workspace_context_menu_workspace-context-menu_2NyvR","icon-menu-item":"workspace_context_menu_icon-menu-item_P3-bA","shape-icon":"workspace_context_menu_shape-icon_xx1Ll","workspace-context-submenu":"workspace_context_menu_workspace-context-submenu_BUNLt","selected-icon":"workspace_context_menu_selected-icon_pZqBp","context-menu-item":"workspace_context_menu_context-menu-item_Tx-Ty","submenu-icon":"workspace_context_menu_submenu-icon_JwYm8","separator":"workspace_context_menu_separator_E9-aR","title":"workspace_context_menu_title_P8iFL","shortcut":"workspace_context_menu_shortcut_rypUe","shortcut-key":"workspace_context_menu_shortcut-key_3rF3t","icon-wrapper":"workspace_context_menu_icon-wrapper_n7VO2"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/context_menu.scss b/frontend/src/app/main/ui/workspace/context_menu.scss index 9bda90ad96..66e58c8908 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.scss +++ b/frontend/src/app/main/ui/workspace/context_menu.scss @@ -6,107 +6,121 @@ @import "refactor/common-refactor.scss"; -.workspace-context-menu, -.workspace-context-submenu { +.workspace-context-menu { position: absolute; top: $s-40; left: $s-736; - display: flex; - flex-direction: column; + z-index: $z-index-4; +} + +.context-list, +.workspace-context-submenu { + @include menuShadow; + display: grid; width: $s-240; padding: $s-4; border-radius: $br-8; + border: $s-2 solid var(--panel-border-color); background-color: var(--menu-background-color); - z-index: $z-index-20; - box-shadow: 0px 0px $s-12 0px var(--menu-shadow-color); - .separator { - height: $s-12; - } - .context-menu-item { - display: flex; - align-items: center; - justify-content: space-between; - height: $s-28; - width: 100%; - padding: $s-6; - border-radius: $br-8; - cursor: pointer; + max-height: 100vh; + overflow-y: auto; +} +.workspace-context-submenu { + position: absolute; +} + +.separator { + height: $s-12; +} + +.context-menu-item { + display: flex; + align-items: center; + justify-content: space-between; + height: $s-28; + width: 100%; + padding: $s-6; + border-radius: $br-8; + cursor: pointer; + + .title { + @include bodySmallTypography; + color: var(--menu-foreground-color); + } + .shortcut { + @include flexCenter; + gap: $s-2; + color: var(--menu-shortcut-foreground-color); + .shortcut-key { + @include bodySmallTypography; + @include flexCenter; + height: $s-20; + padding: $s-2 $s-6; + border-radius: $br-6; + background-color: var(--menu-shortcut-background-color); + } + } + + .submenu-icon svg { + @extend .button-icon-small; + stroke: var(--menu-foreground-color); + } + + &:hover { + background-color: var(--menu-background-color-hover); .title { - @include titleTipography; - color: var(--menu-foreground-color); + color: var(--menu-foreground-color-hover); } .shortcut { - @include flexCenter; - gap: $s-2; - color: var(--menu-shortcut-foreground-color); - .shortcut-key { - @include titleTipography; - @include flexCenter; - height: $s-20; - padding: $s-2 $s-6; - border-radius: $br-6; - background-color: var(--menu-shortcut-background-color); - } - } - - .submenu-icon { - position: absolute; - right: $s-16; - svg { - @extend .button-icon-small; - stroke: var(--menu-foreground-color); - } - } - &:hover { - background-color: var(--menu-background-color-hover); - .title { - color: var(--menu-foreground-color-hover); - } - .shortcut { - color: var(--menu-shortcut-foreground-color-hover); - } - } - &:focus { - border: 1px solid var(--menu-border-color-focus); - background-color: var(--menu-background-color-focus); + color: var(--menu-shortcut-foreground-color-hover); } } - - .icon-menu-item { - display: flex; - justify-content: flex-start; - align-items: center; - height: $s-28; - padding: $s-6; - border-radius: $br-8; - &:hover { - background-color: var(--menu-background-color-hover); - } - - span.title { - margin-left: $s-6; - } - - .selected-icon { - svg { - @extend .button-icon-small; - stroke: var(--menu-foreground-color); - } - } - - .shape-icon { - margin-left: $s-2; - svg { - @extend .button-icon-small; - stroke: var(--menu-foreground-color); - } - } - - .icon-wrapper { - display: grid; - grid-template-columns: 1fr 1fr; - margin: 0; - } + &:focus { + border: 1px solid var(--menu-border-color-focus); + background-color: var(--menu-background-color-focus); } } + +.icon-menu-item { + display: flex; + justify-content: flex-start; + align-items: center; + height: $s-28; + padding: $s-6; + border-radius: $br-8; + &:hover { + background-color: var(--menu-background-color-hover); + } + + span.title { + margin-left: $s-6; + } + + .selected-icon { + svg { + @extend .button-icon-small; + stroke: var(--menu-foreground-color); + } + } + + .shape-icon { + margin-left: $s-2; + svg { + @extend .button-icon-small; + stroke: var(--menu-foreground-color); + } + } + + .icon-wrapper { + display: grid; + grid-template-columns: 1fr 1fr; + margin: 0; + } +} + +.icon-menu-item[disabled], +.context-menu-item[disabled] { + pointer-events: none; + opacity: 0.6; +} diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs deleted file mode 100644 index 81f03930db..0000000000 --- a/frontend/src/app/main/ui/workspace/header.cljs +++ /dev/null @@ -1,674 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.workspace.header - (:require - [app.common.pages.helpers :as cph] - [app.common.uuid :as uuid] - [app.config :as cf] - [app.main.data.events :as ev] - [app.main.data.exports :as de] - [app.main.data.modal :as modal] - [app.main.data.workspace :as dw] - [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.shortcuts :as sc] - [app.main.refs :as refs] - [app.main.repo :as rp] - [app.main.store :as st] - [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.context :as ctx] - [app.main.ui.export :refer [export-progress-widget]] - [app.main.ui.formats :as fmt] - [app.main.ui.hooks.resize :as r] - [app.main.ui.icons :as i] - [app.main.ui.workspace.presence :refer [active-sessions]] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [tr]] - [app.util.keyboard :as kbd] - [app.util.router :as rt] - [beicon.core :as rx] - [cuerdas.core :as str] - [okulary.core :as l] - [potok.core :as ptk] - [rumext.v2 :as mf])) - -(def ref:workspace-persistence - (l/derived :workspace-persistence st/state)) - -;; --- Persistence state Widget - -(mf/defc persistence-state-widget - {::mf/wrap [mf/memo]} - [] - (let [{:keys [status]} (mf/deref ref:workspace-persistence)] - [:div.persistence-status-widget - (case status - :pending - [:div.pending - [:span.label (tr "workspace.header.unsaved")]] - - :saving - [:div.saving - [:span.icon i/toggle] - [:span.label (tr "workspace.header.saving")]] - - :saved - [:div.saved - [:span.icon i/tick] - [:span.label (tr "workspace.header.saved")]] - - :error - [:div.error {:title "There was an error saving the data. Please refresh if this persists."} - [:span.icon i/msg-warning] - [:span.label (tr "workspace.header.save-error")]] - - nil)])) - -;; --- Zoom Widget - -(mf/defc zoom-widget-workspace - {::mf/wrap [mf/memo] - ::mf/wrap-props false} - [{:keys [zoom on-increase on-decrease on-zoom-reset on-zoom-fit on-zoom-selected]}] - (let [open* (mf/use-state false) - open? (deref open*) - - open-dropdown - (mf/use-fn #(reset! open* true)) - - close-dropdown - (mf/use-fn #(reset! open* false)) - - on-increase - (mf/use-fn - (mf/deps on-increase) - (fn [event] - (dom/stop-propagation event) - (on-increase))) - - on-decrease - (mf/use-fn - (mf/deps on-decrease) - (fn [event] - (dom/stop-propagation event) - (on-decrease))) - - zoom (fmt/format-percent zoom {:precision 0})] - - [:div.zoom-widget {:on-click open-dropdown} - [:span.label zoom] - [:span.icon i/arrow-down] - [:& dropdown {:show open? :on-close close-dropdown} - [:ul.dropdown - [:li.basic-zoom-bar - [:span.zoom-btns - [:button {:on-click on-decrease} "-"] - [:p.zoom-size zoom] - [:button {:on-click on-increase} "+"]] - [:button.reset-btn {:on-click on-zoom-reset} (tr "workspace.header.reset-zoom")]] - [:li.separator] - [:li {:on-click on-zoom-fit} - (tr "workspace.header.zoom-fit-all") [:span (sc/get-tooltip :fit-all)]] - [:li {:on-click on-zoom-selected} - (tr "workspace.header.zoom-selected") [:span (sc/get-tooltip :zoom-selected)]]]]])) - -;; --- Header Users - -(mf/defc help-info-menu - {::mf/wrap-props false - ::mf/wrap [mf/memo]} - [{:keys [layout on-close]}] - (let [nav-to-helpc-center - (mf/use-fn #(dom/open-new-window "https://help.penpot.app")) - - nav-to-community - (mf/use-fn #(dom/open-new-window "https://community.penpot.app")) - - nav-to-youtube - (mf/use-fn #(dom/open-new-window "https://www.youtube.com/c/Penpot")) - - nav-to-templates - (mf/use-fn #(dom/open-new-window "https://penpot.app/libraries-templates")) - - nav-to-github - (mf/use-fn #(dom/open-new-window "https://github.com/penpot/penpot")) - - nav-to-terms - (mf/use-fn #(dom/open-new-window "https://penpot.app/terms")) - - nav-to-feedback - (mf/use-fn #(st/emit! (rt/nav-new-window* {:rname :settings-feedback}))) - - show-shortcuts - (mf/use-fn - (mf/deps layout) - (fn [] - (when (contains? layout :collapse-left-sidebar) - (st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) - - (st/emit! - (-> (dw/toggle-layout-flag :shortcuts) - (vary-meta assoc ::ev/origin "workspace-header"))))) - - show-release-notes - (mf/use-fn - (fn [event] - (let [version (:main cf/version)] - (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) - (if (and (kbd/alt? event) (kbd/mod? event)) - (st/emit! (modal/show {:type :onboarding})) - (st/emit! (modal/show {:type :release-notes :version version}))))))] - - [:& dropdown {:show true :on-close on-close} - [:ul.sub-menu.help-info - [:li {:on-click nav-to-helpc-center} - [:span (tr "labels.help-center")]] - [:li {:on-click nav-to-community} - [:span (tr "labels.community")]] - [:li {:on-click nav-to-youtube} - [:span (tr "labels.tutorials")]] - [:li {:on-click show-release-notes} - [:span (tr "labels.release-notes")]] - [:li.separator {:on-click nav-to-templates} - [:span (tr "labels.libraries-and-templates")]] - [:li {:on-click nav-to-github} - [:span (tr "labels.github-repo")]] - [:li {:on-click nav-to-terms} - [:span (tr "auth.terms-of-service")]] - [:li.separator {:on-click show-shortcuts} - [:span (tr "label.shortcuts")] - [:span.shortcut (sc/get-tooltip :show-shortcuts)]] - - (when (contains? cf/flags :user-feedback) - [:* - [:li.feedback {:on-click nav-to-feedback} - [:span (tr "labels.give-feedback")]]])]])) - -(mf/defc preferences-menu - {::mf/wrap-props false - ::mf/wrap [mf/memo]} - [{:keys [layout toggle-flag on-close]}] - (let [show-nudge-options (mf/use-fn #(modal/show! {:type :nudge-option}))] - - [:& dropdown {:show true :on-close on-close} - [:ul.sub-menu.preferences - [:li {:on-click toggle-flag - :data-flag "scale-text"} - [:span - (if (contains? layout :scale-text) - (tr "workspace.header.menu.disable-scale-content") - (tr "workspace.header.menu.enable-scale-content"))] - [:span.shortcut (sc/get-tooltip :toggle-scale-text)]] - - [:li {:on-click toggle-flag - :data-flag "snap-guides"} - [:span - (if (contains? layout :snap-guides) - (tr "workspace.header.menu.disable-snap-guides") - (tr "workspace.header.menu.enable-snap-guides"))] - [:span.shortcut (sc/get-tooltip :toggle-snap-guide)]] - - [:li {:on-click toggle-flag - :data-flag "snap-grid"} - [:span - (if (contains? layout :snap-grid) - (tr "workspace.header.menu.disable-snap-grid") - (tr "workspace.header.menu.enable-snap-grid"))] - [:span.shortcut (sc/get-tooltip :toggle-snap-grid)]] - - [:li {:on-click toggle-flag - :data-flag "dynamic-alignment"} - [:span - (if (contains? layout :dynamic-alignment) - (tr "workspace.header.menu.disable-dynamic-alignment") - (tr "workspace.header.menu.enable-dynamic-alignment"))] - [:span.shortcut (sc/get-tooltip :toggle-alignment)]] - - [:li {:on-click toggle-flag - :data-flag "snap-pixel-grid"} - [:span - (if (contains? layout :snap-pixel-grid) - (tr "workspace.header.menu.disable-snap-pixel-grid") - (tr "workspace.header.menu.enable-snap-pixel-grid"))] - [:span.shortcut (sc/get-tooltip :snap-pixel-grid)]] - - [:li {:on-click show-nudge-options} - [:span (tr "modals.nudge-title")]]]])) - -(mf/defc view-menu - {::mf/wrap-props false - ::mf/wrap [mf/memo]} - [{:keys [layout toggle-flag on-close]}] - (let [read-only? (mf/use-ctx ctx/workspace-read-only?) - - toggle-color-palette - (mf/use-fn - (fn [] - (r/set-resize-type! :bottom) - (st/emit! (dw/remove-layout-flag :textpalette) - (-> (dw/toggle-layout-flag :colorpalette) - (vary-meta assoc ::ev/origin "workspace-menu"))))) - - toggle-text-palette - (mf/use-fn - (fn [] - (r/set-resize-type! :bottom) - (st/emit! (dw/remove-layout-flag :colorpalette) - (-> (dw/toggle-layout-flag :textpalette) - (vary-meta assoc ::ev/origin "workspace-menu")))))] - - [:& dropdown {:show true :on-close on-close} - [:ul.sub-menu.view - [:li {:on-click toggle-flag - :data-flag "rules"} - [:span - (if (contains? layout :rules) - (tr "workspace.header.menu.hide-rules") - (tr "workspace.header.menu.show-rules"))] - [:span.shortcut (sc/get-tooltip :toggle-rules)]] - - [:li {:on-click toggle-flag - :data-flag "display-grid"} - [:span - (if (contains? layout :display-grid) - (tr "workspace.header.menu.hide-grid") - (tr "workspace.header.menu.show-grid"))] - [:span.shortcut (sc/get-tooltip :toggle-grid)]] - - (when-not ^boolean read-only? - [:* - [:li {:on-click toggle-color-palette} - [:span - (if (contains? layout :colorpalette) - (tr "workspace.header.menu.hide-palette") - (tr "workspace.header.menu.show-palette"))] - [:span.shortcut (sc/get-tooltip :toggle-colorpalette)]] - - [:li {:on-click toggle-text-palette} - [:span - (if (contains? layout :textpalette) - (tr "workspace.header.menu.hide-textpalette") - (tr "workspace.header.menu.show-textpalette"))] - [:span.shortcut (sc/get-tooltip :toggle-textpalette)]]]) - - [:li {:on-click toggle-flag - :data-flag "display-artboard-names"} - [:span - (if (contains? layout :display-artboard-names) - (tr "workspace.header.menu.hide-artboard-names") - (tr "workspace.header.menu.show-artboard-names"))]] - - [:li {:on-click toggle-flag - :data-flag "show-pixel-grid"} - [:span - (if (contains? layout :show-pixel-grid) - (tr "workspace.header.menu.hide-pixel-grid") - (tr "workspace.header.menu.show-pixel-grid"))] - [:span.shortcut (sc/get-tooltip :show-pixel-grid)]] - - [:li {:on-click toggle-flag - :data-flag "hide-ui"} - [:span - (tr "workspace.shape.menu.hide-ui")] - [:span.shortcut (sc/get-tooltip :hide-ui)]]]])) - -(mf/defc edit-menu - {::mf/wrap-props false - ::mf/wrap [mf/memo]} - [{:keys [on-close]}] - (let [select-all (mf/use-fn #(st/emit! (dw/select-all))) - undo (mf/use-fn #(st/emit! dwc/undo)) - redo (mf/use-fn #(st/emit! dwc/redo))] - [:& dropdown {:show true :on-close on-close} - [:ul.sub-menu.edit - [:li {:on-click select-all} - [:span (tr "workspace.header.menu.select-all")] - [:span.shortcut (sc/get-tooltip :select-all)]] - - [:li {:on-click undo} - [:span (tr "workspace.header.menu.undo")] - [:span.shortcut (sc/get-tooltip :undo)]] - - [:li {:on-click redo} - [:span (tr "workspace.header.menu.redo")] - [:span.shortcut (sc/get-tooltip :redo)]]]])) - -(mf/defc file-menu - {::mf/wrap-props false} - [{:keys [on-close file team-id]}] - (let [file-id (:id file) - file-name (:name file) - shared? (:is-shared file) - - objects (mf/deref refs/workspace-page-objects) - frames (->> (cph/get-immediate-children objects uuid/zero) - (filterv cph/frame-shape?)) - - add-shared-fn - (mf/use-fn - (mf/deps file-id) - #(st/emit! (dwl/set-file-shared file-id true))) - - on-add-shared - (mf/use-fn - (mf/deps file-name add-shared-fn) - #(modal/show! {:type :confirm - :message "" - :title (tr "modals.add-shared-confirm.message" file-name) - :hint (tr "modals.add-shared-confirm.hint") - :cancel-label :omit - :accept-label (tr "modals.add-shared-confirm.accept") - :accept-style :primary - :on-accept add-shared-fn})) - - on-remove-shared - (mf/use-fn - (mf/deps file-id) - (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (modal/show! - {:type :delete-shared-libraries - :origin :unpublish - :ids #{file-id} - :on-accept #(st/emit! (dwl/set-file-shared file-id false)) - :count-libraries 1}))) - - on-export-shapes - (mf/use-fn #(st/emit! (de/show-workspace-export-dialog))) - - on-export-file - (mf/use-fn - (mf/deps file) - (fn [event-name binary?] - (st/emit! (ptk/event ::ev/event {::ev/name event-name - ::ev/origin "workspace" - :num-files 1})) - - (->> (rx/of file) - (rx/flat-map - (fn [file] - (->> (rp/cmd! :has-file-libraries {:file-id (:id file)}) - (rx/map #(assoc file :has-libraries? %))))) - (rx/reduce conj []) - (rx/subs - (fn [files] - (modal/show! - {:type :export - :team-id team-id - :has-libraries? (->> files (some :has-libraries?)) - :files files - :binary? binary?})))))) - - on-export-binary-file - (mf/use-fn - (mf/deps on-export-file) - (partial on-export-file "export-binary-files" true)) - - on-export-standard-file - (mf/use-fn - (mf/deps on-export-file) - (partial on-export-file "export-standard-files" false)) - - on-export-frames - (mf/use-fn - (mf/deps frames) - (fn [_] - (st/emit! (de/show-workspace-export-frames-dialog (reverse frames)))))] - - [:& dropdown {:show true :on-close on-close} - [:ul.sub-menu.file - (if ^boolean shared? - [:li {:on-click on-remove-shared} - [:span (tr "dashboard.unpublish-shared")]] - [:li {:on-click on-add-shared} - [:span (tr "dashboard.add-shared")]]) - [:li.export-file {:on-click on-export-shapes} - [:span (tr "dashboard.export-shapes")] - [:span.shortcut (sc/get-tooltip :export-shapes)]] - [:li.separator.export-file {:on-click on-export-binary-file} - [:span (tr "dashboard.download-binary-file")]] - [:li.export-file {:on-click on-export-standard-file} - [:span (tr "dashboard.download-standard-file")]] - (when (seq frames) - [:li.separator.export-file {:on-click on-export-frames} - [:span (tr "dashboard.export-frames")]])]])) - -(mf/defc menu - {::mf/wrap-props false} - [{:keys [layout file team-id]}] - (let [show-menu* (mf/use-state false) - show-menu? (deref show-menu*) - sub-menu* (mf/use-state false) - sub-menu (deref sub-menu*) - - open-menu (mf/use-fn #(reset! show-menu* true)) - close-menu (mf/use-fn #(reset! show-menu* false)) - close-sub-menu (mf/use-fn #(reset! sub-menu* nil)) - - on-menu-click - (mf/use-fn - (fn [event] - (dom/stop-propagation event) - (let [menu (-> (dom/get-target event) - (dom/get-data "menu") - (keyword))] - (reset! sub-menu* menu)))) - - toggle-flag - (mf/use-fn - (fn [event] - (let [flag (-> (dom/get-current-target event) - (dom/get-data "flag") - (keyword))] - (st/emit! - (-> (dw/toggle-layout-flag flag) - (vary-meta assoc ::ev/origin "workspace-menu"))))))] - - - [:* - [:div.btn-icon-dark.btn-small {:on-click open-menu} i/actions] - - [:& dropdown {:show show-menu? :on-close close-menu} - [:ul.menu - [:li {:on-click on-menu-click - :on-pointer-enter on-menu-click - :data-menu "file"} - [:span (tr "workspace.header.menu.option.file")] - [:span i/arrow-slide]] - [:li {:on-click on-menu-click - :on-pointer-enter on-menu-click - :data-menu "edit"} - [:span (tr "workspace.header.menu.option.edit")] - [:span i/arrow-slide]] - [:li {:on-click on-menu-click - :on-pointer-enter on-menu-click - :data-menu :view} - [:span (tr "workspace.header.menu.option.view")] - [:span i/arrow-slide]] - [:li {:on-click on-menu-click - :on-pointer-enter on-menu-click - :data-menu "preferences"} - [:span (tr "workspace.header.menu.option.preferences")] - [:span i/arrow-slide]] - [:li.info {:on-click on-menu-click - :on-pointer-enter on-menu-click - :data-menu "help-info"} - [:span (tr "workspace.header.menu.option.help-info")] - [:span i/arrow-slide]]]] - - (case sub-menu - :file - [:& file-menu - {:file file - :team-id team-id - :on-close close-sub-menu}] - - :edit - [:& edit-menu - {:on-close close-sub-menu}] - - :view - [:& view-menu - {:layout layout - :toggle-flag toggle-flag - :on-close close-sub-menu}] - - :preferences - [:& preferences-menu - {:layout layout - :toggle-flag toggle-flag - :on-close close-sub-menu}] - - :help-info - [:& help-info-menu - {:layout layout - :on-close close-sub-menu}] - - nil)])) - -;; --- Header Component - -(mf/defc header - {::mf/wrap-props false} - [{:keys [file layout project page-id]}] - (let [file-id (:id file) - file-name (:name file) - project-id (:id project) - team-id (:team-id project) - shared? (:is-shared file) - - zoom (mf/deref refs/selected-zoom) - read-only? (mf/use-ctx ctx/workspace-read-only?) - - on-increase (mf/use-fn #(st/emit! (dw/increase-zoom nil))) - on-decrease (mf/use-fn #(st/emit! (dw/decrease-zoom nil))) - on-zoom-reset (mf/use-fn #(st/emit! dw/reset-zoom)) - on-zoom-fit (mf/use-fn #(st/emit! dw/zoom-to-fit-all)) - on-zoom-selected (mf/use-fn #(st/emit! dw/zoom-to-selected-shape)) - - - editing* (mf/use-state false) - editing? (deref editing*) - - input-ref (mf/use-ref nil) - - handle-blur - (mf/use-fn - (mf/deps file-id) - (fn [_] - (let [value (str/trim (-> input-ref mf/ref-val dom/get-value))] - (when (not= value "") - (st/emit! (dw/rename-file file-id value))) - (reset! editing* false)))) - - handle-name-keydown - (mf/use-fn - (mf/deps handle-blur) - (fn [event] - (when (kbd/enter? event) - (handle-blur event)))) - - start-editing-name - (mf/use-fn - (fn [event] - (dom/prevent-default event) - (reset! editing* true))) - - go-back - (mf/use-fn - (mf/deps project) - (fn [] - (st/emit! (dw/go-to-dashboard project)))) - - nav-to-viewer - (mf/use-fn - (mf/deps file-id page-id) - (fn [] - (let [params {:page-id page-id - :file-id file-id - :section "interactions"}] - (st/emit! (dw/go-to-viewer params))))) - - nav-to-project - (mf/use-fn - (mf/deps team-id project-id) - #(st/emit! (rt/nav-new-window* {:rname :dashboard-files - :path-params {:team-id team-id - :project-id project-id}}))) - - toggle-history - (mf/use-fn - #(st/emit! (-> (dw/toggle-layout-flag :document-history) - (vary-meta assoc ::ev/origin "workspace-header"))))] - - (mf/with-effect [editing?] - (when ^boolean editing? - (dom/select-text! (mf/ref-val input-ref)))) - - [:header.workspace-header - [:div.left-area - [:div.main-icon - [:a {:on-click go-back} i/logo-icon]] - - [:div.menu-section - [:& menu {:layout layout - :file file - :read-only? read-only? - :team-id team-id - :page-id page-id}] - - [:div.project-tree {:alt (tr "workspace.sitemap")} - [:span.project-name - {:on-click nav-to-project} - (:name project) " /"] - - (if ^boolean editing? - [:input.file-name - {:type "text" - :ref input-ref - :on-blur handle-blur - :on-key-down handle-name-keydown - :auto-focus true - :default-value (:name file "")}] - [:span - {:on-double-click start-editing-name} - file-name])] - - (when ^boolean shared? - [:div.shared-badge i/library])]] - - [:div.center-area - [:div.users-section - [:& active-sessions]]] - - [:div.right-area - [:div.options-section - [:& persistence-state-widget] - [:& export-progress-widget] - (when-not ^boolean read-only? - [:button.document-history - {:alt (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history)) - :aria-label (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history)) - :class (when (contains? layout :document-history) "selected") - :on-click toggle-history} - i/recent])] - - [:div.options-section - [:& zoom-widget-workspace - {:zoom zoom - :on-increase on-increase - :on-decrease on-decrease - :on-zoom-reset on-zoom-reset - :on-zoom-fit on-zoom-fit - :on-zoom-selected on-zoom-selected}] - - [:a.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left - {:alt (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer)) - :on-click nav-to-viewer} - i/play]]]])) - diff --git a/frontend/src/app/main/ui/workspace/left_header.cljs b/frontend/src/app/main/ui/workspace/left_header.cljs new file mode 100644 index 0000000000..1055426dbf --- /dev/null +++ b/frontend/src/app/main/ui/workspace/left_header.cljs @@ -0,0 +1,120 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.left-header + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data.macros :as dm] + [app.main.data.modal :as modal] + [app.main.data.workspace :as dw] + [app.main.data.workspace.colors :as dc] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.context :as ctx] + [app.main.ui.icons :as i] + [app.main.ui.workspace.main-menu :as main-menu] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] + [app.util.router :as rt] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +;; --- Header Component + +(mf/defc left-header + {::mf/wrap-props false} + [{:keys [file layout project page-id class]}] + (let [profile (mf/deref refs/profile) + file-id (:id file) + file-name (:name file) + project-id (:id project) + team-id (:team-id project) + shared? (:is-shared file) + read-only? (mf/use-ctx ctx/workspace-read-only?) + + editing* (mf/use-state false) + editing? (deref editing*) + input-ref (mf/use-ref nil) + + handle-blur + (mf/use-fn + (mf/deps file-id) + (fn [_] + (let [value (str/trim (-> input-ref mf/ref-val dom/get-value))] + (when (not= value "") + (st/emit! (dw/rename-file file-id value))) + (reset! editing* false)))) + + handle-name-keydown + (mf/use-fn + (mf/deps handle-blur) + (fn [event] + (when (kbd/enter? event) + (handle-blur event)))) + + start-editing-name + (mf/use-fn + (fn [event] + (dom/prevent-default event) + (reset! editing* true))) + + close-modals + (mf/use-fn + #(st/emit! (dc/stop-picker) + (modal/hide))) + + go-back + (mf/use-fn + (mf/deps project) + (fn [] + (close-modals) + (st/emit! (dw/set-options-mode :design) + (dw/go-to-dashboard project)))) + + nav-to-project + (mf/use-fn + (mf/deps team-id project-id) + #(st/emit! (rt/nav-new-window* {:rname :dashboard-files + :path-params {:team-id team-id + :project-id project-id}})))] + + (mf/with-effect [editing?] + (when ^boolean editing? + (dom/select-text! (mf/ref-val input-ref)))) + [:header {:class (dm/str class " " (stl/css :workspace-header-left))} + [:a {:on-click go-back + :class (stl/css :main-icon)} i/logo-icon] + [:div {:alt (tr "workspace.sitemap") + :class (stl/css :project-tree)} + [:div + {:class (stl/css :project-name) + :on-click nav-to-project} + (:name project)] + (if ^boolean editing? + [:input + {:class (stl/css :file-name-input) + :type "text" + :ref input-ref + :on-blur handle-blur + :on-key-down handle-name-keydown + :auto-focus true + :default-value (:name file "")}] + [:div + {:class (stl/css :file-name) + :title file-name + :on-double-click start-editing-name} + file-name])] + (when ^boolean shared? + [:span {:class (stl/css :shared-badge)} i/library]) + [:div {:class (stl/css :menu-section)} + [:& main-menu/menu + {:layout layout + :file file + :profile profile + :read-only? read-only? + :team-id team-id + :page-id page-id}]]])) diff --git a/frontend/src/app/main/ui/workspace/left_header.scss b/frontend/src/app/main/ui/workspace/left_header.scss new file mode 100644 index 0000000000..2071017435 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/left_header.scss @@ -0,0 +1,82 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.workspace-header-left { + display: flex; + align-items: center; + padding: $s-12 $s-12 $s-8 $s-12; + min-height: $s-52; +} + +.main-icon { + @include flexCenter; + width: $s-32; + height: $s-32; + min-height: $s-32; + margin-right: $s-4; + svg { + min-height: $s-32; + width: $s-32; + fill: var(--icon-foreground-hover); + } +} + +.project-tree { + position: relative; + flex-grow: 1; + height: $s-32; + min-height: $s-32; + max-width: calc(100% - $s-64); +} + +.project-name, +.file-name { + @include uppercaseTitleTipography; + @include textEllipsis; + height: $s-16; + width: 100%; + padding-bottom: $s-2; + color: var(--title-foreground-color); + cursor: pointer; +} + +.file-name { + @include smallTitleTipography; + text-transform: none; + color: var(--title-foreground-color-hover); +} + +.file-name-input { + @include flexCenter; + width: 100%; + margin: 0; + border: 0; + padding: 0; + border-radius: $br-4; + background-color: var(--input-background-color); + font-size: $fs-14; + color: var(--input-foreground-color); + z-index: $z-index-20; + white-space: break-spaces; + &:focus { + outline: none; + } +} + +.shared-badge { + @include flexCenter; + width: $s-16; + height: $s-32; + margin-right: $s-4; + svg { + stroke: var(--button-secondary-foreground-color-rest); + fill: none; + height: $s-16; + width: $s-16; + } +} diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index e1b6660447..ddbfe354a6 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -5,23 +5,47 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.libraries + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.types.colors-list :as ctcl] [app.common.types.components-list :as ctkl] + [app.common.types.file :as ctf] + [app.common.types.typographies-list :as ctyl] + [app.common.uuid :as uuid] [app.main.data.modal :as modal] + [app.main.data.users :as du] [app.main.data.workspace.libraries :as dwl] [app.main.refs :as refs] + [app.main.render :refer [component-svg]] [app.main.store :as st] + [app.main.ui.components.color-bullet :as cb] + [app.main.ui.components.link-button :as lb] + [app.main.ui.components.search-bar :refer [search-bar]] + [app.main.ui.components.tab-container :refer [tab-container tab-element]] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.icons :as i] + [app.util.color :as uc] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.keyboard :as kbd] [app.util.strings :refer [matches-search]] [cuerdas.core :as str] [okulary.core :as l] [rumext.v2 :as mf])) +(def ^:private close-icon + (i/icon-xref :close (stl/css :close-icon))) + +(def ^:private add-icon + (i/icon-xref :add (stl/css :add-icon))) + +(def ^:private detach-icon + (i/icon-xref :detach (stl/css :detach-icon))) + +(def ^:private library-icon + (i/icon-xref :library (stl/css :library-icon))) + (def ref:workspace-file (l/derived :workspace-file st/state)) @@ -40,59 +64,70 @@ (defn- describe-library [components-count graphics-count colors-count typography-count] - (str - (str/join " · " - (cond-> [] - (pos? components-count) - (conj (tr "workspace.libraries.components" components-count)) + (let [all-zero? (and (zero? components-count) (zero? graphics-count) (zero? colors-count) (zero? typography-count))] + (str + (str/join " · " + (cond-> [] + (or all-zero? (pos? components-count)) + (conj (tr "workspace.libraries.components" components-count)) - (pos? graphics-count) - (conj (tr "workspace.libraries.graphics" graphics-count)) + (or all-zero? (pos? graphics-count)) + (conj (tr "workspace.libraries.graphics" graphics-count)) - (pos? colors-count) - (conj (tr "workspace.libraries.colors" colors-count)) + (or all-zero? (pos? colors-count)) + (conj (tr "workspace.libraries.colors" colors-count)) - (pos? typography-count) - (conj (tr "workspace.libraries.typography" typography-count)))) - "\u00A0")) + (or all-zero? (pos? typography-count)) + (conj (tr "workspace.libraries.typography" typography-count)))) + "\u00A0"))) -(defn- describe-linked-library - [library] - (let [components-count (count (or (ctkl/components-seq (:data library)) [])) - graphics-count (count (dm/get-in library [:data :media] [])) - colors-count (count (dm/get-in library [:data :colors] [])) - typography-count (count (dm/get-in library [:data :typographies] []))] - (describe-library components-count graphics-count colors-count typography-count))) +(mf/defc describe-library-blocks + [{:keys [components-count graphics-count colors-count typography-count] :as props}] + [:* + (when (pos? components-count) + [:li {:class (stl/css :element-count)} + (tr "workspace.libraries.components" components-count)]) + + (when (pos? graphics-count) + [:li {:class (stl/css :element-count)} + (tr "workspace.libraries.graphics" graphics-count)]) + + (when (pos? colors-count) + [:li {:class (stl/css :element-count)} + (tr "workspace.libraries.colors" colors-count)]) + + (when (pos? typography-count) + [:li {:class (stl/css :element-count)} + (tr "workspace.libraries.typography" typography-count)])]) -(defn- describe-external-library - [library] - (let [components-count (dm/get-in library [:library-summary :components :count] 0) - graphics-count (dm/get-in library [:library-summary :media :count] 0) - colors-count (dm/get-in library [:library-summary :colors :count] 0) - typography-count (dm/get-in library [:library-summary :typographies :count] 0)] - (describe-library components-count graphics-count colors-count typography-count))) (mf/defc libraries-tab {::mf/wrap-props false} [{:keys [file-id shared? linked-libraries shared-libraries]}] - (let [search-term* (mf/use-state "") - search-term (deref search-term*) + (let [search-term* (mf/use-state "") + search-term (deref search-term*) + library-ref (mf/with-memo [file-id] + (create-file-library-ref file-id)) + library (deref library-ref) + colors (:colors library) + components (:components library) + media (:media library) + typographies (:typographies library) - library-ref (mf/with-memo [file-id] - (create-file-library-ref file-id)) - library (deref library-ref) - colors (:colors library) - components (:components library) - media (:media library) - typographies (:typographies library) + empty-library? (and + (zero? (count colors)) + (zero? (count components)) + (zero? (count media)) + (zero? (count typographies))) shared-libraries (mf/with-memo [shared-libraries linked-libraries file-id search-term] - (->> shared-libraries - (remove #(= (:id %) file-id)) - (remove #(contains? linked-libraries (:id %))) - (filter #(matches-search (:name %) search-term)) - (sort-by (comp str/lower :name)))) + (when shared-libraries + (->> shared-libraries + (remove #(= (:id %) file-id)) + (remove #(contains? linked-libraries (:id %))) + (filter #(matches-search (:name %) search-term)) + (sort-by (comp str/lower :name))))) linked-libraries (mf/with-memo [linked-libraries] @@ -102,18 +137,13 @@ change-search-term (mf/use-fn (fn [event] - (let [value (-> (dom/get-target event) - (dom/get-value))] - (reset! search-term* value)))) - - clear-search-term - (mf/use-fn #(reset! search-term* "")) + (reset! search-term* event))) link-library (mf/use-fn (mf/deps file-id) (fn [event] - (let [library-id (some-> (dom/get-target event) + (let [library-id (some-> (dom/get-current-target event) (dom/get-data "library-id") (parse-uuid))] (st/emit! (dwl/link-file-to-library file-id library-id))))) @@ -122,7 +152,7 @@ (mf/use-fn (mf/deps file-id) (fn [event] - (let [library-id (some-> (dom/get-target event) + (let [library-id (some-> (dom/get-current-target event) (dom/get-data "library-id") (parse-uuid))] (st/emit! (dwl/unlink-file-from-library file-id library-id) @@ -140,7 +170,20 @@ publish (mf/use-fn (mf/deps file-id) - #(st/emit! (dwl/set-file-shared file-id true))) + (fn [event] + (let [input-node (dom/get-target event) + publish-library #(st/emit! (dwl/set-file-shared file-id true)) + cancel-publish #(st/emit! (modal/show :libraries-dialog {}))] + (if empty-library? + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.publish-empty-library.title") + :message (tr "modals.publish-empty-library.message") + :accept-label (tr "modals.publish-empty-library.accept") + :on-accept publish-library + :on-cancel cancel-publish})) + (publish-library)) + (dom/blur! input-node)))) unpublish (mf/use-fn @@ -152,175 +195,391 @@ :origin :unpublish :on-accept on-delete-accept :on-cancel on-delete-cancel - :count-libraries 1})))) + :count-libraries 1}))))] - handle-key-down - (mf/use-fn - (fn [event] - (let [enter? (kbd/enter? event) - esc? (kbd/esc? event) - input-node (dom/event->target event)] - (when ^boolean enter? - (dom/blur! input-node)) - (when ^boolean esc? - (dom/blur! input-node)))))] + [:div {:class (stl/css :libraries-content)} + [:div {:class (stl/css :lib-section)} + [:& title-bar {:collapsable false + :title (tr "workspace.libraries.in-this-file") + :class (stl/css :title-spacing-lib)}] + [:div {:class (stl/css :section-list)} - [:* - [:div.section - [:div.section-title (tr "workspace.libraries.in-this-file")] - [:div.section-list - - [:div.section-list-item - [:div - [:div.item-name (tr "workspace.libraries.file-library")] - [:div.item-contents (describe-library - (count components) - (count media) - (count colors) - (count typographies))]] - [:div - (if ^boolean shared? - [:input.item-button {:type "button" - :value (tr "common.unpublish") - :on-click unpublish}] - [:input.item-button {:type "button" - :value (tr "common.publish") - :on-click publish}])]] + [:div {:class (stl/css :section-list-item)} + [:div {:class (stl/css :item-content)} + [:div {:class (stl/css :item-name)} (tr "workspace.libraries.file-library")] + [:ul {:class (stl/css :item-contents)} + [:& describe-library-blocks {:components-count (count components) + :graphics-count (count media) + :colors-count (count colors) + :typography-count (count typographies)}]]] + (if ^boolean shared? + [:input {:class (stl/css :item-unpublish) + :type "button" + :value (tr "common.unpublish") + :on-click unpublish}] + [:input {:class (stl/css :item-publish) + :type "button" + :value (tr "common.publish") + :on-click publish}])] (for [{:keys [id name] :as library} linked-libraries] - [:div.section-list-item {:key (dm/str id)} - [:div.item-name name] - [:div.item-contents (describe-linked-library library)] - [:input.item-button {:type "button" - :value (tr "labels.remove") - :data-library-id (dm/str id) - :on-click unlink-library}]])]] + [:div {:class (stl/css :section-list-item) + :key (dm/str id)} + [:div {:class (stl/css :item-content)} + [:div {:class (stl/css :item-name)} name] + [:ul {:class (stl/css :item-contents)} + (let [components-count (count (or (ctkl/components-seq (:data library)) [])) + graphics-count (count (dm/get-in library [:data :media] [])) + colors-count (count (dm/get-in library [:data :colors] [])) + typography-count (count (dm/get-in library [:data :typographies] []))] + [:& describe-library-blocks {:components-count components-count + :graphics-count graphics-count + :colors-count colors-count + :typography-count typography-count}])]] - [:div.section - [:div.section-title (tr "workspace.libraries.shared-libraries")] - [:div.libraries-search - [:input.search-input - {:placeholder (tr "workspace.libraries.search-shared-libraries") - :type "text" - :value search-term - :on-change change-search-term - :on-key-down handle-key-down}] - (if (str/empty? search-term) - [:div.search-icon - i/search] - [:div.search-icon.search-close - {:on-click clear-search-term} - i/close])] + [:button {:class (stl/css :item-button) + :type "button" + :title (tr "workspace.libraries.unlink-library-btn") + :data-library-id (dm/str id) + :on-click unlink-library} + detach-icon]])]] + + [:div {:class (stl/css :shared-section)} + [:& title-bar {:collapsable false + :title (tr "workspace.libraries.shared-libraries") + :class (stl/css :title-spacing-lib)}] + [:& search-bar {:on-change change-search-term + :value search-term + :placeholder (tr "workspace.libraries.search-shared-libraries") + :icon (mf/html [:span {:class (stl/css :search-icon)} i/search])}] (if (seq shared-libraries) - [:div.section-list + [:div {:class (stl/css :section-list-shared)} (for [{:keys [id name] :as library} shared-libraries] - [:div.section-list-item {:key (dm/str id)} - [:div.item-name name] - [:div.item-contents (describe-external-library library)] - [:input.item-button {:type "button" - :value (tr "workspace.libraries.add") - :data-library-id (dm/str id) - :on-click link-library}]])] + [:div {:class (stl/css :section-list-item) + :key (dm/str id)} + [:div {:class (stl/css :item-content)} + [:div {:class (stl/css :item-name)} name] + [:ul {:class (stl/css :item-contents)} + (let [components-count (dm/get-in library [:library-summary :components :count] 0) + graphics-count (dm/get-in library [:library-summary :media :count] 0) + colors-count (dm/get-in library [:library-summary :colors :count] 0) + typography-count (dm/get-in library [:library-summary :typographies :count] 0)] + [:& describe-library-blocks {:components-count components-count + :graphics-count graphics-count + :colors-count colors-count + :typography-count typography-count}])]] + [:button {:class (stl/css :item-button-shared) + :data-library-id (dm/str id) + :title (tr "workspace.libraries.shared-library-btn") + :on-click link-library} + add-icon]])] - [:div.section-list-empty - (if (nil? shared-libraries) - i/loader-pencil - [:* i/library - (if (str/empty? search-term) - (tr "workspace.libraries.no-shared-libraries-available") - (tr "workspace.libraries.no-matches-for" search-term))])])]])) + (when (empty? shared-libraries) + [:div {:class (stl/css :section-list-empty)} + (cond + (nil? shared-libraries) + (tr "workspace.libraries.loading") + (str/empty? search-term) + [:* + [:span {:class (stl/css :empty-state-icon)} + library-icon] + (tr "workspace.libraries.no-shared-libraries-available")] + + :else + (tr "workspace.libraries.no-matches-for" search-term))]))]])) + +(defn- extract-assets + [file-data library summary?] + (let [exceeded (volatile! {:components false + :colors false + :typographies false}) + + truncate (fn [asset-type items] + (if (and summary? (> (count items) 5)) + (do + (vswap! exceeded assoc asset-type true) + (take 5 items)) + items)) + + assets (dwl/assets-need-sync library file-data) + + component-ids (into #{} (->> assets + (filter #(= (:asset-type %) :component)) + (map :asset-id))) + color-ids (into #{} (->> assets + (filter #(= (:asset-type %) :color)) + (map :asset-id))) + typography-ids (into #{} (->> assets + (filter #(= (:asset-type %) :typography)) + (map :asset-id))) + + components (->> component-ids + (map #(ctkl/get-component (:data library) %)) + (sort-by #(str/lower (:name %))) + (truncate :components)) + colors (->> color-ids + (map #(ctcl/get-color (:data library) %)) + (sort-by #(str/lower (:name %))) + (truncate :colors)) + typographies (->> typography-ids + (map #(ctyl/get-typography (:data library) %)) + (sort-by #(str/lower (:name %))) + (truncate :typographies))] + + [library @exceeded {:components components + :colors colors + :typographies typographies}])) (mf/defc updates-tab {::mf/wrap-props false} [{:keys [file-id file-data libraries]}] - (let [libraries (mf/with-memo [file-data libraries] - (filter #(seq (dwl/assets-need-sync % file-data)) - (vals libraries))) + (let [summary?* (mf/use-state true) + summary? (deref summary?*) + updating? (mf/deref refs/updating-library) - update (mf/use-fn - (mf/deps file-id) - (fn [event] - (let [library-id (some-> (dom/get-target event) - (dom/get-data "library-id") - (parse-uuid))] - (st/emit! (dwl/sync-file file-id library-id)))))] - [:div.section - (if (empty? libraries) - [:div.section-list-empty - i/library - (tr "workspace.libraries.no-libraries-need-sync")] - [:* - [:div.section-title (tr "workspace.libraries.library")] - [:div.section-list - (for [{:keys [id name] :as library} libraries] - [:div.section-list-item {:key (dm/str id)} - [:div.item-name name] - [:div.item-contents (describe-external-library library)] - [:input.item-button {:type "button" - :value (tr "workspace.libraries.update") - :data-library-id (dm/str id) - :on-click update}]])]])])) + see-all-assets + (mf/use-fn + (fn [] + (reset! summary?* false))) + libs-assets (mf/with-memo [file-data libraries summary?*] + (->> (vals libraries) + (map #(extract-assets file-data % summary?)) + (filter (fn [[_ _ {:keys [components colors typographies]}]] + (or (seq components) + (seq colors) + (seq typographies)))))) + + update (mf/use-fn + (mf/deps file-id) + (fn [event] + (when-not updating? + (let [library-id (some-> (dom/get-target event) + (dom/get-data "library-id") + (parse-uuid))] + (st/emit! + (dwl/set-updating-library true) + (dwl/sync-file file-id library-id))))))] + + [:div {:class (stl/css :updates-content)} + [:div {:class (stl/css :update-section)} + (if (empty? libs-assets) + [:div {:class (stl/css :section-list-empty)} + [:span {:class (stl/css :empty-state-icon)} + library-icon] + (tr "workspace.libraries.no-libraries-need-sync")] + [:* + [:div {:class (stl/css :section-title)} (tr "workspace.libraries.library-updates")] + + [:div {:class (stl/css :section-list)} + (for [[{:keys [id name] :as library} + exceeded + {:keys [components colors typographies]}] libs-assets] + [:div {:class (stl/css :section-list-item) + :key (dm/str id)} + [:div {:class (stl/css :item-content)} + [:div {:class (stl/css :item-name)} name] + [:ul {:class (stl/css :item-contents)} (describe-library + (count components) + 0 + (count colors) + (count typographies))]] + [:button {:type "button" + :class (stl/css :item-update) + :disabled updating? + :data-library-id (dm/str id) + :on-click update} + (tr "workspace.libraries.update")] + + [:div {:class (stl/css :libraries-updates)} + (when-not (empty? components) + [:div {:class (stl/css :libraries-updates-column)} + (for [component components] + [:div {:class (stl/css :libraries-updates-item) + :key (dm/str (:id component))} + (let [component (ctf/load-component-objects (:data library) component) + root-shape (ctf/get-component-root (:data library) component)] + [:* + [:& component-svg {:root-shape root-shape + :class (stl/css :component-svg) + :objects (:objects component)}] + [:div {:class (stl/css :name-block)} + [:span {:class (stl/css :item-name) + :title (:name component)} + (:name component)]]])]) + (when (:components exceeded) + [:div {:class (stl/css :libraries-updates-item) + :key (uuid/next)} + [:div {:class (stl/css :name-block :ellipsis)} + [:span {:class (stl/css :item-name)} "(...)"]]])]) + + (when-not (empty? colors) + [:div {:class (stl/css :libraries-updates-column) + :style #js {"--bullet-size" "24px"}} + (for [color colors] + (let [default-name (cond + (:gradient color) (uc/gradient-type->string (get-in color [:gradient :type])) + (:color color) (:color color) + :else (:value color))] + [:div {:class (stl/css :libraries-updates-item) + :key (dm/str (:id color))} + [:* + [:& cb/color-bullet {:color {:color (:color color) + :id (:id color) + :opacity (:opacity color)}}] + [:div {:class (stl/css :name-block)} + [:span {:class (stl/css :item-name) + :title (:name color)} + (:name color)] + (when-not (= (:name color) default-name) + [:span.color-value (:color color)])]]])) + (when (:colors exceeded) + [:div {:class (stl/css :libraries-updates-item) + :key (uuid/next)} + [:div {:class (stl/css :name-block.ellipsis)} + [:span {:class (stl/css :item-name)} "(...)"]]])]) + + (when-not (empty? typographies) + [:div {:class (stl/css :libraries-updates-column)} + (for [typography typographies] + [:div {:class (stl/css :libraries-updates-item) + :key (dm/str (:id typography))} + [:* + [:div {:style {:font-family (:font-family typography) + :font-weight (:font-weight typography) + :font-style (:font-style typography)}} + (tr "workspace.assets.typography.sample")] + [:div {:class (stl/css :name-block)} + [:span {:class (stl/css :item-name) + :title (:name typography)} + (:name typography)]]]]) + (when (:typographies exceeded) + [:div {:class (stl/css :libraries-updates-item) + :key (uuid/next)} + [:div {:class (stl/css :name-block.ellipsis)} + [:span {:class (stl/css :item-name)} "(...)"]]])])] + + (when (or (pos? (:components exceeded)) + (pos? (:colors exceeded)) + (pos? (:typographies exceeded))) + [:& lb/link-button {:on-click see-all-assets + :class (stl/css :libraries-updates-see-all) + :value (str "(" (tr "workspace.libraries.update.see-all-changes") ")")}])])]])]])) (mf/defc libraries-dialog {::mf/register modal/components ::mf/register-as :libraries-dialog} - [] - (let [project (mf/deref refs/workspace-project) - file-data (mf/deref refs/workspace-data) - file (mf/deref ref:workspace-file) + [{:keys [starting-tab] :as props :or {starting-tab :libraries}}] + (let [project (mf/deref refs/workspace-project) + file-data (mf/deref refs/workspace-data) + file (mf/deref ref:workspace-file) - team-id (:team-id project) - file-id (:id file) - shared? (:is-shared file) + team-id (:team-id project) + file-id (:id file) + shared? (:is-shared file) - selected-tab* (mf/use-state :libraries) - selected-tab (deref selected-tab*) + selected-tab* (mf/use-state starting-tab) + selected-tab (deref selected-tab*) - libraries (mf/deref refs/workspace-libraries) - libraries (mf/with-memo [libraries] - (d/removem (fn [[_ val]] (:is-indirect val)) libraries)) + libraries (mf/deref refs/workspace-libraries) + libraries (mf/with-memo [libraries] + (d/removem (fn [[_ val]] (:is-indirect val)) libraries)) ;; NOTE: we really don't need react on shared files shared-libraries (mf/deref refs/workspace-shared-files) - select-libraries-tab - (mf/use-fn #(reset! selected-tab* :libraries)) + on-tab-change + (mf/use-fn #(reset! selected-tab* %)) - select-updates-tab - (mf/use-fn #(reset! selected-tab* :updates)) + close-dialog-outside + (mf/use-fn (fn [event] + (when (= (dom/get-target event) (dom/get-current-target event)) + (modal/hide!)))) close-dialog - (mf/use-fn #(modal/hide!))] + (mf/use-fn (fn [_] + (modal/hide!) + (modal/disallow-click-outside!)))] (mf/with-effect [team-id] (when team-id (st/emit! (dwl/fetch-shared-files {:team-id team-id})))) - [:div.modal-overlay - [:div.modal.libraries-dialog - [:a.close {:on-click close-dialog} i/close] - [:div.modal-content - [:div.libraries-header - [:div.header-item - {:class (dom/classnames :active (= selected-tab :libraries)) - :on-click select-libraries-tab} - (tr "workspace.libraries.libraries")] - [:div.header-item - {:class (dom/classnames :active (= selected-tab :updates)) - :on-click select-updates-tab} - (tr "workspace.libraries.updates")]] - [:div.libraries-content - (case selected-tab - :libraries - [:& libraries-tab {:file-id file-id - :shared? shared? - :linked-libraries libraries - :shared-libraries shared-libraries}] - :updates - [:& updates-tab {:file-id file-id - :file-data file-data - :libraries libraries}])]]]])) + [:div {:class (stl/css :modal-overlay) :on-click close-dialog-outside} + [:div {:class (stl/css :modal-dialog)} + [:button {:class (stl/css :close-btn) + :on-click close-dialog} + close-icon] + [:div {:class (stl/css :modal-title)} + (tr "workspace.libraries.libraries")] + [:& tab-container + {:on-change-tab on-tab-change + :selected selected-tab + :collapsable false} + [:& tab-element {:id :libraries :title (tr "workspace.libraries.libraries")} + [:& libraries-tab {:file-id file-id + :shared? shared? + :linked-libraries libraries + :shared-libraries shared-libraries}]] + [:& tab-element {:id :updates :title (tr "workspace.libraries.updates")} + [:& updates-tab {:file-id file-id + :file-data file-data + :libraries libraries}]]]]])) +(mf/defc v2-info-dialog + {::mf/register modal/components + ::mf/register-as :v2-info} + [] + (let [handle-gotit-click + (mf/use-fn + (fn [] + (modal/hide!) + (st/emit! (du/update-profile-props {:v2-info-shown true}))))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog :modal-v2-info)} + [:div {:class (stl/css :modal-v2-title)} + "IMPORTANT INFORMATION ABOUT NEW COMPONENTS"] + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :info-content)} + [:div {:class (stl/css :info-block)} + [:div {:class (stl/css :info-icon)} i/v2-icon-1] + [:div {:class (stl/css :info-block-title)} + "One physical source of truth"] + [:div {:class (stl/css :info-block-content)} + "Main components are now found at the design space. They act as a single source " + "of truth and can be worked on with their copies. This ensures consistency and " + "allows better control and synchronization."]] + + [:div {:class (stl/css :info-block)} + [:div {:class (stl/css :info-icon)} i/v2-icon-2] + [:div {:class (stl/css :info-block-title)} + "Swap components"] + [:div {:class (stl/css :info-block-content)} + "Now, you can replace one component copy with another within your libraries. " + "The swap components functionality streamlines making changes, testing " + "variations, or updating elements without extensive manual adjustments."]] + + [:div {:class (stl/css :info-block)} + [:div {:class (stl/css :info-icon)} i/v2-icon-3] + [:div {:class (stl/css :info-block-title)} + "Graphic assets no longer exist"] + [:div {:class (stl/css :info-block-content)} + "Graphic assets now disappear, so that all graphic assets become components. " + "This way, swapping between them is possible, and we avoid confusion about " + "what should go in each typology."]] + + [:div {:class (stl/css :info-block)} + [:div {:class (stl/css :info-icon)} i/v2-icon-4] + [:div {:class (stl/css :info-block-title)} + "Main components page"] + [:div {:class (stl/css :info-block-content)} + "You might find that a new page called 'Main components' has appeared in " + "your file. On that page, you'll find all the main components that were " + "created in your files previously to this new version."]]] + + [:div {:class (stl/css :info-bottom)} + [:button {:class (stl/css :primary-button) + :on-click handle-gotit-click} "I GOT IT"]]]]])) diff --git a/frontend/src/app/main/ui/workspace/libraries.scss b/frontend/src/app/main/ui/workspace/libraries.scss new file mode 100644 index 0000000000..0762d3b482 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/libraries.scss @@ -0,0 +1,325 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +// Library modal +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-dialog { + @extend .modal-container-base; + display: grid; + grid-template-rows: auto 1fr; + height: $s-520; + max-height: $s-520; + width: $s-712; + max-width: $s-712; +} + +.close-btn { + @extend .modal-close-btn-base; +} + +.close-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} + +.modal-title { + @include headlineMediumTypography; + margin-block-end: $s-16; + color: var(--modal-title-foreground-color); +} + +// Tabs content +.libraries-content, +.updates-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $s-32; + max-height: $s-400; + padding-block-start: $s-16; +} + +.lib-section, +.update-section, +.shared-section { + display: grid; + grid-template-rows: auto 1fr; + gap: $s-8; +} + +.shared-section { + grid-template-rows: auto auto 1fr; +} + +.title-spacing-lib { + margin: 0 0 0 calc(-1 * $s-8); +} + +.section-list, +.section-list-shared { + display: grid; + grid-auto-rows: min-content; + gap: $s-8; + max-height: $s-320; + overflow-y: auto; +} + +.section-list-item { + display: grid; + grid-template-columns: 1fr auto; + gap: $s-8; +} + +.item-content { + height: fit-content; +} + +.item-publish, +.item-unpublish { + @extend .button-primary; + @include uppercaseTitleTipography; + height: $s-32; + min-width: $s-92; + padding: $s-8 $s-24; + margin: 0; + border-radius: $br-8; +} + +.item-unpublish { + @extend .button-secondary; +} + +.item-button, +.item-button-shared { + @extend .button-secondary; + height: $s-32; + width: $s-32; + margin-inline-start: $s-2; + margin-inline-end: $s-8; + padding: $s-8; +} + +.detach-icon, +.add-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} + +.section-list-shared { + max-height: $s-272; +} + +.section-title { + @include headlineSmallTypography; + margin-block-end: $s-12; + color: var(--title-foreground-color); +} + +.search-icon { + @include flexCenter; + width: $s-20; + padding: 0 0 0 $s-8; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } +} + +// empty state +.section-list-empty { + @include bodyMediumTypography; + display: grid; + grid-template-rows: auto 1fr; + justify-items: center; + gap: $s-8; + text-align: center; + height: fit-content; + margin-block: $s-16; + color: var(--modal-title-foreground-color); +} + +.empty-state-icon { + @include flexCenter; + width: $s-48; + height: $s-48; + border-radius: $br-circle; + background-color: var(--pill-background-color); +} + +.library-icon { + @extend .button-icon; + stroke: var(--icon-foreground); + height: $s-32; + width: $s-32; +} + +// Update library tab +.libraries-updates-see-all { + @extend .link; + direction: rtl; + grid-column: span 3; + margin-block-start: $s-8; + margin-inline-start: $s-8; + margin: 0; +} + +.updates-content { + grid-template-columns: 1fr; +} + +.libraries-updates { + display: grid; + grid-column: span 3; + grid-template-columns: repeat(auto-fill, minmax($s-160, 1fr)); + gap: $s-24; + margin-block-start: $s-16; +} + +.libraries-updates-column { + display: grid; + gap: $s-4; +} + +.libraries-updates-item { + @include bodyLargeTypography; + display: grid; + grid-template-columns: auto 1fr; + align-items: start; + gap: $s-8; + color: var(--library-content-foreground-color); +} + +.component-svg { + background-color: var(--color-canvas); + border-radius: $br-4; + border: $s-2 solid transparent; + height: $s-24; + width: $s-24; + min-height: $s-24; + min-width: $s-24; +} + +.name-block { + color: var(--library-content-foreground-color); + width: $s-168; +} + +.ellipsis { + padding-inline-start: calc($s-24 + #{$s-8}); +} + +.item-name { + @include bodyLargeTypography; + @include textEllipsis; + margin: 0; + max-width: $s-244; + color: var(--library-name-foreground-color); +} + +.item-update { + @extend .button-primary; + @include headlineSmallTypography; + height: $s-32; + min-width: $s-92; + padding: $s-8 $s-24; + margin-inline-end: $s-2; + border-radius: $br-8; + &:disabled { + @extend .button-disabled; + } +} + +.item-contents { + @include bodyMediumTypography; + color: var(--library-content-foreground-color); + display: flex; + flex-wrap: wrap; + margin: 0; +} + +.element-count { + white-space: nowrap; + + &:not(:last-child)::after { + content: "·"; + margin-inline: $s-4; + } +} + +// Modal Component v2 update +.modal-v2-info { + width: $s-664; + height: fit-content; + max-height: fit-content; +} + +.modal-v2-title { + @include headlineMediumTypography; + color: var(--modal-title-foreground-color); +} + +.info-content { + display: grid; + grid-template-rows: repeat(4, 1fr); + margin-top: $s-32; + gap: $s-24; +} + +.info-block { + display: grid; + grid-template-columns: auto 1fr; + column-gap: $s-20; + grid-template: + "icon title" + "icon content"; +} + +.info-icon { + grid-area: icon; + width: $s-52; + height: $s-52; + margin-top: $s-8; + border-radius: $br-circle; + background: $db-quaternary; + display: flex; + justify-content: center; + align-items: center; + + svg { + width: $s-32; + height: $s-32; + fill: var(--icon-foreground-active); + } +} + +.info-block-title { + @include bodyLargeTypography; + grid-area: title; + color: var(--modal-title-foreground-color); +} + +.info-block-content { + @include bodyMediumTypography; + grid-area: content; + color: var(--library-content-foreground-color); +} + +.info-bottom { + display: flex; + justify-content: flex-end; + margin-block-start: $s-24; + margin-inline-end: $s-8; +} + +.primary-button { + @extend .button-primary; + @include headlineSmallTypography; + padding: $s-0 $s-16; +} diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs new file mode 100644 index 0000000000..d144706c3b --- /dev/null +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -0,0 +1,747 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.main-menu + (:require-macros [app.main.style :as stl]) + (:require + [app.common.files.helpers :as cfh] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.main.data.common :as dcm] + [app.main.data.events :as ev] + [app.main.data.exports :as de] + [app.main.data.modal :as modal] + [app.main.data.shortcuts :as scd] + [app.main.data.users :as du] + [app.main.data.workspace :as dw] + [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.shortcuts :as sc] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]] + [app.main.ui.context :as ctx] + [app.main.ui.hooks.resize :as r] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] + [app.util.router :as rt] + [potok.v2.core :as ptk] + [rumext.v2 :as mf])) + +;; --- Header menu and submenus + +(mf/defc help-info-menu + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [layout on-close]}] + (let [nav-to-helpc-center + (mf/use-fn #(dom/open-new-window "https://help.penpot.app")) + + nav-to-community + (mf/use-fn #(dom/open-new-window "https://community.penpot.app")) + + nav-to-youtube + (mf/use-fn #(dom/open-new-window "https://www.youtube.com/c/Penpot")) + + nav-to-templates + (mf/use-fn #(dom/open-new-window "https://penpot.app/libraries-templates")) + + nav-to-github + (mf/use-fn #(dom/open-new-window "https://github.com/penpot/penpot")) + + nav-to-terms + (mf/use-fn #(dom/open-new-window "https://penpot.app/terms")) + + nav-to-feedback + (mf/use-fn #(st/emit! (rt/nav-new-window* {:rname :settings-feedback}))) + + show-shortcuts + (mf/use-fn + (mf/deps layout) + (fn [] + (when (contains? layout :collapse-left-sidebar) + (st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) + + (st/emit! + (-> (dw/toggle-layout-flag :shortcuts) + (vary-meta assoc ::ev/origin "workspace-header"))))) + + show-release-notes + (mf/use-fn + (fn [event] + (let [version (:main cf/version)] + (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) + (if (and (kbd/alt? event) (kbd/mod? event)) + (st/emit! (modal/show {:type :onboarding})) + (st/emit! (modal/show {:type :release-notes :version version}))))))] + + [:& dropdown-menu {:show true + :on-close on-close + :list-class (stl/css-case :sub-menu true + :help-info true)} + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-helpc-center + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-helpc-center event))) + :id "file-menu-help-center"} + [:span {:class (stl/css :item-name)} (tr "labels.help-center")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-community + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-community event))) + :id "file-menu-community"} + [:span {:class (stl/css :item-name)} (tr "labels.community")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-youtube + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-youtube event))) + :id "file-menu-youtube"} + [:span {:class (stl/css :item-name)} (tr "labels.tutorials")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click show-release-notes + :on-key-down (fn [event] + (when (kbd/enter? event) + (show-release-notes event))) + :id "file-menu-release-notes"} + [:span {:class (stl/css :item-name)} (tr "labels.release-notes")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-templates + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-templates event))) + :id "file-menu-templates"} + [:span {:class (stl/css :item-name)} (tr "labels.libraries-and-templates")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-github + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-github event))) + :id "file-menu-github"} + [:span {:class (stl/css :item-name)} (tr "labels.github-repo")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-terms + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-terms event))) + :id "file-menu-terms"} + [:span {:class (stl/css :item-name)} (tr "auth.terms-of-service")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click show-shortcuts + :on-key-down (fn [event] + (when (kbd/enter? event) + (show-shortcuts event))) + :id "file-menu-shortcuts"} + [:span {:class (stl/css :item-name)} (tr "label.shortcuts")] + [:span {:class (stl/css :shortcut)} + + (for [sc (scd/split-sc (sc/get-tooltip :show-shortcuts))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + (when (contains? cf/flags :user-feedback) + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click nav-to-feedback + :on-key-down (fn [event] + (when (kbd/enter? event) + (nav-to-feedback event))) + :id "file-menu-feedback"} + [:span {:class (stl/css-case :feedback true + :item-name true)} (tr "labels.give-feedback")]])])) + +(mf/defc preferences-menu + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [layout profile toggle-flag on-close toggle-theme]}] + (let [show-nudge-options (mf/use-fn #(modal/show! {:type :nudge-option}))] + + [:& dropdown-menu {:show true + :list-class (stl/css-case :sub-menu true + :preferences true) + :on-close on-close} + [:> dropdown-menu-item* {:on-click toggle-flag + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "scale-text" + :id "file-menu-scale-text"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :scale-text) + (tr "workspace.header.menu.disable-scale-content") + (tr "workspace.header.menu.enable-scale-content"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :scale))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:on-click toggle-flag + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "snap-ruler-guides" + :id "file-menu-snap-ruler-guides"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :snap-ruler-guides) + (tr "workspace.header.menu.disable-snap-ruler-guides") + (tr "workspace.header.menu.enable-snap-ruler-guides"))] + [:span {:class (stl/css :shortcut)} + + (for [sc (scd/split-sc (sc/get-tooltip :toggle-snap-ruler-guide))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:on-click toggle-flag + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "snap-guides" + :id "file-menu-snap-guides"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :snap-guides) + (tr "workspace.header.menu.disable-snap-guides") + (tr "workspace.header.menu.enable-snap-guides"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-snap-guides))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:on-click toggle-flag + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "dynamic-alignment" + :id "file-menu-dynamic-alignment"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :dynamic-alignment) + (tr "workspace.header.menu.disable-dynamic-alignment") + (tr "workspace.header.menu.enable-dynamic-alignment"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-alignment))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:on-click toggle-flag + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "snap-pixel-grid" + :id "file-menu-pixel-grid"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :snap-pixel-grid) + (tr "workspace.header.menu.disable-snap-pixel-grid") + (tr "workspace.header.menu.enable-snap-pixel-grid"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :snap-pixel-grid))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:on-click show-nudge-options + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (show-nudge-options event))) + :data-test "snap-pixel-grid" + :id "file-menu-nudge"} + [:span {:class (stl/css :item-name)} (tr "modals.nudge-title")]] + + + [:> dropdown-menu-item* {:on-click toggle-theme + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-theme event))) + :data-test "toggle-theme" + :id "file-menu-toggle-theme"} + [:span {:class (stl/css :item-name)} + (if (= (:theme profile) "default") + (tr "workspace.header.menu.toggle-light-theme") + (tr "workspace.header.menu.toggle-dark-theme"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-theme))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]]])) + +(mf/defc view-menu + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [layout toggle-flag on-close]}] + (let [read-only? (mf/use-ctx ctx/workspace-read-only?) + + toggle-color-palette + (mf/use-fn + (fn [] + (r/set-resize-type! :bottom) + (st/emit! (dw/remove-layout-flag :textpalette) + (-> (dw/toggle-layout-flag :colorpalette) + (vary-meta assoc ::ev/origin "workspace-menu"))))) + + toggle-text-palette + (mf/use-fn + (fn [] + (r/set-resize-type! :bottom) + (st/emit! (dw/remove-layout-flag :colorpalette) + (-> (dw/toggle-layout-flag :textpalette) + (vary-meta assoc ::ev/origin "workspace-menu")))))] + + [:& dropdown-menu {:show true + :list-class (stl/css-case :sub-menu true + :view true) + :on-close on-close} + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-flag + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "rulers" + :id "file-menu-rulers"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :rulers) + (tr "workspace.header.menu.hide-rules") + (tr "workspace.header.menu.show-rules"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-rulers))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-flag + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "display-guides" + :id "file-menu-guides"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :display-guides) + (tr "workspace.header.menu.hide-guides") + (tr "workspace.header.menu.show-guides"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-guides))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + + (when-not ^boolean read-only? + [:* + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-color-palette + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-color-palette event))) + :id "file-menu-color-palette"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :colorpalette) + (tr "workspace.header.menu.hide-palette") + (tr "workspace.header.menu.show-palette"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-colorpalette))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-text-palette + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-text-palette event))) + :id "file-menu-text-palette"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :textpalette) + (tr "workspace.header.menu.hide-textpalette") + (tr "workspace.header.menu.show-textpalette"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :toggle-textpalette))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]]]) + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-flag + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "display-artboard-names" + :id "file-menu-artboards"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :display-artboard-names) + (tr "workspace.header.menu.hide-artboard-names") + (tr "workspace.header.menu.show-artboard-names"))]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-flag + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "show-pixel-grid" + :id "file-menu-pixel-grid"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :show-pixel-grid) + (tr "workspace.header.menu.hide-pixel-grid") + (tr "workspace.header.menu.show-pixel-grid"))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :show-pixel-grid))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click toggle-flag + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-test "hide-ui" + :id "file-menu-hide-ui"} + [:span {:class (stl/css :item-name)} + (tr "workspace.shape.menu.hide-ui")] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :hide-ui))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]]])) + +(mf/defc edit-menu + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [on-close]}] + (let [select-all (mf/use-fn #(st/emit! (dw/select-all))) + undo (mf/use-fn #(st/emit! dwc/undo)) + redo (mf/use-fn #(st/emit! dwc/redo))] + [:& dropdown-menu {:show true + :list-class (stl/css-case :sub-menu true + :edit true) + :on-close on-close} + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click select-all + :on-key-down (fn [event] + (when (kbd/enter? event) + (select-all event))) + :id "file-menu-select-all"} + [:span {:class (stl/css :item-name)} + (tr "workspace.header.menu.select-all")] + [:span {:class (stl/css :shortcut)} + + (for [sc (scd/split-sc (sc/get-tooltip :select-all))] + [:span {:class (stl/css :shortcut-key) + :key sc} + sc])]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click undo + :on-key-down (fn [event] + (when (kbd/enter? event) + (undo event))) + :id "file-menu-undo"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.undo")] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :undo))] + [:span {:class (stl/css :shortcut-key) + :key sc} + sc])]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click redo + :on-key-down (fn [event] + (when (kbd/enter? event) + (redo event))) + :id "file-menu-redo"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.redo")] + [:span {:class (stl/css :shortcut)} + + (for [sc (scd/split-sc (sc/get-tooltip :redo))] + [:span {:class (stl/css :shortcut-key) + :key sc} + sc])]]])) + +(mf/defc file-menu + {::mf/wrap-props false} + [{:keys [on-close file]}] + (let [file-id (:id file) + shared? (:is-shared file) + + objects (mf/deref refs/workspace-page-objects) + frames (->> (cfh/get-immediate-children objects uuid/zero) + (filterv cfh/frame-shape?)) + + on-remove-shared + (mf/use-fn + (mf/deps file-id) + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (modal/show! + {:type :delete-shared-libraries + :origin :unpublish + :ids #{file-id} + :on-accept #(st/emit! (dwl/set-file-shared file-id false)) + :count-libraries 1}))) + + on-remove-shared-key-down + (mf/use-fn + (mf/deps on-remove-shared) + (fn [event] + (when (kbd/enter? event) + (on-remove-shared event)))) + + on-add-shared + (mf/use-fn + (mf/deps file-id) + (fn [_event] + (let [on-accept #(st/emit! (dwl/set-file-shared file-id true))] + (st/emit! (dcm/show-shared-dialog file-id on-accept))))) + + on-add-shared-key-down + (mf/use-fn + (mf/deps on-add-shared) + (fn [event] + (when (kbd/enter? event) + (on-add-shared event)))) + + on-export-shapes + (mf/use-fn #(st/emit! (de/show-workspace-export-dialog))) + + on-export-shapes-key-down + (mf/use-fn + (mf/deps on-export-shapes) + (fn [event] + (when (kbd/enter? event) + (on-export-shapes event)))) + + on-export-file + (mf/use-fn + (mf/deps file) + (fn [event] + (let [target (dom/get-current-target event) + binary? (= (dom/get-data target "binary") "true") + evname (if binary? + "export-binary-files" + "export-standard-files")] + (st/emit! + (ptk/event ::ev/event {::ev/name evname + ::ev/origin "workspace" + :num-files 1}) + (dcm/export-files [file] binary?))))) + + on-export-file-key-down + (mf/use-fn + (mf/deps on-export-file) + (fn [event] + (when (kbd/enter? event) + (on-export-file event)))) + + on-export-frames + (mf/use-fn + (mf/deps frames) + (fn [_] + (st/emit! (de/show-workspace-export-frames-dialog (reverse frames))))) + + on-export-frames-key-down + (mf/use-fn + (mf/deps on-export-frames) + (fn [event] + (when (kbd/enter? event) + (on-export-frames event))))] + + [:& dropdown-menu {:show true + :list-class (stl/css-case :sub-menu true + :file true) + :on-close on-close} + + (if ^boolean shared? + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click on-remove-shared + :on-key-down on-remove-shared-key-down + :id "file-menu-remove-shared"} + [:span {:class (stl/css :item-name)} (tr "dashboard.unpublish-shared")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click on-add-shared + :on-key-down on-add-shared-key-down + :id "file-menu-add-shared"} + [:span {:class (stl/css :item-name)} (tr "dashboard.add-shared")]]) + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click on-export-shapes + :on-key-down on-export-shapes-key-down + :id "file-menu-export-shapes"} + [:span {:class (stl/css :item-name)} (tr "dashboard.export-shapes")] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :export-shapes))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click on-export-file + :on-key-down on-export-file-key-down + :data-binary true + :id "file-menu-binary-file"} + [:span {:class (stl/css :item-name)} + (tr "dashboard.download-binary-file")]] + + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click on-export-file + :on-key-down on-export-file-key-down + :data-binary false + :id "file-menu-standard-file"} + [:span {:class (stl/css :item-name)} + (tr "dashboard.download-standard-file")]] + + (when (seq frames) + [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :on-click on-export-frames + :on-key-down on-export-frames-key-down + :id "file-menu-export-frames"} + [:span {:class (stl/css :item-name)} + (tr "dashboard.export-frames")]])])) + +(mf/defc menu + {::mf/wrap-props false} + [{:keys [layout file profile]}] + (let [show-menu* (mf/use-state false) + show-menu? (deref show-menu*) + sub-menu* (mf/use-state false) + sub-menu (deref sub-menu*) + + open-menu + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! show-menu* true))) + + close-menu + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! show-menu* false))) + + close-sub-menu + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! sub-menu* nil))) + + on-menu-click + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (let [menu (-> (dom/get-current-target event) + (dom/get-data "test") + (keyword))] + (reset! sub-menu* menu)))) + + toggle-flag + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (let [flag (-> (dom/get-current-target event) + (dom/get-data "test") + (keyword))] + (st/emit! + (-> (dw/toggle-layout-flag flag) + (vary-meta assoc ::ev/origin "workspace-menu"))) + (reset! show-menu* false) + (reset! sub-menu* nil)))) + + + toggle-theme + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (st/emit! (du/toggle-theme))))] + + + [:* + [:div {:on-click open-menu + :class (stl/css :menu-btn)} i/menu] + + [:& dropdown-menu {:show show-menu? + :on-close close-menu + :list-class (stl/css :menu)} + + [:> dropdown-menu-item* {:class (stl/css :menu-item) + :on-click on-menu-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event))) + :on-pointer-enter on-menu-click + :data-test "file" + :id "file-menu-file"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.file")] + [:span {:class (stl/css :open-arrow)} i/arrow]] + + [:> dropdown-menu-item* {:class (stl/css :menu-item) + :on-click on-menu-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event))) + :on-pointer-enter on-menu-click + :data-test "edit" + :id "file-menu-edit"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.edit")] + [:span {:class (stl/css :open-arrow)} i/arrow]] + + [:> dropdown-menu-item* {:class (stl/css :menu-item) + :on-click on-menu-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event))) + :on-pointer-enter on-menu-click + :data-test "view" + :id "file-menu-view"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.view")] + [:span {:class (stl/css :open-arrow)} i/arrow]] + + [:> dropdown-menu-item* {:class (stl/css :menu-item) + :on-click on-menu-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event))) + :on-pointer-enter on-menu-click + :data-test "preferences" + :id "file-menu-preferences"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.preferences")] + [:span {:class (stl/css :open-arrow)} i/arrow]] + [:div {:class (stl/css :separator)}] + [:> dropdown-menu-item* {:class (stl/css-case :menu-item true) + :on-click on-menu-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event))) + :on-pointer-enter on-menu-click + :data-test "help-info" + :id "file-menu-help-info"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.help-info")] + [:span {:class (stl/css :open-arrow)} i/arrow]]] + + (case sub-menu + :file + [:& file-menu + {:file file + :on-close close-sub-menu}] + + :edit + [:& edit-menu + {:on-close close-sub-menu}] + + :view + [:& view-menu + {:layout layout + :toggle-flag toggle-flag + :on-close close-sub-menu}] + + :preferences + [:& preferences-menu + {:layout layout + :profile profile + :toggle-flag toggle-flag + :toggle-theme toggle-theme + :on-close close-sub-menu}] + + :help-info + [:& help-info-menu + {:layout layout + :on-close close-sub-menu}] + + nil)])) diff --git a/frontend/src/app/main/ui/workspace/main_menu.scss b/frontend/src/app/main/ui/workspace/main_menu.scss new file mode 100644 index 0000000000..55732dab29 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/main_menu.scss @@ -0,0 +1,101 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.menu-btn { + @extend .button-tertiary; + height: $s-32; + width: calc($s-24 + $s-4); + padding: 0; + border-radius: $br-8; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.menu { + @extend .menu-dropdown; + top: $s-48; + left: calc(var(--width, $s-256) - $s-16); + width: $s-192; + margin: 0; +} + +.menu-item { + @extend .menu-item-base; + cursor: pointer; + .open-arrow { + @include flexCenter; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + } + &:hover { + color: var(--menu-foreground-color-hover); + .open-arrow { + svg { + stroke: var(--menu-foreground-color-hover); + } + } + .shortcut-key { + color: var(--menu-shortcut-foreground-color-hover); + } + } +} + +.separator { + margin-top: $s-8; + height: $s-4; + border-top: $s-1 solid $db-secondary; +} + +.shortcut { + @extend .shortcut-base; +} +.shortcut-key { + @extend .shortcut-key-base; +} + +.sub-menu { + @extend .menu-dropdown; + left: calc(var(--width, $s-256) + $s-180); + width: $s-192; + min-width: calc($s-272 - $s-2); + width: 110%; + + .submenu-item { + @extend .menu-item-base; + &:hover { + color: var(--menu-foreground-color-hover); + .shortcut-key { + color: var(--menu-shortcut-foreground-color-hover); + } + } + } + + &.file { + top: $s-48; + } + + &.edit { + top: $s-76; + } + + &.view { + top: $s-116; + } + + &.preferences { + top: $s-148; + } + + &.help-info { + top: $s-196; + } +} diff --git a/frontend/src/app/main/ui/workspace/nudge.cljs b/frontend/src/app/main/ui/workspace/nudge.cljs index 1b466ba4b8..28e5676115 100644 --- a/frontend/src/app/main/ui/workspace/nudge.cljs +++ b/frontend/src/app/main/ui/workspace/nudge.cljs @@ -5,12 +5,13 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.nudge + (:require-macros [app.main.style :as stl]) (:require [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -40,21 +41,24 @@ (->> (events/listen js/document EventType.KEYDOWN on-keydown) (partial events/unlistenByKey))) - [:div.nudge-modal-overlay - [:div.nudge-modal-container - [:div.nudge-modal-header - [:p.nudge-modal-title (tr "modals.nudge-title")] - [:button.modal-close-button {:on-click on-close} i/close]] - [:div.nudge-modal-body - [:div.input-wrapper - [:span - [:p.nudge-subtitle (tr "modals.small-nudge")] - [:> numeric-input {:min 0.01 + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} (tr "modals.nudge-title")] + [:button {:class (stl/css :modal-close-btn) + :on-click on-close} i/close]] + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :input-wrapper)} + [:label {:class (stl/css :modal-msg) + :for "nudge-small"} (tr "modals.small-nudge")] + [:> numeric-input* {:min 0.01 + :id "nudge-small" :value (:small nudge) - :on-change update-small}]]] - [:div.input-wrapper - [:span - [:p.nudge-subtitle (tr "modals.big-nudge")] - [:> numeric-input {:min 0.01 + :on-change update-small}]] + [:div {:class (stl/css :input-wrapper)} + [:label {:class (stl/css :modal-msg) + :for "nudge-big"} (tr "modals.big-nudge")] + [:> numeric-input* {:min 0.01 + :id "nudge-big" :value (:big nudge) - :on-change update-big}]]]]]])) + :on-change update-big}]]]]])) diff --git a/frontend/src/app/main/ui/workspace/nudge.scss b/frontend/src/app/main/ui/workspace/nudge.scss new file mode 100644 index 0000000000..084645b45f --- /dev/null +++ b/frontend/src/app/main/ui/workspace/nudge.scss @@ -0,0 +1,48 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + @extend .modal-container-base; + min-width: $s-408; +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include headlineMediumTypography; + color: var(--modal-title-foreground-color); +} +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content { + @include flexColumn; + gap: $s-24; + @include bodyLargeTypography; + margin-bottom: $s-24; +} + +.input-wrapper { + @extend .input-with-label; + label { + text-transform: none; + } +} + +.modal-msg { + @include bodyLargeTypography; + color: var(--modal-text-foreground-color); + line-height: 1.5; +} diff --git a/frontend/src/app/main/ui/workspace/palette.cljs b/frontend/src/app/main/ui/workspace/palette.cljs index 595b0587ef..eab23c77bc 100644 --- a/frontend/src/app/main/ui/workspace/palette.cljs +++ b/frontend/src/app/main/ui/workspace/palette.cljs @@ -5,8 +5,9 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.palette - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.main.data.events :as ev] [app.main.data.workspace :as dw] @@ -33,32 +34,50 @@ (def viewport (l/derived :vport refs/workspace-local)) +(defn calculate-palette-padding [rulers?] + (let [left-sidebar (dom/get-element "left-sidebar-aside") + left-sidebar-size (-> (dom/get-data left-sidebar "size") + (d/parse-integer)) + rulers-width (if rulers? 22 0) + min-left-sidebar-width 275 + left-padding 4 + calculate-padding-left (+ rulers-width (or left-sidebar-size min-left-sidebar-width) left-padding 1)] + + #js {"paddingLeft" (dm/str calculate-padding-left "px") + "paddingRight" "calc(var(--s-4) * 70)"})) + (mf/defc palette - [{:keys [layout]}] + [{:keys [layout on-change-palette-size]}] (let [color-palette? (:colorpalette layout) text-palette? (:textpalette layout) workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) container (mf/use-ref nil) - state (mf/use-state {:show-menu false :hide-palettes false}) + state* (mf/use-state {:show-menu false :hide-palettes false}) + state (deref state*) + show-menu? (:show-menu state) + hide-palettes? (:hide-palettes state) selected (h/use-shared-state mdc/colorpalette-selected-broadcast-key :recent) - selected-text (mf/use-state :file) + selected-text* (mf/use-state :file) + selected-text (deref selected-text*) on-select (mf/use-fn #(reset! selected %)) + rulers? (mf/deref refs/rulers?) {:keys [on-pointer-down on-lost-pointer-capture on-pointer-move parent-ref size]} - (r/use-resize-hook :palette 72 54 80 :y true :bottom) + (r/use-resize-hook :palette 72 54 80 :y true :bottom on-change-palette-size) vport (mf/deref viewport) vport-width (:width vport) + on-resize (mf/use-callback (fn [_] (let [dom (mf/ref-val container) width (obj/get dom "clientWidth")] - (swap! state assoc :width width)))) + (swap! state* assoc :width width)))) on-close-menu (mf/use-callback (fn [_] - (swap! state assoc :show-menu false))) + (swap! state* assoc :show-menu false))) on-select-palette (mf/use-fn @@ -70,25 +89,48 @@ (keyword value) (parse-uuid value)))))) - on-select-text-palette + on-select-text-palette-menu (mf/use-fn (mf/deps on-select) (fn [lib] (if (or (nil? lib) (= :file lib)) - (reset! selected-text :file) - (reset! selected-text (:id lib))))) + (reset! selected-text* :file) + (reset! selected-text* (:id lib))))) toggle-palettes (mf/use-callback (fn [_] - (swap! state update :hide-palettes not))) + (swap! state* update :hide-palettes not))) + + on-select-color-palette + (mf/use-fn + (fn [event] + (let [node (dom/get-current-target event)] + (r/set-resize-type! :top) + (dom/add-class! (dom/get-element-by-class "color-palette") "fade-out-down") + (ts/schedule 300 #(st/emit! (dw/remove-layout-flag :textpalette) + (-> (dw/toggle-layout-flag :colorpalette) + (vary-meta assoc ::ev/origin "workspace-left-toolbar")))) + (dom/blur! node)))) + + on-select-text-palette + (mf/use-fn + (fn [event] + (let [node (dom/get-current-target event)] + (r/set-resize-type! :top) + (dom/add-class! (dom/get-element-by-class "color-palette") "fade-out-down") + (ts/schedule 300 #(st/emit! (dw/remove-layout-flag :colorpalette) + (-> (dw/toggle-layout-flag :textpalette) + (vary-meta assoc ::ev/origin "workspace-left-toolbar")))) + (dom/blur! node)))) any-palette? (or color-palette? text-palette?) - size-classname (cond - (<= size 64) (css :small-palette) - (<= size 72) (css :mid-palette) - (<= size 80) (css :big-palette))] + size-classname + (cond + (<= size 64) (stl/css :small-palette) + (<= size 72) (stl/css :mid-palette) + (<= size 80) (stl/css :big-palette))] (mf/with-effect [] (let [key1 (events/listen js/window "resize" on-resize)] @@ -97,80 +139,64 @@ (mf/use-layout-effect #(let [dom (mf/ref-val parent-ref) width (obj/get dom "clientWidth")] - (swap! state assoc :width width))) + (swap! state* assoc :width width))) - [:div {:ref parent-ref - :class (dom/classnames (css :palettes) true - size-classname true - (css :wide) any-palette? - (css :hidden-bts) (:hide-palettes @state)) - :style #js {"--height" (dm/str size "px")}} - - [:div {:class (dom/classnames (css :resize-area) true) - :on-pointer-down on-pointer-down - :on-lost-pointer-capture on-lost-pointer-capture - :on-pointer-move on-pointer-move}] + [:div {:class (stl/css :palette-wrapper) + :style (calculate-palette-padding rulers?)} (when-not workspace-read-only? - [:ul {:class (dom/classnames (css :palette-btn-list) true - (css :hidden-bts) (:hide-palettes @state) - size-classname true)} - [:li {:class (dom/classnames (css :palette-item) true)} - [:button - {:title (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette)) - :aria-label (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette)) - :class (dom/classnames (css :palette-btn) true - (css :selected) color-palette?) - :on-click (fn [event] - (let [node (dom/get-current-target event)] - (r/set-resize-type! :top) - (dom/add-class! (dom/get-element-by-class "color-palette") "fade-out-down") - (ts/schedule 300 #(st/emit! (dw/remove-layout-flag :textpalette) - (-> (dw/toggle-layout-flag :colorpalette) - (vary-meta assoc ::ev/origin "workspace-left-toolbar")))) + [:div {:ref parent-ref + :class (dm/str size-classname " " (stl/css-case :palettes true + :wide any-palette? + :hidden-bts hide-palettes?)) + :style #js {"--height" (dm/str size "px")}} - (dom/blur! node)))} - i/drop-refactor]] - - [:li {:class (dom/classnames (css :palette-item) true)} - [:button - {:title (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette)) - :aria-label (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette)) - :class (dom/classnames (css :palette-btn) true - (css :selected) text-palette?) - :on-click (fn [event] - (let [node (dom/get-current-target event)] - (r/set-resize-type! :top) - (dom/add-class! (dom/get-element-by-class "color-palette") "fade-out-down") - (ts/schedule 300 #(st/emit! (dw/remove-layout-flag :colorpalette) - (-> (dw/toggle-layout-flag :textpalette) - (vary-meta assoc ::ev/origin "workspace-left-toolbar")))) - (dom/blur! node)))} - i/text-palette-refactor]]]) + [:div {:class (stl/css :resize-area) + :on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move}] + [:ul {:class (dm/str size-classname " " (stl/css-case :palette-btn-list true + :hidden-bts hide-palettes?))} + [:li {:class (stl/css :palette-item)} + [:button {:title (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette)) + :aria-label (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette)) + :class (stl/css-case :palette-btn true + :selected color-palette?) + :on-click on-select-color-palette} + i/drop-icon]] - (if any-palette? - [:* - [:button {:class (dom/classnames (css :palette-actions) true) - :on-click #(swap! state update :show-menu not)} - i/menu-refactor] - [:div {:class (dom/classnames (css :palette) true) - :ref container} - (when text-palette? - [:* - [:& text-palette-ctx-menu {:show-menu? (:show-menu @state) - :close-menu on-close-menu - :on-select-palette on-select-text-palette - :selected @selected-text}] - [:& text-palette {:size size - :selected @selected-text - :width vport-width}]]) - (when color-palette? - [:* [:& color-palette-ctx-menu {:show-menu? (:show-menu @state) - :close-menu on-close-menu - :on-select-palette on-select-palette - :selected @selected}] - [:& color-palette {:size size - :selected @selected - :width vport-width}]])]] - [:div {:class (dom/classnames (css :handler) true) - :on-click toggle-palettes} - [:div {:class (dom/classnames (css :handler-btn) true)}]])])) + [:li {:class (stl/css :palette-item)} + [:button {:title (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette)) + :aria-label (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette)) + :class (stl/css-case :palette-btn true + :selected text-palette?) + :on-click on-select-text-palette} + i/text-palette]]] + + + (if any-palette? + [:* + [:button {:class (stl/css :palette-actions) + :on-click #(swap! state* update :show-menu not)} + i/menu] + [:div {:class (stl/css :palette) + :ref container} + (when text-palette? + [:* + [:& text-palette-ctx-menu {:show-menu? show-menu? + :close-menu on-close-menu + :on-select-palette on-select-text-palette-menu + :selected selected-text}] + [:& text-palette {:size size + :selected selected-text + :width vport-width}]]) + (when color-palette? + [:* [:& color-palette-ctx-menu {:show-menu? show-menu? + :close-menu on-close-menu + :on-select-palette on-select-palette + :selected @selected}] + [:& color-palette {:size size + :selected @selected + :width vport-width}]])]] + [:div {:class (stl/css :handler) + :on-click toggle-palettes} + [:div {:class (stl/css :handler-btn)}]])])])) diff --git a/frontend/src/app/main/ui/workspace/palette.css.json b/frontend/src/app/main/ui/workspace/palette.css.json deleted file mode 100644 index ec0718e284..0000000000 --- a/frontend/src/app/main/ui/workspace/palette.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"workspace_palette_button-primary_zEUyD","palettes":"workspace_palette_palettes_JHGUw","palette-actions":"workspace_palette_palette-actions_2GwR6","palette-btn-list":"workspace_palette_palette-btn-list_x7gPS","palette-item":"workspace_palette_palette-item_50uj6","palette-btn":"workspace_palette_palette-btn_kP66y","button-secondary":"workspace_palette_button-secondary_ksr24","button-icon":"workspace_palette_button-icon_pmEDv","button-icon-small":"workspace_palette_button-icon-small_vbLDq","wide":"workspace_palette_wide_3G4e1","mid-palette":"workspace_palette_mid-palette_rGR5I","small-palette":"workspace_palette_small-palette_18Otk","resize-area":"workspace_palette_resize-area_0LwVu","selected":"workspace_palette_selected_Z6BFo","palette":"workspace_palette_palette_eqp3q","handler":"workspace_palette_handler_4JV0J","handler-btn":"workspace_palette_handler-btn_7lnlF","hidden-bts":"workspace_palette_hidden-bts_mhbc0"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/palette.scss b/frontend/src/app/main/ui/workspace/palette.scss index 209593e2fe..9474a79090 100644 --- a/frontend/src/app/main/ui/workspace/palette.scss +++ b/frontend/src/app/main/ui/workspace/palette.scss @@ -6,7 +6,16 @@ @import "refactor/common-refactor.scss"; +.palette-wrapper { + position: absolute; + width: 100vw; + left: 0; + bottom: 0; + padding-bottom: $s-4; +} + .palettes { + z-index: $z-index-2; position: relative; right: 0; grid-area: color-palette; @@ -22,14 +31,15 @@ padding: $s-0 $s-0 $s-8 $s-8; border-radius: $br-8; background-color: var(--palette-background-color); - transition: right 1s ease, opacity 1s ease; + border: $s-2 solid var(--panel-border-color); + transition: + right 0.3s, + opacity 0.2s, + width 0.3s; &.wide { width: 100%; } - &.mid-palette, - &.small-palette { - grid-template-columns: $s-64 auto 1fr; - } + .resize-area { grid-area: resize; height: $s-8; @@ -46,7 +56,8 @@ width: $s-32; margin: $s-0; list-style: none; - z-index: $z-index-1; + z-index: $z-index-2; + gap: $s-2; &.mid-palette, &.small-palette { display: flex; @@ -57,32 +68,25 @@ opacity: $op-10; transition: opacity 1s ease; .palette-btn { - @extend .button-primary; + @extend .button-tertiary; height: $s-32; width: $s-32; border-radius: $br-8; - border: $s-2 solid transparent; background-clip: padding-box; padding: 0; svg { @extend .button-icon-small; + stroke: var(--icon-foreground); } &.selected { - border: $s-2 solid var(--palette-btn-border-color-selected); - background-color: var(--palette-btn-background-color-selected); - color: var(--palette-btn-foreground-color-selected); - svg { - stroke: var(--palette-btn-foreground-color-selected); - } - } - &:hover { - border: $s-2 solid transparent; + @extend .button-icon-selected; } } } } + .palette-actions { - @extend .button-primary; + @extend .button-tertiary; grid-area: actions; height: calc(var(--height) - $s-16); width: $s-32; @@ -90,44 +94,73 @@ margin-left: $s-4; border-radius: $br-8; background-color: var(--palette-background-color); - z-index: $z-index-1; + z-index: $z-index-2; svg { @extend .button-icon; + stroke: var(--icon-foreground); } } .palette { grid-area: palette; width: 100%; - } - .handler { - @include buttonStyle; - @include flexCenter; - width: $s-12; - height: 100%; - - .handler-btn { - width: $s-4; - height: 100%; - max-height: $s-40; - margin: $s-8 $s-4; - padding: 0; - border-radius: $s-4; - background-color: var(--palette-handler-background-color); - } - } - &.hidden-bts { - right: $s-40; - z-index: 0; - &.small-palette, - &.mid-palette { - right: $s-72; - } - .palette-btn-list { - .palette-item { - opacity: $op-0; - visibility: hidden; - z-index: 0; - } - } + min-width: 0; + } +} + +.handler { + @include buttonStyle; + @include flexCenter; + width: $s-12; + height: 100%; + .handler-btn { + width: $s-4; + height: 100%; + max-height: $s-40; + margin: $s-8 $s-4; + padding: 0; + border-radius: $s-4; + background-color: var(--palette-handler-background-color); + } +} + +.mid-palette, +.small-palette { + grid-template-columns: $s-64 auto 1fr; +} + +.hidden-bts { + right: $s-2; + z-index: $z-index-1; + width: 22px; + grid-template-columns: $s-8 auto 1fr; + padding: 0; + border-inline-start: 0; + border-start-start-radius: 0; + border-end-start-radius: 0; + .palette-btn-list { + opacity: $op-0; + visibility: hidden; + width: 0; + .palette-item { + opacity: $op-0; + visibility: hidden; + z-index: 0; + } + } + .resize-area { + visibility: hidden; + z-index: 0; + width: 0; + } + .palette-actions { + visibility: hidden; + z-index: 0; + } + .palette { + visibility: hidden; + z-index: 0; + } + .handler { + padding-bottom: $s-8; } } diff --git a/frontend/src/app/main/ui/workspace/presence.cljs b/frontend/src/app/main/ui/workspace/presence.cljs index 16225650f1..cdf6c6e238 100644 --- a/frontend/src/app/main/ui/workspace/presence.cljs +++ b/frontend/src/app/main/ui/workspace/presence.cljs @@ -5,31 +5,74 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.presence + (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] + [app.common.data.macros :as dm] [app.config :as cfg] [app.main.refs :as refs] + [app.util.dom :as dom] + [app.util.timers :as tm] [rumext.v2 :as mf])) -;; --- SESSION WIDGET - (mf/defc session-widget - [{:keys [session profile] :as props}] - [:li.tooltip.tooltip-bottom - {:alt (:fullname profile)} - [:img {:alt (:fullname profile) - :style {:border-color (:color session)} - :src (cfg/resolve-profile-photo-url profile)}]]) + {::mf/props :obj + ::mf/memo true} + [{:keys [color profile index]}] + (let [profile (assoc profile :color color) + full-name (:fullname profile)] + [:li {:class (stl/css :session-icon) + :style {:z-index (dm/str (+ 1 (* -1 index))) + :background-color color} + :title full-name} + [:img {:alt full-name + :style {:background-color color} + :src (cfg/resolve-profile-photo-url profile)}]])) (mf/defc active-sessions - {::mf/wrap [mf/memo]} + {::mf/memo true} [] - (let [users (mf/deref refs/users) - presence (mf/deref refs/workspace-presence)] - [:ul.active-users - (for [session (vals presence)] - [:& session-widget - {:session session - :profile (get users (:profile-id session)) - :key (:id session)}])])) + (let [users (mf/deref refs/users) + presence (mf/deref refs/workspace-presence) + sessions (vals presence) + num-sessions (count sessions) + open* (mf/use-state false) + open? (and ^boolean (deref open*) (> num-sessions 2)) + on-open + (mf/use-fn + (fn [] + (reset! open* true) + (tm/schedule-on-idle + #(dom/focus! (dom/get-element "users-close"))))) + + on-close + (mf/use-fn #(reset! open* false))] + + [:* + (when ^boolean open? + [:button {:id "users-close" + :class (stl/css :active-users-opened) + :on-click on-close + :on-blur on-close} + [:ul {:class (stl/css :active-users-list)} + (for [session sessions] + [:& session-widget + {:color (:color session) + :index 0 + :profile (get users (:profile-id session)) + :key (dm/str (:id session))}])]]) + + [:button {:class (stl/css-case :active-users true) + :on-click on-open} + [:ul {:class (stl/css :active-users-list)} + (when (> num-sessions 2) + [:span {:class (stl/css :users-num)} (dm/str "+" (- num-sessions 2))]) + + (for [[index session] (d/enumerate (take 2 sessions))] + [:& session-widget + {:color (:color session) + :index index + :profile (get users (:profile-id session)) + :key (dm/str (:id session))}])]]])) diff --git a/frontend/src/app/main/ui/workspace/presence.scss b/frontend/src/app/main/ui/workspace/presence.scss new file mode 100644 index 0000000000..22c76e3b02 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/presence.scss @@ -0,0 +1,53 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.active-users, +.active-users-opened { + @include buttonStyle; + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + align-items: center; + margin: 0; + padding: 0 $s-4; + border-radius: $br-8; + .active-users-list { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + margin: 0; + + .users-num { + @extend .user-icon; + background-color: var(--user-count-background-color); + color: var(--user-count-foreground-color); + z-index: $z-index-2; + border: $s-2 solid var(--user-count-foreground-color); + } + .session-icon { + @extend .user-icon; + } + } +} + +.active-users-opened { + position: absolute; + right: calc(-1 * $s-2); + top: calc(-1 * $s-2); + padding: $s-8; + margin: calc(-1 * $s-2) calc(-1 * $s-4) 0 0; + background-color: var(--menu-background-color); + z-index: $z-index-4; + .active-users-list { + gap: $s-4; + .users-num, + .session-icon { + margin-left: 0; + } + } +} diff --git a/frontend/src/app/main/ui/workspace/right_header.cljs b/frontend/src/app/main/ui/workspace/right_header.cljs new file mode 100644 index 0000000000..f2e1b53f0a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/right_header.cljs @@ -0,0 +1,249 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.right-header + (:require-macros [app.main.style :as stl]) + (:require + [app.main.data.events :as ev] + [app.main.data.shortcuts :as scd] + [app.main.data.workspace :as dw] + [app.main.data.workspace.shortcuts :as sc] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.context :as ctx] + [app.main.ui.export :refer [export-progress-widget]] + [app.main.ui.formats :as fmt] + [app.main.ui.icons :as i] + [app.main.ui.workspace.presence :refer [active-sessions]] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.timers :as ts] + [okulary.core :as l] + [rumext.v2 :as mf])) + +(def ref:workspace-persistence + (l/derived :workspace-persistence st/state)) + +;; --- Persistence state Widget + +(mf/defc persistence-state-widget + {::mf/wrap [mf/memo]} + [] + (let [{:keys [status]} (mf/deref ref:workspace-persistence)] + [:div {:class (stl/css :persistence-status-widget)} + (case status + :pending + [:div {:class (stl/css-case :status-icon true + :pending-status true) + :title (tr "workspace.header.unsaved")} + i/status-alert] + + :saving + [:div {:class (stl/css-case :status-icon true + :saving-status true) + :title (tr "workspace.header.saving")} + i/status-update] + + :saved + [:div {:class (stl/css-case :status-icon true + :saved-status true) + :title (tr "workspace.header.saved")} + i/status-tick] + + :error + [:div {:class (stl/css-case :status-icon true + :error-status true) + :title "There was an error saving the data. Please refresh if this persists."} + i/status-wrong] + + nil)])) + +;; --- Zoom Widget + +(mf/defc zoom-widget-workspace + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [{:keys [zoom on-increase on-decrease on-zoom-reset on-zoom-fit on-zoom-selected]}] + (let [open* (mf/use-state false) + open? (deref open*) + + open-dropdown + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! open* true))) + + close-dropdown + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! open* false))) + + on-increase + (mf/use-fn + (mf/deps on-increase) + (fn [event] + (dom/stop-propagation event) + (on-increase))) + + on-decrease + (mf/use-fn + (mf/deps on-decrease) + (fn [event] + (dom/stop-propagation event) + (on-decrease))) + + zoom (fmt/format-percent zoom {:precision 0})] + + [:* + [:div {:on-click open-dropdown + :class (stl/css-case :zoom-widget true + :selected open?) + :title (tr "workspace.header.zoom")} + [:span {:class (stl/css :label)} zoom]] + [:& dropdown {:show open? :on-close close-dropdown} + [:ul {:class (stl/css :dropdown)} + [:li {:class (stl/css :basic-zoom-bar)} + [:span {:class (stl/css :zoom-btns)} + [:button {:class (stl/css :zoom-btn) + :on-click on-decrease} + [:span {:class (stl/css :zoom-icon)} + i/remove-icon]] + [:p {:class (stl/css :zoom-text)} zoom] + [:button {:class (stl/css :zoom-btn) + :on-click on-increase} + [:span {:class (stl/css :zoom-icon)} + i/add]]] + [:button {:class (stl/css :reset-btn) + :on-click on-zoom-reset} + (tr "workspace.header.reset-zoom")]] + [:li {:class (stl/css :zoom-option) + :on-click on-zoom-fit} + (tr "workspace.header.zoom-fit-all") + [:span {:class (stl/css :shortcuts)} + (for [sc (scd/split-sc (sc/get-tooltip :fit-all))] + [:span {:class (stl/css :shortcut-key) + :key (str "zoom-fit-" sc)} sc])]] + [:li {:class (stl/css :zoom-option) + :on-click on-zoom-selected} + (tr "workspace.header.zoom-selected") + [:span {:class (stl/css :shortcuts)} + (for [sc (scd/split-sc (sc/get-tooltip :zoom-selected))] + [:span {:class (stl/css :shortcut-key) + :key (str "zoom-selected-" sc)} sc])]]]]])) + +;; --- Header Component + +(mf/defc right-header + {::mf/wrap-props false} + [{:keys [file layout page-id]}] + (let [file-id (:id file) + + zoom (mf/deref refs/selected-zoom) + read-only? (mf/use-ctx ctx/workspace-read-only?) + selected-drawtool (mf/deref refs/selected-drawing-tool) + + on-increase (mf/use-fn #(st/emit! (dw/increase-zoom nil))) + on-decrease (mf/use-fn #(st/emit! (dw/decrease-zoom nil))) + on-zoom-reset (mf/use-fn #(st/emit! dw/reset-zoom)) + on-zoom-fit (mf/use-fn #(st/emit! dw/zoom-to-fit-all)) + on-zoom-selected (mf/use-fn #(st/emit! dw/zoom-to-selected-shape)) + + editing* (mf/use-state false) + editing? (deref editing*) + + input-ref (mf/use-ref nil) + + nav-to-viewer + (mf/use-fn + (mf/deps file-id page-id) + (fn [] + (let [params {:page-id page-id + :file-id file-id + :section "interactions"}] + (st/emit! (dw/go-to-viewer params))))) + + active-comments + (mf/use-fn + (fn [] + (st/emit! :interrupt + (dw/clear-edition-mode)) + ;; Delay so anything that launched :interrupt can finish + (ts/schedule 100 #(st/emit! (dw/select-for-drawing :comments))))) + + toggle-comments + (mf/use-fn + (mf/deps selected-drawtool) + (fn [_] + (when (contains? layout :document-history) + (st/emit! (-> (dw/remove-layout-flag :document-history) + (vary-meta assoc ::ev/origin "workspace-header")))) + + (if (= :comments selected-drawtool) + (st/emit! :interrupt) + (active-comments)))) + + toggle-history + (mf/use-fn + (mf/deps selected-drawtool) + (fn [] + + (when (= :comments selected-drawtool) + (st/emit! :interrupt + (-> (dw/toggle-layout-flag :comments) + (vary-meta assoc ::ev/origin "workspace-header")))) + + (st/emit! (-> (dw/toggle-layout-flag :document-history) + (vary-meta assoc ::ev/origin "workspace-header")))))] + + (mf/with-effect [editing?] + (when ^boolean editing? + (dom/select-text! (mf/ref-val input-ref)))) + + [:div {:class (stl/css :workspace-header-right)} + [:div {:class (stl/css :users-section)} + [:& active-sessions]] + + [:& persistence-state-widget] + + [:& export-progress-widget] + + [:div {:class (stl/css :separator)}] + + [:div {:class (stl/css :zoom-section)} + [:& zoom-widget-workspace + {:zoom zoom + :on-increase on-increase + :on-decrease on-decrease + :on-zoom-reset on-zoom-reset + :on-zoom-fit on-zoom-fit + :on-zoom-selected on-zoom-selected}]] + + [:div {:class (stl/css :comments-section)} + [:button {:title (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment)) + :aria-label (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment)) + :class (stl/css-case :comments-btn true + :selected (= selected-drawtool :comments)) + :on-click toggle-comments + :data-tool "comments"} + i/comments]] + + (when-not ^boolean read-only? + [:div {:class (stl/css :history-section)} + [:button + {:title (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history)) + :aria-label (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history)) + :class (stl/css-case :selected (contains? layout :document-history) + :history-button true) + :on-click toggle-history} + i/history]]) + + [:a {:class (stl/css :viewer-btn) + :title (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer)) + :on-click nav-to-viewer} + i/play]])) + diff --git a/frontend/src/app/main/ui/workspace/right_header.scss b/frontend/src/app/main/ui/workspace/right_header.scss new file mode 100644 index 0000000000..dd459675b9 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/right_header.scss @@ -0,0 +1,232 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; +.workspace-header-right { + display: flex; + justify-content: space-between; + align-items: center; + min-width: $s-256; + padding: $s-8; + gap: $s-8; + background-color: var(--panel-background-color); +} + +.users-section { + position: relative; + min-width: $s-32; + max-width: $s-72; + padding: $s-4 $s-6; +} + +.separator { + flex: 1; +} + +.zoom-widget { + @include buttonStyle; + display: flex; + align-items: center; + justify-content: center; + height: $s-28; + max-width: $s-48; + width: $s-48; + border-radius: $br-8; + .label { + @include bodySmallTypography; + height: 100%; + padding: $s-8 0; + color: var(--button-tertiary-foreground-color-rest); + } + + &:hover { + .label { + color: var(--button-tertiary-foreground-color-focus); + } + } + &.selected { + .label { + color: var(--button-tertiary-foreground-color-focus); + } + } +} + +.dropdown { + @extend .menu-dropdown; + right: $s-2; + top: calc($s-2 + $s-48); + width: $s-272; +} + +.basic-zoom-bar { + display: flex; + justify-content: space-between; + padding: $s-6; + cursor: auto; +} + +.zoom-btns { + display: flex; +} + +.zoom-btn { + @extend .button-tertiary; + height: $s-28; + width: $s-28; + border-radius: $br-8; + .zoom-icon { + @include flexCenter; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + } + &:hover { + .zoom-icon svg { + stroke: var(--button-tertiary-foreground-color-hover); + } + } +} + +.zoom-text { + @include flexCenter; + height: 100%; + min-width: $s-48; + padding: 0; + margin: 0 $s-2; + color: var(--modal-title-foreground-color); +} + +.reset-btn { + @extend .button-tertiary; + color: var(--button-tertiary-foreground-color-hover); + height: $s-28; + border-radius: $br-8; +} +.zoom-option { + @extend .menu-item-base; + .shortcuts { + @extend .shortcut-base; + .shortcut-key { + @extend .shortcut-key-base; + } + } + &:hover { + color: var(--menu-foreground-color-hover); + .shortcuts { + .shortcut-key { + color: var(--menu-foreground-color-hover); + } + } + } +} + +.comments-btn { + @extend .button-tertiary; + border-radius: $br-8; + margin: 0; + height: $s-28; + width: $s-28; + border: none; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + height: $s-16; + width: $s-16; + } + &:hover { + background-color: transparent; + border: none; + } + &.selected { + background-color: var(--button-tertiary-background-color-selected); + svg { + stroke: var(--button-tertiary-foreground-color-active); + } + } +} + +.history-button { + @extend .button-tertiary; + border-radius: $br-8; + margin: 0; + height: $s-28; + width: $s-28; + border: none; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + height: $s-16; + width: $s-16; + } + &:hover { + background-color: transparent; + border: none; + } + &.selected { + background-color: var(--button-tertiary-background-color-selected); + svg { + stroke: var(--button-tertiary-foreground-color-active); + } + } +} + +.persistence-status-widget { + @include flexCenter; + width: $s-28; + height: $s-28; +} + +.status-icon { + @include flexCenter; + width: $s-24; + height: $s-24; + margin: 0; + border-radius: $br-circle; + svg { + @extend .button-icon; + stroke: var(--status-widget-icon-foreground-color); + } +} + +.pending-status { + background-color: var(--status-widget-background-color-warning); +} + +.saving-status { + background-color: var(--status-widget-background-color-pending); + svg { + animation: spin-animation 1s infinite; + animation-timing-function: linear; + } +} + +.saved-status { + background-color: var(--status-widget-background-color-success); +} + +.error-status { + background-color: var(--status-widget-background-color-error); +} + +.viewer-btn { + @extend .button-tertiary; + border-radius: $br-8; + margin: 0; + width: $s-28; + height: $s-28; + border: none; + svg { + @extend .button-icon; + height: $s-16; + width: $s-16; + stroke: var(--icon-foreground); + } + &:hover { + background-color: transparent; + border: none; + } +} diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index e7b5ae517d..d1889e620e 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -12,8 +12,11 @@ others are defined using a generic wrapper implemented in common." (:require + [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cfh] + [app.common.geom.rect :as grc] + [app.common.geom.shapes :as gsh] [app.common.uuid :as uuid] [app.main.ui.context :as ctx] [app.main.ui.shapes.circle :as circle] @@ -34,95 +37,104 @@ (declare group-wrapper) (declare svg-raw-wrapper) (declare bool-wrapper) -(declare root-frame-wrapper) (declare nested-frame-wrapper) +(declare root-frame-wrapper) (def circle-wrapper (common/generic-wrapper-factory circle/circle-shape)) (def image-wrapper (common/generic-wrapper-factory image/image-shape)) (def rect-wrapper (common/generic-wrapper-factory rect/rect-shape)) +(defn- make-is-frame-overlap + [vbox objects] + (fn [shape] + (let [bounds + (if (dm/get-prop shape :show-content) + (let [children (->> (cfh/get-children-ids objects (dm/get-prop shape :id)) + (map (d/getf objects)))] + (gsh/shapes->rect (cons shape children))) + (dm/get-prop shape :selrect))] + (grc/overlaps-rects? vbox bounds)))) + (mf/defc root-shape "Draws the root shape of the viewport and recursively all the shapes" {::mf/wrap [mf/memo] ::mf/wrap-props false} [props] - (let [objects (obj/get props "objects") - active-frames (obj/get props "active-frames") - shapes (cph/get-immediate-children objects) + (let [objects (obj/get props "objects") + active-frames (obj/get props "active-frames") + shapes (cfh/get-immediate-children objects) + vbox (mf/use-ctx ctx/current-vbox) - ;; We group the objects together per frame-id so if an object of a different - ;; frame changes won't affect the rendering frame - frame-objects - (mf/use-memo - (mf/deps objects) - #(cph/objects-by-frame objects))] + frame-overlap? (mf/with-memo [vbox objects] + #(make-is-frame-overlap vbox objects)) + + shapes (mf/with-memo [shapes vbox frame-overlap?] + (cond->> shapes + (some? vbox) + (filter frame-overlap?)))] [:g {:id (dm/str "shape-" uuid/zero)} [:& (mf/provider ctx/active-frames) {:value active-frames} ;; Render font faces only for shapes that are part of the root ;; frame but don't belongs to any other frame. (let [xform (comp - (remove cph/frame-shape?) - (mapcat #(cph/get-children-with-self objects (:id %))))] + (remove cfh/frame-shape?) + (mapcat #(cfh/get-children-with-self objects (:id %))))] [:& ff/fontfaces-style {:shapes (into [] xform shapes)}]) [:g.frame-children (for [shape shapes] - [:g.ws-shape-wrapper {:key (:id shape)} - (cond - (not (cph/frame-shape? shape)) - [:& shape-wrapper - {:shape shape}] - - (cph/root-frame? shape) + [:g.ws-shape-wrapper {:key (dm/str (dm/get-prop shape :id))} + (if ^boolean (cfh/frame-shape? shape) [:& root-frame-wrapper {:shape shape - :objects (get frame-objects (:id shape)) - :thumbnail? (not (contains? active-frames (:id shape)))}] - - :else - [:& nested-frame-wrapper - {:shape shape - :objects (get frame-objects (:id shape))}])])]]])) + :objects objects + :thumbnail? (not (contains? active-frames (dm/get-prop shape :id)))}] + [:& shape-wrapper {:shape shape}])])]]])) (mf/defc shape-wrapper - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] + {::mf/wrap [#(mf/memo' % common/check-shape-props)] ::mf/wrap-props false} [props] - (let [shape (obj/get props "shape") + (let [shape (unchecked-get props "shape") + shape-type (dm/get-prop shape :type) + shape-id (dm/get-prop shape :id) + ;; FIXME: WARN: this breaks react rule of hooks (hooks can't be under conditional) active-frames - (when (cph/root-frame? shape) (mf/use-ctx ctx/active-frames)) + (when (cfh/root-frame? shape) + (mf/use-ctx ctx/active-frames)) thumbnail? (and (some? active-frames) - (not (contains? active-frames (:id shape)))) + (not (contains? active-frames shape-id))) - opts #js {:shape shape :thumbnail? thumbnail?} + props #js {:shape shape :thumbnail? thumbnail?} - [wrapper wrapper-props] - (if (= :svg-raw (:type shape)) - [mf/Fragment nil] - ["g" #js {:className "workspace-shape-wrapper"}])] + rawsvg? (= :svg-raw shape-type) + wrapper-elem (if ^boolean rawsvg? mf/Fragment "g") + wrapper-props (if ^boolean rawsvg? + #js {} + #js {:className "workspace-shape-wrapper"})] - (when (and (some? shape) (not (:hidden shape))) - [:> wrapper wrapper-props - (case (:type shape) - :path [:> path/path-wrapper opts] - :text [:> text/text-wrapper opts] - :group [:> group-wrapper opts] - :rect [:> rect-wrapper opts] - :image [:> image-wrapper opts] - :circle [:> circle-wrapper opts] - :svg-raw [:> svg-raw-wrapper opts] - :bool [:> bool-wrapper opts] - :frame [:> nested-frame-wrapper opts] + (when (and (some? shape) + (not ^boolean (:hidden shape))) + [:> wrapper-elem wrapper-props + (case shape-type + :path [:> path/path-wrapper props] + :text [:> text/text-wrapper props] + :group [:> group-wrapper props] + :rect [:> rect-wrapper props] + :image [:> image-wrapper props] + :circle [:> circle-wrapper props] + :svg-raw [:> svg-raw-wrapper props] + :bool [:> bool-wrapper props] + :frame [:> nested-frame-wrapper props] nil)]))) (def group-wrapper (group/group-wrapper-factory shape-wrapper)) (def svg-raw-wrapper (svg-raw/svg-raw-wrapper-factory shape-wrapper)) (def bool-wrapper (bool/bool-wrapper-factory shape-wrapper)) -(def root-frame-wrapper (frame/root-frame-wrapper-factory shape-wrapper)) (def nested-frame-wrapper (frame/nested-frame-wrapper-factory shape-wrapper)) - +(def root-frame-wrapper (frame/root-frame-wrapper-factory shape-wrapper)) diff --git a/frontend/src/app/main/ui/workspace/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/shapes/bool.cljs index 62cc6e8212..a3080b704e 100644 --- a/frontend/src/app/main/ui/workspace/shapes/bool.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/bool.cljs @@ -6,47 +6,40 @@ (ns app.main.ui.workspace.shapes.bool (:require - [app.main.data.workspace :as dw] + [app.common.data.macros :as dm] [app.main.refs :as refs] - [app.main.store :as st] - [app.main.streams :as ms] [app.main.ui.shapes.bool :as bool] [app.main.ui.shapes.shape :refer [shape-container]] - [app.util.dom :as dom] + [app.main.ui.workspace.shapes.common :refer [check-shape-props]] + [app.main.ui.workspace.shapes.debug :as wsd] [rumext.v2 :as mf])) -(defn use-double-click [{:keys [id]}] - (mf/use-callback - (mf/deps id) - (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (st/emit! (dw/select-inside-group id @ms/mouse-position))))) - (defn bool-wrapper-factory [shape-wrapper] - (let [shape-component (bool/bool-shape shape-wrapper)] + (let [bool-shape (bool/bool-shape shape-wrapper)] (mf/fnc bool-wrapper - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] + {::mf/wrap [#(mf/memo' % check-shape-props)] ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - child-sel-ref (mf/use-memo - (mf/deps (:id shape)) - #(refs/is-child-selected? (:id shape))) + (let [shape (unchecked-get props "shape") + shape-id (dm/get-prop shape :id) - childs-ref (mf/use-memo - (mf/deps (:id shape)) - #(refs/select-bool-children (:id shape))) + child-sel* (mf/with-memo [shape-id] + (refs/is-child-selected? shape-id)) - child-sel? (mf/deref child-sel-ref) - childs (mf/deref childs-ref) + childs* (mf/with-memo [shape-id] + (refs/select-bool-children shape-id)) - shape (cond-> shape - child-sel? - (dissoc :bool-content))] + child-sel? (mf/deref child-sel*) + childs (mf/deref childs*) + + shape (cond-> shape + ^boolean child-sel? + (dissoc :bool-content))] [:> shape-container {:shape shape} - [:& shape-component {:shape shape - :childs childs}]])))) + [:& bool-shape {:shape shape + :childs childs}] + (when *assert* + [:& wsd/shape-debug {:shape shape}])])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/common.cljs b/frontend/src/app/main/ui/workspace/shapes/common.cljs index cccc1cb7ef..4af042e877 100644 --- a/frontend/src/app/main/ui/workspace/shapes/common.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/common.cljs @@ -6,15 +6,35 @@ (ns app.main.ui.workspace.shapes.common (:require + [app.common.record :as cr] [app.main.ui.shapes.shape :refer [shape-container]] + [app.main.ui.workspace.shapes.debug :as wsd] [rumext.v2 :as mf])) +(def ^:private excluded-attrs + #{:blocked + :hide-fill-on-export + :collapsed + :remote-synced + :exports}) + +(defn check-shape + [new-shape old-shape] + (cr/-equiv-with-exceptions old-shape new-shape excluded-attrs)) + +(defn check-shape-props + [np op] + (check-shape (unchecked-get np "shape") + (unchecked-get op "shape"))) + (defn generic-wrapper-factory [component] (mf/fnc generic-wrapper - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] + {::mf/wrap [#(mf/memo' % check-shape-props)] ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape")] [:> shape-container {:shape shape} - [:& component {:shape shape}]]))) + [:& component {:shape shape}] + (when *assert* + [:& wsd/shape-debug {:shape shape}])]))) diff --git a/frontend/src/app/main/ui/workspace/shapes/debug.cljs b/frontend/src/app/main/ui/workspace/shapes/debug.cljs new file mode 100644 index 0000000000..8844d18bc1 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/debug.cljs @@ -0,0 +1,188 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.shapes.debug + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.path :as gsp] + [app.common.geom.shapes.text :as gst] + [app.common.math :as mth] + [app.common.svg.path.bool :as pb] + [app.common.svg.path.shapes-to-path :as stp] + [app.common.svg.path.subpath :as ups] + [app.main.refs :as refs] + [app.util.color :as uc] + [app.util.debug :as dbg] + [app.util.dom :as dom] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(mf/defc debug-bounding-boxes + [{:keys [shape]}] + (let [points (->> (:points shape) + (map #(dm/fmt "%,%" (dm/get-prop % :x) (dm/get-prop % :y))) + (str/join " ")) + color (mf/use-memo #(uc/random-color)) + sr (:selrect shape)] + [:g.debug-bounding-boxes + [:rect {:transform (gsh/transform-str shape) + :x (:x sr) + :y (:y sr) + :width (:width sr) + :height (:height sr) + :fill color + :opacity 0.2}] + (for [p (:points shape)] + [:circle {:cx (dm/get-prop p :x) + :cy (dm/get-prop p :y) + :r 2 + :fill color}]) + [:polygon {:points points + :stroke-width 1 + :stroke color}]])) + +(mf/defc debug-text-bounds + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + zoom (mf/deref refs/selected-zoom) + bounding-box (gst/shape->rect shape) + ctx (js* "document.createElement(\"canvas\").getContext(\"2d\")")] + [:g {:transform (gsh/transform-str shape)} + [:rect {:x (:x bounding-box) + :y (:y bounding-box) + :width (:width bounding-box) + :height (:height bounding-box) + :style {:fill "none" + :stroke "orange" + :stroke-width (/ 1 zoom)}}] + + (for [[index data] (d/enumerate (:position-data shape))] + (let [{:keys [x y width height]} data + res (dom/measure-text ctx (:font-size data) (:font-family data) (:text data))] + [:g {:key (dm/str index)} + ;; Text fragment bounding box + [:rect {:x x + :y (- y height) + :width width + :height height + :style {:fill "none" + :stroke "red" + :stroke-width (/ 1 zoom)}}] + + ;; Text baseline + [:line {:x1 (mth/round x) + :y1 (mth/round (- (:y data) (:height data))) + :x2 (mth/round (+ x width)) + :y2 (mth/round (- (:y data) (:height data))) + :style {:stroke "blue" + :stroke-width (/ 1 zoom)}}] + + [:line {:x1 (:x data) + :y1 (- (:y data) (:descent res)) + :x2 (+ (:x data) (:width data)) + :y2 (- (:y data) (:descent res)) + :style {:stroke "green" + :stroke-width (/ 2 zoom)}}]]))])) + +(mf/defc debug-bool-shape + {::mf/wrap-props false} + [{:keys [shape]}] + + (let [objects (mf/deref refs/workspace-page-objects) + zoom (mf/deref refs/selected-zoom) + + radius (/ 3 zoom) + + c1 (-> (get objects (first (:shapes shape))) + (stp/convert-to-path objects)) + c2 (-> (get objects (second (:shapes shape))) + (stp/convert-to-path objects)) + + content-a (:content c1) + content-b (:content c2) + + bool-type (:bool-type shape) + should-reverse? (and (not= :union bool-type) + (= (ups/clockwise? content-b) + (ups/clockwise? content-a))) + + content-a (-> (:content c1) + (pb/close-paths) + (pb/add-previous)) + + content-b (-> (:content c2) + (pb/close-paths) + (cond-> should-reverse? (ups/reverse-content)) + (pb/add-previous)) + + + sr-a (gsp/content->selrect content-a) + sr-b (gsp/content->selrect content-b) + + [content-a-split content-b-split] (pb/content-intersect-split content-a content-b sr-a sr-b) + + ;;content-a-geom (gsp/content->geom-data content-a) + ;;content-b-geom (gsp/content->geom-data content-b) + ;;content-a-split (->> content-a-split #_(filter #(pb/contains-segment? % content-b sr-b content-b-geom))) + ;;content-b-split (->> content-b-split #_(filter #(pb/contains-segment? % content-a sr-a content-a-geom))) + ] + [:* + (for [[i cmd] (d/enumerate content-a-split)] + (let [p1 (:prev cmd) + p2 (gsp/command->point cmd) + + hp (case (:command cmd) + :line-to (-> (gsp/command->line cmd) + (gsp/line-values 0.5)) + + :curve-to (-> (gsp/command->bezier cmd) + (gsp/curve-values 0.5)) + nil)] + [:* + (when p1 + [:circle {:data-i i :key (dm/str "c11-" i) :cx (:x p1) :cy (:y p1) :r radius :fill "red"}]) + [:circle {:data-i i :key (dm/str "c12-" i) :cx (:x p2) :cy (:y p2) :r radius :fill "red"}] + + (when hp + [:circle {:data-i i :key (dm/str "c13-" i) :cx (:x hp) :cy (:y hp) :r radius :fill "orange"}])])) + + (for [[i cmd] (d/enumerate content-b-split)] + (let [p1 (:prev cmd) + p2 (gsp/command->point cmd) + + hp (case (:command cmd) + :line-to (-> (gsp/command->line cmd) + (gsp/line-values 0.5)) + + :curve-to (-> (gsp/command->bezier cmd) + (gsp/curve-values 0.5)) + nil)] + [:* + (when p1 + [:circle {:key (dm/str "c21-" i) :cx (:x p1) :cy (:y p1) :r radius :fill "blue"}]) + [:circle {:key (dm/str "c22-" i) :cx (:x p2) :cy (:y p2) :r radius :fill "blue"}] + + (when hp + [:circle {:data-i i :key (dm/str "c13-" i) :cx (:x hp) :cy (:y hp) :r radius :fill "green"}])]))])) + +(mf/defc shape-debug + [{:keys [shape]}] + [:* + (when ^boolean (dbg/enabled? :bounding-boxes) + [:& debug-bounding-boxes {:shape shape}]) + + (when (and ^boolean (dbg/enabled? :bool-shapes) + ^boolean (cfh/bool-shape? shape)) + [:& debug-bool-shape {:shape shape}]) + + (when (and ^boolean (dbg/enabled? :text-outline) + ^boolean (cfh/text-shape? shape) + ^boolean (seq (:position-data shape))) + [:& debug-text-bounds {:shape shape}])]) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index b18e6b5478..f4bd1f9d3a 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -8,46 +8,59 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cfh] + [app.common.geom.shapes.bounds :as gsb] + [app.common.math :as mth] + [app.common.thumbnails :as thc] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.thumbnails :as dwt] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as ctx] - [app.main.ui.hooks :as hooks] - [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.shape :refer [shape-container]] - [app.main.ui.shapes.text.fontfaces :as ff] + [app.main.ui.workspace.shapes.common :refer [check-shape-props]] + [app.main.ui.workspace.shapes.debug :as wsd] [app.main.ui.workspace.shapes.frame.dynamic-modifiers :as fdm] - [app.main.ui.workspace.shapes.frame.node-store :as fns] - [app.main.ui.workspace.shapes.frame.thumbnail-render :as ftr] - [beicon.core :as rx] + [app.util.debug :as dbg] + [app.util.dom :as dom] + [app.util.thumbnails :as th] + [app.util.timers :as tm] + [promesa.core :as p] [rumext.v2 :as mf])) (defn frame-shape-factory [shape-wrapper] (let [frame-shape (frame/frame-shape shape-wrapper)] (mf/fnc frame-shape-inner - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] + {::mf/wrap [#(mf/memo' % check-shape-props)] ::mf/wrap-props false ::mf/forward-ref true} [props ref] (let [shape (unchecked-get props "shape") - childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape))) + shape-id (dm/get-prop shape :id) + + childs-ref (mf/with-memo [shape-id] + (refs/children-objects shape-id)) childs (mf/deref childs-ref)] - [:& (mf/provider embed/context) {:value true} - [:& shape-container {:shape shape :ref ref :disable-shadows? (cph/root-frame? shape)} - [:& frame-shape {:shape shape :childs childs} ]]])))) + [:& shape-container {:shape shape :ref ref :disable-shadows? (cfh/is-direct-child-of-root? shape)} + [:& frame-shape {:shape shape :childs childs}] + (when *assert* + [:& wsd/shape-debug {:shape shape}])])))) (defn check-props [new-props old-props] (and (= (unchecked-get new-props "thumbnail?") (unchecked-get old-props "thumbnail?")) - (= (unchecked-get new-props "shape") - (unchecked-get old-props "shape")))) + + (identical? + (unchecked-get new-props "objects") + (unchecked-get old-props "objects")) + + ^boolean + (check-shape-props new-props old-props))) (defn nested-frame-wrapper-factory [shape-wrapper] @@ -57,91 +70,166 @@ {::mf/wrap [#(mf/memo' % check-props)] ::mf/wrap-props false} [props] + (let [shape (unchecked-get props "shape") + objects (wsh/lookup-page-objects @st/state) - (let [shape (unchecked-get props "shape") - frame-id (:id shape) - objects (wsh/lookup-page-objects @st/state) - node-ref (mf/use-var nil) - modifiers-ref (mf/use-memo (mf/deps frame-id) #(refs/workspace-modifiers-by-frame-id frame-id)) - modifiers (mf/deref modifiers-ref)] + frame-id (dm/get-prop shape :id) - (fdm/use-dynamic-modifiers objects @node-ref modifiers) - (let [shape (unchecked-get props "shape")] - [:& frame-shape {:shape shape :ref node-ref}]))))) + node-ref (mf/use-ref nil) + modifiers* (mf/with-memo [frame-id] + (refs/workspace-modifiers-by-frame-id frame-id)) + modifiers (mf/deref modifiers*)] + + (fdm/use-dynamic-modifiers objects (mf/ref-val node-ref) modifiers) + [:& frame-shape {:shape shape :ref node-ref}])))) + +(defn image-size + [href] + (p/create + (fn [resolve _] + (let [img (js/Image.) + load-fn + (fn [] + (let [width (.-naturalWidth img) + height (.-naturalHeight img)] + (resolve {:width width :height height})))] + (set! (.-onload img) load-fn) + (set! (.-src img) href))))) + +(defn check-thumbnail-size + [image-node bounds file-id page-id frame-id] + (let [href (dom/get-attribute image-node "href") + width (dm/get-prop bounds :width) + height (dm/get-prop bounds :height) + [fixed-width fixed-height] (th/get-relative-size width height)] + ;; Even if looks like we're doing a new request the browser caches the image + ;; so really we don't. We need a different API to check the sizes + (-> (image-size href) + (p/then + (fn [{:keys [width height]}] + (when (or (not (mth/close? width fixed-width 5)) + (not (mth/close? height fixed-height 5))) + (st/emit! (dwt/request-thumbnail file-id page-id frame-id "frame" "check-thumbnail-size")))))))) (defn root-frame-wrapper-factory [shape-wrapper] - (let [frame-shape (frame-shape-factory shape-wrapper)] (mf/fnc frame-wrapper {::mf/wrap [#(mf/memo' % check-props)] ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - thumbnail? (unchecked-get props "thumbnail?") + (let [shape (unchecked-get props "shape") + thumbnail? (unchecked-get props "thumbnail?") + objects (unchecked-get props "objects") - page-id (mf/use-ctx ctx/current-page-id) - frame-id (:id shape) + file-id (mf/use-ctx ctx/current-file-id) + page-id (mf/use-ctx ctx/current-page-id) + frame-id (dm/get-prop shape :id) - objects (wsh/lookup-page-objects @st/state) + container-ref (mf/use-ref nil) + content-ref (mf/use-ref nil) - node* (mf/use-var nil) - force-render* (mf/use-state false) - force-render? (deref force-render*) + bounds (gsb/get-object-bounds objects shape {:ignore-margin? false}) - ;; when `true` we've called the mount for the frame - rendered* (mf/use-var false) + x (dm/get-prop bounds :x) + y (dm/get-prop bounds :y) + width (dm/get-prop bounds :width) + height (dm/get-prop bounds :height) - modifiers-ref (mf/with-memo [frame-id] - (refs/workspace-modifiers-by-frame-id frame-id)) - modifiers (mf/deref modifiers-ref) + thumbnail-uri* (mf/with-memo [file-id page-id frame-id] + (let [object-id (thc/fmt-object-id file-id page-id frame-id "frame")] + (refs/workspace-thumbnail-by-id object-id))) + thumbnail-uri (mf/deref thumbnail-uri*) + modifiers-ref (mf/with-memo [frame-id] + (refs/workspace-modifiers-by-frame-id frame-id)) + modifiers (mf/deref modifiers-ref) - fonts (mf/with-memo [shape objects] - (ff/shape->fonts shape objects)) - fonts (hooks/use-equal-memo fonts) + hidden? (true? (:hidden shape)) + content-visible? (or (not ^boolean thumbnail?) (not ^boolean thumbnail-uri)) - disable-thumbnail? (d/not-empty? (dm/get-in modifiers [frame-id :modifiers])) + tries-ref (mf/use-ref 0) + imposter-ref (mf/use-ref nil) + imposter-loaded (mf/use-state false) + task-ref (mf/use-ref nil) - [on-load-frame-dom render-frame? thumbnail-renderer] - (ftr/use-render-thumbnail page-id shape node* rendered* disable-thumbnail? force-render?) - - on-frame-load - (fns/use-node-store thumbnail? node* rendered* render-frame?)] - - (fdm/use-dynamic-modifiers objects @node* modifiers) + on-load (mf/use-fn (fn [] + ;; We need to check if this is the culprit of the thumbnail regeneration. + ;; (check-thumbnail-size (mf/ref-val imposter-ref) bounds file-id page-id frame-id) + (mf/set-ref-val! tries-ref 0) + (reset! imposter-loaded true))) + on-error (mf/use-fn + (fn [] + (let [current-tries (mf/ref-val tries-ref) + new-tries (mf/set-ref-val! tries-ref (inc current-tries)) + delay-in-ms (* (mth/pow 2 new-tries) 1000) + retry-fn (fn [] + (let [imposter (mf/ref-val imposter-ref)] + (when-not (nil? imposter) + (dom/set-attribute! imposter "href" thumbnail-uri))))] + (when (< new-tries 8) + (mf/set-ref-val! task-ref (tm/schedule delay-in-ms retry-fn))))))] + ;; NOTE: we don't add deps because we want this to be executed + ;; once on mount with only referenced the initial data (mf/with-effect [] - ;; When a change in the data is received a "force-render" event is emitted - ;; that will force the component to be mounted in memory - (let [sub (->> (dwt/force-render-stream frame-id) - (rx/take-while #(not @rendered*)) - (rx/subs #(reset! force-render* true)))] - #(some-> sub rx/dispose!))) + (when-not (some? thumbnail-uri) + (tm/schedule-on-idle + #(st/emit! (dwt/request-thumbnail file-id page-id frame-id "frame" "root-frame")))) + #(when-let [task (mf/ref-val task-ref)] + (d/close! task))) - (mf/with-effect [shape fonts thumbnail? on-load-frame-dom force-render? render-frame?] - (when (and (some? @node*) - (or @rendered* - (not thumbnail?) - force-render? - render-frame?)) - (let [elem (mf/element frame-shape #js {:ref on-load-frame-dom :shape shape :fonts fonts})] - (mf/mount elem @node*) - (when (not @rendered*) - (reset! rendered* true))))) + (mf/with-effect [thumbnail-uri] + (when-let [task (mf/ref-val task-ref)] + (d/close! task))) - [:& shape-container {:shape shape} - [:g.frame-container {:id (dm/str "frame-container-" frame-id) - :key "frame-container" - :ref on-frame-load - :opacity (when (:hidden shape) 0)} - [:& ff/fontfaces-style {:fonts fonts}] - [:g.frame-thumbnail-wrapper - {:id (dm/str "thumbnail-container-" frame-id) - ;; Hide the thumbnail when not displaying - :opacity (when-not thumbnail? 0)} - thumbnail-renderer]] + (fdm/use-dynamic-modifiers objects (mf/ref-val content-ref) modifiers) - ])))) + [:& shape-container {:shape shape :disable-shadows? thumbnail?} + [:g.frame-container + {:id (dm/str "frame-container-" frame-id) + :key "frame-container" + :opacity (when ^boolean hidden? 0)} + + ;; When there is no thumbnail, we generate a empty rect. + (when (and (not ^boolean content-visible?) (not @imposter-loaded)) + [:g.frame-placeholder + [:rect {:x x + :y y + :width width + :height height + :fill "url(#frame-placeholder-gradient)"}]]) + + [:g.frame-imposter + [:image.thumbnail-bitmap + {:x x + :y y + :ref imposter-ref + :width width + :height height + :href thumbnail-uri + :on-load on-load + :on-error on-error + :style {:display (when-not (and ^boolean thumbnail? ^boolean thumbnail-uri) "none")}}] + + ;; Render border around image when we are debugging + ;; thumbnails. + (when (dbg/enabled? :thumbnails) + [:rect {:x (+ x 2) + :y (+ y 2) + :width (- width 4) + :height (- height 4) + :stroke "#f0f" + :stroke-width 2}])] + + ;; When thumbnail is disabled. + (when ^boolean content-visible? + [:g.frame-content + {:id (dm/str "frame-content-" frame-id) + :ref container-ref} + [:& frame-shape {:shape shape :ref content-ref}]])] + + (when *assert* + [:& wsd/shape-debug {:shape shape}])])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs index caba728b9c..41f0584b35 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs @@ -8,17 +8,18 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] [app.common.types.modifiers :as ctm] [app.main.store :as st] [app.main.ui.hooks :as hooks] [app.main.ui.workspace.viewport.utils :as vwu] + [app.util.debug :as dbg] [app.util.dom :as dom] - [app.util.globals :as globals] - [debug :refer [debug?]] + [app.util.timers :as ts] [rumext.v2 :as mf])) (defn get-shape-node @@ -36,9 +37,9 @@ (when (some? base-node) (let [shape-node (get-shape-node base-node id) parent-node (get-shape-node base-node parent-id) - frame? (cph/frame-shape? shape) - group? (cph/group-shape? shape) - text? (cph/text-shape? shape) + frame? (cfh/frame-shape? shape) + group? (cfh/group-shape? shape) + text? (cfh/text-shape? shape) masking-child? (:masking-child? (meta shape))] (cond frame? @@ -54,32 +55,32 @@ [shape-node (dom/query parent-node ".mask-clip-path") (dom/query parent-node ".mask-shape") - (when (debug? :shape-titles) + (when (dbg/enabled? :shape-titles) (dom/query (dm/str "#frame-title-" id)))] group? (let [shape-defs (dom/query shape-node "defs")] (d/concat-vec - [(when (debug? :shape-titles) + [(when (dbg/enabled? :shape-titles) (dom/query (dm/str "#frame-title-" id)))] (dom/query-all shape-defs ".svg-def") (dom/query-all shape-defs ".svg-mask-wrapper"))) text? [shape-node - (when (debug? :shape-titles) + (when (dbg/enabled? :shape-titles) (dom/query (dm/str "#frame-title-" id)))] :else [shape-node - (when (debug? :shape-titles) + (when (dbg/enabled? :shape-titles) (dom/query (dm/str "#frame-title-" id)))])))) (defn transform-region! [node modifiers] (let [{:keys [x y width height]} - (-> (gsh/make-selrect + (-> (grc/make-rect (-> (dom/get-attribute node "data-old-x") d/parse-double) (-> (dom/get-attribute node "data-old-y") d/parse-double) (-> (dom/get-attribute node "data-old-width") d/parse-double) @@ -151,7 +152,8 @@ (dom/class? node "frame-title") (let [shape (gsh/transform-shape shape modifiers) zoom (get-in @st/state [:workspace-local :zoom] 1) - mtx (vwu/title-transform shape zoom)] + edit-grid? (= (dom/get-data node "edit-grid") "true") + mtx (vwu/title-transform shape zoom edit-grid?)] (override-transform-att! node "transform" mtx)) (or (= (dom/get-tag-name node) "mask") @@ -220,7 +222,7 @@ [objects] (fn [{:keys [id parent-id] :as shape}] (let [parent (get objects parent-id) - masking-child? (and (cph/mask-shape? parent) (= id (first (:shapes parent))))] + masking-child? (and (cfh/mask-shape? parent) (= id (first (:shapes parent))))] (cond-> shape masking-child? @@ -228,85 +230,85 @@ (defn use-dynamic-modifiers [objects node modifiers] - (let [transforms - (mf/use-memo - (mf/deps modifiers) - (fn [] - (when (some? modifiers) - (d/mapm (fn [id {current-modifiers :modifiers}] - (let [shape (get objects id) - adapt-text? (and (= :text (:type shape)) (not (ctm/only-move? current-modifiers))) + (mf/with-memo [modifiers] + (when (some? modifiers) + (d/mapm (fn [id {current-modifiers :modifiers}] + (let [shape (get objects id) + adapt-text? (and (= :text (:type shape)) (not (ctm/only-move? current-modifiers))) - current-modifiers - (cond-> current-modifiers - adapt-text? - (adapt-text-modifiers shape))] - (ctm/modifiers->transform current-modifiers))) - modifiers)))) + current-modifiers + (cond-> current-modifiers + adapt-text? + (adapt-text-modifiers shape))] + (ctm/modifiers->transform current-modifiers))) + modifiers))) - add-children (mf/use-memo (mf/deps modifiers) #(ctm/added-children-frames modifiers)) - add-children (hooks/use-equal-memo add-children) - add-children-prev (hooks/use-previous add-children) + add-children + (mf/with-memo [modifiers] + (ctm/added-children-frames modifiers)) shapes - (mf/use-memo - (mf/deps transforms) - (fn [] - (->> (keys transforms) - (filter #(some? (get transforms %))) - (mapv (comp (add-masking-child? objects) (d/getf objects)))))) + (mf/with-memo [transforms] + (->> (keys transforms) + (filter #(some? (get transforms %))) + (mapv (comp (add-masking-child? objects) (d/getf objects))))) - prev-shapes (mf/use-var nil) - prev-modifiers (mf/use-var nil) - prev-transforms (mf/use-var nil)] + add-children (hooks/use-equal-memo add-children) + add-children-prev (hooks/use-previous add-children) + prev-shapes (mf/use-var nil) + prev-modifiers (mf/use-var nil) + prev-transforms (mf/use-var nil)] - (mf/use-effect - (mf/deps add-children) - (fn [] - (doseq [{:keys [shape]} add-children-prev] - (let [shape-node (get-shape-node shape) - mirror-node (dom/query (dm/fmt ".mirror-shape[href='#shape-%'" shape))] - (when mirror-node (.remove mirror-node)) - (dom/remove-attribute! (dom/get-parent shape-node) "display"))) + (mf/with-effect [add-children] + (ts/raf + #(doseq [{:keys [shape]} add-children-prev] + (let [shape-node (get-shape-node shape) + mirror-node (dom/query (dm/fmt ".mirror-shape[href='#shape-%'" shape))] + (when mirror-node (.remove mirror-node)) + (dom/remove-attribute! (dom/get-parent shape-node) "display")))) - (doseq [{:keys [frame shape]} add-children] - (let [frame-node (get-shape-node frame) - shape-node (get-shape-node shape) + (ts/raf + #(doseq [{:keys [frame shape]} add-children] + (let [frame-node (get-shape-node frame) + shape-node (get-shape-node shape) - clip-id - (dom/get-attribute (dom/query frame-node ":scope > defs > .frame-clip-def") "id") + clip-id + (-> (dom/query frame-node ":scope > defs > .frame-clip-def") + (dom/get-attribute "id")) - use-node - (.createElementNS globals/document "http://www.w3.org/2000/svg" "use") + use-node + (dom/create-element "http://www.w3.org/2000/svg" "use") - contents-node - (or (dom/query frame-node ".frame-children") frame-node)] + contents-node + (or (dom/query frame-node ".frame-children") frame-node)] - (dom/set-attribute! use-node "href" (dm/fmt "#shape-%" shape)) - (dom/set-attribute! use-node "clip-path" (dm/fmt "url(#%)" clip-id)) - (dom/add-class! use-node "mirror-shape") - (dom/append-child! contents-node use-node) - (dom/set-attribute! (dom/get-parent shape-node) "display" "none"))))) + (dom/set-attribute! use-node "href" (dm/fmt "#shape-%" shape)) + (dom/set-attribute! use-node "clip-path" (dm/fmt "url(#%)" clip-id)) + (dom/add-class! use-node "mirror-shape") + (dom/append-child! contents-node use-node) + (dom/set-attribute! (dom/get-parent shape-node) "display" "none"))))) - (mf/use-layout-effect - (mf/deps transforms) - (fn [] - (let [curr-shapes-set (into #{} (map :id) shapes) - prev-shapes-set (into #{} (map :id) @prev-shapes) + (mf/with-effect [transforms] + (let [curr-shapes-set (into #{} (map :id) shapes) + prev-shapes-set (into #{} (map :id) @prev-shapes) - new-shapes (->> shapes (remove #(contains? prev-shapes-set (:id %)))) - removed-shapes (->> @prev-shapes (remove #(contains? curr-shapes-set (:id %))))] + new-shapes (->> shapes (remove #(contains? prev-shapes-set (:id %)))) + removed-shapes (->> @prev-shapes (remove #(contains? curr-shapes-set (:id %))))] - (when (d/not-empty? new-shapes) - (start-transform! node new-shapes)) + ;; NOTE: we schedule the dom modifications to be executed + ;; asynchronously for avoid component flickering when react18 + ;; is used. - (when (d/not-empty? shapes) - (update-transform! node shapes transforms modifiers)) + (when (d/not-empty? new-shapes) + (ts/raf #(start-transform! node new-shapes))) - (when (d/not-empty? removed-shapes) - (remove-transform! node removed-shapes))) + (when (d/not-empty? shapes) + (ts/raf #(update-transform! node shapes transforms modifiers))) - (reset! prev-modifiers modifiers) - (reset! prev-transforms transforms) - (reset! prev-shapes shapes))))) + (when (d/not-empty? removed-shapes) + (ts/raf #(remove-transform! node removed-shapes)))) + + (reset! prev-modifiers modifiers) + (reset! prev-transforms transforms) + (reset! prev-shapes shapes)))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs deleted file mode 100644 index d37470c433..0000000000 --- a/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs +++ /dev/null @@ -1,46 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.workspace.shapes.frame.node-store - (:require - [app.util.dom :as dom] - [app.util.globals :as globals] - [rumext.v2 :as mf])) - -(defn use-node-store - "Hook responsible of storing the rendered DOM node in memory while not being used" - [thumbnail? node-ref rendered? render-frame?] - - (let [;; when `true` the node is in memory - in-memory? (mf/use-state true) - - ;; State just for re-rendering - re-render (mf/use-state 0) - - parent-ref (mf/use-var nil) - - on-frame-load - (mf/use-callback - (fn [node] - (when (and (some? node) (nil? @node-ref)) - (let [content (-> (.createElementNS globals/document "http://www.w3.org/2000/svg" "g") - (dom/add-class! "frame-content"))] - (reset! node-ref content) - (reset! parent-ref node) - (swap! re-render inc)))))] - - (mf/use-layout-effect - (mf/deps thumbnail? render-frame?) - (fn [] - (when (and (some? @parent-ref) (some? @node-ref) @rendered? (and thumbnail? (not render-frame?))) - (.removeChild @parent-ref @node-ref) - (reset! in-memory? true)) - - (when (and (some? @node-ref) @in-memory? (or (not thumbnail?) render-frame?)) - (.appendChild @parent-ref @node-ref) - (reset! in-memory? false)))) - - on-frame-load)) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs deleted file mode 100644 index 485a16e038..0000000000 --- a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs +++ /dev/null @@ -1,269 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.workspace.shapes.frame.thumbnail-render - (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.geom.shapes :as gsh] - [app.config :as cf] - [app.main.data.workspace.thumbnails :as dwt] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.hooks :as hooks] - [app.main.ui.shapes.frame :as frame] - [app.util.dom :as dom] - [app.util.thumbnails :as th] - [app.util.timers :as ts] - [app.util.webapi :as wapi] - [beicon.core :as rx] - [cuerdas.core :as str] - [debug :refer [debug?]] - [rumext.v2 :as mf])) - -(defn- remove-image-loading - "Remove the changes related to change a url for its embed value. This is necessary - so we don't have to recalculate the thumbnail when the image loads." - [value] - (if (.isArray js/Array value) - (->> value - (remove (fn [change] - (or (= "data-loading" (.-attributeName change)) - (and (= "attributes" (.-type change)) - (= "href" (.-attributeName change)) - (str/starts-with? (.-oldValue change) "http")))))) - [value])) - -(defn- create-svg-blob-uri-from - [rect node style-node] - (let [{:keys [x y width height]} rect - viewbox (dm/str x " " y " " width " " height) - - ;; Calculate the fixed width and height - ;; We don't want to generate thumbnails - ;; bigger than 2000px - [fixed-width fixed-height] (th/get-proportional-size width height) - - ;; This is way faster than creating a node - ;; through the DOM API - svg-data - (dm/fmt "% %" - viewbox - fixed-width - fixed-height - (if (some? style-node) (dom/node->xml style-node) "") - (dom/node->xml node)) - - ;; create SVG blob - blob (wapi/create-blob svg-data "image/svg+xml;charset=utf-8") - url (dm/str (wapi/create-uri blob) "#svg")] - ;; returns the url and the node - url)) - -(defn use-render-thumbnail - "Hook that will create the thumbnail data" - [page-id {:keys [id] :as shape} node-ref rendered? disable? force-render] - - (let [frame-image-ref (mf/use-ref nil) - - disable* (mf/use-var disable?) - regenerate* (mf/use-var false) - - all-children-ref (mf/with-memo [id] - (refs/all-children-objects id)) - all-children (mf/deref all-children-ref) - - {:keys [x y width height] :as shape-bb} - (if (:show-content shape) - (gsh/selection-rect (concat [shape] all-children)) - (-> shape :points gsh/points->selrect)) - - svg-uri* (mf/use-state nil) - bitmap-uri* (mf/use-state nil) - observer* (mf/use-var nil) - - shape-bb* (hooks/use-update-var shape-bb) - updates-s (mf/use-memo rx/subject) - - thumbnail-uri-ref (mf/with-memo [page-id id] - (refs/thumbnail-frame-data page-id id)) - thumbnail-uri (mf/deref thumbnail-uri-ref) - - ;; State to indicate to the parent that should render the frame - render-frame* (mf/use-state (not thumbnail-uri)) - debug? (debug? :thumbnails) - - on-bitmap-load - (mf/use-fn - (fn [] - ;; We revoke the SVG Blob URI to free memory only when we - ;; are sure that it is not used anymore. - (wapi/revoke-uri @svg-uri*) - (reset! svg-uri* nil))) - - on-svg-load - (mf/use-callback - (fn [] - (let [image-node (mf/ref-val frame-image-ref)] - (dom/set-data! image-node "ready" "true") - - ;; If we don't have the thumbnail data saved (normally the first load) we update the data - ;; when available - (when (not ^boolean @thumbnail-uri-ref) - (st/emit! (dwt/update-thumbnail page-id id))) - - (reset! render-frame* false)))) - - generate-thumbnail - (mf/use-fn - (mf/deps id) - (fn generate-thumbnail [] - (try - ;; When starting generating the canvas we mark it as not ready so its not send to back until - ;; we have time to update it - (let [node @node-ref] - (if (dom/has-children? node) - ;; The frame-content need to have children in order to generate the thumbnail - (let [style-node (dom/query (dm/str "#frame-container-" id " style")) - url (create-svg-blob-uri-from @shape-bb* node style-node)] - (reset! svg-uri* url)) - - ;; Node not yet ready, we schedule a new generation - (ts/raf generate-thumbnail))) - - (catch :default e - (.error js/console e))))) - - on-change-frame - (mf/use-fn - (mf/deps id) - (fn [] - (when (and ^boolean @node-ref - ^boolean @rendered? - ^boolean @regenerate*) - (let [loading-images? (some? (dom/query @node-ref "[data-loading='true']")) - loading-fonts? (some? (dom/query (dm/str "#frame-container-" id " > style[data-loading='true']")))] - (when (and (not loading-images?) - (not loading-fonts?)) - (reset! svg-uri* nil) - (reset! bitmap-uri* nil) - (generate-thumbnail) - (reset! regenerate* false)))))) - - ;; When the frame is updated, it is marked as not ready - ;; so that it is not sent to the background until - ;; it is regenerated. - on-update-frame - (mf/use-fn - (fn [] - (let [image-node (mf/ref-val frame-image-ref)] - (when (not= "false" (dom/get-data image-node "ready")) - (dom/set-data! image-node "ready" "false"))) - (when-not ^boolean @disable* - (reset! svg-uri* nil) - (reset! bitmap-uri* nil) - (reset! render-frame* true) - (reset! regenerate* true)))) - - on-load-frame-dom - (mf/use-fn - (fn [node] - (when (and (some? node) - (nil? @observer*)) - (when-not (some? @thumbnail-uri-ref) - (rx/push! updates-s :update)) - - (let [observer (js/MutationObserver. (partial rx/push! updates-s))] - (.observe observer node #js {:childList true :attributes true :attributeOldValue true :characterData true :subtree true}) - (reset! observer* observer)))))] - - (mf/with-effect [thumbnail-uri] - (when (some? thumbnail-uri) - (reset! bitmap-uri* thumbnail-uri))) - - (mf/with-effect [force-render] - (when ^boolean force-render - (rx/push! updates-s :update))) - - (mf/with-effect [] - (let [subid (->> updates-s - (rx/map remove-image-loading) - (rx/filter d/not-empty?) - (rx/catch (fn [err] (.error js/console err))) - (rx/subs on-update-frame))] - (partial rx/dispose! subid))) - - ;; on-change-frame will get every change in the frame - (mf/with-effect [] - (let [subid (->> updates-s - (rx/debounce 400) - (rx/observe-on :af) - (rx/catch (fn [err] (.error js/console err))) - (rx/subs on-change-frame))] - (partial rx/dispose! subid))) - - (mf/with-effect [disable?] - (when (and ^boolean disable? - (not @disable*)) - (rx/push! updates-s :update)) - (reset! disable* disable?) - nil) - - (mf/with-effect [] - (fn [] - (when (and (some? @node-ref) - ^boolean @rendered?) - (mf/unmount @node-ref) - (reset! node-ref nil) - (reset! rendered? false) - (when (some? @observer*) - (.disconnect @observer*) - (reset! observer* nil))))) - - [on-load-frame-dom - @render-frame* - (mf/html - [:& frame/frame-container {:bounds shape-bb :shape shape} - - ;; Safari don't support filters so instead we add a rectangle around the thumbnail - (when (and (cf/check-browser? :safari) - ^boolean debug?) - [:rect {:x (+ x 2) - :y (+ y 2) - :width (- width 4) - :height (- height 4) - :stroke "blue" - :stroke-width 2}]) - - ;; This is similar to how double-buffering works. - ;; In svg-uri* we keep the SVG image that is used to - ;; render the bitmap until the bitmap is ready - ;; to be rendered on screen. Then we remove the - ;; svg and keep the bitmap one. - ;; This is the "buffer" that keeps the bitmap image. - (when ^boolean @bitmap-uri* - [:image.thumbnail-bitmap - {:x x - :y y - :width width - :height height - :href @bitmap-uri* - :style {:filter (when ^boolean debug? "sepia(1)")} - :on-load on-bitmap-load}]) - - ;; This is the "buffer" that keeps the SVG image. - (when ^boolean @svg-uri* - [:image.thumbnail-canvas - {:x x - :y y - :key (dm/str "thumbnail-canvas-" id) - :data-object-id (dm/str page-id id) - :width width - :height height - :ref frame-image-ref - :href @svg-uri* - :style {:filter (when ^boolean debug? "sepia(0.5)")} - :on-load on-svg-load}])])])) diff --git a/frontend/src/app/main/ui/workspace/shapes/group.cljs b/frontend/src/app/main/ui/workspace/shapes/group.cljs index dd9a1a47e8..d98d58a2f7 100644 --- a/frontend/src/app/main/ui/workspace/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/group.cljs @@ -6,36 +6,32 @@ (ns app.main.ui.workspace.shapes.group (:require - [app.main.data.workspace :as dw] + [app.common.data.macros :as dm] [app.main.refs :as refs] - [app.main.store :as st] - [app.main.streams :as ms] [app.main.ui.shapes.group :as group] [app.main.ui.shapes.shape :refer [shape-container]] - [app.util.dom :as dom] + [app.main.ui.workspace.shapes.common :refer [check-shape-props]] + [app.main.ui.workspace.shapes.debug :as wsd] [rumext.v2 :as mf])) -(defn use-double-click [{:keys [id]}] - (mf/use-callback - (mf/deps id) - (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (st/emit! (dw/select-inside-group id @ms/mouse-position))))) - (defn group-wrapper-factory [shape-wrapper] (let [group-shape (group/group-shape shape-wrapper)] (mf/fnc group-wrapper - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] + {::mf/wrap [#(mf/memo' % check-shape-props)] ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape))) - childs (mf/deref childs-ref)] + (let [shape (unchecked-get props "shape") + shape-id (dm/get-prop shape :id) + + childs* (mf/with-memo [shape-id] + (refs/children-objects shape-id)) + childs (mf/deref childs*)] [:> shape-container {:shape shape} [:& group-shape {:shape shape - :childs childs}]])))) + :childs childs}] + (when *assert* + [:& wsd/shape-debug {:shape shape}])])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index 9093a89409..110238be4b 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -6,11 +6,12 @@ (ns app.main.ui.workspace.shapes.path (:require - [app.common.path.commands :as upc] + [app.common.svg.path.command :as upc] [app.main.data.workspace.path.helpers :as helpers] [app.main.refs :as refs] [app.main.ui.shapes.path :as path] [app.main.ui.shapes.shape :refer [shape-container]] + [app.main.ui.workspace.shapes.debug :as wsd] [app.main.ui.workspace.shapes.path.common :as pc] [rumext.v2 :as mf])) @@ -38,4 +39,6 @@ [:> shape-container {:shape shape :pointer-events (when editing? "none")} - [:& path/path-shape {:shape shape}]])) + [:& path/path-shape {:shape shape}] + (when *assert* + [:& wsd/shape-debug {:shape shape}])])) diff --git a/frontend/src/app/main/ui/workspace/shapes/path/common.cljs b/frontend/src/app/main/ui/workspace/shapes/path/common.cljs index 0322e65717..e6dae1f622 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/common.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/common.cljs @@ -13,11 +13,11 @@ [okulary.core :as l] [rumext.v2 :as mf])) -(def primary-color "var(--color-select)") -(def secondary-color "var(--color-distance)") -(def black-color "var(--color-black)") -(def white-color "var(--color-white)") -(def gray-color "var(--color-gray-20)") +(def accent-color "var(--color-accent-tertiary)") +(def secondary-color "var(--color-accent-quaternary)") +(def black-color "var(--app-black)") +(def white-color "var(--app-white)") +(def gray-color "var(--df-secondary)") (def current-edit-path-ref (l/derived diff --git a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs index 36edbce91c..b6d844db61 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -10,8 +10,8 @@ [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes.path :as gsp] - [app.common.path.commands :as upc] - [app.common.path.shapes-to-path :as ups] + [app.common.svg.path.command :as upc] + [app.common.svg.path.shapes-to-path :as ups] [app.main.data.workspace.path :as drp] [app.main.snap :as snap] [app.main.store :as st] @@ -89,8 +89,8 @@ :style {:stroke-width (/ point-radius-stroke-width zoom) :stroke (cond (or selected? hover?) pc/black-color preview? pc/secondary-color - :else pc/primary-color) - :fill (cond selected? pc/primary-color + :else pc/accent-color) + :fill (cond selected? pc/accent-color :else pc/white-color)}}] [:circle {:cx x :cy y @@ -150,8 +150,8 @@ :style {:stroke-width (/ handler-stroke-width zoom) :stroke (cond (or selected? hover?) pc/black-color - :else pc/primary-color) - :fill (cond selected? pc/primary-color + :else pc/accent-color) + :fill (cond selected? pc/accent-color :else pc/white-color)}}] [:circle {:cx x :cy y @@ -288,7 +288,7 @@ [:g.path-editor {:ref editor-ref} [:path {:d (upf/format-path content) :style {:fill "none" - :stroke pc/primary-color + :stroke pc/accent-color :strokeWidth (/ 1 zoom)}}] (when (and preview (not drag-handler)) [:& path-preview {:command preview diff --git a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs index 27b9aed913..de1701e016 100644 --- a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs @@ -6,10 +6,11 @@ (ns app.main.ui.workspace.shapes.svg-raw (:require + [app.common.svg :as csvg] [app.main.refs :as refs] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.svg-raw :as svg-raw] - [app.util.svg :as usvg] + [app.main.ui.workspace.shapes.debug :as wsd] [rumext.v2 :as mf])) (defn svg-raw-wrapper-factory @@ -23,10 +24,12 @@ childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape))) childs (mf/deref childs-ref) svg-tag (get-in shape [:content :tag])] - (if (contains? usvg/svg-group-safe-tags svg-tag) + (if (contains? csvg/svg-group-safe-tags svg-tag) [:> shape-container {:shape shape} [:& svg-raw-shape {:shape shape - :childs childs}]] + :childs childs}] + (when *assert* + [:& wsd/shape-debug {:shape shape}])] [:& svg-raw-shape {:shape shape :childs childs}]))))) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index d1d34b6c0c..cdaeda400f 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -6,82 +6,34 @@ (ns app.main.ui.workspace.shapes.text (:require - [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.text :as gsht] - [app.common.math :as mth] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.text :as text] - [app.util.dom :as dom] - [debug :refer [debug?]] + [app.main.ui.workspace.shapes.debug :as wsd] [rumext.v2 :as mf])) -(mf/defc debug-text-bounds - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - zoom (mf/deref refs/selected-zoom) - bounding-box (gsht/position-data-selrect shape) - ctx (js* "document.createElement(\"canvas\").getContext(\"2d\")")] - [:g {:transform (gsh/transform-str shape)} - [:rect {:x (:x bounding-box) - :y (:y bounding-box) - :width (:width bounding-box) - :height (:height bounding-box) - :style {:fill "none" - :stroke "orange" - :stroke-width (/ 1 zoom)}}] - - (for [[index data] (d/enumerate (:position-data shape))] - (let [{:keys [x y width height]} data - res (dom/measure-text ctx (:font-size data) (:font-family data) (:text data))] - [:g {:key (dm/str index)} - ;; Text fragment bounding box - [:rect {:x x - :y (- y height) - :width width - :height height - :style {:fill "none" - :stroke "red" - :stroke-width (/ 1 zoom)}}] - - ;; Text baseline - [:line {:x1 (mth/round x) - :y1 (mth/round (- (:y data) (:height data))) - :x2 (mth/round (+ x width)) - :y2 (mth/round (- (:y data) (:height data))) - :style {:stroke "blue" - :stroke-width (/ 1 zoom)}}] - - [:line {:x1 (:x data) - :y1 (- (:y data) (:descent res)) - :x2 (+ (:x data) (:width data)) - :y2 (- (:y data) (:descent res)) - :style {:stroke "green" - :stroke-width (/ 2 zoom)}}]]))])) - ;; --- Text Wrapper for workspace (mf/defc text-wrapper {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") + [{:keys [shape]}] + (let [shape-id (dm/get-prop shape :id) text-modifier-ref - (mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape))) + (mf/with-memo [shape-id] + (refs/workspace-text-modifier-by-id shape-id)) text-modifier (mf/deref text-modifier-ref) - shape (cond-> shape - (some? text-modifier) - (dwt/apply-text-modifier text-modifier))] + shape (if (some? shape) + (dwt/apply-text-modifier shape text-modifier) + shape)] [:> shape-container {:shape shape} - [:g.text-shape {:key (dm/str "text-" (:id shape))} + [:g.text-shape {:key (dm/str shape-id)} [:& text/text-shape {:shape shape}]] - (when (and (debug? :text-outline) (d/not-empty? (:position-data shape))) - [:& debug-text-bounds {:shape shape}])])) + (when *assert* + [:& wsd/shape-debug {:shape shape}])])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index 52afa3514b..7eace14e7c 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -7,10 +7,12 @@ (ns app.main.ui.workspace.shapes.text.editor (:require ["draft-js" :as draft] + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.text :as gsht] + [app.common.geom.shapes.text :as gst] + [app.common.math :as mth] [app.common.text :as txt] [app.config :as cf] [app.main.data.workspace :as dw] @@ -24,9 +26,7 @@ [app.util.object :as obj] [app.util.text-editor :as ted] [goog.events :as events] - [rumext.v2 :as mf]) - (:import - goog.events.EventType)) + [rumext.v2 :as mf])) ;; --- Text Editor Rendering @@ -49,7 +49,7 @@ (let [children (obj/get props "children")] [:span {:style {:background "#ccc" :display "inline-block"}} children])) -(defn render-block +(defn- render-block [block shape] (let [type (ted/get-editor-block-type block)] (case type @@ -60,7 +60,7 @@ :shape shape}} nil))) -(defn styles-fn [shape styles content] +(defn- styles-fn [shape styles content] (let [data (if (= (.getText content) "") (-> (.getData content) (.toJS) @@ -74,19 +74,27 @@ (def empty-editor-state (ted/create-editor-state nil default-decorator)) -(defn get-blocks-to-setup [block-changes] +(defn- get-blocks-to-setup [block-changes] (->> block-changes (filter (fn [[_ v]] (nil? (:old v)))) (mapv first))) -(defn get-blocks-to-add-styles +(defn- get-blocks-to-add-styles [block-changes] (->> block-changes (filter (fn [[_ v]] (and (not= (:old v) (:new v)) (= (:old v) "")))) (mapv first))) +(defn- shape->justify + [{:keys [content]}] + (case (d/nilv (:vertical-align content) "top") + "center" "center" + "top" "flex-start" + "bottom" "flex-end" + nil)) + (mf/defc text-shape-edit-html {::mf/wrap [mf/memo] ::mf/wrap-props false @@ -114,12 +122,11 @@ (fn [event] (dom/stop-propagation event) (when (kbd/esc? event) - (st/emit! :interrupt) - (st/emit! dw/clear-edition-mode))) + (st/emit! :interrupt (dw/clear-edition-mode)))) on-mount (fn [] - (let [keys [(events/listen js/document EventType.KEYUP on-key-up)]] + (let [keys [(events/listen js/document "keyup" on-key-up)]] (st/emit! (dwt/initialize-editor-state shape default-decorator) (dwt/select-all shape)) #(do @@ -248,7 +255,8 @@ :custom-style-fn (partial styles-fn shape) :block-renderer-fn #(render-block % shape) :ref on-editor - :editor-state state}]])) + :editor-state state + :style #js {:border "1px solid red"}}]])) (defn translate-point-from-viewport "Translate a point in the viewport into client coordinates" @@ -262,24 +270,27 @@ (mf/defc text-editor-svg {::mf/wrap-props false} - [props] - (let [shape (obj/get props "shape") - modifiers (obj/get props "modifiers") - modifiers (get-in modifiers [(:id shape) :modifiers]) + [{:keys [shape modifiers]}] + (let [shape-id (dm/get-prop shape :id) + modifiers (dm/get-in modifiers [shape-id :modifiers]) - clip-id - (dm/str "text-edition-clip" (:id shape)) + clip-id (dm/str "text-edition-clip" shape-id) text-modifier-ref - (mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape))) + (mf/with-memo [shape-id] + (refs/workspace-text-modifier-by-id shape-id)) text-modifier (mf/deref text-modifier-ref) - ;; For Safari It's necesary to scale the editor with the zoom level to fix - ;; a problem with foreignObjects not scaling correctly with the viewbox + ;; For Safari It's necesary to scale the editor with the zoom + ;; level to fix a problem with foreignObjects not scaling + ;; correctly with the viewbox + ;; + ;; NOTE: this teoretically breaks hooks rules, but in practice + ;; it is imposible to really break it maybe-zoom - (when (cf/check-browser? :safari) + (when (cf/check-browser? :safari-16) (mf/deref refs/selected-zoom)) shape (cond-> shape @@ -289,28 +300,48 @@ (some? modifiers) (gsh/transform-shape modifiers)) - bounding-box (gsht/position-data-selrect shape) + bounds (gst/shape->rect shape) - x (min (:x bounding-box) (:x shape)) - y (min (:y bounding-box) (:y shape)) - width (max (:width bounding-box) (:width shape)) - height (max (:height bounding-box) (:height shape))] + x (mth/min (dm/get-prop bounds :x) + (dm/get-prop shape :x)) + y (mth/min (dm/get-prop bounds :y) + (dm/get-prop shape :y)) + width (mth/max (dm/get-prop bounds :width) + (dm/get-prop shape :width)) + height (mth/max (dm/get-prop bounds :height) + (dm/get-prop shape :height)) + + style + (cond-> #js {:pointerEvents "all"} + + (not (cf/check-browser? :safari)) + (obj/merge! + #js {:transform (dm/fmt "translate(%px, %px)" (- (dm/get-prop shape :x) x) (- (dm/get-prop shape :y) y))}) + + (cf/check-browser? :safari-17) + (obj/merge! + #js {:height "100%" + :display "flex" + :flexDirection "column" + :justifyContent (shape->justify shape)}) + + (cf/check-browser? :safari-16) + (obj/merge! + #js {:position "fixed" + :left 0 + :top (- (dm/get-prop shape :y) y) + :transform-origin "top left" + :transform (when (some? maybe-zoom) + (dm/fmt "scale(%)" maybe-zoom))}))] [:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id) :transform (dm/str (gsh/transform-matrix shape))} [:defs [:clipPath {:id clip-id} - [:rect {:x x - :y y - :width width - :height height - :fill "red"}]]] + [:rect {:x x :y y :width width :height height}]]] [:foreignObject {:x x :y y :width width :height height} - [:div {:style {:position "fixed" - :left 0 - :top (- (:y shape) y) - :pointer-events "all" - :transform-origin "top left" - :transform (when maybe-zoom (dm/fmt "scale(%)" maybe-zoom))}} - [:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]]])) + [:div {:style style} + [:& text-shape-edit-html + {:shape shape + :key (dm/str shape-id)}]]]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs b/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs index d7766d5174..8a0082f1ee 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs @@ -37,6 +37,6 @@ :width width :height height :transform transform - :style {:stroke "var(--color-select)" + :style {:stroke "var(--color-accent-tertiary)" :stroke-width (/ 1 zoom) :fill "none"}}])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs index 65804663b1..e9a4137140 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs @@ -8,11 +8,11 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.text :as gsht] [app.common.math :as mth] - [app.common.pages.helpers :as cph] [app.common.text :as txt] [app.common.types.modifiers :as ctm] [app.main.data.workspace.modifiers :as mdwm] @@ -26,20 +26,24 @@ [app.util.object :as obj] [app.util.text-editor :as ted] [app.util.text-svg-position :as tsp] - [app.util.timers :as ts] [promesa.core :as p] [rumext.v2 :as mf])) (defn fix-position [shape] - (let [modifiers (:modifiers shape) - shape' (gsh/transform-shape shape modifiers) - ;; We need to remove the movement because the dynamic modifiers will have move it - deltav (gpt/to-vec (gpt/point (:selrect shape')) - (gpt/point (:selrect shape)))] - (-> shape - (gsh/transform-shape (ctm/move modifiers deltav)) - (mdwm/update-grow-type shape) - (dissoc :modifiers)))) + (if-let [modifiers (:modifiers shape)] + (let [shape' (gsh/transform-shape shape modifiers) + + old-sr (dm/get-prop shape :selrect) + new-sr (dm/get-prop shape' :selrect) + + ;; We need to remove the movement because the dynamic modifiers will have move it + deltav (gpt/to-vec (gpt/point new-sr) + (gpt/point old-sr))] + (-> shape + (gsh/transform-shape (ctm/move modifiers deltav)) + (mdwm/update-grow-type shape) + (dissoc :modifiers))) + shape)) (defn- update-with-editor-state "Updates the shape with the current state in the editor" @@ -96,13 +100,7 @@ (assoc :height height)) props)) props)))) - (p/fmap (fn [props] - ;; We need to wait for the text modifier to be updated before - ;; we can update the position data. Otherwise the position data - ;; will be wrong. - ;; TODO: This is a hack. We need to find a better way to do this. - (st/emit! (dwt/update-text-modifier id props)) - (ts/schedule 30 #(update-text-shape shape node)))))) + (p/fmap #(st/emit! (dwt/update-text-modifier id %))))) (mf/defc text-container {::mf/wrap-props false @@ -126,33 +124,32 @@ (defn text-properties-equal? [shape other] (or (identical? shape other) - (and - ;; Check if both shapes are equivalent removing their geometry data - (= (dissoc shape :migrate :points :selrect :height :width :x :y :position-data :modifiers) - (dissoc other :migrate :points :selrect :height :width :x :y :position-data :modifiers)) - - ;; Check if the position and size is close. If any of these changes the shape has changed - ;; and if not there is no geometry relevant change - (mth/close? (:x shape) (:x other)) - (mth/close? (:y shape) (:y other)) - (mth/close? (:width shape) (:width other)) - (mth/close? (:height shape) (:height other))))) + (and (= (:grow-type shape) (:grow-type other)) + (= (:content shape) (:content other)) + ;; Check if the position and size is close. If any of these changes the shape has changed + ;; and if not there is no geometry relevant change + (mth/close? (dm/get-prop shape :x) (dm/get-prop other :x)) + (mth/close? (dm/get-prop shape :y) (dm/get-prop other :y)) + (mth/close? (dm/get-prop shape :width) (dm/get-prop other :width)) + (mth/close? (dm/get-prop shape :height) (dm/get-prop other :height))))) (mf/defc text-changes-renderer {::mf/wrap-props false} [props] - (let [text-shapes (obj/get props "text-shapes") + (let [text-shapes (unchecked-get props "text-shapes") + prev-text-shapes (hooks/use-previous text-shapes) ;; We store in the state the texts still pending to be calculated so we can ;; get its position - pending-update (mf/use-state {}) + pending-update* (mf/use-state {}) + pending-update (deref pending-update*) text-change? (fn [id] (let [new-shape (get text-shapes id) old-shape (get prev-text-shapes id) - remote? (some? (-> new-shape meta :session-id))] + remote? (some? (-> new-shape meta :session-id))] (or (and (not remote?) ;; changes caused by a remote peer are not re-calculated (not (text-properties-equal? old-shape new-shape))) @@ -160,9 +157,8 @@ (nil? (:position-data new-shape))))) changed-texts - (mf/use-memo - (mf/deps text-shapes @pending-update) - #(let [pending-shapes (into #{} (vals @pending-update))] + (mf/with-memo [text-shapes pending-update] + (let [pending-shapes (into #{} (vals pending-update))] (->> (keys text-shapes) (filter (fn [id] (or (contains? pending-shapes id) @@ -170,18 +166,18 @@ (map (d/getf text-shapes))))) handle-update-shape - (mf/use-callback + (mf/use-fn (fn [shape node] ;; Unique to indentify the pending state (let [uid (js/Symbol)] - (swap! pending-update assoc uid (:id shape)) + (swap! pending-update* assoc uid (:id shape)) (p/then (update-text-shape shape node) - #(swap! pending-update dissoc uid)))))] + #(swap! pending-update* dissoc uid)))))] [:.text-changes-renderer (for [{:keys [id] :as shape} changed-texts] - [:& text-container {:key (str (dm/str "text-container-" id)) + [:& text-container {:key (dm/str "text-container-" id) :shape shape :on-update handle-update-shape}])])) @@ -213,7 +209,7 @@ [:.text-changes-renderer (for [{:keys [id] :as shape} changed-texts] - [:& text-container {:key (str (dm/str "text-container-" id)) + [:& text-container {:key (dm/str "text-container-" id) :shape shape :on-update handle-update-shape}])])) @@ -279,7 +275,7 @@ (mf/use-memo (mf/deps objects) (fn [] - (into {} (filter (comp cph/text-shape? second)) objects))) + (into {} (filter (comp cfh/text-shape? second)) objects))) text-shapes (hooks/use-equal-memo text-shapes) @@ -305,14 +301,14 @@ (into {} (keep (fn [[id modifiers]] (when-let [shape (get text-shapes id)] - (vector id (merge shape modifiers))))) + (vector id (d/patch-object shape modifiers))))) modifiers)))] ;; We only need the effect to run on "mount" because the next fonts will be changed when the texts are ;; edited (mf/use-effect (fn [] - (let [text-nodes (->> text-shapes (vals)(mapcat #(txt/node-seq txt/is-text-node? (:content %)))) + (let [text-nodes (->> text-shapes (vals) (mapcat #(txt/node-seq txt/is-text-node? (:content %)))) fonts (into #{} (keep :font-id) text-nodes)] (run! fonts/ensure-loaded! fonts)))) diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index 2f6de0e75b..2e2ab4a399 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -5,25 +5,27 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.tab-container :refer [tab-container tab-element]] - [app.main.ui.context :as ctx] + [app.main.ui.context :as muc] [app.main.ui.hooks.resize :refer [use-resize-hook]] - [app.main.ui.icons :as i] [app.main.ui.workspace.comments :refer [comments-sidebar]] + [app.main.ui.workspace.left-header :refer [left-header]] + [app.main.ui.workspace.right-header :refer [right-header]] [app.main.ui.workspace.sidebar.assets :refer [assets-toolbox]] [app.main.ui.workspace.sidebar.debug :refer [debug-panel]] + [app.main.ui.workspace.sidebar.debug-shape-info :refer [debug-shape-info]] [app.main.ui.workspace.sidebar.history :refer [history-toolbox]] [app.main.ui.workspace.sidebar.layers :refer [layers-toolbox]] [app.main.ui.workspace.sidebar.options :refer [options-toolbox]] [app.main.ui.workspace.sidebar.shortcuts :refer [shortcuts-container]] [app.main.ui.workspace.sidebar.sitemap :refer [sitemap]] - [app.util.dom :as dom] + [app.util.debug :as dbg] [app.util.i18n :refer [tr]] [rumext.v2 :as mf])) @@ -32,122 +34,157 @@ (mf/defc left-sidebar {::mf/wrap [mf/memo] ::mf/wrap-props false} - [{:keys [layout] :as props}] + [{:keys [layout file page-id] :as props}] (let [options-mode (mf/deref refs/options-mode-global) mode-inspect? (= options-mode :inspect) + project (mf/deref refs/workspace-project) + show-pages? (mf/use-state true) + toggle-pages (mf/use-callback #(reset! show-pages? not)) section (cond (or mode-inspect? (contains? layout :layers)) :layers (contains? layout :assets) :assets) + shortcuts? (contains? layout :shortcuts) show-debug? (contains? layout :debug-panel) - new-css? (mf/use-ctx ctx/new-css-system) - {:keys [on-pointer-down on-lost-pointer-capture on-pointer-move parent-ref size]} - (use-resize-hook :left-sidebar 255 255 500 :x false :left) + {on-pointer-down :on-pointer-down on-lost-pointer-capture :on-lost-pointer-capture on-pointer-move :on-pointer-move parent-ref :parent-ref size :size} + (use-resize-hook :left-sidebar 275 275 500 :x false :left) + + {on-pointer-down-pages :on-pointer-down on-lost-pointer-capture-pages :on-lost-pointer-capture on-pointer-move-pages :on-pointer-move size-pages :size} + (use-resize-hook :sitemap 200 38 400 :y false nil) + handle-collapse (mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) on-tab-change - (mf/use-fn #(st/emit! (dw/go-to-layout %))) - ] + (mf/use-fn #(st/emit! (dw/go-to-layout %)))] - [:aside {:ref parent-ref - :class (if ^boolean new-css? - (dom/classnames (css :left-settings-bar) true) - (dom/classnames :settings-bar true - :settings-bar-left true - :two-row (<= size 300) - :three-row (and (> size 300) (<= size 400)) - :four-row (> size 400))) - :style #js {"--width" (dm/str size "px")}} + [:& (mf/provider muc/sidebar) {:value :left} + [:aside {:ref parent-ref + :id "left-sidebar-aside" + :data-size (str size) + :class (stl/css-case :left-settings-bar true + :global/two-row (<= size 300) + :global/three-row (and (> size 300) (<= size 400)) + :global/four-row (> size 400)) + :style #js {"--width" (dm/str size "px")}} - [:div {:on-pointer-down on-pointer-down - :on-lost-pointer-capture on-lost-pointer-capture - :on-pointer-move on-pointer-move - :class (if ^boolean new-css? - (dom/classnames (css :resize-area) true) - (dom/classnames :resize-area true))}] - [:div {:class (if ^boolean new-css? - (dom/classnames (css :settings-bar-inside) true) - (dom/classnames :settings-bar-inside true))} + [:& left-header {:file file :layout layout :project project :page-id page-id + :class (stl/css :left-header)}] + + [:div {:on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move + :class (stl/css :resize-area)}] (cond (true? shortcuts?) - [:& shortcuts-container] + [:& shortcuts-container {:class (stl/css :settings-bar-content)}] (true? show-debug?) - [:& debug-panel] + [:& debug-panel {:class (stl/css :settings-bar-content)}] :else - (if ^boolean new-css? - [:& tab-container - {:on-change-tab on-tab-change - :selected section - :shortcuts? shortcuts? - :collapsable? true - :handle-collapse handle-collapse} + [:div {:class (stl/css :settings-bar-content)} + [:& tab-container + {:on-change-tab on-tab-change + :selected section + :collapsable true + :handle-collapse handle-collapse + :header-class (stl/css :tab-spacing)} - [:& tab-element {:id :layers :title (tr "workspace.sidebar.layers")} - [:div {:class (dom/classnames (css :layers-tab) true)} - [:& sitemap {:layout layout}] - [:& layers-toolbox {:size-parent size}]]] + [:& tab-element {:id :layers + :title (tr "workspace.sidebar.layers")} + [:article {:class (stl/css :layers-tab) + :style #js {"--height" (str size-pages "px")}} - (when-not ^boolean mode-inspect? - [:& tab-element {:id :assets :title (tr "workspace.toolbar.assets")} - [:& assets-toolbox]])] + [:& sitemap {:layout layout + :toggle-pages toggle-pages + :show-pages? @show-pages? + :size size-pages}] - [:* - [:button.collapse-sidebar - {:on-click handle-collapse - :aria-label (tr "workspace.sidebar.collapse")} - i/arrow-slide] + (when @show-pages? + [:div {:class (stl/css :resize-area-horiz) + :on-pointer-down on-pointer-down-pages + :on-lost-pointer-capture on-lost-pointer-capture-pages + :on-pointer-move on-pointer-move-pages}]) - [:& tab-container - {:on-change-tab on-tab-change - :selected section - :shortcuts? shortcuts? - :collapsable? true - :handle-collapse handle-collapse} + [:& layers-toolbox {:size-parent size + :size size-pages}]]] - [:& tab-element {:id :layers :title (tr "workspace.sidebar.layers")} - [:div {:class (dom/classnames :layers-tab true)} - [:& sitemap {:layout layout}] - [:& layers-toolbox {:size-parent size}]]] - - (when-not ^boolean mode-inspect? - [:& tab-element {:id :assets :title (tr "workspace.toolbar.assets")} - [:& assets-toolbox]])]]))]])) + (when-not ^boolean mode-inspect? + [:& tab-element {:id :assets + :title (tr "workspace.toolbar.assets")} + [:& assets-toolbox {:size (- size 58)}]])]])]])) ;; --- Right Sidebar (Component) (mf/defc right-sidebar {::mf/wrap-props false ::mf/wrap [mf/memo]} - [{:keys [layout section] :as props}] + [{:keys [layout section file page-id] :as props}] (let [drawing-tool (:tool (mf/deref refs/workspace-drawing)) is-comments? (= drawing-tool :comments) is-history? (contains? layout :document-history) is-inspect? (= section :inspect) - expanded? (mf/deref refs/inspect-expanded) - can-be-expanded? (and (not is-comments?) - (not is-history?) - is-inspect?)] + ;;expanded? (mf/deref refs/inspect-expanded) + ;;prev-expanded? (hooks/use-previous expanded?) - (mf/with-effect [can-be-expanded?] - (when (not can-be-expanded?) - (st/emit! (dw/set-inspect-expanded false)))) + current-section* (mf/use-state :info) + current-section (deref current-section*) - [:aside.settings-bar.settings-bar-right {:class (when (and can-be-expanded? expanded?) "expanded")} - [:div.settings-bar-inside - (cond - (true? is-comments?) - [:& comments-sidebar] + can-be-expanded? (or (dbg/enabled? :shape-panel) + (and (not is-comments?) + (not is-history?) + is-inspect? + (= current-section :code))) - (true? is-history?) - [:& history-toolbox] + {:keys [on-pointer-down on-lost-pointer-capture on-pointer-move set-size size]} + (use-resize-hook :code 276 276 768 :x true :right) - :else - [:> options-toolbox props])]])) + handle-change-section + (mf/use-callback + (fn [section] + (reset! current-section* section))) + handle-expand + (mf/use-callback + (mf/deps size) + (fn [] + (set-size (if (> size 276) 276 768)))) + + props + (mf/spread props + :on-change-section handle-change-section + :on-expand handle-expand)] + + [:& (mf/provider muc/sidebar) {:value :right} + [:aside {:class (stl/css-case :right-settings-bar true + :not-expand (not can-be-expanded?) + :expanded (> size 276)) + + :id "right-sidebar-aside" + :data-size (str size) + :style #js {"--width" (when can-be-expanded? (dm/str size "px"))}} + (when can-be-expanded? + [:div {:class (stl/css :resize-area) + :on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move}]) + [:& right-header {:file file :layout layout :page-id page-id}] + + [:div {:class (stl/css :settings-bar-inside)} + (cond + (dbg/enabled? :shape-panel) + [:& debug-shape-info] + + (true? is-comments?) + [:& comments-sidebar] + + (true? is-history?) + [:> history-toolbox {}] + + :else + [:> options-toolbox props])]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar.css.json b/frontend/src/app/main/ui/workspace/sidebar.css.json deleted file mode 100644 index c27ff9be21..0000000000 --- a/frontend/src/app/main/ui/workspace/sidebar.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"workspace_sidebar_button-primary_K7xW6","button-secondary":"workspace_sidebar_button-secondary_e2eQE","button-icon":"workspace_sidebar_button-icon_OXdmL","button-icon-small":"workspace_sidebar_button-icon-small_EYb9x","left-settings-bar":"workspace_sidebar_left-settings-bar_7co5t","resize-area":"workspace_sidebar_resize-area_ny1v0","settings-bar-inside":"workspace_sidebar_settings-bar-inside_YnFv8","layers-tab":"workspace_sidebar_layers-tab_soxRL"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/sidebar.scss b/frontend/src/app/main/ui/workspace/sidebar.scss index 6f2d76f8da..192a8416e6 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/sidebar.scss @@ -6,39 +6,88 @@ @import "refactor/common-refactor.scss"; -$width-settings-bar: 256px; -$width-settings-bar-min: 255px; -$width-settings-bar-max: 500px; +$width-settings-bar: $s-276; +$width-settings-bar-max: $s-500; .left-settings-bar { + display: grid; + grid-template-areas: + "header header" + "content resize"; + grid-template-rows: $s-52 1fr; + grid-template-columns: 1fr 0; position: relative; grid-area: left-sidebar; min-width: $width-settings-bar; - max-width: 500px; + max-width: $width-settings-bar-max; width: var(--width, $width-settings-bar); - height: 100%; - border-radius: $br-8; - background-color: var(--color-background-primary); + background-color: var(--panel-background-color); + height: 100vh; + max-height: 100vh; .resize-area { - position: absolute; - right: -8px; - z-index: $z-index-10; - width: $s-8; - height: 100%; - cursor: ew-resize; + grid-area: resize; } +} + +.layers-tab { + padding-top: $s-4; +} + +.left-header { + grid-area: header; +} + +.settings-bar-content { + grid-area: content; + right: calc(-1 * $s-8); +} + +.resize-area { + position: absolute; + top: 0; + left: unset; + z-index: $z-index-4; + width: $s-8; + cursor: ew-resize; + height: 100%; +} + +.tab-spacing { + margin-inline: $s-12; +} + +.right-settings-bar { + grid-area: right-sidebar; + width: $width-settings-bar; + background-color: var(--panel-background-color); + height: 100%; + display: flex; + flex-direction: column; + z-index: 0; + &.not-expand { + max-width: $width-settings-bar; + } + &.expanded { + width: var(--width, $width-settings-bar); + } + .settings-bar-inside { display: grid; grid-template-columns: 100%; grid-template-rows: 100%; - height: calc(100% - 2px); - .layers-tab { - display: grid; - grid-template-rows: auto 1fr; - grid-template-columns: 100%; - height: 100%; - overflow: hidden; - } + + height: calc(100vh - $s-52); + overflow: hidden; } } + +.resize-area-horiz { + position: absolute; + // top: calc($s-88 + var(--height, 200px)); + left: 0; + width: 100%; + // height: $s-8; + border-bottom: $s-2 solid var(--resize-area-border-color); + cursor: ns-resize; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 5a5f381e60..b3f755fc5e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -5,2378 +5,23 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.assets + (:require-macros [app.main.style :as stl]) (:require - [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.media :as cm] - [app.common.pages.helpers :as cph] - [app.common.spec :as us] - [app.common.types.file :as ctf] - [app.config :as cf] - [app.main.data.events :as ev] [app.main.data.modal :as modal] - [app.main.data.workspace :as dw] [app.main.data.workspace.assets :as dwa] - [app.main.data.workspace.colors :as dc] - [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.media :as dwm] - [app.main.data.workspace.texts :as dwt] - [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] - [app.main.render :refer [component-svg]] - [app.main.store :as st] - [app.main.ui.components.color-bullet :as bc] - [app.main.ui.components.context-menu :refer [context-menu]] - [app.main.ui.components.editable-label :refer [editable-label]] - [app.main.ui.components.file-uploader :refer [file-uploader]] - [app.main.ui.components.forms :as fm] + [app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]] + [app.main.ui.components.search-bar :refer [search-bar]] [app.main.ui.context :as ctx] - [app.main.ui.hooks :as h] [app.main.ui.icons :as i] - [app.main.ui.workspace.libraries :refer [create-file-library-ref]] - [app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry]] - [app.util.color :as uc] + [app.main.ui.workspace.sidebar.assets.common :as cmm] + [app.main.ui.workspace.sidebar.assets.file-library :refer [file-library]] [app.util.dom :as dom] - [app.util.dom.dnd :as dnd] [app.util.i18n :as i18n :refer [tr]] - [app.util.keyboard :as kbd] - [app.util.router :as rt] - [app.util.strings :refer [matches-search]] - [app.util.timers :as ts] - [cljs.spec.alpha :as s] [cuerdas.core :as str] - [okulary.core :as l] - [potok.core :as ptk] [rumext.v2 :as mf])) -(def ctx:filters (mf/create-context nil)) -(def ctx:toggle-ordering (mf/create-context nil)) -(def ctx:toggle-list-style (mf/create-context nil)) - -(def lens:selected - (-> (l/in [:workspace-assets :selected]) - (l/derived st/state))) - -(def lens:open-status - (l/derived (l/in [:workspace-assets :open-status]) st/state)) - -(def lens:typography-section-state - (l/derived (fn [gstate] - {:rename-typography (:rename-typography gstate) - :edit-typography (:edit-typography gstate)}) - refs/workspace-global - =)) - -;; ---- Group assets management ---- - -(defn group-assets - "Convert a list of assets in a nested structure like this: - - {'': [{assetA} {assetB}] - 'group1': {'': [{asset1A} {asset1B}] - 'subgroup11': {'': [{asset11A} {asset11B} {asset11C}]} - 'subgroup12': {'': [{asset12A}]}} - 'group2': {'subgroup21': {'': [{asset21A}}}} - " - [assets reverse-sort?] - (when-not (empty? assets) - (reduce (fn [groups {:keys [path] :as asset}] - (let [path (cph/split-path (or path ""))] - (update-in groups - (conj path "") - (fn [group] - (if group - (conj group asset) - [asset]))))) - (sorted-map-by (fn [key1 key2] - (if reverse-sort? - (compare key2 key1) - (compare key1 key2)))) - assets))) - -(defn add-group - [asset group-name] - (-> (:path asset) - (cph/merge-path-item group-name) - (cph/merge-path-item (:name asset)))) - -(defn rename-group - [asset path last-path] - (-> (:path asset) - (str/slice 0 (count path)) - (cph/split-path) - butlast - (vec) - (conj last-path) - (cph/join-path) - (str (str/slice (:path asset) (count path))) - (cph/merge-path-item (:name asset)))) - -(defn ungroup - [asset path] - (-> (:path asset) - (str/slice 0 (count path)) - (cph/split-path) - butlast - (cph/join-path) - (str (str/slice (:path asset) (count path))) - (cph/merge-path-item (:name asset)))) - -(s/def ::asset-name ::us/not-empty-string) -(s/def ::name-group-form - (s/keys :req-un [::asset-name])) - -(mf/defc name-group-dialog - {::mf/register modal/components - ::mf/register-as :name-group-dialog} - [{:keys [path last-path accept] :as ctx - :or {path "" last-path ""}}] - (let [initial (mf/use-memo - (mf/deps last-path) - (constantly {:asset-name last-path})) - form (fm/use-form :spec ::name-group-form - :validators [(fm/validate-not-empty :name (tr "auth.name.not-all-space")) - (fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))] - :initial initial) - - create? (empty? path) - - on-close (mf/use-fn #(modal/hide!)) - - on-accept - (mf/use-fn - (mf/deps form) - (fn [_] - (let [asset-name (get-in @form [:clean-data :asset-name])] - (if create? - (accept asset-name) - (accept path asset-name)) - (modal/hide!))))] - - [:div.modal-overlay - [:div.modal-container.confirm-dialog - [:div.modal-header - [:div.modal-header-title - [:h2 (if create? - (tr "workspace.assets.create-group") - (tr "workspace.assets.rename-group"))]] - [:div.modal-close-button - {:on-click on-close} i/close]] - - [:div.modal-content.generic-form - [:& fm/form {:form form :on-submit on-accept} - [:& fm/input {:name :asset-name - :auto-focus? true - :label (tr "workspace.assets.group-name") - :hint (tr "workspace.assets.create-group-hint")}]]] - - [:div.modal-footer - [:div.action-buttons - [:input.cancel-button - {:type "button" - :value (tr "labels.cancel") - :on-click on-close}] - - [:input.accept-button.primary - {:type "button" - :class (when-not (:valid @form) "btn-disabled") - :disabled (not (:valid @form)) - :value (if create? (tr "labels.create") (tr "labels.rename")) - :on-click on-accept}]]]]])) - - -;; ---- Group assets by drag and drop ---- - -(defn- create-assets-group - [rename components-to-group group-name] - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> components-to-group - (map #(rename - (:id %) - (add-group % group-name))))) - (st/emit! (dwu/commit-undo-transaction undo-id)))) - -(defn- on-drop-asset - [event asset dragging* selected selected-full selected-paths rename] - (let [create-typed-assets-group (partial create-assets-group rename)] - (when (not (dnd/from-child? event)) - (reset! dragging* false) - (when - (and (not (contains? selected (:id asset))) - (every? #(= % (:path asset)) selected-paths)) - (let [components-to-group (conj selected-full asset) - create-typed-assets-group (partial create-typed-assets-group components-to-group)] - (modal/show! :name-group-dialog {:accept create-typed-assets-group})))))) - -(defn- on-drag-enter-asset - [event asset dragging* selected selected-paths] - (when (and - (not (dnd/from-child? event)) - (every? #(= % (:path asset)) selected-paths) - (not (contains? selected (:id asset)))) - (reset! dragging* true))) - -(defn- on-drag-leave-asset - [event dragging*] - (when (not (dnd/from-child? event)) - (reset! dragging* false))) - -(defn- create-counter-element - [asset-count] - (let [counter-el (dom/create-element "div")] - (dom/set-property! counter-el "class" "drag-counter") - (dom/set-text! counter-el (str asset-count)) - counter-el)) - -(defn- set-drag-image - [event item-ref num-selected] - (let [offset (dom/get-offset-position (.-nativeEvent event)) - item-el (mf/ref-val item-ref) - counter-el (create-counter-element num-selected)] - - ;; set-drag-image requires that the element is rendered and - ;; visible to the user at the moment of creating the ghost - ;; image (to make a snapshot), but you may remove it right - ;; afterwards, in the next render cycle. - (dom/append-child! item-el counter-el) - (dnd/set-drag-image! event item-el (:x offset) (:y offset)) - (ts/raf #(.removeChild ^js item-el counter-el)))) - -(defn- on-asset-drag-start - [event file-id asset selected item-ref asset-type on-drag-start] - (let [id-asset (:id asset) - num-selected (if (contains? selected id-asset) - (count selected) - 1)] - (when (not (contains? selected id-asset)) - (st/emit! (dw/unselect-all-assets file-id) - (dw/toggle-selected-assets file-id id-asset asset-type))) - (on-drag-start asset event) - (when (> num-selected 1) - (set-drag-image event item-ref num-selected)))) - -(defn- on-drag-enter-asset-group - [event dragging* prefix selected-paths] - (dom/stop-propagation event) - (when (and (not (dnd/from-child? event)) - (not (every? #(= % prefix) selected-paths))) - (reset! dragging* true))) - -(defn- on-drop-asset-group - [event dragging* prefix selected-paths selected-full rename] - (dom/stop-propagation event) - (when (not (dnd/from-child? event)) - (reset! dragging* false) - (when (not (every? #(= % prefix) selected-paths)) - (doseq [target-asset selected-full] - (st/emit! - (rename - (:id target-asset) - (cph/merge-path-item prefix (:name target-asset)))))))) - -;; ---- Common blocks ---- - -(def ^:private initial-context-menu-state - {:open? false :top nil :left nil}) - -(defn- open-context-menu - [state pos] - (let [top (:y pos) - left (+ (:x pos) 10)] - (assoc state - :open? true - :top top - :left left))) - -(defn- close-context-menu - [state] - (assoc state :open? false)) - -(mf/defc assets-context-menu - {::mf/wrap-props false} - [{:keys [options state on-close]}] - [:& context-menu - {:selectable false - :show (:open? state) - :on-close on-close - :top (:top state) - :left (:left state) - :options options}]) - -(mf/defc asset-section - {::mf/wrap-props false} - [{:keys [children file-id title section assets-count open?]}] - (let [children (->> (if (array? children) children [children]) - (filter some?)) - get-role #(.. % -props -role) - title-buttons (filter #(= (get-role %) :title-button) children) - content (filter #(= (get-role %) :content) children)] - [:div.asset-section - [:div.asset-title {:class (when (not ^boolean open?) "closed")} - [:span {:on-click #(st/emit! (dw/set-assets-section-open file-id section (not open?)))} - i/arrow-slide title] - [:span.num-assets (str "\u00A0(") assets-count ")"] ;; Unicode 00A0 is non-breaking space - title-buttons] - (when ^boolean open? - content)])) - -(mf/defc asset-section-block - [{:keys [children]}] - [:* children]) - -(mf/defc asset-group-title - [{:keys [file-id section path group-open? on-rename on-ungroup]}] - (when-not (empty? path) - (let [[other-path last-path truncated] (cph/compact-path path 35) - menu-state (mf/use-state initial-context-menu-state) - - on-fold-group - (mf/use-fn - (mf/deps file-id section path group-open?) - (fn [event] - (dom/stop-propagation event) - (st/emit! (dw/set-assets-group-open file-id - section - path - (not group-open?))))) - on-context-menu - (mf/use-fn - (fn [event] - (dom/prevent-default event) - (let [pos (dom/get-client-position event)] - (swap! menu-state open-context-menu pos)))) - - on-close-menu - (mf/use-fn #(swap! menu-state close-context-menu))] - - [:div.group-title {:class (when-not group-open? "closed") - :on-click on-fold-group - :on-context-menu on-context-menu} - [:span i/arrow-slide] - (when-not (empty? other-path) - [:span.dim {:title (when truncated path)} - other-path "\u00A0/\u00A0"]) - [:span {:title (when truncated path)} - last-path] - [:& assets-context-menu - {:on-close on-close-menu - :state @menu-state - :options [[(tr "workspace.assets.rename") #(on-rename % path last-path)] - [(tr "workspace.assets.ungroup") #(on-ungroup path)]]}]]))) - - -;;---- Components section ---- - - -(defn- get-component-root-and-container - [file-id component components-v2] - (if (= file-id (:id @refs/workspace-file)) - (let [data @refs/workspace-data] - [(ctf/get-component-root data component) - (if components-v2 - (ctf/get-component-page data component) - component)]) - (let [data (dm/get-in @refs/workspace-libraries [file-id :data])] - [(ctf/get-component-root data component) - (if components-v2 - (ctf/get-component-page data component) - component)]))) - -(mf/defc components-item - {::mf/wrap-props false} - [{:keys [component renaming listing-thumbs? selected - file-id on-asset-click on-context-menu on-drag-start do-rename - cancel-rename selected-full selected-paths]}] - (let [item-ref (mf/use-ref) - - dragging* (mf/use-state false) - dragging? (deref dragging*) - - read-only? (mf/use-ctx ctx/workspace-read-only?) - components-v2 (mf/use-ctx ctx/components-v2) - component-id (:id component) - - ;; NOTE: we don't use reactive deref for it because we don't - ;; really need rerender on any change on the file change. If - ;; the component changes, it will trigger rerender anyway. - [root-shape container] - (get-component-root-and-container file-id component components-v2) - - unselect-all - (mf/use-fn - (fn [] - (st/emit! (dw/unselect-all-assets)))) - - on-component-click - (mf/use-fn - (mf/deps component selected) - (fn [event] - (dom/stop-propagation event) - (on-asset-click component-id unselect-all event))) - - on-component-double-click - (mf/use-fn - (mf/deps file-id component-id) - (fn [event] - (dom/stop-propagation event) - (st/emit! (dw/go-to-main-instance file-id component-id)))) - - on-drop - (mf/use-fn - (mf/deps component dragging* selected selected-full selected-paths) - (fn [event] - (on-drop-asset event component dragging* selected selected-full - selected-paths dwl/rename-component))) - - on-drag-enter - (mf/use-fn - (mf/deps component dragging* selected selected-paths) - (fn [event] - (on-drag-enter-asset event component dragging* selected selected-paths))) - - on-drag-leave - (mf/use-fn - (mf/deps dragging*) - (fn [event] - (on-drag-leave-asset event dragging*))) - - on-component-drag-start - (mf/use-fn - (mf/deps file-id component selected item-ref on-drag-start read-only?) - (fn [event] - (if read-only? - (dom/prevent-default event) - (on-asset-drag-start event file-id component selected item-ref :components on-drag-start)))) - - on-context-menu - (mf/use-fn - (mf/deps on-context-menu component-id) - (partial on-context-menu component-id))] - - [:div {:ref item-ref - :class (dom/classnames - :selected (contains? selected (:id component)) - :grid-cell listing-thumbs? - :enum-item (not listing-thumbs?)) - :id (dm/str "component-shape-id-" (:id component)) - :draggable (not read-only?) - :on-click on-component-click - :on-double-click on-component-double-click - :on-context-menu on-context-menu - :on-drag-start on-component-drag-start - :on-drag-enter on-drag-enter - :on-drag-leave on-drag-leave - :on-drag-over dom/prevent-default - :on-drop on-drop} - - (when (and (some? root-shape) - (some? container)) - [:* - [:& component-svg {:root-shape root-shape - :objects (:objects container)}] - (let [renaming? (= renaming (:id component))] - [:* - [:& editable-label - {:class-name (dom/classnames - :cell-name listing-thumbs? - :item-name (not listing-thumbs?) - :editing renaming?) - :value (cph/merge-path-item (:path component) (:name component)) - :tooltip (cph/merge-path-item (:path component) (:name component)) - :display-value (:name component) - :editing? renaming? - :disable-dbl-click? true - :on-change do-rename - :on-cancel cancel-rename}] - - (when ^boolean dragging? - [:div.dragging])])])])) - -(mf/defc components-group - {::mf/wrap-props false} - [{:keys [file-id prefix groups open-groups renaming listing-thumbs? selected on-asset-click - on-drag-start do-rename cancel-rename on-rename-group on-group on-ungroup on-context-menu - selected-full]}] - - (let [group-open? (get open-groups prefix true) - - dragging* (mf/use-state false) - dragging? (deref dragging*) - - selected-paths (mf/with-memo [selected-full] - (into #{} - (comp (map :path) (d/nilv "")) - selected-full)) - on-drag-enter - (mf/use-fn - (mf/deps dragging* prefix selected-paths) - (fn [event] - (on-drag-enter-asset-group event dragging* prefix selected-paths))) - - on-drag-leave - (mf/use-fn - (mf/deps dragging*) - (fn [event] - (on-drag-leave-asset event dragging*))) - - on-drop - (mf/use-fn - (mf/deps dragging* prefix selected-paths selected-full) - (fn [event] - (on-drop-asset-group event dragging* prefix selected-paths selected-full dwl/rename-component)))] - - [:div {:on-drag-enter on-drag-enter - :on-drag-leave on-drag-leave - :on-drag-over dom/prevent-default - :on-drop on-drop} - - [:& asset-group-title - {:file-id file-id - :section :components - :path prefix - :group-open? group-open? - :on-rename on-rename-group - :on-ungroup on-ungroup}] - - (when group-open? - [:* - (let [components (get groups "" [])] - [:div {:class-name (dom/classnames - :asset-grid listing-thumbs? - :big listing-thumbs? - :asset-enum (not listing-thumbs?) - :drop-space (and - (empty? components) - (some? groups) - (not dragging?))) - :on-drag-enter on-drag-enter - :on-drag-leave on-drag-leave - :on-drag-over dom/prevent-default - :on-drop on-drop} - - (when ^boolean dragging? - [:div.grid-placeholder "\u00A0"]) - - (when (and (empty? components) - (some? groups)) - [:div.drop-space]) - - (for [component components] - [:& components-item - {:component component - :key (dm/str "component-" (:id component)) - :renaming renaming - :listing-thumbs? listing-thumbs? - :file-id file-id - :selected selected - :selected-full selected-full - :selected-paths selected-paths - :on-asset-click on-asset-click - :on-context-menu on-context-menu - :on-drag-start on-drag-start - :on-group on-group - :do-rename do-rename - :cancel-rename cancel-rename}])]) - - (for [[path-item content] groups] - (when-not (empty? path-item) - [:& components-group {:file-id file-id - :key path-item - :prefix (cph/merge-path-item prefix path-item) - :groups content - :open-groups open-groups - :renaming renaming - :listing-thumbs? listing-thumbs? - :selected selected - :on-asset-click on-asset-click - :on-drag-start on-drag-start - :do-rename do-rename - :cancel-rename cancel-rename - :on-rename-group on-rename-group - :on-ungroup on-ungroup - :on-context-menu on-context-menu - :selected-full selected-full}]))])])) - -(mf/defc components-section - {::mf/wrap-props false} - [{:keys [file-id local? components listing-thumbs? open? reverse-sort? selected - on-asset-click on-assets-delete on-clear-selection open-status-ref]}] - - (let [input-ref (mf/use-ref nil) - - state* (mf/use-state {}) - state (deref state*) - - current-component-id (:component-id state) - renaming? (:renaming state) - - open-groups-ref (mf/with-memo [open-status-ref] - (-> (l/in [:groups :components]) - (l/derived open-status-ref))) - - open-groups (mf/deref open-groups-ref) - - menu-state (mf/use-state initial-context-menu-state) - read-only? (mf/use-ctx ctx/workspace-read-only?) - - selected (:components selected) - selected-full (into #{} (filter #(contains? selected (:id %))) components) - multi-components? (> (count selected) 1) - multi-assets? (or (seq (:graphics selected)) - (seq (:colors selected)) - (seq (:typographies selected))) - - groups (mf/with-memo [components reverse-sort?] - (group-assets components reverse-sort?)) - - components-v2 (mf/use-ctx ctx/components-v2) - - add-component - (mf/use-fn - (fn [] - (st/emit! (dw/set-assets-section-open file-id :components true)) - (dom/click (mf/ref-val input-ref)))) - - on-file-selected - (mf/use-fn - (mf/deps file-id) - (fn [blobs] - (let [params {:file-id file-id - :blobs (seq blobs)}] - (st/emit! (dwm/upload-media-components params) - (ptk/event ::ev/event {::ev/name "add-asset-to-library" - :asset-type "components"}))))) - - on-duplicate - (mf/use-fn - (mf/deps current-component-id selected) - (fn [] - (if (empty? selected) - (st/emit! (dwl/duplicate-component file-id current-component-id)) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! (map (partial dwl/duplicate-component file-id) selected)) - (st/emit! (dwu/commit-undo-transaction undo-id)))))) - - on-delete - (mf/use-fn - (mf/deps current-component-id file-id multi-components? multi-assets? on-assets-delete) - (fn [] - (let [undo-id (js/Symbol)] - (if (or multi-components? multi-assets?) - (on-assets-delete) - (st/emit! (dwu/start-undo-transaction undo-id) - (dwl/delete-component {:id current-component-id}) - (dwl/sync-file file-id file-id :components current-component-id) - (dwu/commit-undo-transaction undo-id)))))) - - on-close-menu - (mf/use-fn #(swap! menu-state close-context-menu)) - - on-rename - (mf/use-fn #(swap! state* assoc :renaming true)) - - cancel-rename - (mf/use-fn #(swap! state* dissoc :renaming)) - - do-rename - (mf/use-fn - (mf/deps current-component-id) - (fn [new-name] - (swap! state* dissoc :renaming) - (st/emit! - (dwl/rename-component-and-main-instance current-component-id new-name)))) - - on-context-menu - (mf/use-fn - (mf/deps selected on-clear-selection read-only?) - (fn [component-id event] - (dom/prevent-default event) - (let [pos (dom/get-client-position event)] - (when (and local? (not read-only?)) - (when-not (contains? selected component-id) - (on-clear-selection)) - - (swap! state* assoc :component-id component-id) - (swap! menu-state open-context-menu pos))))) - - create-group - (mf/use-fn - (mf/deps current-component-id components selected on-clear-selection) - (fn [group-name] - (on-clear-selection) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! - (->> components - (filter #(if multi-components? - (contains? selected (:id %)) - (= current-component-id (:id %)))) - (map #(dwl/rename-component - (:id %) - (add-group % group-name))))) - (st/emit! (dwu/commit-undo-transaction undo-id))))) - - rename-group - (mf/use-fn - (mf/deps components) - (fn [path last-path] - (on-clear-selection) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! - (->> components - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/rename-component - (:id %) - (rename-group % path last-path))))) - (st/emit! (dwu/commit-undo-transaction undo-id))))) - - on-group - (mf/use-fn - (mf/deps components selected create-group) - (fn [event] - (dom/stop-propagation event) - (modal/show! :name-group-dialog {:accept create-group}))) - - on-rename-group - (mf/use-fn - (mf/deps components) - (fn [event path last-path] - (dom/stop-propagation event) - (modal/show! :name-group-dialog {:path path - :last-path last-path - :accept rename-group}))) - - on-ungroup - (mf/use-fn - (mf/deps components) - (fn [path] - (on-clear-selection) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! - (->> components - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/rename-component (:id %) (ungroup % path))))) - (st/emit! (dwu/commit-undo-transaction undo-id))))) - - on-drag-start - (mf/use-fn - (mf/deps file-id) - (fn [component event] - (dnd/set-data! event "penpot/component" {:file-id file-id - :component component}) - (dnd/set-allowed-effect! event "move"))) - - on-show-main - (mf/use-fn - (mf/deps current-component-id file-id) - (fn [event] - (dom/stop-propagation event) - (st/emit! (dw/go-to-main-instance file-id current-component-id)))) - - on-asset-click - (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))] - - [:& asset-section {:file-id file-id - :title (tr "workspace.assets.components") - :section :components - :assets-count (count components) - :open? open?} - (when local? - [:& asset-section-block {:role :title-button} - (when (and components-v2 (not read-only?)) - [:div.assets-button {:on-click add-component} - i/plus - [:& file-uploader {:accept cm/str-image-types - :multi true - :ref input-ref - :on-selected on-file-selected}]])]) - - [:& asset-section-block {:role :content} - [:& components-group {:file-id file-id - :prefix "" - :groups groups - :open-groups open-groups - :renaming (when ^boolean renaming? current-component-id) - :listing-thumbs? listing-thumbs? - :selected selected - :on-asset-click on-asset-click - :on-drag-start on-drag-start - :do-rename do-rename - :cancel-rename cancel-rename - :on-rename-group on-rename-group - :on-group on-group - :on-ungroup on-ungroup - :on-context-menu on-context-menu - :selected-full selected-full}] - (when local? - [:& assets-context-menu - {:on-close on-close-menu - :state @menu-state - :options [(when-not (or multi-components? multi-assets?) - [(tr "workspace.assets.rename") on-rename]) - (when-not multi-assets? - [(if components-v2 - (tr "workspace.assets.duplicate-main") - (tr "workspace.assets.duplicate")) on-duplicate]) - [(tr "workspace.assets.delete") on-delete] - (when-not multi-assets? - [(tr "workspace.assets.group") on-group]) - (when (and components-v2 (not multi-assets?)) - [(tr "workspace.shape.menu.show-main") on-show-main])]}])]])) - - -;; ---- Graphics section ---- - -(mf/defc graphics-item - [{:keys [object renaming listing-thumbs? selected-objects file-id - on-asset-click on-context-menu on-drag-start do-rename cancel-rename - selected-full selected-graphics-paths]}] - (let [item-ref (mf/use-ref) - visible? (h/use-visible item-ref :once? true) - object-id (:id object) - - dragging* (mf/use-state false) - dragging? (deref dragging*) - - read-only? (mf/use-ctx ctx/workspace-read-only?) - - on-drop - (mf/use-fn - (mf/deps object dragging* selected-objects selected-full selected-graphics-paths) - (fn [event] - (on-drop-asset event object dragging* selected-objects selected-full - selected-graphics-paths dwl/rename-media))) - - on-drag-enter - (mf/use-fn - (mf/deps object dragging* selected-objects selected-graphics-paths) - (fn [event] - (on-drag-enter-asset event object dragging* selected-objects selected-graphics-paths))) - - on-drag-leave - (mf/use-fn - (mf/deps dragging*) - (fn [event] - (on-drag-leave-asset event dragging*))) - - on-grahic-drag-start - (mf/use-fn - (mf/deps object file-id selected-objects item-ref on-drag-start read-only?) - (fn [event] - (if read-only? - (dom/prevent-default event) - (on-asset-drag-start event file-id object selected-objects item-ref :graphics on-drag-start)))) - - on-context-menu - (mf/use-fn - (mf/deps on-context-menu object-id) - (partial on-context-menu object-id)) - - on-asset-click - (mf/use-fn - (mf/deps object-id on-asset-click) - (partial on-asset-click object-id nil)) - - ] - - [:div {:ref item-ref - :class-name (dom/classnames - :selected (contains? selected-objects object-id) - :grid-cell listing-thumbs? - :enum-item (not listing-thumbs?)) - :draggable (not read-only?) - :on-click on-asset-click - :on-context-menu on-context-menu - :on-drag-start on-grahic-drag-start - :on-drag-enter on-drag-enter - :on-drag-leave on-drag-leave - :on-drag-over dom/prevent-default - :on-drop on-drop} - - (when visible? - [:* - [:img {:src (when visible? (cf/resolve-file-media object true)) - :draggable false}] ;; Also need to add css pointer-events: none - - (let [renaming? (= renaming (:id object))] - [:* - [:& editable-label - {:class-name (dom/classnames - :cell-name listing-thumbs? - :item-name (not listing-thumbs?) - :editing renaming?) - :value (cph/merge-path-item (:path object) (:name object)) - :tooltip (cph/merge-path-item (:path object) (:name object)) - :display-value (:name object) - :editing? renaming? - :disable-dbl-click? true - :on-change do-rename - :on-cancel cancel-rename}] - - (when ^boolean dragging? - [:div.dragging])])])])) - -(mf/defc graphics-group - [{:keys [file-id prefix groups open-groups renaming listing-thumbs? selected-objects on-asset-click - on-drag-start do-rename cancel-rename on-rename-group on-ungroup - on-context-menu selected-full]}] - (let [group-open? (get open-groups prefix true) - - dragging* (mf/use-state false) - dragging? (deref dragging*) - - selected-paths - (mf/with-memo [selected-full] - (into #{} - (comp (map :path) (d/nilv "")) - selected-full)) - - on-drag-enter - (mf/use-fn - (mf/deps dragging* prefix selected-paths) - (fn [event] - (on-drag-enter-asset-group event dragging* prefix selected-paths))) - - on-drag-leave - (mf/use-fn - (mf/deps dragging*) - (fn [event] - (on-drag-leave-asset event dragging*))) - - on-drop - (mf/use-fn - (mf/deps dragging* prefix selected-paths selected-full) - (fn [event] - (on-drop-asset-group event dragging* prefix selected-paths selected-full dwl/rename-media)))] - - [:div {:on-drag-enter on-drag-enter - :on-drag-leave on-drag-leave - :on-drag-over dom/prevent-default - :on-drop on-drop} - [:& asset-group-title {:file-id file-id - :section :graphics - :path prefix - :group-open? group-open? - :on-rename on-rename-group - :on-ungroup on-ungroup}] - (when group-open? - [:* - (let [objects (get groups "" [])] - [:div {:class-name (dom/classnames - :asset-grid listing-thumbs? - :asset-enum (not listing-thumbs?) - :drop-space (and - (empty? objects) - (some? groups) - (not dragging?))) - :on-drag-enter on-drag-enter - :on-drag-leave on-drag-leave - :on-drag-over dom/prevent-default - :on-drop on-drop} - - (when ^boolean dragging? - [:div.grid-placeholder "\u00A0"]) - - (when (and (empty? objects) - (some? groups)) - [:div.drop-space]) - - (for [object objects] - [:& graphics-item {:key (dm/str "object-" (:id object)) - :file-id file-id - :object object - :renaming renaming - :listing-thumbs? listing-thumbs? - :selected-objects selected-objects - :on-asset-click on-asset-click - :on-context-menu on-context-menu - :on-drag-start on-drag-start - :do-rename do-rename - :cancel-rename cancel-rename - :selected-full selected-full - :selected-graphics-paths selected-paths}])]) - (for [[path-item content] groups] - (when-not (empty? path-item) - [:& graphics-group {:file-id file-id - :key path-item - :prefix (cph/merge-path-item prefix path-item) - :groups content - :open-groups open-groups - :renaming renaming - :listing-thumbs? listing-thumbs? - :selected-objects selected-objects - :on-asset-click on-asset-click - :on-drag-start on-drag-start - :do-rename do-rename - :cancel-rename cancel-rename - :on-rename-group on-rename-group - :on-ungroup on-ungroup - :on-context-menu on-context-menu - :selected-full selected-full}]))])])) - -(mf/defc graphics-section - {::mf/wrap-props false} - [{:keys [file-id project-id local? objects listing-thumbs? open? open-status-ref selected reverse-sort? - on-asset-click on-assets-delete on-clear-selection]}] - (let [input-ref (mf/use-ref nil) - state (mf/use-state {:renaming nil :object-id nil}) - - menu-state (mf/use-state initial-context-menu-state) - read-only? (mf/use-ctx ctx/workspace-read-only?) - - open-groups-ref (mf/with-memo [open-status-ref] - (-> (l/in [:groups :graphics]) - (l/derived open-status-ref))) - open-groups (mf/deref open-groups-ref) - - selected (:graphics selected) - selected-full (into #{} (filter #(contains? selected (:id %))) objects) - multi-objects? (> (count selected) 1) - multi-assets? (or (seq (:components selected)) - (seq (:colors selected)) - (seq (:typographies selected))) - - objects (mf/with-memo [objects] - (mapv dwl/extract-path-if-missing objects)) - - groups (mf/with-memo [objects reverse-sort?] - (group-assets objects reverse-sort?)) - - components-v2 (mf/use-ctx ctx/components-v2) - team-id (mf/use-ctx ctx/current-team-id) - - add-graphic - (mf/use-fn - (fn [] - (st/emit! (dw/set-assets-section-open file-id :graphics true)) - (dom/click (mf/ref-val input-ref)))) - - on-file-selected - (mf/use-fn - (mf/deps file-id project-id team-id) - (fn [blobs] - (let [params {:file-id file-id - :blobs (seq blobs)}] - (st/emit! (dwm/upload-media-asset params) - (ptk/event ::ev/event {::ev/name "add-asset-to-library" - :asset-type "graphics" - :file-id file-id - :project-id project-id - :team-id team-id}))))) - on-delete - (mf/use-fn - (mf/deps @state multi-objects? multi-assets?) - (fn [] - (if (or multi-objects? multi-assets?) - (on-assets-delete) - (st/emit! (dwl/delete-media {:id (:object-id @state)}))))) - - on-rename - (mf/use-fn - (fn [] - (swap! state (fn [state] - (assoc state :renaming (:object-id state)))))) - - cancel-rename - (mf/use-fn - (fn [] - (swap! state assoc :renaming nil))) - - do-rename - (mf/use-fn - (mf/deps @state) - (fn [new-name] - (st/emit! (dwl/rename-media (:renaming @state) new-name)) - (swap! state assoc :renaming nil))) - - on-context-menu - (mf/use-fn - (mf/deps selected on-clear-selection read-only?) - (fn [object-id event] - (dom/prevent-default event) - (let [pos (dom/get-client-position event)] - (when (and local? (not read-only?)) - (when-not (contains? selected object-id) - (on-clear-selection)) - (swap! state assoc :object-id object-id) - (swap! menu-state open-context-menu pos))))) - - on-close-menu - (mf/use-fn - (fn [] - (swap! menu-state close-context-menu))) - - create-group - (mf/use-fn - (mf/deps objects selected on-clear-selection (:object-id @state)) - (fn [group-name] - (on-clear-selection) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! - (->> objects - (filter #(if multi-objects? - (contains? selected (:id %)) - (= (:object-id @state) (:id %)))) - (map #(dwl/rename-media (:id %) (add-group % group-name))))) - (st/emit! (dwu/commit-undo-transaction undo-id))))) - - rename-group - (mf/use-fn - (mf/deps objects) - (fn [path last-path] - (on-clear-selection) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! - (->> objects - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/rename-media (:id %) (rename-group % path last-path))))) - (st/emit! (dwu/commit-undo-transaction undo-id))))) - - on-group - (mf/use-fn - (mf/deps objects selected create-group) - (fn [event] - (dom/stop-propagation event) - (modal/show! :name-group-dialog {:accept create-group}))) - - on-rename-group - (mf/use-fn - (mf/deps objects) - (fn [event path last-path] - (dom/stop-propagation event) - (modal/show! :name-group-dialog {:path path - :last-path last-path - :accept rename-group}))) - on-ungroup - (mf/use-fn - (mf/deps objects) - (fn [path] - (on-clear-selection) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! - (->> objects - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/rename-media (:id %) (ungroup % path))))) - (st/emit! (dwu/commit-undo-transaction undo-id))))) - - on-drag-start - (mf/use-fn - (fn [{:keys [name id mtype]} event] - (dnd/set-data! event "text/asset-id" (str id)) - (dnd/set-data! event "text/asset-name" name) - (dnd/set-data! event "text/asset-type" mtype) - (dnd/set-allowed-effect! event "move"))) - - on-asset-click - (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))] - - [:& asset-section {:file-id file-id - :title (tr "workspace.assets.graphics") - :section :graphics - :assets-count (count objects) - :open? open?} - (when local? - [:& asset-section-block {:role :title-button} - (when (and (not components-v2) (not read-only?)) - [:div.assets-button {:on-click add-graphic} - i/plus - [:& file-uploader {:accept cm/str-image-types - :multi true - :ref input-ref - :on-selected on-file-selected}]])]) - - [:& asset-section-block {:role :content} - [:& graphics-group {:file-id file-id - :prefix "" - :groups groups - :open-groups open-groups - :renaming (:renaming @state) - :listing-thumbs? listing-thumbs? - :selected-objects selected - :on-asset-click on-asset-click - :on-drag-start on-drag-start - :do-rename do-rename - :cancel-rename cancel-rename - :on-rename-group on-rename-group - :on-ungroup on-ungroup - :on-context-menu on-context-menu - :selected-full selected-full}] - (when local? - [:& assets-context-menu - {:on-close on-close-menu - :state @menu-state - :options [(when-not (or multi-objects? multi-assets?) - [(tr "workspace.assets.rename") on-rename]) - [(tr "workspace.assets.delete") on-delete] - (when-not multi-assets? - [(tr "workspace.assets.group") on-group])]}])]])) - - -;; ---- Colors section ---- - -(mf/defc color-item - {::mf/wrap-props false} - [{:keys [color local? file-id selected multi-colors? multi-assets? - on-asset-click on-assets-delete on-clear-selection on-group - selected-full selected-paths move-color]}] - - (let [color (mf/with-memo [color file-id] - (cond-> color - (:value color) (assoc :color (:value color) :opacity 1) - (:value color) (dissoc :value) - true (assoc :file-id file-id))) - - - color-id (:id color) - - item-ref (mf/use-ref) - dragging* (mf/use-state false) - dragging? (deref dragging*) - - rename? (= (:color-for-rename @refs/workspace-local) color-id) - input-ref (mf/use-ref) - - editing* (mf/use-state rename?) - editing? (deref editing*) - - menu-state (mf/use-state initial-context-menu-state) - read-only? (mf/use-ctx ctx/workspace-read-only?) - - default-name (cond - (:gradient color) (uc/gradient-type->string (dm/get-in color [:gradient :type])) - (:color color) (:color color) - :else (:value color)) - - apply-color - (mf/use-fn - (mf/deps color) - (fn [event] - (st/emit! (dc/apply-color-from-palette (merge uc/empty-color color) (kbd/alt? event))))) - - rename-color - (mf/use-fn - (mf/deps file-id color-id) - (fn [name] - (st/emit! (dwl/rename-color file-id color-id name)))) - - edit-color - (mf/use-fn - (mf/deps color file-id) - (fn [attrs] - (let [name (cph/merge-path-item (:path color) (:name color)) - color (-> attrs - (assoc :id (:id color)) - (assoc :file-id file-id) - (assoc :name name))] - (st/emit! (dwl/update-color color file-id))))) - - delete-color - (mf/use-fn - (mf/deps multi-colors? multi-assets? file-id color-id) - (fn [] - (if (or multi-colors? multi-assets?) - (on-assets-delete) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id) - (dwl/delete-color color) - (dwl/sync-file file-id file-id :colors color-id) - (dwu/commit-undo-transaction undo-id)))))) - - rename-color-clicked - (mf/use-fn - (mf/deps read-only? local?) - (fn [event] - (when (and local? (not read-only?)) - (dom/prevent-default event) - (reset! editing* true)))) - - input-blur - (mf/use-fn - (mf/deps rename-color) - (fn [event] - (let [target (dom/event->target event) - name (dom/get-value target)] - (rename-color name) - (st/emit! dwl/clear-color-for-rename) - (reset! editing* false)))) - - input-key-down - (mf/use-fn - (mf/deps input-blur) - (fn [event] - (when (kbd/esc? event) - (st/emit! dwl/clear-color-for-rename) - (reset! editing* false)) - (when (kbd/enter? event) - (input-blur event)))) - - edit-color-clicked - (mf/use-fn - (mf/deps edit-color color) - (fn [event] - (modal/show! :colorpicker - {:x (.-clientX ^js event) - :y (.-clientY ^js event) - :on-accept edit-color - :data color - :position :right}))) - - on-context-menu - (mf/use-fn - (mf/deps color-id selected on-clear-selection read-only?) - (fn [event] - (dom/prevent-default event) - (let [pos (dom/get-client-position event)] - (when (and local? (not read-only?)) - (when-not (contains? selected color-id) - (on-clear-selection)) - (swap! menu-state open-context-menu pos))))) - - on-close-menu - (mf/use-fn - (fn [] - (swap! menu-state close-context-menu))) - - on-drop - (mf/use-fn - (mf/deps color dragging* selected selected-full selected-paths move-color) - (fn [event] - (on-drop-asset event color dragging* selected selected-full - selected-paths move-color))) - - on-drag-enter - (mf/use-fn - (mf/deps color dragging* selected selected-paths) - (fn [event] - (on-drag-enter-asset event color dragging* selected selected-paths))) - - on-drag-leave - (mf/use-fn - (mf/deps dragging*) - (fn [event] - (on-drag-leave-asset event dragging*))) - - on-color-drag-start - (mf/use-fn - (mf/deps color file-id selected item-ref read-only?) - (fn [event] - (if read-only? - (dom/prevent-default event) - (on-asset-drag-start event file-id color selected item-ref :colors identity)))) - - on-click - (mf/use-fn - (mf/deps color-id apply-color on-asset-click) - (partial on-asset-click color-id apply-color))] - - (mf/with-effect [editing?] - (when editing? - (let [input (mf/ref-val input-ref)] - (dom/select-text! input) - nil))) - - [:div.asset-list-item - {:class-name (dom/classnames - :selected (contains? selected (:id color))) - :on-context-menu on-context-menu - :on-click (when-not editing? on-click) - :ref item-ref - :draggable (and (not read-only?) (not editing?)) - :on-drag-start on-color-drag-start - :on-drag-enter on-drag-enter - :on-drag-leave on-drag-leave - :on-drag-over dom/prevent-default - :on-drop on-drop} - - [:& bc/color-bullet {:color color}] - - (if ^boolean editing? - [:input.element-name - {:type "text" - :ref input-ref - :on-blur input-blur - :on-key-down input-key-down - :auto-focus true - :default-value (cph/merge-path-item (:path color) (:name color))}] - - [:div.name-block {:title (:name color) - :on-double-click rename-color-clicked} - (:name color) - (when-not (= (:name color) default-name) - [:span default-name])]) - - (when local? - [:& assets-context-menu - {:on-close on-close-menu - :state @menu-state - :options [(when-not (or multi-colors? multi-assets?) - [(tr "workspace.assets.rename") rename-color-clicked]) - (when-not (or multi-colors? multi-assets?) - [(tr "workspace.assets.edit") edit-color-clicked]) - [(tr "workspace.assets.delete") delete-color] - (when-not multi-assets? - [(tr "workspace.assets.group") (on-group (:id color))])]}]) - - (when ^boolean dragging? - [:div.dragging])])) - -(mf/defc colors-group - [{:keys [file-id prefix groups open-groups local? selected - multi-colors? multi-assets? on-asset-click on-assets-delete - on-clear-selection on-group on-rename-group on-ungroup colors - selected-full]}] - (let [group-open? (get open-groups prefix true) - - dragging* (mf/use-state false) - dragging? (deref dragging*) - - selected-paths (mf/with-memo [selected-full] - (into #{} - (comp (map :path) (d/nilv "")) - selected-full)) - - move-color - (mf/use-fn (mf/deps file-id) (partial dwl/rename-color file-id)) - - on-drag-enter - (mf/use-fn - (mf/deps dragging* prefix selected-paths) - (fn [event] - (on-drag-enter-asset-group event dragging* prefix selected-paths))) - - on-drag-leave - (mf/use-fn - (mf/deps dragging*) - (fn [event] - (on-drag-leave-asset event dragging*))) - - on-drop - (mf/use-fn - (mf/deps dragging* prefix selected-paths selected-full move-color) - (fn [event] - (on-drop-asset-group event dragging* prefix selected-paths selected-full move-color)))] - - [:div {:on-drag-enter on-drag-enter - :on-drag-leave on-drag-leave - :on-drag-over dom/prevent-default - :on-drop on-drop} - [:& asset-group-title {:file-id file-id - :section :colors - :path prefix - :group-open? group-open? - :on-rename on-rename-group - :on-ungroup on-ungroup}] - (when group-open? - [:* - (let [colors (get groups "" [])] - [:div.asset-list {:on-drag-enter on-drag-enter - :on-drag-leave on-drag-leave - :on-drag-over dom/prevent-default - :on-drop on-drop} - - (when ^boolean dragging? - [:div.grid-placeholder "\u00A0"]) - - (when (and (empty? colors) - (some? groups)) - [:div.drop-space]) - - (for [color colors] - [:& color-item {:key (dm/str (:id color)) - :color color - :file-id file-id - :local? local? - :selected selected - :multi-colors? multi-colors? - :multi-assets? multi-assets? - :on-asset-click on-asset-click - :on-assets-delete on-assets-delete - :on-clear-selection on-clear-selection - :on-group on-group - :colors colors - :selected-full selected-full - :selected-paths selected-paths - :move-color move-color}])]) - - (for [[path-item content] groups] - (when-not (empty? path-item) - [:& colors-group {:file-id file-id - :prefix (cph/merge-path-item prefix path-item) - :key (dm/str "group-" path-item) - :groups content - :open-groups open-groups - :local? local? - :selected selected - :multi-colors? multi-colors? - :multi-assets? multi-assets? - :on-asset-click on-asset-click - :on-assets-delete on-assets-delete - :on-clear-selection on-clear-selection - :on-group on-group - :on-rename-group on-rename-group - :on-ungroup on-ungroup - :colors colors - :selected-full selected-full}]))])])) - -(mf/defc colors-section - [{:keys [file-id local? colors open? open-status-ref selected reverse-sort? - on-asset-click on-assets-delete on-clear-selection] :as props}] - - (let [selected (:colors selected) - selected-full (mf/with-memo [selected colors] - (into #{} (filter #(contains? selected (:id %))) colors)) - - open-groups-ref (mf/with-memo [open-status-ref] - (-> (l/in [:groups :colors]) - (l/derived open-status-ref))) - open-groups (mf/deref open-groups-ref) - - multi-colors? (> (count selected) 1) - multi-assets? (or (seq (:components selected)) - (seq (:graphics selected)) - (seq (:typographies selected))) - - groups (mf/with-memo [colors reverse-sort?] - (group-assets colors reverse-sort?)) - - read-only? (mf/use-ctx ctx/workspace-read-only?) - - add-color - (mf/use-fn - (fn [value _] - (st/emit! (dwl/add-color value)))) - - add-color-clicked - (mf/use-fn - (fn [event] - (let [position (dom/get-client-position event)] - (st/emit! (dc/select-color position add-color))))) - - create-group - (mf/use-fn - (mf/deps colors selected on-clear-selection file-id) - (fn [color-id] - (fn [group-name] - (on-clear-selection) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! - (->> colors - (filter #(if multi-colors? - (contains? selected (:id %)) - (= color-id (:id %)))) - (map #(dwl/update-color - (assoc % :name - (add-group % group-name)) - file-id)))) - (st/emit! (dwu/commit-undo-transaction undo-id)))))) - - rename-group - (mf/use-fn - (mf/deps colors) - (fn [path last-path] - (on-clear-selection) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! - (->> colors - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/update-color - (assoc % :name - (rename-group % path last-path)) - file-id)))) - (st/emit! (dwu/commit-undo-transaction undo-id))))) - - on-group - (mf/use-fn - (mf/deps colors selected) - (fn [color-id] - (fn [event] - (dom/stop-propagation event) - (modal/show! :name-group-dialog {:accept (create-group color-id)})))) - - on-rename-group - (mf/use-fn - (mf/deps colors) - (fn [event path last-path] - (dom/stop-propagation event) - (modal/show! :name-group-dialog {:path path - :last-path last-path - :accept rename-group}))) - on-ungroup - (mf/use-fn - (mf/deps colors) - (fn [path] - (on-clear-selection) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> colors - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/update-color - (assoc % :name - (ungroup % path)) - file-id)))) - (st/emit! (dwu/commit-undo-transaction undo-id))))) - - on-asset-click - (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))] - - [:& asset-section {:file-id file-id - :title (tr "workspace.assets.colors") - :section :colors - :assets-count (count colors) - :open? open?} - (when local? - [:& asset-section-block {:role :title-button} - (when-not read-only? - [:div.assets-button {:on-click add-color-clicked} - i/plus])]) - - [:& asset-section-block {:role :content} - [:& colors-group {:file-id file-id - :prefix "" - :groups groups - :open-groups open-groups - :local? local? - :selected selected - :multi-colors? multi-colors? - :multi-assets? multi-assets? - :on-asset-click on-asset-click - :on-assets-delete on-assets-delete - :on-clear-selection on-clear-selection - :on-group on-group - :on-rename-group on-rename-group - :on-ungroup on-ungroup - :colors colors - :selected-full selected-full}]]])) - -;; ---- Typography section ---- - -(mf/defc typography-item - {::mf/wrap-props false} - [{:keys [typography file-id local? handle-change selected apply-typography editing-id on-asset-click - on-context-menu selected-full selected-paths move-typography rename?]}] - (let [item-ref (mf/use-ref) - typography-id (:id typography) - - dragging* (mf/use-state false) - dragging? (deref dragging*) - - read-only? (mf/use-ctx ctx/workspace-read-only?) - editing? (= editing-id (:id typography)) - - open* (mf/use-state editing?) - open? (deref open*) - - on-drop - (mf/use-fn - (mf/deps typography dragging* selected selected-full selected-paths move-typography) - (fn [event] - (on-drop-asset event typography dragging* selected selected-full - selected-paths move-typography))) - - on-drag-enter - (mf/use-fn - (mf/deps typography dragging* selected selected-paths) - (fn [event] - (on-drag-enter-asset event typography dragging* selected selected-paths))) - - on-drag-leave - (mf/use-fn - (mf/deps dragging*) - (fn [event] - (on-drag-leave-asset event dragging*))) - - on-typography-drag-start - (mf/use-fn - (mf/deps typography file-id selected item-ref read-only?) - (fn [event] - (if read-only? - (dom/prevent-default event) - (on-asset-drag-start event file-id typography selected item-ref :typographies identity)))) - - on-context-menu - (mf/use-fn - (mf/deps on-context-menu typography-id) - (partial on-context-menu typography-id)) - - handle-change - (mf/use-fn - (mf/deps typography) - (partial handle-change typography)) - - apply-typography - (mf/use-fn - (mf/deps typography) - (partial apply-typography typography)) - - on-asset-click - (mf/use-fn - (mf/deps typography apply-typography on-asset-click) - (partial on-asset-click typography-id apply-typography)) - - ] - - [:div.typography-container {:ref item-ref - :draggable (and (not read-only?) (not open?)) - :on-drag-start on-typography-drag-start - :on-drag-enter on-drag-enter - :on-drag-leave on-drag-leave - :on-drag-over dom/prevent-default - :on-drop on-drop} - [:& typography-entry - {:typography typography - :local? local? - :on-context-menu on-context-menu - :on-change handle-change - :selected? (contains? selected typography-id) - :on-click on-asset-click - :editing? editing? - :focus-name? rename? - :external-open* open* - :file-id file-id - }] - - (when ^boolean dragging? - [:div.dragging])])) - -(mf/defc typographies-group - {::mf/wrap-props false} - [{:keys [file-id prefix groups open-groups file local? selected local-data - editing-id on-asset-click handle-change apply-typography on-rename-group - on-ungroup on-context-menu selected-full]}] - (let [group-open? (get open-groups prefix true) - dragging* (mf/use-state false) - dragging? (deref dragging*) - - selected-paths (mf/with-memo [selected-full] - (into #{} - (comp (map :path) (d/nilv "")) - selected-full)) - move-typography - (mf/use-fn - (mf/deps file-id) - (partial dwl/rename-typography file-id)) - - on-drag-enter - (mf/use-fn - (mf/deps dragging* prefix selected-paths) - (fn [event] - (on-drag-enter-asset-group event dragging* prefix selected-paths))) - - on-drag-leave - (mf/use-fn - (mf/deps dragging*) - (fn [event] - (on-drag-leave-asset event dragging*))) - - on-drop - (mf/use-fn - (mf/deps dragging* prefix selected-paths selected-full move-typography) - (fn [event] - (on-drop-asset-group event dragging* prefix selected-paths selected-full move-typography)))] - - [:div {:on-drag-enter on-drag-enter - :on-drag-leave on-drag-leave - :on-drag-over dom/prevent-default - :on-drop on-drop} - [:& asset-group-title {:file-id file-id - :section :typographies - :path prefix - :group-open? group-open? - :on-rename on-rename-group - :on-ungroup on-ungroup}] - (when group-open? - [:* - (let [typographies (get groups "" [])] - [:div.asset-list {:on-drag-enter on-drag-enter - :on-drag-leave on-drag-leave - :on-drag-over dom/prevent-default - :on-drop on-drop} - - (when ^boolean dragging? - [:div.grid-placeholder "\u00A0"]) - - (when (and - (empty? typographies) - (some? groups)) - [:div.drop-space]) - (for [{:keys [id] :as typography} typographies] - [:& typography-item {:typography typography - :key (dm/str "typography-" id) - :file-id file-id - :local? local? - :handle-change handle-change - :selected selected - :apply-typography apply-typography - :editing-id editing-id - :rename? (= (:rename-typography local-data) id) - :on-asset-click on-asset-click - :on-context-menu on-context-menu - :selected-full selected-full - :selected-paths selected-paths - :move-typography move-typography}])]) - - (for [[path-item content] groups] - (when-not (empty? path-item) - [:& typographies-group {:file-id file-id - :prefix (cph/merge-path-item prefix path-item) - :key (dm/str "group-" path-item) - :groups content - :open-groups open-groups - :file file - :local? local? - :selected selected - :editing-id editing-id - :local-data local-data - :on-asset-click on-asset-click - :handle-change handle-change - :apply-typography apply-typography - :on-rename-group on-rename-group - :on-ungroup on-ungroup - :on-context-menu on-context-menu - :selected-full selected-full}]))])])) - -(mf/defc typographies-section - {::mf/wrap-props false} - [{:keys [file file-id local? typographies open? open-status-ref selected reverse-sort? - on-asset-click on-assets-delete on-clear-selection]}] - (let [state (mf/use-state {:detail-open? false :id nil}) - local-data (mf/deref lens:typography-section-state) - - read-only? (mf/use-ctx ctx/workspace-read-only?) - menu-state (mf/use-state initial-context-menu-state) - typographies (mf/with-memo [typographies] - (mapv dwl/extract-path-if-missing typographies)) - - groups (mf/with-memo [typographies reverse-sort?] - (group-assets typographies reverse-sort?)) - - selected (:typographies selected) - selected-full (mf/with-memo [selected typographies] - (into #{} (filter #(contains? selected (:id %))) typographies)) - - multi-typographies? (> (count selected) 1) - multi-assets? (or (seq (:components selected)) - (seq (:graphics selected)) - (seq (:colors selected))) - - open-groups-ref (mf/with-memo [open-status-ref] - (-> (l/in [:groups :typographies]) - (l/derived open-status-ref))) - - open-groups (mf/deref open-groups-ref) - - add-typography - (mf/use-fn - (mf/deps file-id) - (fn [_] - (st/emit! (dw/set-assets-section-open file-id :typographies true)) - (st/emit! (dwt/add-typography file-id)))) - - handle-change - (mf/use-fn - (mf/deps file-id) - (fn [typography changes] - (st/emit! (dwl/update-typography (merge typography changes) file-id)))) - - apply-typography - (mf/use-fn - (mf/deps file-id) - (fn [typography _event] - (st/emit! (dwt/apply-typography typography file-id)))) - - create-group - (mf/use-fn - (mf/deps typographies selected on-clear-selection file-id (:id @state)) - (fn [group-name] - (on-clear-selection) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! - (->> typographies - (filter #(if multi-typographies? - (contains? selected (:id %)) - (= (:id @state) (:id %)))) - (map #(dwl/update-typography - (assoc % :name - (add-group % group-name)) - file-id)))) - (st/emit! (dwu/commit-undo-transaction undo-id))))) - - rename-group - (mf/use-fn - (mf/deps typographies) - (fn [path last-path] - (on-clear-selection) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! - (->> typographies - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/update-typography - (assoc % :name - (rename-group % path last-path)) - file-id)))) - (st/emit! (dwu/commit-undo-transaction undo-id))))) - - on-group - (mf/use-fn - (mf/deps typographies selected create-group) - (fn [event] - (dom/stop-propagation event) - (modal/show! :name-group-dialog {:accept create-group}))) - - on-rename-group - (mf/use-fn - (mf/deps typographies) - (fn [event path last-path] - (dom/stop-propagation event) - (modal/show! :name-group-dialog {:path path - :last-path last-path - :accept rename-group}))) - on-ungroup - (mf/use-fn - (mf/deps typographies) - (fn [path] - (on-clear-selection) - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (apply st/emit! - (->> typographies - (filter #(str/starts-with? (:path %) path)) - (map #(dwl/rename-typography - file-id - (:id %) - (ungroup % path))))) - (st/emit! (dwu/commit-undo-transaction undo-id))))) - - on-context-menu - (mf/use-fn - (mf/deps selected on-clear-selection read-only?) - (fn [id event] - (dom/prevent-default event) - (let [pos (dom/get-client-position event)] - (when (and local? (not read-only?)) - (when-not (contains? selected id) - (on-clear-selection)) - (swap! state assoc :id id) - (swap! menu-state open-context-menu pos))))) - - on-close-menu - (mf/use-fn - (fn [] - (swap! menu-state close-context-menu))) - - handle-rename-typography-clicked - (fn [] - (st/emit! #(assoc-in % [:workspace-global :rename-typography] (:id @state)))) - - handle-edit-typography-clicked - (fn [] - (st/emit! #(assoc-in % [:workspace-global :edit-typography] (:id @state)))) - - handle-delete-typography - (mf/use-fn - (mf/deps @state multi-typographies? multi-assets?) - (fn [] - (let [undo-id (js/Symbol)] - (if (or multi-typographies? multi-assets?) - (on-assets-delete) - (st/emit! (dwu/start-undo-transaction undo-id) - (dwl/delete-typography (:id @state)) - (dwl/sync-file file-id file-id :typographies (:id @state)) - (dwu/commit-undo-transaction undo-id)))))) - - editing-id (or (:rename-typography local-data) - (:edit-typography local-data)) - - on-asset-click - (mf/use-fn - (mf/deps groups on-asset-click) - (partial on-asset-click groups))] - - (mf/use-effect - (mf/deps local-data) - (fn [] - (when (:rename-typography local-data) - (st/emit! #(update % :workspace-global dissoc :rename-typography))) - (when (:edit-typography local-data) - (st/emit! #(update % :workspace-global dissoc :edit-typography))))) - - [:& asset-section {:file-id file-id - :title (tr "workspace.assets.typography") - :section :typographies - :assets-count (count typographies) - :open? open?} - (when local? - [:& asset-section-block {:role :title-button} - (when-not read-only? - [:div.assets-button {:on-click add-typography} - i/plus])]) - - [:& asset-section-block {:role :content} - [:& typographies-group {:file-id file-id - :prefix "" - :groups groups - :open-groups open-groups - :state state - :file file - :local? local? - :selected selected - :editing-id editing-id - :local-data local-data - :on-asset-click on-asset-click - :handle-change handle-change - :apply-typography apply-typography - :on-rename-group on-rename-group - :on-ungroup on-ungroup - :on-context-menu on-context-menu - :selected-full selected-full}] - - (when local? - [:& assets-context-menu - {:on-close on-close-menu - :state @menu-state - :options [(when-not (or multi-typographies? multi-assets?) - [(tr "workspace.assets.rename") handle-rename-typography-clicked]) - (when-not (or multi-typographies? multi-assets?) - [(tr "workspace.assets.edit") handle-edit-typography-clicked]) - [(tr "workspace.assets.delete") handle-delete-typography] - (when-not multi-assets? - [(tr "workspace.assets.group") on-group])]}])]])) - - -;; --- Assets toolsection ---- - -(defn- apply-filters - [coll {:keys [ordering term] :as filters}] - (let [reverse? (= :desc ordering) - comp-fn (if ^boolean reverse? > <)] - (->> coll - (filter (fn [item] - (or (matches-search (:name item "!$!") term) - (matches-search (:value item "!$!") term)))) - ; Sort by folder order, but - ; putting all "root" items - ; always first, independently - ; of sort order. - (sort-by #(str/lower (cph/merge-path-item (if (empty? (:path %)) - (if reverse? "z" "a") - (:path %)) - (:name %))) - comp-fn)))) - - -(mf/defc file-library-title - {::mf/wrap-props false} - [{:keys [open? local? shared? project-id file-id page-id file-name]}] - (let [router (mf/deref refs/router) - url (rt/resolve router :workspace - {:project-id project-id - :file-id file-id} - {:page-id page-id}) - - toggle-open - (mf/use-fn - (mf/deps file-id open?) - (fn [] - (st/emit! (dw/set-assets-section-open file-id :library (not open?))))) - ] - - [:div.tool-window-bar.library-bar - {:on-click toggle-open} - [:div.collapse-library - {:class (dom/classnames :open open?)} - i/arrow-slide] - - (if local? - [:* - [:span.library-title file-name " (" (tr "workspace.assets.local-library") ")"] - (when shared? - [:span.tool-badge (tr "workspace.assets.shared")])] - [:* - [:span.library-title file-name] - [:span.tool-link.tooltip.tooltip-left {:alt "Open library file"} - [:a {:href (str "#" url) - :target "_blank" - :on-click dom/stop-propagation} - i/chain]]])])) - -(mf/defc file-library-content - {::mf/wrap-props false} - [{:keys [file local? open-status-ref on-clear-selection]}] - (let [components-v2 (mf/use-ctx ctx/components-v2) - open-status (mf/deref open-status-ref) - - file-id (:id file) - project-id (:project-id file) - - filters (mf/use-ctx ctx:filters) - filters-section (:section filters) - filters-term (:term filters) - filters-ordering (:ordering filters) - filters-list-style (:list-style filters) - - reverse-sort? (= :desc filters-ordering) - listing-thumbs? (= :thumbs filters-list-style) - - toggle-ordering (mf/use-ctx ctx:toggle-ordering) - toggle-list-style (mf/use-ctx ctx:toggle-list-style) - - library-ref (mf/with-memo [file-id] - (create-file-library-ref file-id)) - - library (mf/deref library-ref) - colors (:colors library) - components (:components library) - media (:media library) - typographies (:typographies library) - - colors (mf/with-memo [filters colors] - (apply-filters colors filters)) - components (mf/with-memo [filters components] - (apply-filters components filters)) - media (mf/with-memo [filters media] - (apply-filters media filters)) - typographies (mf/with-memo [filters typographies] - (apply-filters typographies filters)) - - show-components? (and (or (= filters-section :all) - (= filters-section :components)) - (or (pos? (count components)) - (str/empty? filters-term))) - show-graphics? (and (or (= filters-section :all) - (= filters-section :graphics)) - (or (pos? (count media)) - (and (str/empty? filters-term) - (not components-v2)))) - show-colors? (and (or (= filters-section :all) - (= filters-section :colors)) - (or (> (count colors) 0) - (str/empty? filters-term))) - show-typography? (and (or (= filters-section :all) - (= filters-section :typographies)) - (or (pos? (count typographies)) - (str/empty? filters-term))) - - - selected-lens (mf/with-memo [file-id] - (-> (l/key file-id) - (l/derived lens:selected))) - selected (mf/deref selected-lens) - selected-count (+ (count (get selected :components)) - (count (get selected :graphics)) - (count (get selected :colors)) - (count (get selected :typographies))) - - extend-selected - (fn [type asset-groups asset-id] - (letfn [(flatten-groups [groups] - (reduce concat [(get groups "" []) - (into [] - (->> (filter #(seq (first %)) groups) - (map second) - (mapcat flatten-groups)))]))] - - (let [selected' (get selected type)] - (if (zero? (count selected')) - (st/emit! (dw/select-single-asset file-id asset-id type)) - (let [all-assets (flatten-groups asset-groups) - click-index (d/index-of-pred all-assets #(= (:id %) asset-id)) - first-index (->> (get selected type) - (map (fn [asset] (d/index-of-pred all-assets #(= (:id %) asset)))) - (sort) - (first)) - - min-index (min first-index click-index) - max-index (max first-index click-index) - ids (->> (d/enumerate all-assets) - (into #{} (comp (filter #(<= min-index (first %) max-index)) - (map (comp :id second)))))] - - (st/emit! (dw/select-assets file-id ids type))))))) - - on-asset-click - (mf/use-fn - (mf/deps file-id extend-selected) - (fn [asset-type asset-groups asset-id default-click event] - (cond - (kbd/mod? event) - (do - (dom/stop-propagation event) - (st/emit! (dw/toggle-selected-assets file-id asset-id asset-type))) - - (kbd/shift? event) - (do - (dom/stop-propagation event) - (extend-selected asset-type asset-groups asset-id)) - - :else - (when default-click - (default-click event))))) - - on-component-click - (mf/use-fn (mf/deps on-asset-click) (partial on-asset-click :components)) - - on-graphics-click - (mf/use-fn (mf/deps on-asset-click) (partial on-asset-click :graphics)) - - on-colors-click - (mf/use-fn (mf/deps on-asset-click) (partial on-asset-click :colors)) - - on-typography-click - (mf/use-fn (mf/deps on-asset-click) (partial on-asset-click :typographies)) - - on-assets-delete - (mf/use-fn - (mf/deps selected file-id) - (fn [] - (let [undo-id (js/Symbol)] - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! (map #(dwl/delete-component {:id %}) - (:components selected))) - (run! st/emit! (map #(dwl/delete-media {:id %}) - (:graphics selected))) - (run! st/emit! (map #(dwl/delete-color {:id %}) - (:colors selected))) - (run! st/emit! (map #(dwl/delete-typography %) - (:typographies selected))) - - (when (or (seq (:components selected)) - (seq (:colors selected)) - (seq (:typographies selected))) - (st/emit! (dwl/sync-file file-id file-id))) - - (st/emit! (dwu/commit-undo-transaction undo-id)))))] - - [:div.tool-window-content - [:div.listing-options - (when (> selected-count 0) - [:span.selected-count - (tr "workspace.assets.selected-count" (i18n/c selected-count))]) - [:div.listing-option-btn.first {:on-click toggle-ordering} - (if reverse-sort? - i/sort-ascending - i/sort-descending)] - [:div.listing-option-btn {:on-click toggle-list-style} - (if listing-thumbs? - i/listing-enum - i/listing-thumbs)]] - - (when ^boolean show-components? - [:& components-section - {:file-id file-id - :local? local? - :components components - :listing-thumbs? listing-thumbs? - :open? (get open-status :components true) - :open-status-ref open-status-ref - :reverse-sort? reverse-sort? - :selected selected - :on-asset-click on-component-click - :on-assets-delete on-assets-delete - :on-clear-selection on-clear-selection}]) - - (when ^boolean show-graphics? - [:& graphics-section - {:file-id file-id - :project-id project-id - :local? local? - :objects media - :listing-thumbs? listing-thumbs? - :open? (get open-status :graphics true) - :open-status-ref open-status-ref - :reverse-sort? reverse-sort? - :selected selected - :on-asset-click on-graphics-click - :on-assets-delete on-assets-delete - :on-clear-selection on-clear-selection}]) - - (when ^boolean show-colors? - [:& colors-section - {:file-id file-id - :local? local? - :colors colors - :open? (get open-status :colors true) - :open-status-ref open-status-ref - :reverse-sort? reverse-sort? - :selected selected - :on-asset-click on-colors-click - :on-assets-delete on-assets-delete - :on-clear-selection on-clear-selection}]) - - (when ^boolean show-typography? - [:& typographies-section - {:file file - :file-id (:id file) - :local? local? - :typographies typographies - :open? (get open-status :typographies true) - :open-status-ref open-status-ref - :reverse-sort? reverse-sort? - :selected selected - :on-asset-click on-typography-click - :on-assets-delete on-assets-delete - :on-clear-selection on-clear-selection}]) - - (when (and (not ^boolean show-components?) - (not ^boolean show-graphics?) - (not ^boolean show-colors?) - (not ^boolean show-typography?)) - [:div.asset-section - [:div.asset-title (tr "workspace.assets.not-found")]])])) - -(mf/defc file-library - {::mf/wrap-props false} - [{:keys [file local? default-open? filters]}] - (let [file-id (:id file) - file-name (:name file) - shared? (:is-shared file) - project-id (:project-id file) - page-id (dm/get-in file [:data :pages 0]) - - open-status-ref (mf/with-memo [file-id] - (-> (l/key file-id) - (l/derived lens:open-status))) - open-status (mf/deref open-status-ref) - open? (d/nilv (:library open-status) default-open?) - - unselect-all - (mf/use-fn - (mf/deps file-id) - (fn [] - (st/emit! (dw/unselect-all-assets file-id)))) - - ] - - [:div.tool-window {:on-context-menu dom/prevent-default - :on-click unselect-all} - [:& file-library-title - {:project-id project-id - :file-id file-id - :page-id page-id - :file-name file-name - :open? open? - :local? local? - :shared? shared?}] - (when ^boolean open? - [:& file-library-content - {:file file - :local? local? - :filters filters - :on-clear-selection unselect-all - :open-status-ref open-status-ref}])])) - (mf/defc assets-libraries {::mf/wrap [mf/memo] ::mf/wrap-props false} @@ -2422,105 +67,133 @@ (mf/defc assets-toolbox {::mf/wrap [mf/memo] ::mf/wrap-props false} - [] - (let [components-v2 (mf/use-ctx ctx/components-v2) - read-only? (mf/use-ctx ctx/workspace-read-only?) - - filters* (mf/use-state - {:term "" - :section :all - :ordering (dwa/get-current-assets-ordering) - :list-style (dwa/get-current-assets-list-style)}) - filters (deref filters*) - term (:term filters) - ordering (:ordering filters) - list-style (:list-style filters) + [{:keys [size]}] + (let [components-v2 (mf/use-ctx ctx/components-v2) + read-only? (mf/use-ctx ctx/workspace-read-only?) + filters* (mf/use-state + {:term "" + :section "all" + :ordering (dwa/get-current-assets-ordering) + :list-style (dwa/get-current-assets-list-style) + :open-menu false}) + filters (deref filters*) + term (:term filters) + list-style (:list-style filters) + menu-open? (:open-menu filters) + section (:section filters) + ordering (:ordering filters) + reverse-sort? (= :desc ordering) toggle-ordering (mf/use-fn - (mf/deps ordering) - (fn [] - (let [new-value (toggle-values ordering [:asc :desc])] - (swap! filters* assoc :ordering new-value) - (dwa/set-current-assets-ordering! new-value)))) + (mf/deps ordering) + (fn [] + (let [new-value (toggle-values ordering [:asc :desc])] + (swap! filters* assoc :ordering new-value) + (dwa/set-current-assets-ordering! new-value)))) toggle-list-style (mf/use-fn - (mf/deps list-style) - (fn [] - (let [new-value (toggle-values list-style [:thumbs :list])] - (swap! filters* assoc :list-style new-value) - (dwa/set-current-assets-list-style! new-value)))) + (mf/deps list-style) + (fn [] + (let [new-value (toggle-values list-style [:thumbs :list])] + (swap! filters* assoc :list-style new-value) + (dwa/set-current-assets-list-style! new-value)))) on-search-term-change (mf/use-fn (fn [event] - (let [value (dom/get-target-val event)] - (swap! filters* assoc :term value)))) - - on-search-clear-click - (mf/use-fn #(swap! filters* assoc :term "")) + (swap! filters* assoc :term event))) on-section-filter-change (mf/use-fn (fn [event] - (let [value (-> (dom/get-target event) - (dom/get-value) - (d/read-string))] - (swap! filters* assoc :section value)))) - - handle-key-down - (mf/use-fn - (fn [event] - (let [enter? (kbd/enter? event) - esc? (kbd/esc? event) - node (dom/event->target event)] - - (when ^boolean enter? (dom/blur! node)) - (when ^boolean esc? (dom/blur! node))))) + (let [value (or (-> (dom/get-target event) + (dom/get-value)) + (as-> (dom/get-current-target event) $ + (dom/get-attribute $ "data-test")))] + (swap! filters* assoc :section value :open-menu false)))) show-libraries-dialog - (mf/use-fn #(modal/show! :libraries-dialog {}))] + (mf/use-fn + (fn [] + (modal/show! :libraries-dialog {}) + (modal/allow-click-outside!))) - [:div.assets-bar - [:div.tool-window - [:div.tool-window-content - [:div.assets-bar-title - (tr "workspace.assets.assets") + on-open-menu + (mf/use-fn #(swap! filters* update :open-menu not)) - (when-not ^boolean read-only? - [:div.libraries-button {:on-click show-libraries-dialog} - i/text-align-justify - (tr "workspace.assets.libraries")])] + on-menu-close + (mf/use-fn #(swap! filters* assoc :open-menu false)) - [:div.search-block - [:input.search-input - {:placeholder (tr "workspace.assets.search") - :type "text" - :value term - :on-change on-search-term-change - :on-key-down handle-key-down}] + options (into [] (remove nil? + [{:option-name (tr "workspace.assets.box-filter-all") + :id "section-all" + :option-handler on-section-filter-change + :data-test "all"} - (if ^boolean (str/empty? term) - [:div.search-icon - i/search] - [:div.search-icon.close - {:on-click on-search-clear-click} - i/close])] + {:option-name (tr "workspace.assets.components") + :id "section-components" + :option-handler on-section-filter-change + :data-test "components"} - [:select.input-select {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :value (:section filters) - :on-change on-section-filter-change} - [:option {:value ":all"} (tr "workspace.assets.box-filter-all")] - [:option {:value ":components"} (tr "workspace.assets.components")] - (when-not components-v2 - [:option {:value ":graphics"} (tr "workspace.assets.graphics")]) - [:option {:value ":colors"} (tr "workspace.assets.colors")] - [:option {:value ":typographies"} (tr "workspace.assets.typography")]]]] + (when (not components-v2) + {:option-name (tr "workspace.assets.graphics") + :id "section-graphics" + :option-handler on-section-filter-change + :data-test "graphics"}) - [:& (mf/provider ctx:filters) {:value filters} - [:& (mf/provider ctx:toggle-ordering) {:value toggle-ordering} - [:& (mf/provider ctx:toggle-list-style) {:value toggle-list-style} - [:div.libraries-wrapper + {:option-name (tr "workspace.assets.colors") + :id "section-color" + :option-handler on-section-filter-change + :data-test "colors"} + + {:option-name (tr "workspace.assets.typography") + :id "section-typography" + :option-handler on-section-filter-change + :data-test "typographies"}]))] + + [:article {:class (stl/css :assets-bar)} + [:div {:class (stl/css :assets-header)} + (when-not ^boolean read-only? + [:button {:class (stl/css :libraries-button) + :on-click show-libraries-dialog} + [:span {:class (stl/css :libraries-icon)} + i/library] + (tr "workspace.assets.libraries")]) + + [:div {:class (stl/css :search-wrapper)} + [:& search-bar {:on-change on-search-term-change + :value term + :placeholder (tr "workspace.assets.search")} + [:button + {:on-click on-open-menu + :title (tr "workspace.assets.filter") + :class (stl/css-case :section-button true + :opened menu-open?)} + i/filter-icon]] + [:& context-menu-a11y + {:on-close on-menu-close + :selectable true + :selected section + :show menu-open? + :fixed? true + :min-width? true + :width size + :top 158 + :left 18 + :options options + :workspace? true}] + [:button {:class (stl/css :sort-button) + :title (tr "workspace.assets.sort") + :on-click toggle-ordering} + (if reverse-sort? + i/asc-sort + i/desc-sort)]]] + + [:& (mf/provider cmm/assets-filters) {:value filters} + [:& (mf/provider cmm/assets-toggle-ordering) {:value toggle-ordering} + [:& (mf/provider cmm/assets-toggle-list-style) {:value toggle-list-style} + [:* [:& assets-local-library {:filters filters}] [:& assets-libraries {:filters filters}]]]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.scss b/frontend/src/app/main/ui/workspace/sidebar/assets.scss new file mode 100644 index 0000000000..f72363a23c --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.scss @@ -0,0 +1,146 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.assets-bar { + display: grid; + height: 100%; + grid-auto-rows: max-content; + // TODO: ugly hack :( Fix this! we shouldn't be hardcoding this height + max-height: calc(100vh - $s-80); + scrollbar-gutter: stable; + overflow-y: auto; + padding-top: $s-8; +} + +.libraries-button { + @extend .button-secondary; + @include uppercaseTitleTipography; + gap: $s-2; + height: $s-32; + width: 100%; + margin-bottom: $s-4; + border-radius: $s-8; + .libraries-icon { + @include flexCenter; + width: $s-24; + height: 100%; + svg { + @include flexCenter; + @extend .button-icon; + stroke: var(--icon-foreground); + } + } + &:hover { + background-color: var(--button-secondary-background-color-hover); + color: var(--button-secondary-foreground-color-hover); + border: $s-1 solid var(--button-secondary-border-color-hover); + svg { + stroke: var(--button-secondary-foreground-color-hover); + } + } + &:focus { + background-color: var(--button-secondary-background-color-focus); + color: var(--button-secondary-foreground-color-focus); + border: $s-1 solid var(--button-secondary-border-color-focus); + svg { + stroke: var(--button-secondary-foreground-color-focus); + } + } +} + +.section-button { + @include flexCenter; + @include buttonStyle; + height: $s-32; + width: $s-32; + margin: 0; + border: $s-1 solid var(--input-border-color-rest); + border-radius: $br-8 $br-2 $br-2 $br-8; + background-color: var(--input-background-color-rest); + svg { + height: $s-16; + width: $s-16; + stroke: var(--icon-foreground); + } + &:focus { + border: $s-1 solid var(--input-border-color-focus); + outline: 0; + background-color: var(--input-background-color-focus); + color: var(--input-foreground-color-focus); + svg { + background-color: var(--input-background-color-focus); + } + } + &:hover { + border: $s-1 solid var(--input-border-color-hover); + background-color: var(--input-background-color-hover); + svg { + background-color: var(--input-background-color-hover); + stroke: var(--button-foreground-hover); + } + &:focus { + border: $s-1 solid var(--input-border-color-focus); + outline: 0; + background-color: var(--input-background-color-focus); + color: var(--input-foreground-color-focus); + svg { + background-color: var(--input-background-color-focus); + } + } + } + + &.opened { + @extend .button-icon-selected; + } +} + +.sections-container { + @include menuShadow; + @include flexColumn; + position: absolute; + top: $s-84; + left: $s-12; + width: $s-192; + padding: $s-4; + border-radius: $br-8; + background-color: var(--menu-background-color); + z-index: $z-index-2; +} + +.section-item { + @include bodySmallTypography; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: $s-6; + border-radius: $br-8; +} + +.section-btn { + @include buttonStyle; +} + +.assets-header { + padding: 0 0 $s-24 $s-12; +} + +.search-wrapper { + display: flex; + gap: $s-4; +} + +.sort-button { + @extend .button-tertiary; + width: $s-32; + border-radius: $br-8; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs new file mode 100644 index 0000000000..02c3fe55be --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs @@ -0,0 +1,510 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.sidebar.assets.colors + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.main.data.events :as ev] + [app.main.data.modal :as modal] + [app.main.data.workspace :as dw] + [app.main.data.workspace.colors :as dc] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.undo :as dwu] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.color-bullet :as cb] + [app.main.ui.context :as ctx] + [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.assets.common :as cmm] + [app.main.ui.workspace.sidebar.assets.groups :as grp] + [app.util.color :as uc] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] + [cuerdas.core :as str] + [okulary.core :as l] + [potok.v2.core :as ptk] + [rumext.v2 :as mf])) + +(mf/defc color-item + {::mf/wrap-props false} + [{:keys [color local? file-id selected multi-colors? multi-assets? + on-asset-click on-assets-delete on-clear-selection on-group + selected-full selected-paths move-color]}] + + (let [color (mf/with-memo [color file-id] + (cond-> color + (:value color) (assoc :color (:value color) :opacity 1) + (:value color) (dissoc :value) + true (assoc :file-id file-id))) + + color-id (:id color) + + item-ref (mf/use-ref) + dragging* (mf/use-state false) + dragging? (deref dragging*) + + rename? (= (:color-for-rename @refs/workspace-local) color-id) + input-ref (mf/use-ref) + + editing* (mf/use-state rename?) + editing? (deref editing*) + + menu-state (mf/use-state cmm/initial-context-menu-state) + read-only? (mf/use-ctx ctx/workspace-read-only?) + + default-name (cond + (:gradient color) (uc/gradient-type->string (dm/get-in color [:gradient :type])) + (:color color) (:color color) + :else (:value color)) + + apply-color + (mf/use-fn + (mf/deps color) + (fn [event] + (st/emit! (dc/apply-color-from-palette (merge uc/empty-color color) (kbd/alt? event))))) + + rename-color + (mf/use-fn + (mf/deps file-id color-id) + (fn [name] + (st/emit! (dwl/rename-color file-id color-id name)))) + + edit-color + (mf/use-fn + (mf/deps color file-id) + (fn [attrs] + (let [name (cfh/merge-path-item (:path color) (:name color)) + color (-> attrs + (assoc :id (:id color)) + (assoc :file-id file-id) + (assoc :name name))] + (st/emit! (dwl/update-color color file-id))))) + + delete-color + (mf/use-fn + (mf/deps multi-colors? multi-assets? file-id color-id) + (fn [] + (if (or multi-colors? multi-assets?) + (on-assets-delete) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id) + (dwl/delete-color color) + (dwl/sync-file file-id file-id :colors color-id) + (dwu/commit-undo-transaction undo-id)))))) + + rename-color-clicked + (mf/use-fn + (mf/deps read-only? local?) + (fn [event] + (when (and local? (not read-only?)) + (dom/prevent-default event) + (reset! editing* true)))) + + input-blur + (mf/use-fn + (mf/deps rename-color) + (fn [event] + (let [name (dom/get-target-val event)] + (rename-color name) + (st/emit! dwl/clear-color-for-rename) + (reset! editing* false)))) + + input-key-down + (mf/use-fn + (mf/deps input-blur) + (fn [event] + (when (kbd/esc? event) + (st/emit! dwl/clear-color-for-rename) + (reset! editing* false)) + (when (kbd/enter? event) + (input-blur event)))) + + edit-color-clicked + (mf/use-fn + (mf/deps edit-color color) + (fn [event] + (modal/show! :colorpicker + {:x (.-clientX ^js event) + :y (.-clientY ^js event) + :on-accept edit-color + :data color + :position :right}))) + + on-context-menu + (mf/use-fn + (mf/deps color-id selected on-clear-selection read-only?) + (fn [event] + (dom/prevent-default event) + (let [pos (dom/get-client-position event)] + (when (and local? (not read-only?)) + (when-not (contains? selected color-id) + (on-clear-selection)) + (swap! menu-state cmm/open-context-menu pos))))) + + on-close-menu + (mf/use-fn + (fn [] + (swap! menu-state cmm/close-context-menu))) + + on-drop + (mf/use-fn + (mf/deps color dragging* selected selected-full selected-paths move-color) + (fn [event] + (cmm/on-drop-asset event color dragging* selected selected-full + selected-paths move-color))) + + on-drag-enter + (mf/use-fn + (mf/deps color dragging* selected selected-paths) + (fn [event] + (cmm/on-drag-enter-asset event color dragging* selected selected-paths))) + + on-drag-leave + (mf/use-fn + (mf/deps dragging*) + (fn [event] + (cmm/on-drag-leave-asset event dragging*))) + + on-color-drag-start + (mf/use-fn + (mf/deps color file-id selected item-ref read-only?) + (fn [event] + (if read-only? + (dom/prevent-default event) + (cmm/on-asset-drag-start event file-id color selected item-ref :colors identity)))) + + on-click + (mf/use-fn + (mf/deps color-id apply-color on-asset-click) + (partial on-asset-click color-id apply-color))] + + (mf/with-effect [editing?] + (when editing? + (let [input (mf/ref-val input-ref)] + (dom/select-text! input) + nil))) + + [:div {:class (stl/css-case :asset-list-item true + :selected (contains? selected (:id color)) + :editing editing?) + :style #js {"--bullet-size" "16px"} + :on-context-menu on-context-menu + :on-click (when-not editing? on-click) + :ref item-ref + :draggable (and (not read-only?) (not editing?)) + :on-drag-start on-color-drag-start + :on-drag-enter on-drag-enter + :on-drag-leave on-drag-leave + :on-drag-over dom/prevent-default + :on-drop on-drop} + + [:div {:class (stl/css :bullet-block)} + [:& cb/color-bullet {:color color + :mini? true}]] + + (if ^boolean editing? + [:input + {:type "text" + :class (stl/css :element-name) + :ref input-ref + :on-blur input-blur + :on-key-down input-key-down + :auto-focus true + :default-value (cfh/merge-path-item (:path color) (:name color))}] + + [:div {:title (if (= (:name color) default-name) + default-name + (dm/str (:name color) " (" default-name ")")) + :class (stl/css :name-block) + :on-double-click rename-color-clicked} + + (if (= (:name color) default-name) + [:span {:class (stl/css :default-name)} default-name] + [:* + (:name color) + [:span {:class (stl/css :default-name :default-name-with-color)} default-name]])]) + + (when local? + [:& cmm/assets-context-menu + {:on-close on-close-menu + :state @menu-state + :options [(when-not (or multi-colors? multi-assets?) + {:option-name (tr "workspace.assets.rename") + :id "assets-rename-color" + :option-handler rename-color-clicked}) + (when-not (or multi-colors? multi-assets?) + {:option-name (tr "workspace.assets.edit") + :id "assets-edit-color" + :option-handler edit-color-clicked}) + + {:option-name (tr "workspace.assets.delete") + :id "assets-delete-color" + :option-handler delete-color} + (when-not multi-assets? + {:option-name (tr "workspace.assets.group") + :id "assets-group-color" + :option-handler (on-group (:id color))})]}]) + + (when ^boolean dragging? + [:div {:class (stl/css :dragging)}])])) + +(mf/defc colors-group + [{:keys [file-id prefix groups open-groups force-open? local? selected + multi-colors? multi-assets? on-asset-click on-assets-delete + on-clear-selection on-group on-rename-group on-ungroup colors + selected-full]}] + (let [group-open? (or ^boolean force-open? + ^boolean (get open-groups prefix (if (= prefix "") true false))) + dragging* (mf/use-state false) + dragging? (deref dragging*) + + selected-paths (mf/with-memo [selected-full] + (into #{} + (comp (map :path) (d/nilv "")) + selected-full)) + + move-color + (mf/use-fn (mf/deps file-id) (partial dwl/rename-color file-id)) + + on-drag-enter + (mf/use-fn + (mf/deps dragging* prefix selected-paths) + (fn [event] + (cmm/on-drag-enter-asset-group event dragging* prefix selected-paths))) + + on-drag-leave + (mf/use-fn + (mf/deps dragging*) + (fn [event] + (cmm/on-drag-leave-asset event dragging*))) + + on-drop + (mf/use-fn + (mf/deps dragging* prefix selected-paths selected-full move-color) + (fn [event] + (cmm/on-drop-asset-group event dragging* prefix selected-paths selected-full move-color)))] + + [:div {:class (stl/css :colors-group) + :on-drag-enter on-drag-enter + :on-drag-leave on-drag-leave + :on-drag-over dom/prevent-default + :on-drop on-drop} + [:& grp/asset-group-title {:file-id file-id + :section :colors + :path prefix + :group-open? group-open? + :on-rename on-rename-group + :on-ungroup on-ungroup}] + (when group-open? + [:* + (let [colors (get groups "" [])] + [:div {:class (stl/css :asset-list) + :on-drag-enter on-drag-enter + :on-drag-leave on-drag-leave + :on-drag-over dom/prevent-default + :on-drop on-drop} + + (when ^boolean dragging? + [:div {:class (stl/css :grid-placeholder)} + "\u00A0"]) + + (when (and (empty? colors) + (some? groups)) + [:div {:class (stl/css :drop-space)}]) + + (for [color colors] + [:& color-item {:key (dm/str (:id color)) + :color color + :file-id file-id + :local? local? + :selected selected + :multi-colors? multi-colors? + :multi-assets? multi-assets? + :on-asset-click on-asset-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection + :on-group on-group + :colors colors + :selected-full selected-full + :selected-paths selected-paths + :move-color move-color}])]) + + (for [[path-item content] groups] + (when-not (empty? path-item) + [:& colors-group {:file-id file-id + :prefix (cfh/merge-path-item prefix path-item) + :key (dm/str "group-" path-item) + :groups content + :open-groups open-groups + :force-open? force-open? + :local? local? + :selected selected + :multi-colors? multi-colors? + :multi-assets? multi-assets? + :on-asset-click on-asset-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection + :on-group on-group + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :colors colors + :selected-full selected-full}]))])])) + +(mf/defc colors-section + [{:keys [file-id local? colors open? force-open? open-status-ref selected reverse-sort? + on-asset-click on-assets-delete on-clear-selection] :as props}] + + (let [selected (:colors selected) + selected-full (mf/with-memo [selected colors] + (into #{} (filter #(contains? selected (:id %))) colors)) + + open-groups-ref (mf/with-memo [open-status-ref] + (-> (l/in [:groups :colors]) + (l/derived open-status-ref))) + open-groups (mf/deref open-groups-ref) + + multi-colors? (> (count selected) 1) + multi-assets? (or (seq (:components selected)) + (seq (:graphics selected)) + (seq (:typographies selected))) + + groups (mf/with-memo [colors reverse-sort?] + (grp/group-assets colors reverse-sort?)) + + read-only? (mf/use-ctx ctx/workspace-read-only?) + + add-color + (mf/use-fn + (fn [value _] + (st/emit! (dwl/add-color value)))) + + add-color-clicked + (mf/use-fn + (mf/deps file-id) + (fn [event] + (let [bounds (-> event + (dom/get-current-target) + (dom/get-bounding-rect)) + x-position (:right bounds) + y-position (:top bounds)] + + (st/emit! (dw/set-assets-section-open file-id :colors true) + (ptk/event ::ev/event {::ev/name "add-asset-to-library" + :asset-type "color"}) + (modal/show :colorpicker + {:x x-position + :y y-position + :on-accept add-color + :data {:color "#406280" + :opacity 1} + :position :right}))))) + + create-group + (mf/use-fn + (mf/deps colors selected on-clear-selection file-id) + (fn [color-id] + (fn [group-name] + (on-clear-selection) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! + (->> colors + (filter #(if multi-colors? + (contains? selected (:id %)) + (= color-id (:id %)))) + (map #(dwl/update-color + (assoc % :name + (cmm/add-group % group-name)) + file-id)))) + (st/emit! (dwu/commit-undo-transaction undo-id)))))) + + rename-group + (mf/use-fn + (mf/deps colors) + (fn [path last-path] + (on-clear-selection) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! + (->> colors + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/update-color + (assoc % :name + (cmm/rename-group % path last-path)) + file-id)))) + (st/emit! (dwu/commit-undo-transaction undo-id))))) + + on-group + (mf/use-fn + (mf/deps colors selected) + (fn [color-id] + (fn [event] + (dom/stop-propagation event) + (modal/show! :name-group-dialog {:accept (create-group color-id)})))) + + on-rename-group + (mf/use-fn + (mf/deps colors) + (fn [event path last-path] + (dom/stop-propagation event) + (modal/show! :name-group-dialog {:path path + :last-path last-path + :accept rename-group}))) + on-ungroup + (mf/use-fn + (mf/deps colors) + (fn [path] + (on-clear-selection) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (apply st/emit! + (->> colors + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/update-color + (assoc % :name + (cmm/ungroup % path)) + file-id)))) + (st/emit! (dwu/commit-undo-transaction undo-id))))) + + on-asset-click + (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))] + + + [:& cmm/asset-section {:file-id file-id + :title (tr "workspace.assets.colors") + :section :colors + :assets-count (count colors) + :open? open?} + (when local? + [:& cmm/asset-section-block {:role :title-button} + (when-not read-only? + [:button {:class (stl/css :assets-btn) + :on-click add-color-clicked} + i/add])]) + + + [:& cmm/asset-section-block {:role :content} + [:& colors-group {:file-id file-id + :prefix "" + :groups groups + :open-groups open-groups + :force-open? force-open? + :local? local? + :selected selected + :multi-colors? multi-colors? + :multi-assets? multi-assets? + :on-asset-click on-asset-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection + :on-group on-group + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :colors colors + :selected-full selected-full}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss new file mode 100644 index 0000000000..ae7193502a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss @@ -0,0 +1,115 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +// TODO: we should be using subgrid in the common "assets component" to avoid +// using this SCSS variable here (we cannot use a CSS var in this CSS module because +// the elements are not part of the same cascade). +$assets-button-width: $s-28; + +.assets-btn { + @extend .button-tertiary; + height: $s-32; + width: $assets-button-width; + padding: 0; + border-radius: $br-8; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.colors-group { + margin-top: $s-4; +} + +.asset-list { + padding: 0 0 0 $s-4; +} + +.asset-list-item { + position: relative; + display: grid; + grid-template-columns: auto 1fr #{$assets-button-width}; + align-items: center; + height: $s-32; + padding: $s-8; + padding-inline-end: 0; + margin-bottom: $s-4; + border-radius: $br-8; + background-color: var(--assets-item-background-color); + cursor: pointer; + + &.selected { + border: $s-1 solid var(--assets-item-border-color); + } + + &.editing { + border: $s-1 solid var(--input-border-color-focus); + input.element-name { + @include textEllipsis; + @include bodySmallTypography; + @include removeInputStyle; + flex-grow: 1; + max-width: calc(var(--parent-size) - (var(--depth) * var(--layer-indentation-size))); + margin: 0; + color: var(--layer-row-foreground-color); + } + } + &:hover { + background-color: var(--assets-item-background-color-hover); + } +} + +.bullet-block { + @include flexCenter; + height: 100%; + justify-content: flex-start; + margin-inline-end: $s-4; +} + +.name-block { + @include bodySmallTypography; + @include textEllipsis; + margin: 0; + color: var(--assets-item-name-foreground-color); +} + +.default-name { + margin-inline-start: $s-4; + color: var(--assets-item-name-foreground-color-rest); +} + +.default-name-with-color { + margin-left: $s-6; +} + +.element-name { + @include textEllipsis; + color: var(--color-foreground-primary); +} + +.grid-placeholder { + height: $s-2; + margin-bottom: $s-2; + background-color: var(--color-accent-primary); +} + +.drop-space { + height: $s-12; +} + +.dragging { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: $s-8; + background-color: var(--assets-item-background-color-drag); + border: $s-2 solid var(--assets-item-border-color-drag); +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs new file mode 100644 index 0000000000..d99e9dcac2 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs @@ -0,0 +1,444 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + + +(ns app.main.ui.workspace.sidebar.assets.common + (:require-macros [app.main.style :as stl]) + (:require + [app.common.files.helpers :as cfh] + [app.common.spec :as us] + [app.common.thumbnails :as thc] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.main.data.modal :as modal] + [app.main.data.workspace :as dw] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.undo :as dwu] + [app.main.refs :as refs] + [app.main.render :refer [component-svg component-svg-thumbnail]] + [app.main.store :as st] + [app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]] + [app.main.ui.components.title-bar :refer [title-bar]] + [app.main.ui.context :as ctx] + [app.main.ui.icons :as i] + [app.util.array :as array] + [app.util.dom :as dom] + [app.util.dom.dnd :as dnd] + [app.util.i18n :as i18n :refer [tr c]] + [app.util.strings :refer [matches-search]] + [app.util.timers :as ts] + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(def assets-filters (mf/create-context nil)) +(def assets-toggle-ordering (mf/create-context nil)) +(def assets-toggle-list-style (mf/create-context nil)) + +(defn apply-filters + [coll {:keys [ordering term] :as filters}] + (let [reverse? (= :desc ordering)] + (cond->> coll + (not ^boolean (str/empty? term)) + (filter (fn [item] + (or (matches-search (:name item "!$!") term) + (matches-search (:path item "!$!") term) + (matches-search (:value item "!$!") term)))) + + ;; Sort by folder order, but putting all "root" items always + ;; first, independently of sort order. + :always + (sort-by (fn [{:keys [path name] :as item}] + (let [path (if (str/empty? path) + (if reverse? "z" "a") + path)] + (str/lower (cfh/merge-path-item path name)))) + (if ^boolean reverse? > <))))) + +(defn add-group + [asset group-name] + (-> (:path asset) + (cfh/merge-path-item group-name) + (cfh/merge-path-item (:name asset)))) + +(defn rename-group + [asset path last-path] + (-> (:path asset) + (str/slice 0 (count path)) + (cfh/split-path) + butlast + (vec) + (conj last-path) + (cfh/join-path) + (str (str/slice (:path asset) (count path))) + (cfh/merge-path-item (:name asset)))) + +(defn ungroup + [asset path] + (-> (:path asset) + (str/slice 0 (count path)) + (cfh/split-path) + butlast + (cfh/join-path) + (str (str/slice (:path asset) (count path))) + (cfh/merge-path-item (:name asset)))) + +(s/def ::asset-name ::us/not-empty-string) +(s/def ::name-group-form + (s/keys :req-un [::asset-name])) + +(def initial-context-menu-state + {:open? false :top nil :left nil}) + +(defn open-context-menu + [state pos] + (let [top (:y pos) + left (+ (:x pos) 10)] + (assoc state + :open? true + :top top + :left left))) + +(defn close-context-menu + [state] + (assoc state :open? false)) + +(mf/defc assets-context-menu + {::mf/wrap-props false} + [{:keys [options state on-close]}] + [:& context-menu-a11y + {:show (:open? state) + :fixed? (or (not= (:top state) 0) (not= (:left state) 0)) + :on-close on-close + :top (:top state) + :left (:left state) + :options options + :workspace? true}]) + +(mf/defc section-icon + {::mf/wrap-props false} + [{:keys [section]}] + (case section + :colors i/drop-icon + :components i/component + :typographies i/text-palette + i/add)) + +(mf/defc asset-section + {::mf/wrap-props false} + [{:keys [children file-id title section assets-count open?]}] + (let [children (-> (array/normalize-to-array children) + (array/without-nils)) + + is-button? #(= :title-button (.. % -props -role)) + is-content? #(= :content (.. % -props -role)) + + buttons (array/filter is-button? children) + content (array/filter is-content? children) + + on-collapsed + (mf/use-fn + (mf/deps file-id section open? assets-count) + (fn [_] + (when (< 0 assets-count) + (st/emit! (dw/set-assets-section-open file-id section (not open?)))))) + + title + (mf/html + [:span {:class (stl/css :title-name)} + [:span {:class (stl/css :section-icon)} + [:& section-icon {:section section}]] + [:span {:class (stl/css :section-name)} + title] + + [:span {:class (stl/css :num-assets)} + assets-count]])] + + [:div {:class (stl/css-case :asset-section true + :opened (and (< 0 assets-count) + open?))} + [:& title-bar + {:collapsable (< 0 assets-count) + :collapsed (not open?) + :all-clickable true + :on-collapsed on-collapsed + :add-icon-gap (= 0 assets-count) + :class (stl/css-case :title-spacing open?) + :title title} + buttons] + (when ^boolean open? content)])) + +(mf/defc asset-section-block + {::mf/wrap-props false} + [{:keys [children]}] + [:* children]) + +(defn create-assets-group + [rename components-to-group group-name] + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (->> components-to-group + (map #(rename + (:id %) + (add-group % group-name))) + (run! st/emit!)) + (st/emit! (dwu/commit-undo-transaction undo-id)))) + +(defn on-drop-asset + [event asset dragging* selected selected-full selected-paths rename] + (let [create-typed-assets-group (partial create-assets-group rename)] + (when (not (dnd/from-child? event)) + (reset! dragging* false) + (when + (and (not (contains? selected (:id asset))) + (every? #(= % (:path asset)) selected-paths)) + (let [components-to-group (conj selected-full asset) + create-typed-assets-group (partial create-typed-assets-group components-to-group)] + (modal/show! :name-group-dialog {:accept create-typed-assets-group})))))) + +(defn on-drag-enter-asset + [event asset dragging* selected selected-paths] + (when (and + (not (dnd/from-child? event)) + (every? #(= % (:path asset)) selected-paths) + (not (contains? selected (:id asset)))) + (reset! dragging* true))) + +(defn on-drag-leave-asset + [event dragging*] + (when (not (dnd/from-child? event)) + (reset! dragging* false))) + +(defn create-counter-element + [asset-count] + (let [counter-el (dom/create-element "div")] + (dom/set-property! counter-el "class" (stl/css :drag-counter)) + (dom/set-text! counter-el (tr "workspace.assets.sidebar.components" (c asset-count))) + counter-el)) + +(defn set-drag-image + [event item-ref num-selected] + (let [offset (dom/get-offset-position (.-nativeEvent event)) + item-el (mf/ref-val item-ref) + counter-el (create-counter-element num-selected)] + + ;; set-drag-image requires that the element is rendered and + ;; visible to the user at the moment of creating the ghost + ;; image (to make a snapshot), but you may remove it right + ;; afterwards, in the next render cycle. + (dom/append-child! item-el counter-el) + (dnd/set-drag-image! event item-el (:x offset) (:y offset)) + (ts/raf #(.removeChild ^js item-el counter-el)))) + +(defn on-asset-drag-start + [event file-id asset selected item-ref asset-type on-drag-start] + (let [id-asset (:id asset) + num-selected (if (contains? selected id-asset) + (count selected) + 1)] + (when (not (contains? selected id-asset)) + (st/emit! (dw/unselect-all-assets file-id) + (dw/toggle-selected-assets file-id id-asset asset-type))) + (on-drag-start asset event) + (when (> num-selected 1) + (set-drag-image event item-ref num-selected)))) + +(defn on-drag-enter-asset-group + [event dragging* prefix selected-paths] + (dom/stop-propagation event) + (when (and (not (dnd/from-child? event)) + (not (every? #(= % prefix) selected-paths))) + (reset! dragging* true))) + +(defn on-drop-asset-group + [event dragging* prefix selected-paths selected-full rename] + (dom/stop-propagation event) + (when (not (dnd/from-child? event)) + (reset! dragging* false) + (when (not (every? #(= % prefix) selected-paths)) + (doseq [target-asset selected-full] + (st/emit! + (rename + (:id target-asset) + (cfh/merge-path-item prefix (:name target-asset)))))))) + +(mf/defc component-item-thumbnail + "Component that renders the thumbnail image or the original SVG." + {::mf/wrap-props false} + [{:keys [file-id root-shape component container class]}] + (let [page-id (:main-instance-page component) + root-id (:main-instance-id component) + + retry (mf/use-state 0) + + thumbnail-uri* (mf/with-memo [file-id page-id root-id] + (let [object-id (thc/fmt-object-id file-id page-id root-id "component")] + (refs/workspace-thumbnail-by-id object-id))) + thumbnail-uri (mf/deref thumbnail-uri*) + + on-error + (mf/use-fn + (mf/deps @retry) + (fn [] + (when (< @retry 3) + (inc retry))))] + + (if (some? thumbnail-uri) + [:& component-svg-thumbnail + {:thumbnail-uri thumbnail-uri + :class class + :on-error on-error + :root-shape root-shape + :objects (:objects container) + :show-grids? true}] + + [:& component-svg + {:root-shape root-shape + :class class + :objects (:objects container) + :show-grids? true}]))) + +(defn generate-components-menu-entries + [shapes components-v2] + (let [multi (> (count shapes) 1) + copies (filter ctk/in-component-copy? shapes) + + current-file-id (mf/use-ctx ctx/current-file-id) + objects (deref refs/workspace-page-objects) + workspace-data (deref refs/workspace-data) + workspace-libraries (deref refs/workspace-libraries) + current-file {:id current-file-id :data workspace-data} + + find-component (fn [shape include-deleted?] + (ctf/resolve-component + shape current-file workspace-libraries {:include-deleted? include-deleted?})) + + local-or-exists (fn [shape] + (let [library-id (:component-file shape)] + (or (= library-id current-file-id) + (some? (get workspace-libraries library-id))))) + + restorable-copies (->> copies + (filter #(nil? (find-component % false))) + (filter #(local-or-exists %))) + + touched-not-dangling (filter #(and (cfh/component-touched? objects (:id %)) + (find-component % false)) copies) + can-reset-overrides? (or (not components-v2) (seq touched-not-dangling)) + + + ;; For when it's only one shape + shape (first shapes) + id (:id shape) + main-instance? (if components-v2 (ctk/main-instance? shape) true) + + component-id (:component-id shape) + library-id (:component-file shape) + + local-component? (= library-id current-file-id) + component (find-component shape false) + lacks-annotation? (nil? (:annotation component)) + is-dangling? (nil? component) + + can-show-component? (and (not multi) + (not main-instance?) + (not is-dangling?)) + + can-update-main? (and (not multi) + (not is-dangling?) + (or (not components-v2) + (and (not main-instance?) + (not (ctn/has-any-copy-parent? objects shape)) + (cfh/component-touched? objects (:id shape))))) + + can-detach? (and (seq copies) + (every? #(not (ctn/has-any-copy-parent? objects %)) copies)) + + + do-detach-component + #(st/emit! (dwl/detach-components (map :id copies))) + + do-reset-component + #(st/emit! (dwl/reset-components (map :id touched-not-dangling))) + + do-update-component-sync + #(st/emit! (dwl/update-component-sync id library-id)) + + do-update-remote-component + (fn [] + (st/emit! (modal/show + {:type :confirm + :message "" + :title (tr "modals.update-remote-component.message") + :hint (tr "modals.update-remote-component.hint") + :cancel-label (tr "modals.update-remote-component.cancel") + :accept-label (tr "modals.update-remote-component.accept") + :accept-style :primary + :on-accept do-update-component-sync}))) + + do-update-component + #(if local-component? + (do-update-component-sync) + (do-update-remote-component)) + + do-show-in-assets + #(st/emit! (if components-v2 + (dw/show-component-in-assets component-id) + (dw/go-to-component component-id))) + + do-create-annotation + #(st/emit! (dw/set-annotations-id-for-create id)) + + do-show-local-component + #(st/emit! (dw/go-to-component component-id)) + + do-show-remote-component + #(let [comp (find-component shape true)] ;; When the show-remote is after a restore, the component may still be deleted + (when comp + (st/emit! (dwl/nav-to-component-file library-id comp)))) + + do-show-component + #(if local-component? + (do-show-local-component) + (do-show-remote-component)) + + do-restore-component + #(let [;; Extract a map of component-id -> component-file in order to avoid duplicates + comps-to-restore (reduce (fn [id-file-map {:keys [component-id component-file]}] + (assoc id-file-map component-id component-file)) + {} + restorable-copies)] + + (st/emit! (dwl/restore-components comps-to-restore)) + (when (= 1 (count comps-to-restore)) + (ts/schedule 1000 do-show-component))) + + menu-entries [(when (and (not multi) main-instance?) + {:msg "workspace.shape.menu.show-in-assets" + :action do-show-in-assets}) + (when (and (not multi) main-instance? local-component? lacks-annotation? components-v2) + {:msg "workspace.shape.menu.create-annotation" + :action do-create-annotation}) + (when can-detach? + {:msg (if (> (count copies) 1) + "workspace.shape.menu.detach-instances-in-bulk" + "workspace.shape.menu.detach-instance") + :action do-detach-component + :shortcut :detach-component}) + (when can-reset-overrides? + {:msg "workspace.shape.menu.reset-overrides" + :action do-reset-component}) + (when (and (seq restorable-copies) components-v2) + {:msg "workspace.shape.menu.restore-main" + :action do-restore-component}) + (when can-show-component? + {:msg "workspace.shape.menu.show-main" + :action do-show-component}) + (when can-update-main? + {:msg "workspace.shape.menu.update-main" + :action do-update-component})]] + (filter (complement nil?) menu-entries))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss new file mode 100644 index 0000000000..bbc0c7d701 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss @@ -0,0 +1,65 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.title-name { + @include uppercaseTitleTipography; + display: flex; + align-items: center; + flex-grow: 1; + width: 100%; +} + +.section-icon { + @include flexCenter; + padding-right: $s-2; + svg { + @include flexCenter; + height: $s-16; + width: $s-16; + fill: none; + stroke: currentColor; + } +} + +.section-name { + display: flex; + align-items: center; + margin: 0 $s-2; +} + +.num-assets { + @include flexCenter; + height: 100%; + padding-left: $s-8; +} + +.title-spacing { + margin-bottom: $s-4; +} + +.asset-section.opened { + margin-bottom: $s-12; +} + +.drag-counter { + @include bodySmallTypography; + @include textEllipsis; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: calc($s-24 - $s-2); + background-color: var(--assets-item-name-background-color); + color: var(--assets-item-name-foreground-color); + display: flex; + justify-content: flex-start; + align-items: center; + margin: $s-4; + padding-inline: $s-4; + z-index: 2; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs new file mode 100644 index 0000000000..72e01e609f --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs @@ -0,0 +1,584 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.sidebar.assets.components + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.media :as cm] + [app.common.types.file :as ctf] + [app.main.data.events :as ev] + [app.main.data.modal :as modal] + [app.main.data.workspace :as dw] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.media :as dwm] + [app.main.data.workspace.undo :as dwu] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.editable-label :refer [editable-label]] + [app.main.ui.components.file-uploader :refer [file-uploader]] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.context :as ctx] + [app.main.ui.hooks :as h] + [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.assets.common :as cmm] + [app.main.ui.workspace.sidebar.assets.groups :as grp] + [app.util.dom :as dom] + [app.util.dom.dnd :as dnd] + [app.util.i18n :as i18n :refer [tr]] + [cuerdas.core :as str] + [okulary.core :as l] + [potok.v2.core :as ptk] + [rumext.v2 :as mf])) + +(def drag-data* (atom {:local? false})) + +(defn set-drag-data! [data] + (reset! drag-data* data)) + +(defn- get-component-root-and-container + [file-id component components-v2] + (if (= file-id (:id @refs/workspace-file)) + (let [data @refs/workspace-data] + [(ctf/get-component-root data component) + (if components-v2 + (ctf/get-component-page data component) + component)]) + (let [data (dm/get-in @refs/workspace-libraries [file-id :data]) + root-shape (ctf/get-component-root data component) + container (if components-v2 + (ctf/get-component-page data component) + component)] + [root-shape container]))) + + + ;; NOTE: We don't schedule the thumbnail generation on idle right now + ;; until we can queue and handle thumbnail batching properly. +#_(mf/with-effect [] + (when-not (some? thumbnail-uri) + (tm/schedule-on-idle + #(st/emit! (dwl/update-component-thumbnail (:id component) file-id))))) + + +(mf/defc components-item + {::mf/wrap-props false} + [{:keys [component renaming listing-thumbs? selected + file-id on-asset-click on-context-menu on-drag-start do-rename + cancel-rename selected-full selected-paths local]}] + (let [item-ref (mf/use-ref) + + dragging* (mf/use-state false) + dragging? (deref dragging*) + + read-only? (mf/use-ctx ctx/workspace-read-only?) + components-v2 (mf/use-ctx ctx/components-v2) + component-id (:id component) + + visible? (h/use-visible item-ref :once? true) + + ;; NOTE: we don't use reactive deref for it because we don't + ;; really need rerender on any change on the file change. If + ;; the component changes, it will trigger rerender anyway. + [root-shape container] + (get-component-root-and-container file-id component components-v2) + + unselect-all + (mf/use-fn + (fn [] + (st/emit! (dw/unselect-all-assets)))) + + on-component-click + (mf/use-fn + (mf/deps component-id on-asset-click) + (fn [event] + (dom/stop-propagation event) + (on-asset-click component-id unselect-all event))) + + on-component-double-click + (mf/use-fn + (mf/deps file-id component local) + (fn [event] + (dom/stop-propagation event) + (if local + (st/emit! (dw/go-to-component component-id)) + (st/emit! (dwl/nav-to-component-file file-id component))))) + + on-drop + (mf/use-fn + (mf/deps component dragging* selected selected-full selected-paths local drag-data*) + (fn [event] + (when (and local (:local? @drag-data*)) + (cmm/on-drop-asset event component dragging* selected selected-full + selected-paths dwl/rename-component-and-main-instance)))) + + on-drag-enter + (mf/use-fn + (mf/deps component dragging* selected selected-paths local drag-data*) + (fn [event] + (when (and local (:local? @drag-data*)) + (cmm/on-drag-enter-asset event component dragging* selected selected-paths)))) + + on-drag-leave + (mf/use-fn + (mf/deps dragging* local drag-data*) + (fn [event] + (when (and local (:local? @drag-data*)) + (cmm/on-drag-leave-asset event dragging*)))) + + on-component-drag-start + (mf/use-fn + (mf/deps file-id component selected item-ref on-drag-start read-only? local) + (fn [event] + (if read-only? + (dom/prevent-default event) + (cmm/on-asset-drag-start event file-id component selected item-ref :components on-drag-start)))) + + on-context-menu + (mf/use-fn + (mf/deps on-context-menu component-id) + (partial on-context-menu component-id)) + + renaming? (= renaming (:id component))] + + [:div {:ref item-ref + :class (stl/css-case :selected (contains? selected (:id component)) + :grid-cell listing-thumbs? + :enum-item (not listing-thumbs?)) + :id (dm/str "component-shape-id-" (:id component)) + :draggable (and (not read-only?) (not renaming?)) + :on-click on-component-click + :on-double-click on-component-double-click + :on-context-menu on-context-menu + :on-drag-start on-component-drag-start + :on-drag-enter on-drag-enter + :on-drag-leave on-drag-leave + :on-drag-over dom/prevent-default + :on-drop on-drop} + (when (and (some? root-shape) + (some? container)) + [:* + [:* + [:& editable-label + {:class (stl/css-case :cell-name listing-thumbs? + :item-name (not listing-thumbs?) + :editing renaming?) + :value (cfh/merge-path-item (:path component) (:name component)) + :tooltip (cfh/merge-path-item (:path component) (:name component)) + :display-value (:name component) + :editing renaming? + :disable-dbl-click true + :on-change do-rename + :on-cancel cancel-rename}] + + (when ^boolean dragging? + [:div {:class (stl/css :dragging)}])] + + (when visible? + [:& cmm/component-item-thumbnail {:file-id file-id + :class (stl/css-case :thumbnail true + :asset-list-thumbnail (not listing-thumbs?)) + :root-shape root-shape + :component component + :container container}])])])) + +(mf/defc components-group + {::mf/wrap-props false} + [{:keys [file-id prefix groups open-groups force-open? renaming listing-thumbs? selected on-asset-click + on-drag-start do-rename cancel-rename on-rename-group on-group on-ungroup on-context-menu + selected-full local]}] + + (let [group-open? (or ^boolean force-open? + ^boolean (get open-groups prefix (if (= prefix "") true false))) + dragging* (mf/use-state false) + dragging? (deref dragging*) + + + selected-paths (mf/with-memo [selected-full] + (into #{} + (comp (map :path) (d/nilv "")) + selected-full)) + on-drag-enter + (mf/use-fn + (mf/deps dragging* prefix selected-paths local drag-data*) + (fn [event] + (when (and local (:local? @drag-data*)) + (cmm/on-drag-enter-asset-group event dragging* prefix selected-paths)))) + + on-drag-leave + (mf/use-fn + (mf/deps dragging* local drag-data*) + (fn [event] + (when (and local (:local? @drag-data*)) + (cmm/on-drag-leave-asset event dragging*)))) + + on-drop + (mf/use-fn + (mf/deps dragging* prefix selected-paths selected-full local drag-data*) + (fn [event] + (when (and local (:local? @drag-data*)) + (cmm/on-drop-asset-group event dragging* prefix selected-paths selected-full dwl/rename-component-and-main-instance))))] + + [:div {:class (stl/css :component-group) + :on-drag-enter on-drag-enter + :on-drag-leave on-drag-leave + :on-drag-over dom/prevent-default + :on-drop on-drop} + [:& grp/asset-group-title + {:file-id file-id + :section :components + :path prefix + :group-open? group-open? + :on-rename on-rename-group + :on-ungroup on-ungroup}] + + + (when group-open? + [:* + (when-let [components (not-empty (get groups "" []))] + [:div {:class-name (stl/css-case :asset-grid listing-thumbs? + :asset-enum (not listing-thumbs?) + :drop-space (and + (empty? components) + (some? groups) + (not dragging?) + local)) + :on-drag-enter on-drag-enter + :on-drag-leave on-drag-leave + :on-drag-over dom/prevent-default + :on-drop on-drop} + + (when ^boolean dragging? + [:div {:class (stl/css :grid-placeholder)} "\u00A0"]) + + + (when (and (empty? components) + (some? groups) + local) + [:div {:class (stl/css :drop-space)}]) + + (for [component components] + [:& components-item + {:component component + :key (dm/str "component-" (:id component)) + :renaming renaming + :listing-thumbs? listing-thumbs? + :file-id file-id + :selected selected + :selected-full selected-full + :selected-paths selected-paths + :on-asset-click on-asset-click + :on-context-menu on-context-menu + :on-drag-start on-drag-start + :on-group on-group + :do-rename do-rename + :cancel-rename cancel-rename + :local local}])]) + + (for [[path-item content] groups] + (when-not (empty? path-item) + [:& components-group {:file-id file-id + :key path-item + :prefix (cfh/merge-path-item prefix path-item) + :groups content + :open-groups open-groups + :force-open? force-open? + :renaming renaming + :listing-thumbs? listing-thumbs? + :selected selected + :on-asset-click on-asset-click + :on-drag-start on-drag-start + :do-rename do-rename + :cancel-rename cancel-rename + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :on-context-menu on-context-menu + :selected-full selected-full + :local local}]))])])) + +(mf/defc components-section + {::mf/wrap-props false} + [{:keys [file-id local? components listing-thumbs? open? force-open? + reverse-sort? selected on-asset-click on-assets-delete + on-clear-selection open-status-ref]}] + + (let [input-ref (mf/use-ref nil) + + state* (mf/use-state {}) + state (deref state*) + + current-component-id (:component-id state) + renaming? (:renaming state) + + open-groups-ref (mf/with-memo [open-status-ref] + (-> (l/in [:groups :components]) + (l/derived open-status-ref))) + + open-groups (mf/deref open-groups-ref) + + menu-state (mf/use-state cmm/initial-context-menu-state) + read-only? (mf/use-ctx ctx/workspace-read-only?) + components-v2 (mf/use-ctx ctx/components-v2) + toggle-list-style (mf/use-ctx cmm/assets-toggle-list-style) + + selected (:components selected) + + selected-full (into #{} (filter #(contains? selected (:id %))) components) + multi-components? (> (count selected) 1) + multi-assets? (or (seq (:graphics selected)) + (seq (:colors selected)) + (seq (:typographies selected))) + + groups (mf/with-memo [components reverse-sort?] + (grp/group-assets components reverse-sort?)) + + add-component + (mf/use-fn + (fn [] + (st/emit! (dw/set-assets-section-open file-id :components true)) + (dom/click (mf/ref-val input-ref)))) + + on-file-selected + (mf/use-fn + (mf/deps file-id) + (fn [blobs] + (let [params {:file-id file-id + :blobs (seq blobs)}] + (st/emit! (dwm/upload-media-components params) + (ptk/event ::ev/event {::ev/name "add-asset-to-library" + :asset-type "components"}))))) + + on-duplicate + (mf/use-fn + (mf/deps current-component-id selected) + (fn [] + (if (empty? selected) + (st/emit! (dwl/duplicate-component file-id current-component-id)) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! (map (partial dwl/duplicate-component file-id) selected)) + (st/emit! (dwu/commit-undo-transaction undo-id)))))) + + on-delete + (mf/use-fn + (mf/deps current-component-id file-id multi-components? multi-assets? on-assets-delete) + (fn [] + (let [undo-id (js/Symbol)] + (if (or multi-components? multi-assets?) + (on-assets-delete) + (st/emit! (dwu/start-undo-transaction undo-id) + (dwl/delete-component {:id current-component-id}) + (dwl/sync-file file-id file-id :components current-component-id) + (dwu/commit-undo-transaction undo-id)))))) + + on-close-menu + (mf/use-fn #(swap! menu-state cmm/close-context-menu)) + + on-rename + (mf/use-fn #(swap! state* assoc :renaming true)) + + cancel-rename + (mf/use-fn #(swap! state* dissoc :renaming)) + + do-rename + (mf/use-fn + (mf/deps current-component-id) + (fn [new-name] + (swap! state* dissoc :renaming) + (when (not (str/blank? new-name)) + (st/emit! + (dwl/rename-component-and-main-instance current-component-id new-name))))) + + on-context-menu + (mf/use-fn + (mf/deps selected on-clear-selection read-only?) + (fn [component-id event] + (dom/stop-propagation event) + (dom/prevent-default event) + (let [pos (dom/get-client-position event)] + + (when (not read-only?) + (when-not (contains? selected component-id) + (on-clear-selection)) + + (swap! state* assoc :component-id component-id) + (swap! menu-state cmm/open-context-menu pos))))) + + create-group + (mf/use-fn + (mf/deps current-component-id components selected on-clear-selection) + (fn [group-name] + (on-clear-selection) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! + (->> components + (filter #(if multi-components? + (contains? selected (:id %)) + (= current-component-id (:id %)))) + (map #(dwl/rename-component-and-main-instance + (:id %) + (cmm/add-group % group-name))))) + (st/emit! (dwu/commit-undo-transaction undo-id))))) + + rename-group + (mf/use-fn + (mf/deps components) + (fn [path last-path] + (on-clear-selection) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! + (->> components + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/rename-component-and-main-instance + (:id %) + (cmm/rename-group % path last-path))))) + (st/emit! (dwu/commit-undo-transaction undo-id))))) + + on-group + (mf/use-fn + (mf/deps components selected create-group) + (fn [event] + (dom/stop-propagation event) + (modal/show! :name-group-dialog {:accept create-group}))) + + on-rename-group + (mf/use-fn + (mf/deps components) + (fn [event path last-path] + (dom/stop-propagation event) + (modal/show! :name-group-dialog {:path path + :last-path last-path + :accept rename-group}))) + + on-ungroup + (mf/use-fn + (mf/deps components) + (fn [path] + (on-clear-selection) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! + (->> components + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/rename-component-and-main-instance (:id %) (cmm/ungroup % path))))) + (st/emit! (dwu/commit-undo-transaction undo-id))))) + + on-drag-start + (mf/use-fn + (mf/deps file-id) + (fn [component event] + + (let [file-data + (d/nilv (dm/get-in @refs/workspace-libraries [file-id :data]) @refs/workspace-data) + + shape-main + (ctf/get-component-root file-data component)] + + ;; dnd api only allow to acces to the dataTransfer data on on-drop (https://html.spec.whatwg.org/dev/dnd.html#concept-dnd-p) + ;; We need to know if the dragged element is from the local library on on-drag-enter, so we need to keep the info elsewhere + (set-drag-data! {:file-id file-id + :component component + :shape shape-main + :local? local?}) + + (dnd/set-data! event "penpot/component" true) + + ;; Remove the ghost image for componentes because we're going to instantiate it on the viewport + (dnd/set-drag-image! event (dnd/invisible-image)) + + (dnd/set-allowed-effect! event "move")))) + + on-show-main + (mf/use-fn + (mf/deps current-component-id file-id local?) + (fn [event] + (dom/stop-propagation event) + (if local? + (st/emit! (dw/go-to-component current-component-id)) + (let [component (d/seek #(= (:id %) current-component-id) components)] + (st/emit! (dwl/nav-to-component-file file-id component)))))) + + on-asset-click + (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))] + + [:& cmm/asset-section {:file-id file-id + :title (tr "workspace.assets.components") + :section :components + :assets-count (count components) + :open? open?} + [:& cmm/asset-section-block {:role :title-button} + (when ^boolean open? + [:div {:class (stl/css :listing-options)} + [:& radio-buttons {:selected (if listing-thumbs? "grid" "list") + :on-change toggle-list-style + :name "listing-style"} + [:& radio-button {:icon i/view-as-list + :value "list" + :id "opt-list"}] + [:& radio-button {:icon i/flex-grid + :value "grid" + :id "opt-grid"}]]]) + + (when (and components-v2 (not read-only?) local?) + [:div {:on-click add-component + :class (stl/css :add-component)} + i/add + [:& file-uploader {:accept cm/str-image-types + :multi true + :ref input-ref + :on-selected on-file-selected}]])] + + [:& cmm/asset-section-block {:role :content} + (when ^boolean open? + [:& components-group {:file-id file-id + :prefix "" + :groups groups + :open-groups open-groups + :force-open? force-open? + :renaming (when ^boolean renaming? current-component-id) + :listing-thumbs? listing-thumbs? + :selected selected + :on-asset-click on-asset-click + :on-drag-start on-drag-start + :do-rename do-rename + :cancel-rename cancel-rename + :on-rename-group on-rename-group + :on-group on-group + :on-ungroup on-ungroup + :on-context-menu on-context-menu + :selected-full selected-full + :local ^boolean local?}]) + + [:& cmm/assets-context-menu + {:on-close on-close-menu + :state @menu-state + :options [(when (and local? (not (or multi-components? multi-assets? read-only?))) + {:option-name (tr "workspace.assets.rename") + :id "assets-rename-component" + :option-handler on-rename}) + (when (and local? (not (or multi-assets? read-only?))) + {:option-name (if components-v2 + (tr "workspace.assets.duplicate-main") + (tr "workspace.assets.duplicate")) + :id "assets-duplicate-component" + :option-handler on-duplicate}) + + (when (and local? (not read-only?)) + {:option-name (tr "workspace.assets.delete") + :id "assets-delete-component" + :option-handler on-delete}) + (when (and local? (not (or multi-assets? read-only?))) + {:option-name (tr "workspace.assets.group") + :id "assets-group-component" + :option-handler on-group}) + + (when (and components-v2 (not multi-assets?)) + {:option-name (tr "workspace.shape.menu.show-main") + :id "assets-show-main-component" + :option-handler on-show-main})]}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss new file mode 100644 index 0000000000..72706d6015 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss @@ -0,0 +1,217 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.drop-space { + height: $s-12; +} + +.asset-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax($s-96, 1fr)); + max-width: 100%; + gap: $s-4; + margin-inline: $s-8; +} + +.grid-cell { + @include flexCenter; + position: relative; + aspect-ratio: 1 / 1; + padding: $s-8; + border-radius: $br-8; + background-color: var(--assets-component-background-color); + overflow: hidden; + cursor: pointer; + + .cell-name { + @include bodySmallTypography; + @include textEllipsis; + display: none; + position: absolute; + left: $s-4; + bottom: $s-4; + height: calc($s-24 - $s-2); + width: calc(100% - 2 * $s-4); + padding: $s-2 $s-6; + column-gap: $s-4; + border-radius: $br-4; + background-color: var(--assets-item-name-background-color); + border: $s-1 solid transparent; + color: var(--assets-item-name-foreground-color); + input { + @include textEllipsis; + @include bodySmallTypography; + @include removeInputStyle; + height: auto; + padding: 0; + } + span { + display: flex; + align-items: center; + height: 100%; + } + &.editing { + border-color: var(--input-border-color-focus); + border-radius: $br-4; + display: flex; + align-items: center; + background-color: var(--input-background-color); + } + } + + &:hover { + .cell-name { + display: block; + } + } + + &.selected { + border: $s-2 solid var(--assets-item-border-color); + &::before { + content: " "; + position: absolute; + z-index: $z-index-2; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: $s-4 solid var(--assets-component-second-border-selected); + border-radius: $br-8; + } + } +} + +.component-group { + display: grid; + grid-template-columns: 1fr; + gap: $s-4; +} + +.thumbnail { + width: 100%; + height: 100%; + object-fit: contain; + pointer-events: none; + border: 0; +} + +.grid-placeholder { + width: 100%; + border-radius: $br-8; + background-color: var(--assets-item-background-color-drag); + border: $s-2 solid var(--assets-item-border-color-drag); +} + +.enum-item { + position: relative; + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + column-gap: $s-8; + height: $s-44; + + padding: $s-2; + border-radius: $br-8; + background-color: var(--assets-item-background-color); + cursor: pointer; + + &:not(:last-child) { + margin-bottom: $s-4; + } + &:hover { + background-color: var(--assets-item-background-color-hover); + .item-name { + color: var(--assets-item-name-foreground-color-hover); + &.editing { + background: var(--input-background-color); + input { + color: var(--input-foreground-color-active); + } + span svg { + stroke: var(--input-foreground-color-active); + } + } + } + } + &.selected { + border: $s-1 solid var(--assets-item-border-color); + } +} + +.item-name { + @include bodySmallTypography; + @include textEllipsis; + order: 2; + color: var(--assets-item-name-foreground-color); + input { + @include textEllipsis; + @include bodySmallTypography; + @include removeInputStyle; + height: $s-32; + padding: $s-4; + } + span { + display: flex; + place-items: center; + padding-inline-end: $s-4; + } + + &.editing { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + column-gap: $s-8; + border: $s-1 solid var(--input-border-color-focus); + border-radius: $br-8; + background-color: var(--input-background-color); + } +} + +.asset-list-thumbnail { + @include flexCenter; + flex-shrink: 0; + padding: $s-2; + height: $s-36; + width: $s-36; + border-radius: $br-6; + background-color: var(--assets-component-background-color); +} + +.grid-placeholder { + height: $s-2; + width: 100%; + background-color: var(--color-accent-primary); +} + +.listing-options { + display: flex; + align-items: center; +} + +.add-component { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + margin-left: $s-2; + border-radius: $br-8; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.dragging { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: $s-8; + background-color: var(--assets-item-background-color-drag); + border: $s-2 solid var(--assets-item-border-color-drag); +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs new file mode 100644 index 0000000000..f822fb5f86 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs @@ -0,0 +1,335 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.sidebar.assets.file-library + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.main.data.workspace :as dw] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.undo :as dwu] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.title-bar :refer [title-bar]] + [app.main.ui.context :as ctx] + [app.main.ui.icons :as i] + [app.main.ui.workspace.libraries :refer [create-file-library-ref]] + [app.main.ui.workspace.sidebar.assets.colors :refer [colors-section]] + [app.main.ui.workspace.sidebar.assets.common :as cmm] + [app.main.ui.workspace.sidebar.assets.components :refer [components-section]] + [app.main.ui.workspace.sidebar.assets.graphics :refer [graphics-section]] + [app.main.ui.workspace.sidebar.assets.typographies :refer [typographies-section]] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] + [app.util.router :as rt] + [cuerdas.core :as str] + [okulary.core :as l] + [rumext.v2 :as mf])) + +(def lens:open-status + (l/derived (l/in [:workspace-assets :open-status]) st/state)) + +(def lens:selected + (-> (l/in [:workspace-assets :selected]) + (l/derived st/state))) + +(mf/defc file-library-title + {::mf/wrap-props false} + [{:keys [open? local? project-id file-id page-id file-name]}] + (let [router (mf/deref refs/router) + url (rt/resolve router :workspace + {:project-id project-id + :file-id file-id} + {:page-id page-id}) + toggle-open + (mf/use-fn + (mf/deps file-id open?) + (fn [] + (st/emit! (dw/set-assets-section-open file-id :library (not open?)))))] + [:div {:class (stl/css-case :library-title true + :open open?)} + [:& title-bar {:collapsable true + :collapsed (not open?) + :all-clickable true + :on-collapsed toggle-open + :title (if local? + (mf/html [:div {:class (stl/css :special-title)} + (tr "workspace.assets.local-library")]) + ;; Do we need to add shared info here? + + (mf/html [:div {:class (stl/css :special-title)} + file-name]))} + (when-not local? + [:span {:title "Open library file"} + [:a {:class (stl/css :file-link) + :href (str "#" url) + :target "_blank" + :on-click dom/stop-propagation} + i/open-link]])]])) + +(mf/defc file-library-content + {::mf/wrap-props false} + [{:keys [file local? open-status-ref on-clear-selection]}] + (let [components-v2 (mf/use-ctx ctx/components-v2) + open-status (mf/deref open-status-ref) + + file-id (:id file) + project-id (:project-id file) + + filters (mf/use-ctx cmm/assets-filters) + filters-section (:section filters) + + filters-term (:term filters) + filters-ordering (:ordering filters) + filters-list-style (:list-style filters) + + reverse-sort? (= :desc filters-ordering) + listing-thumbs? (= :thumbs filters-list-style) + + library-ref (mf/with-memo [file-id] + (create-file-library-ref file-id)) + + library (mf/deref library-ref) + colors (:colors library) + components (:components library) + media (:media library) + typographies (:typographies library) + + colors (mf/with-memo [filters colors] + (cmm/apply-filters colors filters)) + components (mf/with-memo [filters components] + (cmm/apply-filters components filters)) + media (mf/with-memo [filters media] + (cmm/apply-filters media filters)) + typographies (mf/with-memo [filters typographies] + (cmm/apply-filters typographies filters)) + + show-components? (and (or (= filters-section "all") + (= filters-section "components")) + (or (pos? (count components)) + (str/empty? filters-term))) + show-graphics? (and (or (= filters-section "all") + (= filters-section "graphics")) + (or (pos? (count media)) + (and (str/empty? filters-term) + (not components-v2)))) + show-colors? (and (or (= filters-section "all") + (= filters-section "colors")) + (or (> (count colors) 0) + (str/empty? filters-term))) + show-typography? (and (or (= filters-section "all") + (= filters-section "typographies")) + (or (pos? (count typographies)) + (str/empty? filters-term))) + + selected-lens (mf/with-memo [file-id] + (-> (l/key file-id) + (l/derived lens:selected))) + + selected (mf/deref selected-lens) + + has-term? (not ^boolean (str/empty? filters-term)) + force-open-components? (when ^boolean has-term? (> 60 (count components))) + force-open-colors? (when ^boolean has-term? (> 60 (count colors))) + force-open-graphics? (when ^boolean has-term? (> 60 (count media))) + force-open-typographies? (when ^boolean has-term? (> 60 (count typographies))) + + extend-selected + (fn [type asset-groups asset-id] + (letfn [(flatten-groups [groups] + (reduce concat [(get groups "" []) + (into [] + (->> (filter #(seq (first %)) groups) + (map second) + (mapcat flatten-groups)))]))] + + (let [selected' (get selected type)] + (if (zero? (count selected')) + (st/emit! (dw/select-single-asset file-id asset-id type)) + (let [all-assets (flatten-groups asset-groups) + click-index (d/index-of-pred all-assets #(= (:id %) asset-id)) + first-index (->> (get selected type) + (map (fn [asset] (d/index-of-pred all-assets #(= (:id %) asset)))) + (sort) + (first)) + + min-index (min first-index click-index) + max-index (max first-index click-index) + ids (->> (d/enumerate all-assets) + (into #{} (comp (filter #(<= min-index (first %) max-index)) + (map (comp :id second)))))] + + (st/emit! (dw/select-assets file-id ids type))))))) + + on-asset-click + (mf/use-fn + (mf/deps file-id extend-selected) + (fn [asset-type asset-groups asset-id default-click event] + (cond + (kbd/mod? event) + (do + (dom/stop-propagation event) + (st/emit! (dw/toggle-selected-assets file-id asset-id asset-type))) + + (kbd/shift? event) + (do + (dom/stop-propagation event) + (extend-selected asset-type asset-groups asset-id)) + + :else + (when default-click + (default-click event))))) + + on-component-click + (mf/use-fn (mf/deps on-asset-click) (partial on-asset-click :components)) + + on-graphics-click + (mf/use-fn (mf/deps on-asset-click) (partial on-asset-click :graphics)) + + on-colors-click + (mf/use-fn (mf/deps on-asset-click) (partial on-asset-click :colors)) + + on-typography-click + (mf/use-fn (mf/deps on-asset-click) (partial on-asset-click :typographies)) + + on-assets-delete + (mf/use-fn + (mf/deps selected file-id) + (fn [] + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! (map #(dwl/delete-component {:id %}) + (:components selected))) + (run! st/emit! (map #(dwl/delete-media {:id %}) + (:graphics selected))) + (run! st/emit! (map #(dwl/delete-color {:id %}) + (:colors selected))) + (run! st/emit! (map #(dwl/delete-typography %) + (:typographies selected))) + + (when (or (seq (:components selected)) + (seq (:colors selected)) + (seq (:typographies selected))) + (st/emit! (dwl/sync-file file-id file-id))) + + (st/emit! (dwu/commit-undo-transaction undo-id)))))] + + [:div {:class (stl/css :library-content)} + (when ^boolean show-components? + [:& components-section + {:file-id file-id + :local? local? + :components components + :listing-thumbs? listing-thumbs? + :open? (or ^boolean force-open-components? + ^boolean (get open-status :components false)) + :force-open? force-open-components? + :open-status-ref open-status-ref + :reverse-sort? reverse-sort? + :selected selected + :on-asset-click on-component-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection}]) + + (when ^boolean show-graphics? + [:& graphics-section + {:file-id file-id + :project-id project-id + :local? local? + :objects media + :listing-thumbs? listing-thumbs? + :open? (or ^boolean force-open-graphics? + ^boolean (get open-status :graphics false)) + :force-open? force-open-graphics? + :open-status-ref open-status-ref + :reverse-sort? reverse-sort? + :selected selected + :on-asset-click on-graphics-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection}]) + + (when ^boolean show-colors? + [:& colors-section + {:file-id file-id + :local? local? + :colors colors + :open? (or ^boolean force-open-colors? + ^boolean (get open-status :colors false)) + :force-open? force-open-colors? + :open-status-ref open-status-ref + :reverse-sort? reverse-sort? + :selected selected + :on-asset-click on-colors-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection}]) + + (when ^boolean show-typography? + [:& typographies-section + {:file file + :file-id (:id file) + :local? local? + :typographies typographies + :open? (or ^boolean force-open-typographies? + ^boolean (get open-status :typographies false)) + :force-open? force-open-typographies? + :open-status-ref open-status-ref + :reverse-sort? reverse-sort? + :selected selected + :on-asset-click on-typography-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection}]) + + (when (and (not ^boolean show-components?) + (not ^boolean show-graphics?) + (not ^boolean show-colors?) + (not ^boolean show-typography?)) + [:div {:class (stl/css :asset-title)} + [:span {:class (stl/css :no-found-icon)} + i/search] + [:span {:class (stl/css :no-found-text)} + (tr "workspace.assets.not-found")]])])) + + +(mf/defc file-library + {::mf/wrap-props false} + [{:keys [file local? default-open? filters]}] + (let [file-id (:id file) + file-name (:name file) + shared? (:is-shared file) + project-id (:project-id file) + page-id (dm/get-in file [:data :pages 0]) + + open-status-ref (mf/with-memo [file-id] + (-> (l/key file-id) + (l/derived lens:open-status))) + open-status (mf/deref open-status-ref) + open? (d/nilv (:library open-status) default-open?) + + unselect-all + (mf/use-fn + (mf/deps file-id) + (fn [] + (st/emit! (dw/unselect-all-assets file-id))))] + [:div {:class (stl/css :tool-window) + :on-context-menu dom/prevent-default + :on-click unselect-all} + [:& file-library-title + {:project-id project-id + :file-id file-id + :page-id page-id + :file-name file-name + :open? open? + :local? local? + :shared? shared?}] + (when ^boolean open? + [:& file-library-content + {:file file + :local? local? + :filters filters + :on-clear-selection unselect-all + :open-status-ref open-status-ref}])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss new file mode 100644 index 0000000000..b05e5796ec --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss @@ -0,0 +1,76 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; +.tool-window { + padding: 0 0 $s-24 $s-12; + display: grid; + grid-auto-rows: max-content; + gap: $s-4; + height: 100%; +} + +.file-name { + @include bodySmallTypography; + display: flex; + justify-content: flex-start; + align-items: center; + flex-grow: 100; + height: 100%; +} + +.special-title { + @include textEllipsis; + color: var(--title-foreground-color-hover); + margin-left: $s-2; + text-align: left; +} + +.file-link { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + border-radius: $br-8; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + fill: var(--title-foreground-color-hover); + } +} + +.library-content { + width: 100%; + display: grid; + grid-auto-rows: max-content; + gap: $s-4; +} + +.asset-title { + margin-left: $s-28; + display: flex; + flex-direction: column; + align-items: center; + gap: $s-8; +} + +.no-found-icon { + @include flexCenter; + background-color: var(--not-found-background-color); + border-radius: $br-circle; + height: $s-48; + width: $s-48; + svg { + @extend .button-icon; + height: $s-24; + width: $s-24; + stroke: var(--not-found-foreground-color); + } +} + +.no-found-text { + @include bodySmallTypography; + color: var(--not-found-foreground-color); +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/graphics.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/graphics.cljs new file mode 100644 index 0000000000..f8c7e2b8a9 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/graphics.cljs @@ -0,0 +1,430 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.sidebar.assets.graphics + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.media :as cm] + [app.config :as cf] + [app.main.data.events :as ev] + [app.main.data.modal :as modal] + [app.main.data.workspace :as dw] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.media :as dwm] + [app.main.data.workspace.undo :as dwu] + [app.main.store :as st] + [app.main.ui.components.editable-label :refer [editable-label]] + [app.main.ui.components.file-uploader :refer [file-uploader]] + [app.main.ui.context :as ctx] + [app.main.ui.hooks :as h] + [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.assets.common :as cmm] + [app.main.ui.workspace.sidebar.assets.groups :as grp] + [app.util.dom :as dom] + [app.util.dom.dnd :as dnd] + [app.util.i18n :as i18n :refer [tr]] + [cuerdas.core :as str] + [okulary.core :as l] + [potok.v2.core :as ptk] + [rumext.v2 :as mf])) + +(mf/defc graphics-item + [{:keys [object renaming listing-thumbs? selected-objects file-id + on-asset-click on-context-menu on-drag-start do-rename cancel-rename + selected-full selected-graphics-paths]}] + (let [item-ref (mf/use-ref) + visible? (h/use-visible item-ref :once? true) + object-id (:id object) + + dragging* (mf/use-state false) + dragging? (deref dragging*) + + read-only? (mf/use-ctx ctx/workspace-read-only?) + + on-drop + (mf/use-fn + (mf/deps object dragging* selected-objects selected-full selected-graphics-paths) + (fn [event] + (cmm/on-drop-asset event object dragging* selected-objects selected-full + selected-graphics-paths dwl/rename-media))) + + on-drag-enter + (mf/use-fn + (mf/deps object dragging* selected-objects selected-graphics-paths) + (fn [event] + (cmm/on-drag-enter-asset event object dragging* selected-objects selected-graphics-paths))) + + on-drag-leave + (mf/use-fn + (mf/deps dragging*) + (fn [event] + (cmm/on-drag-leave-asset event dragging*))) + + on-grahic-drag-start + (mf/use-fn + (mf/deps object file-id selected-objects item-ref on-drag-start read-only?) + (fn [event] + (if read-only? + (dom/prevent-default event) + (cmm/on-asset-drag-start event file-id object selected-objects item-ref :graphics on-drag-start)))) + + on-context-menu + (mf/use-fn + (mf/deps object-id) + (partial on-context-menu object-id)) + + on-asset-click + (mf/use-fn + (mf/deps object-id on-asset-click) + (partial on-asset-click object-id nil))] + [:div {:ref item-ref + :class-name (stl/css-case + :selected (contains? selected-objects object-id) + :grid-cell listing-thumbs? + :enum-item (not listing-thumbs?)) + :draggable (not read-only?) + :on-click on-asset-click + :on-context-menu on-context-menu + :on-drag-start on-grahic-drag-start + :on-drag-enter on-drag-enter + :on-drag-leave on-drag-leave + :on-drag-over dom/prevent-default + :on-drop on-drop} + + (when visible? + [:* + [:img {:src (when visible? (cf/resolve-file-media object true)) + :class (stl/css :graphic-image) + :draggable false}] ;; Also need to add css pointer-events: none + + (let [renaming? (= renaming (:id object))] + [:* + [:& editable-label + {:class (stl/css-case + :cell-name listing-thumbs? + :item-name (not listing-thumbs?) + :editing renaming?) + :value (cfh/merge-path-item (:path object) (:name object)) + :tooltip (cfh/merge-path-item (:path object) (:name object)) + :display-value (:name object) + :editing renaming? + :disable-dbl-click true + :on-change do-rename + :on-cancel cancel-rename}] + + (when ^boolean dragging? + [:div {:class (stl/css :dragging)}])])])])) + +(mf/defc graphics-group + [{:keys [file-id prefix groups open-groups force-open? renaming listing-thumbs? selected-objects on-asset-click + on-drag-start do-rename cancel-rename on-rename-group on-ungroup + on-context-menu selected-full]}] + (let [group-open? (get open-groups prefix true) + dragging* (mf/use-state false) + dragging? (deref dragging*) + + selected-paths + (mf/with-memo [selected-full] + (into #{} + (comp (map :path) (d/nilv "")) + selected-full)) + + on-drag-enter + (mf/use-fn + (mf/deps dragging* prefix selected-paths) + (fn [event] + (cmm/on-drag-enter-asset-group event dragging* prefix selected-paths))) + + on-drag-leave + (mf/use-fn + (mf/deps dragging*) + (fn [event] + (cmm/on-drag-leave-asset event dragging*))) + + on-drop + (mf/use-fn + (mf/deps dragging* prefix selected-paths selected-full) + (fn [event] + (cmm/on-drop-asset-group event dragging* prefix selected-paths selected-full dwl/rename-media)))] + [:div {:class (stl/css :graphics-group) + :on-drag-enter on-drag-enter + :on-drag-leave on-drag-leave + :on-drag-over dom/prevent-default + :on-drop on-drop} + [:& grp/asset-group-title + {:file-id file-id + :section :graphics + :path prefix + :group-open? group-open? + :on-rename on-rename-group + :on-ungroup on-ungroup}] + (when group-open? + [:* + (let [objects (get groups "" [])] + [:div {:class-name (stl/css-case + :asset-grid listing-thumbs? + :asset-enum (not listing-thumbs?) + :drop-space (and + (empty? objects) + (some? groups) + (not dragging?))) + :on-drag-enter on-drag-enter + :on-drag-leave on-drag-leave + :on-drag-over dom/prevent-default + :on-drop on-drop} + + (when ^boolean dragging? + [:div {:class (stl/css :grid-placeholder)} "\u00A0"]) + + (when (and (empty? objects) + (some? groups)) + [:div {:class (stl/css :drop-space)}]) + + (for [object objects] + [:& graphics-item + {:key (dm/str "object-" (:id object)) + :file-id file-id + :object object + :renaming renaming + :listing-thumbs? listing-thumbs? + :selected-objects selected-objects + :on-asset-click on-asset-click + :on-context-menu on-context-menu + :on-drag-start on-drag-start + :do-rename do-rename + :cancel-rename cancel-rename + :selected-full selected-full + :selected-paths selected-paths}])]) + (for [[path-item content] groups] + (when-not (empty? path-item) + [:& graphics-group {:file-id file-id + :key path-item + :prefix (cfh/merge-path-item prefix path-item) + :groups content + :open-groups open-groups + :force-open? force-open? + :renaming renaming + :listing-thumbs? listing-thumbs? + :selected-objects selected-objects + :on-asset-click on-asset-click + :on-drag-start on-drag-start + :do-rename do-rename + :cancel-rename cancel-rename + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :on-context-menu on-context-menu + :selected-full selected-full + :selected-paths selected-paths}]))])])) + +(mf/defc graphics-section + {::mf/wrap-props false} + [{:keys [file-id project-id local? objects listing-thumbs? open? force-open? open-status-ref selected reverse-sort? + on-asset-click on-assets-delete on-clear-selection]}] + (let [input-ref (mf/use-ref nil) + state (mf/use-state {:renaming nil :object-id nil}) + + menu-state (mf/use-state cmm/initial-context-menu-state) + read-only? (mf/use-ctx ctx/workspace-read-only?) + + open-groups-ref (mf/with-memo [open-status-ref] + (-> (l/in [:groups :graphics]) + (l/derived open-status-ref))) + open-groups (mf/deref open-groups-ref) + + selected (:graphics selected) + selected-full (into #{} (filter #(contains? selected (:id %))) objects) + multi-objects? (> (count selected) 1) + multi-assets? (or (seq (:components selected)) + (seq (:colors selected)) + (seq (:typographies selected))) + + objects (mf/with-memo [objects] + (mapv dwl/extract-path-if-missing objects)) + + groups (mf/with-memo [objects reverse-sort?] + (grp/group-assets objects reverse-sort?)) + + components-v2 (mf/use-ctx ctx/components-v2) + team-id (mf/use-ctx ctx/current-team-id) + + add-graphic + (mf/use-fn + (fn [] + (st/emit! (dw/set-assets-section-open file-id :graphics true)) + (dom/click (mf/ref-val input-ref)))) + + on-file-selected + (mf/use-fn + (mf/deps file-id project-id team-id) + (fn [blobs] + (let [params {:file-id file-id + :blobs (seq blobs)}] + (st/emit! (dwm/upload-media-asset params) + (ptk/event ::ev/event {::ev/name "add-asset-to-library" + :asset-type "graphics" + :file-id file-id + :project-id project-id + :team-id team-id}))))) + on-delete + (mf/use-fn + (mf/deps @state multi-objects? multi-assets?) + (fn [] + (if (or multi-objects? multi-assets?) + (on-assets-delete) + (st/emit! (dwl/delete-media {:id (:object-id @state)}))))) + + on-rename + (mf/use-fn + (fn [] + (swap! state (fn [state] + (assoc state :renaming (:object-id state)))))) + cancel-rename + (mf/use-fn + (fn [] + (swap! state assoc :renaming nil))) + + do-rename + (mf/use-fn + (mf/deps @state) + (fn [new-name] + (st/emit! (dwl/rename-media (:renaming @state) new-name)) + (swap! state assoc :renaming nil))) + + on-context-menu + (mf/use-fn + (mf/deps selected on-clear-selection read-only?) + (fn [object-id event] + (dom/prevent-default event) + (let [pos (dom/get-client-position event)] + (when (and local? (not read-only?)) + (when-not (contains? selected object-id) + (on-clear-selection)) + (swap! state assoc :object-id object-id) + (swap! menu-state cmm/open-context-menu pos))))) + + on-close-menu + (mf/use-fn + (fn [] + (swap! menu-state cmm/close-context-menu))) + + create-group + (mf/use-fn + (mf/deps objects selected on-clear-selection (:object-id @state)) + (fn [group-name] + (on-clear-selection) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! + (->> objects + (filter #(if multi-objects? + (contains? selected (:id %)) + (= (:object-id @state) (:id %)))) + (map #(dwl/rename-media (:id %) (cmm/add-group % group-name))))) + (st/emit! (dwu/commit-undo-transaction undo-id))))) + + rename-group + (mf/use-fn + (mf/deps objects) + (fn [path last-path] + (on-clear-selection) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! + (->> objects + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/rename-media (:id %) (cmm/rename-group % path last-path))))) + (st/emit! (dwu/commit-undo-transaction undo-id))))) + + on-group + (mf/use-fn + (mf/deps objects selected create-group) + (fn [event] + (dom/stop-propagation event) + (modal/show! :name-group-dialog {:accept create-group}))) + + on-rename-group + (mf/use-fn + (mf/deps objects) + (fn [event path last-path] + (dom/stop-propagation event) + (modal/show! :name-group-dialog {:path path + :last-path last-path + :accept rename-group}))) + on-ungroup + (mf/use-fn + (mf/deps objects) + (fn [path] + (on-clear-selection) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! + (->> objects + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/rename-media (:id %) (cmm/ungroup % path))))) + (st/emit! (dwu/commit-undo-transaction undo-id))))) + + on-drag-start + (mf/use-fn + (fn [{:keys [name id mtype]} event] + (dnd/set-data! event "text/asset-id" (str id)) + (dnd/set-data! event "text/asset-name" name) + (dnd/set-data! event "text/asset-type" mtype) + (dnd/set-allowed-effect! event "move"))) + + on-asset-click + (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))] + + [:& cmm/asset-section {:file-id file-id + :title (tr "workspace.assets.graphics") + :section :graphics + :assets-count (count objects) + :open? open?} + (when local? + [:& cmm/asset-section-block {:role :title-button} + (when (and (not components-v2) (not read-only?)) + [:button {:class (stl/css :assets-btn) + :on-click add-graphic} + i/add + [:& file-uploader {:accept cm/str-image-types + :multi true + :ref input-ref + :on-selected on-file-selected}]])]) + + [:& cmm/asset-section-block {:role :content} + [:& graphics-group {:file-id file-id + :prefix "" + :groups groups + :open-groups open-groups + :force-open? force-open? + :renaming (:renaming @state) + :listing-thumbs? listing-thumbs? + :selected selected + :on-asset-click on-asset-click + :on-drag-start on-drag-start + :do-rename do-rename + :cancel-rename cancel-rename + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :on-context-menu on-context-menu + :selected-full selected-full}] + (when local? + [:& cmm/assets-context-menu + {:on-close on-close-menu + :state @menu-state + :options [(when-not (or multi-objects? multi-assets?) + {:option-name (tr "workspace.assets.rename") + :id "assets-rename-graphics" + :option-handler on-rename}) + {:option-name (tr "workspace.assets.delete") + :id "assets-delete-graphics" + :option-handler on-delete} + (when-not multi-assets? + {:option-name (tr "workspace.assets.group") + :id "assets-group-graphics" + :option-handler on-group})]}])]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/graphics.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/graphics.scss new file mode 100644 index 0000000000..fc20278884 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/graphics.scss @@ -0,0 +1,188 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.graphics-group { + .drop-space { + height: $s-12; + } + .asset-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: $s-4; + margin-left: $s-8; + .grid-cell { + @include flexCenter; + position: relative; + padding: $s-8; + border: $s-2 solid transparent; + border-radius: $br-8; + aspect-ratio: 1/1; + background-color: var(--color-foreground-secondary); + overflow: hidden; + cursor: pointer; + img { + height: auto; + width: auto; + max-height: 100%; + max-width: 100%; + pointer-events: none; + } + svg { + height: 10vh; + } + .cell-name { + @include bodySmallTypography; + @include textEllipsis; + display: none; + position: absolute; + left: 0; + bottom: 0; + width: 100%; + padding: $s-2; + + &.editing { + display: block; + } + + .editable-label-input { + height: unset; + width: 100%; + padding: $s-2; + margin: 0; + } + + .editable-label-close { + display: none; + } + } + + &:hover { + background-color: var(--assets-item-background-color-hover); + .cell-name { + display: block; + color: var(--assets-item-name-foreground-color-hover); + background: linear-gradient(to top, rgba(52, 57, 59, 1) 0%, rgba(52, 57, 59, 0) 100%); + } + } + + &.selected { + border: $s-1 solid var(--assets-item-border-color); + } + + .dragging { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: $s-8; + background-color: var(--assets-item-background-color-drag); + border: $s-2 solid var(--assets-item-border-color-drag); + } + } + } + .asset-enum { + padding-bottom: $s-4; + .enum-item { + position: relative; + display: flex; + align-items: center; + height: $s-36; + margin-bottom: $s-4; + padding: $s-2; + border-radius: $br-8; + background-color: var(--assets-item-background-color); + cursor: pointer; + + svg, + img { + @include flexCenter; + padding: $s-2; + height: $s-32; + width: $s-32; + border-radius: $br-6; + background-color: var(--assets-component-background-color); + } + + .item-name { + @include bodySmallTypography; + @include textEllipsis; + padding-left: $s-8; + color: var(--assets-item-name-foreground-color); + + &.editing { + display: flex; + align-items: center; + + .editable-label-input { + height: $s-24; + } + + .editable-label-close { + display: none; + } + } + } + &:hover { + background-color: var(--assets-item-background-color-hover); + .item-name { + color: var(--assets-item-name-foreground-color-hover); + } + } + &.selected { + border: $s-1 solid var(--assets-item-border-color); + } + } + } + .grid-placeholder { + height: $s-2; + background-color: var(--color-accent-primary); + margin-bottom: $s-2; + } +} +.listing-options { + display: flex; + align-items: center; + + .listing-option-btn { + @include flexCenter; + cursor: pointer; + + &.first { + margin-left: auto; + } + + svg { + height: $s-16; + width: $s-16; + } + } +} +.add-component { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + margin-left: $s-2; + border-radius: $br-8; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.assets-btn { + @extend .button-tertiary; + height: $s-32; + width: calc($s-24 + $s-4); + padding: 0; + border-radius: $br-8; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs new file mode 100644 index 0000000000..dc882483ec --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs @@ -0,0 +1,158 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.sidebar.assets.groups + (:require-macros [app.main.style :as stl]) + (:require + [app.common.files.helpers :as cfh] + [app.common.spec :as us] + [app.main.data.modal :as modal] + [app.main.data.workspace :as dw] + [app.main.store :as st] + [app.main.ui.components.forms :as fm] + [app.main.ui.components.title-bar :refer [title-bar]] + [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.assets.common :as cmm] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [cljs.spec.alpha :as s] + [rumext.v2 :as mf])) + +(mf/defc asset-group-title + [{:keys [file-id section path group-open? on-rename on-ungroup]}] + (when-not (empty? path) + (let [[other-path last-path truncated] (cfh/compact-path path 35 true) + menu-state (mf/use-state cmm/initial-context-menu-state) + on-fold-group + (mf/use-fn + (mf/deps file-id section path group-open?) + (fn [event] + (dom/stop-propagation event) + (st/emit! (dw/set-assets-group-open file-id + section + path + (not group-open?))))) + on-context-menu + (mf/use-fn + (fn [event] + (dom/prevent-default event) + (let [pos (dom/get-client-position event)] + (swap! menu-state cmm/open-context-menu pos)))) + + on-close-menu + (mf/use-fn #(swap! menu-state cmm/close-context-menu))] + [:div {:class (stl/css :group-title) + :on-context-menu on-context-menu} + [:& title-bar {:collapsable true + :collapsed (not group-open?) + :all-clickable true + :on-collapsed on-fold-group + :title (mf/html [:* (when-not (empty? other-path) + [:span {:class (stl/css :pre-path) + :title (when truncated path)} + other-path "\u00A0\u2022\u00A0"]) + [:span {:class (stl/css :path) + :title (when truncated path)} + last-path]])}] + [:& cmm/assets-context-menu + {:on-close on-close-menu + :state @menu-state + :options [{:option-name (tr "workspace.assets.rename") + :id "assets-rename-group" + :option-handler #(on-rename % path last-path)} + {:option-name (tr "workspace.assets.ungroup") + :id "assets-ungroup-group" + :option-handler #(on-ungroup path)}]}]]))) + +(defn group-assets + "Convert a list of assets in a nested structure like this: + + {'': [{assetA} {assetB}] + 'group1': {'': [{asset1A} {asset1B}] + 'subgroup11': {'': [{asset11A} {asset11B} {asset11C}]} + 'subgroup12': {'': [{asset12A}]}} + 'group2': {'subgroup21': {'': [{asset21A}}}} + " + [assets reverse-sort?] + (when-not (empty? assets) + (reduce (fn [groups {:keys [path] :as asset}] + (let [path (cfh/split-path (or path ""))] + (update-in groups + (conj path "") + (fn [group] + (if group + (conj group asset) + [asset]))))) + (sorted-map-by (fn [key1 key2] + (if reverse-sort? + (compare key2 key1) + (compare key1 key2)))) + assets))) + +(s/def ::asset-name ::us/not-empty-string) +(s/def ::name-group-form + (s/keys :req-un [::asset-name])) + +(mf/defc name-group-dialog + {::mf/register modal/components + ::mf/register-as :name-group-dialog} + [{:keys [path last-path accept] :as ctx + :or {path "" last-path ""}}] + (let [initial (mf/use-memo + (mf/deps last-path) + (constantly {:asset-name last-path})) + form (fm/use-form :spec ::name-group-form + :validators [(fm/validate-not-empty :asset-name (tr "auth.name.not-all-space")) + (fm/validate-length :asset-name fm/max-length-allowed (tr "auth.name.too-long"))] + :initial initial) + + create? (empty? path) + + on-close (mf/use-fn #(modal/hide!)) + + on-accept + (mf/use-fn + (mf/deps form) + (fn [_] + (let [asset-name (get-in @form [:clean-data :asset-name])] + (if create? + (accept asset-name) + (accept path asset-name)) + (modal/hide!))))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} + (if create? + (tr "workspace.assets.create-group") + (tr "workspace.assets.rename-group"))] + [:button {:class (stl/css :modal-close-btn) + :on-click on-close} i/close]] + + [:div {:class (stl/css :modal-content)} + [:& fm/form {:form form :on-submit on-accept} + [:& fm/input {:name :asset-name + :class (stl/css :input-wrapper) + :auto-focus? true + :label (tr "workspace.assets.group-name") + :hint (tr "workspace.assets.create-group-hint")}]]] + + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:input + {:class (stl/css :cancel-button) + :type "button" + :value (tr "labels.cancel") + :on-click on-close}] + + [:input + {:type "button" + :class (stl/css-case :accept-btn true + :global/disabled (not (:valid @form))) + :disabled (not (:valid @form)) + :value (if create? (tr "labels.create") (tr "labels.rename")) + :on-click on-accept}]]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss new file mode 100644 index 0000000000..1756829e34 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss @@ -0,0 +1,66 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.group-title { + padding-left: $s-4; +} + +.pre-path { + margin-left: $s-2; + text-transform: initial; + color: var(--title-foreground-color); +} + +.path { + @include textEllipsis; + margin-left: $s-2; + text-transform: initial; + color: var(--title-foreground-color-hover); +} + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + @extend .modal-container-base; +} + +.modal-header { + margin-bottom: $s-24; +} + +.modal-title { + @include uppercaseTitleTipography; + color: var(--modal-title-foreground-color); +} +.modal-close-btn { + @extend .modal-close-btn-base; +} + +.modal-content { + @include bodySmallTypography; + margin-bottom: $s-24; +} +.input-wrapper { + @extend .input-with-label; + margin-bottom: $s-8; +} +.action-buttons { + @extend .modal-action-btns; +} +.cancel-button { + @extend .modal-cancel-btn; +} + +.accept-btn { + @extend .modal-accept-btn; + &.danger { + @extend .modal-danger-btn; + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs new file mode 100644 index 0000000000..6c5ae2fd28 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs @@ -0,0 +1,457 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.sidebar.assets.typographies + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.main.data.modal :as modal] + [app.main.data.workspace :as dw] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.texts :as dwt] + [app.main.data.workspace.undo :as dwu] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.context :as ctx] + [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.assets.common :as cmm] + [app.main.ui.workspace.sidebar.assets.groups :as grp] + [app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry]] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [cuerdas.core :as str] + [okulary.core :as l] + [rumext.v2 :as mf])) + +(def lens:typography-section-state + (l/derived (fn [gstate] + {:rename-typography (:rename-typography gstate) + :edit-typography (:edit-typography gstate)}) + refs/workspace-global + =)) + +(mf/defc typography-item + {::mf/wrap-props false} + [{:keys [typography file-id local? handle-change selected apply-typography editing-id renaming-id on-asset-click + on-context-menu selected-full selected-paths move-typography rename?]}] + (let [item-ref (mf/use-ref) + typography-id (:id typography) + + dragging* (mf/use-state false) + dragging? (deref dragging*) + + read-only? (mf/use-ctx ctx/workspace-read-only?) + editing? (= editing-id (:id typography)) + renaming? (= renaming-id (:id typography)) + + open* (mf/use-state editing?) + open? (deref open*) + + on-drop + (mf/use-fn + (mf/deps typography dragging* selected selected-full selected-paths move-typography) + (fn [event] + (cmm/on-drop-asset event typography dragging* selected selected-full + selected-paths move-typography))) + + on-drag-enter + (mf/use-fn + (mf/deps typography dragging* selected selected-paths) + (fn [event] + (cmm/on-drag-enter-asset event typography dragging* selected selected-paths))) + + on-drag-leave + (mf/use-fn + (mf/deps dragging*) + (fn [event] + (cmm/on-drag-leave-asset event dragging*))) + + on-typography-drag-start + (mf/use-fn + (mf/deps typography file-id selected item-ref read-only?) + (fn [event] + (if read-only? + (dom/prevent-default event) + (cmm/on-asset-drag-start event file-id typography selected item-ref :typographies identity)))) + + on-context-menu + (mf/use-fn + (mf/deps on-context-menu typography-id) + (partial on-context-menu typography-id)) + + handle-change + (mf/use-fn + (mf/deps typography) + (partial handle-change typography)) + + apply-typography + (mf/use-fn + (mf/deps typography) + (partial apply-typography typography)) + + on-asset-click + (mf/use-fn + (mf/deps typography apply-typography on-asset-click) + (partial on-asset-click typography-id apply-typography))] + + [:div {:class (stl/css :typography-item) + :ref item-ref + :draggable (and (not read-only?) (not open?)) + :on-drag-start on-typography-drag-start + :on-drag-enter on-drag-enter + :on-drag-leave on-drag-leave + :on-drag-over dom/prevent-default + :on-drop on-drop} + + [:& typography-entry + {:file-id file-id + :typography typography + :local? local? + :selected? (contains? selected typography-id) + :on-click on-asset-click + :on-change handle-change + :on-context-menu on-context-menu + :editing? editing? + :renaming? renaming? + :focus-name? rename? + :external-open* open*}] + (when ^boolean dragging? + [:div {:class (stl/css :dragging)}])])) + +(mf/defc typographies-group + {::mf/wrap-props false} + [{:keys [file-id prefix groups open-groups force-open? file local? selected local-data + editing-id renaming-id on-asset-click handle-change apply-typography on-rename-group + on-ungroup on-context-menu selected-full]}] + (let [group-open? (get open-groups prefix true) + dragging* (mf/use-state false) + dragging? (deref dragging*) + selected-paths (mf/with-memo [selected-full] + (into #{} + (comp (map :path) (d/nilv "")) + selected-full)) + move-typography + (mf/use-fn + (mf/deps file-id) + (partial dwl/rename-typography file-id)) + + on-drag-enter + (mf/use-fn + (mf/deps dragging* prefix selected-paths) + (fn [event] + (cmm/on-drag-enter-asset-group event dragging* prefix selected-paths))) + + on-drag-leave + (mf/use-fn + (mf/deps dragging*) + (fn [event] + (cmm/on-drag-leave-asset event dragging*))) + + on-drop + (mf/use-fn + (mf/deps dragging* prefix selected-paths selected-full move-typography) + (fn [event] + (cmm/on-drop-asset-group event dragging* prefix selected-paths selected-full move-typography)))] + + [:div {:class (stl/css :typographies-group) + :on-drag-enter on-drag-enter + :on-drag-leave on-drag-leave + :on-drag-over dom/prevent-default + :on-drop on-drop} + [:& grp/asset-group-title {:file-id file-id + :section :typographies + :path prefix + :group-open? group-open? + :on-rename on-rename-group + :on-ungroup on-ungroup}] + + (when group-open? + [:* + (let [typographies (get groups "" [])] + [:div {:class (stl/css :assets-list) + :on-drag-enter on-drag-enter + :on-drag-leave on-drag-leave + :on-drag-over dom/prevent-default + :on-drop on-drop} + + (when ^boolean dragging? + [:div {:class (stl/css :grid-placeholder)} "\u00A0"]) + + (when (and + (empty? typographies) + (some? groups)) + [:div {:class (stl/css :drop-space)}]) + (for [{:keys [id] :as typography} typographies] + [:& typography-item {:typography typography + :key (dm/str "typography-" id) + :file-id file-id + :local? local? + :handle-change handle-change + :selected selected + :apply-typography apply-typography + :editing-id editing-id + :renaming-id renaming-id + :rename? (= (:rename-typography local-data) id) + :on-asset-click on-asset-click + :on-context-menu on-context-menu + :selected-full selected-full + :selected-paths selected-paths + :move-typography move-typography}])]) + + (for [[path-item content] groups] + (when-not (empty? path-item) + [:& typographies-group {:file-id file-id + :prefix (cfh/merge-path-item prefix path-item) + :key (dm/str "group-" path-item) + :groups content + :open-groups open-groups + :force-open? force-open? + :file file + :local? local? + :selected selected + :editing-id editing-id + :renaming-id renaming-id + :local-data local-data + :on-asset-click on-asset-click + :handle-change handle-change + :apply-typography apply-typography + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :on-context-menu on-context-menu + :selected-full selected-full}]))])])) + +(mf/defc typographies-section + {::mf/wrap-props false} + [{:keys [file file-id local? typographies open? force-open? open-status-ref selected reverse-sort? + on-asset-click on-assets-delete on-clear-selection]}] + (let [state (mf/use-state {:detail-open? false :id nil}) + local-data (mf/deref lens:typography-section-state) + + read-only? (mf/use-ctx ctx/workspace-read-only?) + menu-state (mf/use-state cmm/initial-context-menu-state) + typographies (mf/with-memo [typographies] + (mapv dwl/extract-path-if-missing typographies)) + + groups (mf/with-memo [typographies reverse-sort?] + (grp/group-assets typographies reverse-sort?)) + + selected (:typographies selected) + selected-full (mf/with-memo [selected typographies] + (into #{} (filter #(contains? selected (:id %))) typographies)) + + multi-typographies? (> (count selected) 1) + multi-assets? (or (seq (:components selected)) + (seq (:graphics selected)) + (seq (:colors selected))) + + open-groups-ref (mf/with-memo [open-status-ref] + (-> (l/in [:groups :typographies]) + (l/derived open-status-ref))) + + open-groups (mf/deref open-groups-ref) + + add-typography + (mf/use-fn + (mf/deps file-id) + (fn [_] + (st/emit! (dw/set-assets-section-open file-id :typographies true)) + (st/emit! (dwt/add-typography file-id)))) + + handle-change + (mf/use-fn + (mf/deps file-id) + (fn [typography changes] + (st/emit! (dwl/update-typography (merge typography changes) file-id)))) + + apply-typography + (mf/use-fn + (mf/deps file-id) + (fn [typography _event] + (st/emit! (dwt/apply-typography typography file-id)))) + + create-group + (mf/use-fn + (mf/deps typographies selected on-clear-selection file-id (:id @state)) + (fn [group-name] + (on-clear-selection) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! + (->> typographies + (filter #(if multi-typographies? + (contains? selected (:id %)) + (= (:id @state) (:id %)))) + (map #(dwl/update-typography + (assoc % :name + (cmm/add-group % group-name)) + file-id)))) + (st/emit! (dwu/commit-undo-transaction undo-id))))) + + rename-group + (mf/use-fn + (mf/deps typographies) + (fn [path last-path] + (on-clear-selection) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! + (->> typographies + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/update-typography + (assoc % :name + (cmm/rename-group % path last-path)) + file-id)))) + (st/emit! (dwu/commit-undo-transaction undo-id))))) + + on-group + (mf/use-fn + (mf/deps typographies selected create-group) + (fn [event] + (dom/stop-propagation event) + (modal/show! :name-group-dialog {:accept create-group}))) + + on-rename-group + (mf/use-fn + (mf/deps typographies) + (fn [event path last-path] + (dom/stop-propagation event) + (modal/show! :name-group-dialog {:path path + :last-path last-path + :accept rename-group}))) + + on-ungroup + (mf/use-fn + (mf/deps typographies) + (fn [path] + (on-clear-selection) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id)) + (apply st/emit! + (->> typographies + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/rename-typography + file-id + (:id %) + (cmm/ungroup % path))))) + (st/emit! (dwu/commit-undo-transaction undo-id))))) + + on-context-menu + (mf/use-fn + (mf/deps selected on-clear-selection read-only?) + (fn [id event] + (dom/prevent-default event) + (let [pos (dom/get-client-position event)] + (when (not read-only?) + (when-not (contains? selected id) + (on-clear-selection)) + (swap! state assoc :id id) + (swap! menu-state cmm/open-context-menu pos))))) + + on-close-menu + (mf/use-fn + (fn [] + (swap! menu-state cmm/close-context-menu))) + + handle-rename-typography-clicked + (fn [] + (st/emit! #(assoc-in % [:workspace-global :rename-typography] (:id @state)))) + + handle-edit-typography-clicked + (fn [] + (st/emit! #(assoc-in % [:workspace-global :edit-typography] (:id @state)))) + + handle-delete-typography + (mf/use-fn + (mf/deps @state multi-typographies? multi-assets?) + (fn [] + (let [undo-id (js/Symbol)] + (if (or multi-typographies? multi-assets?) + (on-assets-delete) + (st/emit! (dwu/start-undo-transaction undo-id) + (dwl/delete-typography (:id @state)) + (dwl/sync-file file-id file-id :typographies (:id @state)) + (dwu/commit-undo-transaction undo-id)))))) + + editing-id (:edit-typography local-data) + + renaming-id (:rename-typography local-data) + + on-asset-click + (mf/use-fn + (mf/deps groups on-asset-click) + (partial on-asset-click groups))] + + (mf/use-effect + (mf/deps local-data) + (fn [] + (when (:edit-typography local-data) + (st/emit! #(update % :workspace-global dissoc :edit-typography))))) + + [:* + [:& cmm/asset-section {:file-id file-id + :title (tr "workspace.assets.typography") + :section :typographies + :assets-count (count typographies) + :open? open?} + (when local? + [:& cmm/asset-section-block {:role :title-button} + (when-not read-only? + [:button {:class (stl/css :assets-btn) + :on-click add-typography} + i/add])]) + + [:& cmm/asset-section-block {:role :content} + [:& typographies-group {:file-id file-id + :prefix "" + :groups groups + :open-groups open-groups + :force-open? force-open? + :state state + :file file + :local? local? + :selected selected + :editing-id editing-id + :renaming-id renaming-id + :local-data local-data + :on-asset-click on-asset-click + :handle-change handle-change + :apply-typography apply-typography + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :on-context-menu on-context-menu + :selected-full selected-full}] + + (if local? + [:& cmm/assets-context-menu + {:on-close on-close-menu + :state @menu-state + :options [(when-not (or multi-typographies? multi-assets?) + {:option-name (tr "workspace.assets.rename") + :id "assets-rename-typography" + :option-handler handle-rename-typography-clicked}) + + (when-not (or multi-typographies? multi-assets?) + {:option-name (tr "workspace.assets.edit") + :id "assets-edit-typography" + :option-handler handle-edit-typography-clicked}) + + {:option-name (tr "workspace.assets.delete") + :id "assets-delete-typography" + :option-handler handle-delete-typography} + + (when-not multi-assets? + {:option-name (tr "workspace.assets.group") + :id "assets-group-typography" + :option-handler on-group})]}] + + [:& cmm/assets-context-menu + {:on-close on-close-menu + :state @menu-state + :options [{:option-name "show info" + :id "assets-rename-typography" + :option-handler handle-edit-typography-clicked}]}])]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.scss new file mode 100644 index 0000000000..e0ad31a23a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.scss @@ -0,0 +1,53 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.assets-btn { + @extend .button-tertiary; + height: $s-32; + width: calc($s-24 + $s-4); + padding: 0; + border-radius: $br-8; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.assets-list { + padding: 0 0 0 $s-4; +} + +.drop-space { + height: $s-12; +} + +.grid-placeholder { + height: $s-2; + width: 100%; + background-color: var(--color-accent-primary); +} + +.typography-item { + position: relative; + display: flex; + align-items: center; + margin-bottom: $s-4; + border-radius: $br-8; + background-color: var(--assets-item-background-color); +} + +.dragging { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + border: $s-2 solid var(--assets-item-border-color-drag); + border-radius: $s-8; + background-color: var(--assets-item-background-color-drag); +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.cljs b/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.cljs index 86ef4cf188..31c9b0af59 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.cljs @@ -5,29 +5,23 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.collapsable-button - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.main.data.workspace :as dw] [app.main.store :as st] - [app.main.ui.context :as ctx] [app.main.ui.icons :as i] - [app.util.dom :as dom] [app.util.i18n :refer [tr]] [rumext.v2 :as mf])) (mf/defc collapsed-button {::mf/wrap-props false} [] - (let [new-css? (mf/use-ctx ctx/new-css-system) - on-click (mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))] - (if ^boolean new-css? - [:div {:class (dom/classnames (css :collapsed-sidebar) true)} - [:div {:class (dom/classnames (css :collapsed-title) true)} - [:button {:class (dom/classnames (css :collapsed-button) true) - :on-click on-click - :aria-label (tr "workspace.sidebar.expand")} - i/arrow-refactor]]] - [:button.collapse-sidebar.collapsed - {:on-click on-click - :aria-label (tr "workspace.sidebar.expand")} - i/arrow-slide]))) + (let [on-click (mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))] + [:div {:id "left-sidebar-aside" + :data-size "0" + :class (stl/css :collapsed-sidebar)} + [:div {:class (stl/css :collapsed-title)} + [:button {:class (stl/css :collapsed-button) + :on-click on-click + :aria-label (tr "workspace.sidebar.expand")} + i/arrow]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.css.json b/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.css.json deleted file mode 100644 index e4c9e7e0a4..0000000000 --- a/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"sidebar_collapsable_button_button-primary_qaRce","button-secondary":"sidebar_collapsable_button_button-secondary_OqDpe","button-icon":"sidebar_collapsable_button_button-icon_P4-xy","button-icon-small":"sidebar_collapsable_button_button-icon-small_lQUE3","collapsed-sidebar":"sidebar_collapsable_button_collapsed-sidebar_uQnGJ","collapsed-title":"sidebar_collapsable_button_collapsed-title_Jb62g","collapsed-button":"sidebar_collapsable_button_collapsed-button_LT5ME"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.scss b/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.scss index 4163f1d0ca..072a072a58 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.scss @@ -10,7 +10,7 @@ @include flexCenter; position: absolute; top: $s-48; - left: $s-48; + left: 0; padding: $s-4; border-radius: $br-8; background: var(--color-background-primary); @@ -30,7 +30,7 @@ border-radius: $br-5; svg { @include flexCenter; - height: $s-12; + height: $s-16; width: $s-16; color: transparent; fill: none; diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug.cljs b/frontend/src/app/main/ui/workspace/sidebar/debug.cljs index 82c099f847..56f4f61194 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/debug.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/debug.cljs @@ -5,47 +5,48 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.debug + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.main.data.workspace :as dw] [app.main.store :as st] [app.main.ui.icons :as i] + [app.util.debug :as dbg] [app.util.dom :as dom] - [debug :as dbg] [rumext.v2 :as mf])) (mf/defc debug-panel - [] + [{:keys [class] :as props}] (let [on-toggle-enabled - (mf/use-callback + (mf/use-fn (fn [event option] (dom/prevent-default event) (dom/stop-propagation event) - (if (contains? @dbg/*debug* option) - (dbg/-debug! option) - (dbg/debug! option)))) + (dbg/toggle! option) + (js* "app.main.reinit(true)"))) - close-fn - (mf/use-callback + handle-close + (mf/use-fn (fn [] (st/emit! (dw/remove-layout-flag :debug-panel))))] - [:div.debug-panel - [:div.debug-panel-header - [:div.debug-panel-close-button - {:on-click close-fn} i/close] - [:div.debug-panel-title "Debugging tools"]] - - [:div.debug-panel-inner - [:* - (for [option (sort-by d/name dbg/debug-options)] - [:div.debug-option {:key (d/name option) - :on-click #(on-toggle-enabled % option)} - [:input {:type "checkbox" - :id (d/name option) - :on-change #(on-toggle-enabled % option) - :checked (contains? @dbg/*debug* option)}] - [:div.field.check - (if (contains? @dbg/*debug* option) - [:span.checked i/checkbox-checked] - [:span.unchecked i/checkbox-unchecked])] - [:label {:for (d/name option)} (d/name option)]])]]])) + + [:div {:class (dm/str class " " (stl/css :debug-panel))} + [:div {:class (stl/css :panel-title)} + [:span "Debugging tools"] + [:div {:class (stl/css :close-button) :on-click handle-close} + i/close]] + + [:div {:class (stl/css :debug-panel-inner)} + (for [option (sort-by d/name dbg/options)] + [:div {:class (stl/css :checkbox-wrapper)} + [:span {:class (stl/css-case :checkbox-icon true :global/checked (dbg/enabled? option)) + :on-click #(on-toggle-enabled % option)} + (when (dbg/enabled? option) i/status-tick)] + + [:input {:type "checkbox" + :id (d/name option) + :key (d/name option) + :on-change #(on-toggle-enabled % option) + :checked (dbg/enabled? option)}] + [:label {:for (d/name option)} (d/name option)]])]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug.scss b/frontend/src/app/main/ui/workspace/sidebar/debug.scss new file mode 100644 index 0000000000..6cb0c14056 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/debug.scss @@ -0,0 +1,59 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "common/refactor/common-refactor.scss" as *; + +.debug-panel { + display: flex; + flex-direction: column; + background-color: var(--panel-background-color); +} + +.panel-title { + @include flexCenter; + @include uppercaseTitleTipography; + position: relative; + height: $s-32; + min-height: $s-32; + margin: $s-8 $s-8 0 $s-8; + border-radius: $br-8; + background-color: var(--panel-title-background-color); + + span { + @include flexCenter; + flex-grow: 1; + color: var(--title-foreground-color-hover); + } +} + +.close-button { + @extend .button-tertiary; + position: absolute; + right: $s-2; + top: $s-2; + height: $s-28; + width: $s-28; + border-radius: $br-6; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.checkbox-wrapper { + @extend .input-checkbox; + height: $s-32; + padding: 0; +} + +.checkbox-icon { + @extend .checkbox-icon; + cursor: pointer; +} + +.debug-panel-inner { + padding: $s-16 $s-8; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.cljs b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.cljs new file mode 100644 index 0000000000..12745448f5 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.cljs @@ -0,0 +1,152 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.sidebar.debug-shape-info + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.geom.matrix :as gmt] + [app.main.data.workspace :as dw] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.icons :as i] + [debug :as dbg] + [rumext.v2 :as mf])) + +(def display-attrs + [:type + :id + :parent-id + :frame-id + :shapes + :component-id + :component-file + :component-root + :main-instance + :shape-ref + :x + :y + :width + :height + :selrect + :points + :transform + :transform-inverse]) + +(def remove-attrs + #{:name, :remote-synced}) + +(def vertical-layout-attrs + #{}) + +(defn get-attrs + [shape] + (let [shape-attrs (->> (keys shape) + (remove (set display-attrs)) + (remove remove-attrs) + (sort-by name))] + (as-> display-attrs $ + (d/removev #(nil? (get shape %)) $) + (into $ shape-attrs)))) + +(def custom-renderer + {:parent-id :shape-link + :frame-id :shape-link + :shapes :shape-list + :shape-ref :shape-link + :transform :matrix-render + :transform-inverse :matrix-render + :selrect :rect-render + :points :points-render + :layout-grid-cells :cells-render}) + +(mf/defc shape-link + [{:keys [id objects]}] + [:a {:class (stl/css :shape-link) + :on-click #(st/emit! (dw/select-shape id))} + (dm/str (dm/get-in objects [id :name]) " #" id)]) + +(mf/defc cells-render + [{:keys [cells objects]}] + [:div {:class (stl/css :cells-render)} + (for [[id cell] cells] + + [:div {:key (dm/str "cell-" id) + :class (stl/css :cell-container)} + [:div {:class (stl/css :cell-position)} + (dm/fmt "(%, %) -> (%, %)" + (:row cell) + (:column cell) + (+ (:row cell) (dec (:row-span cell))) + (+ (:column cell) (dec (:column-span cell))))] + + [:div {:class (stl/css :cell-shape)} + (if (empty? (:shapes cell)) + [:div ""] + [:& shape-link {:id (first (:shapes cell)) :objects objects}])]])]) + +(mf/defc debug-shape-attr + [{:keys [attr value objects]}] + + (case (get custom-renderer attr) + :shape-link + [:& shape-link {:id value :objects objects}] + + :shape-list + [:div {:class (stl/css :shape-list)} + (for [id value] + [:& shape-link {:key (dm/str "child-" id) + :id id :objects objects}])] + + :matrix-render + [:div (dm/str (gmt/format-precision value 2))] + + :rect-render + [:div (dm/fmt "X:% Y:% W:% H:%" (:x value) (:y value) (:width value) (:height value))] + + :points-render + [:div {:class (stl/css :point-list)} + (for [[idx point] (d/enumerate value)] + [:div {:key (dm/str "point-" idx)} (dm/fmt "(%, %)" (:x point) (:y point))])] + + :cells-render + [:& cells-render {:cells value :objects objects}] + + [:div {:class (stl/css :attrs-container-value)} (str value)])) + +(mf/defc debug-shape-info + [] + (let [objects (mf/deref refs/workspace-page-objects) + selected (->> (mf/deref refs/selected-shapes) + (map (d/getf objects)))] + + [:div {:class (stl/css :shape-info)} + [:div {:class (stl/css :shape-info-title)} + [:span "Debug"] + [:div {:class (stl/css :close-button) + :on-click #(dbg/disable! :shape-panel)} + i/close]] + + (if (empty? selected) + [:div {:class (stl/css :attrs-container)} "No shapes selected"] + (for [[idx current] (d/enumerate selected)] + [:div {:class (stl/css :attrs-container) :key (dm/str "shape" idx)} + [:div {:class (stl/css :shape-title)} + [:div {:class (stl/css :shape-name)} (:name current)] + [:button {:on-click #(debug/dump-object (dm/str (:id current)))} "object"] + [:button {:on-click #(debug/dump-subtree (dm/str (:id current)) true)} "tree"]] + + [:div {:class (stl/css :shape-attrs)} + (let [attrs (get-attrs current)] + (for [attr attrs] + (when-let [value (get current attr)] + [:div {:class (stl/css-case :attrs-container-attr true + :vertical-layout (contains? vertical-layout-attrs attr)) + :key (dm/str "att-" idx "-" attr)} + [:div {:class (stl/css :attrs-container-name)} (d/name attr)] + + [:& debug-shape-attr {:attr attr :value value :objects objects}]])))]]))])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss new file mode 100644 index 0000000000..516a1f7058 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss @@ -0,0 +1,104 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.shape-info { + display: flex; + flex-direction: column; + background-color: var(--panel-background-color); + color: $df-primary; + font-size: $fs-12; + user-select: text; +} + +.shape-info-title { + @include flexCenter; + @include uppercaseTitleTipography; + position: relative; + height: $s-32; + min-height: $s-32; + margin: $s-8 $s-8 0 $s-8; + border-radius: $br-8; + background-color: var(--panel-title-background-color); + + span { + @include flexCenter; + flex-grow: 1; + color: var(--title-foreground-color-hover); + } +} + +.close-button { + @extend .button-tertiary; + position: absolute; + right: $s-2; + top: $s-2; + height: $s-28; + width: $s-28; + border-radius: $br-6; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.attrs-container { + padding: $s-16 $s-8; + overflow: hidden; +} + +.shape-title { + font-size: $fs-14; + padding-bottom: $s-4; + background: $db-quaternary; + color: $df-primary; + padding: $s-8; + border-radius: $s-8; + display: flex; + gap: $s-4; +} +.shape-name { + flex: 1; +} + +.attrs-container-attr { + display: grid; + grid-template-columns: 25% auto; + padding: $s-4 0; + + &.vertical-layout { + grid-template-columns: auto; + grid-template-rows: auto 1fr; + } +} + +.shape-attrs { + overflow: auto; + height: calc(100% - 8px); + padding-bottom: 8px; +} + +.shape-link { + color: $df-primary; + text-decoration: underline; +} + +.shape-list { + display: flex; + flex-direction: column; + gap: $s-4; +} + +.point-list { + display: flex; + gap: $s-8; +} + +.cell-container { + display: grid; + grid-template-columns: 100px 1fr; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/history.cljs b/frontend/src/app/main/ui/workspace/sidebar/history.cljs index 908491bf5a..38b83de94d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/history.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/history.cljs @@ -5,8 +5,12 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.history + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.data.macros :as dm] + [app.main.data.events :as ev] + [app.main.data.workspace :as dw] [app.main.data.workspace.common :as dwc] [app.main.refs :as refs] [app.main.store :as st] @@ -146,20 +150,20 @@ (defn entry->icon [{:keys [type]}] (case type - :page i/file-html - :shape i/layers - :rect i/box - :circle i/circle + :page i/document + :shape i/svg + :rect i/rectangle + :circle i/elipse :text i/text - :path i/curve - :frame i/artboard - :group i/folder - :color i/palette - :typography i/titlecase + :path i/path + :frame i/board + :group i/group + :color i/drop-icon + :typography i/text-palette :component i/component - :media i/image - :image i/image - i/layers)) + :media i/img + :image i/img + i/svg)) (defn is-shape? [type] (contains? #{:shape :rect :circle :text :path :frame :group} type)) @@ -180,7 +184,7 @@ (let [;; Group by id and type entries (->> candidates (remove nil?) - (group-by #(vector (:type %) (:operation %) (:id %)) )) + (group-by #(vector (:type %) (:operation %) (:id %)))) single? (fn [coll] (= (count coll) 1)) @@ -258,20 +262,20 @@ (let [{entries :items} (mf/deref workspace-undo) objects (mf/deref refs/workspace-page-objects)] - [:div.history-entry-detail + [:div {:class (stl/css :history-entry-detail)} (case (:operation entry) :new (:name (get-object (:detail entry) entries objects)) :delete - [:ul.history-entry-details-list + [:ul {:class (stl/css :history-entry-details-list)} (for [id (:detail entry)] (let [shape-name (:name (get-object id entries objects))] [:li {:key id} shape-name]))] :modify - [:ul.history-entry-details-list + [:ul {:class (stl/css :history-entry-details-list)} (for [[id attributes] (:detail entry)] (let [shape-name (:name (get-object id entries objects))] [:li {:key id} @@ -281,39 +285,61 @@ nil)])) (mf/defc history-entry [{:keys [locale entry idx-entry disabled? current?]}] - (let [hover? (mf/use-state false) - show-detail? (mf/use-state false)] - [:div.history-entry {:class (dom/classnames - :disabled disabled? - :current current? - :hover @hover? - :show-detail @show-detail?) - :on-pointer-enter #(reset! hover? true) - :on-pointer-leave #(reset! hover? false) - :on-click #(st/emit! (dwc/undo-to-index idx-entry))} - [:div.history-entry-summary - [:div.history-entry-summary-icon (entry->icon entry)] - [:div.history-entry-summary-text (entry->message locale entry)] - (when (:detail entry) - [:div.history-entry-summary-button {:on-click #(when (:detail entry) - (swap! show-detail? not))} - i/arrow-slide])] + {::mf/props :obj} + (let [hover? (mf/use-state false) + show-detail? (mf/use-state false) + toggle-show-detail + (mf/use-fn + (fn [event] + (let [has-entry? (-> (dom/get-current-target event) + (dom/get-data "has-entry") + (parse-boolean))] + (dom/stop-propagation event) + (when has-entry? + (swap! show-detail? not)))))] + [:div {:class (stl/css-case :history-entry true + :disabled disabled? + :current current? + :hover @hover? + :show-detail @show-detail?) + :on-pointer-enter #(reset! hover? true) + :on-pointer-leave #(reset! hover? false) + :on-click #(st/emit! (dwc/undo-to-index idx-entry))} - (when show-detail? + [:div {:class (stl/css :history-entry-summary)} + [:div {:class (stl/css :history-entry-summary-icon)} + (entry->icon entry)] + [:div {:class (stl/css :history-entry-summary-text)} (entry->message locale entry)] + (when (:detail entry) + [:div {:class (stl/css-case :history-entry-summary-button true + :button-opened @show-detail?) + :on-click toggle-show-detail + :data-has-entry (dm/str (not (nil? (:detail entry))))} + i/arrow])] + + (when @show-detail? [:& history-entry-details {:entry entry}])])) (mf/defc history-toolbox [] (let [locale (mf/deref i18n/locale) objects (mf/deref refs/workspace-page-objects) {:keys [items index]} (mf/deref workspace-undo) - entries (parse-entries items objects)] - [:div.history-toolbox - [:div.history-toolbox-title (t locale "workspace.undo.title")] + entries (parse-entries items objects) + toggle-history + (mf/use-fn + #(st/emit! (-> (dw/toggle-layout-flag :document-history) + (vary-meta assoc ::ev/origin "history-toolbox"))))] + [:div {:class (stl/css :history-toolbox)} + [:div {:class (stl/css :history-toolbox-title)} + [:span (t locale "workspace.undo.title")] + [:div {:class (stl/css :close-button) + :on-click toggle-history} + i/close]] (if (empty? entries) - [:div.history-entry-empty - [:div.history-entry-empty-icon i/recent] - [:div.history-entry-empty-msg (t locale "workspace.undo.empty")]] - [:ul.history-entries + [:div {:class (stl/css :history-entry-empty)} + [:div {:class (stl/css :history-entry-empty-icon)} i/history] + [:div {:class (stl/css :history-entry-empty-msg)} (t locale "workspace.undo.empty")]] + [:ul {:class (stl/css :history-entries)} (for [[idx-entry entry] (->> entries (map-indexed vector) reverse)] #_[i (range 0 10)] [:& history-entry {:key (str "entry-" idx-entry) :locale locale @@ -322,3 +348,5 @@ :current? (= idx-entry index) :disabled? (> idx-entry index)}])])])) + + diff --git a/frontend/src/app/main/ui/workspace/sidebar/history.scss b/frontend/src/app/main/ui/workspace/sidebar/history.scss new file mode 100644 index 0000000000..9292059016 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/history.scss @@ -0,0 +1,149 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.history-toolbox { + display: flex; + flex-direction: column; + background-color: var(--panel-background-color); +} + +.history-toolbox-title { + @include flexCenter; + @include uppercaseTitleTipography; + position: relative; + height: $s-32; + min-height: $s-32; + margin: $s-8 $s-8 0 $s-8; + border-radius: $br-8; + background-color: var(--panel-title-background-color); + + span { + @include flexCenter; + flex-grow: 1; + color: var(--title-foreground-color-hover); + } +} + +.close-button { + @extend .button-tertiary; + position: absolute; + right: $s-2; + top: $s-2; + height: $s-28; + width: $s-28; + border-radius: $br-6; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.history-entry-empty { + @include flexCenter; + flex-direction: column; + gap: $s-16; + padding: $s-28 $s-16; + text-align: center; +} + +.history-entry-empty-icon { + @extend .empty-icon; + svg { + margin-left: calc(-1 * $s-2); + } +} + +.history-entry-empty-msg { + @include bodySmallTypography; + color: var(--empty-message-foreground-color); +} + +.history-entries { + height: calc(100vh - $s-100); + padding: $s-12; + overflow-x: hidden; + overflow-y: auto; + font-size: $fs-12; +} + +.history-entry { + display: flex; + justify-content: center; + flex-direction: column; + min-height: $s-32; + margin: $s-4; + padding: $s-4 $s-8; + border: $s-2 solid transparent; + border-radius: $s-8; + background-color: var(--entry-background-color); + cursor: pointer; + transition: border 0.2s; + + .history-entry-summary { + display: flex; + align-items: center; + .history-entry-summary-icon { + svg { + @extend .button-icon-small; + stroke: var(--entry-foreground-color); + } + } + .history-entry-summary-text { + margin: 0 $s-8; + color: $df-primary; + } + .history-entry-summary-button { + opacity: $op-0; + margin-left: auto; + &.button-opened { + svg { + transform: rotate(90deg); + } + } + svg { + @extend .button-icon-small; + stroke: var(--entry-foreground-color); + } + } + } + + .history-entry-detail { + display: block; + padding-top: $s-16; + color: var(--modal-text-foreground-color); + .history-entry-details-list { + margin: 0; + } + } + + &.disabled { + border-color: var(--entry-border-color-disabled); + background-color: var(--entry-background-color-disabled); + } + + &.hover, + &:hover { + background-color: var(--entry-background-color-hover); + color: var(--entry-foreground-color-hover); + .history-entry-summary { + .history-entry-summary-icon { + svg { + stroke: var(--entry-foreground-color-hover); + } + } + .history-entry-summary-button { + opacity: $op-10; + &.button-opened { + svg { + transform: rotate(90deg); + } + } + } + } + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs index 25bb03405d..a2e008fa92 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs @@ -5,18 +5,20 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.layer-item - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] - [app.common.pages.helpers :as cph] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] [app.main.data.workspace.collapse :as dwc] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.shape-icon :as si] - [app.main.ui.components.shape-icon-refactor :as sic] + [app.main.ui.components.shape-icon :as sic] [app.main.ui.context :as ctx] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] @@ -25,37 +27,160 @@ [app.util.i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.timers :as ts] - [beicon.core :as rx] + [beicon.v2.core :as rx] [okulary.core :as l] [rumext.v2 :as mf])) +(mf/defc layer-item-inner + {::mf/wrap-props false + ::mf/forward-ref true} + [{:keys [item depth parent-size name-ref children + ;; Flags + read-only? highlighted? selected? component-tree? + filtered? expanded? dnd-over? dnd-over-top? dnd-over-bot? hide-toggle? + ;; Callbacks + on-select-shape on-context-menu on-pointer-enter on-pointer-leave on-zoom-to-selected + on-toggle-collapse on-enable-drag on-disable-drag on-toggle-visibility on-toggle-blocking]} + dref] + + (let [id (:id item) + name (:name item) + blocked? (:blocked item) + hidden? (:hidden item) + has-shapes? (-> item :shapes seq boolean) + touched? (-> item :touched seq boolean) + parent-board? (and (cfh/frame-shape? item) + (= uuid/zero (:parent-id item))) + absolute? (ctl/item-absolute? item) + components-v2 (mf/use-ctx ctx/components-v2) + main-instance? (or (not components-v2) (:main-instance item))] + [:* + [:div {:id id + :ref dref + :on-click on-select-shape + :on-context-menu on-context-menu + :class (stl/css-case + :layer-row true + :highlight highlighted? + :component (ctk/instance-head? item) + :masked (:masked-group item) + :selected selected? + :type-frame (cfh/frame-shape? item) + :type-bool (cfh/bool-shape? item) + :type-comp component-tree? + :hidden hidden? + :dnd-over dnd-over? + :dnd-over-top dnd-over-top? + :dnd-over-bot dnd-over-bot? + :root-board parent-board?)} + [:span {:class (stl/css-case + :tab-indentation true + :filtered filtered?) + :style {"--depth" depth}}] + [:div {:class (stl/css-case + :element-list-body true + :filtered filtered? + :selected selected? + :icon-layer (= (:type item) :icon)) + :style {"--depth" depth} + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-double-click dom/stop-propagation} + + (if (< 0 (count (:shapes item))) + [:div {:class (stl/css :button-content)} + (when (and (not hide-toggle?) (not filtered?)) + [:button {:class (stl/css-case + :toggle-content true + :inverse expanded?) + :on-click on-toggle-collapse} + i/arrow]) + + [:div {:class (stl/css :icon-shape) + :on-double-click on-zoom-to-selected} + (when absolute? + [:div {:class (stl/css :absolute)}]) + + [:& sic/element-icon + {:shape item + :main-instance? main-instance?}]]] + + [:div {:class (stl/css :button-content)} + (when (not ^boolean filtered?) + [:span {:class (stl/css :toggle-content)}]) + [:div {:class (stl/css :icon-shape) + :on-double-click on-zoom-to-selected} + (when ^boolean absolute? + [:div {:class (stl/css :absolute)}]) + [:& sic/element-icon + {:shape item + :main-instance? main-instance?}]]]) + + [:& layer-name {:ref name-ref + :shape-id id + :shape-name name + :shape-touched? touched? + :disabled-double-click read-only? + :on-start-edit on-disable-drag + :on-stop-edit on-enable-drag + :depth depth + :parent-size parent-size + :selected? selected? + :type-comp component-tree? + :type-frame (cfh/frame-shape? item) + :hidden? hidden?}] + + (when (not read-only?) + [:div {:class (stl/css-case + :element-actions true + :is-parent has-shapes? + :selected hidden? + :selected blocked?)} + [:button {:class (stl/css-case + :toggle-element true + :selected hidden?) + :title (if hidden? + (tr "workspace.shape.menu.show") + (tr "workspace.shape.menu.hide")) + :on-click on-toggle-visibility} + (if ^boolean hidden? i/hide i/shown)] + [:button {:class (stl/css-case + :block-element true + :selected blocked?) + :title (if (:blocked item) + (tr "workspace.shape.menu.unlock") + (tr "workspace.shape.menu.lock")) + :on-click on-toggle-blocking} + (if ^boolean blocked? i/lock i/unlock)]])]] + + children])) (mf/defc layer-item - [{:keys [index item selected objects sortable? filtered? recieved-depth parent-size component-child?] :as props}] - (let [id (:id item) - blocked? (:blocked item) - hidden? (:hidden item) + {::mf/props :obj + ::mf/memo true} + [{:keys [index item selected objects sortable? filtered? depth parent-size component-child? highlighted]}] + (let [id (:id item) + blocked? (:blocked item) + hidden? (:hidden item) - disable-drag (mf/use-state false) - scroll-to-middle? (mf/use-var true) - expanded-iref (mf/with-memo [id] - (-> (l/in [:expanded id]) - (l/derived refs/workspace-local))) + drag-disabled* (mf/use-state false) + drag-disabled? (deref drag-disabled*) - expanded? (mf/deref expanded-iref) - selected? (contains? selected id) - container? (or (cph/frame-shape? item) - (cph/group-shape? item)) - absolute? (ctl/layout-absolute? item) + scroll-to-middle? (mf/use-var true) + expanded-iref (mf/with-memo [id] + (-> (l/in [:expanded id]) + (l/derived refs/workspace-local))) + expanded? (mf/deref expanded-iref) - components-v2 (mf/use-ctx ctx/components-v2) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) - new-css-system (mf/use-ctx ctx/new-css-system) - main-instance? (if components-v2 - (:main-instance? item) - true) - parent-board? (and (= :frame (:type item)) - (= uuid/zero (:parent-id item))) + selected? (contains? selected id) + highlighted? (contains? highlighted id) + + container? (or (cfh/frame-shape? item) + (cfh/group-shape? item)) + + read-only? (mf/use-ctx ctx/workspace-read-only?) + parent-board? (and (cfh/frame-shape? item) + (= uuid/zero (:parent-id item))) toggle-collapse (mf/use-fn (mf/deps expanded?) @@ -108,22 +233,22 @@ on-pointer-enter (mf/use-fn (mf/deps id) - (fn [_event] + (fn [_] (st/emit! (dw/highlight-shape id)))) on-pointer-leave (mf/use-fn (mf/deps id) - (fn [_event] + (fn [_] (st/emit! (dw/dehighlight-shape id)))) on-context-menu (mf/use-fn - (mf/deps item workspace-read-only?) + (mf/deps item read-only?) (fn [event] (dom/prevent-default event) (dom/stop-propagation event) - (when-not workspace-read-only? + (when-not read-only? (let [pos (dom/get-client-position event)] (st/emit! (dw/show-shape-context-menu {:position pos :shape item})))))) @@ -136,13 +261,34 @@ on-drop (mf/use-fn - (mf/deps id index objects) + (mf/deps id index objects expanded? selected) (fn [side _data] - (if (= side :center) - (st/emit! (dw/relocate-selected-shapes id 0)) - (let [to-index (if (= side :top) (inc index) index) - parent-id (cph/get-parent-id objects id)] - (st/emit! (dw/relocate-selected-shapes parent-id to-index)))))) + (let [single? (= (count selected) 1) + same? (and single? (= (first selected) id))] + (when-not same? + (let [shape (get objects id) + + parent-id + (cond + (= side :center) + id + + (and expanded? (= side :bot) (d/not-empty? (:shapes shape))) + id + + :else + (cfh/get-parent-id objects id)) + + [parent-id _] (ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected)) + + parent (get objects parent-id) + + to-index (cond + (= side :center) 0 + (and expanded? (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent)) + (= side :top) (inc index) + :else index)] + (st/emit! (dw/relocate-selected-shapes parent-id to-index))))))) on-hold (mf/use-fn @@ -151,22 +297,35 @@ (when-not expanded? (st/emit! (dwc/toggle-collapse id))))) + zoom-to-selected + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + (st/emit! dw/zoom-to-selected-shape))) + [dprops dref] (hooks/use-sortable :data-type "penpot/layer" :on-drop on-drop :on-drag on-drag :on-hold on-hold - :disabled @disable-drag + :disabled drag-disabled? :detect-center? container? :data {:id (:id item) :index index :name (:name item)} - :draggable? (and sortable? (not workspace-read-only?))) + :draggable? (and + sortable? + (not read-only?) + (not (ctn/has-any-copy-parent? objects item)))) ;; We don't want to change the structure of component copies - ref (mf/use-ref) - depth (+ recieved-depth 1) - component-tree? (or component-child? (:component-root? item))] + ref (mf/use-ref) + depth (+ depth 1) + component-tree? (or component-child? (ctk/instance-root? item) (ctk/instance-head? item)) + + enable-drag (mf/use-fn #(reset! drag-disabled* false)) + disable-drag (mf/use-fn #(reset! drag-disabled* true))] (mf/with-effect [selected? selected] (let [single? (= (count selected) 1) @@ -183,184 +342,60 @@ (let [scroll-to @scroll-to-middle?] (ts/schedule 100 - #(let [scroll-distance-ratio (dom/get-scroll-distance-ratio node scroll-node) - scroll-behavior (if (> scroll-distance-ratio 1) "instant" "smooth")] - (if scroll-to - (dom/scroll-into-view! first-child-node #js {:block "center" :behavior scroll-behavior :inline "start"}) - (do - (dom/scroll-into-view-if-needed! first-child-node #js {:block "center" :behavior scroll-behavior :inline "start"}) - (reset! scroll-to-middle? true)))))))] + #(when (and node scroll-node) + (let [scroll-distance-ratio (dom/get-scroll-distance-ratio node scroll-node) + scroll-behavior (if (> scroll-distance-ratio 1) "instant" "smooth")] + (if scroll-to + (dom/scroll-into-view! first-child-node #js {:block "center" :behavior scroll-behavior :inline "start"}) + (do + (dom/scroll-into-view-if-needed! first-child-node #js {:block "center" :behavior scroll-behavior :inline "start"}) + (reset! scroll-to-middle? true))))))))] #(when (some? subid) (rx/dispose! subid)))) - (if new-css-system - [:* - [:div {:on-context-menu on-context-menu - :ref dref - :on-click select-shape - :id id - :class (dom/classnames - (css :layer-row) true - (css :component) (not (nil? (:component-id item))) - (css :masked) (:masked-group? item) - (css :selected) selected? - (css :type-frame) (= :frame (:type item)) - (css :type-bool) (= :bool (:type item)) - (css :type-comp) component-tree? - (css :hidden) (:hidden item) - :dnd-over (= (:over dprops) :center) - :dnd-over-top (= (:over dprops) :top) - :dnd-over-bot (= (:over dprops) :bot) - :root-board parent-board?)} - [:span {:class (dom/classnames (css :tab-indentation) true - (css :filtered) filtered?) - :style #js {"--depth" depth}}] - [:div {:class (dom/classnames (css :element-list-body) true - (css :filtered) filtered? - (css :selected) selected? - (css :icon-layer) (= (:type item) :icon)) - :style #js {"--depth" depth} - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave - :on-double-click #(dom/stop-propagation %)} + [:& layer-item-inner + {:ref dref + :item item + :depth depth + :parent-size parent-size + :name-ref ref + :read-only? read-only? + :highlighted? highlighted? + :selected? selected? + :component-tree? component-tree? + :filtered? filtered? + :expanded? expanded? + :dnd-over? (= (:over dprops) :center) + :dnd-over-top? (= (:over dprops) :top) + :dnd-over-bot? (= (:over dprops) :bot) + :on-select-shape select-shape + :on-context-menu on-context-menu + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-zoom-to-selected zoom-to-selected + :on-toggle-collapse toggle-collapse + :on-enable-drag enable-drag + :on-disable-drag disable-drag + :on-toggle-visibility toggle-visibility + :on-toggle-blocking toggle-blocking} - (if (< 0 (count (:shapes item))) - [:div {:class (dom/classnames (css :button-content) true)} - (when (not filtered?) - [:button {:class (dom/classnames (css :toggle-content) true - (css :inverse) expanded?) - :on-click toggle-collapse} - i/arrow-refactor]) - - [:div {:class (dom/classnames (css :icon-shape) true) - :on-double-click #(do (dom/stop-propagation %) - (dom/prevent-default %) - (st/emit! dw/zoom-to-selected-shape))} - (when absolute? - [:div {:class (dom/classnames (css :absolute) true)} ]) - [:& sic/element-icon-refactor {:shape item - :main-instance? main-instance?}]]] - [:div {:class (dom/classnames (css :button-content) true)} - (when (not filtered?) - [:span {:class (dom/classnames (css :toggle-content) true)}]) - [:div {:class (dom/classnames (css :icon-shape) true) - :on-double-click #(do (dom/stop-propagation %) - (dom/prevent-default %) - (st/emit! dw/zoom-to-selected-shape))} - (when absolute? - [:div {:class (dom/classnames (css :absolute) true)} ]) - [:& sic/element-icon-refactor {:shape item - :main-instance? main-instance?}]]]) - - [:& layer-name {:shape item - :name-ref ref - :disabled-double-click workspace-read-only? - :on-start-edit #(reset! disable-drag true) - :on-stop-edit #(reset! disable-drag false) - :depth depth - :parent-size parent-size - :selected? selected? - :type-comp component-tree? - :type-frame (= :frame (:type item)) - :hidden (:hidden item)}] - [:div {:class (dom/classnames (css :element-actions) true - (css :is-parent) (:shapes item) - (css :selected) (:hidden item) - (css :selected) (:blocked item))} - [:button {:class (dom/classnames (css :toggle-element) true - (css :selected) (:hidden item)) - :title (if (:hidden item) - (tr "workspace.shape.menu.show") - (tr "workspace.shape.menu.hide")) - :on-click toggle-visibility} - (if (:hidden item) i/hide-refactor i/shown-refactor)] - [:button {:class (dom/classnames (css :block-element) true - (css :selected) (:blocked item)) - :title (if (:blocked item) - (tr "workspace.shape.menu.unlock") - (tr "workspace.shape.menu.lock")) - :on-click toggle-blocking} - (if (:blocked item) i/lock-refactor i/unlock-refactor)]]]] - (when (and (:shapes item) expanded?) - [:div {:class (dom/classnames (css :element-children) true - (css :parent-selected) selected? - :sticky-children parent-board?) - :data-id (when parent-board? (:id item))} - (for [[index id] (reverse (d/enumerate (:shapes item)))] - (when-let [item (get objects id)] - [:& layer-item - {:item item - :selected selected - :index index - :objects objects - :key (:id item) - :sortable? sortable? - :recieved-depth depth - :parent-size parent-size - :component-child? component-tree?}]))])] - [:li {:on-context-menu on-context-menu - :ref dref - :class (dom/classnames - :component (not (nil? (:component-id item))) - :masked (:masked-group? item) - :dnd-over (= (:over dprops) :center) - :dnd-over-top (= (:over dprops) :top) - :dnd-over-bot (= (:over dprops) :bot) - :selected selected? - :type-frame (= :frame (:type item)))} - - [:div.element-list-body {:class (dom/classnames :selected selected? - :icon-layer (= (:type item) :icon)) - :on-click select-shape - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave - :on-double-click #(dom/stop-propagation %)} - - [:div.icon {:on-double-click #(do (dom/stop-propagation %) - (dom/prevent-default %) - (st/emit! dw/zoom-to-selected-shape))} - (when absolute? - [:div.absolute i/position-absolute]) - [:& si/element-icon {:shape item - :main-instance? main-instance?}]] - [:& layer-name {:shape item - :name-ref ref - :disabled-double-click workspace-read-only? - :on-start-edit #(reset! disable-drag true) - :on-stop-edit #(reset! disable-drag false) - :selected? selected? - :type-comp component-tree? - :type-frame (= :frame (:type item)) - :hidden (:hidden item)}] - - [:div.element-actions {:class (when (:shapes item) "is-parent")} - [:div.toggle-element {:class (when (:hidden item) "selected") - :title (if (:hidden item) - (tr "workspace.shape.menu.show") - (tr "workspace.shape.menu.hide")) - :on-click toggle-visibility} - (if (:hidden item) i/eye-closed i/eye)] - [:div.block-element {:class (when (:blocked item) "selected") - :on-click toggle-blocking - :title (if (:blocked item) - (tr "workspace.shape.menu.unlock") - (tr "workspace.shape.menu.lock"))} - (if (:blocked item) i/lock i/unlock)]] - - (when (:shapes item) - (when (not filtered?) [:span.toggle-content - {:on-click toggle-collapse - :class (when expanded? "inverse")} - i/arrow-slide]))] - (when (and (:shapes item) expanded?) - [:ul.element-children - (for [[index id] (reverse (d/enumerate (:shapes item)))] - (when-let [item (get objects id)] - [:& layer-item - {:item item - :selected selected - :index index - :objects objects - :key (:id item) - :sortable? sortable?}]))])]))) + (when (and (:shapes item) expanded?) + [:div {:class (stl/css-case + :element-children true + :parent-selected selected? + :sticky-children parent-board?) + :data-id (when ^boolean parent-board? id)} + (for [[index id] (reverse (d/enumerate (:shapes item)))] + (when-let [item (get objects id)] + [:& layer-item + {:item item + :highlighted highlighted + :selected selected + :index index + :objects objects + :key (dm/str id) + :sortable? sortable? + :depth depth + :parent-size parent-size + :component-child? component-tree?}]))])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.css.json b/frontend/src/app/main/ui/workspace/sidebar/layer_item.css.json deleted file mode 100644 index 96e75da537..0000000000 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"sidebar_layer_item_button-primary_74ST4","button-secondary":"sidebar_layer_item_button-secondary_e4u9V","button-icon":"sidebar_layer_item_button-icon_-D7KH","button-icon-small":"sidebar_layer_item_button-icon-small_1RfDl","layer-row":"sidebar_layer_item_layer-row_KibLX","element-list-body":"sidebar_layer_item_element-list-body_832JO","element-actions":"sidebar_layer_item_element-actions_ACGJI","toggle-element":"sidebar_layer_item_toggle-element_4bhRW","block-element":"sidebar_layer_item_block-element_RhKz-","button-content":"sidebar_layer_item_button-content_bPwop","icon-shape":"sidebar_layer_item_icon-shape_g9Wxn","toggle-content":"sidebar_layer_item_toggle-content_MKhsv","filtered":"sidebar_layer_item_filtered_V5CHf","inverse":"sidebar_layer_item_inverse_zzZ54","absolute":"sidebar_layer_item_absolute_mYIKg","selected":"sidebar_layer_item_selected_O7P-j","element-children":"sidebar_layer_item_element-children_3iA4Q","parent-selected":"sidebar_layer_item_parent-selected_uIIyQ","hidden":"sidebar_layer_item_hidden_JRbJO","type-comp":"sidebar_layer_item_type-comp_FBSRt","tab-indentation":"sidebar_layer_item_tab-indentation_e-2dQ"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss b/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss index d43daf24d4..124ee6f48e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss @@ -12,472 +12,246 @@ align-items: center; width: 100%; background-color: var(--layer-row-background-color); + border: $s-2 solid transparent; - .element-list-body { - display: flex; - align-items: center; - height: $s-32; - width: calc(100% - (var(--depth) * var(--layer-indentation-size))); - padding-right: $s-12; - - &.filtered { - width: calc(100% - $s-12); - } - .button-content { - display: flex; - height: 100%; - .toggle-content { - @include buttonStyle; - display: grid; - grid-template-columns: 1fr 1fr; - align-items: center; - height: 100%; - width: $s-24; - padding: 0 4px 0 8px; - svg { - @extend .button-icon-small; - } - &.inverse { - svg { - transform: rotate(90deg); - } - .icon-shape { - transform: rotate(-90deg); - } - } - } - .icon-shape { - @include flexCenter; - @include buttonStyle; - position: relative; - justify-self: flex-end; - width: $s-16; - height: 100%; - width: $s-24; - padding: 0 $s-8 0 $s-4; - svg { - @extend .button-icon-small; - } - - .absolute { - position: absolute; - background-color: var(--layer-row-foreground-color); - opacity: $op-4; - width: $s-12; - height: $s-12; - border-radius: $br-2; - } - } - } - .element-actions { - display: none; - height: 100%; - .toggle-element, - .block-element { - @include buttonStyle; - @include flexCenter; - height: 100%; - width: $s-24; - margin: 0; - display: none; - svg { - @extend .button-icon-small; - } - } - &.selected { - display: flex; - .toggle-element, - .block-element { - display: flex; - opacity: $op-0; - &.selected { - opacity: $op-10; - } - } - } - } - } - .element-children { - width: 100%; - ul { - margin-bottom: 0; - } - &.parent-selected { - .layer-row { - background-color: var(--layer-child-row-background-color); - } - } - } - &.hidden { - .element-list-body { - .button-content { - .toggle-content { - svg { - opacity: $op-7; - } - } - .icon-shape { - svg { - opacity: $op-7; - } - .absolute { - opacity: $op-1; - } - } - } - .element-actions { - .toggle-element, - .block-element { - svg { - opacity: $op-7; - } - } - } - } - } + &.highlight, &:hover { --context-hover-color: var(--layer-row-foreground-color-hover); --context-hover-opacity: $op-10; background-color: var(--layer-row-background-color-hover); + color: var(--layer-row-foreground-color-hover); + box-shadow: $s-16 $s-0 $s-0 $s-0 var(--layer-row-background-color-hover); &.hidden { opacity: $op-10; } - .element-list-body { - .button-content { - .toggle-content { - background-color: var(--layer-row-background-color-hover); - svg { - opacity: $op-10; - stroke: var(--layer-row-foreground-color-hover); - } - } - .icon-shape { - opacity: $op-10; - svg { - stroke: var(--layer-row-foreground-color-hover); - } - & .absolute { - opacity: $op-4; - background-color: var(--layer-row-foreground-color-hover); - } - } - } - .element-actions { - display: flex; - .toggle-element, - .block-element { - display: flex; - svg { - opacity: $op-10; - stroke: var(--layer-row-foreground-color-hover); - } - } - &.selected { - .toggle-element, - .block-element { - opacity: $op-10; - } - } - } - } - .element-children { - .layer-row { - background-color: transparent; - color: var(--layer-row-foreground-color-hover); - &:hover { - background-color: var(--layer-row-background-color-hover); - } - } - } } + &.selected { background-color: var(--layer-row-background-color-selected); - .element-list-body { - .button-content { - .toggle-content { - background-color: var(--layer-row-background-color-selected); - svg { - stroke: var(--layer-row-foreground-color-selected); - } - } - .icon-shape { - svg { - stroke: var(--layer-row-foreground-color-selected); - } - .absolute { - background-color: var(--layer-row-foreground-color-selected); - } - } - } - .element-actions { - .toggle-element, - .block-element { - display: flex; - svg { - stroke: var(--layer-row-foreground-color-selected); - } - } - &.selected { - .toggle-element, - .block-element { - display: flex; - opacity: $op-10; - &.selected { - opacity: $op-10; - } - } - } - } - } - .element-children { - background-color: transparent; - color: var(--layer-row-foreground-color-selected); - &:hover { - background-color: var(--layer-row-background-color-selected); - } - } - &:hover { - background-color: var(--layer-row-background-color-selected); - } + box-shadow: $s-16 $s-0 $s-0 $s-0 var(--layer-row-background-color-selected); } - &.type-comp { - .button-content { - .toggle-content { - svg { - stroke: var(--layer-row-component-foreground-color); - } - } - .icon-shape { - svg { - stroke: var(--layer-row-component-foreground-color); - } - .absolute { - background-color: var(--layer-row-component-foreground-color); - } - } - } - .element-actions { - .toggle-element, - .block-element { - svg { - stroke: var(--layer-row-component-foreground-color); - } - } - } - .element-children { - color: var(--layer-row-component-foreground-color); - } - &.hidden { - .element-list-body { - .button-content { - .toggle-content { - opacity: $op-7; - } - .icon-shape { - opacity: $op-7; - .absolute { - opacity: $op-1; - } - } - } - .element-actions { - .toggle-element, - .block-element { - svg { - opacity: $op-7; - } - } - } - } - &:hover { - .element-list-body { - .button-content { - .toggle-content { - opacity: $op-10; - svg { - stroke: var(--layer-row-foreground-color-hover); - } - } - .icon-shape { - opacity: $op-10; - svg { - stroke: var(--layer-row-foreground-color-hover); - } - & .absolute { - opacity: $op-4; - } - } - } - .element-actions { - .toggle-element, - .block-element { - svg { - opacity: $op-10; - stroke: var(--layer-row-foreground-color-hover); - } - } - } - } - } - } + + &.selected.highlight, + &.selected:hover { + background-color: var(--layer-row-background-color-selected); } - &.type-comp.selected { - .button-content { - .toggle-content { - svg { - stroke: var(--layer-row-component-foreground-color); - } - } - .icon-shape { - svg { - stroke: var(--layer-row-component-foreground-color); - } - .absolute { - background-color: var(--layer-row-component-foreground-color); - } - } - } - .element-actions { - .toggle-element, - .block-element { - svg { - stroke: var(--layer-row-component-foreground-color); - } - } - } - .element-children { - color: var(--layer-row-component-foreground-color); - } - &.hidden { - .element-list-body { - .button-content { - .toggle-content { - opacity: $op-7; - } - .icon-shape { - opacity: $op-7; - .absolute { - opacity: $op-1; - } - } - } - .element-actions { - .toggle-element, - .block-element { - svg { - opacity: $op-7; - } - } - } - } - &:hover { - .element-list-body { - .button-content { - .toggle-content { - opacity: $op-10; - svg { - stroke: var(--layer-row-foreground-color-hover); - } - } - .icon-shape { - opacity: $op-10; - & .absolute { - opacity: $op-4; - } - } - } - .element-actions { - .toggle-element, - .block-element { - svg { - opacity: $op-10; - stroke: var(--layer-row-foreground-color-hover); - } - } - } - } - } - } - &:hover { - .element-list-body { - .button-content { - .toggle-content { - svg { - stroke: var(--layer-row-foreground-color-hover); - } - } - .icon-shape { - svg { - stroke: var(--layer-row-foreground-color-hover); - } - } - } - .element-actions { - .toggle-element, - .block-element { - svg { - stroke: var(--layer-row-foreground-color-hover); - } - } - } - } - } + + .parent-selected & { + background-color: var(--layer-child-row-background-color); + box-shadow: $s-16 $s-0 $s-0 $s-0 var(--layer-child-row-background-color); } - &:global(.sticky) { - position: sticky; - top: 0px; - z-index: 3; + + .parent-selected &.highlight, + .parent-selected &:hover { + background-color: var(--layer-row-background-color-hover); + box-shadow: $s-16 $s-0 $s-0 $s-0 var(--layer-row-background-color-hover); + } + + &.dnd-over-bot { + border-bottom: $s-2 solid var(--layer-row-foreground-color-hover); + } + &.dnd-over-top { + border-top: $s-2 solid var(--layer-row-foreground-color-hover); + } + &.dnd-over { + border: $s-2 solid var(--layer-row-foreground-color-hover); } } -.parent-selected .layer-row { - background-color: var(--layer-child-row-background-color); - &:hover { - background-color: var(--layer-row-background-color-hover); - &.hidden { + +.element-children { + .layer-row.highlight &, + .layer-row:hover & { + background-color: var(--layer-row-background-color-selected); + box-shadow: $s-16 $s-0 $s-0 $s-0 var(--layer-row-background-color-selected); + } + .layer-row.type-comp & { + color: var(--layer-row-component-foreground-color); + } + .layer-row.selected & { + background-color: transparent; + color: var(--layer-row-foreground-color-selected); + } +} + +.element-list-body { + align-items: center; + display: grid; + grid-template-columns: auto 1fr auto; + column-gap: $s-4; + height: $s-32; + width: calc(100% - (var(--depth) * var(--layer-indentation-size))); + cursor: pointer; + + &.filtered { + width: calc(100% - $s-12); + } +} + +.element-actions { + display: none; + height: 100%; + &.selected { + display: flex; + } + .layer-row.highlight &, + .layer-row:hover & { + display: flex; + } +} + +.button-content { + display: flex; + height: 100%; +} + +.icon-shape { + @include flexCenter; + @include buttonStyle; + position: relative; + justify-self: flex-end; + width: $s-16; + height: 100%; + width: $s-24; + padding-inline-start: $s-4; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + + .layer-row.selected & { + stroke: var(--layer-row-foreground-color-selected); + } + .layer-row.type-comp & { + stroke: var(--layer-row-component-foreground-color); + } + } + .inverse & { + transform: rotate(-90deg); + } + .layer-row.hidden & { + opacity: $op-7; + } + .layer-row.highlight &, + .layer-row:hover & { + opacity: $op-10; + svg { + stroke: var(--layer-row-foreground-color-hover); + } + } +} + +.absolute { + position: absolute; + background-color: var(--layer-row-foreground-color); + opacity: $op-4; + width: $s-12; + height: $s-12; + border-radius: $br-2; + + .layer-row.hidden & { + opacity: $op-1; + } + .layer-row.type-comp & { + background-color: var(--layer-row-component-foreground-color); + } + .layer-row.highlight &, + .layer-row:hover & { + opacity: $op-4; + background-color: var(--layer-row-foreground-color-hover); + } + .layer-row.selected & { + background-color: var(--layer-row-foreground-color-selected); + } +} + +.toggle-content { + @include buttonStyle; + display: grid; + grid-template-columns: 1fr 1fr; + align-items: center; + height: 100%; + width: $s-24; + padding-inline-start: $s-8; + + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + + .layer-row.hidden & { + opacity: $op-7; + } + .layer-row.selected & { + stroke: var(--layer-row-foreground-color-selected); + } + .layer-row.type-comp & { + stroke: var(--layer-row-component-foreground-color); + } + .layer-row.highlight &, + .layer-row:hover & { + opacity: $op-10; + stroke: var(--layer-row-foreground-color-hover); + } + } + + .layer-row.selected & { + background-color: var(--layer-row-background-color-selected); + } + &.inverse svg { + transform: rotate(90deg); + } +} + +.toggle-element, +.block-element { + @include buttonStyle; + @include flexCenter; + height: 100%; + width: $s-24; + margin: 0; + display: none; + + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + + .layer-row.hidden & { + opacity: $op-7; + } + .type-comp & { + stroke: var(--layer-row-component-foreground-color); + } + } + + .element-actions.selected & { + display: flex; + opacity: $op-0; + + &.selected { opacity: $op-10; } - .element-list-body { - .button-content { - .toggle-content { - background-color: var(--layer-row-background-color-hover); - svg { - stroke: var(--layer-row-foreground-color-hover); - } - } - .icon-shape { - svg { - stroke: var(--layer-row-foreground-color-hover); - } - .absolute { - background-color: var(--layer-row-foreground-color-hover); - } - } - } - .element-actions { - .toggle-element, - .block-element { - display: flex; - svg { - stroke: var(--layer-row-foreground-color-hover); - } - } - &.selected { - .toggle-element, - .block-element { - opacity: $op-10; - } - } - } + } + + .layer-row.highlight &, + .layer-row:hover & { + display: flex; + svg { + opacity: $op-10; + stroke: var(--layer-row-foreground-color-hover); } - .element-children :global(.layer-row) { - background-color: transparent; - color: var(--layer-row-foreground-color-hover); - &:hover { - background-color: var(--layer-row-background-color-hover); - } + } + .layer-row.selected & { + display: flex; + svg { + stroke: var(--layer-row-foreground-color-selected); } } } + +:global(.sticky) { + position: sticky; + top: $s-0; + z-index: $z-index-4; +} + .tab-indentation { display: block; height: $s-16; diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs index b7d47067d4..45d0a9015e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs @@ -5,84 +5,112 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.layer-name - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] + [app.common.data.macros :as dm] [app.main.data.workspace :as dw] [app.main.store :as st] - [app.main.ui.context :as ctx] + [app.util.debug :as dbg] [app.util.dom :as dom] [app.util.keyboard :as kbd] [cuerdas.core :as str] [okulary.core :as l] [rumext.v2 :as mf])) -(def shape-for-rename-ref - (l/derived (l/in [:workspace-local :shape-for-rename]) st/state)) +(def ^:private space-for-icons 110) + +(def lens:shape-for-rename + (-> (l/in [:workspace-local :shape-for-rename]) + (l/derived st/state))) (mf/defc layer-name - [{:keys [shape on-start-edit disabled-double-click on-stop-edit name-ref depth parent-size selected? type-comp type-frame hidden] :as props}] - (let [local (mf/use-state {}) - shape-for-rename (mf/deref shape-for-rename-ref) - new-css-system (mf/use-ctx ctx/new-css-system) + {::mf/wrap-props false + ::mf/forward-ref true} + [{:keys [shape-id shape-name shape-touched? disabled-double-click + on-start-edit on-stop-edit depth parent-size selected? + type-comp type-frame hidden?]} external-ref] + (let [edition* (mf/use-state false) + edition? (deref edition*) - start-edit (fn [] - (when (not disabled-double-click) - (on-start-edit) - (swap! local assoc :edition true) - (st/emit! (dw/start-rename-shape (:id shape))))) + local-ref (mf/use-ref) + ref (d/nilv external-ref local-ref) - accept-edit (fn [] - (let [name-input (mf/ref-val name-ref) - name (str/trim (dom/get-value name-input))] - (on-stop-edit) - (swap! local assoc :edition false) - (st/emit! (dw/end-rename-shape name)))) + shape-for-rename (mf/deref lens:shape-for-rename) - cancel-edit (fn [] - (on-stop-edit) - (swap! local assoc :edition false) - (st/emit! (dw/end-rename-shape nil))) + has-path? (str/includes? shape-name "/") - on-key-down (fn [event] - (when (kbd/enter? event) (accept-edit)) - (when (kbd/esc? event) (cancel-edit))) + start-edit + (mf/use-fn + (mf/deps disabled-double-click on-start-edit shape-id) + (fn [] + (when (not disabled-double-click) + (on-start-edit) + (reset! edition* true) + (st/emit! (dw/start-rename-shape shape-id))))) - space-for-icons 110 - parent-size (str (- parent-size space-for-icons) "px")] + accept-edit + (mf/use-fn + (mf/deps on-stop-edit) + (fn [] + (let [name-input (mf/ref-val ref) + name (str/trim (dom/get-value name-input))] + (on-stop-edit) + (reset! edition* false) + (st/emit! (dw/end-rename-shape name))))) - (mf/with-effect [shape-for-rename] - (when (and (= shape-for-rename (:id shape)) - (not (:edition @local))) + cancel-edit + (mf/use-fn + (mf/deps on-stop-edit) + (fn [] + (on-stop-edit) + (reset! edition* false) + (st/emit! (dw/end-rename-shape nil)))) + + on-key-down + (mf/use-fn + (mf/deps accept-edit cancel-edit) + (fn [event] + (when (kbd/enter? event) (accept-edit)) + (when (kbd/esc? event) (cancel-edit)))) + + parent-size (dm/str (- parent-size space-for-icons) "px")] + + (mf/with-effect [shape-for-rename edition? start-edit shape-id] + (when (and (= shape-for-rename shape-id) + (not ^boolean edition?)) (start-edit))) - (mf/with-effect [(:edition @local)] - (when (:edition @local) - (let [name-input (mf/ref-val name-ref)] - (dom/select-text! name-input) - nil))) + (mf/with-effect [edition?] + (when edition? + (some-> (mf/ref-val ref) dom/select-text!) + nil)) - (if (:edition @local) + (if ^boolean edition? [:input - {:class (if new-css-system - (dom/classnames (css :element-name-input) true) - (dom/classnames :element-name true)) - :style #js {"--depth" depth "--parent-size" parent-size} + {:class (stl/css :element-name + :element-name-input) + :style {"--depth" depth "--parent-size" parent-size} :type "text" - :ref name-ref + :ref ref :on-blur accept-edit :on-key-down on-key-down :auto-focus true - :default-value (:name shape "")}] - [:span - {:class (if new-css-system - (dom/classnames (css :element-name) true - (css :selected) selected? - (css :hidden) hidden - (css :type-comp) type-comp - (css :type-frame) type-frame) - (dom/classnames :element-name true)) - :style #js {"--depth" depth "--parent-size" parent-size} - :ref name-ref - :on-double-click start-edit} - (:name shape "") - (when (seq (:touched shape)) " *")]))) + :default-value (d/nilv shape-name "")}] + [:* + [:span + {:class (stl/css-case + :element-name true + :left-ellipsis has-path? + :selected selected? + :hidden hidden? + :type-comp type-comp + :type-frame type-frame) + :style {"--depth" depth "--parent-size" parent-size} + :ref ref + :on-double-click start-edit} + (if (dbg/enabled? :show-ids) + (str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24)) + (d/nilv shape-name ""))] + (when (and (dbg/enabled? :show-touched) ^boolean shape-touched?) + [:span {:class (stl/css :element-name-touched)} "*"])]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.css.json b/frontend/src/app/main/ui/workspace/sidebar/layer_name.css.json deleted file mode 100644 index 40f1367aaf..0000000000 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"sidebar_layer_name_button-primary_V-6Cp","button-secondary":"sidebar_layer_name_button-secondary_Q14Qj","button-icon":"sidebar_layer_name_button-icon_UQXjw","button-icon-small":"sidebar_layer_name_button-icon-small_At5P8","element-name":"sidebar_layer_name_element-name_hZ-lA","selected":"sidebar_layer_name_selected_MKxdm","type-comp":"sidebar_layer_name_type-comp_TNGM-","hidden":"sidebar_layer_name_hidden_e-K3G","element-name-input":"sidebar_layer_name_element-name-input_Wpnkf"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss b/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss index 8e6ede5708..c9fbf235bc 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss @@ -8,7 +8,7 @@ .element-name { @include textEllipsis; - @include titleTipography; + @include bodySmallTypography; flex-grow: 1; color: var(--context-hover-color, var(--layer-row-foreground-color)); &.selected { @@ -27,15 +27,17 @@ } .element-name-input { @include textEllipsis; - @include titleTipography; + @include bodySmallTypography; + @include removeInputStyle; flex-grow: 1; height: $s-28; max-width: calc(var(--parent-size) - (var(--depth) * var(--layer-indentation-size))); margin: 0; padding-left: $s-6; border-radius: $br-8; - border: 1px solid var(--input-border-color-focus); - outline: none; - background-color: transparent; + border: $s-1 solid var(--input-border-color-focus); color: var(--layer-row-foreground-color); } +.element-name-touched { + color: var(--layer-row-component-foreground-color); +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 9541f5cb74..afe62a2b1d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -5,85 +5,106 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.layers - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.types.shape :as cts] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.shape-icon-refactor :as sic] - [app.main.ui.context :as ctx] + [app.main.ui.components.search-bar :refer [search-bar]] + [app.main.ui.components.shape-icon :as sic] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] + [app.main.ui.notifications.badge :refer [badge-notification]] [app.main.ui.workspace.sidebar.layer-item :refer [layer-item]] [app.util.dom :as dom] + [app.util.globals :as globals] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] - [app.util.timers :as ts] + [app.util.rxops :refer [throttle-fn]] + [beicon.v2.core :as rx] [cuerdas.core :as str] - [rumext.v2 :as mf])) - + [goog.events :as events] + [rumext.v2 :as mf]) + (:import goog.events.EventType)) ;; This components is a piece for sharding equality check between top ;; level frames and try to avoid rerender frames that are does not ;; affected by the selected set. (mf/defc frame-wrapper - {::mf/wrap-props false - ::mf/wrap [mf/memo - #(mf/deferred % ts/idle-then-raf)]} - [props] - [:> layer-item props]) + {::mf/props :obj} + [{:keys [selected] :as props}] + (let [pending-selected (mf/use-var selected) + current-selected (mf/use-state selected) + props (mf/spread props :selected @current-selected) + + set-selected + (mf/use-memo + (fn [] + (throttle-fn + 50 + #(when-let [pending-selected @pending-selected] + (reset! current-selected pending-selected)))))] + + (mf/with-effect [selected set-selected] + (reset! pending-selected selected) + (set-selected) + (fn [] + (reset! pending-selected nil) + #(rx/dispose! set-selected))) + + [:> layer-item props])) (mf/defc layers-tree - {::mf/wrap [#(mf/memo % =) - #(mf/throttle % 200)]} + {::mf/wrap [mf/memo #(mf/throttle % 200)] + ::mf/wrap-props false} [{:keys [objects filtered? parent-size] :as props}] - (let [selected (mf/deref refs/selected-shapes) - selected (hooks/use-equal-memo selected) - root (get objects uuid/zero) - new-css-system (mf/use-ctx ctx/new-css-system)] - [:ul - {:class (if new-css-system - (dom/classnames (css :element-list) true) - (dom/classnames :element-list true))} + (let [selected (mf/deref refs/selected-shapes) + selected (hooks/use-equal-memo selected) + highlighted (mf/deref refs/highlighted-shapes) + highlighted (hooks/use-equal-memo highlighted) + root (get objects uuid/zero)] + [:div {:class (stl/css :element-list)} [:& hooks/sortable-container {} (for [[index id] (reverse (d/enumerate (:shapes root)))] (when-let [obj (get objects id)] - (if (= (:type obj) :frame) + (if (cfh/frame-shape? obj) [:& frame-wrapper {:item obj :selected selected + :highlighted highlighted :index index :objects objects :key id :sortable? true :filtered? filtered? :parent-size parent-size - :recieved-depth -1}] + :depth -1}] [:& layer-item {:item obj :selected selected + :highlighted highlighted :index index :objects objects :key id :sortable? true :filtered? filtered? - :recieved-depth -1 + :depth -1 :parent-size parent-size}])))]])) (mf/defc filters-tree - {::mf/wrap [#(mf/memo % =) - #(mf/throttle % 200)]} - [{:keys [objects parent-size] :as props}] - (let [selected (mf/deref refs/selected-shapes) - selected (hooks/use-equal-memo selected) - root (get objects uuid/zero) - new-css-system (mf/use-ctx ctx/new-css-system)] - [:ul {:class (if new-css-system - (dom/classnames (css :element-list) true) - (dom/classnames :element-list true))} + {::mf/wrap [mf/memo #(mf/throttle % 200)] + ::mf/wrap-props false} + [{:keys [objects parent-size]}] + (let [selected (mf/deref refs/selected-shapes) + selected (hooks/use-equal-memo selected) + root (get objects uuid/zero)] + [:ul {:class (stl/css :element-list)} (for [[index id] (d/enumerate (:shapes root))] (when-let [obj (get objects id)] [:& layer-item @@ -94,12 +115,11 @@ :key id :sortable? false :filtered? true - :recieved-depth -1 + :depth -1 :parent-size parent-size}]))])) (defn calc-reparented-objects [objects] - (let [reparented-objects (d/mapm (fn [_ val] (assoc val :parent-id uuid/zero :shapes nil)) @@ -114,314 +134,356 @@ ;; --- Layers Toolbox +;; FIXME: optimize +(defn- match-filters? + [state [id shape]] + (let [search (:search-text state) + filters (:filters state) + filters (cond-> filters + (contains? filters :shape) + (conj :rect :circle :path :bool))] + (or (= uuid/zero id) + (and (or (str/includes? (str/lower (:name shape)) (str/lower search)) + ;; Only for local development we allow search for ids. Otherwise will be hard + ;; search for numbers or single letter shape names (ie: "A") + (and *assert* + (str/includes? (dm/str (:id shape)) (str/lower search)))) + (or (empty? filters) + (and (contains? filters :component) + (contains? shape :component-id)) + (and (contains? filters :image) + (some? (cts/has-images? shape))) + + (let [direct-filters (into #{} (filter #{:frame :rect :circle :path :bool :text}) filters)] + (contains? direct-filters (:type shape))) + (and (contains? filters :group) + (and (cfh/group-shape? shape) + (not (contains? shape :component-id)) + (or (not (contains? shape :masked-group)) + (false? (:masked-group shape))))) + (and (contains? filters :mask) + (true? (:masked-group shape)))))))) + (defn use-search [page objects] - (let [filter-state (mf/use-state {:show-search-box false - :show-filters-menu false - :search-text "" - :active-filters #{} - :num-items 100}) - new-css-system (mf/use-ctx ctx/new-css-system) + (let [state* (mf/use-state + {:show-search false + :show-menu false + :search-text "" + :filters #{} + :num-items 100}) + state (deref state*) + current-filters (:filters state) + current-items (:num-items state) + current-search (:search-text state) + show-menu? (:show-menu state) + show-search? (:show-search state) + clear-search-text - (mf/use-callback - (fn [] - (swap! filter-state assoc :search-text "" :num-items 100))) + (mf/use-fn + #(swap! state* assoc :search-text "" :num-items 100)) - update-search-text - (mf/use-callback - (fn [event] - (let [value (-> event dom/get-target dom/get-value)] - (swap! filter-state assoc :search-text value :num-items 100)))) - - toggle-search - (mf/use-callback - (fn [event] - (let [node (dom/get-current-target event)] - (swap! filter-state assoc :search-text "") - (swap! filter-state assoc :active-filters #{}) - (swap! filter-state assoc :show-filters-menu false) - (swap! filter-state assoc :num-items 100) - (swap! filter-state update :show-search-box not) - (dom/blur! node)))) toggle-filters - (mf/use-callback - (fn [] - (swap! filter-state update :show-filters-menu not))) + (mf/use-fn + #(swap! state* update :show-menu not)) + + on-toggle-filters-click + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (toggle-filters))) + + hide-menu + (mf/use-fn + #(swap! state* assoc :show-menu false)) + + on-key-down + (mf/use-fn + (fn [event] + (when (kbd/esc? event) (hide-menu)))) + + update-search-text + (mf/use-fn + (fn [value _event] + (swap! state* assoc :search-text value :num-items 100))) + + toggle-search + (mf/use-fn + (fn [event] + (let [node (dom/get-current-target event)] + (dom/blur! node) + (swap! state* (fn [state] + (-> state + (assoc :search-text "") + (assoc :filters #{}) + (assoc :show-menu false) + (assoc :num-items 100) + (update :show-search not))))))) remove-filter - (mf/use-callback - (mf/deps @filter-state) - (fn [key] - (fn [_] - (swap! filter-state update :active-filters disj key) - (swap! filter-state assoc :num-items 100)))) + (mf/use-fn + (fn [event] + (let [fkey (-> (dom/get-current-target event) + (dom/get-data "filter") + (keyword))] + (swap! state* (fn [state] + (-> state + (update :filters disj fkey) + (assoc :num-items 100))))))) add-filter - (mf/use-callback - (mf/deps @filter-state (:show-filters-menu @filter-state)) - (fn [key] - (fn [_] - (swap! filter-state update :active-filters conj key) - (swap! filter-state assoc :num-items 100) - (toggle-filters)))) + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (let [key (-> (dom/get-current-target event) + (dom/get-data "filter") + (keyword))] + (swap! state* (fn [state] + (-> state + (update :filters conj key) + (update :show-menu not) + (assoc :num-items 100))))))) active? - (and - (:show-search-box @filter-state) - (or (d/not-empty? (:search-text @filter-state)) - (d/not-empty? (:active-filters @filter-state)))) + (and ^boolean show-search? + (or ^boolean (d/not-empty? current-search) + ^boolean (d/not-empty? current-filters))) - search-and-filters - (fn [[id shape]] - (let [search (:search-text @filter-state) - filters (:active-filters @filter-state) - filters (cond-> filters - (some #{:shape} filters) - (conj :rect :circle :path :bool))] - (or - (= uuid/zero id) - (and - (or (str/includes? (str/lower (:name shape)) (str/lower search)) - (str/includes? (dm/str (:id shape)) (str/lower search))) - (or - (empty? filters) - (and - (some #{:component} filters) - (contains? shape :component-id)) - (let [direct_filters (filter #{:frame :rect :circle :path :bool :image :text} filters)] - (some #{(:type shape)} direct_filters)) - (and - (some #{:group} filters) - (and (= :group (:type shape)) - (not (contains? shape :component-id)) - (or (not (contains? shape :masked-group?)) (false? (:masked-group? shape))))) - (and - (some #{:mask} filters) - (true? (:masked-group? shape)))))))) + filtered-objects-all + (mf/with-memo [active? objects state] + (when active? + (into [] (filter (partial match-filters? state)) objects))) filtered-objects-total - (mf/use-memo - (mf/deps objects active? @filter-state) - #(when active? - ;; filterv so count is constant time - (filterv search-and-filters objects))) + (count filtered-objects-all) filtered-objects - (mf/use-memo - (mf/deps filtered-objects-total) - #(when active? - (calc-reparented-objects - (into {} - (take (:num-items @filter-state)) - filtered-objects-total)))) + (mf/with-memo [active? filtered-objects-all current-items] + (when active? + (->> filtered-objects-all + (into {} (take current-items)) + (calc-reparented-objects)))) handle-show-more - (fn [] - (when (<= (:num-items @filter-state) (count filtered-objects-total)) - (swap! filter-state update :num-items + 100))) + (mf/use-fn + (mf/deps filtered-objects-total current-items) + (fn [_] + (when (<= current-items filtered-objects-total) + (swap! state* update :num-items + 100))))] - handle-key-down - (mf/use-callback - (fn [event] - (let [enter? (kbd/enter? event) - esc? (kbd/esc? event) - input-node (dom/event->target event)] - - (when enter? - (dom/blur! input-node)) - (when esc? - (dom/blur! input-node)))))] + (mf/with-effect [] + (let [keys [(events/listen globals/document EventType.KEYDOWN on-key-down) + (events/listen globals/document EventType.CLICK hide-menu)]] + (fn [] (doseq [key keys] (events/unlistenByKey key))))) [filtered-objects handle-show-more - (mf/html - (if (:show-search-box @filter-state) - [:* - [:div {:class (if new-css-system - (dom/classnames (css :tool-window-bar) true - (css :search) true) - (dom/classnames :tool-window-bar true - :search true))} - [:span {:class (if new-css-system - (dom/classnames (css :search-box) true) - (dom/classnames :search-box true))} - [:button - {:on-click toggle-filters - :class (if new-css-system - (dom/classnames :active active? - (css :filter-button) true) - (dom/classnames :active active? - :filter true))} - (if new-css-system - i/filter-refactor - i/icon-filter)] - [:div {:class (dom/classnames (css :search-input-wrapper) new-css-system)} - [:input {:on-change update-search-text - :value (:search-text @filter-state) - :auto-focus (:show-search-box @filter-state) - :placeholder (tr "workspace.sidebar.layers.search") - :on-key-down handle-key-down}] - (when (not (= "" (:search-text @filter-state))) - [:button {:class (if new-css-system - (dom/classnames (css :clear) true) - (dom/classnames :clear true)) - :on-click clear-search-text} - (if new-css-system - i/delete-text-refactor - i/exclude)])]] - [:button {:class (dom/classnames (css :close-search) new-css-system) - :on-click toggle-search} - (if new-css-system - i/close-refactor - i/cross)]] - [:div {:class (if new-css-system - (dom/classnames (css :active-filters) true) - (dom/classnames :active-filters true))} - (for [f (:active-filters @filter-state)] - (let [name (case f - :frame (tr "workspace.sidebar.layers.frames") - :group (tr "workspace.sidebar.layers.groups") - :mask (tr "workspace.sidebar.layers.masks") - :component (tr "workspace.sidebar.layers.components") - :text (tr "workspace.sidebar.layers.texts") - :image (tr "workspace.sidebar.layers.images") - :shape (tr "workspace.sidebar.layers.shapes") - (tr f))] - (if new-css-system - [:button {:class (dom/classnames (css :layer-filter) true) - :on-click (remove-filter f)} - [:span {:class (dom/classnames (css :layer-filter-icon) true)} - [:& sic/element-icon-refactor-by-type {:type f - :main-instance? (= f :component)}]] - [:span {:class (dom/classnames (css :layer-filter-name) true)} - name] - [:span {:class (dom/classnames (css :layer-filter-close) true)} - i/close-small-refactor]] - [:span {:on-click (remove-filter f)} - name i/cross])))] + #(mf/html + (if show-search? + [:* + [:div {:class (stl/css :tool-window-bar :search)} + [:& search-bar {:on-change update-search-text + :value current-search + :on-clear clear-search-text + :placeholder (tr "workspace.sidebar.layers.search")} + [:button {:on-click on-toggle-filters-click + :class (stl/css-case + :filter-button true + :opened show-menu? + :active active?)} + i/filter-icon]] - (when (:show-filters-menu @filter-state) - (if new-css-system - [:ul {:class (dom/classnames (css :filters-container) true)} - [:li {:key "frames-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :frame)) - :on-click (add-filter :frame)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/board-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.frames")]] - (when (contains? (:active-filters @filter-state) :frame) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])] - [:li {:key "groups-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :group)) - :on-click (add-filter :group)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/group-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.groups")]] - (when (contains? (:active-filters @filter-state) :group) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])] - [:li {:key "masks-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :mask)) - :on-click (add-filter :mask)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/mask-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.masks")]] - (when (contains? (:active-filters @filter-state) :mask) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])] - [:li {:key "components-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :component)) - :on-click (add-filter :component)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/component-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.components")]] - (when (contains? (:active-filters @filter-state) :component) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])] - [:li {:key "texts-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :text)) - :on-click (add-filter :text)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/text-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.texts")]] - (when (contains? (:active-filters @filter-state) :text) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])] - [:li {:key "images-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :image)) - :on-click (add-filter :image)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/img-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.images")]] - (when (contains? (:active-filters @filter-state) :image) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])] - [:li {:key "shapes-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :shape)) - :on-click (add-filter :shape)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/path-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.shapes")]] - (when (contains? (:active-filters @filter-state) :shape) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])]] + [:button {:class (stl/css :close-search) + :on-click toggle-search} + i/close]] + + [:div {:class (stl/css :active-filters)} + (for [fkey current-filters] + (let [fname (d/name fkey) + name (case fkey + :frame (tr "workspace.sidebar.layers.frames") + :group (tr "workspace.sidebar.layers.groups") + :mask (tr "workspace.sidebar.layers.masks") + :component (tr "workspace.sidebar.layers.components") + :text (tr "workspace.sidebar.layers.texts") + :image (tr "workspace.sidebar.layers.images") + :shape (tr "workspace.sidebar.layers.shapes") + (tr fkey))] + + [:button {:class (stl/css :layer-filter) + :key fname + :data-filter fname + :on-click remove-filter} + + [:span {:class (stl/css :layer-filter-icon)} + [:& sic/element-icon-by-type + {:type fkey + :main-instance? (= fkey :component)}]] + [:span {:class (stl/css :layer-filter-name)} + name] + [:span {:class (stl/css :layer-filter-close)} + i/close-small]]))] + + (when ^boolean show-menu? + [:ul {:class (stl/css :filters-container)} + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :frame)) + :data-filter "frame" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:span {:class (stl/css :filter-menu-item-icon)} + i/board] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.frames")]] + + (when (contains? current-filters :frame) + [:span {:class (stl/css :filter-menu-item-tick)} + i/tick])] + + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :group)) + :data-filter "group" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:span {:class (stl/css :filter-menu-item-icon)} + i/group] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.groups")]] + + (when (contains? current-filters :group) + [:span {:class (stl/css :filter-menu-item-tick)} + i/tick])] + + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :mask)) + :data-filter "mask" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:span {:class (stl/css :filter-menu-item-icon)} + i/mask] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.masks")]] + + (when (contains? current-filters :mask) + [:span {:class (stl/css :filter-menu-item-tick)} + i/tick])] + + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :component)) + :data-filter "component" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:span {:class (stl/css :filter-menu-item-icon)} + i/component] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.components")]] + + (when (contains? current-filters :component) + [:span {:class (stl/css :filter-menu-item-tick)} + i/tick])] + + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :text)) + :data-filter "text" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:span {:class (stl/css :filter-menu-item-icon)} + i/text] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.texts")]] + + (when (contains? current-filters :text) + [:span {:class (stl/css :filter-menu-item-tick)} + i/tick])] + + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :image)) + :data-filter "image" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:span {:class (stl/css :filter-menu-item-icon)} + i/img] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.images")]] + + (when (contains? current-filters :image) + [:span {:class (stl/css :filter-menu-item-tick)} + i/tick])] + + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :shape)) + :data-filter "shape" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:span {:class (stl/css :filter-menu-item-icon)} + i/path] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.shapes")]] + + (when (contains? current-filters :shape) + [:span {:class (stl/css :filter-menu-item-tick)} + i/tick])]])] + + [:div {:class (stl/css :tool-window-bar)} + [:& title-bar {:collapsable false + :title (:name page) + :on-btn-click toggle-search + :btn-children i/search}]]))])) + + +(defn- on-scroll + [event] + (let [children (dom/get-elements-by-class "sticky-children") + length (alength children)] + (when (pos? length) + (let [target (dom/get-target event) + target-top (:top (dom/get-bounding-rect target)) + frames (dom/get-elements-by-class "root-board") + + last-hidden-frame + (->> frames + (filter #(<= (- (:top (dom/get-bounding-rect %)) target-top) 0)) + last) + + frame-id (dom/get-attribute last-hidden-frame "id") + + last-hidden-children + (->> children + (filter #(< (- (:top (dom/get-bounding-rect %)) target-top) 0)) + last) + + is-children-shown? + (and last-hidden-children + (> (- (:bottom (dom/get-bounding-rect last-hidden-children)) target-top) 0)) + + children-frame-id (dom/get-attribute last-hidden-children "data-id") + + ;; We want to check that root-board is out of view but its children are not. + ;; only in that case we make root board sticky. + sticky? (and last-hidden-frame + is-children-shown? + (= frame-id children-frame-id))] + + (run! #(dom/remove-class! % "sticky") frames) + + (when sticky? + (dom/add-class! last-hidden-frame "sticky")))))) - [:div.filters-container - [:span {:on-click (add-filter :frame)} i/artboard (tr "workspace.sidebar.layers.frames")] - [:span {:on-click (add-filter :group)} i/folder (tr "workspace.sidebar.layers.groups")] - [:span {:on-click (add-filter :mask)} i/mask (tr "workspace.sidebar.layers.masks")] - [:span {:on-click (add-filter :component)} i/component (tr "workspace.sidebar.layers.components")] - [:span {:on-click (add-filter :text)} i/text (tr "workspace.sidebar.layers.texts")] - [:span {:on-click (add-filter :image)} i/image (tr "workspace.sidebar.layers.images")] - [:span {:on-click (add-filter :shape)} i/curve (tr "workspace.sidebar.layers.shapes")]]))] - [:div {:class (if new-css-system - (dom/classnames (css :tool-window-bar) true) - (dom/classnames :tool-window-bar true))} - [:span {:class (if new-css-system - (dom/classnames (css :page-name) true) - (dom/classnames :page-name true))} - (:name page)] - [:button {:class (if new-css-system - (dom/classnames (css :icon-search) true) - (dom/classnames :icon-search true)) - :on-click toggle-search} - (if new-css-system - i/search-refactor - i/search)]]))])) (mf/defc layers-toolbox - {:wrap [mf/memo]} - [{:keys [size-parent] :as props}] - (let [page (mf/deref refs/workspace-page) - focus (mf/deref refs/workspace-focus-selected) - objects (hooks/with-focus-objects (:objects page) focus) - title (when (= 1 (count focus)) (get-in objects [(first focus) :name])) - new-css-system (mf/use-ctx ctx/new-css-system) - observer-var (mf/use-var nil) - lazy-load-ref (mf/use-ref nil) + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [{:keys [size-parent]}] + (let [page (mf/deref refs/workspace-page) + focus (mf/deref refs/workspace-focus-selected) + + objects (hooks/with-focus-objects (:objects page) focus) + title (when (= 1 (count focus)) + (dm/get-in objects [(first focus) :name])) + + observer-var (mf/use-var nil) + lazy-load-ref (mf/use-ref nil) [filtered-objects show-more filter-component] (use-search page objects) @@ -432,106 +494,61 @@ on-render-container (fn [element] - (let [options #js {:root element} - lazy-el (mf/ref-val lazy-load-ref)] + (when-let [lazy-node (mf/ref-val lazy-load-ref)] (cond (and (some? element) (not (some? @observer-var))) - (let [observer (js/IntersectionObserver. intersection-callback options)] - (.observe observer lazy-el) + (let [observer (js/IntersectionObserver. intersection-callback + #js {:root element})] + (.observe observer lazy-node) (reset! observer-var observer)) (and (nil? element) (some? @observer-var)) - (do (.disconnect @observer-var) + (do (.disconnect ^js @observer-var) (reset! observer-var nil))))) - on-scroll - (fn [event] - (let [children (dom/get-elements-by-class "sticky-children") - length (.-length children)] - (when (< 0 length) - (let [target (dom/get-target event) - target-top (:top (dom/get-bounding-rect target)) - frames (dom/get-elements-by-class "root-board") + toogle-focus-mode + (mf/use-fn + #(st/emit! (dw/toggle-focus-mode)))] - last-hidden-frame (->> frames - (filter #(<= (- (:top (dom/get-bounding-rect %)) target-top) 0)) - last) - frame-id (dom/get-attribute last-hidden-frame "id") - - last-hidden-children (->> children - (filter #(< (- (:top (dom/get-bounding-rect %)) target-top) 0)) - last) - - is-children-shown? (and last-hidden-children - (> (- (:bottom (dom/get-bounding-rect last-hidden-children)) target-top) 0)) - - children-frame-id (dom/get-attribute last-hidden-children "data-id") - ;; We want to check that root-board is out of view but its children are not. - ;; only in that case we make root board sticky. - sticky? (and last-hidden-frame - is-children-shown? - (= frame-id children-frame-id))] - (doseq [frame frames] - (dom/remove-class! frame "sticky")) - - (when sticky? - (dom/add-class! last-hidden-frame "sticky"))))))] - [:div#layers - {:class (if new-css-system - (dom/classnames (css :layers) true) - (dom/classnames :tool-window true))} + [:div#layers {:class (stl/css :layers)} (if (d/not-empty? focus) - [:div - {:class (if new-css-system - (dom/classnames (css :tool-window-bar) true) - (dom/classnames :tool-window-bar true))} - [:button {:class (if new-css-system - (dom/classnames (css :focus-title) true) - (dom/classnames :focus-title true)) - :on-click #(st/emit! (dw/toggle-focus-mode))} - [:span {:class (if new-css-system - (dom/classnames (css :back-button) true) - (dom/classnames :back-button true))} - (if new-css-system - i/arrow-refactor - i/arrow-slide)] - [:div {:class (if new-css-system - (dom/classnames (css :focus-name) true) - (dom/classnames :focus-name true))} + [:div {:class (stl/css :tool-window-bar)} + [:button {:class (stl/css :focus-title) + :on-click toogle-focus-mode} + [:span {:class (stl/css :back-button)} + i/arrow] + + [:div {:class (stl/css :focus-name)} (or title (tr "workspace.sidebar.layers"))] - (if new-css-system - [:div {:class (dom/classnames (css :focus-mode-tag-wrapper) true)} - [:div {:class (dom/classnames (css :focus-mode-tag) true)} (tr "workspace.focus.focus-mode")]] - [:div.focus-mode (tr "workspace.focus.focus-mode")])]] - filter-component) + + [:div {:class (stl/css :focus-mode-tag-wrapper)} + [:& badge-notification {:content (tr "workspace.focus.focus-mode") :size :small :is-focus true}]]]] + + (filter-component)) + (if (some? filtered-objects) [:* - [:div {:data-scroll-container true - :class (if new-css-system - (dom/classnames (css :tool-window-content) true) - (dom/classnames :tool-window-content true)) - :ref on-render-container :key "filters"} + [:div {:class (stl/css :tool-window-content) + :data-scroll-container true + :ref on-render-container} [:& filters-tree {:objects filtered-objects :key (dm/str (:id page)) :parent-size size-parent}] - [:div.lazy {:ref lazy-load-ref - :key "lazy-load" - :style {:min-height 16}}]] + [:div {:ref lazy-load-ref + :style {:min-height 16}}]] [:div {:on-scroll on-scroll + :class (stl/css :tool-window-content) :data-scroll-container true - :class (if new-css-system - (dom/classnames (css :tool-window-content) true) - (dom/classnames :tool-window-content true)) :style {:display (when (some? filtered-objects) "none")}} + [:& layers-tree {:objects filtered-objects :key (dm/str (:id page)) :filtered? true :parent-size size-parent}]]] + [:div {:on-scroll on-scroll + :class (stl/css :tool-window-content) :data-scroll-container true - :class (if new-css-system - (dom/classnames (css :tool-window-content) true) - (dom/classnames :tool-window-content true)) :style {:display (when (some? filtered-objects) "none")}} [:& layers-tree {:objects objects :key (dm/str (:id page)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.css.json b/frontend/src/app/main/ui/workspace/sidebar/layers.css.json deleted file mode 100644 index f9c4de1291..0000000000 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"sidebar_layers_button-primary_q9e2I","layers":"sidebar_layers_layers_87ZOo","tool-window-bar":"sidebar_layers_tool-window-bar_lg54C","search":"sidebar_layers_search_zjs2x","close-search":"sidebar_layers_close-search_baIhK","icon-search":"sidebar_layers_icon-search_6kWUn","button-secondary":"sidebar_layers_button-secondary_H4lpi","active-filters":"sidebar_layers_active-filters_-JMMP","layer-filter":"sidebar_layers_layer-filter_rHZTz","search-box":"sidebar_layers_search-box_JtqOV","search-input-wrapper":"sidebar_layers_search-input-wrapper_oJa-7","clear":"sidebar_layers_clear_sLcl1","button-icon":"sidebar_layers_button-icon_SD8PP","button-icon-small":"sidebar_layers_button-icon-small_v5L-u","filters-container":"sidebar_layers_filters-container_c1Ux9","filter-menu-item":"sidebar_layers_filter-menu-item_aZd0D","filter-menu-item-tick":"sidebar_layers_filter-menu-item-tick_JNdIK","filter-menu-item-name-wrapper":"sidebar_layers_filter-menu-item-name-wrapper_DtGkH","filter-menu-item-icon":"sidebar_layers_filter-menu-item-icon_Oi3Ix","layer-filter-icon":"sidebar_layers_layer-filter-icon_0yKrb","layer-filter-close":"sidebar_layers_layer-filter-close_3mV4i","focus-title":"sidebar_layers_focus-title_35PvQ","back-button-icon":"sidebar_layers_back-button-icon_mHv6g","page-name":"sidebar_layers_page-name_8ZDRR","filter-button":"sidebar_layers_filter-button_KXxHh","focus-name":"sidebar_layers_focus-name_Fderf","focus-mode-tag-wrapper":"sidebar_layers_focus-mode-tag-wrapper_OHXCG","focus-mode-tag":"sidebar_layers_focus-mode-tag_J5ItD","layer-filter-name":"sidebar_layers_layer-filter-name_Y4PuB","filter-menu-item-name":"sidebar_layers_filter-menu-item-name_rxeut","selected":"sidebar_layers_selected_V5Vv3","tool-window-content":"sidebar_layers_tool-window-content_YnpDB","element-list":"sidebar_layers_element-list_nAntB"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.scss b/frontend/src/app/main/ui/workspace/sidebar/layers.scss index 34f76f5740..2dbde9595c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.scss @@ -6,303 +6,244 @@ @import "refactor/common-refactor.scss"; +.tool-window-bar { + display: flex; + align-items: center; + justify-content: space-between; + height: $s-32; + min-height: $s-32; + margin: $s-8 0 $s-4 $s-8; + padding-right: $s-12; + + &.search { + padding: 0 $s-12 0 $s-8; + gap: $s-4; + .filter-button { + @include flexCenter; + @include buttonStyle; + height: $s-32; + width: $s-32; + margin: 0; + border: $s-1 solid var(--color-background-tertiary); + border-radius: $br-8 $br-2 $br-2 $br-8; + background-color: var(--color-background-tertiary); + svg { + height: $s-16; + width: $s-16; + stroke: var(--icon-foreground); + } + &:focus { + border: $s-1 solid var(--input-border-color-focus); + outline: 0; + background-color: var(--input-background-color-active); + color: var(--input-foreground-color-active); + svg { + background-color: var(--input-background-color-active); + } + } + &:hover { + border: $s-1 solid var(--input-border-color-hover); + background-color: var(--input-background-color-hover); + svg { + background-color: var(--input-background-color-hover); + stroke: var(--button-foreground-hover); + } + } + &.opened { + @extend .button-icon-selected; + } + } + .close-search { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + min-width: $s-28; + padding: 0; + border-radius: $br-8; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } + } +} + +.page-name { + @include uppercaseTitleTipography; + padding: 0 $s-12; + color: var(--title-foreground-color); +} + +.icon-search { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + border-radius: $br-8; + margin-right: $s-8; + padding: 0; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.focus-title { + @include buttonStyle; + display: grid; + grid-template-columns: auto 1fr auto; + width: 100%; + padding: 0; +} + +.back-button { + @include flexCenter; + height: $s-32; + width: $s-24; + padding: 0 $s-4 0 $s-8; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + transform: rotate(180deg); + } +} + +.focus-name { + @include bodySmallTypography; + display: flex; + align-items: center; + height: 100%; + padding-left: $s-4; + color: var(--title-foreground-color); +} + +.focus-mode-tag-wrapper { + @include flexCenter; + height: 100%; + margin-right: $s-12; +} + +.active-filters { + @include flexRow; + flex-wrap: wrap; + margin: 0 $s-12; +} + +.layer-filter { + @extend .button-tag; + gap: $s-6; + height: $s-24; + margin: $s-2 0; + border-radius: $br-6; + background-color: var(--pill-background-color); + cursor: pointer; +} + +.layer-filter-icon, +.layer-filter-close { + @extend .button-icon-small; + stroke: var(--pill-foreground-color); + svg { + height: $s-12; + width: $s-12; + } +} + +.layer-filter-name { + @include flexCenter; + @include bodySmallTypography; + color: var(--pill-foreground-color); +} + .layers { position: relative; - display: flex; - flex-direction: column; - overflow: auto; - box-sizing: border-box; - .tool-window-bar { +} + +.filters-container { + @extend .menu-dropdown; + position: absolute; + left: $s-20; + width: $s-192; + .filter-menu-item { + @include bodySmallTypography; display: flex; align-items: center; justify-content: space-between; - height: $s-32; - min-height: $s-32; - margin-top: $s-2; - margin-bottom: $s-4; - .page-name { - @include tabTitleTipography; - padding: 0 $s-12; - color: var(--title-foreground-color); - } - .icon-search { - @extend .button-primary; - height: $s-32; - width: calc($s-24 + $s-4); - border-radius: $br-8; - margin-right: $s-8; - padding: 0; - svg { - @extend .button-icon; - } - } - &.search { - padding: 0 $s-8 0 $s-12; - .search-box { - display: grid; - grid-template-columns: auto 1fr; - gap: $s-2; - height: $s-32; - width: 100%; - margin-right: $s-4; - border-radius: $br-8; - background-color: var(--color-background-primary); - .filter-button { - @include flexCenter; - @include buttonStyle; - height: $s-32; - width: $s-32; - margin: 0; - border: 1px solid var(--color-background-tertiary); - border-radius: $br-8 $br-2 $br-2 $br-8; - background-color: var(--color-background-tertiary); - svg { - height: $s-16; - width: $s-16; - stroke: var(--icon-foreground); - } - &:focus { - border: 1px solid var(--input-border-color-focus); - outline: 0; - background-color: var(--input-background-color-active); - color: var(--input-foreground-color-active); - svg { - background-color: var(--input-background-color-active); - } - } - &:hover { - border: 1px solid var(--input-background-color-hover); - background-color: var(--input-background-color-hover); - svg { - background-color: var(--input-background-color-hover); - stroke: var(--button-foreground-hover); - } - } - } - .search-input-wrapper { - @include flexCenter; - height: $s-32; - width: 100%; - border: 1px solid var(--color-background-tertiary); - border-radius: $br-2 $br-8 $br-8 $br-2; - background-color: var(--color-background-tertiary); - input { - width: 100%; - margin: $s-8; - border: 0; - background-color: var(--input-background-color); - font-size: $fs-12; - color: var(--input-foreground-color); - &:focus { - outline: none; - } - } - &:hover { - border: 1px solid var(--input-background-color-hover); - background-color: var(--input-background-color-hover); - input { - background-color: var(--input-background-color-hover); - } - } - &:focus-within { - background-color: var(--input-background-color-active); - color: var(--input-foreground-color-active); - border: 1px solid var(--input-border-color-focus); - input { - background-color: var(--input-background-color-active); - } - } - - .clear { - @extend .button-secondary; - border-radius: $br-8; - height: 100%; - svg { - @extend .button-icon-small; - color: transparent; - } - } - } - } - .close-search { - @extend .button-primary; - height: $s-32; - width: $s-28; - min-width: $s-28; - padding: 0; - border-radius: $br-8; - svg { - @extend .button-icon-small; - } - } - } - .focus-title { - @include buttonStyle; - display: grid; - grid-template-columns: auto 1fr auto; - width: 100%; - padding: 0; - .back-button-icon { - @include flexCenter; - height: $s-32; - width: $s-24; - padding: 0 $s-4 0 $s-8; - svg { - @extend .button-icon-small; - transform: rotate(180deg); - } - } - .focus-name { - @include titleTipography; - display: flex; - align-items: center; - height: 100%; - padding-left: $s-4; - color: var(--title-foreground-color); - } - .focus-mode-tag-wrapper { - @include flexCenter; - height: 100%; - margin-right: $s-12; - - .focus-mode-tag { - @include flexCenter; - @include titleTipography; - height: $s-20; - padding: $s-4 $s-6; - border: 1px solid var(--tag-background-color); - border-radius: $br-6; - color: var(--tag-background-color); - } - } - } - } - .active-filters { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: $s-4; - margin: 0 $s-12; - .layer-filter { - @extend .button-secondary; - @include buttonStyle; - gap: $s-6; - height: $s-24; - margin: $s-2 0; - border-radius: $br-6; - background-color: var(--pill-background-color); - cursor: pointer; - .layer-filter-icon, - .layer-filter-close { - @extend .button-icon-small; - stroke: var(--pill-foreground-color); - svg { - height: $s-12; - width: $s-12; - } - } - .layer-filter-name { - @include flexCenter; - @include titleTipography; - color: var(--pill-foreground-color); - } - } - } - .filters-container { - position: absolute; - top: $s-44; - left: $s-12; - display: flex; - flex-direction: column; - gap: $s-4; - width: $s-192; - padding: $s-4; + width: 100%; + padding: $s-6; border-radius: $br-8; - background-color: var(--menu-background-color); - z-index: $z-index-4; - box-shadow: 0px 0px 10px 0px var(--menu-shadow-color); - .filter-menu-item { - @include titleTipography; + + .filter-menu-item-name-wrapper { display: flex; align-items: center; - justify-content: space-between; - width: 100%; - padding: $s-6; - border-radius: $br-8; - - .filter-menu-item-name-wrapper { - display: flex; - align-items: center; - gap: $s-8; - .filter-menu-item-icon { - svg { - @extend .button-icon-small; - stroke: var(--menu-foreground-color); - } - } - .filter-menu-item-name { - padding-top: $s-2; - color: var(--menu-foreground-color); - } - } - .filter-menu-item-tick { + gap: $s-8; + .filter-menu-item-icon { svg { @extend .button-icon-small; stroke: var(--menu-foreground-color); } } + .filter-menu-item-name { + padding-top: $s-2; + color: var(--menu-foreground-color); + } + } + .filter-menu-item-tick { + svg { + @extend .button-icon-small; + stroke: var(--menu-foreground-color); + } + } - &.selected { - background-color: var(--menu-background-color-selected); - .filter-menu-item-name-wrapper { - .filter-menu-item-icon { - svg { - stroke: var(--menu-foreground-color-selected); - } - } - .filter-menu-item-name { - color: var(--menu-foreground-color-selected); + &.selected { + background-color: var(--menu-background-color-selected); + .filter-menu-item-name-wrapper { + .filter-menu-item-icon { + svg { + stroke: var(--menu-foreground-color); } } - .filter-menu-item-tick { - svg { - stroke: var(--menu-foreground-color-selected); - } + .filter-menu-item-name { + color: var(--menu-foreground-color); } } - - &:hover { - background-color: var(--menu-background-color-hover); - .filter-menu-item-name-wrapper { - .filter-menu-item-icon { - svg { - stroke: var(--menu-foreground-color-hover); - } - } - .filter-menu-item-name { - color: var(--menu-foreground-color-hover); - } + .filter-menu-item-tick { + svg { + stroke: var(--menu-foreground-color); } - .filter-menu-item-tick { + } + } + + &:hover { + background-color: var(--menu-background-color-hover); + .filter-menu-item-name-wrapper { + .filter-menu-item-icon { svg { stroke: var(--menu-foreground-color-hover); } } + .filter-menu-item-name { + color: var(--menu-foreground-color-hover); + } + } + .filter-menu-item-tick { + svg { + stroke: var(--menu-foreground-color-hover); + } } } } - .tool-window-content { - display: flex; - flex-direction: column; - height: 100%; +} + +.tool-window-content { + --calculated-height: calc(#{$s-136} + var(--height, #{$s-200})); + display: flex; + flex-direction: column; + height: calc(100vh - var(--calculated-height)); + width: 100%; + overflow-x: hidden; + overflow-y: overlay; + scrollbar-gutter: stable; + .element-list { width: 100%; - border-radius: $br-8; - overflow-y: auto; - overflow-x: hidden; - scrollbar-gutter: stable; - overflow-y: overlay; - .element-list { - width: 100%; - } } } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index efe244bcd5..ea54143606 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -5,25 +5,30 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] + [app.common.types.shape.layout :as ctl] [app.main.data.workspace :as udw] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.tabs-container :refer [tabs-container tabs-element]] + [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.context :as ctx] [app.main.ui.viewer.inspect.right-sidebar :as hrs] [app.main.ui.workspace.sidebar.options.menus.align :refer [align-options]] [app.main.ui.workspace.sidebar.options.menus.bool :refer [bool-options]] + [app.main.ui.workspace.sidebar.options.menus.component :refer [component-menu]] [app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-menu]] + [app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell] [app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu]] + [app.main.ui.workspace.sidebar.options.menus.layout-container :as layout-container] [app.main.ui.workspace.sidebar.options.page :as page] [app.main.ui.workspace.sidebar.options.shapes.bool :as bool] [app.main.ui.workspace.sidebar.options.shapes.circle :as circle] [app.main.ui.workspace.sidebar.options.shapes.frame :as frame] - [app.main.ui.workspace.sidebar.options.shapes.grid-cell :as grid-cell] [app.main.ui.workspace.sidebar.options.shapes.group :as group] [app.main.ui.workspace.sidebar.options.shapes.image :as image] [app.main.ui.workspace.sidebar.options.shapes.multiple :as multiple] @@ -32,7 +37,6 @@ [app.main.ui.workspace.sidebar.options.shapes.svg-raw :as svg-raw] [app.main.ui.workspace.sidebar.options.shapes.text :as text] [app.util.i18n :as i18n :refer [tr]] - [app.util.object :as obj] [rumext.v2 :as mf])) ;; --- Options @@ -62,84 +66,118 @@ :page-id page-id :file-id file-id}]])) -(mf/defc options-content +(mf/defc specialized-panel {::mf/wrap [mf/memo]} - [{:keys [selected section shapes shapes-with-children page-id file-id]}] + [{:keys [panel]}] + (when (= (:type panel) :component-swap) + [:& component-menu {:shapes (:shapes panel) :swap-opened? true}])) + + +(mf/defc options-content + {::mf/memo true + ::mf/props :obj} + [{:keys [selected section shapes shapes-with-children page-id file-id on-change-section on-expand]}] (let [drawing (mf/deref refs/workspace-drawing) objects (mf/deref refs/workspace-page-objects) shared-libs (mf/deref refs/workspace-libraries) + edition (mf/deref refs/selected-edition) grid-edition (mf/deref refs/workspace-grid-edition) + sp-panel (mf/deref refs/specialized-panel) + selected-shapes (into [] (keep (d/getf objects)) selected) first-selected-shape (first selected-shapes) - shape-parent-frame (cph/get-frame objects (:frame-id first-selected-shape)) + shape-parent-frame (cfh/get-frame objects (:frame-id first-selected-shape)) - [grid-id {[row-selected col-selected] :selected}] - (d/seek (fn [[_ {:keys [selected]}]] (some? selected)) grid-edition) - - grid-cell-selected? (and (some? grid-id) (some? row-selected) (some? col-selected)) + edit-grid? (ctl/grid-layout? objects edition) + selected-cells (->> (dm/get-in grid-edition [edition :selected]) + (map #(dm/get-in objects [edition :layout-grid-cells %]))) on-change-tab (fn [options-mode] - (st/emit! (udw/set-options-mode options-mode) - (udw/set-inspect-expanded false)) - (if (= options-mode :inspect) ;;TODO maybe move this logic to set-options-mode + (st/emit! (udw/set-options-mode options-mode)) + (if (= options-mode :inspect) (st/emit! :interrupt (udw/set-workspace-read-only true)) (st/emit! :interrupt (udw/set-workspace-read-only false))))] - [:div.tool-window - [:div.tool-window-content - [:& tabs-container {:on-change-tab on-change-tab - :selected section} - [:& tabs-element {:id :design - :title (tr "workspace.options.design")} - [:div.element-options - [:& align-options] - [:& bool-options] - (cond - grid-cell-selected? [:& grid-cell/options {:shape (get objects grid-id) - :row row-selected - :column col-selected}] - (d/not-empty? drawing) [:& shape-options {:shape (:object drawing) - :page-id page-id - :file-id file-id - :shared-libs shared-libs}] - (= 0 (count selected)) [:& page/options] - (= 1 (count selected)) [:& shape-options {:shape (first selected-shapes) - :page-id page-id - :file-id file-id - :shared-libs shared-libs - :shapes-with-children shapes-with-children}] - :else [:& multiple/options {:shapes-with-children shapes-with-children - :shapes selected-shapes - :page-id page-id - :file-id file-id - :shared-libs shared-libs}])]] + [:div {:class (stl/css :tool-window)} + [:& tab-container + {:on-change-tab on-change-tab + :selected section + :collapsable false + :content-class (stl/css-case + :content-class true + :inspect (= section :inspect)) + :header-class (stl/css :tab-spacing)} + [:& tab-element {:id :design + :title (tr "workspace.options.design")} + [:div {:class (stl/css :element-options)} + [:& align-options] + [:& bool-options] - [:& tabs-element {:id :prototype - :title (tr "workspace.options.prototype")} - [:div.element-options - [:& interactions-menu {:shape (first shapes)}]]] + (cond + (and edit-grid? (d/not-empty? selected-cells)) + [:& grid-cell/options + {:shape (get objects edition) + :cells selected-cells}] - [:& tabs-element {:id :inspect - :title (tr "workspace.options.inspect")} - [:div.element-options - [:& hrs/right-sidebar {:page-id page-id - :file-id file-id - :frame shape-parent-frame - :shapes selected-shapes - :from :workspace}]]]]]])) + edit-grid? + [:& layout-container/grid-layout-edition + {:ids [edition] + :values (get objects edition)}] + + (not (nil? sp-panel)) + [:& specialized-panel {:panel sp-panel}] + + (d/not-empty? drawing) + [:& shape-options + {:shape (:object drawing) + :page-id page-id + :file-id file-id + :shared-libs shared-libs}] + + (= 0 (count selected)) + [:& page/options] + + (= 1 (count selected)) + [:& shape-options + {:shape (first selected-shapes) + :page-id page-id + :file-id file-id + :shared-libs shared-libs + :shapes-with-children shapes-with-children}] + + :else + [:& multiple/options + {:shapes-with-children shapes-with-children + :shapes selected-shapes + :page-id page-id + :file-id file-id + :shared-libs shared-libs}])]] + [:& tab-element {:id :prototype + :title (tr "workspace.options.prototype")} + [:div {:class (stl/css :element-options)} + [:& interactions-menu {:shape (first shapes)}]]] + [:& tab-element {:id :inspect + :title (tr "workspace.options.inspect")} + [:div {:class (stl/css :element-options)} + [:& hrs/right-sidebar {:page-id page-id + :objects objects + :file-id file-id + :frame shape-parent-frame + :shapes selected-shapes + :on-change-section on-change-section + :on-expand on-expand + :from :workspace}]]]]])) ;; TODO: this need optimizations, selected-objects and ;; selected-objects-with-children are derefed always but they only ;; need on multiple selection in majority of cases (mf/defc options-toolbox - {::mf/wrap [mf/memo] - ::mf/wrap-props false} - [props] - (let [section (obj/get props "section") - selected (obj/get props "selected") - page-id (mf/use-ctx ctx/current-page-id) + {::mf/memo true + ::mf/props :obj} + [{:keys [section selected on-change-section on-expand]}] + (let [page-id (mf/use-ctx ctx/current-page-id) file-id (mf/use-ctx ctx/current-file-id) shapes (mf/deref refs/selected-objects) shapes-with-children (mf/deref refs/selected-shapes-with-children)] @@ -149,4 +187,6 @@ :shapes-with-children shapes-with-children :file-id file-id :page-id page-id - :section section}])) + :section section + :on-change-section on-change-section + :on-expand on-expand}])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.scss b/frontend/src/app/main/ui/workspace/sidebar/options.scss new file mode 100644 index 0000000000..80a1229947 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options.scss @@ -0,0 +1,40 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.tool-window { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + padding-left: $s-12; +} + +.tab-spacing { + margin-right: $s-12; +} + +.content-class { + overflow-y: auto; + overflow-x: hidden; + height: calc(100vh - $s-96); + scrollbar-gutter: stable; +} + +.element-options { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + gap: $s-8; + padding-top: $s-8; +} + +.inspect { + scrollbar-gutter: unset; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs index 00f081aeed..1efb38e5a0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs @@ -5,11 +5,13 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.common + (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.util.dom :as dom] [rumext.v2 :as mf])) -(mf/defc advanced-options [{:keys [visible? children]}] +(mf/defc advanced-options [{:keys [visible? class children]}] (let [ref (mf/use-ref nil)] (mf/use-effect (mf/deps visible?) @@ -17,9 +19,8 @@ (when-let [node (mf/ref-val ref)] (when visible? (dom/scroll-into-view-if-needed! node))))) - (when visible? - [:div.advanced-options-wrapper {:ref ref} - [:div.advanced-options {} - children]]))) + [:div {:class (dm/str class " " (stl/css :advanced-options-wrapper)) + :ref ref} + children]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/common.scss b/frontend/src/app/main/ui/workspace/sidebar/options/common.scss new file mode 100644 index 0000000000..ce1ffb4d6f --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/common.scss @@ -0,0 +1,11 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.advanced-options-wrapper { + @include flexColumn; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.cljs index 3ecf47267a..d91fc97577 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.cljs @@ -5,74 +5,107 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.align + (:require-macros [app.main.style :as stl]) (:require [app.main.data.workspace :as dw] [app.main.data.workspace.shortcuts :as sc] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.icons :as i] + [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) (mf/defc align-options [] - (let [selected (mf/deref refs/selected-shapes) - + (let [selected (mf/deref refs/selected-shapes) ;; don't need to watch objects, only read the value - objects (deref refs/workspace-page-objects) + objects (deref refs/workspace-page-objects) - disabled (not (dw/can-align? selected objects)) + disabled-align (not (dw/can-align? selected objects)) + disabled-distribute (not (dw/can-distribute? selected)) - disabled-distribute (not(dw/can-distribute? selected))] + align-objects + (mf/use-fn + (fn [event] + (let [value (-> (dom/get-current-target event) + (dom/get-data "value") + (keyword))] + (st/emit! (dw/align-objects value))))) - [:div.align-options - [:div.align-group - [:div.align-button.tooltip.tooltip-bottom - {:alt (tr "workspace.align.hleft" (sc/get-tooltip :align-left)) - :class (when disabled "disabled") - :on-click #(st/emit! (dw/align-objects :hleft))} - i/shape-halign-left] + distribute-objects + (mf/use-fn + (fn [event] + (let [value (-> (dom/get-current-target event) + (dom/get-data "value") + (keyword))] + (st/emit! (dw/distribute-objects value)))))] - [:div.align-button.tooltip.tooltip-bottom - {:alt (tr "workspace.align.hcenter" (sc/get-tooltip :align-hcenter)) - :class (when disabled "disabled") - :on-click #(st/emit! (dw/align-objects :hcenter))} - i/shape-halign-center] + (when (not (and disabled-align disabled-distribute)) + [:div {:class (stl/css :align-options)} + [:div {:class (stl/css :align-group)} + [:button {:class (stl/css-case :align-button true + :disabled disabled-align) + :disabled disabled-align + :title (tr "workspace.align.hleft" (sc/get-tooltip :align-left)) + :data-value "hleft" + :on-click align-objects} + i/align-left] - [:div.align-button.tooltip.tooltip-bottom - {:alt (tr "workspace.align.hright" (sc/get-tooltip :align-right)) - :class (when disabled "disabled") - :on-click #(st/emit! (dw/align-objects :hright))} - i/shape-halign-right] + [:button {:class (stl/css-case :align-button true + :disabled disabled-align) + :disabled disabled-align + :title (tr "workspace.align.hcenter" (sc/get-tooltip :align-hcenter)) + :data-value "hcenter" + :on-click align-objects} + i/align-horizontal-center] - [:div.align-button.tooltip.tooltip-bottom - {:alt (tr "workspace.align.hdistribute" (sc/get-tooltip :h-distribute)) - :class (when disabled-distribute "disabled") - :on-click #(st/emit! (dw/distribute-objects :horizontal))} - i/shape-hdistribute]] + [:button {:class (stl/css-case :align-button true + :disabled disabled-align) + :disabled disabled-align + :title (tr "workspace.align.hright" (sc/get-tooltip :align-right)) + :data-value "hright" + :on-click align-objects} + i/align-right] - [:div.align-group - [:div.align-button.tooltip.tooltip-bottom-left - {:alt (tr "workspace.align.vtop" (sc/get-tooltip :align-top)) - :class (when disabled "disabled") - :on-click #(st/emit! (dw/align-objects :vtop))} - i/shape-valign-top] + [:button {:class (stl/css-case :align-button true + :disabled disabled-distribute) + :disabled disabled-distribute + :title (tr "workspace.align.hdistribute" (sc/get-tooltip :h-distribute)) + :data-value "horizontal" + :on-click distribute-objects} + i/distribute-horizontally]] - [:div.align-button.tooltip.tooltip-bottom-left - {:alt (tr "workspace.align.vcenter" (sc/get-tooltip :align-vcenter)) - :class (when disabled "disabled") - :on-click #(st/emit! (dw/align-objects :vcenter))} - i/shape-valign-center] + [:div {:class (stl/css :align-group)} + [:button {:class (stl/css-case :align-button true + :disabled disabled-align) + :disabled disabled-align + :title (tr "workspace.align.vtop" (sc/get-tooltip :align-top)) + :data-value "vtop" + :on-click align-objects} + i/align-top] - [:div.align-button.tooltip.tooltip-bottom-left - {:alt (tr "workspace.align.vbottom" (sc/get-tooltip :align-bottom)) - :class (when disabled "disabled") - :on-click #(st/emit! (dw/align-objects :vbottom))} - i/shape-valign-bottom] + [:button {:class (stl/css-case :align-button true + :disabled disabled-align) + :disabled disabled-align + :title (tr "workspace.align.vcenter" (sc/get-tooltip :align-vcenter)) + :data-value "vcenter" + :on-click align-objects} + i/align-vertical-center] - [:div.align-button.tooltip.tooltip-bottom-left - {:alt (tr "workspace.align.vdistribute" (sc/get-tooltip :v-distribute)) - :class (when disabled-distribute "disabled") - :on-click #(st/emit! (dw/distribute-objects :vertical))} - i/shape-vdistribute]]])) + [:button {:class (stl/css-case :align-button true + :disabled disabled-align) + :disabled disabled-align + :title (tr "workspace.align.vbottom" (sc/get-tooltip :align-bottom)) + :data-value "vbottom" + :on-click align-objects} + i/align-bottom] + + [:button {:title (tr "workspace.align.vdistribute" (sc/get-tooltip :v-distribute)) + :class (stl/css-case :align-button true + :disabled disabled-distribute) + :disabled disabled-distribute + :data-value "vertical" + :on-click distribute-objects} + i/distribute-vertical-spacing]]]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.scss new file mode 100644 index 0000000000..f5a14d50e2 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.scss @@ -0,0 +1,41 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.align-options { + display: flex; + gap: $s-4; + height: $s-32; + margin: 0 calc(-1 * $s-2); +} +.align-group { + @include flexRow; +} + +.align-button { + @extend .button-tertiary; + height: $s-28; + width: $s-28; + padding: 0; + border-radius: $br-8; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + &.disabled { + cursor: default; + svg { + stroke: var(--button-foreground-color-disabled); + } + &:hover { + background-color: var(--panel-background-color); + svg { + stroke: var(--button-foreground-color-disabled); + } + } + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs index 8b6eb78952..89ecb0bc9a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs @@ -5,12 +5,14 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.blur + (:require-macros [app.main.style :as stl]) (:require [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.store :as st] + [app.main.ui.components.numeric-input :refer [numeric-input*]] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.icons :as i] - [app.main.ui.workspace.sidebar.options.rows.input-row :refer [input-row]] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) @@ -24,30 +26,40 @@ :hidden false})) (mf/defc blur-menu [{:keys [ids type values]}] - (let [blur (:blur values) - has-value? (not (nil? blur)) - multiple? (= blur :multiple) + (let [blur (:blur values) + has-value? (not (nil? blur)) + + state* (mf/use-state {:show-content true + :show-more-options false}) + state (deref state*) + open? (:show-content state) + more-options? (:show-more-options state) + + toggle-content (mf/use-fn #(swap! state* update :show-content not)) + + toggle-more-options (mf/use-fn #(swap! state* update :show-more-options not)) + hidden? (:hidden blur) change! - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [update-fn] (st/emit! (dch/update-shapes ids update-fn)))) handle-add - (mf/use-callback + (mf/use-fn (mf/deps change!) (fn [] (change! #(assoc % :blur (create-blur))))) handle-delete - (mf/use-callback + (mf/use-fn (mf/deps change!) (fn [] (change! #(dissoc % :blur)))) handle-change - (mf/use-callback + (mf/use-fn (mf/deps change!) (fn [value] (change! #(cond-> % @@ -58,33 +70,52 @@ (assoc-in [:blur :value] value))))) handle-toggle-visibility - (mf/use-callback + (mf/use-fn (mf/deps change!) (fn [] (change! #(update-in % [:blur :hidden] not))))] - [:div.element-set - [:div.element-set-title - [:span - (case type - :multiple (tr "workspace.options.blur-options.title.multiple") - :group (tr "workspace.options.blur-options.title.group") - (tr "workspace.options.blur-options.title"))] - - [:div.element-set-title-actions - (when (and has-value? (not multiple?)) - [:div.add-page {:on-click handle-toggle-visibility} (if (:hidden blur) i/eye-closed i/eye)]) - - (if has-value? - [:div.add-page {:on-click handle-delete} i/minus] - [:div.add-page {:on-click handle-add} i/close])]] - - (cond - has-value? - [:div.element-set-content - [:& input-row {:label "Value" - :class "pixels" - :min "0" - :value (:value blur) - :placeholder (tr "settings.multiple") - :on-change handle-change}]])])) + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-title)} + [:& title-bar {:collapsable has-value? + :collapsed (not open?) + :on-collapsed toggle-content + :title (case type + :multiple (tr "workspace.options.blur-options.title.multiple") + :group (tr "workspace.options.blur-options.title.group") + (tr "workspace.options.blur-options.title")) + :class (stl/css-case :title-spacing-blur (not has-value?))} + (when-not has-value? + [:button {:class (stl/css :add-blur) + :on-click handle-add} i/add])]] + (when (and open? has-value?) + [:div {:class (stl/css :element-set-content)} + [:div {:class (stl/css-case :first-row true + :hidden hidden?)} + [:div {:class (stl/css :blur-info)} + [:button {:class (stl/css-case :show-more true + :selected more-options?) + :on-click toggle-more-options} + i/menu] + [:span {:class (stl/css :label)} + (tr "workspace.options.blur-options.title")]] + [:div {:class (stl/css :actions)} + [:button {:class (stl/css :action-btn) + :on-click handle-toggle-visibility} + (if hidden? + i/hide + i/shown)] + [:button {:class (stl/css :action-btn) + :on-click handle-delete} i/remove-icon]]] + (when more-options? + [:div {:class (stl/css :second-row)} + [:label {:class (stl/css :label) + :for "blur-input-sidebar"} + (tr "inspect.attributes.blur.value")] + [:> numeric-input* + {:className (stl/css :numeric-input) + :placeholder "--" + :id "blur-input-sidebar" + :min "0" + :on-change handle-change + :value (:value blur)}]])])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss new file mode 100644 index 0000000000..170d43f0c7 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss @@ -0,0 +1,108 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + margin: 0; +} + +.title-spacing-blur { + padding-left: $s-2; + margin: 0; +} + +.add-blur { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.element-set-content { + @include flexColumn; + margin-bottom: $s-8; +} + +.first-row { + @include flexRow; + width: 100%; + .blur-info { + display: flex; + align-items: center; + gap: $s-1; + flex-grow: 1; + border-radius: $br-8; + background-color: var(--input-details-color); + .show-more { + @extend .button-secondary; + height: $s-32; + width: $s-28; + border-radius: $br-8 0 0 $br-8; + box-sizing: border-box; + border: $s-1 solid var(--button-secondary-background-color-rest); + svg { + @extend .button-icon; + } + &.selected { + background-color: var(--button-radio-background-color-active); + svg { + stroke: var(--button-radio-foreground-color-active); + } + } + } + .label { + @include bodySmallTypography; + flex-grow: 1; + display: flex; + align-items: center; + height: $s-32; + padding: 0 $s-8; + border-radius: 0 $br-8 $br-8 0; + background-color: var(--input-background-color); + color: var(--menu-foreground-color); + box-sizing: border-box; + border: $s-1 solid var(--input-border-color); + } + } + .actions { + @include flexRow; + .action-btn { + @extend .button-tertiary; + box-sizing: border-box; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } + } + } + + &.hidden { + .blur-info { + @include hiddenElement; + .show-more { + @include hiddenElement; + border: $s-1 solid var(--input-border-color-disabled); + } + .label { + @include hiddenElement; + border: $s-1 solid var(--input-border-color-disabled); + } + } + } +} + +.second-row { + @extend .input-element; + width: $s-92; + .label { + padding-left: $s-8; + width: $s-60; + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs index 29f79ca7a0..e223622b8e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs @@ -5,84 +5,91 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.bool + (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] [app.main.data.workspace :as dw] [app.main.data.workspace.shortcuts :as sc] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] [app.main.ui.icons :as i] - [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) +(def ^:private flatten-icon + (i/icon-xref :boolean-flatten (stl/css :flatten-icon))) + (mf/defc bool-options [] - (let [selected (mf/deref refs/selected-objects) + (let [selected (mf/deref refs/selected-objects) + head (first selected) selected-with-children (mf/deref refs/selected-shapes-with-children) - - has-invalid-shapes? (->> selected-with-children - (some (comp #{:frame :text} :type))) + has-invalid-shapes? (->> selected-with-children + (some (comp #{:frame :text} :type))) + is-group? (and (some? head) (= :group (:type head))) + is-bool? (and (some? head) (= :bool (:type head))) + head-bool-type (and (some? head) is-bool? (:bool-type head)) first-not-group-like? (and (= (count selected) 1) (not (contains? #{:group :bool} (:type (first selected))))) disabled-bool-btns (or (empty? selected) has-invalid-shapes? first-not-group-like?) - disabled-flatten (or (empty? selected) has-invalid-shapes?) - - head (first selected) - is-group? (and (some? head) (= :group (:type head))) - is-bool? (and (some? head) (= :bool (:type head))) - head-bool-type (and (some? head) is-bool? (:bool-type head)) + disabled-flatten (or (empty? selected) has-invalid-shapes?) set-bool - (fn [bool-type] - #(cond - (> (count selected) 1) - (st/emit! (dw/create-bool bool-type)) + (mf/use-fn + (mf/deps selected is-group? is-bool?) + (fn [bool-type] + (let [bool-type (keyword bool-type)] + (cond + (> (count selected) 1) + (st/emit! (dw/create-bool bool-type)) - (and (= (count selected) 1) is-group?) - (st/emit! (dw/group-to-bool (:id head) bool-type)) + (and (= (count selected) 1) is-group?) + (st/emit! (dw/group-to-bool (:id head) bool-type)) - (and (= (count selected) 1) is-bool?) - (if (= head-bool-type bool-type) - (st/emit! (dw/bool-to-group (:id head))) - (st/emit! (dw/change-bool-type (:id head) bool-type)))))] + (and (= (count selected) 1) is-bool?) + (if (= head-bool-type bool-type) + (st/emit! (dw/bool-to-group (:id head))) + (st/emit! (dw/change-bool-type (:id head) bool-type))))))) - [:div.align-options - [:div.align-group - [:div.align-button.tooltip.tooltip-bottom - {:alt (str (tr "workspace.shape.menu.union") " (" (sc/get-tooltip :bool-union) ")") - :class (dom/classnames :disabled disabled-bool-btns - :selected (= head-bool-type :union)) - :on-click (set-bool :union)} - i/bool-union] + flatten-objects (mf/use-fn #(st/emit! (dw/convert-selected-to-path)))] - [:div.align-button.tooltip.tooltip-bottom - {:alt (str (tr "workspace.shape.menu.difference") " (" (sc/get-tooltip :bool-difference) ")") - :class (dom/classnames :disabled disabled-bool-btns - :selected (= head-bool-type :difference)) - :on-click (set-bool :difference)} - i/bool-difference] - - [:div.align-button.tooltip.tooltip-bottom - {:alt (str (tr "workspace.shape.menu.intersection") " (" (sc/get-tooltip :bool-intersection) ")") - :class (dom/classnames :disabled disabled-bool-btns - :selected (= head-bool-type :intersection)) - :on-click (set-bool :intersection)} - i/bool-intersection] - - [:div.align-button.tooltip.tooltip-bottom - {:alt (str (tr "workspace.shape.menu.exclude") " (" (sc/get-tooltip :bool-exclude) ")") - :class (dom/classnames :disabled disabled-bool-btns - :selected (= head-bool-type :exclude)) - :on-click (set-bool :exclude)} - i/bool-exclude]] - - [:div.align-group - [:div.align-button.tooltip.tooltip-bottom - {:alt (tr "workspace.shape.menu.flatten") - :class (dom/classnames :disabled disabled-flatten) - :on-click #(st/emit! (dw/convert-selected-to-path))} - i/bool-flatten]]])) + (when (not (and disabled-bool-btns disabled-flatten)) + [:div {:class (stl/css :boolean-options)} + [:div {:class (stl/css :bool-group)} + [:& radio-buttons {:selected (d/name head-bool-type) + :class (stl/css :boolean-radio-btn) + :on-change set-bool + :name "bool-options"} + [:& radio-button {:icon i/boolean-union + :value "union" + :disabled disabled-bool-btns + :title (str (tr "workspace.shape.menu.union") " (" (sc/get-tooltip :bool-union) ")") + :id "bool-opt-union"}] + [:& radio-button {:icon i/boolean-difference + :value "difference" + :disabled disabled-bool-btns + :title (str (tr "workspace.shape.menu.difference") " (" (sc/get-tooltip :bool-difference) ")") + :id "bool-opt-differente"}] + [:& radio-button {:icon i/boolean-intersection + :value "intersection" + :disabled disabled-bool-btns + :title (str (tr "intersection") " (" (sc/get-tooltip :bool-intersection) ")") + :id "bool-opt-intersection"}] + [:& radio-button {:icon i/boolean-exclude + :value "exclude" + :disabled disabled-bool-btns + :title (str (tr "exclude") " (" (sc/get-tooltip :bool-exclude) ")") + :id "bool-opt-exclude"}]]] + [:button + {:title (tr "workspace.shape.menu.flatten") + :class (stl/css-case + :flatten-button true + :disabled disabled-flatten) + :disabled disabled-flatten + :on-click flatten-objects} + flatten-icon]]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.scss new file mode 100644 index 0000000000..a325143a92 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.scss @@ -0,0 +1,48 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.boolean-options { + display: grid; + grid-template-columns: repeat(8, $s-28); + column-gap: $s-4; + height: $s-32; +} + +.bool-group { + display: grid; + grid-template-columns: subgrid; + grid-column: 1 / span 4; +} + +.flatten-button { + @extend .button-tertiary; + height: $s-32; + width: $s-32; + border-radius: $br-8; + grid-column: 5 / span 1; + --flatten-icon-foreground-color: var(--icon-foreground); + + &.disabled { + cursor: default; + --flatten-icon-foreground-color: var(--button-foreground-color-disabled); + + &:hover { + background-color: var(--panel-background-color); + --flatten-icon-foreground-color: var(--button-foreground-color-disabled); + } + } +} + +.flatten-icon { + @extend .button-icon; + stroke: var(--flatten-icon-foreground-color); +} + +.boolean-radio-btn { + background-color: transparent; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs index 87bc5af239..88f0ef0b40 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.color-selection + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] @@ -12,7 +13,7 @@ [app.main.data.workspace.colors :as dc] [app.main.data.workspace.selection :as dws] [app.main.store :as st] - [app.main.ui.icons :as i] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] [app.util.i18n :as i18n :refer [tr]] [cuerdas.core :as str] @@ -24,6 +25,7 @@ color-id (:fill-color-ref-id fill) shared-libs-colors (dm/get-in shared-libs [color-file-id :data :colors]) is-shared? (contains? shared-libs-colors color-id) + has-color? (or (not (nil? (:fill-color fill))) (not (nil? (:fill-color-gradient fill)))) attrs (if (or is-shared? (= color-file-id file-id)) (d/without-nils {:color (str/lower (:fill-color fill)) :opacity (:fill-opacity fill) @@ -33,10 +35,11 @@ (d/without-nils {:color (str/lower (:fill-color fill)) :opacity (:fill-opacity fill) :gradient (:fill-color-gradient fill)}))] - {:attrs attrs - :prop :fill - :shape-id (:shape-id fill) - :index (:index fill)})) + (when has-color? + {:attrs attrs + :prop :fill + :shape-id (:shape-id fill) + :index (:index fill)}))) (defn stroke->color-att [stroke file-id shared-libs] @@ -44,7 +47,7 @@ color-id (:stroke-color-ref-id stroke) shared-libs-colors (dm/get-in shared-libs [color-file-id :data :colors]) is-shared? (contains? shared-libs-colors color-id) - has-color? (not (nil? (:stroke-color stroke))) + has-color? (or (not (nil? (:stroke-color stroke))) (not (nil? (:stroke-color-gradient stroke)))) attrs (if (or is-shared? (= color-file-id file-id)) (d/without-nils {:color (str/lower (:stroke-color stroke)) :opacity (:stroke-opacity stroke) @@ -149,10 +152,19 @@ :library-colors library-colors})) (mf/defc color-selection-menu - {::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]} - [{:keys [shapes file-id shared-libs] :as props}] - (let [{:keys [ grouped-colors library-colors colors]} (mf/with-memo [shapes file-id shared-libs] - (prepare-colors shapes file-id shared-libs)) + {::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))] + ::mf/wrap-props false} + [{:keys [shapes file-id shared-libs]}] + (let [{:keys [grouped-colors library-colors colors]} (mf/with-memo [shapes file-id shared-libs] + (prepare-colors shapes file-id shared-libs)) + + state* (mf/use-state true) + open? (deref state*) + + has-colors? (or (some? (seq colors)) (some? (seq library-colors))) + + toggle-content (mf/use-fn #(swap! state* not)) + expand-lib-color (mf/use-state false) expand-color (mf/use-state false) @@ -164,7 +176,9 @@ (fn [new-color old-color from-picker?] (let [old-color (-> old-color (dissoc :name :path) d/without-nils) - ;; When dragging on the color picker sometimes all the shapes hasn't updated the color to the prev value so we need this extra calculation + ;; When dragging on the color picker sometimes all + ;; the shapes hasn't updated the color to the prev + ;; value so we need this extra calculation shapes-by-old-color (get @grouped-colors* old-color) prev-color (d/seek #(get @grouped-colors* %) @prev-colors*) shapes-by-prev-color (get @grouped-colors* prev-color) @@ -202,54 +216,60 @@ (mf/with-effect [grouped-colors] (reset! grouped-colors* grouped-colors)) - [:div.element-set - [:div.element-set-title - [:span (tr "workspace.options.selection-color")]] - [:div.element-set-content - [:div.selected-colors - (for [[index color] (d/enumerate (take 3 library-colors))] - [:& color-row {:key (dm/str "library-color-" (:color color)) - :color color - :index index - :on-detach on-detach - :select-only select-only - :on-change #(on-change %1 color %2) - :on-open on-open - :on-close on-close}]) - (when (and (false? @expand-lib-color) (< 3 (count library-colors))) - [:div.expand-colors {:on-click #(reset! expand-lib-color true)} - [:span i/actions] - [:span.text (tr "workspace.options.more-lib-colors")]]) - (when @expand-lib-color - (for [[index color] (d/enumerate (drop 3 library-colors))] - [:& color-row {:key (dm/str "library-color-" (:color color)) + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-title)} + [:& title-bar {:collapsable has-colors? + :collapsed (not open?) + :on-collapsed toggle-content + :title (tr "workspace.options.selection-color") + :class (stl/css-case :title-spacing-selected-colors (not has-colors?))}]] + + (when open? + [:div {:class (stl/css :element-content)} + [:div {:class (stl/css :selected-color-group)} + (for [[index color] (d/enumerate (take 3 library-colors))] + [:& color-row {:key (dm/str "library-color-" index) :color color :index index :on-detach on-detach :select-only select-only :on-change #(on-change %1 color %2) :on-open on-open - :on-close on-close}]))] - - [:div.selected-colors - (for [[index color] (d/enumerate (take 3 colors))] - [:& color-row {:key (dm/str "color-" index) - :color color - :index index - :select-only select-only - :on-change #(on-change %1 color %2) - :on-open on-open - :on-close on-close}]) - (when (and (false? @expand-color) (< 3 (count colors))) - [:div.expand-colors {:on-click #(reset! expand-color true)} - [:span i/actions] - [:span.text (tr "workspace.options.more-colors")]]) - (when @expand-color - (for [[index color] (d/enumerate (drop 3 colors))] - [:& color-row {:key (dm/str "color-" (:color color)) + :on-close on-close}]) + (when (and (false? @expand-lib-color) (< 3 (count library-colors))) + [:button {:class (stl/css :more-colors-btn) + :on-click #(reset! expand-lib-color true)} + (tr "workspace.options.more-lib-colors")]) + (when @expand-lib-color + (for [[index color] (d/enumerate (drop 3 library-colors))] + [:& color-row {:key (dm/str "library-color-" index) + :color color + :index index + :on-detach on-detach + :select-only select-only + :on-change #(on-change %1 color %2) + :on-open on-open + :on-close on-close}]))] + [:div {:class (stl/css :selected-color-group)} + (for [[index color] (d/enumerate (take 3 colors))] + [:& color-row {:key (dm/str "color-" index) :color color :index index :select-only select-only :on-change #(on-change %1 color %2) :on-open on-open - :on-close on-close}]))]]])) + :on-close on-close}]) + (when (and (false? @expand-color) (< 3 (count colors))) + [:button {:class (stl/css :more-colors-btn) + :on-click #(reset! expand-color true)} + (tr "workspace.options.more-colors")]) + + (when @expand-color + (for [[index color] (d/enumerate (drop 3 colors))] + [:& color-row {:key (dm/str "color-" (:color color)) + :color color + :index index + :select-only select-only + :on-change #(on-change %1 color %2) + :on-open on-open + :on-close on-close}]))]])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss new file mode 100644 index 0000000000..0ded56cb7a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss @@ -0,0 +1,40 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + margin: 0; +} + +.title-spacing-selected-colors { + padding-left: $s-2; + margin: 0; +} + +.add-fill { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.element-content { + @include flexColumn; + margin-bottom: $s-8; +} + +.selected-color-group { + @include flexColumn; +} + +.more-colors-btn { + @extend .button-secondary; + @include uppercaseTitleTipography; + height: $s-32; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index 8ff2ebdb3a..570b25faf8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -5,267 +5,642 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.component + (:require-macros [app.main.style :as stl]) (:require - [app.common.pages.helpers :as cph] - [app.common.types.components-list :as ctkl] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.types.component :as ctk] [app.common.types.file :as ctf] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.specialized-panel :as dwsp] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.context-menu :refer [context-menu]] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.components.search-bar :refer [search-bar]] + [app.main.ui.components.select :refer [select]] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.context :as ctx] + [app.main.ui.hooks :as h] [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.assets.common :as cmm] + [app.util.debug :as dbg] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [app.util.timers :as tm] [cuerdas.core :as str] + [okulary.core :as l] [rumext.v2 :as mf])) -(def component-attrs [:component-id :component-file :shape-ref :main-instance? :annotation]) - +(def ref:annotations-state + (l/derived :workspace-annotations st/state)) (mf/defc component-annotation - [{:keys [id values shape component] :as props}] - (let [main-instance? (:main-instance? values) - component-id (:component-id values) - annotation (:annotation component) - editing? (mf/use-state false) - invalid-text? (mf/use-state (or (nil? annotation)(str/empty? annotation))) - size (mf/use-state (count annotation)) - textarea-ref (mf/use-ref) + {::mf/props :obj} + [{:keys [id shape component]}] + (let [main-instance? (:main-instance shape) + component-id (:component-id shape) + annotation (:annotation component) + shape-id (:id shape) - ;; hack to create an autogrowing textarea - ;; based on https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/ - autogrow #(let [textarea (mf/ref-val textarea-ref) - text (when textarea (.-value textarea))] - (reset! invalid-text? (str/empty? text)) - (when textarea - (reset! size (count text)) - (aset (.-dataset (.-parentNode textarea)) "replicatedValue" text))) - initialize #(let [textarea (mf/ref-val textarea-ref)] - (when textarea - (aset textarea "value" annotation) - (autogrow))) + editing* (mf/use-state false) + editing? (deref editing*) - discard (fn [event] - (dom/stop-propagation event) - (let [textarea (mf/ref-val textarea-ref)] - (aset textarea "value" annotation) - (reset! editing? false) - (st/emit! (dw/set-annotations-id-for-create nil)))) - save (fn [event] - (dom/stop-propagation event) - (let [textarea (mf/ref-val textarea-ref) - text (.-value textarea)] - (when-not (str/blank? text) - (reset! editing? false) - (st/emit! - (dw/set-annotations-id-for-create nil) - (dw/update-component-annotation component-id text))))) - workspace-annotations (mf/deref refs/workspace-annotations) - annotations-expanded? (:expanded? workspace-annotations) - creating? (= id (:id-for-create workspace-annotations)) + invalid-text* (mf/use-state #(str/blank? annotation)) + invalid-text? (deref invalid-text*) - expand #(when-not (or @editing? creating?) - (st/emit! (dw/set-annotations-expanded %))) - edit (fn [event] - (dom/stop-propagation event) - (when main-instance? - (let [textarea (mf/ref-val textarea-ref)] - (reset! editing? true) - (dom/focus! textarea)))) - on-delete-annotation - (mf/use-callback - (mf/deps shape) + size* (mf/use-state #(count annotation)) + size (deref size*) + + textarea-ref (mf/use-ref) + + state (mf/deref ref:annotations-state) + expanded? (:expanded state) + create-id (:id-for-create state) + creating? (= id create-id) + + ;; hack to create an autogrowing textarea based on + ;; https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/ + adjust-textarea-size + (mf/use-fn + #(when-let [textarea (mf/ref-val textarea-ref)] + (let [text (dom/get-value textarea)] + (reset! invalid-text* (str/blank? text)) + (reset! size* (count text)) + (let [^js parent (.-parentNode textarea) + ^js dataset (.-dataset parent)] + (set! (.-replicatedValue dataset) text))))) + + on-toggle-expand + (mf/use-fn + (mf/deps expanded? editing? creating?) + (fn [_] + (st/emit! (dw/set-annotations-expanded (not expanded?))))) + + on-discard + (mf/use-fn + (mf/deps adjust-textarea-size creating?) (fn [event] (dom/stop-propagation event) - (st/emit! (modal/show - {:type :confirm - :title (tr "modals.delete-component-annotation.title") - :message (tr "modals.delete-component-annotation.message") - :accept-label (tr "ds.confirm-ok") - :on-accept (fn [] - (st/emit! - (dw/set-annotations-id-for-create nil) - (dw/update-component-annotation component-id nil)))}))))] + (when-let [textarea (mf/ref-val textarea-ref)] + (dom/set-value! textarea annotation) + (reset! editing* false) + (when creating? + (st/emit! (dw/set-annotations-id-for-create nil))) + (adjust-textarea-size)))) - (mf/use-effect - (mf/deps shape) - (fn [] - (initialize) - (when (and (not creating?) (:id-for-create workspace-annotations)) ;; cleanup set-annotations-id-for-create if we aren't on the marked component - (st/emit! (dw/set-annotations-id-for-create nil))) - (fn [] (st/emit! (dw/set-annotations-id-for-create nil))))) ;; cleanup set-annotationsid-for-create on unload + on-edit + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (when ^boolean main-instance? + (when-let [textarea (mf/ref-val textarea-ref)] + (reset! editing* true) + (dom/focus! textarea))))) + + on-save + (mf/use-fn + (mf/deps creating?) + (fn [event] + (dom/stop-propagation event) + (when-let [textarea (mf/ref-val textarea-ref)] + (let [text (dom/get-value textarea)] + (when-not (str/blank? text) + (reset! editing* false) + (st/emit! (dw/update-component-annotation component-id text)) + (when ^boolean creating? + (st/emit! (dw/set-annotations-id-for-create nil)))))))) + + + on-delete-annotation + (mf/use-fn + (mf/deps shape-id component-id creating?) + (fn [event] + (dom/stop-propagation event) + (let [on-accept (fn [] + (st/emit! + ;; (ptk/data-event {::ev/name "delete-component-annotation"}) + (when creating? + (dw/set-annotations-id-for-create nil)) + (dw/update-component-annotation component-id nil)))] + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.delete-component-annotation.title") + :message (tr "modals.delete-component-annotation.message") + :accept-label (tr "ds.confirm-ok") + :on-accept on-accept})))))] + + (mf/with-effect [shape-id state create-id creating?] + (when-let [textarea (mf/ref-val textarea-ref)] + (dom/set-value! textarea annotation) + (adjust-textarea-size)) + + ;; cleanup set-annotations-id-for-create if we aren't on the marked component + (when (and (not creating?) (some? create-id)) + (st/emit! (dw/set-annotations-id-for-create nil))) + + ;; cleanup set-annotationsid-for-create on unload + (fn [] + (when creating? + (st/emit! (dw/set-annotations-id-for-create nil))))) (when (or creating? annotation) - [:div.component-annotation {:class (dom/classnames :editing @editing? :creating creating?)} - [:div.title {:class (dom/classnames :expandeable (not (or @editing? creating?))) - :on-click #(expand (not annotations-expanded?))} - [:div (if (or @editing? creating?) - (if @editing? - (tr "workspace.options.component.edit-annotation") - (tr "workspace.options.component.create-annotation")) - [:* (if annotations-expanded? - [:div.expand i/arrow-down] - [:div.expand i/arrow-slide]) - (tr "workspace.options.component.annotation")])] - [:div - (when (and main-instance? annotations-expanded?) - (if (or @editing? creating?) - [:* - [:div.icon {:title (if creating? (tr "labels.create") (tr "labels.save")) - :on-click save - :class (dom/classnames :hidden @invalid-text?)} i/tick] - [:div.icon {:title (tr "labels.discard") - :on-click discard} i/cross]] - [:* - [:div.icon {:title (tr "labels.edit") - :on-click edit} i/pencil] - [:div.icon {:title (tr "labels.delete") - :on-click on-delete-annotation} i/trash]]))]] + [:div {:class (stl/css-case + :component-annotation true + :editing editing? + :creating creating?)} + [:div {:class (stl/css-case + :annotation-title true + :expandeable (not (or editing? creating?)) + :expanded expanded?) + :on-click on-toggle-expand} - [:div {:class (dom/classnames :hidden (not annotations-expanded?))} - [:div.grow-wrap - [:div.texarea-copy] + (if (or editing? creating?) + [:span {:class (stl/css :annotation-text)} + (if editing? + (tr "workspace.options.component.edit-annotation") + (tr "workspace.options.component.create-annotation"))] + + [:* + [:span {:class (stl/css-case + :icon-arrow true + :expanded expanded?)} + i/arrow] + [:span {:class (stl/css :annotation-text)} + (tr "workspace.options.component.annotation")]]) + + [:div {:class (stl/css :icons-wrapper)} + (when (and ^boolean main-instance? + ^boolean expanded?) + (if (or ^boolean editing? + ^boolean creating?) + [:* + [:div {:title (if ^boolean creating? + (tr "labels.create") + (tr "labels.save")) + :on-click on-save + :class (stl/css-case + :icon true + :icon-tick true + :hidden invalid-text?)} + i/tick] + [:div {:class (stl/css :icon :icon-cross) + :title (tr "labels.discard") + :on-click on-discard} + i/close]] + + [:* + [:div {:class (stl/css :icon :icon-edit) + :title (tr "labels.edit") + :on-click on-edit} + i/curve] + [:div {:class (stl/css :icon :icon-trash) + :title (tr "labels.delete") + :on-click on-delete-annotation} + i/delete]]))]] + + [:div {:class (stl/css-case :hidden (not expanded?))} + [:div {:class (stl/css :grow-wrap)} + [:div {:class (stl/css :texarea-copy)}] [:textarea {:ref textarea-ref :id "annotation-textarea" :data-debug annotation - :auto-focus true + :auto-focus (or editing? creating?) :maxLength 300 - :on-input autogrow + :on-input adjust-textarea-size :default-value annotation - :read-only (not (or creating? @editing?))}]] - (when (or @editing? creating?) - [:div.counter (str @size "/300")])]]))) + :read-only (not (or creating? editing?))}]] + (when (or editing? creating?) + [:div {:class (stl/css :counter)} (str size "/300")])]]))) + +(mf/defc component-swap-item + {::mf/props :obj} + [{:keys [item loop shapes file-id root-shape container component-id is-search listing-thumbs]}] + (let [on-select + (mf/use-fn + (mf/deps shapes file-id item) + #(when-not loop + (st/emit! (dwl/component-multi-swap shapes file-id (:id item))))) + + item-ref (mf/use-ref) + visible? (h/use-visible item-ref :once? true)] + [:div {:ref item-ref + :title (if is-search (:full-name item) (:name item)) + :class (stl/css-case :component-item (not listing-thumbs) + :grid-cell listing-thumbs + :selected (= (:id item) component-id) + :disabled loop) + :key (str "swap-item-" (:id item)) + :on-click on-select} + (when visible? + [:& cmm/component-item-thumbnail {:file-id (:file-id item) + :root-shape root-shape + :component item + :container container}]) + [:span {:class (stl/css-case :component-name true + :selected (= (:id item) component-id))} + (if is-search (:full-name item) (:name item))]])) + +(mf/defc component-group-item + {::mf/props :obj} + [{:keys [item on-enter-group]}] + (let [group-name (:name item) + path (cfh/butlast-path-with-dots group-name) + on-group-click #(on-enter-group group-name)] + [:div {:class (stl/css :component-group) + :on-click on-group-click + :title group-name} + + [:div {:class (stl/css :path-wrapper)} + (when-not (str/blank? path) + [:span {:class (stl/css :component-group-path)} + (str "\u00A0\u2022\u00A0" path)]) + [:span {:class (stl/css :component-group-name)} + (cfh/last-path group-name)]] + + [:span {:class (stl/css :arrow-icon)} + i/arrow]])) + +(def ^:private ref:swap-libraries + (letfn [(get-libraries [state] + (let [file (:workspace-file state) + data (:workspace-data state) + libs (:workspace-libraries state)] + (assoc libs (:id file) + (assoc file :data data))))] + (l/derived get-libraries st/state))) +(defn- find-common-path + ([components] + (let [paths (map (comp cfh/split-path :path) components)] + (find-common-path paths [] 0))) + ([paths path n] + (let [current (nth (first paths) n nil)] + (if (or (nil? current) + (not (every? #(= current (nth % n nil)) paths))) + path + (find-common-path paths (conj path current) (inc n)))))) + +(defn- same-component-file? + [shape-a shape-b] + (= (:component-file shape-a) + (:component-file shape-b))) + +(defn- same-component? + [shape-a shape-b] + (= (:component-id shape-a) + (:component-id shape-b))) + + +(mf/defc component-swap + {::mf/props :obj} + [{:keys [shapes]}] + (let [single? (= 1 (count shapes)) + shape (first shapes) + current-file-id (mf/use-ctx ctx/current-file-id) + libraries (mf/deref ref:swap-libraries) + objects (mf/deref refs/workspace-page-objects) + + ^boolean + every-same-file? (every? (partial same-component-file? shape) shapes) + + component-id (if (every? (partial same-component? shape) shapes) + (:component-id shape) + nil) + + file-id (if every-same-file? + (:component-file shape) + current-file-id) + + components (map #(ctf/get-component libraries (:component-file %) (:component-id %)) shapes) + + path (if single? + (:path (first components)) + (cfh/join-path (if (not every-same-file?) + "" + (find-common-path components)))) + + filters* (mf/use-state + {:term "" + :file-id file-id + :path (or path "") + :listing-thumbs? false}) + + filters (deref filters*) + + is-search? (not (str/blank? (:term filters))) + + + current-library-id (if (contains? libraries (:file-id filters)) + (:file-id filters) + current-file-id) + + current-library-name (if (= current-library-id current-file-id) + (str/upper (tr "workspace.assets.local-library")) + (dm/get-in libraries [current-library-id :name])) + + components (->> (get-in libraries [current-library-id :data :components]) + vals + (remove #(true? (:deleted %))) + (map #(assoc % :full-name (cfh/merge-path-item-with-dot (:path %) (:name %))))) + + get-subgroups (fn [path] + (let [split-path (cfh/split-path path)] + (reduce (fn [acc dir] + (conj acc (str (last acc) " / " dir))) + [(first split-path)] (rest split-path)))) + + xform (comp + (map :path) + (mapcat get-subgroups) + (remove str/empty?) + (remove nil?) + (distinct) + (filter #(= (cfh/butlast-path %) (:path filters)))) + + groups (when-not is-search? + (->> (sort (sequence xform components)) + (map (fn [name] {:name name})))) + + components (if is-search? + (filter #(str/includes? (str/lower (:full-name %)) (str/lower (:term filters))) components) + (filter #(= (:path %) (:path filters)) components)) + + items (if (or is-search? (:listing-thumbs? filters)) + (sort-by :full-name components) + (->> (concat groups components) + (sort-by :name))) + + find-parent-components + (mf/use-fn + (mf/deps objects) + (fn [shape] + (->> (cfh/get-parents objects (:id shape)) + (map :component-id) + (remove nil?)))) + + ;; Get the ids of the components that are parents of the shapes, to avoid loops + parent-components (mapcat find-parent-components shapes) + + + libraries-options (map (fn [library] {:value (:id library) :label (:name library)}) + (vals libraries)) + + on-library-change + (mf/use-fn + (fn [id] + (swap! filters* assoc :file-id id :term "" :path ""))) + + on-search-term-change + (mf/use-fn + (fn [term] + (swap! filters* assoc :term term))) + + + on-search-clear-click + (mf/use-fn #(swap! filters* assoc :term "")) + + on-go-back + (mf/use-fn + (mf/deps (:path filters)) + #(swap! filters* assoc :path (cfh/butlast-path (:path filters)))) + + on-enter-group + (mf/use-fn #(swap! filters* assoc :path %)) + + toggle-list-style + (mf/use-fn + (fn [style] + (swap! filters* assoc :listing-thumbs? (= style "grid")))) + + filters-but-last (cfh/butlast-path (:path filters)) + last-filters (cfh/last-path (:path filters)) + filter-path-with-dots (->> filters-but-last (cfh/split-path) (cfh/join-path-with-dot))] + + [:div {:class (stl/css :component-swap)} + [:div {:class (stl/css :element-set-title)} + [:span (tr "workspace.options.component.swap")]] + [:div {:class (stl/css :component-swap-content)} + [:div {:class (stl/css :fields-wrapper)} + [:div {:class (stl/css :search-field)} + [:& search-bar {:on-change on-search-term-change + :clear-action on-search-clear-click + :class (stl/css :search-wrapper) + :id "swap-component-search-filter" + :value (:term filters) + :placeholder (str (tr "labels.search") " " (get-in libraries [current-library-id :name])) + :icon (mf/html [:span {:class (stl/css :search-icon)} + i/search])}]] + + [:& select {:class (stl/css :select-library) + :default-value current-library-id + :options libraries-options + :on-change on-library-change}]] + + [:div {:class (stl/css :swap-wrapper)} + [:div {:class (stl/css :library-name-wrapper)} + [:div {:class (stl/css :library-name)} current-library-name] + + [:div {:class (stl/css :listing-options-wrapper)} + [:& radio-buttons {:class (stl/css :listing-options) + :selected (if (:listing-thumbs? filters) "grid" "list") + :on-change toggle-list-style + :name "swap-listing-style"} + [:& radio-button {:icon i/view-as-list + :value "list" + :id "swap-opt-list"}] + [:& radio-button {:icon i/flex-grid + :value "grid" + :id "swap-opt-grid"}]]]] + + (when-not (or is-search? (str/empty? (:path filters))) + [:button {:class (stl/css :component-path) + :on-click on-go-back + :title filter-path-with-dots} + [:span {:class (stl/css :back-arrow)} i/arrow] + (when-not (= "" filter-path-with-dots) + [:span {:class (stl/css :path-name)} + (dm/str "\u00A0\u2022\u00A0" filter-path-with-dots)]) + [:span {:class (stl/css :path-name-last)} last-filters]]) + + (when (empty? items) + [:div {:class (stl/css :component-list-empty)} + (tr "workspace.options.component.swap.empty")]) ;;TODO review this empty space + + (when (:listing-thumbs? filters) + [:div {:class (stl/css :component-list)} + (for [item groups] + [:& component-group-item {:item item :on-enter-group on-enter-group}])]) + + [:div {:class (stl/css-case :component-grid (:listing-thumbs? filters) + :component-list (not (:listing-thumbs? filters)))} + (for [item items] + (if (:id item) + (let [data (dm/get-in libraries [current-library-id :data]) + container (ctf/get-component-page data item) + root-shape (ctf/get-component-root data item) + components (->> (cfh/get-children-with-self (:objects container) (:id root-shape)) + (keep :component-id) + set) + loop? (some #(contains? components %) parent-components)] + [:& component-swap-item {:key (dm/str (:id item)) + :item item + :loop loop? + :shapes shapes + :file-id current-library-id + :root-shape root-shape + :container container + :component-id component-id + :is-search is-search? + :listing-thumbs (:listing-thumbs? filters)}]) + + [:& component-group-item {:item item + :key (:name item) + :on-enter-group on-enter-group}]))]]]])) + +(mf/defc component-ctx-menu + {::mf/props :obj} + [{:keys [menu-entries on-close show main-instance]}] + (let [do-action + (fn [action event] + (dom/stop-propagation event) + (action) + (on-close))] + [:& dropdown {:show show :on-close on-close} + [:ul {:class (stl/css-case :custom-select-dropdown true + :not-main (not main-instance))} + (for [{:keys [msg] :as entry} menu-entries] + (when (some? msg) + [:li {:key msg + :class (stl/css :dropdown-element) + :on-click (partial do-action (:action entry))} + [:span {:class (stl/css :dropdown-label)} (tr msg)]]))]])) (mf/defc component-menu - [{:keys [ids values shape] :as props}] + {::mf/props :obj} + [{:keys [shapes swap-opened?]}] (let [current-file-id (mf/use-ctx ctx/current-file-id) components-v2 (mf/use-ctx ctx/components-v2) - - objects (deref refs/workspace-page-objects) - touched? (cph/component-touched? objects (:id shape)) - can-update-main? (or (not components-v2) touched?) - - id (first ids) - local (mf/use-state {:menu-open false}) - - shape-name (:name shape) - - component-id (:component-id values) - library-id (:component-file values) - show? (some? component-id) - main-instance? (if components-v2 - (:main-instance? values) - true) - main-component? (:main-instance? values) - local-component? (= library-id current-file-id) workspace-data (deref refs/workspace-data) workspace-libraries (deref refs/workspace-libraries) - component (if local-component? - (ctkl/get-component workspace-data component-id) - (ctf/get-component workspace-libraries library-id component-id)) - is-dangling? (nil? component) - lacks-annotation? (nil? (:annotation component)) - lib-exists? (and (not local-component?) - (some? (get workspace-libraries library-id))) + state* (mf/use-state {:show-content true + :menu-open false}) + state (deref state*) + open? (:show-content state) + menu-open? (:menu-open state) + + shapes (filter ctk/instance-head? shapes) + multi (> (count shapes) 1) + copies (filter ctk/in-component-copy? shapes) + can-swap? (and components-v2 (seq copies)) + + ;; For when it's only one shape + shape (first shapes) + id (:id shape) + shape-name (:name shape) + component (ctf/resolve-component shape + {:id current-file-id + :data workspace-data} + workspace-libraries + {:include-deleted? true}) + main-instance? (if components-v2 (ctk/main-instance? shape) true) + + toggle-content + (mf/use-fn #(swap! state* update :show-content not)) on-menu-click - (mf/use-callback + (mf/use-fn (fn [event] (dom/prevent-default event) (dom/stop-propagation event) - (swap! local assoc :menu-open true))) + (swap! state* update :menu-open not))) on-menu-close - (mf/use-callback - #(swap! local assoc :menu-open false)) + (mf/use-fn + #(swap! state* assoc :menu-open false)) - do-detach-component - #(st/emit! (dwl/detach-component id)) + on-component-back + (mf/use-fn + #(st/emit! ::dwsp/interrupt)) - do-reset-component - #(st/emit! (dwl/reset-component id)) + open-component-panel + (mf/use-fn + (mf/deps can-swap? shapes) + (fn [] + (let [search-id "swap-component-search-filter"] + (when can-swap? (st/emit! (dwsp/open-specialized-panel :component-swap))) + (tm/schedule-on-idle #(dom/focus! (dom/get-element search-id)))))) - do-update-component - #(st/emit! (dwl/update-component-sync id library-id)) + menu-entries (cmm/generate-components-menu-entries shapes components-v2) + show-menu? (seq menu-entries) + path (->> component (:path) (cfh/split-path) (cfh/join-path-with-dot))] - do-restore-component - #(st/emit! (dwl/restore-component library-id component-id) - (dw/go-to-main-instance nil component-id)) + (when (seq shapes) + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-title)} + (if swap-opened? + [:button {:class (stl/css :title-back) + :on-click on-component-back} + [:span {:class (stl/css :icon-back)} i/arrow] + [:span (tr "workspace.options.component")]] - do-update-remote-component - #(st/emit! (modal/show - {:type :confirm - :message "" - :title (tr "modals.update-remote-component.message") - :hint (tr "modals.update-remote-component.hint") - :cancel-label (tr "modals.update-remote-component.cancel") - :accept-label (tr "modals.update-remote-component.accept") - :accept-style :primary - :on-accept do-update-component})) + [:& title-bar {:collapsable true + :collapsed (not open?) + :on-collapsed toggle-content + :title (tr "workspace.options.component") + :class (stl/css :title-spacing-component)} + [:span {:class (stl/css :copy-text)} + (if main-instance? + (tr "workspace.options.component.main") + (tr "workspace.options.component.copy"))]])] - do-show-component #(st/emit! (dw/go-to-component component-id)) - do-show-in-assets #(st/emit! (if components-v2 - (dw/show-component-in-assets component-id) - (dw/go-to-component component-id))) - do-create-annotation #(st/emit! (dw/set-annotations-id-for-create id)) - do-navigate-component-file #(st/emit! (dwl/nav-to-component-file library-id))] - (when show? - [:div.element-set - [:div.element-set-title - [:span (tr "workspace.options.component")]] - [:div.element-set-content - [:div.row-flex.component-row - (if main-instance? - i/component - i/component-copy) - shape-name - [:div.row-actions - {:on-click on-menu-click} - i/actions - ;; WARNING: this menu is the same as the shape context menu. - ;; If you change it, you must change equally the file - ;; app/main/ui/workspace/context_menu.cljs - [:& context-menu {:on-close on-menu-close - :show (:menu-open @local) - :options - (if main-component? - [[(tr "workspace.shape.menu.show-in-assets") do-show-in-assets] - (when (and components-v2 local-component? lacks-annotation?) - [(tr "workspace.shape.menu.create-annotation") do-create-annotation])] - (if local-component? - (if is-dangling? - [[(tr "workspace.shape.menu.detach-instance") do-detach-component] - (when can-update-main? - [(tr "workspace.shape.menu.reset-overrides") do-reset-component]) - (when components-v2 - [(tr "workspace.shape.menu.restore-main") do-restore-component])] + (when open? + [:div {:class (stl/css :element-content)} + [:div {:class (stl/css-case :component-wrapper true + :with-actions show-menu? + :without-actions (not show-menu?))} + [:button {:class (stl/css-case :component-name-wrapper true + :with-main (and can-swap? (not multi)) + :swappeable (and can-swap? (not swap-opened?))) + :on-click open-component-panel} - [[(tr "workspace.shape.menu.detach-instance") do-detach-component] - (when can-update-main? - [(tr "workspace.shape.menu.reset-overrides") do-reset-component]) - (when can-update-main? - [(tr "workspace.shape.menu.update-main") do-update-component]) - [(tr "workspace.shape.menu.show-main") do-show-component]]) + [:span {:class (stl/css :component-icon)} + (if main-instance? + i/component + i/component-copy)] - (if is-dangling? - [[(tr "workspace.shape.menu.detach-instance") do-detach-component] - (when can-update-main? - [(tr "workspace.shape.menu.reset-overrides") do-reset-component]) - (when (and components-v2 lib-exists?) - [(tr "workspace.shape.menu.restore-main") do-restore-component])] - [[(tr "workspace.shape.menu.detach-instance") do-detach-component] - (when can-update-main? - [(tr "workspace.shape.menu.reset-overrides") do-reset-component]) - (when can-update-main? - [(tr "workspace.shape.menu.update-main") do-update-remote-component]) - [(tr "workspace.shape.menu.go-main") do-navigate-component-file]])))}]]] + [:div {:class (stl/css :name-wrapper)} + [:div {:class (stl/css :component-name)} + [:span {:class (stl/css :component-name-inside)} + (if multi + (tr "settings.multiple") + (cfh/last-path shape-name))]] - (when components-v2 - [:& component-annotation {:id id :values values :shape shape :component component}])]]))) + (when (and can-swap? (not multi)) + [:div {:class (stl/css :component-parent-name)} + (cfh/merge-path-item-with-dot path (:name component))])]] + + (when show-menu? + [:div {:class (stl/css :component-actions)} + [:button {:class (stl/css-case :menu-btn true + :selected menu-open?) + :on-click on-menu-click} + i/menu] + + [:& component-ctx-menu {:show menu-open? + :on-close on-menu-close + :menu-entries menu-entries + :main-instance main-instance?}]])] + + (when swap-opened? + [:& component-swap {:shapes copies}]) + + (when (and (not swap-opened?) (not multi) components-v2) + [:& component-annotation {:id id :shape shape :component component}]) + (when (dbg/enabled? :display-touched) + [:div ":touched " (str (:touched shape))])])]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss new file mode 100644 index 0000000000..d024187a78 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss @@ -0,0 +1,667 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; +.element-set { + margin: 0; + padding-top: $s-8; +} + +.element-content { + @include flexColumn; +} + +.title-back { + @include uppercaseTitleTipography; + display: flex; + align-items: center; + gap: $s-4; + width: 100%; + height: $s-32; + padding: 0; + border: 0; + border-radius: $br-8; + background-color: var(--title-background-color); + color: var(--title-foreground-color); + cursor: pointer; +} + +.icon-back { + @include flexCenter; + width: $s-12; + height: 100%; + svg { + height: $s-12; + width: $s-12; + stroke: var(--icon-foreground); + transform: rotate(180deg); + } +} + +.component-wrapper { + width: 100%; + min-height: $s-32; + border-radius: $br-8; + + &.with-actions { + display: grid; + grid-template-columns: 1fr $s-28; + gap: $s-2; + } + + &.without-actions { + padding-right: 0.5rem; + .component-name-wrapper { + width: 100%; + border-radius: $br-8; + } + } +} + +.component-name-wrapper { + @include buttonStyle; + cursor: default; + display: grid; + grid-template-columns: $s-12 1fr; + gap: $s-4; + padding: 0 $s-8; + border-radius: $br-8 0 0 $br-8; + background-color: var(--assets-item-background-color); + color: var(--assets-item-name-foreground-color-hover); + &:hover { + background-color: var(--assets-item-background-color-hover); + color: var(--assets-item-name-foreground-color-hover); + } +} + +.component-icon { + @include flexCenter; + height: $s-32; + width: $s-12; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } +} + +.name-wrapper { + @include flexColumn; + min-height: $s-32; + padding: $s-8 0 $s-8 $s-2; + border-radius: $br-8 0 0 $br-8; + overflow: hidden; +} + +.component-name { + @include bodySmallTypography; + @include textEllipsis; + direction: rtl; + text-align: left; + min-height: $s-16; +} + +.component-name-inside { + direction: ltr; + unicode-bidi: bidi-override; +} + +.component-parent-name { + @include bodySmallTypography; + @include textEllipsis; + direction: rtl; + text-align: left; + min-height: $s-16; + max-width: $s-184; + color: var(--title-foreground-color); +} + +.component-actions { + position: relative; +} + +.menu-btn { + @extend .button-tertiary; + height: 100%; + width: $s-28; + border-radius: 0 $br-8 $br-8 0; + background-color: var(--assets-item-background-color); + color: var(--assets-item-name-foreground-color-hover); + svg { + @extend .button-icon; + min-height: $s-16; + min-width: $s-16; + } + &:hover { + background-color: var(--assets-item-background-color-hover); + color: var(--assets-item-name-foreground-color-hover); + &.selected { + @extend .button-icon-selected; + } + } +} + +.menu-btn.selected { + @extend .button-icon-selected; +} + +.copy-text { + @include bodySmallTypography; + height: 100%; + display: flex; + align-items: center; + color: var(--title-foreground-color); + margin-right: $s-8; +} + +.swappeable { + cursor: pointer; +} + +.custom-select-dropdown { + @extend .dropdown-wrapper; + right: 0; + left: unset; + width: $s-252; +} + +.not-main { + top: $s-56; +} + +.dropdown-element { + @extend .dropdown-element-base; +} + +.icon-wrapper { + display: flex; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } +} + +.input-text { + @include removeInputStyle; + height: $s-32; + width: 100%; + margin: 0; + padding: $s-4; + border: 0; + font-size: $fs-12; + color: var(--input-foreground-color-active); + &::placeholder { + color: var(--input-foreground-color-disabled); + } + &:focus-visible { + border-color: var(--input-border-outline-color-active); + } +} + +.clear-btn { + @include buttonStyle; + @include flexCenter; + height: $s-16; + width: $s-16; + .clear-icon { + @include flexCenter; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } +} + +.search-icon { + @include flexCenter; + width: $s-12; + margin-left: $s-8; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } +} + +.component-path { + display: flex; + align-items: center; + gap: $s-4; + width: 100%; + height: $s-32; + padding: 0; + border: 0; + background-color: var(--title-background-color); + color: var(--title-foreground-color); + cursor: pointer; +} + +.back-arrow { + @include flexCenter; + height: $s-32; + svg { + height: $s-12; + width: $s-12; + stroke: var(--icon-foreground); + transform: rotate(180deg); + } +} + +.path-name { + @include bodySmallTypography; + @include textEllipsis; + direction: rtl; + height: $s-32; + padding: $s-8 0 $s-8 $s-2; +} + +.path-name-last { + @include bodySmallTypography; + @include textEllipsis; + height: $s-32; + padding: $s-8 0 $s-8 $s-2; + color: white; +} + +.component-list-empty { + @include bodySmallTypography; + margin: 0 $s-4 0 $s-8; + color: $df-secondary; +} + +.component-item { + display: flex; + align-items: center; + margin-bottom: $s-4; + padding-right: $s-8; + font-size: $s-12; + cursor: pointer; + width: 100%; + height: $s-44; + border-radius: $br-8; + background-color: var(--assets-item-background-color); + color: var(--assets-item-name-foreground-color); + + .component-name { + @include textEllipsis; + width: 80%; + } + + svg, + img { + background-color: var(--assets-component-background-color); + border-radius: $br-8; + height: $s-36; + width: $s-36; + margin: $s-2 $s-8 $s-2 $s-2; + } + + .selected { + color: var(--assets-item-name-foreground-color-hover); + } + + &:hover { + color: var(--assets-item-name-foreground-color-hover); + background-color: var(--assets-item-background-color-hover); + } + + &.disabled { + cursor: auto; + color: var(--assets-item-name-foreground-color-disabled); + background-color: var(--assets-item-background-color); + + svg { + cursor: auto; + } + } +} + +.component-grid { + display: grid; + grid-template-columns: repeat(2, $s-124); + grid-auto-rows: $s-124; + gap: $s-4; +} + +.grid-cell { + @include flexCenter; + place-items: center; + flex-wrap: wrap; + position: relative; + padding: $s-8; + border-radius: $br-8; + background-color: var(--assets-component-background-color); + overflow: hidden; + --assets-component-current-border-color: var(--assets-component-border-color); + border: $s-4 solid var(--assets-component-current-border-color); + cursor: pointer; + img { + height: auto; + width: auto; + max-height: 100%; + max-width: 100%; + pointer-events: none; + border: 0; + } + svg { + height: 100%; + width: 100%; + stroke: none; + object-fit: contain; + } + .component-name { + @include bodySmallTypography; + @include textEllipsis; + display: none; + position: absolute; + left: 0; + bottom: 0; + width: 100%; + padding: $s-2; + text-align: center; + direction: rtl; + } + + &:hover { + background-color: var(--assets-item-background-color-hover); + .component-name { + display: block; + color: var(--assets-item-name-foreground-color-hover); + background: linear-gradient(to top, var(--assets-item-background-color-hover) 0%, transparent 100%); + } + } + + &.selected { + --assets-component-current-border-color: var(--assets-item-border-color); + .component-name { + color: var(--assets-item-name-foreground-color-hover); + } + } + + &.disabled { + background: var(--assets-component-background-color-disabled); + cursor: auto; + svg { + cursor: auto; + } + .component-name { + background: linear-gradient( + to top, + var(--assets-component-background-color-disabled) 0%, + transparent 100% + ); + color: var(--assets-item-name-foreground-color-hover); + } + } +} + +.element-set-title { + @include uppercaseTitleTipography; + display: flex; + align-items: center; + height: $s-32; + padding-left: $s-2; + color: var(--title-foreground-color); +} + +// Component swap + +.component-swap { + padding-top: $s-12; +} + +.component-swap-content { + @include flexColumn; + gap: $s-16; +} + +.fields-wrapper { + @include flexColumn; + gap: $s-4; +} + +.search-field { + display: flex; + align-items: center; + height: $s-32; + border-radius: $br-8; + font-family: "worksans", sans-serif; + background-color: var(--input-background-color); +} + +.library-name-wrapper { + display: grid; + grid-template-columns: 1fr auto; +} + +.library-name { + @include bodySmallTypography; + @include textEllipsis; + color: var(--title-foreground-color); + padding: $s-8 0 $s-8 $s-2; +} + +.swap-wrapper { + @include flexColumn; + gap: $s-4; +} + +.listing-options-wrapper { + width: 100%; +} + +.listing-options { + display: flex; + align-items: center; +} + +.component-group { + @include bodySmallTypography; + display: grid; + grid-template-columns: 1fr $s-12; + height: $s-32; + cursor: pointer; + + .component-group-name { + @include textEllipsis; + color: var(--assets-item-name-foreground-color); + } + &:hover { + color: var(--assets-item-name-foreground-color-hover); + .component-group-name { + color: var(--assets-item-name-foreground-color-hover); + } + } +} + +.arrow-icon { + @include flexCenter; + height: $s-32; + svg { + height: $s-12; + width: $s-12; + stroke: var(--icon-foreground); + } +} + +.path-wrapper { + display: flex; + max-width: $s-232; + padding: $s-8 0 $s-8 $s-2; +} + +.component-group-path { + @include textEllipsis; + direction: rtl; + color: var(--assets-item-name-foreground-color-rest); +} + +// Component annotation + +.component-annotation { + @include bodySmallTypography; + color: var(--entry-foreground-color); + border-radius: $br-8; + + .annotation-title { + display: flex; + align-items: center; + height: $s-32; + + &.expanded { + border-bottom: $s-1 solid var(--entry-border-color-disabled); + } + + &.expandeable { + cursor: pointer; + } + + div { + display: flex; + align-items: center; + line-height: 2.5; + } + + .icon-arrow { + @include flexCenter; + width: $s-28; + height: $s-32; + display: flex; + margin: 0; + padding: 0; + cursor: pointer; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + width: $s-16; + height: $s-16; + } + &.expanded svg { + transform: rotate(90deg); + } + } + + .icon { + @include flexCenter; + width: $s-28; + height: $s-32; + border-radius: $br-8; + display: none; + margin: 0; + padding: 0; + cursor: pointer; + + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + width: $s-16; + height: $s-16; + } + + &.icon-tick:hover, + &.icon-edit:hover { + svg { + stroke: var(--icon-foreground-accept); + } + } + + &.icon-cross:hover, + &.icon-trash:hover { + svg { + stroke: var(--icon-foreground-discard); + } + } + } + + .annotation-text { + flex-grow: 1; + margin-left: $s-12; + } + + &:hover { + .icon { + display: flex; + } + } + } + + &.editing { + border: $s-1 solid var(--input-border-color-success); + .annotation-title { + border-bottom: $s-1 solid var(--entry-border-color-disabled); + .icon { + display: flex; + } + } + + textarea { + min-height: $s-252; + } + } + + &.creating { + border: $s-1 solid var(--input-border-color-success); + .annotation-title .icon { + display: flex; + } + textarea { + min-height: $s-252; + } + } + + .hidden { + display: none; + svg { + display: none; + } + } + + .counter { + @include bodySmallTypography; + text-align: right; + color: var(--entry-foreground-color); + margin: 0 $s-8 $s-8 0; + } + + // Auto growing text + .grow-wrap { + // easy way to plop the elements on top of each other and have them both sized based on the tallest one's height + display: grid; + + &:after { + // The space is needed to preventy jumpy behavior + content: attr(data-replicated-value) " "; + white-space: pre-wrap; + visibility: hidden; + } + + textarea { + background-color: var(--input-background-color-active); + color: var(--input-foreground-color-active); + padding: $s-12; + + border: none; + overflow: hidden; + outline: none; + + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + + resize: none; /*remove the resize handle on the bottom right*/ + } + + textarea, + &:after { + /* Identical styling required!! */ + font: inherit; + overflow-wrap: anywhere; + + padding: 10px; + + /* Place on top of each other */ + grid-area: 1 / 1 / 2 / 2; + } + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs index bb7a3af8d8..ce3bdedca6 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs @@ -5,13 +5,17 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.constraints + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.select :refer [select]] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -26,13 +30,19 @@ (mf/defc constraints-menu [{:keys [ids values] :as props}] - (let [old-shapes (deref (refs/objects-by-id ids)) - frames (map #(deref (refs/object-by-id (:frame-id %))) old-shapes) + (let [state* (mf/use-state true) + open? (deref state*) + + toggle-content (mf/use-fn #(swap! state* not)) + + old-shapes (deref (refs/objects-by-id ids)) + frames (map #(deref (refs/object-by-id (:frame-id %))) old-shapes) shapes (as-> old-shapes $ (map gsh/translate-to-frame $ frames)) - values (let [{:keys [x y]} (-> shapes first :points gsh/points->selrect)] + ;; FIXME: performance rect + values (let [{:keys [x y]} (-> shapes first :points grc/points->rect)] (cond-> values (not= (:x values) :multiple) (assoc :x x) (not= (:y values) :multiple) (assoc :y y))) @@ -51,127 +61,173 @@ constraints-h (or (get values :constraints-h) (gsh/default-constraints-h values)) constraints-v (or (get values :constraints-v) (gsh/default-constraints-v values)) + on-constraint-button-clicked - (mf/use-callback - (mf/deps [ids values]) - (fn [button] - (fn [_] - (let [constraints-h (get values :constraints-h :scale) - constraints-v (get values :constraints-v :scale) + (mf/use-fn + (mf/deps ids values) + (fn [event] + (let [button (-> (dom/get-current-target event) + (dom/get-data "value") + (keyword)) + constraints-h (get values :constraints-h :scale) + constraints-v (get values :constraints-v :scale) - [constraint new-value] - (case button - :top (case constraints-v - :top [:constraints-v :scale] - :topbottom [:constraints-v :bottom] - :bottom [:constraints-v :topbottom] - [:constraints-v :top]) - :bottom (case constraints-v - :bottom [:constraints-v :scale] - :topbottom [:constraints-v :top] - :top [:constraints-v :topbottom] - [:constraints-v :bottom]) - :left (case constraints-h - :left [:constraints-h :scale] - :leftright [:constraints-h :right] - :right [:constraints-h :leftright] - [:constraints-h :left]) - :right (case constraints-h - :right [:constraints-h :scale] - :leftright [:constraints-h :left] - :left [:constraints-h :leftright] - [:constraints-h :right]) - :centerv (case constraints-v - :center [:constraints-v :scale] - [:constraints-v :center]) - :centerh (case constraints-h - :center [:constraints-h :scale] - [:constraints-h :center]))] - (st/emit! (dch/update-shapes - ids - #(assoc % constraint new-value))))))) + [constraint new-value] + (case button + :top (case constraints-v + :top [:constraints-v :scale] + :topbottom [:constraints-v :bottom] + :bottom [:constraints-v :topbottom] + [:constraints-v :top]) + :bottom (case constraints-v + :bottom [:constraints-v :scale] + :topbottom [:constraints-v :top] + :top [:constraints-v :topbottom] + [:constraints-v :bottom]) + :left (case constraints-h + :left [:constraints-h :scale] + :leftright [:constraints-h :right] + :right [:constraints-h :leftright] + [:constraints-h :left]) + :right (case constraints-h + :right [:constraints-h :scale] + :leftright [:constraints-h :left] + :left [:constraints-h :leftright] + [:constraints-h :right]) + :centerv (case constraints-v + :center [:constraints-v :scale] + [:constraints-v :center]) + :centerh (case constraints-h + :center [:constraints-h :scale] + [:constraints-h :center]) + nil ())] - on-constraint-select-changed - (mf/use-callback - (mf/deps [ids values]) - (fn [constraint] - (fn [event] - (let [value (-> (dom/get-target-val event) (keyword))] - (when-not (str/empty? value) - (st/emit! (dch/update-shapes - ids - #(assoc % constraint value)))))))) + (st/emit! (dch/update-shapes + ids + #(assoc % constraint new-value)))))) + + on-constraint-h-select-changed + (mf/use-fn + (mf/deps ids) + (fn [value] + (when-not (str/empty? value) + (st/emit! (dch/update-shapes + ids + #(assoc % :constraints-h (keyword value))))))) + + on-constraint-v-select-changed + (mf/use-fn + (mf/deps ids) + (fn [value] + (when-not (str/empty? value) + (st/emit! (dch/update-shapes + ids + #(assoc % :constraints-v (keyword value))))))) on-fixed-scroll-clicked - (mf/use-callback - (mf/deps [ids values]) + (mf/use-fn + (mf/deps ids) (fn [_] - (st/emit! (dch/update-shapes ids #(update % :fixed-scroll not)))))] + (st/emit! (dch/update-shapes ids #(update % :fixed-scroll not))))) - ;; CONSTRAINTS - (when in-frame? - [:div.element-set - [:div.element-set-title - [:span (tr "workspace.options.constraints")]] + options-h + (mf/with-memo [constraints-h] + (d/concat-vec + (when (= constraints-h :multiple) + [{:value "" :label (tr "settings.multiple")}]) + [{:value "left" :label (tr "workspace.options.constraints.left")} + {:value "right" :label (tr "workspace.options.constraints.right")} + {:value "leftright" :label (tr "workspace.options.constraints.leftright")} + {:value "center" :label (tr "workspace.options.constraints.center")} + {:value "scale" :label (tr "workspace.options.constraints.scale")}])) - [:div.element-set-content - [:div.row-flex.align-top + options-v + (mf/with-memo [constraints-v] + (d/concat-vec + (when (= constraints-v :multiple) + [{:value "" :label (tr "settings.multiple")}]) + [{:value "top" :label (tr "workspace.options.constraints.top")} + {:value "bottom" :label (tr "workspace.options.constraints.bottom")} + {:value "topbottom" :label (tr "workspace.options.constraints.topbottom")} + {:value "center" :label (tr "workspace.options.constraints.center")} + {:value "scale" :label (tr "workspace.options.constraints.scale")}]))] - [:div.constraints-widget - [:div.constraints-box] - [:div.constraint-button.top - {:class (dom/classnames :active (or (= constraints-v :top) - (= constraints-v :topbottom))) - :on-click (on-constraint-button-clicked :top)}] - [:div.constraint-button.bottom - {:class (dom/classnames :active (or (= constraints-v :bottom) - (= constraints-v :topbottom))) - :on-click (on-constraint-button-clicked :bottom)}] - [:div.constraint-button.left - {:class (dom/classnames :active (or (= constraints-h :left) - (= constraints-h :leftright))) - :on-click (on-constraint-button-clicked :left)}] - [:div.constraint-button.right - {:class (dom/classnames :active (or (= constraints-h :right) - (= constraints-h :leftright))) - :on-click (on-constraint-button-clicked :right)}] - [:div.constraint-button.centerv - {:class (dom/classnames :active (= constraints-v :center)) - :on-click (on-constraint-button-clicked :centerv)}] - [:div.constraint-button.centerh - {:class (dom/classnames :active (= constraints-h :center)) - :on-click (on-constraint-button-clicked :centerh)}]] - [:div.constraints-form - [:div.row-flex - [:span.left-right i/full-screen] - [:select.input-select {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :on-change (on-constraint-select-changed :constraints-h) - :value (d/name constraints-h "scale")} - (when (= constraints-h :multiple) - [:option {:value ""} (tr "settings.multiple")]) - [:option {:value "left"} (tr "workspace.options.constraints.left")] - [:option {:value "right"} (tr "workspace.options.constraints.right")] - [:option {:value "leftright"} (tr "workspace.options.constraints.leftright")] - [:option {:value "center"} (tr "workspace.options.constraints.center")] - [:option {:value "scale"} (tr "workspace.options.constraints.scale")]]] - [:div.row-flex - [:span.top-bottom i/full-screen] - [:select.input-select {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :on-change (on-constraint-select-changed :constraints-v) - :value (d/name constraints-v "scale")} - (when (= constraints-v :multiple) - [:option {:value ""} (tr "settings.multiple")]) - [:option {:value "top"} (tr "workspace.options.constraints.top")] - [:option {:value "bottom"} (tr "workspace.options.constraints.bottom")] - [:option {:value "topbottom"} (tr "workspace.options.constraints.topbottom")] - [:option {:value "center"} (tr "workspace.options.constraints.center")] - [:option {:value "scale"} (tr "workspace.options.constraints.scale")]]] + ;; CONSTRAINTS + (when in-frame? + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-title)} + [:& title-bar {:collapsable true + :collapsed (not open?) + :on-collapsed toggle-content + :title (tr "workspace.options.constraints")}]] + (when open? + [:div {:class (stl/css :element-set-content)} + [:div {:class (stl/css :constraints-widget)} + [:div {:class (stl/css :constraints-top)} + [:button {:class (stl/css-case :constraint-btn true + :active (or (= constraints-v :top) + (= constraints-v :topbottom))) + :data-value "top" + :on-click on-constraint-button-clicked} + [:span {:class (stl/css :resalted-area)}]]] + [:div {:class (stl/css :constraints-left)} + [:button {:class (stl/css-case :constraint-btn true + :constraint-btn-rotated true + :active (or (= constraints-h :left) + (= constraints-h :leftright))) + :data-value "left" + :on-click on-constraint-button-clicked} + [:span {:class (stl/css :resalted-area)}]]] + [:div {:class (stl/css :constraints-center)} + [:button {:class (stl/css-case :constraint-btn true + :active (= constraints-h :center)) + :data-value "centerh" + :on-click on-constraint-button-clicked} + [:span {:class (stl/css :resalted-area)}]] + [:button {:class (stl/css-case :constraint-btn-special true + :constraint-btn-rotated true + :active (= constraints-v :center)) + :data-value "centerv" + :on-click on-constraint-button-clicked} + [:span {:class (stl/css :resalted-area)}]]] + [:div {:class (stl/css :constraints-right)} + [:button {:class (stl/css-case :constraint-btn true + :constraint-btn-rotated true + :active (or (= constraints-h :right) + (= constraints-h :leftright))) + :data-value "right" + :on-click on-constraint-button-clicked} + [:span {:class (stl/css :resalted-area)}]]] + [:div {:class (stl/css :constraints-bottom)} + [:button {:class (stl/css-case :constraint-btn true + :active (or (= constraints-v :bottom) + (= constraints-v :topbottom))) + :data-value "bottom" + :on-click on-constraint-button-clicked} + [:span {:class (stl/css :resalted-area)}]]]] + [:div {:class (stl/css :contraints-selects)} + [:div {:class (stl/css :horizontal-select)} + [:& select + {:default-value (d/nilv (d/name constraints-h) "scale") + :options options-h + :on-change on-constraint-h-select-changed}]] + [:div {:class (stl/css :vertical-select)} + [:& select + {:default-value (d/nilv (d/name constraints-v) "scale") + :options options-v + :on-change on-constraint-v-select-changed}]] (when first-level? - [:div.row-flex - [:div.fix-when {:class (dom/classnames :active (:fixed-scroll values)) - :on-click on-fixed-scroll-clicked} - (if (:fixed-scroll values) - i/pin-fill - i/pin) - [:span (tr "workspace.options.constraints.fix-when-scrolling")]]])]]]]))) + [:div {:class (stl/css :checkbox)} + + [:label {:for "fixed-on-scroll" + :class (stl/css-case :checked (:fixed-scroll values))} + [:span {:class (stl/css-case :check-mark true + :checked (:fixed-scroll values))} + (when (:fixed-scroll values) + i/status-tick)] + (tr "workspace.options.constraints.fix-when-scrolling") + [:input {:type "checkbox" + :id "fixed-on-scroll" + :checked (:fixed-scroll values) + :on-change on-fixed-scroll-clicked}]]])]])]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss new file mode 100644 index 0000000000..88da9410d8 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss @@ -0,0 +1,164 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + margin: 0; +} + +.element-set-content { + display: flex; + gap: $s-4; +} + +.constraints-widget { + background-color: var(--constraint-widget-background-color); + display: grid; + grid-template-columns: $s-24 $s-60 $s-24; + grid-template-rows: $s-24 $s-60 $s-24; + grid-template-areas: + "top top top" + "left center right" + "bottom bottom bottom"; + height: $s-108; + width: $s-108; + border-radius: $br-8; +} + +.constraints-top, +.constraints-left, +.constraints-center, +.constraints-right, +.constraints-bottom { + @include flexCenter; + grid-area: top; + .constraint-btn, + .constraint-btn-special, + .constraint-btn-rotated { + @include buttonStyle; + @include flexCenter; + width: 100%; + height: 100%; + .resalted-area { + width: $s-32; + height: $s-3; + border-radius: $br-8; + background-color: var(--button-constraint-background-color-rest); + padding: 0; + margin: 0; + } + &.active .resalted-area { + outline: $s-4 solid var(--button-constraint-border-color-hover); + background-color: var(--button-constraint-background-color-hover); + } + &:hover .resalted-area, + &:focus .resalted-area { + outline: $s-4 solid var(--button-constraint-border-color-hover); + background-color: var(--button-constraint-background-color-hover); + } + } +} +.constraints-left { + grid-area: left; + .constraint-btn-rotated { + height: $s-60; + width: $s-24; + .resalted-area { + height: $s-32; + width: $s-3; + } + } +} +.constraints-center { + grid-area: center; + position: relative; + background-color: var(--constraint-center-area-background-color); + border-radius: $br-8; + .constraint-btn { + width: $s-60; + height: $s-24; + .resalted-area { + width: $s-32; + height: $s-3; + } + } + .constraint-btn-special { + position: absolute; + height: $s-60; + width: $s-24; + .resalted-area { + height: $s-32; + width: $s-3; + } + } +} + +.constraints-right { + grid-area: right; + .constraint-btn-rotated { + height: $s-72; + width: $s-24; + .resalted-area { + height: $s-32; + width: $s-3; + } + } +} + +.constraints-bottom { + grid-area: bottom; +} + +.contraints-selects { + @include flexColumn; +} + +.horizontal-select, +.vertical-select { + width: $s-124; + padding: 0; +} + +.checkbox { + display: flex; + align-items: center; + margin-bottom: $s-8; + margin-top: $s-8; + padding-left: 0; + input { + margin: 0; + } + + label { + @include bodySmallTypography; + display: flex; + align-items: center; + gap: $s-2; + cursor: pointer; + color: var(--input-checkbox-text-foreground-color); + .check-mark { + @include flexCenter; + width: $s-16; + height: $s-16; + border-radius: $br-6; + background-color: var(--input-checkbox-inactive-background-color); + &.checked { + background-color: var(--input-checkbox-background-color-active); + svg { + @extend .button-icon-small; + stroke: var(--input-details-color); + } + } + &:hover { + border-color: var(--input-checkbox-border-color-hover); + } + &:focus { + border-color: var(--input-checkbox-border-color-focus); + } + } + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs index 9fe6cad425..216ad42316 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.exports + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.main.data.exports :as de] @@ -12,6 +13,8 @@ [app.main.data.workspace.state-helpers :as wsh] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.select :refer [select]] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.export] [app.main.ui.icons :as i] [app.util.dom :as dom] @@ -27,6 +30,12 @@ {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "page-id" "file-id"]))]} [{:keys [ids type values page-id file-id] :as props}] (let [exports (:exports values []) + has-exports? (or (= :multiple exports) (some? (seq exports))) + + comp-state* (mf/use-state true) + open? (deref comp-state*) + + toggle-content (mf/use-fn #(swap! comp-state* not)) state (mf/deref refs/export) in-progress? (:in-progress state) @@ -42,7 +51,7 @@ (str suffix)))) scale-enabled? - (mf/use-callback + (mf/use-fn (fn [export] (#{:png :jpeg} (:type export)))) @@ -84,7 +93,7 @@ ;; TODO: maybe move to specific events for avoid to have this logic here? add-export - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [] (let [xspec {:type :png :suffix "" :scale 1}] @@ -93,7 +102,7 @@ (assoc shape :exports (into [xspec] (:exports shape))))))))) delete-export - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [position] (let [remove-fill-by-index (fn [values index] (->> (d/enumerate values) @@ -104,101 +113,125 @@ (st/emit! (dch/update-shapes ids remove))))) on-scale-change - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [index event] - (let [target (dom/get-target event) - value (dom/get-value target) - value (d/parse-double value)] + (let [scale (d/parse-double event)] (st/emit! (dch/update-shapes ids (fn [shape] - (assoc-in shape [:exports index :scale] value))))))) + (assoc-in shape [:exports index :scale] scale))))))) on-suffix-change - (mf/use-callback + (mf/use-fn (mf/deps ids) - (fn [index event] - (let [target (dom/get-target event) - value (dom/get-value target)] + (fn [event] + (let [value (dom/get-target-val event) + index (-> (dom/get-current-target event) + (dom/get-data "value") + (d/parse-integer))] (st/emit! (dch/update-shapes ids (fn [shape] (assoc-in shape [:exports index :suffix] value))))))) on-type-change - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [index event] - (let [target (dom/get-target event) - value (dom/get-value target) - value (keyword value)] + (let [type (keyword event)] (st/emit! (dch/update-shapes ids (fn [shape] - (assoc-in shape [:exports index :type] value))))))) + (assoc-in shape [:exports index :type] type))))))) on-remove-all - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [] (st/emit! (dch/update-shapes ids (fn [shape] (assoc shape :exports [])))))) manage-key-down - (mf/use-callback + (mf/use-fn (fn [event] (let [esc? (kbd/esc? event)] (when esc? - (dom/blur! (dom/get-target event))))))] + (dom/blur! (dom/get-target event)))))) - [:div.element-set.exports-options - [:div.element-set-title - [:span (tr (if (> (count ids) 1) "workspace.options.export-multiple" "workspace.options.export"))] - (when (not (= :multiple exports)) - [:div.add-page {:on-click add-export} i/close])] + size-options [{:value "0.5" :label "0.5x"} + {:value "0.75" :label "0.75x"} + {:value "1" :label "1x"} + {:value "1.5" :label "1.5x"} + {:value "2" :label "2x"} + {:value "4" :label "4x"} + {:value "6" :label "6x"}] - (cond - (= :multiple exports) - [:div.element-set-options-group - [:div.element-set-label (tr "settings.multiple")] - [:div.element-set-actions - [:div.element-set-actions-button {:on-click on-remove-all} - i/minus]]] + format-options [{:value "png" :label "PNG"} + {:value "jpeg" :label "JPG"} + {:value "svg" :label "SVG"} + {:value "pdf" :label "PDF"}]] - (seq exports) - [:div.element-set-content - (for [[index export] (d/enumerate exports)] - [:div.element-set-options-group - {:key index} - (when (scale-enabled? export) - [:select.input-select {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :on-change (partial on-scale-change index) - :value (:scale export)} - [:option {:value "0.5"} "0.5x"] - [:option {:value "0.75"} "0.75x"] - [:option {:value "1"} "1x"] - [:option {:value "1.5"} "1.5x"] - [:option {:value "2"} "2x"] - [:option {:value "4"} "4x"] - [:option {:value "6"} "6x"]]) - [:input.input-text {:value (:suffix export) - :placeholder (tr "workspace.options.export.suffix") - :on-change (partial on-suffix-change index) - :on-key-down manage-key-down}] - [:select.input-select {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :value (d/name (:type export)) - :on-change (partial on-type-change index)} - [:option {:value "png"} "PNG"] - [:option {:value "jpeg"} "JPEG"] - [:option {:value "svg"} "SVG"] - [:option {:value "pdf"} "PDF"]] - [:div.delete-icon {:on-click (partial delete-export index)} - i/minus]])]) + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-title)} + [:& title-bar {:collapsable has-exports? + :collapsed (not open?) + :on-collapsed toggle-content + :title (tr (if (> (count ids) 1) "workspace.options.export-multiple" "workspace.options.export")) + :class (stl/css-case :title-spacing-export (not has-exports?))} + [:button {:class (stl/css :add-export) + :on-click add-export} + i/add]]] + (when open? + [:div {:class (stl/css :element-set-content)} - (when (or (= :multiple exports) (seq exports)) - [:div.btn-icon-dark.download-button - {:on-click (when-not in-progress? on-download) - :class (dom/classnames - :btn-disabled in-progress?) - :disabled in-progress?} - (if in-progress? - (tr "workspace.options.exporting-object") - (tr "workspace.options.export-object" (c (count shapes-with-exports))))])])) + (cond + (= :multiple exports) + [:div {:class (stl/css :multiple-exports)} + [:div {:class (stl/css :label)} (tr "settings.multiple")] + [:div {:class (stl/css :actions)} + [:button {:class (stl/css :action-btn) + :on-click on-remove-all} + i/remove-icon]]] + + (seq exports) + [:* + (for [[index export] (d/enumerate exports)] + [:div {:class (stl/css :element-group) + :key index} + [:div {:class (stl/css :input-wrapper)} + [:div {:class (stl/css :format-select)} + [:& select + {:default-value (d/name (:type export)) + :options format-options + :dropdown-class (stl/css :dropdown-upwards) + :on-change (partial on-type-change index)}]] + (when (scale-enabled? export) + [:div {:class (stl/css :size-select)} + [:& select + {:default-value (str (:scale export)) + :options size-options + :dropdown-class (stl/css :dropdown-upwards) + :on-change (partial on-scale-change index)}]]) + [:label {:class (stl/css :suffix-input) + :for "suffix-export-input"} + [:input {:class (stl/css :type-input) + :id "suffix-export-input" + :type "text" + :value (:suffix export) + :placeholder (tr "workspace.options.export.suffix") + :data-value (str index) + :on-change on-suffix-change + :on-key-down manage-key-down}]]] + + [:button {:class (stl/css :action-btn) + :on-click (partial delete-export index)} + i/remove-icon]])]) + + (when (or (= :multiple exports) (seq exports)) + [:button + {:on-click (when-not in-progress? on-download) + :class (stl/css-case + :export-btn true + :btn-disabled in-progress?) + :disabled in-progress?} + (if in-progress? + (tr "workspace.options.exporting-object") + (tr "workspace.options.export-object" (c (count shapes-with-exports))))])])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss new file mode 100644 index 0000000000..49dd4fe1c5 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss @@ -0,0 +1,102 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + margin: 0; +} + +.title-spacing-export { + padding-left: $s-2; + margin: 0; +} + +.add-export { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.element-set-content { + @include flexColumn; + margin: $s-4 0 $s-8 0; +} + +.multiple-exports { + @include flexRow; + .label { + @extend .mixed-bar; + } + .actions { + @include flexRow; + .action-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } + } + } +} + +.element-group { + display: grid; + grid-template-columns: repeat(8, 1fr); + column-gap: $s-4; + + .action-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } + } +} + +.input-wrapper { + grid-column: span 7; + display: grid; + grid-template-columns: subgrid; +} + +.format-select { + grid-column: span 2; + padding: 0; + + .dropdown-upwards { + bottom: $s-36; + width: $s-80; + top: unset; + } +} + +.size-select { + grid-column: span 2; + padding: 0; + .dropdown-upwards { + bottom: $s-36; + top: unset; + width: $s-80; + } +} + +.suffix-input { + grid-column: span 3; + @extend .input-element; +} + +.export-btn { + @extend .button-secondary; + @include uppercaseTitleTipography; + height: $s-32; + width: $s-252; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index d6e82eeea3..82e8fe530b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -5,12 +5,14 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.fill + (:require-macros [app.main.style :as stl]) (:require [app.common.colors :as clr] [app.common.data :as d] - [app.common.pages :as cp] + [app.common.types.shape.attrs :refer [default-color]] [app.main.data.workspace.colors :as dc] [app.main.store :as st] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.hooks :as h] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] @@ -48,27 +50,41 @@ ;; Excluding nil values values (d/without-nils values) + fills (:fills values) + has-fills? (or (= :multiple fills) (some? (seq fills))) + + + state* (mf/use-state has-fills?) + open? (deref state*) + + toggle-content (mf/use-fn #(swap! state* not)) + + open-content (mf/use-fn #(reset! state* true)) + + close-content (mf/use-fn #(reset! state* false)) hide-fill-on-export? (:hide-fill-on-export values false) checkbox-ref (mf/use-ref) on-add - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [_] - (st/emit! (dc/add-fill ids {:color cp/default-color - :opacity 1})))) + (st/emit! (dc/add-fill ids {:color default-color + :opacity 1})) + + (when (not (some? (seq fills))) (open-content)))) on-change - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [index] (fn [color] (st/emit! (dc/change-fill ids color index))))) on-reorder - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [new-index] (fn [index] @@ -77,15 +93,16 @@ on-remove (fn [index] (fn [] - (st/emit! (dc/remove-fill ids {:color cp/default-color - :opacity 1} index)))) + (st/emit! (dc/remove-fill ids {:color default-color + :opacity 1} index)) + (when (= 1 (count (seq fills))) (close-content)))) on-remove-all (fn [_] (st/emit! (dc/remove-all-fills ids {:color clr/black :opacity 1}))) on-detach - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [index] (fn [color] @@ -94,7 +111,7 @@ (st/emit! (dc/change-fill ids color index)))))) on-change-show-fill-on-export - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [event] (let [value (-> event dom/get-target dom/checked?)] @@ -103,45 +120,53 @@ disable-drag (mf/use-state false) on-focus (fn [_] - (reset! disable-drag true)) + (reset! disable-drag true)) on-blur (fn [_] (reset! disable-drag false))] (mf/use-layout-effect - (mf/deps hide-fill-on-export?) - #(let [checkbox (mf/ref-val checkbox-ref)] - (when checkbox + (mf/deps hide-fill-on-export?) + #(let [checkbox (mf/ref-val checkbox-ref)] + (when checkbox ;; Note that the "indeterminate" attribute only may be set by code, not as a static attribute. ;; See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#attr-indeterminate - (if (= hide-fill-on-export? :multiple) - (dom/set-attribute! checkbox "indeterminate" true) - (dom/remove-attribute! checkbox "indeterminate"))))) + (if (= hide-fill-on-export? :multiple) + (dom/set-attribute! checkbox "indeterminate" true) + (dom/remove-attribute! checkbox "indeterminate"))))) - [:div.element-set - [:div.element-set-title - [:span label] - (when (and (not disable-remove?) (not (= :multiple (:fills values)))) - [:div.add-page {:on-click on-add} i/close])] + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-title)} + [:& title-bar {:collapsable has-fills? + :collapsed (not open?) + :on-collapsed toggle-content + :title label + :class (stl/css-case :title-spacing-fill (not has-fills?))} - [:div.element-set-content + (when (and (not disable-remove?) (not (= :multiple fills))) + [:button {:class (stl/css :add-fill) + :on-click on-add} i/add])]] + (when open? + [:div {:class (stl/css :element-content)} (cond - (= :multiple (:fills values)) - [:div.element-set-options-group - [:div.element-set-label (tr "settings.multiple")] - [:div.element-set-actions - [:div.element-set-actions-button {:on-click on-remove-all} - i/minus]]] + (= :multiple fills) + [:div {:class (stl/css :element-set-options-group)} + [:div {:class (stl/css :group-label)} + (tr "settings.multiple")] + [:button {:on-click on-remove-all + :class (stl/css :remove-btn)} + i/remove-icon]] - (seq (:fills values)) + (seq fills) [:& h/sortable-container {} (for [[index value] (d/enumerate (:fills values []))] [:& color-row {:color {:color (:fill-color value) :opacity (:fill-opacity value) :id (:fill-color-ref-id value) :file-id (:fill-color-ref-file value) - :gradient (:fill-color-gradient value)} + :gradient (:fill-color-gradient value) + :image (:fill-image value)} :key index :index index :title (tr "workspace.options.fill") @@ -151,17 +176,21 @@ :on-remove (on-remove index) :disable-drag disable-drag :on-focus on-focus - :data-select-on-focus (not @disable-drag) + :select-on-focus (not @disable-drag) :on-blur on-blur}])]) (when (or (= type :frame) (and (= type :multiple) (some? (:hide-fill-on-export values)))) - [:div.input-checkbox - [:input {:type "checkbox" - :id "show-fill-on-export" - :ref checkbox-ref - :checked (not hide-fill-on-export?) - :on-change on-change-show-fill-on-export}] - - [:label {:for "show-fill-on-export"} - (tr "workspace.options.show-fill-on-export")]])]])) + [:div {:class (stl/css :checkbox)} + [:label {:for "show-fill-on-export" + :class (stl/css-case :global/checked (not hide-fill-on-export?))} + [:span {:class (stl/css-case :check-mark true + :checked (not hide-fill-on-export?))} + (when (not hide-fill-on-export?) + i/status-tick)] + (tr "workspace.options.show-fill-on-export") + [:input {:type "checkbox" + :id "show-fill-on-export" + :ref checkbox-ref + :checked (not hide-fill-on-export?) + :on-change on-change-show-fill-on-export}]]])])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.scss new file mode 100644 index 0000000000..08b5581dbd --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.scss @@ -0,0 +1,65 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + margin: 0; +} + +.element-title { + margin: 0; +} + +.title-spacing-fill { + padding-left: $s-2; + margin: 0; +} + +.add-fill { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.element-content { + display: flex; + flex-direction: column; + gap: $s-12; + margin: $s-4 0 $s-8 0; +} + +.element-set-options-group { + @include flexRow; +} + +.group-label { + @extend .mixed-bar; +} + +.remove-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.checkbox { + @extend .input-checkbox; + padding-left: $s-8; + span.checked { + background-color: var(--input-border-color-active); + svg { + @extend .button-icon-small; + stroke: var(--input-details-color); + } + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs index 0573166a3d..7b7e69aedc 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs @@ -5,18 +5,19 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.frame-grid + (:require-macros [app.main.style :as stl]) (:require + [app.common.geom.grid :as gg] [app.main.data.workspace.grid :as dw] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.editable-select :refer [editable-select]] - [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.components.select :refer [select]] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] - [app.main.ui.workspace.sidebar.options.rows.input-row :refer [input-row input-row-v2]] - [app.util.geom.grid :as gg] [app.util.i18n :as i18n :refer [tr]] [okulary.core :as l] [rumext.v2 :as mf])) @@ -32,17 +33,30 @@ (mf/defc grid-options {::mf/wrap [mf/memo]} [{:keys [shape-id index grid frame-width frame-height default-grid-params]}] - (let [on-change (mf/use-fn (mf/deps shape-id index) #(st/emit! (dw/set-frame-grid shape-id index %))) - on-remove (mf/use-fn (mf/deps shape-id index) #(st/emit! (dw/remove-frame-grid shape-id index))) - on-save-default (mf/use-fn #(st/emit! (dw/set-default-grid (:type %) (:params %)))) + (let [on-change (mf/use-fn (mf/deps shape-id index) #(st/emit! (dw/set-frame-grid shape-id index %))) + on-remove (mf/use-fn (mf/deps shape-id index) #(st/emit! (dw/remove-frame-grid shape-id index))) + on-save-default (mf/use-fn #(st/emit! (dw/set-default-grid (:type %) (:params %)))) - size-options (mf/use-memo get-size-options) - state (mf/use-state {:show-advanced-options false}) + size-options (mf/use-memo get-size-options) + state* (mf/use-state {:show-advanced-options false + :show-more-options false}) + state (deref state*) + + open? (:show-advanced-options state) + show-more-options? (:show-more-options state) + + is-hidden? (not (:display grid)) {:keys [type display params]} grid toggle-advanced-options - (mf/use-fn #(swap! state update :show-advanced-options not)) + (mf/use-fn #(swap! state* update :show-advanced-options not)) + + toggle-more-options + (mf/use-fn #(swap! state* update :show-more-options not)) + + close-more-options + (mf/use-fn #(swap! state* assoc :show-more-options false)) handle-toggle-visibility (mf/use-fn @@ -91,9 +105,10 @@ (mf/use-fn (mf/deps grid) (fn [color] - (-> grid - (update :params assoc :color color) - (on-change)))) + (let [color (dissoc color :id :file-id)] + (-> grid + (update :params assoc :color color) + (on-change))))) handle-detach-color (mf/use-fn @@ -113,151 +128,200 @@ (assoc-in [:color :color] color) (update :color dissoc :value))] (when on-change - (on-change (assoc grid :params params)))))) + (on-change (assoc grid :params params))) + (close-more-options)))) handle-set-as-default - (mf/use-fn (mf/deps grid) #(on-save-default grid)) + (mf/use-fn + (mf/deps grid) + (fn [] + (on-save-default grid) + (close-more-options))) is-default (= (->> grid :params) - (->> grid :type default-grid-params)) + (->> grid :type default-grid-params))] - open? (:show-advanced-options @state)] + [:div {:class (stl/css :grid-option)} + [:div {:class (stl/css :grid-title)} + [:div {:class (stl/css-case :option-row true + :hidden is-hidden?)} + [:button {:class (stl/css-case :show-options true + :selected open?) + :on-click toggle-advanced-options} + i/menu] + [:div {:class (stl/css :type-select-wrapper)} + [:& select + {:class (stl/css :grid-type-select) + :default-value type + :options [{:value :square :label (tr "workspace.options.grid.square")} + {:value :column :label (tr "workspace.options.grid.column")} + {:value :row :label (tr "workspace.options.grid.row")}] + :on-change handle-change-type}]] + (if (= type :square) + [:div {:class (stl/css :grid-size) + :title (tr "workspace.options.size")} + [:> numeric-input* {:min 0.01 + :value (or (:size params) "") + :no-validate true + :className (stl/css :numeric-input) + :on-change (handle-change :params :size)}]] - [:div.grid-option - [:div.grid-option-main {:style {:display (when open? "none")}} - [:button.custom-button {:class (when open? "is-active") - :on-click toggle-advanced-options} i/actions] + [:div {:class (stl/css :editable-select-wrapper)} + [:& editable-select {:value (:size params) + :type "number" + :class (stl/css :column-select) + :input-class (stl/css :numeric-input) + :min 1 + :options size-options + :placeholder "Auto" + :on-change handle-change-size}]])] - [:& select - {:class "flex-grow" - :default-value type - :options [{:value :square :label (tr "workspace.options.grid.square")} - {:value :column :label (tr "workspace.options.grid.column")} - {:value :row :label (tr "workspace.options.grid.row")}] - :on-change handle-change-type}] + [:div {:class (stl/css :actions)} + [:button {:class (stl/css :action-btn) + :on-click handle-toggle-visibility} + (if display i/shown i/hide)] + [:button {:class (stl/css :action-btn) + :on-click on-remove} + i/remove-icon]]] - (if (= type :square) - [:div.input-element.pixels {:title (tr "workspace.options.size")} - [:> numeric-input {:min 0.01 - :value (or (:size params) "") - :no-validate true - :on-change (handle-change :params :size)}]] + (when (:display grid) + [:& advanced-options {:class (stl/css :grid-advanced-options) + :visible? open? + :on-close toggle-advanced-options} + ;; square + (when (= :square type) + [:div {:class (stl/css :square-row)} + [:div {:class (stl/css :advanced-row)} + [:& color-row {:color (:color params) + :title (tr "workspace.options.grid.params.color") + :disable-gradient true + :disable-image true + :on-change handle-change-color + :on-detach handle-detach-color}] + [:button {:class (stl/css-case :show-more-options true + :selected show-more-options?) + :on-click toggle-more-options} + i/menu]] + (when show-more-options? + [:div {:class (stl/css :second-row)} + [:button {:class (stl/css-case :btn-options true + :disabled is-default) + :disabled is-default + :on-click handle-use-default} + [:span (tr "workspace.options.grid.params.use-default")]] + [:button {:class (stl/css-case :btn-options true + :disabled is-default) + :disabled is-default + :on-click handle-set-as-default} + [:span (tr "workspace.options.grid.params.set-default")]]])]) - [:& editable-select {:value (:size params) - :type "number" - :class "input-option" - :min 1 - :options size-options - :placeholder "Auto" - :on-change handle-change-size}]) + (when (or (= :column type) (= :row type)) + [:div {:class (stl/css :column-row)} + [:div {:class (stl/css :advanced-row)} + [:div {:class (stl/css :orientation-select-wrapper)} + [:& select {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element + :default-value (:type params) + :class (stl/css :orientation-select) + :options [{:value :stretch :label (tr "workspace.options.grid.params.type.stretch")} + {:value :left :label (if (= type :row) + (tr "workspace.options.grid.params.type.top") + (tr "workspace.options.grid.params.type.left"))} + {:value :center :label (tr "workspace.options.grid.params.type.center")} + {:value :right :label (if (= type :row) + (tr "workspace.options.grid.params.type.bottom") + (tr "workspace.options.grid.params.type.right"))}] + :on-change (handle-change :params :type)}]] - [:div.grid-option-main-actions - [:button.custom-button {:on-click handle-toggle-visibility} (if display i/eye i/eye-closed)] - [:button.custom-button {:on-click on-remove} i/minus]]] + [:div {:class (stl/css :color-wrapper)} + [:& color-row {:color (:color params) + :title (tr "workspace.options.grid.params.color") + :disable-gradient true + :disable-image true + :on-change handle-change-color + :on-detach handle-detach-color}]]] - [:& advanced-options {:visible? open? :on-close toggle-advanced-options} - [:button.custom-button {:on-click toggle-advanced-options} i/actions] - (when (= :square type) - [:& input-row {:label (tr "workspace.options.grid.params.size") - :class "pixels" - :min 0.01 - :value (:size params) - :on-change (handle-change :params :size)}]) + [:div {:class (stl/css :advanced-row)} + [:div {:class (stl/css :height) + :title (if (= :row type) + (tr "workspace.options.grid.params.height") + (tr "workspace.options.grid.params.width"))} + [:span {:class (stl/css :icon-text)} + (if (= :row type) + "H" + "W")] + [:> numeric-input* {:placeholder "Auto" + :on-change handle-change-item-length + :nillable true + :className (stl/css :numeric-input) + :value (or (:item-length params) "")}]] - (when (= :row type) - [:& input-row {:label (tr "workspace.options.grid.params.rows") - :type :editable-select - :options size-options - :value (:size params) - :min 1 - :placeholder "Auto" - :on-change handle-change-size}]) + [:div {:class (stl/css :gutter) + :title (tr "workspace.options.grid.params.gutter")} + [:span {:class (stl/css-case :icon true + :rotated (= type :row))} + i/gap-horizontal] + [:> numeric-input* {:placeholder "0" + :on-change (handle-change :params :gutter) + :nillable true + :className (stl/css :numeric-input) + :value (or (:gutter params) 0)}]] - (when (= :column type) - [:& input-row {:label (tr "workspace.options.grid.params.columns") - :type :editable-select - :options size-options - :value (:size params) - :min 1 - :placeholder "Auto" - :on-change handle-change-size}]) + [:div {:class (stl/css :margin) + :title (tr "workspace.options.grid.params.margin")} + [:span {:class (stl/css-case :icon true + :rotated (= type :column))} + i/grid-margin] + [:> numeric-input* {:placeholder "0" + :on-change (handle-change :params :margin) + :nillable true + :className (stl/css :numeric-input) + :value (or (:margin params) 0)}]] - (when (#{:row :column} type) - [:& input-row {:label (tr "workspace.options.grid.params.type") - :type :select - :options [{:value :stretch :label (tr "workspace.options.grid.params.type.stretch")} - {:value :left :label (if (= type :row) - (tr "workspace.options.grid.params.type.top") - (tr "workspace.options.grid.params.type.left"))} - {:value :center :label (tr "workspace.options.grid.params.type.center")} - {:value :right :label (if (= type :row) - (tr "workspace.options.grid.params.type.bottom") - (tr "workspace.options.grid.params.type.right"))}] - :value (:type params) - :on-change (handle-change :params :type)}]) + [:button {:class (stl/css-case :show-more-options true + :selected show-more-options?) + :on-click toggle-more-options + :disabled is-default} + i/menu] + (when show-more-options? + [:div {:class (stl/css :more-options)} + [:button {:class (stl/css :option-btn) + :on-click handle-use-default} (tr "workspace.options.grid.params.use-default")] + [:button {:class (stl/css :option-btn) + :on-click handle-set-as-default} (tr "workspace.options.grid.params.set-default")]])]])])])) - (when (#{:row :column} type) - [:& input-row-v2 - {:class "pixels" - :label (if (= :row type) - (tr "workspace.options.grid.params.height") - (tr "workspace.options.grid.params.width"))} - [:> numeric-input - {:placeholder "Auto" - :value (or (:item-length params) "") - :nillable true - :on-change handle-change-item-length}]]) +(mf/defc frame-grid + [{:keys [shape]}] + (let [state* (mf/use-state true) + open? (deref state*) + frame-grids (:grids shape) + has-frame-grids? (or (= :multiple frame-grids) (some? (seq frame-grids))) - (when (#{:row :column} type) - [:* - [:& input-row {:label (tr "workspace.options.grid.params.gutter") - :class "pixels" - :value (:gutter params) - :min 0 - :nillable true - :default 0 - :placeholder "0" - :on-change (handle-change :params :gutter)}] - [:& input-row {:label (tr "workspace.options.grid.params.margin") - :class "pixels" - :min 0 - :nillable true - :default 0 - :placeholder "0" - :value (:margin params) - :on-change (handle-change :params :margin)}]]) - - [:& color-row {:color (:color params) - :title (tr "workspace.options.grid.params.color") - :disable-gradient true - :on-change handle-change-color - :on-detach handle-detach-color}] - [:div.row-flex - [:button.btn-options {:disabled is-default - :on-click handle-use-default} (tr "workspace.options.grid.params.use-default")] - [:button.btn-options {:disabled is-default - :on-click handle-set-as-default} (tr "workspace.options.grid.params.set-default")]]]])) - -(mf/defc frame-grid [{:keys [shape]}] - (let [id (:id shape) + toggle-content (mf/use-fn #(swap! state* not)) + id (:id shape) saved-grids (mf/deref workspace-saved-grids) default-grid-params (mf/use-memo (mf/deps saved-grids) #(merge dw/default-grid-params saved-grids)) handle-create-grid (mf/use-fn (mf/deps id) #(st/emit! (dw/add-frame-grid id)))] - [:div.element-set - [:div.element-set-title - [:span (tr "workspace.options.grid.grid-title")] - [:div.add-page {:on-click handle-create-grid} i/close]] - (when (seq (:grids shape)) - [:div.element-set-content - (for [[index grid] (map-indexed vector (:grids shape))] + [:div {:class (stl/css :element-set)} + [:& title-bar {:collapsable has-frame-grids? + :collapsed (not open?) + :on-collapsed toggle-content + :class (stl/css-case :title-spacing-board-grid (not has-frame-grids?)) + :title (tr "workspace.options.guides.title")} + + [:button {:on-click handle-create-grid + :class (stl/css :add-grid)} + i/add]] + + (when (and open? (seq frame-grids)) + [:div {:class (stl/css :element-set-content)} + (for [[index grid] (map-indexed vector frame-grids)] [:& grid-options {:key (str id "-" index) :shape-id id :grid grid :index index :frame-width (:width shape) :frame-height (:height shape) - :default-grid-params default-grid-params - }])])])) + :default-grid-params default-grid-params}])])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss new file mode 100644 index 0000000000..bf1a29c77f --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss @@ -0,0 +1,257 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + margin: 0; +} + +.title-spacing-board-grid { + padding-left: $s-2; + margin: 0; +} + +.add-grid { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.element-set-content { + @include flexColumn; + margin: $s-4 0 $s-8 0; +} + +.grid-title { + @include flexRow; +} + +.option-row { + display: flex; + align-items: center; + gap: $s-1; + border-radius: $br-8; + background-color: var(--input-details-color); + .show-options { + @extend .button-secondary; + height: $s-32; + width: $s-28; + border-radius: $br-8 0 0 $br-8; + box-sizing: border-box; + border: $s-1 solid var(--input-border-color); + svg { + @extend .button-icon; + } + &.selected { + @extend .button-icon-selected; + } + } + .type-select-wrapper { + width: $s-96; + padding: 0; + border-radius: 0; + height: $s-32; + .grid-type-select { + border-radius: 0; + height: 100%; + box-sizing: border-box; + border: $s-1 solid var(--input-border-color); + &:hover { + border: $s-1 solid var(--input-border-color-hover); + } + } + } + .grid-size { + @extend .asset-element; + width: $s-60; + margin: 0; + padding: 0; + padding-left: $s-8; + border-radius: 0 $br-8 $br-8 0; + .numeric-input { + @extend .input-base; + } + } + .editable-select-wrapper { + @extend .asset-element; + width: $s-60; + margin: 0; + padding: 0; + position: relative; + border-radius: 0 $br-8 $br-8 0; + .column-select { + height: $s-32; + border-radius: 0 $br-8 $br-8 0; + box-sizing: border-box; + border: $s-1 solid var(--input-border-color); + .numeric-input { + @extend .input-base; + margin: 0; + padding: 0; + } + span { + @include flexCenter; + svg { + @extend .button-icon; + } + } + } + } + + &.hidden { + .show-options { + @include hiddenElement; + border: $s-1 solid var(--input-border-color-disabled); + } + .type-select-wrapper, + .editable-select-wrapper { + @include hiddenElement; + .column-select, + .grid-type-select { + @include hiddenElement; + border: $s-1 solid var(--input-border-color-disabled); + } + .column-select { + @include hiddenElement; + border-radius: 0 $br-8 $br-8 0; + .numeric-input { + @include hiddenElement; + } + } + } + .grid-size { + @include hiddenElement; + border: $s-1 solid var(--input-border-color-disabled); + .icon { + stroke: var(--input-foreground-color-disabled); + } + .numeric-input { + color: var(--input-foreground-color-disabled); + } + } + .actions { + .hidden-btn, + .lock-btn { + background-color: transparent; + svg { + stroke: var(--input-foreground-color-disabled); + } + } + } + } +} + +.actions { + @include flexRow; +} + +.action-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.grid-advanced-options { + @include flexColumn; + margin-top: $s-4; +} + +.column-row, +.square-row { + @include flexColumn; + position: relative; +} + +.advanced-row { + position: relative; + display: flex; + gap: $s-4; + .orientation-select-wrapper { + width: $s-92; + padding: 0; + } + .color-wrapper { + width: $s-156; + } + .show-more-options { + @extend .button-tertiary; + height: $s-32; + width: $s-32; + svg { + @extend .button-icon; + } + &.selected { + @extend .button-icon-selected; + } + } + .height { + @extend .input-element; + width: $s-108; + .icon-text { + padding-top: $s-1; + } + } + .gutter, + .margin { + @extend .input-element; + width: $s-108; + .icon { + &.rotated svg { + transform: rotate(90deg); + } + } + } + + .more-options { + @include menuShadow; + @include flexColumn; + position: absolute; + top: calc($s-2 + $s-28); + right: 0; + width: $s-156; + max-height: $s-300; + padding: $s-2; + margin: 0 0 $s-40 0; + margin-top: $s-4; + border-radius: $br-8; + z-index: $z-index-4; + overflow-y: auto; + background-color: var(--menu-background-color); + .option-btn { + @include buttonStyle; + display: flex; + align-items: center; + height: $s-32; + padding: 0 $s-8; + border-radius: $br-6; + color: var(--menu-foreground-color); + + &:hover { + background-color: var(--menu-background-color-hover); + color: var(--menu-foreground-color-hover); + } + } + } +} + +.second-row { + @extend .dropdown-wrapper; + left: unset; + right: 0; + width: $s-108; + .btn-options { + @include buttonStyle; + @extend .dropdown-element-base; + width: 100%; + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs new file mode 100644 index 0000000000..db5de12bd5 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs @@ -0,0 +1,275 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.sidebar.options.menus.grid-cell + (:require-macros [app.main.style :as stl]) + (:require + [app.common.attrs :as attrs] + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.types.shape.layout :as ctl] + [app.main.data.workspace :as dw] + [app.main.data.workspace.grid-layout.editor :as dwge] + [app.main.data.workspace.shape-layout :as dwsl] + [app.main.store :as st] + [app.main.ui.components.numeric-input :refer [numeric-input*]] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.components.title-bar :refer [title-bar]] + [app.main.ui.hooks :as hooks] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [rumext.v2 :as mf])) + +(def cell-props [:id + :position + :row + :row-span + :column + :column-span + :align-self + :justify-self + :area-name]) + +(mf/defc set-self-alignment + [{:keys [is-col? alignment set-alignment] :as props}] + (let [alignment (or alignment :auto) + type (if is-col? "col" "row") + + handle-set-alignment + (mf/use-callback + (mf/deps set-alignment) + (fn [value] + (set-alignment (-> value keyword))))] + + [:div {:class (stl/css :self-align-menu)} + [:& radio-buttons {:selected (d/name alignment) + :on-change handle-set-alignment + :allow-empty true + :name (dm/str "flex-align-items-" type)} + [:& radio-button {:value "start" + :icon (if is-col? + i/align-self-row-left + i/align-self-column-top) + :title "Align self start" + :id (dm/str "align-self-start-" type)}] + + [:& radio-button {:value "center" + :icon (if is-col? + i/align-self-row-center + i/align-self-column-center) + :title "Align self center" + :id (dm/str "align-self-center-" type)}] + + [:& radio-button {:value "end" + :icon (if is-col? + i/align-self-row-right + i/align-self-column-bottom) + :title "Align self end" + :id (dm/str "align-self-end-" type)}] + + [:& radio-button {:value "stretch" + :icon (if is-col? + i/align-self-row-stretch + i/align-self-column-stretch) + :title "Align self stretch" + :id (dm/str "align-self-stretch-" type)}]]])) + + +(mf/defc options + {::mf/wrap [mf/memo]} + [{:keys [shape cell cells] :as props}] + + (let [state* (mf/use-state {:open true}) + open? (:open @state*) + + cells (hooks/use-equal-memo cells) + cell (or cell (attrs/get-attrs-multi cells cell-props)) + + multiple? (= :multiple (:id cell)) + cell-ids (if multiple? (->> cells (map :id)) [(:id cell)]) + cell-ids (hooks/use-equal-memo cell-ids) + + {:keys [position area-name align-self justify-self column column-span row row-span]} cell + + column-end (when (and (d/num? column) (d/num? column-span)) + (+ column column-span)) + row-end (when (and (d/num? row) (d/num? row-span)) + (+ row row-span)) + + cell-mode (or position :auto) + cell-mode (if (and (= :auto cell-mode) + (or (> (:column-span cell) 1) + (> (:row-span cell) 1))) + :manual + cell-mode) + + valid-area-cells? (mf/use-memo + (mf/deps cells) + #(ctl/valid-area-cells? cells)) + + set-alignment + (mf/use-callback + (mf/deps align-self (:id shape) cell-ids) + (fn [value] + (if (= align-self value) + (st/emit! (dwsl/update-grid-cells (:id shape) cell-ids {:align-self nil})) + (st/emit! (dwsl/update-grid-cells (:id shape) cell-ids {:align-self value}))))) + + set-justify-self + (mf/use-callback + (mf/deps justify-self (:id shape) cell-ids) + (fn [value] + (if (= justify-self value) + (st/emit! (dwsl/update-grid-cells (:id shape) cell-ids {:justify-self nil})) + (st/emit! (dwsl/update-grid-cells (:id shape) cell-ids {:justify-self value}))))) + + on-grid-coordinates + (mf/use-callback + (mf/deps column row (:id shape) (:id cell)) + (fn [field type value] + (when-not multiple? + (let [[property value] + (cond + (and (= type :column) (or (= field :all) (= field :start))) + [:column value] + + (and (= type :column) (= field :end)) + [:column-span (max 1 (- value column))] + + (and (= type :row) (or (= field :all) (= field :start))) + [:row value] + + (and (= type :row) (= field :end)) + [:row-span (max 1 (- value row))])] + + (st/emit! (dwsl/update-grid-cell-position (:id shape) (:id cell) {property value})))))) + + on-area-name-change + (mf/use-callback + (mf/deps (:id shape) cell-ids) + (fn [event] + (let [value (dom/get-value (dom/get-target event))] + (if (= value "") + (st/emit! (dwsl/update-grid-cells (:id shape) cell-ids {:area-name nil})) + (st/emit! (dwsl/update-grid-cells (:id shape) cell-ids {:area-name value})))))) + + set-cell-mode + (mf/use-callback + (mf/deps (:id shape) cell-ids) + (fn [mode] + (let [mode (-> mode keyword)] + (st/emit! (dwsl/change-cells-mode (:id shape) cell-ids mode))))) + + toggle-edit-mode + (mf/use-fn + (mf/deps (:id shape)) + (fn [] + (st/emit! (dw/start-edition-mode (:id shape)) + (dwge/clear-selection (:id shape)))))] + + + [:div {:class (stl/css :grid-cell-menu)} + [:div {:class (stl/css :grid-cell-menu-title)} + [:& title-bar {:collapsable true + :collapsed (not open?) + :on-collapsed #(swap! state* update :open not) + :title "Grid cell"}]] + + (when open? + [:div {:class (stl/css :grid-cell-menu-container)} + [:div {:class (stl/css :cell-mode :row)} + [:& radio-buttons {:selected (d/name cell-mode) + :on-change set-cell-mode + :name "cell-mode" + :wide true} + [:& radio-button {:value "auto" :id :auto}] + [:& radio-button {:value "manual" :id :manual}] + [:& radio-button {:value "area" + :id :area + :disabled (not valid-area-cells?)}]]] + + (when (= :area cell-mode) + [:div {:class (stl/css :row)} + [:input + {:class (stl/css :area-input) + :key (dm/str "name-" (:id cell)) + :id "grid-area-name" + :type "text" + :aria-label "grid-area-name" + :placeholder "Area name" + :default-value area-name + :auto-complete "off" + :on-change on-area-name-change}]]) + + (when (and (not multiple?) (= :auto cell-mode)) + [:div {:class (stl/css :row)} + [:div {:class (stl/css :grid-coord-group)} + [:span {:class (stl/css :icon)} i/flex-vertical] + [:div {:class (stl/css :coord-input)} + [:> numeric-input* + {:placeholder "--" + :title "Column" + :on-click #(dom/select-target %) + :on-change (partial on-grid-coordinates :all :column) + :value column}]]] + + [:div {:class (stl/css :grid-coord-group)} + [:span {:class (stl/css :icon)} i/flex-horizontal] + [:div {:class (stl/css :coord-input)} + [:> numeric-input* + {:placeholder "--" + :title "Row" + :on-click #(dom/select-target %) + :on-change (partial on-grid-coordinates :all :row) + :value row}]]]]) + + (when (and (not multiple?) (or (= :manual cell-mode) (= :area cell-mode))) + [:div {:class (stl/css :row)} + [:div {:class (stl/css :grid-coord-group)} + [:span {:class (stl/css :icon)} i/flex-vertical] + [:div {:class (stl/css :coord-input)} + [:> numeric-input* + {:placeholder "--" + :on-pointer-down #(dom/select-target %) + :on-change (partial on-grid-coordinates :start :column) + :value column}]] + [:div {:class (stl/css :coord-input)} + [:> numeric-input* + {:placeholder "--" + :on-pointer-down #(dom/select-target %) + :on-change (partial on-grid-coordinates :end :column) + :value column-end}]]] + + [:div {:class (stl/css :grid-coord-group)} + [:span {:class (stl/css :icon)} i/flex-horizontal] + [:div {:class (stl/css :coord-input :double)} + [:> numeric-input* + {:placeholder "--" + :on-pointer-down #(dom/select-target %) + :on-change (partial on-grid-coordinates :start :row) + :value row}]] + [:div {:class (stl/css :coord-input)} + [:> numeric-input* + {:placeholder "--" + :on-pointer-down #(dom/select-target %) + :on-change (partial on-grid-coordinates :end :row) + :value row-end}]]]]) + + [:div {:class (stl/css :row)} + [:& set-self-alignment {:is-col? false + :alignment align-self + :set-alignment set-alignment}] + [:& set-self-alignment {:is-col? true + :alignment justify-self + :set-alignment set-justify-self}]] + + [:div {:class (stl/css :row)} + [:button + {:class (stl/css :edit-grid-btn) + :alt (tr "workspace.layout_grid.editor.options.edit-grid") + :on-click toggle-edit-mode} + (tr "workspace.layout_grid.editor.options.edit-grid")]]])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss new file mode 100644 index 0000000000..60e990dfa3 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss @@ -0,0 +1,56 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.grid-cell-menu-container { + @include flexColumn; + margin-top: $s-8; + gap: $s-16; +} + +.grid-cell-menu-title { + font-size: $fs-11; +} + +.row { + @include flexRow; +} + +.cell-mode :global(label) { + padding: 0 $s-12; +} + +.edit-grid-btn { + @extend .button-secondary; + @include uppercaseTitleTipography; + width: 100%; + padding: $s-8; +} + +.area-input { + @extend .input-element; + width: 100%; + padding: $s-8; +} + +.grid-coord-group { + @include flexRow; + border-radius: $br-8; + padding-left: $s-4; + background-color: var(--input-background-color); +} + +.icon svg { + @extend .button-icon; + stroke: var(--icon-foreground); +} + +.coord-input { + @extend .input-element; + border-radius: 0 $br-8 $br-8 0; + border-left: $s-1 solid var(--panel-background-color); +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index 36e7ecb97c..3a1ba4bd9b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -5,10 +5,11 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.interactions + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cfh] [app.common.types.page :as ctp] [app.common.types.shape-tree :as ctt] [app.common.types.shape.interactions :as ctsi] @@ -17,7 +18,10 @@ [app.main.data.workspace.interactions :as dwi] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.numeric-input :refer [numeric-input*]] + [app.main.ui.components.radio-buttons :refer [radio-buttons radio-button]] + [app.main.ui.components.select :refer [select]] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -29,7 +33,7 @@ (defn- event-type-names [] {:click (tr "workspace.options.interaction-on-click") - ; TODO: need more UX research + ;; TODO: need more UX research ;; :mouse-over (tr "workspace.options.interaction-while-hovering") ;; :mouse-press (tr "workspace.options.interaction-while-pressing") :mouse-enter (tr "workspace.options.interaction-mouse-enter") @@ -40,15 +44,6 @@ [interaction] (get (event-type-names) (:event-type interaction) "--")) -(defn- action-type-names - [] - {:navigate (tr "workspace.options.interaction-navigate-to") - :open-overlay (tr "workspace.options.interaction-open-overlay") - :toggle-overlay (tr "workspace.options.interaction-toggle-overlay") - :close-overlay (tr "workspace.options.interaction-close-overlay") - :prev-screen (tr "workspace.options.interaction-prev-screen") - :open-url (tr "workspace.options.interaction-open-url")}) - (defn- action-summary [interaction destination] (case (:action-type interaction) @@ -57,40 +52,25 @@ :open-overlay (tr "workspace.options.interaction-open-overlay-dest" (get destination :name (tr "workspace.options.interaction-none"))) :toggle-overlay (tr "workspace.options.interaction-toggle-overlay-dest" - (get destination :name (tr "workspace.options.interaction-none"))) + (get destination :name (tr "workspace.options.interaction-none"))) :close-overlay (tr "workspace.options.interaction-close-overlay-dest" (get destination :name (tr "workspace.options.interaction-self"))) :prev-screen (tr "workspace.options.interaction-prev-screen") :open-url (tr "workspace.options.interaction-open-url") "--")) -(defn- overlay-pos-type-names - [] - {:manual (tr "workspace.options.interaction-pos-manual") - :center (tr "workspace.options.interaction-pos-center") - :top-left (tr "workspace.options.interaction-pos-top-left") - :top-right (tr "workspace.options.interaction-pos-top-right") - :top-center (tr "workspace.options.interaction-pos-top-center") - :bottom-left (tr "workspace.options.interaction-pos-bottom-left") - :bottom-right (tr "workspace.options.interaction-pos-bottom-right") - :bottom-center (tr "workspace.options.interaction-pos-bottom-center")}) +(defn- get-frames-options + [frames shape] + (->> frames + (filter #(and (not= (:id %) (:id shape)) ; A frame cannot navigate to itself + (not= (:id %) (:frame-id shape)))) ; nor a shape to its container frame + (map (fn [frame] + {:value (str (:id frame)) :label (:name frame)})))) -(defn- animation-type-names - [interaction] - (cond-> - {:dissolve (tr "workspace.options.interaction-animation-dissolve") - :slide (tr "workspace.options.interaction-animation-slide")} - - (ctsi/allow-push? (:action-type interaction)) - (assoc :push (tr "workspace.options.interaction-animation-push")))) - -(defn- easing-names - [] - {:linear (tr "workspace.options.interaction-easing-linear") - :ease (tr "workspace.options.interaction-easing-ease") - :ease-in (tr "workspace.options.interaction-easing-ease-in") - :ease-out (tr "workspace.options.interaction-easing-ease-out") - :ease-in-out (tr "workspace.options.interaction-easing-ease-in-out")}) +(defn- get-shared-frames-options + [shared-frames] + (map (fn [frame] + {:value (str (:id frame)) :label (:name frame)}) shared-frames)) (def flow-for-rename-ref (l/derived (l/in [:workspace-local :flow-for-rename]) st/state)) @@ -106,7 +86,7 @@ accept-edit (fn [] (let [name-input (mf/ref-val name-ref) - name (dom/get-value name-input)] + name (str/trim (dom/get-value name-input))] (reset! editing? false) (st/emit! (dwi/end-rename-flow) (when-not (str/empty? name) @@ -118,450 +98,605 @@ on-key-down (fn [event] (when (kbd/enter? event) (accept-edit)) - (when (kbd/esc? event) (cancel-edit)))] + (when (kbd/esc? event) (cancel-edit))) + + start-flow + (mf/use-fn + (mf/deps flow) + #(st/emit! (dw/select-shape (:starting-frame flow)))) + + ;; rename-flow + ;; (mf/use-fn + ;; (mf/deps flow) + ;; #(st/emit! (dwi/start-rename-flow (:id flow)))) + + remove-flow + (mf/use-fn + (mf/deps flow) + #(st/emit! (dwi/remove-flow (:id flow))))] (mf/use-effect - (fn [] - #(when editing? - (cancel-edit)))) + (fn [] + #(when editing? + (cancel-edit)))) (mf/use-effect - (mf/deps flow-for-rename) - #(when (and (= flow-for-rename (:id flow)) - (not @editing?)) - (start-edit))) + (mf/deps flow-for-rename) + #(when (and (= flow-for-rename (:id flow)) + (not @editing?)) + (start-edit))) (mf/use-effect - (mf/deps @editing?) - #(when @editing? - (let [name-input (mf/ref-val name-ref)] - (dom/select-text! name-input)) - nil)) + (mf/deps @editing?) + #(when @editing? + (let [name-input (mf/ref-val name-ref)] + (dom/select-text! name-input)) + nil)) - [:div.flow-element - [:div.flow-button {:on-click #(st/emit! (dw/select-shape (:starting-frame flow)))} - i/play] - (if @editing? - [:input.element-name - {:type "text" - :ref name-ref - :on-blur accept-edit - :on-key-down on-key-down - :auto-focus true - :default-value (:name flow "")}] - [:span.element-label.flow-name - {:on-double-click #(st/emit! (dwi/start-rename-flow (:id flow)))} - (:name flow)]) - [:div.add-page {:on-click #(st/emit! (dwi/remove-flow (:id flow)))} - i/minus]])) + [:div {:class (stl/css :flow-element)} + [:span {:class (stl/css :flow-info)} + [:span {:class (stl/css :flow-name-wrapper)} + [:button {:class (stl/css :start-flow-btn) + :on-click start-flow} + [:span {:class (stl/css :button-icon)} + i/play]] + [:span {:class (stl/css :flow-input-wrapper)} + [:input + {:class (stl/css :flow-input) + :type "text" + :ref name-ref + :on-blur accept-edit + :on-key-down on-key-down + :default-value (:name flow "")}]]]] + + [:button {:class (stl/css :remove-flow-btn) + :on-click remove-flow} + i/remove-icon]])) (mf/defc page-flows [{:keys [flows]}] (when (seq flows) - [:div.element-set.interactions-options - [:div.element-set-title - [:span (tr "workspace.options.flows.flow-starts")]] + [:div {:class (stl/css :interaction-options)} + [:& title-bar {:collapsable false + :title (tr "workspace.options.flows.flow-starts") + :class (stl/css :title-spacing-layout-flow)}] (for [flow flows] [:& flow-item {:flow flow :key (str (:id flow))}])])) (mf/defc shape-flows [{:keys [flows shape]}] (when (= (:type shape) :frame) - (let [flow (ctp/get-frame-flow flows (:id shape))] - [:div.element-set.interactions-options - [:div.element-set-title - [:span (tr "workspace.options.flows.flow-start")]] - (if (nil? flow) - [:div.flow-element - [:span.element-label (tr "workspace.options.flows.add-flow-start")] - [:div.add-page {:on-click #(st/emit! (dwi/add-flow-selected-frame))} - i/plus]] + (let [flow (ctp/get-frame-flow flows (:id shape)) + add-flow (mf/use-fn #(st/emit! (dwi/add-flow-selected-frame)))] + + [:div {:class (stl/css :element-set)} + [:& title-bar {:collapsable false + :title (tr "workspace.options.flows.flow") + :class (stl/css :title-spacing-layout-flow)} + (when (nil? flow) + [:button {:class (stl/css :add-flow-btn) + :title (tr "workspace.options.flows.add-flow-start") + :on-click add-flow} + i/add])] + + (when flow [:& flow-item {:flow flow :key (str (:id flow))}])]))) +(def ^:private corner-center-icon + (i/icon-xref :corner-center (stl/css :corner-icon))) +(def ^:private corner-bottom-icon + (i/icon-xref :corner-bottom (stl/css :corner-icon))) +(def ^:private corner-bottomleft-icon + (i/icon-xref :corner-bottom-left (stl/css :corner-icon))) +(def ^:private corner-bottomright-icon + (i/icon-xref :corner-bottom-right (stl/css :corner-icon))) +(def ^:private corner-top-icon + (i/icon-xref :corner-top (stl/css :corner-icon))) +(def ^:private corner-topleft-icon + (i/icon-xref :corner-top-left (stl/css :corner-icon))) +(def ^:private corner-topright-icon + (i/icon-xref :corner-top-right (stl/css :corner-icon))) + (mf/defc interaction-entry [{:keys [index shape interaction update-interaction remove-interaction]}] (let [objects (deref refs/workspace-page-objects) destination (get objects (:destination interaction)) frames (mf/with-memo [objects] (ctt/get-viewer-frames objects {:all-frames? true})) - shape-parent-ids (mf/with-memo [objects] (cph/get-parent-ids objects (:id shape))) + shape-parent-ids (mf/with-memo [objects] (cfh/get-parent-ids objects (:id shape))) shape-parents (mf/with-memo [frames shape] (filter (comp (set shape-parent-ids) :id) frames)) overlay-pos-type (:overlay-pos-type interaction) close-click-outside? (:close-click-outside interaction false) background-overlay? (:background-overlay interaction false) preserve-scroll? (:preserve-scroll interaction false) + way (-> interaction :animation :way) direction (-> interaction :animation :direction) - extended-open? (mf/use-state false) + state* (mf/use-state false) + extended-open? (deref state*) + + toggle-extended (mf/use-fn #(swap! state* not)) ext-delay-ref (mf/use-ref nil) ext-duration-ref (mf/use-ref nil) change-event-type - (fn [event] - (let [value (-> event dom/get-target dom/get-value d/read-string)] - (update-interaction index #(ctsi/set-event-type % value shape)))) + (mf/use-fn + (mf/deps index) + (fn [event] + (let [value (keyword event)] + (update-interaction index #(ctsi/set-event-type % value shape))))) change-action-type - (fn [event] - (let [value (-> event dom/get-target dom/get-value d/read-string)] - (update-interaction index #(ctsi/set-action-type % value)))) + (mf/use-fn + (mf/deps index) + (fn [event] + (let [value (keyword event)] + (update-interaction index #(ctsi/set-action-type % value))))) change-delay - (fn [value] - (update-interaction index #(ctsi/set-delay % value))) + (mf/use-fn + (mf/deps index) + (fn [value] + (update-interaction index #(ctsi/set-delay % value)))) change-destination - (fn [event] - (let [value (-> event dom/get-target dom/get-value) - value (when (not= value "") (uuid/uuid value))] - (update-interaction index #(ctsi/set-destination % value)))) + (mf/use-fn + (mf/deps index) + (fn [event] + (let [value event + value (when (not= value "") (uuid/uuid value))] + (update-interaction index #(ctsi/set-destination % value))))) change-position-relative-to - (fn [event] - (let [value (-> event - dom/get-target - dom/get-value - uuid/uuid)] - (update-interaction index #(ctsi/set-position-relative-to % value)))) + (mf/use-fn + (mf/deps index) + (fn [event] + (let [value (uuid/uuid event)] + (update-interaction index #(ctsi/set-position-relative-to % value))))) change-preserve-scroll - (fn [event] - (let [value (-> event dom/get-target dom/checked?)] - (update-interaction index #(ctsi/set-preserve-scroll % value)))) + (mf/use-fn + (mf/deps index) + (fn [event] + (let [value (-> event dom/get-target dom/checked?)] + (update-interaction index #(ctsi/set-preserve-scroll % value))))) change-url - (fn [event] - (let [target (dom/get-target event) - value (dom/get-value target) - has-prefix? (or (str/starts-with? value "http://") - (str/starts-with? value "https://")) - value (if has-prefix? - value - (str "http://" value))] - (when-not has-prefix? - (dom/set-value! target value)) - (if (dom/valid? target) - (do - (dom/remove-class! target "error") - (update-interaction index #(ctsi/set-url % value))) - (dom/add-class! target "error")))) + (mf/use-fn + (mf/deps index) + (fn [event] + (let [target (dom/get-target event) + value (dom/get-value target) + has-prefix? (or (str/starts-with? value "http://") + (str/starts-with? value "https://")) + value (if has-prefix? + value + (str "http://" value))] + (when-not has-prefix? + (dom/set-value! target value)) + (if (dom/valid? target) + (do + (dom/remove-class! target "error") + (update-interaction index #(ctsi/set-url % value))) + (dom/add-class! target "error"))))) change-overlay-pos-type - (fn [shape-id event] - (let [value (-> event dom/get-target dom/get-value d/read-string)] - (update-interaction index #(ctsi/set-overlay-pos-type % value shape objects)) - (when (= value :manual) - (update-interaction index #(ctsi/set-position-relative-to % shape-id))))) - + (mf/use-fn + (mf/deps shape) + (fn [value] + (let [shape-id (:id shape)] + (update-interaction index #(ctsi/set-overlay-pos-type % value shape objects)) + (when (= value :manual) + (update-interaction index #(ctsi/set-position-relative-to % shape-id)))))) toggle-overlay-pos-type - (fn [pos-type] - (update-interaction index #(ctsi/toggle-overlay-pos-type % pos-type shape objects))) + (mf/use-fn + (mf/deps index) + (fn [event] + (let [pos-type (-> (dom/get-current-target event) + (dom/get-data "value") + (keyword))] + (update-interaction index #(ctsi/toggle-overlay-pos-type % pos-type shape objects))))) change-close-click-outside - (fn [event] - (let [value (-> event dom/get-target dom/checked?)] - (update-interaction index #(ctsi/set-close-click-outside % value)))) + (mf/use-fn + (mf/deps index) + (fn [event] + (let [value (-> event dom/get-target dom/checked?)] + (update-interaction index #(ctsi/set-close-click-outside % value))))) change-background-overlay - (fn [event] - (let [value (-> event dom/get-target dom/checked?)] - (update-interaction index #(ctsi/set-background-overlay % value)))) + (mf/use-fn + (mf/deps index) + (fn [event] + (let [value (-> event dom/get-target dom/checked?)] + (update-interaction index #(ctsi/set-background-overlay % value))))) change-animation-type - (fn [event] - (let [value (-> event dom/get-target dom/get-value d/read-string)] - (update-interaction index #(ctsi/set-animation-type % value)))) + (mf/use-fn + (mf/deps index) + (fn [event] + (let [value (if (= "" event) + nil + (keyword event))] + (update-interaction index #(ctsi/set-animation-type % value))))) change-duration - (fn [value] - (update-interaction index #(ctsi/set-duration % value))) + (mf/use-fn (fn [value] + (update-interaction index #(ctsi/set-duration % value)))) change-easing - (fn [event] - (let [value (-> event dom/get-target dom/get-value d/read-string)] - (update-interaction index #(ctsi/set-easing % value)))) + (mf/use-fn + (mf/deps index) + (fn [event] + (let [value (keyword event)] + (update-interaction index #(ctsi/set-easing % value))))) change-way - (fn [event] - (let [value (-> event dom/get-target dom/get-value d/read-string)] - (update-interaction index #(ctsi/set-way % value)))) + (mf/use-fn + (mf/deps index) + (fn [event] + (let [value (keyword event)] + (update-interaction index #(ctsi/set-way % value))))) change-direction - (fn [value] - (update-interaction index #(ctsi/set-direction % value))) + (mf/use-fn + (mf/deps index) + (fn [event] + (let [value (keyword event)] + (update-interaction index #(ctsi/set-direction % value))))) change-offset-effect - (fn [event] - (let [value (-> event dom/get-target dom/checked?)] - (update-interaction index #(ctsi/set-offset-effect % value))))] + (mf/use-fn + (mf/deps index) + (fn [event] + (let [value (-> event dom/get-target dom/checked?)] + (update-interaction index #(ctsi/set-offset-effect % value))))) - [:* - [:div.element-set-options-group {:class (dom/classnames - :open @extended-open?)} - ; Summary - [:div.element-set-actions-button {:on-click #(swap! extended-open? not)} - i/actions] - [:div.interactions-summary {:on-click #(swap! extended-open? not)} - [:div.trigger-name (event-type-name interaction)] - [:div.action-summary (action-summary interaction destination)]] - [:div.element-set-actions {:on-click #(remove-interaction index)} - [:div.element-set-actions-button i/minus]] + event-type-options (-> [{:value :click :label (tr "workspace.options.interaction-on-click")} + ;; TODO: need more UX research + ;; :mouse-over (tr "workspace.options.interaction-while-hovering") + ;; :mouse-press (tr "workspace.options.interaction-while-pressing") + {:value :mouse-enter :label (tr "workspace.options.interaction-mouse-enter")} + {:value :mouse-leave :label (tr "workspace.options.interaction-mouse-leave")}] + (cond-> (cfh/frame-shape? shape) + (conj {:value :after-delay :label (tr "workspace.options.interaction-after-delay")}))) - (when @extended-open? - [:div.element-set-content + action-type-options [{:value :navigate :label (tr "workspace.options.interaction-navigate-to")} + {:value :open-overlay :label (tr "workspace.options.interaction-open-overlay")} + {:value :toggle-overlay :label (tr "workspace.options.interaction-toggle-overlay")} + {:value :close-overlay :label (tr "workspace.options.interaction-close-overlay")} + {:value :prev-screen :label (tr "workspace.options.interaction-prev-screen")} + {:value :open-url :label (tr "workspace.options.interaction-open-url")}] - ; Trigger select - [:div.interactions-element.separator - [:span.element-set-subtitle.wide (tr "workspace.options.interaction-trigger")] - [:select.input-select - {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :value (str (:event-type interaction)) - :on-change change-event-type} - (for [[value name] (event-type-names)] - (when-not (and (= value :after-delay) - (not= (:type shape) :frame)) - [:option {:key (dm/str value) - :value (dm/str value)} name]))]] + frames-opts (get-frames-options frames shape) - ; Delay - (when (ctsi/has-delay interaction) - [:div.interactions-element - [:span.element-set-subtitle.wide (tr "workspace.options.interaction-delay")] - [:div.input-element {:title (tr "workspace.options.interaction-ms")} - [:> numeric-input {:ref ext-delay-ref + default-opts [(if (= (:action-type interaction) :close-overlay) + {:value "" :label (tr "workspace.options.interaction-self")} + {:value "" :label (tr "workspace.options.interaction-none")})] + destination-options + (mf/with-memo [frames-opts default-opts] + (let [sorted-frames-opts (sort-by :label frames-opts)] + (d/concat-vec default-opts sorted-frames-opts))) + + shape-parents-opts (get-shared-frames-options shape-parents) + + relative-to-opts + (mf/with-memo [shape-parents-opts] + (if (not= (:overlay-pos-type interaction) :manual) + (d/concat-vec [{:value "" :label (tr "workspace.options.interaction-auto")}] + shape-parents-opts + [{:value (str (:id shape)) :label (str (:name shape) " (" (tr "workspace.options.interaction-self") ")")}]) + [{:value (str (:id shape)) :label (str (:name shape) " (" (tr "workspace.options.interaction-self") ")")}])) + + overlay-position-opts [{:value :manual :label (tr "workspace.options.interaction-pos-manual")} + {:value :center :label (tr "workspace.options.interaction-pos-center")} + {:value :top-left :label (tr "workspace.options.interaction-pos-top-left")} + {:value :top-right :label (tr "workspace.options.interaction-pos-top-right")} + {:value :top-center :label (tr "workspace.options.interaction-pos-top-center")} + {:value :bottom-left :label (tr "workspace.options.interaction-pos-bottom-left")} + {:value :bottom-right :label (tr "workspace.options.interaction-pos-bottom-right")} + {:value :bottom-center :label (tr "workspace.options.interaction-pos-bottom-center")}] + + basic-animation-opts [{:value "" :label (tr "workspace.options.interaction-animation-none")} + {:value :dissolve :label (tr "workspace.options.interaction-animation-dissolve")} + {:value :slide :label (tr "workspace.options.interaction-animation-slide")}] + + animation-opts + (mf/with-memo [basic-animation-opts] + (if (ctsi/allow-push? (:action-type interaction)) + (d/concat-vec basic-animation-opts [{:value :push :label (tr "workspace.options.interaction-animation-push")}]) + basic-animation-opts)) + + easing-options [{:icon :easing-linear :value :linear :label (tr "workspace.options.interaction-easing-linear")} + {:icon :easing-ease :value :ease :label (tr "workspace.options.interaction-easing-ease")} + {:icon :easing-ease-in :value :ease-in :label (tr "workspace.options.interaction-easing-ease-in")} + {:icon :easing-ease-out :value :ease-out :label (tr "workspace.options.interaction-easing-ease-out")} + {:icon :easing-ease-in-out :value :ease-in-out :label (tr "workspace.options.interaction-easing-ease-in-out")}]] + + + [:div {:class (stl/css-case :element-set-options-group true + :element-set-options-group-open extended-open?)} + ; Summary + [:div {:class (stl/css :interactions-summary)} + [:button {:class (stl/css-case :extend-btn true + :extended extended-open?) + :on-click toggle-extended} + i/menu] + + [:div {:class (stl/css :interactions-info) + :on-click toggle-extended} + [:div {:class (stl/css :trigger-name)} (event-type-name interaction)] + [:div {:class (stl/css :action-summary)} (action-summary interaction destination)]] + [:button {:class (stl/css :remove-btn) + :data-value index + :on-click #(remove-interaction index)} + i/remove-icon]] + + (when extended-open? + [:div {:class (stl/css :extended-options)} + ;; Trigger select + [:div {:class (stl/css :property-row)} + [:span {:class (stl/css :interaction-name)} + (tr "workspace.options.interaction-trigger")] + [:div {:class (stl/css :select-wrapper)} + [:& select {:class (stl/css :interaction-type-select) + :default-value (:event-type interaction) + :options event-type-options + :on-change change-event-type}]]] + + ;; Delay + (when (ctsi/has-delay interaction) + [:div {:class (stl/css :property-row)} + [:span {:class (stl/css :interaction-name)} + (tr "workspace.options.interaction-delay")] + [:div {:class (stl/css :input-element-wrapper) + :title (tr "workspace.options.interaction-ms")} + [:span.after (tr "workspace.options.interaction-ms")] + [:> numeric-input* {:ref ext-delay-ref + :className (stl/css :numeric-input) :on-change change-delay :value (:delay interaction) - :title (tr "workspace.options.interaction-ms")}] - [:span.after (tr "workspace.options.interaction-ms")]]]) + :title (tr "workspace.options.interaction-ms")}]]]) - ; Action select - [:div.interactions-element.separator - [:span.element-set-subtitle.wide (tr "workspace.options.interaction-action")] - [:select.input-select - {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :value (str (:action-type interaction)) - :on-change change-action-type} - (for [[value name] (action-type-names)] - [:option {:key (dm/str "action-" value) - :value (str value)} name])]] + ;; Action select + [:div {:class (stl/css :property-row)} + [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-action")] + [:div {:class (stl/css :select-wrapper)} + [:& select {:class (stl/css :interaction-type-select) + :default-value (:action-type interaction) + :options action-type-options + :on-change change-action-type}]]] - ; Destination - (when (ctsi/has-destination interaction) - [:div.interactions-element - [:span.element-set-subtitle.wide (tr "workspace.options.interaction-destination")] - [:select.input-select - {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :value (str (:destination interaction)) - :on-change change-destination} - (if (= (:action-type interaction) :close-overlay) - [:option {:value ""} (tr "workspace.options.interaction-self")] - [:option {:value ""} (tr "workspace.options.interaction-none")]) - (for [frame frames] - (when (and (not= (:id frame) (:id shape)) ; A frame cannot navigate to itself - (not= (:id frame) (:frame-id shape))) ; nor a shape to its container frame - [:option {:key (dm/str "destination-" (:id frame)) - :value (str (:id frame))} (:name frame)]))]]) + ;; Destination + (when (ctsi/has-destination interaction) + [:div {:class (stl/css :property-row)} + [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-destination")] + [:div {:class (stl/css :select-wrapper)} + [:& select {:class (stl/css :interaction-type-select) + :default-value (str (:destination interaction)) + :options destination-options + :on-change change-destination}]]]) - ; Preserve scroll - (when (ctsi/has-preserve-scroll interaction) - [:div.interactions-element - [:div.input-checkbox + ;; Preserve scroll + (when (ctsi/has-preserve-scroll interaction) + [:div {:class (stl/css :property-row)} + [:div {:class (stl/css :checkbox-option)} + [:label {:for (str "preserve-" index) + :class (stl/css-case :global/checked preserve-scroll?)} + [:span {:class (stl/css-case :global/checked preserve-scroll?)} + (when preserve-scroll? + i/status-tick)] + (tr "workspace.options.interaction-preserve-scroll") [:input {:type "checkbox" :id (str "preserve-" index) :checked preserve-scroll? - :on-change change-preserve-scroll}] - [:label {:for (str "preserve-" index)} - (tr "workspace.options.interaction-preserve-scroll")]]]) + :on-change change-preserve-scroll}]]]]) - ; URL - (when (ctsi/has-url interaction) - [:div.interactions-element - [:span.element-set-subtitle.wide (tr "workspace.options.interaction-url")] - [:input.input-text {:type "url" - :placeholder "http://example.com" - :default-value (str (:url interaction)) - :on-blur change-url}]]) + ;; URL + (when (ctsi/has-url interaction) + [:div {:class (stl/css :property-row)} + [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-url")] + [:div {:class (stl/css :input-element-wrapper)} + [:input {:class (stl/css :input-text) + :type "url" + :placeholder "http://example.com" + :default-value (str (:url interaction)) + :on-blur change-url}]]]) - (when (ctsi/has-overlay-opts interaction) - [:* - ; Overlay position relative-to (select) - [:div.interactions-element - [:span.element-set-subtitle.wide (tr "workspace.options.interaction-relative-to")] - [:select.input-select - {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :value (str (:position-relative-to interaction)) - :on-change change-position-relative-to} - (when (not= (:overlay-pos-type interaction) :manual) - [:* - [:option {:value ""} (tr "workspace.options.interaction-auto")] - (for [frame shape-parents] - [:option {:key (dm/str "position-relative-to-" (:id frame)) - :value (str (:id frame))} (:name frame)])]) - [:option {:key (dm/str "position-relative-to-" (:id shape)) - :value (str (:id shape))} (:name shape) " (" (tr "workspace.options.interaction-self") ")"]]] + (when (ctsi/has-overlay-opts interaction) + [:* + ;; Overlay position relative-to (select) + [:div {:class (stl/css :property-row)} + [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-relative-to")] + [:div {:class (stl/css :select-wrapper)} + [:& select {:class (stl/css :interaction-type-select) + :default-value (str (:position-relative-to interaction)) + :options relative-to-opts + :on-change change-position-relative-to}]]] - ; Overlay position (select) - [:div.interactions-element - [:span.element-set-subtitle.wide (tr "workspace.options.interaction-position")] - [:select.input-select - {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :value (str (:overlay-pos-type interaction)) - :on-change (partial change-overlay-pos-type (:id shape))} - (for [[value name] (overlay-pos-type-names)] - [:option {:value (str value)} name])]] + ;; Overlay position (select) + [:div {:class (stl/css :property-row)} + [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-position")] + [:div {:class (stl/css :select-wrapper)} + [:& select {:class (stl/css :interaction-type-select) + :default-value (:overlay-pos-type interaction) + :options overlay-position-opts + :on-change change-overlay-pos-type}]]] - ; Overlay position (buttons) - [:div.interactions-element.interactions-pos-buttons - [:div.element-set-actions-button - {:class (dom/classnames :active (= overlay-pos-type :center)) - :on-click #(toggle-overlay-pos-type :center)} - i/position-center] - [:div.element-set-actions-button - {:class (dom/classnames :active (= overlay-pos-type :top-left)) - :on-click #(toggle-overlay-pos-type :top-left)} - i/position-top-left] - [:div.element-set-actions-button - {:class (dom/classnames :active (= overlay-pos-type :top-right)) - :on-click #(toggle-overlay-pos-type :top-right)} - i/position-top-right] - [:div.element-set-actions-button - {:class (dom/classnames :active (= overlay-pos-type :top-center)) - :on-click #(toggle-overlay-pos-type :top-center)} - i/position-top-center] - [:div.element-set-actions-button - {:class (dom/classnames :active (= overlay-pos-type :bottom-left)) - :on-click #(toggle-overlay-pos-type :bottom-left)} - i/position-bottom-left] - [:div.element-set-actions-button - {:class (dom/classnames :active (= overlay-pos-type :bottom-right)) - :on-click #(toggle-overlay-pos-type :bottom-right)} - i/position-bottom-right] - [:div.element-set-actions-button - {:class (dom/classnames :active (= overlay-pos-type :bottom-center)) - :on-click #(toggle-overlay-pos-type :bottom-center)} - i/position-bottom-center]] + ;; Overlay position (buttons) + [:div {:class (stl/css-case :property-row true + :big-row true)} + [:div {:class (stl/css :position-btns-wrapper)} + [:button {:class (stl/css-case :direction-btn true + :center-btn true + :active (= overlay-pos-type :center)) + :data-value "center" + :on-click toggle-overlay-pos-type} + corner-center-icon] + [:button {:class (stl/css-case :direction-btn true + :top-left-btn true + :active (= overlay-pos-type :top-left)) + :data-value "top-left" + :on-click toggle-overlay-pos-type} + corner-topleft-icon] + [:button {:class (stl/css-case :direction-btn true + :top-right-btn true + :active (= overlay-pos-type :top-right)) + :data-value "top-right" + :on-click toggle-overlay-pos-type} + corner-topright-icon] - ; Overlay click outside - [:div.interactions-element - [:div.input-checkbox - [:input {:type "checkbox" - :id (str "close-" index) - :checked close-click-outside? - :on-change change-close-click-outside}] - [:label {:for (str "close-" index)} - (tr "workspace.options.interaction-close-outside")]]] + [:button {:class (stl/css-case :direction-btn true + :top-center-btn true + :active (= overlay-pos-type :top-center)) + :data-value "top-center" + :on-click toggle-overlay-pos-type} + corner-top-icon] - ; Overlay background - [:div.interactions-element - [:div.input-checkbox - [:input {:type "checkbox" - :id (str "background-" index) - :checked background-overlay? - :on-change change-background-overlay}] - [:label {:for (str "background-" index)} - (tr "workspace.options.interaction-background")]]]]) + [:button {:class (stl/css-case :direction-btn true + :bottom-left-btn true + :active (= overlay-pos-type :bottom-left)) + :data-value "bottom-left" + :on-click toggle-overlay-pos-type} + corner-bottomleft-icon] + [:button {:class (stl/css-case :direction-btn true + :bottom-right-btn true + :active (= overlay-pos-type :bottom-right)) + :data-value "bottom-right" + :on-click toggle-overlay-pos-type} + corner-bottomright-icon] + [:button {:class (stl/css-case :direction-btn true + :bottom-center-btn true + :active (= overlay-pos-type :bottom-center)) + :data-value "bottom-center" + :on-click toggle-overlay-pos-type} + corner-bottom-icon]]] - (when (ctsi/has-animation? interaction) - [:* - ; Animation select - [:div.interactions-element.separator - [:span.element-set-subtitle.wide (tr "workspace.options.interaction-animation")] - [:select.input-select - {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :value (str (-> interaction :animation :animation-type)) - :on-change change-animation-type} - [:option {:value ""} (tr "workspace.options.interaction-animation-none")] - (for [[value name] (animation-type-names interaction)] - [:option {:value (str value)} name])]] + ;; Overlay click outside - ; Direction - (when (ctsi/has-way? interaction) - [:div.interactions-element.interactions-way-buttons - [:div.input-radio - [:input {:type "radio" - :id "way-in" - :checked (= :in way) - :name "animation-way" - :value ":in" - :on-change change-way}] - [:label {:for "way-in"} (tr "workspace.options.interaction-in")]] - [:div.input-radio - [:input {:type "radio" - :id "way-out" - :checked (= :out way) - :name "animation-way" - :value ":out" - :on-change change-way}] - [:label {:for "way-out"} (tr "workspace.options.interaction-out")]]]) + [:ul {:class (stl/css :property-list)} + [:li {:class (stl/css :property-row)} + [:div {:class (stl/css :checkbox-option)} + [:label {:for (str "close-" index) + :class (stl/css-case :global/checked close-click-outside?)} + [:span {:class (stl/css-case :global/checked close-click-outside?)} + (when close-click-outside? + i/status-tick)] + (tr "workspace.options.interaction-close-outside") + [:input {:type "checkbox" + :id (str "close-" index) + :checked close-click-outside? + :on-change change-close-click-outside}]]]] - ; Direction - (when (ctsi/has-direction? interaction) - [:div.interactions-element.interactions-direction-buttons - [:div.element-set-actions-button - {:class (dom/classnames :active (= direction :right)) - :on-click #(change-direction :right)} - i/animate-right] - [:div.element-set-actions-button - {:class (dom/classnames :active (= direction :down)) - :on-click #(change-direction :down)} - i/animate-down] - [:div.element-set-actions-button - {:class (dom/classnames :active (= direction :left)) - :on-click #(change-direction :left)} - i/animate-left] - [:div.element-set-actions-button - {:class (dom/classnames :active (= direction :up)) - :on-click #(change-direction :up)} - i/animate-up]]) + ;; Overlay background + [:li {:class (stl/css :property-row)} + [:div {:class (stl/css :checkbox-option)} + [:label {:for (str "background-" index) + :class (stl/css-case :global/checked background-overlay?)} + [:span {:class (stl/css-case :global/checked background-overlay?)} + (when background-overlay? + i/status-tick)] + (tr "workspace.options.interaction-background") + [:input {:type "checkbox" + :id (str "background-" index) + :checked background-overlay? + :on-change change-background-overlay}]]]]]]) - ; Duration - (when (ctsi/has-duration? interaction) - [:div.interactions-element - [:span.element-set-subtitle.wide (tr "workspace.options.interaction-duration")] - [:div.input-element {:title (tr "workspace.options.interaction-ms")} - [:> numeric-input {:ref ext-duration-ref - :on-change change-duration - :value (-> interaction :animation :duration) - :title (tr "workspace.options.interaction-ms")}] - [:span.after (tr "workspace.options.interaction-ms")]]]) + (when (ctsi/has-animation? interaction) + [:* + ;; Animation select + [:div {:class (stl/css :property-row)} + [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-animation")] + [:div {:class (stl/css :select-wrapper)} + [:& select {:class (stl/css :animation-select) + :default-value (or (-> interaction :animation :animation-type) "") + :options animation-opts + :on-change change-animation-type}]]] - ; Easing - (when (ctsi/has-easing? interaction) - [:div.interactions-element - [:span.element-set-subtitle.wide (tr "workspace.options.interaction-easing")] - [:select.input-select - {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :value (str (-> interaction :animation :easing)) - :on-change change-easing} - (for [[value name] (easing-names)] - [:option {:value (str value)} name])] - [:div.interactions-easing-icon - (case (-> interaction :animation :easing) - :linear i/easing-linear - :ease i/easing-ease - :ease-in i/easing-ease-in - :ease-out i/easing-ease-out - :ease-in-out i/easing-ease-in-out)]]) + ;; Direction + (when (ctsi/has-way? interaction) + [:div {:class (stl/css :property-row)} + [:div {:class (stl/css :inputs-wrapper)} - ; Offset effect - (when (ctsi/has-offset-effect? interaction) - [:div.interactions-element - [:div.input-checkbox - [:input {:type "checkbox" - :id (str "offset-effect-" index) - :checked (-> interaction :animation :offset-effect) - :on-change change-offset-effect}] - [:label {:for (str "offset-effect-" index)} - (tr "workspace.options.interaction-offset-effect")]]])])])]])) + [:& radio-buttons {:selected (d/name way) + :on-change change-way + :name "animation-way"} + [:& radio-button {:value "in" + :id "animation-way-in"}] + [:& radio-button {:id "animation-way-out" + :value "out"}]]]]) + + ;; Direction + (when (ctsi/has-direction? interaction) + [:div {:class (stl/css :property-row)} + [:div {:class (stl/css :buttons-wrapper)} + [:& radio-buttons {:selected (d/name direction) + :on-change change-direction + :name "animation-direction"} + [:& radio-button {:icon i/column + :icon-class (stl/css :right) + :value "right" + :id "animation-right"}] + [:& radio-button {:icon i/column + :icon-class (stl/css :left) + :id "animation-left" + :value "left"}] + [:& radio-button {:icon i/column + :icon-class (stl/css :down) + :id "animation-down" + :value "down"}] + [:& radio-button {:icon i/column + :icon-class (stl/css :up) + :id "animation-up" + :value "up"}]]]]) + + ;; Duration + (when (ctsi/has-duration? interaction) + [:div {:class (stl/css :property-row)} + [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-duration")] + [:div {:class (stl/css :input-element-wrapper) + :title (tr "workspace.options.interaction-ms")} + [:span {:class (stl/css :after)} + (tr "workspace.options.interaction-ms")] + [:> numeric-input* {:ref ext-duration-ref + :on-change change-duration + :value (-> interaction :animation :duration) + :title (tr "workspace.options.interaction-ms")}]]]) + + ;; Easing + (when (ctsi/has-easing? interaction) + [:div {:class (stl/css :property-row)} + [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-easing")] + [:div {:class (stl/css :select-wrapper)} + [:& select {:class (stl/css :easing-select) + :dropdown-class (stl/css :dropdown-upwards) + :default-value (-> interaction :animation :easing) + :options easing-options + :on-change change-easing}]]]) + + ;; Offset effect + (when (ctsi/has-offset-effect? interaction) + [:div {:class (stl/css :property-row)} + [:div {:class (stl/css :checkbox-option)} + [:label {:for (str "offset-effect-" index) + :class (stl/css-case :global/checked (-> interaction :animation :offset-effect))} + [:span {:class (stl/css-case :global/checked (-> interaction :animation :offset-effect))} + (when (-> interaction :animation :offset-effect) + i/status-tick)] + (tr "workspace.options.interaction-offset-effect") + [:input {:type "checkbox" + :id (str "offset-effect-" index) + :checked (-> interaction :animation :offset-effect) + :on-change change-offset-effect}]]]])])])])) (mf/defc interactions-menu [{:keys [shape] :as props}] - (let [interactions (get shape :interactions []) + (let [interactions (get shape :interactions []) options (mf/deref refs/workspace-page-options) flows (:flows options) @@ -577,30 +712,38 @@ update-interaction (fn [index update-fn] (st/emit! (dwi/update-interaction shape index update-fn)))] - [:* + [:div {:class (stl/css :interactions-content)} (if shape [:& shape-flows {:flows flows :shape shape}] [:& page-flows {:flows flows}]) + [:div {:class (stl/css :interaction-options)} + (when (and shape (not (cfh/unframed-shape? shape))) + [:div {:class (stl/css :element-title)} + [:& title-bar {:collapsable false + :title (tr "workspace.options.interactions") + :class (stl/css :title-spacing-layout-interactions)} - [:div.element-set.interactions-options - (when (and shape (not (cph/unframed-shape? shape))) - [:div.element-set-title - [:span (tr "workspace.options.interactions")] - [:div.add-page {:on-click add-interaction} - i/plus]]) - [:div.element-set-content - (when (= (count interactions) 0) - [:* - (when (and shape (not (cph/unframed-shape? shape))) - [:* - [:div.interactions-help-icon i/plus] - [:div.interactions-help.separator (tr "workspace.options.add-interaction")]]) - [:div.interactions-help-icon i/interaction] - [:div.interactions-help (tr "workspace.options.select-a-shape")] - [:div.interactions-help-icon i/play] - [:div.interactions-help (tr "workspace.options.use-play-button")]])] - [:div.groups + [:button {:class (stl/css :add-interaction-btn) + :on-click add-interaction} + i/add]]]) + + (when (= (count interactions) 0) + [:div {:class (stl/css :help-content)} + (when (and shape (not (cfh/unframed-shape? shape))) + [:div {:class (stl/css :help-group)} + [:div {:class (stl/css :interactions-help-icon)} i/add] + [:div {:class (stl/css :interactions-help)} + (tr "workspace.options.add-interaction")]]) + [:div {:class (stl/css :help-group)} + [:div {:class (stl/css :interactions-help-icon)} i/interaction] + [:div {:class (stl/css :interactions-help)} + (tr "workspace.options.select-a-shape")]] + [:div {:class (stl/css :help-group)} + [:div {:class (stl/css :interactions-help-icon)} i/play] + [:div {:class (stl/css :interactions-help)} + (tr "workspace.options.use-play-button")]]]) + [:div {:class (stl/css :groups)} (for [[index interaction] (d/enumerate interactions)] [:& interaction-entry {:key (dm/str (:id shape) "-" index) :index index diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss new file mode 100644 index 0000000000..d4ec6f5a14 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss @@ -0,0 +1,374 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.interactions-content { + display: flex; + flex-direction: column; + gap: $s-8; +} + +.interaction-options { + @include flexColumn; +} + +.add-interaction-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.help-content { + padding: $s-32 0; + width: $s-200; + margin: 0 auto; +} + +.help-group { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: $s-40; + gap: $s-12; +} + +.interactions-help-icon { + @include flexCenter; + width: $s-48; + height: $s-48; + border-radius: $br-circle; + background-color: var(--pill-background-color); + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + height: $s-32; + width: $s-32; + } +} + +.after { + @include bodySmallTypography; + margin-top: $s-1; +} + +.interactions-help { + @include bodySmallTypography; + text-align: center; + color: var(--title-foreground-color); +} + +.element-set { + @include flexColumn; +} + +.add-flow-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.interactions-info { + flex-grow: 1; + display: grid; +} + +.trigger-name { + color: var(--color-foreground-primary); +} + +.action-summary { + color: var(--color-foreground-secondary); + @include textEllipsis; +} + +.groups { + @include flexColumn($s-12); +} + +.element-set-options-group-open { + @include flexColumn; +} + +.extended-options { + @include flexColumn; +} + +.property-list { + list-style: none; + margin: 0; + display: grid; + row-gap: $s-16; + margin-block: calc(#{$s-16} - #{$s-4}); +} + +.property-row { + @extend .attr-row; + height: auto; + &.big-row { + height: 100%; + } + .interaction-name { + @include twoLineTextEllipsis; + @include bodySmallTypography; + padding-left: $s-4; + width: $s-92; + margin: auto 0; + grid-area: name; + color: var(--title-foreground-color); + } + .select-wrapper { + display: flex; + align-items: center; + grid-area: content; + .easing-select { + width: $s-156; + padding: 0 $s-8; + .dropdown-upwards { + bottom: $s-36; + width: $s-156; + top: unset; + } + } + } + .input-element-wrapper { + @extend .input-element; + grid-area: content; + } + .buttons-wrapper { + grid-area: content; + .right svg { + transform: rotate(-90deg); + } + .left svg { + transform: rotate(90deg); + } + .up svg { + transform: rotate(180deg); + } + } + .inputs-wrapper { + grid-area: content; + @include flexRow; + .radio-btn { + @extend .input-checkbox; + } + } +} + +.position-btns-wrapper { + grid-area: content; + display: grid; + grid-template-areas: + "topleft top topright" + "left center right" + "bottomleft bottom bottomright"; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(3, 1fr); + width: $s-84; + height: $s-84; + border-radius: $br-8; + background-color: var(--color-background-tertiary); + .center-btn { + grid-area: center; + } + .top-left-btn { + grid-area: topleft; + } + .top-right-btn { + grid-area: topright; + } + .top-center-btn { + grid-area: top; + } + .bottom-left-btn { + grid-area: bottomleft; + } + .bottom-right-btn { + grid-area: bottomright; + } + .bottom-center-btn { + grid-area: bottom; + } +} + +.direction-btn { + @extend .button-tertiary; + height: $s-28; + width: $s-28; + + &.active { + @extend .button-icon-selected; + } +} + +.checkbox-option { + @extend .input-checkbox; + grid-area: content; + line-height: 1.2; + label { + align-items: start; + } +} + +.interactions-summary { + @extend .asset-element; + height: $s-44; + padding: 0; + gap: $s-8; + + .remove-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon-small; + } + } +} + +.extend-btn { + @extend .button-tertiary; + --button-tertiary-border-width: var(--expand-button-icon-border-width); + height: 100%; + width: $s-28; + border-end-end-radius: 0; + border-start-end-radius: 0; + padding: 0; + svg { + @extend .button-icon; + } + position: relative; + &:after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-inline-end: $s-1 solid var(--panel-background-color); + } + &.extended { + @extend .button-icon-selected; + --button-tertiary-border-width: var(--expand-button-icon-border-width-selected); + } +} + +.corner-icon { + fill: none; + stroke: currentColor; + width: $s-12; + height: $s-12; +} + +.flow-element { + @include flexRow; +} + +.flow-info { + display: flex; + align-items: center; + gap: $s-2; + border-radius: $s-8; + background-color: var(--input-details-color); + height: $s-32; + width: 100%; + flex-grow: 1; +} + +.flow-name-wrapper { + @include bodySmallTypography; + @include focusInput; + display: flex; + align-items: center; + gap: $s-4; + flex-grow: 1; + height: $s-32; + width: 100%; + border-radius: $br-8; + padding: 0; + margin-right: 0; + background-color: var(--input-background-color); + border: $s-1 solid var(--input-border-color); + color: var(--input-foreground-color); + .start-flow-btn { + @include buttonStyle; + height: $s-32; + width: $s-28; + padding: 0 $s-2 0 $s-8; + border-radius: $br-8 0 0 $br-8; + background-color: transparent; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + &:hover { + stroke: var(--input-foreground-color-active); + } + } + } + + .flow-input { + @extend .input-base; + background-color: transparent; + height: $s-28; + } + + .flow-input-wrapper { + @include bodySmallTypography; + display: flex; + align-items: center; + height: $s-28; + padding: 0; + width: 100%; + margin: 0; + flex-grow: 1; + background-color: transparent; + color: var(--input-foreground-color); + border-radius: $br-8; + } + + &:hover { + background-color: var(--input-background-color-hover); + border: $s-1 solid var(--input-border-color-hover); + &:active { + background-color: var(--input-background-color-hover); + .flow-input-wrapper { + background-color: var(--input-background-color-hover); + } + } + } + + &:focus, + &:focus-within { + background-color: var(--input-background-color-focus); + border: $s-1 solid var(--input-border-color-focus); + &:hover { + border: $s-1 solid var(--input-border-color-focus); + } + } + + &.editing { + background-color: var(--input-background-color-active); + border: $s-1 solid var(--input-border-color-active); + } +} + +.remove-flow-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + min-width: $s-28; + svg { + @extend .button-icon; + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs index c76fcb474f..938d2d8789 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs @@ -5,13 +5,14 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.layer + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.main.data.workspace :as dw] [app.main.data.workspace.changes :as dch] [app.main.store :as st] - [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.components.select :refer [select]] [app.main.ui.icons :as i] [app.util.i18n :as i18n :refer [tr]] @@ -22,21 +23,23 @@ (defn opacity->string [opacity] - (if (= opacity :multiple) - "" + (if (not= opacity :multiple) (dm/str (-> opacity (d/coalesce 1) - (* 100))))) + (* 100))) + :multiple)) (mf/defc layer-menu {::mf/wrap-props false} [props] (let [ids (unchecked-get props "ids") - type (unchecked-get props "type") values (unchecked-get props "values") + hidden? (:hidden values) + blocked? (:blocked values) + current-blend-mode (or (:blend-mode values) :normal) - current-opacity (:opacity values) + current-opacity (opacity->string (:opacity values)) state* (mf/use-state {:selected-blend-mode current-blend-mode @@ -59,9 +62,9 @@ (mf/deps on-change) (fn [value] (swap! state* assoc - :selected-blend-mode value - :option-highlighted? false - :preview-complete? true) + :selected-blend-mode value + :option-highlighted? false + :preview-complete? true) (st/emit! (dw/unset-preview-blend-mode ids)) (on-change :blend-mode value))) @@ -70,8 +73,8 @@ (mf/deps on-change current-blend-mode) (fn [value] (swap! state* assoc - :preview-complete? false - :option-highlighted? true) + :preview-complete? false + :option-highlighted? true) (st/emit! (dw/set-preview-blend-mode ids value)))) handle-blend-mode-leave @@ -142,45 +145,46 @@ preview-complete?)) (swap! state* assoc :selected-blend-mode current-blend-mode))) - [:div.element-set - [:div.element-set-title - [:span - (case type - :multiple (tr "workspace.options.layer-options.title.multiple") - :group (tr "workspace.options.layer-options.title.group") - (tr "workspace.options.layer-options.title"))]] - - [:div.element-set-content - [:div.row-flex + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css-case :element-set-content true + :hidden hidden?)} + [:div {:class (stl/css :select)} [:& select - {:class "flex-grow no-check" - :default-value selected-blend-mode + {:default-value selected-blend-mode :options options :on-change handle-change-blend-mode :is-open? option-highlighted? + :class (stl/css-case :hidden-select hidden?) :on-pointer-enter-option handle-blend-mode-enter - :on-pointer-leave-option handle-blend-mode-leave}] + :on-pointer-leave-option handle-blend-mode-leave}]] + [:div {:class (stl/css :input) + :title (tr "workspace.options.opacity")} + [:span {:class (stl/css :icon)} "%"] + [:> numeric-input* + {:value current-opacity + :placeholder "--" + :on-change handle-opacity-change + :min 0 + :max 100 + :className (stl/css :numeric-input)}]] - [:div.input-element {:title (tr "workspace.options.opacity") - :class "percentail"} - [:> numeric-input - {:value (opacity->string current-opacity) - :placeholder (tr "settings.multiple") - :on-change handle-opacity-change - :min 0 - :max 100}]] - [:div.element-set-actions.layer-actions - (cond - (or (= :multiple (:hidden values)) (not (:hidden values))) - [:div.element-set-actions-button {:on-click handle-set-hidden} i/eye] + [:div {:class (stl/css :actions)} + (cond + (or (= :multiple hidden?) (not hidden?)) + [:button {:on-click handle-set-hidden + :class (stl/css :hidden-btn)} i/shown] - :else - [:div.element-set-actions-button {:on-click handle-set-visible} i/eye-closed]) + :else + [:button {:on-click handle-set-visible + :class (stl/css :hidden-btn)} i/hide]) - (cond - (or (= :multiple (:blocked values)) (not (:blocked values))) - [:div.element-set-actions-button {:on-click handle-set-blocked} i/unlock] + (cond + (or (= :multiple blocked?) (not blocked?)) + [:button {:on-click handle-set-blocked + :class (stl/css :lock-btn)} i/unlock] - :else - [:div.element-set-actions-button {:on-click handle-set-unblocked} i/lock])]]]])) + :else + [:button {:on-click handle-set-unblocked + :class (stl/css-case :lock-btn true + :locked blocked?)} i/lock])]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss new file mode 100644 index 0000000000..93be5aa25c --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss @@ -0,0 +1,56 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + margin-bottom: $s-8; +} +.element-set-content { + display: flex; + height: $s-32; + gap: $s-4; + .select { + width: $s-124; + padding: 0; + } + .input { + @extend .input-element; + width: $s-60; + } + .actions { + display: flex; + gap: $s-4; + .hidden-btn, + .lock-btn { + @extend .button-tertiary; + border-radius: $br-8; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + } + } + + &.hidden { + .hidden-select { + @include hiddenElement; + border: $s-1 solid var(--input-border-color-disabled); + } + .input { + @include hiddenElement; + border: $s-1 solid var(--input-border-color-disabled); + .icon { + stroke: var(--input-foreground-color-disabled); + } + .numeric-input { + color: var(--input-foreground-color-disabled); + } + } + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs index 1e987b4884..b7a5dde44f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs @@ -5,21 +5,44 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.layout-container + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.math :as mth] + [app.common.types.shape.layout :as ctl] + [app.config :as cf] + [app.main.data.events :as-alias ev] [app.main.data.workspace :as udw] + [app.main.data.workspace.grid-layout.editor :as dwge] [app.main.data.workspace.shape-layout :as dwsl] + [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.components.numeric-input :refer [numeric-input*]] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] [app.main.ui.components.select :refer [select]] + [app.main.ui.components.title-bar :refer [title-bar]] + [app.main.ui.formats :as fmt] + [app.main.ui.hooks :as h] [app.main.ui.icons :as i] [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] [cuerdas.core :as str] [rumext.v2 :as mf])) +(defn- dir-icons-refactor + [val] + (case val + :row i/grid-row + :row-reverse i/row-reverse + :column i/column + :column-reverse i/column-reverse)) + +;; FLEX COMPONENTS + (def layout-container-flex-attrs [:layout ;; :flex, :grid in the future :layout-flex-dir ;; :row, :row-reverse, :column, :column-reverse @@ -39,25 +62,21 @@ :layout-grid-rows]) (defn get-layout-flex-icon - [type val is-col?] + [type val ^boolean column?] (case type :align-items - (if is-col? + (if column? (case val :start i/align-items-column-start :end i/align-items-column-end - :center i/align-items-column-center - :stretch i/align-items-column-strech - :baseline i/align-items-column-baseline) + :center i/align-items-column-center) (case val :start i/align-items-row-start :end i/align-items-row-end - :center i/align-items-row-center - :stretch i/align-items-row-strech - :baseline i/align-items-row-baseline)) + :center i/align-items-row-center)) :justify-content - (if is-col? + (if column? (case val :start i/justify-content-column-start :end i/justify-content-column-end @@ -74,7 +93,7 @@ :space-between i/justify-content-row-between)) :align-content - (if is-col? + (if column? (case val :start i/align-content-column-start :end i/align-content-column-end @@ -93,458 +112,1053 @@ :space-between i/align-content-row-between :stretch nil)) - (case val - :start i/align-content-row-start - :end i/align-content-row-end - :center i/align-content-row-center - :space-around i/align-content-row-around - :space-between i/align-content-row-between - :stretch nil) - :align-self - (if is-col? + (if column? (case val - :auto i/minus + :auto i/remove-icon :start i/align-self-row-left :end i/align-self-row-right - :center i/align-self-row-center - :stretch i/align-self-row-strech - :baseline i/align-self-row-baseline) + :center i/align-self-row-center) (case val - :auto i/minus + :auto i/remove-icon :start i/align-self-column-top :end i/align-self-column-bottom - :center i/align-self-column-center - :stretch i/align-self-column-strech - :baseline i/align-self-column-baseline)))) + :center i/align-self-column-center)))) (defn get-layout-grid-icon - [type val is-col?] + [type val ^boolean column?] (case type + :align-items + (if column? + (case val + :auto i/remove-icon + :start i/align-self-row-left + :end i/align-self-row-right + :center i/align-self-row-center) + (case val + :auto i/remove-icon + :start i/align-self-column-top + :end i/align-self-column-bottom + :center i/align-self-column-center)) + :justify-items - (if is-col? + (if (not column?) (case val - :start i/grid-justify-content-column-start - :end i/grid-justify-content-column-end - :center i/grid-justify-content-column-center - :space-around i/grid-justify-content-column-around - :space-between i/grid-justify-content-column-between) + :start i/align-content-column-start + :center i/align-content-column-center + :end i/align-content-column-end + :space-around i/align-content-column-around + :space-between i/align-content-column-between + :stretch i/align-content-column-stretch) (case val - :start i/grid-justify-content-row-start - :end i/grid-justify-content-row-end - :center i/grid-justify-content-row-center - :space-around i/grid-justify-content-row-around - :space-between i/grid-justify-content-row-between)))) + :start i/align-content-row-start + :center i/align-content-row-center + :end i/align-content-row-end + :space-around i/align-content-row-around + :space-between i/align-content-row-between + :stretch i/align-content-row-stretch)))) -(mf/defc direction-btn - [{:keys [dir saved-dir set-direction icon?] :as props}] - (let [handle-on-click - (mf/use-callback - (mf/deps set-direction dir) - (fn [] - (when (some? set-direction) - (set-direction dir))))] - - [:button.dir.tooltip.tooltip-bottom - {:class (dom/classnames :active (= saved-dir dir) - :row (= :row dir) - :row-reverse (= :row-reverse dir) - :column-reverse (= :column-reverse dir) - :column (= :column dir)) - :key (dm/str "direction-" dir) - :alt (str/replace (str/capital (d/name dir)) "-" " ") - :on-click handle-on-click} - (if icon? - i/auto-direction - (str/replace (str/capital (d/name dir)) "-" " "))])) +(mf/defc direction-row-flex + {::mf/props :obj + ::mf/private true} + [{:keys [value on-change]}] + [:& radio-buttons {:selected (d/name value) + :decode-fn keyword + :on-change on-change + :name "flex-direction"} + [:& radio-button {:value "row" + :id "flex-direction-row" + :title "Row" + :icon (dir-icons-refactor :row)}] + [:& radio-button {:value "row-reverse" + :id "flex-direction-row-reverse" + :title "Row reverse" + :icon (dir-icons-refactor :row-reverse)}] + [:& radio-button {:value "column" + :id "flex-direction-column" + :title "Column" + :icon (dir-icons-refactor :column)}] + [:& radio-button {:value "column-reverse" + :id "flex-direction-column-reverse" + :title "Column reverse" + :icon (dir-icons-refactor :column-reverse)}]]) (mf/defc wrap-row - [{:keys [wrap-type set-wrap] :as props}] - [:* - [:button.tooltip.tooltip-bottom - {:class (dom/classnames :active (= wrap-type :nowrap)) - :alt "No wrap" - :on-click #(set-wrap :nowrap) - :style {:padding 0}} - [:span.no-wrap i/minus]] - [:button.wrap.tooltip.tooltip-bottom - {:class (dom/classnames :active (= wrap-type :wrap)) - :alt "Wrap" - :on-click #(set-wrap :wrap)} - i/auto-wrap]]) + {::mf/props :obj} + [{:keys [wrap-type on-click]}] + [:button {:class (stl/css-case :wrap-button true + :selected (= wrap-type :wrap)) + :title (if (= :wrap wrap-type) + "No wrap" + "Wrap") + :on-click on-click} + i/wrap]) (mf/defc align-row - [{:keys [is-col? align-items set-align] :as props}] - - [:div.align-items-style - (for [align [:start :center :end #_:stretch #_:baseline]] - [:button.align-start.tooltip - {:class (dom/classnames :active (= align-items align) - :tooltip-bottom-left (not= align :start) - :tooltip-bottom (= align :start)) - :alt (dm/str "Align items " (d/name align)) - :on-click #(set-align align) - :key (dm/str "align-items" (d/name align))} - (get-layout-flex-icon :align-items align is-col?)])]) + {::mf/props :obj} + [{:keys [is-column value on-change]}] + [:& radio-buttons {:selected (d/name value) + :decode-fn keyword + :on-change on-change + :name "flex-align-items"} + [:& radio-button {:value "start" + :icon (get-layout-flex-icon :align-items :start is-column) + :title "Align items start" + :id "align-items-start"}] + [:& radio-button {:value "center" + :icon (get-layout-flex-icon :align-items :center is-column) + :title "Align items center" + :id "align-items-center"}] + [:& radio-button {:value "end" + :icon (get-layout-flex-icon :align-items :end is-column) + :title "Align items end" + :id "align-items-end"}]]) (mf/defc align-content-row - [{:keys [is-col? align-content set-align-content] :as props}] - [:* - [:div.align-content-style - (for [align [:start :center :end]] - [:button.align-content.tooltip - {:class (dom/classnames :active (= align-content align) - :tooltip-bottom-left (not= align :start) - :tooltip-bottom (= align :start)) - :alt (dm/str "Align content " (d/name align)) - :on-click #(set-align-content align) - :key (dm/str "align-content" (d/name align))} - (get-layout-flex-icon :align-content align is-col?)])] - [:div.align-content-style - (for [align [:space-between :space-around :space-evenly]] - [:button.align-content.tooltip - {:class (dom/classnames :active (= align-content align) - :tooltip-bottom-left (not= align :space-between) - :tooltip-bottom (= align :space-between)) - :alt (dm/str "Align content " (d/name align)) - :on-click #(set-align-content align) - :key (dm/str "align-content" (d/name align))} - (get-layout-flex-icon :align-content align is-col?)])]]) + {::mf/props :obj} + [{:keys [is-column value on-change]}] + [:& radio-buttons {:selected (d/name value) + :decode-fn keyword + :on-change on-change + :name "flex-align-content"} + [:& radio-button {:value "start" + :icon (get-layout-flex-icon :align-content :start is-column) + :title "Align content start" + :id "align-content-start"}] + [:& radio-button {:value "center" + :icon (get-layout-flex-icon :align-content :center is-column) + :title "Align content center" + :id "align-content-center"}] + [:& radio-button {:value "end" + :icon (get-layout-flex-icon :align-content :end is-column) + :title "Align content end" + :id "align-content-end"}] + [:& radio-button {:value "space-between" + :icon (get-layout-flex-icon :align-content :space-between is-column) + :title "Align content space-between" + :id "align-content-space-between"}] + [:& radio-button {:value "space-around" + :icon (get-layout-flex-icon :align-content :space-around is-column) + :title "Align content space-around" + :id "align-content-space-around"}] + [:& radio-button {:value "space-evenly" + :icon (get-layout-flex-icon :align-content :space-evenly is-column) + :title "Align content space-evenly" + :id "align-content-space-evenly"}]]) (mf/defc justify-content-row - [{:keys [is-col? justify-content set-justify] :as props}] - [:* - [:div.justify-content-style - (for [justify [:start :center :end]] - [:button.justify.tooltip - {:class (dom/classnames :active (= justify-content justify) - :tooltip-bottom-left (not= justify :start) - :tooltip-bottom (= justify :start)) - :alt (dm/str "Justify content " (d/name justify)) - :on-click #(set-justify justify) - :key (dm/str "justify-content" (d/name justify))} - (get-layout-flex-icon :justify-content justify is-col?)])] - [:div.justify-content-style - (for [justify [:space-between :space-around :space-evenly]] - [:button.justify.tooltip - {:class (dom/classnames :active (= justify-content justify) - :tooltip-bottom-left (not= justify :space-between) - :tooltip-bottom (= justify :space-between)) - :alt (dm/str "Justify content " (d/name justify)) - :on-click #(set-justify justify) - :key (dm/str "justify-content" (d/name justify))} - (get-layout-flex-icon :justify-content justify is-col?)])]]) + {::mf/props :obj} + [{:keys [is-column justify-content on-change]}] + [:& radio-buttons {:selected (d/name justify-content) + :on-change on-change + :name "flex-justify"} + [:& radio-button {:value "start" + :icon (get-layout-flex-icon :justify-content :start is-column) + :title "Justify content start" + :id "justify-content-start"}] + [:& radio-button {:value "center" + :icon (get-layout-flex-icon :justify-content :center is-column) + :title "Justify content center" + :id "justify-content-center"}] + [:& radio-button {:value "end" + :icon (get-layout-flex-icon :justify-content :end is-column) + :title "Justify content end" + :id "justify-content-end"}] + [:& radio-button {:value "space-between" + :icon (get-layout-flex-icon :justify-content :space-between is-column) + :title "Justify content space-between" + :id "justify-content-space-between"}] + [:& radio-button {:value "space-around" + :icon (get-layout-flex-icon :justify-content :space-around is-column) + :title "Justify content space-around" + :id "justify-content-space-around"}] + [:& radio-button {:value "space-evenly" + :icon (get-layout-flex-icon :justify-content :space-evenly is-column) + :title "Justify content space-evenly" + :id "justify-content-space-evenly"}]]) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; PADDING +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- select-padding + ([p] + (select-padding (= p :p1) (= p :p2) (= p :p3) (= p :p4))) + ([p1? p2? p3? p4?] + (st/emit! (udw/set-paddings-selected {:p1 p1? :p2 p2? :p3 p3? :p4 p4?})))) + +(defn- on-padding-blur + [_event] + (select-padding false false false false)) + +(mf/defc simple-padding-selection + {::mf/props :obj} + [{:keys [value on-change]}] + (let [p1 (:p1 value) + p2 (:p2 value) + p3 (:p3 value) + p4 (:p4 value) + + p1 (if (and (not (= :multiple value)) + (= p1 p3)) + p1 + "--") + + p2 (if (and (not (= :multiple value)) + (= p2 p4)) + p2 + "--") + + on-change' + (mf/use-fn + (mf/deps on-change) + (fn [value event] + (let [attr (-> (dom/get-current-target event) + (dom/get-data "attr") + (keyword))] + (on-change :simple attr value event)))) + + on-focus + (mf/use-fn + (fn [event] + (let [attr (-> (dom/get-current-target event) + (dom/get-data "attr") + (keyword))] + + (case attr + :p1 (select-padding true false true false) + :p2 (select-padding false true false true)) + + (dom/select-target event))))] + + [:div {:class (stl/css :paddings-simple)} + [:div {:class (stl/css :padding-simple) + :title "Vertical padding"} + [:span {:class (stl/css :icon)} + i/padding-top-bottom] + [:> numeric-input* + {:class (stl/css :numeric-input) + :placeholder "--" + :data-attr "p1" + :on-change on-change' + :on-focus on-focus + :nillable true + :min 0 + :value p1}]] + [:div {:class (stl/css :padding-simple) + :title "Horizontal padding"} + + [:span {:class (stl/css :icon)} + i/padding-left-right] + [:> numeric-input* + {:className (stl/css :numeric-input) + :placeholder "--" + :data-attr "p2" + :on-change on-change' + :on-focus on-focus + :on-blur on-padding-blur + :nillable true + :min 0 + :value p2}]]])) + +(mf/defc multiple-padding-selection + {::mf/props :obj} + [{:keys [value on-change]}] + (let [p1 (:p1 value) + p2 (:p2 value) + p3 (:p3 value) + p4 (:p4 value) + + on-change' + (mf/use-fn + (mf/deps on-change) + (fn [value event] + (let [attr (-> (dom/get-current-target event) + (dom/get-data "attr") + (keyword))] + (on-change :multiple attr value event)))) + + on-focus + (mf/use-fn + (fn [event] + (let [attr (-> (dom/get-current-target event) + (dom/get-data "attr") + (keyword))] + + (select-padding attr) + (dom/select-target event))))] + + [:div {:class (stl/css :paddings-multiple)} + [:div {:class (stl/css :padding-multiple) + :title "Top padding"} + [:span {:class (stl/css :icon)} + i/padding-top] + [:> numeric-input* + {:class (stl/css :numeric-input) + :placeholder "--" + :data-attr "p1" + :on-change on-change' + :on-focus on-focus + :on-blur on-padding-blur + :nillable true + :min 0 + :value p1}]] + + [:div {:class (stl/css :padding-multiple) + :title "Right padding"} + [:span {:class (stl/css :icon)} + i/padding-right] + [:> numeric-input* + {:class (stl/css :numeric-input) + :placeholder "--" + :data-attr "p2" + :on-change on-change' + :on-focus on-focus + :on-blur on-padding-blur + :nillable true + :min 0 + :value p2}]] + + [:div {:class (stl/css :padding-multiple) + :title "Bottom padding"} + [:span {:class (stl/css :icon)} + i/padding-bottom] + [:> numeric-input* + {:class (stl/css :numeric-input) + :placeholder "--" + :data-attr "p3" + :on-change on-change' + :on-focus on-focus + :on-blur on-padding-blur + :nillable true + :min 0 + :value p3}]] + + [:div {:class (stl/css :padding-multiple) + :title "Left padding"} + [:span {:class (stl/css :icon)} + i/padding-left] + [:> numeric-input* + {:class (stl/css :numeric-input) + :placeholder "--" + :data-attr "p4" + :on-change on-change' + :on-focus on-focus + :on-blur on-padding-blur + :nillable true + :min 0 + :value p4}]]])) (mf/defc padding-section - [{:keys [values on-change-style on-change] :as props}] + {::mf/props :obj} + [{:keys [type on-type-change on-change] :as props}] + (let [on-type-change' + (mf/use-fn + (mf/deps on-type-change) + (fn [event] + (let [type (-> (dom/get-current-target event) + (dom/get-data "type")) + type (if (= type "multiple") :simple :multiple)] + (on-type-change type)))) - (let [padding-type (:layout-padding-type values) - p1 (if (and (not (= :multiple (:layout-padding values))) - (= (dm/get-in values [:layout-padding :p1]) - (dm/get-in values [:layout-padding :p3]))) - (dm/get-in values [:layout-padding :p1]) - "--") + props (mf/spread props {:on-change on-change})] - p2 (if (and (not (= :multiple (:layout-padding values))) - (= (dm/get-in values [:layout-padding :p2]) - (dm/get-in values [:layout-padding :p4]))) - (dm/get-in values [:layout-padding :p2]) - "--") + (mf/with-effect [] + ;; on destroy component + (fn [] + (on-padding-blur nil))) - select-paddings - (fn [p1? p2? p3? p4?] - (st/emit! (udw/set-paddings-selected {:p1 p1? :p2 p2? :p3 p3? :p4 p4?}))) + [:div {:class (stl/css :padding-group)} + [:div {:class (stl/css :padding-inputs)} + (cond + (= type :simple) + [:> simple-padding-selection props] - select-padding #(select-paddings (= % :p1) (= % :p2) (= % :p3) (= % :p4))] + (= type :multiple) + [:> multiple-padding-selection props])] - (mf/use-effect - (fn [] - (fn [] - ;;on destroy component - (select-paddings false false false false)))) + [:button {:class (stl/css-case + :padding-toggle true + :selected (= type :multiple)) + :title (tr "workspace.layout_grid.editor.padding.expand") + :data-type (d/name type) + :on-click on-type-change'} + i/padding-extended]])) - [:div.padding-row - (cond - (= padding-type :simple) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; GAP +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - [:div.padding-group - [:div.padding-item.tooltip.tooltip-bottom-left - {:alt "Vertical padding"} - [:span.icon.rotated i/auto-padding-both-sides] - [:> numeric-input - {:placeholder "--" - :on-change (partial on-change :simple :p1) - :on-focus #(do - (dom/select-target %) - (select-paddings true false true false)) - :value p1}]] +(defn- select-gap! + [value] + (st/emit! (udw/set-gap-selected value))) - [:div.padding-item.tooltip.tooltip-bottom-left - {:alt "Horizontal padding"} - [:span.icon i/auto-padding-both-sides] - [:> numeric-input - {:placeholder "--" - :on-change (partial on-change :simple :p2) - :on-focus #(do (dom/select-target %) - (select-paddings false true false true)) - :on-blur #(select-paddings false false false false) - :value p2}]]] +(defn- on-gap-focus + [event] + (let [type (-> (dom/get-current-target event) + (dom/get-data "type") + (keyword))] + (select-gap! type) + (dom/select-target event))) - (= padding-type :multiple) - [:div.wrapper - (for [num [:p1 :p2 :p3 :p4]] - [:div.tooltip.tooltip-bottom - {:key (dm/str "padding-" (d/name num)) - :alt (case num - :p1 "Top" - :p2 "Right" - :p3 "Bottom" - :p4 "Left")} - [:div.input-element.auto - [:> numeric-input - {:placeholder "--" - :on-change (partial on-change :multiple num) - :on-focus #(do (dom/select-target %) - (select-padding num)) - :on-blur #(select-paddings false false false false) - :value (num (:layout-padding values))}]]])]) - - [:div.padding-icons - [:div.padding-icon.tooltip.tooltip-bottom-left - {:class (dom/classnames :selected (= padding-type :multiple)) - :alt "Independent paddings" - :on-click #(on-change-style (if (= padding-type :multiple) :simple :multiple))} - i/auto-padding-side]]])) +(defn- on-gap-blur + [_event] + (select-gap! nil)) (mf/defc gap-section - [{:keys [is-col? wrap-type gap-selected? set-gap gap-value]}] - (let [select-gap - (fn [gap] - (st/emit! (udw/set-gap-selected gap)))] + {::mf/props :obj} + [{:keys [is-column wrap-type on-change value] + :or {wrap-type :none} + :as props}] + (let [nowrap? (= :nowrap wrap-type) - (mf/use-effect - (fn [] - (fn [] - ;;on destroy component - (select-gap nil)))) + row-gap-disabled? + (and ^boolean nowrap? + (not ^boolean is-column)) - [:div.layout-row - [:div.gap.row-title "Gap"] - [:div.gap-group - [:div.gap-row.tooltip.tooltip-bottom-left - {:alt "Column gap"} - [:span.icon - i/auto-gap] - [:> numeric-input {:no-validate true - :placeholder "--" - :on-focus (fn [event] - (select-gap :column-gap) - (reset! gap-selected? :column-gap) - (dom/select-target event)) - :on-change (partial set-gap (= :nowrap wrap-type) :column-gap) - :on-blur (fn [_] - (select-gap nil) - (reset! gap-selected? :none)) - :value (:column-gap gap-value) - :disabled (and (= :nowrap wrap-type) is-col?)}]] + col-gap-disabled? + (and ^boolean nowrap? + ^boolean is-column) - [:div.gap-row.tooltip.tooltip-bottom-left - {:alt "Row gap"} - [:span.icon.rotated - i/auto-gap] - [:> numeric-input {:no-validate true - :placeholder "--" - :on-focus (fn [event] - (select-gap :row-gap) - (reset! gap-selected? :row-gap) - (dom/select-target event)) - :on-change (partial set-gap (= :nowrap wrap-type) :row-gap) - :on-blur (fn [_] - (select-gap nil) - (reset! gap-selected? :none)) - :value (:row-gap gap-value) - :disabled (and (= :nowrap wrap-type) (not is-col?))}]]]])) + on-change' + (mf/use-fn + (mf/deps on-change) + (fn [value event] + (let [target (dom/get-current-target event) + wrap-type (dom/get-data target "wrap-type") + type (keyword (dom/get-data target "type"))] + (on-change (= "nowrap" wrap-type) type value event))))] + + (mf/with-effect [] + ;; on destroy component + (fn [] + (on-gap-blur nil))) + + [:div {:class (stl/css :gap-group)} + + [:div {:class (stl/css-case + :row-gap true + :disabled row-gap-disabled?) + :title "Row gap"} + [:span {:class (stl/css :icon)} i/gap-vertical] + [:> numeric-input* + {:class (stl/css :numeric-input true) + :no-validate true + :placeholder "--" + :data-type "row-gap" + :data-wrap-type (d/name wrap-type) + :on-focus on-gap-focus + :on-change on-change' + :on-blur on-gap-blur + :nillable true + :min 0 + :value (:row-gap value) + :disabled row-gap-disabled?}]] + + [:div {:class (stl/css-case + :column-gap true + :disabled col-gap-disabled?) + :title "Column gap"} + [:span {:class (stl/css :icon)} i/gap-horizontal] + [:> numeric-input* + {:class (stl/css :numeric-input true) + :no-validate true + :placeholder "--" + :data-type "column-gap" + :data-wrap-type (d/name wrap-type) + :on-focus on-gap-focus + :on-change on-change' + :on-blur on-gap-blur + :nillable true + :min 0 + :value (:column-gap value) + :disabled col-gap-disabled?}]]])) + +;; GRID COMPONENTS + +(mf/defc direction-row-grid + {::mf/props :obj} + [{:keys [value on-change] :as props}] + [:& radio-buttons {:selected (d/name value) + :decode-fn keyword + :on-change on-change + :name "grid-direction"} + [:& radio-button {:value "row" + :id "grid-direction-row" + :title "Row" + :icon (dir-icons-refactor :row)}] + [:& radio-button {:value "column" + :id "grid-direction-column" + :title "Column" + :icon (dir-icons-refactor :column)}]]) (mf/defc grid-edit-mode - [{:keys [id] :as props}] + {::mf/props :obj} + [{:keys [id]}] (let [edition (mf/deref refs/selected-edition) active? (= id edition) toggle-edit-mode - (mf/use-callback + (mf/use-fn (mf/deps id edition) (fn [] (if-not active? (st/emit! (udw/start-edition-mode id)) (st/emit! :interrupt))))] - - [:button.tooltip.tooltip-bottom-left - {:class (dom/classnames :active active?) - :alt "Grid edit mode" - :on-click #(toggle-edit-mode) - :style {:padding 0}} - i/grid-layout-mode])) + [:button + {:class (stl/css :edit-mode-btn) + :alt "Grid edit mode" + :on-click toggle-edit-mode} + (tr "workspace.layout_grid.editor.options.edit-grid")])) (mf/defc align-grid-row - [{:keys [is-col? align-items set-align] :as props}] - (let [type (if is-col? :column :row)] - [:div.align-items-style - (for [align [:start :center :end :stretch :baseline]] - [:button.align-start.tooltip - {:class (dom/classnames :active (= align-items align) - :tooltip-bottom-left (not= align :start) - :tooltip-bottom (= align :start)) - :alt (dm/str "Align items " (d/name align)) - :on-click #(set-align align type) - :key (dm/str "align-items" (d/name align))} - (get-layout-flex-icon :align-items align is-col?)])])) + {::mf/props :obj + ::mf/private true} + [{:keys [is-column value on-change]}] + (let [type (if ^boolean is-column "column" "row")] + [:& radio-buttons {:selected (d/name value) + :decode-fn keyword + :on-change on-change + :name (dm/str "flex-align-items-" type)} + [:& radio-button {:value "start" + :icon (get-layout-grid-icon :align-items :start is-column) + :title "Align items start" + :id (dm/str "align-items-start-" type)}] + [:& radio-button {:value "center" + :icon (get-layout-grid-icon :align-items :center is-column) + :title "Align items center" + :id (dm/str "align-items-center-" type)}] + [:& radio-button {:value "end" + :icon (get-layout-grid-icon :align-items :end is-column) + :title "Align items end" + :id (dm/str "align-items-end-" type)}]])) (mf/defc justify-grid-row - [{:keys [is-col? justify-items set-justify] :as props}] - (let [type (if is-col? :column :row)] - [:div.justify-content-style - (for [align [:start :center :end :space-around :space-between]] - [:button.align-start.tooltip - {:class (dom/classnames :active (= justify-items align) - :tooltip-bottom-left (not= align :start) - :tooltip-bottom (= align :start)) - :alt (dm/str "Justify content " (d/name align)) - :on-click #(set-justify align type) - :key (dm/str "justify-content" (d/name align))} - (get-layout-grid-icon :justify-items align is-col?)])])) + {::mf/props :obj + ::mf/private :obj} + [{:keys [is-column value on-change]}] + (let [type (if ^boolean is-column "column" "row")] + [:& radio-buttons {:selected (d/name value) + :on-change on-change + :decode-fn keyword + :name (dm/str "grid-justify-items-" type)} -(defn manage-values [{:keys [value type]}] + [:& radio-button {:key "justify-item-start" + :value "start" + :icon (get-layout-grid-icon :justify-items :start is-column) + :title "Justify items start" + :id (dm/str "justify-items-start-" type)}] + + [:& radio-button {:key "justify-item-center" + :value "center" + :icon (get-layout-grid-icon :justify-items :center is-column) + :title "Justify items center" + :id (dm/str "justify-items-center-" type)}] + + [:& radio-button {:key "justify-item-end" + :value "end" + :icon (get-layout-grid-icon :justify-items :end is-column) + :title "Justify items end" + :id (dm/str "justify-items-end-" type)}] + + [:& radio-button {:key "justify-item-space-around" + :value "space-around" + :icon (get-layout-grid-icon :justify-items :space-around is-column) + :title "Justify items space-around" + :id (dm/str "justify-items-space-around-" type)}] + + [:& radio-button {:key "justify-item-space-between" + :value "space-between" + :icon (get-layout-grid-icon :justify-items :space-between is-column) + :title "Justify items space-between" + :id (dm/str "justify-items-space-between-" type)}] + + [:& radio-button {:key "justify-item-stretch" + :value "stretch" + :icon (get-layout-grid-icon :justify-items :stretch is-column) + :title "Justify items stretch" + :id (dm/str "justify-items-stretch-" type)}]])) + +(defn- manage-values + [{:keys [type value]}] (case type :auto "auto" - :percent (dm/str value "%") - :flex (dm/str value "fr") - :fixed (dm/str value "px") + :percent (fmt/format-percent value) + :flex (fmt/format-frs value) + :fixed (fmt/format-pixels value) value)) +(mf/defc grid-track-info + {::mf/props :obj} + [{:keys [is-column + type + index + column + set-column-value + set-column-type + remove-element + reorder-track + hover-track + on-select-track]}] + + (let [drop-track + (mf/use-fn + (mf/deps type reorder-track index) + (fn [drop-position data event] + (reorder-track type (:index data) (if (= :top drop-position) (dec index) index) (not (kbd/mod? event))))) + + pointer-enter + (mf/use-fn + (mf/deps type hover-track index) + (fn [] + (hover-track type index true))) + + pointer-leave + (mf/use-fn + (mf/deps type hover-track index) + (fn [] + (hover-track type index false))) + + handle-select-track + (mf/use-fn + (mf/deps on-select-track type index) + (fn [] + (when on-select-track + (on-select-track type index)))) + + [dprops dref] + (h/use-sortable + :data-type "penpot/grid-track" + :on-drop drop-track + :data {:is-column is-column + :index index + :column column} + :draggable? true)] + + [:div {:class (stl/css-case :track-info true + :dnd-over-top (or (= (:over dprops) :top) + (= (:over dprops) :center)) + :dnd-over-bot (= (:over dprops) :bot)) + :ref dref + :on-pointer-enter pointer-enter + :on-pointer-leave pointer-leave} + + [:div {:class (stl/css :track-info-container)} + [:div {:class (stl/css :track-info-dir-icon) + :on-click handle-select-track} + (if is-column i/flex-vertical i/flex-horizontal)] + + [:div {:class (stl/css :track-info-value)} + [:> numeric-input* {:no-validate true + :value (:value column) + :on-change #(set-column-value type index %) + :placeholder "--" + :min 0 + :disabled (= :auto (:type column))}]] + + [:div {:class (stl/css :track-info-unit)} + [:& select {:class (stl/css :track-info-unit-selector) + :default-value (:type column) + :options [{:value :flex :label "FR"} + {:value :auto :label "AUTO"} + {:value :fixed :label "PX"} + {:value :percent :label "%"}] + :on-change #(set-column-type type index %)}]]] + + [:button {:class (stl/css :remove-track-btn) + :on-click #(remove-element type index)} + i/remove-icon]])) + (mf/defc grid-columns-row - [{:keys [is-col? expanded? column-values toggle add-new-element set-column-value set-column-type remove-element] :as props}] + {::mf/props :obj} + [{:keys [is-column expanded? column-values toggle add-new-element set-column-value set-column-type + remove-element reorder-track hover-track on-select-track]}] (let [column-num (count column-values) direction (if (> column-num 1) - (if is-col? "Columns " "Rows ") - (if is-col? "Column " "Row ")) + (if ^boolean is-column "Columns " "Rows ") + (if ^boolean is-column "Column " "Row ")) - column-vals (str/join ", " (map manage-values column-values)) - generated-name (dm/str direction (if (= column-num 0) " - empty" (dm/str column-num " (" column-vals ")"))) - type (if is-col? :column :row)] + track-name (dm/str direction (if (= column-num 0) " - empty" column-num)) + track-detail (str/join ", " (map manage-values column-values)) - [:div.grid-columns - [:div.grid-columns-header - [:button.expand-icon - {:on-click toggle} i/actions] + type (if is-column :column :row) - [:div.columns-info {:title generated-name - :on-click toggle} generated-name] - [:button.add-column {:on-click #(do - (when-not expanded? (toggle)) - (add-new-element type {:type :fixed :value 100}))} i/plus]] + add-track + #(do + (when-not expanded? (toggle)) + (add-new-element type ctl/default-track-value))] + + [:div {:class (stl/css :grid-tracks)} + [:div {:class (stl/css :grid-track-header)} + [:button {:class (stl/css :expand-icon) :on-click toggle} i/menu] + [:div {:class (stl/css :track-title) :on-click toggle} + [:div {:class (stl/css :track-name) :title track-name} track-name] + [:div {:class (stl/css :track-detail) :title track-detail} track-detail]] + [:button {:class (stl/css :add-column) :on-click add-track} i/add]] (when expanded? - [:div.columns-info-wrapper - (for [[index column] (d/enumerate column-values)] - [:div.column-info - [:div.direction-grid-icon - (if is-col? - i/layout-rows - i/layout-columns)] + [:& h/sortable-container {} + [:div {:class (stl/css :grid-tracks-info-container)} + (for [[index column] (d/enumerate column-values)] + [:& grid-track-info {:key (dm/str index "-" (d/name type)) + :type type + :is-column is-column + :index index + :column column + :set-column-value set-column-value + :set-column-type set-column-type + :remove-element remove-element + :reorder-track reorder-track + :hover-track hover-track + :on-select-track on-select-track}])]])])) - [:div.grid-column-value - [:> numeric-input {:no-validate true - :value (:value column) - :on-change #(set-column-value type index %) - :placeholder "--"}]] - [:div.grid-column-unit - [:& select - {:class "grid-column-unit-selector" - :default-value (:type column) - :options [{:value :flex :label "fr"} - {:value :auto :label "auto"} - {:value :fixed :label "px"} - {:value :percent :label "%"}] - :on-change #(set-column-type type index %)}]] - [:button.remove-grid-column - {:on-click #(remove-element type index)} - i/minus]])])])) +;; LAYOUT COMPONENT + +(defn- open-flex-help + [_] + (st/emit! (dom/open-new-window cf/flex-help-uri))) + +(defn- open-grid-help + [_] + (st/emit! (dom/open-new-window cf/grid-help-uri))) (mf/defc layout-container-menu - {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "multiple"]))]} - [{:keys [ids _type values multiple] :as props}] - (let [open? (mf/use-state false) + {::mf/memo #{:ids :values :multiple} + ::mf/props :obj} + [{:keys [ids values multiple]}] + (let [;; Display + layout-type (:layout values) + has-layout? (some? layout-type) - ;; Display - layout-type (:layout values) + show-dropdown* (mf/use-state false) + show-dropdown? @show-dropdown* + + open* (mf/use-state #(if layout-type true false)) + open? (deref open*) + + on-toggle-visibility + (mf/use-fn #(swap! open* not)) on-add-layout - (fn [type] - (st/emit! (dwsl/create-layout type)) - (reset! open? true)) + (mf/use-fn + (fn [event] + (let [type (-> (dom/get-current-target event) + (dom/get-data "type") + (keyword))] + (st/emit! (with-meta (dwsl/create-layout type) + {::ev/origin "workspace:sidebar"})) + (reset! open* true)))) on-remove-layout - (fn [_] - (st/emit! (dwsl/remove-layout ids)) - (reset! open? false)) - - _set-flex - (fn [] - (st/emit! (dwsl/remove-layout ids)) - (on-add-layout :flex)) - - _set-grid - (fn [] - (st/emit! (dwsl/remove-layout ids)) - (on-add-layout :grid)) - - ;; Flex-direction + (mf/use-fn + (mf/deps ids) + (fn [_] + (st/emit! (dwsl/remove-layout ids)) + (reset! open* false))) saved-dir (:layout-flex-dir values) - is-col? (or (= :column saved-dir) (= :column-reverse saved-dir)) + is-column (or (= :column saved-dir) (= :column-reverse saved-dir)) ;; Wrap type + wrap-type (:layout-wrap-type values) + + toggle-wrap + (mf/use-fn + (mf/deps wrap-type ids) + (fn [] + (let [type (if (= wrap-type :wrap) + :nowrap + :wrap)] + (st/emit! (dwsl/update-layout ids {:layout-wrap-type type}))))) + - wrap-type (:layout-wrap-type values) - set-wrap (fn [type] - (st/emit! (dwsl/update-layout ids {:layout-wrap-type type}))) ;; Align items - align-items (:layout-align-items values) - set-align-items (fn [value] - (st/emit! (dwsl/update-layout ids {:layout-align-items value}))) + + set-align-items + (mf/use-fn + (mf/deps ids) + (fn [value] + (st/emit! (dwsl/update-layout ids {:layout-align-items (keyword value)})))) ;; Justify content - justify-content (:layout-justify-content values) - set-justify-content (fn [value] - (st/emit! (dwsl/update-layout ids {:layout-justify-content value}))) + + set-justify-content + (mf/use-fn + (mf/deps ids) + (fn [value] + (st/emit! (dwsl/update-layout ids {:layout-justify-content (keyword value)})))) ;; Align content - align-content (:layout-align-content values) - set-align-content (fn [value] - (if (= align-content value) - (st/emit! (dwsl/update-layout ids {:layout-align-content :stretch})) - (st/emit! (dwsl/update-layout ids {:layout-align-content value})))) + + ;; FIXME revisit??? + on-align-content-change + (mf/use-fn + (mf/deps ids align-content) + (fn [value] + (if (= align-content value) + (st/emit! (dwsl/update-layout ids {:layout-align-content :stretch})) + (st/emit! (dwsl/update-layout ids {:layout-align-content (keyword value)}))))) ;; Gap - - gap-selected? (mf/use-state :none) - - set-gap - (fn [gap-multiple? type val] + on-gap-change + (fn [multiple? type val] (let [val (mth/finite val 0)] - (if gap-multiple? + (cond + ^boolean multiple? (st/emit! (dwsl/update-layout ids {:layout-gap {:row-gap val :column-gap val}})) + + (some? type) (st/emit! (dwsl/update-layout ids {:layout-gap {type val}}))))) ;; Padding + on-padding-type-change + (mf/use-fn + (mf/deps ids) + (fn [type] + (st/emit! (dwsl/update-layout ids {:layout-padding-type type})))) - change-padding-type - (fn [type] - (st/emit! (dwsl/update-layout ids {:layout-padding-type type}))) + on-padding-change + (mf/use-fn + (mf/deps ids) + (fn [type prop val] + (let [val (mth/finite val 0)] + (cond + (and (= type :simple) (= prop :p1)) + (st/emit! (dwsl/update-layout ids {:layout-padding {:p1 val :p3 val}})) + + (and (= type :simple) (= prop :p2)) + (st/emit! (dwsl/update-layout ids {:layout-padding {:p2 val :p4 val}})) + + (some? prop) + (st/emit! (dwsl/update-layout ids {:layout-padding {prop val}})))))) + + ;; Grid-direction + + saved-grid-dir (:layout-grid-dir values) + + on-direction-change + (mf/use-fn + (mf/deps layout-type ids) + (fn [dir] + (if (= :flex layout-type) + (st/emit! (dwsl/update-layout ids {:layout-flex-dir dir})) + (st/emit! (dwsl/update-layout ids {:layout-grid-dir dir}))))) + + ;; Align grid + align-items-row (:layout-align-items values) + align-items-column (:layout-justify-items values) + + on-column-align-change + (mf/use-fn + (mf/deps ids) + (fn [value] + (st/emit! (dwsl/update-layout ids {:layout-justify-items value})))) + + on-row-align-change + (mf/use-fn + (mf/deps ids) + (fn [value] + (st/emit! (dwsl/update-layout ids {:layout-align-items value})))) + + ;; Justify grid + grid-justify-content-row (:layout-justify-content values) + grid-justify-content-column (:layout-align-content values) + + grid-enabled? (features/use-feature "layout/grid") + + on-column-justify-change + (mf/use-fn + (mf/deps ids) + (fn [value] + (st/emit! (dwsl/update-layout ids {:layout-align-content value})))) + + on-row-justify-change + (mf/use-fn + (mf/deps ids) + (fn [value] + (st/emit! (dwsl/update-layout ids {:layout-justify-content value})))) + + on-toggle-dropdown-visibility + (mf/use-fn #(swap! show-dropdown* not)) + + on-hide-dropdown + (mf/use-fn #(reset! show-dropdown* false))] + + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-title)} + [:& title-bar + {:collapsable has-layout? + :collapsed (not open?) + :on-collapsed on-toggle-visibility + :title "Layout" + :class (stl/css-case :title-spacing-layout (not has-layout?))} + + (if (and (not multiple) (:layout values)) + [:div {:class (stl/css :title-actions)} + (when ^boolean grid-enabled? + [:* + [:button {:class (stl/css :add-layout) + :on-click on-toggle-dropdown-visibility} + i/menu] + + [:& dropdown {:show show-dropdown? + :on-close on-hide-dropdown} + [:div {:class (stl/css :layout-options)} + [:button {:class (stl/css :layout-option) + :data-type "flex" + :on-click on-add-layout} + "Flex layout"] + [:button {:class (stl/css :layout-option) + :data-type "grid" + :on-click on-add-layout} + "Grid layout"]]]]) + + (when has-layout? + [:button {:class (stl/css :remove-layout) + :on-click on-remove-layout} + i/remove-icon])] + + [:div {:class (stl/css :title-actions)} + (if ^boolean grid-enabled? + [:* + [:button {:class (stl/css :add-layout) + :on-click on-toggle-dropdown-visibility} + i/add] + + [:& dropdown {:show show-dropdown? + :on-close on-hide-dropdown} + [:div {:class (stl/css :layout-options)} + [:button {:class (stl/css :layout-option) + :data-type "flex" + :on-click on-add-layout} + "Flex layout"] + [:button {:class (stl/css :layout-option) + :data-type "grid" + :on-click on-add-layout} + "Grid layout"]]]] + + [:button {:class (stl/css :add-layout) + :data-type "flex" + :on-click on-add-layout} + i/add]) + (when has-layout? + [:button {:class (stl/css :remove-layout) + :on-click on-remove-layout} + i/remove-icon])])]] + + (when (and ^boolean open? + ^boolean has-layout? + (not= :multiple layout-type)) + (case layout-type + :flex + [:div {:class (stl/css :flex-layout-menu)} + [:div {:class (stl/css :first-row)} + [:& align-row {:is-column is-column + :value align-items + :on-change set-align-items}] + + [:& direction-row-flex {:on-change on-direction-change + :value saved-dir}] + + [:& wrap-row {:wrap-type wrap-type + :on-click toggle-wrap}]] + + [:div {:class (stl/css :second-row :help-button-wrapper)} + [:& justify-content-row {:is-column is-column + :justify-content justify-content + :on-change set-justify-content}] + + [:button {:on-click open-flex-help + :class (stl/css :help-button)} + i/help]] + (when (= :wrap wrap-type) + [:div {:class (stl/css :third-row)} + [:& align-content-row {:is-column is-column + :value align-content + :on-change on-align-content-change}]]) + [:div {:class (stl/css :forth-row)} + [:& gap-section {:is-column is-column + :wrap-type wrap-type + :on-change on-gap-change + :value (:layout-gap values)}] + + [:& padding-section {:value (:layout-padding values) + :type (:layout-padding-type values) + :on-type-change on-padding-type-change + :on-change on-padding-change}]]] + + :grid + [:div {:class (stl/css :grid-layout-menu)} + (when (= 1 (count ids)) + [:div {:class (stl/css :edit-grid-wrapper)} + [:& grid-edit-mode {:id (first ids)}] + [:button {:on-click open-grid-help + :class (stl/css :help-button)} i/help]]) + + [:div {:class (stl/css :row :first-row)} + [:div {:class (stl/css :direction-edit)} + [:div {:class (stl/css :direction)} + [:& direction-row-grid {:value saved-grid-dir + :on-change on-direction-change}]]] + + [:& align-grid-row {:is-column false + :value align-items-row + :on-change on-row-align-change}] + [:& align-grid-row {:is-column true + :value align-items-column + :on-change on-column-align-change}]] + + [:div {:class (stl/css :row :grid-layout-align)} + [:& justify-grid-row {:is-column true + :value grid-justify-content-column + :on-change on-column-justify-change}] + [:& justify-grid-row {:is-column false + :value grid-justify-content-row + :on-change on-row-justify-change}]] + + [:div {:class (stl/css :row)} + [:& gap-section {:on-change on-gap-change + :value (:layout-gap values)}]] + [:div {:class (stl/css :row :padding-section)} + [:& padding-section {:value (:layout-padding values) + :type (:layout-padding-type values) + :on-type-change on-padding-type-change + :on-change on-padding-change}]]] + + nil))])) + +(mf/defc grid-layout-edition + {::mf/memo #{:ids :values} + ::mf/props :obj} + [{:keys [ids values]}] + (let [;; Gap + saved-grid-dir (:layout-grid-dir values) + + on-direction-change + (mf/use-fn + (mf/deps ids) + (fn [dir] + (st/emit! (dwsl/update-layout ids {:layout-grid-dir dir})))) + + on-gap-change + (mf/use-fn + (mf/deps ids) + (fn [multiple? type val] + (let [val (mth/finite val 0)] + (if multiple? + (st/emit! (dwsl/update-layout ids {:layout-gap {:row-gap val :column-gap val}})) + (st/emit! (dwsl/update-layout ids {:layout-gap {type val}})))))) + + ;; Padding + on-padding-type-change + (mf/use-fn + (mf/deps ids) + (fn [type] + (st/emit! (dwsl/update-layout ids {:layout-padding-type type})))) on-padding-change (fn [type prop val] @@ -559,217 +1173,170 @@ :else (st/emit! (dwsl/update-layout ids {:layout-padding {prop val}}))))) - ;; Grid-direction - - saved-grid-dir (:layout-grid-dir values) - - set-direction - (fn [dir type] - (if (= :flex type) - (st/emit! (dwsl/update-layout ids {:layout-flex-dir dir})) - (st/emit! (dwsl/update-layout ids {:layout-grid-dir dir})))) - ;; Align grid align-items-row (:layout-align-items values) align-items-column (:layout-justify-items values) - set-align-grid - (fn [value type] - (if (= type :row) - (st/emit! (dwsl/update-layout ids {:layout-align-items value})) - (st/emit! (dwsl/update-layout ids {:layout-justify-items value})))) + on-column-align-change + (mf/use-fn + (mf/deps ids) + (fn [value] + (st/emit! (dwsl/update-layout ids {:layout-justify-items value})))) + + on-row-align-change + (mf/use-fn + (mf/deps ids) + (fn [value] + (st/emit! (dwsl/update-layout ids {:layout-align-items value})))) ;; Justify grid - grid-justify-content-row (:layout-align-content values) - grid-justify-content-column (:layout-justify-content values) + grid-justify-content-row (:layout-justify-content values) + grid-justify-content-column (:layout-align-content values) - set-justify-grid - (mf/use-callback + on-column-justify-change + (mf/use-fn (mf/deps ids) - (fn [value type] - (if (= type :row) - (st/emit! (dwsl/update-layout ids {:layout-align-content value})) - (st/emit! (dwsl/update-layout ids {:layout-justify-content value}))))) + (fn [value] + (st/emit! (dwsl/update-layout ids {:layout-align-content value})))) + on-row-justify-change + (mf/use-fn + (mf/deps ids) + (fn [value] + (st/emit! (dwsl/update-layout ids {:layout-justify-content value})))) - ;;Grid columns - column-grid-values (:layout-grid-columns values) - grid-columns-open? (mf/use-state false) - toggle-columns-info (mf/use-callback - (fn [_] - (swap! grid-columns-open? not))) + columns-open? (mf/use-state false) + rows-open? (mf/use-state false) - ; Grid rows / columns - rows-grid-values (:layout-grid-rows values) - grid-rows-open? (mf/use-state false) - toggle-rows-info - (mf/use-callback - (fn [_] - (swap! grid-rows-open? not))) + column-values (:layout-grid-columns values) + rows-values (:layout-grid-rows values) + + toggle-columns-open + (mf/use-fn #(swap! columns-open? not)) + + toggle-rows-open + (mf/use-fn #(swap! rows-open? not)) add-new-element - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [type value] (st/emit! (dwsl/add-layout-track ids type value)))) remove-element - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [type index] (st/emit! (dwsl/remove-layout-track ids type index)))) + reorder-track + (mf/use-fn + (mf/deps ids) + (fn [type from-index to-index move-content?] + (st/emit! (dwsl/reorder-layout-track ids type from-index to-index move-content?)))) + + hover-track + (mf/use-fn + (mf/deps ids) + (fn [type index hover?] + (st/emit! (dwsl/hover-layout-track ids type index hover?)))) + + handle-select-track + (mf/use-fn + (mf/deps ids) + (fn [type index] + (st/emit! (dwge/select-track-cells (first ids) type index)))) + set-column-value - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [type index value] (st/emit! (dwsl/change-layout-track ids type index {:value value})))) set-column-type - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [type index track-type] - (st/emit! (dwsl/change-layout-track ids type index {:type track-type}))))] + (let [value (case track-type + :auto nil + :flex 1 + :percent 20 + :fixed 100)] + (st/emit! (dwsl/change-layout-track ids type index {:value value + :type track-type}))))) + handle-locate-grid + (mf/use-fn + (mf/deps ids) + (fn [] + (st/emit! (dwge/locate-board (first ids)))))] - [:div.element-set - [:div.element-set-title - [:* - [:span "Layout"] - (if (and (not multiple) (:layout values)) - [:div.title-actions - #_[:div.layout-btns - [:button {:on-click set-flex - :class (dom/classnames - :active (= :flex layout-type))} "Flex"] - [:button {:on-click set-grid - :class (dom/classnames - :active (= :grid layout-type))} "Grid"]] - [:button.remove-layout {:on-click on-remove-layout} i/minus]] + [:div {:class (stl/css :grid-layout-menu)} + [:div {:class (stl/css :row)} + [:div {:class (stl/css :grid-layout-menu-title)} "GRID LAYOUT"] + [:button {:on-click open-grid-help + :class (stl/css :help-button)} i/help] + [:button {:class (stl/css :exit-btn) + :on-click #(st/emit! (udw/clear-edition-mode))} + (tr "workspace.layout_grid.editor.options.exit")]] - [:button.add-page {:on-click #(on-add-layout :flex)} i/close])]] + [:div {:class (stl/css :row :first-row)} + [:div {:class (stl/css :direction-edit)} + [:div {:class (stl/css :direction)} + [:& direction-row-grid {:value saved-grid-dir + :on-change on-direction-change}]]] - (when (:layout values) - (when (not= :multiple layout-type) - (case layout-type - :flex + [:& align-grid-row {:is-column false + :value align-items-row + :on-change on-row-align-change}] - [:div.element-set-content.layout-menu - [:div.layout-row - [:div.direction-wrap.row-title "Direction"] - [:div.btn-wrapper - [:div.direction - [:* - (for [dir [:row :row-reverse :column :column-reverse]] - [:& direction-btn {:key (d/name dir) - :dir dir - :saved-dir saved-dir - :set-direction #(set-direction dir :flex) - :icon? true}])]] + [:& align-grid-row {:is-column true + :value align-items-column + :on-change on-column-align-change}]] - [:div.wrap-type - [:& wrap-row {:wrap-type wrap-type - :set-wrap set-wrap}]]]] + [:div {:class (stl/css :row :grid-layout-align)} + [:& justify-grid-row {:is-column true + :value grid-justify-content-column + :on-change on-column-justify-change}] + [:& justify-grid-row {:is-column false + :value grid-justify-content-row + :on-change on-row-justify-change}] - (when (= :wrap wrap-type) - [:div.layout-row - [:div.align-content.row-title "Content"] - [:div.btn-wrapper.align-content - [:& align-content-row {:is-col? is-col? - :align-content align-content - :set-align-content set-align-content}]]]) + [:button {:on-click handle-locate-grid + :class (stl/css :locate-button) + :title (tr "workspace.layout_grid.editor.top-bar.locate.tooltip")} + i/locate]] - [:div.layout-row - [:div.align-items.row-title "Align"] - [:div.btn-wrapper - [:& align-row {:is-col? is-col? - :align-items align-items - :set-align set-align-items}]]] + [:div {:class (stl/css :row)} + [:& gap-section {:on-change on-gap-change + :value (:layout-gap values)}]] - [:div.layout-row - [:div.justify-content.row-title "Justify"] - [:div.btn-wrapper.justify-content - [:& justify-content-row {:is-col? is-col? - :justify-content justify-content - :set-justify set-justify-content}]]] - [:& gap-section {:is-col? is-col? - :wrap-type wrap-type - :gap-selected? gap-selected? - :set-gap set-gap - :gap-value (:layout-gap values)}] + [:div {:class (stl/css :row :padding-section)} + [:& padding-section {:value (:layout-padding values) + :type (:layout-padding-type values) + :on-type-change on-padding-type-change + :on-change on-padding-change}]] + [:div {:class (stl/css :row :grid-tracks-row)} + [:& grid-columns-row {:is-column true + :expanded? @columns-open? + :toggle toggle-columns-open + :column-values column-values + :add-new-element add-new-element + :set-column-value set-column-value + :set-column-type set-column-type + :remove-element remove-element + :reorder-track reorder-track + :hover-track hover-track + :on-select-track handle-select-track}] - [:& padding-section {:values values - :on-change-style change-padding-type - :on-change on-padding-change}]] - - :grid - - [:div.element-set-content.layout-menu - [:div.layout-row - [:div.direction-wrap.row-title "Direction"] - [:div.btn-wrapper - [:div.direction - [:* - (for [dir [:row :column]] - [:& direction-btn {:key (d/name dir) - :dir dir - :saved-dir saved-grid-dir - :set-direction #(set-direction dir :grid) - :icon? false}])]] - - (when (= 1 (count ids)) - [:div.edit-mode - [:& grid-edit-mode {:id (first ids)}]])]] - - [:div.layout-row - [:div.align-items-grid.row-title "Align"] - [:div.btn-wrapper.align-grid - [:& align-grid-row {:is-col? false - :align-items align-items-row - :set-align set-align-grid}] - - [:& align-grid-row {:is-col? true - :align-items align-items-column - :set-align set-align-grid}]]] - - [:div.layout-row - [:div.jusfiy-content-grid.row-title "Justify"] - [:div.btn-wrapper.align-grid - [:& justify-grid-row {:is-col? true - :justify-items grid-justify-content-column - :set-justify set-justify-grid}] - [:& justify-grid-row {:is-col? false - :justify-items grid-justify-content-row - :set-justify set-justify-grid}]]] - - [:& grid-columns-row {:is-col? true - :expanded? @grid-columns-open? - :toggle toggle-columns-info - :column-values column-grid-values - :add-new-element add-new-element - :set-column-value set-column-value - :set-column-type set-column-type - :remove-element remove-element}] - - [:& grid-columns-row {:is-col? false - :expanded? @grid-rows-open? - :toggle toggle-rows-info - :column-values rows-grid-values - :add-new-element add-new-element - :set-column-value set-column-value - :set-column-type set-column-type - :remove-element remove-element}] - - [:& gap-section {:is-col? is-col? - :wrap-type wrap-type - :gap-selected? gap-selected? - :set-gap set-gap - :gap-value (:layout-gap values)}] - - [:& padding-section {:values values - :on-change-style change-padding-type - :on-change on-padding-change}]] - - - ;; Default if not grid or flex - nil)))])) + [:& grid-columns-row {:is-column false + :expanded? @rows-open? + :toggle toggle-rows-open + :column-values rows-values + :add-new-element add-new-element + :set-column-value set-column-value + :set-column-type set-column-type + :remove-element remove-element + :reorder-track reorder-track + :hover-track hover-track + :on-select-track handle-select-track}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss new file mode 100644 index 0000000000..a51118e292 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss @@ -0,0 +1,362 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + margin: 0; + .element-title { + .title-spacing-layout { + padding-left: $s-2; + margin: 0; + } + + .title-actions { + position: relative; + display: flex; + gap: $s-4; + height: $s-32; + padding: 0; + margin: 0; + .layout-options { + width: $s-92; + } + .layout-option { + white-space: nowrap; + } + .remove-layout, + .add-layout { + @extend .button-tertiary; + border-radius: $br-8; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + } + } + } + .flex-layout-menu { + margin-bottom: $s-8; + .first-row { + display: flex; + gap: $s-4; + margin-bottom: $s-12; + margin-top: $s-4; + .wrap-button { + @extend .button-tertiary; + border-radius: $br-8; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + &.selected { + @extend .button-icon-selected; + } + } + } + .second-row, + .third-row { + margin-bottom: $s-12; + } + .forth-row { + @include flexColumn; + } + .help-button-wrapper { + position: relative; + .help-button { + position: absolute; + top: 0; + right: 0; + } + } + } +} + +.gap-group { + display: flex; + gap: $s-4; + .column-gap { + @extend .input-element; + width: $s-108; + &.disabled { + @extend .disabled-input; + } + } + .row-gap { + @extend .input-element; + width: $s-108; + &.disabled { + @extend .disabled-input; + } + } +} + +.padding-group { + display: flex; + gap: $s-4; + + .padding-inputs { + display: flex; + gap: $s-4; + } + + .paddings-simple { + display: flex; + gap: $s-4; + + .padding-simple { + @extend .input-element; + max-width: $s-108; + } + } + + .paddings-multiple { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $s-4; + + .padding-multiple { + @extend .input-element; + max-width: $s-108; + } + } + + .padding-toggle { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + border-radius: $br-8; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + &.selected { + @extend .button-icon-selected; + } + } +} + +.grid-layout-menu { + @include flexColumn; + gap: $s-8; + + .row { + @include flexRow; + } + + .first-row { + margin-bottom: $s-8; + } + + .grid-layout-align { + @include flexColumn; + gap: $s-4; + align-items: flex-start; + position: relative; + + .locate-button { + position: absolute; + top: 0; + right: 0; + } + } + + .grid-layout-menu-title { + flex: 1; + font-size: $fs-11; + color: var(--title-foreground-color-hover); + } + + .edit-mode-btn { + @extend .button-secondary; + @include uppercaseTitleTipography; + width: 100%; + padding: $s-8; + } + + .exit-btn { + @extend .button-secondary; + @include uppercaseTitleTipography; + padding: $s-8 $s-20; + } + + .grid-tracks-info-container { + @include flexColumn; + margin-top: $s-4; + } + + .padding-section { + margin-top: $s-8; + } + + .grid-tracks-row { + @include flexColumn; + margin: $s-8 0; + gap: $s-12; + } + + .edit-grid-wrapper { + @include flexRow; + } +} + +.track-info { + display: flex; + + &.dnd-over-top { + border-top: $s-2 solid var(--button-foreground-hover); + } + + &.dnd-over-bot { + border-bottom: $s-2 solid var(--button-foreground-hover); + } + + .track-info-container { + display: flex; + } + + .track-info-dir-icon { + cursor: pointer; + border-radius: $br-8 0 0 $br-8; + background-color: var(--input-background-color); + padding: 0 $s-8; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + height: 100%; + } + &:hover svg { + stroke: var(--icon-foreground-hover); + } + } + + .track-info-value { + @extend .input-element; + border-radius: 0; + border-right: $s-1 solid var(--panel-background-color); + } + + .track-info-unit-selector { + border-radius: 0 $br-8 $br-8 0; + width: $s-96; + } + + .remove-track-btn { + @extend .button-tertiary; + padding: $s-8; + + svg { + @extend .button-icon; + width: $s-12; + height: $s-12; + stroke: var(--icon-foreground); + fill: var(--icon-foreground); + } + } +} + +.grid-tracks { + width: 100%; + margin-top: $s-8; + + .grid-track-header { + @include flexRow; + font-size: $fs-12; + border-radius: $br-8; + overflow: hidden; + background: var(--button-secondary-background-color-rest); + height: $s-52; + } + + .track-title { + @include flexColumn; + flex-grow: 1; + padding: $s-8; + gap: 0; + overflow: hidden; + } + + .track-name { + color: $df-primary; + } + + .track-detail { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + width: 100%; + color: $df-secondary; + } + + .expand-icon { + @extend .button-secondary; + height: $s-52; + + border-radius: $s-8 0 0 $s-8; + border-right: $s-1 solid var(--panel-background-color); + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + fill: var(--icon-foreground); + } + &:hover, + &:active { + svg { + stroke: var(--button-foreground-hover); + fill: var(--button-foreground-hover); + } + } + } + + .columns-info { + } + + .add-column { + @extend .button-tertiary; + height: $s-52; + + svg { + @extend .button-icon; + height: $s-12; + width: $s-12; + stroke: var(--icon-foreground); + fill: var(--icon-foreground); + } + } +} + +.locate-button, +.help-button { + @extend .button-tertiary; + padding: $s-8; + svg { + fill: none; + width: $s-16; + height: $s-16; + } +} + +.layout-options { + @extend .dropdown-wrapper; + @include flexColumn; + right: 0; + left: initial; + + button { + @include buttonStyle; + padding: $s-8; + color: $df-primary; + border-radius: $br-6; + + &:hover { + background: $db-quaternary; + } + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs index e59c05c163..5092d025bf 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs @@ -5,15 +5,17 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.layout-item + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] - [app.common.data.macros :as dm] [app.common.types.shape.layout :as ctl] [app.main.data.workspace :as udw] [app.main.data.workspace.shape-layout :as dwsl] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.numeric-input :refer [numeric-input*]] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.menus.layout-container :refer [get-layout-flex-icon]] [app.util.dom :as dom] @@ -33,282 +35,543 @@ :layout-item-absolute :layout-item-z-index]) +(defn- select-margins + [m1? m2? m3? m4?] + (st/emit! (udw/set-margins-selected {:m1 m1? :m2 m2? :m3 m3? :m4 m4?}))) + +(defn- select-margin + [prop] + (select-margins (= prop :m1) (= prop :m2) (= prop :m3) (= prop :m4))) + +(mf/defc margin-simple + {::mf/props :obj} + [{:keys [value on-change on-blur]}] + (let [m1 (:m1 value) + m2 (:m2 value) + m3 (:m3 value) + m4 (:m4 value) + + m1 (when (and (not= value :multiple) (= m1 m3)) m1) + m2 (when (and (not= value :multiple) (= m2 m4)) m2) + + on-focus + (mf/use-fn + (fn [event] + (let [attr (-> (dom/get-current-target event) + (dom/get-data "name") + (keyword))] + (case attr + :m1 (select-margins true false true false) + :m2 (select-margins false true false true)) + + (dom/select-target event)))) + + on-change' + (mf/use-fn + (mf/deps on-change) + (fn [value event] + (let [attr (-> (dom/get-current-target event) + (dom/get-data "name") + (keyword))] + (on-change :simple attr value))))] + + + [:div {:class (stl/css :margin-simple)} + [:div {:class (stl/css :vertical-margin) + :title "Vertical margin"} + [:span {:class (stl/css :icon)} + i/margin-top-bottom] + [:> numeric-input* {:class (stl/css :numeric-input) + :placeholder "--" + :data-name "m1" + :on-focus on-focus + :on-change on-change' + :on-blur on-blur + :nillable true + :value m1}]] + + [:div {:class (stl/css :horizontal-margin) + :title "Horizontal margin"} + [:span {:class (stl/css :icon)} + i/margin-left-right] + [:> numeric-input* {:class (stl/css :numeric-input) + :placeholder "--" + :data-name "m2" + :on-focus on-focus + :on-change on-change' + :on-blur on-blur + :nillable true + :value m2}]]])) + +(mf/defc margin-multiple + {::mf/props :obj} + [{:keys [value on-change on-blur]}] + (let [m1 (:m1 value) + m2 (:m2 value) + m3 (:m3 value) + m4 (:m4 value) + + on-focus + (mf/use-fn + (fn [event] + (let [attr (-> (dom/get-current-target event) + (dom/get-data "name") + (keyword))] + (select-margin attr) + (dom/select-target event)))) + + on-change' + (mf/use-fn + (mf/deps on-change) + (fn [value event] + (let [attr (-> (dom/get-current-target event) + (dom/get-data "name") + (keyword))] + (on-change :multiple attr value))))] + + [:div {:class (stl/css :margin-multiple)} + [:div {:class (stl/css :top-margin) + :title "Top margin"} + [:span {:class (stl/css :icon)} + i/margin-top] + [:> numeric-input* {:class (stl/css :numeric-input) + :placeholder "--" + :data-name "m1" + :on-focus on-focus + :on-change on-change' + :on-blur on-blur + :nillable true + :value m1}]] + [:div {:class (stl/css :right-margin) + :title "Right margin"} + [:span {:class (stl/css :icon)} + i/margin-right] + [:> numeric-input* {:class (stl/css :numeric-input) + :placeholder "--" + :data-name "m2" + :on-focus on-focus + :on-change on-change' + :on-blur on-blur + :nillable true + :value m2}]] + + [:div {:class (stl/css :bottom-margin) + :title "Bottom margin"} + [:span {:class (stl/css :icon)} + i/margin-bottom] + [:> numeric-input* {:class (stl/css :numeric-input) + :placeholder "--" + :data-name "m3" + :on-focus on-focus + :on-change on-change' + :on-blur on-blur + :nillable true + :value m3}]] + + [:div {:class (stl/css :left-margin) + :title "Left margin"} + [:span {:class (stl/css :icon)} + i/margin-left] + [:> numeric-input* {:class (stl/css :numeric-input) + :placeholder "--" + :data-name "m4" + :on-focus on-focus + :on-change on-change' + :on-blur on-blur + :nillable true + :value m4}]]])) + + (mf/defc margin-section - [{:keys [values change-margin-style on-margin-change] :as props}] + {::mf/props :obj + ::mf/private true + ::mf/expect-props #{:value :type :on-type-change :on-change}} + [{:keys [type on-type-change] :as props}] + (let [type (d/nilv type :simple) + on-blur (mf/use-fn #(select-margins false false false false)) + props (mf/spread props :on-blur on-blur) - (let [margin-type (or (:layout-item-margin-type values) :simple) - m1 (if (and (not (= :multiple (:layout-item-margin values))) - (= (dm/get-in values [:layout-item-margin :m1]) - (dm/get-in values [:layout-item-margin :m3]))) - (dm/get-in values [:layout-item-margin :m1]) - "--") + on-type-change' + (mf/use-fn + (mf/deps type on-type-change) + (fn [_] + (if (= type :multiple) + (on-type-change :simple) + (on-type-change :multiple))))] - m2 (if (and (not (= :multiple (:layout-item-margin values))) - (= (dm/get-in values [:layout-item-margin :m2]) - (dm/get-in values [:layout-item-margin :m4]))) - (dm/get-in values [:layout-item-margin :m2]) - "--") - select-margins - (fn [m1? m2? m3? m4?] - (st/emit! (udw/set-margins-selected {:m1 m1? :m2 m2? :m3 m3? :m4 m4?}))) + (mf/with-effect [] + (fn [] (on-blur))) - select-margin #(select-margins (= % :m1) (= % :m2) (= % :m3) (= % :m4))] + [:div {:class (stl/css :margin-row)} + [:div {:class (stl/css :inputs-wrapper)} + (cond + (= type :simple) + [:> margin-simple props] - (mf/use-effect - (fn [] - (fn [] - ;;on destroy component - (select-margins false false false false)))) + (= type :multiple) + [:> margin-multiple props])] - [:div.margin-row - (cond - (= margin-type :simple) + [:button {:class (stl/css-case + :margin-mode true + :selected (= type :multiple)) + :title "Margin - multiple" + :on-click on-type-change'} + i/margin]])) - [:div.margin-item-group - [:div.margin-item.tooltip.tooltip-bottom-left - {:alt "Vertical margin"} - [:span.icon i/auto-margin-both-sides] - [:> numeric-input - {:placeholder "--" - :on-focus (fn [event] - (select-margins true false true false) - (dom/select-target event)) - :on-change (partial on-margin-change :simple :m1) - :on-blur #(select-margins false false false false) - :value m1}]] +(mf/defc element-behaviour-horizontal + {::mf/props :obj + ::mf/private true} + [{:keys [^boolean is-auto ^boolean has-fill value on-change]}] + [:div {:class (stl/css-case + :horizontal-behaviour true + :one-element (and (not has-fill) (not is-auto)) + :two-element (or has-fill is-auto) + :three-element (and has-fill is-auto))} + [:& radio-buttons + {:selected (d/name value) + :decode-fn keyword + :on-change on-change + :wide true + :name "flex-behaviour-h"} - [:div.margin-item.tooltip.tooltip-bottom-left - {:alt "Horizontal margin"} - [:span.icon.rotated i/auto-margin-both-sides] - [:> numeric-input - {:placeholder "--" - :on-focus (fn [event] - (select-margins false true false true) - (dom/select-target event)) - :on-change (partial on-margin-change :simple :m2) - :on-blur #(select-margins false false false false) - :value m2}]]] + [:& radio-button + {:value "fix" + :icon i/fixed-width + :title "Fix width" + :id "behaviour-h-fix"}] - (= margin-type :multiple) - [:div.wrapper - (for [num [:m1 :m2 :m3 :m4]] - [:div.tooltip.tooltip-bottom - {:key (dm/str "margin-" (d/name num)) - :alt (case num - :m1 "Top" - :m2 "Right" - :m3 "Bottom" - :m4 "Left")} - [:div.input-element.auto - [:> numeric-input - {:placeholder "--" - :on-focus (fn [event] - (select-margin num) - (dom/select-target event)) - :on-change (partial on-margin-change :multiple num) - :on-blur #(select-margins false false false false) - :value (num (:layout-item-margin values))}]]])]) + (when has-fill + [:& radio-button + {:value "fill" + :icon i/fill-content + :title "Width 100%" + :id "behaviour-h-fill"}]) + (when is-auto + [:& radio-button + {:value "auto" + :icon i/hug-content + :title "Fit content" + :id "behaviour-h-auto"}])]]) - [:div.margin-item-icons - [:div.margin-item-icon.tooltip.tooltip-bottom-left - {:class (dom/classnames :selected (= margin-type :multiple)) - :alt "Margin - multiple" - :on-click #(change-margin-style (if (= margin-type :multiple) :simple :multiple))} - i/auto-margin]]])) +(mf/defc element-behaviour-vertical + {::mf/props :obj + ::mf/private true} + [{:keys [^boolean is-auto ^boolean has-fill value on-change]}] + [:div {:class (stl/css-case + :vertical-behaviour true + :one-element (and (not has-fill) (not is-auto)) + :two-element (or has-fill is-auto) + :three-element (and has-fill is-auto))} + [:& radio-buttons + {:selected (d/name value) + :decode-fn keyword + :on-change on-change + :wide true + :name "flex-behaviour-v"} -(mf/defc element-behavior - [{:keys [is-layout-container? is-layout-child? layout-item-h-sizing layout-item-v-sizing on-change-behavior] :as props}] - (let [fill? is-layout-child? - auto? is-layout-container?] - - [:div.btn-wrapper - {:class (when (and fill? auto?) "wrap")} - [:div.layout-behavior.horizontal - [:button.behavior-btn.tooltip.tooltip-bottom - {:alt "Fix width" - :class (dom/classnames :active (= layout-item-h-sizing :fix)) - :on-click #(on-change-behavior :h :fix)} - i/auto-fix-layout] - (when fill? - [:button.behavior-btn.tooltip.tooltip-bottom - {:alt "Width 100%" - :class (dom/classnames :active (= layout-item-h-sizing :fill)) - :on-click #(on-change-behavior :h :fill)} - i/auto-fill]) - (when auto? - [:button.behavior-btn.tooltip.tooltip-bottom - {:alt "Fit content" - :class (dom/classnames :active (= layout-item-h-sizing :auto)) - :on-click #(on-change-behavior :h :auto)} - i/auto-hug])] - - [:div.layout-behavior - [:button.behavior-btn.tooltip.tooltip-bottom - {:alt "Fix height" - :class (dom/classnames :active (= layout-item-v-sizing :fix)) - :on-click #(on-change-behavior :v :fix)} - i/auto-fix-layout] - (when fill? - [:button.behavior-btn.tooltip.tooltip-bottom-left - {:alt "Height 100%" - :class (dom/classnames :active (= layout-item-v-sizing :fill)) - :on-click #(on-change-behavior :v :fill)} - i/auto-fill]) - (when auto? - [:button.behavior-btn.tooltip.tooltip-bottom-left - {:alt "Fit content" - :class (dom/classnames :active (= layout-item-v-sizing :auto)) - :on-click #(on-change-behavior :v :auto)} - i/auto-hug])]])) + [:& radio-button + {:value "fix" + :icon i/fixed-width + :icon-class (stl/css :rotated) + :title "Fix height" + :id "behaviour-v-fix"}] + (when has-fill + [:& radio-button + {:value "fill" + :icon i/fill-content + :icon-class (stl/css :rotated) + :title "Height 100%" + :id "behaviour-v-fill"}]) + (when is-auto + [:& radio-button + {:value "auto" + :icon i/hug-content + :icon-class (stl/css :rotated) + :title "Fit content" + :id "behaviour-v-auto"}])]]) (mf/defc align-self-row - [{:keys [is-col? align-self set-align-self] :as props}] - (let [dir-v [:start :center :end #_:stretch #_:baseline]] - [:div.align-self-style - (for [align dir-v] - [:button.align-self.tooltip.tooltip-bottom - {:class (dom/classnames :active (= align-self align) - :tooltip-bottom-left (not= align :start) - :tooltip-bottom (= align :start)) - :alt (dm/str "Align self " (d/name align)) - :on-click #(set-align-self align) - :key (str "align-self" align)} - (get-layout-flex-icon :align-self align is-col?)])])) + {::mf/props :obj} + [{:keys [^boolean is-col value on-change]}] + [:& radio-buttons {:selected (d/name value) + :decode-fn keyword + :on-change on-change + :name "flex-align-self" + :allow-empty true} + [:& radio-button {:value "start" + :icon (get-layout-flex-icon :align-self :start is-col) + :title "Align self start" + :id "align-self-start"}] + [:& radio-button {:value "center" + :icon (get-layout-flex-icon :align-self :center is-col) + :title "Align self center" + :id "align-self-center"}] + [:& radio-button {:value "end" + :icon (get-layout-flex-icon :align-self :end is-col) + :title "Align self end" + :id "align-self-end"}]]) (mf/defc layout-item-menu - {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "is-layout-child?"]))]} - [{:keys [ids values is-layout-child? is-layout-container?] :as props}] + {::mf/memo #{:ids :values :type :is-layout-child? :is-grid-parent :is-flex-parent?} + ::mf/props :obj} + [{:keys [ids values + ^boolean is-layout-child? + ^boolean is-layout-container? + ^boolean is-grid-parent? + ^boolean is-flex-parent? + ^boolean is-flex-layout? + ^boolean is-grid-layout?]}] - (let [selection-parents-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) - selection-parents (mf/deref selection-parents-ref) + (let [selection-parents* (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) + selection-parents (mf/deref selection-parents*) - change-margin-style - (fn [type] - (st/emit! (dwsl/update-layout-child ids {:layout-item-margin-type type}))) + ^boolean + is-absolute? (:layout-item-absolute values) + ^boolean + is-col? (every? ctl/col? selection-parents) + + ^boolean + is-layout-child? (and is-layout-child? (not is-absolute?)) + + state* (mf/use-state true) + open? (deref state*) + + toggle-content (mf/use-fn #(swap! state* not)) + has-content? (or is-layout-child? + is-flex-parent? + is-grid-parent? + is-layout-container?) + + ;; Align self align-self (:layout-item-align-self values) - set-align-self (fn [value] - (if (= align-self value) - (st/emit! (dwsl/update-layout-child ids {:layout-item-align-self nil})) - (st/emit! (dwsl/update-layout-child ids {:layout-item-align-self value})))) - - is-absolute? (:layout-item-absolute values) + h-sizing (:layout-item-h-sizing values) + v-sizing (:layout-item-v-sizing values) - is-col? (every? ctl/col? selection-parents) + title + (cond + (and is-layout-container? + is-flex-layout? + (not is-layout-child?)) + "Flex board" + + (and is-layout-container? + is-grid-layout? + (not is-layout-child?)) + "Grid board" + + (and is-layout-container? + (not is-layout-child?)) + "Layout board" + + is-flex-parent? + "Flex element" + + is-grid-parent? + "Grid element" + + :else + "Layout element") + + on-align-self-change + (mf/use-fn + (mf/deps ids align-self) + (fn [value] + (if (= align-self value) + (st/emit! (dwsl/update-layout-child ids {:layout-item-align-self nil})) + (st/emit! (dwsl/update-layout-child ids {:layout-item-align-self value}))))) + + ;; Margin + on-margin-type-change + (mf/use-fn + (mf/deps ids) + (fn [type] + (st/emit! (dwsl/update-layout-child ids {:layout-item-margin-type type})))) on-margin-change - (fn [type prop val] - (cond - (and (= type :simple) (= prop :m1)) - (st/emit! (dwsl/update-layout-child ids {:layout-item-margin {:m1 val :m3 val}})) + (mf/use-fn + (mf/deps ids) + (fn [type prop val] + (cond + (and (= type :simple) (= prop :m1)) + (st/emit! (dwsl/update-layout-child ids {:layout-item-margin {:m1 val :m3 val}})) - (and (= type :simple) (= prop :m2)) - (st/emit! (dwsl/update-layout-child ids {:layout-item-margin {:m2 val :m4 val}})) + (and (= type :simple) (= prop :m2)) + (st/emit! (dwsl/update-layout-child ids {:layout-item-margin {:m2 val :m4 val}})) - :else - (st/emit! (dwsl/update-layout-child ids {:layout-item-margin {prop val}})))) + :else + (st/emit! (dwsl/update-layout-child ids {:layout-item-margin {prop val}}))))) - on-change-behavior - (fn [dir value] - (if (= dir :h) - (st/emit! (dwsl/update-layout-child ids {:layout-item-h-sizing value})) - (st/emit! (dwsl/update-layout-child ids {:layout-item-v-sizing value})))) + ;; Behaviour + on-behaviour-h-change + (mf/use-fn + (mf/deps ids) + (fn [value] + (st/emit! (dwsl/update-layout-child ids {:layout-item-h-sizing value})))) + on-behaviour-v-change + (mf/use-fn + (mf/deps ids) + (fn [value] + (st/emit! (dwsl/update-layout-child ids {:layout-item-v-sizing value})))) + + ;; Size and position on-size-change - (fn [measure value] - (st/emit! (dwsl/update-layout-child ids {measure value}))) + (mf/use-fn + (mf/deps ids) + (fn [value event] + (let [attr (-> (dom/get-current-target event) + (dom/get-data "attr") + (keyword))] + (st/emit! (dwsl/update-layout-child ids {attr value}))))) on-change-position - (fn [value] - (when (= value :static) - (st/emit! (dwsl/update-layout-child ids {:layout-item-z-index nil}))) - (st/emit! (dwsl/update-layout-child ids {:layout-item-absolute (= value :absolute)}))) + (mf/use-fn + (mf/deps ids) + (fn [value] + (when (= value :static) + (st/emit! (dwsl/update-layout-child ids {:layout-item-z-index nil}))) + (st/emit! (dwsl/update-layout-child ids {:layout-item-absolute (= value :absolute)})))) + ;; Z Index on-change-z-index - (fn [value] - (st/emit! (dwsl/update-layout-child ids {:layout-item-z-index value}))) + (mf/use-fn + (mf/deps ids) + (fn [value] + (st/emit! (dwsl/update-layout-child ids {:layout-item-z-index value}))))] - is-layout-child? (and is-layout-child? (not is-absolute?))] + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-title)} + [:& title-bar {:collapsable has-content? + :collapsed (not open?) + :on-collapsed toggle-content + :title title + :class (stl/css-case :title-spacing-layout-element true + :title-spacing-empty (not has-content?))}]] + (when open? + [:div {:class (stl/css :flex-element-menu)} + (when (or is-layout-child? is-absolute?) + [:div {:class (stl/css :row)} + [:div {:class (stl/css :position-options)} + [:& radio-buttons {:selected (if is-absolute? "absolute" "static") + :decode-fn keyword + :on-change on-change-position + :name "layout-style" + :wide true} + [:& radio-button {:value "static" + :id :static-position}] + [:& radio-button {:value "absolute" + :id :absolute-position}]]] - [:div.element-set - [:div.element-set-title - [:span (if (and is-layout-container? (not is-layout-child?)) - "Flex board" - "Flex element")]] + [:div {:class (stl/css :z-index-wrapper) + :title "z-index"} - [:div.element-set-content.layout-item-menu - (when (or is-layout-child? is-absolute?) - [:div.layout-row - [:div.row-title.sizing "Position"] - [:div.btn-wrapper - [:div.absolute - [:button.behavior-btn.tooltip.tooltip-bottom - {:alt "Static" - :class (dom/classnames :active (not (:layout-item-absolute values))) - :on-click #(on-change-position :static)} - "Static"] - [:button.behavior-btn.tooltip.tooltip-bottom - {:alt "Absolute" - :class (dom/classnames :active (and (:layout-item-absolute values) (not= :multiple (:layout-item-absolute values)))) - :on-click #(on-change-position :absolute)} - "Absolute"]] + [:span {:class (stl/css :icon-text)} "Z"] + [:> numeric-input* + {:class (stl/css :numeric-input) + :placeholder "--" + :on-focus #(dom/select-target %) + :on-change #(on-change-z-index %) + :nillable true + :value (:layout-item-z-index values)}]]]) - [:div.tooltip.tooltip-bottom-left.z-index {:alt "z-index"} - i/layers - [:> numeric-input - {:placeholder "--" - :on-focus #(dom/select-target %) - :on-change #(on-change-z-index %) - :nillable true - :value (:layout-item-z-index values)}]]]]) + [:div {:class (stl/css :row)} + [:div {:class (stl/css-case + :behaviour-menu true + :wrap (and ^boolean is-layout-child? + ^boolean is-layout-container?))} + [:& element-behaviour-horizontal + {:is-auto is-layout-container? + :has-fill is-layout-child? + :value (:layout-item-h-sizing values) + :on-change on-behaviour-h-change}] + [:& element-behaviour-vertical + {:is-auto is-layout-container? + :has-fill is-layout-child? + :value (:layout-item-v-sizing values) + :on-change on-behaviour-v-change}]]] - [:* - [:div.layout-row - [:div.row-title.sizing "Sizing"] - [:& element-behavior {:is-layout-child? is-layout-child? - :is-layout-container? is-layout-container? - :layout-item-v-sizing (or (:layout-item-v-sizing values) :fix) - :layout-item-h-sizing (or (:layout-item-h-sizing values) :fix) - :on-change-behavior on-change-behavior}]] + (when (and is-layout-child? is-flex-parent?) + [:div {:class (stl/css :row)} + [:& align-self-row {:is-col is-col? + :value align-self + :on-change on-align-self-change}]]) - (when is-layout-child? - [:div.layout-row - [:div.row-title "Align"] - [:div.btn-wrapper - [:& align-self-row {:is-col? is-col? - :align-self align-self - :set-align-self set-align-self}]]]) + (when is-layout-child? + [:div {:class (stl/css :row)} + [:& margin-section {:value (:layout-item-margin values) + :type (:layout-item-margin-type values) + :on-type-change on-margin-type-change + :on-change on-margin-change}]]) - (when is-layout-child? - [:& margin-section {:values values - :change-margin-style change-margin-style - :on-margin-change on-margin-change}]) + (when (or (= h-sizing :fill) + (= v-sizing :fill)) + [:div {:class (stl/css :row)} + [:div {:class (stl/css :advanced-options)} + (when (= (:layout-item-h-sizing values) :fill) + [:div {:class (stl/css :horizontal-fill)} + [:div {:class (stl/css :layout-item-min-w) + :title (tr "workspace.options.layout-item.layout-item-min-w")} - [:div.advanced-ops-body - [:div.input-wrapper - (for [item (cond-> [] - (= (:layout-item-h-sizing values) :fill) - (conj :layout-item-min-w :layout-item-max-w) + [:span {:class (stl/css :icon-text)} "MIN W"] + [:> numeric-input* + {:class (stl/css :numeric-input) + :no-validate true + :min 0 + :data-wrap true + :placeholder "--" + :data-attr "layout-item-min-w" + :on-focus dom/select-target + :on-change on-size-change + :value (get values :layout-item-min-w) + :nillable true}]] - (= (:layout-item-v-sizing values) :fill) - (conj :layout-item-min-h :layout-item-max-h))] + [:div {:class (stl/css :layout-item-max-w) + :title (tr "workspace.options.layout-item.layout-item-max-w")} + [:span {:class (stl/css :icon-text)} "MAX W"] + [:> numeric-input* + {:class (stl/css :numeric-input) + :no-validate true + :min 0 + :data-wrap true + :placeholder "--" + :data-attr "layout-item-max-w" + :on-focus dom/select-target + :on-change on-size-change + :value (get values :layout-item-max-w) + :nillable true}]]]) - [:div.tooltip.tooltip-bottom - {:key (d/name item) - :alt (tr (dm/str "workspace.options.layout-item.title." (d/name item))) - :class (dom/classnames "maxH" (= item :layout-item-max-h) - "minH" (= item :layout-item-min-h) - "maxW" (= item :layout-item-max-w) - "minW" (= item :layout-item-min-w))} - [:div.input-element - {:alt (tr (dm/str "workspace.options.layout-item." (d/name item)))} - [:> numeric-input - {:no-validate true - :min 0 - :data-wrap true - :placeholder "--" - :on-focus #(dom/select-target %) - :on-change (partial on-size-change item) - :value (get values item) - :nillable true}]]])]]]]])) + (when (= v-sizing :fill) + [:div {:class (stl/css :vertical-fill)} + [:div {:class (stl/css :layout-item-min-h) + :title (tr "workspace.options.layout-item.layout-item-min-h")} + + [:span {:class (stl/css :icon-text)} "MIN H"] + [:> numeric-input* + {:class (stl/css :numeric-input) + :no-validate true + :min 0 + :data-wrap true + :placeholder "--" + :data-attr "layout-item-min-h" + :on-focus dom/select-target + :on-change on-size-change + :value (get values :layout-item-min-h) + :nillable true}]] + + [:div {:class (stl/css :layout-item-max-h) + :title (tr "workspace.options.layout-item.layout-item-max-h")} + + [:span {:class (stl/css :icon-text)} "MAX H"] + [:> numeric-input* + {:class (stl/css :numeric-input) + :no-validate true + :min 0 + :data-wrap true + :placeholder "--" + :data-attr "layout-item-max-h" + :on-focus dom/select-target + :on-change on-size-change + :value (get values :layout-item-max-h) + :nillable true}]]])]])])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.scss new file mode 100644 index 0000000000..26a6d6d993 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.scss @@ -0,0 +1,136 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + margin: 0; +} + +.title-spacing-layout-element { + margin: 0 0 $s-4 0; +} + +.title-spacing-empty { + padding-left: $s-2; +} + +.flex-element-menu { + @include flexColumn; + gap: $s-12; + margin-block-end: $s-8; +} + +.behaviour-menu { + display: flex; + gap: $s-4; +} + +.horizontal-behaviour { + &.one-element { + width: $s-28; + } + &.two-element { + width: $s-60; + } + &.three-element { + width: $s-92; + } +} + +.vertical-behaviour { + .rotated { + transform: rotate(90deg); + } + &.one-element { + width: $s-28; + } + &.two-element { + width: $s-60; + } + &.three-element { + width: $s-92; + } +} + +.z-index-wrapper { + @extend .input-element; + width: $s-60; +} + +.row { + display: flex; + gap: $s-4; +} + +.position-options { + width: $s-188; +} + +.margin-row { + display: flex; + align-items: flex-start; + gap: $s-4; +} + +.margin-mode { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } + &.selected { + @extend .button-icon-selected; + } +} + +.margin-simple { + display: flex; + gap: $s-4; + .vertical-margin, + .horizontal-margin { + @extend .input-element; + width: $s-108; + } +} + +.margin-multiple { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $s-4; +} + +.top-margin, +.bottom-margin, +.left-margin, +.right-margin { + @extend .input-element; + width: $s-108; +} + +.advanced-options { + @include flexColumn; +} + +.horizontal-fill, +.vertical-fill { + display: flex; + gap: $s-4; +} + +.layout-item-min-w, +.layout-item-min-h, +.layout-item-max-w, +.layout-item-max-h { + @extend .input-element; + width: $s-108; + .icon-text { + justify-content: flex-start; + width: $s-80; + padding-top: $s-2; + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index 7c245ed051..13c4ffe534 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.measures + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.geom.shapes :as gsh] @@ -18,7 +19,9 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.numeric-input :refer [numeric-input*]] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -49,6 +52,11 @@ :svg-raw #{:size :position :rotation} :text #{:size :position :rotation}}) +(def ^:private clip-content-icon (i/icon-xref :clip-content (stl/css :checkbox-button))) +(def ^:private play-icon (i/icon-xref :play (stl/css :checkbox-button))) +(def ^:private locked-icon (i/icon-xref :detach (stl/css :lock-ratio-icon))) +(def ^:private unlocked-icon (i/icon-xref :detached (stl/css :lock-ratio-icon))) + (defn select-measure-keys "Consider some shapes can be drawn from bottom to top or from left to right" [shape] @@ -68,7 +76,9 @@ ;; -- User/drawing coords (mf/defc measures-menu - [{:keys [ids ids-with-children values type all-types shape] :as props}] + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [ids ids-with-children values type all-types shape]}] (let [options (if (= type :multiple) (reduce #(union %1 %2) (map #(get type->options %) all-types)) (get type->options type)) @@ -80,19 +90,21 @@ [shape]) frames (map #(deref (refs/object-by-id (:frame-id %))) old-shapes) + ids (hooks/use-equal-memo ids) + selection-parents-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) selection-parents (mf/deref selection-parents-ref) - flex-child? (->> selection-parents (some ctl/flex-layout?)) - absolute? (ctl/layout-absolute? shape) - flex-container? (ctl/flex-layout? shape) - flex-auto-width? (ctl/auto-width? shape) - flex-fill-width? (ctl/fill-width? shape) + flex-child? (->> selection-parents (some ctl/flex-layout?)) + absolute? (ctl/item-absolute? shape) + flex-container? (ctl/flex-layout? shape) + flex-auto-width? (ctl/auto-width? shape) + flex-fill-width? (ctl/fill-width? shape) flex-auto-height? (ctl/auto-height? shape) flex-fill-height? (ctl/fill-height? shape) - disabled-position-x? (and flex-child? (not absolute?)) - disabled-position-y? (and flex-child? (not absolute?)) + disabled-position-x? (and flex-child? (not absolute?)) + disabled-position-y? (and flex-child? (not absolute?)) disabled-width-sizing? (and (or flex-child? flex-container?) (or flex-auto-width? flex-fill-width?) (not absolute?)) @@ -109,7 +121,7 @@ ;; For rotated or stretched shapes, the origin point we show in the menu ;; is not the (:x :y) shape attribute, but the top left coordinate of the ;; wrapping rectangle. - values (let [{:keys [x y]} (gsh/selection-rect [(first shapes)])] + values (let [{:keys [x y]} (gsh/shapes->rect [(first shapes)])] (cond-> values (not= (:x values) :multiple) (assoc :x x) (not= (:y values) :multiple) (assoc :y y) @@ -135,9 +147,9 @@ (cond-> values (not= (:rotation values) :multiple) (assoc :rotation rotation))) - proportion-lock (:proportion-lock values) + proportion-lock (:proportion-lock values) + - show-presets-dropdown? (mf/use-state false) radius-mode (ctsr/radius-mode values) all-equal? (ctsr/all-equal? values) @@ -147,49 +159,94 @@ clip-content-ref (mf/use-ref nil) show-in-viewer-ref (mf/use-ref nil) - on-preset-selected - (fn [width height] - (st/emit! (udw/update-dimensions ids :width width) - (udw/update-dimensions ids :height height))) + ;; PRESETS + preset-state* (mf/use-state false) + show-presets-dropdown? (deref preset-state*) - on-orientation-clicked - (fn [orientation] - (st/emit! (udw/change-orientation ids orientation))) + open-presets + (mf/use-fn + (mf/deps show-presets-dropdown?) + (fn [] + (reset! preset-state* true))) + + close-presets + (mf/use-fn + (mf/deps show-presets-dropdown?) + (fn [] + (reset! preset-state* false))) + + on-preset-selected + (mf/use-fn + (mf/deps ids) + (fn [event] + (let [width (-> (dom/get-current-target event) + (dom/get-data "width") + (d/read-string)) + height (-> (dom/get-current-target event) + (dom/get-data "height") + (d/read-string))] + (st/emit! (udw/update-dimensions ids :width width) + (udw/update-dimensions ids :height height))))) + + ;; ORIENTATION + + orientation (when (= type :frame) + (cond (> (:width values) (:height values)) + :horiz + :else + :vert)) + + on-orientation-change + (mf/use-fn + (mf/deps ids) + (fn [orientation] + (st/emit! (udw/change-orientation ids (keyword orientation))))) + + ;; SIZE AND PROPORTION LOCK on-size-change - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [value attr] - (st/emit! (udw/update-dimensions ids attr value)))) + (st/emit! (udw/trigger-bounding-box-cloaking ids) + (udw/update-dimensions ids attr value)))) on-proportion-lock-change - (mf/use-callback - (mf/deps ids) + (mf/use-fn + (mf/deps ids proportion-lock) (fn [_] (let [new-lock (if (= proportion-lock :multiple) true (not proportion-lock))] (run! #(st/emit! (udw/set-shape-proportion-lock % new-lock)) ids)))) + ;; POSITION + do-position-change - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [shape' frame' value attr] (let [to (+ value (attr frame'))] (st/emit! (udw/update-position (:id shape') {attr to}))))) on-position-change - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [value attr] + (st/emit! (udw/trigger-bounding-box-cloaking ids)) (doall (map #(do-position-change %1 %2 value attr) shapes frames)))) + ;; ROTATION + on-rotation-change - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [value] - (st/emit! (udw/increase-rotation ids value)))) + (st/emit! (udw/trigger-bounding-box-cloaking ids) + (udw/increase-rotation ids value)))) + + ;; RADIUS change-radius - (mf/use-callback + (mf/use-fn (mf/deps ids-with-children) (fn [update-fn] (dch/update-shapes ids-with-children @@ -201,29 +258,37 @@ :attrs [:rx :ry :r1 :r2 :r3 :r4]}))) on-switch-to-radius-1 - (mf/use-callback - (mf/deps ids) + (mf/use-fn + (mf/deps ids change-radius) (fn [_value] (if all-equal? (st/emit! (change-radius ctsr/switch-to-radius-1)) (reset! radius-multi? true)))) on-switch-to-radius-4 - (mf/use-callback - (mf/deps ids) + (mf/use-fn + (mf/deps ids change-radius) (fn [_value] (st/emit! (change-radius ctsr/switch-to-radius-4)) (reset! radius-multi? false))) + toggle-radius-mode + (mf/use-fn + (mf/deps radius-mode) + (fn [] + (if (= :radius-1 radius-mode) + (on-switch-to-radius-4) + (on-switch-to-radius-1)))) + on-radius-1-change - (mf/use-callback - (mf/deps ids) + (mf/use-fn + (mf/deps ids change-radius) (fn [value] (st/emit! (change-radius #(ctsr/set-radius-1 % value))))) on-radius-multi-change - (mf/use-callback - (mf/deps ids) + (mf/use-fn + (mf/deps ids change-radius) (fn [event] (let [value (-> event dom/get-target dom/get-value d/parse-integer)] (when (some? value) @@ -232,8 +297,8 @@ (reset! radius-multi? false))))) on-radius-4-change - (mf/use-callback - (mf/deps ids) + (mf/use-fn + (mf/deps ids change-radius) (fn [value attr] (st/emit! (change-radius #(ctsr/set-radius-4 % attr value))))) @@ -246,15 +311,16 @@ on-radius-r3-change #(on-radius-4-change % :r3) on-radius-r4-change #(on-radius-4-change % :r4) + ;; CLIP CONTENT AND SHOW IN VIEWER on-change-clip-content - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [event] (let [value (-> event dom/get-target dom/checked?)] (st/emit! (dch/update-shapes ids (fn [shape] (assoc shape :show-content (not value)))))))) on-change-show-in-viewer - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [event] (let [value (-> event dom/get-target dom/checked?) @@ -279,178 +345,222 @@ ;; restore focus to the newly created numeric-input (let [radius-input (mf/ref-val radius-input-ref)] (dom/focus! radius-input))))) + [:div {:class (stl/css :element-set)} + (when (and (options :presets) + (or (nil? all-types) (= (count all-types) 1))) + [:div {:class (stl/css :presets)} + [:div {:class (stl/css-case :presets-wrapper true + :opened show-presets-dropdown?) + :on-click open-presets} + [:span {:class (stl/css :select-name)} (tr "workspace.options.size-presets")] + [:span {:class (stl/css :collapsed-icon)} i/arrow] - [:* - [:div.element-set - [:div.element-set-content + [:& dropdown {:show show-presets-dropdown? + :on-close close-presets} + [:ul {:class (stl/css :custom-select-dropdown)} + (for [size-preset size-presets] + (if-not (:width size-preset) + [:li {:key (:name size-preset) + :class (stl/css-case :dropdown-element true + :disabled true)} + [:span {:class (stl/css :preset-name)} (:name size-preset)]] - ;; FRAME PRESETS - (when (and (options :presets) - (or (nil? all-types) (= (count all-types) 1))) ;; Don't show presets if multi selected - [:div.row-flex ;; some frames and some non frames - [:div.presets.custom-select.flex-grow {:class (when @show-presets-dropdown? "opened") - :on-click #(reset! show-presets-dropdown? true)} - [:span (tr "workspace.options.size-presets")] - [:span.dropdown-button i/arrow-down] - [:& dropdown {:show @show-presets-dropdown? - :on-close #(reset! show-presets-dropdown? false)} - [:ul.custom-select-dropdown - (for [size-preset size-presets] - (if-not (:width size-preset) - [:li.dropdown-label {:key (:name size-preset)} - [:span (:name size-preset)]] + (let [preset-match (and (= (:width size-preset) (d/parse-integer (:width values) 0)) + (= (:height size-preset) (d/parse-integer (:height values) 0)))] [:li {:key (:name size-preset) - :on-click #(on-preset-selected (:width size-preset) (:height size-preset))} - (:name size-preset) - [:span (:width size-preset) " x " (:height size-preset)]]))]]] - [:span.orientation-icon {:on-click #(on-orientation-clicked :vert)} i/size-vert] - [:span.orientation-icon {:on-click #(on-orientation-clicked :horiz)} i/size-horiz]]) + :class (stl/css-case :dropdown-element true + :match preset-match) + :data-width (str (:width size-preset)) + :data-height (str (:height size-preset)) + :on-click on-preset-selected} + [:div {:class (stl/css :name-wrapper)} + [:span {:class (stl/css :preset-name)} (:name size-preset)] + [:span {:class (stl/css :preset-size)} (:width size-preset) " x " (:height size-preset)]] + (when preset-match + [:span {:class (stl/css :check-icon)} i/tick])])))]]] - ;; WIDTH & HEIGHT - (when (options :size) - [:div.row-flex - [:span.element-set-subtitle (tr "workspace.options.size")] - [:div.input-element.width {:title (tr "workspace.options.width")} - [:> numeric-input {:min 0.01 - :no-validate true - :placeholder "--" - :on-change on-width-change - :disabled disabled-width-sizing? - :value (:width values)}]] + [:& radio-buttons {:selected (or (d/name orientation) "") + :on-change on-orientation-change + :name "frame-otientation"} + [:& radio-button {:icon i/size-vertical + :value "vert" + :id "size-vertical"}] + [:& radio-button {:icon i/size-horizontal + :value "horiz" + :id "size-horizontal"}]]]) + (when (options :size) + [:div {:class (stl/css :size)} + [:div {:class (stl/css-case :width true + :disabled disabled-width-sizing?) + :title (tr "workspace.options.width")} + [:span {:class (stl/css :icon-text)} "W"] + [:> numeric-input* {:min 0.01 + :no-validate true + :placeholder (if (= :multiple (:width values)) (tr "settings.multiple") "--") + :on-change on-width-change + :disabled disabled-width-sizing? + :className (stl/css :numeric-input) + :value (:width values)}]] + [:div {:class (stl/css-case :height true + :disabled disabled-height-sizing?) + :title (tr "workspace.options.height")} + [:span {:class (stl/css :icon-text)} "H"] + [:> numeric-input* {:min 0.01 + :no-validate true + :placeholder (if (= :multiple (:height values)) (tr "settings.multiple") "--") + :on-change on-height-change + :disabled disabled-height-sizing? + :className (stl/css :numeric-input) + :value (:height values)}]] + [:button {:class (stl/css-case + :lock-size-btn true + :selected (true? proportion-lock) + :disabled (= proportion-lock :multiple)) + :on-click on-proportion-lock-change} + (if proportion-lock + locked-icon + unlocked-icon)]]) + (when (options :position) + [:div {:class (stl/css :position)} + [:div {:class (stl/css-case :x-position true + :disabled disabled-position-x?) + :title (tr "workspace.options.x")} + [:span {:class (stl/css :icon-text)} "X"] + [:> numeric-input* {:no-validate true + :placeholder (if (= :multiple (:x values)) (tr "settings.multiple") "--") + :on-change on-pos-x-change + :disabled disabled-position-x? + :className (stl/css :numeric-input) + :value (:x values)}]] - [:div.input-element.height {:title (tr "workspace.options.height")} - [:> numeric-input {:min 0.01 - :no-validate true - :placeholder "--" - :on-change on-height-change - :disabled disabled-height-sizing? - :value (:height values)}]] + [:div {:class (stl/css-case :y-position true + :disabled disabled-position-y?) + :title (tr "workspace.options.y")} + [:span {:class (stl/css :icon-text)} "Y"] + [:> numeric-input* {:no-validate true + :placeholder (if (= :multiple (:y values)) (tr "settings.multiple") "--") + :disabled disabled-position-y? + :on-change on-pos-y-change + :className (stl/css :numeric-input) + :value (:y values)}]]]) + (when (or (options :rotation) (options :radius)) + [:div {:class (stl/css :rotation-radius)} - [:div.lock-size {:class (dom/classnames - :selected (true? proportion-lock) - :disabled (= proportion-lock :multiple)) - :on-click on-proportion-lock-change} - (if proportion-lock - i/lock - i/unlock)]]) - - ;; POSITION - (when (options :position) - [:div.row-flex - [:span.element-set-subtitle (tr "workspace.options.position")] - [:div.input-element.Xaxis {:title (tr "workspace.options.x")} - [:> numeric-input {:no-validate true - :placeholder "--" - :on-change on-pos-x-change - :disabled disabled-position-x? - :value (:x values)}]] - [:div.input-element.Yaxis {:title (tr "workspace.options.y")} - [:> numeric-input {:no-validate true - :placeholder "--" - :disabled disabled-position-y? - :on-change on-pos-y-change - :value (:y values)}]]]) - - ;; ROTATION - (when (options :rotation) - [:div.row-flex - [:span.element-set-subtitle (tr "workspace.options.rotation")] - [:div.input-element.degrees {:title (tr "workspace.options.rotation")} - [:> numeric-input + (when (options :rotation) + [:div {:class (stl/css :rotation) + :title (tr "workspace.options.rotation")} + [:span {:class (stl/css :icon)} i/rotation] + [:> numeric-input* {:no-validate true - :min 0 + :min -359 :max 359 :data-wrap true - :placeholder "--" + :placeholder (if (= :multiple (:rotation values)) (tr "settings.multiple") "--") :on-change on-rotation-change - :value (:rotation values)}]]]) + :className (stl/css :numeric-input) + :value (:rotation values)}]]) - ;; RADIUS - (when (options :radius) - [:div.row-flex - [:div.radius-options - [:div.radius-icon.tooltip.tooltip-bottom - {:class (dom/classnames - :selected (or (= radius-mode :radius-1) @radius-multi?)) - :alt (tr "workspace.options.radius.all-corners") - :on-click on-switch-to-radius-1} - i/radius-1] - [:div.radius-icon.tooltip.tooltip-bottom - {:class (dom/classnames - :selected (and (= radius-mode :radius-4) (not @radius-multi?))) - :alt (tr "workspace.options.radius.single-corners") - :on-click on-switch-to-radius-4} - i/radius-4]] + (when (options :radius) + [:div {:class (stl/css :radius)} + [:div {:class (stl/css :radius-inputs)} + (cond + (= radius-mode :radius-1) + [:div {:class (stl/css :radius-1) + :title (tr "workspace.options.radius")} + [:span {:class (stl/css :icon)} i/corner-radius] + [:> numeric-input* + {:placeholder (if (= :multiple (:rx values)) (tr "settings.multiple") "--") + :ref radius-input-ref + :min 0 + :on-change on-radius-1-change + :className (stl/css :numeric-input) + :value (:rx values)}]] - (cond - (= radius-mode :radius-1) - [:div.input-element.mini {:title (tr "workspace.options.radius")} - [:> numeric-input - {:placeholder "--" - :ref radius-input-ref - :min 0 - :on-change on-radius-1-change - :value (:rx values)}]] + @radius-multi? + [:div {:class (stl/css :radius-1) + :title (tr "workspace.options.radius")} + [:span {:class (stl/css :icon)} i/corner-radius] + [:input.input-text + {:type "number" + :placeholder "Mixed" + :min 0 + :on-change on-radius-multi-change + :className (stl/css :numeric-input) + :value (if all-equal? (:rx values) nil)}]] - @radius-multi? - [:div.input-element.mini {:title (tr "workspace.options.radius")} - [:input.input-text - {:type "number" - :placeholder "--" - :min 0 - :on-change on-radius-multi-change - :value ""}]] - (= radius-mode :radius-4) - [:* - [:div.input-element.mini {:title (tr "workspace.options.radius-top-left")} - [:> numeric-input - {:placeholder "--" - :min 0 - :on-change on-radius-r1-change - :value (:r1 values)}]] + (= radius-mode :radius-4) + [:div {:class (stl/css :radius-4)} + [:div {:class (stl/css :small-input) + :title (tr "workspace.options.radius-top-left")} + [:> numeric-input* + {:placeholder "--" + :min 0 + :on-change on-radius-r1-change + :className (stl/css :numeric-input) + :value (:r1 values)}]] - [:div.input-element.mini {:title (tr "workspace.options.radius-top-right")} - [:> numeric-input - {:placeholder "--" - :min 0 - :on-change on-radius-r2-change - :value (:r2 values)}]] + [:div {:class (stl/css :small-input) + :title (tr "workspace.options.radius-top-right")} + [:> numeric-input* + {:placeholder "--" + :min 0 + :on-change on-radius-r2-change + :className (stl/css :numeric-input) + :value (:r2 values)}]] - [:div.input-element.mini {:title (tr "workspace.options.radius-bottom-right")} - [:> numeric-input - {:placeholder "--" - :min 0 - :on-change on-radius-r3-change - :value (:r3 values)}]] + [:div {:class (stl/css :small-input) + :title (tr "workspace.options.radius-bottom-left")} + [:> numeric-input* + {:placeholder "--" + :min 0 + :on-change on-radius-r4-change + :className (stl/css :numeric-input) + :value (:r4 values)}]] - [:div.input-element.mini {:title (tr "workspace.options.radius-bottom-left")} - [:> numeric-input - {:placeholder "--" - :min 0 - :on-change on-radius-r4-change - :value (:r4 values)}]]])]) + [:div {:class (stl/css :small-input) + :title (tr "workspace.options.radius-bottom-right")} + [:> numeric-input* + {:placeholder "--" + :min 0 + :on-change on-radius-r3-change + :className (stl/css :numeric-input) + :value (:r3 values)}]]])] + [:button {:class (stl/css-case :radius-mode true + :selected (= radius-mode :radius-4)) + :title (if (= radius-mode :radius-4) + (tr "workspace.options.radius.all-corners") + (tr "workspace.options.radius.single-corners")) + :on-click toggle-radius-mode} + i/corner-radius]])]) + (when (or (options :clip-content) (options :show-in-viewer)) + [:div {:class (stl/css :clip-show)} + (when (options :clip-content) + [:div {:class (stl/css :clip-content)} + [:input {:type "checkbox" + :id "clip-content" + :ref clip-content-ref + :class (stl/css :clip-content-input) + :checked (not (:show-content values)) + :on-change on-change-clip-content}] - (when (options :clip-content) - [:div.input-checkbox - [:input {:type "checkbox" - :id "clip-content" - :ref clip-content-ref - :checked (not (:show-content values)) - :on-change on-change-clip-content}] + [:label {:for "clip-content" + :title (tr "workspace.options.clip-content") + :class (stl/css-case :clip-content-label true + :selected (not (:show-content values)))} + clip-content-icon]]) + (when (options :show-in-viewer) + [:div {:class (stl/css :show-in-viewer)} + [:input {:type "checkbox" + :id "show-in-viewer" + :ref show-in-viewer-ref + :class (stl/css :clip-content-input) + :checked (not (:hide-in-viewer values)) + :on-change on-change-show-in-viewer}] - [:label {:for "clip-content"} - (tr "workspace.options.clip-content")]]) - - (when (options :show-in-viewer) - [:div.input-checkbox - [:input {:type "checkbox" - :id "show-in-viewer" - :ref show-in-viewer-ref - :checked (not (:hide-in-viewer values)) - :on-change on-change-show-in-viewer}] - - [:label {:for "show-in-viewer"} - (tr "workspace.options.show-in-viewer")]]) - - ]]])) + [:label {:for "show-in-viewer" + :title (tr "workspace.options.show-in-viewer") + :class (stl/css-case :clip-content-label true + :selected (not (:hide-in-viewer values)))} + [:span {:class (stl/css :icon)} + play-icon]]])])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss new file mode 100644 index 0000000000..71fdbefa7b --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss @@ -0,0 +1,238 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + @include flexColumn; + margin-bottom: $s-8; +} + +.presets { + display: flex; + align-items: flex-start; + gap: $s-4; +} + +.presets-wrapper { + @extend .asset-element; + position: relative; + display: flex; + height: $s-32; + width: $s-188; + padding: $s-8; + border-radius: $br-8; + + .collapsed-icon { + @include flexCenter; + cursor: pointer; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + transform: rotate(90deg); + } + } + + &:hover { + .collapsed-icon svg { + stroke: var(--input-foreground-color-active); + } + } +} + +.select-name { + @include bodySmallTypography; + display: flex; + justify-content: flex-start; + align-items: center; + flex-grow: 1; + cursor: pointer; +} + +.custom-select-dropdown { + @extend .dropdown-wrapper; + margin-top: $s-2; + width: $s-252; + .dropdown-element { + @extend .dropdown-element-base; + .name-wrapper { + display: flex; + gap: $s-8; + flex-grow: 1; + .preset-name { + color: var(--menu-foreground-color-rest); + } + .preset-size { + color: var(--menu-foreground-color-rest); + } + } + + .check-icon { + @include flexCenter; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } + + &.disabled { + pointer-events: none; + cursor: default; + .preset-name { + color: var(--menu-foreground-color); + } + } + + &.match { + .name-wrapper .preset-name { + color: var(--menu-foreground-color-hover); + } + .check-icon svg { + stroke: var(--menu-foreground-color-hover); + } + } + + &:hover { + background-color: var(--menu-background-color-hover); + .name-wrapper .preset-name { + color: var(--menu-foreground-color-hover); + } + .check-icon svg { + stroke: var(--menu-foreground-color-hover); + } + } + } +} + +.size { + @include flexRow; +} + +.height, +.width { + @extend .input-element; + width: $s-108; + .icon-text { + padding-top: $s-1; + } + &.disabled { + @extend .disabled-input; + } +} + +.lock-size-btn { + @extend .button-tertiary; + border-radius: $br-8; + height: $s-32; + width: $s-28; + &.selected { + @extend .button-icon-selected; + } +} + +.lock-ratio-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} + +.position { + @include flexRow; +} + +.x-position, +.y-position { + @extend .input-element; + width: $s-108; + .icon-text { + padding-top: $s-1; + } + &.disabled { + @extend .disabled-input; + } +} + +.rotation-radius { + display: flex; + align-items: flex-start; + justify-content: flex-start; + gap: $s-4; +} + +.rotation { + @extend .input-element; + width: $s-108; + .icon-text { + padding-top: $s-1; + } +} +.radius { + display: flex; + align-items: flex-start; + justify-content: flex-start; + gap: $s-4; +} + +.radius-inputs { + display: flex; +} + +.radius-1 { + @extend .input-element; + width: $s-108; +} + +.radius-4 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $s-4; + .small-input { + @extend .input-element; + width: $s-52; + } +} + +.radius-mode { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + border-radius: $br-8; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + &.selected { + @extend .button-icon-selected; + } +} + +.clip-show { + display: flex; + align-items: flex-start; + justify-content: flex-start; + gap: $s-4; +} + +.clip-content, +.show-in-viewer { + .clip-content-input { + display: none; + } +} + +.clip-content-label { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + border-radius: $br-8; +} + +.selected { + @extend .button-icon-selected; +} + +.checkbox-button { + @extend .button-icon; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs index b463a1e1bc..b3a8072a06 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.shadow + (:require-macros [app.main.style :as stl]) (:require [app.common.colors :as clr] [app.common.data :as d] @@ -15,7 +16,9 @@ [app.main.data.workspace.colors :as dc] [app.main.data.workspace.undo :as dwu] [app.main.store :as st] - [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.numeric-input :refer [numeric-input*]] + [app.main.ui.components.select :refer [select]] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.hooks :as h] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] @@ -56,22 +59,21 @@ adv-blur-ref (mf/use-ref nil) adv-spread-ref (mf/use-ref nil) - shadow-style (dm/str (:style value)) + shadow-style (:style value) + shadow-id (:id value) open-status-ref (mf/with-memo [open-state-ref shadow-id] (-> (l/key shadow-id) (l/derived open-state-ref))) open-shadow (mf/deref open-status-ref) + hidden? (:hidden value) on-remove-shadow (mf/use-fn - (mf/deps ids) - (fn [event] - (let [index (-> (dom/get-current-target event) - (dom/get-data "index") - (parse-long))] - (st/emit! (dch/update-shapes ids #(update % :shadow remove-shadow-by-index index)))))) + (mf/deps ids index) + (fn [] + (st/emit! (dch/update-shapes ids #(update % :shadow remove-shadow-by-index index))))) on-drop (mf/use-fn @@ -105,129 +107,142 @@ (dom/set-value! update-node value)))))) update-color - (fn [index] - (fn [color] - (st/emit! (dch/update-shapes - ids - #(assoc-in % [:shadow index :color] color))))) + (mf/use-fn + (mf/deps ids index) + (fn [color] + (st/emit! (dch/update-shapes + ids + #(assoc-in % [:shadow index :color] (d/without-nils color)))))) detach-color - (fn [index] - (fn [_color _opacity] - (when-not (string? (:color value)) - (st/emit! (dch/update-shapes - ids - #(assoc-in % [:shadow index :color] - (dissoc (:color value) :id :file-id))))))) + (mf/use-fn + (mf/deps ids index value) + (fn [_color _opacity] + (when-not (string? (:color value)) + (st/emit! (dch/update-shapes + ids + #(assoc-in % [:shadow index :color] + (dissoc (:color value) :id :file-id))))))) toggle-visibility - (fn [index] - (fn [] - (st/emit! (dch/update-shapes ids #(update-in % [:shadow index :hidden] not))))) + (mf/use-fn + (mf/deps ids index) + (fn [] + (st/emit! (dch/update-shapes ids #(update-in % [:shadow index :hidden] not))))) on-toggle-open-shadow (fn [] - (swap! open-state-ref update shadow-id not))] - [:* - [:div.shadow-option {:class (dom/classnames - :dnd-over-top (= (:over dprops) :top) - :dnd-over-bot (= (:over dprops) :bot)) - :ref dref} - [:div.shadow-option-main {:style {:display (when open-shadow "none")}} - [:div.element-set-actions-button - {:on-click on-toggle-open-shadow} - i/actions] + (swap! open-state-ref update shadow-id not)) - [:select.input-select - {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :default-value shadow-style - :on-change (fn [event] - (let [value (-> event dom/get-target dom/get-value d/read-string)] - (st/emit! (dch/update-shapes ids #(assoc-in % [:shadow index :style] value)))))} - [:option {:value ":drop-shadow" - :selected (when (= shadow-style ":drop-shadow") "selected")} - (tr "workspace.options.shadow-options.drop-shadow")] - [:option {:value ":inner-shadow" - :selected (when (= shadow-style ":inner-shadow") "selected")} - (tr "workspace.options.shadow-options.inner-shadow")]] + on-type-change + (mf/use-fn + (mf/deps ids index) + (fn [event] + (let [value (keyword event)] + (st/emit! (dch/update-shapes ids #(assoc-in % [:shadow index :style] value)))))) - [:div.shadow-option-main-actions - [:div.element-set-actions-button {:on-click (toggle-visibility index)} - (if (:hidden value) i/eye-closed i/eye)] - [:div.element-set-actions-button - {:data-index index - :on-click on-remove-shadow} - i/minus]]] + type-options [{:value "drop-shadow" :label (tr "workspace.options.shadow-options.drop-shadow")} + {:value "inner-shadow" :label (tr "workspace.options.shadow-options.inner-shadow")}] - [:& advanced-options {:visible? open-shadow - :on-close on-toggle-open-shadow} - [:div.color-data - [:div.element-set-actions-button - {:on-click on-toggle-open-shadow} - i/actions] - [:select.input-select - {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :default-value shadow-style - :on-change (fn [event] - (let [value (-> event dom/get-target dom/get-value d/read-string)] - (st/emit! (dch/update-shapes ids #(assoc-in % [:shadow index :style] value)))))} - [:option {:value ":drop-shadow" - :selected (when (= shadow-style ":drop-shadow") "selected")} - (tr "workspace.options.shadow-options.drop-shadow")] - [:option {:value ":inner-shadow" - :selected (when (= shadow-style ":inner-shadow") "selected")} - (tr "workspace.options.shadow-options.inner-shadow")]]] + manage-on-open #(st/emit! (dwu/start-undo-transaction :color-row)) + manage-on-close #(st/emit! (dwu/commit-undo-transaction :color-row))] - [:div.row-grid-2 - [:div.input-element {:title (tr "workspace.options.shadow-options.offsetx")} - [:> numeric-input {:ref adv-offset-x-ref - :no-validate true - :placeholder "--" - :on-change (update-attr index :offset-x basic-offset-x-ref) - :on-blur on-blur - :value (:offset-x value)}] - [:span.after (tr "workspace.options.shadow-options.offsetx")]] - [:div.input-element {:title (tr "workspace.options.shadow-options.offsety")} - [:> numeric-input {:ref adv-offset-y-ref - :no-validate true - :placeholder "--" - :on-change (update-attr index :offset-y basic-offset-y-ref) - :on-blur on-blur - :value (:offset-y value)}] - [:span.after (tr "workspace.options.shadow-options.offsety")]]] + [:div {:class (stl/css-case :global/shadow-option true + :shadow-element true + :dnd-over-top (= (:over dprops) :top) + :dnd-over-bot (= (:over dprops) :bot)) + :ref dref} + [:* + [:div {:class (stl/css :basic-options)} + [:div {:class (stl/css-case :shadow-info true + :hidden hidden?)} + [:button {:class (stl/css-case :more-options true + :selected open-shadow) + :on-click on-toggle-open-shadow} + i/menu] + [:div {:class (stl/css :type-select)} + [:& select + {:class (stl/css :shadow-type-select) + :default-value (d/name shadow-style) + :options type-options + :on-change on-type-change}]]] + [:div {:class (stl/css :actions)} + [:button {:class (stl/css :action-btn) + :on-click toggle-visibility} + (if hidden? + i/hide + i/shown)] + [:button {:class (stl/css :action-btn) + :on-click on-remove-shadow} + i/remove-icon]]] + (when open-shadow + [:& advanced-options {:class (stl/css :shadow-advanced-options) + :visible? open-shadow + :on-close on-toggle-open-shadow} - [:div.row-grid-2 - [:div.input-element {:title (tr "workspace.options.shadow-options.blur")} - [:> numeric-input {:ref adv-blur-ref - :no-validate true - :placeholder "--" - :on-change (update-attr index :blur basic-blur-ref) - :on-blur on-blur - :min 0 - :value (:blur value)}] - [:span.after (tr "workspace.options.shadow-options.blur")]] + [:div {:class (stl/css :first-row)} + [:div {:class (stl/css :offset-x-input) + :title (tr "workspace.options.shadow-options.offsetx")} + [:span {:class (stl/css :input-label)} + "X"] + [:> numeric-input* {:className (stl/css :numeric-input) + :ref adv-offset-x-ref + :no-validate true + :placeholder "--" + :on-change (update-attr index :offset-x basic-offset-x-ref) + :on-blur on-blur + :value (:offset-x value)}]] - [:div.input-element {:title (tr "workspace.options.shadow-options.spread")} - [:> numeric-input {:ref adv-spread-ref - :no-validate true - :placeholder "--" - :on-change (update-attr index :spread) - :on-blur on-blur - :value (:spread value)}] - [:span.after (tr "workspace.options.shadow-options.spread")]]] + [:div {:class (stl/css :blur-input) + :title (tr "workspace.options.shadow-options.blur")} + [:span {:class (stl/css :input-label)} + (tr "workspace.options.shadow-options.blur")] + [:> numeric-input* {:ref adv-blur-ref + :className (stl/css :numeric-input) + :no-validate true + :placeholder "--" + :on-change (update-attr index :blur basic-blur-ref) + :on-blur on-blur + :min 0 + :value (:blur value)}]] + + [:div {:class (stl/css :spread-input) + :title (tr "workspace.options.shadow-options.spread")} + [:span {:class (stl/css :input-label)} + (tr "workspace.options.shadow-options.spread")] + [:> numeric-input* {:ref adv-spread-ref + :className (stl/css :numeric-input) + :no-validate true + :placeholder "--" + :on-change (update-attr index :spread) + :on-blur on-blur + :value (:spread value)}]]] + + [:div {:class (stl/css :second-row)} + [:div {:class (stl/css :offset-y-input) + :title (tr "workspace.options.shadow-options.offsety")} + [:span {:class (stl/css :input-label)} + "Y"] + [:> numeric-input* {:ref adv-offset-y-ref + :className (stl/css :numeric-input) + :no-validate true + :placeholder "--" + :on-change (update-attr index :offset-y basic-offset-y-ref) + :on-blur on-blur + :value (:offset-y value)}]] + [:& color-row {:color (if (string? (:color value)) + ;; Support for old format colors + {:color (:color value) :opacity (:opacity value)} + (:color value)) + :title (tr "workspace.options.shadow-options.color") + :disable-gradient true + :disable-image true + :on-change update-color + :on-detach detach-color + :on-open manage-on-open + :on-close manage-on-close}]]])]])) - [:div.color-row-wrap - [:& color-row {:color (if (string? (:color value)) - ;; Support for old format colors - {:color (:color value) :opacity (:opacity value)} - (:color value)) - :title (tr "workspace.options.shadow-options.color") - :disable-gradient true - :on-change (update-color index) - :on-detach (detach-color index) - :on-open #(st/emit! (dwu/start-undo-transaction :color-row)) - :on-close #(st/emit! (dwu/commit-undo-transaction :color-row))}]]]]])) (mf/defc shadow-menu {::mf/wrap-props false} [props] @@ -237,9 +252,17 @@ shadows (:shadow values []) open-state-ref (mf/with-memo [] (l/atom {})) + has-shadows? (or (= :multiple shadows) (some? (seq shadows))) - disable-drag* (mf/use-state false) - disable-drag? (deref disable-drag*) + state* (mf/use-state {:show-content true + :disable-drag false}) + + state (deref state*) + open? (:show-content state) + disable-drag? (:disable-drag state) + + toggle-content + (mf/use-fn #(swap! state* update :show-content not)) on-remove-all (mf/use-fn @@ -255,43 +278,49 @@ on-blur (mf/use-fn - #(reset! disable-drag* false)) + #(swap! state* assoc :disable-drag false)) on-add-shadow (mf/use-fn (mf/deps ids) #(st/emit! (dc/add-shadow ids (create-shadow))))] - [:div.element-set.shadow-options - [:div.element-set-title - [:span - (case type - :multiple (tr "workspace.options.shadow-options.title.multiple") - :group (tr "workspace.options.shadow-options.title.group") - (tr "workspace.options.shadow-options.title"))] + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-title)} + [:& title-bar {:collapsable has-shadows? + :collapsed (not open?) + :on-collapsed toggle-content + :title (case type + :multiple (tr "workspace.options.shadow-options.title.multiple") + :group (tr "workspace.options.shadow-options.title.group") + (tr "workspace.options.shadow-options.title")) + :class (stl/css-case :title-spacing-shadow (not has-shadows?))} - (when-not (= :multiple shadows) - [:div.add-page {:on-click on-add-shadow} i/close])] + (when-not (= :multiple shadows) + [:button {:class (stl/css :add-shadow) + :on-click on-add-shadow} i/add])]] - (cond - (= :multiple shadows) - [:div.element-set-content - [:div.element-set-options-group - [:div.element-set-label (tr "settings.multiple")] - [:div.element-set-actions - [:div.element-set-actions-button {:on-click on-remove-all} - i/minus]]]] + (when open? + (cond + (= :multiple shadows) + [:div {:class (stl/css :element-set-content)} + [:div {:class (stl/css :multiple-shadows)} + [:div {:class (stl/css :label)} (tr "settings.multiple")] + [:div {:class (stl/css :actions)} + [:button {:class (stl/css :action-btn) + :on-click on-remove-all} + i/remove-icon]]]] - (seq shadows) - [:& h/sortable-container {} - [:div.element-set-content - (for [[index value] (d/enumerate shadows)] - [:& shadow-entry - {:key (dm/str "shadow-" index) - :ids ids - :value value - :on-reorder handle-reorder - :disable-drag? disable-drag? - :on-blur on-blur - :index index - :open-state-ref open-state-ref}])]])])) + (seq shadows) + [:& h/sortable-container {} + [:div {:class (stl/css :element-set-content)} + (for [[index value] (d/enumerate shadows)] + [:& shadow-entry + {:key (dm/str "shadow-" index) + :ids ids + :value value + :on-reorder handle-reorder + :disable-drag? disable-drag? + :on-blur on-blur + :index index + :open-state-ref open-state-ref}])]]))])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.scss new file mode 100644 index 0000000000..a9ef8ebd20 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.scss @@ -0,0 +1,134 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + margin: 0; +} + +.title-spacing-shadow { + margin: 0; + padding-left: $s-2; +} +.add-shadow { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.element-set-content { + margin-top: $s-4; + @include flexColumn; +} + +.multiple-shadows { + @include flexRow; +} + +.label { + @extend .mixed-bar; +} + +.actions { + @include flexRow; +} + +.action-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.shadow-element { + @include flexColumn; +} + +.basic-options { + @include flexRow; +} + +.shadow-info { + display: flex; + align-items: center; + gap: $s-1; + width: $s-188; + .more-options { + @extend .button-secondary; + height: $s-32; + width: $s-28; + border-radius: $br-8 0 0 $br-8; + svg { + @extend .button-icon; + } + &.selected { + background-color: var(--button-radio-background-color-active); + svg { + stroke: var(--button-radio-foreground-color-active); + } + } + } + .type-select { + padding: 0; + border-radius: 0 $br-8 $br-8 0; + flex-grow: 1; + .shadow-type-select { + flex-grow: 1; + border-radius: 0 $br-8 $br-8 0; + } + } + + &.hidden { + .more-options { + @include hiddenElement; + border: $s-1 solid var(--input-border-color-disabled); + } + .type-select { + @include hiddenElement; + .shadow-type-select { + @include hiddenElement; + border: $s-1 solid var(--input-border-color-disabled); + } + } + } +} + +.shadow-advanced-options { + @include flexColumn; +} + +.first-row, +.second-row { + @include flexRow; + .offset-x-input, + .blur-input, + .spread-input, + .offset-y-input { + @extend .input-element; + width: $s-60; + min-width: $s-60; + align-items: baseline; + input { + width: $s-32; + } + } + .blur-input, + .spread-input { + width: $s-92; + .input-label { + width: $s-44; + } + } + .spread-input { + gap: $s-8; + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index dba1339cca..3539f693f1 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -5,12 +5,14 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.stroke + (:require-macros [app.main.style :as stl]) (:require [app.common.colors :as clr] [app.common.data :as d] [app.common.data.macros :as dm] [app.main.data.workspace.colors :as dc] [app.main.store :as st] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.hooks :as h] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.rows.stroke-row :refer [stroke-row]] @@ -40,62 +42,68 @@ :group (tr "workspace.options.group-stroke") (tr "workspace.options.stroke")) - handle-change-stroke-color - (mf/use-callback + state* (mf/use-state true) + open? (deref state*) + + toggle-content (mf/use-fn #(swap! state* not)) + open-content (mf/use-fn #(reset! state* true)) + + strokes (:strokes values) + has-strokes? (or (= :multiple strokes) (some? (seq strokes))) + + + on-color-change + (mf/use-fn + (mf/deps ids) + (fn [index color] + (st/emit! (dc/change-stroke ids color index)))) + + + on-remove + (mf/use-fn (mf/deps ids) (fn [index] - (fn [color] + (st/emit! (dc/remove-stroke ids index)))) + + handle-remove-all + (mf/use-fn + (mf/deps ids) + (fn [_] + (st/emit! (dc/remove-all-strokes ids)))) + + on-color-detach + (mf/use-fn + (mf/deps ids) + (fn [index color] + (let [color (-> color + (assoc :id nil :file-id nil))] (st/emit! (dc/change-stroke ids color index))))) - handle-remove - (mf/use-callback - (mf/deps ids) - (fn [index] - (fn [] - (st/emit! (dc/remove-stroke ids index))))) - - handle-remove-remove-all - (fn [_] - (st/emit! (dc/remove-all-strokes ids))) - - handle-detach - (mf/use-callback - (mf/deps ids) - (fn [index] - (fn [color] - (let [color (-> color - (assoc :id nil :file-id nil))] - (st/emit! (dc/change-stroke ids color index)))))) - handle-reorder - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [new-index] (fn [index] (st/emit! (dc/reorder-strokes ids index new-index))))) on-stroke-style-change - (fn [index] - (fn [event] - (let [value (-> (dom/get-target event) - (dom/get-value) - (d/read-string))] - (st/emit! (dc/change-stroke ids {:stroke-style value} index))))) + (mf/use-fn + (mf/deps ids) + (fn [index value] + (st/emit! (dc/change-stroke ids {:stroke-style value} index)))) on-stroke-alignment-change - (fn [index] - (fn [event] - (let [value (-> (dom/get-target event) - (dom/get-value) - (d/read-string))] - (when-not (str/empty? value) - (st/emit! (dc/change-stroke ids {:stroke-alignment value} index)))))) + (fn [index value] + (when-not (str/empty? value) + (st/emit! (dc/change-stroke ids {:stroke-alignment value} index)))) + + on-stroke-width-change - (fn [index] - (fn [value] - (when-not (str/empty? value) - (st/emit! (dc/change-stroke ids {:stroke-width value} index))))) + (fn [index value] + (when-not (str/empty? value) + (st/emit! (dc/change-stroke ids {:stroke-width value} index)))) + open-caps-select (fn [caps-state] @@ -139,56 +147,64 @@ :stroke-cap-end stroke-cap-start} index))))) on-add-stroke (fn [_] - (st/emit! (dc/add-stroke ids {:stroke-style :solid + (st/emit! (dc/add-stroke ids {:stroke-alignment :inner + :stroke-style :solid :stroke-color clr/black :stroke-opacity 1 - :stroke-width 1}))) + :stroke-width 1})) + (when (not (some? (seq strokes))) (open-content))) disable-drag (mf/use-state false) on-focus (fn [_] - (reset! disable-drag true)) + (reset! disable-drag true)) on-blur (fn [_] (reset! disable-drag false))] - [:div.element-set - [:div.element-set-title - [:span label] - [:div.add-page {:on-click on-add-stroke} i/close]] + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-title)} + [:& title-bar {:collapsable has-strokes? + :collapsed (not open?) + :on-collapsed toggle-content + :title label + :class (stl/css-case :title-spacing-stroke (not has-strokes?))} - [:div.element-set-content - (cond - (= :multiple (:strokes values)) - [:div.element-set-options-group - [:div.element-set-label (tr "settings.multiple")] - [:div.element-set-actions - [:div.element-set-actions-button {:on-click handle-remove-remove-all} - i/minus]]] - - - (seq (:strokes values)) - [:& h/sortable-container {} - (for [[index value] (d/enumerate (:strokes values []))] - [:& stroke-row {:key (dm/str "stroke-" index) - :stroke value - :title (tr "workspace.options.stroke-color") - :index index - :show-caps show-caps - :on-color-change handle-change-stroke-color - :on-color-detach handle-detach - :on-stroke-width-change on-stroke-width-change - :on-stroke-style-change on-stroke-style-change - :on-stroke-alignment-change on-stroke-alignment-change - :open-caps-select open-caps-select - :close-caps-select close-caps-select - :on-stroke-cap-start-change on-stroke-cap-start-change - :on-stroke-cap-end-change on-stroke-cap-end-change - :on-stroke-cap-switch on-stroke-cap-switch - :on-remove handle-remove - :on-reorder (handle-reorder index) - :disable-drag disable-drag - :on-focus on-focus - :data-select-on-focus (not @disable-drag) - :on-blur on-blur - :disable-stroke-style disable-stroke-style}])])]])) + [:button {:class (stl/css :add-stroke) + :on-click on-add-stroke} i/add]]] + (when open? + [:div {:class (stl/css-case :element-content true + :empty-content (not has-strokes?))} + (cond + (= :multiple strokes) + [:div {:class (stl/css :element-set-options-group)} + [:div {:class (stl/css :group-label)} + (tr "settings.multiple")] + [:button {:on-click handle-remove-all + :class (stl/css :remove-btn)} + i/remove-icon]] + (seq strokes) + [:& h/sortable-container {} + (for [[index value] (d/enumerate (:strokes values []))] + [:& stroke-row {:key (dm/str "stroke-" index) + :stroke value + :title (tr "workspace.options.stroke-color") + :index index + :show-caps show-caps + :on-color-change on-color-change + :on-color-detach on-color-detach + :on-stroke-width-change on-stroke-width-change + :on-stroke-style-change on-stroke-style-change + :on-stroke-alignment-change on-stroke-alignment-change + :open-caps-select open-caps-select + :close-caps-select close-caps-select + :on-stroke-cap-start-change on-stroke-cap-start-change + :on-stroke-cap-end-change on-stroke-cap-end-change + :on-stroke-cap-switch on-stroke-cap-switch + :on-remove on-remove + :on-reorder (handle-reorder index) + :disable-drag disable-drag + :on-focus on-focus + :select-on-focus (not @disable-drag) + :on-blur on-blur + :disable-stroke-style disable-stroke-style}])])])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.scss new file mode 100644 index 0000000000..53f6af64e0 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.scss @@ -0,0 +1,55 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + margin: 0; +} + +.element-title { + margin: 0; +} + +.title-spacing-stroke { + padding-left: $s-2; + margin: 0; +} +.add-stroke { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.element-content { + display: flex; + flex-direction: column; + gap: $s-12; + margin: $s-4 0 $s-8 0; + &.empty-content { + margin: 0; + } +} + +.element-set-options-group { + @include flexRow; +} + +.group-label { + @extend .mixed-bar; +} + +.remove-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs index 4db6b521bf..11f1264286 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs @@ -5,57 +5,63 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.svg-attrs + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.main.data.workspace.changes :as dch] [app.main.store :as st] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.icons :as i] - [app.main.ui.workspace.sidebar.options.rows.input-row :refer [input-row]] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [rumext.v2 :as mf])) (mf/defc attribute-value [{:keys [attr value on-change on-delete] :as props}] (let [handle-change - (mf/use-callback + (mf/use-fn (mf/deps attr on-change) (fn [event] (on-change attr (dom/get-target-val event)))) handle-delete - (mf/use-callback + (mf/use-fn (mf/deps attr on-delete) (fn [] (on-delete attr))) label (->> attr last d/name)] - [:div.element-set-content + [:* (if (string? value) - [:div.row-flex.row-flex-removable - [:& input-row {:label label - :type :text - :class "large" - :value (str value) - :on-change handle-change}] - [:div.element-set-actions - [:div.element-set-actions-button {:on-click handle-delete} - i/minus]]] - - [:* - [:div.element-set-title - {:style {:border-bottom "1px solid #444" :margin-bottom "0.5rem"}} - [:span (str (d/name (last attr)))]] - + [:div {:class (stl/css :attr-content)} + [:span {:class (stl/css :attr-name)} label] + [:div {:class (stl/css :attr-input)} + [:input {:value value + :class "input-text" + :on-change handle-change}]] + [:div {:class (stl/css :attr-actions)} + [:button {:class (stl/css :attr-action-btn) + :on-click handle-delete} + i/remove-icon]]] + [:div {:class (stl/css :attr-nested-content)} + [:div {:class (stl/css :attr-title)} + (str (d/name (last attr)))] (for [[key value] value] - [:& attribute-value {:key key - :attr (conj attr key) - :value value - :on-change on-change - :on-delete on-delete}])])])) + [:div {:class (stl/css :attr-row) :key key} + [:& attribute-value {:key key + :attr (conj attr key) + :value value + :on-change on-change + :on-delete on-delete}]])])])) (mf/defc svg-attrs-menu [{:keys [ids values]}] - (let [handle-change - (mf/use-callback + (let [state* (mf/use-state true) + open? (deref state*) + attrs (:svg-attrs values) + has-attributes? (or (= :multiple attrs) (some? (seq attrs))) + + toggle-content (mf/use-fn #(swap! state* not)) + handle-change + (mf/use-fn (mf/deps ids) (fn [attr value] (let [update-fn @@ -63,7 +69,7 @@ (st/emit! (dch/update-shapes ids update-fn))))) handle-delete - (mf/use-callback + (mf/use-fn (mf/deps ids) (fn [attr] (let [update-fn @@ -75,18 +81,21 @@ (empty? (get-in shape [:svg-attrs :style])) (update :svg-attrs dissoc :style))] shape))] - (st/emit! (dch/update-shapes ids update-fn))))) + (st/emit! (dch/update-shapes ids update-fn)))))] - ] - - (when-not (empty? (:svg-attrs values)) - [:div.element-set - [:div.element-set-title - [:span (tr "workspace.sidebar.options.svg-attrs.title")]] - - (for [[attr-key attr-value] (:svg-attrs values)] - [:& attribute-value {:key attr-key - :attr [attr-key] - :value attr-value - :on-change handle-change - :on-delete handle-delete}])]))) + (when-not (empty? attrs) + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-set-title)} + [:& title-bar {:collapsable has-attributes? + :collapsed (not open?) + :on-collapsed toggle-content + :title (tr "workspace.sidebar.options.svg-attrs.title") + :class (stl/css-case :title-spacing-svg-attrs (not has-attributes?))}]] + (when open? + [:div {:class (stl/css :element-set-content)} + (for [[attr-key attr-value] attrs] + [:& attribute-value {:key attr-key + :attr [attr-key] + :value attr-value + :on-change handle-change + :on-delete handle-delete}])])]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss new file mode 100644 index 0000000000..c117ca6a1f --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss @@ -0,0 +1,73 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + margin: 0; +} + +.title-spacing-svg-attrs { + padding-left: $s-2; + margin: 0; +} + +.element-set-content { + @include flexColumn; + margin: $s-4 0 0 0; +} + +.attr-content { + display: flex; + gap: $s-4; +} + +.attr-name { + @include bodySmallTypography; + @include twoLineTextEllipsis; + width: $s-88; + margin: auto $s-4; + margin-right: 0; + display: inline-block; + color: var(--title-foreground-color); +} + +.attr-input { + @extend .input-element; + width: $s-124; +} + +.attr-actions { + display: flex; + gap: $s-4; +} + +.attr-action-btn { + @extend .button-tertiary; + width: $s-28; + height: $s-32; + svg { + @extend .button-icon; + } +} + +.attr-nested-content { + display: grid; + row-gap: $s-4; +} + +.attr-title { + @include bodySmallTypography; + font-size: $fs-10; + text-transform: uppercase; + margin-inline-start: $s-4; + color: $df-primary; +} + +.attr-row { + display: flex; + gap: $s-4; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index a7f85fe0ff..5fbb3b8407 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.text + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.text :as txt] @@ -16,9 +17,11 @@ [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.context :as ctx] [app.main.ui.icons :as i] - [app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry typography-options]] + [app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry text-options]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.timers :as ts] @@ -28,157 +31,192 @@ [{:keys [values on-change on-blur] :as props}] (let [{:keys [text-align]} values handle-change - (fn [_ new-align] - (on-change {:text-align new-align}) - (when (some? on-blur) (on-blur)))] + (mf/use-fn + (mf/deps on-blur) + (fn [value] + (on-change {:text-align value}) + (when (some? on-blur) (on-blur))))] ;; --- Align - [:div.align-icons - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.text-align-left" (sc/get-tooltip :text-align-left)) - :class (dom/classnames :current (= "left" text-align)) - :on-click #(handle-change % "left")} - i/text-align-left] - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.text-align-center" (sc/get-tooltip :text-align-center)) - :class (dom/classnames :current (= "center" text-align)) - :on-click #(handle-change % "center")} - i/text-align-center] - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.text-align-right" (sc/get-tooltip :text-align-right)) - :class (dom/classnames :current (= "right" text-align)) - :on-click #(handle-change % "right")} - i/text-align-right] - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.text-align-justify" (sc/get-tooltip :text-align-justify)) - :class (dom/classnames :current (= "justify" text-align)) - :on-click #(handle-change % "justify")} - i/text-align-justify]])) + [:div {:class (stl/css :align-options)} + [:& radio-buttons {:selected text-align + :on-change handle-change + :name "align-text-options"} + [:& radio-button {:value "left" + :id "text-align-left" + :title (tr "workspace.options.text-options.text-align-left" (sc/get-tooltip :text-align-left)) + :icon i/text-align-left}] + [:& radio-button {:value "center" + :id "text-align-center" + :title (tr "workspace.options.text-options.text-align-center" (sc/get-tooltip :text-align-center)) + :icon i/text-align-center}] + [:& radio-button {:value "right" + :id "text-align-right" + :title (tr "workspace.options.text-options.text-align-right" (sc/get-tooltip :text-align-right)) + :icon i/text-align-right}] + [:& radio-button {:value "justify" + :id "text-align-justify" + :title (tr "workspace.options.text-options.text-align-justify" (sc/get-tooltip :text-align-justify)) + :icon i/text-justify}]]])) (mf/defc text-direction-options [{:keys [values on-change on-blur] :as props}] (let [direction (:text-direction values) handle-change - (fn [_ val] - (on-change {:text-direction val}) - (when (some? on-blur) (on-blur)))] - ;; --- Align - [:div.align-icons - [:span.tooltip.tooltip-bottom-left - {:alt (tr "workspace.options.text-options.direction-ltr") - :class (dom/classnames :current (= "ltr" direction)) - :on-click #(handle-change % "ltr")} - i/text-direction-ltr] - [:span.tooltip.tooltip-bottom-left - {:alt (tr "workspace.options.text-options.direction-rtl") - :class (dom/classnames :current (= "rtl" direction)) - :on-click #(handle-change % "rtl")} - i/text-direction-rtl]])) + (mf/use-fn + (mf/deps direction) + (fn [value] + (let [dir (if (= value direction) + "none" + value)] + (on-change {:text-direction dir}) + (when (some? on-blur) (on-blur)))))] + + [:div {:class (stl/css :text-direction-options)} + [:& radio-buttons {:selected direction + :on-change handle-change + :name "text-direction-options"} + [:& radio-button {:value "ltr" + :type "checkbox" + :id "ltr-text-direction" + :title (tr "workspace.options.text-options.direction-ltr") + :icon i/text-ltr}] + [:& radio-button {:value "rtl" + :type "checkbox" + :id "rtl-text-direction" + :title (tr "workspace.options.text-options.direction-rtl") + :icon i/text-rtl}]]])) (mf/defc vertical-align [{:keys [values on-change on-blur] :as props}] (let [{:keys [vertical-align]} values vertical-align (or vertical-align "top") handle-change - (fn [_ new-align] - (on-change {:vertical-align new-align}) - (when (some? on-blur) (on-blur)))] + (mf/use-fn + (mf/deps on-blur) + (fn [value] + (on-change {:vertical-align value}) + (when (some? on-blur) (on-blur))))] - [:div.align-icons - [:span.tooltip.tooltip-bottom-left - {:alt (tr "workspace.options.text-options.align-top") - :class (dom/classnames :current (= "top" vertical-align)) - :on-click #(handle-change % "top")} - i/align-top] - [:span.tooltip.tooltip-bottom-left - {:alt (tr "workspace.options.text-options.align-middle") - :class (dom/classnames :current (= "center" vertical-align)) - :on-click #(handle-change % "center")} - i/align-middle] - [:span.tooltip.tooltip-bottom-left - {:alt (tr "workspace.options.text-options.align-bottom") - :class (dom/classnames :current (= "bottom" vertical-align)) - :on-click #(handle-change % "bottom")} - i/align-bottom]])) + [:div {:class (stl/css :vertical-align-options)} + [:& radio-buttons {:selected vertical-align + :on-change handle-change + :name "vertical-align-text-options"} + [:& radio-button {:value "top" + :id "vertical-text-align-top" + :title (tr "workspace.options.text-options.align-top") + :icon i/text-top}] + [:& radio-button {:value "center" + :id "vertical-text-align-center" + :title (tr "workspace.options.text-options.align-middle") + :icon i/text-middle}] + [:& radio-button {:value "bottom" + :id "vertical-text-align-bottom" + :title (tr "workspace.options.text-options.align-bottom") + :icon i/text-bottom}]]])) (mf/defc grow-options [{:keys [ids values on-blur] :as props}] (let [grow-type (:grow-type values) - handle-change-grow - (fn [_ grow-type] - (let [uid (js/Symbol)] - (st/emit! - (dwu/start-undo-transaction uid) - (dch/update-shapes ids #(assoc % :grow-type grow-type))) - ;; We asynchronously commit so every sychronous event is resolved first and inside the transaction - (ts/schedule #(st/emit! (dwu/commit-undo-transaction uid)))) - (when (some? on-blur) (on-blur)))] - [:div.align-icons - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.grow-fixed") - :class (dom/classnames :current (= :fixed grow-type)) - :on-click #(handle-change-grow % :fixed)} - i/auto-fix] - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.grow-auto-width") - :class (dom/classnames :current (= :auto-width grow-type)) - :on-click #(handle-change-grow % :auto-width)} - i/auto-width] - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.grow-auto-height") - :class (dom/classnames :current (= :auto-height grow-type)) - :on-click #(handle-change-grow % :auto-height)} - i/auto-height]])) + handle-change-grow + (mf/use-fn + (mf/deps ids on-blur) + (fn [value] + (let [uid (js/Symbol) + grow-type (keyword value)] + (st/emit! + (dwu/start-undo-transaction uid) + (dch/update-shapes ids #(assoc % :grow-type grow-type))) + ;; We asynchronously commit so every sychronous event is resolved first and inside the transaction + (ts/schedule #(st/emit! (dwu/commit-undo-transaction uid)))) + (when (some? on-blur) (on-blur))))] + + [:div {:class (stl/css :grow-options)} + [:& radio-buttons {:selected (d/name grow-type) + :on-change handle-change-grow + :name "grow-text-options"} + [:& radio-button {:value "fixed" + :id "text-fixed-grow" + :title (tr "workspace.options.text-options.grow-fixed") + :icon i/text-fixed}] + [:& radio-button {:value "auto-width" + :id "text-auto-width-grow" + :title (tr "workspace.options.text-options.grow-auto-width") + :icon i/text-auto-width}] + [:& radio-button {:value "auto-height" + :id "text-auto-height-grow" + :title (tr "workspace.options.text-options.grow-auto-height") + :icon i/text-auto-height}]]])) (mf/defc text-decoration-options [{:keys [values on-change on-blur] :as props}] (let [text-decoration (or (:text-decoration values) "none") handle-change - (fn [_ type] - (on-change {:text-decoration type}) - (when (some? on-blur) (on-blur)))] - [:div.align-icons - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.none") - :class (dom/classnames :current (= "none" text-decoration)) - :on-click #(handle-change % "none")} - i/minus] - - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.underline" (sc/get-tooltip :underline)) - :class (dom/classnames :current (= "underline" text-decoration)) - :on-click #(handle-change % "underline")} - i/underline] - - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.strikethrough" (sc/get-tooltip :line-through)) - :class (dom/classnames :current (= "line-through" text-decoration)) - :on-click #(handle-change % "line-through")} - i/strikethrough]])) + (mf/use-fn + (mf/deps text-decoration) + (fn [value] + (let [decoration (if (= value text-decoration) + "none" + value)] + (on-change {:text-decoration decoration}) + (when (some? on-blur) (on-blur)))))] + [:div {:class (stl/css :text-decoration-options)} + [:& radio-buttons {:selected text-decoration + :on-change handle-change + :name "text-decoration-options"} + [:& radio-button {:value "underline" + :type "checkbox" + :id "underline-text-decoration" + :title (tr "workspace.options.text-options.underline" (sc/get-tooltip :underline)) + :icon i/text-underlined}] + [:& radio-button {:value "line-through" + :type "checkbox" + :id "line-through-text-decoration" + :title (tr "workspace.options.text-options.strikethrough" (sc/get-tooltip :line-through)) + :icon i/text-stroked}]]])) (mf/defc text-menu {::mf/wrap [mf/memo]} [{:keys [ids type values] :as props}] - (let [file-id (mf/use-ctx ctx/current-file-id) - typographies (mf/deref refs/workspace-file-typography) - shared-libs (mf/deref refs/workspace-libraries) - label (case type - :multiple (tr "workspace.options.text-options.title-selection") - :group (tr "workspace.options.text-options.title-group") - (tr "workspace.options.text-options.title")) + (let [file-id (mf/use-ctx ctx/current-file-id) + typographies (mf/deref refs/workspace-file-typography) + shared-libs (mf/deref refs/workspace-libraries) + label (case type + :multiple (tr "workspace.options.text-options.title-selection") + :group (tr "workspace.options.text-options.title-group") + (tr "workspace.options.text-options.title")) + + state* (mf/use-state {:main-menu true + :more-options false}) + state (deref state*) + main-menu-open? (:main-menu state) + more-options-open? (:more-options state) + + toggle-main-menu + (mf/use-fn + (mf/deps main-menu-open?) + #(swap! state* assoc-in [:main-menu] (not main-menu-open?))) + + toggle-more-options + (mf/use-fn + (mf/deps more-options-open?) + #(swap! state* assoc-in [:more-options] (not more-options-open?))) + + typography-id (:typography-ref-id values) + typography-file-id (:typography-ref-file values) emit-update! - (mf/use-callback + (mf/use-fn (mf/deps values) (fn [ids attrs] (st/emit! (dwt/save-font (-> (merge txt/default-text-attrs values attrs) - (select-keys dwt/text-attrs))) + (select-keys txt/text-node-attrs))) (dwt/update-all-attrs ids attrs)))) on-change - (mf/use-callback + (mf/use-fn (mf/deps ids emit-update!) (fn [attrs] (emit-update! ids attrs))) @@ -188,25 +226,25 @@ (mf/deps values file-id shared-libs) (fn [] (cond - (and (:typography-ref-id values) - (not= (:typography-ref-id values) :multiple) - (not= (:typography-ref-file values) file-id)) + (and typography-id + (not= typography-id :multiple) + (not= typography-file-id file-id)) (-> shared-libs - (get-in [(:typography-ref-file values) :data :typographies (:typography-ref-id values)]) - (assoc :file-id (:typography-ref-file values))) + (get-in [typography-file-id :data :typographies typography-id]) + (assoc :file-id typography-file-id)) - (and (:typography-ref-id values) - (not= (:typography-ref-id values) :multiple) - (= (:typography-ref-file values) file-id)) - (get typographies (:typography-ref-id values))))) + (and typography-id + (not= typography-id :multiple) + (= typography-file-id file-id)) + (get typographies typography-id)))) on-convert-to-typography (fn [_] (let [set-values (-> (d/without-nils values) (select-keys - (d/concat-vec dwt/text-font-attrs - dwt/text-spacing-attrs - dwt/text-transform-attrs))) + (d/concat-vec txt/text-font-attrs + txt/text-spacing-attrs + txt/text-transform-attrs))) typography (merge txt/default-typography set-values) typography (dwt/generate-typography-name typography) id (uuid/next)] @@ -216,14 +254,14 @@ :typography-ref-file file-id}))) handle-detach-typography - (mf/use-callback + (mf/use-fn (mf/deps on-change) (fn [] (on-change {:typography-ref-file nil :typography-ref-id nil}))) handle-change-typography - (mf/use-callback + (mf/use-fn (mf/deps typography file-id) (fn [changes] (st/emit! (dwl/update-typography (merge typography changes) file-id)))) @@ -243,39 +281,48 @@ (let [node (dom/get-element-by-class "public-DraftEditor-content")] (dom/focus! node))))))}] - [:div.element-set - [:div.element-set-title - [:span label] - (when (and (not typography) (not multiple?)) - [:div.add-page {:on-click on-convert-to-typography} i/close])] + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-title)} + [:& title-bar {:collapsable true + :collapsed (not main-menu-open?) + :on-collapsed toggle-main-menu + :title label + :class (stl/css :title-spacing-text)} + (when (and (not typography) (not multiple?)) + [:button {:class (stl/css :add-typography) + :on-click on-convert-to-typography} + i/add])]] - (cond - typography - [:& typography-entry {:typography typography - :local? (= (:typography-ref-file values) file-id) - :file (get shared-libs (:typography-ref-file values)) - :on-detach handle-detach-typography - :on-change handle-change-typography}] + (when main-menu-open? + [:div {:class (stl/css :element-content)} + (cond + typography + [:& typography-entry {:file-id typography-file-id + :typography typography + :local? (= typography-file-id file-id) + :on-detach handle-detach-typography + :on-change handle-change-typography}] - (= (:typography-ref-id values) :multiple) - [:div.multiple-typography - [:div.multiple-typography-text (tr "workspace.libraries.text.multiple-typography")] - [:div.multiple-typography-button {:on-click handle-detach-typography - :title (tr "workspace.libraries.text.multiple-typography-tooltip")} i/unchain]] + (= typography-id :multiple) + [:div {:class (stl/css :multiple-typography)} + [:span {:class (stl/css :multiple-text)} (tr "workspace.libraries.text.multiple-typography")] + [:div {:class (stl/css :multiple-typography-button) + :on-click handle-detach-typography + :title (tr "workspace.libraries.text.multiple-typography-tooltip")} + i/detach]] - :else - [:> typography-options opts]) + :else + [:> text-options opts]) - [:div.element-set-content + [:div {:class (stl/css :text-align-options)} + [:> text-align-options opts] + [:> grow-options opts] + [:button {:class (stl/css :more-options) + :on-click toggle-more-options} + i/menu]] - [:div.row-flex - [:> text-align-options opts] - [:> vertical-align opts]] - - [:div.row-flex - [:> text-decoration-options opts] - [:> text-direction-options opts]] - - [:div.row-flex - [:> grow-options opts] - [:div.align-icons]]]])) + (when more-options-open? + [:div {:class (stl/css :text-decoration-options)} + [:> vertical-align opts] + [:> text-decoration-options opts] + [:> text-direction-options opts]])])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss new file mode 100644 index 0000000000..3d3a4ddef2 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss @@ -0,0 +1,75 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.element-set { + margin: 0; +} + +.element-title { + margin: 0; +} + +.add-typography { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.element-content { + @include flexColumn; + margin-top: $s-4; +} + +.multiple-typography { + @extend .mixed-bar; +} + +.multiple-text { + @include bodySmallTypography; + flex-grow: 1; + color: var(--input-foreground-color-active); +} + +.multiple-typography-button { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.text-align-options { + display: flex; + gap: $s-4; +} + +.align-options, +.text-direction-options, +.vertical-align-options, +.grow-options, +.text-decoration-options { + height: $s-32; +} + +.more-options { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} + +.text-decoration-options { + display: flex; + gap: $s-4; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 7cb9be14e3..44d3de561a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.typography + (:require-macros [app.main.style :as stl]) (:require ["react-virtualized" :as rvt] [app.common.data :as d] @@ -18,10 +19,12 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.editable-select :refer [editable-select]] - [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.numeric-input :refer [numeric-input*]] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.components.search-bar :refer [search-bar]] + [app.main.ui.components.select :refer [select]] [app.main.ui.context :as ctx] [app.main.ui.icons :as i] - [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] @@ -67,12 +70,14 @@ (when-not (dom/is-in-viewport? element) (dom/scroll-into-view! element)))))) - [:div.font-item {:ref item-ref - :style style - :class (when current? "selected") - :on-click on-click} - [:span.icon (when current? i/tick)] - [:span.label (:name font)]])) + [:div {:class (stl/css :font-wrapper) + :style style + :ref item-ref + :on-click on-click} + [:div {:class (stl/css-case :font-item true + :selected current?)} + [:span {:class (stl/css :label)} (:name font)] + [:span {:class (stl/css :icon)} (when current? i/tick)]]])) (declare row-renderer) @@ -87,25 +92,18 @@ (comp (filter #(contains? backends (:backend %)))))] (into [] xform fonts))) -;; (defn- toggle-backend -;; [backends id] -;; (if (contains? backends id) -;; (disj backends id) -;; (conj backends id))) - (mf/defc font-selector - [{:keys [on-select on-close current-font show-recent] :as props}] - (let [selected (mf/use-state current-font) - state (mf/use-state {:term "" :backends #{}}) + [{:keys [on-select on-close current-font show-recent full-size] :as props}] + (let [selected (mf/use-state current-font) + state (mf/use-state {:term "" :backends #{}}) - flist (mf/use-ref) - input (mf/use-ref) + flist (mf/use-ref) + input (mf/use-ref) - fonts (mf/use-memo (mf/deps @state) #(filter-fonts @state @fonts/fonts)) - fontsdb (mf/deref fonts/fontsdb) - ;; Filtering deleted fonts - recent-fonts (->> (mf/deref refs/workspace-recent-fonts) - (into [] (filter #(some? (get fontsdb (:id %)))))) + fonts (mf/use-memo (mf/deps @state) #(filter-fonts @state @fonts/fonts)) + recent-fonts (mf/deref refs/workspace-recent-fonts) + + full-size? (boolean (and full-size show-recent)) select-next (mf/use-fn @@ -136,10 +134,8 @@ on-filter-change (mf/use-fn - (mf/deps) (fn [event] - (let [value (dom/get-target-val event)] - (swap! state assoc :term value)))) + (swap! state assoc :term event))) on-select-and-close (mf/use-fn @@ -148,8 +144,8 @@ (on-select font) (on-close)))] - (mf/with-effect [] - (st/emit! (fts/load-recent-fonts))) + (mf/with-effect [fonts] + (st/emit! (fts/load-recent-fonts fonts))) (mf/with-effect [fonts] (let [key (events/listen js/document "keydown" on-key-down)] @@ -175,48 +171,25 @@ #(let [offset (.getOffsetForRow ^js inst #js {:alignment "center" :index index})] (.scrollToPosition ^js inst offset))))) - [:div.font-selector - [:div.font-selector-dropdown - [:header - [:input {:placeholder (tr "workspace.options.search-font") - :value (:term @state) - :ref input - :spell-check false - :on-change on-filter-change}] + [:div {:class (stl/css :font-selector)} + [:div {:class (stl/css-case :font-selector-dropdown true :font-selector-dropdown-full-size full-size?)} + [:div {:class (stl/css :header)} + [:& search-bar {:on-change on-filter-change + :value (:term @state) + :auto-focus true + :placeholder (tr "workspace.options.search-font")}] (when (and recent-fonts show-recent) - [:* - [:hr] - [:p.title (tr "workspace.options.recent-fonts")] + [:section {:class (stl/css :show-recent)} + [:p {:class (stl/css :title)} (tr "workspace.options.recent-fonts")] (for [[idx font] (d/enumerate recent-fonts)] [:& font-item {:key (dm/str "font-" idx) :font font :style {} :on-click on-select-and-close - :current? (= (:id font) (:id @selected))}])]) + :current? (= (:id font) (:id @selected))}])])] - #_[:div.options - {:on-click #(swap! state assoc :show-options true) - :class (when (seq (:backends @state)) "active")} - i/picker-hsv] - - #_[:& dropdown {:show (:show-options @state false) - :on-close #(swap! state dissoc :show-options)} - (let [backends (:backends @state)] - [:div.backend-filters.dropdown {:ref ddown} - [:div.backend-filter - {:class (when (backends :custom) "selected") - :on-click #(swap! state update :backends toggle-backend :custom)} - [:div.checkbox-icon i/tick] - [:div.backend-name (tr "labels.custom-fonts")]] - [:div.backend-filter - {:class (when (backends :google) "selected") - :on-click #(swap! state update :backends toggle-backend :google)} - [:div.checkbox-icon i/tick] - [:div.backend-name "Google Fonts"]]])]] - - [:hr] - - [:div.fonts-list + [:div {:class (stl/css-case :fonts-list true + :fonts-list-full-size full-size?)} [:> rvt/AutoSizer {} (fn [props] (let [width (unchecked-get props "width") @@ -227,8 +200,9 @@ :ref flist :width width :rowCount (count fonts) - :rowHeight 32 + :rowHeight 36 :rowRenderer render}])))]]]])) + (defn row-renderer [fonts selected on-select props] (let [index (unchecked-get props "index") @@ -244,7 +218,7 @@ (mf/defc font-options {::mf/wrap-props false} - [{:keys [values on-change on-blur show-recent]}] + [{:keys [values on-change on-blur show-recent full-size-selector]}] (let [{:keys [font-id font-size font-variant-id]} values font-id (or font-id (:font-id txt/default-text-attrs)) @@ -253,9 +227,7 @@ fonts (mf/deref fonts/fontsdb) font (get fonts font-id) - ;; Filtering deleted fonts - recent-fonts (->> (mf/deref refs/workspace-recent-fonts) - (into [] (filter #(some? (get fonts (:id %)))))) + recent-fonts (mf/deref refs/workspace-recent-fonts) last-font (mf/use-ref nil) open-selector? (mf/use-state false) @@ -283,15 +255,14 @@ on-font-variant-change (mf/use-fn (mf/deps font on-change) - (fn [event] - (let [new-variant-id (dom/get-target-val event) - variant (d/seek #(= new-variant-id (:id %)) (:variants font))] + (fn [new-variant-id] + (let [variant (d/seek #(= new-variant-id (:id %)) (:variants font))] (on-change {:font-id (:id font) :font-family (:family font) :font-variant-id new-variant-id :font-weight (:weight variant) :font-style (:style variant)}) - (dom/blur! (dom/get-target event))))) + (dom/blur! (dom/get-target new-variant-id))))) on-font-select (mf/use-fn @@ -310,8 +281,7 @@ (when (some? on-blur) (on-blur)) (when (mf/ref-val last-font) - (st/emit! (fts/add-recent-font (mf/ref-val last-font)))) - ))] + (st/emit! (fts/add-recent-font (mf/ref-val last-font))))))] [:* (when @open-selector? @@ -319,49 +289,60 @@ {:current-font font :on-close on-font-selector-close :on-select on-font-select + :full-size full-size-selector :show-recent show-recent}]) - [:div.row-flex - [:div.input-select.font-option - {:on-click #(reset! open-selector? true)} - (cond - (= :multiple font-id) - "--" + [:div {:class (stl/css :font-option) + :on-click #(reset! open-selector? true)} + (cond + (= :multiple font-id) + "--" - (some? font) - (:name font) + (some? font) + [:* + [:span {:class (stl/css :name)} + (:name font)] + [:span {:class (stl/css :icon)} + i/arrow]] - :else - (tr "dashboard.fonts.deleted-placeholder"))]] + :else + (tr "dashboard.fonts.deleted-placeholder"))] + [:div {:class (stl/css :font-modifiers)} + [:div {:class (stl/css :font-size-options)} + (let [size-options [8 9 10 11 12 14 16 18 24 36 48 72] + size-options (if (= font-size :multiple) (into [""] size-options) size-options)] + [:& editable-select + {:value (attr->string font-size) + :class (stl/css :font-size-select) + :input-class (stl/css :numeric-input) + :options size-options + :type "number" + :placeholder "--" + :min 3 + :max 1000 + :on-change on-font-size-change + :on-blur on-blur}])] - [:div.row-flex - (let [size-options [8 9 10 11 12 14 16 18 24 36 48 72] - size-options (if (= font-size :multiple) (into [""] size-options) size-options)] - [:& editable-select - {:value (attr->string font-size) - :class "input-option size-option" - :options size-options - :type "number" - :placeholder "--" - :min 3 - :max 1000 - :on-change on-font-size-change - :on-blur on-blur}]) - - [:select.input-select.variant-option - {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :disabled (= font-id :multiple) - :value (attr->string font-variant-id) - :on-change on-font-variant-change - :on-blur on-blur} - (when (or (= font-id :multiple) (= font-variant-id :multiple)) - [:option {:value ""} "--"]) - (for [variant (:variants font)] - [:option {:value (:id variant) - :key (pr-str variant)} - (:name variant)])]]])) - + [:div {:class (stl/css :font-variant-options)} + (let [basic-variant-options (->> (:variants font) + (map (fn [variant] + {:value (:id variant) + :key (pr-str variant) + :label (:name variant)}))) + variant-options (if (= font-size :multiple) + (conj basic-variant-options + {:value :multiple + :key :multiple-variants + :label "--"}) + basic-variant-options)] + ;; TODO Add disabled mode + [:& select + {:class (stl/css :font-variant-select) + :default-value (attr->string font-variant-id) + :options variant-options + :on-change on-font-variant-change + :on-blur on-blur}])]]])) (mf/defc spacing-options {::mf/wrap-props false} @@ -371,37 +352,39 @@ line-height (or line-height "1.2") letter-spacing (or letter-spacing "0") - line-height-nillable (if (= (str line-height) "1.2") false true) handle-change (fn [value attr] (on-change {attr (str value)}))] - [:div.spacing-options - [:div.input-icon - [:span.icon-before.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.line-height")} - i/line-height] - [:> numeric-input + [:div {:class (stl/css :spacing-options)} + [:div {:class (stl/css :line-height)} + [:span {:class (stl/css :icon) + :alt (tr "workspace.options.text-options.line-height")} + i/text-lineheight] + [:> numeric-input* {:min -200 :max 200 :step 0.1 :default "1.2" + :class (stl/css :line-height-input) :value (attr->string line-height) :placeholder (tr "settings.multiple") :nillable line-height-nillable :on-change #(handle-change % :line-height) :on-blur on-blur}]] - [:div.input-icon - [:span.icon-before.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.letter-spacing")} - i/letter-spacing] - [:> numeric-input + [:div {:class (stl/css :letter-spacing)} + [:span + {:class (stl/css :icon) + :alt (tr "workspace.options.text-options.letter-spacing")} + i/text-letterspacing] + [:> numeric-input* {:min -200 :max 200 :step 0.1 + :class (stl/css :letter-spacing-input) :value (attr->string letter-spacing) :placeholder (tr "settings.multiple") :on-change #(handle-change % :letter-spacing) @@ -412,60 +395,144 @@ [{:keys [values on-change on-blur]}] (let [text-transform (or (:text-transform values) "none") handle-change - (fn [_ type] - (on-change {:text-transform type}) + (fn [type] + (if (= text-transform type) + (on-change {:text-transform "unset"}) + (on-change {:text-transform type})) (when (some? on-blur) (on-blur)))] - [:div.align-icons - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.none") - :class (dom/classnames :current (= "none" text-transform)) - :on-focus #(dom/prevent-default %) - :on-click #(handle-change % "none")} - i/minus] - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.uppercase") - :class (dom/classnames :current (= "uppercase" text-transform)) - :on-click #(handle-change % "uppercase")} - i/uppercase] - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.lowercase") - :class (dom/classnames :current (= "lowercase" text-transform)) - :on-click #(handle-change % "lowercase")} - i/lowercase] - [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.titlecase") - :class (dom/classnames :current (= "capitalize" text-transform)) - :on-click #(handle-change % "capitalize")} - i/titlecase]])) -(mf/defc typography-options + [:div {:class (stl/css :text-transform)} + [:& radio-buttons {:selected text-transform + :on-change handle-change + :name "text-transform"} + [:& radio-button {:icon i/text-uppercase + :type "checkbox" + :value "uppercase" + :id "text-transform-uppercase"}] + [:& radio-button {:icon i/text-mixed + :type "checkbox" + :value "capitalize" + :id "text-transform-capitalize"}] + [:& radio-button {:icon i/text-lowercase + :type "checkbox" + :value "lowercase" + :id "text-transform-lowercase"}]]])) + +(mf/defc text-options {::mf/wrap-props false} [{:keys [ids editor values on-change on-blur show-recent]}] - (let [opts #js {:editor editor + (let [full-size-selector? (and show-recent (= (mf/use-ctx ctx/sidebar) :right)) + opts #js {:editor editor :ids ids :values values :on-change on-change :on-blur on-blur - :show-recent show-recent}] - [:div.element-set-content + :show-recent show-recent + :full-size-selector full-size-selector?}] + [:div {:class (stl/css-case :text-options true + :text-options-full-size full-size-selector?)} [:> font-options opts] - [:div.row-flex - [:> spacing-options opts]] - [:div.row-flex + [:div {:class (stl/css :typography-variations)} + [:> spacing-options opts] [:> text-transform-options opts]]])) +(mf/defc typography-advanced-options + {::mf/wrap [mf/memo]} + [{:keys [visible? typography editable? name-input-ref on-close on-change on-name-blur local? navigate-to-library on-key-down]}] + (let [ref (mf/use-ref nil) + font-data (fonts/get-font-data (:font-id typography))] + (fonts/ensure-loaded! (:font-id typography)) + + (mf/use-effect + (mf/deps visible?) + (fn [] + (when-let [node (mf/ref-val ref)] + (when visible? + (dom/scroll-into-view-if-needed! node))))) + + (when visible? + [:div {:ref ref + :class (stl/css :advanced-options-wrapper)} + + (if ^boolean editable? + [:* + [:div {:class (stl/css :font-name-wrapper)} + [:div {:class (stl/css :typography-sample-input) + :style {:font-family (:font-family typography) + :font-weight (:font-weight typography) + :font-style (:font-style typography)}} + (tr "workspace.assets.typography.sample")] + + [:input + {:class (stl/css :adv-typography-name) + :type "text" + :ref name-input-ref + :default-value (:name typography) + :on-key-down on-key-down + :on-blur on-name-blur}] + + [:div {:class (stl/css :action-btn) + :on-click on-close} + i/tick]] + + [:& text-options {:values typography + :on-change on-change + :show-recent false}]] + + [:div {:class (stl/css :typography-info-wrapper)} + [:div {:class (stl/css :typography-name-wrapper)} + [:div {:class (stl/css :typography-sample) + + :style {:font-family (:font-family typography) + :font-weight (:font-weight typography) + :font-style (:font-style typography)}} + (tr "workspace.assets.typography.sample")] + + [:div {:class (stl/css :typography-name) + :title (:name typography)} + (:name typography)] + [:span {:class (stl/css :typography-font)} + (:name font-data)] + [:div {:class (stl/css :action-btn) + :on-click on-close} + i/menu]] + + [:div {:class (stl/css :info-row)} + [:span {:class (stl/css :info-label)} (tr "workspace.assets.typography.font-variant-id")] + [:span {:class (stl/css :info-content)} (:font-variant-id typography)]] + + [:div {:class (stl/css :info-row)} + [:span {:class (stl/css :info-label)} (tr "workspace.assets.typography.font-size")] + [:span {:class (stl/css :info-content)} (:font-size typography)]] + + [:div {:class (stl/css :info-row)} + [:span {:class (stl/css :info-label)} (tr "workspace.assets.typography.line-height")] + [:span {:class (stl/css :info-content)} (:line-height typography)]] + + [:div {:class (stl/css :info-row)} + [:span {:class (stl/css :info-label)} (tr "workspace.assets.typography.letter-spacing")] + [:span {:class (stl/css :info-content)} (:letter-spacing typography)]] + + [:div {:class (stl/css :info-row)} + [:span {:class (stl/css :info-label)} (tr "workspace.assets.typography.text-transform")] + [:span {:class (stl/css :info-content)} (:text-transform typography)]] + + (when-not local? + [:a {:class (stl/css :link-btn) + :on-click navigate-to-library} + (tr "workspace.assets.typography.go-to-edit")])])]))) + (mf/defc typography-entry {::mf/wrap-props false} - [{:keys [file-id typography local? selected? on-click on-change on-detach on-context-menu editing? focus-name? external-open*]}] - (let [hover-detach* (mf/use-state false) - hover-detach? (deref hover-detach*) - - name-input-ref (mf/use-ref) + [{:keys [file-id typography local? selected? on-click on-change on-detach on-context-menu editing? renaming? focus-name? external-open*]}] + (let [name-input-ref (mf/use-ref) read-only? (mf/use-ctx ctx/workspace-read-only?) editable? (and local? (not read-only?)) open* (mf/use-state editing?) open? (deref open*) + font-data (fonts/get-font-data (:font-id typography)) + name-only? (= (:name typography) (:name font-data)) on-name-blur (mf/use-fn @@ -473,13 +540,8 @@ (fn [event] (let [name (dom/get-target-val event)] (when-not (str/blank? name) - (on-change {:name name}))))) - - on-pointer-enter - (mf/use-fn #(reset! hover-detach* true)) - - on-pointer-leave - (mf/use-fn #(reset! hover-detach* false)) + (on-change {:name name}) + (st/emit! #(update % :workspace-global dissoc :rename-typography)))))) on-open (mf/use-fn #(reset! open* true)) @@ -494,7 +556,16 @@ (when file-id (st/emit! (dw/navigate-to-library file-id))))) - ] + on-key-down + (mf/use-fn + (fn [event] + (let [enter? (kbd/enter? event) + esc? (kbd/esc? event) + input-node (dom/get-target event)] + (when ^boolean enter? + (dom/blur! input-node)) + (when ^boolean esc? + (dom/blur! input-node)))))] (mf/with-effect [editing?] (when editing? @@ -512,86 +583,61 @@ (dom/select-text! node))))) [:* - [:div.element-set-options-group.typography-entry - {:class (when ^boolean selected? "selected") - :style {:display (when ^boolean open? "none")}} - [:div.typography-selection-wrapper - {:class (when ^boolean on-click "is-selectable") - :on-click on-click - :on-context-menu on-context-menu} - [:div.typography-sample - {:style {:font-family (:font-family typography) - :font-weight (:font-weight typography) - :font-style (:font-style typography)}} - (tr "workspace.assets.typography.sample")] - [:div.typography-name {:title (:name typography)}(:name typography)]] - [:div.element-set-actions + [:div {:class (stl/css-case :typography-entry true + :selected ^boolean selected?) + :style {:display (when ^boolean open? "none")}} + (if renaming? + [:div {:class (stl/css :font-name-wrapper)} + [:div + {:class (stl/css :typography-sample-input) + :style {:font-family (:font-family typography) + :font-weight (:font-weight typography) + :font-style (:font-style typography)}} + (tr "workspace.assets.typography.sample")] + + [:input + {:class (stl/css :adv-typography-name) + :type "text" + :ref name-input-ref + :default-value (:name typography) + :on-key-down on-key-down + :on-blur on-name-blur}]] + [:div + {:class (stl/css-case :typography-selection-wrapper true + :is-selectable ^boolean on-click) + :on-click on-click + :on-context-menu on-context-menu} + [:div + {:class (stl/css :typography-sample) + :style {:font-family (:font-family typography) + :font-weight (:font-weight typography) + :font-style (:font-style typography)}} + (tr "workspace.assets.typography.sample")] + + [:div {:class (stl/css :typography-name) + :title (:name typography)} (:name typography)] + + (when-not name-only? + [:div {:class (stl/css :typography-font) + :title (:name font-data)} + (:name font-data)])]) + [:div {:class (stl/css :element-set-actions)} (when ^boolean on-detach - [:div.element-set-actions-button - {:on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave - :on-click on-detach} - (if ^boolean hover-detach? i/unchain i/chain)]) + [:button {:class (stl/css :element-set-actions-button) + :on-click on-detach} + i/detach]) + [:button {:class (stl/css :menu-btn) + :on-click on-open} + i/menu]]] - [:div.element-set-actions-button - {:on-click on-open} - i/actions]]] - - [:& advanced-options {:visible? open? :on-close on-close} - (if ^boolean editable? - [:* - [:div.element-set-content - [:div.row-flex - [:input.element-name.adv-typography-name - {:type "text" - :ref name-input-ref - :default-value (:name typography) - :on-blur on-name-blur}] - - [:div.element-set-actions-button - {:on-click on-close} - i/actions]]] - - [:& typography-options {:values typography - :on-change on-change - :show-recent false}]] - - [:div.element-set-content.typography-read-only-data - [:div.row-flex.typography-name - [:span {:title (:name typography)} (:name typography)]] - - [:div.row-flex - [:span.label (tr "workspace.assets.typography.font-id")] - [:span (:font-id typography)]] - - [:div.element-set-actions-button.actions-inside - {:on-click on-close} - i/actions] - - [:div.row-flex - [:span.label (tr "workspace.assets.typography.font-variant-id")] - [:span (:font-variant-id typography)]] - - [:div.row-flex - [:span.label (tr "workspace.assets.typography.font-size")] - [:span (:font-size typography)]] - - [:div.row-flex - [:span.label (tr "workspace.assets.typography.line-height")] - [:span (:line-height typography)]] - - [:div.row-flex - [:span.label (tr "workspace.assets.typography.letter-spacing")] - [:span (:letter-spacing typography)]] - - [:div.row-flex - [:span.label (tr "workspace.assets.typography.text-transform")] - [:span (:text-transform typography)]] - - (when-not local? - [:div.row-flex - [:a.go-to-lib-button - {:on-click navigate-to-library} - (tr "workspace.assets.typography.go-to-edit")]])] - - )]])) + [:& typography-advanced-options + {:visible? open? + :on-close on-close + :typography typography + :editable? editable? + :name-input-ref name-input-ref + :on-change on-change + :on-name-blur on-name-blur + :on-key-down on-key-down + :local? local? + :navigate-to-library navigate-to-library}]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss new file mode 100644 index 0000000000..b5c417a417 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss @@ -0,0 +1,452 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.typography-entry { + display: flex; + flex-direction: row; + align-items: center; + height: $s-32; + width: 100%; + border-radius: $br-8; + background-color: var(--assets-item-background-color); + color: var(--assets-item-name-foreground-color-hover); + &:hover, + &:focus-within { + background-color: var(--assets-item-background-color-hover); + color: var(--assets-item-name-foreground-color-hover); + } + + &.selected { + border: $s-1 solid var(--assets-item-border-color); + } + + .element-set-actions { + display: flex; + visibility: hidden; + .element-set-actions-button, + .menu-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } + &:active { + background-color: transparent; + } + } + } + + &:hover { + background-color: var(--assets-item-background-color-hover); + .element-set-actions { + visibility: visible; + } + } +} + +.typography-selection-wrapper { + display: grid; + grid-template-columns: $s-24 auto 1fr; + flex: 1; + height: 100%; + width: 100%; + padding: 0 $s-12; + + &.is-selectable { + cursor: pointer; + } +} + +.typography-sample { + @include flexCenter; + min-width: $s-24; + height: $s-32; + color: var(--assets-item-name-foreground-color); +} + +.typography-name, +.typography-font { + @include bodySmallTypography; + @include textEllipsis; + display: flex; + align-items: center; + justify-content: flex-start; + margin-left: $s-6; + color: var(--assets-item-name-foreground-color); +} + +.typography-font { + display: flex; + align-items: center; + justify-content: flex-start; + min-width: 0; + color: var(--assets-item-name-foreground-color-rest); +} + +.font-name-wrapper { + @include bodySmallTypography; + display: flex; + align-items: center; + height: $s-32; + width: 100%; + border-radius: $br-8; + border: $s-1 solid transparent; + box-sizing: border-box; + background-color: var(--assets-item-background-color); + margin-bottom: $s-4; + padding: $s-8 $s-0 $s-8 $s-12; + + .typography-sample-input { + @include flexCenter; + width: $s-24; + height: 100%; + font-size: $fs-16; + color: var(--assets-item-name-foreground-color-hover); + } + .adv-typography-name { + @include removeInputStyle; + font-size: $fs-12; + color: var(--input-foreground-color-active); + flex-grow: 1; + padding-left: $s-6; + margin: 0; + } + .action-btn { + @extend .button-tertiary; + @include flexCenter; + width: $s-28; + height: $s-28; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + &:active { + background-color: transparent; + } + } + &:focus-within { + border: $s-1 solid var(--input-border-color-active); + .adv-typography-name { + color: var(--input-foreground-color-active); + } + } + &:hover { + background-color: var(--assets-item-background-color-hover); + } +} + +.advanced-options-wrapper { + height: 100%; + width: 100%; + background-color: var(--assets-title-background-color); +} + +.typography-info-wrapper { + @include flexColumn; + margin-bottom: $s-12; + .typography-name-wrapper { + @extend .asset-element; + display: grid; + grid-template-columns: $s-24 auto 1fr $s-28; + flex: 1; + height: $s-32; + width: 100%; + padding: 0 0 0 $s-12; + background-color: var(--assets-item-background-color-hover); + margin-bottom: $s-4; + .typography-sample { + @include flexCenter; + min-width: $s-24; + font-size: $fs-16; + height: $s-32; + padding: 0; + color: var(--assets-item-name-foreground-color-hover); + } + .typography-name { + @include bodySmallTypography; + @include textEllipsis; + display: flex; + align-items: center; + justify-content: flex-start; + margin-left: $s-6; + color: var(--assets-item-name-foreground-color-hover); + } + .typography-font { + @include bodySmallTypography; + @include textEllipsis; + margin-left: $s-6; + display: flex; + align-items: center; + justify-content: flex-start; + min-width: 0; + color: var(--assets-item-name-foreground-color); + } + .action-btn { + @extend .button-tertiary; + width: $s-28; + height: $s-32; + svg { + @extend .button-icon; + } + &:active { + background-color: transparent; + } + } + } + + .info-row { + display: grid; + grid-template-columns: 50% 50%; + height: $s-32; + --calcualted-width: calc(var(--width) - $s-48); + padding-left: $s-2; + .info-label { + @include bodySmallTypography; + @include textEllipsis; + width: calc(var(--calcualted-width) / 2); + padding-top: $s-8; + color: var(--assets-item-name-foreground-color); + } + .info-content { + @include bodySmallTypography; + @include textEllipsis; + padding-top: $s-8; + width: calc(var(--calcualted-width) / 2); + color: var(--assets-item-name-foreground-color-hover); + } + } + + .link-btn { + @include uppercaseTitleTipography; + @extend .button-secondary; + width: 100%; + height: $s-32; + border-radius: $br-8; + &:hover { + background-color: var(--button-secondary-background-color-hover); + color: var(--button-secondary-foreground-color-hover); + border: $s-1 solid var(--button-secondary-border-color-hover); + text-decoration: none; + svg { + stroke: var(--button-secondary-foreground-color-hover); + } + } + &:focus { + background-color: var(--button-secondary-background-color-focus); + color: var(--button-secondary-foreground-color-focus); + border: $s-1 solid var(--button-secondary-border-color-focus); + svg { + stroke: var(--button-secondary-foreground-color-focus); + } + } + } +} + +.text-options { + @include flexColumn; + &:not(.text-options-full-size) { + position: relative; + } + .font-option { + @include bodySmallTypography; + @extend .asset-element; + padding: $s-8 0 $s-8 $s-8; + cursor: pointer; + .name { + flex-grow: 1; + } + .icon { + @include flexCenter; + height: $s-28; + width: $s-28; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + transform: rotate(90deg); + } + } + } + .font-modifiers { + display: flex; + gap: $s-4; + .font-size-options { + @extend .asset-element; + @include bodySmallTypography; + flex-grow: 1; + width: $s-60; + margin: 0; + padding: 0; + border: $s-1 solid var(--input-border-color); + position: relative; + + .icon { + @include flexCenter; + height: $s-28; + min-width: $s-28; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + transform: rotate(90deg); + } + } + } + .font-variant-options { + padding: 0; + flex-grow: 2; + } + } + .typography-variations { + @include flexRow; + .spacing-options { + @include flexRow; + .line-height, + .letter-spacing { + @extend .input-element; + .icon { + @include flexCenter; + width: $s-28; + svg { + @extend .button-icon-small; + } + } + } + } + .text-transform { + @extend .asset-element; + width: fit-content; + padding: 0; + background-color: var(--radio-btns-background-color); + &:hover { + background-color: var(--radio-btns-background-color); + } + } + } +} + +.font-size-select { + @include removeInputStyle; + @include bodySmallTypography; + height: $s-32; + height: 100%; + width: 100%; + margin: 0; + padding: $s-8; + .numeric-input { + @extend .input-base; + padding: 0; + } +} + +.font-selector { + @include flexCenter; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + width: 100%; + z-index: $z-index-4; +} + +.show-recent { + border-radius: $br-8 $br-8 0 0; + background: var(--dropdown-background-color); + border: $s-1 solid var(--color-background-quaternary); + border-block-end: none; +} + +.font-selector-dropdown { + width: 100%; + &:not(.font-selector-dropdown-full-size) { + display: flex; + flex-direction: column; + flex-grow: 1; + height: 100%; + } + .header { + display: grid; + row-gap: $s-2; + .title { + @include uppercaseTitleTipography; + color: var(--title-foreground-color); + margin: 0; + padding: $s-12; + } + } +} + +.font-wrapper { + padding-bottom: $s-4; + cursor: pointer; +} + +.font-item { + @extend .asset-element; + margin-bottom: $s-4; + border-radius: $br-8; + display: flex; + .icon { + @include flexCenter; + height: $s-28; + width: $s-28; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } + &.selected { + color: var(--assets-item-name-foreground-color-hover); + .icon { + svg { + stroke: var(--assets-item-name-foreground-color-hover); + } + } + } + + .label { + @include bodySmallTypography; + flex-grow: 1; + } +} + +.font-selector-dropdown-full-size { + height: calc(100vh - 48px); // TODO: ugly hack :( Find a workaround for this. + display: grid; + grid-template-rows: auto 1fr; + padding: $s-2 $s-12 $s-12 $s-12; +} + +.fonts-list { + position: relative; + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 100%; + width: 100%; + height: 100%; + padding: $s-2; + border-radius: $br-8; + background-color: var(--dropdown-background-color); + overflow: hidden; + &:not(.fonts-list-full-size) { + margin-block-start: $s-2; + } +} + +.fonts-list-full-size { + border-start-start-radius: 0; + border-start-end-radius: 0; + border: $s-1 solid var(--color-background-quaternary); + + // TODO: this should belong to typography-entry , but atm we don't have a clear + // way of accessing whether we are in fullsize mode or not + .selected { + padding-inline-end: 0; + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs index dd9341be88..791d7d3da7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs @@ -6,42 +6,40 @@ (ns app.main.ui.workspace.sidebar.options.page "Page options menu entries." + (:require-macros [app.main.style :as stl]) (:require [app.common.colors :as clr] [app.main.data.workspace :as dw] [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) (mf/defc options - {::mf/wrap [mf/memo]} + {::mf/wrap [mf/memo] + ::mf/wrap-props false} [] - (let [options (mf/deref refs/workspace-page-options) - - on-change - (fn [value] - (st/emit! (dw/change-canvas-color value))) - - on-open - (fn [] - (st/emit! (dwu/start-undo-transaction :options))) - - on-close - (fn [] - (st/emit! (dwu/commit-undo-transaction :options)))] - - [:div.element-set - [:div.element-set-title (tr "workspace.options.canvas-background")] - [:div.element-set-content - [:& color-row {:disable-gradient true - :disable-opacity true - :title (tr "workspace.options.canvas-background") - :color {:color (get options :background clr/canvas) - :opacity 1} - :on-change on-change - :on-open on-open - :on-close on-close}]]])) + (let [options (mf/deref refs/workspace-page-options) + on-change (mf/use-fn #(st/emit! (dw/change-canvas-color %))) + on-open (mf/use-fn #(st/emit! (dwu/start-undo-transaction :options))) + on-close (mf/use-fn #(st/emit! (dwu/commit-undo-transaction :options)))] + [:div {:class (stl/css :element-set)} + [:div {:class (stl/css :element-title)} + [:& title-bar {:collapsable false + :title (tr "workspace.options.canvas-background") + :class (stl/css :title-spacing-page)}]] + [:div {:class (stl/css :element-content)} + [:& color-row + {:disable-gradient true + :disable-opacity true + :disable-image true + :title (tr "workspace.options.canvas-background") + :color {:color (get options :background clr/canvas) + :opacity 1} + :on-change on-change + :on-open on-open + :on-close on-close}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/page.scss b/frontend/src/app/main/ui/workspace/sidebar/options/page.scss new file mode 100644 index 0000000000..da131647f1 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.scss @@ -0,0 +1,7 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 7bd25df76a..dcbabace67 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -5,17 +5,19 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.rows.color-row + (:require-macros [app.main.style :as stl]) (:require + [app.common.colors :as cc] [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages :as cp] + [app.common.types.shape.attrs :refer [default-color]] [app.main.data.modal :as modal] [app.main.data.workspace.libraries :as dwl] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.color-bullet :as cb] - [app.main.ui.components.color-input :refer [color-input]] - [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.color-input :refer [color-input*]] + [app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.context :as ctx] [app.main.ui.formats :as fmt] [app.main.ui.hooks :as h] @@ -25,6 +27,10 @@ [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) + +(def ^:private detach-icon + (i/icon-xref :detach (stl/css :detach-icon))) + (defn opacity->string [opacity] (if (= opacity :multiple) @@ -39,9 +45,9 @@ (if (= v :multiple) nil v)) (mf/defc color-row - [{:keys [index color disable-gradient disable-opacity on-change - on-reorder on-detach on-open on-close title on-remove - disable-drag on-focus on-blur select-only data-select-on-focus]}] + [{:keys [index color disable-gradient disable-opacity disable-image on-change + on-reorder on-detach on-open on-close on-remove + disable-drag on-focus on-blur select-only select-on-focus]}] (let [current-file-id (mf/use-ctx ctx/current-file-id) file-colors (mf/deref refs/workspace-file-colors) shared-libs (mf/deref refs/workspace-libraries) @@ -53,6 +59,39 @@ color-name (dm/get-in src-colors [(:id color) :name]) + multiple-colors? (uc/multiple? color) + library-color? (and (:id color) color-name (not multiple-colors?)) + gradient-color? (and (not multiple-colors?) + (:gradient color) + (get-in color [:gradient :type])) + image-color? (and (not multiple-colors?) + (:image color)) + + editing-text* (mf/use-state false) + editing-text? (deref editing-text*) + + opacity? + (and (not gradient-color?) + (not multiple-colors?) + (not library-color?) + (not disable-opacity)) + + on-focus + (mf/use-fn + (mf/deps on-focus) + (fn [event] + (reset! editing-text* true) + (when on-focus + (on-focus event)))) + + on-blur + (mf/use-fn + (mf/deps on-blur) + (fn [event] + (reset! editing-text* false) + (when on-blur + (on-blur event)))) + parse-color (mf/use-fn (fn [color] @@ -79,7 +118,7 @@ (assoc :color new-value) (dissoc :gradient))] (st/emit! (dwl/add-recent-color color) - (on-change color))))) + (on-change color))))) handle-opacity-change (mf/use-fn @@ -90,15 +129,15 @@ :id nil :file-id nil)] (st/emit! (dwl/add-recent-color color) - (on-change color))))) + (on-change color))))) handle-click-color (mf/use-fn - (mf/deps disable-gradient disable-opacity on-change on-close on-open) + (mf/deps disable-gradient disable-opacity disable-image on-change on-close on-open) (fn [color event] (let [color (cond - (uc/multiple? color) - {:color cp/default-color + multiple-colors? + {:color default-color :opacity 1} (= :multiple (:opacity color)) @@ -113,6 +152,7 @@ :y y :disable-gradient disable-gradient :disable-opacity disable-opacity + :disable-image disable-image ;; on-change second parameter means if the source is the color-picker :on-change #(on-change (merge uc/empty-color %) true) :on-close (fn [value opacity id file-id] @@ -125,7 +165,6 @@ (modal/show! :colorpicker props)))) - prev-color (h/use-previous color) on-drop @@ -149,73 +188,87 @@ (when (not= prev-color color) (modal/update-props! :colorpicker {:data (parse-color color)}))) - [:div.row-flex.color-data {:title title - :class (dom/classnames - :dnd-over-top (= (:over dprops) :top) - :dnd-over-bot (= (:over dprops) :bot)) - :ref dref} - [:& cb/color-bullet {:color (cond-> color - (nil? color-name) (assoc - :id nil - :file-id nil)) - :on-click handle-click-color}] + [:div {:class (stl/css-case + :color-data true + :dnd-over-top (= (:over dprops) :top) + :dnd-over-bot (= (:over dprops) :bot)) + :ref dref} + [:div {:class (stl/css :color-info)} + [:div {:class (stl/css-case :color-name-wrapper true + :no-opacity (or disable-opacity + (not opacity?)) + :library-name-wrapper library-color? + :editing editing-text? + :gradient-name-wrapper gradient-color?)} + [:div {:class (stl/css :color-bullet-wrapper)} + [:& cb/color-bullet {:color (cond-> color + (nil? color-name) (assoc + :id nil + :file-id nil)) + :mini? true + :on-click handle-click-color}]] + (cond + ;; Rendering a color with ID + library-color? + [:* + [:div {:class (stl/css :color-name) + :title (str color-name)} - (cond - ;; Rendering a color with ID - (and (:id color) color-name (not (uc/multiple? color))) - [:* - [:div.color-info - [:div.color-name (str color-name)]] - (when on-detach - [:div.element-set-actions-button - {:on-pointer-enter #(reset! hover-detach true) - :on-pointer-leave #(reset! hover-detach false) - :on-click detach-value} - (if @hover-detach i/unchain i/chain)]) + (str color-name)] + (when on-detach + [:button + {:class (stl/css :detach-btn) + :title (tr "settings.detach") + :on-pointer-enter #(reset! hover-detach true) + :on-pointer-leave #(reset! hover-detach false) + :on-click detach-value} + detach-icon])] - (when select-only - [:div.element-set-actions-button {:on-click handle-select} - i/pointer-inner])] + ;; Rendering a gradient + gradient-color? + [:* + [:div {:class (stl/css :color-name)} + (uc/gradient-type->string (get-in color [:gradient :type]))]] - ;; Rendering a gradient - (and (not (uc/multiple? color)) - (:gradient color) - (get-in color [:gradient :type])) - [:* - [:div.color-info - [:div.color-name (uc/gradient-type->string (get-in color [:gradient :type]))]] - (when select-only - [:div.element-set-actions-button {:on-click handle-select} - i/pointer-inner])] + ;; Rendering an image + image-color? + [:* + [:div {:class (stl/css :color-name)} + (tr "media.image")]] + ;; Rendering a plain color + :else + [:span {:class (stl/css :color-input-wrapper)} + [:> color-input* {:value (if multiple-colors? + "" + (-> color :color cc/remove-hash)) + :placeholder (tr "settings.multiple") + :className (stl/css :color-input) + :on-focus on-focus + :on-blur on-blur + :on-change handle-value-change}]])] - ;; Rendering a plain color/opacity - :else - [:* - [:div.color-info - [:> color-input {:value (if (uc/multiple? color) - "" - (-> color :color uc/remove-hash)) - :placeholder (tr "settings.multiple") - :on-focus on-focus - :on-blur on-blur - :on-change handle-value-change}]] + (when opacity? + [:div {:class (stl/css :opacity-element-wrapper)} + [:span {:class (stl/css :icon-text)} + "%"] + [:> numeric-input* {:value (-> color :opacity opacity->string) + :className (stl/css :opacity-input) + :placeholder "--" + :select-on-focus select-on-focus + :on-focus on-focus + :on-blur on-blur + :on-change handle-opacity-change + :default 100 + :min 0 + :max 100}]])] - (when (and (not disable-opacity) - (not (:gradient color))) - [:div.input-element - {:class (dom/classnames :percentail (not= (:opacity color) :multiple))} - [:> numeric-input {:value (-> color :opacity opacity->string) - :placeholder (tr "settings.multiple") - :data-select-on-focus data-select-on-focus - :on-focus on-focus - :on-blur on-blur - :on-change handle-opacity-change - :min 0 - :max 100}]]) - (when select-only - [:div.element-set-actions-button {:on-click handle-select} - i/pointer-inner])]) (when (some? on-remove) - [:div.element-set-actions-button.remove {:on-click on-remove} i/minus])])) + [:button {:class (stl/css :remove-btn) + :on-click on-remove} i/remove-icon]) + (when select-only + [:button {:class (stl/css :select-btn) + :title (tr "settings.select-this-color") + :on-click handle-select} + i/move])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss new file mode 100644 index 0000000000..cf3ed21e05 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss @@ -0,0 +1,192 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.color-data { + @include flexRow; + + &.dnd-over-top { + border-block-start: $s-1 solid var(--layer-row-foreground-color-drag); + } + &.dnd-over-bot { + border-block-end: $s-1 solid var(--layer-row-foreground-color-drag); + } +} + +.color-info { + --detach-icon-foreground-color: none; + + display: grid; + flex: 1; + grid-template-columns: 1fr auto; + align-items: center; + gap: $s-2; + border-radius: $s-8; + background-color: var(--input-details-color); + height: $s-32; + + &:hover { + --detach-icon-foreground-color: var(--input-foreground-color-active); + + .detach-btn, + .select-btn { + background-color: transparent; + } + } +} + +.color-name-wrapper { + @extend .input-element; + flex-grow: 1; + width: 100%; + min-width: 0; + border-radius: $br-8 0 0 $br-8; + padding: 0; + margin-inline-end: 0; + gap: $s-4; + + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + input { + padding: 0; + } + .color-bullet-wrapper { + height: $s-28; + padding: 0 $s-2 0 $s-8; + border-radius: $br-8 0 0 $br-8; + background-color: transparent; + display: flex; + align-items: center; + &:hover { + background-color: transparent; + } + } + .color-name { + @include bodySmallTypography; + @include textEllipsis; + padding-inline: $s-6; + border-radius: $br-8; + color: var(--input-foreground-color-active); + } + .detach-btn { + @extend .button-tertiary; + height: $s-28; + width: $s-28; + margin-inline-start: auto; + border-radius: 0 $br-8 $br-8 0; + display: none; + } + .detach-icon { + @extend .button-icon; + stroke: var(--detach-icon-foreground-color); + } + .color-input-wrapper { + @include bodySmallTypography; + display: flex; + align-items: center; + height: $s-28; + padding: 0 $s-0; + width: 100%; + margin: 0; + flex-grow: 1; + background-color: var(--input-background-color); + color: var(--input-foreground-color); + border-radius: $br-0; + } + &.no-opacity { + border-radius: $br-8; + .color-input-wrapper { + border-radius: $br-8; + } + } + &:hover { + --detach-icon-foreground-color: var(--input-foreground-color-active); + + background-color: var(--input-background-color-hover); + border: $s-1 solid var(--input-border-color-hover); + .color-bullet-wrapper, + .color-name, + .detach-btn, + .color-input-wrapper { + background-color: var(--input-background-color-hover); + } + .detach-btn { + display: flex; + } + &.editing { + background-color: var(--input-background-color-active); + .color-bullet-wrapper, + .color-name, + .detach-btn, + .color-input-wrapper { + background-color: var(--input-background-color-active); + } + } + &:focus, + &:focus-within { + background-color: var(--input-background-color-focus); + border: $s-1 solid var(--input-border-color-focus); + } + } + + &:focus, + &:focus-within { + background-color: var(--input-background-color-focus); + border: $s-1 solid var(--input-border-color-focus); + &:hover { + background-color: var(--input-background-color-hover); + border: $s-1 solid var(--input-border-color-focus); + } + } + + &.editing { + background-color: var(--input-background-color-active); + &:hover { + border: $s-1 solid var(--input-border-color-active); + } + } +} + +.gradient-name-wrapper { + border-radius: 0 $br-8 $br-8 0; + .color-name { + @include flexRow; + border-radius: 0 $br-8 $br-8 0; + } +} + +.library-name-wrapper { + border-radius: $br-8; +} + +.opacity-element-wrapper { + @extend .input-element; + width: $s-60; + border-radius: 0 $br-8 $br-8 0; + .opacity-input { + padding: 0; + border-radius: 0 $br-8 $br-8 0; + min-width: $s-28; + } + .icon-text { + @include flexCenter; + height: $s-32; + margin-inline-end: $s-4; + margin-block-start: $s-2; + } +} + +.remove-btn, +.select-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs deleted file mode 100644 index b7252590d6..0000000000 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs +++ /dev/null @@ -1,71 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.workspace.sidebar.options.rows.input-row - (:require - [app.main.ui.components.editable-select :refer [editable-select]] - [app.main.ui.components.numeric-input :refer [numeric-input]] - [app.main.ui.components.select :refer [select]] - [app.util.object :as obj] - [rumext.v2 :as mf])) - -(mf/defc input-row [{:keys [label options value class min max on-change type placeholder default nillable on-focus data-select-on-focus]}] - [:div.row-flex.input-row - [:span.element-set-subtitle label] - [:div.input-element {:class class} - - (case type - :select - [:& select {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :default-value value - :class "input-option" - :options options - :on-change on-change}] - - :editable-select - [:& editable-select {:value value - :class "input-option" - :options options - :type "number" - :min min - :max max - :placeholder placeholder - :on-change on-change}] - - :text - [:input {:value value - :class "input-text" - :on-change on-change} ] - - [:> numeric-input - {:placeholder placeholder - :min min - :max max - :default default - :nillable nillable - :on-change on-change - :on-focus on-focus - :data-select-on-focus data-select-on-focus - :value (or value "")}])]]) - - -;; NOTE: (by niwinz) this is a new version of input-row, I didn't -;; touched the original one because it is used in many sites and I -;; don't have intention to refactor all the code right now. We should -;; consider to use the new one and once we have migrated all to the -;; new component, we can proceed to rename it and delete the old one. - -(mf/defc input-row-v2 - {::mf/wrap-props false} - [props] - (let [label (obj/get props "label") - class (obj/get props "class") - children (obj/get props "children")] - [:div.row-flex.input-row - [:span.element-set-subtitle label] - [:div.input-element {:class class} - children]])) - diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index 52f6e6d804..7bcd264a95 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -5,58 +5,40 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.rows.stroke-row + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] - [app.common.data.macros :as dm] - [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.numeric-input :refer [numeric-input*]] + [app.main.ui.components.select :refer [select]] [app.main.ui.hooks :as h] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] - [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) -(defn- width->string [width] - (if (= width :multiple) - "" - (str (or width 1)))) - -(defn- enum->string [value] - (if (= value :multiple) - "" - (pr-str value))) - -(defn- stroke-cap-names [] - [[nil (tr "workspace.options.stroke-cap.none") false] - [:line-arrow (tr "workspace.options.stroke-cap.line-arrow") true] - [:triangle-arrow (tr "workspace.options.stroke-cap.triangle-arrow") false] - [:square-marker (tr "workspace.options.stroke-cap.square-marker") false] - [:circle-marker (tr "workspace.options.stroke-cap.circle-marker") false] - [:diamond-marker (tr "workspace.options.stroke-cap.diamond-marker") false] - [:round (tr "workspace.options.stroke-cap.round") true] - [:square (tr "workspace.options.stroke-cap.square") false]]) - -(defn- value->img [value] - (when (and value (not= value :multiple)) - (str "images/cap-" (name value) ".svg"))) - -(defn- value->name [value] - (if (= value :multiple) - "--" - (-> (d/seek #(= (first %) value) (stroke-cap-names)) - (second)))) - (mf/defc stroke-row {::mf/wrap-props false} - [{:keys [index stroke title show-caps on-color-change on-reorder on-color-detach on-remove on-stroke-width-change on-stroke-style-change on-stroke-alignment-change open-caps-select close-caps-select on-stroke-cap-start-change on-stroke-cap-end-change on-stroke-cap-switch disable-drag on-focus on-blur disable-stroke-style data-select-on-focus]}] - (let [start-caps-state (mf/use-state {:open? false - :top 0 - :left 0}) - end-caps-state (mf/use-state {:open? false - :top 0 - :left 0}) - on-drop + [{:keys [index + stroke + title + show-caps + on-color-change + on-reorder + on-color-detach + on-remove + on-stroke-width-change + on-stroke-style-change + on-stroke-alignment-change + on-stroke-cap-start-change + on-stroke-cap-end-change + on-stroke-cap-switch + disable-drag + on-focus + on-blur + disable-stroke-style + select-on-focus]}] + + (let [on-drop (fn [_ data] (on-reorder (:index data))) @@ -69,99 +51,162 @@ :data {:id (str "stroke-row-" index) :index index :name (str "Border row" index)}) - [nil nil])] + [nil nil]) - [:div.border-data {:class (dom/classnames + on-color-change-refactor + (mf/use-callback + (mf/deps index on-color-change) + (fn [color] + (on-color-change index color))) + + on-color-detach + (mf/use-callback + (mf/deps index on-color-detach) + (fn [color] + (on-color-detach index color))) + + on-remove + (mf/use-callback + (mf/deps index on-remove) + #(on-remove index)) + + stroke-width (:stroke-width stroke) + + on-width-change + (mf/use-callback + (mf/deps index on-stroke-width-change) + #(on-stroke-width-change index %)) + + stroke-alignment (or (:stroke-alignment stroke) :center) + + stroke-alignment-options + (mf/with-memo [stroke-alignment] + (d/concat-vec + (when (= :multiple stroke-alignment) + [{:value :multiple :label "--"}]) + [{:value :center :label (tr "workspace.options.stroke.center")} + {:value :inner :label (tr "workspace.options.stroke.inner")} + {:value :outer :label (tr "workspace.options.stroke.outer")}])) + + on-alignment-change + (mf/use-callback + (mf/deps index on-stroke-alignment-change) + #(on-stroke-alignment-change index (keyword %))) + + stroke-style (or (:stroke-style stroke) :solid) + + stroke-style-options + (mf/with-memo [stroke-style] + (d/concat-vec + (when (= :multiple stroke-style) + [{:value :multiple :label "--"}]) + [{:value :solid :label (tr "workspace.options.stroke.solid")} + {:value :dotted :label (tr "workspace.options.stroke.dotted")} + {:value :dashed :label (tr "workspace.options.stroke.dashed")} + {:value :mixed :label (tr "workspace.options.stroke.mixed")}])) + + on-style-change + (mf/use-callback + (mf/deps index on-stroke-style-change) + #(on-stroke-style-change index (keyword %))) + + on-caps-start-change + (mf/use-callback + (mf/deps index on-stroke-cap-start-change) + #(on-stroke-cap-start-change index (keyword %))) + + on-caps-end-change + (mf/use-callback + (mf/deps index on-stroke-cap-end-change) + #(on-stroke-cap-end-change index (keyword %))) + + stroke-caps-options + [{:value nil :label (tr "workspace.options.stroke-cap.none")} + :separator + {:value :line-arrow :label (tr "workspace.options.stroke-cap.line-arrow-short") :icon :stroke-arrow} + {:value :triangle-arrow :label (tr "workspace.options.stroke-cap.triangle-arrow-short") :icon :stroke-triangle} + {:value :square-marker :label (tr "workspace.options.stroke-cap.square-marker-short") :icon :stroke-rectangle} + {:value :circle-marker :label (tr "workspace.options.stroke-cap.circle-marker-short") :icon :stroke-circle} + {:value :diamond-marker :label (tr "workspace.options.stroke-cap.diamond-marker-short") :icon :stroke-diamond} + :separator + {:value :round :label (tr "workspace.options.stroke-cap.round") :icon :stroke-rounded} + {:value :square :label (tr "workspace.options.stroke-cap.square") :icon :stroke-squared}] + + on-cap-switch + (mf/use-callback + (mf/deps index on-stroke-cap-switch) + #(on-stroke-cap-switch index))] + + [:div {:class (stl/css-case + :stroke-data true :dnd-over-top (= (:over dprops) :top) :dnd-over-bot (= (:over dprops) :bot)) :ref dref} - ;; Stroke Color + ;; Stroke Color [:& color-row {:color {:color (:stroke-color stroke) :opacity (:stroke-opacity stroke) :id (:stroke-color-ref-id stroke) :file-id (:stroke-color-ref-file stroke) - :gradient (:stroke-color-gradient stroke)} + :gradient (:stroke-color-gradient stroke) + :image (:stroke-image stroke)} :index index :title title - :on-change (on-color-change index) - :on-detach (on-color-detach index) - :on-remove (on-remove index) + :on-change on-color-change-refactor + :on-detach on-color-detach + :on-remove on-remove :disable-drag disable-drag :on-focus on-focus - :data-select-on-focus data-select-on-focus + :select-on-focus select-on-focus :on-blur on-blur}] - ;; Stroke Width, Alignment & Style - [:div.row-flex - [:div.input-element - {:class (dom/classnames :pixels (not= (:stroke-width stroke) :multiple)) - :title (tr "workspace.options.stroke-width")} - - [:> numeric-input + ;; Stroke Width, Alignment & Style + [:div {:class (stl/css :stroke-options)} + [:div {:class (stl/css :stroke-width-input-element) + :title (tr "workspace.options.stroke-width")} + [:span {:class (stl/css :icon)} + i/stroke-size] + [:> numeric-input* {:min 0 - :value (-> (:stroke-width stroke) width->string) + :className (stl/css :stroke-width-input) + :value stroke-width :placeholder (tr "settings.multiple") - :on-change (on-stroke-width-change index) + :on-change on-width-change :on-focus on-focus - :data-select-on-focus data-select-on-focus + :select-on-focus select-on-focus :on-blur on-blur}]] - [:select#style.input-select {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :value (enum->string (:stroke-alignment stroke)) - :on-change (on-stroke-alignment-change index)} - (when (= (:stroke-alignment stroke) :multiple) - [:option {:value ""} "--"]) - [:option {:value ":center"} (tr "workspace.options.stroke.center")] - [:option {:value ":inner"} (tr "workspace.options.stroke.inner")] - [:option {:value ":outer"} (tr "workspace.options.stroke.outer")]] + [:div {:class (stl/css :select-wrapper) + :data-test "stroke.alignment"} + [:& select + {:default-value stroke-alignment + :options stroke-alignment-options + :on-change on-alignment-change}]] (when-not disable-stroke-style - [:select#style.input-select {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element - :value (enum->string (:stroke-style stroke)) - :on-change (on-stroke-style-change index)} - (when (= (:stroke-style stroke) :multiple) - [:option {:value ""} "--"]) - [:option {:value ":solid"} (tr "workspace.options.stroke.solid")] - [:option {:value ":dotted"} (tr "workspace.options.stroke.dotted")] - [:option {:value ":dashed"} (tr "workspace.options.stroke.dashed")] - [:option {:value ":mixed"} (tr "workspace.options.stroke.mixed")]])] + [:div {:class (stl/css :select-wrapper) + :data-test "stroke.style"} + [:& select + {:default-value stroke-style + :options stroke-style-options + :on-change on-style-change}]])] - ;; Stroke Caps + ;; Stroke Caps (when show-caps - [:div.row-flex - [:div.cap-select {:tab-index 0 ;; tab-index to make the element focusable - :on-click (open-caps-select start-caps-state)} - (value->name (:stroke-cap-start stroke)) - [:span.cap-select-button - i/arrow-down]] - [:& dropdown {:show (:open? @start-caps-state) - :on-close (close-caps-select start-caps-state)} - [:ul.dropdown.cap-select-dropdown {:style {:top (:top @start-caps-state) - :left (:left @start-caps-state)}} - (for [[idx [value label separator]] (d/enumerate (stroke-cap-names))] - (let [img (value->img value)] - [:li {:key (dm/str "start-cap-" idx) - :class (dom/classnames :separator separator) - :on-click #(on-stroke-cap-start-change index value)} - (when img [:img {:src (value->img value)}]) - label]))]] + [:div {:class (stl/css :stroke-caps-options)} + [:div {:class (stl/css :cap-select)} + [:& select + {:default-value (:stroke-cap-start stroke) + :dropdown-class (stl/css :stroke-cap-dropdown-start) + :options stroke-caps-options + :on-change on-caps-start-change}]] - [:div.element-set-actions-button {:on-click #(on-stroke-cap-switch index)} + [:button {:class (stl/css :swap-caps-btn) + :on-click on-cap-switch} i/switch] - [:div.cap-select {:tab-index 0 - :on-click (open-caps-select end-caps-state)} - (value->name (:stroke-cap-end stroke)) - [:span.cap-select-button - i/arrow-down]] - [:& dropdown {:show (:open? @end-caps-state) - :on-close (close-caps-select end-caps-state)} - [:ul.dropdown.cap-select-dropdown {:style {:top (:top @end-caps-state) - :left (:left @end-caps-state)}} - (for [[idx [value label separator]] (d/enumerate (stroke-cap-names))] - (let [img (value->img value)] - [:li {:key (dm/str "end-cap-" idx) - :class (dom/classnames :separator separator) - :on-click #(on-stroke-cap-end-change index value)} - (when img [:img {:src (value->img value)}]) - label]))]]])])) + [:div {:class (stl/css :cap-select)} + [:& select + {:default-value (:stroke-cap-end stroke) + :dropdown-class (stl/css :stroke-cap-dropdown) + :options stroke-caps-options + :on-change on-caps-end-change}]]])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss new file mode 100644 index 0000000000..5f49ab1675 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss @@ -0,0 +1,54 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.stroke-data { + @include flexColumn; + .stroke-options { + @include flexRow; + .stroke-width-input-element { + @extend .input-element; + width: $s-60; + } + .select-wrapper { + width: $s-124; + } + } + .stroke-caps-options { + @include flexRow; + .cap-select { + width: $s-124; + } + .stroke-cap-dropdown, + .stroke-cap-dropdown-start { + min-width: $s-124; + width: fit-content; + max-width: $s-252; + right: 0; + left: unset; + } + + .stroke-cap-dropdown-start { + left: 0; + right: unset; + } + .swap-caps-btn { + @extend .button-secondary; + height: $s-32; + width: $s-28; + svg { + @extend .button-icon; + } + } + } + &.dnd-over-top { + border-top: $s-1 solid var(--layer-row-foreground-color-drag); + } + &.dnd-over-bot { + border-bottom: $s-1 solid var(--layer-row-foreground-color-drag); + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs index 458acf5d91..4cf2ef29fd 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs @@ -8,11 +8,13 @@ (:require [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] + [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] - [app.main.ui.workspace.sidebar.options.menus.layout-container :refer [layout-container-flex-attrs layout-container-menu]] + [app.main.ui.workspace.sidebar.options.menus.layout-container :refer [layout-container-flex-attrs layout-container-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-item :refer [layout-item-attrs layout-item-menu]] [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] @@ -30,31 +32,55 @@ layout-item-values (select-keys shape layout-item-attrs) layout-container-values (select-keys shape layout-container-flex-attrs) - is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) - is-flex-layout-child? (mf/deref is-flex-layout-child-ref) + is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) + is-layout-child? (mf/deref is-layout-child-ref) - is-layout-child-absolute? (ctl/layout-absolute? shape)] + is-flex-parent-ref (mf/use-memo (mf/deps ids) #(refs/flex-layout-child? ids)) + is-flex-parent? (mf/deref is-flex-parent-ref) + + is-grid-parent-ref (mf/use-memo (mf/deps ids) #(refs/grid-layout-child? ids)) + is-grid-parent? (mf/deref is-grid-parent-ref) + + is-layout-child-absolute? (ctl/item-absolute? shape) + + ids (hooks/use-equal-memo ids) + parents-by-ids-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) + parents (mf/deref parents-by-ids-ref)] [:* + [:& layer-menu {:ids ids + :type type + :values layer-values}] + [:& measures-menu {:ids ids :type type :values measure-values :shape shape}] - [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - - (when is-flex-layout-child? + + [:& layout-container-menu + {:type type + :ids [(:id shape)] + :values layout-container-values + :multiple false}] + + (when (and (= (count ids) 1) is-layout-child? is-grid-parent?) + [:& grid-cell/options + {:shape (first parents) + :cell (ctl/get-cell-by-shape-id (first parents) (first ids))}]) + + (when is-layout-child? [:& layout-item-menu {:ids ids :type type :values layout-item-values :is-layout-child? true + :is-flex-parent? is-flex-parent? + :is-grid-parent? is-grid-parent? :shape shape}]) - (when (or (not is-flex-layout-child?) is-layout-child-absolute?) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) - [:& layer-menu {:ids ids - :type type - :values layer-values}] + [:& fill-menu {:ids ids :type type :values (select-keys shape fill-attrs)}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs index 78b938ca72..6bc6394b45 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs @@ -8,11 +8,13 @@ (:require [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] + [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] - [app.main.ui.workspace.sidebar.options.menus.layout-container :refer [layout-container-flex-attrs layout-container-menu]] + [app.main.ui.workspace.sidebar.options.menus.layout-container :refer [layout-container-flex-attrs layout-container-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-item :refer [layout-item-attrs layout-item-menu]] [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] @@ -32,29 +34,55 @@ layout-item-values (select-keys shape layout-item-attrs) layout-container-values (select-keys shape layout-container-flex-attrs) - is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) - is-flex-layout-child? (mf/deref is-flex-layout-child-ref) - is-layout-child-absolute? (ctl/layout-absolute? shape)] + is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) + is-layout-child? (mf/deref is-layout-child-ref) + + is-flex-parent-ref (mf/use-memo (mf/deps ids) #(refs/flex-layout-child? ids)) + is-flex-parent? (mf/deref is-flex-parent-ref) + + is-grid-parent-ref (mf/use-memo (mf/deps ids) #(refs/grid-layout-child? ids)) + is-grid-parent? (mf/deref is-grid-parent-ref) + + is-layout-child-absolute? (ctl/item-absolute? shape) + + ids (hooks/use-equal-memo ids) + parents-by-ids-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) + parents (mf/deref parents-by-ids-ref)] [:* + [:& layer-menu {:ids ids + :type type + :values layer-values}] + [:& measures-menu {:ids ids :type type :values measure-values :shape shape}] - [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - - (when is-flex-layout-child? + + [:& layout-container-menu + {:type type + :ids [(:id shape)] + :values layout-container-values + :multiple false}] + + (when (and (= (count ids) 1) is-layout-child? is-grid-parent?) + [:& grid-cell/options + {:shape (first parents) + :cell (ctl/get-cell-by-shape-id (first parents) (first ids))}]) + + (when is-layout-child? [:& layout-item-menu {:ids ids :type type :values layout-item-values :is-layout-child? true :is-layout-container? false + :is-flex-parent? is-flex-parent? + :is-grid-parent? is-grid-parent? :shape shape}]) - (when (or (not is-flex-layout-child?) is-layout-child-absolute?) + + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) - [:& layer-menu {:ids ids - :type type - :values layer-values}] + [:& fill-menu {:ids ids :type type :values (select-keys shape fill-attrs)}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs index ed9932b8e1..96ba853b6b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs @@ -9,12 +9,14 @@ [app.common.data :as d] [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] + [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu]] - [app.main.ui.workspace.sidebar.options.menus.component :refer [component-attrs component-menu]] + [app.main.ui.workspace.sidebar.options.menus.component :refer [component-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs-shape fill-menu]] [app.main.ui.workspace.sidebar.options.menus.frame-grid :refer [frame-grid]] + [app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-container :refer [layout-container-flex-attrs layout-container-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-item :refer [layout-item-attrs layout-item-menu]] @@ -34,37 +36,65 @@ constraint-values (select-keys shape constraint-attrs) layout-container-values (select-keys shape layout-container-flex-attrs) layout-item-values (select-keys shape layout-item-attrs) - [comp-ids comp-values] [[(:id shape)] (select-keys shape component-attrs)] - is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) - is-flex-layout-child? (mf/deref is-flex-layout-child-ref) - is-flex-layout-container? (ctl/flex-layout? shape) - is-layout-child-absolute? (ctl/layout-absolute? shape)] + ids (hooks/use-equal-memo ids) + + is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) + is-layout-child? (mf/deref is-layout-child-ref) + + is-flex-parent-ref (mf/use-memo (mf/deps ids) #(refs/flex-layout-child? ids)) + is-flex-parent? (mf/deref is-flex-parent-ref) + + is-grid-parent-ref (mf/use-memo (mf/deps ids) #(refs/grid-layout-child? ids)) + is-grid-parent? (mf/deref is-grid-parent-ref) + + is-layout-container? (ctl/any-layout? shape) + is-flex-layout? (ctl/flex-layout? shape) + is-grid-layout? (ctl/grid-layout? shape) + is-layout-child-absolute? (ctl/item-absolute? shape) + + ids (hooks/use-equal-memo ids) + parents-by-ids-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) + parents (mf/deref parents-by-ids-ref)] [:* + [:& layer-menu {:ids ids + :type type + :values layer-values}] [:& measures-menu {:ids [(:id shape)] :values measure-values :type type :shape shape}] - [:& component-menu {:ids comp-ids - :values comp-values - :shape shape}] - (when (or (not is-flex-layout-child?) is-layout-child-absolute?) - [:& constraints-menu {:ids ids - :values constraint-values}]) - [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when (or is-flex-layout-child? is-flex-layout-container?) + [:& component-menu {:shapes [shape]}] + + [:& layout-container-menu + {:type type + :ids [(:id shape)] + :values layout-container-values + :multiple false}] + + (when (and (= (count ids) 1) is-layout-child? is-grid-parent?) + [:& grid-cell/options + {:shape (first parents) + :cell (ctl/get-cell-by-shape-id (first parents) (first ids))}]) + + (when (or is-layout-child? is-layout-container?) [:& layout-item-menu {:ids ids :type type :values layout-item-values - :is-layout-child? is-flex-layout-child? - :is-layout-container? is-flex-layout-container? + :is-flex-parent? is-flex-parent? + :is-grid-parent? is-grid-parent? + :is-flex-layout? is-flex-layout? + :is-grid-layout? is-grid-layout? + :is-layout-child? is-layout-child? + :is-layout-container? is-layout-container? :shape shape}]) - [:& layer-menu {:ids ids - :type type - :values layer-values}] + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) + [:& constraints-menu {:ids ids + :values constraint-values}]) + [:& fill-menu {:ids ids :type type :values (select-keys shape fill-attrs-shape)}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/grid_cell.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/grid_cell.cljs deleted file mode 100644 index 5b4f96dd87..0000000000 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/grid_cell.cljs +++ /dev/null @@ -1,171 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.workspace.sidebar.options.shapes.grid-cell - (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.main.ui.components.numeric-input :refer [numeric-input]] - [app.main.ui.icons :as i] - [app.main.ui.workspace.sidebar.options.menus.layout-container :as lyc] - [app.util.dom :as dom] - [rumext.v2 :as mf])) - -(mf/defc set-self-alignment - [{:keys [is-col? alignment set-alignment] :as props}] - (let [dir-v [:auto :start :center :end :stretch #_:baseline]] - [:div.align-self-style - (for [align dir-v] - [:button.align-self.tooltip.tooltip-bottom - {:class (dom/classnames :active (= alignment align) - :tooltip-bottom-left (not= align :start) - :tooltip-bottom (= align :start)) - :alt (dm/str "Align self " (d/name align)) ;; TODO fix this tooltip - :on-click #(set-alignment align) - :key (str "align-self" align)} - (lyc/get-layout-flex-icon :align-self align is-col?)])])) - - -(mf/defc options - {::mf/wrap [mf/memo]} - [{:keys [_shape row column] :as props}] - - (let [position-mode (mf/use-state :auto) ;; TODO this should come from shape - - set-position-mode (fn [mode] - (reset! position-mode mode)) - - - align-self (mf/use-state :auto) ;; TODO this should come from shape - justify-alignment (mf/use-state :auto) ;; TODO this should come from shape - set-alignment (fn [value] - (reset! align-self value) - #_(if (= align-self value) - (st/emit! (dwsl/update-layout-child ids {:layout-item-align-self nil})) - (st/emit! (dwsl/update-layout-child ids {:layout-item-align-self value})))) - set-justify-self (fn [value] - (reset! justify-alignment value) - #_(if (= align-self value) - (st/emit! (dwsl/update-layout-child ids {:layout-item-align-self nil})) - (st/emit! (dwsl/update-layout-child ids {:layout-item-align-self value})))) - column-start column - column-end (inc column) - row-start row - row-end (inc row) - - on-change - (fn [_side _orientation _value] - ;; TODO - #_(if (= orientation :column) - (case side - :all ((reset! column-start value) - (reset! column-end value)) - :start (reset! column-start value) - :end (reset! column-end value)) - (case side - :all ((reset! row-start value) - (reset! row-end value)) - :start (reset! row-start value) - :end (reset! row-end value)))) - - area-name (mf/use-state "header") ;; TODO this should come from shape - - on-area-name-change (fn [value] - (reset! area-name value)) - on-key-press (fn [_event])] - - [:div.element-set - [:div.element-set-title - [:span "Grid Cell"]] - - [:div.element-set-content.layout-grid-item-menu - [:div.layout-row - [:div.row-title.sizing "Position"] - [:div.position-wrapper - [:button.position-btn - {:on-click #(set-position-mode :auto) - :class (dom/classnames :active (= :auto @position-mode))} "Auto"] - [:button.position-btn - {:on-click #(set-position-mode :manual) - :class (dom/classnames :active (= :manual @position-mode))} "Manual"] - [:button.position-btn - {:on-click #(set-position-mode :area) - :class (dom/classnames :active (= :area @position-mode))} "Area"]]] - [:div.manage-grid-columns - (when (= :auto @position-mode) - [:div.grid-auto - [:div.grid-columns-auto - [:spam.icon i/layout-rows] - [:div.input-wrapper - [:> numeric-input - {:placeholder "--" - :on-click #(dom/select-target %) - :on-change (partial on-change :all :column) ;; TODO cambiar este on-change y el value - :value column-start}]]] - [:div.grid-rows-auto - [:spam.icon i/layout-columns] - [:div.input-wrapper - [:> numeric-input - {:placeholder "--" - :on-click #(dom/select-target %) - :on-change (partial on-change :all :row) ;; TODO cambiar este on-change y el value - :value row-start}]]]]) - (when (= :area @position-mode) - [:div.input-wrapper - [:input.input-text - {:key "grid-area-name" - :id "grid-area-name" - :type "text" - :aria-label "grid-area-name" - :placeholder "--" - :default-value @area-name - :auto-complete "off" - :on-change on-area-name-change - :on-key-press on-key-press}]]) - - (when (or (= :manual @position-mode) (= :area @position-mode)) - [:div.grid-manual - [:div.grid-columns-auto - [:spam.icon i/layout-rows] - [:div.input-wrapper - [:> numeric-input - {:placeholder "--" - :on-click #(dom/select-target %) - :on-change (partial on-change :start :column) - :value column-start}] - [:> numeric-input - {:placeholder "--" - :on-click #(dom/select-target %) - :on-change (partial on-change :end :column) - :value column-end}]]] - [:div.grid-rows-auto - [:spam.icon i/layout-columns] - [:div.input-wrapper - [:> numeric-input - {:placeholder "--" - :on-click #(dom/select-target %) - :on-change (partial on-change :start :row) - :value row-start}] - [:> numeric-input - {:placeholder "--" - :on-click #(dom/select-target %) - :on-change (partial on-change :end :row) - :value row-end}]]]])] - - [:div.layout-row - [:div.row-title "Align"] - [:div.btn-wrapper - [:& set-self-alignment {:is-col? false - :alignment @align-self - :set-alignment set-alignment}]]] - - - [:div.layout-row - [:div.row-title "Justify"] - [:div.btn-wrapper - [:& set-self-alignment {:is-col? true - :alignment @justify-alignment - :set-alignment set-justify-self}]]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs index e3c0a917b2..f605ac5444 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs @@ -5,15 +5,18 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.shapes.group + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] + [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu]] - [app.main.ui.workspace.sidebar.options.menus.component :refer [component-attrs component-menu]] + [app.main.ui.workspace.sidebar.options.menus.component :refer [component-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-container :refer [layout-container-flex-attrs layout-container-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-item :refer [layout-item-menu]] @@ -36,41 +39,63 @@ file-id (unchecked-get props "file-id") layout-container-values (select-keys shape layout-container-flex-attrs) ids [(:id shape)] - is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) - is-flex-layout-child? (mf/deref is-flex-layout-child-ref) - is-layout-child-absolute? (ctl/layout-absolute? shape) + is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) + is-layout-child? (mf/deref is-layout-child-ref) + + is-flex-parent-ref (mf/use-memo (mf/deps ids) #(refs/flex-layout-child? ids)) + is-flex-parent? (mf/deref is-flex-parent-ref) + + is-grid-parent-ref (mf/use-memo (mf/deps ids) #(refs/grid-layout-child? ids)) + is-grid-parent? (mf/deref is-grid-parent-ref) + + is-layout-child-absolute? (ctl/item-absolute? shape) + + ids (hooks/use-equal-memo ids) + parents-by-ids-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) + parents (mf/deref parents-by-ids-ref) type :group [measure-ids measure-values] (get-attrs [shape] objects :measure) [layer-ids layer-values] (get-attrs [shape] objects :layer) [constraint-ids constraint-values] (get-attrs [shape] objects :constraint) [fill-ids fill-values] (get-attrs [shape] objects :fill) - [shadow-ids shadow-values] (get-attrs [shape] objects :shadow) + [shadow-ids _] (get-attrs [shape] objects :shadow) [blur-ids blur-values] (get-attrs [shape] objects :blur) [stroke-ids stroke-values] (get-attrs [shape] objects :stroke) [text-ids text-values] (get-attrs [shape] objects :text) [svg-ids svg-values] [[(:id shape)] (select-keys shape [:svg-attrs])] - [comp-ids comp-values] [[(:id shape)] (select-keys shape component-attrs)] [layout-item-ids layout-item-values] (get-attrs [shape] objects :layout-item)] - [:div.options - [:& measures-menu {:type type :ids measure-ids :values measure-values :shape shape}] - [:& component-menu {:ids comp-ids :values comp-values :shape shape}] ;;remove this in components-v2 - [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when is-flex-layout-child? + [:div {:class (stl/css :options)} + [:& layer-menu {:type type :ids layer-ids :values layer-values}] + [:& measures-menu {:type type :ids measure-ids :values measure-values :shape shape}] + [:& component-menu {:shapes [shape]}] ;;remove this in components-v2 + + [:& layout-container-menu + {:type type + :ids [(:id shape)] + :values layout-container-values + :multiple false}] + + (when (and (= (count ids) 1) is-layout-child? is-grid-parent?) + [:& grid-cell/options + {:shape (first parents) + :cell (ctl/get-cell-by-shape-id (first parents) (first ids))}]) + + (when is-layout-child? [:& layout-item-menu {:type type :ids layout-item-ids :is-layout-child? true :is-layout-container? false + :is-flex-parent? is-flex-parent? + :is-grid-parent? is-grid-parent? :values layout-item-values}]) - (when (or (not is-flex-layout-child?) is-layout-child-absolute?) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) [:& constraints-menu {:ids constraint-ids :values constraint-values}]) - [:& layer-menu {:type type :ids layer-ids :values layer-values}] - (when-not (empty? fill-ids) [:& fill-menu {:type type :ids fill-ids :values fill-values}]) @@ -83,7 +108,7 @@ :shared-libs shared-libs}] (when-not (empty? shadow-ids) - [:& shadow-menu {:type type :ids shadow-ids :values shadow-values}]) + [:& shadow-menu {:type type :ids ids :values (select-keys shape [:shadow])}]) (when-not (empty? blur-ids) [:& blur-menu {:type type :ids blur-ids :values blur-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.scss b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.scss new file mode 100644 index 0000000000..06669fb047 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.scss @@ -0,0 +1,14 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.options { + width: 100%; + display: flex; + flex-direction: column; + gap: $s-16; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs index 7d93273044..a05e465757 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs @@ -8,9 +8,11 @@ (:require [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] + [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-container :refer [layout-container-flex-attrs layout-container-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-item :refer [layout-item-attrs layout-item-menu]] @@ -32,32 +34,55 @@ layout-item-values (select-keys shape layout-item-attrs) layout-container-values (select-keys shape layout-container-flex-attrs) - is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) - is-flex-layout-child? (mf/deref is-flex-layout-child-ref) - is-layout-child-absolute? (ctl/layout-absolute? shape)] + is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) + is-layout-child? (mf/deref is-layout-child-ref) + + is-flex-parent-ref (mf/use-memo (mf/deps ids) #(refs/flex-layout-child? ids)) + is-flex-parent? (mf/deref is-flex-parent-ref) + + is-grid-parent-ref (mf/use-memo (mf/deps ids) #(refs/grid-layout-child? ids)) + is-grid-parent? (mf/deref is-grid-parent-ref) + + is-layout-child-absolute? (ctl/item-absolute? shape) + + ids (hooks/use-equal-memo ids) + parents-by-ids-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) + parents (mf/deref parents-by-ids-ref)] [:* + [:& layer-menu {:ids ids + :type type + :values layer-values}] + [:& measures-menu {:ids ids :type type :values measure-values :shape shape}] - [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - - (when is-flex-layout-child? + + [:& layout-container-menu + {:type type + :ids [(:id shape)] + :values layout-container-values + :multiple false}] + + (when (and (= (count ids) 1) is-layout-child? is-grid-parent?) + [:& grid-cell/options + {:shape (first parents) + :cell (ctl/get-cell-by-shape-id (first parents) (first ids))}]) + + (when is-layout-child? [:& layout-item-menu {:ids ids :type type :values layout-item-values :is-layout-child? true + :is-flex-parent? is-flex-parent? + :is-grid-parent? is-grid-parent? :shape shape}]) - (when (or (not is-flex-layout-child?) is-layout-child-absolute?) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) - [:& layer-menu {:ids ids - :type type - :values layer-values}] - [:& fill-menu {:ids ids :type type :values fill-values}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index 0180b09ff6..af81ea6528 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -5,18 +5,20 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.shapes.multiple + (:require-macros [app.main.style :as stl]) (:require [app.common.attrs :as attrs] [app.common.data :as d] [app.common.geom.shapes :as gsh] - [app.common.pages.common :as cpc] [app.common.text :as txt] + [app.common.types.component :as ctk] + [app.common.types.shape.attrs :refer [editable-attrs]] [app.common.types.shape.layout :as ctl] - [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-attrs blur-menu]] [app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu]] + [app.main.ui.workspace.sidebar.options.menus.component :refer [component-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-attrs exports-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] @@ -160,7 +162,7 @@ :shadow shadow-attrs :blur blur-attrs :stroke stroke-attrs - :text dwt/attrs + :text txt/text-all-attrs :exports exports-attrs :layout-container layout-container-flex-attrs :layout-item layout-item-attrs}) @@ -208,32 +210,41 @@ :else (attrs/get-attrs-multi [v1 v2] attrs))) extract-attrs - (fn [[ids values] {:keys [id type content] :as shape}] + (fn [[ids values] {:keys [id type] :as shape}] (let [read-mode (get-in type->read-mode [type attr-group]) - editable-attrs (filter (get cpc/editable-attrs (:type shape)) attrs)] + editable-attrs (filter (get editable-attrs (:type shape)) attrs)] (case read-mode - :ignore [ids values] + :ignore + [ids values] - :shape (let [;; Get the editable attrs from the shape, ensuring that all attributes - ;; are present, with value nil if they are not present in the shape. - shape-values (merge - (into {} (map #(vector % nil)) editable-attrs) - (cond - (= attr-group :measure) (select-measure-keys shape) - :else (select-keys shape editable-attrs)))] - [(conj ids id) - (merge-attrs values shape-values)]) + :shape + (let [;; Get the editable attrs from the shape, ensuring that all attributes + ;; are present, with value nil if they are not present in the shape. + shape-values (merge + (into {} (map #(vector % nil)) editable-attrs) + (cond + (= attr-group :measure) (select-measure-keys shape) + :else (select-keys shape editable-attrs)))] + [(conj ids id) + (merge-attrs values shape-values)]) - :text [(conj ids id) - (-> values - (merge-attrs (select-keys shape attrs)) - (merge-attrs (merge - (select-keys txt/default-text-attrs attrs) - (attrs/get-attrs-multi (txt/node-seq content) attrs))))] + :text + (let [shape-attrs (select-keys shape attrs) - :children (let [children (->> (:shapes shape []) (map #(get objects %))) - [new-ids new-values] (get-attrs* children objects attr-group)] - [(d/concat-vec ids new-ids) (merge-attrs values new-values)]) + content-attrs + (attrs/get-text-attrs-multi shape txt/default-text-attrs attrs) + + new-values + (-> values + (merge-attrs shape-attrs) + (merge-attrs content-attrs))] + [(conj ids id) + new-values]) + + :children + (let [children (->> (:shapes shape []) (map #(get objects %))) + [new-ids new-values] (get-attrs* children objects attr-group)] + [(d/concat-vec ids new-ids) (merge-attrs values new-values)]) [])))] @@ -264,7 +275,7 @@ {::mf/wrap [#(mf/memo' % (mf/check-props ["shapes" "shapes-with-children" "page-id" "file-id"]))] ::mf/wrap-props false} [props] - (let [shapes (unchecked-get props "shapes") + (let [shapes (unchecked-get props "shapes") shapes-with-children (unchecked-get props "shapes-with-children") ;; remove children from bool shapes @@ -294,15 +305,21 @@ all-types (into #{} (map :type shapes)) ids (->> shapes (map :id)) - is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) - is-flex-layout-child? (mf/deref is-flex-layout-child-ref) + is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) + is-layout-child? (mf/deref is-layout-child-ref) + + is-flex-parent-ref (mf/use-memo (mf/deps ids) #(refs/flex-layout-child? ids)) + is-flex-parent? (mf/deref is-flex-parent-ref) + + is-grid-parent-ref (mf/use-memo (mf/deps ids) #(refs/grid-layout-child? ids)) + is-grid-parent? (mf/deref is-grid-parent-ref) has-text? (contains? all-types :text) has-flex-layout-container? (->> shapes (some ctl/flex-layout?)) - all-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/all-flex-layout-child? ids)) - all-flex-layout-child? (mf/deref all-flex-layout-child-ref) + all-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/all-layout-child? ids)) + all-layout-child? (mf/deref all-layout-child-ref) all-flex-layout-container? (->> shapes (every? ctl/flex-layout?)) @@ -333,29 +350,39 @@ (get-attrs shapes objects-no-measures :stroke) (get-attrs shapes objects-no-measures :exports) (get-attrs shapes objects-no-measures :layout-container) - (get-attrs shapes objects-no-measures :layout-item) - ])))] + (get-attrs shapes objects-no-measures :layout-item)]))) + + components (filter ctk/instance-head? shapes)] + + [:div {:class (stl/css :options)} + (when-not (empty? layer-ids) + [:& layer-menu {:type type :ids layer-ids :values layer-values}]) - [:div.options (when-not (empty? measure-ids) [:& measures-menu {:type type :all-types all-types :ids measure-ids :values measure-values :shape shapes}]) - [:& layout-container-menu {:type type :ids layout-container-ids :values layout-container-values :multiple true}] + (when-not (empty? components) + [:& component-menu {:shapes components}]) - (when (or is-flex-layout-child? has-flex-layout-container?) + [:& layout-container-menu + {:type type + :ids layout-container-ids + :values layout-container-values + :multiple true}] + + (when (or is-layout-child? has-flex-layout-container?) [:& layout-item-menu {:type type :ids layout-item-ids - :is-layout-child? all-flex-layout-child? + :is-layout-child? all-layout-child? :is-layout-container? all-flex-layout-container? + :is-flex-parent? is-flex-parent? + :is-grid-parent? is-grid-parent? :values layout-item-values}]) - (when-not (or (empty? constraint-ids) is-flex-layout-child?) + (when-not (or (empty? constraint-ids) ^boolean is-layout-child?) [:& constraints-menu {:ids constraint-ids :values constraint-values}]) - (when-not (empty? layer-ids) - [:& layer-menu {:type type :ids layer-ids :values layer-values}]) - (when-not (empty? text-ids) [:& ot/text-menu {:type type :ids text-ids :values text-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.scss b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.scss new file mode 100644 index 0000000000..06669fb047 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.scss @@ -0,0 +1,14 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.options { + width: 100%; + display: flex; + flex-direction: column; + gap: $s-16; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs index 5a7b1bd953..db0aba0094 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs @@ -8,9 +8,11 @@ (:require [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] + [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-container :refer [layout-container-flex-attrs layout-container-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-item :refer [layout-item-attrs layout-item-menu]] @@ -32,29 +34,54 @@ layout-item-values (select-keys shape layout-item-attrs) layout-container-values (select-keys shape layout-container-flex-attrs) - is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) - is-flex-layout-child? (mf/deref is-flex-layout-child-ref) - is-layout-child-absolute? (ctl/layout-absolute? shape)] + is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) + is-layout-child? (mf/deref is-layout-child-ref) + + is-flex-parent-ref (mf/use-memo (mf/deps ids) #(refs/flex-layout-child? ids)) + is-flex-parent? (mf/deref is-flex-parent-ref) + + is-grid-parent-ref (mf/use-memo (mf/deps ids) #(refs/grid-layout-child? ids)) + is-grid-parent? (mf/deref is-grid-parent-ref) + + is-layout-child-absolute? (ctl/item-absolute? shape) + + ids (hooks/use-equal-memo ids) + parents-by-ids-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) + parents (mf/deref parents-by-ids-ref)] [:* + [:& layer-menu {:ids ids + :type type + :values layer-values}] [:& measures-menu {:ids ids :type type :values measure-values :shape shape}] - [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when is-flex-layout-child? + [:& layout-container-menu + {:type type + :ids [(:id shape)] + :values layout-container-values + :multiple false}] + + (when (and (= (count ids) 1) is-layout-child? is-grid-parent?) + [:& grid-cell/options + {:shape (first parents) + :cell (ctl/get-cell-by-shape-id (first parents) (first ids))}]) + + (when is-layout-child? [:& layout-item-menu {:ids ids :type type :values layout-item-values :is-layout-child? true :is-layout-container? false + :is-flex-parent? is-flex-parent? + :is-grid-parent? is-grid-parent? :shape shape}]) - (when (or (not is-flex-layout-child?) is-layout-child-absolute?) + + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) - [:& layer-menu {:ids ids - :type type - :values layer-values}] + [:& fill-menu {:ids ids :type type :values (select-keys shape fill-attrs)}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs index 401ef70ef4..b7406ddca0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs @@ -8,9 +8,11 @@ (:require [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] + [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-container :refer [layout-container-flex-attrs layout-container-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-item :refer [layout-item-attrs layout-item-menu]] @@ -21,42 +23,68 @@ [rumext.v2 :as mf])) (mf/defc options - {::mf/wrap [mf/memo]} - [{:keys [shape] :as props}] - (let [ids [(:id shape)] - type (:type shape) - measure-values (select-measure-keys shape) - layer-values (select-keys shape layer-attrs) - constraint-values (select-keys shape constraint-attrs) - fill-values (select-keys shape fill-attrs) - stroke-values (select-keys shape stroke-attrs) - layout-item-values (select-keys shape layout-item-attrs) - layout-container-values (select-keys shape layout-container-flex-attrs) - is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) - is-flex-layout-child? (mf/deref is-flex-layout-child-ref) - is-layout-child-absolute? (ctl/layout-absolute? shape)] + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [{:keys [shape]}] + (let [shape-id (:id shape) + ids (hooks/use-equal-memo [shape-id]) + type (:type shape) + measure-values (select-measure-keys shape) + layer-values (select-keys shape layer-attrs) + constraint-values (select-keys shape constraint-attrs) + fill-values (select-keys shape fill-attrs) + stroke-values (select-keys shape stroke-attrs) + layout-item-values (select-keys shape layout-item-attrs) + layout-container-values (select-keys shape layout-container-flex-attrs) + + is-layout-child* (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) + is-layout-child? (mf/deref is-layout-child*) + + is-flex-parent* (mf/use-memo (mf/deps ids) #(refs/flex-layout-child? ids)) + is-flex-parent? (mf/deref is-flex-parent*) + + is-grid-parent* (mf/use-memo (mf/deps ids) #(refs/grid-layout-child? ids)) + is-grid-parent? (mf/deref is-grid-parent*) + + is-layout-child-absolute? (ctl/item-absolute? shape) + + parents-by-ids* (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) + parents (mf/deref parents-by-ids*)] + [:* + [:& layer-menu {:ids ids + :type type + :values layer-values}] [:& measures-menu {:ids ids :type type :values measure-values :shape shape}] - [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when is-flex-layout-child? + + [:& layout-container-menu + {:type type + :ids ids + :values layout-container-values + :multiple false}] + + (when (and (= (count ids) 1) is-layout-child? is-grid-parent?) + [:& grid-cell/options + {:shape (first parents) + :cell (ctl/get-cell-by-shape-id (first parents) (first ids))}]) + + (when ^boolean is-layout-child? [:& layout-item-menu {:ids ids :type type :values layout-item-values :is-layout-child? true + :is-flex-parent? is-flex-parent? + :is-grid-parent? is-grid-parent? :shape shape}]) - (when (or (not is-flex-layout-child?) is-layout-child-absolute?) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) - [:& layer-menu {:ids ids - :type type - :values layer-values}] - [:& fill-menu {:ids ids :type type :values fill-values}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs index 0560de5494..bf13cf76c8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs @@ -6,20 +6,21 @@ (ns app.main.ui.workspace.sidebar.options.shapes.svg-raw (:require - [app.common.colors :as clr] + [app.common.colors :as cc] [app.common.data :as d] [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] + [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell] [app.main.ui.workspace.sidebar.options.menus.layout-container :refer [layout-container-flex-attrs layout-container-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-item :refer [layout-item-attrs layout-item-menu]] [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] [app.main.ui.workspace.sidebar.options.menus.svg-attrs :refer [svg-attrs-menu]] - [app.util.color :as uc] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -43,7 +44,7 @@ {:color :multiple :opacity :multiple} - :else {:color (uc/parse-color color) + :else {:color (cc/parse color) :opacity 1}) (catch :default e @@ -69,7 +70,7 @@ (get-in shape [:content :attrs :style :stroke])) (parse-color)) - stroke-color (:color color clr/black) + stroke-color (:color color cc/black) stroke-opacity (:opacity color 1) stroke-style (-> (or (get-in shape [:content :attrs :stroke-style]) (get-in shape [:content :attrs :style :stroke-style]) @@ -106,9 +107,20 @@ layout-item-values (select-keys shape layout-item-attrs) layout-container-values (select-keys shape layout-container-flex-attrs) - is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) - is-flex-layout-child? (mf/deref is-flex-layout-child-ref) - is-layout-child-absolute? (ctl/layout-absolute? shape)] + is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) + is-layout-child? (mf/deref is-layout-child-ref) + + is-flex-parent-ref (mf/use-memo (mf/deps ids) #(refs/flex-layout-child? ids)) + is-flex-parent? (mf/deref is-flex-parent-ref) + + is-grid-parent-ref (mf/use-memo (mf/deps ids) #(refs/grid-layout-child? ids)) + is-grid-parent? (mf/deref is-grid-parent-ref) + + is-layout-child-absolute? (ctl/item-absolute? shape) + + ids (hooks/use-equal-memo ids) + parents-by-ids-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) + parents (mf/deref parents-by-ids-ref)] (when (contains? svg-elements tag) [:* @@ -116,17 +128,29 @@ :type type :values measure-values :shape shape}] - [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when is-flex-layout-child? + [:& layout-container-menu + {:type type + :ids [(:id shape)] + :values layout-container-values + :multiple false}] + + (when (and (= (count ids) 1) is-layout-child? is-grid-parent?) + [:& grid-cell/options + {:shape (first parents) + :cell (ctl/get-cell-by-shape-id (first parents) (first ids))}]) + + (when is-layout-child? [:& layout-item-menu {:ids ids :type type :values layout-item-values :is-layout-child? true + :is-flex-parent? is-flex-parent? + :is-grid-parent? is-grid-parent? :shape shape}]) - (when (or (not is-flex-layout-child?) is-layout-child-absolute?) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs index 8c93c8f9e1..a3b9f3c4e0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs @@ -7,13 +7,16 @@ (ns app.main.ui.workspace.sidebar.options.shapes.text (:require [app.common.data :as d] + [app.common.text :as txt] [app.common.types.shape.layout :as ctl] - [app.main.data.workspace.texts :as dwt :refer [text-fill-attrs root-attrs paragraph-attrs text-attrs]] + [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] + [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-menu fill-attrs]] + [app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-container :refer [layout-container-flex-attrs layout-container-menu]] [app.main.ui.workspace.sidebar.options.menus.layout-item :refer [layout-item-attrs layout-item-menu]] @@ -28,10 +31,22 @@ (let [ids [(:id shape)] type (:type shape) - is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) - is-flex-layout-child? (mf/deref is-flex-layout-child-ref) + is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) + is-layout-child? (mf/deref is-layout-child-ref) + + is-flex-parent-ref (mf/use-memo (mf/deps ids) #(refs/flex-layout-child? ids)) + is-flex-parent? (mf/deref is-flex-parent-ref) + + is-grid-parent-ref (mf/use-memo (mf/deps ids) #(refs/grid-layout-child? ids)) + is-grid-parent? (mf/deref is-grid-parent-ref) + layout-container-values (select-keys shape layout-container-flex-attrs) - is-layout-child-absolute? (ctl/layout-absolute? shape) + is-layout-child-absolute? (ctl/item-absolute? shape) + + ids (hooks/use-equal-memo ids) + parents-by-ids-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) + parents (mf/deref parents-by-ids-ref) + state-map (mf/deref refs/workspace-editor-state) shared-libs (mf/deref refs/workspace-libraries) @@ -42,7 +57,7 @@ fill-values (-> (dwt/current-text-values {:editor-state editor-state :shape shape - :attrs (conj text-fill-attrs :fills)}) + :attrs (conj txt/text-fill-attrs :fills)}) (d/update-in-when [:fill-color-gradient :type] keyword)) fill-values (if (not (contains? fill-values :fills)) @@ -57,42 +72,53 @@ (select-keys shape fill-attrs) (dwt/current-root-values {:shape shape - :attrs root-attrs}) + :attrs txt/root-attrs}) (dwt/current-paragraph-values {:editor-state editor-state :shape shape - :attrs paragraph-attrs}) + :attrs txt/paragraph-attrs}) (dwt/current-text-values {:editor-state editor-state :shape shape - :attrs text-attrs})) + :attrs txt/text-node-attrs})) layout-item-values (select-keys shape layout-item-attrs)] [:* + [:& layer-menu {:ids ids + :type type + :values layer-values}] [:& measures-menu {:ids ids :type type :values (select-keys shape measure-attrs) :shape shape}] - [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when is-flex-layout-child? + [:& layout-container-menu + {:type type + :ids [(:id shape)] + :values layout-container-values + :multiple false}] + + (when (and (= (count ids) 1) is-layout-child? is-grid-parent?) + [:& grid-cell/options + {:shape (first parents) + :cell (ctl/get-cell-by-shape-id (first parents) (first ids))}]) + + (when is-layout-child? [:& layout-item-menu {:ids ids :type type :values layout-item-values :is-layout-child? true + :is-flex-parent? is-flex-parent? + :is-grid-parent? is-grid-parent? :shape shape}]) - (when (or (not is-flex-layout-child?) is-layout-child-absolute?) + (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?) [:& constraints-menu {:ids ids :values (select-keys shape constraint-attrs)}]) - [:& layer-menu {:ids ids - :type type - :values layer-values}] - [:& text-menu {:ids ids :type type diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs index 26d554ffe2..20d41a9f7e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs @@ -5,34 +5,30 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.shortcuts - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.config :as cf] [app.main.data.dashboard.shortcuts] - [app.main.data.events :as ev] [app.main.data.shortcuts :as ds] [app.main.data.viewer.shortcuts] [app.main.data.workspace :as dw] [app.main.data.workspace.path.shortcuts] [app.main.data.workspace.shortcuts] [app.main.store :as st] - [app.main.ui.context :as ctx] + [app.main.ui.components.search-bar :refer [search-bar]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :refer [tr]] - [app.util.keyboard :as kbd] [app.util.strings :refer [matches-search]] [clojure.set :as set] [clojure.string] - [cuerdas.core :as str] [rumext.v2 :as mf])) (mf/defc converted-chars [{:keys [char command] :as props}] - (let [new-css-system (mf/use-ctx ctx/new-css-system) - modified-keys {:up ds/up-arrow + (let [modified-keys {:up ds/up-arrow :down ds/down-arrow :left ds/left-arrow :right ds/right-arrow @@ -50,10 +46,8 @@ char (if (contains? modified-keys (keyword char)) ((keyword char) modified-keys) char) char (if (and is-macos? (contains? macos-keys (keyword char))) ((keyword char) macos-keys) char) unique-key (str (d/name command) "-" char)] - (if new-css-system - [:span {:class (css :key) - :key unique-key} char] - [:span.char-box {:key unique-key} char]))) + [:span {:class (stl/css :key) + :key unique-key} char])) (defn translation-keyname [type keyname] @@ -177,15 +171,15 @@ ;; shortcuts.toggle-assets ;; shortcuts.toggle-colorpalette ;; shortcuts.toggle-focus-mode - ;; shortcuts.toggle-grid + ;; shortcuts.toggle-guides ;; shortcuts.toggle-history ;; shortcuts.toggle-layers ;; shortcuts.toggle-lock ;; shortcuts.toggle-lock-size ;; shortcuts.toggle-rules - ;; shortcuts.toggle-scale-text - ;; shortcuts.toggle-snap-grid - ;; shortcuts.toggle-snap-guide + ;; shortcuts.scale + ;; shortcuts.toggle-snap-guides + ;; shortcuts.toggle-snap-ruler-guide ;; shortcuts.toggle-textpalette ;; shortcuts.toggle-visibility ;; shortcuts.toggle-zoom-style @@ -195,6 +189,7 @@ ;; shortcuts.unmask ;; shortcuts.v-distribute ;; shortcuts.zoom-selected + ;; shortcuts.toggle-layout-grid (let [translat-pre (case type :sc "shortcuts." :sec "shortcut-section." @@ -221,8 +216,7 @@ (mf/defc shortcuts-keys [{:keys [content command] :as props}] - (let [new-css-system (mf/use-ctx ctx/new-css-system) - managed-list (if (coll? content) + (let [managed-list (if (coll? content) content (conj () content)) chars-list (map ds/split-sc managed-list) @@ -231,43 +225,25 @@ chars-list (drop-last chars-list)) penultimate (last short-char-list)] - (if new-css-system - [:span {:class (css :keys)} - (for [chars short-char-list] - [:* - (for [char chars] - [:& converted-chars {:key (dm/str char "-" (name command)) - :char char - :command command}]) - (when (not= chars penultimate) [:span {:class (css :space)} ","])]) - (when (not= last-element penultimate) - [:* - [:span {:class (css :space)} (tr "shortcuts.or")] - (for [char last-element] - [:& converted-chars {:key (dm/str char "-" (name command)) - :char char - :command command}])])] - - [:span.keys - (for [chars short-char-list] - [:* - (for [char chars] - [:& converted-chars {:key (dm/str char "-" (name command)) - :char char - :command command}]) - (when (not= chars penultimate) [:span.space ","])]) - (when (not= last-element penultimate) - [:* - [:span.space (tr "shortcuts.or")] - (for [char last-element] - [:& converted-chars {:key (dm/str char "-" (name command)) - :char char - :command command}])])]))) + [:span {:class (stl/css :keys)} + (for [chars short-char-list] + [:* + (for [char chars] + [:& converted-chars {:key (dm/str char "-" (name command)) + :char char + :command command}]) + (when (not= chars penultimate) [:span {:class (stl/css :space)} ","])]) + (when (not= last-element penultimate) + [:* + [:span {:class (stl/css :space)} (tr "shortcuts.or")] + (for [char last-element] + [:& converted-chars {:key (dm/str char "-" (name command)) + :char char + :command command}])])])) (mf/defc shortcut-row [{:keys [elements filter-term match-section? match-subsection?] :as props}] - (let [new-css-system (mf/use-ctx ctx/new-css-system) - shortcut-name (keys elements) + (let [shortcut-name (keys elements) shortcut-translations (map #(translation-keyname :sc %) shortcut-name) match-shortcut? (some #(matches-search % @filter-term) shortcut-translations) filtered (if (and (or match-section? match-subsection?) (not match-shortcut?)) @@ -275,46 +251,32 @@ (filter #(matches-search % @filter-term) shortcut-translations)) sorted-filtered (sort filtered)] - (if new-css-system - [:ul {:class (css :sub-menu)} - (for [command-translate sorted-filtered] - (let [sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements)) - [command comand-info] sc-by-translate - content (or (:show-command comand-info)(:command comand-info))] - [:li {:class (css :shortcuts-name) - :key command-translate} - [:span {:class (css :command-name)} - command-translate] - [:& shortcuts-keys {:content content - :command command}]]))] - - [:ul.sub-menu - (for [command-translate sorted-filtered] - (let [sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements)) - [command comand-info] sc-by-translate - content (or (:show-command comand-info) (:command comand-info))] - [:li.shortcut-name {:key command-translate} - [:span.command-name command-translate] - [:& shortcuts-keys {:content content - :command command}]]))]))) + [:ul {:class (stl/css :sub-menu)} + (for [command-translate sorted-filtered] + (let [sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements)) + [command comand-info] sc-by-translate + content (or (:show-command comand-info) (:command comand-info))] + [:li {:class (stl/css :shortcuts-name) + :key command-translate} + [:span {:class (stl/css :command-name)} + command-translate] + [:& shortcuts-keys {:content content + :command command}]]))])) (mf/defc section-title [{:keys [is-visible? name is-sub?] :as props}] - (let [new-css-system (mf/use-ctx ctx/new-css-system)] - (if new-css-system - [:div {:class (if is-sub? (css :subsection-title) (css :section-title))} - [:span {:class (dom/classnames (css :open) is-visible? - (css :collapsed-shortcuts) true)} i/arrow-refactor] - [:span {:class (if is-sub? (css :subsection-name) (css :section-name))} name]] - - [:div {:class (if is-sub? "subsection-title" "section-title")} - [:span.collapesed-shortcuts {:class (when is-visible? "open")} i/arrow-slide] - [:span {:class (if is-sub? "subsection-name" "section-name")} name]]))) + [:div {:class (if is-sub? + (stl/css :subsection-title) + (stl/css :section-title))} + [:span {:class (stl/css-case :open is-visible? + :collapsed-shortcuts true)} i/arrow] + [:span {:class (if is-sub? + (stl/css :subsection-name) + (stl/css :section-name))} name]]) (mf/defc shortcut-subsection [{:keys [subsections manage-sections filter-term match-section? open-sections] :as props}] - (let [new-css-system (mf/use-ctx ctx/new-css-system) - subsections-names (keys subsections) + (let [subsections-names (keys subsections) subsection-translations (if (= :none (first subsections-names)) (map #(translation-keyname :sc %) subsections-names) (map #(translation-keyname :sub-sec %) subsections-names)) @@ -326,9 +288,8 @@ :filter-term filter-term :match-section? match-section? :match-subsection? true}]) - - [:ul {:class (dom/classnames (css :subsection-menu) new-css-system - :subsection-menu (not new-css-system))} + + [:ul {:class (stl/css :subsection-menu)} (for [sub-translated sorted-translations] (let [sub-by-translate (first (filter #(= (:translation (second %)) sub-translated) subsections)) [sub-name sub-info] sub-by-translate @@ -365,9 +326,9 @@ translations (map #(translation-keyname :sc %) (keys subs-bodys)) match-shortcut? (some #(matches-search % @filter-term) translations) visible? (some #(= % section-id) @open-sections)] - + (when (or match-section? match-subsection? match-shortcut?) - [:div {:class (css :section) + [:div {:class (stl/css :section) :on-click (manage-sections section-id)} [:& section-title {:is-visible? visible? :is-sub? false @@ -381,9 +342,8 @@ :filter-term filter-term}]]]))) (mf/defc shortcuts-container - [] - (let [new-css-system (mf/use-ctx ctx/new-css-system) - workspace-shortcuts app.main.data.workspace.shortcuts/shortcuts + [{:keys [class] :as props}] + (let [workspace-shortcuts app.main.data.workspace.shortcuts/shortcuts path-shortcuts app.main.data.workspace.path.shortcuts/shortcuts all-workspace-shortcuts (->> (d/deep-merge path-shortcuts workspace-shortcuts) (add-translation :sc) @@ -495,105 +455,40 @@ (manage-section-on-search :viewer term))] (reset! open-sections ids)))) - on-search-term-change + on-search-term-change-2 (mf/use-callback - (fn [event] - (let [value (dom/get-target-val event)] - (manage-sections-on-search value) - (reset! filter-term value)))) - + (fn [value] + (manage-sections-on-search value) + (reset! filter-term value))) on-search-clear-click (mf/use-callback (fn [_] (reset! open-sections [[1]]) - (reset! filter-term ""))) - - manage-key-down - (mf/use-callback - (fn [event] - (when (kbd/esc? event) - (st/emit! (-> (dw/toggle-layout-flag :shortcuts) - (vary-meta assoc ::ev/origin "shortcuts-panel")))))) - - on-key-down - (mf/use-callback - (fn [event] - (when (kbd/enter? event) - (on-search-clear-click) - (dom/focus! (dom/get-element "shortcut-search")))))] + (reset! filter-term "")))] (mf/with-effect [] (dom/focus! (dom/get-element "shortcut-search"))) - (if new-css-system - [:div {:class (css :shortcuts)} - [:div {:class (css :shortcuts-header)} - [:div {:class (css :shortcuts-title)} "Keyboard Shortcuts"] - [:div {:class (css :shortcuts-close-button) - :on-click close-fn} - i/close-refactor]] - ;; TODO Change this for search bar component - [:div {:class (css :search-field)} - [:div {:class (css :search-box)} - [:span {:class (css :icon-wrapper)} - i/search-refactor] - [:input {:class (dom/classnames (css :input-text) true) - :id "shortcut-search" - :placeholder (tr "shortcuts.title") - :type "text" - :value @filter-term - :on-change on-search-term-change - :auto-complete "off" - :on-key-down manage-key-down}] - (when (not (str/empty? @filter-term)) + [:div {:class (dm/str class " " (stl/css :shortcuts))} + [:div {:class (stl/css :shortcuts-header)} + [:div {:class (stl/css :shortcuts-title)} (tr "shortcuts.title")] + [:div {:class (stl/css :shortcuts-close-button) + :on-click close-fn} + i/close]] + [:div {:class (stl/css :search-field)} - [:button - {:class (css :clear-btn) - :on-click on-search-clear-click - :on-key-down on-key-down} - [:span {:class (css :clear-icon)} - i/delete-text-refactor]])]] + [:& search-bar {:on-change on-search-term-change-2 + :clear-action on-search-clear-click + :value @filter-term + :placeholder (tr "shortcuts.title") + :icon (mf/html [:span {:class (stl/css :search-icon)} i/search])}]] - (if match-any? - [:div {:class (dom/classnames (css :shortcuts-list) true)} - (for [section all-shortcuts] - [:& shortcut-section - {:section section - :manage-sections manage-sections - :open-sections open-sections - :filter-term filter-term}])] - [:div {:class (css :not-found)} (tr "shortcuts.not-found")])] - - [:div.shortcuts - [:div.shortcuts-header - [:div.shortcuts-close-button - {:on-click close-fn} i/close] - [:div.shortcuts-title (tr "shortcuts.title")]] - [:div.search-field - [:div.search-box - [:input.input-text - {:id "shortcut-search" - :placeholder (tr "shortcuts.search-placeholder") - :type "text" - :value @filter-term - :on-change on-search-term-change - :auto-complete "off" - :on-key-down manage-key-down}] - (if (str/empty? @filter-term) - [:span.icon-wrapper - i/search] - [:button.icon-wrapper - {:on-click on-search-clear-click - :on-key-down on-key-down} - [:span.icon.close - i/close]])]] - (if match-any? - [:div.shortcut-list - (for [section all-shortcuts] - [:& shortcut-section - {:section section - :manage-sections manage-sections - :open-sections open-sections - :filter-term filter-term}])] - - [:div.not-found (tr "shortcuts.not-found")])]))) + (if match-any? + [:div {:class (stl/css :shortcuts-list)} + (for [section all-shortcuts] + [:& shortcut-section + {:section section + :manage-sections manage-sections + :open-sections open-sections + :filter-term filter-term}])] + [:div {:class (stl/css :not-found)} (tr "shortcuts.not-found")])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.css.json b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.css.json deleted file mode 100644 index c2e9635b92..0000000000 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"sidebar_shortcuts_button-primary_aIZ1F","shortcuts":"sidebar_shortcuts_shortcuts_cOJNo","shortcuts-header":"sidebar_shortcuts_shortcuts-header_0SZ19","shortcuts-close-button":"sidebar_shortcuts_shortcuts-close-button_gT7kn","button-secondary":"sidebar_shortcuts_button-secondary_dtWEN","button-icon":"sidebar_shortcuts_button-icon_rCHmV","button-icon-small":"sidebar_shortcuts_button-icon-small_9BnNh","shortcuts-list":"sidebar_shortcuts_shortcuts-list_z7osI","section-title":"sidebar_shortcuts_section-title_Dv7S-","collapsed-shortcuts":"sidebar_shortcuts_collapsed-shortcuts_XrOj5","subsection-title":"sidebar_shortcuts_subsection-title_--5j4","search-field":"sidebar_shortcuts_search-field_cDecA","search-box":"sidebar_shortcuts_search-box_vmYAl","clear-btn":"sidebar_shortcuts_clear-btn_vRbGu","clear-icon":"sidebar_shortcuts_clear-icon_ZL4ae","icon-wrapper":"sidebar_shortcuts_icon-wrapper_XaR8m","shortcuts-title":"sidebar_shortcuts_shortcuts-title_P38o9","input-text":"sidebar_shortcuts_input-text_e9n1x","section":"sidebar_shortcuts_section_Jxkqa","open":"sidebar_shortcuts_open_SxghD","subsection-name":"sidebar_shortcuts_subsection-name_rWvFY","section-name":"sidebar_shortcuts_section-name_SyF9-","subsection-menu":"sidebar_shortcuts_subsection-menu_FdH9L","sub-menu":"sidebar_shortcuts_sub-menu_95jTY","shortcuts-name":"sidebar_shortcuts_shortcuts-name_hPkq6","command-name":"sidebar_shortcuts_command-name_Cujed","keys":"sidebar_shortcuts_keys_-pUnF","key":"sidebar_shortcuts_key_QyU8q","space":"sidebar_shortcuts_space_aODdu","not-found":"sidebar_shortcuts_not-found_bKEb0"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss index 42c3b385d3..54ca47a4ae 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss @@ -7,183 +7,202 @@ @import "refactor/common-refactor.scss"; .shortcuts { - .shortcuts-header { - @include flexCenter; - @include tabTitleTipography; - position: relative; - height: $s-32; - padding: $s-2 $s-2 $s-2 0; - margin: $s-4 $s-4 0 $s-4; - border-radius: $br-6; - background-color: var(--title-background-color); + display: grid; + grid-template-rows: auto auto 1fr; + // TODO: Fix this once we start implementing the DS. + // We should not be doign these hardcoded calc's. + height: calc(100vh - #{$s-60}); +} - .shortcuts-title { - @include flexCenter; - flex-grow: 1; - color: var(--title-foreground-color-hover); - } - - .shortcuts-close-button { - @extend .button-primary; - position: absolute; - right: $s-2; - top: $s-2; - height: $s-28; - width: $s-28; - border-radius: $br-5; - svg { - @extend .button-icon; - } - } - } - - .search-field { - display: flex; +.search-field { + display: flex; + align-items: center; + height: $s-32; + margin: $s-16 $s-12 $s-4 $s-12; + border-radius: $br-8; + font-family: "worksans", sans-serif; + background-color: var(--color-background-tertiary); + .search-box { align-items: center; - height: $s-32; - margin: $s-16 $s-12 $s-4 $s-12; - border-radius: $br-8; - font-family: "worksans", sans-serif; - background-color: var(--color-background-tertiary); - - .search-box { - align-items: center; - display: flex; - width: 100%; - - .icon-wrapper { - display: flex; - svg { - @extend .button-icon-small; - } - } - - .input-text { - height: $s-32; - width: 100%; - margin: 0; - padding: $s-4; - border: 0; - font-size: $fs-12; - color: var(--color-foreground-primary); - background-color: transparent; - &::placeholder { - color: var(--color-foreground-secondary); - } - &:focus-visible { - border-color: var(--color-accent-primary-muted); - } - } - .clear-btn { - @include buttonStyle; - @include flexCenter; - height: $s-16; - width: $s-16; - .clear-icon { - @include flexCenter; - svg { - @extend .button-icon-small; - } - } - } - } - } - - .section { - margin: 0; - } - .shortcuts-list { display: flex; - flex-direction: column; - height: 90%; - padding: $s-12; - margin-bottom: $s-12; - scrollbar-gutter: stable; - overflow-y: overlay; - font-size: $fs-12; - color: var(--title-foreground-color); + width: 100%; - .section-title, - .subsection-title { - @include tabTitleTipography; + .icon-wrapper { display: flex; - align-items: center; - margin: 0; - padding: $s-8 0; - cursor: pointer; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } - .collapsed-shortcuts { + .input-text { + @include removeInputStyle; + height: $s-32; + width: 100%; + margin: 0; + padding: $s-4; + border: 0; + font-size: $fs-12; + color: var(--color-foreground-primary); + &::placeholder { + color: var(--color-foreground-secondary); + } + &:focus-visible { + border-color: var(--color-accent-primary-muted); + } + } + .clear-btn { + @include buttonStyle; + @include flexCenter; + height: $s-16; + width: $s-16; + .clear-icon { @include flexCenter; svg { @extend .button-icon-small; - } - &.open { - transform: rotate(90deg); - } - } - .subsection-name, - .section-name { - padding-left: $s-4; - } - &:hover { - color: var(--title-foreground-color-hover); - .collapsed-shortcuts { - svg { - stroke: var(--title-foreground-color-hover); - } - } - } - } - - .subsection-title { - text-transform: none; - padding-left: $s-12; - } - .subsection-menu { - margin-bottom: $s-4; - } - .sub-menu { - margin-bottom: $s-4; - - .shortcuts-name { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - min-height: $s-32; - padding: $s-6; - margin-bottom: $s-4; - border-radius: $br-8; - background-color: var(--pill-background-color); - - .command-name { - @include titleTipography; - margin-left: $s-2; - color: var(--pill-foreground-color); - } - .keys { - @include flexCenter; - gap: $s-2; - color: var(--pill-foreground-color); - - .key { - @include titleTipography; - @include flexCenter; - text-transform: capitalize; - height: $s-20; - padding: $s-2 $s-6; - border-radius: $s-6; - background-color: var(--menu-shortcut-background-color); - } - .space { - margin: 0 $s-2; - } + stroke: var(--icon-foreground); } } } } - .not-found { - @include titleTipography; - margin: $s-12; + .search-icon { + @include flexCenter; + width: $s-28; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } +} + +.shortcuts-header { + @include flexCenter; + @include uppercaseTitleTipography; + position: relative; + height: $s-32; + padding: $s-2 $s-2 $s-2 0; + margin: $s-4 $s-4 0 $s-4; + border-radius: $br-6; + background-color: var(--panel-title-background-color); + + .shortcuts-title { + @include flexCenter; + flex-grow: 1; + color: var(--title-foreground-color-hover); + } + + .shortcuts-close-button { + @extend .button-tertiary; + position: absolute; + right: $s-2; + top: $s-2; + height: $s-28; + width: $s-28; + border-radius: $br-5; + + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + } +} + +.section { + margin: 0; +} + +.not-found { + @include bodySmallTypography; + color: var(--empty-message-foreground-color); + margin: $s-12; +} + +.shortcuts-list { + display: flex; + flex-direction: column; + height: 100%; + padding: $s-12; + overflow-y: scroll; + font-size: $fs-12; + color: var(--title-foreground-color); + + .section-title, + .subsection-title { + @include uppercaseTitleTipography; + display: flex; + align-items: center; + margin: 0; + padding: $s-8 0; + cursor: pointer; + + .collapsed-shortcuts { + @include flexCenter; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + &.open { + transform: rotate(90deg); + } + } + .subsection-name, + .section-name { + padding-left: $s-4; + } + &:hover { + color: var(--title-foreground-color-hover); + .collapsed-shortcuts { + svg { + stroke: var(--title-foreground-color-hover); + } + } + } + } + + .subsection-title { + text-transform: none; + padding-left: $s-12; + } + .subsection-menu { + margin-bottom: $s-4; + } + .sub-menu { + margin-bottom: $s-4; + + .shortcuts-name { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + min-height: $s-32; + padding: $s-6; + margin-bottom: $s-4; + border-radius: $br-8; + background-color: var(--pill-background-color); + + .command-name { + @include bodySmallTypography; + margin-left: $s-2; + color: var(--pill-foreground-color); + } + .keys { + @include flexCenter; + gap: $s-2; + color: var(--pill-foreground-color); + + .key { + @include bodySmallTypography; + @include flexCenter; + text-transform: capitalize; + height: $s-20; + padding: $s-2 $s-6; + border-radius: $s-6; + background-color: var(--menu-shortcut-background-color); + } + .space { + margin: 0 $s-2; + } + } + } } } diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index f6397ad3bf..9756150482 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -5,7 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.sitemap - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] @@ -13,10 +13,11 @@ [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.context :as ctx] [app.main.ui.hooks :as hooks] - [app.main.ui.hooks.resize :refer [use-resize-hook]] [app.main.ui.icons :as i] + [app.main.ui.notifications.badge :refer [badge-notification]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] @@ -26,16 +27,17 @@ ;; --- Page Item -(mf/defc page-item [{:keys [page index deletable? selected? editing?] :as props}] +(mf/defc page-item + {::mf/wrap-props false} + [{:keys [page index deletable? selected? editing? hovering?]}] (let [input-ref (mf/use-ref) id (:id page) - new-css-system (mf/use-ctx ctx/new-css-system) - delete-fn (mf/use-callback (mf/deps id) #(st/emit! (dw/delete-page id))) - navigate-fn (mf/use-callback (mf/deps id) #(st/emit! :interrupt (dw/go-to-page id))) + delete-fn (mf/use-fn (mf/deps id) #(st/emit! (dw/delete-page id))) + navigate-fn (mf/use-fn (mf/deps id) #(st/emit! :interrupt (dw/go-to-page id))) workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) on-delete - (mf/use-callback + (mf/use-fn (mf/deps id) #(st/emit! (modal/show {:type :confirm @@ -44,7 +46,7 @@ :on-accept delete-fn}))) on-double-click - (mf/use-callback + (mf/use-fn (mf/deps workspace-read-only?) (fn [event] (dom/prevent-default event) @@ -53,16 +55,15 @@ (st/emit! (dw/start-rename-page-item id))))) on-blur - (mf/use-callback + (mf/use-fn (fn [event] - (let [target (dom/event->target event) - name (str/trim (dom/get-value target))] + (let [name (str/trim (dom/get-target-val event))] (when-not (str/empty? name) (st/emit! (dw/rename-page id name))) (st/emit! (dw/stop-rename-page-item))))) on-key-down - (mf/use-callback + (mf/use-fn (fn [event] (cond (kbd/enter? event) @@ -72,7 +73,7 @@ (st/emit! (dw/stop-rename-page-item))))) on-drop - (mf/use-callback + (mf/use-fn (mf/deps id index) (fn [side {:keys [id] :as data}] (let [index (if (= :bot side) (inc index) index)] @@ -88,7 +89,7 @@ :draggable? (not workspace-read-only?)) on-context-menu - (mf/use-callback + (mf/use-fn (mf/deps id workspace-read-only?) (fn [event] (dom/prevent-default event) @@ -115,62 +116,40 @@ (dom/select-text! edit-input)) nil))) - [:* - [:li {:class (if new-css-system - (dom/classnames - (css :page-element) true - (css :selected) selected? - (css :dnd-over-top) (= (:over dprops) :top) - (css :dnd-over-bot) (= (:over dprops) :bot)) - (dom/classnames - :selected selected? - :dnd-over-top (= (:over dprops) :top) - :dnd-over-bot (= (:over dprops) :bot))) - :ref dref} - [:div - {:class (if new-css-system - (dom/classnames - (css :element-list-body) true - (css :selected) selected?) - (dom/classnames - :element-list-body true - :selected selected?)) - :data-test (dm/str "page-" id) - :tab-index "0" - :on-click navigate-fn - :on-double-click on-double-click - :on-context-menu on-context-menu} - [:div {:class (if new-css-system - (dom/classnames (css :page-icon) true) - (dom/classnames :page-icon true))} - (if new-css-system - i/document-refactor - i/file-html)] - (if editing? - [:* - [:input {:class (if new-css-system - (dom/classnames (css :element-name) true) - (dom/classnames :element-name true)) - :type "text" - :ref input-ref - :on-blur on-blur - :on-key-down on-key-down - :auto-focus true - :default-value (:name page "")}]] - [:* - [:span {:class (if new-css-system - (dom/classnames (css :page-name) true) - (dom/classnames :page-name true))} - (:name page)] - [:div - {:class (if new-css-system - (dom/classnames (css :page-actions) true) - (dom/classnames :page-actions true))} - (when (and deletable? (not workspace-read-only?)) - [:button {:on-click on-delete} - (if new-css-system - i/delete-refactor - i/trash)])]])]]])) + [:li {:class (stl/css-case + :page-element true + :selected selected? + :dnd-over-top (= (:over dprops) :top) + :dnd-over-bot (= (:over dprops) :bot)) + :ref dref} + [:div {:class (stl/css-case + :element-list-body true + :hover hovering? + :selected selected?) + :data-test (dm/str "page-" id) + :tab-index "0" + :on-click navigate-fn + :on-double-click on-double-click + :on-context-menu on-context-menu} + [:div {:class (stl/css :page-icon)} + i/document] + + (if editing? + [:* + [:input {:class (stl/css :element-name) + :type "text" + :ref input-ref + :on-blur on-blur + :on-key-down on-key-down + :auto-focus true + :default-value (:name page "")}]] + [:* + [:span {:class (stl/css :page-name)} + (:name page)] + [:div {:class (stl/css :page-actions)} + (when (and deletable? (not workspace-read-only?)) + [:button {:on-click on-delete} + i/delete])]])]])) ;; --- Page Item Wrapper @@ -182,7 +161,8 @@ st/state =)) (mf/defc page-item-wrapper - [{:keys [page-id index deletable? selected? editing?] :as props}] + {::mf/wrap-props false} + [{:keys [page-id index deletable? selected? editing?]}] (let [page-ref (mf/use-memo (mf/deps page-id) #(make-page-ref page-id)) page (mf/deref page-ref)] [:& page-item {:page page @@ -194,16 +174,13 @@ ;; --- Pages List (mf/defc pages-list - [{:keys [file] :as props}] + {::mf/wrap-props false} + [{:keys [file]}] (let [pages (:pages file) deletable? (> (count pages) 1) editing-page-id (mf/deref refs/editing-page-item) - current-page-id (mf/use-ctx ctx/current-page-id) - new-css-system (mf/use-ctx ctx/new-css-system)] - [:ul - {:class (if new-css-system - (dom/classnames (css :pages-list) true) - (dom/classnames :pages-list true))} + current-page-id (mf/use-ctx ctx/current-page-id)] + [:ul {:class (stl/css :page-list)} [:& hooks/sortable-container {} (for [[index page-id] (d/enumerate pages)] [:& page-item-wrapper @@ -217,70 +194,37 @@ ;; --- Sitemap Toolbox (mf/defc sitemap - [] - (let [{:keys [on-pointer-down on-lost-pointer-capture on-pointer-move parent-ref size]} - (use-resize-hook :sitemap 200 38 400 :y false nil) + {::mf/wrap-props false} + [{:keys [size show-pages? toggle-pages]}] + (let [file (mf/deref refs/workspace-file) + file-id (get file :id) + project-id (get file :project-id) - file (mf/deref refs/workspace-file) - create (mf/use-callback - (mf/deps file) - (fn [event] - (let [node (dom/get-current-target event)] - (st/emit! (dw/create-page {:file-id (:id file) - :project-id (:project-id file)})) - (dom/blur! node)))) - show-pages? (mf/use-state true) - size (if @show-pages? size 32) - toggle-pages (mf/use-callback #(reset! show-pages? not)) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) - new-css-system (mf/use-ctx ctx/new-css-system)] + on-create (mf/use-fn + (mf/deps file-id project-id) + (fn [event] + (st/emit! (dw/create-page {:file-id file-id :project-id project-id})) + (-> event dom/get-current-target dom/blur!))) + size (if show-pages? size 32) + read-only? (mf/use-ctx ctx/workspace-read-only?)] - (if new-css-system - [:div {:class (dom/classnames (css :sitemap) true) - :ref parent-ref - :style #js {"--height" (str size "px")}} - [:div {:class (dom/classnames (css :pages-tool-bar) true)} + [:div {:class (stl/css :sitemap) + :style #js {"--height" (str size "px")}} - [:button {:class (dom/classnames (css :page-tool-bar-title) true) - :on-click toggle-pages} - [:span {:class (dom/classnames (css :collapsable-button) true) - :style {:transform (when (not @show-pages?) "rotate(-90deg)")}} - i/arrow-refactor] - (tr "workspace.sidebar.sitemap")] - (if workspace-read-only? - [:div - {:class (dom/classnames (css :view-only-mode) true)} - (tr "labels.view-only")] - [:* - [:button {:class (dom/classnames (css :add-page) true) - :on-click create} - i/add-refactor]])] + [:& title-bar {:collapsable true + :collapsed (not show-pages?) + :on-collapsed toggle-pages + :all-clickable true + :title (tr "workspace.sidebar.sitemap") + :class (stl/css :title-spacing-sitemap)} - [:div {:class (dom/classnames (css :tool-window-content) true)} - [:& pages-list {:file file :key (:id file)}]] + (if ^boolean read-only? + [:& badge-notification {:is-focus true + :size :small + :content (tr "labels.view-only")}] + [:button {:class (stl/css :add-page) + :on-click on-create} + i/add])] - (when @show-pages? - [:div {:class (dom/classnames (css :resize-area) true) - :on-pointer-down on-pointer-down - :on-lost-pointer-capture on-lost-pointer-capture - :on-pointer-move on-pointer-move}])] - - - - [:div#sitemap.tool-window {:ref parent-ref - :style #js {"--height" (str size "px")}} - [:div.tool-window-bar - [:span.pages-title (tr "workspace.sidebar.sitemap")] - (if workspace-read-only? - [:div.view-only-mode (tr "labels.view-only")] - [:div.add-page {:on-click create} i/close]) - [:div.collapse-pages {:on-click toggle-pages - :style {:transform (when (not @show-pages?) "rotate(-90deg)")}} i/arrow-slide]] - - [:div.tool-window-content - [:& pages-list {:file file :key (:id file)}]] - - (when @show-pages? - [:div.resize-area {:on-pointer-down on-pointer-down - :on-lost-pointer-capture on-lost-pointer-capture - :on-pointer-move on-pointer-move}])]))) + [:div {:class (stl/css :tool-window-content)} + [:& pages-list {:file file :key (:id file)}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.css.json b/frontend/src/app/main/ui/workspace/sidebar/sitemap.css.json deleted file mode 100644 index 46ae1745fb..0000000000 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"sidebar_sitemap_button-primary_Z-bKW","sitemap":"sidebar_sitemap_sitemap_kvKKx","pages-tool-bar":"sidebar_sitemap_pages-tool-bar_n1yfA","add-page":"sidebar_sitemap_add-page_r8Ibb","button-secondary":"sidebar_sitemap_button-secondary_a56LZ","button-icon":"sidebar_sitemap_button-icon_MkibT","page-tool-bar-title":"sidebar_sitemap_page-tool-bar-title_D7h-S","collapsable-button":"sidebar_sitemap_collapsable-button_Xt79y","button-icon-small":"sidebar_sitemap_button-icon-small_Mhipv","tool-window-content":"sidebar_sitemap_tool-window-content_G-Nut","pages-list":"sidebar_sitemap_pages-list_cb1Mx","page-element":"sidebar_sitemap_page-element_iR9wf","element-list-body":"sidebar_sitemap_element-list-body_OIVac","page-actions":"sidebar_sitemap_page-actions_QTuKw","page-icon":"sidebar_sitemap_page-icon_ujSjM","view-only-mode":"sidebar_sitemap_view-only-mode_JrsYg","resize-area":"sidebar_sitemap_resize-area_JgdjZ","dnd-over-top":"sidebar_sitemap_dnd-over-top_kGfcb","dnd-over-bot":"sidebar_sitemap_dnd-over-bot_352W2","dnd-over":"sidebar_sitemap_dnd-over_Sf5e2","page-name":"sidebar_sitemap_page-name_601Ii","element-name":"sidebar_sitemap_element-name_iMex0","on-drag":"sidebar_sitemap_on-drag_v3GM8","selected":"sidebar_sitemap_selected_mCOlT","hidden":"sidebar_sitemap_hidden_viFSn"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss index 037a925f39..cd12ae5727 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss @@ -12,249 +12,220 @@ flex-direction: column; flex: 1; width: 100%; - height: var(--height, 200px); + height: var(--height, $s-200); +} - .pages-tool-bar { +.title { + margin-left: $s-2; + color: var(--title-foreground-color-hover); +} + +.add-page { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + padding: 0; + margin-right: $s-12; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + transform: rotate(90deg); + } +} + +.resize-area { + position: absolute; + bottom: calc(-1 * $s-8); + left: 0; + width: 100%; + height: $s-12; + border-top: $s-2 solid var(--resize-area-border-color); + background-color: var(--resize-area-background-color); + cursor: ns-resize; + &:hover { + border-color: var(--resize-area-border-color); + } +} + +.tool-window-content { + display: flex; + flex-direction: column; + height: calc(-38px + var(--height, $s-200)); + width: 100%; + overflow-x: hidden; + overflow-y: overlay; + scrollbar-gutter: stable; +} + +.pages-list { + width: 100%; + max-height: $s-152; + margin-bottom: $s-12; +} + +.page-element { + @include bodySmallTypography; + min-height: $s-32; + width: 100%; + cursor: pointer; + &.dnd-over-top { + border-top: $s-1 solid var(--layer-row-foreground-color-drag); + } + &.dnd-over-bot { + border-bottom: $s-1 solid var(--layer-row-foreground-color-drag); + } + .dnd-over > .element-list-body { + border: $s-1 solid var(--layer-row-foreground-color-drag); + } + .element-list-body { display: flex; align-items: center; - min-height: $s-32; - padding: 0 $s-8 0 0; - margin: $s-2 0; - .page-tool-bar-title { - @include flexCenter; - @include tabTitleTipography; - @include buttonStyle; + height: $s-32; + width: 100%; + padding: 0 $s-12 0 0; + transition: none; + color: var(--layer-row-foreground-color); + .page-name { + @include textEllipsis; flex-grow: 1; - gap: $s-4; - justify-content: flex-start; - padding: 0; - margin: 0; - color: var(--title-foreground-color); - .collapsable-button { + padding-left: $s-2; + } + .page-icon { + @include flexCenter; + height: $s-32; + width: $s-24; + padding: 0 $s-4 0 $s-8; + svg { + @extend .button-icon-small; + height: $s-12; + width: $s-12; + color: transparent; + fill: none; + stroke: var(--icon-foreground); + } + } + .page-actions { + height: $s-32; + button { + @include buttonStyle; @include flexCenter; - height: $s-24; width: $s-24; - padding: 0 $s-4 0 $s-8; - border-radius: $br-8; + height: 100%; + opacity: $op-0; svg { - @extend .button-icon; + @extend .button-icon-small; height: $s-12; width: $s-12; - transform: rotate(90deg); - } - } - &:hover { - color: var(--title-foreground-color-hover); - svg { - stroke: var(--icon-foreground-hover); + color: transparent; + fill: none; + stroke: var(--icon-foreground); } } } - .add-page { - @extend .button-primary; - height: $s-32; - width: calc($s-24 + $s-4); - padding: 0; + .element-name { + @include textEllipsis; + color: var(--layer-row-foreground-color-focus); + } + input.element-name { + @include textEllipsis; + @include bodySmallTypography; + @include removeInputStyle; + flex-grow: 1; + height: $s-28; + max-width: calc(var(--parent-size) - (var(--depth) * var(--layer-indentation-size))); + padding-left: $s-6; + margin: 0; border-radius: $br-8; - svg { - @extend .button-icon; - transform: rotate(90deg); + border: $s-1 solid var(--input-border-color-focus); + color: var(--layer-row-foreground-color); + } + } + &:active, + &.on-drag { + .element-list-body { + color: var(--layer-row-foreground-color-drag); + background-color: var(--layer-row-background-color-drag); + .page-actions button { + svg { + stroke: var(--layer-row-foreground-color-drag); + } + } + .page-icon svg { + stroke: var(--layer-row-foreground-color-drag); } } - .view-only-mode { - @include flexCenter; - @include titleTipography; - height: $s-20; - padding: $s-4 $s-6; - border: 1px solid var(--tag-background-color); - border-radius: $br-6; - color: var(--tag-background-color); + } + &.selected, + &.selected:hover { + .element-list-body { + color: var(--layer-row-foreground-color-selected); + background-color: var(--layer-row-background-color-selected); + box-shadow: $s-16 $s-0 $s-0 $s-0 var(--layer-row-background-color-selected); + .page-actions button { + svg { + stroke: var(--layer-row-foreground-color-selected); + } + } + .page-icon svg { + stroke: var(--layer-row-foreground-color-selected); + } } } - .resize-area { - position: absolute; - bottom: -8px; - left: 0; - width: 100%; - height: $s-12; - border-top: 2px solid var(--color-background-secondary); - background-color: var(--color-background-primary); - cursor: ns-resize; - &:hover { - border-color: var(--color-background-quaternary); + &:hover, + &.hover { + .element-list-body { + color: var(--layer-row-foreground-color-hover); + background-color: var(--layer-row-background-color-hover); + box-shadow: $s-16 $s-0 $s-0 $s-0 var(--layer-row-background-color-hover); + .page-actions button { + opacity: $op-10; + svg { + stroke: var(--layer-row-foreground-color-hover); + } + } + .page-icon svg { + stroke: var(--layer-row-foreground-color-hover); + } + } + } + &:focus { + .element-list-body { + color: var(--layer-row-foreground-color-focus); + border: $s-1 solid var(--layer-row-border-color-focus); + outline: none; + .page-actions button { + opacity: $op-10; + } + } + } + &:focus-within { + .element-list-body { + outline: none; + .page-actions button { + opacity: $op-10; + } } } - .tool-window-content { - display: flex; - flex-direction: column; - overflow-y: auto; - overflow-x: hidden; - scrollbar-gutter: stable; - overflow-y: overlay; - height: 100%; - width: 100%; - .pages-list { - width: 100%; - max-height: $s-152; - margin-bottom: $s-12; - .page-element { - @include titleTipography; - min-height: $s-32; - width: 100%; - cursor: pointer; - &.dnd-over-top { - border-top: 1px solid var(--layer-row-foreground-color-drag); - } - &.dnd-over-bot { - border-bottom: 1px solid var(--layer-row-foreground-color-drag); - } - .dnd-over > .element-list-body { - border: 1px solid var(--layer-row-foreground-color-drag); - } - .element-list-body { - display: flex; - align-items: center; - height: $s-32; - width: 100%; - padding: 0 $s-12 0 0; - transition: none; - color: var(--layer-row-foreground-color); - .page-name { - flex-grow: 1; - padding-left: $s-2; - } - .page-icon { - @include flexCenter; - height: $s-32; - width: $s-24; - padding: 0 $s-4 0 $s-8; - svg { - @extend .button-icon-small; - height: $s-12; - width: $s-12; - color: transparent; - fill: none; - stroke: var(--icon-foreground); - } - } - .page-actions { - height: $s-32; - button { - @include buttonStyle; - @include flexCenter; - width: $s-24; - height: 100%; - opacity: $op-0; - svg { - @extend .button-icon-small; - height: $s-12; - width: $s-12; - color: transparent; - fill: none; - stroke: var(--icon-foreground); - } - } - } - .element-name { - @include textEllipsis; - color: var(--color-foreground-primary); - } - input.element-name { - @include textEllipsis; - @include titleTipography; - flex-grow: 1; - height: $s-28; - max-width: calc(var(--parent-size) - (var(--depth) * var(--layer-indentation-size))); - margin: 0; - padding-left: $s-6; - border-radius: $br-8; - border: 1px solid var(--input-border-color-focus); - outline: none; - background-color: transparent; - color: var(--layer-row-foreground-color); - } - } - &:active, - &.on-drag { - .element-list-body { - color: var(--layer-row-foreground-color-drag); - background-color: var(--layer-row-background-color-drag); - .page-actions button { - svg { - stroke: var(--layer-row-foreground-color-drag); - } - } - .page-icon svg { - stroke: var(--layer-row-foreground-color-drag); - } - } - } - &.selected, - &.selected:hover { - .element-list-body { - color: var(--layer-row-foreground-color-selected); - background-color: var(--layer-row-background-color-selected); - .page-actions button { - svg { - stroke: var(--layer-row-foreground-color-selected); - } - } - .page-icon svg { - stroke: var(--layer-row-foreground-color-selected); - } - } - } - &:hover { - .element-list-body { - color: var(--layer-row-foreground-color-hover); - background-color: var(--layer-row-background-color-hover); - .page-actions button { - opacity: $op-10; - svg { - stroke: var(--layer-row-foreground-color-hover); - } - } - .page-icon svg { - stroke: var(--layer-row-foreground-color-hover); - } - } - } - &:focus { - .element-list-body { - color: var(--layer-row-foreground-color-focus); - border: 1px solid var(--layer-row-border-color-focus); - outline: none; - .page-actions button { - opacity: $op-10; - } - } - } - &:focus-within { - .element-list-body { - outline: none; - .page-actions button { - opacity: $op-10; - } - } - } - - &.hidden { - .element-list-body { - color: var(--layer-row-foreground-color-hidden); - background-color: var(--layer-row-background-color-hidden); - opacity: $op-7; - .page-actions button { - svg { - stroke: var(--layer-row-foreground-color-hidden); - } - } - .page-icon svg { - stroke: var(--layer-row-foreground-color-hidden); - } - } + &.hidden { + .element-list-body { + color: var(--layer-row-foreground-color-hidden); + background-color: var(--layer-row-background-color-hidden); + opacity: $op-7; + .page-actions button { + svg { + stroke: var(--layer-row-foreground-color-hidden); } } + .page-icon svg { + stroke: var(--layer-row-foreground-color-hidden); + } } } } + +.title-spacing-sitemap { + padding-inline-start: $s-8; + margin-block-start: $s-8; + margin-block-end: $s-4; +} diff --git a/frontend/src/app/main/ui/workspace/text_palette.cljs b/frontend/src/app/main/ui/workspace/text_palette.cljs index 83037e8b0a..3869a3ec36 100644 --- a/frontend/src/app/main/ui/workspace/text_palette.cljs +++ b/frontend/src/app/main/ui/workspace/text_palette.cljs @@ -5,7 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.text-palette - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.main.data.workspace.texts :as dwt] @@ -14,7 +14,6 @@ [app.main.store :as st] [app.main.ui.context :as ctx] [app.main.ui.icons :as i] - [app.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.object :as obj] [cuerdas.core :as str] @@ -42,11 +41,11 @@ :attrs attrs})) selected-ids))))] [:div {:on-click handle-click - :class (dom/classnames (css :typography-item) true - (css :mid-item) (<= size 72) - (css :small-item) (<= size 64))} + :class (stl/css-case :typography-item true + :mid-item (<= size 72) + :small-item (<= size 64))} [:div - {:class (dom/classnames (css :typography-name) true) + {:class (stl/css :typography-name) :title (:name typography) :style {:font-family (:font-family typography) :font-weight (:font-weight typography) @@ -54,9 +53,9 @@ (:name typography)] (when-not name-only? [:* - [:div {:class (dom/classnames (css :typography-font) true)} + [:div {:class (stl/css :typography-font)} (:name font-data)] - [:div {:class (dom/classnames (css :typography-data) true)} + [:div {:class (stl/css :typography-data)} (str (:font-size typography) "px | " (:name variant-data))]])])) (mf/defc palette @@ -132,26 +131,25 @@ (when (not= 0 (:offset @state)) (swap! state assoc :offset 0))) - [:div {:class (dom/classnames (css :text-palette) true) + [:div {:class (stl/css :text-palette) :style #js {"--height" (str size "px")}} - (when show-arrows? - [:button {:class (dom/classnames (css :left-arrow) true) + [:button {:class (stl/css :left-arrow) :disabled (= offset 0) - :on-click on-left-arrow-click} i/arrow-refactor]) + :on-click on-left-arrow-click} i/arrow]) - [:div {:class (dom/classnames (css :text-palette-content) true) + [:div {:class (stl/css :text-palette-content) :ref container :on-wheel on-wheel} (if (empty? current-typographies) - [:div {:class (dom/classnames (css :text-palette-empty) true) + [:div {:class (stl/css :text-palette-empty) :style {:position "absolute" :left "50%" :top "50%" :transform "translate(-50%, -50%)"}} (tr "workspace.libraries.colors.empty-typography-palette")] [:div - {:class (dom/classnames (css :text-palette-inside) true) + {:class (stl/css :text-palette-inside) :style {:position "relative" :max-width (str width "px") :right (str (* offset-step offset) "px")}} @@ -164,9 +162,9 @@ :size size}])])] (when show-arrows? - [:button {:class (dom/classnames (css :right-arrow) true) + [:button {:class (stl/css :right-arrow) :disabled (= offset max-offset) - :on-click on-right-arrow-click} i/arrow-refactor])])) + :on-click on-right-arrow-click} i/arrow])])) (mf/defc text-palette {::mf/wrap [mf/memo]} diff --git a/frontend/src/app/main/ui/workspace/text_palette.css.json b/frontend/src/app/main/ui/workspace/text_palette.css.json deleted file mode 100644 index 236e19b66e..0000000000 --- a/frontend/src/app/main/ui/workspace/text_palette.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"workspace_text_palette_button-primary_1umSD","button-secondary":"workspace_text_palette_button-secondary_VOIWz","button-icon":"workspace_text_palette_button-icon_bcydd","text-palette":"workspace_text_palette_text-palette_0yeGp","left-arrow":"workspace_text_palette_left-arrow_iSjPL","right-arrow":"workspace_text_palette_right-arrow_cWHr6","button-icon-small":"workspace_text_palette_button-icon-small_wGyH7","disabled":"workspace_text_palette_disabled_EF36J","text-palette-content":"workspace_text_palette_text-palette-content_anJb5","text-palette-inside":"workspace_text_palette_text-palette-inside_LgHnf","typography-item":"workspace_text_palette_typography-item_d0vFL","typography-name":"workspace_text_palette_typography-name_NVBRv","typography-font":"workspace_text_palette_typography-font_paqmC","typography-data":"workspace_text_palette_typography-data_eKyme","mid-item":"workspace_text_palette_mid-item_uTcD2","small-item":"workspace_text_palette_small-item_1Y6mx"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/text_palette.scss b/frontend/src/app/main/ui/workspace/text_palette.scss index 0e8fab650a..fcbea076fa 100644 --- a/frontend/src/app/main/ui/workspace/text_palette.scss +++ b/frontend/src/app/main/ui/workspace/text_palette.scss @@ -9,126 +9,132 @@ .text-palette { height: 100%; display: flex; - .left-arrow, - .right-arrow { - @include buttonStyle; - @include flexCenter; - position: relative; +} +.left-arrow, +.right-arrow { + @include buttonStyle; + @include flexCenter; + position: relative; + height: 100%; + width: $s-24; + padding: 0; + z-index: $z-index-2; + svg { + @extend .button-icon; + } + &::after { + content: ""; + position: absolute; + bottom: 0; + left: calc(-1 * $s-80); height: 100%; - width: $s-24; - padding: 0; - z-index: $z-index-4; + width: $s-80; + z-index: $z-index-1; + background-image: linear-gradient( + to left, + var(--palette-button-shadow-initial) 0%, + var(--palette-button-shadow-final) 100% + ); + pointer-events: none; + } + &:hover { svg { - @extend .button-icon; - } - &::after { - content: ""; - position: absolute; - bottom: 0; - left: calc(-1 * $s-80); - height: 100%; - width: $s-80; - z-index: $z-index-1; - background-image: linear-gradient( - to left, - var(--palette-button-shadow-initial) 0%, - var(--palette-button-shadow-final) 100% - ); - pointer-events: none; - } - &:hover { - svg { - stroke: var(--button-foreground-hover); - } - } - &:disabled { - svg { - stroke: var(--button-foreground-color-disabled); - } - &::after { - background-image: none; - } + stroke: var(--button-foreground-hover); } } - .left-arrow { - &::after { - left: $s-24; - background-image: linear-gradient( - to right, - var(--palette-button-shadow-initial) 0%, - var(--palette-button-shadow-final) 100% - ); + &:disabled { + svg { + stroke: var(--button-foreground-color-disabled); } - &.disabled ::after { + &::after { background-image: none; } - - svg { - transform: rotate(180deg); - } - } - - .text-palette-content { - display: flex; - overflow: hidden; - - .text-palette-inside { - display: flex; - gap: $s-8; - } - .typography-item { - @include titleTipography; - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - height: 100%; - width: $s-136; - padding: $s-8; - border-radius: $br-8; - background-color: var(--palette-text-background-color); - &:first-child { - margin-left: $s-8; - } - .typography-name { - @include tabTitleTipography; - @include textEllipsis; - height: $s-16; - width: $s-120; - color: var(--palette-text-color-selected); - } - .typography-font { - @include textEllipsis; - height: $s-16; - width: $s-120; - color: var(--palette-text-color); - } - .typography-data { - @include textEllipsis; - height: $s-16; - width: $s-120; - color: var(--palette-text-color); - } - &.mid-item { - .typography-name { - height: $s-16; - } - .typography-data { - display: none; - } - } - &.small-item { - .typography-name { - height: $s-12; - } - .typography-data, - .typography-font { - display: none; - } - } - &:hover { - background-color: var(--palette-text-background-color-hover); - } - } } } +.left-arrow { + &::after { + left: $s-24; + background-image: linear-gradient( + to right, + var(--palette-button-shadow-initial) 0%, + var(--palette-button-shadow-final) 100% + ); + } + &.disabled ::after { + background-image: none; + } + + svg { + transform: rotate(180deg); + } +} + +.text-palette-content { + display: flex; + overflow: hidden; +} + +.text-palette-inside { + display: flex; + gap: $s-8; +} + +.typography-item { + @include bodySmallTypography; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + height: 100%; + width: $s-136; + padding: $s-8; + border-radius: $br-8; + background-color: var(--palette-text-background-color); + &:first-child { + margin-left: $s-8; + } + + .typography-name { + @include textEllipsis; + height: $s-16; + width: $s-120; + color: var(--palette-text-color-selected); + } + + .typography-font { + @include textEllipsis; + height: $s-16; + width: $s-120; + color: var(--palette-text-color); + } + + .typography-data { + @include textEllipsis; + height: $s-16; + width: $s-120; + color: var(--palette-text-color); + } + + &.mid-item { + .typography-name { + height: $s-16; + } + .typography-data { + display: none; + } + } + &.small-item { + .typography-data, + .typography-font { + display: none; + } + } + &:hover { + background-color: var(--palette-text-background-color-hover); + } +} + +.text-palette-empty { + @include bodySmallTypography; + color: var(--palette-text-color); +} diff --git a/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.cljs b/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.cljs index 2f6e65054d..3f1b573dfe 100644 --- a/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.cljs +++ b/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.cljs @@ -5,14 +5,13 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.text-palette-ctx-menu - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.main.refs :as refs] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.icons :as i] - [app.util.dom :as dom] [app.util.i18n :refer [tr]] - [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -22,31 +21,35 @@ shared-libs (mf/deref refs/workspace-libraries)] [:& dropdown {:show show-menu? :on-close close-menu} - [:ul {:class (dom/classnames (css :workspace-context-menu) true)} + [:ul {:class (stl/css :text-context-menu)} (for [[idx cur-library] (map-indexed vector (vals shared-libs))] (let [typographies (-> cur-library (get-in [:data :typographies]) vals)] [:li - {:class (dom/classnames (css :palette-library) true - (css :selected) (= selected (:id cur-library))) + {:class (stl/css-case :palette-library true + :selected (= selected (:id cur-library))) :key (str "library-" idx) - :on-click #(on-select-palette cur-library)} + :on-click #(on-select-palette cur-library)} [:div - {:class (dom/classnames (css :library-name) true)} - (str (:name cur-library) " " (str/format "(%s)" (count typographies)))] - + {:class (stl/css :library-name)} + [:span {:class (stl/css :lib-name)} + (dm/str (:name cur-library))] + [:span {:class (stl/css :lib-num)} + (dm/str "(" (count typographies) ")")]] + (when (= selected (:id cur-library)) - [:span {:class (dom/classnames (css :icon-wrapper) true)} - i/tick-refactor])])) + [:span {:class (stl/css :icon-wrapper)} + i/tick])])) [:li - {:class (dom/classnames (css :file-library) true - (css :selected) (= selected :file)) + {:class (stl/css-case :file-library true + :selected (= selected :file)) :on-click #(on-select-palette :file)} - - [:div {:class (dom/classnames (css :library-name) true)} - (str (tr "workspace.libraries.colors.file-library") - (str/format " (%s)" (count file-typographies)))] + + [:div {:class (stl/css :library-name)} + [:span {:class (stl/css :lib-name)} + (tr "workspace.libraries.colors.file-library")] + [:span {:class (stl/css :lib-num)} + (dm/str "(" (count file-typographies) ")")]] (when (= selected :file) - [:span {:class (dom/classnames (css :icon-wrapper) true)} - i/tick-refactor]) - ]]])) + [:span {:class (stl/css :icon-wrapper)} + i/tick])]]])) diff --git a/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.css.json b/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.css.json deleted file mode 100644 index 3629792d0a..0000000000 --- a/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.css.json +++ /dev/null @@ -1 +0,0 @@ -{"button-primary":"workspace_text_palette_ctx_menu_button-primary_bkGXB","button-secondary":"workspace_text_palette_ctx_menu_button-secondary_mbPs7","button-icon":"workspace_text_palette_ctx_menu_button-icon_oklnh","button-icon-small":"workspace_text_palette_ctx_menu_button-icon-small_ebriD","workspace-context-menu":"workspace_text_palette_ctx_menu_workspace-context-menu_OShZn","palette-library":"workspace_text_palette_ctx_menu_palette-library_pDyi5","selected":"workspace_text_palette_ctx_menu_selected_k3kOd","icon-wrapper":"workspace_text_palette_ctx_menu_icon-wrapper_Xoj9o","file-library":"workspace_text_palette_ctx_menu_file-library_t-25M","library-name":"workspace_text_palette_ctx_menu_library-name_TGs9Z"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss b/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss index 98cbbe191f..6647385623 100644 --- a/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss +++ b/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss @@ -6,14 +6,14 @@ @import "refactor/common-refactor.scss"; -.workspace-context-menu { +.text-context-menu { position: absolute; left: auto; - bottom: $s-0; - max-width: $s-248; + bottom: var(--height); + max-width: $s-480; padding: $s-4; margin: 0 0 $s-4 0; - z-index: $z-index-10; + z-index: $z-index-4; border-radius: $br-10; background-color: var(--context-menu-background-color); @@ -22,6 +22,7 @@ position: relative; display: flex; justify-content: space-between; + width: 100%; gap: $s-8; padding: $s-8; margin-bottom: $s-4; @@ -31,8 +32,27 @@ margin-bottom: 0; } .library-name { - @include titleTipography; + @include bodySmallTypography; color: var(--context-menu-foreground-color); + display: grid; + grid-template-columns: 1fr $s-24; + max-width: $s-400; + .lib-name { + @include textEllipsis; + max-width: $s-380; + } + .lib-num { + margin-left: $s-4; + } + } + .icon-wrapper { + margin-left: $s-4; + @include flexCenter; + svg { + @include flexCenter; + @extend .button-icon-small; + stroke: var(--icon-foreground); + } } &.selected, &:hover { @@ -41,6 +61,7 @@ svg { @include flexCenter; @extend .button-icon-small; + stroke: var(--context-menu-foreground-color-selected); } } .library-name { diff --git a/frontend/src/app/main/ui/workspace/textpalette.cljs b/frontend/src/app/main/ui/workspace/textpalette.cljs deleted file mode 100644 index 9877b9c465..0000000000 --- a/frontend/src/app/main/ui/workspace/textpalette.cljs +++ /dev/null @@ -1,159 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.workspace.textpalette - (:require - [app.common.data :as d] - [app.main.data.workspace.texts :as dwt] - [app.main.fonts :as f] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.context :as ctx] - [app.main.ui.hooks.resize :refer [use-resize-hook]] - [app.main.ui.icons :as i] - [app.util.dom :as dom] - [app.util.i18n :refer [tr]] - [cuerdas.core :as str] - [rumext.v2 :as mf])) - -(mf/defc typography-item - [{:keys [file-id selected-ids typography name-only?]}] - (let [font-data (f/get-font-data (:font-id typography)) - font-variant-id (:font-variant-id typography) - variant-data (->> font-data :variants (d/seek #(= (:id %) font-variant-id))) - - handle-click - (mf/use-callback - (mf/deps typography selected-ids) - (fn [] - (let [attrs (merge - {:typography-ref-file file-id - :typography-ref-id (:id typography)} - (dissoc typography :id :name))] - - (run! #(st/emit! - (dwt/update-text-attrs - {:id % - :editor (get @refs/workspace-editor-state %) - :attrs attrs})) - selected-ids))))] - - [:div.typography-item {:title (:name typography) - :on-click handle-click} - [:div.typography-name - {:style {:font-family (:font-family typography) - :font-weight (:font-weight typography) - :font-style (:font-style typography)}} - (:name typography)] - (when-not name-only? - [:* - [:div.typography-font (:name font-data)] - [:div.typography-data (str (:font-size typography) "px | " (:name variant-data))]])])) - -(mf/defc palette - [{:keys [selected-ids current-file-id file-typographies shared-libs]}] - - (let [state (mf/use-state {:show-menu false}) - selected (mf/use-state :file) - - file-id - (case @selected - :recent nil - :file current-file-id - @selected) - - current-typographies - (case @selected - :recent [] - :file (sort-by #(str/lower (:name %)) (vals file-typographies)) - (sort-by #(str/lower (:name %)) (vals (get-in shared-libs [@selected :data :typographies])))) - - container (mf/use-ref nil) - - on-left-arrow-click - (mf/use-callback - (fn [] - (when-let [node (mf/ref-val container)] - (.scrollBy node #js {:left -200 :behavior "smooth"})))) - - on-right-arrow-click - (mf/use-callback - (fn [] - (when-let [node (mf/ref-val container)] - (.scrollBy node #js {:left 200 :behavior "smooth"})))) - - on-wheel - (mf/use-callback - (fn [event] - (let [delta (+ (.. event -nativeEvent -deltaY) (.. event -nativeEvent -deltaX))] - (if (pos? delta) - (on-right-arrow-click) - (on-left-arrow-click))))) - - {:keys [on-pointer-down on-lost-pointer-capture on-pointer-move parent-ref size]} - (use-resize-hook :palette 72 54 80 :y true :bottom)] - - [:div.color-palette {:ref parent-ref - :class (dom/classnames :no-text (< size 72)) - :style #js {"--height" (str size "px")}} - [:div.resize-area {:on-pointer-down on-pointer-down - :on-lost-pointer-capture on-lost-pointer-capture - :on-pointer-move on-pointer-move}] - [:& dropdown {:show (:show-menu @state) - :on-close #(swap! state assoc :show-menu false)} - - [:ul.workspace-context-menu.palette-menu - (for [[idx cur-library] (map-indexed vector (vals shared-libs))] - (let [typographies (-> cur-library (get-in [:data :typographies]) vals)] - [:li.palette-library - {:key (str "library-" idx) - :on-click #(reset! selected (:id cur-library))} - - (when (= @selected (:id cur-library)) i/tick) - - [:div.library-name (str (:name cur-library) " " (str/format "(%s)" (count typographies)))]])) - - [:li.palette-library - {:on-click #(reset! selected :file)} - (when (= selected :file) i/tick) - [:div.library-name (str (tr "workspace.libraries.colors.file-library") - (str/format " (%s)" (count file-typographies)))]]]] - - [:div.color-palette-actions - {:on-click #(swap! state assoc :show-menu true)} - [:div.color-palette-actions-button i/actions]] - - [:span.left-arrow {:on-click on-left-arrow-click} i/arrow-slide] - - [:div.color-palette-content {:ref container :on-wheel on-wheel} - (if (empty? current-typographies) - [:div.color-palette-empty {:style {:position "absolute" - :left "50%" - :top "50%" - :transform "translate(-50%, -50%)"}} - (tr "workspace.libraries.colors.empty-typography-palette")] - [:div.color-palette-inside - (for [[idx item] (map-indexed vector current-typographies)] - [:& typography-item - {:key idx - :file-id file-id - :selected-ids selected-ids - :typography item}])])] - - [:span.right-arrow {:on-click on-right-arrow-click} i/arrow-slide]])) - -(mf/defc textpalette - {::mf/wrap [mf/memo]} - [] - (let [selected-ids (mf/deref refs/selected-shapes) - file-typographies (mf/deref refs/workspace-file-typography) - shared-libs (mf/deref refs/workspace-libraries) - current-file-id (mf/use-ctx ctx/current-file-id)] - [:& palette {:current-file-id current-file-id - :selected-ids selected-ids - :file-typographies file-typographies - :shared-libs shared-libs}])) diff --git a/frontend/src/app/main/ui/workspace/left_toolbar.cljs b/frontend/src/app/main/ui/workspace/top_toolbar.cljs similarity index 51% rename from frontend/src/app/main/ui/workspace/left_toolbar.cljs rename to frontend/src/app/main/ui/workspace/top_toolbar.cljs index 2a61a31206..979c84e19b 100644 --- a/frontend/src/app/main/ui/workspace/left_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/top_toolbar.cljs @@ -4,36 +4,40 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.main.ui.workspace.left-toolbar +(ns app.main.ui.workspace.top-toolbar + (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.media :as cm] [app.main.data.events :as ev] [app.main.data.workspace :as dw] + [app.main.data.workspace.common :as dwc] [app.main.data.workspace.media :as dwm] + [app.main.data.workspace.path.state :as pst] [app.main.data.workspace.shortcuts :as sc] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.context :as ctx] - [app.main.ui.hooks.resize :as r] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.timers :as ts] + [okulary.core :as l] [rumext.v2 :as mf])) (mf/defc image-upload {::mf/wrap [mf/memo]} [] - (let [ref (mf/use-ref nil) - file-id (mf/use-ctx ctx/current-file-id) + (let [ref (mf/use-ref nil) + file-id (mf/use-ctx ctx/current-file-id) on-click - (mf/use-fn - (fn [] - (st/emit! :interrupt dw/clear-edition-mode) - (dom/click (mf/ref-val ref)))) + (mf/use-fn + (fn [] + (st/emit! :interrupt (dw/clear-edition-mode)) + (dom/click (mf/ref-val ref)))) on-selected (mf/use-fn @@ -48,13 +52,13 @@ :blobs (seq blobs) :position (gpt/point x y)}] (st/emit! (dwm/upload-media-workspace params)))))] - [:li [:button {:title (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image)) :aria-label (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image)) - :on-click on-click} - i/image + :on-click on-click + :class (stl/css :main-toolbar-options-button)} + i/img [:& file-uploader {:input-id "image-upload" :accept cm/str-image-types @@ -62,18 +66,26 @@ :ref ref :on-selected on-selected}]]])) -(mf/defc left-toolbar +(def toolbar-hidden + (l/derived + (fn [state] + (let [visibility (dm/get-in state [:workspace-local :hide-toolbar]) + editing? (pst/path-editing? state) + hidden? (if editing? true visibility)] + hidden?)) + st/state)) + +(mf/defc top-toolbar {::mf/wrap [mf/memo] ::mf/wrap-props false} [{:keys [layout]}] (let [selected-drawtool (mf/deref refs/selected-drawing-tool) edition (mf/deref refs/selected-edition) - new-css? (mf/use-ctx ctx/new-css-system) read-only? (mf/use-ctx ctx/workspace-read-only?) - show-palette-btn? (and (not ^boolean read-only?) (not ^boolean new-css?)) - + rulers? (mf/deref refs/rulers?) + hide-toolbar? (mf/deref toolbar-hidden) interrupt (mf/use-fn #(st/emit! :interrupt)) @@ -84,44 +96,11 @@ (let [tool (-> (dom/get-current-target event) (dom/get-data "tool") (keyword))] - (st/emit! :interrupt - dw/clear-edition-mode) + (st/emit! :interrupt (dw/clear-edition-mode)) ;; Delay so anything that launched :interrupt can finish (ts/schedule 100 #(st/emit! (dw/select-for-drawing tool)))))) - toggle-text-palette - (mf/use-fn - (fn [] - (r/set-resize-type! :bottom) - (-> (dom/get-element-by-class "color-palette") - (dom/add-class! "fade-out-down")) - (ts/schedule 300 #(st/emit! (dw/remove-layout-flag :colorpalette) - (-> (dw/toggle-layout-flag :textpalette) - (vary-meta assoc ::ev/origin "workspace-left-toolbar")))))) - - toggle-color-palette - (mf/use-fn - (fn [] - (r/set-resize-type! :bottom) - (-> (dom/get-element-by-class "color-palette") - (dom/add-class! "fade-out-down")) - (ts/schedule 300 #(st/emit! (dw/remove-layout-flag :textpalette) - (-> (dw/toggle-layout-flag :colorpalette) - (vary-meta assoc ::ev/origin "workspace-left-toolbar")))))) - - toggle-shortcuts - (mf/use-fn - (mf/deps layout) - (fn [] - (let [is-sidebar-closed? (contains? layout :collapse-left-sidebar)] - (when is-sidebar-closed? - (st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) - (st/emit! - (dw/remove-layout-flag :debug-panel) - (-> (dw/toggle-layout-flag :shortcuts) - (vary-meta assoc ::ev/origin "workspace-left-toolbar")))))) - toggle-debug-panel (mf/use-fn (mf/deps layout) @@ -134,52 +113,57 @@ (-> (dw/toggle-layout-flag :debug-panel) (vary-meta assoc ::ev/origin "workspace-left-toolbar")))))) - ] + toggle-toolbar + (mf/use-fn + #(st/emit! (dwc/toggle-toolbar-visibility)))] - [:aside.left-toolbar - [:ul.left-toolbar-options - [:li - [:button - {:title (tr "workspace.toolbar.move" (sc/get-tooltip :move)) - :aria-label (tr "workspace.toolbar.move" (sc/get-tooltip :move)) - :class (when (and (nil? selected-drawtool) - (not edition)) "selected") - :on-click interrupt} - i/pointer-inner]] - (when-not ^boolean read-only? + (when-not ^boolean read-only? + [:aside {:class (stl/css-case :main-toolbar true + :main-toolbar-no-rulers (not rulers?) + :main-toolbar-hidden hide-toolbar?)} + [:ul {:class (stl/css :main-toolbar-options)} + [:li + [:button + {:title (tr "workspace.toolbar.move" (sc/get-tooltip :move)) + :aria-label (tr "workspace.toolbar.move" (sc/get-tooltip :move)) + :class (stl/css-case :main-toolbar-options-button true + :selected (and (nil? selected-drawtool) + (not edition))) + :on-click interrupt} + i/move]] [:* [:li [:button {:title (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame)) :aria-label (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame)) - :class (when (= selected-drawtool :frame) "selected") + :class (stl/css-case :main-toolbar-options-button true :selected (= selected-drawtool :frame)) :on-click select-drawtool :data-tool "frame" :data-test "artboard-btn"} - i/artboard]] + i/board]] [:li [:button {:title (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect)) :aria-label (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect)) - :class (when (= selected-drawtool :rect) "selected") + :class (stl/css-case :main-toolbar-options-button true :selected (= selected-drawtool :rect)) :on-click select-drawtool :data-tool "rect" :data-test "rect-btn"} - i/box]] + i/rectangle]] [:li [:button {:title (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse)) :aria-label (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse)) - :class (when (= selected-drawtool :circle) "selected") + :class (stl/css-case :main-toolbar-options-button true :selected (= selected-drawtool :circle)) :on-click select-drawtool :data-tool "circle" :data-test "ellipse-btn"} - i/circle]] + i/elipse]] [:li [:button {:title (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text)) :aria-label (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text)) - :class (when (= selected-drawtool :text) "selected") + :class (stl/css-case :main-toolbar-options-button true :selected (= selected-drawtool :text)) :on-click select-drawtool :data-tool "text"} i/text]] @@ -190,59 +174,31 @@ [:button {:title (tr "workspace.toolbar.curve" (sc/get-tooltip :draw-curve)) :aria-label (tr "workspace.toolbar.curve" (sc/get-tooltip :draw-curve)) - :class (when (= selected-drawtool :curve) "selected") + :class (stl/css-case :main-toolbar-options-button true :selected (= selected-drawtool :curve)) :on-click select-drawtool :data-tool "curve" :data-test "curve-btn"} - i/pencil]] + i/curve]] [:li [:button {:title (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path)) :aria-label (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path)) - :class (when (= selected-drawtool :path) "selected") + :class (stl/css-case :main-toolbar-options-button true :selected (= selected-drawtool :path)) :on-click select-drawtool :data-tool "path" :data-test "path-btn"} - i/pen]]]) + i/path]] - [:li - [:button - {:title (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment)) - :aria-label (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment)) - :class (when (= selected-drawtool :comments) "selected") - :on-click select-drawtool - :data-tool "comments"} - i/chat]]] + (when *assert* + [:li + [:button + {:title "Debugging tool" + :class (stl/css-case :main-toolbar-options-button true :selected (contains? layout :debug-panel)) + :on-click toggle-debug-panel} + i/bug]])]] - [:ul.left-toolbar-options.panels - (when ^boolean show-palette-btn? - [:* - [:li - [:button - {:title (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette)) - :aria-label (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette)) - :class (when (contains? layout :textpalette) "selected") - :on-click toggle-text-palette} - "Ag"]] + [:button {:class (stl/css :toolbar-handler) + :on-click toggle-toolbar} + [:div {:class (stl/css :toolbar-handler-btn)}]]]))) - [:li - [:button - {:title (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette)) - :aria-label (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette)) - :class (when (contains? layout :colorpalette) "selected") - :on-click toggle-color-palette} - i/palette]]]) - [:li - [:button - {:title (tr "workspace.toolbar.shortcuts" (sc/get-tooltip :show-shortcuts)) - :aria-label (tr "workspace.toolbar.shortcuts" (sc/get-tooltip :show-shortcuts)) - :class (when (contains? layout :shortcuts) "selected") - :on-click toggle-shortcuts} - i/shortcut] - (when *assert* - [:button - {:title "Debugging tool" - :class (when (contains? layout :debug-panel) "selected") - :on-click toggle-debug-panel} - i/bug])]]])) diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.scss b/frontend/src/app/main/ui/workspace/top_toolbar.scss new file mode 100644 index 0000000000..625e5384e4 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/top_toolbar.scss @@ -0,0 +1,100 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.main-toolbar { + cursor: initial; + position: absolute; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + flex-direction: column; + height: $s-56; + padding: $s-8 $s-16; + border-radius: $s-8; + border: $s-2 solid var(--panel-border-color); + z-index: $z-index-3; + background-color: var(--color-background-primary); + transition: + top 0.3s, + height 0.3s, + opacity 0.3s; + + --toolbar-position-y: #{$s-28}; + --toolbar-offset-y: 0px; + top: calc(var(--toolbar-position-y) + var(--toolbar-offset-y)); +} + +.main-toolbar-no-rulers { + --toolbar-position-y: 0px; + --toolbar-offset-y: #{$s-8}; +} + +.main-toolbar-hidden { + --toolbar-offset-y: -#{$s-4}; + height: $s-16; + z-index: $z-index-1; + border-radius: 0 0 $s-8 $s-8; + border-block-start: 0; + .main-toolbar-options { + opacity: $op-0; + } +} + +.main-toolbar-options { + position: relative; + display: flex; + align-items: center; + margin: 0; + opacity: $op-10; + transition: opacity 0.3s ease; + li { + position: relative; + } +} + +.main-toolbar-options-button { + @extend .button-tertiary; + height: $s-36; + width: $s-36; + flex-shrink: 0; + border-radius: $s-8; + margin: 0 $s-2; + + svg { + @extend .button-icon; + stroke: var(--color-foreground-secondary); + } + + &.selected { + @extend .button-icon-selected; + } +} + +.toolbar-handler { + @include flexCenter; + @include buttonStyle; + position: absolute; + left: 0; + bottom: 0; + height: $s-12; + width: 100%; + + .toolbar-handler-btn { + height: $s-4; + width: 100%; + max-width: $s-64; + padding: 0; + border-radius: $s-4; + background-color: var(--palette-handler-background-color); + } +} + +ul.main-toolbar-panels { + display: none; +} diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index d84b525df1..8539def89e 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -9,15 +9,16 @@ [app.common.colors :as clr] [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] + [app.common.types.shape-tree :as ctt] [app.common.types.shape.layout :as ctl] [app.main.data.workspace.modifiers :as dwm] [app.main.refs :as refs] [app.main.ui.context :as ctx] + [app.main.ui.flex-controls :as mfc] [app.main.ui.hooks :as ui-hooks] [app.main.ui.measurements :as msr] - [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.export :as use] [app.main.ui.workspace.shapes :as shapes] [app.main.ui.workspace.shapes.text.editor :as editor] @@ -36,16 +37,17 @@ [app.main.ui.workspace.viewport.outline :as outline] [app.main.ui.workspace.viewport.pixel-overlay :as pixel-overlay] [app.main.ui.workspace.viewport.presence :as presence] - [app.main.ui.workspace.viewport.rules :as rules] + [app.main.ui.workspace.viewport.rulers :as rulers] [app.main.ui.workspace.viewport.scroll-bars :as scroll-bars] [app.main.ui.workspace.viewport.selection :as selection] [app.main.ui.workspace.viewport.snap-distances :as snap-distances] [app.main.ui.workspace.viewport.snap-points :as snap-points] + [app.main.ui.workspace.viewport.top-bar :as top-bar] [app.main.ui.workspace.viewport.utils :as utils] [app.main.ui.workspace.viewport.viewport-ref :refer [create-viewport-ref]] [app.main.ui.workspace.viewport.widgets :as widgets] - [beicon.core :as rx] - [debug :refer [debug?]] + [app.util.debug :as dbg] + [beicon.v2.core :as rx] [rumext.v2 :as mf])) ;; --- Viewport @@ -58,7 +60,7 @@ objects id (fn [shape] (cond-> shape - (and (cph/text-shape? shape) (contains? text-modifiers id)) + (and (cfh/text-shape? shape) (contains? text-modifiers id)) (dwm/apply-text-modifier (get text-modifiers id)) (contains? modifiers id) @@ -68,7 +70,7 @@ selected)) (mf/defc viewport - [{:keys [wlocal wglobal selected layout file] :as props}] + [{:keys [selected wglobal wlocal layout file palete-size] :as props}] (let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check ;; that the new parameter is sent {:keys [edit-path @@ -87,6 +89,8 @@ show-distances? picking-color?]} wglobal + vbox' (mf/use-debounce 100 vbox) + ;; CONTEXT page-id (mf/use-ctx ctx/current-page-id) @@ -119,6 +123,7 @@ cursor (mf/use-state (utils/get-cursor :pointer-inner)) hover-ids (mf/use-state nil) hover (mf/use-state nil) + measure-hover (mf/use-state nil) hover-disabled? (mf/use-state false) hover-top-frame-id (mf/use-state nil) frame-hover (mf/use-state nil) @@ -140,7 +145,7 @@ (fn [] (let [parent-id (->> @hover-ids - (d/seek (partial cph/root-frame? base-objects)))] + (d/seek (partial cfh/root-frame? base-objects)))] (when (some? parent-id) (get base-objects parent-id))))) @@ -159,7 +164,7 @@ create-comment? (= :comments drawing-tool) drawing-path? (or (and edition (= :draw (get-in edit-path [edition :edit-mode]))) (and (some? drawing-obj) (= :path (:type drawing-obj)))) - node-editing? (and edition (not= :text (get-in base-objects [edition :type]))) + node-editing? (and edition (= :path (get-in base-objects [edition :type]))) text-editing? (and edition (= :text (get-in base-objects [edition :type]))) grid-editing? (and edition (ctl/grid-layout? base-objects edition)) @@ -168,10 +173,13 @@ on-click (actions/on-click hover selected edition drawing-path? drawing-tool space? selrect z?) on-context-menu (actions/on-context-menu hover hover-ids workspace-read-only?) - on-double-click (actions/on-double-click hover hover-ids drawing-path? base-objects edition drawing-tool z? workspace-read-only?) - on-drag-enter (actions/on-drag-enter) - on-drag-over (actions/on-drag-over) - on-drop (actions/on-drop file) + on-double-click (actions/on-double-click hover hover-ids hover-top-frame-id drawing-path? base-objects edition drawing-tool z? workspace-read-only?) + + comp-inst-ref (mf/use-ref false) + on-drag-enter (actions/on-drag-enter comp-inst-ref) + on-drag-over (actions/on-drag-over move-stream) + on-drag-end (actions/on-drag-over comp-inst-ref) + on-drop (actions/on-drop file comp-inst-ref) on-pointer-down (actions/on-pointer-down @hover selected edition drawing-tool text-editing? node-editing? grid-editing? drawing-path? create-comment? space? panning z? workspace-read-only?) @@ -192,7 +200,7 @@ show-cursor-tooltip? tooltip show-draw-area? drawing-obj show-gradient-handlers? (= (count selected) 1) - show-grids? (contains? layout :display-grid) + show-grids? (contains? layout :display-guides) show-frame-outline? (= transform :move) show-outlines? (and (nil? transform) @@ -203,6 +211,10 @@ show-pixel-grid? (and (contains? layout :show-pixel-grid) (>= zoom 8)) show-text-editor? (and editing-shape (= :text (:type editing-shape))) + + hover-grid? (and (some? @hover-top-frame-id) + (ctl/grid-layout? objects @hover-top-frame-id)) + show-grid-editor? (and editing-shape (ctl/grid-layout? editing-shape)) show-presence? page-id show-prototypes? (= options-mode :prototype) @@ -211,61 +223,67 @@ (= transform :move) (seq selected)) show-snap-points? (and (or (contains? layout :dynamic-alignment) - (contains? layout :snap-grid)) + (contains? layout :snap-guides)) (or drawing-obj transform)) show-selrect? (and selrect (empty? drawing) (not text-editing?)) show-measures? (and (not transform) (not node-editing?) (or show-distances? mode-inspect?)) show-artboard-names? (contains? layout :display-artboard-names) - show-rules? (and (contains? layout :rules) (not (contains? layout :hide-ui))) + hide-ui? (contains? layout :hide-ui) + show-rulers? (and (contains? layout :rulers) (not hide-ui?)) - disabled-guides? (or drawing-tool transform) + disabled-guides? (or drawing-tool transform drawing-path? node-editing?) - one-selected-shape? (= (count selected-shapes) 1) + single-select? (= (count selected-shapes) 1) - show-padding? (and (nil? transform) - one-selected-shape? - (= (:type (first selected-shapes)) :frame) - (= (:layout (first selected-shapes)) :flex) - (zero? (:rotation (first selected-shapes)))) + first-shape (first selected-shapes) + show-padding? + (and (nil? transform) + single-select? + (= (:type first-shape) :frame) + (= (:layout first-shape) :flex) + (zero? (:rotation first-shape))) - show-margin? (and (nil? transform) - one-selected-shape? - (= (:layout selected-frame) :flex) - (zero? (:rotation (first selected-shapes)))) + show-margin? + (and (nil? transform) + single-select? + (= (:layout selected-frame) :flex) + (zero? (:rotation first-shape))) - first-selected-shape (first selected-shapes) - selecting-first-level-frame? (and one-selected-shape? - (cph/root-frame? first-selected-shape)) + selecting-first-level-frame? (and single-select? (cfh/root-frame? first-shape)) offset-x (if selecting-first-level-frame? - (:x first-selected-shape) + (:x first-shape) (:x selected-frame)) offset-y (if selecting-first-level-frame? - (:y (first selected-shapes)) - (:y selected-frame))] + (:y first-shape) + (:y selected-frame)) + + rule-area-size (/ rulers/ruler-area-size zoom)] (hooks/setup-dom-events zoom disable-paste in-viewport? workspace-read-only?) (hooks/setup-viewport-size vport viewport-ref) (hooks/setup-cursor cursor alt? mod? space? panning drawing-tool drawing-path? node-editing? z? workspace-read-only?) (hooks/setup-keyboard alt? mod? space? z? shift?) - (hooks/setup-hover-shapes page-id move-stream base-objects transform selected mod? hover hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures?) + (hooks/setup-hover-shapes page-id move-stream base-objects transform selected mod? hover measure-hover + hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures?) (hooks/setup-viewport-modifiers modifiers base-objects) - (hooks/setup-shortcuts node-editing? drawing-path? text-editing?) + (hooks/setup-shortcuts node-editing? drawing-path? text-editing? grid-editing?) (hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox) - [:div.viewport + [:div.viewport {:style #js {"--zoom" zoom}} + [:& top-bar/top-bar {:layout layout}] [:div.viewport-overlays ;; The behaviour inside a foreign object is a bit different that in plain HTML so we wrap ;; inside a foreign object "dummy" so this awkward behaviour is take into account - [:svg {:style {:top 0 :left 0 :position "fixed" :width "100%" :height "100%" :opacity (when-not (debug? :html-text) 0)}} + [:svg {:style {:top 0 :left 0 :position "fixed" :width "100%" :height "100%" :opacity (when-not (dbg/enabled? :html-text) 0)}} [:foreignObject {:x 0 :y 0 :width "100%" :height "100%"} - [:div {:style {:pointer-events (when-not (debug? :html-text) "none") + [:div {:style {:pointer-events (when-not (dbg/enabled? :html-text) "none") ;; some opacity because to debug auto-width events will fill the screen :opacity 0.6}} [:& stvh/viewport-texts @@ -288,9 +306,7 @@ :vbox vbox :options options :layout layout - :viewport-ref viewport-ref}]) - - [:& widgets/viewport-actions]] + :viewport-ref viewport-ref}])] [:svg.render-shapes {:id "render" @@ -306,7 +322,20 @@ :pointer-events "none"} :fill "none"} - (when (debug? :show-export-metadata) + [:defs + [:linearGradient {:id "frame-placeholder-gradient"} + [:animateTransform + {:attributeName "gradientTransform" + :type "translate" + :from "-1 0" + :to "1 0" + :dur "2s" + :repeatCount "indefinite"}] + [:stop {:offset "0%" :stop-color (str "color-mix(in srgb-linear, " background " 90%, #777)") :stop-opacity 1}] + [:stop {:offset "50%" :stop-color (str "color-mix(in srgb-linear, " background " 80%, #777)") :stop-opacity 1}] + [:stop {:offset "100%" :stop-color (str "color-mix(in srgb-linear, " background " 90%, #777)") :stop-opacity 1}]]] + + (when (dbg/enabled? :show-export-metadata) [:& use/export-page {:options options}]) ;; We need a "real" background shape so layer transforms work properly in firefox @@ -316,9 +345,9 @@ :y (:y vbox 0) :fill background}] - [:& (mf/provider use/include-metadata-ctx) {:value (debug? :show-export-metadata)} - [:& (mf/provider embed/context) {:value true} - ;; Render root shape + [:& (mf/provider ctx/current-vbox) {:value vbox'} + [:& (mf/provider use/include-metadata-ctx) {:value (dbg/enabled? :show-export-metadata)} + ;; Render root shape [:& shapes/root-shape {:key page-id :objects base-objects :active-frames @active-frames}]]]] @@ -339,6 +368,7 @@ :on-double-click on-double-click :on-drag-enter on-drag-enter :on-drag-over on-drag-over + :on-drag-end on-drag-end :on-drop on-drop :on-pointer-down on-pointer-down :on-pointer-enter on-pointer-enter @@ -346,6 +376,14 @@ :on-pointer-move on-pointer-move :on-pointer-up on-pointer-up} + [:defs + ;; This clip is so the handlers are not over the rulers + [:clipPath {:id "clip-handlers"} + [:rect {:x (+ (:x vbox) rule-area-size) + :y (+ (:y vbox) rule-area-size) + :width (max 0 (- (:width vbox) rule-area-size)) + :height (max 0 (- (:height vbox) rule-area-size))}]]] + [:g {:style {:pointer-events (if disable-events? "none" "auto")}} (when show-text-editor? [:& editor/text-editor-svg {:shape editing-shape @@ -354,7 +392,7 @@ (when show-frame-outline? (let [outlined-frame-id (->> @hover-ids - (filter #(cph/frame-shape? (get base-objects %))) + (filter #(cfh/frame-shape? (get base-objects %))) (remove selected) (first)) outlined-frame (get objects outlined-frame-id)] @@ -402,28 +440,32 @@ {:bounds vbox :selected-shapes selected-shapes :frame selected-frame - :hover-shape @hover + :hover-shape @measure-hover :zoom zoom}]) (when show-padding? - [:* - [:& msr/padding - {:frame (first selected-shapes) - :hover @frame-hover - :zoom zoom - :alt? @alt? - :shift? @shift?}] + [:& mfc/padding-control + {:frame first-shape + :hover @frame-hover + :zoom zoom + :alt? @alt? + :shift? @shift? + :on-move-selected on-move-selected + :on-context-menu on-menu-selected}]) - [:& msr/gap - {:frame (first selected-shapes) - :hover @frame-hover - :zoom zoom - :alt? @alt? - :shift? @shift?}]]) + (when show-padding? + [:& mfc/gap-control + {:frame first-shape + :hover @frame-hover + :zoom zoom + :alt? @alt? + :shift? @shift? + :on-move-selected on-move-selected + :on-context-menu on-menu-selected}]) (when show-margin? - [:& msr/margin - {:shape (first selected-shapes) + [:& mfc/margin-control + {:shape first-shape :parent selected-frame :hover @frame-hover :zoom zoom @@ -501,21 +543,17 @@ [:& presence/active-cursors {:page-id page-id}]) - [:& scroll-bars/viewport-scrollbars - {:objects base-objects - :zoom zoom - :vbox vbox}] - - (when show-rules? - [:& rules/rules + (when-not hide-ui? + [:& rulers/rulers {:zoom zoom :zoom-inverse zoom-inverse :vbox vbox :selected-shapes selected-shapes :offset-x offset-x - :offset-y offset-y}]) + :offset-y offset-y + :show-rulers? show-rulers?}]) - (when show-rules? + (when (and show-rulers? show-grids?) [:& guides/viewport-guides {:zoom zoom :vbox vbox @@ -524,31 +562,31 @@ :modifiers modifiers}]) ;; DEBUG LAYOUT DROP-ZONES - (when (debug? :layout-drop-zones) + (when (dbg/enabled? :layout-drop-zones) [:& wvd/debug-drop-zones {:selected-shapes selected-shapes :objects base-objects :hover-top-frame-id @hover-top-frame-id :zoom zoom}]) - (when (debug? :layout-content-bounds) + (when (dbg/enabled? :layout-content-bounds) [:& wvd/debug-content-bounds {:selected-shapes selected-shapes :objects base-objects :hover-top-frame-id @hover-top-frame-id :zoom zoom}]) - (when (debug? :layout-lines) + (when (dbg/enabled? :layout-lines) [:& wvd/debug-layout-lines {:selected-shapes selected-shapes :objects base-objects :hover-top-frame-id @hover-top-frame-id :zoom zoom}]) - (when (debug? :parent-bounds) + (when (dbg/enabled? :parent-bounds) [:& wvd/debug-parent-bounds {:selected-shapes selected-shapes :objects base-objects :hover-top-frame-id @hover-top-frame-id :zoom zoom}]) - (when (debug? :grid-layout) + (when (dbg/enabled? :grid-layout) [:& wvd/debug-grid-layout {:selected-shapes selected-shapes :objects base-objects :hover-top-frame-id @hover-top-frame-id @@ -556,15 +594,6 @@ (when show-selection-handlers? [:g.selection-handlers {:clipPath "url(#clip-handlers)"} - [:defs - (let [rule-area-size (/ rules/rule-area-size zoom)] - ;; This clip is so the handlers are not over the rules - [:clipPath {:id "clip-handlers"} - [:rect {:x (+ (:x vbox) rule-area-size) - :y (+ (:y vbox) rule-area-size) - :width (max 0 (- (:width vbox) (* rule-area-size 2))) - :height (max 0 (- (:height vbox) (* rule-area-size 2)))}]])] - [:& selection/selection-handlers {:selected selected :shapes selected-shapes @@ -586,8 +615,31 @@ {:id (first selected) :zoom zoom}]) - (when show-grid-editor? - [:& grid-layout/editor - {:zoom zoom - :objects base-objects - :shape (get base-objects edition)}])]]])) + [:g.grid-layout-editor {:clipPath "url(#clip-handlers)"} + (when (or show-grid-editor? hover-grid?) + [:& grid-layout/editor + {:zoom zoom + :objects base-objects + :modifiers modifiers + :shape (or (get base-objects edition) + (get base-objects @hover-top-frame-id)) + :view-only (not show-grid-editor?)}]) + + (for [frame (ctt/get-frames objects)] + (when (and (ctl/grid-layout? frame) + (empty? (:shapes frame)) + (not= edition (:id frame)) + (not= @hover-top-frame-id (:id frame))) + [:& grid-layout/editor + {:zoom zoom + :key (dm/str (:id frame)) + :objects base-objects + :modifiers modifiers + :shape frame + :view-only true}])) + + [:& scroll-bars/viewport-scrollbars + {:objects base-objects + :zoom zoom + :vbox vbox + :bottom-padding (when palete-size (+ palete-size 8))}]]]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 4f54030684..013a157dc4 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -1,4 +1,4 @@ -; This Source Code Form is subject to the terms of the Mozilla Public +;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; @@ -6,9 +6,11 @@ (ns app.main.ui.workspace.viewport.actions (:require + [app.common.data :as d] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.math :as mth] - [app.common.pages.helpers :as cph] + [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.config :as cfg] [app.main.data.workspace :as dw] @@ -16,18 +18,21 @@ [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.media :as dwm] [app.main.data.workspace.path :as dwdp] + [app.main.data.workspace.specialized-panel :as-alias dwsp] [app.main.refs :as refs] [app.main.store :as st] - [app.main.streams :as ms] + [app.main.ui.workspace.sidebar.assets.components :as wsac] [app.main.ui.workspace.viewport.viewport-ref :as uwvv] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] [app.util.dom.normalize-wheel :as nw] [app.util.keyboard :as kbd] + [app.util.mouse :as mse] [app.util.object :as obj] - [app.util.timers :as timers] + [app.util.rxops :refer [throttle-fn]] + [app.util.timers :as ts] [app.util.webapi :as wapi] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -40,7 +45,6 @@ (mf/deps id blocked hidden type selected edition drawing-tool text-editing? node-editing? grid-editing? drawing-path? create-comment? @z? @space? panning workspace-read-only?) - (fn [bevent] ;; We need to handle editor related stuff here because ;; handling on editor dom node does not works properly. @@ -53,9 +57,10 @@ (.setPointerCapture editor (.-pointerId bevent)) (.setPointerCapture target (.-pointerId bevent)))) - (when (or (dom/class? (dom/get-target bevent) "viewport-controls") - (dom/class? (dom/get-target bevent) "viewport-selrect")) + (dom/class? (dom/get-target bevent) "viewport-selrect") + (dom/child? (dom/get-target bevent) (dom/query ".grid-layout-editor"))) + (dom/stop-propagation bevent) (when-not @z? @@ -79,13 +84,13 @@ (st/emit! (dw/start-zooming pt))) (st/emit! (dw/start-panning)))) - left-click? (do - (st/emit! (ms/->MouseEvent :down ctrl? shift? alt? meta?)) + (st/emit! (mse/->MouseEvent :down ctrl? shift? alt? meta?) + ::dwsp/interrupt) (when (and (not= edition id) (or text-editing? grid-editing?)) - (st/emit! dw/clear-edition-mode)) + (st/emit! (dw/clear-edition-mode))) (when (and (not text-editing?) (not blocked) @@ -103,7 +108,7 @@ (st/emit! (dd/start-drawing drawing-tool))) (or (not id) mod?) - (st/emit! (dw/handle-area-selection shift? mod?)) + (st/emit! (dw/handle-area-selection shift?)) (not drawing-tool) (when-not workspace-read-only? @@ -123,6 +128,7 @@ (not mod?) (not shift?) (not @space?)) + (dom/prevent-default bevent) (dom/stop-propagation bevent) (when-not (or workspace-read-only? @z?) @@ -160,6 +166,7 @@ (fn [event] (when (and (nil? selrect) (or (dom/class? (dom/get-target event) "viewport-controls") + (dom/child? (dom/get-target event) (dom/query ".grid-layout-editor")) (dom/class? (dom/get-target event) "viewport-selrect"))) (let [ctrl? (kbd/ctrl? event) shift? (kbd/shift? event) @@ -168,7 +175,7 @@ hovering? (some? @hover) raw-pt (dom/get-client-position event) pt (uwvv/point->viewport raw-pt)] - (st/emit! (ms/->MouseEvent :click ctrl? shift? alt? meta?)) + (st/emit! (mse/->MouseEvent :click ctrl? shift? alt? meta?)) (when (and hovering? (not @space?) @@ -187,10 +194,10 @@ (st/emit! (dw/increase-zoom pt))))))))) (defn on-double-click - [hover hover-ids drawing-path? objects edition drawing-tool z? workspace-read-only?] + [hover hover-ids hover-top-frame-id drawing-path? objects edition drawing-tool z? workspace-read-only?] (mf/use-callback - (mf/deps @hover @hover-ids drawing-path? edition drawing-tool @z? workspace-read-only?) + (mf/deps @hover @hover-ids @hover-top-frame-id drawing-path? edition drawing-tool @z? workspace-read-only?) (fn [event] (dom/stop-propagation event) (when-not @z? @@ -201,12 +208,17 @@ {:keys [id type] :as shape} (or @hover (get objects (first @hover-ids))) - editable? (contains? #{:text :rect :path :image :circle} type)] + editable? (contains? #{:text :rect :path :image :circle} type) - (st/emit! (ms/->MouseEvent :double-click ctrl? shift? alt? meta?)) + hover-shape (->> @hover-ids (filter (partial cfh/is-child? objects id)) first) + selected-shape (get objects hover-shape) - ;; Emit asynchronously so the double click to exit shapes won't break - (timers/schedule + grid-layout-id (->> @hover-ids reverse (d/seek (partial ctl/grid-layout? objects)))] + + (st/emit! (mse/->MouseEvent :double-click ctrl? shift? alt? meta?)) + + ;; Emit asynchronously so the double click to exit shapes won't break + (ts/schedule (fn [] (when (and (not drawing-path?) shape) (cond @@ -214,28 +226,27 @@ (st/emit! (dw/select-shape id) (dw/start-editing-selected)) - :else - (let [;; We only get inside childrens of the hovering shape - hover-ids (->> @hover-ids (filter (partial cph/is-child? objects id))) - selected (get objects (first hover-ids))] - (when (some? selected) - (reset! hover selected) - (st/emit! (dw/select-shape (:id selected)))))))))))))) + (some? selected-shape) + (do (reset! hover selected-shape) + (st/emit! (dw/select-shape (:id selected-shape)))) + + (and (not selected-shape) (some? grid-layout-id) (not workspace-read-only?)) + (st/emit! (dw/start-edition-mode grid-layout-id))))))))))) (defn on-context-menu [hover hover-ids workspace-read-only?] (mf/use-fn (mf/deps @hover @hover-ids workspace-read-only?) (fn [event] - (if workspace-read-only? - (dom/prevent-default event) + (dom/prevent-default event) + (when-not workspace-read-only? (when (or (dom/class? (dom/get-target event) "viewport-controls") - (dom/class? (dom/get-target event) "viewport-selrect")) - (dom/prevent-default event) - + (dom/child? (dom/get-target event) (dom/query ".grid-layout-editor")) + (dom/class? (dom/get-target event) "viewport-selrect") + workspace-read-only?) (let [position (dom/get-client-position event)] - ;; Delayed callback because we need to wait to the previous context menu to be closed - (timers/schedule + ;; Delayed callback because we need to wait to the previous context menu to be closed + (ts/schedule #(st/emit! (if (some? @hover) (dw/show-shape-context-menu {:position position @@ -274,14 +285,14 @@ middle-click? (= 2 (.-which event))] (when left-click? - (st/emit! (ms/->MouseEvent :up ctrl? shift? alt? meta?))) + (st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?))) (when middle-click? (dom/prevent-default event) ;; We store this so in Firefox the middle button won't do a paste of the content (reset! disable-paste true) - (timers/schedule #(reset! disable-paste false))) + (ts/schedule #(reset! disable-paste false))) (st/emit! (dw/finish-panning) (dw/finish-zooming)))))) @@ -305,33 +316,37 @@ shift? (kbd/shift? event) alt? (kbd/alt? event) meta? (kbd/meta? event) + mod? (kbd/mod? event) target (dom/get-target event) + editing? (or (some? (.closest ^js target ".public-DraftEditor-content")) (= "rich-text" (obj/get target "className")) (= "INPUT" (obj/get target "tagName")) (= "TEXTAREA" (obj/get target "tagName")))] (when-not (.-repeat bevent) - (st/emit! (ms/->KeyboardEvent :down key shift? ctrl? alt? meta? editing?))))))) + (st/emit! (kbd/->KeyboardEvent :down key shift? ctrl? alt? meta? mod? editing? event))))))) (defn on-key-up [] (mf/use-callback (fn [event] - (let [key (.-key event) - ctrl? (kbd/ctrl? event) - shift? (kbd/shift? event) - alt? (kbd/alt? event) - meta? (kbd/meta? event) + (let [key (.-key event) + ctrl? (kbd/ctrl? event) + shift? (kbd/shift? event) + alt? (kbd/alt? event) + meta? (kbd/meta? event) + mod? (kbd/mod? event) target (dom/get-target event) + editing? (or (some? (.closest ^js target ".public-DraftEditor-content")) (= "rich-text" (obj/get target "className")) (= "INPUT" (obj/get target "tagName")) (= "TEXTAREA" (obj/get target "tagName")))] - (st/emit! (ms/->KeyboardEvent :up key shift? ctrl? alt? meta? editing?)))))) + (st/emit! (kbd/->KeyboardEvent :up key shift? ctrl? alt? meta? mod? editing? event)))))) (defn on-pointer-move [move-stream] (let [last-position (mf/use-var nil)] - (mf/use-callback + (mf/use-fn (fn [event] (let [raw-pt (dom/get-client-position event) pt (uwvv/point->viewport raw-pt) @@ -341,18 +356,19 @@ delta (if @last-position (gpt/subtract raw-pt @last-position) (gpt/point 0 0))] + (rx/push! move-stream pt) (reset! last-position raw-pt) - (st/emit! (ms/->PointerEvent :delta delta - (kbd/ctrl? event) - (kbd/shift? event) - (kbd/alt? event) - (kbd/meta? event))) - (st/emit! (ms/->PointerEvent :viewport pt - (kbd/ctrl? event) - (kbd/shift? event) - (kbd/alt? event) - (kbd/meta? event)))))))) + (st/emit! (mse/->PointerEvent :delta delta + (kbd/ctrl? event) + (kbd/shift? event) + (kbd/alt? event) + (kbd/meta? event))) + (st/emit! (mse/->PointerEvent :viewport pt + (kbd/ctrl? event) + (kbd/shift? event) + (kbd/alt? event) + (kbd/meta? event)))))))) (defn on-mouse-wheel [zoom] (mf/use-callback @@ -386,9 +402,28 @@ (st/emit! (dw/update-viewport-position {:x #(+ % (/ delta-x zoom)) :y #(+ % (/ delta-y zoom))})))))))))) -(defn on-drag-enter [] +(defn on-drag-enter + [comp-inst-ref] (mf/use-callback (fn [e] + (let [component-inst? (mf/ref-val comp-inst-ref)] + (when (and (dnd/has-type? e "penpot/component") + (dom/class? (dom/get-target e) "viewport-controls") + (not component-inst?)) + (let [point (gpt/point (.-clientX e) (.-clientY e)) + viewport-coord (uwvv/point->viewport point) + {:keys [component file-id shape]} @wsac/drag-data* + + ;; shape (get-in component [:objects (:id component)]) + final-x (- (:x viewport-coord) (/ (:width shape) 2)) + final-y (- (:y viewport-coord) (/ (:height shape) 2))] + + (mf/set-ref-val! comp-inst-ref true) + (st/emit! (dwl/instantiate-component + file-id + (:id component) + (gpt/point final-x final-y) + {:start-move? true :initial-point viewport-coord}))))) (when (or (dnd/has-type? e "penpot/shape") (dnd/has-type? e "penpot/component") (dnd/has-type? e "Files") @@ -396,18 +431,31 @@ (dnd/has-type? e "text/asset-id")) (dom/prevent-default e))))) -(defn on-drag-over [] +(defn on-drag-end + [comp-inst-ref] (mf/use-callback - (fn [e] - (when (or (dnd/has-type? e "penpot/shape") - (dnd/has-type? e "penpot/component") - (dnd/has-type? e "Files") - (dnd/has-type? e "text/uri-list") - (dnd/has-type? e "text/asset-id")) - (dom/prevent-default e))))) + (fn [] + (mf/set-ref-val! comp-inst-ref false)))) + +(defn on-drag-over [move-stream] + (let [on-pointer-move (on-pointer-move move-stream) + + ;; Drag-over is not the same as pointer-move. Drag over is fired less frequently so we need + ;; to create a throttle so the events that cannot be processed at a certain path are + ;; discarded. + on-pointer-move (throttle-fn 50 (fn [e] (ts/raf #(on-pointer-move e))))] + (mf/use-callback + (fn [e] + (when (or (dnd/has-type? e "penpot/shape") + (dnd/has-type? e "penpot/component") + (dnd/has-type? e "Files") + (dnd/has-type? e "text/uri-list") + (dnd/has-type? e "text/asset-id")) + (on-pointer-move e) + (dom/prevent-default e)))))) (defn on-drop - [file] + [file comp-inst-ref] (mf/use-fn (fn [event] (dom/prevent-default event) @@ -418,7 +466,7 @@ asset-type (dnd/get-data event "text/asset-type")] (cond (dnd/has-type? event "penpot/shape") - (let [shape (dnd/get-data event "penpot/shape") + (let [shape (dnd/get-data event "penpot/shape") final-x (- (:x viewport-coord) (/ (:width shape) 2)) final-y (- (:y viewport-coord) (/ (:height shape) 2))] (st/emit! (dw/add-shape (-> shape @@ -427,13 +475,13 @@ (assoc :y final-y))))) (dnd/has-type? event "penpot/component") - (let [{:keys [component file-id]} (dnd/get-data event "penpot/component") - shape (get-in component [:objects (:id component)]) - final-x (- (:x viewport-coord) (/ (:width shape) 2)) - final-y (- (:y viewport-coord) (/ (:height shape) 2))] - (st/emit! (dwl/instantiate-component file-id - (:id component) - (gpt/point final-x final-y)))) + (let [event (.-nativeEvent event) + ctrl? (kbd/ctrl? event) + shift? (kbd/shift? event) + alt? (kbd/alt? event) + meta? (kbd/meta? event)] + (st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?)) + (mf/set-ref-val! comp-inst-ref false)) ;; Will trigger when the user drags an image from a browser ;; to the viewport (firefox and chrome do it a bit different @@ -489,13 +537,15 @@ :blobs (seq files)}] (st/emit! (dwm/upload-media-workspace params)))))))) -(defn on-paste [disable-paste in-viewport? workspace-read-only?] - (mf/use-callback +(defn on-paste + [disable-paste in-viewport? workspace-read-only?] + (mf/use-fn (mf/deps workspace-read-only?) (fn [event] - ;; We disable the paste just after mouse-up of a middle button so when panning won't - ;; paste the content into the workspace + ;; We disable the paste just after mouse-up of a middle button so + ;; when panning won't paste the content into the workspace (let [tag-name (-> event dom/get-target dom/get-tag-name)] - (when (and (not (#{"INPUT" "TEXTAREA"} tag-name)) (not @disable-paste) (not workspace-read-only?)) + (when (and (not (#{"INPUT" "TEXTAREA"} tag-name)) + (not @disable-paste) + (not workspace-read-only?)) (st/emit! (dw/paste-from-event event @in-viewport?))))))) - diff --git a/frontend/src/app/main/ui/workspace/viewport/comments.cljs b/frontend/src/app/main/ui/workspace/viewport/comments.cljs index a63f97ca2c..f75559b5e5 100644 --- a/frontend/src/app/main/ui/workspace/viewport/comments.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/comments.cljs @@ -5,61 +5,74 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.viewport.comments + (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.main.data.comments :as dcm] [app.main.data.workspace.comments :as dwcm] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.comments :as cmt] - [cuerdas.core :as str] [okulary.core :as l] [rumext.v2 :as mf])) +(defn- update-position + [positions {:keys [id] :as thread}] + (if (contains? positions id) + (-> thread + (assoc :position (dm/get-in positions [id :position])) + (assoc :frame-id (dm/get-in positions [id :frame-id]))) + thread)) + (mf/defc comments-layer + {::mf/props :obj} [{:keys [vbox vport zoom file-id page-id drawing] :as props}] - (let [pos-x (* (- (:x vbox)) zoom) - pos-y (* (- (:y vbox)) zoom) + (let [vbox-x (dm/get-prop vbox :x) + vbox-y (dm/get-prop vbox :y) + vport-w (dm/get-prop vport :width) + vport-h (dm/get-prop vport :height) - profile (mf/deref refs/profile) - users (mf/deref refs/current-file-comments-users) - local (mf/deref refs/comments-local) - threads-position-ref (l/derived (l/in [:workspace-data :pages-index page-id :options :comment-threads-position]) st/state) - threads-position-map (mf/deref threads-position-ref) - threads-map (mf/deref refs/threads-ref) + pos-x (* (- vbox-x) zoom) + pos-y (* (- vbox-y) zoom) - update-thread-position (fn update-thread-position [thread] - (if (contains? threads-position-map (:id thread)) - (-> thread - (assoc :position (get-in threads-position-map [(:id thread) :position])) - (assoc :frame-id (get-in threads-position-map [(:id thread) :frame-id]))) - thread)) + profile (mf/deref refs/profile) + users (mf/deref refs/current-file-comments-users) + local (mf/deref refs/comments-local) - threads (->> (vals threads-map) - (filter #(= (:page-id %) page-id)) - (mapv update-thread-position) - (dcm/apply-filters local profile)) + positions-ref + (mf/with-memo [page-id] + (-> (l/in [:workspace-data :pages-index page-id :options :comment-threads-position]) + (l/derived st/state))) + + positions (mf/deref positions-ref) + threads-map (mf/deref refs/threads-ref) + + threads + (mf/with-memo [threads-map positions local profile] + (->> (vals threads-map) + (filter #(= (:page-id %) page-id)) + (mapv (partial update-position positions)) + (dcm/apply-filters local profile))) on-draft-cancel - (mf/use-callback - #(st/emit! :interrupt)) + (mf/use-fn #(st/emit! :interrupt)) on-draft-submit - (mf/use-callback + (mf/use-fn (fn [draft] (st/emit! (dcm/create-thread-on-workspace draft))))] - (mf/use-effect - (mf/deps file-id) - (fn [] - (st/emit! (dwcm/initialize-comments file-id)) - (fn [] - (st/emit! ::dwcm/finalize)))) + (mf/with-effect [file-id] + (st/emit! (dwcm/initialize-comments file-id)) + (fn [] (st/emit! ::dwcm/finalize))) - [:div.comments-section - [:div.workspace-comments-container - {:style {:width (str (:width vport) "px") - :height (str (:height vport) "px")}} - [:div.threads {:style {:transform (str/format "translate(%spx, %spx)" pos-x pos-y)}} + [:div {:class (stl/css :comments-section)} + [:div + {:class (stl/css :workspace-comments-container) + :style {:width (dm/str vport-w "px") + :height (dm/str vport-h "px")}} + [:div {:class (stl/css :threads) + :style {:transform (dm/fmt "translate(%px, %px)" pos-x pos-y)}} (for [item threads] [:& cmt/thread-bubble {:thread item :zoom zoom @@ -68,12 +81,15 @@ (when-let [id (:open local)] (when-let [thread (get threads-map id)] - [:& cmt/thread-comments {:thread (update-thread-position thread) - :users users - :zoom zoom}])) + (when (seq (dcm/apply-filters local profile [thread])) + [:& cmt/thread-comments {:thread (update-position positions thread) + :users users + :viewport {:offset-x pos-x :offset-y pos-y :width (:width vport) :height (:height vport)} + :zoom zoom}]))) (when-let [draft (:comment drawing)] [:& cmt/draft-thread {:draft draft :on-cancel on-draft-cancel :on-submit on-draft-submit :zoom zoom}])]]])) + diff --git a/frontend/src/app/main/ui/workspace/viewport/comments.scss b/frontend/src/app/main/ui/workspace/viewport/comments.scss new file mode 100644 index 0000000000..9c587d5968 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/comments.scss @@ -0,0 +1,27 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.workspace-comments-container { + width: 100%; + height: 100%; + grid-column: 1 / span 2; + grid-row: 1 / span 2; + z-index: 1000; + pointer-events: none; + overflow: hidden; + user-select: text; + position: absolute; +} + +.threads { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} diff --git a/frontend/src/app/main/ui/workspace/viewport/debug.cljs b/frontend/src/app/main/ui/workspace/viewport/debug.cljs index 7f686435a8..c14ad650bf 100644 --- a/frontend/src/app/main/ui/workspace/viewport/debug.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/debug.cljs @@ -8,12 +8,12 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.flex-layout :as gsl] [app.common.geom.shapes.grid-layout :as gsg] [app.common.geom.shapes.points :as gpo] - [app.common.pages.helpers :as cph] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [cuerdas.core :as str] @@ -37,11 +37,29 @@ shape (or selected-frame (get objects hover-top-frame-id))] (when (and shape (:layout shape)) - (let [children (->> (cph/get-immediate-children objects (:id shape)) + (let [children (->> (cfh/get-immediate-children objects (:id shape)) (remove :hidden)) bounds (d/lazy-map (keys objects) #(dm/get-in objects [% :points])) - layout-bounds (gsl/layout-content-bounds bounds shape children) - layout-points (flatten (gsl/layout-content-points bounds shape children))] + + children+bounds (->> children (map (fn [shape] [@(get bounds (:id shape)) shape]))) + + grid-layout-data + (when (ctl/grid-layout? shape) + (gsg/calc-layout-data shape (:points shape) children+bounds bounds objects)) + + layout-bounds + (cond (ctl/flex-layout? shape) + (gsl/layout-content-bounds bounds shape children objects) + + (ctl/grid-layout? shape) + (gsg/layout-content-bounds bounds shape grid-layout-data)) + layout-points + (cond (ctl/flex-layout? shape) + (flatten (gsl/layout-content-points bounds shape children objects)) + + (ctl/grid-layout? shape) + (flatten (gsg/layout-content-points bounds shape grid-layout-data)))] + [:g.debug-layout {:pointer-events "none"} [:polygon {:points (->> layout-bounds (map #(dm/fmt "%, %" (:x %) (:y %))) (str/join " ")) :style {:stroke "red" :fill "none"}}] @@ -73,10 +91,13 @@ (let [row? (ctl/row? shape) col? (ctl/col? shape) - children (->> (cph/get-immediate-children objects (:id shape)) + children (->> (cfh/get-immediate-children objects (:id shape)) (remove :hidden) (map #(vector (gpo/parent-coords-bounds (:points %) (:points shape)) %))) - layout-data (gsl/calc-layout-data shape children (:points shape)) + + bounds (d/lazy-map (keys objects) #(dm/get-in objects [% :points])) + + layout-data (gsl/calc-layout-data shape (:points shape) children bounds objects) layout-bounds (:layout-bounds layout-data) xv #(gpo/start-hv layout-bounds %) @@ -110,10 +131,12 @@ (when (and (= (count selected-shapes) 1) (= :frame (-> selected-shapes first :type))) (first selected-shapes)) - shape (or selected-frame (get objects hover-top-frame-id))] + shape (or selected-frame (get objects hover-top-frame-id)) + + bounds (d/lazy-map (keys objects) #(dm/get-in objects [% :points]))] (when (and shape (:layout shape)) - (let [drop-areas (gsl/get-drop-areas shape objects)] + (let [drop-areas (gsl/get-drop-areas shape objects bounds)] [:g.debug-layout {:pointer-events "none" :transform (gsh/transform-str shape)} (for [[idx drop-area] (d/enumerate drop-areas)] @@ -173,10 +196,12 @@ (first selected-shapes)) parent (or selected-frame (get objects hover-top-frame-id)) - parent-bounds (:points parent)] + parent-bounds (:points parent) + + bounds (d/lazy-map (keys objects) #(dm/get-in objects [% :points]))] (when (and (some? parent) (not= uuid/zero (:id parent))) - (let [children (->> (cph/get-immediate-children objects (:id parent)) + (let [children (->> (cfh/get-immediate-children objects (:id parent)) (remove :hidden))] [:g.debug-parent-bounds {:pointer-events "none"} (for [[idx child] (d/enumerate children)] @@ -189,7 +214,7 @@ (let [child-bounds (:points child) points (if (or (ctl/fill-height? child) (ctl/fill-height? child)) - (gsl/child-layout-bound-points parent child parent-bounds child-bounds) + (gsl/child-layout-bound-points parent child parent-bounds child-bounds bounds objects) child-bounds)] (for [point points] [:circle {:cx (:x point) @@ -211,10 +236,12 @@ (first selected-shapes)) parent (or selected-frame (get objects hover-top-frame-id)) - parent-bounds (:points parent)] + parent-bounds (:points parent) + + bounds (d/lazy-map (keys objects) #(dm/get-in objects [% :points]))] (when (and (some? parent) (not= uuid/zero (:id parent))) - (let [children (->> (cph/get-immediate-children objects (:id parent)) + (let [children (->> (cfh/get-immediate-children objects (:id parent)) (remove :hidden) (map #(vector (gpo/parent-coords-bounds (:points %) (:points parent)) %))) @@ -226,7 +253,7 @@ origin (gpo/origin parent-bounds) {:keys [row-tracks column-tracks]} - (gsg/calc-layout-data parent children parent-bounds)] + (gsg/calc-layout-data parent parent-bounds children bounds objects)] [:* (for [row-data row-tracks] diff --git a/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs b/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs index 356af918e1..19870dfa64 100644 --- a/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs @@ -8,6 +8,7 @@ "Drawing components." (:require [app.common.math :as mth] + [app.common.types.shape :as cts] [app.main.ui.shapes.path :refer [path-shape]] [app.main.ui.workspace.shapes :as shapes] [app.main.ui.workspace.shapes.path.editor :refer [path-editor]] @@ -19,14 +20,16 @@ (mf/defc draw-area [{:keys [shape zoom tool] :as props}] - [:g.draw-area - [:g {:style {:pointer-events "none"}} - [:& shapes/shape-wrapper {:shape shape}]] + ;; Prevent rendering something that it's not a shape. + (when (cts/shape? shape) + [:g.draw-area + [:g {:style {:pointer-events "none"}} + [:& shapes/shape-wrapper {:shape shape}]] - (case tool - :path [:& path-editor {:shape shape :zoom zoom}] - :curve [:& path-shape {:shape shape :zoom zoom}] - #_:default [:& generic-draw-area {:shape shape :zoom zoom}])]) + (case tool + :path [:& path-editor {:shape shape :zoom zoom}] + :curve [:& path-shape {:shape shape :zoom zoom}] + #_:default [:& generic-draw-area {:shape shape :zoom zoom}])])) (mf/defc generic-draw-area [{:keys [shape zoom]}] @@ -38,7 +41,7 @@ [:rect.main {:x x :y y :width width :height height - :style {:stroke "var(--color-select)" + :style {:stroke "var(--color-accent-tertiary)" :fill "none" :stroke-width (/ 1 zoom)}}]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs index 37a7265863..61246ea705 100644 --- a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs @@ -8,13 +8,13 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.geom.shapes :as gsh] + [app.common.files.helpers :as cfh] + [app.common.geom.grid :as gg] + [app.common.geom.rect :as grc] [app.common.math :as mth] - [app.common.pages.helpers :as cph] [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] [app.main.refs :as refs] - [app.util.geom.grid :as gg] [rumext.v2 :as mf])) (mf/defc square-grid [{:keys [frame zoom grid] :as props}] @@ -117,10 +117,10 @@ (reduce (fn [sr parent] (cond-> sr - (and (not (cph/root? parent)) - (cph/frame-shape? parent) + (and (not (cfh/root? parent)) + (cfh/frame-shape? parent) (not (:show-content parent))) - (gsh/clip-selrect (:selrect parent)))) + (grc/clip-rect (:selrect parent)))) selrect parents)) @@ -173,7 +173,7 @@ [:g.grid-display {:style {:pointer-events "none"}} (for [frame frames] (when (and #_(not (is-transform? frame)) - (not (ctst/rotated-frame? frame)) + (not (ctst/rotated-frame? frame)) (or (empty? focus) (contains? focus (:id frame)))) [:& grid-display-frame {:key (str "grid-" (:id frame)) :zoom zoom diff --git a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs index 4b86156559..c035b31341 100644 --- a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs @@ -15,21 +15,21 @@ [app.main.data.workspace.colors :as dc] [app.main.refs :as refs] [app.main.store :as st] - [app.main.streams :as ms] [app.util.dom :as dom] - [beicon.core :as rx] + [app.util.mouse :as mse] + [beicon.v2.core :as rx] [cuerdas.core :as str] [rumext.v2 :as mf])) (def gradient-line-stroke-width 2) -(def gradient-line-stroke-color "var(--color-white)") +(def gradient-line-stroke-color "var(--app-white)") (def gradient-square-width 15) (def gradient-square-radius 2) (def gradient-square-stroke-width 2) (def gradient-width-handler-radius 5) -(def gradient-width-handler-color "var(--color-white)") -(def gradient-square-stroke-color "var(--color-white)") -(def gradient-square-stroke-color-selected "var(--color-select)") +(def gradient-width-handler-color "var(--app-white)") +(def gradient-square-stroke-color "var(--app-white)") +(def gradient-square-stroke-color-selected "var(--color-accent-tertiary)") (mf/defc shadow [{:keys [id x y width height offset]}] [:filter {:id id @@ -109,7 +109,7 @@ :rx (/ gradient-square-radius zoom) :width (/ gradient-square-width zoom) :height (/ gradient-square-width zoom) - :stroke (if selected "var(--color-primary)" "var(--color-white)") + :stroke (if selected "var(--color-accent-tertiary)" "var(--app-white)") :stroke-width (/ gradient-square-stroke-width zoom) :fill (:value color) :fill-opacity (:opacity color) @@ -154,14 +154,14 @@ (mf/deps @moving-point from-p to-p width-p) (fn [] (let [subs (->> st/stream - (rx/filter ms/pointer-event?) - (rx/filter #(= :viewport (:source %))) - (rx/map :pt) - (rx/subs + (rx/filter mse/pointer-event?) + (rx/filter #(= :viewport (mse/get-pointer-source %))) + (rx/map mse/get-pointer-position) + (rx/subs! (fn [pt] (case @moving-point - :from-p (when on-change-start (on-change-start pt)) - :to-p (when on-change-finish (on-change-finish pt)) + :from-p (when on-change-start (on-change-start pt)) + :to-p (when on-change-finish (on-change-finish pt)) :width-p (when on-change-width (let [width-v (gpt/unit (gpt/to-vec from-p width-p)) distance (gpt/point-line-distance pt from-p to-p) @@ -173,13 +173,13 @@ (fn [] (rx/dispose! subs))))) [:g.gradient-handlers [:defs - [:& gradient-line-drop-shadow-filter {:id "gradient_line_drop_shadow" :from-p from-p :to-p to-p :zoom zoom}] - [:& gradient-line-drop-shadow-filter {:id "gradient_width_line_drop_shadow" :from-p from-p :to-p width-p :zoom zoom}] - [:& gradient-square-drop-shadow-filter {:id "gradient_square_from_drop_shadow" :point from-p :zoom zoom}] - [:& gradient-square-drop-shadow-filter {:id "gradient_square_to_drop_shadow" :point to-p :zoom zoom}] - [:& gradient-width-handler-shadow-filter {:id "gradient_width_handler_drop_shadow" :point width-p :zoom zoom}]] + [:& gradient-line-drop-shadow-filter {:id "gradient-line-drop-shadow" :from-p from-p :to-p to-p :zoom zoom}] + [:& gradient-line-drop-shadow-filter {:id "gradient-width-line-drop-shadow" :from-p from-p :to-p width-p :zoom zoom}] + [:& gradient-square-drop-shadow-filter {:id "gradient-square-from-drop-shadow" :point from-p :zoom zoom}] + [:& gradient-square-drop-shadow-filter {:id "gradient-square-to-drop-shadow" :point to-p :zoom zoom}] + [:& gradient-width-handler-shadow-filter {:id "gradient-width-handler-drop-shadow" :point width-p :zoom zoom}]] - [:g {:filter "url(#gradient_line_drop_shadow)"} + [:g {:filter "url(#gradient-line-drop-shadow)"} [:line {:x1 (:x from-p) :y1 (:y from-p) :x2 (:x to-p) @@ -188,7 +188,7 @@ :stroke-width (/ gradient-line-stroke-width zoom)}]] (when width-p - [:g {:filter "url(#gradient_width_line_drop_shadow)"} + [:g {:filter "url(#gradient-width-line-drop-shadow)"} [:line {:x1 (:x from-p) :y1 (:y from-p) :x2 (:x width-p) @@ -197,7 +197,7 @@ :stroke-width (/ gradient-line-stroke-width zoom)}]]) (when width-p - [:g {:filter "url(#gradient_width_handler_drop_shadow)"} + [:g {:filter "url(#gradient-width-handler-drop-shadow)"} [:circle {:data-allow-click-modal "colorpicker" :cx (:x width-p) :cy (:y width-p) @@ -208,7 +208,7 @@ [:& gradient-color-handler {:selected (or (not editing) (= editing 0)) - :filter-id "gradient_square_from_drop_shadow" + :filter-id "gradient-square-from-drop-shadow" :zoom zoom :point from-p :color from-color @@ -219,7 +219,7 @@ [:& gradient-color-handler {:selected (= editing 1) - :filter-id "gradient_square_to_drop_shadow" + :filter-id "gradient-square-to-drop-shadow" :zoom zoom :point to-p :color to-color @@ -250,7 +250,7 @@ width-v (-> gradient-vec (gpt/normal-left) - (gpt/multiply (gpt/point (* (:width gradient) (/ gradient-length (/ height 2) )))) + (gpt/multiply (gpt/point (* (:width gradient) (/ gradient-length (/ height 2))))) (gpt/multiply (gpt/point (/ width 2)))) width-p (gpt/add from-p width-v) diff --git a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs index c9f351e903..572c1068de 100644 --- a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs @@ -5,23 +5,643 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.viewport.grid-layout-editor + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.line :as gl] + [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] [app.common.geom.shapes.grid-layout :as gsg] [app.common.geom.shapes.points :as gpo] - [app.common.pages.helpers :as cph] + [app.common.math :as mth] + [app.common.types.modifiers :as ctm] + [app.common.types.shape.layout :as ctl] + [app.main.data.workspace :as dw] [app.main.data.workspace.grid-layout.editor :as dwge] + [app.main.data.workspace.modifiers :as dwm] + [app.main.data.workspace.shape-layout :as dwsl] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.css-cursors :as cur] + [app.main.ui.formats :as fmt] + [app.main.ui.icons :as i] + [app.main.ui.workspace.viewport.viewport-ref :as uwvv] + [app.util.debug :as dbg] [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] + [app.util.object :as obj] [cuerdas.core :as str] [rumext.v2 :as mf])) +(def small-size-limit 60) +(def medium-size-limit 110) + (defn apply-to-point [result next-fn] (conj result (next-fn (last result)))) +(defn format-size [{:keys [type value]}] + (case type + :fixed (dm/str (fmt/format-number value) "PX") + :percent (dm/str (fmt/format-number value) "%") + :flex (dm/str (fmt/format-number value) "FR") + :auto "AUTO")) + +(mf/defc grid-edition-actions + {::mf/wrap-props false} + [{:keys [shape]}] + [:div {:class (stl/css :grid-actions)} + [:div {:class (stl/css :grid-actions-container)} + [:div {:class (stl/css :grid-actions-title)} + (tr "workspace.layout_grid.editor.title") " " [:span {:stl/css :board-name} (:name shape)]] + [:button {:class (stl/css :locate-btn) + :on-click #(st/emit! (dwge/locate-board (:id shape)))} + (tr "workspace.layout_grid.editor.top-bar.locate")] + [:button {:class (stl/css :done-btn) + :on-click #(st/emit! (dw/clear-edition-mode))} + (tr "workspace.layout_grid.editor.top-bar.done")]]]) + +(mf/defc grid-editor-frame + {::mf/wrap-props false} + [props] + + (let [bounds (unchecked-get props "bounds") + width (unchecked-get props "width") + height (unchecked-get props "height") + zoom (unchecked-get props "zoom") + hv #(gpo/start-hv bounds %) + vv #(gpo/start-vv bounds %) + origin (gpo/origin bounds) + + frame-points + (reduce + apply-to-point + [origin] + [#(gpt/add % (hv (+ width (/ 70 zoom)))) + #(gpt/subtract % (vv (/ 40 zoom))) + #(gpt/subtract % (hv (+ width (/ 110 zoom)))) + #(gpt/add % (vv (+ height (/ 110 zoom)))) + #(gpt/add % (hv (/ 40 zoom)))])] + + [:polygon + {:class (stl/css :grid-frame) + :points (->> frame-points + (map #(dm/fmt "%,%" (:x %) (:y %))) + (str/join " "))}])) + +(mf/defc plus-btn + {::mf/wrap-props false} + [props] + (let [start-p (unchecked-get props "start-p") + zoom (unchecked-get props "zoom") + type (unchecked-get props "type") + on-click (unchecked-get props "on-click") + + [rect-x rect-y icon-x icon-y] + (if (= type :column) + [(:x start-p) + (- (:y start-p) (/ 40 zoom)) + (+ (:x start-p) (/ 9 zoom)) + (- (:y start-p) (/ 31 zoom))] + + [(- (:x start-p) (/ 40 zoom)) + (:y start-p) + (- (:x start-p) (/ 31 zoom)) + (+ (:y start-p) (/ 9 zoom))]) + + handle-click + (mf/use-fn + (mf/deps on-click) + #(when on-click (on-click)))] + + [:g {:class (stl/css :grid-plus-button) + :on-click handle-click} + + [:rect {:class (stl/css :grid-plus-shape) + :x (+ rect-x (/ 6 zoom)) + :y (+ rect-y (/ 6 zoom)) + :width (/ (- 40 12) zoom) + :height (/ (- 40 12) zoom) + :rx (/ 4 zoom) + :ry (/ 4 zoom)}] + + [:use {:class (stl/css :grid-plus-icon) + :x icon-x + :y icon-y + :width (/ 22 zoom) + :height (/ 22 zoom) + :href "#icon-add"}]])) + +(defn use-drag + [{:keys [on-drag-start on-drag-end on-drag-delta on-drag-position]}] + (let [dragging-ref (mf/use-ref false) + start-pos-ref (mf/use-ref nil) + current-pos-ref (mf/use-ref nil) + + handle-pointer-down + (mf/use-fn + (mf/deps on-drag-start) + (fn [event] + (let [raw-pt (dom/get-client-position event) + position (uwvv/point->viewport raw-pt)] + (dom/capture-pointer event) + (mf/set-ref-val! dragging-ref true) + (mf/set-ref-val! start-pos-ref raw-pt) + (mf/set-ref-val! current-pos-ref raw-pt) + (when on-drag-start (on-drag-start event position))))) + + handle-lost-pointer-capture + (mf/use-fn + (mf/deps on-drag-end) + (fn [event] + (let [raw-pt (mf/ref-val current-pos-ref) + position (uwvv/point->viewport raw-pt)] + (dom/release-pointer event) + (mf/set-ref-val! dragging-ref false) + (mf/set-ref-val! start-pos-ref nil) + (when on-drag-end (on-drag-end event position))))) + + handle-pointer-move + (mf/use-fn + (mf/deps on-drag-delta on-drag-position) + (fn [event] + (when (mf/ref-val dragging-ref) + (let [start (mf/ref-val start-pos-ref) + pos (dom/get-client-position event) + pt (uwvv/point->viewport pos)] + (mf/set-ref-val! current-pos-ref pos) + (when on-drag-delta (on-drag-delta event (gpt/to-vec start pos))) + (when on-drag-position (on-drag-position event pt))))))] + + {:handle-pointer-down handle-pointer-down + :handle-lost-pointer-capture handle-lost-pointer-capture + :handle-pointer-move handle-pointer-move})) + +(mf/defc resize-cell-handler + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + x (unchecked-get props "x") + y (unchecked-get props "y") + width (unchecked-get props "width") + height (unchecked-get props "height") + handler (unchecked-get props "handler") + + objects (mf/deref refs/workspace-page-objects) + {cell-id :id} (unchecked-get props "cell") + {:keys [row column row-span column-span]} (get-in shape [:layout-grid-cells cell-id]) + + direction (unchecked-get props "direction") + layout-data (unchecked-get props "layout-data") + + handle-drag-position + (mf/use-fn + (mf/deps shape row column row-span column-span) + (fn [_ position] + (let [[drag-row drag-column] (gsg/get-position-grid-coord layout-data position) + + [new-row new-column new-row-span new-column-span] + (case handler + :top + (let [new-row (min (+ row (dec row-span)) drag-row) + new-row-span (+ (- row new-row) row-span)] + [new-row column new-row-span column-span]) + + :left + (let [new-column (min (+ column (dec column-span)) drag-column) + new-column-span (+ (- column new-column) column-span)] + [row new-column row-span new-column-span]) + + :bottom + (let [new-row-span (max 1 (inc (- drag-row row)))] + [row column new-row-span column-span]) + + :right + (let [new-column-span (max 1 (inc (- drag-column column)))] + [row column row-span new-column-span])) + + shape + (-> (ctl/resize-cell-area shape row column new-row new-column new-row-span new-column-span) + (ctl/assign-cells objects)) + + modifiers + (-> (ctm/empty) + (ctm/change-property :layout-grid-rows (:layout-grid-rows shape)) + (ctm/change-property :layout-grid-columns (:layout-grid-columns shape)) + (ctm/change-property :layout-grid-cells (:layout-grid-cells shape)))] + (st/emit! (dwm/set-modifiers (dwm/create-modif-tree [(:id shape)] modifiers)))))) + + handle-drag-end + (mf/use-fn + (fn [] + (st/emit! (dwm/apply-modifiers)))) + + {:keys [handle-pointer-down handle-lost-pointer-capture handle-pointer-move]} + (use-drag {:on-drag-position handle-drag-position + :on-drag-end handle-drag-end})] + [:rect + {:x x + :y y + :height height + :width width + :class (if (= direction :row) + (cur/get-dynamic "scale-ns" (:rotation shape)) + (cur/get-dynamic "scale-ew" (:rotation shape))) + :style {:fill "transparent" + :stroke-width 0} + :on-pointer-down handle-pointer-down + :on-lost-pointer-capture handle-lost-pointer-capture + :on-pointer-move handle-pointer-move}])) + +(mf/defc grid-cell-area-label + {::mf/wrap-props false} + [props] + + (let [cell-origin (unchecked-get props "origin") + cell-width (unchecked-get props "width") + zoom (unchecked-get props "zoom") + text (unchecked-get props "text") + + area-width (/ (* 10 (count text)) zoom) + area-height (/ 25 zoom) + area-x (- (+ (:x cell-origin) cell-width) area-width) + area-y (:y cell-origin) + + area-text-x (+ area-x (/ area-width 2)) + area-text-y (+ area-y (/ area-height 2))] + + [:g {:pointer-events "none"} + [:rect {:x area-x + :y area-y + :width area-width + :height area-height + :style {:fill "var(--grid-editor-area-background)" + :fill-opacity 0.3}}] + [:text {:x area-text-x + :y area-text-y + :style {:fill "var(--grid-editor-area-text)" + :font-family "worksans" + :font-weight 600 + :font-size (/ 14 zoom) + :alignment-baseline "central" + :text-anchor "middle"}} + text]])) + +(mf/defc grid-cell + {::mf/memo #{:shape :cell :layout-data :zoom :hover? :selected?} + ::mf/props :obj} + [{:keys [shape cell layout-data zoom hover? selected?]}] + (let [cell-bounds (gsg/cell-bounds layout-data cell) + cell-origin (gpo/origin cell-bounds) + cell-width (gpo/width-points cell-bounds) + cell-height (gpo/height-points cell-bounds) + cell-center (gsh/points->center cell-bounds) + cell-origin (gpt/transform cell-origin (gmt/transform-in cell-center (:transform-inverse shape))) + + handle-pointer-enter + (mf/use-fn + (mf/deps (:id shape) (:id cell)) + (fn [] + (st/emit! (dwge/hover-grid-cell (:id shape) (:id cell) true)))) + + handle-pointer-leave + (mf/use-fn + (mf/deps (:id shape) (:id cell)) + (fn [] + (st/emit! (dwge/hover-grid-cell (:id shape) (:id cell) false)))) + + handle-pointer-down + (mf/use-fn + (mf/deps (:id shape) (:id cell) selected?) + (fn [event] + (when (dom/left-mouse? event) + (cond + (and selected? (or (kbd/mod? event) (kbd/shift? event))) + (st/emit! (dwge/remove-selection (:id shape) (:id cell))) + + (and (not selected?) (kbd/mod? event)) + (st/emit! (dwge/add-to-selection (:id shape) (:id cell))) + + (and (not selected?) (kbd/shift? event)) + (st/emit! (dwge/add-to-selection (:id shape) (:id cell) true)) + + :else + (st/emit! (dwge/set-selection (:id shape) (:id cell))))))) + + handle-context-menu + (mf/use-fn + (mf/deps (:id shape) (:id cell) selected?) + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (let [position (dom/get-client-position event)] + (if selected? + (st/emit! (dw/show-grid-cell-context-menu {:position position :grid-id (:id shape)})) + + ;; If right-click on a non-selected cell we select the cell and then open the menu + (st/emit! (dwge/set-selection (:id shape) (:id cell)) + (dw/show-grid-cell-context-menu {:position position :grid-id (:id shape)}))))))] + + [:g.cell-editor + ;; DEBUG OVERLAY + (when (dbg/enabled? :grid-cells) + [:g.debug-cell {:pointer-events "none" + :transform (dm/str (gmt/transform-in cell-center (:transform shape)))} + + [:rect + {:x (:x cell-origin) + :y (:y cell-origin) + :width cell-width + :height cell-height + :fill (cond + (= (:position cell) :auto) "green" + (= (:position cell) :manual) "red" + (= (:position cell) :area) "yellow" + :else "black") + :fill-opacity 0.2}] + + (when (seq (:shapes cell)) + [:circle + {:cx (+ (:x cell-origin) cell-width (- (/ 7 zoom))) + :cy (+ (:y cell-origin) (/ 7 zoom)) + :r (/ 5 zoom) + :fill "red"}])]) + [:rect + {:transform (dm/str (gmt/transform-in cell-center (:transform shape))) + :class (dom/classnames (stl/css :grid-cell-outline) true + (stl/css :hover) hover? + (stl/css :selected) selected?) + :x (:x cell-origin) + :y (:y cell-origin) + :width cell-width + :height cell-height + + :on-context-menu handle-context-menu + :on-pointer-enter handle-pointer-enter + :on-pointer-leave handle-pointer-leave + :on-pointer-down handle-pointer-down}] + + (when (:area-name cell) + [:& grid-cell-area-label {:origin cell-origin + :width cell-width + :zoom zoom + :text (:area-name cell)}]) + + (when selected? + (let [handlers + ;; Handlers positions, size and cursor + [[:top (:x cell-origin) (+ (:y cell-origin) (/ -10 zoom)) cell-width (/ 20 zoom) :row] + [:right (+ (:x cell-origin) cell-width (/ -10 zoom)) (:y cell-origin) (/ 20 zoom) cell-height :column] + [:bottom (:x cell-origin) (+ (:y cell-origin) cell-height (/ -10 zoom)) cell-width (/ 20 zoom) :row] + [:left (+ (:x cell-origin) (/ -10 zoom)) (:y cell-origin) (/ 20 zoom) cell-height :column]]] + [:g {:transform (dm/str (gmt/transform-in cell-center (:transform shape)))} + (for [[handler x y width height dir] handlers] + [:& resize-cell-handler {:key (dm/str "resize-" (d/name handler) "-" (:id cell)) + :shape shape + :handler handler + :x x + :y y + :cell cell + :width width + :height height + :direction dir + :layout-data layout-data}])]))])) + +(defn use-resize-track + [type shape index track-before track-after zoom snap-pixel?] + + (let [start-size-before (mf/use-var nil) + start-size-after (mf/use-var nil) + + handle-drag-start + (mf/use-fn + (mf/deps shape track-before track-after) + (fn [] + (reset! start-size-before (:size track-before)) + (reset! start-size-after (:size track-after)) + (let [tracks-prop + (if (= :column type) :layout-grid-columns :layout-grid-rows) + shape + (-> shape + (cond-> (some? track-before) + (update-in [tracks-prop (dec index)] merge {:type :fixed :value (:size track-before)})) + (cond-> (some? track-after) + (update-in [tracks-prop index] merge {:type :fixed :value (:size track-after)}))) + + modifiers + (-> (ctm/empty) + (ctm/change-property tracks-prop (get shape tracks-prop)))] + (st/emit! (dwm/set-modifiers (dwm/create-modif-tree [(:id shape)] modifiers)))))) + + handle-drag-position + (mf/use-fn + (mf/deps shape track-before track-after) + (fn [_ position] + (let [[tracks-prop axis] + (if (= :column type) [:layout-grid-columns :x] [:layout-grid-rows :y]) + + precision (if snap-pixel? mth/round identity) + delta (/ (get position axis) zoom) + + new-size-before (max 0 (precision (+ @start-size-before delta))) + new-size-after (max 0 (precision (- @start-size-after delta))) + + shape + (-> shape + (cond-> (some? track-before) + (update-in [tracks-prop (dec index)] merge {:type :fixed :value new-size-before})) + (cond-> (some? track-after) + (update-in [tracks-prop index] merge {:type :fixed :value new-size-after}))) + + modifiers + (-> (ctm/empty) + (ctm/change-property tracks-prop (get shape tracks-prop)))] + (st/emit! (dwm/set-modifiers (dwm/create-modif-tree [(:id shape)] modifiers)))))) + + handle-drag-end + (mf/use-fn + (mf/deps track-before track-after) + (fn [] + (reset! start-size-before nil) + (reset! start-size-after nil) + (st/emit! (dwm/apply-modifiers))))] + + (use-drag {:on-drag-start handle-drag-start + :on-drag-delta handle-drag-position + :on-drag-end handle-drag-end}))) + +(mf/defc resize-track-handler + {::mf/wrap-props false} + [props] + + (let [shape (unchecked-get props "shape") + index (unchecked-get props "index") + last? (unchecked-get props "last?") + drop? (unchecked-get props "drop?") + track-before (unchecked-get props "track-before") + track-after (unchecked-get props "track-after") + snap-pixel? (unchecked-get props "snap-pixel?") + + {:keys [column-total-size column-total-gap row-total-size row-total-gap] :as layout-data} + (unchecked-get props "layout-data") + + start-p (unchecked-get props "start-p") + type (unchecked-get props "type") + zoom (unchecked-get props "zoom") + + bounds (:points shape) + hv #(gpo/start-hv bounds %) + vv #(gpo/start-vv bounds %) + + [layout-gap-row layout-gap-col] (ctl/gaps shape) + + {:keys [handle-pointer-down handle-lost-pointer-capture handle-pointer-move]} + (use-resize-track type shape index track-before track-after zoom snap-pixel?) + + [width height] + (if (= type :column) + [(max 0 (- layout-gap-col (/ 10 zoom)) (/ 8 zoom)) + (+ row-total-size row-total-gap)] + + [(+ column-total-size column-total-gap) + (max 0 (- layout-gap-row (/ 10 zoom)) (/ 8 zoom))]) + + start-p-resize + (cond-> start-p + (and (= type :column) (= index 0)) + (gpt/subtract (hv (/ width 2))) + + (and (= type :row) (= index 0)) + (gpt/subtract (vv (/ height 2))) + + (and (= type :column) (not= index 0) (not last?)) + (-> (gpt/subtract (hv (/ layout-gap-col 2))) + (gpt/subtract (hv (/ width 2)))) + + (and (= type :row) (not= index 0) (not last?)) + (-> (gpt/subtract (vv (/ layout-gap-row 2))) + (gpt/subtract (vv (/ height 2))))) + + start-p-drop + (cond-> start-p + (and (= type :column) (= index 0)) + (gpt/subtract (hv (/ width 2))) + + (and (= type :row) (= index 0)) + (gpt/subtract (vv (/ height 2))) + + (and (= type :column) last?) + (gpt/add (hv (/ width 2))) + + (and (= type :row) last?) + (gpt/add (vv (/ height 2))) + + (and (= type :column) (not= index 0) (not last?)) + (-> (gpt/subtract (hv (/ layout-gap-col 2))) + (gpt/subtract (hv (/ 5 zoom)))) + + (and (= type :row) (not= index 0) (not last?)) + (-> (gpt/subtract (vv (/ layout-gap-row 2))) + (gpt/subtract (vv (/ 5 zoom)))))] + [:* + (when drop? + [:rect.drop + {:x (:x start-p-drop) + :y (:y start-p-drop) + :width (if (= type :column) (/ 10 zoom) width) + :height (if (= type :row) (/ 10 zoom) height) + :fill "var(--grid-editor-area-background)"}]) + + [:rect.resize-track-handler + {:x (:x start-p-resize) + :y (:y start-p-resize) + :height height + :width width + :on-pointer-down handle-pointer-down + :on-lost-pointer-capture handle-lost-pointer-capture + :on-pointer-move handle-pointer-move + :transform (dm/str (gmt/transform-in start-p (:transform shape))) + :class (if (= type :column) + (cur/get-dynamic "resize-ew" (:rotation shape)) + (cur/get-dynamic "resize-ns" (:rotation shape))) + :style {:fill "transparent" + :stroke-width 0}}]])) + +(def marker-width 24) +(def marker-h1 20) +(def marker-h2 10) +(def marker-bradius 2) + +(defn marker-shape-d + [center zoom] + (let [marker-width (/ marker-width zoom) + marker-h1 (/ marker-h1 zoom) + marker-h2 (/ marker-h2 zoom) + + marker-bradius (/ marker-bradius zoom) + marker-half-width (/ marker-width 2) + marker-half-height (/ (+ marker-h1 marker-h2) 2) + + start-p + (gpt/subtract center (gpt/point marker-half-width marker-half-height)) + + [a b c d e] + (reduce + apply-to-point + [start-p] + [#(gpt/add % (gpt/point marker-width 0)) + #(gpt/add % (gpt/point 0 marker-h1)) + #(gpt/add % (gpt/point (- marker-half-width) marker-h2)) + #(gpt/subtract % (gpt/point marker-half-width marker-h2))]) + + vea (gpt/to-vec e a) + vab (gpt/to-vec a b) + vbc (gpt/to-vec b c) + vcd (gpt/to-vec c d) + vde (gpt/to-vec d e) + + lea (gpt/length vea) + lab (gpt/length vab) + lbc (gpt/length vbc) + lcd (gpt/length vcd) + lde (gpt/length vde) + + a1 (gpt/add e (gpt/resize vea (- lea marker-bradius))) + a2 (gpt/add a (gpt/resize vab marker-bradius)) + + b1 (gpt/add a (gpt/resize vab (- lab marker-bradius))) + b2 (gpt/add b (gpt/resize vbc marker-bradius)) + + c1 (gpt/add b (gpt/resize vbc (- lbc marker-bradius))) + c2 (gpt/add c (gpt/resize vcd marker-bradius)) + + d1 (gpt/add c (gpt/resize vcd (- lcd marker-bradius))) + d2 (gpt/add d (gpt/resize vde marker-bradius)) + + e1 (gpt/add d (gpt/resize vde (- lde marker-bradius))) + e2 (gpt/add e (gpt/resize vea marker-bradius))] + (dm/str + (dm/fmt "M%,%" (:x a1) (:y a1)) + (dm/fmt "Q%,%,%,%" (:x a) (:y a) (:x a2) (:y a2)) + + (dm/fmt "L%,%" (:x b1) (:y b1)) + (dm/fmt "Q%,%,%,%" (:x b) (:y b) (:x b2) (:y b2)) + + (dm/fmt "L%,%" (:x c1) (:y c1)) + (dm/fmt "Q%,%,%,%" (:x c) (:y c) (:x c2) (:y c2)) + + (dm/fmt "L%,%" (:x d1) (:y d1)) + (dm/fmt "Q%,%,%,%" (:x d) (:y d) (:x d2) (:y d2)) + + (dm/fmt "L%,%" (:x e1) (:y e1)) + (dm/fmt "Q%,%,%,%" (:x e) (:y e) (:x e2) (:y e2)) + + (dm/fmt "L%,%" (:x a1) (:y a1)) + "Z"))) + (mf/defc track-marker {::mf/wrap-props false} [props] @@ -29,295 +649,517 @@ (let [center (unchecked-get props "center") value (unchecked-get props "value") zoom (unchecked-get props "zoom") - - marker-points - (reduce - apply-to-point - [(gpt/subtract center - (gpt/point (/ 13 zoom) (/ 16 zoom)))] - [#(gpt/add % (gpt/point (/ 26 zoom) 0)) - #(gpt/add % (gpt/point 0 (/ 24 zoom))) - #(gpt/add % (gpt/point (- (/ 13 zoom)) (/ 8 zoom))) - #(gpt/subtract % (gpt/point (/ 13 zoom) (/ 8 zoom)))]) + shape (unchecked-get props "shape") + index (unchecked-get props "index") + type (unchecked-get props "type") + track-before (unchecked-get props "track-before") + track-after (unchecked-get props "track-after") + snap-pixel? (unchecked-get props "snap-pixel?") text-x (:x center) - text-y (:y center)] + text-y (:y center) - [:g.grid-track-marker - [:polygon {:points (->> marker-points - (map #(dm/fmt "%,%" (:x %) (:y %))) - (str/join " ")) + {:keys [handle-pointer-down handle-lost-pointer-capture handle-pointer-move]} + (use-resize-track type shape index track-before track-after zoom snap-pixel?)] - :style {:fill "var(--color-distance)" - :fill-opacity 0.3}}] - [:text {:x text-x + [:g {:on-pointer-down handle-pointer-down + :on-lost-pointer-capture handle-lost-pointer-capture + :on-pointer-move handle-pointer-move + :class (dom/classnames (stl/css :grid-track-marker) true + (cur/get-dynamic "resize-ew" (:rotation shape)) (= type :column) + (cur/get-dynamic "resize-ns" (:rotation shape)) (= type :row)) + :transform (dm/str (gmt/transform-in center (:transform shape)))} + + [:path {:class (stl/css :marker-shape) + :d (marker-shape-d center zoom)}] + [:text {:class (stl/css :marker-text) + :x text-x :y text-y :width (/ 26.26 zoom) - :height (/ 32 zoom) - :font-size (/ 16 zoom) + :height (/ 36 zoom) :text-anchor "middle" - :dominant-baseline "middle" - :style {:fill "var(--color-distance)"}} + :dominant-baseline "middle"} (dm/str value)]])) -(mf/defc grid-editor-frame - {::mf/wrap-props false} +(mf/defc track + {::mf/wrap [mf/memo] + ::mf/wrap-props false} [props] + (let [shape (unchecked-get props "shape") + zoom (unchecked-get props "zoom") + type (unchecked-get props "type") + index (unchecked-get props "index") + snap-pixel? (unchecked-get props "snap-pixel?") + track-data (unchecked-get props "track-data") + layout-data (unchecked-get props "layout-data") + hovering? (unchecked-get props "hovering?") + drop? (unchecked-get props "drop?") - (let [bounds (unchecked-get props "bounds") - zoom (unchecked-get props "zoom") - hv #(gpo/start-hv bounds %) + on-start-reorder-track (unchecked-get props "on-start-reorder-track") + on-move-reorder-track (unchecked-get props "on-move-reorder-track") + on-end-reorder-track (unchecked-get props "on-end-reorder-track") + + track-input-ref (mf/use-ref) + [layout-gap-row layout-gap-col] (ctl/gaps shape) + + bounds (:points shape) vv #(gpo/start-vv bounds %) - width (gpo/width-points bounds) - height (gpo/height-points bounds) - origin (gpo/origin bounds) - - frame-points - (reduce - apply-to-point - [origin] - [#(gpt/add % (hv width)) - #(gpt/subtract % (vv (/ 40 zoom))) - #(gpt/subtract % (hv (+ width (/ 40 zoom)))) - #(gpt/add % (vv (+ height (/ 40 zoom)))) - #(gpt/add % (hv (/ 40 zoom)))])] - - [:polygon {:points (->> frame-points - (map #(dm/fmt "%,%" (:x %) (:y %))) - (str/join " ")) - :style {:stroke "var(--color-distance)" - :stroke-width (/ 1 zoom)}}])) - -(mf/defc plus-btn - {::mf/wrap-props false} - [props] - - (let [start-p (unchecked-get props "start-p") - zoom (unchecked-get props "zoom") - type (unchecked-get props "type") - - [rect-x rect-y icon-x icon-y] - (if (= type :column) - [(:x start-p) - (- (:y start-p) (/ 40 zoom)) - (+ (:x start-p) (/ 12 zoom)) - (- (:y start-p) (/ 28 zoom))] - - [(- (:x start-p) (/ 40 zoom)) - (:y start-p) - (- (:x start-p) (/ 28 zoom)) - (+ (:y start-p) (/ 12 zoom))])] - - [:g.plus-button - [:rect {:x rect-x - :y rect-y - :width (/ 40 zoom) - :height (/ 40 zoom) - :style {:fill "var(--color-distance)" - :stroke "var(--color-distance)" - :stroke-width (/ 1 zoom)}}] - - [:use {:x icon-x - :y icon-y - :width (/ 16 zoom) - :height (/ 16 zoom) - :href (dm/str "#icon-plus") - :fill "white"}]])) - -(mf/defc grid-cell - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - {:keys [row-tracks column-tracks]} (unchecked-get props "layout-data") - bounds (unchecked-get props "bounds") - zoom (unchecked-get props "zoom") - - hover? (unchecked-get props "hover?") - selected? (unchecked-get props "selected?") - - row (unchecked-get props "row") - column (unchecked-get props "column") - - column-track (nth column-tracks (dec column) nil) - row-track (nth row-tracks (dec row) nil) - - - origin (gpo/origin bounds) hv #(gpo/start-hv bounds %) - vv #(gpo/start-vv bounds %) - start-p (-> origin - (gpt/add (hv (:distance column-track))) - (gpt/add (vv (:distance row-track)))) + start-p (:start-p track-data) - end-p (-> start-p - (gpt/add (hv (:value column-track))) - (gpt/add (vv (:value row-track))))] + hpt (gpo/project-point bounds :h start-p) + vpt (gpo/project-point bounds :v start-p) - [:rect.cell-editor - {:x (:x start-p) - :y (:y start-p) - :width (- (:x end-p) (:x start-p)) - :height (- (:y end-p) (:y start-p)) - - :on-pointer-enter #(st/emit! (dwge/hover-grid-cell (:id shape) row column true)) - :on-pointer-leave #(st/emit! (dwge/hover-grid-cell (:id shape) row column false)) - - :on-click #(st/emit! (dwge/select-grid-cell (:id shape) row column)) - - :style {:fill "transparent" - :stroke "var(--color-distance)" - :stroke-dasharray (when-not (or hover? selected?) - (str/join " " (map #(/ % zoom) [0 8]) )) - :stroke-linecap "round" - :stroke-width (/ 2 zoom)}}])) - -(mf/defc resize-handler - {::mf/wrap-props false} - [props] - - (let [start-p (unchecked-get props "start-p") - type (unchecked-get props "type") - bounds (unchecked-get props "bounds") - zoom (unchecked-get props "zoom") - - width (gpo/width-points bounds) - height (gpo/height-points bounds) - - dragging-ref (mf/use-ref false) - start-ref (mf/use-ref nil) - - on-pointer-down - (mf/use-callback - (fn [event] - (dom/capture-pointer event) - (mf/set-ref-val! dragging-ref true) - (mf/set-ref-val! start-ref (dom/get-client-position event)))) - - on-lost-pointer-capture - (mf/use-callback - (fn [event] - (dom/release-pointer event) - (mf/set-ref-val! dragging-ref false) - (mf/set-ref-val! start-ref nil))) - - on-pointer-move - (mf/use-callback - (fn [event] - (when (mf/ref-val dragging-ref) - (let [start (mf/ref-val start-ref) - pos (dom/get-client-position event) - _delta (-> (gpt/to-vec start pos) - (get (if (= type :column) :x :y)))] - - ;; TODO Implement resize - #_(prn ">Delta" delta))))) - - - [x y width height] + marker-p (if (= type :column) - [(- (:x start-p) (/ 8 zoom)) - (- (:y start-p) (/ 40 zoom)) - (/ 16 zoom) - (+ height (/ 40 zoom))] + (-> hpt + (gpt/subtract (vv (/ 20 zoom))) + (cond-> (not= index 0) + (gpt/subtract (hv (/ layout-gap-col 2))))) + (-> vpt + (gpt/subtract (hv (/ 20 zoom))) + (cond-> (not= index 0) + (gpt/subtract (vv (/ layout-gap-row 2)))))) - [(- (:x start-p) (/ 40 zoom)) - (- (:y start-p) (/ 8 zoom)) - (+ width (/ 40 zoom)) - (/ 16 zoom)])] + text-p (if (= type :column) hpt vpt) - [:rect.resize-handler - {:x x - :y y - :class (if (= type :column) - "resize-ew-0" - "resize-ns-0") - :height height - :width width - :on-pointer-down on-pointer-down - :on-lost-pointer-capture on-lost-pointer-capture - :on-pointer-move on-pointer-move - :style {:fill "transparent"}}])) + handle-blur-track-input + (mf/use-fn + (mf/deps (:id shape)) + (fn [event] + (let [target (-> event dom/get-target) + value (-> target dom/get-input-value str/upper) + value-int (d/parse-integer value) + value-int (when value-int (max 0 value-int)) + + [track-type value] + (cond + (str/ends-with? value "%") + [:percent (d/nilv value-int 50)] + + (str/ends-with? value "FR") + [:flex (d/nilv value-int 1)] + + (some? value-int) + [:fixed (d/nilv value-int 100)] + + :else + [:auto nil]) + + track-data (when (some? track-type) {:type track-type :value value})] + + (dom/set-value! (mf/ref-val track-input-ref) (format-size track-data)) + (if (some? track-type) + (do (st/emit! (dwsl/change-layout-track [(:id shape)] type index track-data)) + (dom/set-data! target "default-value" (format-size track-data))) + (obj/set! target "value" (dom/get-attribute target "data-default-value")))))) + + handle-keydown-track-input + (mf/use-fn + (fn [event] + (let [enter? (kbd/enter? event) + esc? (kbd/esc? event)] + (when enter? + (dom/blur! (dom/get-target event))) + (when esc? + (dom/blur! (dom/get-target event)))))) + + handle-pointer-enter + (mf/use-fn + (mf/deps (:id shape) type index) + (fn [] + (st/emit! (dwsl/hover-layout-track [(:id shape)] type index true)))) + + handle-pointer-leave + (mf/use-fn + (mf/deps (:id shape) type index) + (fn [] + (st/emit! (dwsl/hover-layout-track [(:id shape)] type index false)))) + + track-list-prop (if (= type :column) :column-tracks :row-tracks) + [text-x text-y text-width text-height] + (if (= type :column) + [(:x text-p) (- (:y text-p) (/ 36 zoom)) (max 0 (:size track-data)) (/ 36 zoom)] + [(- (:x text-p) (max 0 (:size track-data))) (- (:y text-p) (/ 36 zoom)) (max 0 (:size track-data)) (/ 36 zoom)]) + + handle-drag-start + (mf/use-fn + (mf/deps on-start-reorder-track type index) + (fn [] + (on-start-reorder-track type index))) + + handle-drag-end + (mf/use-fn + (mf/deps on-end-reorder-track type index) + (fn [event position] + (on-end-reorder-track type index position (not (kbd/mod? event))))) + + handle-drag-position + (mf/use-fn + (mf/deps on-move-reorder-track type index) + (fn [_ position] + (on-move-reorder-track type index position))) + + handle-show-track-menu + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + (let [position (cond-> (dom/get-client-position event) + (= type :column) (update :y + 40) + (= type :row) (update :x + 30))] + (st/emit! (dw/show-track-context-menu {:position position + :grid-id (:id shape) + :type type + :index index}))))) + + trackwidth (* text-width zoom) + medium? (and (>= trackwidth small-size-limit) (< trackwidth medium-size-limit)) + small? (< trackwidth small-size-limit) + + track-before (get-in layout-data [track-list-prop (dec index)]) + + {:keys [handle-pointer-down handle-lost-pointer-capture handle-pointer-move]} + (use-drag {:on-drag-start handle-drag-start + :on-drag-end handle-drag-end + :on-drag-position handle-drag-position})] + + (mf/use-effect + (mf/deps track-data) + (fn [] + (dom/set-value! (mf/ref-val track-input-ref) (format-size track-data)))) + + + [:g.track + [:g {:on-pointer-enter handle-pointer-enter + :on-pointer-leave handle-pointer-leave + :transform (if (= type :column) + (dm/str (gmt/transform-in text-p (:transform shape))) + (dm/str (gmt/transform-in text-p (gmt/rotate (:transform shape) -90))))} + + [:rect {:class (stl/css :grid-editor-header-hover) + :x (+ text-x (/ 18 zoom)) + :y text-y + :width (- text-width (/ 36 zoom)) + :height (- text-height (/ 5 zoom)) + :rx (/ 3 zoom) + :style {:cursor "pointer"} + :opacity (if (and hovering? (not small?)) 0.2 0)}] + (when (not small?) + [:foreignObject {:x text-x :y text-y :width text-width :height text-height} + [:div {:class (stl/css :grid-editor-wrapper) + :on-context-menu handle-show-track-menu + :on-pointer-down handle-pointer-down + :on-lost-pointer-capture handle-lost-pointer-capture + :on-pointer-move handle-pointer-move} + [:input + {:ref track-input-ref + :style {} + :class (stl/css :grid-editor-label) + :type "text" + :default-value (format-size track-data) + :data-default-value (format-size track-data) + :on-key-down handle-keydown-track-input + :on-blur handle-blur-track-input}] + (when (and hovering? (not medium?) (not small?)) + [:button {:class (stl/css :grid-editor-button) + :on-click handle-show-track-menu} i/menu])]])] + + [:g {:transform (when (= type :row) (dm/fmt "rotate(-90 % %)" (:x marker-p) (:y marker-p)))} + [:& track-marker + {:center marker-p + :index index + :shape shape + :snap-pixel? snap-pixel? + :track-after track-data + :track-before track-before + :type type + :value (dm/str (inc index)) + :zoom zoom}]] + + [:& resize-track-handler + {:index index + :layout-data layout-data + :shape shape + :snap-pixel? snap-pixel? + :drop? drop? + :start-p start-p + :track-after track-data + :track-before track-before + :type type + :zoom zoom}]])) (mf/defc editor - {::mf/wrap-props false} + {::mf/memo true + ::mf/props :obj} [props] + (let [base-shape (unchecked-get props "shape") + objects (unchecked-get props "objects") + modifiers (unchecked-get props "modifiers") + zoom (unchecked-get props "zoom") + view-only (unchecked-get props "view-only") - (let [shape (unchecked-get props "shape") - objects (unchecked-get props "objects") - zoom (unchecked-get props "zoom") - bounds (:points shape) + shape + (mf/use-memo + (mf/deps modifiers base-shape) + #(gsh/transform-shape + base-shape + (dm/get-in modifiers [(:id base-shape) :modifiers]))) + + snap-pixel? (mf/deref refs/snap-pixel?) + + grid-edition-id-ref + (mf/use-memo + (mf/deps (:id shape)) + #(refs/workspace-grid-edition-id (:id shape))) - grid-edition-id-ref (mf/use-memo #(refs/workspace-grid-edition-id (:id shape))) grid-edition (mf/deref grid-edition-id-ref) hover-cells (:hover grid-edition) selected-cells (:selected grid-edition) - children (->> (cph/get-immediate-children objects (:id shape)) - (remove :hidden) - (map #(vector (gpo/parent-coords-bounds (:points %) (:points shape)) %))) + hover-columns + (->> (:hover-track grid-edition) + (filter (fn [[t _]] (= t :column))) + (map (fn [[_ idx]] idx)) + (into #{})) + hover-rows + (->> (:hover-track grid-edition) + (filter (fn [[t _]] (= t :row))) + (map (fn [[_ idx]] idx)) + (into #{})) + + bounds (:points shape) hv #(gpo/start-hv bounds %) vv #(gpo/start-vv bounds %) - width (gpo/width-points bounds) - height (gpo/height-points bounds) origin (gpo/origin bounds) - {:keys [row-tracks column-tracks] :as layout-data} - (gsg/calc-layout-data shape children bounds)] + layout-data + (mf/use-memo + (mf/deps shape modifiers) + (fn [] + (let [objects (gsh/apply-objects-modifiers objects modifiers) + ids (cfh/get-children-ids objects (:id shape)) + objects (gsh/update-shapes-geometry objects (reverse ids)) - (mf/use-effect - (fn [] - #(st/emit! (dwge/stop-grid-layout-editing (:id shape))))) + children + (->> (cfh/get-immediate-children objects (:id shape) {:remove-hidden true}) + (map #(vector (gpo/parent-coords-bounds (:points %) (:points shape)) %))) - [:g.grid-editor - [:& grid-editor-frame {:zoom zoom - :bounds bounds}] - (let [start-p (-> origin (gpt/add (hv width)))] - [:& plus-btn {:start-p start-p - :zoom zoom - :type :column}]) + children-bounds (d/lazy-map ids #(gsh/shape->points (get objects %)))] + (gsg/calc-layout-data shape bounds children children-bounds objects)))) - (let [start-p (-> origin (gpt/add (vv height)))] - [:& plus-btn {:start-p start-p - :zoom zoom - :type :row}]) + {:keys [row-tracks column-tracks column-total-size column-total-gap row-total-size row-total-gap]} layout-data - (for [[_ {:keys [column row]}] (:layout-grid-cells shape)] - [:& grid-cell {:shape shape - :layout-data layout-data - :row row - :column column - :bounds bounds - :zoom zoom - :hover? (contains? hover-cells [row column]) - :selected? (= selected-cells [row column]) - }]) + width (max (gpo/width-points bounds) (+ column-total-size column-total-gap (ctl/h-padding shape))) + height (max (gpo/height-points bounds) (+ row-total-size row-total-gap (ctl/v-padding shape))) - (for [[idx column-data] (d/enumerate column-tracks)] - (let [start-p (-> origin (gpt/add (hv (:distance column-data)))) - marker-p (-> start-p (gpt/subtract (vv (/ 20 zoom))))] + handle-pointer-down + (mf/use-fn + (fn [event] + (let [left-click? (= 1 (.-which (.-nativeEvent event)))] + (when left-click? + (dom/stop-propagation event))))) + + handle-add-column + (mf/use-fn + (mf/deps (:id shape)) + (fn [] + (st/emit! (st/emit! (dwsl/add-layout-track [(:id shape)] :column ctl/default-track-value))))) + + handle-add-row + (mf/use-fn + (mf/deps (:id shape)) + (fn [] + (st/emit! (st/emit! (dwsl/add-layout-track [(:id shape)] :row ctl/default-track-value))))) + + target-tracks* (mf/use-ref nil) + drop-track-type* (mf/use-state nil) + drop-track-target* (mf/use-state nil) + + handle-start-reorder-track + (mf/use-fn + (mf/deps layout-data) + (fn [type _from-idx] + ;; Initialize target-tracks + (let [line-vec (if (= type :column) (vv 1) (hv 1)) + + first-point origin + last-point (if (= type :column) (nth bounds 1) (nth bounds 3)) + mid-points + (if (= type :column) + (->> (:column-tracks layout-data) + (mapv #(gpt/add (:start-p %) (hv (/ (:size %) 2))))) + + (->> (:row-tracks layout-data) + (mapv #(gpt/add (:start-p %) (vv (/ (:size %) 2)))))) + + tracks + (->> (d/with-prev (d/concat-vec [first-point] mid-points [last-point])) + (d/enumerate) + (keep + (fn [[index [current prev]]] + (when (some? prev) + [[prev current line-vec] (dec index)]))))] + + (mf/set-ref-val! target-tracks* tracks) + (reset! drop-track-type* type)))) + + handle-move-reorder-track + (mf/use-fn + (fn [_type _from-idx position] + (let [index + (->> (mf/ref-val target-tracks*) + (d/seek (fn [[[p1 p2 v] _]] + (gl/is-inside-lines? [p1 v] [p2 v] position))) + (second))] + (when (some? index) + (reset! drop-track-target* index))))) + + handle-end-reorder-track + (mf/use-fn + (mf/deps base-shape @drop-track-target*) + (fn [type from-index _position move-content?] + (when-let [to-index @drop-track-target*] + (let [ids [(:id base-shape)]] + (cond + (< from-index to-index) + (st/emit! (dwsl/reorder-layout-track ids type from-index (dec to-index) move-content?)) + + (> from-index to-index) + (st/emit! (dwsl/reorder-layout-track ids type from-index (dec to-index) move-content?))))) + + (mf/set-ref-val! target-tracks* nil) + (reset! drop-track-type* nil) + (reset! drop-track-target* nil)))] + + (mf/with-effect [] + #(st/emit! (dwge/stop-grid-layout-editing (:id shape)))) + + (when (and (not (:hidden shape)) (not (:blocked shape))) + [:g.grid-editor {:pointer-events (when view-only "none") + :on-pointer-down handle-pointer-down} + [:g.cells + (for [cell (ctl/get-cells shape {:sort? true})] + [:& grid-cell {:key (dm/str "cell-" (:id cell)) + :shape base-shape + :layout-data layout-data + :cell cell + :zoom zoom + :hover? (contains? hover-cells (:id cell)) + :selected? (contains? selected-cells (:id cell))}])] + + (when-not ^boolean view-only [:* - [:& track-marker {:center marker-p - :value (dm/str (inc idx)) - :zoom zoom}] + [:& grid-editor-frame {:zoom zoom + :bounds bounds + :width width + :height height}] + (let [start-p (-> origin (gpt/add (hv (+ width (/ 30 zoom)))))] + [:g {:transform (dm/str (gmt/transform-in start-p (:transform shape)))} + [:& plus-btn {:start-p start-p + :zoom zoom + :type :column + :on-click handle-add-column}]]) - [:& resize-handler {:type :column - :start-p start-p - :zoom zoom - :bounds bounds}]])) + (let [start-p (-> origin (gpt/add (vv (+ height (/ 30 zoom)))))] + [:g {:transform (dm/str (gmt/transform-in start-p (:transform shape)))} + [:& plus-btn {:start-p start-p + :zoom zoom + :type :row + :on-click handle-add-row}]]) - (for [[idx row-data] (d/enumerate row-tracks)] - (let [start-p (-> origin (gpt/add (vv (:distance row-data)))) - marker-p (-> start-p (gpt/subtract (hv (/ 20 zoom))))] - [:* - [:g {:transform (dm/fmt "rotate(-90 % %)" (:x marker-p) (:y marker-p))} - [:& track-marker {:center marker-p - :value (dm/str (inc idx)) - :zoom zoom}]] + (for [[idx column-data] (d/enumerate column-tracks)] + (let [drop? (and (= :column @drop-track-type*) + (= idx @drop-track-target*))] + [:& track {:key (dm/str "column-track-" idx) + :shape shape + :zoom zoom + :type :column + :index idx + :layout-data layout-data + :snap-pixel? snap-pixel? + :drop? drop? + :track-data column-data + :hovering? (contains? hover-columns idx) + :on-start-reorder-track handle-start-reorder-track + :on-move-reorder-track handle-move-reorder-track + :on-end-reorder-track handle-end-reorder-track}])) - [:& resize-handler {:type :row - :start-p start-p - :zoom zoom - :bounds bounds}]]))])) + ;; Last track resize handler + (when-not (empty? column-tracks) + (let [last-track (last column-tracks) + start-p (:start-p last-track) + end-p (gpt/add start-p (hv (:size last-track))) + marker-p (-> (gpo/project-point bounds :h end-p) + (gpt/subtract (vv (/ 20 zoom))))] + [:g.track + [:& track-marker {:center marker-p + :index (count column-tracks) + :shape shape + :snap-pixel? snap-pixel? + :track-before (last column-tracks) + :type :column + :value (dm/str (inc (count column-tracks))) + :zoom zoom}] + (let [drop? (and (= :column @drop-track-type*) + (= (count column-tracks) @drop-track-target*))] + [:& resize-track-handler + {:index (count column-tracks) + :last? true + :drop? drop? + :shape shape + :layout-data layout-data + :snap-pixel? snap-pixel? + :start-p end-p + :type :column + :track-before (last column-tracks) + :zoom zoom}])])) + + (for [[idx row-data] (d/enumerate row-tracks)] + (let [drop? (and (= :row @drop-track-type*) + (= idx @drop-track-target*))] + [:& track {:index idx + :key (dm/str "row-track-" idx) + :layout-data layout-data + :shape shape + :snap-pixel? snap-pixel? + :drop? drop? + :track-data row-data + :type :row + :zoom zoom + :hovering? (contains? hover-rows idx) + :on-start-reorder-track handle-start-reorder-track + :on-move-reorder-track handle-move-reorder-track + :on-end-reorder-track handle-end-reorder-track}])) + (when-not (empty? row-tracks) + (let [last-track (last row-tracks) + start-p (:start-p last-track) + end-p (gpt/add start-p (vv (:size last-track))) + marker-p (-> (gpo/project-point bounds :v end-p) + (gpt/subtract (hv (/ 20 zoom))))] + [:g.track + [:g {:transform (dm/fmt "rotate(-90 % %)" (:x marker-p) (:y marker-p))} + [:& track-marker {:center marker-p + :index (count row-tracks) + :shape shape + :snap-pixel? snap-pixel? + :track-before (last row-tracks) + :type :row + :value (dm/str (inc (count row-tracks))) + :zoom zoom}]] + (let [drop? (and (= :row @drop-track-type*) + (= (count row-tracks) @drop-track-target*))] + [:& resize-track-handler + {:index (count row-tracks) + :last? true + :drop? drop? + :shape shape + :layout-data layout-data + :start-p end-p + :type :row + :track-before (last row-tracks) + :snap-pixel? snap-pixel? + :zoom zoom}])]))])]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss new file mode 100644 index 0000000000..f12752f6ca --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss @@ -0,0 +1,161 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.grid-track-marker { + .marker-shape { + fill: var(--grid-editor-marker-color); + } + .marker-text { + fill: var(--app-white); + font-size: calc($s-12 / var(--zoom)); + font-family: worksans; + } +} + +.grid-editor-wrapper { + cursor: grab; + width: 100%; + height: 80%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.grid-editor-header-hover { + fill: var(--grid-editor-marker-color); +} + +.grid-editor-label { + flex: 1; + background: none; + border: 0; + color: var(--grid-editor-marker-text); + font-family: worksans; + font-size: calc($fs-12 / var(--zoom)); + font-weight: 400; + margin: 0; + max-width: calc($s-60 / var(--zoom)); + padding: 0; + padding: calc($s-4 / var(--zoom)); + text-align: center; + + &:focus { + outline: none; + } +} + +.grid-editor-button { + background: none; + border: none; + cursor: pointer; + margin: 0; + padding: 0; + position: absolute; + top: calc($s-6 / var(--zoom)); + right: calc($s-20 / var(--zoom)); + width: calc($s-20 / var(--zoom)); + height: calc($s-20 / var(--zoom)); + + svg { + position: absolute; + top: 0; + left: 0; + width: calc($s-16 / var(--zoom)); + height: auto; + stroke: var(--grid-editor-marker-color); + } +} + +.grid-frame { + fill: var(--grid-editor-marker-color); + fill-opacity: 0.1; +} + +.grid-plus-button { + cursor: pointer; + opacity: 0.5; + + .grid-plus-shape { + fill: var(--grid-editor-plus-btn-background); + stroke: var(--grid-editor-plus-btn-background); + stroke-width: calc($s-1 / var(--zoom)); + } + + .grid-plus-icon { + stroke: var(--grid-editor-plus-btn-foreground); + } + + &:hover { + opacity: 1; + } +} + +.grid-cell-outline { + fill: transparent; + stroke: var(--grid-editor-line-color); + stroke-opacity: 0.5; + stroke-width: calc(1 / var(--zoom)); + + &.hover, + &.selected { + stroke-opacity: 1; + stroke-width: calc(2 / var(--zoom)); + } +} + +.grid-actions { + pointer-events: none; + position: absolute; + top: $s-44; + left: 50%; + z-index: $z-index-20; + + .grid-actions-container { + @include flexRow; + background: var(--panel-background-color); + border-radius: $br-12; + box-shadow: 0px 0px $s-12 0px var(--menu-shadow-color); + gap: $s-8; + height: $s-48; + margin-left: -50%; + padding: $s-8; + cursor: initial; + pointer-events: initial; + width: $s-512; + } + + .grid-actions-title { + flex: 1; + font-size: $fs-12; + color: var(--color-foreground-secondary); + padding-left: $s-8; + } + + .board-name { + } + + .locate-btn { + @extend .button-secondary; + text-transform: uppercase; + padding: $s-8 $s-20; + font-size: $fs-11; + } + .done-btn { + @extend .button-primary; + text-transform: uppercase; + padding: $s-8 $s-20; + font-size: $fs-11; + } + .close-btn { + @extend .button-tertiary; + svg { + @extend .button-icon; + } + } +} diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index 1182ca4ea4..79321b508d 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -7,10 +7,10 @@ (ns app.main.ui.workspace.viewport.guides (:require [app.common.colors :as colors] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.common.pages.helpers :as cph] [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] @@ -19,14 +19,14 @@ [app.main.streams :as ms] [app.main.ui.css-cursors :as cur] [app.main.ui.formats :as fmt] - [app.main.ui.workspace.viewport.rules :as rules] + [app.main.ui.workspace.viewport.rulers :as rulers] [app.util.dom :as dom] [rumext.v2 :as mf])) (def guide-width 1) (def guide-opacity 0.7) (def guide-opacity-hover 1) -(def guide-color colors/primary) +(def guide-color colors/new-danger) (def guide-pill-width 34) (def guide-pill-height 20) (def guide-pill-corner-radius 4) @@ -129,7 +129,7 @@ (defn guide-area-axis [pos vbox zoom frame axis] - (let [rules-pos (/ rules/rules-pos zoom) + (let [rulers-pos (/ rulers/rulers-pos zoom) guide-active-area (/ guide-active-area zoom)] (cond (and (some? frame) (= axis :x)) @@ -146,12 +146,12 @@ (= axis :x) {:x (- pos (/ guide-active-area 2)) - :y (+ (:y vbox) rules-pos) + :y (+ (:y vbox) rulers-pos) :width guide-active-area :height (:height vbox)} :else - {:x (+ (:x vbox) rules-pos) + {:x (+ (:x vbox) rulers-pos) :y (- pos (/ guide-active-area 2)) :width (:width vbox) :height guide-active-area}))) @@ -198,23 +198,23 @@ (defn guide-pill-axis [pos vbox zoom axis] - (let [rules-pos (/ rules/rules-pos zoom) + (let [rulers-pos (/ rulers/rulers-pos zoom) guide-pill-width (/ guide-pill-width zoom) guide-pill-height (/ guide-pill-height zoom)] (if (= axis :x) {:rect-x (- pos (/ guide-pill-width 2)) - :rect-y (+ (:y vbox) rules-pos (- (/ guide-pill-width 2)) (/ 3 zoom)) + :rect-y (+ (:y vbox) rulers-pos (- (/ guide-pill-width 2)) (/ 3 zoom)) :rect-width guide-pill-width :rect-height guide-pill-height :text-x pos - :text-y (+ (:y vbox) rules-pos (- (/ 3 zoom)))} + :text-y (+ (:y vbox) rulers-pos (- (/ 3 zoom)))} - {:rect-x (+ (:x vbox) rules-pos (- (/ guide-pill-height 2)) (- (/ 4 zoom))) + {:rect-x (+ (:x vbox) rulers-pos (- (/ guide-pill-height 2)) (- (/ 4 zoom))) :rect-y (- pos (/ guide-pill-width 2)) :rect-width guide-pill-height :rect-height guide-pill-width - :text-x (+ (:x vbox) rules-pos (- (/ 3 zoom))) + :text-x (+ (:x vbox) rulers-pos (- (/ 3 zoom))) :text-y pos}))) (defn guide-inside-vbox? @@ -222,7 +222,7 @@ (partial guide-inside-vbox? zoom vbox)) ([zoom {:keys [x y width height]} {:keys [axis position]}] - (let [rule-area-size (/ rules/rule-area-size zoom) + (let [rule-area-size (/ rulers/ruler-area-size zoom) x1 x x2 (+ x width) y1 y @@ -250,11 +250,11 @@ [guide frame] (if (= :x (:axis guide)) - (and (>= (:position guide) (:x frame) ) - (<= (:position guide) (+ (:x frame) (:width frame)) )) + (and (>= (:position guide) (:x frame)) + (<= (:position guide) (+ (:x frame) (:width frame)))) - (and (>= (:position guide) (:y frame) ) - (<= (:position guide) (+ (:y frame) (:height frame)) )))) + (and (>= (:position guide) (:y frame)) + (<= (:position guide) (+ (:y frame) (:height frame)))))) (mf/defc guide {::mf/wrap [mf/memo]} @@ -293,7 +293,7 @@ (not (is-guide-inside-frame? (assoc guide :position pos) frame)))] (when (or (nil? frame) - (and (cph/root-frame? frame) + (and (cfh/root-frame? frame) (not (ctst/rotated-frame? frame)))) [:g.guide-area {:opacity (when frame-guide-outside? 0)} (when-not disabled-guides? @@ -376,9 +376,9 @@ :text-anchor "middle" :dominant-baseline "middle" :transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")")) - :style {:font-size (/ rules/font-size zoom) - :font-family rules/font-family - :fill colors/black}} + :style {:font-size (/ rulers/font-size zoom) + :font-family rulers/font-family + :fill colors/white}} ;; If the guide is associated to a frame we show the position relative to the frame (fmt/format-number (- pos (if (= axis :x) (:x frame) (:y frame))))]]))]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 0a12452391..c2d4ab55b7 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -8,14 +8,16 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.focus :as cpf] + [app.common.files.helpers :as cfh] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.pages :as cp] - [app.common.pages.helpers :as cph] [app.common.types.component :as ctk] [app.common.types.shape-tree :as ctt] [app.common.uuid :as uuid] [app.main.data.shortcuts :as dsc] [app.main.data.workspace :as dw] + [app.main.data.workspace.grid-layout.shortcuts :as gsc] [app.main.data.workspace.path.shortcuts :as psc] [app.main.data.workspace.shortcuts :as wsc] [app.main.data.workspace.text.shortcuts :as tsc] @@ -26,10 +28,13 @@ [app.main.ui.workspace.viewport.actions :as actions] [app.main.ui.workspace.viewport.utils :as utils] [app.main.worker :as uw] + [app.util.debug :as dbg] [app.util.dom :as dom] [app.util.globals :as globals] - [beicon.core :as rx] - [debug :refer [debug?]] + [app.util.keyboard :as kbd] + [app.util.mouse :as mse] + [beicon.v2.core :as rx] + [beicon.v2.operators :as rxo] [goog.events :as events] [rumext.v2 :as mf]) (:import goog.events.EventType)) @@ -38,7 +43,8 @@ (let [on-key-down (actions/on-key-down) on-key-up (actions/on-key-up) on-mouse-wheel (actions/on-mouse-wheel zoom) - on-paste (actions/on-paste disable-paste in-viewport? workspace-read-only?)] + on-paste (actions/on-paste disable-paste in-viewport? workspace-read-only?) + on-blur (mf/use-fn #(st/emit! (mse/->BlurEvent)))] (mf/use-layout-effect (mf/deps on-key-down on-key-up on-mouse-wheel on-paste workspace-read-only?) @@ -48,7 +54,8 @@ ;; bind with passive=false to allow the event to be cancelled ;; https://stackoverflow.com/a/57582286/3219895 (events/listen js/window EventType.WHEEL on-mouse-wheel #js {:passive false}) - (events/listen js/window EventType.PASTE on-paste)]] + (events/listen js/window EventType.PASTE on-paste) + (events/listen js/window EventType.BLUR on-blur)]] (fn [] (doseq [key keys] (events/unlistenByKey key)))))))) @@ -97,14 +104,57 @@ (when (not= @cursor new-cursor) (reset! cursor new-cursor)))))) -(defn setup-keyboard [alt? mod? space? z? shift?] - (hooks/use-stream ms/keyboard-alt #(reset! alt? %)) - (hooks/use-stream ms/keyboard-mod #(do - (reset! mod? %) - (when-not % (reset! z? false)))) ;; In mac after command+z there is no event for the release of the z key - (hooks/use-stream ms/keyboard-space #(reset! space? %)) - (hooks/use-stream ms/keyboard-z #(reset! z? %)) - (hooks/use-stream ms/keyboard-shift #(reset! shift? %))) +(defn setup-keyboard + [alt* mod* space* z* shift*] + (let [kbd-zoom-s + (mf/with-memo [] + (->> ms/keyboard + (rx/filter kbd/key-down-event?) + (rx/filter kbd/mod-event?) + (rx/filter (fn [kevent] + (or ^boolean (kbd/minus? kevent) + ^boolean (kbd/underscore? kevent) + ^boolean (kbd/equals? kevent) + ^boolean (kbd/plus? kevent)))) + (rx/pipe (rxo/distinct-contiguous)))) + + kbd-shift-s + (mf/with-memo [] + (->> ms/keyboard + (rx/filter kbd/shift-key?) + (rx/filter (complement kbd/editing-event?)) + (rx/map kbd/key-down-event?) + (rx/pipe (rxo/distinct-contiguous)))) + + kbd-z-s + (mf/with-memo [] + (->> ms/keyboard + (rx/filter kbd/z?) + (rx/filter (complement kbd/editing-event?)) + (rx/map kbd/key-down-event?) + (rx/pipe (rxo/distinct-contiguous))))] + + (hooks/use-stream ms/keyboard-alt (partial reset! alt*)) + (hooks/use-stream ms/keyboard-space (partial reset! space*)) + (hooks/use-stream kbd-z-s (partial reset! z*)) + (hooks/use-stream kbd-shift-s (partial reset! shift*)) + (hooks/use-stream ms/keyboard-mod + (fn [value] + (reset! mod* value) + ;; In mac after command+z there is no event + ;; for the release of the z key + (when-not ^boolean value + (reset! z* false)))) + + (hooks/use-stream kbd-zoom-s + (fn [kevent] + (dom/prevent-default kevent) + (st/emit! + (if (or ^boolean (kbd/minus? kevent) + ^boolean (kbd/underscore? kevent)) + (dw/decrease-zoom) + (dw/increase-zoom))))))) + (defn group-empty-space? "Given a group `group-id` check if `hover-ids` contains any of its children. If it doesn't means @@ -112,15 +162,14 @@ [group-id objects hover-ids] (and (contains? #{:group :bool} (get-in objects [group-id :type])) - ;; If there are no children in the hover-ids we're in the empty side (->> hover-ids (remove #(contains? #{:group :bool} (get-in objects [% :type]))) - (some #(cph/is-parent? objects % group-id)) + (some #(cfh/is-parent? objects % group-id)) (not)))) (defn setup-hover-shapes - [page-id move-stream objects transform selected mod? hover hover-ids hover-top-frame-id hover-disabled? focus zoom show-measures?] + [page-id move-stream objects transform selected mod? hover measure-hover hover-ids hover-top-frame-id hover-disabled? focus zoom show-measures?] (let [;; We use ref so we don't recreate the stream on a change zoom-ref (mf/use-ref zoom) mod-ref (mf/use-ref @mod?) @@ -137,7 +186,8 @@ (mf/deps page-id) (fn [point] (let [zoom (mf/ref-val zoom-ref) - rect (gsh/center->rect point (/ 5 zoom) (/ 5 zoom))] + rect (grc/center->rect point (/ 5 zoom))] + (if (mf/ref-val hover-disabled-ref) (rx/of nil) (->> (uw/ask-buffered! @@ -145,26 +195,32 @@ :page-id page-id :rect rect :include-frames? true - :clip-children? true}) + :clip-children? true + :using-selrect? false}) ;; When the ask-buffered is canceled returns null. We filter them ;; to improve the behavior (rx/filter some?)))))) over-shapes-stream - (mf/use-memo - (fn [] - (rx/merge - ;; This stream works to "refresh" the outlines when the control is pressed - ;; but the mouse has not been moved from its position. - (->> mod-str - (rx/observe-on :async) - (rx/map #(deref last-point-ref)) - (rx/merge-map query-point)) + (mf/with-memo [move-stream mod-str] + (->> (rx/merge + ;; This stream works to "refresh" the outlines when the control is pressed + ;; but the mouse has not been moved from its position. + (->> mod-str + (rx/observe-on :async) + (rx/map #(deref last-point-ref)) + (rx/filter some?) + (rx/merge-map query-point)) - (->> move-stream - (rx/tap #(reset! last-point-ref %)) - ;; When transforming shapes we stop querying the worker - (rx/merge-map query-point)))))] + (->> move-stream + (rx/tap #(reset! last-point-ref %)) + ;; When transforming shapes we stop querying the worker + (rx/merge-map query-point))) + + (rx/share))) + + over-shapes-stream-debounced + (->> over-shapes-stream (rx/debounce 50))] ;; Refresh the refs on a value change (mf/use-effect @@ -194,64 +250,118 @@ #(mf/set-ref-val! focus-ref focus)) (hooks/use-stream - over-shapes-stream - (mf/deps page-id objects show-measures?) - (fn [ids] - (let [selected (mf/ref-val selected-ref) - focus (mf/ref-val focus-ref) - mod? (mf/ref-val mod-ref) + over-shapes-stream-debounced + (mf/deps objects) + (fn [_] + (reset! hover-top-frame-id (ctt/top-nested-frame objects (deref last-point-ref))))) - ids (into - (d/ordered-set) - (remove #(dm/get-in objects [% :blocked])) - (ctt/sort-z-index objects ids {:bottom-frames? mod?})) + ;; This ref is a cache of sorted ids. Sorting is expensive so we save the list + (let [sorted-ids-cache (mf/use-ref {})] + (hooks/use-stream + over-shapes-stream + (mf/deps page-id objects show-measures?) + (fn [ids] + (let [selected (mf/ref-val selected-ref) + focus (mf/ref-val focus-ref) + mod? (mf/ref-val mod-ref) + cached-ids (mf/ref-val sorted-ids-cache) - grouped? (fn [id] (contains? #{:group :bool} (get-in objects [id :type]))) + make-sorted-ids + (fn [mod? ids] + (let [sorted-ids + (into (d/ordered-set) + (comp (remove #(dm/get-in objects [% :blocked])) + (remove (partial cfh/svg-raw-shape? objects))) + (ctt/sort-z-index objects ids {:bottom-frames? mod?}))] + (mf/set-ref-val! sorted-ids-cache (assoc cached-ids [mod? ids] sorted-ids)) + sorted-ids)) - selected-with-parents - (into #{} (mapcat #(cph/get-parent-ids objects %)) selected) + ids (or (get cached-ids [mod? ids]) (make-sorted-ids mod? ids)) - root-frame-with-data? - #(as-> (get objects %) obj - (and (cph/root-frame? obj) - (d/not-empty? (:shapes obj)) - (not (ctk/instance-head? obj)) - (not (ctk/main-instance? obj)))) + grouped? + (fn [id] + (and (cfh/group-shape? objects id) + (not (cfh/mask-shape? objects id)))) - ;; Set with the elements to remove from the hover list - remove-id-xf - (cond - mod? - (filter grouped?) + selected-with-parents + (into #{} (mapcat #(cfh/get-parent-ids objects %)) selected) - show-measures? - (filter #(group-empty-space? % objects ids)) + root-frame-with-data? + #(as-> (get objects %) obj + (and (cfh/root-frame? obj) + (d/not-empty? (:shapes obj)) + (not (ctk/instance-head? obj)) + (not (ctk/main-instance? obj)))) - (not mod?) - (filter #(or (root-frame-with-data? %) - (group-empty-space? % objects ids)))) + ;; Set with the elements to remove from the hover list + remove-hover-xf + (cond + mod? + (filter grouped?) - remove-id? - (into selected-with-parents remove-id-xf ids) + (not mod?) + (let [child-parent? + (into #{} + (comp (remove #(cfh/group-like-shape? objects %)) + (mapcat #(cfh/get-parent-ids objects %))) + ids)] + (filter #(or (root-frame-with-data? %) + (and (contains? #{:group :bool} (dm/get-in objects [% :type])) + (not (contains? child-parent? %))))))) - no-fill-nested-frames? - (fn [id] - (and (cph/frame-shape? objects id) - (not (cph/root-frame? objects id)) - (empty? (dm/get-in objects [id :fills])))) + remove-measure-xf + (cond + mod? + (filter grouped?) - hover-shape - (->> ids - (remove remove-id?) - (remove (partial cph/hidden-parent? objects)) - (remove #(and mod? (no-fill-nested-frames? %))) - (filter #(or (empty? focus) (cp/is-in-focus? objects focus %))) - (first) - (get objects))] + (not mod?) + (let [child-parent? + (into #{} + (comp (remove #(cfh/group-like-shape? objects %)) + (mapcat #(cfh/get-parent-ids objects %))) + ids)] + (filter #(and (contains? #{:group :bool} (dm/get-in objects [% :type])) + (not (contains? child-parent? %)))))) - (reset! hover hover-shape) - (reset! hover-ids ids) - (reset! hover-top-frame-id (ctt/top-nested-frame objects (deref last-point-ref)))))))) + remove-hover? + (into selected-with-parents remove-hover-xf ids) + + remove-measure? + (into selected-with-parents remove-measure-xf ids) + + no-fill-nested-frames? + (fn [id] + (let [shape (get objects id)] + (and (cfh/frame-shape? shape) + (not (cfh/is-direct-child-of-root? shape)) + (empty? (get shape :fills))))) + + hover-shape + (->> ids + (remove remove-hover?) + (remove (partial cfh/hidden-parent? objects)) + (remove #(and mod? (no-fill-nested-frames? %))) + (filter #(or (empty? focus) (cpf/is-in-focus? objects focus %))) + (first) + (get objects)) + + ;; We keep track of a diferent shape for measures + measure-hover-shape + (when show-measures? + (->> ids + (remove remove-measure?) + (remove (partial cfh/hidden-parent? objects)) + (remove #(and mod? (no-fill-nested-frames? %))) + (filter #(or (empty? focus) (cpf/is-in-focus? objects focus %))) + (first) + (get objects)))] + (reset! hover hover-shape) + (reset! measure-hover measure-hover-shape) + (reset! hover-ids ids))) + + (fn [] + ;; Clean the cache + (mf/set-ref-val! sorted-ids-cache {})))))) (defn setup-viewport-modifiers [modifiers objects] @@ -272,8 +382,8 @@ (let [all-frames (mf/use-memo (mf/deps objects) #(ctt/get-root-frames-ids objects)) selected-frames (mf/use-memo (mf/deps selected) #(->> all-frames (filter selected))) - xf-selected-frame (comp (remove cph/root-frame?) - (map #(cph/get-shape-id-root-frame objects %))) + xf-selected-frame (comp (remove cfh/root-frame?) + (map #(cfh/get-shape-id-root-frame objects %))) selected-shapes-frames (mf/use-memo (mf/deps selected) #(into #{} xf-selected-frame selected)) @@ -297,7 +407,7 @@ ;; - If no hovering over any frames we keep the previous active one ;; - Check always that the active frames are inside the vbox - (let [hover-ids? (set (->> @hover-ids (map #(cph/get-shape-id-root-frame objects %)))) + (let [hover-ids? (set (->> @hover-ids (map #(cfh/get-shape-id-root-frame objects %)))) is-active-frame? (fn [id] @@ -328,10 +438,10 @@ ;; Debug only: Disable the thumbnails new-active-frames (cond - (debug? :disable-frame-thumbnails) + (dbg/enabled? :disable-frame-thumbnails) (into #{} all-frames) - (debug? :force-frame-thumbnails) + (dbg/enabled? :force-frame-thumbnails) #{} :else @@ -344,15 +454,18 @@ ;; this shortcuts outside the viewport? (defn setup-shortcuts - [path-editing? drawing-path? text-editing?] + [path-editing? drawing-path? text-editing? grid-editing?] (hooks/use-shortcuts ::workspace wsc/shortcuts) (mf/use-effect - (mf/deps path-editing? drawing-path?) + (mf/deps path-editing? drawing-path? grid-editing?) (fn [] (cond + grid-editing? + (do (st/emit! (dsc/push-shortcuts ::grid gsc/shortcuts)) + #(st/emit! (dsc/pop-shortcuts ::grid))) (or drawing-path? path-editing?) (do (st/emit! (dsc/push-shortcuts ::path psc/shortcuts)) #(st/emit! (dsc/pop-shortcuts ::path))) text-editing? - (do (st/emit! (dsc/push-shortcuts ::text tsc/shortcuts)) - #(st/emit! (dsc/pop-shortcuts ::text))))))) + (do (st/emit! (dsc/push-shortcuts ::text tsc/shortcuts)) + #(st/emit! (dsc/pop-shortcuts ::text))))))) diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index 6b4ba2ad44..4f46f4133b 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -9,9 +9,9 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.pages.helpers :as cph] [app.common.types.shape.interactions :as ctsi] [app.main.data.workspace :as dw] [app.main.refs :as refs] @@ -55,11 +55,11 @@ dest-x-center (+ dest-x-left (/ (:width dest-rect) 2)) orig-pos (if (<= orig-x-right dest-x-left) :right - (if (>= orig-x-left dest-x-right) :left - (if (<= orig-x-center dest-x-center) :left :right))) + (if (>= orig-x-left dest-x-right) :left + (if (<= orig-x-center dest-x-center) :left :right))) dest-pos (if (<= orig-x-right dest-x-left) :left - (if (>= orig-x-left dest-x-right) :right - (if (<= orig-x-center dest-x-center) :left :right))) + (if (>= orig-x-left dest-x-right) :right + (if (<= orig-x-center dest-x-center) :left :right))) orig-x (if (= orig-pos :right) orig-x-right orig-x-left) dest-x (if (= dest-pos :right) dest-x-right dest-x-left) @@ -84,11 +84,11 @@ dest-y (:y dest-point) orig-pos (if (<= orig-x-right dest-x) :right - (if (>= orig-x-left dest-x) :left - (if (<= orig-x-center dest-x) :right :left))) + (if (>= orig-x-left dest-x) :left + (if (<= orig-x-center dest-x) :right :left))) dest-pos (if (<= orig-x-right dest-x) :left - (if (>= orig-x-left dest-x) :right - (if (<= orig-x-center dest-x) :right :left))) + (if (>= orig-x-left dest-x) :right + (if (<= orig-x-center dest-x) :right :left))) orig-x (if (= orig-pos :right) orig-x-right orig-x-left) orig-y (+ (:y orig-rect) (/ (:height orig-rect) 2))] @@ -121,21 +121,21 @@ nil) inv-zoom (/ 1 zoom)] [:* - [:circle {:cx 0 - :cy 0 - :r (if (some? action-type) 11 4) - :fill stroke - :transform (str - "scale(" inv-zoom ", " inv-zoom ") " - "translate(" (* zoom x) ", " (* zoom y) ")")}] - (when icon-pdata - [:path {:fill stroke - :stroke-width 2 - :stroke "var(--color-white)" - :d icon-pdata - :transform (str - "scale(" inv-zoom ", " inv-zoom ") " - "translate(" (* zoom x) ", " (* zoom y) ")")}])])) + [:circle {:cx 0 + :cy 0 + :r (if (some? action-type) 11 4) + :fill stroke + :transform (str + "scale(" inv-zoom ", " inv-zoom ") " + "translate(" (* zoom x) ", " (* zoom y) ")")}] + (when icon-pdata + [:path {:fill stroke + :stroke-width 2 + :stroke "var(--app-white)" + :d icon-pdata + :transform (str + "scale(" inv-zoom ", " inv-zoom ") " + "translate(" (* zoom x) ", " (* zoom y) ")")}])])) (mf/defc interaction-path @@ -164,7 +164,7 @@ (if-not selected? [:g {:on-pointer-down #(on-pointer-down % index orig-shape)} - [:path {:stroke "var(--color-gray-20)" + [:path {:stroke "var(--df-secondary)" :fill "none" :pointer-events "visible" :stroke-width (/ 2 zoom) @@ -173,13 +173,13 @@ [:& interaction-marker {:index index :x dest-x :y dest-y - :stroke "var(--color-gray-20)" + :stroke "var(--df-secondary)" :action-type action-type :arrow-dir arrow-dir :zoom zoom}])] [:g {:on-pointer-down #(on-pointer-down % index orig-shape)} - [:path {:stroke "var(--color-primary)" + [:path {:stroke "var(--color-accent-tertiary)" :fill "none" :pointer-events "visible" :stroke-width (/ 2 zoom) @@ -188,17 +188,17 @@ (when dest-shape [:& outline {:zoom zoom :shape dest-shape - :color "var(--color-primary)"}]) + :color "var(--color-accent-tertiary)"}]) [:& interaction-marker {:index index :x orig-x :y orig-y - :stroke "var(--color-primary)" + :stroke "var(--color-accent-tertiary)" :zoom zoom}] [:& interaction-marker {:index index :x dest-x :y dest-y - :stroke "var(--color-primary)" + :stroke "var(--color-accent-tertiary)" :action-type action-type :arrow-dir arrow-dir :zoom zoom}]]))) @@ -212,7 +212,7 @@ [:g {:on-pointer-down #(on-pointer-down % index shape)} [:& interaction-marker {:x handle-x :y handle-y - :stroke "var(--color-primary)" + :stroke "var(--color-accent-tertiary)" :action-type :navigate :arrow-dir :right :zoom zoom}]])) @@ -225,7 +225,7 @@ (st/emit! (dw/start-move-overlay-pos index)))] (when dest-shape - (let [orig-frame (cph/get-frame objects orig-shape) + (let [orig-frame (cfh/get-frame objects orig-shape) marker-x (+ (:x orig-frame) (:x position)) marker-y (+ (:y orig-frame) (:y position)) width (:width dest-shape) @@ -240,7 +240,10 @@ dest-shape-id (:id dest-shape) - thumbnail-data-ref (mf/use-memo (mf/deps page-id dest-shape-id) #(refs/thumbnail-frame-data page-id dest-shape-id)) + ;; FIXME: broken + thumbnail-data-ref (mf/use-memo + (mf/deps page-id dest-shape-id) + #(refs/workspace-thumbnail-by-id dest-shape-id)) thumbnail-data (mf/deref thumbnail-data-ref) dest-shape (cond-> dest-shape @@ -249,12 +252,12 @@ [:g {:on-pointer-down start-move-position :on-pointer-enter #(reset! hover-disabled? true) :on-pointer-leave #(reset! hover-disabled? false)} - [:g {:transform (gmt/translate-matrix (gpt/point (- marker-x dest-x) (- marker-y dest-y))) } + [:g {:transform (gmt/translate-matrix (gpt/point (- marker-x dest-x) (- marker-y dest-y)))} [:& (mf/provider muc/render-thumbnails) {:value true} [:& (mf/provider embed/context) {:value false} [:& shape-wrapper {:shape dest-shape}]]]] - [:path {:stroke "var(--color-primary)" - :fill "var(--color-black)" + [:path {:stroke "var(--color-accent-tertiary)" + :fill "var(--app-black)" :fill-opacity 0.5 :stroke-width 1 :d (dm/str "M" marker-x " " marker-y " " @@ -268,7 +271,7 @@ [:circle {:cx (+ marker-x (/ width 2)) :cy (+ marker-y (/ height 2)) :r 8 - :fill "var(--color-primary)"}]])))) + :fill "var(--color-accent-tertiary)"}]])))) (mf/defc interactions [{:keys [current-transform objects zoom selected hover-disabled? page-id] :as props}] @@ -358,7 +361,7 @@ :objects objects :hover-disabled? hover-disabled?}]))]))) (when (and shape - (not (cph/unframed-shape? shape)) + (not (cfh/unframed-shape? shape)) (not (#{:move :rotate} current-transform))) [:& interaction-handle {:key (:id shape) :index nil diff --git a/frontend/src/app/main/ui/workspace/viewport/outline.cljs b/frontend/src/app/main/ui/workspace/viewport/outline.cljs index 9cad0c7326..176128102c 100644 --- a/frontend/src/app/main/ui/workspace/viewport/outline.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/outline.cljs @@ -7,7 +7,9 @@ (ns app.main.ui.workspace.viewport.outline (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] [app.common.types.container :as ctn] [app.main.refs :as refs] @@ -16,83 +18,94 @@ [app.util.object :as obj] [app.util.path.format :as upf] [clojure.set :as set] - [rumext.v2 :as mf] - [rumext.v2.util :refer [map->obj]])) + [rumext.v2 :as mf])) (mf/defc outline {::mf/wrap-props false} [props] - (let [shape (obj/get props "shape") - zoom (obj/get props "zoom" 1) - modifier (obj/get props "modifier") + (let [shape (unchecked-get props "shape") + modifier (unchecked-get props "modifier") - shape (gsh/transform-shape shape (:modifiers modifier)) + zoom (d/nilv (unchecked-get props "zoom") 1) + shape (gsh/transform-shape shape (:modifiers modifier)) transform (gsh/transform-str shape) - path? (= :path (:type shape)) - path-data - (mf/use-memo - (mf/deps shape) - #(when path? - (or (ex/ignoring (upf/format-path (:content shape))) - ""))) - - ;; Note that we don't use mf/deref to avoid a repaint dependency here - objects (deref refs/workspace-page-objects) + ;; NOTE: that we don't use mf/deref to avoid a repaint dependency here + objects (deref refs/workspace-page-objects) color (if (ctn/in-any-component? objects shape) "var(--color-component-highlight)" - "var(--color-primary)") + "var(--color-accent-tertiary)") - {:keys [x y width height selrect]} shape + x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + width (dm/get-prop shape :width) + height (dm/get-prop shape :height) + selrect (dm/get-prop shape :selrect) + type (dm/get-prop shape :type) + content (get shape :content) + path? (cfh/path-shape? shape) - border-radius-attrs (attrs/extract-border-radius shape) + path-data + (mf/with-memo [path? content] + (when (and ^boolean path? (some? content)) + (d/nilv (ex/ignoring (upf/format-path content)) ""))) - path? (some? (.-d border-radius-attrs)) + border-attrs + (attrs/get-border-props shape) - outline-type (case (:type shape) - :circle "ellipse" - :path "path" - (if path? "path" "rect")) + outline-type + (case type + :circle "ellipse" + :path "path" + (if (some? (obj/get border-attrs "d")) + "path" + "rect")) - common {:fill "none" - :stroke color - :strokeWidth (/ 2 zoom) - :pointerEvents "none" - :transform transform} + props + (obj/merge! + #js {:fill "none" + :stroke color + :strokeWidth (/ 1 zoom) + :pointerEvents "none" + :transform transform} - props (case (:type shape) - :circle - {:cx (+ x (/ width 2)) - :cy (+ y (/ height 2)) - :rx (/ width 2) - :ry (/ height 2)} + (case type + :circle + #js {:cx (+ x (/ width 2)) + :cy (+ y (/ height 2)) + :rx (/ width 2) + :ry (/ height 2)} - :path - {:d path-data - :transform nil} + :path + #js {:d path-data + :transform nil} - {:x (:x selrect) - :y (:y selrect) - :width (:width selrect) - :height (:height selrect) - :rx (.-rx border-radius-attrs) - :ry (.-ry border-radius-attrs) - :d (.-d border-radius-attrs)})] + (let [x (dm/get-prop selrect :x) + y (dm/get-prop selrect :y) + w (dm/get-prop selrect :width) + h (dm/get-prop selrect :height)] + #js {:x x + :y y + :width w + :height h + :rx (obj/get border-attrs "rx") + :ry (obj/get border-attrs "ry") + :d (obj/get border-attrs "d")})))] - [:> outline-type (map->obj (merge common props))])) + [:> outline-type props])) (mf/defc shape-outlines-render {::mf/wrap-props false ::mf/wrap [#(mf/memo' % (mf/check-props ["shapes" "zoom" "modifiers"]))]} [props] - - (let [shapes (obj/get props "shapes") - zoom (obj/get props "zoom") - modifiers (obj/get props "modifiers")] + (let [shapes (unchecked-get props "shapes") + zoom (unchecked-get props "zoom") + modifiers (unchecked-get props "modifiers")] (for [shape shapes] - (let [modifier (get modifiers (:id shape))] - [:& outline {:key (str "outline-" (:id shape)) + (let [shape-id (dm/get-prop shape :id) + modifier (get modifiers shape-id)] + [:& outline {:key (dm/str "outline-" shape-id) :shape shape :modifier modifier :zoom zoom}])))) @@ -100,7 +113,8 @@ (defn- show-outline? [shape] (and (not (:hidden shape)) - (not (:blocked shape)))) + (not (:blocked shape)) + (not (:transforming shape)))) (mf/defc shape-outlines {::mf/wrap-props false} diff --git a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs index d7eaef1535..0da6131055 100644 --- a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.viewport.path-actions + (:require-macros [app.main.style :as stl]) (:require [app.main.data.workspace.path :as drp] [app.main.data.workspace.path.shortcuts :as sc] @@ -15,6 +16,38 @@ [app.util.path.tools :as upt] [rumext.v2 :as mf])) + +(def ^:private pentool-icon + (i/icon-xref :pentool (stl/css :pentool-icon :pathbar-icon))) + +(def ^:private move-icon + (i/icon-xref :move (stl/css :move-icon :pathbar-icon))) + +(def ^:private add-icon + (i/icon-xref :add (stl/css :add-icon :pathbar-icon))) + +(def ^:private remove-icon + (i/icon-xref :remove (stl/css :remove :pathbar-icon))) + +(def ^:private merge-nodes-icon + (i/icon-xref :merge-nodes (stl/css :merge-nodes-icon :pathbar-icon))) + +(def ^:private join-nodes-icon + (i/icon-xref :join-nodes (stl/css :join-nodes-icon :pathbar-icon))) + +(def ^:private separate-nodes-icon + (i/icon-xref :separate-nodes (stl/css :separate-nodes-icon :pathbar-icon))) + +(def ^:private to-corner-icon + (i/icon-xref :to-corner (stl/css :to-corner-icon :pathbar-icon))) + +(def ^:private to-curve-icon + (i/icon-xref :to-curve (stl/css :to-curve-icon :pathbar-icon))) + +(def ^:private snap-nodes-icon + (i/icon-xref :snap-nodes (stl/css :snap-nodes-icon :pathbar-icon))) + + (defn check-enabled [content selected-points] (let [segments (upt/get-segments content selected-points) num-segments (count segments) @@ -35,6 +68,7 @@ :join-nodes (and points-selected? (>= num-points 2) (< num-segments max-segments)) :separate-nodes segments-selected?})) + (mf/defc path-actions [{:keys [shape]}] (let [{:keys [edit-mode selected-points snap-toggled] :as all} (mf/deref pc/current-edit-path-ref) content (:content shape) @@ -107,79 +141,80 @@ (mf/use-callback (fn [_] (st/emit! (drp/toggle-snap))))] - [:div.path-actions - [:div.viewport-actions-group + + [:div {:class (stl/css :sub-actions)} + [:div {:class (stl/css :sub-actions-group)} ;; Draw Mode - [:div.viewport-actions-entry.tooltip.tooltip-bottom - {:class (when (= edit-mode :draw) "is-toggled") - :alt (tr "workspace.path.actions.draw-nodes" (sc/get-tooltip :draw-nodes)) - :on-click on-select-draw-mode} - i/pen] + [:button {:class (stl/css-case :is-toggled (= edit-mode :draw) + :topbar-btn true) + :title (tr "workspace.path.actions.draw-nodes" (sc/get-tooltip :draw-nodes)) + :on-click on-select-draw-mode} + pentool-icon] - ;; Edit mode - [:div.viewport-actions-entry.tooltip.tooltip-bottom - {:class (when (= edit-mode :move) "is-toggled") - :alt (tr "workspace.path.actions.move-nodes" (sc/get-tooltip :move-nodes)) - :on-click on-select-edit-mode} - i/pointer-inner]] + ;; Edit mode + [:button {:class (stl/css-case :is-toggled (= edit-mode :move) + :topbar-btn true) + :title (tr "workspace.path.actions.move-nodes" (sc/get-tooltip :move-nodes)) + :on-click on-select-edit-mode} + move-icon]] - [:div.viewport-actions-group + [:div {:class (stl/css :sub-actions-group)} ;; Add Node - [:div.viewport-actions-entry.tooltip.tooltip-bottom - {:class (when-not (:add-node enabled-buttons) "is-disabled") - :alt (tr "workspace.path.actions.add-node" (sc/get-tooltip :add-node)) - :on-click on-add-node} - i/nodes-add] + [:button {:disabled (not (:add-node enabled-buttons)) + :class (stl/css :topbar-btn) + :title (tr "workspace.path.actions.add-node" (sc/get-tooltip :add-node)) + :on-click on-add-node} + add-icon] ;; Remove node - [:div.viewport-actions-entry.tooltip.tooltip-bottom - {:class (when-not (:remove-node enabled-buttons) "is-disabled") - :alt (tr "workspace.path.actions.delete-node" (sc/get-tooltip :delete-node)) - :on-click on-remove-node} - i/nodes-remove]] + [:button {:disabled (not (:remove-node enabled-buttons)) + :class (stl/css :topbar-btn) + :title (tr "workspace.path.actions.delete-node" (sc/get-tooltip :delete-node)) + :on-click on-remove-node} + remove-icon]] - [:div.viewport-actions-group + [:div {:class (stl/css :sub-actions-group)} ;; Merge Nodes - [:div.viewport-actions-entry.tooltip.tooltip-bottom - {:class (when-not (:merge-nodes enabled-buttons) "is-disabled") - :alt (tr "workspace.path.actions.merge-nodes" (sc/get-tooltip :merge-nodes)) - :on-click on-merge-nodes} - i/nodes-merge] + [:button {:disabled (not (:merge-nodes enabled-buttons)) + :class (stl/css :topbar-btn) + :title (tr "workspace.path.actions.merge-nodes" (sc/get-tooltip :merge-nodes)) + :on-click on-merge-nodes} + merge-nodes-icon] ;; Join Nodes - [:div.viewport-actions-entry.tooltip.tooltip-bottom - {:class (when-not (:join-nodes enabled-buttons) "is-disabled") - :alt (tr "workspace.path.actions.join-nodes" (sc/get-tooltip :join-nodes)) - :on-click on-join-nodes} - i/nodes-join] + [:button {:disabled (not (:join-nodes enabled-buttons)) + :class (stl/css :topbar-btn) + :title (tr "workspace.path.actions.join-nodes" (sc/get-tooltip :join-nodes)) + :on-click on-join-nodes} + join-nodes-icon] ;; Separate Nodes - [:div.viewport-actions-entry.tooltip.tooltip-bottom - {:class (when-not (:separate-nodes enabled-buttons) "is-disabled") - :alt (tr "workspace.path.actions.separate-nodes" (sc/get-tooltip :separate-nodes)) - :on-click on-separate-nodes} - i/nodes-separate]] + [:button {:disabled (not (:separate-nodes enabled-buttons)) + :class (stl/css :topbar-btn) + :title (tr "workspace.path.actions.separate-nodes" (sc/get-tooltip :separate-nodes)) + :on-click on-separate-nodes} + separate-nodes-icon]] - ;; Make Corner - [:div.viewport-actions-group - [:div.viewport-actions-entry.tooltip.tooltip-bottom - {:class (when-not (:make-corner enabled-buttons) "is-disabled") - :alt (tr "workspace.path.actions.make-corner" (sc/get-tooltip :make-corner)) - :on-click on-make-corner} - i/nodes-corner] + [:div {:class (stl/css :sub-actions-group)} + ; Make Corner + [:button {:disabled (not (:make-corner enabled-buttons)) + :class (stl/css :topbar-btn) + :title (tr "workspace.path.actions.make-corner" (sc/get-tooltip :make-corner)) + :on-click on-make-corner} + to-corner-icon] ;; Make Curve - [:div.viewport-actions-entry.tooltip.tooltip-bottom - {:class (when-not (:make-curve enabled-buttons) "is-disabled") - :alt (tr "workspace.path.actions.make-curve" (sc/get-tooltip :make-curve)) - :on-click on-make-curve} - i/nodes-curve]] + [:button {:disabled (not (:make-curve enabled-buttons)) + :class (stl/css :topbar-btn) + :title (tr "workspace.path.actions.make-curve" (sc/get-tooltip :make-curve)) + :on-click on-make-curve} + to-curve-icon]] + [:div {:class (stl/css :sub-actions-group)} + ;; Toggle snap + [:button {:class (stl/css-case :is-toggled snap-toggled + :topbar-btn true) + :title (tr "workspace.path.actions.snap-nodes" (sc/get-tooltip :snap-nodes)) + :on-click on-toggle-snap} + snap-nodes-icon]]])) - ;; Toggle snap - [:div.viewport-actions-group - [:div.viewport-actions-entry.tooltip.tooltip-bottom - {:class (when snap-toggled "is-toggled") - :alt (tr "workspace.path.actions.snap-nodes" (sc/get-tooltip :snap-nodes)) - :on-click on-toggle-snap} - i/nodes-snap]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/path_actions.scss b/frontend/src/app/main/ui/workspace/viewport/path_actions.scss new file mode 100644 index 0000000000..c5e5ecc1b2 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/path_actions.scss @@ -0,0 +1,60 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.sub-actions { + cursor: initial; + pointer-events: initial; + position: absolute; + top: $s-12; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + height: $s-56; + padding: $s-8 $s-16; + border-radius: $s-8; + gap: $s-16; + border: $s-2 solid var(--panel-border-color); + z-index: $z-index-3; + background-color: var(--color-background-primary); + transition: + top 0.3s, + height 0.3s, + opacity 0.3s; +} + +.sub-actions-group { + position: relative; + display: flex; + align-items: center; + margin: 0; + opacity: $op-10; + transition: opacity 0.3s ease; +} + +.topbar-btn { + --pathbar-icon-color: var(--color-foreground-secondary); + @extend .button-tertiary; + height: $s-36; + width: $s-36; + flex-shrink: 0; + background-color: transparent; + border-radius: $s-8; + border: none; + margin: 0 $s-2; + + &.is-toggled { + --pathbar-icon-color: var(--button-radio-foreground-color-active); + background-color: var(--button-radio-background-color-active); + } + + .pathbar-icon { + @extend .button-icon; + stroke: var(--pathbar-icon-color); + } +} diff --git a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs index 8c133aeeca..b600551632 100644 --- a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs @@ -6,104 +6,62 @@ (ns app.main.ui.workspace.viewport.pixel-overlay (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.pages.helpers :as cph] - [app.common.uuid :as uuid] + [app.common.math :as mth] + [app.config :as cfg] [app.main.data.modal :as modal] [app.main.data.workspace.colors :as dwc] [app.main.data.workspace.undo :as dwu] - [app.main.refs :as refs] + [app.main.fonts :as fonts] + [app.main.rasterizer :as thr] [app.main.store :as st] [app.main.ui.css-cursors :as cur] - [app.main.ui.workspace.shapes :as shapes] [app.util.dom :as dom] - [app.util.http :as http] [app.util.keyboard :as kbd] [app.util.object :as obj] - [app.util.webapi :as wapi] - [beicon.core :as rx] - [cuerdas.core :as str] + [beicon.v2.core :as rx] [goog.events :as events] - [promesa.core :as p] [rumext.v2 :as mf]) (:import goog.events.EventType)) -(defn- resolve-svg-images! - [svg-node] - (let [image-nodes (dom/query-all svg-node "image:not([href^=data])") - noop-fn (constantly nil)] - (if (empty? image-nodes) - (rx/of svg-node) - (->> (rx/from image-nodes) - (rx/mapcat - (fn [image] - (let [href (dom/get-attribute image "href")] - (->> (http/fetch {:method :get :uri href}) - (rx/mapcat (fn [response] (.blob ^js response))) - (rx/mapcat wapi/read-file-as-data-url) - (rx/tap (fn [data] - (dom/set-attribute! image "href" data))) - (rx/reduce noop-fn))))) - (rx/map (fn [_] svg-node)))))) +(defn create-offscreen-canvas + [width height] + (js/OffscreenCanvas. width height)) -(defn- svg-as-data-url - "Transforms SVG as data-url resolving any blob, http or https url to - its data equivalent." - [svg] - (let [svg-clone (.cloneNode svg true)] - (->> (resolve-svg-images! svg-clone) - (rx/mapcat (fn [svg-node] - (let [xml (js/XMLSerializer.) - xmlstr (.serializeToString xml svg-node)] - (->> (rx/of xmlstr) - (rx/map #(dm/str "data:image/svg+xml;charset=utf-8," (js/encodeURIComponent %)))))))))) +(defn resize-offscreen-canvas + [canvas width height] + (let [resized (volatile! false)] + (when-not (= (unchecked-get canvas "width") width) + (obj/set! canvas "width" width) + (vreset! resized true)) + (when-not (= (unchecked-get canvas "height") height) + (obj/set! canvas "height" height) + (vreset! resized true)) + canvas)) -(defn format-viewbox [vbox] - (str/join " " [(:x vbox 0) - (:y vbox 0) - (:width vbox 0) - (:height vbox 0)])) - -(mf/defc overlay-frames - {::mf/wrap [mf/memo] - ::mf/wrap-props false} - [] - (let [data (mf/deref refs/workspace-page) - objects (:objects data) - root (get objects uuid/zero) - shapes (->> (:shapes root) - (map (d/getf objects)))] - [:g.shapes - (for [shape shapes] - (cond - (not (cph/frame-shape? shape)) - [:& shapes/shape-wrapper - {:shape shape - :key (:id shape)}] - - (cph/root-frame? shape) - [:& shapes/root-frame-wrapper - {:shape shape - :key (:id shape) - :objects objects}] - - :else - [:& shapes/nested-frame-wrapper - {:shape shape - :key (:id shape) - :objects objects}]))])) +(def get-offscreen-canvas ((fn [] + (let [internal-state #js {:canvas nil}] + (fn [width height] + (let [canvas (unchecked-get internal-state "canvas")] + (if canvas + (resize-offscreen-canvas canvas width height) + (let [new-canvas (create-offscreen-canvas width height)] + (obj/set! internal-state "canvas" new-canvas) + new-canvas)))))))) (mf/defc pixel-overlay {::mf/wrap-props false} [props] - (let [vport (unchecked-get props "vport") - viewport-ref (unchecked-get props "viewport-ref") - viewport-node (mf/ref-val viewport-ref) - canvas-ref (mf/use-ref nil) - img-ref (mf/use-ref nil) + (let [vport (unchecked-get props "vport") - update-str (rx/subject) + viewport-ref (unchecked-get props "viewport-ref") + viewport-node (mf/ref-val viewport-ref) + + canvas (get-offscreen-canvas (:width vport) (:height vport)) + canvas-context (.getContext canvas "2d" #js {:willReadFrequently true}) + canvas-image-data (mf/use-ref nil) + zoom-view-context (mf/use-ref nil) + + update-str (rx/subject) handle-keydown (mf/use-callback @@ -118,29 +76,44 @@ (mf/use-callback (mf/deps viewport-node) (fn [event] - (when-let [zoom-view-node (.getElementById js/document "picker-detail")] - (let [canvas-node (mf/ref-val canvas-ref) + (when-let [image-data (mf/ref-val canvas-image-data)] + (when-let [zoom-view-node (dom/get-element "picker-detail")] + (when-not (mf/ref-val zoom-view-context) + (mf/set-ref-val! zoom-view-context (.getContext zoom-view-node "2d"))) + (let [canvas-width 260 + canvas-height 140 + {brx :left bry :top} (dom/get-bounding-rect viewport-node) - {brx :left bry :top} (dom/get-bounding-rect viewport-node) - x (- (.-clientX event) brx) - y (- (.-clientY event) bry) + x (mth/floor (- (.-clientX event) brx)) + y (mth/floor (- (.-clientY event) bry)) - zoom-context (.getContext zoom-view-node "2d" #js {:willReadFrequently true}) - canvas-context (.getContext canvas-node "2d" #js {:willReadFrequently true}) - pixel-data (.getImageData canvas-context x y 1 1) - rgba (.-data pixel-data) - r (obj/get rgba 0) - g (obj/get rgba 1) - b (obj/get rgba 2) - a (obj/get rgba 3) - area-data (.getImageData canvas-context (- x 25) (- y 20) 50 40)] - (-> (js/createImageBitmap area-data) - (p/then - (fn [image] - ;; Draw area - (obj/set! zoom-context "imageSmoothingEnabled" false) - (.drawImage zoom-context image 0 0 200 160)))) - (st/emit! (dwc/pick-color [r g b a])))))) + zoom-context (mf/ref-val zoom-view-context) + + offset (* (+ (* y (unchecked-get image-data "width")) x) 4) + rgba (unchecked-get image-data "data") + + r (obj/get rgba (+ 0 offset)) + g (obj/get rgba (+ 1 offset)) + b (obj/get rgba (+ 2 offset)) + a (obj/get rgba (+ 3 offset)) + + ;; I don't know why, but the zoom view is offset by 24px + ;; instead of 25. + sx (- x 32) + + ;; Safari has a different offset fro the y coord + sy (if (cfg/check-browser? :safari) y (- y 17)) + sw 65 + sh 35 + dx 0 + dy 0 + dw canvas-width + dh canvas-height] + (when (obj/get zoom-context "imageSmoothingEnabled") + (obj/set! zoom-context "imageSmoothingEnabled" false)) + (.clearRect zoom-context 0 0 canvas-width canvas-height) + (.drawImage zoom-context canvas sx sy sw sh dx dy dw dh) + (st/emit! (dwc/pick-color [r g b a]))))))) handle-pointer-down-picker (mf/use-callback @@ -159,30 +132,34 @@ (dwc/stop-picker)) (modal/disallow-click-outside!))) - handle-image-load - (mf/use-callback - (mf/deps img-ref) - (fn [] - (let [canvas-node (mf/ref-val canvas-ref) - img-node (mf/ref-val img-ref) - canvas-context (.getContext canvas-node "2d")] - (.drawImage canvas-context img-node 0 0)))) - handle-draw-picker-canvas (mf/use-callback - (mf/deps img-ref) (fn [] - (let [img-node (mf/ref-val img-ref) - svg-node (dom/get-element "render")] - (->> (svg-as-data-url svg-node) - (rx/subs (fn [uri] - (obj/set! img-node "src" uri))))))) + (let [svg-node (dom/get-element "render") + fonts (fonts/get-node-fonts svg-node) + result {:node svg-node + :width (:width vport) + :result "image-bitmap"}] + (->> (fonts/render-font-styles-cached fonts) + (rx/map (fn [styles] + (assoc result + :styles styles))) + (rx/mapcat thr/render-node) + (rx/subs! (fn [image-bitmap] + (.drawImage canvas-context image-bitmap 0 0) + (let [width (unchecked-get canvas "width") + height (unchecked-get canvas "height") + image-data (.getImageData canvas-context 0 0 width height)] + (mf/set-ref-val! canvas-image-data image-data)))))))) handle-svg-change (mf/use-callback (fn [] (rx/push! update-str :update)))] + (when (obj/get canvas-context "imageSmoothingEnabled") + (obj/set! canvas-context "imageSmoothingEnabled" false)) + (mf/use-effect (fn [] (let [listener (events/listen js/document EventType.KEYDOWN handle-keydown)] @@ -192,7 +169,7 @@ (fn [] (let [sub (->> update-str (rx/debounce 10) - (rx/subs handle-draw-picker-canvas))] + (rx/subs! handle-draw-picker-canvas))] #(rx/dispose! sub)))) (mf/use-effect @@ -202,8 +179,7 @@ :subtree true :characterData true} svg-node (dom/get-element "render") - observer (js/MutationObserver. handle-svg-change) - ] + observer (js/MutationObserver. handle-svg-change)] (.observe observer svg-node config) (handle-svg-change) @@ -217,16 +193,4 @@ :class (cur/get-static "picker") :on-pointer-down handle-pointer-down-picker :on-pointer-up handle-pointer-up-picker - :on-pointer-move handle-pointer-move-picker} - [:div {:style {:display "none"}} - [:img {:ref img-ref - :on-load handle-image-load - :style {:position "absolute" - :width "100%" - :height "100%"}}] - [:canvas {:ref canvas-ref - :width (:width vport 0) - :height (:height vport 0) - :style {:position "absolute" - :width "100%" - :height "100%"}}]]]])) + :on-pointer-move handle-pointer-move-picker}]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/presence.cljs b/frontend/src/app/main/ui/workspace/viewport/presence.cljs index 0aa64ade46..e5d019464f 100644 --- a/frontend/src/app/main/ui/workspace/viewport/presence.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/presence.cljs @@ -6,63 +6,74 @@ (ns app.main.ui.workspace.viewport.presence (:require + [app.common.data.macros :as dm] [app.main.refs :as refs] [app.util.time :as dt] [app.util.timers :as ts] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] [rumext.v2 :as mf])) -(def pointer-icon-path - (str "M11.58,-0.47L11.47,-0.35L0.34,10.77L0.30,10.96L-0.46," - "15.52L4.29,14.72L15.53,3.47L11.58,-0.47ZL11.58," - "-0.47ZL11.58,-0.47ZM11.58,1.3C12.31,2.05,13.02," - "2.742,13.76,3.47L4.0053,13.23C3.27,12.50,2.55," - "11.78,1.82,11.05L11.58,1.30ZL11.58,1.30ZM1.37,12.15L2.90," - "13.68L1.67,13.89L1.165,13.39L1.37,12.15ZL1.37,12.15Z")) +(def pointer-path + (dm/str "M11.58,-0.47L11.47,-0.35L0.34,10.77L0.30,10.96L-0.46," + "15.52L4.29,14.72L15.53,3.47L11.58,-0.47ZL11.58," + "-0.47ZL11.58,-0.47ZM11.58,1.3C12.31,2.05,13.02," + "2.742,13.76,3.47L4.0053,13.23C3.27,12.50,2.55," + "11.78,1.82,11.05L11.58,1.30ZL11.58,1.30ZM1.37,12.15L2.90," + "13.68L1.67,13.89L1.165,13.39L1.37,12.15ZL1.37,12.15Z")) (mf/defc session-cursor - [{:keys [session profile] :as props}] - (let [zoom (mf/deref refs/selected-zoom) - point (:point session) - background-color (:color session "var(--color-black)") - text-color (:text-color session "var(--color-white)") - transform (str/fmt "translate(%s, %s) scale(%s)" (:x point) (:y point) (/ 1 zoom)) - shown-name (if (> (count (:fullname profile)) 16) - (str (str/slice (:fullname profile) 0 12) "...") - (:fullname profile))] + {::mf/props :obj + ::mf/memo true} + [{:keys [session profile zoom]}] + (let [point (:point session) + bg-color (:color session) + fg-color "var(--app-white)" + transform (str/ffmt "translate(%, %) scale(%)" + (dm/get-prop point :x) + (dm/get-prop point :y) + (/ 1 zoom)) + + + fullname (:fullname profile) + fullname (if (> (count fullname) 16) + (dm/str (str/slice fullname 0 12) "...") + fullname)] + [:g.multiuser-cursor {:transform transform} - [:path {:fill background-color - :d pointer-icon-path}] + [:path {:fill bg-color :d pointer-path}] [:g {:transform "translate(17 -10)"} [:foreignObject {:x -0.3 :y -12.5 :width 300 :height 120} - [:div.profile-name {:style {:background-color background-color - :color text-color}} - shown-name]]]])) + [:div.profile-name {:style {:background-color bg-color + :color fg-color}} + fullname]]]])) (mf/defc active-cursors - {::mf/wrap [mf/memo]} - [{:keys [page-id] :as props}] + {::mf/props :obj} + [{:keys [page-id]}] (let [counter (mf/use-state 0) users (mf/deref refs/users) sessions (mf/deref refs/workspace-presence) + zoom (mf/deref refs/selected-zoom) + sessions (->> (vals sessions) + (filter :point) (filter #(= page-id (:page-id %))) - (filter #(>= 5000 (- (inst-ms (dt/now)) (inst-ms (:updated-at %))))))] - (mf/use-effect - nil - (fn [] - (let [sem (ts/schedule 1000 #(swap! counter inc))] - (fn [] (rx/dispose! sem))))) + (filter #(>= 5000 (- (inst-ms (dt/now)) + (inst-ms (:updated-at %))))))] + (mf/with-effect nil + (let [sem (ts/schedule 1000 #(swap! counter inc))] + (fn [] (rx/dispose! sem)))) (for [session sessions] - (when (:point session) - [:& session-cursor {:session session - :profile (get users (:profile-id session)) - :key (:id session)}])))) + [:& session-cursor + {:session session + :zoom zoom + :profile (get users (:profile-id session)) + :key (dm/str (:id session))}]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/rulers.cljs b/frontend/src/app/main/ui/workspace/viewport/rulers.cljs new file mode 100644 index 0000000000..bec8eaf330 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/rulers.cljs @@ -0,0 +1,350 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.viewport.rulers + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.geom.shapes :as gsh] + [app.common.math :as mth] + [app.main.ui.formats :as fmt] + [app.main.ui.hooks :as hooks] + [app.util.object :as obj] + [rumext.v2 :as mf])) + +(def rulers-pos 15) +(def rulers-size 4) +(def rulers-width 1) +(def ruler-area-size 22) +(def ruler-area-half-size (/ ruler-area-size 2)) +(def rulers-background "var(--panel-background-color)") +(def selection-area-color "var(--color-accent-tertiary)") +(def selection-area-opacity 0.3) +(def over-number-size 100) +(def over-number-opacity 0.8) +(def over-number-percent 0.75) + +(def font-size 12) +(def font-family "worksans") +(def font-color "var(--layer-row-foreground-color)") +(def canvas-border-radius 12) + +;; ---------------- +;; RULERS +;; ---------------- + +(defn- calculate-step-size + [zoom] + (cond + (< 0 zoom 0.008) 10000 + (< 0.008 zoom 0.015) 5000 + (< 0.015 zoom 0.04) 2500 + (< 0.04 zoom 0.07) 1000 + (< 0.07 zoom 0.2) 500 + (< 0.2 zoom 0.5) 250 + (< 0.5 zoom 1) 100 + (<= 1 zoom 2) 50 + (< 2 zoom 4) 25 + (< 4 zoom 6) 10 + (< 6 zoom 15) 5 + (< 15 zoom 25) 2 + (< 25 zoom) 1 + :else 1)) + +(defn get-clip-area + [vbox zoom-inverse axis] + (if (= axis :x) + (let [x (+ (:x vbox) (* 25 zoom-inverse)) + y (:y vbox) + width (- (:width vbox) (* 21 zoom-inverse)) + height (* 25 zoom-inverse)] + {:x x :y y :width width :height height}) + + (let [x (:x vbox) + y (+ (:y vbox) (* 25 zoom-inverse)) + width (* 25 zoom-inverse) + height (- (:height vbox) (* 21 zoom-inverse))] + {:x x :y y :width width :height height}))) + +(defn get-background-area + [vbox zoom-inverse axis] + (if (= axis :x) + (let [x (:x vbox) + y (:y vbox) + width (:width vbox) + height (* ruler-area-size zoom-inverse)] + {:x x :y y :width width :height height}) + + (let [x (:x vbox) + y (+ (:y vbox) (* ruler-area-size zoom-inverse)) + width (* ruler-area-size zoom-inverse) + height (- (:height vbox) (* 21 zoom-inverse))] + {:x x :y y :width width :height height}))) + +(defn get-ruler-params + [vbox axis] + (if (= axis :x) + (let [start (:x vbox) + end (+ start (:width vbox))] + {:start start :end end}) + + (let [start (:y vbox) + end (+ start (:height vbox))] + {:start start :end end}))) + +(defn get-ruler-axis + [val vbox zoom-inverse axis] + (let [rulers-pos (* rulers-pos zoom-inverse) + rulers-size (* rulers-size zoom-inverse)] + (if (= axis :x) + {:text-x val + :text-y (+ (:y vbox) rulers-pos (* -1 zoom-inverse)) + :line-x1 val + :line-y1 (+ (:y vbox) rulers-pos (* 2 zoom-inverse)) + :line-x2 val + :line-y2 (+ (:y vbox) rulers-pos (* 2 zoom-inverse) rulers-size)} + + {:text-x (+ (:x vbox) rulers-pos (* -1 zoom-inverse)) + :text-y val + :line-x1 (+ (:x vbox) rulers-pos (* 2 zoom-inverse)) + :line-y1 val + :line-x2 (+ (:x vbox) rulers-pos (* 2 zoom-inverse) rulers-size) + :line-y2 val}))) + +(defn rulers-outside-path + "Path data for the viewport outside" + [x1 y1 x2 y2] + (dm/str + "M" x1 "," y1 + "L" x2 "," y1 + "L" x2 "," y2 + "L" x1 "," y2 + "Z")) + +(defn rulers-inside-path + "Calculates the path for the inside of the viewport frame" + [x1 y1 x2 y2 br bw] + (dm/str + "M" (+ x1 bw) "," (+ y1 bw br) + "Q" (+ x1 bw) "," (+ y1 bw) "," (+ x1 bw br) "," (+ y1 bw) + + "L" (- x2 br) "," (+ y1 bw) + "Q" x2 "," (+ y1 bw) "," x2 "," (+ y1 bw br) + + "L" x2 "," (- y2 br) + "Q" x2 "," y2 "," (- x2 br) "," y2 + + "L" (+ x1 bw br) "," y2 + "Q" (+ x1 bw) "," y2 "," (+ x1 bw) "," (- y2 br) + + "Z")) + +(mf/defc rulers-text + "Draws the text for the rulers in a specific axis" + [{:keys [vbox step offset axis zoom-inverse]}] + (let [clip-id (str "clip-ruler-" (d/name axis)) + {:keys [start end]} (get-ruler-params vbox axis) + minv (max start -100000) + minv (* (mth/ceil (/ minv step)) step) + maxv (min end 100000) + maxv (* (mth/floor (/ maxv step)) step) + + ;; These extra operations ensure that we are selecting a frame its initial location is rendered in the ruler + minv (+ minv (mod offset step)) + maxv (+ maxv (mod offset step)) + + rulers-width (* rulers-width zoom-inverse)] + + [:g.rulers {:clipPath (str "url(#" clip-id ")")} + [:defs + [:clipPath {:id clip-id} + (let [{:keys [x y width height]} (get-clip-area vbox zoom-inverse axis)] + [:rect {:x x :y y :width width :height height}])]] + + (for [step-val (range minv (inc maxv) step)] + (let [{:keys [text-x text-y line-x1 line-y1 line-x2 line-y2]} + (get-ruler-axis step-val vbox zoom-inverse axis)] + [:* {:key (dm/str "text-" (d/name axis) "-" step-val)} + [:text {:x text-x + :y text-y + :text-anchor "middle" + :transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")")) + :style {:font-size (* font-size zoom-inverse) + :font-family font-family + :fill font-color}} + ;; If the guide is associated to a frame we show the position relative to the frame + (fmt/format-number (- step-val offset))] + + [:line {:key (str "line-" (d/name axis) "-" step-val) + :x1 line-x1 + :y1 line-y1 + :x2 line-x2 + :y2 line-y2 + :style {:stroke font-color + :stroke-width rulers-width}}]]))])) + +(mf/defc viewport-frame + [{:keys [show-rulers? zoom zoom-inverse vbox offset-x offset-y]}] + + (let [{:keys [width height] x1 :x y1 :y} vbox + x2 (+ x1 width) + y2 (+ y1 height) + bw (if show-rulers? (* ruler-area-size zoom-inverse) 0) + br (/ canvas-border-radius zoom) + bs (* 4 zoom-inverse)] + [:* + [:g.viewport-frame-background + ;; Fix for a Firefox bug that shows some strange artifacts when creating shape + [:rect {:x 0 :y 0 :width 1 :height 1 + :fill "none" + :stroke-width 0.1 + :stroke "rgba(0,0,0,0)"}] + + ;; This goes behind because if it goes in front the background bleeds through + [:path {:d (rulers-inside-path x1 y1 x2 y2 br bw) + :fill "none" + :stroke-width bs + :stroke "var(--panel-border-color)"}] + + [:path {:d (dm/str (rulers-outside-path x1 y1 x2 y2) + (rulers-inside-path x1 y1 x2 y2 br bw)) + :fill-rule "evenodd" + :fill rulers-background}]] + + (when show-rulers? + (let [step (calculate-step-size zoom)] + [:g.viewport-frame-rulers + [:& rulers-text {:vbox vbox :offset offset-x :step step :zoom-inverse zoom-inverse :axis :x}] + [:& rulers-text {:vbox vbox :offset offset-y :step step :zoom-inverse zoom-inverse :axis :y}]]))])) + +(mf/defc selection-area + [{:keys [vbox zoom-inverse selection-rect offset-x offset-y]}] + ;; When using the format-number callls we consider if the guide is associated to a frame and we show the position relative to it with the offset + [:g.selection-area + [:defs + [:linearGradient {:id "selection-gradient-start"} + [:stop {:offset "0%" :stop-color rulers-background :stop-opacity 0}] + [:stop {:offset "40%" :stop-color rulers-background :stop-opacity 1}] + [:stop {:offset "100%" :stop-color rulers-background :stop-opacity 1}]] + + [:linearGradient {:id "selection-gradient-end"} + [:stop {:offset "0%" :stop-color rulers-background :stop-opacity 1}] + [:stop {:offset "60%" :stop-color rulers-background :stop-opacity 1}] + [:stop {:offset "100%" :stop-color rulers-background :stop-opacity 0}]]] + [:g + [:rect {:x (- (:x selection-rect) (* (* over-number-size over-number-percent) zoom-inverse)) + :y (:y vbox) + :width (* over-number-size zoom-inverse) + :height (* ruler-area-size zoom-inverse) + :fill "url('#selection-gradient-start')"}] + + [:rect {:x (- (:x2 selection-rect) (* over-number-size (- 1 over-number-percent))) + :y (:y vbox) + :width (* over-number-size zoom-inverse) + :height (* ruler-area-size zoom-inverse) + :fill "url('#selection-gradient-end')"}] + + [:rect {:x (:x selection-rect) + :y (:y vbox) + :width (:width selection-rect) + :height (* ruler-area-size zoom-inverse) + :style {:fill selection-area-color + :fill-opacity selection-area-opacity}}] + + [:text {:x (- (:x1 selection-rect) (* 4 zoom-inverse)) + :y (+ (:y vbox) (* 13.6 zoom-inverse)) + :text-anchor "end" + :style {:font-size (* font-size zoom-inverse) + :font-family font-family + :fill selection-area-color}} + (fmt/format-number (- (:x1 selection-rect) offset-x))] + + [:text {:x (+ (:x2 selection-rect) (* 4 zoom-inverse)) + :y (+ (:y vbox) (* 13.6 zoom-inverse)) + :text-anchor "start" + :style {:font-size (* font-size zoom-inverse) + :font-family font-family + :fill selection-area-color}} + (fmt/format-number (- (:x2 selection-rect) offset-x))]] + + (let [center-x (+ (:x vbox) (* ruler-area-half-size zoom-inverse)) + center-y (- (+ (:y selection-rect) (/ (:height selection-rect) 2)) (* ruler-area-half-size zoom-inverse))] + + [:g {:transform (str "rotate(-90 " center-x "," center-y ")")} + [:rect {:x (- center-x (/ (:height selection-rect) 2) (* ruler-area-half-size zoom-inverse)) + :y (- center-y (* ruler-area-half-size zoom-inverse)) + :width (:height selection-rect) + :height (* ruler-area-size zoom-inverse) + :style {:fill selection-area-color + :fill-opacity selection-area-opacity}}] + + [:rect {:x (- center-x (/ (:height selection-rect) 2) (* ruler-area-half-size zoom-inverse) (* over-number-size zoom-inverse)) + :y (- center-y (* ruler-area-half-size zoom-inverse)) + :width (* over-number-size zoom-inverse) + :height (* ruler-area-size zoom-inverse) + :style {:fill rulers-background + :fill-opacity over-number-opacity}}] + + [:rect {:x (+ (- center-x (/ (:height selection-rect) 2) (* ruler-area-half-size zoom-inverse)) (:height selection-rect)) + :y (- center-y (* ruler-area-half-size zoom-inverse)) + :width (* over-number-size zoom-inverse) + :height (* ruler-area-size zoom-inverse) + :style {:fill rulers-background + :fill-opacity over-number-opacity}}] + + [:text {:x (- center-x (/ (:height selection-rect) 2) (* 15 zoom-inverse)) + :y (+ center-y (* 4 zoom-inverse)) + :text-anchor "end" + :style {:font-size (* font-size zoom-inverse) + :font-family font-family + :fill selection-area-color}} + (fmt/format-number (- (:y2 selection-rect) offset-y))] + + [:text {:x (+ center-x (/ (:height selection-rect) 2)) + :y (+ center-y (* 4 zoom-inverse)) + :text-anchor "start" + :style {:font-size (* font-size zoom-inverse) + :font-family font-family + :fill selection-area-color}} + (fmt/format-number (- (:y1 selection-rect) offset-y))]])]) + +(mf/defc rulers + {::mf/wrap-props false + ::mf/wrap [#(mf/memo' % (mf/check-props ["zoom" "vbox" "selected-shapes" "show-rulers?"]))]} + [props] + (let [zoom (obj/get props "zoom") + zoom-inverse (obj/get props "zoom-inverse") + vbox (obj/get props "vbox") + offset-x (obj/get props "offset-x") + offset-y (obj/get props "offset-y") + selected-shapes (-> (obj/get props "selected-shapes") + (hooks/use-equal-memo)) + show-rulers? (obj/get props "show-rulers?") + + selection-rect + (mf/use-memo + (mf/deps selected-shapes) + #(when (d/not-empty? selected-shapes) + (gsh/shapes->rect selected-shapes)))] + + (when (some? vbox) + [:g.viewport-frame {:pointer-events "none"} + [:& viewport-frame + {:show-rulers? show-rulers? + :zoom zoom + :zoom-inverse zoom-inverse + :vbox vbox + :offset-x offset-x + :offset-y offset-y}] + + (when (and show-rulers? (some? selection-rect)) + [:& selection-area + {:zoom zoom + :zoom-inverse zoom-inverse + :vbox vbox + :selection-rect selection-rect + :offset-x offset-x + :offset-y offset-y}])]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/rules.cljs b/frontend/src/app/main/ui/workspace/viewport/rules.cljs deleted file mode 100644 index 6eca1a7ec8..0000000000 --- a/frontend/src/app/main/ui/workspace/viewport/rules.cljs +++ /dev/null @@ -1,281 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.workspace.viewport.rules - (:require - [app.common.colors :as colors] - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.geom.shapes :as gsh] - [app.common.math :as mth] - [app.main.ui.formats :as fmt] - [app.main.ui.hooks :as hooks] - [app.util.object :as obj] - [rumext.v2 :as mf])) - -(def rules-pos 15) -(def rules-size 4) -(def rules-width 1) -(def rule-area-size 22) -(def rule-area-half-size (/ rule-area-size 2)) -(def rules-background "var(--color-gray-50)") -(def selection-area-color "var(--color-primary)") -(def selection-area-opacity 0.3) -(def over-number-size 50) -(def over-number-opacity 0.7) - -(def font-size 12) -(def font-family "worksans") - -;; ---------------- -;; RULES -;; ---------------- - -(defn- calculate-step-size - [zoom] - (cond - (< 0 zoom 0.008) 10000 - (< 0.008 zoom 0.015) 5000 - (< 0.015 zoom 0.04) 2500 - (< 0.04 zoom 0.07) 1000 - (< 0.07 zoom 0.2) 500 - (< 0.2 zoom 0.5) 250 - (< 0.5 zoom 1) 100 - (<= 1 zoom 2) 50 - (< 2 zoom 4) 25 - (< 4 zoom 6) 10 - (< 6 zoom 15) 5 - (< 15 zoom 25) 2 - (< 25 zoom) 1 - :else 1)) - -(defn get-clip-area - [vbox zoom-inverse axis] - (if (= axis :x) - (let [x (+ (:x vbox) (* 25 zoom-inverse)) - y (:y vbox) - width (- (:width vbox) (* 21 zoom-inverse)) - height (* 25 zoom-inverse)] - {:x x :y y :width width :height height}) - - (let [x (:x vbox) - y (+ (:y vbox) (* 25 zoom-inverse)) - width (* 25 zoom-inverse) - height (- (:height vbox) (* 21 zoom-inverse))] - {:x x :y y :width width :height height}))) - -(defn get-background-area - [vbox zoom-inverse axis] - (if (= axis :x) - (let [x (:x vbox) - y (:y vbox) - width (:width vbox) - height (* rule-area-size zoom-inverse)] - {:x x :y y :width width :height height}) - - (let [x (:x vbox) - y (+ (:y vbox) (* rule-area-size zoom-inverse)) - width (* rule-area-size zoom-inverse) - height (- (:height vbox) (* 21 zoom-inverse))] - {:x x :y y :width width :height height}))) - -(defn get-rule-params - [vbox axis] - (if (= axis :x) - (let [start (:x vbox) - end (+ start (:width vbox))] - {:start start :end end}) - - (let [start (:y vbox) - end (+ start (:height vbox))] - {:start start :end end}))) - -(defn get-rule-axis - [val vbox zoom-inverse axis] - (let [rules-pos (* rules-pos zoom-inverse) - rules-size (* rules-size zoom-inverse)] - (if (= axis :x) - {:text-x val - :text-y (+ (:y vbox) (- rules-pos (* 4 zoom-inverse))) - :line-x1 val - :line-y1 (+ (:y vbox) rules-pos (* 2 zoom-inverse)) - :line-x2 val - :line-y2 (+ (:y vbox) rules-pos (* 2 zoom-inverse) rules-size)} - - {:text-x (+ (:x vbox) (- rules-pos (* 4 zoom-inverse))) - :text-y val - :line-x1 (+ (:x vbox) rules-pos (* 2 zoom-inverse)) - :line-y1 val - :line-x2 (+ (:x vbox) rules-pos (* 2 zoom-inverse) rules-size) - :line-y2 val}))) - -(mf/defc rules-axis - [{:keys [zoom zoom-inverse vbox axis offset]}] - (let [rules-width (* rules-width zoom-inverse) - step (calculate-step-size zoom) - clip-id (str "clip-rule-" (d/name axis))] - - [:* - (let [{:keys [x y width height]} (get-background-area vbox zoom-inverse axis)] - [:rect {:x x :y y :width width :height height :style {:fill rules-background}}]) - - [:g.rules {:clipPath (str "url(#" clip-id ")")} - - [:defs - [:clipPath {:id clip-id} - (let [{:keys [x y width height]} (get-clip-area vbox zoom-inverse axis)] - [:rect {:x x :y y :width width :height height}])]] - - (let [{:keys [start end]} (get-rule-params vbox axis) - minv (max start -100000) - minv (* (mth/ceil (/ minv step)) step) - maxv (min end 100000) - maxv (* (mth/floor (/ maxv step)) step) - - ;; These extra operations ensure that we are selecting a frame its initial location is rendered in the rule - minv (+ minv (mod offset step)) - maxv (+ maxv (mod offset step))] - - (for [step-val (range minv (inc maxv) step)] - (let [{:keys [text-x text-y line-x1 line-y1 line-x2 line-y2]} - (get-rule-axis step-val vbox zoom-inverse axis)] - [:* {:key (dm/str "text-" (d/name axis) "-" step-val)} - [:text {:x text-x - :y text-y - :text-anchor "middle" - :dominant-baseline "middle" - :transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")")) - :style {:font-size (* font-size zoom-inverse) - :font-family font-family - :fill colors/gray-30}} - ;; If the guide is associated to a frame we show the position relative to the frame - (fmt/format-number (- step-val offset))] - - [:line {:key (str "line-" (d/name axis) "-" step-val) - :x1 line-x1 - :y1 line-y1 - :x2 line-x2 - :y2 line-y2 - :style {:stroke colors/gray-30 - :stroke-width rules-width}}]])))]])) - -(mf/defc selection-area - [{:keys [vbox zoom-inverse selection-rect offset-x offset-y]}] - ;; When using the format-number callls we consider if the guide is associated to a frame and we show the position relative to it with the offset - [:g.selection-area - [:g - [:rect {:x (:x selection-rect) - :y (:y vbox) - :width (:width selection-rect) - :height (* rule-area-size zoom-inverse) - :style {:fill selection-area-color - :fill-opacity selection-area-opacity}}] - - [:rect {:x (- (:x selection-rect) (* over-number-size zoom-inverse)) - :y (:y vbox) - :width (* over-number-size zoom-inverse) - :height (* rule-area-size zoom-inverse) - :style {:fill rules-background - :fill-opacity over-number-opacity}}] - - [:text {:x (- (:x1 selection-rect) (* 4 zoom-inverse)) - :y (+ (:y vbox) (* 12 zoom-inverse)) - :text-anchor "end" - :dominant-baseline "middle" - :style {:font-size (* font-size zoom-inverse) - :font-family font-family - :fill selection-area-color}} - (fmt/format-number (- (:x1 selection-rect) offset-x))] - - [:rect {:x (:x2 selection-rect) - :y (:y vbox) - :width (* over-number-size zoom-inverse) - :height (* rule-area-size zoom-inverse) - :style {:fill rules-background - :fill-opacity over-number-opacity}}] - - [:text {:x (+ (:x2 selection-rect) (* 4 zoom-inverse)) - :y (+ (:y vbox) (* 12 zoom-inverse)) - :text-anchor "start" - :dominant-baseline "middle" - :style {:font-size (* font-size zoom-inverse) - :font-family font-family - :fill selection-area-color}} - (fmt/format-number (- (:x2 selection-rect) offset-x))]] - - (let [center-x (+ (:x vbox) (* rule-area-half-size zoom-inverse)) - center-y (- (+ (:y selection-rect) (/ (:height selection-rect) 2)) (* rule-area-half-size zoom-inverse))] - - [:g {:transform (str "rotate(-90 " center-x "," center-y ")")} - [:rect {:x (- center-x (/ (:height selection-rect) 2) (* rule-area-half-size zoom-inverse)) - :y (- center-y (* rule-area-half-size zoom-inverse)) - :width (:height selection-rect) - :height (* rule-area-size zoom-inverse) - :style {:fill selection-area-color - :fill-opacity selection-area-opacity}}] - - [:rect {:x (- center-x (/ (:height selection-rect) 2) (* rule-area-half-size zoom-inverse) (* over-number-size zoom-inverse)) - :y (- center-y (* rule-area-half-size zoom-inverse)) - :width (* over-number-size zoom-inverse) - :height (* rule-area-size zoom-inverse) - :style {:fill rules-background - :fill-opacity over-number-opacity}}] - - [:rect {:x (+ (- center-x (/ (:height selection-rect) 2) (* rule-area-half-size zoom-inverse) ) (:height selection-rect)) - :y (- center-y (* rule-area-half-size zoom-inverse)) - :width (* over-number-size zoom-inverse) - :height (* rule-area-size zoom-inverse) - :style {:fill rules-background - :fill-opacity over-number-opacity}}] - - [:text {:x (- center-x (/ (:height selection-rect) 2) (* 15 zoom-inverse)) - :y center-y - :text-anchor "end" - :dominant-baseline "middle" - :style {:font-size (* font-size zoom-inverse) - :font-family font-family - :fill selection-area-color}} - (fmt/format-number (- (:y2 selection-rect) offset-y))] - - [:text {:x (+ center-x (/ (:height selection-rect) 2) ) - :y center-y - :text-anchor "start" - :dominant-baseline "middle" - :style {:font-size (* font-size zoom-inverse) - :font-family font-family - :fill selection-area-color}} - (fmt/format-number (- (:y1 selection-rect) offset-y))]])]) - -(mf/defc rules - {::mf/wrap-props false - ::mf/wrap [#(mf/memo' % (mf/check-props ["zoom" "vbox" "selected-shapes"]))]} - [props] - (let [zoom (obj/get props "zoom") - zoom-inverse (obj/get props "zoom-inverse") - vbox (obj/get props "vbox") - offset-x (obj/get props "offset-x") - offset-y (obj/get props "offset-y") - selected-shapes (-> (obj/get props "selected-shapes") - (hooks/use-equal-memo)) - - selection-rect - (mf/use-memo - (mf/deps selected-shapes) - #(when (d/not-empty? selected-shapes) - (gsh/selection-rect selected-shapes)))] - - (when (some? vbox) - [:g.rules {:pointer-events "none"} - [:& rules-axis {:zoom zoom :zoom-inverse zoom-inverse :vbox vbox :axis :x :offset offset-x}] - [:& rules-axis {:zoom zoom :zoom-inverse zoom-inverse :vbox vbox :axis :y :offset offset-y}] - - (when (some? selection-rect) - [:& selection-area {:zoom zoom - :zoom-inverse zoom-inverse - :vbox vbox - :selection-rect selection-rect - :offset-x offset-x - :offset-y offset-y}])]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs b/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs index 8a346f1fea..1dd7a6f79e 100644 --- a/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs @@ -7,8 +7,9 @@ (ns app.main.ui.workspace.viewport.scroll-bars (:require [app.common.colors :as clr] + [app.common.files.helpers :as cfh] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] [app.main.data.workspace :as dw] [app.main.store :as st] [app.main.ui.workspace.viewport.viewport-ref :refer [point->viewport]] @@ -27,7 +28,7 @@ (mf/defc viewport-scrollbars {::mf/wrap [mf/memo]} - [{:keys [objects zoom vbox]}] + [{:keys [objects zoom vbox bottom-padding]}] (let [v-scrolling? (mf/use-state false) h-scrolling? (mf/use-state false) @@ -52,8 +53,13 @@ base-objects-rect (mf/with-memo [objects] (-> objects - (cph/get-immediate-children) - (gsh/selection-rect))) + (cfh/get-immediate-children) + (gsh/shapes->rect))) + + ;; Padding for bottom palette + vbox (cond-> vbox + (some? bottom-padding) + (update :height - (/ bottom-padding zoom))) inv-zoom (/ 1 zoom) vbox-height (- (:height vbox) (* inv-zoom scroll-height)) @@ -64,6 +70,7 @@ (max 0) (* vbox-height) (/ (:height base-objects-rect))) + ;; left space hidden because of the scroll left-offset (-> (- vbox-x (:x base-objects-rect)) (max 0) @@ -163,9 +170,11 @@ :y2 (+ vbox-y (:height vbox)) :width (:width vbox) :height (:height vbox)} - containing-rect (gsh/join-selrects [base-objects-rect vbox-rect]) - height-factor (/ (:height containing-rect) vbox-height) - width-factor (/ (:width containing-rect) vbox-width)] + + containing-rect (grc/join-rects [base-objects-rect vbox-rect]) + height-factor (/ (:height containing-rect) vbox-height) + width-factor (/ (:width containing-rect) vbox-width)] + (mf/set-ref-val! start-ref start-pt) (mf/set-ref-val! v-scrollbar-y-padding-ref v-scrollbar-y-padding) (mf/set-ref-val! h-scrollbar-x-padding-ref h-scrollbar-x-padding) diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs index b7d419f32f..8d04c1ac24 100644 --- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs @@ -8,6 +8,7 @@ "Selection handlers component." (:require [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] @@ -19,226 +20,268 @@ [app.main.ui.context :as ctx] [app.main.ui.css-cursors :as cur] [app.main.ui.workspace.shapes.path.editor :refer [path-editor]] + [app.util.array :as array] + [app.util.debug :as dbg] [app.util.dom :as dom] [app.util.object :as obj] - [debug :refer [debug?]] - [rumext.v2 :as mf] - [rumext.v2.util :refer [map->obj]])) + [rumext.v2 :as mf])) (def rotation-handler-size 20) (def resize-point-radius 4) (def resize-point-circle-radius 10) (def resize-point-rect-size 8) (def resize-side-height 8) -(def selection-rect-color-normal "var(--color-select)") +(def selection-rect-color-normal "var(--color-accent-tertiary)") (def selection-rect-color-component "var(--color-component-highlight)") (def selection-rect-width 1) (def min-selrect-side 10) (def small-selrect-side 30) (mf/defc selection-rect + {::mf/wrap-props false} [{:keys [transform rect zoom color on-move-selected on-context-menu]}] - (when rect - (let [{:keys [x y width height]} rect] - [:rect.main.viewport-selrect - {:x x - :y y - :width width - :height height - :transform (str transform) - :on-pointer-down on-move-selected - :on-context-menu on-context-menu - :style {:stroke color - :stroke-width (/ selection-rect-width zoom) - :fill "none"}}]))) + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + width (dm/get-prop rect :width) + height (dm/get-prop rect :height)] + [:rect.main.viewport-selrect + {:x x + :y y + :width width + :height height + :transform (str transform) + :on-pointer-down on-move-selected + :on-context-menu on-context-menu + :style {:stroke color + :stroke-width (/ selection-rect-width zoom) + :fill "none"}}])) -(defn- handlers-for-selection [{:keys [x y width height]} {:keys [type]} zoom] - (let [threshold-small (/ 25 zoom) - threshold-tiny (/ 10 zoom) +(defn- calculate-handlers + "Calculates selection handlers for the current selection." + [selection shape zoom] + (let [x (dm/get-prop selection :x) + y (dm/get-prop selection :y) + width (dm/get-prop selection :width) + height (dm/get-prop selection :height) - small-width? (<= width threshold-small) - tiny-width? (<= width threshold-tiny) + threshold-small (/ 25 zoom) + threshold-tiny (/ 10 zoom) - small-height? (<= height threshold-small) - tiny-height? (<= height threshold-tiny) + small-width? (<= width threshold-small) + tiny-width? (<= width threshold-tiny) - vertical-line? (and (= type :path) tiny-width?) - horizontal-line? (and (= type :path) tiny-height?) + small-height? (<= height threshold-small) + tiny-height? (<= height threshold-tiny) - align (if (or small-width? small-height?) - :outside - :inside)] - (->> - [ ;; TOP-LEFT - {:type :rotation - :position :top-left - :props {:cx x :cy y}} + path? (cfh/path-shape? shape) + vertical-line? (and ^boolean path? ^boolean tiny-width?) + horizontal-line? (and ^boolean path? ^boolean tiny-height?) - {:type :rotation - :position :top-right - :props {:cx (+ x width) :cy y}} + align (if (or ^boolean small-width? ^boolean small-height?) + :outside + :inside) - {:type :rotation - :position :bottom-right - :props {:cx (+ x width) :cy (+ y height)}} + result #js [#js {:type :rotation + :position :top-left + :props #js {:cx x :cy y}} - {:type :rotation - :position :bottom-left - :props {:cx x :cy (+ y height)}} + #js {:type :rotation + :position :top-right + :props #js {:cx (+ x width) :cy y}} - (when-not horizontal-line? - (let [x (if small-width? (+ x (/ (- width threshold-small) 2)) x) - length (if small-width? threshold-small width)] - {:type :resize-side - :position :top - :props {:x x - :y y - :length length - :angle 0 - :align align - :show-handler? tiny-width?}})) + #js {:type :rotation + :position :bottom-right + :props #js {:cx (+ x width) :cy (+ y height)}} - (when-not horizontal-line? - (let [x (if small-width? (+ x (/ (+ width threshold-small) 2)) (+ x width)) - length (if small-width? threshold-small width)] - {:type :resize-side - :position :bottom - :props {:x x - :y (+ y height) - :length length - :angle 180 - :align align - :show-handler? tiny-width?}})) + #js {:type :rotation + :position :bottom-left + :props #js {:cx x :cy (+ y height)}}]] - (when-not vertical-line? - (let [y (if small-height? (+ y (/ (- height threshold-small) 2)) y) - length (if small-height? threshold-small height)] - {:type :resize-side - :position :right - :props {:x (+ x width) - :y y - :length length - :angle 90 - :align align - :show-handler? tiny-height?}})) - (when-not vertical-line? - (let [y (if small-height? (+ y (/ (+ height threshold-small) 2)) (+ y height)) - length (if small-height? threshold-small height)] - {:type :resize-side - :position :left - :props {:x x - :y y - :length length - :angle 270 - :align align - :show-handler? tiny-height?}})) + (when-not ^boolean horizontal-line? + (array/conj! result + #js {:type :resize-side + :position :top + :props #js {:x (if ^boolean small-width? + (+ x (/ (- width threshold-small) 2)) + x) + :y y + :length (if ^boolean small-width? + threshold-small + width) + :angle 0 + :align align + :show-handler tiny-width?}} + #js {:type :resize-side + :position :bottom + :props #js {:x (if ^boolean small-width? + (+ x (/ (+ width threshold-small) 2)) + (+ x width)) + :y (+ y height) + :length (if small-width? threshold-small width) + :angle 180 + :align align + :show-handler tiny-width?}})) - (when (and (not tiny-width?) (not tiny-height?)) - {:type :resize-point - :position :top-left - :props {:cx x :cy y :align align}}) + (when-not vertical-line? + (array/conj! result + #js {:type :resize-side + :position :right + :props #js {:x (+ x width) + :y (if small-height? (+ y (/ (- height threshold-small) 2)) y) + :length (if small-height? threshold-small height) + :angle 90 + :align align + :show-handler tiny-height?}} - (when (and (not tiny-width?) (not tiny-height?)) - {:type :resize-point - :position :top-right - :props {:cx (+ x width) :cy y :align align}}) + #js {:type :resize-side + :position :left + :props #js {:x x + :y (if ^boolean small-height? + (+ y (/ (+ height threshold-small) 2)) + (+ y height)) + :length (if ^boolean small-height? + threshold-small + height) + :angle 270 + :align align + :show-handler tiny-height?}})) - (when (and (not tiny-width?) (not tiny-height?)) - {:type :resize-point - :position :bottom-right - :props {:cx (+ x width) :cy (+ y height) :align align}}) + (when (and (not tiny-width?) (not tiny-height?)) + (array/conj! result + #js {:type :resize-point + :position :top-left + :props #js {:cx x :cy y :align align}} + #js {:type :resize-point + :position :top-right + :props #js {:cx (+ x width) :cy y :align align}} + #js {:type :resize-point + :position :bottom-right + :props #js {:cx (+ x width) :cy (+ y height) :align align}} + #js {:type :resize-point + :position :bottom-left + :props #js {:cx x :cy (+ y height) :align align}})))) - (when (and (not tiny-width?) (not tiny-height?)) - {:type :resize-point - :position :bottom-left - :props {:cx x :cy (+ y height) :align align}})] +(mf/defc rotation-handler + {::mf/wrap-props false} + [{:keys [cx cy transform position rotation zoom on-rotate] :as props}] + (let [size (/ rotation-handler-size zoom) + delta-x (if (or (= position :top-left) + (= position :bottom-left)) + size + 0) + delta-y (if (or (= :top-left position) + (= :top-right position)) + size + 0) - (filterv (comp not nil?))))) - -(mf/defc rotation-handler [{:keys [cx cy transform position rotation zoom on-rotate]}] - (let [size (/ rotation-handler-size zoom) - x (- cx (if (#{:top-left :bottom-left} position) size 0)) - y (- cy (if (#{:top-left :top-right} position) size 0)) - angle (case position - :top-left 0 - :top-right 90 - :bottom-right 180 - :bottom-left 270)] + x (- cx delta-x) + y (- cy delta-y) + angle (case position + :top-left 0 + :top-right 90 + :bottom-right 180 + :bottom-left 270)] [:rect {:x x :y y :class (cur/get-dynamic "rotate" (+ rotation angle)) :width size :height size - :fill (if (debug? :handlers) "blue" "none") + :fill (if (dbg/enabled? :handlers) "blue" "none") :stroke-width 0 :transform (dm/str transform) :on-pointer-down on-rotate}])) (mf/defc resize-point-handler - [{:keys [cx cy zoom position on-resize transform rotation color align]}] - (let [layout (mf/deref refs/workspace-layout) - scale-text (:scale-text layout) - cursor (if (#{:top-left :bottom-right} position) - (if scale-text (cur/get-dynamic "scale-nesw" rotation) (cur/get-dynamic "resize-nesw" rotation)) - (if scale-text (cur/get-dynamic "scale-nwse" rotation) (cur/get-dynamic "resize-nwse" rotation))) - {cx' :x cy' :y} (gpt/transform (gpt/point cx cy) transform)] + {::mf/wrap-props false} + [{:keys [cx cy zoom position on-resize transform rotation color align scale-text]}] + (let [cursor (if (or (= position :top-left) + (= position :bottom-right)) + (if ^boolean scale-text + (cur/get-dynamic "scale-nesw" rotation) + (cur/get-dynamic "resize-nesw" rotation)) + (if ^boolean scale-text + (cur/get-dynamic "scale-nwse" rotation) + (cur/get-dynamic "resize-nwse" rotation))) + + pt (gpt/transform (gpt/point cx cy) transform) + cx' (dm/get-prop pt :x) + cy' (dm/get-prop pt :y)] [:g.resize-handler [:circle {:r (/ resize-point-radius zoom) :style {:fillOpacity "1" :strokeWidth "1px" :vectorEffect "non-scaling-stroke"} - :fill "var(--color-white)" + :fill "var(--app-white)" :stroke color :cx cx' :cy cy'}] (if (= align :outside) (let [resize-point-circle-radius (/ resize-point-circle-radius zoom) - offset-x (if (#{:top-right :bottom-right} position) 0 (- resize-point-circle-radius)) - offset-y (if (#{:bottom-left :bottom-right} position) 0 (- resize-point-circle-radius)) - cx (+ cx offset-x) - cy (+ cy offset-y) - {cx' :x cy' :y} (gpt/transform (gpt/point cx cy) transform)] + offset-x (if (or (= position :top-right) + (= position :bottom-right)) + 0 + (- resize-point-circle-radius)) + offset-y (if (or (= position :bottom-left) + (= position :bottom-right)) + 0 + (- resize-point-circle-radius)) + cx (+ cx offset-x) + cy (+ cy offset-y) + pt (gpt/transform (gpt/point cx cy) transform) + cx' (dm/get-prop pt :x) + cy' (dm/get-prop pt :y)] [:rect {:x cx' :y cy' + :data-position (name position) :class cursor + :style {:fill (if (dbg/enabled? :handlers) "red" "none") + :stroke-width 0} :width resize-point-circle-radius :height resize-point-circle-radius - :transform (when rotation (dm/fmt "rotate(%, %, %)" rotation cx' cy')) - :style {:fill (if (debug? :handlers) "red" "none") - :stroke-width 0} - :on-pointer-down #(on-resize {:x cx' :y cy'} %)}]) + :transform (when (some? rotation) + (dm/fmt "rotate(%, %, %)" rotation cx' cy')) + :on-pointer-down on-resize}]) - [:circle {:on-pointer-down #(on-resize {:x cx' :y cy'} %) + [:circle {:on-pointer-down on-resize :r (/ resize-point-circle-radius zoom) + :data-position (name position) :cx cx' :cy cy' + :data-x cx' + :data-y cy' :class cursor - :style {:fill (if (debug? :handlers) "red" "none") + :style {:fill (if (dbg/enabled? :handlers) "red" "none") :stroke-width 0}}])])) +;; The side handler is always rendered horizontally and then rotated (mf/defc resize-side-handler - "The side handler is always rendered horizontally and then rotated" - [{:keys [x y length align angle zoom position rotation transform on-resize color show-handler?]}] - (let [res-point (if (#{:top :bottom} position) - {:y y} - {:x x}) - layout (mf/deref refs/workspace-layout) - scale-text (:scale-text layout) - height (/ resize-side-height zoom) - offset-y (if (= align :outside) (- height) (- (/ height 2))) - target-y (+ y offset-y) - transform-str (dm/str (gmt/multiply transform (gmt/rotate-matrix angle (gpt/point x y))))] + {::mf/wrap-props false} + [{:keys [x y length align angle zoom position rotation transform on-resize color show-handler scale-text]}] + (let [height (/ resize-side-height zoom) + offset-y (if (= align :outside) (- height) (- (/ height 2))) + target-y (+ y offset-y) + transform-str (dm/str (gmt/multiply transform (gmt/rotate-matrix angle (gpt/point x y)))) + cursor (if (or (= position :left) + (= position :right)) + (if ^boolean scale-text + (cur/get-dynamic "scale-ew" rotation) + (cur/get-dynamic "resize-ew" rotation)) + (if ^boolean scale-text + (cur/get-dynamic "scale-ns" rotation) + (cur/get-dynamic "resize-ns" rotation)))] + [:g.resize-handler - (when show-handler? + (when ^boolean show-handler [:circle {:r (/ resize-point-radius zoom) :style {:fillOpacity 1 :stroke color :strokeWidth "1px" - :fill "var(--color-white)" + :fill "var(--app-white)" :vectorEffect "non-scaling-stroke"} + :data-position (name position) :cx (+ x (/ length 2)) :cy y :transform transform-str}]) @@ -246,41 +289,25 @@ :y target-y :width length :height height - :class (if (#{:left :right} position) - (if scale-text (cur/get-dynamic "scale-ew" rotation) (cur/get-dynamic "resize-ew" rotation)) - (if scale-text (cur/get-dynamic "scale-ns" rotation) (cur/get-dynamic "resize-ns" rotation))) + :class cursor + :data-position (name position) :transform transform-str - :on-pointer-down #(on-resize res-point %) - :style {:fill (if (debug? :handlers) "yellow" "none") + :on-pointer-down on-resize + :style {:fill (if (dbg/enabled? :handlers) "yellow" "none") :stroke-width 0}}]])) -(defn minimum-selrect [{:keys [x y width height] :as selrect}] - (let [final-width (max width min-selrect-side) - final-height (max height min-selrect-side) - offset-x (/ (- final-width width) 2) - offset-y (/ (- final-height height) 2)] - {:x (- x offset-x) - :y (- y offset-y) - :width final-width - :height final-height})) - (mf/defc controls-selection {::mf/wrap-props false} - [props] - (let [shape (obj/get props "shape") - zoom (obj/get props "zoom") - color (obj/get props "color") - on-move-selected (obj/get props "on-move-selected") - on-context-menu (obj/get props "on-context-menu") - disable-handlers (obj/get props "disable-handlers") + [{:keys [shape zoom color on-move-selected on-context-menu disable-handlers]}] + (let [selrect (dm/get-prop shape :selrect) + transform-type (mf/deref refs/current-transform) + transform (gsh/transform-str shape)] - current-transform (mf/deref refs/current-transform) - - selrect (:selrect shape) - transform (gsh/transform-str shape)] - - (when (not (#{:move :rotate} current-transform)) - [:g.controls {:pointer-events (if disable-handlers "none" "visible")} + (when (and (some? selrect) + (not (:transforming shape)) + (not (or (= transform-type :move) + (= transform-type :rotate)))) + [:g.controls {:pointer-events (if ^boolean disable-handlers "none" "visible")} ;; Selection rect [:& selection-rect {:rect selrect :transform transform @@ -291,53 +318,61 @@ (mf/defc controls-handlers {::mf/wrap-props false} - [props] - (let [shape (obj/get props "shape") - zoom (obj/get props "zoom") - color (obj/get props "color") - on-resize (obj/get props "on-resize") - on-rotate (obj/get props "on-rotate") - disable-handlers (obj/get props "disable-handlers") - current-transform (mf/deref refs/current-transform) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) + [{:keys [shape zoom color on-resize on-rotate disable-handlers]}] + (let [transform-type (mf/deref refs/current-transform) + read-only? (mf/use-ctx ctx/workspace-read-only?) - selrect (:selrect shape) - transform (gsh/transform-matrix shape) + layout (mf/deref refs/workspace-layout) + scale-text? (contains? layout :scale-text) - rotation (-> (gpt/point 1 0) - (gpt/transform (:transform shape)) - (gpt/angle) - (mod 360))] + selrect (dm/get-prop shape :selrect) + transform (gsh/transform-matrix shape) - (when (and (not (#{:move :rotate} current-transform)) - (not workspace-read-only?)) - [:g.controls {:pointer-events (if disable-handlers "none" "visible")} - ;; Handlers - (for [{:keys [type position props]} (handlers-for-selection selrect shape zoom)] - (let [rotation - (cond - (and (#{:top-left :bottom-right} position) - (or (and (:flip-x shape) (not (:flip-y shape))) - (and (:flip-y shape) (not (:flip-x shape))))) - (- rotation 90) + rotation (-> (gpt/point 1 0) + (gpt/transform (:transform shape)) + (gpt/angle) + (mod 360)) - (and (#{:top-right :bottom-left} position) - (or (and (:flip-x shape) (not (:flip-y shape))) - (and (:flip-y shape) (not (:flip-x shape))))) - (+ rotation 90) + flip-x (get shape :flip-x) + flip-y (get shape :flip-y) + half-flip? (or (and flip-x (not flip-y)) + (and flip-y (not flip-x)))] - :else - rotation) + (when (and (not ^boolean read-only?) + (not (:transforming shape)) + (not (or (= transform-type :move) + (= transform-type :rotate)))) - common-props {:key (dm/str (name type) "-" (name position)) - :zoom zoom - :position position - :on-rotate on-rotate - :on-resize (partial on-resize position) - :transform transform - :rotation rotation - :color color} - props (map->obj (merge common-props props))] + [:g.controls {:pointer-events (if ^boolean disable-handlers "none" "visible")} + (for [handler (calculate-handlers selrect shape zoom)] + (let [type (obj/get handler "type") + position (obj/get handler "position") + props (obj/get handler "props") + rotation (cond + (and ^boolean half-flip? + (or (= position :top-left) + (= position :bottom-right))) + (- rotation 90) + + (and ^boolean half-flip? + (or (= position :top-right) + (= position :bottom-left))) + (+ rotation 90) + + :else + rotation) + + props (obj/merge! + #js {:key (dm/str (name type) "-" (name position)) + :scale-text scale-text? + :zoom zoom + :position position + :on-rotate on-rotate + :on-resize on-resize + :transform transform + :rotation rotation + :color color} + props)] (case type :rotation [:> rotation-handler props] :resize-point [:> resize-point-handler props] @@ -346,8 +381,12 @@ ;; --- Selection Handlers (Component) (mf/defc text-edition-selection - [{:keys [shape color zoom] :as props}] - (let [{:keys [x y width height]} shape] + {::mf/wrap-props false} + [{:keys [shape color zoom]}] + (let [x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + width (dm/get-prop shape :width) + height (dm/get-prop shape :height)] [:g.controls [:rect.main {:x x :y y :transform (gsh/transform-str shape) @@ -360,23 +399,31 @@ :fill "none"}}]])) (mf/defc multiple-handlers - [{:keys [shapes selected zoom color disable-handlers] :as props}] - (let [shape (mf/use-memo - (mf/deps shapes) - #(->> shapes - (gsh/selection-rect) - (cts/setup-shape))) + {::mf/wrap-props false} + [{:keys [shapes selected zoom color disable-handlers]}] + (let [shape (mf/with-memo [shapes] + (-> shapes + (gsh/shapes->rect) + (assoc :type :multiple) + (cts/setup-shape))) + on-resize - (fn [current-position _initial-position event] - (when (dom/left-mouse? event) - (dom/stop-propagation event) - (st/emit! (dw/start-resize current-position selected shape)))) + (mf/use-fn + (mf/deps selected shape) + (fn [event] + (when (dom/left-mouse? event) + (dom/stop-propagation event) + (let [target (dom/get-current-target event) + position (keyword (dom/get-data target "position"))] + (st/emit! (dw/start-resize position selected shape)))))) on-rotate - (fn [event] - (when (dom/left-mouse? event) - (dom/stop-propagation event) - (st/emit! (dw/start-rotate shapes))))] + (mf/use-fn + (mf/deps shapes) + (fn [event] + (when (dom/left-mouse? event) + (dom/stop-propagation event) + (st/emit! (dw/start-rotate shapes)))))] [:& controls-handlers {:shape shape @@ -387,12 +434,13 @@ :on-rotate on-rotate}])) (mf/defc multiple-selection - [{:keys [shapes zoom color disable-handlers on-move-selected on-context-menu] :as props}] - (let [shape (mf/use-memo - (mf/deps shapes) - #(->> shapes - (gsh/selection-rect) - (cts/setup-shape)))] + {::mf/wrap-props false} + [{:keys [shapes zoom color disable-handlers on-move-selected on-context-menu]}] + (let [shape (mf/with-memo [shapes] + (-> shapes + (gsh/shapes->rect) + (assoc :type :multiple) + (cts/setup-shape)))] [:& controls-selection {:shape shape @@ -403,20 +451,28 @@ :on-context-menu on-context-menu}])) (mf/defc single-handlers - [{:keys [shape zoom color disable-handlers] :as props}] - (let [shape-id (:id shape) + {::mf/wrap-props false} + [{:keys [shape zoom color disable-handlers]}] + (let [shape-id (dm/get-prop shape :id) on-resize - (fn [current-position _initial-position event] - (when (dom/left-mouse? event) - (dom/stop-propagation event) - (st/emit! (dw/start-resize current-position #{shape-id} shape)))) + (mf/use-fn + (mf/deps shape-id shape) + (fn [event] + (when (dom/left-mouse? event) + (dom/stop-propagation event) + (let [target (dom/get-current-target event) + position (-> (dom/get-data target "position") + (keyword))] + (st/emit! (dw/start-resize position #{shape-id} shape)))))) on-rotate - (fn [event] - (when (dom/left-mouse? event) - (dom/stop-propagation event) - (st/emit! (dw/start-rotate [shape]))))] + (mf/use-fn + (mf/deps shape) + (fn [event] + (when (dom/left-mouse? event) + (dom/stop-propagation event) + (st/emit! (dw/start-rotate [shape])))))] [:& controls-handlers {:shape shape @@ -427,7 +483,8 @@ :on-resize on-resize}])) (mf/defc single-selection - [{:keys [shape zoom color disable-handlers on-move-selected on-context-menu] :as props}] + {::mf/wrap-props false} + [{:keys [shape zoom color disable-handlers on-move-selected on-context-menu]}] [:& controls-selection {:shape shape :zoom zoom @@ -437,23 +494,25 @@ :on-context-menu on-context-menu}]) (mf/defc selection-area - {::mf/wrap [mf/memo]} - [{:keys [shapes edition zoom disable-handlers on-move-selected on-context-menu] :as props}] - (let [num (count shapes) - {:keys [type] :as shape} (first shapes) + {::mf/wrap-props false} + [{:keys [shapes edition zoom disable-handlers on-move-selected on-context-menu]}] + (let [total (count shapes) + + shape (first shapes) + shape-id (dm/get-prop shape :id) ;; Note that we don't use mf/deref to avoid a repaint dependency here objects (deref refs/workspace-page-objects) - color (if (and (= num 1) - (ctn/in-any-component? objects shape)) - selection-rect-color-component - selection-rect-color-normal)] + color (if (and (= total 1) ^boolean (ctn/in-any-component? objects shape)) + selection-rect-color-component + selection-rect-color-normal)] + (cond - (zero? num) + (zero? total) nil - (> num 1) + (> total 1) [:& multiple-selection {:shapes shapes :zoom zoom @@ -462,13 +521,14 @@ :on-move-selected on-move-selected :on-context-menu on-context-menu}] - (and (= type :text) (= edition (:id shape))) + (and (cfh/text-shape? shape) + (= edition shape-id)) [:& text-edition-selection {:shape shape :zoom zoom :color color}] - (= edition (:id shape)) + (= edition shape-id) nil :else @@ -481,23 +541,25 @@ :on-context-menu on-context-menu}]))) (mf/defc selection-handlers - {::mf/wrap [mf/memo]} - [{:keys [shapes selected edition zoom disable-handlers] :as props}] - (let [num (count shapes) - {:keys [type] :as shape} (first shapes) + {::mf/wrap-props false} + [{:keys [shapes selected edition zoom disable-handlers]}] + (let [total (count shapes) + + shape (first shapes) + shape-id (dm/get-prop shape :id) ;; Note that we don't use mf/deref to avoid a repaint dependency here objects (deref refs/workspace-page-objects) - color (if (and (= num 1) - (ctn/in-any-component? objects shape)) - selection-rect-color-component - selection-rect-color-normal)] + color (if (and (= total 1) ^boolean (ctn/in-any-component? objects shape)) + selection-rect-color-component + selection-rect-color-normal)] + (cond - (zero? num) + (zero? total) nil - (> num 1) + (> total 1) [:& multiple-handlers {:shapes shapes :selected selected @@ -505,10 +567,11 @@ :color color :disable-handlers disable-handlers}] - (and (= type :text) (= edition (:id shape))) + (and (cfh/text-shape? shape) + (= edition shape-id)) nil - (= edition (:id shape)) + (= edition shape-id) [:& path-editor {:zoom zoom :shape shape}] diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs index 185d50e74d..c903a19389 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs @@ -7,23 +7,26 @@ (ns app.main.ui.workspace.viewport.snap-distances (:require [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cph] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] [app.main.snap :as ams] [app.main.ui.formats :as fmt] - [beicon.core :as rx] + [beicon.v2.core :as rx] [clojure.set :as set] [cuerdas.core :as str] [rumext.v2 :as mf])) -(def ^:private line-color "var(--color-snap)") +(def ^:private line-color "var(--color-accent-quaternary)") (def ^:private segment-gap 2) (def ^:private segment-gap-side 5) (defn selected->cross-selrec [frame selrect coord] - (let [areas (gsh/selrect->areas (:selrect frame) selrect)] + (let [areas (gsh/get-areas (:selrect frame) selrect)] (if (= :x coord) [(gsh/pad-selrec (:left areas)) (gsh/pad-selrec (:right areas))] @@ -33,12 +36,12 @@ (defn half-point "Calculates the middle point of the overlap between two selrects in the opposite axis" [coord sr1 sr2] - (let [c1 (max (get sr1 (if (= :x coord) :y1 :x1)) - (get sr2 (if (= :x coord) :y1 :x1))) - c2 (min (get sr1 (if (= :x coord) :y2 :x2)) - (get sr2 (if (= :x coord) :y2 :x2))) - half-point (+ c1 (/ (- c2 c1) 2))] - half-point)) + (let [c1 (mth/max (get sr1 (if (= :x coord) :y1 :x1)) + (get sr2 (if (= :x coord) :y1 :x1))) + c2 (mth/min (get sr1 (if (= :x coord) :y2 :x2)) + (get sr2 (if (= :x coord) :y2 :x2)))] + + (+ c1 (/ (- c2 c1) 2)))) (def pill-text-width-letter 6) (def pill-text-width-margin 6) @@ -50,10 +53,10 @@ (mf/defc shape-distance-segment "Displays a segment between two selrects with the distance between them" [{:keys [sr1 sr2 coord zoom]}] - (let [from-c (min (get sr1 (if (= :x coord) :x2 :y2)) - (get sr2 (if (= :x coord) :x2 :y2))) - to-c (max (get sr1 (if (= :x coord) :x1 :y1)) - (get sr2 (if (= :x coord) :x1 :y1))) + (let [from-c (mth/min (get sr1 (if (= :x coord) :x2 :y2)) + (get sr2 (if (= :x coord) :x2 :y2))) + to-c (mth/max (get sr1 (if (= :x coord) :x1 :y1)) + (get sr2 (if (= :x coord) :x1 :y1))) distance (- to-c from-c) distance-str (fmt/format-number distance) @@ -82,7 +85,7 @@ [:text {:x (if (= coord :x) x (+ x (/ width 2))) :y (- (+ y (/ (/ pill-text-height zoom) 2) (- (/ 6 zoom))) (if (= coord :x) (/ 2 zoom) 0)) :font-size (/ pill-text-font-size zoom) - :fill "var(--color-white)" + :fill "var(--app-white)" :text-anchor "middle"} (fmt/format-number distance)]]) @@ -130,7 +133,8 @@ (and (>= s1c1 s2c1) (<= s1c1 s2c2)) (and (>= s1c2 s2c1) (<= s1c2 s2c2))))) -(defn calculate-segments [coord selrect lt-shapes gt-shapes] +(defn calculate-segments + [coord selrect lt-shapes gt-shapes] (let [distance-to-selrect (fn [shape] (let [sr (:selrect shape)] @@ -156,10 +160,13 @@ ;; Left/Top shapes and right/bottom shapes (depends on `coord` parameter) ;; Gets the distance to the current selection - distances-xf (comp (map distance-to-selrect) (filter pos?)) + distances-xf (comp (filter some?) + (map distance-to-selrect) + (filter pos?)) + lt-distances (into #{} distances-xf lt-shapes) gt-distances (into #{} distances-xf gt-shapes) - distances (set/union lt-distances gt-distances) + distances (set/union lt-distances gt-distances) ;; We'll show the distances that match a distance from the selrect show-candidate? #(check-in-set % distances) @@ -200,6 +207,26 @@ segments-to-display)) +(defn- query-worker + [page-id coord [selrect selected frame]] + (let [lt-side (if (= coord :x) :left :top) + gt-side (if (= coord :x) :right :bottom) + + vbox (deref refs/vbox) + frame-sr (when-not (cph/root? frame) (dm/get-prop frame :selrect)) + bounds (d/nilv (grc/clip-rect frame-sr vbox) vbox) + areas (gsh/get-areas bounds selrect) + + query-side + (fn [side] + (let [rect (get areas side)] + (if (and (> (:width rect) 0) (> (:height rect) 0)) + (ams/select-shapes-area page-id (:id frame) selected @refs/workspace-page-objects rect) + (rx/of nil))))] + + (rx/combine-latest (query-side lt-side) + (query-side gt-side)))) + (mf/defc shape-distance {::mf/wrap-props false} [props] @@ -211,51 +238,46 @@ selected (unchecked-get props "selected") subject (mf/use-memo #(rx/subject)) - to-measure (mf/use-state []) - query-worker - (fn [[selrect selected frame]] - (let [lt-side (if (= coord :x) :left :top) - gt-side (if (= coord :x) :right :bottom) - vbox (gsh/rect->selrect @refs/vbox) - areas (gsh/selrect->areas - (or (gsh/clip-selrect (:selrect frame) vbox) vbox) - selrect) + lt-shapes* (mf/use-state nil) + lt-shapes (deref lt-shapes*) - query-side (fn [side] - (let [rect (get areas side)] - (if (and (> (:width rect) 0) (> (:height rect) 0)) - (ams/select-shapes-area page-id (:id frame) selected @refs/workspace-page-objects rect) - (rx/of nil))))] - (rx/combine-latest (query-side lt-side) - (query-side gt-side)))) + gt-shapes* (mf/use-state nil) + gt-shapes (deref gt-shapes*) - [lt-shapes gt-shapes] @to-measure + segments-to-display + (mf/with-memo [lt-shapes gt-shapes selrect] + (calculate-segments coord selrect lt-shapes gt-shapes))] - segments-to-display (mf/use-memo - (mf/deps @to-measure) - #(calculate-segments coord selrect lt-shapes gt-shapes))] - - (mf/use-effect - (fn [] - (let [sub (->> subject - (rx/throttle 100) - (rx/switch-map query-worker) - (rx/subs #(reset! to-measure %)))] - ;; On unmount dispose - #(rx/dispose! sub)))) + (mf/with-effect [page-id] + (let [sub (->> subject + (rx/throttle 100) + ;; NOTE: we don't put coord on deps because we + ;; know it is a static value and will not go to + ;; change + (rx/switch-map (partial query-worker page-id coord)) + (rx/subs! (fn [[lt-shapes gt-shapes]] + (reset! lt-shapes* lt-shapes) + (reset! gt-shapes* gt-shapes))))] + ;; On unmount dispose + #(rx/dispose! sub))) (mf/use-effect (mf/deps selrect) #(rx/push! subject [selrect selected frame])) (for [[sr1 sr2] segments-to-display] - [:& shape-distance-segment {:key (str/join "-" [(:x sr1) (:y sr1) (:x sr2) (:y sr2)]) - :sr1 sr1 - :sr2 sr2 - :coord coord - :zoom zoom}]))) + [:& shape-distance-segment + {:key (str/ffmt "%-%-%-%" + (dm/get-prop sr1 :x) + (dm/get-prop sr1 :y) + (dm/get-prop sr2 :x) + (dm/get-prop sr2 :y)) + :sr1 sr1 + :sr2 sr2 + :coord coord + :zoom zoom}]))) (mf/defc snap-distances {::mf/wrap-props false} @@ -266,7 +288,7 @@ selected-shapes (unchecked-get props "selected-shapes") frame-id (-> selected-shapes first :frame-id) frame (mf/deref (refs/object-by-id frame-id)) - selrect (gsh/selection-rect selected-shapes)] + selrect (gsh/shapes->rect selected-shapes)] (when-not (ctl/any-layout? frame) [:g.distance diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs index a614d83543..d65ae80f06 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs @@ -8,15 +8,15 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] + [app.common.geom.snap :as sp] [app.common.types.shape.layout :as ctl] [app.main.snap :as snap] - [app.util.geom.snap-points :as sp] - [beicon.core :as rx] + [beicon.v2.core :as rx] [rumext.v2 :as mf])) -(def ^:private line-color "var(--color-snap)") +(def ^:private line-color "var(--color-accent-quaternary)") (def ^:private line-opacity 0.6) (def ^:private line-width 1) @@ -52,16 +52,16 @@ (defn get-snap [coord {:keys [shapes page-id remove-snap? zoom]}] - (let [bounds (gsh/selection-rect shapes) + (let [bounds (gsh/shapes->rect shapes) frame-id (snap/snap-frame-id shapes)] (->> (rx/of bounds) - (rx/flat-map + (rx/merge-map (fn [bounds] - (->> (sp/selrect-snap-points bounds) + (->> (sp/rect->snap-points bounds) (map #(vector frame-id %))))) - (rx/flat-map + (rx/merge-map (fn [[frame-id point]] (->> (snap/get-snap-points page-id frame-id remove-snap? zoom point coord) (rx/map #(mapcat second %)) @@ -124,7 +124,7 @@ (fn [result] (apply d/concat-vec (seq result)))) - (rx/subs + (rx/subs! (fn [data] (let [rs (filter (fn [[_ snaps _]] (> (count snaps) 0)) data)] (reset! state rs)))))] @@ -159,7 +159,7 @@ (let [shapes (into [] (keep (d/getf objects)) selected) filter-shapes - (into selected (mapcat #(cph/get-children-ids objects %)) selected) + (into selected (mapcat #(cfh/get-children-ids objects %)) selected) remove-snap-base? (mf/with-memo [layout filter-shapes objects focus] diff --git a/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs b/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs new file mode 100644 index 0000000000..1cbd37d1cb --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs @@ -0,0 +1,78 @@ +; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.viewport.top-bar + (:require-macros [app.main.style :as stl]) + (:require + [app.common.files.helpers :as cfh] + [app.common.types.shape.layout :as ctl] + [app.main.data.workspace :as dw] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.context :as ctx] + [app.main.ui.workspace.top-toolbar :refer [top-toolbar]] + [app.main.ui.workspace.viewport.grid-layout-editor :refer [grid-edition-actions]] + [app.main.ui.workspace.viewport.path-actions :refer [path-actions]] + [app.util.i18n :as i18n :refer [tr]] + [rumext.v2 :as mf])) + +(mf/defc view-only-actions + [] + (let [handle-close-view-mode + (mf/use-callback + (fn [] + (st/emit! :interrupt + (dw/set-options-mode :design) + (dw/set-workspace-read-only false))))] + [:div {:class (stl/css :viewport-actions)} + [:div {:class (stl/css :viewport-actions-container)} + [:div {:class (stl/css :viewport-actions-title)} + [:& i18n/tr-html {:tag-name "span" + :label "workspace.top-bar.view-only"}]] + [:button {:class (stl/css :done-btn) + :on-click handle-close-view-mode} + (tr "workspace.top-bar.read-only.done")]]])) + +(mf/defc top-bar + {::mf/wrap [mf/memo]} + [{:keys [layout]}] + (let [edition (mf/deref refs/selected-edition) + selected (mf/deref refs/selected-objects) + drawing (mf/deref refs/workspace-drawing) + rulers? (mf/deref refs/rulers?) + drawing-obj (:object drawing) + shape (or drawing-obj (-> selected first)) + + single? (= (count selected) 1) + editing? (= (:id shape) edition) + draw-path? (and (some? drawing-obj) + (cfh/path-shape? drawing-obj) + (not= :curve (:tool drawing))) + + workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) + hide-ui? (:hide-ui layout) + + path-edition? (or (and single? editing? + (and (not (cfh/text-shape? shape)) + (not (cfh/frame-shape? shape)))) + draw-path?) + + grid-edition? (and single? editing? (ctl/grid-layout? shape))] + + [:* + (when-not hide-ui? + [:& top-toolbar {:layout layout}]) + + (cond + workspace-read-only? + [:& view-only-actions] + + path-edition? + [:div {:class (stl/css-case :viewport-actions-path true :viewport-actions-no-rulers (not rulers?))} + [:& path-actions {:shape shape}]] + + grid-edition? + [:& grid-edition-actions {:shape shape}])])) diff --git a/frontend/src/app/main/ui/workspace/viewport/top_bar.scss b/frontend/src/app/main/ui/workspace/viewport/top_bar.scss new file mode 100644 index 0000000000..9109d8e8ba --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/top_bar.scss @@ -0,0 +1,56 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.viewport-actions, +.viewport-actions-path { + pointer-events: none; + position: absolute; + --actions-toolbar-position-y: #{$s-28}; + --actions-toolbar-offset-y: #{$s-6}; + + top: calc(var(--actions-toolbar-position-y) + var(--actions-toolbar-offset-y)); + left: 50%; + z-index: $z-index-20; +} + +.viewport-actions-path { + z-index: $z-index-3; +} + +.viewport-actions-container { + @include flexRow; + background: var(--panel-background-color); + border-radius: $br-12; + box-shadow: 0 0 $s-12 0 var(--menu-shadow-color); + gap: $s-8; + height: $s-48; + margin-left: -50%; + padding: $s-8; + cursor: initial; + pointer-events: initial; + width: $s-400; + border: $s-2 solid var(--panel-border-color); +} + +.viewport-actions-title { + flex: 1; + font-size: $fs-12; + color: var(--color-foreground-secondary); + padding-left: $s-8; +} + +.done-btn { + @extend .button-primary; + text-transform: uppercase; + padding: $s-8 $s-20; + font-size: $fs-11; +} + +.viewport-actions-no-rulers { + --actions-toolbar-position-y: 0px; +} diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index 5587022d89..fac3c4ba00 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -12,8 +12,10 @@ [app.main.ui.css-cursors :as cur] [app.main.ui.formats :refer [format-number]])) -(defn format-viewbox [vbox] - (dm/str (format-number(:x vbox 0)) " " +(defn format-viewbox + "Format a viewbox to a string" + [vbox] + (dm/str (format-number (:x vbox 0)) " " (format-number (:y vbox 0)) " " (format-number (:width vbox 0)) " " (format-number (:height vbox 0)))) @@ -69,7 +71,8 @@ (> (:x cand) (:x cur)) cand :else cur))) -(defn title-transform [{:keys [points] :as shape} zoom] +(defn title-transform + [{:keys [points] :as shape} zoom grid-edition?] (let [leftmost (->> points (reduce left?)) topmost (->> points (remove #{leftmost}) (reduce top?)) rightmost (->> points (remove #{leftmost topmost}) (reduce right?)) @@ -81,14 +84,18 @@ top-right-angle (gpt/angle top-right) ;; Choose the position that creates the less angle between left-side and top-side - [label-pos angle v-pos] + [label-pos angle h-pos v-pos] (if (< (mth/abs left-top-angle) (mth/abs top-right-angle)) - [leftmost left-top-angle (gpt/perpendicular left-top)] - [topmost top-right-angle (gpt/perpendicular top-right)]) + [leftmost left-top-angle left-top (gpt/perpendicular left-top)] + [topmost top-right-angle top-right (gpt/perpendicular top-right)]) + delta-x (if grid-edition? 40 0) + delta-y (if grid-edition? 50 10) label-pos - (gpt/subtract label-pos (gpt/scale (gpt/unit v-pos) (/ 10 zoom)))] + (-> label-pos + (gpt/subtract (gpt/scale (gpt/unit v-pos) (/ delta-y zoom))) + (gpt/subtract (gpt/scale (gpt/unit h-pos) (/ delta-x zoom))))] (dm/fmt "rotate(% %,%) scale(%, %) translate(%, %)" ;; rotate diff --git a/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs b/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs index 41c379d024..4ba3d44fea 100644 --- a/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs @@ -11,7 +11,10 @@ [app.common.geom.point :as gpt] [app.main.store :as st] [app.util.dom :as dom] - [rumext.v2 :as mf])) + [app.util.mouse :as mse] + [goog.events :as events] + [rumext.v2 :as mf]) + (:import goog.events.EventType)) (defonce viewport-ref (atom nil)) (defonce current-observer (atom nil)) @@ -45,6 +48,8 @@ #(fn [node] (mf/set-ref-val! ref node) (reset! viewport-ref node) + (when (some? node) + (events/listen node EventType.MOUSELEAVE (fn [] (st/emit! (mse/->BlurEvent))))) (init-observer node on-change-bounds)))])) (defn point->viewport diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 71294dd084..6674446fcb 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -5,13 +5,15 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.viewport.widgets + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] - [app.common.pages.helpers :as cph] + [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.shape-tree :as ctt] + [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] [app.main.data.workspace.interactions :as dwi] @@ -21,11 +23,10 @@ [app.main.ui.context :as ctx] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] - [app.main.ui.workspace.viewport.path-actions :refer [path-actions]] [app.main.ui.workspace.viewport.utils :as vwu] + [app.util.debug :as dbg] [app.util.dom :as dom] [app.util.timers :as ts] - [debug :refer [debug?]] [rumext.v2 :as mf])) (mf/defc pixel-grid @@ -39,8 +40,8 @@ :pattern-units "userSpaceOnUse"} [:path {:d "M 1 0 L 0 0 0 1" :style {:fill "none" - :stroke (if (debug? :pixel-grid) "red" "var(--color-info)") - :stroke-opacity (if (debug? :pixel-grid) 1 "0.2") + :stroke (if (dbg/enabled? :pixel-grid) "red" "var(--color-info)") + :stroke-opacity (if (dbg/enabled? :pixel-grid) 1 "0.2") :stroke-width (str (/ 1 zoom))}}]]] [:rect {:x (:x vbox) :y (:y vbox) @@ -49,24 +50,6 @@ :fill (str "url(#pixel-grid)") :style {:pointer-events "none"}}]]) -(mf/defc viewport-actions - {::mf/wrap [mf/memo]} - [] - (let [edition (mf/deref refs/selected-edition) - selected (mf/deref refs/selected-objects) - drawing (mf/deref refs/workspace-drawing) - drawing-obj (:object drawing) - shape (or drawing-obj (-> selected first))] - (when (or (and (= (count selected) 1) - (= (:id shape) edition) - (and (not (cph/text-shape? shape)) - (not (cph/frame-shape? shape)))) - (and (some? drawing-obj) - (cph/path-shape? drawing-obj) - (not= :curve (:tool drawing)))) - [:div.viewport-actions - [:& path-actions {:shape shape}]]))) - (mf/defc cursor-tooltip [{:keys [zoom tooltip] :as props}] (let [coords (some-> (hooks/use-rxsub ms/mouse-position) @@ -87,26 +70,25 @@ :width (:width data) :height (:height data) :style {;; Primary with 0.1 opacity - :fill "rgb(49, 239, 184, 0.1)" - - ;; Primary color - :stroke "rgb(49, 239, 184)" + :fill "var(--color-accent-tertiary-muted)" + :stroke "var(--color-accent-tertiary)" :stroke-width (/ 1 zoom)}}])) (mf/defc frame-title {::mf/wrap [mf/memo #(mf/deferred % ts/raf)]} - [{:keys [frame selected? zoom show-artboard-names? show-id? on-frame-enter on-frame-leave on-frame-select]}] + [{:keys [frame selected? zoom show-artboard-names? show-id? on-frame-enter on-frame-leave on-frame-select grid-edition?]}] (let [workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) ;; Note that we don't use mf/deref to avoid a repaint dependency here objects (deref refs/workspace-page-objects) - color (when selected? + color (if selected? (if (ctn/in-any-component? objects frame) "var(--color-component-highlight)" - "var(--color-primary-dark)")) + "var(--color-accent-tertiary)") + "#8f9da3") ;; TODO: Set this color on the DS on-pointer-down (mf/use-callback @@ -114,8 +96,8 @@ (fn [bevent] (let [event (.-nativeEvent bevent)] (when (= 1 (.-which event)) - (dom/prevent-default event) - (dom/stop-propagation event) + (dom/prevent-default bevent) + (dom/stop-propagation bevent) (on-frame-select event (:id frame)))))) on-double-click @@ -146,36 +128,57 @@ (mf/deps (:id frame) on-frame-leave) (fn [_] (on-frame-leave (:id frame)))) - text-pos-x (if (:use-for-thumbnail? frame) 15 0)] + + main-instance? (ctk/main-instance? frame) + + text-width (* (:width frame) zoom) + show-icon? (and (or (:use-for-thumbnail frame) grid-edition? main-instance?) + (not (<= text-width 15))) + text-pos-x (if show-icon? 15 0)] (when (not (:hidden frame)) [:g.frame-title {:id (dm/str "frame-title-" (:id frame)) - :transform (vwu/title-transform frame zoom) + :data-edit-grid grid-edition? + :transform (vwu/title-transform frame zoom grid-edition?) :pointer-events (when (:blocked frame) "none")} - (when (:use-for-thumbnail? frame) + (cond + show-icon? [:svg {:x 0 :y -9 :width 12 :height 12 :class "workspace-frame-icon" - :style {:fill color} + :style {:stroke color + :fill "none"} :visibility (if show-artboard-names? "visible" "hidden")} - [:use {:href "#icon-set-thumbnail"}]]) - [:text {:x text-pos-x - :y 0 - :width (:width frame) - :height 20 - :class "workspace-frame-label" - :style {:fill color} - :visibility (if show-artboard-names? "visible" "hidden") + (cond + (:use-for-thumbnail frame) + [:use {:href "#icon-boards-thumbnail"}] + + grid-edition? + [:use {:href "#icon-grid"}] + + main-instance? + [:use {:href "#icon-component"}])]) + + + [:foreignObject {:x text-pos-x + :y -11 + :width (max 0 (- text-width text-pos-x)) + :height 20 + :class (stl/css :workspace-frame-label-wrapper) + :style {:fill color} + :visibility (if show-artboard-names? "visible" "hidden")} + [:div {:class (stl/css :workspace-frame-label) + :style {:color color} :on-pointer-down on-pointer-down :on-double-click on-double-click :on-context-menu on-context-menu :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave} - (if show-id? - (dm/str (dm/str (:id frame)) " - " (:name frame)) - (:name frame))]]))) + (if show-id? + (dm/str (dm/str (:id frame)) " - " (:name frame)) + (:name frame))]]]))) (mf/defc frame-titles {::mf/wrap-props false @@ -190,28 +193,32 @@ on-frame-select (unchecked-get props "on-frame-select") components-v2 (mf/use-ctx ctx/components-v2) shapes (ctt/get-frames objects {:skip-copies? components-v2}) - shapes (if (debug? :shape-titles) + shapes (if (dbg/enabled? :shape-titles) (into (set shapes) (map (d/getf objects)) selected) shapes) - focus (unchecked-get props "focus")] + focus (unchecked-get props "focus") + + edition (mf/deref refs/selected-edition) + grid-edition? (ctl/grid-layout? objects edition)] [:g.frame-titles (for [{:keys [id parent-id] :as shape} shapes] (when (and (not= id uuid/zero) - (or (debug? :shape-titles) (= parent-id uuid/zero)) + (or (dbg/enabled? :shape-titles) (= parent-id uuid/zero)) (or (empty? focus) (contains? focus id))) [:& frame-title {:key (dm/str "frame-title-" id) :frame shape :selected? (contains? selected id) :zoom zoom :show-artboard-names? show-artboard-names? - :show-id? (debug? :shape-titles) + :show-id? (dbg/enabled? :shape-titles) :on-frame-enter on-frame-enter :on-frame-leave on-frame-leave - :on-frame-select on-frame-select}]))])) + :on-frame-select on-frame-select + :grid-edition? (and (= id edition) grid-edition?)}]))])) (mf/defc frame-flow [{:keys [flow frame selected? zoom on-frame-enter on-frame-leave on-frame-select]}] @@ -232,8 +239,8 @@ on-double-click (mf/use-callback - (mf/deps (:id frame)) - #(st/emit! (dwi/start-rename-flow (:id flow)))) + (mf/deps (:id frame)) + #(st/emit! (dwi/start-rename-flow (:id flow)))) on-pointer-enter (mf/use-callback @@ -252,11 +259,13 @@ :width 100000 :height 24 :transform (vwu/text-transform flow-pos zoom)} - [:div.flow-badge {:class (dom/classnames :selected selected?)} - [:div.content {:on-pointer-down on-pointer-down - :on-double-click on-double-click - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave} + [:div {:class (stl/css-case :flow-badge true + :selected selected?)} + [:div {:class (stl/css :content) + :on-pointer-down on-pointer-down + :on-double-click on-double-click + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave} i/play [:span (:name flow)]]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.scss b/frontend/src/app/main/ui/workspace/viewport/widgets.scss new file mode 100644 index 0000000000..0ac3997444 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.scss @@ -0,0 +1,78 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.flow-element { + display: flex; + align-items: center; + + .element-label { + } + + .flow-name { + cursor: pointer; + } + + & input.element-name { + background: transparent; + } +} + +.flow-badge { + cursor: pointer; + display: flex; + .content { + @include bodySmallTypography; + display: flex; + align-items: center; + height: $s-24; + border-radius: $br-6; + background-color: var(--flow-tag-background-color); + svg { + @extend .button-icon; + height: $s-24; + width: $s-12; + stroke: var(--icon-foreground); + margin: 0 $s-8; + } + + span { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + margin-right: $s-8; + color: var(--flow-tag-foreground-color); + } + } + + &.selected .content, + &:hover .content { + background-color: var(--flow-tag-background-color-hover); + svg { + stroke: var(--flow-tag-foreground-color-hover); + } + + span { + color: var(--flow-tag-foreground-color-hover); + } + } +} + +.workspace-frame-label-wrapper { + pointer-events: none; +} + +.workspace-frame-label { + font-size: $fs-12; + color: black; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: fit-content; + pointer-events: all; +} diff --git a/frontend/src/app/thumbnail_renderer.cljs b/frontend/src/app/rasterizer.cljs similarity index 57% rename from frontend/src/app/thumbnail_renderer.cljs rename to frontend/src/app/rasterizer.cljs index f60b3d3ecf..e2bf9ede6e 100644 --- a/frontend/src/app/thumbnail_renderer.cljs +++ b/frontend/src/app/rasterizer.cljs @@ -4,8 +4,8 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.thumbnail-renderer - "A main entry point for the thumbnail renderer process that is +(ns app.rasterizer + "A main entry point for the rasterizer process that is executed on a separated iframe." (:require [app.common.data :as d] @@ -17,14 +17,16 @@ [app.util.http :as http] [app.util.object :as obj] [app.util.webapi :as wapi] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str])) -(log/set-level! :trace) +(log/set-level! :info) (declare send-success!) (declare send-failure!) +(defonce data-uri-cache (js/Map.)) + (defonce parent-origin (dm/str cf/public-uri)) @@ -52,18 +54,34 @@ (obj/set! image "onerror" nil) (obj/set! image "onabort" nil)))))) -(defn- svg-get-size +(defn- svg-get-adjusted-size + "Returns the adjusted size of an SVG." + [width height max] + (let [ratio (/ width height)] + (if (< width height) + [max (* max (/ 1 ratio))] + [(* max ratio) max]))) + +(defn- svg-get-size-from-viewbox + "Returns the size of an SVG from its viewbox." [svg max] (let [doc (get-document-element svg) vbox (dom/get-attribute doc "viewBox")] (when (string? vbox) (let [[_ _ width height] (str/split vbox #"\s+") width (d/parse-integer width 0) - height (d/parse-integer height 0) - ratio (/ width height)] - (if (> width height) - [max (* max (/ 1 ratio))] - [(* max ratio) max]))))) + height (d/parse-integer height 0)] + (svg-get-adjusted-size width height max))))) + +(defn- svg-get-size-from-intrinsic-size + "Returns the size of an SVG from its intrinsic size." + [svg max] + (let [doc (get-document-element svg) + width (dom/get-attribute doc "width") + height (dom/get-attribute doc "height") + width (d/parse-integer width 0) + height (d/parse-integer height 0)] + (svg-get-adjusted-size width height max))) (defn- svg-has-intrinsic-size? "Returns true if the SVG has an intrinsic size." @@ -73,35 +91,53 @@ height (dom/get-attribute doc "height")] (d/num? width height))) +(defn- svg-get-size + [svg max] + (if (svg-has-intrinsic-size? svg) + (svg-get-size-from-intrinsic-size svg max) + (svg-get-size-from-viewbox svg max))) + (defn- svg-set-intrinsic-size! "Sets the intrinsic size of an SVG to the given max size." [^js svg max] - (when-not (svg-has-intrinsic-size? svg) - (let [doc (get-document-element svg) - [w h] (svg-get-size svg max)] - (dom/set-attribute! doc "width" (dm/str w)) - (dom/set-attribute! doc "height" (dm/str h)))) + (let [doc (get-document-element svg) + [w h] (svg-get-size svg max)] + (dom/set-attribute! doc "width" (dm/str w)) + (dom/set-attribute! doc "height" (dm/str h))) svg) (defn- fetch-as-data-uri "Fetches a URL as a Data URI." [uri] - (->> (http/send! {:uri uri - :response-type :blob - :method :get - :mode :cors - :omit-default-headers true}) - (rx/map :body) - (rx/mapcat wapi/read-file-as-data-url))) + (if (.has data-uri-cache uri) + (let [blob (.get data-uri-cache uri)] + (rx/from (.text blob))) + (->> (http/send! {:uri uri + :response-type :blob + :method :get + :mode :cors + :omit-default-headers true}) + (rx/catch (fn [cause] + (log/error :hint "fetching data uri" + :cause cause) + (rx/of nil))) + (rx/mapcat (fn [response] + (if (nil? response) + (rx/of uri) + (->> (rx/of (:body response)) + (rx/mapcat wapi/read-file-as-data-url) + (rx/tap (fn [data-uri] (.set data-uri-cache uri (wapi/create-blob data-uri "text/plain"))))))))))) (defn- svg-update-image! "Updates an image in an SVG to a Data URI." [image] (if-let [href (dom/get-attribute image "href")] - (->> (fetch-as-data-uri href) - (rx/map (fn [url] - (dom/set-attribute! image "href" url) - image))) + (if (str/starts-with? href "data:") + (rx/of image) + (->> (fetch-as-data-uri href) + (rx/map (fn [url] + (dom/set-attribute! image "href" url) + image)))) (rx/empty))) (defn- svg-resolve-images! @@ -119,27 +155,53 @@ (dom/append-child! style (dom/create-text svg styles)) (dom/append-child! doc style))) -(defn- svg-resolve-styles! - "Resolves all fonts in an SVG to Data URIs." - [svg styles] +(defn- svg-resolve-external-resources + "Resolves all external resources in an SVG to Data URIs." + [styles] (->> (rx/from (re-seq #"url\((https?://[^)]+)\)" styles)) (rx/map second) (rx/mapcat (fn [url] - (->> (fetch-as-data-uri url) - (rx/map (fn [uri] [url uri]))))) - + (->> (fetch-as-data-uri url) + (rx/map (fn [uri] [url uri]))))) (rx/reduce (fn [styles [url uri]] (str/replace styles url uri)) - styles) + styles))) + +(defn- svg-resolve-styles! + "Resolves all fonts in an SVG to Data URIs." + [svg styles] + (->> (svg-resolve-external-resources styles) (rx/tap (partial svg-add-style! svg)) (rx/ignore))) +(defn- svg-resolve-inline-styles! + "Resolves all inline styles in an SVG to Data URIs." + [svg] + (->> (rx/from (dom/query-all svg "[style]")) + (rx/mapcat (fn [node] + (let [styles (dom/get-attribute node "style")] + (->> (svg-resolve-external-resources styles) + (rx/tap (fn [styles] (dom/set-attribute! node "style" styles))))))) + (rx/ignore))) + +(defn- svg-resolve-style-elements! + "Resolves all style elements in an SVG to Data URIs." + [svg] + (->> (rx/from (dom/query-all svg "style")) + (rx/mapcat (fn [node] + (let [styles (dom/get-text node)] + (->> (svg-resolve-external-resources styles) + (rx/tap (fn [styles] (dom/set-text! node styles))))))) + (rx/ignore))) + (defn- svg-resolve-all! "Resolves all images and fonts in an SVG to Data URIs." [svg styles] (rx/concat (svg-resolve-images! svg) (svg-resolve-styles! svg styles) + (svg-resolve-inline-styles! svg) + (svg-resolve-style-elements! svg) (rx/of svg))) (defn- svg-parse @@ -178,21 +240,35 @@ (constantly nil))))) -(defn- render - "Renders a thumbnail using it's SVG and returns an ArrayBuffer of the image." +(defn- render-image-bitmap + "Renders a thumbnail using it's SVG and returns an ImageBitmap of the image." [payload] (let [data (unchecked-get payload "data") styles (unchecked-get payload "styles") - width (d/nilv (unchecked-get payload "width") 300)] + width (d/nilv (unchecked-get payload "width") 300) + quality (d/nilv (unchecked-get payload "quality") "medium")] (->> (svg-prepare data styles width) (rx/map #(wapi/create-blob % "image/svg+xml")) (rx/map wapi/create-uri) (rx/mapcat (fn [uri] (->> (create-image uri) - (rx/mapcat #(wapi/create-image-bitmap % #js {:resizeWidth width - :resizeQuality "medium"})) - (rx/tap #(wapi/revoke-uri uri))))) - (rx/mapcat bitmap->blob)))) + (rx/mapcat #(wapi/create-image-bitmap-with-workaround % #js {:resizeWidth width + :resizeQuality quality})) + (rx/tap #(wapi/revoke-uri uri)))))))) + +(defn- render-blob + "Renders a thumbnail using it's SVG and returns a Blob of the image." + [payload] + (->> (render-image-bitmap payload) + (rx/mapcat bitmap->blob))) + +(defn- render + "Renders a thumbnail and returns a stream." + [payload] + (let [result (d/nilv (unchecked-get payload "result") "blob")] + (case result + "image-bitmap" (render-image-bitmap payload) + (render-blob payload)))) (defn- on-message "Handles messages from the main thread." @@ -204,10 +280,10 @@ payload (unchecked-get evdata "payload") scope (unchecked-get evdata "scope")] (when (and (some? payload) - (= scope "penpot/thumbnail-renderer")) + (= scope "penpot/rasterizer")) (->> (render payload) - (rx/subs (partial send-success! id) - (partial send-failure! id)))))))) + (rx/subs! (partial send-success! id) + (partial send-failure! id)))))))) (defn- listen "Initializes the listener for messages from the main thread." @@ -219,10 +295,12 @@ [id type payload] (let [message #js {:id id :type type - :scope "penpot/thumbnail-renderer" + :scope "penpot/rasterizer" :payload payload}] (when-not (identical? js/window js/parent) - (.postMessage js/parent message parent-origin)))) + (if (instance? js/ImageBitmap payload) + (.postMessage js/parent message parent-origin #js [payload]) + (.postMessage js/parent message parent-origin))))) (defn- send-success! "Sends a success message." diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index a8324e7ad1..8dbcbf43fe 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -8,121 +8,54 @@ "The main entry point for UI part needed by the exporter." (:require [app.common.geom.shapes.bounds :as gsb] - [app.common.logging :as l] + [app.common.logging :as log] [app.common.math :as mth] - [app.common.spec :as us] + [app.common.schema :as sm] [app.common.types.components-list :as ctkl] [app.common.uri :as u] [app.main.data.fonts :as df] - [app.main.features :as feat] + [app.main.data.users :as du] + [app.main.features :as features] [app.main.render :as render] [app.main.repo :as repo] [app.main.store :as st] [app.util.dom :as dom] [app.util.globals :as glob] - [beicon.core :as rx] - [clojure.spec.alpha :as s] + [beicon.v2.core :as rx] [cuerdas.core :as str] [garden.core :refer [css]] + [okulary.core :as l] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; SETUP -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(log/setup! {:app :info}) -(l/setup! {:app :info}) - -(declare ^:private render-single-object) -(declare ^:private render-components) -(declare ^:private render-objects) - -(defn- parse-params - [loc] - (let [href (unchecked-get loc "href")] - (some-> href u/uri :query u/query-string->map))) - -(defn init-ui - [] - (when-let [params (parse-params glob/location)] - (when-let [component (case (:route params) - "objects" (render-objects params) - "components" (render-components params) - nil)] - (mf/mount component (dom/get-element "app"))))) - -(defn ^:export init - [] - (st/emit! (feat/initialize)) - (init-ui)) - -(defn reinit - [] - (mf/unmount (dom/get-element "app")) - (init-ui)) - -(defn ^:dev/after-load after-load - [] - (reinit)) +(defn- fetch-team + [& {:keys [file-id]}] + (ptk/reify ::fetch-team + ptk/WatchEvent + (watch [_ _ _] + (->> (repo/cmd! :get-team {:file-id file-id}) + (rx/mapcat (fn [team] + (rx/of (du/set-current-team team) + (ptk/data-event ::team-fetched team)))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; COMPONENTS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; ---- SINGLE OBJECT - -(defn use-resource - "A general purpose hook for retrieve or subscribe to remote changes - using the reactive-streams mechanism mechanism. - - It receives a function to execute for retrieve the stream that will - be used for creating the subscription. The function should be - stable, so is the responsibility of the user of this hook to - properly memoize it. - - TODO: this should be placed in some generic hooks namespace but his - right now is pending of refactor and it will be done later." - [f] - (let [[state ^js update-state!] (mf/useState {:loaded? false})] - (mf/with-effect [f] - (update-state! (fn [prev] (assoc prev :refreshing? true))) - (let [on-value (fn [data] - (update-state! #(-> % - (assoc :refreshing? false) - (assoc :loaded? true) - (merge data)))) - subs (rx/subscribe (f) on-value)] - #(rx/dispose! subs))) - state)) +(def ^:private ref:objects + (l/derived :objects st/state)) (mf/defc object-svg - [{:keys [page-id file-id share-id object-id render-embed?]}] - (let [components-v2 (feat/use-feature :components-v2) - fetch-state (mf/use-fn - (mf/deps file-id page-id share-id object-id components-v2) - (fn [] - (let [features (cond-> #{} components-v2 (conj "components/v2"))] - (->> (rx/zip - (repo/cmd! :get-font-variants {:file-id file-id :share-id share-id}) - (repo/cmd! :get-page {:file-id file-id - :page-id page-id - :share-id share-id - :object-id object-id - :features features})) - (rx/tap (fn [[fonts]] - (when (seq fonts) - (st/emit! (df/fonts-fetched fonts))))) - (rx/map (comp :objects second)) - (rx/map (fn [objects] - (let [objects (render/adapt-objects-for-shape objects object-id)] - {:objects objects - :object (get objects object-id)}))))))) - - {:keys [objects object]} (use-resource fetch-state)] + {::mf/wrap-props false} + [{:keys [object-id embed]}] + (let [objects (mf/deref ref:objects)] ;; Set the globa CSS to assign the page size, needed for PDF ;; exportation process. - (mf/with-effect [object] - (when object + (mf/with-effect [objects] + (when-let [object (get objects object-id)] (let [{:keys [width height]} (gsb/get-object-bounds [objects] object)] (dom/set-page-style! {:size (str/concat @@ -133,90 +66,107 @@ [:& render/object-svg {:objects objects :object-id object-id - :render-embed? render-embed?}]))) + :embed embed}]))) (mf/defc objects-svg - [{:keys [page-id file-id share-id object-ids render-embed?]}] - (let [components-v2 (feat/use-feature :components-v2) - fetch-state (mf/use-fn - (mf/deps file-id page-id share-id components-v2) - (fn [] - (let [features (cond-> #{} components-v2 (conj "components/v2"))] - (->> (rx/zip - (repo/cmd! :get-font-variants {:file-id file-id :share-id share-id}) - (repo/cmd! :get-page {:file-id file-id - :page-id page-id - :share-id share-id - :features features})) - (rx/tap (fn [[fonts]] - (when (seq fonts) - (st/emit! (df/fonts-fetched fonts))))) - (rx/map (fn [[_ page]] {:objects (:objects page)})))))) + {::mf/wrap-props false} + [{:keys [object-ids embed]}] + (when-let [objects (mf/deref ref:objects)] + (for [object-id object-ids] + (let [objects (render/adapt-objects-for-shape objects object-id)] + [:& render/object-svg + {:objects objects + :key (str object-id) + :object-id object-id + :embed embed}])))) - {:keys [objects]} (use-resource fetch-state)] +(defn- fetch-objects-bundle + [& {:keys [file-id page-id share-id object-id] :as options}] + (ptk/reify ::fetch-objects-bundle + ptk/WatchEvent + (watch [_ state _] + (let [features (features/get-team-enabled-features state)] + (->> (rx/zip + (repo/cmd! :get-font-variants {:file-id file-id :share-id share-id}) + (repo/cmd! :get-page {:file-id file-id + :page-id page-id + :share-id share-id + :object-id object-id + :features features})) + (rx/tap (fn [[fonts]] + (when (seq fonts) + (st/emit! (df/fonts-fetched fonts))))) + (rx/observe-on :async) + (rx/map (comp :objects second)) + (rx/map (fn [objects] + (let [objects (render/adapt-objects-for-shape objects object-id)] + #(assoc % :objects objects))))))))) - (when objects - (for [object-id object-ids] - (let [objects (render/adapt-objects-for-shape objects object-id)] - [:& render/object-svg - {:objects objects - :key (str object-id) - :object-id object-id - :render-embed? render-embed?}]))))) +(def ^:private schema:render-objects + [:map {:title "render-objets"} + [:page-id ::sm/uuid] + [:file-id ::sm/uuid] + [:share-id {:optional true} ::sm/uuid] + [:embed {:optional true} :boolean] + [:object-id + [:or + ::sm/uuid + ::sm/coll-of-uuid]]]) -(s/def ::page-id ::us/uuid) -(s/def ::file-id ::us/uuid) -(s/def ::share-id ::us/uuid) -(s/def ::object-id - (s/or :single ::us/uuid - :multiple (s/coll-of ::us/uuid))) -(s/def ::embed ::us/boolean) +(def ^:private render-objects-decoder + (sm/lazy-decoder schema:render-objects + sm/default-transformer)) -(s/def ::render-objects - (s/keys :req-un [::file-id ::page-id ::object-id] - :opt-un [::render-embed ::share-id])) +(def ^:private render-objects-validator + (sm/lazy-validator schema:render-objects)) (defn- render-objects [params] - (let [{:keys [file-id - page-id - render-embed - share-id] - :as params} - (us/conform ::render-objects params) + (let [{:keys [file-id page-id embed share-id object-id] :as params} (render-objects-decoder params)] + (if-not (render-objects-validator params) + (do + (js/console.error "invalid arguments") + (sm/pretty-explain schema:render-objects params) + nil) - [type object-id] (:object-id params)] - (case type - :single - (mf/html - [:& object-svg - {:file-id file-id - :page-id page-id - :share-id share-id - :object-id object-id - :render-embed? render-embed}]) + (do + (st/emit! (ptk/reify ::initialize-render-objects + ptk/WatchEvent + (watch [_ _ stream] + (rx/merge + (rx/of (fetch-team :file-id file-id)) - :multiple - (mf/html - [:& objects-svg - {:file-id file-id - :page-id page-id - :share-id share-id - :object-ids (into #{} object-id) - :render-embed? render-embed}])))) + (->> stream + (rx/filter (ptk/type? ::team-fetched)) + (rx/observe-on :async) + (rx/map (constantly params)) + (rx/map fetch-objects-bundle)))))) + + (if (uuid? object-id) + (mf/html + [:& object-svg + {:file-id file-id + :page-id page-id + :share-id share-id + :object-id object-id + :embed embed}]) + + (mf/html + [:& objects-svg + {:file-id file-id + :page-id page-id + :share-id share-id + :object-ids (into #{} object-id) + :embed embed}])))))) ;; ---- COMPONENTS SPRITE -(mf/defc components-sprite-svg - [{:keys [file-id embed] :as props}] - (let [fetch (mf/use-fn - (mf/deps file-id) - (fn [] (repo/cmd! :get-file {:id file-id}))) - - file (use-resource fetch) - state (mf/use-state nil)] - - (when file +(mf/defc components-svg + {::mf/wrap-props false} + [{:keys [embed component-id]}] + (let [file-ref (mf/with-memo [] (l/derived :file st/state)) + state (mf/use-state {:component-id component-id})] + (when-let [file (mf/deref file-ref)] [:* [:style (css [[:body @@ -262,25 +212,100 @@ [:a {:on-click on-click} (:name data)]]))] [:main - [:& render/components-sprite-svg + [:& render/components-svg {:data (:data file) :embed embed} (when-let [component-id (:component-id @state)] - [:use {:x 0 :y 0 :href (str "#" component-id)}])]] + [:use {:x 0 :y 0 :href (str "#" component-id)}])]]]))) - ]))) +(defn- fetch-components-bundle + [& {:keys [file-id]}] + (ptk/reify ::fetch-components-bundle + ptk/WatchEvent + (watch [_ state _] + (let [features (features/get-team-enabled-features state)] + (->> (repo/cmd! :get-file {:id file-id :features features}) + (rx/map (fn [file] #(assoc % :file file)))))))) -(s/def ::component-id ::us/uuid) -(s/def ::render-components - (s/keys :req-un [::file-id] - :opt-un [::embed ::component-id])) +(def ^:private schema:render-components + [:map {:title "render-components"} + [:file-id ::sm/uuid] + [:embed {:optional true} :boolean] + [:component-id {:optional true} ::sm/uuid]]) + +(def ^:private render-components-decoder + (sm/lazy-decoder schema:render-components + sm/default-transformer)) + +(def ^:private render-components-validator + (sm/lazy-validator schema:render-components)) (defn render-components [params] - (let [{:keys [file-id component-id embed]} (us/conform ::render-components params)] - (mf/html - [:& components-sprite-svg - {:file-id file-id - :component-id component-id - :embed embed}]))) + (let [{:keys [file-id component-id embed] :as params} (render-components-decoder params)] + (if-not (render-components-validator params) + (do + (js/console.error "invalid arguments") + (sm/pretty-explain schema:render-components params) + nil) + + (do + (st/emit! (ptk/reify ::initialize-render-components + ptk/WatchEvent + (watch [_ _ stream] + (rx/merge + (rx/of (fetch-team :file-id file-id)) + + (->> stream + (rx/filter (ptk/type? ::team-fetched)) + (rx/observe-on :async) + (rx/map (constantly params)) + (rx/map fetch-components-bundle)))))) + + (mf/html + [:& components-svg + {:component-id component-id + :embed embed}]))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SETUP +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defonce app-root + (let [el (dom/get-element "app")] + (mf/create-root el))) + +(declare ^:private render-single-object) +(declare ^:private render-components) +(declare ^:private render-objects) + +(defn- parse-params + [loc] + (let [href (unchecked-get loc "href")] + (some-> href u/uri :query u/query-string->map))) + +(defn init-ui + [] + (when-let [params (parse-params glob/location)] + (when-let [component (case (:route params) + "objects" (render-objects params) + "components" (render-components params) + nil)] + (mf/render! app-root component)))) + +(defn ^:export init + [] + (st/emit! (features/initialize)) + (init-ui)) + +(defn reinit + [] + (init-ui)) + +(defn ^:dev/after-load after-load + [] + (reinit)) + + + diff --git a/frontend/src/app/util/array.cljs b/frontend/src/app/util/array.cljs index fa128c37e4..012b62c0ae 100644 --- a/frontend/src/app/util/array.cljs +++ b/frontend/src/app/util/array.cljs @@ -6,10 +6,46 @@ (ns app.util.array "A collection of helpers for work with javascript arrays." - (:refer-clojure :exclude [conj!])) + (:refer-clojure :exclude [conj! conj filter])) -(defn conj! +(defn conj "A conj like function for js arrays." [a v] - (.push ^js a v) - a) + (js* "[...~{}, ~{}]" a v)) + +(defn conj! + "A conj! like function for js arrays." + ([a v] + (.push ^js a v) + a) + ([a v1 v2] + (.push ^js a v1 v2) + a) + ([a v1 v2 v3] + (.push ^js a v1 v2 v3) + a) + ([a v1 v2 v3 v4] + (.push ^js a v1 v2 v3 v4) + a) + ([a v1 v2 v3 v4 v5] + (.push ^js a v1 v2 v3 v4 v5) + a) + ([a v1 v2 v3 v4 v5 v6] + (.push ^js a v1 v2 v3 v4 v5 v6) + a)) + +(defn normalize-to-array + "If `o` is an array, returns it as-is, if not, wrap into an array." + [o] + (if (array? o) + o + #js [o])) + +(defn without-nils + [^js/Array o] + (.filter o (fn [v] (some? v)))) + +(defn filter + "A specific filter for js arrays." + [pred ^js/Array o] + (.filter o pred)) diff --git a/frontend/src/app/util/avatars.cljs b/frontend/src/app/util/avatars.cljs index 016a1838dd..a72664b9a4 100644 --- a/frontend/src/app/util/avatars.cljs +++ b/frontend/src/app/util/avatars.cljs @@ -11,14 +11,18 @@ (defn generate* [{:keys [name color size] - :or {color "#000000" size 128}}] + :or {size 128}}] (let [parts (str/words (str/upper name)) letters (if (= 1 (count parts)) (ffirst parts) (str (ffirst parts) (first (second parts)))) canvas (.createElement js/document "canvas") - context (.getContext canvas "2d")] + context (.getContext canvas "2d") + text-color (if color + "#2e3434" + "#fff") + color (or color "#000000")] (obj/set! canvas "width" size) (obj/set! canvas "height" size) @@ -28,7 +32,7 @@ (obj/set! context "font" (str (/ size 2) "px Arial")) (obj/set! context "textAlign" "center") - (obj/set! context "fillStyle" "#ffffff") + (obj/set! context "fillStyle" text-color) (.fillText context letters (/ size 2) (/ size 1.5)) (.toDataURL canvas))) diff --git a/frontend/src/app/util/cache.cljs b/frontend/src/app/util/cache.cljs index fcb54c9174..61cd08adcc 100644 --- a/frontend/src/app/util/cache.cljs +++ b/frontend/src/app/util/cache.cljs @@ -7,7 +7,7 @@ (ns app.util.cache (:require [app.util.time :as dt] - [beicon.core :as rx])) + [beicon.v2.core :as rx])) (defonce cache (atom {})) (defonce pending (atom {})) diff --git a/frontend/src/app/util/code_beautify.cljs b/frontend/src/app/util/code_beautify.cljs new file mode 100644 index 0000000000..c18ff885c0 --- /dev/null +++ b/frontend/src/app/util/code_beautify.cljs @@ -0,0 +1,26 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.code-beautify + (:require + ["js-beautify" :as beautify] + [cuerdas.core :as str])) + +(defn format-html + [data] + (beautify/html data #js {:indent_size 2})) + +(defn format-code + [code type] + (let [type (if (keyword? type) (name type) type)] + (cond-> code + (= type "svg") + (-> (str/replace "" "") + (str/replace "><" ">\n<")) + + (or (= type "svg") (= type "html")) + (format-html)))) + diff --git a/frontend/src/app/util/code_gen.cljs b/frontend/src/app/util/code_gen.cljs index f50247b564..1c8e255b9c 100644 --- a/frontend/src/app/util/code_gen.cljs +++ b/frontend/src/app/util/code_gen.cljs @@ -6,351 +6,21 @@ (ns app.util.code-gen (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.pages.helpers :as cph] - [app.common.text :as txt] - [app.common.types.shape.layout :as ctl] - [app.main.ui.formats :as fmt] - [app.util.color :as uc] - [cuerdas.core :as str])) + [app.util.code-gen.markup-html :as html] + [app.util.code-gen.markup-svg :as svg] + [app.util.code-gen.style-css :as css])) -(defn shadow->css [shadow] - (let [{:keys [style offset-x offset-y blur spread]} shadow - css-color (uc/color->background (:color shadow))] - (dm/str - (if (= style :inner-shadow) "inset " "") - (str/fmt "%spx %spx %spx %spx %s" offset-x offset-y blur spread css-color)))) +(defn generate-markup-code + [objects type shapes] + (let [generate-markup + (case type + "html" html/generate-markup + "svg" svg/generate-markup)] + (generate-markup objects shapes))) -(defn fill-color->background - [fill] - (uc/color->background {:color (:fill-color fill) - :opacity (:fill-opacity fill) - :gradient (:fill-color-gradient fill)})) - -(defn format-fill-color [_ shape] - (let [fills (:fills shape) - first-fill (first fills) - colors (if (> (count fills) 1) - (map (fn [fill] - (let [color (fill-color->background fill)] - (if (some? (:fill-color-gradient fill)) - color - (str/format "linear-gradient(%s,%s)" color color)))) - (:fills shape)) - [(fill-color->background first-fill)])] - (str/join ", " colors))) - -(defn format-stroke [_ shape] - (let [first-stroke (first (:strokes shape)) - width (:stroke-width first-stroke) - style (let [style (:stroke-style first-stroke)] - (when (keyword? style) (d/name style))) - color {:color (:stroke-color first-stroke) - :opacity (:stroke-opacity first-stroke) - :gradient (:stroke-color-gradient first-stroke)}] - (when-not (= :none (:stroke-style first-stroke)) - (str/format "%spx %s %s" width style (uc/color->background color))))) - -(defn format-position [_ shape] - (let [relative? (cph/frame-shape? shape) - absolute? (or (empty? (:flex-items shape)) - (and (ctl/any-layout? (:parent shape)) (ctl/layout-absolute? shape)))] - (cond - absolute? "absolute" - relative? "relative" - - ;; static is default value in css - :else nil))) - -(defn get-size - [type values] - (let [value (cond - (number? values) values - (string? values) values - (type values) (type values) - :else (type (:selrect values)))] - - (if (= :width type) - (fmt/format-size :width value values) - (fmt/format-size :heigth value values)))) - -(defn format-border-radius - [values] - - (and (coll? values) (d/not-empty? values)) - (->> values - (map fmt/format-pixels) - (str/join " "))) - -(defn styles-data - [shape] - {:position {:props [:type] - :to-prop {:type "position"} - :format {:type format-position}} - :layout {:props (if (or (empty? (:flex-items shape)) - (ctl/layout-absolute? shape)) - [:width :height :x :y :radius :rx :r1] - [:width :height :radius :rx :r1]) - :to-prop {:x "left" - :y "top" - :rotation "transform" - :rx "border-radius" - :r1 "border-radius"} - :format {:rotation #(str/fmt "rotate(%sdeg)" %) - :r1 format-border-radius - :width #(get-size :width %) - :height #(get-size :height %)} - :multi {:r1 [:r1 :r2 :r3 :r4]}} - :fill {:props [:fills] - :to-prop {:fills (if (> (count (:fills shape)) 1) "background-image" "background-color")} - :format {:fills format-fill-color}} - :stroke {:props [:strokes] - :to-prop {:strokes "border"} - :format {:strokes format-stroke}} - :shadow {:props [:shadow] - :to-prop {:shadow :box-shadow} - :format {:shadow #(str/join ", " (map shadow->css %1))}} - :blur {:props [:blur] - :to-prop {:blur "filter"} - :format {:blur #(str/fmt "blur(%spx)" (:value %))}} - :layout-flex {:props [:layout - :layout-flex-dir - :layout-align-items - :layout-justify-content - :layout-gap - :layout-padding - :layout-wrap-type] - :to-prop {:layout "display" - :layout-flex-dir "flex-direction" - :layout-align-items "align-items" - :layout-justify-content "justify-content" - :layout-wrap-type "flex-wrap" - :layout-gap "gap" - :layout-padding "padding"} - :format {:layout d/name - :layout-flex-dir d/name - :layout-align-items d/name - :layout-justify-content d/name - :layout-wrap-type d/name - :layout-gap fmt/format-gap - :layout-padding fmt/format-padding}}}) - -(def style-text - {:props [:fills - :font-family - :font-style - :font-size - :font-weight - :line-height - :letter-spacing - :text-decoration - :text-transform] - :to-prop {:fills "color"} - :format {:font-family #(dm/str "'" % "'") - :font-style #(dm/str %) - :font-size #(dm/str % "px") - :font-weight #(dm/str %) - :line-height #(dm/str %) - :letter-spacing #(dm/str % "px") - :text-decoration d/name - :text-transform d/name - :fills format-fill-color}}) - -(def layout-flex-item-params - {:props [:layout-item-margin - :layout-item-max-h - :layout-item-min-h - :layout-item-max-w - :layout-item-min-w - :layout-item-align-self] - :to-prop {:layout-item-margin "margin" - :layout-item-max-h "max-height" - :layout-item-min-h "min-height" - :layout-item-max-w "max-width" - :layout-item-min-w "min-width" - :layout-item-align-self "align-self"} - :format {:layout-item-margin fmt/format-margin - :layout-item-max-h #(dm/str % "px") - :layout-item-min-h #(dm/str % "px") - :layout-item-max-w #(dm/str % "px") - :layout-item-min-w #(dm/str % "px") - :layout-item-align-self d/name}}) - -(def layout-align-content - {:props [:layout-align-content] - :to-prop {:layout-align-content "align-content"} - :format {:layout-align-content d/name}}) - -(defn get-specific-value - [values prop] - (let [result (if (get values prop) - (get values prop) - (get (:selrect values) prop)) - result (if (= :width prop) - (get-size :width values) - result) - result (if (= :height prop) - (get-size :height values) - result)] - - result)) - -(defn generate-css-props - ([values properties] - (generate-css-props values properties nil)) - - ([values properties params] - (let [{:keys [to-prop format tab-size multi] - :or {to-prop {} tab-size 0 multi {}}} params - - ;; We allow the :format and :to-prop to be a map for different properties - ;; or just a value for a single property. This code transform a single - ;; property to a uniform one - properties (if-not (coll? properties) [properties] properties) - - format (if (not (map? format)) - (into {} (map #(vector % format) properties)) - format) - - to-prop (if (not (map? to-prop)) - (into {} (map #(vector % to-prop) properties)) - to-prop) - - get-value (fn [prop] - (if-let [props (prop multi)] - (map #(get values %) props) - (get-specific-value values prop))) - - null? (fn [value] - (if (coll? value) - (every? #(or (nil? %) (= % 0)) value) - (or (nil? value) (= value 0)))) - - default-format (fn [value] (dm/str (fmt/format-pixels value))) - format-property (fn [prop] - (let [css-prop (or (prop to-prop) (d/name prop)) - format-fn (or (prop format) default-format) - css-val (format-fn (get-value prop) values)] - (when css-val - (dm/str - (str/repeat " " tab-size) - (str/fmt "%s: %s;" css-prop css-val)))))] - - (->> properties - (remove #(null? (get-value %))) - (map format-property) - (filter (comp not nil?)) - (str/join "\n"))))) - -(defn shape->properties [shape] - (let [;; This property is added in an earlier step (code.cljs), - ;; it will come with a vector of flex-items if any. - ;; If there are none it will continue as usual. - flex-items (:flex-items shape) - props (->> (styles-data shape) vals (mapcat :props)) - to-prop (->> (styles-data shape) vals (map :to-prop) (reduce merge)) - format (->> (styles-data shape) vals (map :format) (reduce merge)) - multi (->> (styles-data shape) vals (map :multi) (reduce merge)) - props (cond-> props - (seq flex-items) (concat (:props layout-flex-item-params)) - (= :wrap (:layout-wrap-type shape)) (concat (:props layout-align-content))) - to-prop (cond-> to-prop - (seq flex-items) (merge (:to-prop layout-flex-item-params)) - (= :wrap (:layout-wrap-type shape)) (merge (:to-prop layout-align-content))) - format (cond-> format - (seq flex-items) (merge (:format layout-flex-item-params)) - (= :wrap (:layout-wrap-type shape)) (merge (:format layout-align-content)))] - (generate-css-props shape props {:to-prop to-prop - :format format - :multi multi - :tab-size 2}))) - -(defn search-text-attrs - [node attrs] - (->> (txt/node-seq node) - (map #(select-keys % attrs)) - (reduce d/merge))) - - -;; TODO: used on inspect -(defn parse-style-text-blocks - [node attrs] - (letfn - [(rec-style-text-map [acc node style] - (let [node-style (merge style (select-keys node attrs)) - head (or (-> acc first) [{} ""]) - [head-style head-text] head - - new-acc - (cond - (:children node) - (reduce #(rec-style-text-map %1 %2 node-style) acc (:children node)) - - (not= head-style node-style) - (cons [node-style (:text node "")] acc) - - :else - (cons [node-style (dm/str head-text "" (:text node))] (rest acc))) - - ;; We add an end-of-line when finish a paragraph - new-acc - (if (= (:type node) "paragraph") - (let [[hs ht] (first new-acc)] - (cons [hs (dm/str ht "\n")] (rest new-acc))) - new-acc)] - new-acc))] - - (-> (rec-style-text-map [] node {}) - reverse))) - -(defn text->properties [shape] - (let [flex-items (:flex-items shape) - text-shape-style (select-keys (styles-data shape) [:layout :shadow :blur]) - - shape-props (->> text-shape-style vals (mapcat :props)) - shape-to-prop (->> text-shape-style vals (map :to-prop) (reduce merge)) - shape-format (->> text-shape-style vals (map :format) (reduce merge)) - - shape-props (cond-> shape-props - (seq flex-items) (concat (:props layout-flex-item-params))) - shape-to-prop (cond-> shape-to-prop - (seq flex-items) (merge (:to-prop layout-flex-item-params))) - shape-format (cond-> shape-format - (seq flex-items) (merge (:format layout-flex-item-params))) - - text-values (->> (search-text-attrs - (:content shape) - (conj (:props style-text) :fill-color-gradient :fill-opacity)) - (d/merge txt/default-text-attrs))] - (str/join - "\n" - [(generate-css-props shape - shape-props - {:to-prop shape-to-prop - :format shape-format - :tab-size 2}) - (generate-css-props text-values - (:props style-text) - {:to-prop (:to-prop style-text) - :format (:format style-text) - :tab-size 2})]))) - -(defn generate-css [shape] - (let [name (:name shape) - properties (if (= :text (:type shape)) - (text->properties shape) - (shape->properties shape)) - selector (str/css-selector name) - selector (if (str/starts-with? selector "-") (subs selector 1) selector)] - (str/join "\n" [(str/fmt "/* %s */" name) - (str/fmt ".%s {" selector) - properties - "}"]))) - -(defn generate-style-code [type shapes] - (let [generate-style-fn (case type - "css" generate-css)] - (->> shapes - (map generate-style-fn) - (str/join "\n\n")))) +(defn generate-style-code + [objects type root-shapes all-shapes] + (let [generate-style + (case type + "css" css/generate-style)] + (generate-style objects root-shapes all-shapes))) diff --git a/frontend/src/app/util/code_gen/common.cljs b/frontend/src/app/util/code_gen/common.cljs new file mode 100644 index 0000000000..7335431502 --- /dev/null +++ b/frontend/src/app/util/code_gen/common.cljs @@ -0,0 +1,66 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.code-gen.common + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.matrix :as gmt] + [app.common.types.shape.layout :as ctl] + [cuerdas.core :as str])) + +(defn shape->selector + [shape] + (if shape + (let [name (-> (:name shape) + (subs 0 (min 10 (count (:name shape)))) + (str/replace #"[^a-zA-Z0-9\s\:]+" "")) + ;; selectors cannot start with numbers + name (if (re-matches #"^\d.*" name) (dm/str "c-" name) name) + id (-> (dm/str (:id shape)) + (subs 24 36)) + selector (str/css-selector (dm/str name " " id)) + selector (if (str/starts-with? selector "-") (subs selector 1) selector)] + selector) + "")) + +(defn svg-markup? + "Function to determine whether a shape is rendered in HTML+CSS or is rendered + through a SVG" + [shape] + (or + ;; path and path-like shapes + (cfh/path-shape? shape) + (cfh/bool-shape? shape) + + ;; imported SVG images + (cfh/svg-raw-shape? shape) + (some? (:svg-attrs shape)) + + ;; CSS masks are not enough we need to delegate to SVG + (cfh/mask-shape? shape) + + ;; Texts with shadows or strokes we render in SVG + (and (cfh/text-shape? shape) + (or (d/not-empty? (:shadow shape)) + (d/not-empty? (:strokes shape)))) + + ;; When a shape has several fills + (> (count (:fills shape)) 1) + + ;; When a shape has several strokes or the stroke is not a "border" + (or (> (count (:strokes shape)) 1) + (and (= (count (:strokes shape)) 1) + (not= (-> shape :strokes first :stroke-alignment) :inner))))) + +(defn has-wrapper? + [objects shape] + ;; Layout children with a transform should be wrapped + (and (ctl/any-layout-immediate-child? objects shape) + (not (ctl/position-absolute? shape)) + (not (gmt/unit? (:transform shape))))) + diff --git a/frontend/src/app/util/code_gen/markup_html.cljs b/frontend/src/app/util/code_gen/markup_html.cljs new file mode 100644 index 0000000000..cb21d00ed7 --- /dev/null +++ b/frontend/src/app/util/code_gen/markup_html.cljs @@ -0,0 +1,93 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.code-gen.markup-html + (:require + ["react-dom/server" :as rds] + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.types.shape.layout :as ctl] + [app.config :as cfg] + [app.main.ui.shapes.text.html-text :as text] + [app.util.code-gen.common :as cgc] + [app.util.code-gen.markup-svg :refer [generate-svg]] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn generate-html + ([objects shape] + (generate-html objects shape 0)) + + ([objects shape level] + (when (and (some? shape) (some? (:selrect shape))) + (let [indent (str/repeat " " level) + + shape-html + (cond + (cgc/svg-markup? shape) + (let [svg-markup (generate-svg objects shape)] + (dm/fmt "%
\n%\n%
" + indent + (dm/str "shape " (d/name (:type shape)) " " + (cgc/shape->selector shape)) + svg-markup + indent)) + + (cfh/text-shape? shape) + (let [text-shape-html (rds/renderToStaticMarkup (mf/element text/text-shape #js {:shape shape :code? true}))] + (dm/fmt "%
\n%\n%
" + indent + (dm/str "shape " (d/name (:type shape)) " " + (cgc/shape->selector shape)) + text-shape-html + indent)) + + (cfh/image-shape? shape) + (let [data (or (:metadata shape) (:fill-image shape)) + image-url (cfg/resolve-file-media data)] + (dm/fmt "%\n%" + indent + image-url + (dm/str "shape " (d/name (:type shape)) " " + (cgc/shape->selector shape)) + indent)) + + (empty? (:shapes shape)) + (dm/fmt "%
\n%
" + indent + (dm/str "shape " (d/name (:type shape)) " " + (cgc/shape->selector shape)) + indent) + + :else + (let [children (->> shape :shapes (map #(get objects %))) + reverse? (ctl/any-layout? shape) + ;; The order for layout elements is the reverse of SVG order + children (cond-> children reverse? reverse)] + (dm/fmt "%
\n%\n%
" + indent + (dm/str (d/name (:type shape)) " " + (cgc/shape->selector shape)) + (->> children + (map #(generate-html objects % (inc level))) + (str/join "\n")) + indent))) + + shape-html + (if (cgc/has-wrapper? objects shape) + (dm/fmt "
%
" + (dm/str (cgc/shape->selector shape) "-wrapper") + shape-html) + + shape-html)] + (dm/fmt "%\n%" indent (dm/str (d/name (:type shape)) ": " (:name shape)) shape-html))))) + +(defn generate-markup + [objects shapes] + (->> shapes + (keep #(generate-html objects %)) + (str/join "\n"))) diff --git a/frontend/src/app/util/code_gen/markup_svg.cljs b/frontend/src/app/util/code_gen/markup_svg.cljs new file mode 100644 index 0000000000..a25177bee2 --- /dev/null +++ b/frontend/src/app/util/code_gen/markup_svg.cljs @@ -0,0 +1,26 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.code-gen.markup-svg + (:require + ["react-dom/server" :as rds] + [app.main.render :as render] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn generate-svg + [objects shape] + (rds/renderToStaticMarkup + (mf/element + render/object-svg + #js {:objects objects + :object-id (-> shape :id)}))) + +(defn generate-markup + [objects shapes] + (->> shapes + (map #(generate-svg objects %)) + (str/join "\n"))) diff --git a/frontend/src/app/util/code_gen/style_css.cljs b/frontend/src/app/util/code_gen/style_css.cljs new file mode 100644 index 0000000000..3a1ace59f6 --- /dev/null +++ b/frontend/src/app/util/code_gen/style_css.cljs @@ -0,0 +1,313 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.code-gen.style-css + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.matrix :as gmt] + [app.common.geom.shapes.bounds :as gsb] + [app.common.geom.shapes.points :as gpo] + [app.common.text :as txt] + [app.common.types.shape.layout :as ctl] + [app.main.ui.shapes.text.styles :as sts] + [app.util.code-gen.common :as cgc] + [app.util.code-gen.style-css-formats :refer [format-value format-shadow]] + [app.util.code-gen.style-css-values :refer [get-value]] + [cuerdas.core :as str])) + +;; +;; Common styles to display always. Will be attached as a prelude to the generated CSS +;; +(def prelude " +html, body { + margin: 0; + min-height: 100%; + min-width: 100%; + padding: 0; +} + +body { + display: flex; + flex-direction: column; + align-items: center; + width: 100vw; + min-height: 100vh; +} + +* { + box-sizing: border-box; +} + +.text-node { background-clip: text !important; -webkit-background-clip: text !important; } + +") + +(def shape-wrapper-css-properties + #{:flex-shrink + :margin + :max-height + :min-height + :max-width + :min-width + :align-self + :justify-self + :grid-column + :grid-row + :z-index + :top + :left + :position}) + +(def shape-css-properties + [:position + :left + :top + :width + :height + :transform + :background + :border + :border-radius + :box-shadow + :filter + :opacity + :overflow + + ;; Flex/grid related properties + :display + :align-items + :align-content + :justify-items + :justify-content + :gap + :column-gap + :row-gap + :padding + :z-index + + ;; Flex related properties + :flex-direction + :flex-wrap + :flex + :flex-grow + + ;; Grid related properties + :grid-template-rows + :grid-template-columns + :grid-template-areas + :grid-auto-flow + + ;; Flex/grid self properties + :flex-shrink + :margin + :max-height + :min-height + :max-width + :min-width + :align-self + :justify-self + + ;; Grid cell properties + :grid-column + :grid-row + :grid-area]) + +(defn shape->css-property + [shape objects property options] + (when-let [value (get-value property shape objects options)] + [property value])) + +(defn shape->wrapper-css-properties + [shape objects] + (when (and (ctl/any-layout-immediate-child? objects shape) + (not (gmt/unit? (:transform shape)))) + (let [parent (get objects (:parent-id shape)) + bounds (gpo/parent-coords-bounds (:points shape) (:points parent)) + width (gpo/width-points bounds) + height (gpo/height-points bounds)] + (cond-> [[:width width] + [:height height]] + + (or (not (ctl/any-layout-immediate-child? objects shape)) + (not (ctl/position-absolute? shape))) + (conj [:position "relative"]))))) + +(defn shape->wrapper-child-css-properties + [shape objects] + (when (and (ctl/any-layout-immediate-child? objects shape) (not (gmt/unit? (:transform shape)))) + [[:position "absolute"] + [:left "50%"] + [:top "50%"]])) + +(defn shape->svg-props + [shape objects] + (let [bounds (gsb/get-object-bounds objects shape)] + [[:position "absolute"] + [:top 0] + [:left 0] + [:transform (dm/fmt "translate(%,%)" + (dm/str (- (:x bounds) (-> shape :selrect :x)) "px") + (dm/str (- (:y bounds) (-> shape :selrect :y)) "px"))]])) + +(defn shape->css-properties + "Given a shape extract the CSS properties in the format of list [property value]" + [shape objects properties options] + (->> properties + (keep (fn [property] + (when-let [value (get-value property shape objects options)] + [property value]))))) + +(defn format-css-value + ([[property value] options] + (format-css-value property value options)) + + ([property value options] + (when (some? value) + (format-value property value options)))) + +(defn format-css-property + [[property value] options] + (when (some? value) + (let [formatted-value (format-css-value property value options)] + (dm/fmt "%: %;" (d/name property) formatted-value)))) + +(defn format-css-properties + "Format a list of [property value] into a list of css properties in the format 'property: value;'" + [properties options] + (when properties + (->> properties + (map #(dm/str " " (format-css-property % options))) + (str/join "\n")))) + +(defn get-shape-properties-css + ([objects shape properties] + (get-shape-properties-css objects shape properties nil)) + + ([objects shape properties options] + (-> shape + (shape->css-properties objects properties options) + (format-css-properties options)))) + +(defn format-js-styles + [properties _options] + (format-css-properties + (->> (.keys js/Object properties) + (remove #(str/starts-with? % "--")) + (mapv (fn [key] + [(str/kebab key) (unchecked-get properties key)]))) + nil)) + +(defn node->css + [shape shape-selector node] + (let [properties + (case (:type node) + (:root "root") + (sts/generate-root-styles shape node true) + + (:paragraph-set "paragraph-set") + (sts/generate-paragraph-set-styles shape) + + (:paragraph "paragraph") + (sts/generate-paragraph-styles shape node) + + (sts/generate-text-styles shape node))] + (dm/fmt + ".% {\n%\n}" + (dm/str shape-selector " ." (:$id node)) + (format-js-styles properties nil)))) + +(defn generate-text-css + [shape] + (let [selector (cgc/shape->selector shape)] + (->> shape + :content + (txt/index-content) + (txt/node-seq) + (map #(node->css shape selector %)) + (str/join "\n")))) + +(defn get-shape-css-selector + ([objects shape] + (get-shape-css-selector shape objects nil)) + + ([shape objects options] + (when (and (some? shape) (some? (:selrect shape))) + (let [selector (cgc/shape->selector shape) + + wrapper? (cgc/has-wrapper? objects shape) + svg? (cgc/svg-markup? shape) + + css-properties + (if wrapper? + (filter (complement shape-wrapper-css-properties) shape-css-properties) + shape-css-properties) + + properties + (-> shape + (shape->css-properties objects css-properties options) + (format-css-properties options)) + + wrapper-properties + (when wrapper? + (-> (d/concat-vec + (shape->css-properties shape objects shape-wrapper-css-properties options) + (shape->wrapper-css-properties shape objects)) + (format-css-properties options))) + + wrapper-child-properties + (when wrapper? + (-> shape + (shape->wrapper-child-css-properties objects) + (format-css-properties options))) + + svg-child-props + (when svg? + (-> shape + (shape->svg-props objects) + (format-css-properties options)))] + + (str/join + "\n" + (filter some? [(str/fmt "/* %s */" (:name shape)) + (when wrapper? (str/fmt ".%s-wrapper {\n%s\n}" selector wrapper-properties)) + (when wrapper? (str/fmt ".%s-wrapper > * {\n%s\n}" selector wrapper-child-properties)) + (when svg? (str/fmt ".%s > svg {\n%s\n}" selector svg-child-props)) + (str/fmt ".%s {\n%s\n}" selector properties) + (when (cfh/text-shape? shape) (generate-text-css shape))])))))) + +(defn get-css-property + ([objects shape property] + (get-css-property objects shape property nil)) + + ([objects shape property options] + (-> shape + (shape->css-property objects property options) + (format-css-property options)))) + +(defn get-css-value + ([objects shape property] + (get-css-value objects shape property nil)) + + ([objects shape property options] + (when-let [prop (shape->css-property shape objects property options)] + (format-css-value prop options)))) + +(defn generate-style + ([objects root-shapes all-shapes] + (generate-style objects root-shapes all-shapes nil)) + ([objects root-shapes all-shapes options] + (let [options (assoc options :root-shapes (into #{} (map :id) root-shapes))] + (dm/str + prelude + (->> all-shapes + (keep #(get-shape-css-selector % objects options)) + (str/join "\n\n")))))) + +(defn shadow->css + [shadow] + (dm/str "box-shadow: " (format-shadow shadow {}))) diff --git a/frontend/src/app/util/code_gen/style_css_formats.cljs b/frontend/src/app/util/code_gen/style_css_formats.cljs new file mode 100644 index 0000000000..0a9cdd5157 --- /dev/null +++ b/frontend/src/app/util/code_gen/style_css_formats.cljs @@ -0,0 +1,157 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.code-gen.style-css-formats + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.config :as cfg] + [app.main.ui.formats :as fmt] + [app.util.color :as uc] + [cuerdas.core :as str])) + +(def css-formatters + {:left :position + :top :position + :width :size + :height :size + :min-width :size + :min-height :size + :max-width :size + :max-height :size + :background :color + :border :border + :border-radius :string-or-size-array + :box-shadow :shadows + :filter :blur + :gap :size-array + :row-gap :size-array + :column-gap :size-array + :padding :size-array + :margin :size-array + :grid-template-rows :tracks + :grid-template-columns :tracks}) + +(defmulti format-value + (fn [property _value _options] (css-formatters property))) + +(defmethod format-value :position + [_ value _options] + (cond + (number? value) (fmt/format-pixels value) + :else value)) + +(defmethod format-value :size + [_ value _options] + (cond + (= value :fill) "100%" + (= value :auto) "auto" + (number? value) (fmt/format-pixels value) + :else value)) + +(defn format-color + [value _options] + (cond + (:image value) + (let [image-url (cfg/resolve-file-media (:image value)) + opacity-color (when (not= (:opacity value) 1) + (uc/gradient->css {:type :linear + :stops [{:color "#FFFFFF" :opacity (:opacity value)} + {:color "#FFFFFF" :opacity (:opacity value)}]}))] + (if opacity-color + ;; CSS doesn't allow setting directly opacity to background image, we should add a dummy gradient to get it + (dm/fmt "%, url(%) no-repeat center center / cover" opacity-color image-url) + (dm/fmt "url(%) no-repeat center center / cover" image-url))) + + (not= (:opacity value) 1) + (uc/color->background value) + + :else + (str/upper (:color value)))) + +(defmethod format-value :color + [_ value options] + (format-color value options)) + +(defmethod format-value :color-array + [_ value options] + (->> value + (map #(format-color % options)) + (str/join ", "))) + +(defmethod format-value :border + [_ {:keys [color style width]} options] + (dm/fmt "% % %" + (fmt/format-pixels width) + (d/name style) + (format-color color options))) + +(defmethod format-value :size-array + [_ value _options] + (cond + (and (coll? value) (d/not-empty? value)) + (->> value + (map fmt/format-pixels) + (str/join " ")) + + (some? value) + value)) + +(defmethod format-value :string-or-size-array + [_ value _] + (cond + (string? value) + value + + (and (coll? value) (d/not-empty? value)) + (->> value + (map fmt/format-pixels) + (str/join " ")) + + (some? value) + value)) + +(defmethod format-value :keyword + [_ value _options] + (d/name value)) + +(defmethod format-value :tracks + [_ value _options] + (->> value + (map (fn [{:keys [type value]}] + (case type + :flex (dm/str (fmt/format-number value) "fr") + :percent (fmt/format-percent (/ value 100)) + :auto "auto" + (fmt/format-pixels value)))) + (str/join " "))) + +(defn format-shadow + [{:keys [style offset-x offset-y blur spread color]} options] + (let [css-color (format-color color options)] + (dm/str + (if (= style :inner-shadow) "inset " "") + (str/fmt "%spx %spx %spx %spx %s" offset-x offset-y blur spread css-color)))) + +(defmethod format-value :shadows + [_ value options] + (->> value + (map #(format-shadow % options)) + (str/join ", "))) + +(defmethod format-value :blur + [_ value _options] + (dm/fmt "blur(%)" (fmt/format-pixels value))) + +(defmethod format-value :matrix + [_ value _options] + (fmt/format-matrix value)) + +(defmethod format-value :default + [_ value _options] + (if (keyword? value) + (d/name value) + value)) diff --git a/frontend/src/app/util/code_gen/style_css_values.cljs b/frontend/src/app/util/code_gen/style_css_values.cljs new file mode 100644 index 0000000000..fe2179c8d4 --- /dev/null +++ b/frontend/src/app/util/code_gen/style_css_values.cljs @@ -0,0 +1,426 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + + +(ns app.util.code-gen.style-css-values + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.geom.matrix :as gmt] + [app.common.geom.shapes :as gsh] + [app.common.types.shape.layout :as ctl] + [app.main.ui.formats :as fmt] + [app.util.code-gen.common :as cgc] + [cuerdas.core :as str])) + +(defn fill->color + [{:keys [fill-color fill-opacity fill-color-gradient fill-image]}] + {:color fill-color + :opacity fill-opacity + :gradient fill-color-gradient + :image fill-image}) + +(defmulti get-value + (fn [property _shape _objects _options] property)) + +(defmethod get-value :position + [_ shape objects _] + (cond + (or (and (ctl/any-layout-immediate-child? objects shape) + (not (ctl/position-absolute? shape)) + (or (cfh/group-like-shape? shape) + (cfh/frame-shape? shape) + (cgc/svg-markup? shape))) + (cfh/root-frame? shape)) + :relative + + (and (ctl/any-layout-immediate-child? objects shape) + (not (ctl/position-absolute? shape))) + nil + + :else + :absolute)) + +(defn get-shape-position + [shape objects coord] + + (when (and (not (cfh/root-frame? shape)) + (or (not (ctl/any-layout-immediate-child? objects shape)) + (ctl/position-absolute? shape))) + + (let [parent (get objects (:parent-id shape)) + + parent-value (dm/get-in parent [:selrect coord]) + + [selrect _ _] + (-> (:points shape) + (gsh/transform-points (gsh/shape->center parent) (:transform-inverse parent (gmt/matrix))) + (gsh/calculate-geometry)) + + shape-value (get selrect coord)] + (- shape-value parent-value)))) + +(defmethod get-value :left + [_ shape objects _] + (get-shape-position shape objects :x)) + +(defmethod get-value :top + [_ shape objects _] + (get-shape-position shape objects :y)) + +(defmethod get-value :flex + [_ shape objects _] + (let [parent (cfh/get-parent objects (:id shape))] + (when (and (ctl/flex-layout-immediate-child? objects shape) + (or (and (contains? #{:row :row-reverse} (:layout-flex-dir parent)) + (= :fill (:layout-item-h-sizing shape))) + (and (contains? #{:column :column-reverse} (:layout-flex-dir parent)) + (= :fill (:layout-item-v-sizing shape))))) + 1))) + +(defn get-shape-size + [shape objects type] + (let [parent (cfh/get-parent objects (:id shape)) + sizing (if (= type :width) + (:layout-item-h-sizing shape) + (:layout-item-v-sizing shape))] + (cond + (and (ctl/flex-layout-immediate-child? objects shape) + (or (and (= type :height) + (contains? #{:row :row-reverse} (:layout-flex-dir parent)) + (= :fill (:layout-item-v-sizing shape))) + (and (= type :width) + (contains? #{:column :column-reverse} (:layout-flex-dir parent)) + (= :fill (:layout-item-h-sizing shape))))) + :fill + + (and (ctl/flex-layout-immediate-child? objects shape) (= sizing :fill)) + nil + + (or (and (ctl/any-layout? shape) (= sizing :auto) (not (cgc/svg-markup? shape))) + (and (ctl/grid-layout-immediate-child? objects shape) (= sizing :fill))) + sizing + + (some? (:selrect shape)) + (-> shape :selrect type) + + (some? (get shape type)) + (get shape type)))) + +(defmethod get-value :width + [_ shape objects options] + (let [root? (contains? (:root-shapes options) (:id shape))] + (if (and root? (ctl/any-layout? shape)) + :fill + (get-shape-size shape objects :width)))) + +(defmethod get-value :height + [_ shape objects options] + (let [root? (contains? (:root-shapes options) (:id shape))] + (when-not (and root? (ctl/any-layout? shape)) + (get-shape-size shape objects :height)))) + +(defmethod get-value :flex-grow + [_ shape _ options] + (let [root? (contains? (:root-shapes options) (:id shape))] + (when (and root? (ctl/any-layout? shape)) + 1))) + +(defmethod get-value :transform + [_ shape objects _] + (if (cgc/svg-markup? shape) + (let [parent (get objects (:parent-id shape)) + transform + (:transform-inverse parent (gmt/matrix)) + + transform-str (when-not (gmt/unit? transform) (fmt/format-matrix transform))] + + (if (cgc/has-wrapper? objects shape) + (dm/str "translate(-50%, -50%) " (d/nilv transform-str "")) + transform-str)) + + (let [parent (get objects (:parent-id shape)) + + transform + (gmt/multiply (:transform shape (gmt/matrix)) + (:transform-inverse parent (gmt/matrix))) + + transform-str (when-not (gmt/unit? transform) (fmt/format-matrix transform))] + + (if (cgc/has-wrapper? objects shape) + (dm/str "translate(-50%, -50%) " (d/nilv transform-str "")) + transform-str)))) + +(defmethod get-value :background + [_ {:keys [fills] :as shape} _ _] + (let [single-fill? (= (count fills) 1)] + (when (and (not (cgc/svg-markup? shape)) (not (cfh/group-shape? shape)) single-fill?) + (fill->color (first fills))))) + +(defn get-stroke-data + [stroke] + (let [width (:stroke-width stroke) + style (:stroke-style stroke) + color {:color (:stroke-color stroke) + :opacity (:stroke-opacity stroke) + :gradient (:stroke-color-gradient stroke)}] + + (when (and (some? stroke) (not= :none (:stroke-style stroke))) + {:color color + :style style + :width width}))) + +(defmethod get-value :border + [_ shape _ _] + (when-not (cgc/svg-markup? shape) + (get-stroke-data (first (:strokes shape))))) + +(defmethod get-value :border-radius + [_ {:keys [rx r1 r2 r3 r4] :as shape} _ _] + (cond + (cfh/circle-shape? shape) + "50%" + + (some? rx) + [rx] + + (every? some? [r1 r2 r3 r4]) + [r1 r2 r3 r4])) + +(defmethod get-value :box-shadow + [_ shape _ _] + (when-not (cgc/svg-markup? shape) + (:shadow shape))) + +(defmethod get-value :filter + [_ shape _ _] + (when-not (cgc/svg-markup? shape) + (get-in shape [:blur :value]))) + +(defmethod get-value :display + [_ shape _ _] + (cond + (:hidden shape) "none" + (ctl/flex-layout? shape) "flex" + (ctl/grid-layout? shape) "grid")) + +(defmethod get-value :opacity + [_ shape _ _] + (when (< (:opacity shape) 1) + (:opacity shape))) + +(defmethod get-value :overflow + [_ shape _ _] + (when (and (cfh/frame-shape? shape) + (not (cgc/svg-markup? shape)) + (not (:show-content shape))) + "hidden")) + +(defmethod get-value :flex-direction + [_ shape _ _] + (:layout-flex-dir shape)) + +(defmethod get-value :align-items + [_ shape _ _] + (:layout-align-items shape)) + +(defmethod get-value :align-content + [_ shape _ _] + (:layout-align-content shape)) + +(defmethod get-value :justify-items + [_ shape _ _] + (:layout-justify-items shape)) + +(defmethod get-value :justify-content + [_ shape _ _] + (:layout-justify-content shape)) + +(defmethod get-value :flex-wrap + [_ shape _ _] + (:layout-wrap-type shape)) + +(defmethod get-value :gap + [_ shape _ _] + (let [[g1 g2] (ctl/gaps shape)] + (when (and (= g1 g2) (or (not= g1 0) (not= g2 0))) + [g1]))) + +(defmethod get-value :row-gap + [_ shape _ _] + (let [[g1 g2] (ctl/gaps shape)] + (when (and (not= g1 g2) (not= g1 0)) [g1]))) + +(defmethod get-value :column-gap + [_ shape _ _] + (let [[g1 g2] (ctl/gaps shape)] + (when (and (not= g1 g2) (not= g2 0)) [g2]))) + +(defmethod get-value :padding + [_ {:keys [layout-padding]} _ _] + (when (some? layout-padding) + (let [default-padding {:p1 0 :p2 0 :p3 0 :p4 0} + {:keys [p1 p2 p3 p4]} (merge default-padding layout-padding)] + (when (or (not= p1 0) (not= p2 0) (not= p3 0) (not= p4 0)) + [p1 p2 p3 p4])))) + +(defmethod get-value :grid-template-rows + [_ shape _ _] + (:layout-grid-rows shape)) + +(defmethod get-value :grid-template-columns + [_ shape _ _] + (:layout-grid-columns shape)) + +(defn area-cell? + [{:keys [position area-name]}] + (and (= position :area) (d/not-empty? area-name))) + +(defmethod get-value :grid-template-areas + [_ shape _ _] + (when (and (ctl/grid-layout? shape) + (some area-cell? (vals (:layout-grid-cells shape)))) + (let [result + (->> (d/enumerate (:layout-grid-rows shape)) + (map + (fn [[row _]] + (dm/str + "\"" + (->> (d/enumerate (:layout-grid-columns shape)) + (map (fn [[column _]] + (let [cell (ctl/get-cell-by-position shape (inc row) (inc column))] + (str/replace (:area-name cell ".") " " "-")))) + (str/join " ")) + "\""))) + (str/join "\n"))] + result))) + +(defn get-grid-coord + [shape objects prop span-prop] + (when (and (ctl/grid-layout-immediate-child? objects shape) + (not (ctl/position-absolute? shape))) + (let [parent (get objects (:parent-id shape)) + cell (ctl/get-cell-by-shape-id parent (:id shape))] + (when (and + (not (and (= (:position cell) :area) (d/not-empty? (:area-name cell)))) + (or (= (:position cell) :manual) + (> (:row-span cell) 1) + (> (:column-span cell) 1))) + (if (> (get cell span-prop) 1) + (dm/str (get cell prop) " / " (+ (get cell prop) (get cell span-prop))) + (get cell prop)))))) + +(defmethod get-value :grid-column + [_ shape objects _] + (get-grid-coord shape objects :column :column-span)) + +(defmethod get-value :grid-row + [_ shape objects _] + (get-grid-coord shape objects :row :row-span)) + +(defmethod get-value :grid-area + [_ shape objects _] + (when (and (ctl/grid-layout-immediate-child? objects shape) + (not (ctl/position-absolute? shape))) + (let [parent (get objects (:parent-id shape)) + cell (ctl/get-cell-by-shape-id parent (:id shape))] + (when (and (= (:position cell) :area) (d/not-empty? (:area-name cell))) + (str/replace (:area-name cell) " " "-"))))) + +(defmethod get-value :flex-shrink + [_ shape objects _] + (when (and (ctl/flex-layout-immediate-child? objects shape) + + (not (and (contains? #{:row :reverse-row} (:layout-flex-dir shape)) + (= :fill (:layout-item-h-sizing shape)))) + + (not (and (contains? #{:column :column-row} (:layout-flex-dir shape)) + (= :fill (:layout-item-v-sizing shape)))) + + ;;(not= :fill (:layout-item-h-sizing shape)) + ;;(not= :fill (:layout-item-v-sizing shape)) + (not= :auto (:layout-item-h-sizing shape)) + (not= :auto (:layout-item-v-sizing shape))) + 0)) + +(defmethod get-value :margin + [_ {:keys [layout-item-margin] :as shape} objects _] + + (when (ctl/any-layout-immediate-child? objects shape) + (let [default-margin {:m1 0 :m2 0 :m3 0 :m4 0} + {:keys [m1 m2 m3 m4]} (merge default-margin layout-item-margin)] + (when (or (not= m1 0) (not= m2 0) (not= m3 0) (not= m4 0)) + [m1 m2 m3 m4])))) + +(defmethod get-value :z-index + [_ {:keys [layout-item-z-index] :as shape} objects _] + (cond + (cfh/root-frame? shape) + 0 + + (ctl/any-layout-immediate-child? objects shape) + layout-item-z-index)) + +(defmethod get-value :max-height + [_ shape objects _] + (cond + (ctl/any-layout-immediate-child? objects shape) + (:layout-item-max-h shape))) + +(defmethod get-value :min-height + [_ shape objects _] + (cond + (and (ctl/any-layout-immediate-child? objects shape) (some? (:layout-item-min-h shape))) + (:layout-item-min-h shape) + + (and (ctl/auto-height? shape) (cfh/frame-shape? shape) (not (:show-content shape))) + (-> shape :selrect :height))) + +(defmethod get-value :max-width + [_ shape objects _] + (cond + (ctl/any-layout-immediate-child? objects shape) + (:layout-item-max-w shape))) + +(defmethod get-value :min-width + [_ shape objects _] + (cond + (and (ctl/any-layout-immediate-child? objects shape) (some? (:layout-item-min-w shape))) + (:layout-item-min-w shape) + + (and (ctl/auto-width? shape) (cfh/frame-shape? shape) (not (:show-content shape))) + (-> shape :selrect :width))) + +(defmethod get-value :align-self + [_ shape objects _] + (cond + (ctl/flex-layout-immediate-child? objects shape) + (:layout-item-align-self shape) + + (ctl/grid-layout-immediate-child? objects shape) + (let [parent (get objects (:parent-id shape)) + cell (ctl/get-cell-by-shape-id parent (:id shape)) + align-self (:align-self cell)] + (when (not= align-self :auto) align-self)))) + +(defmethod get-value :justify-self + [_ shape objects _] + (cond + (ctl/grid-layout-immediate-child? objects shape) + (let [parent (get objects (:parent-id shape)) + cell (ctl/get-cell-by-shape-id parent (:id shape)) + justify-self (:justify-self cell)] + (when (not= justify-self :auto) justify-self)))) + +(defmethod get-value :grid-auto-flow + [_ shape _ _] + (when (and (ctl/grid-layout? shape) (= (:layout-grid-dir shape) :column)) + "column")) + +(defmethod get-value :default + [property shape _ _] + (get shape property)) diff --git a/frontend/src/app/util/code_highlight.cljs b/frontend/src/app/util/code_highlight.cljs new file mode 100644 index 0000000000..62c7617202 --- /dev/null +++ b/frontend/src/app/util/code_highlight.cljs @@ -0,0 +1,15 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.code-highlight + (:require + ["highlight.js" :as hljs] + [app.util.dom :as dom])) + +(defn highlight! + [node] + (dom/set-data! node "highlighted" nil) + (hljs/highlightElement node)) diff --git a/frontend/src/app/util/color.cljs b/frontend/src/app/util/color.cljs index 5b8ca29fdd..aeb95d007b 100644 --- a/frontend/src/app/util/color.cljs +++ b/frontend/src/app/util/color.cljs @@ -5,125 +5,22 @@ ;; Copyright (c) KALEIDOS INC (ns app.util.color - "Color conversion utils." + "FIXME: this is legacy namespace, all functions of this ns should be + relocated under app.common.types on the respective colors related + namespace. All generic color conversion and other helpers are moved to + app.common.colors namespace." (:require + [app.common.colors :as cc] [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.math :as mth] [app.util.i18n :as i18n :refer [tr]] - [app.util.object :as obj] - [app.util.strings :as ust] - [cuerdas.core :as str] - [goog.color :as gcolor])) - -(defn rgb->str - [color] - {:pre [(vector? color)]} - (if (= (count color) 3) - (apply str/format "rgb(%s,%s,%s)" color) - (apply str/format "rgba(%s,%s,%s,%s)" color))) - -(defn rgb->hsv - [[r g b]] - (into [] (gcolor/rgbToHsv r g b))) - -(defn hsv->rgb - [[h s v]] - (into [] (gcolor/hsvToRgb h s v))) - -(defn hex->rgb - [v] - (try - (into [] (gcolor/hexToRgb v)) - (catch :default _e [0 0 0]))) - -(defn rgb->hex - [[r g b]] - (gcolor/rgbToHex r g b)) - -(defn hex->hsv - [v] - (into [] (gcolor/hexToHsv v))) - -(defn hex->rgba - [^string data ^number opacity] - (-> (hex->rgb data) - (conj opacity))) - -(defn hex->hsl [hex] - (try - (into [] (gcolor/hexToHsl hex)) - (catch :default _e [0 0 0]))) - -(defn hex->hsla - [^string data ^number opacity] - (-> (hex->hsl data) - (conj opacity))) - -(defn format-hsla - [[h s l a]] - (let [precision 2 - rounded-s (* 100 (ust/format-precision s precision)) - rounded-l (* 100 (ust/format-precision l precision))] - - (str/fmt "%s, %s%, %s%, %s" h rounded-s rounded-l a))) - -(defn hsl->rgb - [[h s l]] - (gcolor/hslToRgb h s l)) - -(defn hsl->hex - [[h s l]] - (gcolor/hslToHex h s l)) - -(defn hex? - [v] - (and (string? v) - (re-seq #"^#[0-9A-Fa-f]{6}$" v))) - -(defn hsl->hsv - [[h s l]] - (gcolor/hslToHsv h s l)) - -(defn hsv->hex - [[h s v]] - (gcolor/hsvToHex h s v)) - -(defn hsv->hsl - [hsv] - (hex->hsl (hsv->hex hsv))) - -(defn expand-hex - [v] - (cond - (re-matches #"^[0-9A-Fa-f]$" v) - (str v v v v v v) - - (re-matches #"^[0-9A-Fa-f]{2}$" v) - (str v v v) - - (re-matches #"^[0-9A-Fa-f]{3}$" v) - (let [a (nth v 0) - b (nth v 1) - c (nth v 2)] - (str a a b b c c)) - - :else - v)) - -(defn prepend-hash - [color] - (gcolor/prependHashIfNecessaryHelper color)) - -(defn remove-hash - [color] - (if (str/starts-with? color "#") - (subs color 1) - color)) + [cuerdas.core :as str])) (defn gradient->css [{:keys [type stops]}] (let [parse-stop (fn [{:keys [offset color opacity]}] - (let [[r g b] (hex->rgb color)] + (let [[r g b] (cc/hex->rgb color)] (str/fmt "rgba(%s, %s, %s, %s) %s" r g b opacity (str (* offset 100) "%")))) stops-css (str/join "," (map parse-stop stops))] @@ -140,14 +37,19 @@ ;; TODO: REMOVE `VALUE` WHEN COLOR IS INTEGRATED (defn color->background [{:keys [color opacity gradient value]}] - (let [color (or color value) + (let [color (d/nilv color value) opacity (or opacity 1)] + (cond (and gradient (not= :multiple gradient)) (gradient->css gradient) - (not= color :multiple) - (let [[r g b] (hex->rgb (or color value))] + (and (some? color) (not= color :multiple)) + (let [color + (-> (str/replace color "#" "") + (cc/expand-hex) + (cc/prepend-hash)) + [r g b] (cc/hex->rgb color)] (str/fmt "rgba(%s, %s, %s, %s)" r g b opacity)) :else "transparent"))) @@ -160,55 +62,26 @@ (not= color :multiple) (case format - :rgba (let [[r g b] (hex->rgb color)] - (str/fmt "rgba(%s, %s, %s, %s)" r g b opacity)) + :rgba (let [[r g b] (cc/hex->rgb color)] + (str/fmt "rgba(%s, %s, %s, %s)" r g b opacity)) - :hsla (let [[h s l] (hex->hsl color)] + :hsla (let [[h s l] (cc/hex->hsl color)] (str/fmt "hsla(%s, %s, %s, %s)" h (* 100 s) (* 100 l) opacity)) :hex (str color (str/upper (d/opacity-to-hex opacity)))) :else "transparent"))) -(defn multiple? [{:keys [id file-id value color gradient]}] +(defn multiple? + [{:keys [id file-id value color gradient]}] (or (= value :multiple) (= color :multiple) (= gradient :multiple) (= id :multiple) (= file-id :multiple))) -(defn color? - [color] - (and (string? color) - (gcolor/isValidColor color))) - -(defn parse-color - [color] - (when (color? color) - (let [result (gcolor/parse color)] - (dm/str (.-hex ^js result))))) - -(def color-names - (obj/get-keys ^js gcolor/names)) - (def empty-color - (into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity])) - -(defn next-rgb - "Given a color in rgb returns the next color" - [[r g b]] - (cond - (and (= 255 r) (= 255 g) (= 255 b)) - (throw (ex-info "cannot get next color" {:r r :g g :b b})) - - (and (= 255 g) (= 255 b)) - [(inc r) 0 0] - - (= 255 b) - [r (inc g) 0] - - :else - [r g (inc b)])) + (into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity :image])) (defn get-color-name [color] @@ -216,3 +89,10 @@ (:name color) (:color color) (gradient-type->string (:type (:gradient color))))) + +(defn random-color + [] + (dm/fmt "rgb(%, %, %)" + (mth/floor (* (js/Math.random) 256)) + (mth/floor (* (js/Math.random) 256)) + (mth/floor (* (js/Math.random) 256)))) diff --git a/frontend/src/app/util/css.cljs b/frontend/src/app/util/css.cljs index c0fbae2b66..5681bd3077 100644 --- a/frontend/src/app/util/css.cljs +++ b/frontend/src/app/util/css.cljs @@ -32,6 +32,6 @@ (dom/set-attribute! style "type" "text/css") (dom/append-child! js/document.head style) (wrap-style-sheet style))))) - + diff --git a/frontend/src/app/util/debug.cljs b/frontend/src/app/util/debug.cljs new file mode 100644 index 0000000000..094550cef4 --- /dev/null +++ b/frontend/src/app/util/debug.cljs @@ -0,0 +1,111 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.debug) + +(defonce state (atom #{#_:events})) + +(def options + #{;; Displays the bounding box for the shapes + :bounding-boxes + + ;; Displays an overlay over the groups + :group + + ;; Displays in the console log the events through the application + :events + + ;; Display the boxes that represent the rotation and resize handlers + :handlers + + ;; Displays the center of a selection + :selection-center + + ;; When active the single selection will not take into account previous transformations + ;; this is useful to debug transforms + :simple-selection + + ;; When active the thumbnails will be displayed with a sepia filter + :thumbnails + + ;; When active we can check in the browser the export values + :show-export-metadata + + ;; Show text fragments outlines + :text-outline + + ;; Disable thumbnail cache + :disable-thumbnail-cache + + ;; Disable frame thumbnails + :disable-frame-thumbnails + + ;; Force thumbnails always (independent of selection or zoom level) + :force-frame-thumbnails + + ;; Enable a widget to show the auto-layout drop-zones + :layout-drop-zones + + ;; Display the layout lines + :layout-lines + + ;; Display the bounds for the hug content adjust + :layout-content-bounds + + ;; Makes the pixel grid red so its more visibile + :pixel-grid + + ;; Show the bounds relative to the parent + :parent-bounds + + ;; Show html text + :html-text + + ;; Show history overlay + :history-overlay + + ;; Show shape name and id + :shape-titles + + ;; Show an asterisk for touched copies + :show-touched + + ;; Show the id with the name + :show-ids + + ;; + :grid-layout + + ;; Show an overlay to the grid cells to know its properties + :grid-cells + + ;; Show info about shapes + :shape-panel + + ;; Show what is touched in copies + :display-touched + + ;; Show some visual indicators for bool shape + :bool-shapes}) + +(defn enable! + [option] + (swap! state conj option)) + +(defn disable! + [option] + (swap! state disj option)) + +(defn enabled? + ^boolean + [option] + (contains? @state option)) + +(defn toggle! + [option] + (if (enabled? option) + (disable! option) + (enable! option))) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index d02d96780c..01d34e5823 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.logging :as log] [app.common.media :as cm] [app.util.globals :as globals] @@ -16,7 +17,22 @@ [app.util.webapi :as wapi] [cuerdas.core :as str] [goog.dom :as dom] - [promesa.core :as p])) + [promesa.core :as p]) + (:import goog.events.BrowserEvent)) + +(extend-type BrowserEvent + cljs.core/IDeref + (-deref [it] (.getBrowserEvent it))) + +(declare get-window-size) + +(defn browser-event? + [o] + (instance? BrowserEvent o)) + +(defn native-event? + [o] + (instance? js/Event o)) (log/set-level! :warn) @@ -311,6 +327,11 @@ (.removeChild ^js el child)) el) +(defn remove! + [^js el] + (when (some? el) + (.remove ^js el))) + (defn get-first-child [^js el] (when (some? el) @@ -367,11 +388,19 @@ y (.-offsetY event)] (gpt/point x y)))) +(defn get-delta-position + [event] + (let [e (if (browser-event? event) + (deref event) + event) + x (.-deltaX ^js e) + y (.-deltaY ^js e)] + (gpt/point x y))) + (defn get-client-size [^js node] (when (some? node) - {:width (.-clientWidth ^js node) - :height (.-clientHeight ^js node)})) + (grc/make-rect 0 0 (.-clientWidth node) (.-clientHeight node)))) (defn get-bounding-rect [node] @@ -383,19 +412,47 @@ :width (.-width ^js rect) :height (.-height ^js rect)})) +(defn is-bounding-rect-outside? + [rect] + (let [{:keys [left top right bottom]} rect + {:keys [width height]} (get-window-size)] + (or (< left 0) + (< top 0) + (> right width) + (> bottom height)))) + +(defn is-element-outside? + [element] + (is-bounding-rect-outside? (get-bounding-rect element))) + (defn bounding-rect->rect [rect] (when (some? rect) - {:x (or (.-left rect) (:left rect) 0) - :y (or (.-top rect) (:top rect) 0) - :width (or (.-width rect) (:width rect) 1) - :height (or (.-height rect) (:height rect) 1)})) + (grc/make-rect + (or (.-left rect) (:left rect) 0) + (or (.-top rect) (:top rect) 0) + (or (.-width rect) (:width rect) 1) + (or (.-height rect) (:height rect) 1)))) (defn get-window-size [] {:width (.-innerWidth ^js js/window) :height (.-innerHeight ^js js/window)}) +(defn get-computed-styles + [node] + (js/getComputedStyle node)) + +(defn get-property-value + [o prop] + (.getPropertyValue ^js o prop)) + +(defn get-css-variable + ([variable element] + (.getPropertyValue (.getComputedStyle js/window element) variable)) + ([variable] + (.getPropertyValue (.getComputedStyle js/window (.-documentElement js/document)) variable))) + (defn focus! [^js node] (when (some? node) @@ -467,6 +524,10 @@ (.setAttribute node property value)) node) +(defn get-text [^js node] + (when (some? node) + (.-textContent node))) + (defn set-text! [^js node text] (when (some? node) (set! (.-textContent node) text)) @@ -506,6 +567,11 @@ [] (partition 2 params)))) +(defn ^boolean id? + [node id] + (when (some? node) + (= (.-id ^js node) id))) + (defn ^boolean class? [node class-name] (when (some? node) (let [class-list (.-classList ^js node)] @@ -533,11 +599,31 @@ (when (some? node) (= (get-active) node))) -(defn get-data [^js node ^string attr] +(defn get-data + [^js node ^string attr] + ;; NOTE: we use getAttribute instead of .dataset for performance + ;; reasons. The getAttribute is x2 faster than dataset. See more on: + ;; https://www.measurethat.net/Benchmarks/Show/14432/0/getattribute-vs-dataset (when (some? node) (.getAttribute node (dm/str "data-" attr)))) -(defn set-data! [^js node ^string attr value] +(defn- resolve-node + [event] + (cond + (instance? js/Element event) + event + + :else + (get-current-target event))) + +(defn get-boolean-data + [node attr] + (some-> (resolve-node node) + (get-data attr) + (parse-boolean))) + +(defn set-data! + [^js node ^string attr value] (when (some? node) (.setAttribute node (dm/str "data-" attr) (dm/str value)))) @@ -559,6 +645,12 @@ (when (some? element) (.-scrollLeft element))) +(defn scroll-to + ([^js element options] + (.scrollTo element options)) + ([^js element x y] + (.scrollTo element x y))) + (defn set-scroll-pos! [^js element scroll] (when (some? element) @@ -670,6 +762,12 @@ [] (.reload (.-location js/window))) +(defn scroll-by! + ([element x y] + (.scrollBy ^js element x y)) + ([x y] + (scroll-by! js/window x y))) + (defn animate! ([item keyframes duration] (animate! item keyframes duration nil)) ([item keyframes duration onfinish] diff --git a/frontend/src/app/util/dom/dnd.cljs b/frontend/src/app/util/dom/dnd.cljs index 06d21bc852..0f29caab50 100644 --- a/frontend/src/app/util/dom/dnd.cljs +++ b/frontend/src/app/util/dom/dnd.cljs @@ -42,14 +42,14 @@ (let [;;currentTarget (.-currentTarget event) relatedTarget (.-relatedTarget event)] (js/console.log - label - "[" (:name data) "]" + label + "[" (:name data) "]" ;; (if currentTarget ;; (str "<" (.-localName currentTarget) " " (.-textContent currentTarget) ">") ;; "null") - (if relatedTarget - (str "<" (.-localName relatedTarget) " " (.-textContent relatedTarget) ">") - "null")))) + (if relatedTarget + (str "<" (.-localName relatedTarget) " " (.-textContent relatedTarget) ">") + "null")))) (defn set-data! ([e data] @@ -62,6 +62,13 @@ (.setData dt data-type data)) e))) +(defn invisible-image + [] + (let [img (js/Image.) + imd "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="] + (set! (.-src img) imd) + img)) + (defn set-drag-image! ([e image] (set-drag-image! e image 0 0)) @@ -108,11 +115,13 @@ ([e] (get-data e "penpot/data")) ([e data-type] - (let [dt (.-dataTransfer e)] - (if (or (str/starts-with? data-type "penpot") - (= data-type "application/json")) - (t/decode-str (.getData dt data-type)) - (.getData dt data-type))))) + (let [dt (.-dataTransfer e) + data (.getData dt data-type)] + (cond-> data + (and (some? data) (not= data "") + (or (str/starts-with? data-type "penpot") + (= data-type "application/json"))) + (t/decode-str))))) (defn get-files [e] diff --git a/frontend/src/app/util/extends.cljs b/frontend/src/app/util/extends.cljs new file mode 100644 index 0000000000..4cec0733f3 --- /dev/null +++ b/frontend/src/app/util/extends.cljs @@ -0,0 +1,14 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.extends + "A dummy namespace for closure library and other global objects + extensions" + (:require + [promesa.impl :as pi]) + (:import goog.async.Deferred)) + +(pi/extend-promise! Deferred) diff --git a/frontend/src/app/util/geom/snap_points.cljs b/frontend/src/app/util/geom/snap_points.cljs deleted file mode 100644 index a6120d50fd..0000000000 --- a/frontend/src/app/util/geom/snap_points.cljs +++ /dev/null @@ -1,49 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.util.geom.snap-points - (:require - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] - [app.common.types.shape-tree :as ctst])) - -(defn selrect-snap-points [{:keys [x y width height] :as selrect}] - #{(gpt/point x y) - (gpt/point (+ x width) y) - (gpt/point (+ x width) (+ y height)) - (gpt/point x (+ y height)) - (gsh/center-selrect selrect)}) - -(defn frame-snap-points [{:keys [x y width height blocked hidden] :as selrect}] - (when (and (not blocked) (not hidden)) - (into (selrect-snap-points selrect) - #{(gpt/point (+ x (/ width 2)) y) - (gpt/point (+ x width) (+ y (/ height 2))) - (gpt/point (+ x (/ width 2)) (+ y height)) - (gpt/point x (+ y (/ height 2)))}))) - -(defn shape-snap-points - [{:keys [hidden blocked] :as shape}] - (when (and (not blocked) (not hidden)) - (case (:type shape) - :frame (-> shape :points gsh/points->selrect frame-snap-points) - (into #{(gsh/center-shape shape)} (:points shape))))) - -(defn guide-snap-points - [guide frame] - - (cond - (and (some? frame) - (not (ctst/rotated-frame? frame)) - (not (cph/root-frame? frame))) - #{} - - (= :x (:axis guide)) - #{(gpt/point (:position guide) 0)} - - :else - #{(gpt/point 0 (:position guide))})) diff --git a/frontend/src/app/util/http.cljs b/frontend/src/app/util/http.cljs index 58c6d0ebac..220128bdf6 100644 --- a/frontend/src/app/util/http.cljs +++ b/frontend/src/app/util/http.cljs @@ -15,7 +15,7 @@ [app.util.globals :as globals] [app.util.time :as dt] [app.util.webapi :as wapi] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] [promesa.core :as p])) @@ -58,7 +58,7 @@ :or {mode :cors headers {} credentials "same-origin"}}] - (rx/Observable.create + (rx/create (fn [subscriber] (let [controller (js/AbortController.) signal (.-signal ^js controller) @@ -105,17 +105,22 @@ (defn send! [{:keys [response-type] :or {response-type :text} :as params}] - (letfn [(on-response [response] - (let [body (case response-type - :json (.json ^js response) - :text (.text ^js response) - :blob (.blob ^js response))] - (->> (rx/from body) - (rx/map (fn [body] - {::response response - :status (.-status ^js response) - :headers (parse-headers (.-headers ^js response)) - :body body})))))] + (letfn [(on-response [^js response] + (if (= :stream response-type) + (rx/of {:status (.-status response) + :headers (parse-headers (.-headers response)) + :body (.-body response) + ::response response}) + (let [body (case response-type + :json (.json ^js response) + :text (.text ^js response) + :blob (.blob ^js response))] + (->> (rx/from body) + (rx/map (fn [body] + {::response response + :status (.-status ^js response) + :headers (parse-headers (.-headers ^js response)) + :body body}))))))] (->> (fetch params) (rx/mapcat on-response)))) @@ -146,9 +151,9 @@ (defn conditional-error-decode-transit [{:keys [body status] :as response}] - (if (and (>= status 400) (string? body)) - (assoc response :body (t/decode-str body)) - response)) + (if (and (>= status 400) (string? body)) + (assoc response :body (t/decode-str body)) + response)) (defn success? [{:keys [status]}] @@ -167,7 +172,7 @@ (p/create (fn [resolve reject] (->> (rx/take 1 observable) - (rx/subs resolve reject))))) + (rx/subs! resolve reject))))) (defn fetch-data-uri ([uri] diff --git a/frontend/src/app/util/i18n.cljs b/frontend/src/app/util/i18n.cljs index 8f0429993d..39575d6014 100644 --- a/frontend/src/app/util/i18n.cljs +++ b/frontend/src/app/util/i18n.cljs @@ -7,6 +7,7 @@ (ns app.util.i18n "A i18n foundation." (:require + [app.common.data :as d] [app.common.logging :as log] [app.config :as cfg] [app.util.dom :as dom] @@ -29,6 +30,7 @@ {:label "Euskera (community)" :value "eu"} {:label "Français (community)" :value "fr"} {:label "Gallego (Community)" :value "gl"} + {:label "Hausa (Community)" :value "ha"} {:label "Hrvatski (Community)" :value "hr"} {:label "Italiano (community)" :value "it"} {:label "Norsk - Bokmål (community)" :value "nb_no"} @@ -175,8 +177,12 @@ {::mf/wrap-props false} [props] (let [label (obj/get props "label") - tag-name (obj/get props "tag-name" "p")] - [:> tag-name {:dangerouslySetInnerHTML #js {:__html (tr label)}}])) + class (obj/get props "class") + tag-name (obj/get props "tag-name" "p") + params (obj/get props "params" []) + html (apply tr (d/concat-vec [label] params))] + [:> tag-name {:dangerouslySetInnerHTML #js {:__html html} + :className class}])) ;; DEPRECATED (defn use-locale diff --git a/frontend/src/app/util/keyboard.cljs b/frontend/src/app/util/keyboard.cljs index 7d4ecf95a9..5151b2f502 100644 --- a/frontend/src/app/util/keyboard.cljs +++ b/frontend/src/app/util/keyboard.cljs @@ -9,15 +9,44 @@ [app.config :as cfg] [cuerdas.core :as str])) +(defrecord KeyboardEvent [type key shift ctrl alt meta mod editing native-event] + Object + (preventDefault [_] + (.preventDefault native-event)) + + (stopPropagation [_] + (.stopPropagation native-event))) + +(defn keyboard-event? + [o] + (instance? KeyboardEvent o)) + +(defn key-up-event? + [^KeyboardEvent event] + (= :up (.-type event))) + +(defn key-down-event? + [^KeyboardEvent event] + (= :down (.-type event))) + +(defn mod-event? + [^KeyboardEvent event] + (true? (.-mod event))) + +(defn editing-event? + [^KeyboardEvent event] + (true? (.-editing event))) + (defn is-key? [^string key] - (fn [^js e] + (fn [^KeyboardEvent e] (= (.-key e) key))) (defn is-key-ignore-case? [^string key] - (fn [^js e] - (= (str/upper (.-key e)) (str/upper key)))) + (let [key (str/upper key)] + (fn [^KeyboardEvent e] + (= (str/upper (.-key e)) key)))) (defn ^boolean alt? [^js event] @@ -45,6 +74,10 @@ (def enter? (is-key? "Enter")) (def space? (is-key? " ")) (def z? (is-key-ignore-case? "z")) +(def equals? (is-key? "=")) +(def plus? (is-key? "+")) +(def minus? (is-key? "-")) +(def underscore? (is-key? "_")) (def up-arrow? (is-key? "ArrowUp")) (def down-arrow? (is-key? "ArrowDown")) (def left-arrow? (is-key? "ArrowLeft")) @@ -58,6 +91,3 @@ (def home? (is-key? "Home")) (def tab? (is-key? "Tab")) -(defn editing? [e] - (.-editing ^js e)) - diff --git a/frontend/src/app/util/mouse.cljs b/frontend/src/app/util/mouse.cljs new file mode 100644 index 0000000000..4576ed3251 --- /dev/null +++ b/frontend/src/app/util/mouse.cljs @@ -0,0 +1,83 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.mouse + (:require + [beicon.v2.core :as rx])) + +(defrecord MouseEvent [type ctrl shift alt meta]) +(defrecord PointerEvent [source pt ctrl shift alt meta]) +(defrecord ScrollEvent [point]) +(defrecord BlurEvent []) + +(defn mouse-event? + [v] + (instance? MouseEvent v)) + +(defn pointer-event? + [v] + (instance? PointerEvent v)) + +(defn scroll-event? + [v] + (instance? ScrollEvent v)) + +(defn blur-event? + [v] + (instance? BlurEvent v)) + +(defn mouse-down-event? + [^MouseEvent v] + (= :down (.-type v))) + +(defn mouse-up-event? + [^MouseEvent v] + (= :up (.-type v))) + +(defn mouse-click-event? + [^MouseEvent v] + (= :click (.-type v))) + +(defn mouse-double-click-event? + [^MouseEvent v] + (= :double-click (.-type v))) + +(defn get-pointer-source + [^PointerEvent ev] + (.-source ev)) + +(defn get-pointer-position + [^PointerEvent ev] + (.-pt ev)) + +(defn get-pointer-ctrl-mod + [^PointerEvent ev] + (.-ctrl ev)) + +(defn get-pointer-meta-mod + [^PointerEvent ev] + (.-meta ev)) + +(defn get-pointer-alt-mod + [^PointerEvent ev] + (.-alt ev)) + +(defn get-pointer-shift-mod + [^PointerEvent ev] + (.-shift ev)) + +(defn drag-stopper + "Creates a stream to stop drag events. Takes into account the mouse and also + if the window loses focus or the esc key is pressed." + [stream] + (rx/merge + (->> stream + (rx/filter blur-event?)) + (->> stream + (rx/filter mouse-event?) + (rx/filter mouse-up-event?)) + (->> stream + (rx/filter #(= % :interrupt))))) diff --git a/frontend/src/app/util/object.cljs b/frontend/src/app/util/object.cljs index 6020411717..74af901023 100644 --- a/frontend/src/app/util/object.cljs +++ b/frontend/src/app/util/object.cljs @@ -6,16 +6,23 @@ (ns app.util.object "A collection of helpers for work with javascript objects." - (:refer-clojure :exclude [set! new get get-in merge clone contains?]) + (:refer-clojure :exclude [set! new get get-in merge clone contains? array? into-array]) (:require - ["lodash/omit" :as omit] [cuerdas.core :as str])) +(defn array? + [o] + (.isArray js/Array o)) + +(defn into-array + [o] + (js/Array.from o)) + (defn create [] #js {}) (defn get ([obj k] - (when-not (nil? obj) + (when (some? obj) (unchecked-get obj k))) ([obj k default] (let [result (get obj k)] @@ -23,7 +30,8 @@ (defn contains? [obj k] - (some? (unchecked-get obj k))) + (when (some? obj) + (js/Object.hasOwn obj k))) (defn get-keys [obj] @@ -43,15 +51,6 @@ (rest keys) (unchecked-get res key)))))) -#_:clj-kondo/ignore -(defn without - [obj keys] - (let [keys (cond - (vector? keys) (into-array keys) - (array? keys) keys - :else (throw (js/Error. "unexpected input")))] - (omit obj keys))) - (defn clone [a] (js/Object.assign #js {} a)) @@ -73,6 +72,11 @@ (unchecked-set obj key value) obj) +(defn unset! + [obj key] + (js-delete obj key) + obj) + (defn update! [obj key f & args] (let [found (get obj key ::not-found)] @@ -82,10 +86,16 @@ obj))) (defn- props-key-fn - [key] - (if (or (= key :class) (= key :class-name)) - "className" - (str/camel (name key)))) + [k] + (if (or (keyword? k) (symbol? k)) + (let [nword (name k)] + (cond + (= nword "class") "className" + (str/starts-with? nword "--") nword + (str/starts-with? nword "data-") nword + (str/starts-with? nword "aria-") nword + :else (str/camel nword))) + k)) (defn clj->props [props] @@ -94,3 +104,29 @@ (defn ^boolean in? [obj prop] (js* "~{} in ~{}" prop obj)) + +(defn map->obj + [x] + (cond + (nil? x) + nil + + (keyword? x) + (name x) + + (map? x) + (reduce-kv (fn [m k v] + (let [k (if (keyword? k) (name k) k)] + (unchecked-set m k (^function map->obj v)) + m)) + #js {} + x) + + (coll? x) + (reduce (fn [arr v] + (.push arr v) + arr) + (array) + x) + + :else x)) diff --git a/frontend/src/app/util/path/format.cljs b/frontend/src/app/util/path/format.cljs index c0d38829db..5a5860341b 100644 --- a/frontend/src/app/util/path/format.cljs +++ b/frontend/src/app/util/path/format.cljs @@ -6,8 +6,8 @@ (ns app.util.path.format (:require - [app.common.path.commands :as upc] - [app.common.path.subpaths :refer [pt=]] + [app.common.svg.path.command :as upc] + [app.common.svg.path.subpath :refer [pt=]] [app.util.array :as arr])) (def path-precision 3) @@ -30,8 +30,7 @@ (.toFixed a path-precision) (.toFixed b path-precision) (.toFixed c path-precision) - (.toFixed d path-precision) - )) + (.toFixed d path-precision))) ([a b c d e] (js* "\"\"+~{}+\",\"+~{}+\",\"+~{}+\",\"+~{}+\",\"+~{}" (.toFixed a path-precision) @@ -46,8 +45,7 @@ (.toFixed c path-precision) (.toFixed d path-precision) (.toFixed e path-precision) - (.toFixed f path-precision) - )) + (.toFixed f path-precision))) ([a b c d e f g] (js* "\"\"+~{}+\",\"+~{}+\",\"+~{}+\",\"+~{}+\",\"+~{}+\",\"+~{}+\",\"+~{}" (.toFixed a path-precision) diff --git a/frontend/src/app/util/path/tools.cljs b/frontend/src/app/util/path/tools.cljs index f6877b733b..516ef047df 100644 --- a/frontend/src/app/util/path/tools.cljs +++ b/frontend/src/app/util/path/tools.cljs @@ -9,7 +9,7 @@ [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.geom.shapes.path :as upg] - [app.common.path.commands :as upc] + [app.common.svg.path.command :as upc] [clojure.set :as set])) (defn remove-line-curves diff --git a/frontend/src/app/util/perf.cljs b/frontend/src/app/util/perf.cljs index f962e993eb..f958ba34f5 100644 --- a/frontend/src/app/util/perf.cljs +++ b/frontend/src/app/util/perf.cljs @@ -8,11 +8,11 @@ "Performance profiling for react components." (:require-macros [app.util.perf]) (:require - [app.common.math :as mth] - [rumext.v2 :as mf] - [goog.functions :as f] ["react" :as react] - ["tdigest" :as td])) + ["tdigest" :as td] + [app.common.math :as mth] + [goog.functions :as f] + [rumext.v2 :as mf])) ;; For use it, just wrap the component you want to profile with ;; `perf/profiler` component and pass a label for debug purpose. @@ -107,26 +107,40 @@ children))) (defn benchmark - [& {:keys [f warmup iterations name] + [& {:keys [run-fn chk-fn iterations name gc] :or {iterations 10000}}] - (let [end-mark (str name ":end")] + (let [end-mark (str name ":end") + blackhole (volatile! nil)] (println "=> benchmarking:" name) - (println "--> warming up:" iterations) - (loop [i iterations] + (when gc + (println "-> force gc: true")) + + (println "--> warming up: " (* iterations 2)) + (when (fn? gc) (gc)) + (loop [i (* iterations 2)] (when (pos? i) - (f) + (vreset! blackhole (run-fn)) (recur (dec i)))) (println "--> benchmarking:" iterations) + (when (fn? gc) (gc)) (js/performance.mark name) (loop [i iterations] (when (pos? i) - (f) + (vreset! blackhole (run-fn)) (recur (dec i)))) (js/performance.measure end-mark name) + + (when (fn? chk-fn) + (when-not (chk-fn @blackhole) + (println "--> EE: failed chk-fn"))) + + (let [[result] (js/performance.getEntriesByName end-mark) duration (mth/precision (.-duration ^js result) 4) avg (mth/precision (/ duration iterations) 4)] - (println "--> TOTAL:" (str duration "ms") "AVG:" (str avg "ms")) + (println "--> TOTAL:" (str duration " ms")) + (println "--> AVG :" (str avg " ms")) + (println "") (js/performance.clearMarks name) (js/performance.clearMeasures end-mark) #js {:duration duration diff --git a/frontend/src/app/util/queue.cljs b/frontend/src/app/util/queue.cljs new file mode 100644 index 0000000000..1c68763f14 --- /dev/null +++ b/frontend/src/app/util/queue.cljs @@ -0,0 +1,115 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.queue + (:require [app.common.logging :as l] + [app.common.math :as mth] + [app.util.time :as t])) + +(l/set-level! :info) + +(declare process) +(declare dequeue) + +(defrecord Queue [f items timeout time threshold max-iterations]) + +(defn create + [f threshold] + (Queue. f + #js [] + nil + 0 + threshold + ##Inf)) + +(defn- measure-fn + [f & args] + (let [tp (t/tpoint-ms) + _ (apply f args) + duration (tp)] + (l/dbg :hint "queue::measure-fn" :duration duration) + duration)) + +(defn- next-process-time + [queue] + (let [time (unchecked-get queue "time") + threshold (unchecked-get queue "threshold") + max-time 5000 + min-time 1000 + calc-time (mth/min (mth/max (* (- time threshold) 10) min-time) max-time)] + (l/dbg :hint "queue::next-process-time" :time time :threshold threshold :calc-time calc-time :max-time max-time :min-time min-time) + calc-time)) + +(defn- has-requested-process? + [queue] + (not (nil? (unchecked-get queue "timeout")))) + +(defn- request-process + [queue time] + (l/dbg :hint "queue::request-process" :time time) + (unchecked-set queue "timeout" (js/setTimeout (fn [] (process queue)) time))) + +;; NOTE: Right now there are no cases where we need to cancel a process +;; but if we do, we can use this function +#_(defn- cancel-process + [queue] + (l/dbg :hint "queue::cancel-process") + (let [timeout (unchecked-get queue "timeout")] + (when (some? timeout) + (js/clearTimeout timeout)) + (unchecked-set queue "timeout" nil))) + +(defn- process + [queue] + (unchecked-set queue "timeout" nil) + (unchecked-set queue "time" 0) + (let [threshold (unchecked-get queue "threshold") + max-iterations (unchecked-get queue "max-iterations") + f (unchecked-get queue "f")] + (loop [item (dequeue queue) + iterations 0] + (l/dbg :hint "queue::process" :item item) + (when (some? item) + (let [duration (measure-fn f item) + time (unchecked-get queue "time") + time (unchecked-set queue "time" (+ time duration))] + (if (or (> time threshold) (>= iterations max-iterations)) + (request-process queue (next-process-time queue)) + (recur (dequeue queue) (inc iterations)))))))) + +(defn- dequeue + [queue] + (let [items (unchecked-get queue "items")] + (.shift items))) + +(defn enqueue-first + [queue item] + (assert (instance? Queue queue)) + (let [items (unchecked-get queue "items")] + (.unshift items item) + (when-not (has-requested-process? queue) + (request-process queue (next-process-time queue))))) + +(defn enqueue-last + [queue item] + (assert (instance? Queue queue)) + (let [items (unchecked-get queue "items")] + (.push items item) + (when-not (has-requested-process? queue) + (request-process queue (next-process-time queue))))) + +(defn enqueue-unique + [queue item f] + (assert (instance? Queue queue)) + (let [items (unchecked-get queue "items")] + ;; If tag is "frame", then they are added to the front of the queue + ;; so that they are processed first, anything else is added to the + ;; end of the queue. + (if (= (unchecked-get item "tag") "frame") + (when-not (.find ^js items f) + (enqueue-first queue item)) + (when-not (.findLast ^js items f) + (enqueue-last queue item))))) diff --git a/frontend/src/app/util/router.cljs b/frontend/src/app/util/router.cljs index 2fd4b738d5..c4d541cfd8 100644 --- a/frontend/src/app/util/router.cljs +++ b/frontend/src/app/util/router.cljs @@ -7,14 +7,16 @@ (ns app.util.router (:refer-clojure :exclude [resolve]) (:require + [app.common.data.macros :as dm] [app.common.uri :as u] [app.config :as cf] + [app.main.data.events :as ev] [app.util.browser-history :as bhistory] [app.util.dom :as dom] [app.util.timers :as ts] - [beicon.core :as rx] + [beicon.v2.core :as rx] [goog.events :as e] - [potok.core :as ptk] + [potok.v2.core :as ptk] [reitit.core :as r])) ;; --- Router API @@ -59,8 +61,13 @@ (defn navigated [match] (ptk/reify ::navigated - IDeref - (-deref [_] match) + ev/Event + (-data [_] + (let [route (dm/get-in match [:data :name]) + params (get match :path-params)] + (assoc params + ::ev/name "navigate" + :route (name route)))) ptk/UpdateEvent (update [_ state] @@ -149,14 +156,14 @@ ptk/EffectEvent (effect [_ state stream] - (let [stoper (rx/filter (ptk/type? ::initialize-history) stream) + (let [stopper (rx/filter (ptk/type? ::initialize-history) stream) history (:history state) router (:router state)] (ts/schedule #(on-change router (.getToken ^js history))) (->> (rx/create (fn [subs] - (let [key (e/listen history "navigate" (fn [o] (rx/push! subs (.-token ^js o))))] - (fn [] - (bhistory/disable! history) - (e/unlistenByKey key))))) - (rx/take-until stoper) - (rx/subs #(on-change router %))))))) + (let [key (e/listen history "navigate" (fn [o] (rx/push! subs (.-token ^js o))))] + (fn [] + (bhistory/disable! history) + (e/unlistenByKey key))))) + (rx/take-until stopper) + (rx/subs! #(on-change router %))))))) diff --git a/frontend/src/app/util/rxops.cljs b/frontend/src/app/util/rxops.cljs new file mode 100644 index 0000000000..7a36a8c3ed --- /dev/null +++ b/frontend/src/app/util/rxops.cljs @@ -0,0 +1,72 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.rxops + (:require + [beicon.v2.core :as rx])) + +(defn throttle-fn + [delay f] + (let [state + #js {:lastExecTime 0 + :timeoutId nil + :context nil + :args nil} + + execute-fn + (fn [] + (let [context (.-context ^js state) + args (.-args ^js state)] + (.apply f context args) + (set! (.-lastExecTime state) (js/Date.now)) + (set! (.-timeoutId state) nil))) + + wrapped-fn + (fn [] + (let [ctime (js/Date.now) + ltime (.-lastExecTime ^js state) + args (js-arguments)] + + (this-as this + (set! (.-context state) this) + (set! (.-args state) args)) + + (let [timeout-id (.-timeoutId state)] + (if (>= (- ctime ltime) delay) + (do + (when ^boolean timeout-id + (js/clearTimeout timeout-id) + (set! (.-timeoutId state) nil)) + (execute-fn)) + + (when-not ^boolean timeout-id + (set! (.-timeoutId state) + (js/setTimeout execute-fn (- delay (- ctime ltime)))))))))] + + (specify! wrapped-fn + rx/IDisposable + (-dispose [_] + (js/clearTimeout (.-timeoutId state)) + (set! (.-lastExecTime state) 0) + (set! (.-timeoutId state) nil))))) + + +(defn throttle + "High performance rxjs throttle operation. It does not saturates the + macro-task queue of the js runtime on long burst of mouse + movements." + [delay] + (fn [source] + (rx/create + (fn [subs] + (let [next-fn (throttle-fn delay (partial rx/push! subs)) + error-fn (fn [cause] + (rx/dispose! next-fn) + (rx/error! subs cause)) + end-fn (fn [] + (rx/dispose! next-fn) + (rx/end! subs))] + (rx/sub! source next-fn error-fn end-fn)))))) diff --git a/frontend/src/app/util/simple_math.cljs b/frontend/src/app/util/simple_math.cljs index 33049a22ec..7b203fdd12 100644 --- a/frontend/src/app/util/simple_math.cljs +++ b/frontend/src/app/util/simple_math.cljs @@ -15,7 +15,7 @@ (def parser (insta/parser - "opt-expr = '' | expr + "opt-expr = '' | expr expr = term ( ('+'|'-') expr)* | ('+'|'-'|'*'|'/') factor term = factor ( ('*'|'/') term)* @@ -98,7 +98,7 @@ (map :expecting) (filter some?))] (js/console.debug - (str "Invalid value '" text "' at index " index - ". Expected one of " expecting ".")) + (str "Invalid value '" text "' at index " index + ". Expected one of " expecting ".")) nil)))) diff --git a/frontend/src/app/util/snap_data.cljs b/frontend/src/app/util/snap_data.cljs index f9cf779fad..2fef2aa7bc 100644 --- a/frontend/src/app/util/snap_data.cljs +++ b/frontend/src/app/util/snap_data.cljs @@ -10,13 +10,13 @@ https://en.wikipedia.org/wiki/Range_tree" (:require [app.common.data :as d] - [app.common.pages.diff :as diff] - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cfh] + [app.common.files.page-diff :as diff] + [app.common.geom.grid :as gg] + [app.common.geom.snap :as snap] [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] - [app.util.geom.grid :as gg] - [app.util.geom.snap-points :as snap] [app.util.range-tree :as rt])) (def snap-attrs [:frame-id :x :y :width :height :hidden :selrect :grids]) @@ -72,19 +72,22 @@ (defn- add-frame [objects page-data frame] - (let [frame-id (:id frame) - parent-id (:parent-id frame) - frame-data (->> (snap/shape-snap-points frame) - (mapv #(array-map :type :shape - :id frame-id - :pt %))) + (let [frame-id (:id frame) + parent-id (:parent-id frame) + + frame-data (if (:blocked frame) + [] + (->> (snap/shape->snap-points frame) + (mapv #(array-map :type :shape + :id frame-id + :pt %)))) grid-x-data (get-grids-snap-points frame :x) grid-y-data (get-grids-snap-points frame :y)] (cond-> page-data (and (not (ctl/any-layout-descent? objects frame)) (not (:hidden frame)) - (not (cph/hidden-parent? objects frame-id))) + (not (cfh/hidden-parent? objects frame-id))) (-> ;; Update root frame information (assoc-in [uuid/zero :objects-data frame-id] frame-data) @@ -101,7 +104,9 @@ (defn- add-shape [objects page-data shape] (let [frame-id (:frame-id shape) - snap-points (snap/shape-snap-points shape) + snap-points (if (:blocked shape) + [] + (snap/shape->snap-points shape)) shape-data (->> snap-points (mapv #(array-map :type :shape @@ -110,7 +115,7 @@ (cond-> page-data (and (not (ctl/any-layout-descent? objects shape)) (not (:hidden shape)) - (not (cph/hidden-parent? objects (:id shape)))) + (not (cfh/hidden-parent? objects (:id shape)))) (-> (assoc-in [frame-id :objects-data (:id shape)] shape-data) (update-in [frame-id :x] (make-insert-tree-data shape-data :x)) (update-in [frame-id :y] (make-insert-tree-data shape-data :y)))))) @@ -119,7 +124,7 @@ [objects page-data guide] (let [frame (get objects (:frame-id guide)) - guide-data (->> (snap/guide-snap-points guide frame) + guide-data (->> (snap/guide->snap-points guide frame) (mapv #(array-map :type :guide :id (:id guide) @@ -130,7 +135,7 @@ ;; Guide inside frame, we add the information only on that frame (cond-> page-data (and (not (:hidden frame)) - (not (cph/hidden-parent? objects frame-id))) + (not (cfh/hidden-parent? objects frame-id))) (-> (assoc-in [frame-id :objects-data (:id guide)] guide-data) (update-in [frame-id (:axis guide)] (make-insert-tree-data guide-data (:axis guide))))) @@ -209,7 +214,7 @@ [snap-data {:keys [objects options] :as page}] (let [frames (ctst/get-frames objects) shapes (->> (vals (:objects page)) - (remove cph/frame-shape?)) + (remove cfh/frame-shape?)) guides (vals (:guides options)) page-data diff --git a/frontend/src/app/util/sse.cljs b/frontend/src/app/util/sse.cljs new file mode 100644 index 0000000000..0913f052ae --- /dev/null +++ b/frontend/src/app/util/sse.cljs @@ -0,0 +1,54 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.sse + (:require + ["eventsource-parser/stream" :as sse] + [beicon.v2.core :as rx])) + +(defn create-stream + [^js/ReadableStream stream] + (.. stream + (pipeThrough (js/TextDecoderStream.)) + (pipeThrough (sse/EventSourceParserStream.)))) + +(defn read-stream + [^js/ReadableStream stream decode-fn] + (letfn [(read-items [^js reader] + (->> (rx/from (.read reader)) + (rx/mapcat (fn [result] + (if (.-done result) + (rx/empty) + (rx/concat + (rx/of (.-value result)) + (read-items reader)))))))] + (->> (read-items (.getReader stream)) + (rx/mapcat (fn [^js event] + (let [type (.-event event) + data (.-data event) + data (decode-fn data)] + (if (= "error" type) + (rx/throw (ex-info "stream exception" data)) + (rx/of #js {:type type :data data})))))))) + +(defn get-type + [event] + (unchecked-get event "type")) + +(defn get-payload + [event] + (unchecked-get event "data")) + +(defn end-of-stream? + [event] + (= "end" (get-type event))) + +(defn event? + [event] + (= "event" (get-type event))) + + + diff --git a/frontend/src/app/util/strings.cljs b/frontend/src/app/util/strings.cljs index 3f74c9e6c1..edbe863544 100644 --- a/frontend/src/app/util/strings.cljs +++ b/frontend/src/app/util/strings.cljs @@ -42,9 +42,3 @@ (let [st (str/trim (str/lower search-term)) nm (str/trim (str/lower name))] (str/includes? nm st)))) - -(defn camelize - [str] - ;; str.replace(":", "-").replace(/-./g, x=>x[1].toUpperCase()) - (when (not (nil? str)) - (js* "~{}.replace(\":\", \"-\").replace(/-./g, x=>x[1].toUpperCase())", str))) diff --git a/frontend/src/app/util/text_svg_position.cljs b/frontend/src/app/util/text_svg_position.cljs index a79f49b06c..7047413ff9 100644 --- a/frontend/src/app/util/text_svg_position.cljs +++ b/frontend/src/app/util/text_svg_position.cljs @@ -12,22 +12,21 @@ [app.main.fonts :as fonts] [app.util.dom :as dom] [app.util.text-position-data :as tpd] + [cuerdas.core :as str] [promesa.core :as p])) (defn parse-text-nodes "Given a text node retrieves the rectangles for everyone of its paragraphs and its text." [parent-node direction text-node text-align] - (letfn [(parse-entry [^js entry] (when (some? (.-position entry)) {:node (.-node entry) :position (dom/bounding-rect->rect (.-position entry)) :text (.-text entry) :direction direction}))] - (into - [] - (keep parse-entry) - (tpd/parse-text-nodes parent-node text-node text-align)))) + (into [] + (keep parse-entry) + (tpd/parse-text-nodes parent-node text-node text-align)))) (def load-promises (atom {})) @@ -40,76 +39,76 @@ load-promise))) (defn resolve-font - [^js node] + [node] + (let [styles (dom/get-computed-styles node) + font (dom/get-property-value styles "font") + font (if (or (not font) (empty? font)) + ;; Firefox 95 won't return the font correctly. + ;; We can get the font shorthand with the font-size + font-family + (str/ffmt "% %" + (dom/get-property-value styles "font-size") + (dom/get-property-value styles "font-family")) + font) - (let [styles (js/getComputedStyle node) - font (.getPropertyValue styles "font") - font (if (or (not font) (empty? font)) - ;; Firefox 95 won't return the font correctly. - ;; We can get the font shorthand with the font-size + font-family - (dm/str (.getPropertyValue styles "font-size") - " " - (.getPropertyValue styles "font-family")) - font) + font-id (dom/get-property-value styles "--font-id")] - font-id (.getPropertyValue styles "--font-id")] + (->> (fonts/ensure-loaded! font-id) + (p/fmap (fn [] + (when-not ^boolean (dom/check-font? font) + (load-font font)))) + (p/merr (fn [_cause] + (js/console.error (str/ffmt "Cannot load font %" font-id)) + (p/resolved nil)))))) - (-> (fonts/ensure-loaded! font-id) - (p/then #(when (not (dom/check-font? font)) - (load-font font))) - (p/catch #(.error js/console (dm/str "Cannot load font " font-id) %))))) + +(defn- process-text-node + [parent-node] + (let [root (dom/get-parent-with-selector parent-node ".text-node-html") + paragraph (dom/get-parent-with-selector parent-node ".paragraph") + shape-x (d/parse-double (dom/get-attribute root "data-x")) + shape-y (d/parse-double (dom/get-attribute root "data-y")) + direction (.-direction ^js (dom/get-computed-styles parent-node)) + text-align (.-textAlign ^js (dom/get-computed-styles paragraph))] + + (sequence + (comp + (mapcat #(parse-text-nodes parent-node direction % text-align)) + (map #(-> % + (update-in [:position :x] + shape-x) + (update-in [:position :y] + shape-y)))) + (seq (.-childNodes parent-node))))) (defn- calc-text-node-positions [shape-id] - - (when (some? shape-id) - (let [text-nodes (-> (dom/query (dm/fmt "#html-text-node-%" shape-id)) - (dom/query-all ".text-node")) - load-fonts (->> text-nodes (map resolve-font)) - - process-text-node - (fn [parent-node] - (let [root (dom/get-parent-with-selector parent-node ".text-node-html") - paragraph (dom/get-parent-with-selector parent-node ".paragraph") - shape-x (-> (dom/get-attribute root "data-x") d/parse-double) - shape-y (-> (dom/get-attribute root "data-y") d/parse-double) - direction (.-direction (js/getComputedStyle parent-node)) - text-align (.-textAlign (js/getComputedStyle paragraph))] - - (->> (.-childNodes parent-node) - (mapcat #(parse-text-nodes parent-node direction % text-align)) - (mapv #(-> % - (update-in [:position :x] + shape-x) - (update-in [:position :y] + shape-y))))))] - (-> (p/all load-fonts) - (p/then - (fn [] - (->> text-nodes (mapcat process-text-node)))))))) + (let [text-nodes (-> (dom/query (dm/fmt "#html-text-node-%" shape-id)) + (dom/query-all ".text-node"))] + (->> (p/all (map resolve-font text-nodes)) + (p/fmap #(mapcat process-text-node text-nodes))))) (defn calc-position-data [shape-id] - (when (some? shape-id) - (p/let [text-data (calc-text-node-positions shape-id)] - (->> text-data - (mapv (fn [{:keys [node position text direction]}] - (let [{:keys [x y width height]} position - styles (js/getComputedStyle ^js node) - get (fn [prop] - (let [value (.getPropertyValue styles prop)] - (when (and value (not= value "")) - value)))] - (d/without-nils - {:x x - :y (+ y height) - :width width - :height height - :direction direction - :font-family (str (get "font-family")) - :font-size (str (get "font-size")) - :font-weight (str (get "font-weight")) - :text-transform (str (get "text-transform")) - :text-decoration (str (get "text-decoration")) - :letter-spacing (str (get "letter-spacing")) - :font-style (str (get "font-style")) - :fills (transit/decode-str (get "--fills")) - :text text})))))))) + (letfn [(get-prop [styles prop] + (let [value (.getPropertyValue styles prop)] + (when (and (some? value) (not= value "")) + value))) + + (transform-data [{:keys [node position text direction]}] + (let [styles (dom/get-computed-styles node) + position (assoc position :y (+ (dm/get-prop position :y) + (dm/get-prop position :height)))] + (into position (filter val) + {:direction direction + :font-family (dm/str (get-prop styles "font-family")) + :font-size (dm/str (get-prop styles "font-size")) + :font-weight (dm/str (get-prop styles "font-weight")) + :text-transform (dm/str (get-prop styles "text-transform")) + :text-decoration (dm/str (get-prop styles "text-decoration")) + :letter-spacing (dm/str (get-prop styles "letter-spacing")) + :font-style (dm/str (get-prop styles "font-style")) + :fills (transit/decode-str (get-prop styles "--fills")) + :text text})))] + + (when (some? shape-id) + (->> (calc-text-node-positions shape-id) + (p/fmap (fn [text-data] + (mapv transform-data text-data))))))) diff --git a/frontend/src/app/util/theme.cljs b/frontend/src/app/util/theme.cljs index e962bcbf1d..a31c79f832 100644 --- a/frontend/src/app/util/theme.cljs +++ b/frontend/src/app/util/theme.cljs @@ -11,7 +11,7 @@ [app.config :as cfg] [app.util.dom :as dom] [app.util.storage :refer [storage]] - [beicon.core :as rx] + [beicon.v2.core :as rx] [rumext.v2 :as mf])) (defonce theme (get @storage ::theme cfg/default-theme)) diff --git a/frontend/src/app/util/thumbnails.cljs b/frontend/src/app/util/thumbnails.cljs index e44a7be3e0..e129632283 100644 --- a/frontend/src/app/util/thumbnails.cljs +++ b/frontend/src/app/util/thumbnails.cljs @@ -8,8 +8,31 @@ (:require [app.common.math :as mth])) -(def ^:const min-size 250) -(def ^:const max-size 2000) +(def ^:const max-recommended-size (mth/pow 2 11)) ;; 2^11 = 2048 +(def ^:const max-absolute-size (mth/pow 2 14)) ;; 2^14 = 16384 + +(def ^:const min-size 1) +(def ^:const max-size max-recommended-size) + +(def ^:const min-aspect-ratio 0.5) +(def ^:const max-aspect-ratio 2.0) + +(defn get-aspect-ratio + "Returns the aspect ratio of a given width and height." + [width height] + (/ width height)) + +(defn get-size-from + [ref-size opp-size clamped-size] + (/ (* opp-size clamped-size) ref-size)) + +(defn get-height-from-width + ([width height clamped-width] + (get-size-from width height clamped-width))) + +(defn get-width-from-height + ([width height clamped-height] + (get-size-from height width clamped-height))) (defn get-proportional-size "Returns a proportional size given a width and height and some size constraints." @@ -18,12 +41,16 @@ ([width height min-size max-size] (get-proportional-size width height min-size max-size min-size max-size)) ([width height min-width max-width min-height max-height] - (let [[fixed-width fixed-height] - (if (> width height) - [(mth/clamp width min-width max-width) - (/ (* height (mth/clamp width min-width max-width)) width)] - [(/ (* width (mth/clamp height min-height max-height)) height) - (mth/clamp height min-height max-height)])] - [fixed-width fixed-height]))) - + (let [clamped-width (mth/clamp width min-width max-width) + clamped-height (mth/clamp height min-height max-height)] + (if (> width height) + [clamped-width (get-height-from-width width height clamped-width)] + [(get-width-from-height width height clamped-height) clamped-height])))) +(defn get-relative-size + "Returns a recommended size given a width and height." + [width height] + (let [aspect-ratio (get-aspect-ratio width height)] + (if (or (< aspect-ratio min-aspect-ratio) (> aspect-ratio max-aspect-ratio)) + (get-proportional-size width height min-size max-absolute-size) + (get-proportional-size width height min-size max-recommended-size)))) diff --git a/frontend/src/app/util/time.cljs b/frontend/src/app/util/time.cljs index 3e9d020925..4a91fbdfce 100644 --- a/frontend/src/app/util/time.cljs +++ b/frontend/src/app/util/time.cljs @@ -6,22 +6,7 @@ (ns app.util.time (:require - ["date-fns/format" :default dateFnsFormat] - ["date-fns/formatDistanceToNowStrict" :default dateFnsFormatDistanceToNowStrict] - ["date-fns/locale/ar-SA" :default dateFnsLocalesAr] - ["date-fns/locale/ca" :default dateFnsLocalesCa] - ["date-fns/locale/de" :default dateFnsLocalesDe] - ["date-fns/locale/el" :default dateFnsLocalesEl] - ["date-fns/locale/en-US" :default dateFnsLocalesEnUs] - ["date-fns/locale/es" :default dateFnsLocalesEs] - ["date-fns/locale/fa-IR" :default dateFnsLocalesFa] - ["date-fns/locale/fr" :default dateFnsLocalesFr] - ["date-fns/locale/he" :default dateFnsLocalesHe] - ["date-fns/locale/pt-BR" :default dateFnsLocalesPtBr] - ["date-fns/locale/ro" :default dateFnsLocalesRo] - ["date-fns/locale/ru" :default dateFnsLocalesRu] - ["date-fns/locale/tr" :default dateFnsLocalesTr] - ["date-fns/locale/zh-CN" :default dateFnsLocalesZhCn] + ["./time_impl.js" :as impl] [app.common.data.macros :as dm] [app.common.time :as common-time] [app.util.object :as obj] @@ -207,22 +192,6 @@ :json (.toJSON it) (.toFormat ^js it fmt)))) -(def ^:private locales - #js {:en dateFnsLocalesEnUs - :ar dateFnsLocalesAr - :he dateFnsLocalesHe - :fr dateFnsLocalesFr - :tr dateFnsLocalesTr - :es dateFnsLocalesEs - :ca dateFnsLocalesCa - :el dateFnsLocalesEl - :ru dateFnsLocalesRu - :ro dateFnsLocalesRo - :de dateFnsLocalesDe - :fa dateFnsLocalesFa - :pt_br dateFnsLocalesPtBr - :zh_cn dateFnsLocalesZhCn}) - (defn timeago ([v] (timeago v nil)) ([v {:keys [locale] :or {locale "en"}}] @@ -230,19 +199,18 @@ (let [v (if (datetime? v) (format v :date) v)] (->> #js {:includeSeconds true :addSuffix true - :locale (obj/get locales locale)} - (dateFnsFormatDistanceToNowStrict v)))))) + :locale (obj/get impl/locales locale)} + (impl/format-distance-to-now v)))))) (defn format-date-locale ([v] (format-date-locale v nil)) ([v {:keys [locale] :or {locale "en"}}] (when v (let [v (if (datetime? v) (format v :date) v) - locale (obj/get locales locale) - f (.date (.-formatLong locale) v)] + locale (obj/get impl/locales locale) + f (.date (.-formatLong ^js locale) v)] (->> #js {:locale locale} - (dateFnsFormat v f)))))) - + (impl/format v f)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Measurement Helpers diff --git a/frontend/src/app/util/time_impl.js b/frontend/src/app/util/time_impl.js new file mode 100644 index 0000000000..d6bb5f32b4 --- /dev/null +++ b/frontend/src/app/util/time_impl.js @@ -0,0 +1,71 @@ +import fmt1 from "date-fns/format"; +import fmt2 from "date-fns/formatDistanceToNowStrict"; + +import {arSA} from "date-fns/locale/ar-SA"; +import {ca} from "date-fns/locale/ca"; +import {de} from "date-fns/locale/de"; +import {el} from "date-fns/locale/el"; +import {enUS} from "date-fns/locale/en-US"; +import {es} from "date-fns/locale/es"; +import {faIR} from "date-fns/locale/fa-IR"; +import {fr} from "date-fns/locale/fr"; +import {he} from "date-fns/locale/he"; +import {pt} from "date-fns/locale/pt"; +import {ptBR} from "date-fns/locale/pt-BR"; +import {ro} from "date-fns/locale/ro"; +import {ru} from "date-fns/locale/ru"; +import {tr} from "date-fns/locale/tr"; +import {zhCN} from "date-fns/locale/zh-CN"; +import {nl} from "date-fns/locale/nl"; +import {eu} from "date-fns/locale/eu"; +import {gl} from "date-fns/locale/gl"; +import {hr} from "date-fns/locale/hr"; +import {it} from "date-fns/locale/it"; +import {nb} from "date-fns/locale/nb"; +import {pl} from "date-fns/locale/pl"; +import {id} from "date-fns/locale/id"; +import {uk} from "date-fns/locale/uk"; +import {cs} from "date-fns/locale/cs"; +import {lv} from "date-fns/locale/lv"; +import {ko} from "date-fns/locale/ko"; +import {ja} from "date-fns/locale/ja"; + +export const locales = { + "ar": arSA, + "ca": ca, + "de": de, + "el": el, + "en": enUS, + "en_us": enUS, + "es": es, + "es_es": es, + "fa": faIR, + "fa_ir": faIR, + "fr": fr, + "he": he, + "pt": pt, + "pt_pt": pt, + "pt_br": ptBR, + "ro": ro, + "ru": ru, + "tr": tr, + "zh_cn": zhCN, + "nl": nl, + "eu": eu, + "gl": gl, + "hr": hr, + "it": it, + "nb": nb, + "nb_no": nb, + "pl": pl, + "id": id, + "uk": uk, + "cs": cs, + "lv": lv, + "ko": ko, + "ja": ja, + "ja_jp": ja, +}; + +export const format = fmt1.format; +export const format_distance_to_now = fmt2.formatDistanceToNowStrict; diff --git a/frontend/src/app/util/timers.cljs b/frontend/src/app/util/timers.cljs index 5c56f22987..6df8e2c3a8 100644 --- a/frontend/src/app/util/timers.cljs +++ b/frontend/src/app/util/timers.cljs @@ -6,7 +6,8 @@ (ns app.util.timers (:require - [beicon.core :as rx] + [app.common.data :as d] + [beicon.v2.core :as rx] [promesa.core :as p])) (defn schedule @@ -14,7 +15,12 @@ (schedule 0 func)) ([ms func] (let [sem (js/setTimeout #(func) ms)] - (reify rx/IDisposable + (reify + d/ICloseable + (close! [_] + (js/clearTimeout sem)) + + rx/IDisposable (-dispose [_] (js/clearTimeout sem)))))) @@ -37,7 +43,7 @@ (if (and (exists? js/window) (.-requestIdleCallback js/window)) (do - (def ^:private request-idle-callback #(js/requestIdleCallback %)) + (def ^:private request-idle-callback #(js/requestIdleCallback % #js {:timeout 30000})) ;; 30s timeout (def ^:private cancel-idle-callback #(js/cancelIdleCallback %))) (do (def ^:private request-idle-callback #(js/setTimeout % 250)) diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs index 21675a01b4..9d1ba5c2e1 100644 --- a/frontend/src/app/util/webapi.cljs +++ b/frontend/src/app/util/webapi.cljs @@ -10,10 +10,8 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.logging :as log] - [app.config :as cf] [app.util.object :as obj] - [app.util.thumbnails :as th] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str] [promesa.core :as p])) @@ -57,6 +55,14 @@ ([content mtype] (js/Blob. #js [content] #js {:type mtype}))) +(defn create-blob-from-canvas + ([canvas] + (create-blob-from-canvas canvas nil)) + ([canvas options] + (if (obj/in? canvas "convertToBlob") + (.convertToBlob canvas options) + (p/create (fn [resolve _] (.toBlob #(resolve %) canvas options)))))) + (defn revoke-uri [url] (when ^boolean (str/starts-with? url "blob:") @@ -74,17 +80,23 @@ (defn data-uri->blob [data-uri] - (let [[mtype b64-data] (str/split data-uri ";base64,") + (let [[mtype b64-data] (str/split data-uri ";base64," 2) mtype (subs mtype (inc (str/index-of mtype ":"))) decoded (.atob js/window b64-data) size (.-length ^js decoded) content (js/Uint8Array. size)] - (doseq [i (range 0 size)] - (aset content i (.charCodeAt ^js decoded i))) + (loop [i 0] + (when (< i size) + (aset content i (.charCodeAt ^js decoded i)) + (recur (inc i)))) (create-blob content mtype))) +(defn get-current-selected-text + [] + (.. js/window getSelection toString)) + (defn write-to-clipboard [data] (assert (string? data) "`data` should be string") @@ -93,44 +105,47 @@ (defn read-from-clipboard [] - (let [cboard (unchecked-get js/navigator "clipboard")] - (if (.-readText ^js cboard) - (rx/from (.readText ^js cboard)) - (throw (ex-info "This browser does not implement read from clipboard protocol" - {:not-implemented true}))))) + (try + (let [cboard (unchecked-get js/navigator "clipboard")] + (if (.-readText ^js cboard) + (rx/from (.readText ^js cboard)) + (rx/throw (ex-info "This browser does not implement read from clipboard protocol" + {:not-implemented true})))) + (catch :default cause + (rx/throw cause)))) (defn read-image-from-clipboard [] - (let [cboard (unchecked-get js/navigator "clipboard") - read-item (fn [item] - (let [img-type (->> (.-types ^js item) - (d/seek #(str/starts-with? % "image/")))] - (if img-type - (rx/from (.getType ^js item img-type)) - (rx/empty))))] - (->> (rx/from (.read ^js cboard)) ;; Get a stream of item lists - (rx/mapcat identity) ;; Convert each item into an emission - (rx/switch-map read-item)))) + (try + (let [cboard (unchecked-get js/navigator "clipboard") + read-item (fn [item] + (let [img-type (->> (.-types ^js item) + (d/seek #(str/starts-with? % "image/")))] + (if img-type + (rx/from (.getType ^js item img-type)) + (rx/empty))))] + (->> (rx/from (.read ^js cboard)) ;; Get a stream of item lists + (rx/mapcat identity) ;; Convert each item into an emission + (rx/switch-map read-item))) + (catch :default cause + (rx/throw cause)))) (defn read-from-paste-event [event] (let [target (.-target ^js event)] - (when (and (not (.-isContentEditable target)) ;; ignore when pasting into - (not= (.-tagName target) "INPUT")) ;; an editable control + (when (and (not (.-isContentEditable ^js target)) ;; ignore when pasting into + (not= (.-tagName ^js target) "INPUT")) ;; an editable control (.. ^js event getBrowserEvent -clipboardData)))) (defn extract-text [clipboard-data] - (when clipboard-data - (.getData clipboard-data "text"))) + (.getData clipboard-data "text")) (defn extract-images + "Get image files from clipboard data. Returns a native js array." [clipboard-data] - (when clipboard-data - (let [file-list (-> (.-files ^js clipboard-data))] - (->> (range (.-length ^js file-list)) - (map #(.item ^js file-list %)) - (filter #(str/starts-with? (.-type %) "image/")))))) + (let [files (obj/into-array (.-files ^js clipboard-data))] + (.filter ^js files #(str/starts-with? (obj/get % "type") "image/")))) (defn create-canvas-element [width height] @@ -152,15 +167,19 @@ (js/createImageBitmap image options))) (defn create-image - [src width height] - (p/create - (fn [resolve reject] - (let [img (.createElement js/document "img")] - (obj/set! img "width" width) - (obj/set! img "height" height) - (obj/set! img "src" src) - (obj/set! img "onload" #(resolve img)) - (obj/set! img "onerror" reject))))) + ([src] + (create-image src nil nil)) + ([src width height] + (p/create + (fn [resolve reject] + (let [img (.createElement js/document "img")] + (when-not (nil? width) + (obj/set! img "width" width)) + (when-not (nil? height) + (obj/set! img "height" height)) + (obj/set! img "src" src) + (obj/set! img "onload" #(resolve img)) + (obj/set! img "onerror" reject)))))) ;; Why this? Because as described in https://bugs.chromium.org/p/chromium/issues/detail?id=1463435 ;; the createImageBitmap seems to apply premultiplied alpha multiples times on the same image @@ -169,23 +188,10 @@ ([image] (create-image-bitmap-with-workaround image nil)) ([^js image options] - (let [width (.-value (.-baseVal (.-width image))) - height (.-value (.-baseVal (.-height image))) - [width height] (th/get-proportional-size width height) - - image-source - (if (cf/check-browser? :safari) - (let [src (.-baseVal (.-href image))] - (create-image src width height)) - (p/resolved image))] - - (-> image-source - (p/then - (fn [html-img] - (let [offscreen-canvas (create-offscreen-canvas width height) - offscreen-context (.getContext offscreen-canvas "2d")] - (.drawImage offscreen-context html-img 0 0) - (create-image-bitmap offscreen-canvas options)))))))) + (let [offscreen-canvas (create-offscreen-canvas (.-width image) (.-height image)) + offscreen-context (.getContext offscreen-canvas "2d")] + (.drawImage offscreen-context image 0 0) + (create-image-bitmap offscreen-canvas options)))) (defn request-fullscreen [el] @@ -241,7 +247,7 @@ (fn [blob] (->> (read-file-as-data-url blob) (rx/catch (fn [err] (reject err))) - (rx/subs (fn [result] (resolve result))))))) + (rx/subs! (fn [result] (resolve result))))))) (catch :default e (reject e)))))) diff --git a/frontend/src/app/util/websocket.cljs b/frontend/src/app/util/websocket.cljs index 61187b2a6c..016eb9c0ae 100644 --- a/frontend/src/app/util/websocket.cljs +++ b/frontend/src/app/util/websocket.cljs @@ -8,7 +8,7 @@ "A interface to webworkers exposed functionality." (:require [app.common.transit :as t] - [beicon.core :as rx] + [beicon.v2.core :as rx] [goog.events :as ev]) (:import goog.net.WebSocket diff --git a/frontend/src/app/util/worker.cljs b/frontend/src/app/util/worker.cljs index 2100b58508..f1b27cc7c6 100644 --- a/frontend/src/app/util/worker.cljs +++ b/frontend/src/app/util/worker.cljs @@ -10,7 +10,7 @@ [app.common.uuid :as uuid] [app.util.object :as obj] [app.worker.messages :as wm] - [beicon.core :as rx])) + [beicon.v2.core :as rx])) (declare handle-response) (defrecord Worker [instance stream]) diff --git a/frontend/src/app/util/zip.cljs b/frontend/src/app/util/zip.cljs index fbb0b27872..bf1995a690 100644 --- a/frontend/src/app/util/zip.cljs +++ b/frontend/src/app/util/zip.cljs @@ -9,7 +9,7 @@ (:require ["jszip" :as zip] [app.util.http :as http] - [beicon.core :as rx] + [beicon.v2.core :as rx] [promesa.core :as p])) (defn compress-files @@ -29,7 +29,7 @@ :response-type :blob :method :get}) (rx/map :body) - (rx/flat-map zip/loadAsync))) + (rx/merge-map zip/loadAsync))) (defn- process-file [entry path type] @@ -65,4 +65,4 @@ (.forEach zip get-file) (->> (rx/from (p/all @promises)) - (rx/flat-map identity)))) + (rx/merge-map identity)))) diff --git a/frontend/src/app/worker.cljs b/frontend/src/app/worker.cljs index 4b0171cb63..4b36decc98 100644 --- a/frontend/src/app/worker.cljs +++ b/frontend/src/app/worker.cljs @@ -17,30 +17,31 @@ [app.worker.selection] [app.worker.snaps] [app.worker.thumbnails] - [beicon.core :as rx] + [beicon.v2.core :as rx] [promesa.core :as p])) (log/setup! {:app :info}) ;; --- Messages Handling -(def schema:message - [:map {:title "WorkerMessage"} - [:sender-id ::sm/uuid] - [:payload - [:map - [:cmd :keyword]]] - [:buffer? {:optional true} :boolean]]) - -(def message? - (sm/pred-fn schema:message)) +(def ^:private + schema:message + (sm/define + [:map {:title "WorkerMessage"} + [:sender-id ::sm/uuid] + [:payload + [:map + [:cmd :keyword]]] + [:buffer? {:optional true} :boolean]])) (def buffer (rx/subject)) (defn- handle-message "Process the message and returns to the client" [{:keys [sender-id payload transfer] :as message}] - (dm/assert! (message? message)) + (dm/assert! + "expected valid message" + (sm/check! schema:message message)) (letfn [(post [msg] (let [msg (-> msg (assoc :reply-to sender-id) (wm/encode))] (.postMessage js/self msg))) @@ -86,7 +87,9 @@ (defn- drop-message "Sends to the client a notification that its messages have been dropped" [{:keys [sender-id] :as message}] - (dm/assert! (message? message)) + (dm/assert! + "expected valid message" + (sm/check! schema:message message)) (.postMessage js/self (wm/encode {:reply-to sender-id :dropped true}))) @@ -127,18 +130,18 @@ ;; 1ms debounce, after 1ms without messages will process the buffer (rx/debounce 1) - (rx/subs (fn [[messages dropped last]] + (rx/subs! (fn [[messages dropped last]] ;; Send back the dropped messages replies - (doseq [msg dropped] - (drop-message msg)) + (doseq [msg dropped] + (drop-message msg)) ;; Process the message - (doseq [msg (vals messages)] - (handle-message msg)) + (doseq [msg (vals messages)] + (handle-message msg)) ;; After process the buffer we send a clear - (when-not (= last ::clear) - (rx/push! buffer ::clear))))))) + (when-not (= last ::clear) + (rx/push! buffer ::clear))))))) (defonce process-message-sub (subscribe-buffer-messages)) diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs index 479f40ce5e..604845c73c 100644 --- a/frontend/src/app/worker/export.cljs +++ b/frontend/src/app/worker/export.cljs @@ -10,7 +10,9 @@ [app.common.media :as cm] [app.common.text :as ct] [app.common.types.components-list :as ctkl] + [app.common.types.file :as ctf] [app.config :as cfg] + [app.main.features.pointer-map :as fpmap] [app.main.render :as r] [app.main.repo :as rp] [app.util.http :as http] @@ -18,14 +20,14 @@ [app.util.webapi :as wapi] [app.util.zip :as uz] [app.worker.impl :as impl] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cuerdas.core :as str])) (def ^:const current-version 2) (defn create-manifest "Creates a manifest entry for the given files" - [team-id file-id export-type files components-v2] + [team-id file-id export-type files features] (letfn [(format-page [manifest page] (-> manifest (assoc (str (:id page)) @@ -38,10 +40,7 @@ (mapv str)) index (->> (get-in file [:data :pages-index]) (vals) - (reduce format-page {})) - features (cond-> [] - components-v2 - (conj "components/v2"))] + (reduce format-page {}))] (-> manifest (assoc (str (:id file)) {:name name @@ -140,7 +139,7 @@ (->> (rx/from (vals media)) (rx/map #(assoc % :file-id file-id)) - (rx/flat-map + (rx/merge-map (fn [media] (let [file-path (str/concat file-id "/media/" (:id media) (cm/mtype->extension (:mtype media)))] (->> (http/send! @@ -161,71 +160,14 @@ (rx/map #(vector (str (:id file) "/deleted-components.svg") %)))) (defn fetch-file-with-libraries - [file-id components-v2] - (let [features (cond-> #{} components-v2 (conj "components/v2"))] - (->> (rx/zip (rp/cmd! :get-file {:id file-id :features features}) - (rp/cmd! :get-file-libraries {:file-id file-id})) - (rx/map - (fn [[file file-libraries]] - (let [libraries-ids (->> file-libraries (map :id) (filterv #(not= (:id file) %)))] - (assoc file :libraries libraries-ids))))))) - -(defn get-component-ref-file - [objects shape] - - (cond - (contains? shape :component-file) - (get shape :component-file) - - (contains? shape :shape-ref) - (recur objects (get objects (:parent-id shape))) - - :else - nil)) - -(defn detach-external-references - [file file-id] - (let [detach-text - (fn [content] - (->> content - (ct/transform-nodes - #(cond-> % - (not= file-id (:fill-color-ref-file %)) - (dissoc :fill-color-ref-id :fill-color-ref-file) - - (not= file-id (:typography-ref-file %)) - (dissoc :typography-ref-id :typography-ref-file))))) - - detach-shape - (fn [objects shape] - (cond-> shape - (not= file-id (:fill-color-ref-file shape)) - (dissoc :fill-color-ref-id :fill-color-ref-file) - - (not= file-id (:stroke-color-ref-file shape)) - (dissoc :stroke-color-ref-id :stroke-color-ref-file) - - (not= file-id (get-component-ref-file objects shape)) - (dissoc :component-id :component-file :shape-ref :component-root?) - - (= :text (:type shape)) - (update :content detach-text))) - - detach-objects - (fn [objects] - (->> objects - (d/mapm #(detach-shape objects %2)))) - - detach-pages - (fn [pages-index] - (->> pages-index - (d/mapm - (fn [_ data] - (-> data - (update :objects detach-objects))))))] - - (-> file - (update-in [:data :pages-index] detach-pages)))) + [file-id features] + (->> (rx/zip (->> (rp/cmd! :get-file {:id file-id :features features}) + (rx/mapcat fpmap/resolve-file)) + (rp/cmd! :get-file-libraries {:file-id file-id})) + (rx/map + (fn [[file file-libraries]] + (let [libraries-ids (->> file-libraries (map :id) (filterv #(not= (:id file) %)))] + (assoc file :libraries libraries-ids)))))) (defn make-local-external-references [file file-id] @@ -287,8 +229,7 @@ (contains? node :typography-ref-id) (conj {:id (:typography-ref-id node) - :file-id (:typography-ref-file node)}) - ))) + :file-id (:typography-ref-file node)})))) (into []))) @@ -359,12 +300,11 @@ (update file-id make-local-external-references file-id) (update file-id dissoc :libraries))) :detach (-> (select-keys files [file-id]) - (update file-id detach-external-references file-id) + (update file-id ctf/detach-external-references file-id) (update file-id dissoc :libraries)))) (defn collect-files - [file-id export-type components-v2] - + [file-id export-type features] (letfn [(fetch-dependencies [[files pending]] (if (empty? pending) ;; When not pending, we finish the generation @@ -377,7 +317,7 @@ ;; The file is already in the result (rx/of [files pending]) - (->> (fetch-file-with-libraries next components-v2) + (->> (fetch-file-with-libraries next features) (rx/map (fn [file] [(-> files @@ -393,56 +333,55 @@ (rx/map #(process-export file-id export-type %)))))) (defn export-file - [team-id file-id export-type components-v2] - - (let [files-stream (->> (collect-files file-id export-type components-v2) + [team-id file-id export-type features] + (let [files-stream (->> (collect-files file-id export-type features) (rx/share)) manifest-stream (->> files-stream - (rx/map #(create-manifest team-id file-id export-type % components-v2)) + (rx/map #(create-manifest team-id file-id export-type % features)) (rx/map #(vector "manifest.json" %))) render-stream (->> files-stream - (rx/flat-map vals) - (rx/flat-map process-pages) + (rx/merge-map vals) + (rx/merge-map process-pages) (rx/observe-on :async) - (rx/flat-map get-page-data) + (rx/merge-map get-page-data) (rx/share)) colors-stream (->> files-stream - (rx/flat-map vals) + (rx/merge-map vals) (rx/map #(vector (:id %) (get-in % [:data :colors]))) (rx/filter #(d/not-empty? (second %))) (rx/map parse-library-color)) typographies-stream (->> files-stream - (rx/flat-map vals) + (rx/merge-map vals) (rx/map #(vector (:id %) (get-in % [:data :typographies]))) (rx/filter #(d/not-empty? (second %))) (rx/map parse-library-typographies)) media-stream (->> files-stream - (rx/flat-map vals) + (rx/merge-map vals) (rx/map #(vector (:id %) (get-in % [:data :media]))) (rx/filter #(d/not-empty? (second %))) - (rx/flat-map parse-library-media)) + (rx/merge-map parse-library-media)) components-stream (->> files-stream - (rx/flat-map vals) + (rx/merge-map vals) (rx/filter #(d/not-empty? (ctkl/components-seq (:data %)))) - (rx/flat-map parse-library-components)) + (rx/merge-map parse-library-components)) deleted-components-stream (->> files-stream - (rx/flat-map vals) + (rx/merge-map vals) (rx/filter #(d/not-empty? (get-in % [:data :deleted-components]))) - (rx/flat-map parse-deleted-components)) + (rx/merge-map parse-deleted-components)) pages-stream (->> render-stream @@ -465,9 +404,9 @@ typographies-stream) (rx/reduce conj []) (rx/with-latest-from files-stream) - (rx/flat-map (fn [[data files]] - (->> (uz/compress-files data) - (rx/map #(vector (get files file-id) %))))))))) + (rx/merge-map (fn [[data files]] + (->> (uz/compress-files data) + (rx/map #(vector (get files file-id) %))))))))) (defmethod impl/handler :export-binary-file [{:keys [files export-type] :as message}] @@ -475,8 +414,8 @@ (rx/mapcat (fn [file] (->> (rp/cmd! :export-binfile {:file-id (:id file) - :include-libraries? (= export-type :all) - :embed-assets? (= export-type :merge)}) + :include-libraries (= export-type :all) + :embed-assets (= export-type :merge)}) (rx/map #(hash-map :type :finish :file-id (:id file) :filename (:name file) @@ -490,12 +429,12 @@ :file-id (:id file)})))))))) (defmethod impl/handler :export-standard-file - [{:keys [team-id files export-type components-v2] :as message}] + [{:keys [team-id files export-type features] :as message}] (->> (rx/from files) (rx/mapcat (fn [file] - (->> (export-file team-id (:id file) export-type components-v2) + (->> (export-file team-id (:id file) export-type features) (rx/map (fn [value] (if (contains? value :type) @@ -507,8 +446,8 @@ :mtype "application/zip" :description "Penpot export (*.zip)" :uri (wapi/create-uri export-blob)})))) - (rx/catch - (fn [err] - (rx/of {:type :error - :error (str err) - :file-id (:id file)})))))))) + (rx/catch (fn [err] + (js/console.error err) + (rx/of {:type :error + :error (str err) + :file-id (:id file)})))))))) diff --git a/frontend/src/app/worker/impl.cljs b/frontend/src/app/worker/impl.cljs index f2ca409ba9..38beeb28d4 100644 --- a/frontend/src/app/worker/impl.cljs +++ b/frontend/src/app/worker/impl.cljs @@ -7,8 +7,8 @@ (ns app.worker.impl (:require [app.common.data.macros :as dm] + [app.common.files.changes :as ch] [app.common.logging :as log] - [app.common.pages.changes :as ch] [app.config :as cf] [okulary.core :as l])) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 3fcfe2c686..998459c604 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -9,22 +9,24 @@ (:require ["jszip" :as zip] [app.common.data :as d] - [app.common.file-builder :as fb] + [app.common.files.builder :as fb] [app.common.geom.point :as gpt] [app.common.geom.shapes.path :as gpa] [app.common.logging :as log] [app.common.media :as cm] + [app.common.pprint :as pp] [app.common.text :as ct] [app.common.uuid :as uuid] [app.main.repo :as rp] [app.util.http :as http] [app.util.i18n :as i18n :refer [tr]] - [app.util.import.parser :as cip] [app.util.json :as json] + [app.util.sse :as sse] [app.util.webapi :as wapi] [app.util.zip :as uz] [app.worker.impl :as impl] - [beicon.core :as rx] + [app.worker.import.parser :as parser] + [beicon.v2.core :as rx] [cuerdas.core :as str] [tubax.core :as tubax])) @@ -126,17 +128,20 @@ (defn create-file "Create a new file on the back-end" - [context] + [context features] (let [resolve-fn (:resolve context) - file-id (resolve-fn (:file-id context)) - features (into #{} (:features context))] + file-id (resolve-fn (:file-id context))] (rp/cmd! :create-temp-file {:id file-id :name (:name context) :is-shared (:shared context) :project-id (:project-id context) :create-page false - :features features}))) + + ;; If the features object exists send that. Otherwise we remove the components/v2 because + ;; if the features attribute doesn't exist is a version < 2.0. The other features will + ;; be kept so the shapes are created full featured + :features (d/nilv (:features context) (disj features "components/v2"))}))) (defn link-file-libraries "Create a new file on the back-end" @@ -146,14 +151,15 @@ libraries (->> context :libraries (mapv resolve))] (->> (rx/from libraries) (rx/map #(hash-map :file-id file-id :library-id %)) - (rx/flat-map (partial rp/cmd! :link-file-to-library))))) + (rx/merge-map (partial rp/cmd! :link-file-to-library))))) (defn send-changes "Creates batches of changes to be sent to the backend" [context file] (let [file-id (:id file) session-id (uuid/next) - batches (->> (fb/generate-changes file) + changes (fb/generate-changes file) + batches (->> changes (partition change-batch-size change-batch-size nil) (mapv vec)) @@ -196,9 +202,10 @@ :content blob :is-local true})) (rx/tap #(progress! context :upload-media name)) - (rx/flat-map #(rp/cmd! :upload-file-media-object %)))) + (rx/merge-map #(rp/cmd! :upload-file-media-object %)))) -(defn resolve-text-content [node context] +(defn resolve-text-content + [node context] (let [resolve (:resolve context)] (->> node (ct/transform-nodes @@ -229,7 +236,16 @@ (d/update-when :shape-ref resolve) (cond-> (= type :text) - (d/update-when :content resolve-text-content context))))) + (d/update-when :content resolve-text-content context)) + + (cond-> (and (= type :frame) (= :grid (:layout data))) + (update + :layout-grid-cells + (fn [cells] + (->> (vals cells) + (reduce (fn [cells {:keys [id shapes]}] + (assoc-in cells [id :shapes] (mapv resolve shapes))) + cells)))))))) (defn- translate-frame [data type file] @@ -248,8 +264,8 @@ (defn process-import-node [context file node] - (let [type (cip/get-type node) - close? (cip/close? node)] + (let [type (parser/get-type node) + close? (parser/close? node)] (if close? (case type :frame (fb/close-artboard file) @@ -259,11 +275,11 @@ #_default file) (let [resolve (:resolve context) - old-id (cip/get-id node) - interactions (->> (cip/parse-interactions node) + old-id (parser/get-id node) + interactions (->> (parser/parse-interactions node) (mapv #(update % :destination resolve))) - data (-> (cip/parse-data type node) + data (-> (parser/parse-data type node) (resolve-data-ids type context) (cond-> (some? old-id) (assoc :id (resolve old-id))) @@ -308,43 +324,72 @@ (defn resolve-media [context file-id node] - (if (and (not (cip/close? node)) - (cip/has-image? node)) - (let [name (cip/get-image-name node) - image-data (cip/get-image-data node) - image-fill (cip/get-image-fill node)] - (->> (upload-media-files context file-id name image-data) - (rx/catch #(do (.error js/console "Error uploading media: " name) - (rx/of node))) - (rx/map - (fn [media] - (-> node - (assoc-in [:attrs :penpot:media-id] (:id media)) - (assoc-in [:attrs :penpot:media-width] (:width media)) - (assoc-in [:attrs :penpot:media-height] (:height media)) - (assoc-in [:attrs :penpot:media-mtype] (:mtype media)) + (if (or (and (not (parser/close? node)) + (parser/has-image? node)) + (parser/has-stroke-images? node) + (parser/has-fill-images? node)) + (let [name (parser/get-image-name node) + has-image (parser/has-image? node) + image-data (parser/get-image-data node) + image-fill (parser/get-image-fill node) + fill-images-data (->> (parser/get-fill-images-data node) + (map #(assoc % :type :fill))) + stroke-images-data (->> (parser/get-stroke-images-data node) + (map #(assoc % :type :stroke))) - (assoc-in [:attrs :penpot:fill-color] (:fill image-fill)) - (assoc-in [:attrs :penpot:fill-color-ref-file] (:fill-color-ref-file image-fill)) - (assoc-in [:attrs :penpot:fill-color-ref-id] (:fill-color-ref-id image-fill)) - (assoc-in [:attrs :penpot:fill-opacity] (:fill-opacity image-fill)) - (assoc-in [:attrs :penpot:fill-color-gradient] (:fill-color-gradient image-fill))))))) + images-data (concat + fill-images-data + stroke-images-data + (when has-image + [{:href image-data}]))] + (->> (rx/from images-data) + (rx/mapcat (fn [image-data] + (->> (upload-media-files context file-id name (:href image-data)) + (rx/catch #(do (.error js/console "Error uploading media: " name) + (rx/of node))) + (rx/map (fn [data] + (let [data + (cond-> data + (some? (:keep-aspect-ratio image-data)) + (assoc :keep-aspect-ratio (:keep-aspect-ratio image-data)))] + [(:id image-data) data])))))) + (rx/reduce (fn [acc [id data]] (assoc acc id data)) {}) + (rx/map + (fn [images] + (let [media (get images nil)] + (-> node + (assoc :images images) + (cond-> (some? media) + (-> + (assoc-in [:attrs :penpot:media-id] (:id media)) + (assoc-in [:attrs :penpot:media-width] (:width media)) + (assoc-in [:attrs :penpot:media-height] (:height media)) + (assoc-in [:attrs :penpot:media-mtype] (:mtype media)) + (cond-> (some? (:keep-aspect-ratio media)) + (assoc-in [:attrs :penpot:media-keep-aspect-ratio] (:keep-aspect-ratio media))) + (assoc-in [:attrs :penpot:fill-color] (:fill image-fill)) + (assoc-in [:attrs :penpot:fill-color-ref-file] (:fill-color-ref-file image-fill)) + (assoc-in [:attrs :penpot:fill-color-ref-id] (:fill-color-ref-id image-fill)) + (assoc-in [:attrs :penpot:fill-opacity] (:fill-opacity image-fill)) + (assoc-in [:attrs :penpot:fill-color-gradient] (:fill-color-gradient image-fill)))))))))) ;; If the node is not an image just return the node (->> (rx/of node) (rx/observe-on :async)))) (defn media-node? [node] - (and (cip/shape? node) - (cip/has-image? node) - (not (cip/close? node)))) + (or (and (parser/shape? node) + (parser/has-image? node) + (not (parser/close? node))) + (parser/has-stroke-images? node) + (parser/has-fill-images? node))) (defn import-page [context file [page-id page-name content]] - (let [nodes (->> content cip/node-seq) + (let [nodes (->> content parser/node-seq) file-id (:id file) resolve (:resolve context) - page-data (-> (cip/parse-page-data content) + page-data (-> (parser/parse-page-data content) (assoc :name page-name) (assoc :id (resolve page-id))) flows (->> (get-in page-data [:options :flows]) @@ -370,39 +415,40 @@ (rx/mapcat (fn [node] (->> (resolve-media context file-id node) - (rx/map (fn [result] [node result]))))) + (rx/map (fn [result] + [node result]))))) (rx/reduce conj {}))] (->> pre-process-images - (rx/flat-map + (rx/merge-map (fn [pre-proc] (->> (rx/from nodes) - (rx/filter cip/shape?) + (rx/filter parser/shape?) (rx/map (fn [node] (or (get pre-proc node) node))) (rx/reduce (partial process-import-node context) file) (rx/map (comp fb/close-page setup-interactions)))))))) (defn import-component [context file node] (let [resolve (:resolve context) - content (cip/find-node node :g) + content (parser/find-node node :g) file-id (:id file) - old-id (cip/get-id node) + old-id (parser/get-id node) id (resolve old-id) path (get-in node [:attrs :penpot:path] "") - type (cip/get-type content) + type (parser/get-type content) main-instance-id (resolve (uuid (get-in node [:attrs :penpot:main-instance-id] ""))) main-instance-page (resolve (uuid (get-in node [:attrs :penpot:main-instance-page] ""))) - data (-> (cip/parse-data type content) + data (-> (parser/parse-data type content) (assoc :path path) (assoc :id id) (assoc :main-instance-id main-instance-id) (assoc :main-instance-page main-instance-page)) file (-> file (fb/start-component data type)) - children (cip/node-seq node)] + children (parser/node-seq node)] (->> (rx/from children) - (rx/filter cip/shape?) + (rx/filter parser/shape?) (rx/skip 1) ;; Skip the outer component and the respective closint tag (rx/skip-last 1) ;; because they are handled in start-component an finish-component (rx/mapcat (partial resolve-media context file-id)) @@ -411,18 +457,18 @@ (defn import-deleted-component [context file node] (let [resolve (:resolve context) - content (cip/find-node node :g) + content (parser/find-node node :g) file-id (:id file) - old-id (cip/get-id node) + old-id (parser/get-id node) id (resolve old-id) path (get-in node [:attrs :penpot:path] "") main-instance-id (resolve (uuid (get-in node [:attrs :penpot:main-instance-id] ""))) main-instance-page (resolve (uuid (get-in node [:attrs :penpot:main-instance-page] ""))) main-instance-x (get-in node [:attrs :penpot:main-instance-x] "") main-instance-y (get-in node [:attrs :penpot:main-instance-y] "") - type (cip/get-type content) + type (parser/get-type content) - data (-> (cip/parse-data type content) + data (-> (parser/parse-data type content) (assoc :path path) (assoc :id id) (assoc :main-instance-id main-instance-id) @@ -432,10 +478,10 @@ file (-> file (fb/start-component data)) component-id (:current-component-id file) - children (cip/node-seq node)] + children (parser/node-seq node)] (->> (rx/from children) - (rx/filter cip/shape?) + (rx/filter parser/shape?) (rx/skip 1) (rx/skip-last 1) (rx/mapcat (partial resolve-media context file-id)) @@ -476,7 +522,7 @@ (assoc :id (resolve id)))] (fb/add-library-color file color)))] (->> (get-file context :colors) - (rx/flat-map (comp d/kebab-keys cip/string->uuid)) + (rx/merge-map (comp d/kebab-keys parser/string->uuid)) (rx/reduce add-color file))) (rx/of file))) @@ -486,7 +532,7 @@ (if (:has-typographies context) (let [resolve (:resolve context)] (->> (get-file context :typographies) - (rx/flat-map (comp d/kebab-keys cip/string->uuid)) + (rx/merge-map (comp d/kebab-keys parser/string->uuid)) (rx/map (fn [[id typography]] (-> typography (d/kebab-keys) @@ -500,10 +546,12 @@ (if (:has-media context) (let [resolve (:resolve context)] (->> (get-file context :media-list) - (rx/flat-map (comp d/kebab-keys cip/string->uuid)) + (rx/merge-map (comp d/kebab-keys parser/string->uuid)) (rx/mapcat (fn [[id media]] - (let [media (assoc media :id (resolve id))] + (let [media (-> media + (assoc :id (resolve id)) + (update :name str))] (->> (get-file context :media id media) (rx/map (fn [blob] (let [content (.slice blob 0 (.-size blob) (:mtype media))] @@ -515,7 +563,7 @@ (rx/tap #(progress! context :upload-media (:name %))) (rx/merge-map #(rp/cmd! :upload-file-media-object %)) (rx/map (constantly media)) - (rx/catch #(do (.error js/console (str "Error uploading media: " (:name media)) ) + (rx/catch #(do (.error js/console (str "Error uploading media: " (:name media))) (rx/empty))))))) (rx/reduce fb/add-library-media file))) (rx/of file))) @@ -524,11 +572,11 @@ [context file] (if (:has-components context) (let [split-components - (fn [content] (->> (cip/node-seq content) + (fn [content] (->> (parser/node-seq content) (filter #(= :symbol (:tag %)))))] (->> (get-file context :components) - (rx/flat-map split-components) + (rx/merge-map split-components) (rx/concat-reduce (partial import-component context) file))) (rx/of file))) @@ -536,11 +584,11 @@ [context file] (if (:has-deleted-components context) (let [split-components - (fn [content] (->> (cip/node-seq content) + (fn [content] (->> (parser/node-seq content) (filter #(= :symbol (:tag %)))))] (->> (get-file context :deleted-components) - (rx/flat-map split-components) + (rx/merge-map split-components) (rx/concat-reduce (partial import-deleted-component context) file))) (rx/of file))) @@ -551,34 +599,34 @@ context (assoc context :progress progress-str)] [progress-str (->> (rx/of file) - (rx/flat-map (partial process-pages context)) + (rx/merge-map (partial process-pages context)) (rx/tap #(progress! context :process-colors)) - (rx/flat-map (partial process-library-colors context)) + (rx/merge-map (partial process-library-colors context)) (rx/tap #(progress! context :process-typographies)) - (rx/flat-map (partial process-library-typographies context)) + (rx/merge-map (partial process-library-typographies context)) (rx/tap #(progress! context :process-media)) - (rx/flat-map (partial process-library-media context)) + (rx/merge-map (partial process-library-media context)) (rx/tap #(progress! context :process-components)) - (rx/flat-map (partial process-library-components context)) + (rx/merge-map (partial process-library-components context)) (rx/tap #(progress! context :process-deleted-components)) - (rx/flat-map (partial process-deleted-components context)) - (rx/flat-map (partial send-changes context)) + (rx/merge-map (partial process-deleted-components context)) + (rx/merge-map (partial send-changes context)) (rx/tap #(rx/end! progress-str)))])) (defn create-files - [context files] + [{:keys [system-features] :as context} files] (let [data (group-by :file-id files)] (rx/concat (->> (rx/from files) (rx/map #(merge context %)) - (rx/flat-map (fn [context] - (->> (create-file context) - (rx/map #(vector % (first (get data (:file-id context))))))))) + (rx/merge-map (fn [context] + (->> (create-file context system-features) + (rx/map #(vector % (first (get data (:file-id context))))))))) (->> (rx/from files) (rx/map #(merge context %)) - (rx/flat-map link-file-libraries) + (rx/merge-map link-file-libraries) (rx/ignore))))) (defn parse-mtype [ba] @@ -590,10 +638,10 @@ "other"))) (defmethod impl/handler :analyze-import - [{:keys [files]}] + [{:keys [files features]}] (->> (rx/from files) - (rx/flat-map + (rx/merge-map (fn [file] (let [st (->> (http/send! {:uri (:uri file) @@ -608,10 +656,19 @@ (->> (rx/merge (->> st (rx/filter (fn [data] (= "application/zip" (:type data)))) - (rx/flat-map #(zip/loadAsync (:body %))) - (rx/flat-map #(get-file {:zip %} :manifest)) - (rx/map (comp d/kebab-keys cip/string->uuid)) - (rx/map #(hash-map :uri (:uri file) :data % :type "application/zip"))) + (rx/merge-map #(zip/loadAsync (:body %))) + (rx/merge-map #(get-file {:zip %} :manifest)) + (rx/map (comp d/kebab-keys parser/string->uuid)) + (rx/map + (fn [data] + ;; Checks if the file is exported with components v2 and the current team only + ;; supports components v1 + (let [has-file-v2? + (->> (:files data) + (d/seek (fn [[_ file]] (contains? (set (:features file)) "components/v2"))))] + (if (and has-file-v2? (not (contains? features "components/v2"))) + {:uri (:uri file) :error "dashboard.import.analyze-error.components-v2"} + (hash-map :uri (:uri file) :data data :type "application/zip")))))) (->> st (rx/filter (fn [data] (= "application/octet-stream" (:type data)))) (rx/map (fn [_] @@ -631,56 +688,77 @@ (let [error (or (.-message data) (tr "dashboard.import.analyze-error"))] (rx/of {:uri (:uri file) :error error})))))))))) + (defmethod impl/handler :import-files - [{:keys [project-id files]}] + [{:keys [project-id files features]}] (let [context {:project-id project-id - :resolve (resolve-factory)} + :resolve (resolve-factory) + :system-features features} zip-files (filter #(= "application/zip" (:type %)) files) binary-files (filter #(= "application/octet-stream" (:type %)) files)] - (->> (rx/merge - (->> (create-files context zip-files) - (rx/flat-map - (fn [[file data]] - (->> (uz/load-from-url (:uri data)) - (rx/map #(-> context (assoc :zip %) (merge data))) - (rx/merge-map - (fn [context] - ;; process file retrieves a stream that will emit progress notifications - ;; and other that will emit the files once imported - (let [[progress-stream file-stream] (process-file context file)] - (rx/merge progress-stream - (->> file-stream - (rx/map - (fn [file] + (rx/merge + (->> (create-files context zip-files) + (rx/merge-map + (fn [[file data]] + (->> (uz/load-from-url (:uri data)) + (rx/map #(-> context (assoc :zip %) (merge data))) + (rx/merge-map + (fn [context] + ;; process file retrieves a stream that will emit progress notifications + ;; and other that will emit the files once imported + (let [[progress-stream file-stream] (process-file context file)] + (rx/merge progress-stream + (->> file-stream + (rx/map + (fn [file] + {:status :import-finish + :errors (:errors file) + :file-id (:file-id data)}))))))) + (rx/catch (fn [cause] + (log/error :hint (ex-message cause) + :file-id (:file-id data) + :cause cause) + (rx/of {:status :import-error + :file-id (:file-id data) + :error (ex-message cause) + :error-data (ex-data cause)}))))))) + + (->> (rx/from binary-files) + (rx/merge-map + (fn [data] + (->> (http/send! + {:uri (:uri data) + :response-type :blob + :method :get}) + (rx/map :body) + (rx/mapcat (fn [file] + (->> (rp/cmd! ::sse/import-binfile + {:name (str/replace (:name data) #".penpot$" "") + :file file + :project-id project-id}) + (rx/tap (fn [event] + (let [payload (sse/get-payload event) + type (sse/get-type event)] + (if (= type "progress") + (log/dbg :hint "import-binfile: progress" :section (:section payload) :name (:name payload)) + (log/dbg :hint "import-binfile: end"))))) + (rx/filter sse/end-of-stream?) + (rx/map (fn [_] {:status :import-finish - :errors (:errors file) - :file-id (:file-id data)}))))))) - (rx/catch (fn [cause] - (log/error :hint (ex-message cause) :file-id (:file-id data) :cause cause) - (rx/of {:status :import-error - :file-id (:file-id data) - :error (ex-message cause) - :error-data (ex-data cause)}))))))) + :file-id (:file-id data)}))))) + (rx/catch (fn [cause] + (log/error :hint "unexpected error on import process" + :project-id project-id + ::log/sync? true) + (let [edata (if (map? cause) cause (ex-data cause))] + (println "Error data:") + (pp/pprint (dissoc edata :explain) {:level 3 :length 10}) - (->> (rx/from binary-files) - (rx/flat-map - (fn [data] - (->> (http/send! - {:uri (:uri data) - :response-type :blob - :method :get}) - (rx/map :body) - (rx/mapcat #(rp/cmd! :import-binfile {:file % - :project-id project-id})) - (rx/map - (fn [_] - {:status :import-finish - :file-id (:file-id data)}))))))) + (when (string? (:explain edata)) + (js/console.log (:explain edata))) - (rx/catch (fn [cause] - (log/error :hint "unexpected error on import process" - :project-id project-id - :cause cause)))))) + (rx/of {:status :import-error + :file-id (:file-id data)}))))))))))) diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/worker/import/parser.cljs similarity index 80% rename from frontend/src/app/util/import/parser.cljs rename to frontend/src/app/worker/import/parser.cljs index 208d0bffdc..a0da1e60d0 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/worker/import/parser.cljs @@ -4,17 +4,17 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.util.import.parser +(ns app.worker.import.parser (:require + [app.common.colors :as cc] [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.svg.path :as svg.path] [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] - [app.util.color :as uc] [app.util.json :as json] - [app.util.path.parser :as upp] [cuerdas.core :as str])) (def url-regex @@ -101,6 +101,10 @@ (get-in (get-data m) [:attrs ns-att]))] (when val (val-fn val))))) +(defn find-node-by-metadata-value + [meta value coll] + (->> coll (d/seek #(= value (get-meta % meta))))) + (defn get-children [node] (cond-> (:content node) @@ -191,15 +195,22 @@ (d/deep-mapm (fn [pair] (->> pair (mapv convert))))))) -(def search-data-node? #{:rect :image :path :circle}) +(def search-data-node? #{:rect :path :circle}) (defn get-svg-data [type node] - (let [node-attrs (add-attrs {} (:attrs node))] (cond (search-data-node? type) - (let [data-tags #{:ellipse :rect :path :text :foreignObject :image}] + (let [data-tags #{:ellipse :rect :path :text :foreignObject}] + (->> node + (node-seq) + (filter #(contains? data-tags (:tag %))) + (map #(:attrs %)) + (reduce add-attrs node-attrs))) + + (= type :image) + (let [data-tags #{:rect :image}] (->> node (node-seq) (filter #(contains? data-tags (:tag %))) @@ -278,7 +289,7 @@ (defn parse-path [props center svg-data] - (let [content (upp/parse-path (:d svg-data))] + (let [content (svg.path/parse (:d svg-data))] (-> props (assoc :content content) (assoc :center center)))) @@ -413,10 +424,10 @@ :component-file component-file) component-root? - (assoc :component-root? component-root?) + (assoc :component-root component-root?) main-instance? - (assoc :main-instance? main-instance?) + (assoc :main-instance main-instance?) (some? shape-ref) (assoc :shape-ref shape-ref)))) @@ -429,7 +440,7 @@ fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid) meta-fill-color (get-meta node :fill-color) meta-fill-opacity (get-meta node :fill-opacity) - meta-fill-color-gradient (if (str/starts-with? meta-fill-color "url") + meta-fill-color-gradient (if (str/starts-with? meta-fill-color "url#fill-color-gradient") (parse-gradient node meta-fill-color) (get-meta node :fill-color-gradient)) gradient (when (str/starts-with? fill "url") @@ -454,7 +465,7 @@ :fill-color nil :fill-opacity nil) - (uc/hex? fill) + (cc/valid-hex-color? fill) (assoc :fill-color fill :fill-opacity (-> svg-data (:fill-opacity "1") d/parse-double)) @@ -465,13 +476,14 @@ (defn add-stroke [props node svg-data] - (let [stroke-style (get-meta node :stroke-style keyword) + (let [stroke-style (get-meta node :stroke-style keyword) stroke-alignment (get-meta node :stroke-alignment keyword) - stroke (:stroke svg-data) - gradient (when (str/starts-with? stroke "url") - (parse-gradient node stroke)) + stroke (:stroke svg-data) + gradient (when (str/starts-with? stroke "url(#stroke-color-gradient") + (parse-gradient node stroke)) + stroke-cap-start (get-meta node :stroke-cap-start keyword) - stroke-cap-end (get-meta node :stroke-cap-end keyword)] + stroke-cap-end (get-meta node :stroke-cap-end keyword)] (cond-> props :always @@ -518,7 +530,8 @@ (let [metadata {:id (get-meta node :media-id) :width (get-meta node :media-width) :height (get-meta node :media-height) - :mtype (get-meta node :media-mtype)}] + :mtype (get-meta node :media-mtype) + :keep-aspect-ratio (get-meta node :media-keep-aspect-ratio str->bool)}] (cond-> props (= type :image) (assoc :metadata metadata) @@ -538,7 +551,7 @@ (let [mask? (get-meta node :masked-group str->bool)] (cond-> props mask? - (assoc :masked-group? true)))) + (assoc :masked-group true)))) (defn add-bool-data [props node] @@ -728,17 +741,22 @@ (defn parse-fills [node svg-data] (let [fills-node (get-data node :penpot:fills) + images (:images node) fills (->> (find-all-nodes fills-node :penpot:fill) (mapv (fn [fill-node] - {:fill-color (when (not (str/starts-with? (get-meta fill-node :fill-color) "url")) - (get-meta fill-node :fill-color)) - :fill-color-gradient (when (str/starts-with? (get-meta fill-node :fill-color) "url") - (parse-gradient node (get-meta fill-node :fill-color))) - :fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/uuid) - :fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/uuid) - :fill-opacity (get-meta fill-node :fill-opacity d/parse-double)})) + (let [fill-image-id (get-meta fill-node :fill-image-id)] + {:fill-color (when (not (str/starts-with? (get-meta fill-node :fill-color) "url")) + (get-meta fill-node :fill-color)) + :fill-color-gradient (when (str/starts-with? (get-meta fill-node :fill-color) "url(#fill-color-gradient") + (parse-gradient node (get-meta fill-node :fill-color))) + :fill-image (when fill-image-id + (get images fill-image-id)) + :fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/uuid) + :fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/uuid) + :fill-opacity (get-meta fill-node :fill-opacity d/parse-double)}))) (mapv d/without-nils) (filterv #(not= (:fill-color %) "none")))] + (if (seq fills) fills (->> [(-> (add-fill {} node svg-data) @@ -748,22 +766,27 @@ (defn parse-strokes [node svg-data] (let [strokes-node (get-data node :penpot:strokes) + images (:images node) strokes (->> (find-all-nodes strokes-node :penpot:stroke) (mapv (fn [stroke-node] - {:stroke-color (when (not (str/starts-with? (get-meta stroke-node :stroke-color) "url")) - (get-meta stroke-node :stroke-color)) - :stroke-color-gradient (when (str/starts-with? (get-meta stroke-node :stroke-color) "url") - (parse-gradient node (get-meta stroke-node :stroke-color))) - :stroke-color-ref-file (get-meta stroke-node :stroke-color-ref-file uuid/uuid) - :stroke-color-ref-id (get-meta stroke-node :stroke-color-ref-id uuid/uuid) - :stroke-opacity (get-meta stroke-node :stroke-opacity d/parse-double) - :stroke-style (get-meta stroke-node :stroke-style keyword) - :stroke-width (get-meta stroke-node :stroke-width d/parse-double) - :stroke-alignment (get-meta stroke-node :stroke-alignment keyword) - :stroke-cap-start (get-meta stroke-node :stroke-cap-start keyword) - :stroke-cap-end (get-meta stroke-node :stroke-cap-end keyword)})) + (let [stroke-image-id (get-meta stroke-node :stroke-image-id)] + {:stroke-color (when (not (str/starts-with? (get-meta stroke-node :stroke-color) "url")) + (get-meta stroke-node :stroke-color)) + :stroke-color-gradient (when (str/starts-with? (get-meta stroke-node :stroke-color) "url(#stroke-color-gradient") + (parse-gradient node (get-meta stroke-node :stroke-color))) + :stroke-image (when stroke-image-id + (get images stroke-image-id)) + :stroke-color-ref-file (get-meta stroke-node :stroke-color-ref-file uuid/uuid) + :stroke-color-ref-id (get-meta stroke-node :stroke-color-ref-id uuid/uuid) + :stroke-opacity (get-meta stroke-node :stroke-opacity d/parse-double) + :stroke-style (get-meta stroke-node :stroke-style keyword) + :stroke-width (get-meta stroke-node :stroke-width d/parse-double) + :stroke-alignment (get-meta stroke-node :stroke-alignment keyword) + :stroke-cap-start (get-meta stroke-node :stroke-cap-start keyword) + :stroke-cap-end (get-meta stroke-node :stroke-cap-end keyword)}))) (mapv d/without-nils) (filterv #(not= (:stroke-color %) "none")))] + (if (seq strokes) strokes (->> [(-> (add-stroke {} node svg-data) @@ -797,13 +820,35 @@ (defn add-frame-data [props node] (let [grids (parse-grids node) show-content (get-meta node :show-content str->bool) - hide-in-viewer (get-meta node :hide-in-viewer str->bool)] + hide-in-viewer (get-meta node :hide-in-viewer str->bool) + use-for-thumbnail (get-meta node :use-for-thumbnail str->bool)] (-> props (assoc :show-content show-content) (assoc :hide-in-viewer hide-in-viewer) + (cond-> use-for-thumbnail + (assoc :use-for-thumbnail use-for-thumbnail)) (cond-> (d/not-empty? grids) (assoc :grids grids))))) +(defn get-stroke-images-data + [node] + (let [strokes + (-> node + (find-node :penpot:shape) + (find-node :penpot:strokes))] + (->> (find-all-nodes strokes :penpot:stroke) + (mapv (fn [stroke-node] + (let [id (get-in stroke-node [:attrs :penpot:stroke-image-id]) + image-node (->> node (node-seq) (find-node-by-id id))] + {:id id + :href (get-in image-node [:attrs :href])}))) + (filterv #(some? (:id %)))))) + +(defn has-stroke-images? + [node] + (let [stroke-images (get-stroke-images-data node)] + (> (count stroke-images) 0))) + (defn has-image? [node] (let [type (get-type node) @@ -812,7 +857,9 @@ (find-node :defs) (find-node :pattern) (find-node :g) + (find-node :g) (find-node :image))] + (or (= type :image) (some? pattern-image)))) @@ -827,12 +874,33 @@ (find-node :defs) (find-node :pattern) (find-node :g) + (find-node :g) (find-node :image) :attrs) image-data (get-svg-data :image node) - svg-data (or image-data pattern-data)] + svg-data (or pattern-data image-data)] (or (:href svg-data) (:xlink:href svg-data)))) +(defn get-fill-images-data + [node] + (let [fills + (-> node + (find-node :penpot:shape) + (find-node :penpot:fills))] + (->> (find-all-nodes fills :penpot:fill) + (mapv (fn [fill-node] + (let [id (get-in fill-node [:attrs :penpot:fill-image-id]) + image-node (->> node (node-seq) (find-node-by-id id))] + {:id id + :href (get-in image-node [:attrs :href]) + :keep-aspect-ratio (not= (get-in image-node [:attrs :preserveAspectRatio]) "none")}))) + (filterv #(some? (:id %)))))) + +(defn has-fill-images? + [node] + (let [fill-images (get-fill-images-data node)] + (> (count fill-images) 0))) + (defn get-image-fill [node] (let [linear-gradient-node (-> node @@ -871,12 +939,64 @@ :style parse-style)))) +(defn parse-grid-tracks + [node label] + (let [node (-> (get-data node :penpot:layout) + (find-node label))] + (->> node + :content + (mapv + (fn [track-node] + (let [{:keys [type value]} (-> track-node :attrs remove-penpot-prefix)] + {:type (keyword type) + :value (d/parse-double value)})))))) + +(defn parse-grid-cells + [node] + (let [node (-> (get-data node :penpot:layout) + (find-node :penpot:grid-cells))] + (->> node + :content + (mapv + (fn [cell-node] + (let [{:keys [id + area-name + row + row-span + column + column-span + position + align-self + justify-self + shapes]} (-> cell-node :attrs remove-penpot-prefix) + id (uuid/uuid id)] + [id (d/without-nils + {:id id + :area-name area-name + :row (d/parse-integer row) + :row-span (d/parse-integer row-span) + :column (d/parse-integer column) + :column-span (d/parse-integer column-span) + :position (keyword position) + :align-self (keyword align-self) + :justify-self (keyword justify-self) + :shapes (if (and (some? shapes) (d/not-empty? shapes)) + (->> (str/split shapes " ") + (mapv uuid/uuid)) + [])})]))) + (into {})))) + (defn add-layout-container-data [props node] (if-let [data (get-data node :penpot:layout)] - (merge props + (let [layout-grid-rows (parse-grid-tracks node :penpot:grid-rows) + layout-grid-columns (parse-grid-tracks node :penpot:grid-columns) + layout-grid-cells (parse-grid-cells node)] + (-> props + (merge (d/without-nils {:layout (get-meta data :layout keyword) :layout-flex-dir (get-meta data :layout-flex-dir keyword) + :layout-grid-dir (get-meta data :layout-grid-dir keyword) :layout-wrap-type (get-meta data :layout-wrap-type keyword) :layout-gap-type (get-meta data :layout-gap-type keyword) @@ -893,9 +1013,17 @@ :p3 (get-meta data :layout-padding-p3 d/parse-double) :p4 (get-meta data :layout-padding-p4 d/parse-double)}) + :layout-justify-items (get-meta data :layout-justify-items keyword) :layout-justify-content (get-meta data :layout-justify-content keyword) :layout-align-items (get-meta data :layout-align-items keyword) :layout-align-content (get-meta data :layout-align-content keyword)})) + + (cond-> (d/not-empty? layout-grid-rows) + (assoc :layout-grid-rows layout-grid-rows)) + (cond-> (d/not-empty? layout-grid-columns) + (assoc :layout-grid-columns layout-grid-columns)) + (cond-> (d/not-empty? layout-grid-cells) + (assoc :layout-grid-cells layout-grid-cells)))) props)) (defn add-layout-item-data [props node] @@ -916,7 +1044,9 @@ :layout-item-min-h (get-meta data :layout-item-min-h d/parse-double) :layout-item-max-w (get-meta data :layout-item-max-w d/parse-double) :layout-item-min-w (get-meta data :layout-item-min-w d/parse-double) - :layout-item-align-self (get-meta data :layout-item-align-self keyword)})) + :layout-item-align-self (get-meta data :layout-item-align-self keyword) + :layout-item-align-absolute (get-meta data :layout-item-align-absolute str->bool) + :layout-item-align-index (get-meta data :layout-item-align-index d/parse-double)})) props)) (defn parse-data diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index 5a4d6563a1..fa73fe356b 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -7,16 +7,21 @@ (ns app.worker.selection (:require [app.common.data :as d] + [app.common.files.helpers :as cfh] + [app.common.files.indices :as cfi] + [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.text :as gte] - [app.common.pages :as cp] - [app.common.pages.helpers :as cph] + [app.common.geom.shapes.text :as gst] + [app.common.types.modifiers :as ctm] [app.common.uuid :as uuid] [app.util.quadtree :as qdt] [app.worker.impl :as impl] [clojure.set :as set] [okulary.core :as l])) +;; FIXME: performance shape & rect static props + (def ^:const padding-percent 0.10) (defonce state (l/atom {})) @@ -26,13 +31,14 @@ (fn [index shape] (let [{:keys [x y width height]} (cond - (and (= :text (:type shape)) - (some? (:position-data shape)) - (d/not-empty? (:position-data shape))) - (gte/position-data-bounding-box shape) + (and ^boolean (cfh/text-shape? shape) + ^boolean (some? (:position-data shape)) + ^boolean (d/not-empty? (:position-data shape))) + (gst/shape->bounds shape) :else - (gsh/points->selrect (:points shape))) + (grc/points->rect (:points shape))) + shape-bound #js {:x x :y y :width width :height height} parents (get parents-index (:id shape)) @@ -55,7 +61,7 @@ (-> objects (dissoc uuid/zero) vals - gsh/selection-rect)) + gsh/shapes->rect)) (defn add-padding-bounds "Adds a padding to the bounds defined as a percent in the constant `padding-percent`. @@ -75,11 +81,11 @@ (defn- create-index [objects] (let [shapes (-> objects (dissoc uuid/zero) vals) - parents-index (cp/generate-child-all-parents-index objects) - clip-parents-index (cp/create-clip-index objects parents-index) + parents-index (cfi/generate-child-all-parents-index objects) + clip-parents-index (cfi/create-clip-index objects parents-index) - root-shapes (cph/get-immediate-children objects uuid/zero) - bounds (-> root-shapes gsh/selection-rect add-padding-bounds) + root-shapes (cfh/get-immediate-children objects uuid/zero) + bounds (-> root-shapes gsh/shapes->rect add-padding-bounds) index-shape (make-index-shape objects parents-index clip-parents-index) initial-quadtree (qdt/create (clj->js bounds)) @@ -97,13 +103,13 @@ changed-ids (into #{} (comp (filter #(not= % uuid/zero)) (filter changes?) - (mapcat #(into [%] (cph/get-children-ids new-objects %)))) + (mapcat #(into [%] (cfh/get-children-ids new-objects %)))) (set/union (set (keys old-objects)) (set (keys new-objects)))) shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?))) - parents-index (cp/generate-child-all-parents-index new-objects shapes) - clip-parents-index (cp/create-clip-index new-objects parents-index) + parents-index (cfi/generate-child-all-parents-index new-objects shapes) + clip-parents-index (cfi/create-clip-index new-objects parents-index) new-index (qdt/remove-all index changed-ids) @@ -113,7 +119,7 @@ (assoc data :index index))) (defn- query-index - [{index :index} rect frame-id full-frame? include-frames? ignore-groups? clip-children?] + [{index :index} rect frame-id full-frame? include-frames? ignore-groups? clip-children? using-selrect?] (let [result (-> (qdt/search index (clj->js rect)) (es6-iterator-seq)) @@ -121,7 +127,7 @@ match-criteria? (fn [shape] (and (not (:hidden shape)) - (or (= :frame (:type shape)) ;; We return frames even if blocked + (or (cfh/frame-shape? shape) ;; We return frames even if blocked (not (:blocked shape))) (or (not frame-id) (= frame-id (:frame-id shape))) (case (:type shape) @@ -129,16 +135,77 @@ (:bool :group) (not ignore-groups?) true) + ;; This condition controls when to check for overlapping. Otherwise the + ;; shape needs to be fully contained. (or (not full-frame?) - (not= :frame (:type shape)) + (and (not ignore-groups?) (contains? shape :component-id)) + (and (not ignore-groups?) (not (cfh/root-frame? shape))) (and (d/not-empty? (:shapes shape)) (gsh/rect-contains-shape? rect shape)) (and (empty? (:shapes shape)) (gsh/overlaps? shape rect))))) + overlaps-outer-shape? + (fn [shape] + (let [padding (->> (:strokes shape) + (map #(case (get % :stroke-alignment :center) + :center (:stroke-width % 0) + :outer (* 2 (:stroke-width % 0)) + :inner 0)) + (reduce d/max 0)) + + scalev (gpt/point (/ (+ (:width shape) padding) + (:width shape)) + (/ (+ (:height shape) padding) + (:height shape))) + + outer-shape (-> shape + (gsh/transform-shape (-> (ctm/empty) + (ctm/resize scalev (gsh/shape->center shape)))))] + + (gsh/overlaps? outer-shape rect))) + + overlaps-inner-shape? + (fn [shape] + (let [padding (->> (:strokes shape) + (map #(case (get % :stroke-alignment :center) + :center (:stroke-width % 0) + :outer 0 + :inner (* 2 (:stroke-width % 0)))) + (reduce d/max 0)) + + scalev (gpt/point (/ (- (:width shape) padding) + (:width shape)) + (/ (- (:height shape) padding) + (:height shape))) + + inner-shape (-> shape + (gsh/transform-shape (-> (ctm/empty) + (ctm/resize scalev (gsh/shape->center shape)))))] + (gsh/overlaps? inner-shape rect))) + + overlaps-path? + (fn [shape] + (let [padding (->> (:strokes shape) + (map :stroke-width) + (reduce d/max 5)) + center (grc/rect->center rect) + rect (grc/center->rect center padding)] + (gsh/overlaps-path? shape rect false))) + overlaps? (fn [shape] - (gsh/overlaps? shape rect)) + (if (and (false? using-selrect?) + (empty? (:fills shape)) + (not (contains? (-> shape :svg-attrs) :fill)) + (not (contains? (-> shape :svg-attrs :style) :fill))) + (case (:type shape) + ;; If the shape has no fills the overlap depends on the stroke + :rect (and (overlaps-outer-shape? shape) (not (overlaps-inner-shape? shape))) + :circle (and (overlaps-outer-shape? shape) (not (overlaps-inner-shape? shape))) + :path (overlaps-path? shape) + (gsh/overlaps? shape rect)) + (gsh/overlaps? shape rect))) overlaps-parent? (fn [clip-parents] @@ -176,14 +243,14 @@ ;; we can update the index. Otherwise we need to ;; re-create it. (if (and (some? index) - (gsh/contains-selrect? old-bounds new-bounds)) + (grc/contains-rect? old-bounds new-bounds)) (update-index index old-objects new-objects) (create-index new-objects))))) nil) (defmethod impl/handler :selection/query - [{:keys [page-id rect frame-id full-frame? include-frames? ignore-groups? clip-children?] - :or {full-frame? false include-frames? false clip-children? true} + [{:keys [page-id rect frame-id full-frame? include-frames? ignore-groups? clip-children? using-selrect?] + :or {full-frame? false include-frames? false clip-children? true using-selrect? false} :as message}] (when-let [index (get @state page-id)] - (query-index index rect frame-id full-frame? include-frames? ignore-groups? clip-children?))) + (query-index index rect frame-id full-frame? include-frames? ignore-groups? clip-children? using-selrect?))) diff --git a/frontend/src/app/worker/snaps.cljs b/frontend/src/app/worker/snaps.cljs index 8eeb04d622..77bf5d8f5a 100644 --- a/frontend/src/app/worker/snaps.cljs +++ b/frontend/src/app/worker/snaps.cljs @@ -6,7 +6,7 @@ (ns app.worker.snaps (:require - [app.common.geom.shapes.rect :as gpr] + [app.common.geom.rect :as grc] [app.util.snap-data :as sd] [app.worker.impl :as impl] [okulary.core :as l])) @@ -28,7 +28,9 @@ [{:keys [page-id frame-id axis ranges bounds] :as message}] (let [match-bounds? (fn [[_ data]] - (some #(gpr/contains-point? bounds %) (map :pt data)))] + (some #(or (= :guide (:type %)) + (= :layout (:type %)) + (grc/contains-point? bounds (:pt %))) data))] (->> (into [] (comp (mapcat #(sd/query @state page-id frame-id axis %)) (distinct)) diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index faba81cc11..161ed28d5a 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -14,12 +14,9 @@ [app.main.fonts :as fonts] [app.main.render :as render] [app.util.http :as http] - [app.util.time :as ts] - [app.util.webapi :as wapi] [app.worker.impl :as impl] - [beicon.core :as rx] + [beicon.v2.core :as rx] [okulary.core :as l] - [promesa.core :as p] [rumext.v2 :as mf])) (log/set-level! :trace) @@ -62,36 +59,34 @@ (defn- render-thumbnail [{:keys [page file-id revn] :as params}] - (binding [fonts/loaded-hints (l/atom #{})] - (let [objects (:objects page) - frame (some->> page :thumbnail-frame-id (get objects)) - element (if frame - (mf/element render/frame-svg #js {:objects objects :frame frame :show-thumbnails? true}) - (mf/element render/page-svg #js {:data page :thumbnails? true :render-embed? true})) - data (rds/renderToStaticMarkup element)] + (try + (binding [fonts/loaded-hints (l/atom #{})] + (let [objects (:objects page) + frame (some->> page :thumbnail-frame-id (get objects)) + background-color (dm/get-in page [:options :background]) + element (if frame + (mf/element render/frame-svg #js + {:objects objects + :frame frame + :use-thumbnails true + :background-color background-color + :aspect-ratio (/ 2 3)}) - {:data data - :fonts @fonts/loaded-hints - :file-id file-id - :revn revn}))) + (mf/element render/page-svg #js + {:data page + :use-thumbnails true + :embed true + :aspect-ratio (/ 2 3)})) + data (rds/renderToStaticMarkup element)] + {:data data + :fonts @fonts/loaded-hints + :file-id file-id + :revn revn})) + (catch :default cause + (js/console.error "unexpected error on rendering thumbnail" cause) + nil))) (defmethod impl/handler :thumbnails/generate-for-file [{:keys [file-id revn features] :as message} _] (->> (request-data-for-thumbnail file-id revn features) (rx/map render-thumbnail))) - -(defmethod impl/handler :thumbnails/render-offscreen-canvas - [_ ibpm] - (let [canvas (js/OffscreenCanvas. (.-width ^js ibpm) (.-height ^js ibpm)) - ctx (.getContext ^js canvas "bitmaprenderer") - tp (ts/tpoint-ms)] - - (.transferFromImageBitmap ^js ctx ibpm) - - (->> (.convertToBlob ^js canvas #js {:type "image/png"}) - (p/fmap (fn [blob] - {:result (wapi/create-uri blob)})) - (p/fnly (fn [_] - (log/debug :hint "generated thumbnail" :elapsed (dm/str (tp) "ms")) - (.close ^js ibpm)))))) - diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index ee8f76d87f..7f5c6a562d 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -7,99 +7,47 @@ (ns debug (:require [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.repair :as cfr] + [app.common.files.validate :as cfv] [app.common.logging :as l] [app.common.math :as mth] [app.common.transit :as t] [app.common.types.file :as ctf] + [app.common.uri :as u] [app.common.uuid :as uuid] + [app.config :as cf] [app.main.data.dashboard.shortcuts] + [app.main.data.preview :as dp] [app.main.data.viewer.shortcuts] [app.main.data.workspace :as dw] - [app.main.data.workspace.changes :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.path.shortcuts] + [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shortcuts] + [app.main.errors :as errors] + [app.main.features :as features] + [app.main.repo :as rp] [app.main.store :as st] + [app.util.debug :as dbg] [app.util.dom :as dom] + [app.util.http :as http] [app.util.object :as obj] [app.util.timers :as timers] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.pprint :refer [pprint]] [cuerdas.core :as str] - [potok.core :as ptk] + [potok.v2.core :as ptk] [promesa.core :as p])) +(l/set-level! :debug) + (defn ^:export set-logging ([level] (l/set-level! :app (keyword level))) ([ns level] (l/set-level! (keyword ns) (keyword level)))) -(def debug-options - #{;; Displays the bounding box for the shapes - :bounding-boxes - - ;; Displays an overlay over the groups - :group - - ;; Displays in the console log the events through the application - :events - - ;; Display the boxes that represent the rotation and resize handlers - :handlers - - ;; Displays the center of a selection - :selection-center - - ;; When active the single selection will not take into account previous transformations - ;; this is useful to debug transforms - :simple-selection - - ;; When active the thumbnails will be displayed with a sepia filter - :thumbnails - - ;; When active we can check in the browser the export values - :show-export-metadata - - ;; Show text fragments outlines - :text-outline - - ;; Disable thumbnail cache - :disable-thumbnail-cache - - ;; Disable frame thumbnails - :disable-frame-thumbnails - - ;; Force thumbnails always (independent of selection or zoom level) - :force-frame-thumbnails - - ;; Enable a widget to show the auto-layout drop-zones - :layout-drop-zones - - ;; Display the layout lines - :layout-lines - - ;; Display the bounds for the hug content adjust - :layout-content-bounds - - ;; Makes the pixel grid red so its more visibile - :pixel-grid - - ;; Show the bounds relative to the parent - :parent-bounds - - ;; Show html text - :html-text - - ;; Show history overlay - :history-overlay - - ;; Show shape name and id - :shape-titles - - ;; - :grid-layout - }) - ;; These events are excluded when we activate the :events flag (def debug-exclude-events #{:app.main.data.workspace.notifications/handle-pointer-update @@ -109,39 +57,36 @@ :app.main.data.websocket/send-message :app.main.data.workspace.selection/change-hover-state}) -(defonce ^:dynamic *debug* (atom #{#_:events})) - -(defn debug-all! [] - (reset! *debug* debug-options) - (js* "app.main.reinit()")) - -(defn debug-none! [] - (reset! *debug* #{}) - (js* "app.main.reinit()")) - -(defn debug! [option] - (swap! *debug* conj option) +(defn enable! + [option] + (dbg/enable! option) (when (= :events option) (set! st/*debug-events* true)) - (js* "app.main.reinit()")) -(defn -debug! [option] - (swap! *debug* disj option) +(defn disable! + [option] + (dbg/disable! option) (when (= :events option) (set! st/*debug-events* false)) (js* "app.main.reinit()")) -(defn ^:export ^boolean debug? - [option] - (boolean (@*debug* option))) +(defn ^:export toggle-debug + [name] + (let [option (keyword name)] + (if (dbg/enabled? option) + (disable! option) + (enable! option)))) -(defn ^:export toggle-debug [name] (let [option (keyword name)] - (if (debug? option) - (-debug! option) - (debug! option)))) -(defn ^:export debug-all [] (debug-all!)) -(defn ^:export debug-none [] (debug-none!)) +(defn ^:export debug-all + [] + (reset! dbg/state dbg/options) + (js* "app.main.reinit()")) + +(defn ^:export debug-none + [] + (reset! dbg/state #{}) + (js* "app.main.reinit()")) (defn ^:export tap "Transducer function that can execute a side-effect `effect-fn` per input" @@ -155,21 +100,21 @@ (rf result input))))) (defn prettify - "Prepare x fror cleaner output when logged." + "Prepare x for cleaner output when logged." [x] (cond (map? x) (d/mapm #(prettify %2) x) (vector? x) (mapv prettify x) (seq? x) (map prettify x) - (set? x) (into #{} (map prettify x)) + (set? x) (into #{} (map prettify) x) (number? x) (mth/precision x 4) - (uuid? x) (str "#uuid " x) + (uuid? x) (str/concat "#uuid " x) :else x)) (defn ^:export logjs ([str] (tap (partial logjs str))) ([str val] - (js/console.log str (clj->js (prettify val))) + (js/console.log str (clj->js (prettify val) :keyword-fn (fn [v] (str/concat v)))) val)) (when (exists? js/window) @@ -270,6 +215,10 @@ [] (dump-selected' @st/state)) +(defn ^:export preview-selected + [] + (st/emit! (dp/open-preview-selected))) + (defn ^:export parent [] (let [state @st/state @@ -294,29 +243,59 @@ (prn (str (:name frame) " - " (:id frame)))) nil)) +(defn ^:export select-by-object-id + [object-id] + (let [[_ page-id shape-id _] (str/split object-id #"/")] + (st/emit! (dw/go-to-page (uuid/uuid page-id))) + (st/emit! (dws/select-shape (uuid/uuid shape-id))))) + +(defn ^:export select-by-id + [shape-id] + (st/emit! (dws/select-shape (uuid/uuid shape-id)))) + (defn dump-tree' ([state] (dump-tree' state false false false)) ([state show-ids] (dump-tree' state show-ids false false)) ([state show-ids show-touched] (dump-tree' state show-ids show-touched false)) ([state show-ids show-touched show-modified] (let [page-id (get state :current-page-id) - file-data (get state :workspace-data) + file (assoc (get state :workspace-file) + :data (get state :workspace-data)) libraries (get state :workspace-libraries)] - (ctf/dump-tree file-data page-id libraries show-ids show-touched show-modified)))) - + (ctf/dump-tree file page-id libraries {:show-ids show-ids + :show-touched show-touched + :show-modified show-modified})))) (defn ^:export dump-tree ([] (dump-tree' @st/state)) ([show-ids] (dump-tree' @st/state show-ids false false)) ([show-ids show-touched] (dump-tree' @st/state show-ids show-touched false)) ([show-ids show-touched show-modified] (dump-tree' @st/state show-ids show-touched show-modified))) +(defn ^:export dump-subtree' + ([state shape-id] (dump-subtree' state shape-id false false false)) + ([state shape-id show-ids] (dump-subtree' state shape-id show-ids false false)) + ([state shape-id show-ids show-touched] (dump-subtree' state shape-id show-ids show-touched false)) + ([state shape-id show-ids show-touched show-modified] + (let [page-id (get state :current-page-id) + file (assoc (get state :workspace-file) + :data (get state :workspace-data)) + libraries (get state :workspace-libraries)] + (ctf/dump-subtree file page-id shape-id libraries {:show-ids show-ids + :show-touched show-touched + :show-modified show-modified})))) +(defn ^:export dump-subtree + ([shape-id] (dump-subtree' @st/state (uuid/uuid shape-id))) + ([shape-id show-ids] (dump-subtree' @st/state (uuid/uuid shape-id) show-ids false false)) + ([shape-id show-ids show-touched] (dump-subtree' @st/state (uuid/uuid shape-id) show-ids show-touched false)) + ([shape-id show-ids show-touched show-modified] (dump-subtree' @st/state (uuid/uuid shape-id) show-ids show-touched show-modified))) + (when *assert* (defonce debug-subscription (->> st/stream (rx/filter ptk/event?) - (rx/filter (fn [s] (and (debug? :events) + (rx/filter (fn [s] (and (dbg/enabled? :events) (not (debug-exclude-events (ptk/type s)))))) - (rx/subs #(println "[stream]: " (ptk/repr-event %)))))) + (rx/subs! #(println "[stream]: " (ptk/repr-event %)))))) (defn ^:export apply-changes "Takes a Transit JSON changes" @@ -324,7 +303,7 @@ (let [file-id (:current-file-id @st/state) changes (t/decode-str changes*)] - (st/emit! (dwc/commit-changes {:redo-changes changes + (st/emit! (dch/commit-changes {:redo-changes changes :undo-changes [] :save-undo? true :file-id file-id})))) @@ -398,6 +377,133 @@ [read-only?] (st/emit! (dw/set-workspace-read-only read-only?))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; REPAIR & VALIDATION +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Validation and repair + +(defn ^:export validate + ([] (validate nil)) + ([shape-id] + (let [file (assoc (get @st/state :workspace-file) + :data (get @st/state :workspace-data)) + libraries (get @st/state :workspace-libraries)] + + (try + (->> (if-let [shape-id (some-> shape-id parse-uuid)] + (let [page (dm/get-in file [:data :pages-index (get @st/state :current-page-id)])] + (cfv/validate-shape shape-id file page libraries)) + (cfv/validate-file file libraries)) + (group-by :code) + (clj->js)) + (catch :default cause + (errors/print-error! cause)))))) + +(defn ^:export validate-schema + [] + (try + (-> (get @st/state :workspace-file) + (assoc :data (get @st/state :workspace-data)) + (cfv/validate-file-schema!)) + (catch :default cause + (errors/print-error! cause)))) + +(defn ^:export repair + [reload?] + (st/emit! + (ptk/reify ::repair-current-file + ptk/EffectEvent + (effect [_ state _] + (let [features (features/get-team-enabled-features state) + sid (:session-id state) + + file (get state :workspace-file) + fdata (get state :workspace-data) + + file (assoc file :data fdata) + libs (get state :workspace-libraries) + + errors (cfv/validate-file file libs) + _ (l/dbg :hint "repair current file" :errors (count errors)) + + changes (cfr/repair-file file libs errors) + + params {:id (:id file) + :revn (:revn file) + :session-id sid + :changes changes + :features features + :skip-validate true}] + + + (->> (rp/cmd! :update-file params) + (rx/subs! (fn [_] + (when reload? + (dom/reload-current-window))) + (fn [cause] + (errors/print-error! cause))))))))) + (defn ^:export fix-orphan-shapes [] (st/emit! (dw/fix-orphan-shapes))) + +(defn ^:export find-components-norefs + [] + (st/emit! (dw/find-components-norefs))) + +(defn ^:export set-shape-ref + [id shape-ref] + (st/emit! (dw/set-shape-ref id shape-ref))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SNAPSHOTS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn ^:export list-available-snapshots + [file-id] + (let [file-id (or (d/parse-uuid file-id) + (:current-file-id @st/state))] + (->> (http/send! {:method :get + :uri (u/join cf/public-uri "api/rpc/command/get-file-snapshots") + :query {:file-id file-id}}) + (rx/map http/conditional-decode-transit) + (rx/mapcat rp/handle-response) + (rx/subs! (fn [result] + (let [result (map (fn [row] + (update row :id str)) + result)] + (js/console.table (clj->js result)))) + (fn [cause] + (js/console.log "EE:" cause)))) + nil)) + +(defn ^:export take-snapshot + [label file-id] + (when-let [file-id (or (d/parse-uuid file-id) + (:current-file-id @st/state))] + (->> (http/send! {:method :post + :uri (u/join cf/public-uri "api/rpc/command/take-file-snapshot") + :body (http/transit-data {:file-id file-id :label label})}) + (rx/map http/conditional-decode-transit) + (rx/mapcat rp/handle-response) + (rx/subs! (fn [{:keys [id]}] + (println "Snapshot saved:" (str id))) + (fn [cause] + (js/console.log "EE:" cause)))))) + +(defn ^:export restore-snapshot + [id file-id] + (when-let [file-id (or (d/parse-uuid file-id) + (:current-file-id @st/state))] + (when-let [id (d/parse-uuid id)] + (->> (http/send! {:method :post + :uri (u/join cf/public-uri "api/rpc/command/restore-file-snapshot") + :body (http/transit-data {:file-id file-id :id id})}) + (rx/map http/conditional-decode-transit) + (rx/mapcat rp/handle-response) + (rx/subs! (fn [_] + (println "Snapshot restored " id) + #_(.reload js/location)) + (fn [cause] + (js/console.log "EE:" cause))))))) diff --git a/frontend/src/features.cljs b/frontend/src/features.cljs index c129ae84e4..366a70207b 100644 --- a/frontend/src/features.cljs +++ b/frontend/src/features.cljs @@ -7,15 +7,19 @@ ;; This namespace is only to export the functions for toggle features (ns features (:require - [app.main.features :as features])) - -(defn ^:export components-v2 [] - (features/toggle-feature! :components-v2) - nil) + [app.main.features :as features] + [app.main.store :as st] + [app.util.timers :as tm])) (defn ^:export is-components-v2 [] - (let [active? (features/active-feature :components-v2)] - @active?)) + (features/active-feature? @st/state "components/v2")) -(defn ^:export new-css-system [] - (features/toggle-feature! :new-css-system)) +(defn ^:export grid [] + (tm/schedule-on-idle #(st/emit! (features/toggle-feature "layout/grid"))) + nil) + +(defn ^:export get-enabled [] + (clj->js (features/get-enabled-features @st/state))) + +(defn ^:export get-team-enabled [] + (clj->js (features/get-team-enabled-features @st/state))) diff --git a/frontend/test/frontend_tests/helpers/events.cljs b/frontend/test/frontend_tests/helpers/events.cljs index e398e04206..21353f768c 100644 --- a/frontend/test/frontend_tests/helpers/events.cljs +++ b/frontend/test/frontend_tests/helpers/events.cljs @@ -8,58 +8,66 @@ (:require [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.pages :as cp] - [app.common.pages.helpers :as cph] + [app.common.pprint :as pp] + [app.common.schema :as sm] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] - [beicon.core :as rx] - [cljs.pprint :refer [pprint]] - [cljs.test :as t :include-macros true] - [potok.core :as ptk])) + [beicon.v2.core :as rx] + [cljs.test :as t] + [potok.v2.core :as ptk])) ;; ---- Helpers to manage global events +(defn on-error + [cause] + + (js/console.log "STORE ERROR" (.-stack cause)) + (when-let [data (some-> cause ex-data ::sm/explain)] + (pp/pprint (sm/humanize-explain data)))) (defn prepare-store - "Create a store with the given initial state. Wait until - a :the/end event occurs, and then call the function with - the final state at this point." - [state done completed-cb] - (let [store (ptk/store {:state state}) - stream (ptk/input-stream store) - stream (->> stream - (rx/take-until (rx/filter #(= :the/end %) stream)) - (rx/last) - (rx/do - (fn [] - (completed-cb @store))) - (rx/subs done #(throw %)))] - store)) + "Create a store with the given initial state. Wait until a :the/end + event occurs, and then call the function with the final state at + this point." + [state done completed-cb] + (let [store (ptk/store {:state state :on-error on-error}) + stream (ptk/input-stream store) + stream (->> stream + (rx/take-until (rx/filter #(= :the/end %) stream)) + (rx/last) + (rx/tap (fn [] + (completed-cb @store))) + (rx/subs! (fn [_] (done)) + (fn [cause] + (js/console.log "[error]:" cause)) + (fn [_] + (js/console.log "[complete]"))))] + store)) ;; Remove definitely when we ensure that the above method works ;; well in more advanced tests. #_(defn do-update - "Execute an update event and returns the new state." - [event state] - (ptk/update event state)) + "Execute an update event and returns the new state." + [event state] + (ptk/update event state)) #_(defn do-watch - "Execute a watch event and return an observable, that + "Execute a watch event and return an observable, that emits once a list with all new events." - [event state] - (->> (ptk/watch event state nil) - (rx/reduce conj []))) + [event state] + (->> (ptk/watch event state nil) + (rx/reduce conj []))) #_(defn do-watch-update - "Execute a watch event and return an observable, that + "Execute a watch event and return an observable, that emits once the new state, after all new events applied in sequence (considering they are all update events)." - [event state] - (->> (do-watch event state) - (rx/map (fn [new-events] - (reduce - (fn [new-state new-event] - (do-update new-event new-state)) - state - new-events))))) + [event state] + (->> (do-watch event state) + (rx/map (fn [new-events] + (reduce + (fn [new-state new-event] + (do-update new-event new-state)) + state + new-events))))) diff --git a/frontend/test/frontend_tests/helpers/libraries.cljs b/frontend/test/frontend_tests/helpers/libraries.cljs index 359667a314..1d9137f565 100644 --- a/frontend/test/frontend_tests/helpers/libraries.cljs +++ b/frontend/test/frontend_tests/helpers/libraries.cljs @@ -6,7 +6,7 @@ (ns frontend-tests.helpers.libraries (:require - [app.common.pages.helpers :as cph] + [app.common.files.helpers :as cfh] [app.common.types.container :as ctn] [app.common.types.file :as ctf] [app.main.data.workspace.state-helpers :as wsh] @@ -19,18 +19,18 @@ [shape] (t/is (nil? (:shape-ref shape))) (t/is (some? (:component-id shape))) - (t/is (= (:component-root? shape) true))) + (t/is (= (:component-root shape) true))) (defn is-main-instance-subroot [shape] (t/is (some? (:component-id shape))) ; shape-ref may or may be not nil - (t/is (= (:component-root? shape) true))) + (t/is (nil? (:component-root shape)))) (defn is-main-instance-child [shape] (t/is (nil? (:component-id shape))) ; shape-ref may or may be not nil (t/is (nil? (:component-file shape))) - (t/is (nil? (:component-root? shape)))) + (t/is (nil? (:component-root shape)))) (defn is-main-instance-inner [shape] @@ -42,20 +42,20 @@ [shape] (t/is (some? (:shape-ref shape))) (t/is (some? (:component-id shape))) - (t/is (= (:component-root? shape) true))) + (t/is (= (:component-root shape) true))) (defn is-instance-subroot [shape] (t/is (some? (:shape-ref shape))) (t/is (some? (:component-id shape))) - (t/is (nil? (:component-root? shape)))) + (t/is (nil? (:component-root shape)))) (defn is-instance-child [shape] (t/is (some? (:shape-ref shape))) (t/is (nil? (:component-id shape))) (t/is (nil? (:component-file shape))) - (t/is (nil? (:component-root? shape)))) + (t/is (nil? (:component-root shape)))) (defn is-instance-inner [shape] @@ -68,8 +68,8 @@ (t/is (nil? (:shape-ref shape))) (t/is (nil? (:component-id shape))) (t/is (nil? (:component-file shape))) - (t/is (nil? (:component-root? shape))) - (t/is (nil? (:remote-synced? shape))) + (t/is (nil? (:component-root shape))) + (t/is (nil? (:remote-synced shape))) (t/is (nil? (:touched shape)))) (defn is-from-file @@ -82,7 +82,7 @@ verify that they are a well constructed instance tree." [state root-id] (let [page (thp/current-page state) - shapes (cph/get-children-with-self (:objects page) + shapes (cfh/get-children-with-self (:objects page) root-id)] (is-instance-root (first shapes)) (run! is-instance-inner (rest shapes)) @@ -94,7 +94,7 @@ verify that they are not a component instance." [state root-id] (let [page (thp/current-page state) - shapes (cph/get-children-with-self (:objects page) + shapes (cfh/get-children-with-self (:objects page) root-id)] (run! is-noninstance shapes) @@ -109,13 +109,13 @@ ([state root-inst-id subinstance?] (let [page (thp/current-page state) root-inst (ctn/get-shape page root-inst-id) - main-instance? (:main-instance? root-inst) + main-instance? (:main-instance root-inst) libs (wsh/get-libraries state) - component (ctf/get-component libs (:component-id root-inst)) + component (ctf/get-component libs (:component-file root-inst) (:component-id root-inst)) library (ctf/get-component-library libs root-inst) - shapes-inst (cph/get-children-with-self (:objects page) root-inst-id) + shapes-inst (cfh/get-children-with-self (:objects page) root-inst-id) shapes-main (ctf/get-component-shapes (:data library) component) unique-refs (into #{} (map :shape-ref) shapes-inst) @@ -152,10 +152,10 @@ root-inst (ctn/get-shape page root-inst-id) libs (wsh/get-libraries state) - component (ctf/get-component libs (:component-id root-inst)) + component (ctf/get-component libs (:component-file root-inst) (:component-id root-inst)) library (ctf/get-component-library libs root-inst) - shapes-inst (cph/get-children-with-self (:objects page) root-inst-id) + shapes-inst (cfh/get-children-with-self (:objects page) root-inst-id) shapes-main (ctf/get-component-shapes (:data library) component)] ;; Validate that the instance tree is well constructed @@ -165,9 +165,9 @@ (defn resolve-component "Get the component with the given id and all its shapes." - [state component-id] + [state component-file component-id] (let [libs (wsh/get-libraries state) - component (ctf/get-component libs component-id) + component (ctf/get-component libs component-file component-id) library (ctf/get-component-library libs component) shapes-main (ctf/get-component-shapes (:data library) component)] diff --git a/frontend/test/frontend_tests/helpers/pages.cljs b/frontend/test/frontend_tests/helpers/pages.cljs index b4c76dbe5b..8f28aebb41 100644 --- a/frontend/test/frontend_tests/helpers/pages.cljs +++ b/frontend/test/frontend_tests/helpers/pages.cljs @@ -7,10 +7,12 @@ (ns frontend-tests.helpers.pages (:require [app.common.data :as d] + [app.common.files.changes :as cp] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] + [app.common.files.libraries-helpers :as cflh] + [app.common.files.shapes-helpers :as cfsh] [app.common.geom.point :as gpt] - [app.common.pages :as cp] - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] [app.common.types.shape :as cts] [app.common.uuid :as uuid] [app.main.data.workspace.groups :as dwg] @@ -34,7 +36,7 @@ :pages [] :pages-index {}} :workspace-libraries {} - :features {:components-v2 true}}) + :features/team #{"components/v2"}}) (def ^:private idmap (atom {})) @@ -58,7 +60,7 @@ (defn get-children [state label] (let [page (current-page state)] - (cph/get-children (:objects page) (id label)))) + (cfh/get-children (:objects page) (id label)))) (defn sample-page ([state] (sample-page state {})) @@ -79,8 +81,8 @@ ([state label type] (sample-shape state type {})) ([state label type props] (let [page (current-page state) - frame (cph/get-frame (:objects page)) - shape (cts/make-shape type {:x 0 :y 0 :width 1 :height 1} props)] + frame (cfh/get-frame (:objects page)) + shape (cts/setup-shape (merge {:type type :x 0 :y 0 :width 1 :height 1} props))] (swap! idmap assoc label (:id shape)) (update state :workspace-data cp/process-changes @@ -113,7 +115,7 @@ (if (empty? shapes) state (let [[frame changes] - (dwsh/prepare-create-artboard-from-selection changes + (cfsh/prepare-create-artboard-from-selection changes nil nil (:objects page) @@ -133,17 +135,17 @@ shapes (dwg/shapes-for-grouping objects shape-ids) [group component-id changes] - (dwlh/generate-add-component nil + (cflh/generate-add-component nil shapes (:objects page) (:id page) current-file-id true dwg/prepare-create-group - dwsh/prepare-create-artboard-from-selection)] + cfsh/prepare-create-artboard-from-selection)] (swap! idmap assoc instance-label (:id group) - component-label component-id) + component-label component-id) (update state :workspace-data cp/process-changes (:redo-changes changes)))) @@ -153,12 +155,14 @@ ([state label component-id file-id] (let [page (current-page state) libraries (wsh/get-libraries state) + objects (:objects page) changes (-> (pcb/empty-changes nil (:id page)) - (pcb/with-objects (:objects page))) + (pcb/with-objects objects)) [new-shape changes] (dwlh/generate-instantiate-component changes + objects file-id component-id (gpt/point 100 100) diff --git a/frontend/test/frontend_tests/helpers_shapes_test.cljs b/frontend/test/frontend_tests/helpers_shapes_test.cljs index aec0f6a0fa..929b4301fb 100644 --- a/frontend/test/frontend_tests/helpers_shapes_test.cljs +++ b/frontend/test/frontend_tests/helpers_shapes_test.cljs @@ -9,9 +9,8 @@ [app.common.colors :as clr] [app.common.data :as d] [app.common.geom.point :as gpt] - [app.common.pages.helpers :as cph] [app.main.data.workspace.libraries :as dwl] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.pprint :refer [pprint]] [cljs.test :as t :include-macros true] [clojure.stacktrace :as stk] @@ -19,7 +18,7 @@ [frontend-tests.helpers.libraries :as thl] [frontend-tests.helpers.pages :as thp] [linked.core :as lks] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (t/use-fixtures :each {:before thp/reset-idmap!}) @@ -47,13 +46,13 @@ color {:color clr/white} store (the/prepare-store state done - (fn [new-state] - (t/is (= (get-in new-state [:workspace-data - :recent-colors]) - [color]))))] + (fn [new-state] + (t/is (= (get-in new-state [:workspace-data + :recent-colors]) + [color]))))] (ptk/emit! - store - (dwl/add-recent-color color) - :the/end))))) + store + (dwl/add-recent-color color) + :the/end))))) diff --git a/frontend/test/frontend_tests/setup.cljs b/frontend/test/frontend_tests/setup.cljs deleted file mode 100644 index 8ab8d8a80e..0000000000 --- a/frontend/test/frontend_tests/setup.cljs +++ /dev/null @@ -1,22 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns frontend-tests.setup - (:require - [cljs.test :as t :include-macros true])) - -#_(enable-console-print!) - -(defmethod t/report [:cljs.test/default :end-run-tests] - [m] - (if (t/successful? m) - (set! (.-exitCode js/process) 0) - (set! (.-exitCode js/process) 1))) - -#_(set! *main-cli-fn* - #(t/run-tests 'frontend-tests.test-snap-data - 'frontend-tests.test-simple-math - 'frontend-tests.test-range-tree)) diff --git a/frontend/test/frontend_tests/setup_test.cljs b/frontend/test/frontend_tests/setup_test.cljs new file mode 100644 index 0000000000..81eab9c0e8 --- /dev/null +++ b/frontend/test/frontend_tests/setup_test.cljs @@ -0,0 +1,31 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns frontend-tests.setup-test + (:require + [app.common.pprint :as pp] + [app.common.schema :as sm] + [cljs.test :as t])) + +(.on js/process "uncaughtException" (fn [cause] + (try + (js/console.log "EE" (.-stack cause)) + (when-let [data (some-> cause ex-data ::sm/explain)] + (pp/pprint (sm/humanize-explain data))) + (finally + (js/console.log "EXIT") + (.exit js/process -1))))) + +(defmethod t/report [:cljs.test/default :end-run-tests] + [m] + (if (t/successful? m) + (set! (.-exitCode js/process) 0) + (set! (.-exitCode js/process) 1))) + +#_(set! *main-cli-fn* + #(t/run-tests 'frontend-tests.test-snap-data + 'frontend-tests.test-simple-math + 'frontend-tests.test-range-tree)) diff --git a/frontend/test/frontend_tests/state_components_sync_test.cljs b/frontend/test/frontend_tests/state_components_sync_test.cljs index d8191c93e7..4928ad74c0 100644 --- a/frontend/test/frontend_tests/state_components_sync_test.cljs +++ b/frontend/test/frontend_tests/state_components_sync_test.cljs @@ -17,12 +17,12 @@ [frontend-tests.helpers.events :as the] [frontend-tests.helpers.libraries :as thl] [frontend-tests.helpers.pages :as thp] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (t/use-fixtures :each {:before thp/reset-idmap!}) -; === Test touched ====================== +;; === Test touched ====================== (t/deftest test-touched (t/async done @@ -47,19 +47,19 @@ ;; (get new-state :current-page-id) ;; (get new-state :workspace-libraries) ;; false true) - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1* ---> Rect 1 - ; #{:fill-group} - ; - ; [Rect 1] - ; page1 / Rect 1 - ; + ;; Expected shape tree: + ;;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1* ---> Rect 1 + ;; #{:fill-group} + ;;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;;; (let [[[group shape1] [c-group c-shape1] _component] (thl/resolve-instance-and-main new-state @@ -89,2189 +89,2169 @@ (t/deftest test-touched-children-add (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1"})) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1"})) - instance1 (thp/get-shape state :instance1) - shape2 (thp/get-shape state :shape2) + instance1 (thp/get-shape state :instance1) + shape2 (thp/get-shape state :shape2) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1* #--> Rect 1 - ; #{:shapes-group} - ; Circle 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[group shape1 shape2] [c-group c-shape1] _component] - (thl/resolve-instance-and-main-allow-dangling - new-state - (thp/id :instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; [Page: Page 1] + ;; Root Frame + ;; {Rect 1} + ;; Rect1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; + ;; [Component: Rect 1] core.cljs:200:23 + ;; --> [Page 1] Rect 1 - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) #{:shapes-group})) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) + (let [[[group shape1] [c-group c-shape1] _component] + (thl/resolve-instance-and-main-allow-dangling + new-state + (thp/id :instance1))] - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)))))] + (t/is (= (:name group) "Rect 1")) + (t/is (nil? (:touched group))) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (not= (:shape-ref shape1) nil)) - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) - :the/end)))) + (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)))))] + + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) ;; We cant't change the structure of component copies, so this operation will do nothing + :the/end)))) (t/deftest test-touched-children-delete (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) - [_group1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [_group1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 1 - ; Rect 2 - ; Component 1 #--> Component 1 - ; Rect 1* ---> Rect 1 - ; #{:visibility-group} - ; Rect 2 ---> Rect 2 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [[[group shape1 shape2] [c-group c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main-allow-dangling - new-state - (thp/id :instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;;; + ;; [Page] + ;; Root Frame + ;; Component 1 + ;; Rect 1 + ;; Rect 2 + ;; Component 1 #--> Component 1 + ;; Rect 1* ---> Rect 1 + ;; #{:visibility-group} + ;; Rect 2 ---> Rect 2 + ;;; + ;; [Component 1] + ;; page1 / Component 1 + ;; + (let [[[group shape1 shape2] [c-group c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main-allow-dangling + new-state + (thp/id :instance1))] - (t/is (= (:name group) "Component 1")) - (t/is (= (:touched group) nil)) - (t/is (not= (:shape-ref group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:hidden shape1) true)) ; Instance shapes are not deleted but hidden - (t/is (= (:touched shape1) #{:visibility-group})) - (t/is (not= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) + (t/is (= (:name group) "Component 1")) + (t/is (= (:touched group) nil)) + (t/is (not= (:shape-ref group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:hidden shape1) true)) ; Instance shapes are not deleted but hidden + (t/is (= (:touched shape1) #{:visibility-group})) + (t/is (not= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:touched shape2) nil)) + (t/is (not= (:shape-ref shape2) nil)) - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)) - (t/is (= (:name c-shape2) "Rect 2")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:shape-ref c-shape2) nil)))))] + (t/is (= (:name c-group) "Component 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)) + (t/is (= (:name c-shape2) "Rect 2")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:shape-ref c-shape2) nil)))))] - (ptk/emit! - store - (dwsh/delete-shapes #{(:id shape1')}) - :the/end)))) + (ptk/emit! + store + (dwsh/delete-shapes #{(:id shape1')}) + :the/end)))) (t/deftest test-touched-children-move (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/sample-shape :shape3 :rect - {:name "Rect 3"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2) - (thp/id :shape3)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/sample-shape :shape3 :rect + {:name "Rect 3"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2) + (thp/id :shape3)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) - [group1' shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [group1' shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 1 - ; Rect 2 - ; Rect 3 - ; Component 1* #--> Component 1 - ; #{:shapes-group} - ; Rect 2 ---> Rect 2 - ; Rect 1 ---> Rect 1 - ; Rect 3 ---> Rect 3 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [[[group shape1 shape2 shape3] - [c-group c-shape1 c-shape2 c-shape3] _component] - (thl/resolve-instance-and-main-allow-dangling - new-state - (thp/id :instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; [Page: Page 1] + ;; Root Frame + ;; {Component 1} # + ;; Rect 1 + ;; Rect 2 + ;; Rect 3 + ;; Component 1 #--> Component 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 + ;; Rect 3 ---> Rect 3 + ;; + ;; ========= Local library + ;; + ;; [Component: Component 1] + ;; --> [Page 1] Component 1 - (t/is (= (:name group) "Component 1")) - (t/is (= (:touched group) #{:shapes-group})) - (t/is (= (:name shape1) "Rect 2")) - (t/is (= (:touched shape1) nil)) - (t/is (not= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) - (t/is (= (:name shape3) "Rect 3")) - (t/is (= (:touched shape3) nil)) - (t/is (not= (:shape-ref shape3) nil)) + (let [[[group shape1 shape2 shape3] + [c-group c-shape1 c-shape2 c-shape3] _component] + (thl/resolve-instance-and-main-allow-dangling + new-state + (thp/id :instance1))] - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)) - (t/is (= (:name c-shape2) "Rect 2")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:shape-ref c-shape2) nil)) - (t/is (= (:name c-shape3) "Rect 3")) - (t/is (= (:touched c-shape3) nil)) - (t/is (= (:shape-ref c-shape3) nil)))))] + (t/is (= (:name group) "Component 1")) + (t/is (nil? (:touched group))) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (not= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:touched shape2) nil)) + (t/is (not= (:shape-ref shape2) nil)) + (t/is (= (:name shape3) "Rect 3")) + (t/is (= (:touched shape3) nil)) + (t/is (not= (:shape-ref shape3) nil)) - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape1')} (:id group1') 2) - :the/end)))) + (t/is (= (:name c-group) "Component 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)) + (t/is (= (:name c-shape2) "Rect 2")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:shape-ref c-shape2) nil)) + (t/is (= (:name c-shape3) "Rect 3")) + (t/is (= (:touched c-shape3) nil)) + (t/is (= (:shape-ref c-shape3) nil)))))] + + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape1')} (:id group1') 2) ;; We cant't change the structure of component copies, so this operation will do nothing + :the/end)))) (t/deftest test-touched-from-lib (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page) - (thp/instantiate-component :instance1 - (thp/id :component1) - (thp/id :lib1))) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/move-to-library :lib1 "Library 1") + (thp/sample-page) + (thp/instantiate-component :instance1 + (thp/id :component1) + (thp/id :lib1))) - [_group1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [_group1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> Rect 1 - ; Rect 1* ---> Rect 1 - ; #{:fill-group} - ; - (let [[[group shape1] [c-group c-shape1] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> Rect 1 + ;; Rect 1* ---> Rect 1 + ;; #{:fill-group} + ;; + (let [[[group shape1] [c-group c-shape1] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1))] - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:touched shape1) #{:fill-group})) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) + (t/is (= (:name group) "Rect 1")) + (t/is (= (:touched group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) #{:fill-group})) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/white)) - (t/is (= (:fill-opacity c-shape1) 1)))))] + (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/white)) + (t/is (= (:fill-opacity c-shape1) 1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + :the/end)))) (t/deftest test-touched-nested-upper (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :main2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :main2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2))) - [_instance2 _instance1 shape1' _shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [_instance2 _instance1 shape1' _shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1* ---> Circle 1 - ; #{:fill-group} - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1* ---> Circle 1 + ;; #{:fill-group} + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) #{:fill-group})) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/white)) - (t/is (= (:fill-opacity shape2) 1)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) #{:fill-group})) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:fill-opacity shape1) 0.5)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:fill-color shape2) clr/white)) + (t/is (= (:fill-opacity shape2) 1)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/white)) - (t/is (= (:fill-opacity c-shape2) 1)))))] + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:fill-color c-shape2) clr/white)) + (t/is (= (:fill-opacity c-shape2) 1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + :the/end)))) (t/deftest test-touched-nested-lower-near (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :instance2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :instance2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2))) - [_instance2 _instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [_instance2 _instance1 _shape1' shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1* ---> Rect 1 - ; #{:fill-group} - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1* ---> Rect 1 + ;; #{:fill-group} + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) #{:fill-group})) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/black)) + (t/is (= (:fill-opacity shape1) 0)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) #{:fill-group})) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/white)) - (t/is (= (:fill-opacity c-shape2) 1)))))] + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:fill-color c-shape2) clr/white)) + (t/is (= (:fill-opacity c-shape2) 1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape2')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + :the/end)))) (t/deftest test-touched-nested-lower-remote (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :instance2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :instance2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2))) - [instance2 _instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [instance2 _instance1 _shape1' shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1* ---> Rect 1 - ; #{:fill-group} - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1* ---> Rect 1 + ;; #{:fill-group} + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/black)) + (t/is (= (:fill-opacity shape1) 0)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) #{:fill-group})) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) #{:fill-group})))))] - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) #{:fill-group})) - (t/is (= (:fill-color c-shape2) clr/test)) - (t/is (= (:fill-opacity c-shape2) 0.5)))))] + (ptk/emit! + store + (dch/update-shapes [(:id shape2')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component (:id instance2)) + :the/end)))) - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component (:id instance2)) - :the/end)))) - -;; ; === Test reset changes ====================== +;; === Test reset changes ====================== (t/deftest test-reset-changes (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[group shape1] [c-group c-shape1] _component] - (thl/resolve-instance-and-main - new-state - (:id instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[group shape1] [c-group c-shape1] _component] + (thl/resolve-instance-and-main + new-state + (:id instance1))] - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/white)) - (t/is (= (:fill-opacity shape1) 1)) - (t/is (= (:touched shape1) nil)) + (t/is (= (:name group) "Rect 1")) + (t/is (= (:touched group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:fill-color shape1) clr/white)) + (t/is (= (:fill-opacity shape1) 1)) + (t/is (= (:touched shape1) nil)) - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:fill-color c-shape1) clr/white)) - (t/is (= (:fill-opacity c-shape1) 1)) - (t/is (= (:touched c-shape1) nil)))))] + (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:fill-color c-shape1) clr/white)) + (t/is (= (:fill-opacity c-shape1) 1)) + (t/is (= (:touched c-shape1) nil)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/reset-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/reset-component (:id instance1)) + :the/end)))) (t/deftest test-reset-children-add (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1"})) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1"})) - instance1 (thp/get-shape state :instance1) - shape2 (thp/get-shape state :shape2) + instance1 (thp/get-shape state :instance1) + shape2 (thp/get-shape state :shape2) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[group shape1] [c-group c-shape1] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[group shape1] [c-group c-shape1] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1))] - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) nil)) - (t/is (not= (:shape-ref group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:touched shape1) nil)) - (t/is (not= (:shape-ref shape1) nil)) + (t/is (= (:name group) "Rect 1")) + (t/is (= (:touched group) nil)) + (t/is (not= (:shape-ref group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (not= (:shape-ref shape1) nil)) - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)))))] + (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)))))] - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) - (dwl/reset-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) + (dwl/reset-component (:id instance1)) + :the/end)))) (t/deftest test-reset-children-delete (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 1 - ; Rect 2 - ; Component 1 #--> Component 1 - ; Rect 1 ---> Rect 1 - ; Rect 2 ---> Rect 2 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [[[group shape1 shape2] - [c-group c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Component 1 + ;; Rect 1 + ;; Rect 2 + ;; Component 1 #--> Component 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 + ;; + ;; [Component 1] + ;; page1 / Component 1 + ;; + (let [[[group shape1 shape2] + [c-group c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1))] - (t/is (= (:name group) "Component 1")) - (t/is (= (:touched group) nil)) - (t/is (not= (:shape-ref group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:touched shape1) nil)) - (t/is (not= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) + (t/is (= (:name group) "Component 1")) + (t/is (= (:touched group) nil)) + (t/is (not= (:shape-ref group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (not= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:touched shape2) nil)) + (t/is (not= (:shape-ref shape2) nil)) - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)) - (t/is (= (:name c-shape2) "Rect 2")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:shape-ref c-shape2) nil)))))] + (t/is (= (:name c-group) "Component 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)) + (t/is (= (:name c-shape2) "Rect 2")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:shape-ref c-shape2) nil)))))] - (ptk/emit! - store - (dwsh/delete-shapes #{(:id shape1')}) - (dwl/reset-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dwsh/delete-shapes #{(:id shape1')}) + (dwl/reset-component (:id instance1)) + :the/end)))) (t/deftest test-reset-children-move (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/sample-shape :shape3 :rect - {:name "Rect 3"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2) - (thp/id :shape3)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/sample-shape :shape3 :rect + {:name "Rect 3"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2) + (thp/id :shape3)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 1 - ; Rect 2 - ; Rect 3 - ; Component 1 #--> Component 1 - ; Rect 1 ---> Rect 1 - ; Rect 2 ---> Rect 2 - ; Rect 3 ---> Rect 3 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [[[group shape1 shape2 shape3] [c-group c-shape1 c-shape2 c-shape3] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Component 1 + ;; Rect 1 + ;; Rect 2 + ;; Rect 3 + ;; Component 1 #--> Component 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 + ;; Rect 3 ---> Rect 3 + ;; + ;; [Component 1] + ;; page1 / Component 1 + ;; + (let [[[group shape1 shape2 shape3] [c-group c-shape1 c-shape2 c-shape3] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1))] - (t/is (= (:name group) "Component 1")) - (t/is (= (:touched group) nil)) - (t/is (not= (:shape-ref group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:touched shape1) nil)) - (t/is (not= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) - (t/is (= (:name shape3) "Rect 3")) - (t/is (= (:touched shape3) nil)) - (t/is (not= (:shape-ref shape3) nil)) + (t/is (= (:name group) "Component 1")) + (t/is (= (:touched group) nil)) + (t/is (not= (:shape-ref group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (not= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:touched shape2) nil)) + (t/is (not= (:shape-ref shape2) nil)) + (t/is (= (:name shape3) "Rect 3")) + (t/is (= (:touched shape3) nil)) + (t/is (not= (:shape-ref shape3) nil)) - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)) - (t/is (= (:name c-shape2) "Rect 2")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:shape-ref c-shape2) nil)) - (t/is (= (:name c-shape3) "Rect 3")) - (t/is (= (:touched c-shape3) nil)) - (t/is (= (:shape-ref c-shape3) nil)))))] + (t/is (= (:name c-group) "Component 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)) + (t/is (= (:name c-shape2) "Rect 2")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:shape-ref c-shape2) nil)) + (t/is (= (:name c-shape3) "Rect 3")) + (t/is (= (:touched c-shape3) nil)) + (t/is (= (:shape-ref c-shape3) nil)))))] - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape1')} (:id instance1) 2) - (dwl/reset-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape1')} (:id instance1) 2) + (dwl/reset-component (:id instance1)) + :the/end)))) (t/deftest test-reset-from-lib (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :instance1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page) - (thp/instantiate-component :instance2 - (thp/id :component1) - (thp/id :lib1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :instance1 :component1 + [(thp/id :shape1)]) + (thp/move-to-library :lib1 "Library 1") + (thp/sample-page) + (thp/instantiate-component :instance2 + (thp/id :component1) + (thp/id :lib1))) - [instance2 shape2] - (thl/resolve-instance state (thp/id :instance2)) + [instance2 shape2] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - (let [[[group shape1] [c-group c-shape1] _component] - (thl/resolve-instance-and-main - new-state - (:id instance2))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + (let [[[group shape1] [c-group c-shape1] _component] + (thl/resolve-instance-and-main + new-state + (:id instance2))] - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/white)) - (t/is (= (:fill-opacity shape1) 1)) - (t/is (= (:touched shape1) nil)) + (t/is (= (:name group) "Rect 1")) + (t/is (= (:touched group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:fill-color shape1) clr/white)) + (t/is (= (:fill-opacity shape1) 1)) + (t/is (= (:touched shape1) nil)) - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:fill-color c-shape1) clr/white)) - (t/is (= (:fill-opacity c-shape1) 1)) - (t/is (= (:touched c-shape1) nil)))))] + (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:fill-color c-shape1) clr/white)) + (t/is (= (:fill-opacity c-shape1) 1)) + (t/is (= (:touched c-shape1) nil)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape2)] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/reset-component (:id instance2)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape2)] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/reset-component (:id instance2)) + :the/end)))) (t/deftest test-reset-nested-upper (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :main2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :main2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2))) - [instance2 _instance1 shape1' _shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [instance2 _instance1 shape1' _shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/white)) - (t/is (= (:fill-opacity shape2) 1)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/black)) + (t/is (= (:fill-opacity shape1) 0)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:fill-color shape2) clr/white)) + (t/is (= (:fill-opacity shape2) 1)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/white)) - (t/is (= (:fill-opacity c-shape2) 1)))))] + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:fill-color c-shape2) clr/white)) + (t/is (= (:fill-opacity c-shape2) 1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/reset-component (:id instance2)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/reset-component (:id instance2)) + :the/end)))) -(t/deftest test-reset-nested-lower-near - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :instance2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) - - [instance2 instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) - - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] - - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/white)) - (t/is (= (:fill-opacity shape2) 1)) - - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/white)) - (t/is (= (:fill-opacity c-shape2) 1)))))] - - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component (:id instance1)) - (dwl/reset-component (:id instance2)) - :the/end)))) +;; (t/deftest test-reset-nested-lower-near +;; (t/async done +;; (let [state (-> thp/initial-state +;; (thp/sample-page) +;; (thp/sample-shape :shape1 :rect +;; {:name "Rect 1" +;; :fill-color clr/white +;; :fill-opacity 1}) +;; (thp/make-component :main1 :component1 +;; [(thp/id :shape1)]) +;; (thp/instantiate-component :instance1 +;; (thp/id :component1)) +;; (thp/sample-shape :shape2 :circle +;; {:name "Circle 1" +;; :fill-color clr/black +;; :fill-opacity 0}) +;; (thp/frame-shapes :frame1 +;; [(thp/id :instance1) +;; (thp/id :shape2)]) +;; (thp/make-component :instance2 :component2 +;; [(thp/id :frame1)]) +;; (thp/instantiate-component :instance2 +;; (thp/id :component2))) +;; +;; [instance2 instance1 _shape1' shape2'] +;; (thl/resolve-instance state (thp/id :instance2)) +;; +;; store (the/prepare-store state done +;; (fn [new-state] +;; ;; Expected shape tree: +;; ;; +;; ;; [Page] +;; ;; Root Frame +;; ;; Rect 1 +;; ;; Rect 1 +;; ;; Group +;; ;; Rect 1 #--> Rect 1 +;; ;; Rect 1 ---> Rect 1 +;; ;; Circle 1 +;; ;; Group #--> Group +;; ;; Rect 1 @--> Rect 1 +;; ;; Rect 1 ---> Rect 1 +;; ;; Circle 1 ---> Circle 1 +;; ;; +;; ;; [Rect 1] +;; ;; page1 / Rect 1 +;; ;; +;; ;; [Group] +;; ;; page1 / Group +;; ;; +;; (let [[[instance2 instance1 shape1 shape2] +;; [c-instance2 c-instance1 c-shape1 c-shape2] _component] +;; (thl/resolve-instance-and-main +;; new-state +;; (thp/id :instance2))] +;; +;; (t/is (= (:name instance2) "Board")) +;; (t/is (= (:touched instance2) nil)) +;; (t/is (= (:name instance1) "Rect 1")) +;; (t/is (= (:touched instance1) nil)) +;; (t/is (= (:name shape1) "Circle 1")) +;; (t/is (= (:touched shape1) nil)) +;; (t/is (= (:fill-color shape1) clr/black)) +;; (t/is (= (:fill-opacity shape1) 0)) +;; (t/is (= (:name shape2) "Rect 1")) +;; (t/is (= (:touched shape2) nil)) +;; (t/is (= (:fill-color shape2) clr/white)) +;; (t/is (= (:fill-opacity shape2) 1)) +;; +;; (t/is (= (:name c-instance2) "Board")) +;; (t/is (= (:touched c-instance2) nil)) +;; (t/is (= (:name c-instance1) "Rect 1")) +;; (t/is (= (:touched c-instance1) nil)) +;; (t/is (= (:name c-shape1) "Circle 1")) +;; (t/is (= (:touched c-shape1) nil)) +;; (t/is (= (:fill-color c-shape1) clr/black)) +;; (t/is (= (:fill-opacity c-shape1) 0)) +;; (t/is (= (:name c-shape2) "Rect 1")) +;; (t/is (= (:touched c-shape2) nil)) +;; (t/is (= (:fill-color c-shape2) clr/white)) +;; (t/is (= (:fill-opacity c-shape2) 1)))))] +;; +;; (ptk/emit! +;; store +;; (dch/update-shapes [(:id shape2')] +;; (fn [shape] +;; (merge shape {:fill-color clr/test +;; :fill-opacity 0.5}))) +;; (dwl/update-component (:id instance1)) +;; (dwl/reset-component (:id instance2)) +;; :the/end)))) (t/deftest test-reset-nested-lower-remote (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :instance2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :instance2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2))) - [instance2 instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [instance2 instance1 _shape1' shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1* ---> Rect 1 - ; #{:fill-group} - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; (remote-synced) - ; Rect 1 ---> Rect 1 - ; (remote-synced) - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1* ---> Rect 1 + ;; #{:fill-group} + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; (remote-synced) + ;; Rect 1 ---> Rect 1 + ;; (remote-synced) + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/white)) - (t/is (= (:fill-opacity shape2) 1)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/black)) + (t/is (= (:fill-opacity shape1) 0)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) #{:fill-group})) - (t/is (= (:fill-color c-shape2) clr/test)) - (t/is (= (:fill-opacity c-shape2) 0.5)))))] + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) #{:fill-group})) + (t/is (= (:fill-color c-shape2) clr/test)) + (t/is (= (:fill-opacity c-shape2) 0.5)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component (:id instance2)) - (dwl/reset-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape2')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component (:id instance2)) + (dwl/reset-component (:id instance1)) + :the/end)))) -;; ; === Test update component ====================== +;; === Test update component ====================== (t/deftest test-update-component (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1))) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 <== (not updated) - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[main1 shape1] [c-main1 c-shape1] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 <== (not updated) + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[main1 shape1] [c-main1 c-shape1] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape2] [c-instance1 c-shape2] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape2] [c-instance1 c-shape2] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape3] [c-instance2 c-shape3] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape3] [c-instance2 c-shape3] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Rect 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) + (t/is (= (:name main1) "Rect 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:fill-opacity shape1) 0.5)) + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) (:id c-shape1))) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) (:id c-shape1))) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape3) "Rect 1")) - (t/is (= (:fill-color shape3) clr/white)) - (t/is (= (:fill-opacity shape3) 1)) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) (:id c-shape1))) + (t/is (= (:name instance2) "Rect 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape3) "Rect 1")) + (t/is (= (:fill-color shape3) clr/white)) + (t/is (= (:fill-opacity shape3) 1)) + (t/is (= (:touched shape3) nil)) + (t/is (= (:shape-ref shape3) (:id c-shape1))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape2 c-shape1)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape3 c-shape1)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape2 c-shape1)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape3 c-shape1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component (:id instance1)) + :the/end)))) (t/deftest test-update-component-and-sync (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - [_instance2 _shape1''] - (thl/resolve-instance state (thp/id :instance2)) + [_instance2 _shape1''] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[main1 shape1] [c-main1 c-shape1] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[main1 shape1] [c-main1 c-shape1] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape2] [c-instance1 c-shape2] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape2] [c-instance1 c-shape2] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape3] [c-instance2 c-shape3] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape3] [c-instance2 c-shape3] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Rect 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) + (t/is (= (:name main1) "Rect 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:fill-opacity shape1) 0.5)) + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) (:id c-shape1))) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) (:id c-shape1))) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape3) "Rect 1")) - (t/is (= (:fill-color shape3) clr/test)) - (t/is (= (:fill-opacity shape3) 0.5)) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) (:id c-shape1))) + (t/is (= (:name instance2) "Rect 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape3) "Rect 1")) + (t/is (= (:fill-color shape3) clr/test)) + (t/is (= (:fill-opacity shape3) 0.5)) + (t/is (= (:touched shape3) nil)) + (t/is (= (:shape-ref shape3) (:id c-shape1))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape2 c-shape1)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape3 c-shape1)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape2 c-shape1)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape3 c-shape1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component-sync (:id instance1) (:id file)) + :the/end)))) (t/deftest test-update-preserve-touched (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - [_instance2 shape1''] - (thl/resolve-instance state (thp/id :instance2)) + [_instance2 shape1''] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1* ---> Rect 1 - ; #{:stroke-group} - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[main1 shape1] [c-main1 c-shape1] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1* ---> Rect 1 + ;; #{:stroke-group} + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[main1 shape1] [c-main1 c-shape1] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape2] [c-instance1 c-shape2] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape2] [c-instance1 c-shape2] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape3] [c-instance2 c-shape3] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape3] [c-instance2 c-shape3] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Rect 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:stroke-width shape1) 0.5)) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) + (t/is (= (:name main1) "Rect 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:stroke-width shape1) 0.5)) + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:stroke-width shape2) 0.5)) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) (:id c-shape1))) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:stroke-width shape2) 0.5)) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) (:id c-shape1))) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape3) "Rect 1")) - (t/is (= (:fill-color shape3) clr/test)) - (t/is (= (:stroke-width shape3) 0.2)) - (t/is (= (:touched shape3) #{:stroke-group})) - (t/is (= (:shape-ref shape3) (:id c-shape1))) + (t/is (= (:name instance2) "Rect 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape3) "Rect 1")) + (t/is (= (:fill-color shape3) clr/test)) + (t/is (= (:stroke-width shape3) 0.2)) + (t/is (= (:touched shape3) #{:stroke-group})) + (t/is (= (:shape-ref shape3) (:id c-shape1))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape2 c-shape1)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape3 c-shape1)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape2 c-shape1)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape3 c-shape1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :stroke-width 0.5}))) - (dch/update-shapes [(:id shape1'')] - (fn [shape] - (merge shape {:stroke-width 0.2}))) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :stroke-width 0.5}))) + (dch/update-shapes [(:id shape1'')] + (fn [shape] + (merge shape {:stroke-width 0.2}))) + (dwl/update-component-sync (:id instance1) (:id file)) + :the/end)))) (t/deftest test-update-children-add (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1"})) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1"})) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - instance1 (thp/get-shape state :instance1) - shape2 (thp/get-shape state :shape2) + instance1 (thp/get-shape state :instance1) + shape2 (thp/get-shape state :shape2) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Circle 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Circle 1 ---> Circle 1 - ; Rect 1 ---> Rect 1 - ; Rect 1 #--> Rect 1 - ; Circle 1 ---> Circle 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[main1 shape1 shape2] - [c-main1 c-shape1 c-shape2] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page: Page 1] + ;; Root Frame + ;; {Rect 1} # + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; + ;; ========= Local library + ;; + ;; [Component: Rect 1] + ;; --> [Page 1] Rect 1 + ;; + (let [[[main1 shape1] + [c-main1 c-shape1] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape3 shape4] - [c-instance1 c-shape3 c-shape4] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape2] + [c-instance1 c-shape2] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape5 shape6] - [c-instance2 c-shape5 c-shape6] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape3] + [c-instance2 c-shape3] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Rect 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) nil)) + (t/is (= (:name main1) "Rect 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape3) "Circle 1")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) (:id c-shape1))) - (t/is (= (:name shape4) "Rect 1")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:shape-ref shape4) (:id c-shape2))) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) (:id c-shape1))) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape5) "Circle 1")) - (t/is (= (:touched shape5) nil)) - (t/is (= (:shape-ref shape5) (:id c-shape1))) - (t/is (= (:name shape6) "Rect 1")) - (t/is (= (:touched shape6) nil)) - (t/is (= (:shape-ref shape4) (:id c-shape2))) + (t/is (= (:name instance2) "Rect 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape3) "Rect 1")) + (t/is (= (:touched shape3) nil)) + (t/is (= (:shape-ref shape2) (:id c-shape1))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-shape2 shape2)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape3 c-shape1)) - (t/is (= c-shape4 c-shape2)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape5 c-shape1)) - (t/is (= c-shape6 c-shape2)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape2 c-shape1)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape3 c-shape1)))))] - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) ;; We cant't change the structure of component copies, so this operation will do nothing + (dwl/update-component-sync (:id instance1) (:id file)) + :the/end)))) (t/deftest test-update-children-delete (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [instance1 shape1' _shape2'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1' _shape2'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 1 - ; Rect 2 - ; Component 1 #--> Component 1 - ; Rect 1 ---> Rect 1 - ; Rect 2 ---> Rect 2 - ; Component 1 #--> Component 1 - ; Rect 1 ---> Rect 1 - ; Rect 2 ---> Rect 2 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [[[main1 shape1 shape2] - [c-main1 c-shape1 c-shape2] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Component 1 + ;; Rect 1 + ;; Rect 2 + ;; Component 1 #--> Component 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 + ;; Component 1 #--> Component 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 + ;; + ;; [Component 1] + ;; page1 / Component 1 + ;; + (let [[[main1 shape1 shape2] + [c-main1 c-shape1 c-shape2] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape3 shape4] - [c-instance1 c-shape3 c-shape4] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape3 shape4] + [c-instance1 c-shape3 c-shape4] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape5 shape6] - [c-instance2 c-shape5 c-shape6] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape5 shape6] + [c-instance2 c-shape5 c-shape6] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Component 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:hidden shape1) true)) ; Instance shapes are not deleted but hidden - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:hidden shape2) nil)) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) nil)) + (t/is (= (:name main1) "Component 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:hidden shape1) true)) ;; Instance shapes are not deleted but hidden + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:hidden shape2) nil)) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) nil)) - (t/is (= (:name instance1) "Component 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape3) "Rect 1")) - (t/is (= (:hidden shape3) true)) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) (:id c-shape1))) - (t/is (= (:name shape4) "Rect 2")) - (t/is (= (:hidden shape4) nil)) - (t/is (= (:touched shape4) nil)) - (t/is (= (:shape-ref shape4) (:id c-shape2))) + (t/is (= (:name instance1) "Component 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape3) "Rect 1")) + (t/is (= (:hidden shape3) true)) + (t/is (= (:touched shape3) nil)) + (t/is (= (:shape-ref shape3) (:id c-shape1))) + (t/is (= (:name shape4) "Rect 2")) + (t/is (= (:hidden shape4) nil)) + (t/is (= (:touched shape4) nil)) + (t/is (= (:shape-ref shape4) (:id c-shape2))) - (t/is (= (:name instance2) "Component 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape5) "Rect 1")) - (t/is (= (:hidden shape5) true)) - (t/is (= (:touched shape5) nil)) - (t/is (= (:shape-ref shape5) (:id c-shape1))) - (t/is (= (:name shape6) "Rect 2")) - (t/is (= (:hidden shape6) nil)) - (t/is (= (:touched shape6) nil)) - (t/is (= (:shape-ref shape6) (:id c-shape2))) + (t/is (= (:name instance2) "Component 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape5) "Rect 1")) + (t/is (= (:hidden shape5) true)) + (t/is (= (:touched shape5) nil)) + (t/is (= (:shape-ref shape5) (:id c-shape1))) + (t/is (= (:name shape6) "Rect 2")) + (t/is (= (:hidden shape6) nil)) + (t/is (= (:touched shape6) nil)) + (t/is (= (:shape-ref shape6) (:id c-shape2))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-shape2 shape2)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape3 c-shape1)) - (t/is (= c-shape4 c-shape2)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape5 c-shape1)) - (t/is (= c-shape6 c-shape2)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-shape2 shape2)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape3 c-shape1)) + (t/is (= c-shape4 c-shape2)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape5 c-shape1)) + (t/is (= c-shape6 c-shape2)))))] - (ptk/emit! - store - (dwsh/delete-shapes #{(:id shape1')}) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) + (ptk/emit! + store + (dwsh/delete-shapes #{(:id shape1')}) + (dwl/update-component-sync (:id instance1) (:id file)) + :the/end)))) (t/deftest test-update-children-move (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/sample-shape :shape3 :rect - {:name "Rect 3"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2) - (thp/id :shape3)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/sample-shape :shape3 :rect + {:name "Rect 3"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2) + (thp/id :shape3)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [instance1 shape1' _shape2' _shape3'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1' _shape2' _shape3'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 2 - ; Rect 1 - ; Rect 3 - ; Component 1 #--> Component 1 - ; Rect 2 ---> Rect 2 - ; Rect 1 ---> Rect 1 - ; Rect 3 ---> Rect 3 - ; Component 1 #--> Component 1 - ; Rect 2 ---> Rect 2 - ; Rect 1 ---> Rect 1 - ; Rect 3 ---> Rect 3 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [[[main1 shape1 shape2 shape3] - [c-main1 c-shape1 c-shape2 c-shape3] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Component 1 + ;; Rect 1 + ;; Rect 2 + ;; Rect 3 + ;; Component 1 #--> Component 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 + ;; Rect 3 ---> Rect 3 + ;; Component 1 #--> Component 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 + ;; Rect 3 ---> Rect 3 + ;; + ;; [Component 1] + ;; page1 / Component 1 + ;; + (let [[[main1 shape1 shape2 shape3] + [c-main1 c-shape1 c-shape2 c-shape3] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape4 shape5 shape6] - [c-instance1 c-shape4 c-shape5 c-shape6] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape4 shape5 shape6] + [c-instance1 c-shape4 c-shape5 c-shape6] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape7 shape8 shape9] - [c-instance2 c-shape7 c-shape8 c-shape9] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape7 shape8 shape9] + [c-instance2 c-shape7 c-shape8 c-shape9] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Component 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 2")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) nil)) - (t/is (= (:name shape3) "Rect 3")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) nil)) + (t/is (= (:name main1) "Component 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) nil)) + (t/is (= (:name shape3) "Rect 3")) + (t/is (= (:touched shape3) nil)) + (t/is (= (:shape-ref shape3) nil)) - (t/is (= (:name instance1) "Component 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape4) "Rect 2")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:shape-ref shape4) (:id c-shape1))) - (t/is (= (:name shape5) "Rect 1")) - (t/is (= (:touched shape5) nil)) - (t/is (= (:shape-ref shape5) (:id c-shape2))) - (t/is (= (:name shape6) "Rect 3")) - (t/is (= (:touched shape6) nil)) - (t/is (= (:shape-ref shape6) (:id c-shape3))) + (t/is (= (:name instance1) "Component 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape4) "Rect 1")) + (t/is (= (:touched shape4) nil)) + (t/is (= (:shape-ref shape4) (:id c-shape1))) + (t/is (= (:name shape5) "Rect 2")) + (t/is (= (:touched shape5) nil)) + (t/is (= (:shape-ref shape5) (:id c-shape2))) + (t/is (= (:name shape6) "Rect 3")) + (t/is (= (:touched shape6) nil)) + (t/is (= (:shape-ref shape6) (:id c-shape3))) - (t/is (= (:name instance2) "Component 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape7) "Rect 2")) - (t/is (= (:touched shape7) nil)) - (t/is (= (:shape-ref shape7) (:id c-shape1))) - (t/is (= (:name shape8) "Rect 1")) - (t/is (= (:touched shape8) nil)) - (t/is (= (:shape-ref shape8) (:id c-shape2))) - (t/is (= (:name shape9) "Rect 3")) - (t/is (= (:touched shape9) nil)) - (t/is (= (:shape-ref shape9) (:id c-shape3))) + (t/is (= (:name instance2) "Component 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape7) "Rect 1")) + (t/is (= (:touched shape7) nil)) + (t/is (= (:shape-ref shape7) (:id c-shape1))) + (t/is (= (:name shape8) "Rect 2")) + (t/is (= (:touched shape8) nil)) + (t/is (= (:shape-ref shape8) (:id c-shape2))) + (t/is (= (:name shape9) "Rect 3")) + (t/is (= (:touched shape9) nil)) + (t/is (= (:shape-ref shape9) (:id c-shape3))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-shape2 shape2)) - (t/is (= c-shape3 shape3)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape4 c-shape4)) - (t/is (= c-shape5 c-shape5)) - (t/is (= c-shape6 c-shape6)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape7 c-shape7)) - (t/is (= c-shape8 c-shape8)) - (t/is (= c-shape9 c-shape9)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-shape2 shape2)) + (t/is (= c-shape3 shape3)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape4 c-shape4)) + (t/is (= c-shape5 c-shape5)) + (t/is (= c-shape6 c-shape6)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape7 c-shape7)) + (t/is (= c-shape8 c-shape8)) + (t/is (= c-shape9 c-shape9)))))] - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape1')} (:id instance1) 2) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape1')} (:id instance1) 2) + (dwl/update-component-sync (:id instance1) (:id file)) + :the/end)))) (t/deftest test-update-from-lib (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page) - (thp/instantiate-component :instance1 - (thp/id :component1) - (thp/id :lib1)) - (thp/instantiate-component :instance2 - (thp/id :component1) - (thp/id :lib1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/move-to-library :lib1 "Library 1") + (thp/sample-page) + (thp/instantiate-component :instance1 + (thp/id :component1) + (thp/id :lib1)) + (thp/instantiate-component :instance2 + (thp/id :component1) + (thp/id :lib1))) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - (let [[[instance1 shape1] [c-instance1 c-shape1] _component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + (let [[[instance1 shape1] [c-instance1 c-shape1] _component1] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape2] [_c-instance2 _c-shape2] _component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape2] [_c-instance2 _c-shape2] _component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:touched shape1) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:fill-opacity shape1) 0.5)) + (t/is (= (:touched shape1) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:fill-color c-shape1) clr/test)) - (t/is (= (:fill-opacity c-shape1) 0.5)) - (t/is (= (:touched c-shape1) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:fill-color c-shape1) clr/test)) + (t/is (= (:fill-opacity c-shape1) 0.5)) + (t/is (= (:touched c-shape1) nil)) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:touched shape2) nil)))))] + (t/is (= (:name instance2) "Rect 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:touched shape2) nil)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component-sync (:id instance1) (thp/id :lib1)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component-sync (:id instance1) (thp/id :lib1)) + :the/end)))) (t/deftest test-update-nested-upper (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :main2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2)) - (thp/instantiate-component :instance3 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :main2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2)) + (thp/instantiate-component :instance3 + (thp/id :component2))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [instance2 _instance1 shape1' _shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [instance2 _instance1 shape1' _shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component1] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2)) - [[instance4 instance3 shape3 shape4] - [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance3))] + [[instance4 instance3 shape3 shape4] + [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance3))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/white)) - (t/is (= (:fill-opacity shape2) 1)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:fill-opacity shape1) 0.5)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:fill-color shape2) clr/white)) + (t/is (= (:fill-opacity shape2) 1)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/test)) - (t/is (= (:fill-opacity c-shape1) 0.5)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/white)) - (t/is (= (:fill-opacity c-shape2) 1)) + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/test)) + (t/is (= (:fill-opacity c-shape1) 0.5)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:fill-color c-shape2) clr/white)) + (t/is (= (:fill-opacity c-shape2) 1)) - (t/is (= (:name instance4) "Board")) - (t/is (= (:touched instance4) nil)) - (t/is (= (:name instance3) "Rect 1")) - (t/is (= (:touched instance3) nil)) - (t/is (= (:name shape3) "Circle 1")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:fill-color shape3) clr/test)) - (t/is (= (:fill-opacity shape3) 0.5)) - (t/is (= (:name shape4) "Rect 1")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:fill-color shape4) clr/white)) - (t/is (= (:fill-opacity shape4) 1)))))] + (t/is (= (:name instance4) "Board")) + (t/is (= (:touched instance4) nil)) + (t/is (= (:name instance3) "Rect 1")) + (t/is (= (:touched instance3) nil)) + (t/is (= (:name shape3) "Circle 1")) + (t/is (= (:touched shape3) nil)) + (t/is (= (:fill-color shape3) clr/test)) + (t/is (= (:fill-opacity shape3) 0.5)) + (t/is (= (:name shape4) "Rect 1")) + (t/is (= (:touched shape4) nil)) + (t/is (= (:fill-color shape4) clr/white)) + (t/is (= (:fill-opacity shape4) 1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component-sync (:id instance2) (:id file)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component-sync (:id instance2) (:id file)) + :the/end)))) (t/deftest test-update-nested-lower-near (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :main2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2)) - (thp/instantiate-component :instance3 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :main2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2)) + (thp/instantiate-component :instance3 + (thp/id :component2))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [instance2 instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [instance2 instance1 _shape1' shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component1] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2)) - [[instance4 instance3 shape3 shape4] - [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance3))] + [[instance4 instance3 shape3 shape4] + [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance3))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/black)) + (t/is (= (:fill-opacity shape1) 0)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/test)) - (t/is (= (:fill-opacity c-shape2) 0.5)) + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:fill-color c-shape2) clr/test)) + (t/is (= (:fill-opacity c-shape2) 0.5)) - (t/is (= (:name instance4) "Board")) - (t/is (= (:touched instance4) nil)) - (t/is (= (:name instance3) "Rect 1")) - (t/is (= (:touched instance3) nil)) - (t/is (= (:name shape3) "Circle 1")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:fill-color shape3) clr/black)) - (t/is (= (:fill-opacity shape3) 0)) - (t/is (= (:name shape4) "Rect 1")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:fill-color shape4) clr/test)) - (t/is (= (:fill-opacity shape4) 0.5)))))] + (t/is (= (:name instance4) "Board")) + (t/is (= (:touched instance4) nil)) + (t/is (= (:name instance3) "Rect 1")) + (t/is (= (:touched instance3) nil)) + (t/is (= (:name shape3) "Circle 1")) + (t/is (= (:touched shape3) nil)) + (t/is (= (:fill-color shape3) clr/black)) + (t/is (= (:fill-opacity shape3) 0)) + (t/is (= (:name shape4) "Rect 1")) + (t/is (= (:touched shape4) nil)) + (t/is (= (:fill-color shape4) clr/test)) + (t/is (= (:fill-opacity shape4) 0.5)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component (:id instance1)) - (dwl/update-component-sync (:id instance2) (:id file)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape2')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component (:id instance1)) + (dwl/update-component-sync (:id instance2) (:id file)) + :the/end)))) -(t/deftest test-update-nested-lower-remote - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :main2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2)) - (thp/instantiate-component :instance3 - (thp/id :component2))) - - file (wsh/get-local-file state) - - [_instance2 instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) - - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; (remote-synced) - ; Rect 1 ---> Rect 1 - ; (remote-synced) - ; Circle 1 ---> Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2)) - - [[instance4 instance3 shape3 shape4] - [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance3))] - - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) - - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/test)) - (t/is (= (:fill-opacity c-shape2) 0.5)) - - (t/is (= (:name instance4) "Board")) - (t/is (= (:touched instance4) nil)) - (t/is (= (:name instance3) "Rect 1")) - (t/is (= (:touched instance3) nil)) - (t/is (= (:name shape3) "Circle 1")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:fill-color shape3) clr/black)) - (t/is (= (:fill-opacity shape3) 0)) - (t/is (= (:name shape4) "Rect 1")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:fill-color shape4) clr/test)) - (t/is (= (:fill-opacity shape4) 0.5)))))] - - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) +;; (t/deftest test-update-nested-lower-remote +;; (t/async done +;; (let [state (-> thp/initial-state +;; (thp/sample-page) +;; (thp/sample-shape :shape1 :rect +;; {:name "Rect 1" +;; :fill-color clr/white +;; :fill-opacity 1}) +;; (thp/make-component :main1 :component1 +;; [(thp/id :shape1)]) +;; (thp/instantiate-component :instance1 +;; (thp/id :component1)) +;; (thp/sample-shape :shape2 :circle +;; {:name "Circle 1" +;; :fill-color clr/black +;; :fill-opacity 0}) +;; (thp/frame-shapes :frame1 +;; [(thp/id :instance1) +;; (thp/id :shape2)]) +;; (thp/make-component :main2 :component2 +;; [(thp/id :frame1)]) +;; (thp/instantiate-component :instance2 +;; (thp/id :component2)) +;; (thp/instantiate-component :instance3 +;; (thp/id :component2))) +;; +;; file (wsh/get-local-file state) +;; +;; [_instance2 instance1 _shape1' shape2'] +;; (thl/resolve-instance state (thp/id :instance2)) +;; +;; store (the/prepare-store state done +;; (fn [new-state] +;; ;; Expected shape tree: +;; ;; +;; ;; [Page] +;; ;; Root Frame +;; ;; Rect 1 +;; ;; Rect 1 +;; ;; Group +;; ;; Rect 1 #--> Rect 1 +;; ;; Rect 1 ---> Rect 1 +;; ;; Circle 1 +;; ;; Group #--> Group +;; ;; Rect 1 @--> Rect 1 +;; ;; (remote-synced) +;; ;; Rect 1 ---> Rect 1 +;; ;; (remote-synced) +;; ;; Circle 1 ---> Circle 1 +;; ;; Group #--> Group +;; ;; Rect 1 @--> Rect 1 +;; ;; Rect 1 ---> Rect 1 +;; ;; Circle 1 ---> Circle 1 +;; ;; +;; ;; [Rect 1] +;; ;; page1 / Rect 1 +;; ;; +;; ;; [Group] +;; ;; page1 / Group +;; ;; +;; (let [[[instance2 instance1 shape1 shape2] +;; [c-instance2 c-instance1 c-shape1 c-shape2] _component1] +;; (thl/resolve-instance-and-main +;; new-state +;; (thp/id :instance2)) +;; +;; [[instance4 instance3 shape3 shape4] +;; [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] +;; (thl/resolve-instance-and-main +;; new-state +;; (thp/id :instance3))] +;; +;; (t/is (= (:name instance2) "Board")) +;; (t/is (= (:touched instance2) nil)) +;; (t/is (= (:name instance1) "Rect 1")) +;; (t/is (= (:touched instance1) nil)) +;; (t/is (= (:name shape1) "Circle 1")) +;; (t/is (= (:touched shape1) nil)) +;; (t/is (= (:fill-color shape1) clr/black)) +;; (t/is (= (:fill-opacity shape1) 0)) +;; (t/is (= (:name shape2) "Rect 1")) +;; (t/is (= (:touched shape2) nil)) +;; (t/is (= (:fill-color shape2) clr/test)) +;; (t/is (= (:fill-opacity shape2) 0.5)) +;; +;; (t/is (= (:name c-instance2) "Board")) +;; (t/is (= (:touched c-instance2) nil)) +;; (t/is (= (:name c-instance1) "Rect 1")) +;; (t/is (= (:touched c-instance1) nil)) +;; (t/is (= (:name c-shape1) "Circle 1")) +;; (t/is (= (:touched c-shape1) nil)) +;; (t/is (= (:fill-color c-shape1) clr/black)) +;; (t/is (= (:fill-opacity c-shape1) 0)) +;; (t/is (= (:name c-shape2) "Rect 1")) +;; (t/is (= (:touched c-shape2) nil)) +;; (t/is (= (:fill-color c-shape2) clr/test)) +;; (t/is (= (:fill-opacity c-shape2) 0.5)) +;; +;; (t/is (= (:name instance4) "Board")) +;; (t/is (= (:touched instance4) nil)) +;; (t/is (= (:name instance3) "Rect 1")) +;; (t/is (= (:touched instance3) nil)) +;; (t/is (= (:name shape3) "Circle 1")) +;; (t/is (= (:touched shape3) nil)) +;; (t/is (= (:fill-color shape3) clr/black)) +;; (t/is (= (:fill-opacity shape3) 0)) +;; (t/is (= (:name shape4) "Rect 1")) +;; (t/is (= (:touched shape4) nil)) +;; (t/is (= (:fill-color shape4) clr/test)) +;; (t/is (= (:fill-opacity shape4) 0.5)))))] +;; +;; (ptk/emit! +;; store +;; (dch/update-shapes [(:id shape2')] +;; (fn [shape] +;; (merge shape {:fill-color clr/test +;; :fill-opacity 0.5}))) +;; (dwl/update-component-sync (:id instance1) (:id file)) +;; :the/end)))) diff --git a/frontend/test/frontend_tests/state_components_test.cljs b/frontend/test/frontend_tests/state_components_test.cljs index 36286c18cb..3a87510195 100644 --- a/frontend/test/frontend_tests/state_components_test.cljs +++ b/frontend/test/frontend_tests/state_components_test.cljs @@ -13,7 +13,7 @@ [frontend-tests.helpers.libraries :as thl] [frontend-tests.helpers.pages :as thp] [linked.core :as lks] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (t/use-fixtures :each {:before thp/reset-idmap!}) @@ -28,91 +28,91 @@ {:name "Rect 1"})) store (the/prepare-store state done - (fn [new-state] - ;; Uncomment to debug - ;; (ctf/dump-tree (get new-state :workspace-data) - ;; (get new-state :current-page-id) - ;; (get new-state :workspace-libraries) - ;; false true) + (fn [new-state] + ;; Uncomment to debug + ;; (ctf/dump-tree (get new-state :workspace-data) + ;; (get new-state :current-page-id) + ;; (get new-state :workspace-libraries) + ;; false true) - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; Rect 1 - ; Rect 1 - ; - (let [shape1 (thp/get-shape new-state :shape1) + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; Rect 1 + ;; Rect 1 + ;; + (let [shape1 (thp/get-shape new-state :shape1) - [[group shape1] [c-group c-shape1] component] - (thl/resolve-instance-and-main - new-state - (:parent-id shape1)) + [[group shape1] [c-group c-shape1] component] + (thl/resolve-instance-and-main + new-state + (:parent-id shape1)) - file (wsh/get-local-file new-state)] + file (wsh/get-local-file new-state)] - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name group) "Rect 1")) - (t/is (= (:name component) "Rect 1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name group) "Rect 1")) + (t/is (= (:name component) "Rect 1")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-group) "Rect 1")) - (thl/is-from-file group file))))] + (thl/is-from-file group file))))] (ptk/emit! - store - (dw/select-shape (thp/id :shape1)) - (dwl/add-component) - :the/end))))) + store + (dw/select-shape (thp/id :shape1)) + (dwl/add-component) + :the/end))))) (t/deftest test-add-component-from-several-shapes (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect-2"})) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect-2"})) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 1 - ; Rect-2 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [shape1 (thp/get-shape new-state :shape1) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; [Page] + ;; Root Frame + ;; Component 1 + ;; Rect 1 + ;; Rect-2 + ;; + ;; [Component 1] + ;; page1 / Component 1 + ;; + (let [shape1 (thp/get-shape new-state :shape1) - [[group shape1 shape2] - [c-group c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - (:parent-id shape1)) + [[group shape1 shape2] + [c-group c-shape1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + (:parent-id shape1)) - file (wsh/get-local-file new-state)] + file (wsh/get-local-file new-state)] - (t/is (= (:name group) "Component 1")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect-2")) - (t/is (= (:name component) "Component 1")) - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect-2")) + (t/is (= (:name group) "Component 1")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name shape2) "Rect-2")) + (t/is (= (:name component) "Component 1")) + (t/is (= (:name c-group) "Component 1")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect-2")) - (thl/is-from-file group file))))] + (thl/is-from-file group file))))] - (ptk/emit! + (ptk/emit! store (dw/select-shapes (lks/set (thp/id :shape1) (thp/id :shape2))) @@ -121,111 +121,158 @@ (t/deftest test-add-component-from-frame (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect-2"}) - (thp/frame-shapes :frame1 - [(thp/id :shape1) - (thp/id :shape2)])) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect-2"}) + (thp/frame-shapes :frame1 + [(thp/id :shape1) + (thp/id :shape2)])) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Group - ; Rect 1 - ; Rect-2 - ; - ; [Group] - ; page1 / Group - ; - (let [[[group shape1 shape2] - [c-group c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - (thp/id :frame1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Group + ;; Rect 1 + ;; Rect-2 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[group shape1 shape2] + [c-group c-shape1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + (thp/id :frame1)) - file (wsh/get-local-file new-state)] + file (wsh/get-local-file new-state)] - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect-2")) - (t/is (= (:name group) "Board")) - (t/is (= (:name component) "Board")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect-2")) - (t/is (= (:name c-group) "Board")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name shape2) "Rect-2")) + (t/is (= (:name group) "Board")) + (t/is (= (:name component) "Board")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect-2")) + (t/is (= (:name c-group) "Board")) - (thl/is-from-file group file))))] + (thl/is-from-file group file))))] - (ptk/emit! + (ptk/emit! store (dw/select-shape (thp/id :frame1)) (dwl/add-component) :the/end)))) -(t/deftest test-add-component-from-component +(t/deftest test-add-component-from-component-instance (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)])) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 (thp/id :component1))) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[instance1 shape1] - [c-instance1 c-shape1] - component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1) - true) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page: Page] + ;; Root Frame + ;; Rect 1 # + ;; Rect 1 + ;; Rect 1 # + ;; Rect 1* @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + (let [[[instance1 shape1] + [c-instance1 c-shape1] + component1] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1) + true) - [[instance2 instance1' shape1'] - [c-instance2 c-instance1' c-shape1'] - component2] - (thl/resolve-instance-and-main - new-state - (:parent-id instance1))] + [[instance2 instance1' shape1'] + [c-instance2 c-instance1' c-shape1'] + component2] + (thl/resolve-instance-and-main + new-state + (:parent-id instance1))] - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name component1) "Rect 1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:name component1) "Rect 1")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name shape1') "Rect 1")) - (t/is (= (:name instance1') "Rect 1")) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:name component2) "Rect 1")) - (t/is (= (:name c-shape1') "Rect 1")) - (t/is (= (:name c-instance1') "Rect 1")) - (t/is (= (:name c-instance2) "Rect 1")))))] + (t/is (= (:name shape1') "Rect 1")) + (t/is (= (:name instance1') "Rect 1")) + (t/is (= (:name instance2) "Rect 1")) + (t/is (= (:name component2) "Rect 1")) + (t/is (= (:name c-shape1') "Rect 1")) + (t/is (= (:name c-instance1') "Rect 1")) + (t/is (= (:name c-instance2) "Rect 1")))))] - (ptk/emit! + (ptk/emit! + store + (dw/select-shape (thp/id :instance1)) + (dwl/add-component) + :the/end)))) + + +(t/deftest test-add-component-from-component-main + (t/async + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)])) + + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [file (wsh/get-local-file new-state) + components (ctkl/components file) + page (thp/current-page new-state) + shape1 (thp/get-shape new-state :shape1) + parent1 (ctn/get-shape page (:parent-id shape1)) + main1 (thp/get-shape state :main1) + [[instance1 shape1] + [c-instance1 c-shape1] + component1] + (thl/resolve-instance-and-main + new-state + (:id main1))] + ;; Creating a component from a main doesn't generate a new component + (t/is (= (count components) 1)) + + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:name component1) "Rect 1")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-instance1) "Rect 1")))))] + + (ptk/emit! store (dw/select-shape (thp/id :main1)) (dwl/add-component) @@ -233,360 +280,410 @@ (t/deftest test-rename-component (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)])) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)])) - main1 (thp/get-shape state :main1) + main1 (thp/get-shape state :main1) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; - ; [Renamed component] - ; page1 / Rect 1 - ; - (let [libs (wsh/get-libraries new-state) - component (ctf/get-component libs - (:component-file main1) - (:component-id main1))] - (t/is (= (:name component) - "Renamed component")))))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; + ;; [Renamed component] + ;; page1 / Rect 1 + ;; + (let [libs (wsh/get-libraries new-state) + component (ctf/get-component libs + (:component-file main1) + (:component-id main1))] + (t/is (= (:name component) + "Renamed component")))))] - (ptk/emit! - store - (dwl/rename-component (:component-id main1) "Renamed component") - :the/end)))) + (ptk/emit! + store + (dwl/rename-component (:component-id main1) "Renamed component") + :the/end)))) (t/deftest test-duplicate-component (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)])) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)])) - main1 (thp/get-shape state :main1) - component-id (:component-id main1) + main1 (thp/get-shape state :main1) + component-id (:component-id main1) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [new-component-id (->> (get-in new-state - [:workspace-data - :components]) - (keys) - (filter #(not= % component-id)) - (first)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [new-component-id (->> (get-in new-state + [:workspace-data + :components]) + (keys) + (filter #(not= % component-id)) + (first)) - [[_instance1 _shape1] - [_c-instance1 _c-shape1] - _component1] - (thl/resolve-instance-and-main - new-state - (:id main1)) + [[_instance1 _shape1] + [_c-instance1 _c-shape1] + _component1] + (thl/resolve-instance-and-main + new-state + (:id main1)) - [[_c-component2 _c-shape2] - component2] - (thl/resolve-component - new-state - new-component-id)] + [[_c-component2 _c-shape2] + component2] + (thl/resolve-component + new-state + (:current-file-id new-state) + new-component-id)] - (t/is (= (:name component2) "Rect 1")))))] + (t/is (= (:name component2) "Rect 1")))))] - (ptk/emit! + (ptk/emit! store (dwl/duplicate-component thp/current-file-id component-id) :the/end)))) (t/deftest test-delete-component - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + (t/async done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect {:name "Rect 1"}) + (thp/make-component :main1 :component1 [(thp/id :shape1)]) + (thp/instantiate-component :instance1 (thp/id :component1))) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> ? - ; Rect 1 ---> ? - ; - (let [[main1 shape1] - (thl/resolve-noninstance - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> ? + ;; Rect 1 ---> ? + ;;; + (let [[main1 shape1] + (thl/resolve-noninstance + new-state + (thp/id :main1)) - [[instance1 shape2] [c-instance1 c-shape2] component1] - (thl/resolve-instance-and-main-allow-dangling - new-state - (thp/id :instance1)) + [[instance1 shape2] [c-instance1 c-shape2] component1] + (thl/resolve-instance-and-main-allow-dangling + new-state + (thp/id :instance1)) - file (wsh/get-local-file new-state) - component2 (ctkl/get-component file (thp/id :component1)) - component3 (ctkl/get-deleted-component file (thp/id :component1)) + file (wsh/get-local-file new-state) + component2 (ctkl/get-component file (thp/id :component1)) + component3 (ctkl/get-deleted-component file (thp/id :component1)) - saved-objects (:objects component3) - saved-main1 (get saved-objects (:shape-ref instance1)) - saved-shape2 (get saved-objects (:shape-ref shape2))] + saved-objects (:objects component3) + saved-main1 (get saved-objects (:shape-ref instance1)) + saved-shape2 (get saved-objects (:shape-ref shape2))] - (t/is (nil? main1)) - (t/is (nil? shape1)) + (t/is (nil? main1)) + (t/is (nil? shape1)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (not= (:shape-ref instance1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) - (t/is (nil? c-instance1)) - (t/is (nil? c-shape2)) - (t/is (nil? component1)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (not= (:shape-ref instance1) nil)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (not= (:shape-ref shape2) nil)) + (t/is (nil? c-instance1)) + (t/is (nil? c-shape2)) + (t/is (nil? component1)) - (t/is (nil? component2)) + (t/is (nil? component2)) - (t/is (= (:name component3) "Rect 1")) - (t/is (= (:deleted component3) true)) - (t/is (some? (:objects component3))) + (t/is (= (:name component3) "Rect 1")) + (t/is (= (:deleted component3) true)) + (t/is (some? (:objects component3))) - (t/is (= (:name saved-main1) "Rect 1")) - (t/is (= (:name saved-shape2) "Rect 1")))))] - - (ptk/emit! - store - (dwl/delete-component {:id (thp/id :component1)}) - :the/end)))) + (t/is (= (:name saved-main1) "Rect 1")) + (t/is (= (:name saved-shape2) "Rect 1")))))] + (ptk/emit! store + (dwl/delete-component {:id (thp/id :component1)}) + :the/end)))) (t/deftest test-restore-component (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Rect 1 - ; Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[instance1 shape2] [c-instance1 c-shape2] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 1 + ;; Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[instance1 shape2] [c-instance1 c-shape2] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - file (wsh/get-local-file new-state) - component2 (ctkl/get-component file (thp/id :component1)) + file (wsh/get-local-file new-state) + component2 (ctkl/get-component file (thp/id :component1)) - saved-objects (:objects component2)] + saved-objects (:objects component2)] - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 1")) - (t/is (some? component1)) - (t/is (some? component2)) - (t/is (nil? saved-objects)))))] + (t/is (some? component1)) + (t/is (some? component2)) + (t/is (nil? saved-objects)))))] - (ptk/emit! - store - (dwl/delete-component {:id (thp/id :component1)}) - (dwl/restore-component thp/current-file-id (thp/id :component1)) - :the/end)))) + (ptk/emit! + store + (dwl/delete-component {:id (thp/id :component1)}) + (dwl/restore-component thp/current-file-id (thp/id :component1)) + :the/end)))) (t/deftest test-instantiate-component (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)])) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)])) - file (wsh/get-local-file state) - component-id (thp/id :component1) - main1 (thp/get-shape state :main1) + file (wsh/get-local-file state) + component-id (thp/id :component1) + main1 (thp/get-shape state :main1) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [new-instance-id (-> new-state - wsh/lookup-selected - first) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [new-instance-id (-> new-state + wsh/lookup-selected + first) - [[instance1 shape2] - [c-instance1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - new-instance-id)] + [[instance1 shape2] + [c-instance1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + new-instance-id)] - (t/is (not= (:id main1) (:id instance1))) - (t/is (= (:id component) component-id)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:component-file instance1) - thp/current-file-id)))))] + (t/is (not= (:id main1) (:id instance1))) + (t/is (= (:id component) component-id)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:component-file instance1) + thp/current-file-id)))))] - (ptk/emit! - store - (dwl/instantiate-component (:id file) - component-id - (gpt/point 100 100)) - :the/end)))) + (ptk/emit! + store + (dwl/instantiate-component (:id file) + component-id + (gpt/point 100 100)) + :the/end)))) (t/deftest test-instantiate-component-from-lib (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page)) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/move-to-library :lib1 "Library 1") + (thp/sample-page)) - library-id (thp/id :lib1) - component-id (thp/id :component1) + library-id (thp/id :lib1) + component-id (thp/id :component1) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - (let [new-instance-id (-> new-state - wsh/lookup-selected - first) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + (let [new-instance-id (-> new-state + wsh/lookup-selected + first) - [[instance1 shape2] - [c-instance1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - new-instance-id)] + [[instance1 shape2] + [c-instance1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + new-instance-id)] - (t/is (= (:id component) component-id)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:component-file instance1) library-id)))))] + (t/is (= (:id component) component-id)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:component-file instance1) library-id)))))] - (ptk/emit! - store - (dwl/instantiate-component library-id - component-id - (gpt/point 100 100)) - :the/end)))) + (ptk/emit! + store + (dwl/instantiate-component library-id + component-id + (gpt/point 100 100)) + :the/end)))) (t/deftest test-detach-component (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) - instance1 (thp/get-shape state :instance1) + instance1 (thp/get-shape state :instance1) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 - ; Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[instance2 shape1] - (thl/resolve-noninstance - new-state - (:id instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[instance2 shape1] + (thl/resolve-noninstance + new-state + (:id instance1))] - (t/is (some? instance2)) - (t/is (some? shape1)))))] + (t/is (some? instance2)) + (t/is (some? shape1)))))] - (ptk/emit! - store - (dwl/detach-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dwl/detach-component (:id instance1)) + :the/end)))) -(t/deftest test-add-nested-component + + +(t/deftest test-add-nested-component-instance + (t/async + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 (thp/id :component1))) + + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Board + ;; Rect 1 + ;; Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Board] + ;; page1 / Board + ;; + (let [instance1 (thp/get-shape new-state :instance1) + + [[group shape1 shape2] + [c-group c-shape1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + (:parent-id instance1))] + + (t/is (= (:name group) "Board")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:name component) "Board")) + (t/is (= (:name c-group) "Board")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 1")))))] + + (ptk/emit! + store + (dw/select-shape (thp/id :instance1)) + (dwsh/create-artboard-from-selection) + (dwl/add-component) + :the/end)))) + +(t/deftest test-add-nested-component-main (t/async done (let [state (-> thp/initial-state @@ -595,167 +692,169 @@ {:name "Rect 1"})) store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Group - ; Rect 1 - ; Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [page (thp/current-page new-state) - shape1 (thp/get-shape new-state :shape1) - parent1 (ctn/get-shape page (:parent-id shape1)) + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Board + ;; Rect 1 + ;; Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; + (let [file (wsh/get-local-file new-state) + components (ctkl/components file) + page (thp/current-page new-state) - [[group shape1 shape2] - [c-group c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - (:parent-id parent1))] + shape1 (thp/get-shape new-state :shape1) + parent1 (ctn/get-shape page (:parent-id shape1)) - (t/is (= (:name group) "Board")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name component) "Board")) - (t/is (= (:name c-group) "Board")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")))))] + [[group shape1] + [c-group c-shape1] + component] + (thl/resolve-instance-and-main + new-state + (:parent-id shape1))] + + ;; Creating a component from something containing a main doesn't generate a new component + (t/is (= (count components) 1)) + + (t/is (= (:name group) "Rect 1")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name component) "Rect 1")) + (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:name c-shape1) "Rect 1")))))] (ptk/emit! - store - (dw/select-shape (thp/id :shape1)) - (dwl/add-component) - (dwsh/create-artboard-from-selection) - (dwl/add-component) - :the/end)))) + store + (dw/select-shape (thp/id :shape1)) + (dwl/add-component) + (dwsh/create-artboard-from-selection) + (dwl/add-component) + :the/end)))) (t/deftest test-instantiate-nested-component (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/make-component :main2 :component-2 - [(thp/id :main1)])) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/make-component :main2 :component-2 + [(thp/id :main1)])) - file (wsh/get-local-file state) - main1 (thp/get-shape state :main1) - main2 (thp/get-shape state :main2) - component-id (:component-id main2) + file (wsh/get-local-file state) + main1 (thp/get-shape state :main1) + main2 (thp/get-shape state :main2) + component-id (:component-id main2) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [new-instance-id (-> new-state - wsh/lookup-selected - first) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [new-instance-id (-> new-state + wsh/lookup-selected + first) - [[instance1 shape1 shape2] - [c-instance1 c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - new-instance-id)] + [[instance1 shape1 shape2] + [c-instance1 c-shape1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + new-instance-id)] - ; TODO: get and check the instance inside component [Rect-2] + ;; TODO: get and check the instance inside component [Rect-2] - (t/is (not= (:id main1) (:id instance1))) - (t/is (= (:id component) component-id)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")))))] + (t/is (not= (:id main1) (:id instance1))) + (t/is (= (:id component) component-id)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 1")))))] - (ptk/emit! - store - (dwl/instantiate-component (:id file) - (:component-id main2) - (gpt/point 100 100)) - :the/end)))) + (ptk/emit! + store + (dwl/instantiate-component (:id file) + (:component-id main2) + (gpt/point 100 100)) + :the/end)))) (t/deftest test-instantiate-nested-component-from-lib (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page) - (thp/instantiate-component :instance1 - (thp/id :component1) - (thp/id :lib1))) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/move-to-library :lib1 "Library 1") + (thp/sample-page) + (thp/instantiate-component :instance1 + (thp/id :component1) + (thp/id :lib1))) - file (wsh/get-local-file state) - library-id (thp/id :lib1) + file (wsh/get-local-file state) + library-id (thp/id :lib1) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [instance1 (thp/get-shape new-state :instance1) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [instance1 (thp/get-shape new-state :instance1) - [[group1 shape1 shape2] [c-group1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (:parent-id instance1))] + [[group1 shape1 shape2] [c-group1 c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (:parent-id instance1))] - (t/is (= (:name group1) "Board")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-group1) "Board")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:component-file group1) thp/current-file-id)) - (t/is (= (:component-file shape1) library-id)) - (t/is (= (:component-file shape2) nil)) - (t/is (= (:component-file c-group1) (:id file))) - (t/is (= (:component-file c-shape1) library-id)) - (t/is (= (:component-file c-shape2) nil)))))] + (t/is (= (:name group1) "Board")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:name c-group1) "Board")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:component-file group1) thp/current-file-id)) + (t/is (= (:component-file shape1) library-id)) + (t/is (= (:component-file shape2) nil)) + (t/is (= (:component-file c-group1) (:id file))) + (t/is (= (:component-file c-shape1) library-id)) + (t/is (= (:component-file c-shape2) nil)))))] - (ptk/emit! - store - (dw/select-shape (thp/id :instance1)) - (dwsh/create-artboard-from-selection) - (dwl/add-component) - :the/end)))) + (ptk/emit! + store + (dw/select-shape (thp/id :instance1)) + (dwsh/create-artboard-from-selection) + (dwl/add-component) + :the/end)))) diff --git a/frontend/test/frontend_tests/test_helpers_shapes.cljs b/frontend/test/frontend_tests/test_helpers_shapes.cljs index 6b84b8dd42..11e5525424 100644 --- a/frontend/test/frontend_tests/test_helpers_shapes.cljs +++ b/frontend/test/frontend_tests/test_helpers_shapes.cljs @@ -3,17 +3,16 @@ [app.common.colors :as clr] [app.common.data :as d] [app.common.geom.point :as gpt] - [app.common.pages.helpers :as cph] [app.main.data.workspace.libraries :as dwl] [app.test-helpers.events :as the] [app.test-helpers.libraries :as thl] [app.test-helpers.pages :as thp] - [beicon.core :as rx] + [beicon.v2.core :as rx] [cljs.pprint :refer [pprint]] [cljs.test :as t :include-macros true] [clojure.stacktrace :as stk] [linked.core :as lks] - [potok.core :as ptk])) + [potok.v2.core :as ptk])) (t/use-fixtures :each {:before thp/reset-idmap!}) @@ -41,13 +40,13 @@ color {:color clr/white} store (the/prepare-store state done - (fn [new-state] - (t/is (= (get-in new-state [:workspace-data - :recent-colors]) - [color]))))] + (fn [new-state] + (t/is (= (get-in new-state [:workspace-data + :recent-colors]) + [color]))))] (ptk/emit! - store - (dwl/add-recent-color color) - :the/end))))) + store + (dwl/add-recent-color color) + :the/end))))) diff --git a/frontend/test/frontend_tests/util_range_tree_test.cljs b/frontend/test/frontend_tests/util_range_tree_test.cljs index 5f3627f731..4d5e75e669 100644 --- a/frontend/test/frontend_tests/util_range_tree_test.cljs +++ b/frontend/test/frontend_tests/util_range_tree_test.cljs @@ -18,14 +18,14 @@ (t/deftest test-insert-and-retrieve-data (t/testing "Retrieve on empty tree" - (let [tree (rt/make-tree)] - (t/is (= (rt/get tree 100) nil)))) + (let [tree (rt/make-tree)] + (t/is (= (rt/get tree 100) nil)))) (t/testing "First insert/retrieval" - (let [tree (-> (rt/make-tree) - (rt/insert 100 :a))] - (t/is (= (rt/get tree 100) [:a])) - (t/is (= (rt/get tree 200) nil)))) + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a))] + (t/is (= (rt/get tree 100) [:a])) + (t/is (= (rt/get tree 200) nil)))) (t/testing "Insert best case scenario" (let [tree (-> (rt/make-tree) diff --git a/frontend/test/frontend_tests/util_simple_math_test.cljs b/frontend/test/frontend_tests/util_simple_math_test.cljs index acd23526ca..15bb2198c2 100644 --- a/frontend/test/frontend_tests/util_simple_math_test.cljs +++ b/frontend/test/frontend_tests/util_simple_math_test.cljs @@ -6,10 +6,10 @@ (ns frontend-tests.util-simple-math-test (:require - [cljs.test :as t :include-macros true] - [cljs.pprint :refer [pprint]] [app.common.math :as cm] - [app.util.simple-math :as sm])) + [app.util.simple-math :as sm] + [cljs.pprint :refer [pprint]] + [cljs.test :as t :include-macros true])) (t/deftest test-parser-inst (t/testing "Evaluate an empty string" @@ -86,7 +86,5 @@ (t/testing "Evaluate a complex operation with decimals" (let [result1 (sm/expr-eval "(20.333 + 10%) * (1 / 3)" 20) result2 (sm/expr-eval "(20,333 + 10%) * (1 / 3)" 20)] - (t/is (cm/close? result1 result2 7.44433333)))) - - ) + (t/is (cm/close? result1 result2 7.44433333))))) diff --git a/frontend/test/frontend_tests/util_snap_data_test.cljs b/frontend/test/frontend_tests/util_snap_data_test.cljs index f0a656cd08..bdd42b5af5 100644 --- a/frontend/test/frontend_tests/util_snap_data_test.cljs +++ b/frontend/test/frontend_tests/util_snap_data_test.cljs @@ -6,7 +6,8 @@ (ns frontend-tests.util-snap-data-test (:require - [app.common.file-builder :as fb] + [app.common.files.builder :as fb] + [app.common.types.shape :as cts] [app.common.uuid :as uuid] [app.util.snap-data :as sd] [cljs.pprint :refer [pprint]] @@ -180,17 +181,17 @@ (fb/close-artboard)) shape-id (:last-id file) - page (fb/get-current-page file) + page (fb/get-current-page file) ;; frame-id (:last-id file) - data (-> (sd/make-snap-data) - (sd/add-page page)) + data (-> (sd/make-snap-data) + (sd/add-page page)) - file (-> file - (fb/delete-object shape-id)) + file (-> file + (fb/delete-object shape-id)) new-page (fb/get-current-page file) - data (sd/update-page data page new-page) + data (sd/update-page data page new-page) result-x (sd/query data (:id page) uuid/zero :x [0 100]) result-y (sd/query data (:id page) uuid/zero :y [0 100])] @@ -332,18 +333,20 @@ :height 100}) (fb/close-artboard)) - frame-id (:last-id file) - page (fb/get-current-page file) - data (-> (sd/make-snap-data) (sd/add-page page)) + frame-id (:last-id file) + page (fb/get-current-page file) + data (-> (sd/make-snap-data) (sd/add-page page)) - frame (fb/lookup-shape file frame-id) + frame (fb/lookup-shape file frame-id) new-frame (-> frame - (assoc :x 200 :y 200)) + (dissoc :selrect :points) + (assoc :x 200 :y 200) + (cts/setup-shape)) - file (fb/update-object file frame new-frame) - new-page (fb/get-current-page file) + file (fb/update-object file frame new-frame) + new-page (fb/get-current-page file) - data (sd/update-page data page new-page) + data (sd/update-page data page new-page) result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100]) result-frame-x-1 (sd/query data (:id page) frame-id :x [0 100]) @@ -371,6 +374,7 @@ shape (fb/lookup-shape file shape-id) new-shape (-> shape + (dissoc :selrect :points) (assoc :x 200 :y 200)) file (fb/update-object file shape new-shape) @@ -414,8 +418,7 @@ result-zero-x-2 (sd/query data (:id page) uuid/zero :x [0 200]) result-zero-y-2 (sd/query data (:id page) uuid/zero :y [0 200]) result-frame-x-2 (sd/query data (:id page) frame-id :x [0 200]) - result-frame-y-2 (sd/query data (:id page) frame-id :y [0 200]) - ] + result-frame-y-2 (sd/query data (:id page) frame-id :y [0 200])] (t/is (some? data)) @@ -427,5 +430,4 @@ (t/is (= (count result-zero-x-2) 1)) (t/is (= (count result-zero-y-2) 0)) (t/is (= (count result-frame-x-2) 1)) - (t/is (= (count result-frame-y-2) 0)))) - ) + (t/is (= (count result-frame-y-2) 0))))) diff --git a/frontend/translations/af.po b/frontend/translations/af.po new file mode 100644 index 0000000000..f037855e10 --- /dev/null +++ b/frontend/translations/af.po @@ -0,0 +1,413 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2023-10-13 18:01+0000\n" +"Last-Translator: Hugo Vermaak \n" +"Language-Team: Afrikaans \n" +"Language: af\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.1-dev\n" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 dae" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.add-shared" +msgstr "Voeg by as Gedeelde Biblioteek" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "Die token het nie 'n verval datum nie" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-gitlab-submit" +msgstr "GitLab" + +msgid "common.share-link.placeholder" +msgstr "Deelbare skakel sal hier verskyn" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-title" +msgstr "Wagwoord vergeet?" + +msgid "common.share-link.current-tag" +msgstr "(huidige)" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.invalid-token-error" +msgstr "Die hersteltoken is ongeldig." + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-title" +msgstr "Wonderlik om jou weer te sien!" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "Genereer nuwe token" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "auth.notifications.team-invitation-accepted" +msgstr "Het suksesvol by die span aangesluit" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.title" +msgstr "Span saam!" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 dae" + +msgid "dashboard.export-frames" +msgstr "Voer borde as PDF uit" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-profile" +msgstr "Wil jy dit net probeer?" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-submit" +msgstr "Herstel Wagwoord" + +#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs +msgid "dashboard.copy-suffix" +msgstr "(kopieer)" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "Die naam word vereis" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.create-new-team" +msgstr "Skep 'n nuwe span" + +msgid "common.share-link.destroy-link" +msgstr "Vernietig skakel" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "Token gekopieer" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.info" +msgstr "Gaan stap deur Penpot en leer sy hoofkenmerke ken." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.already-have-account" +msgstr "Reeds 'n rekening?" + +msgid "common.share-link.view-all" +msgstr "Kies Alles" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.check-your-email" +msgstr "" +"Gaan jou e-pos na en klik op die skakel om te verifieer en Penpot te begin " +"gebruik." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "...handelsmerk, illustrasies, bemarkingsstukke, ens." + +msgid "auth.terms-of-service" +msgstr "Diensbepalings" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.demo-warning" +msgstr "" +"Dit is 'n DEMO-diens, MOENIE vir werklike werk gebruik nie, die projekte sal " +"periodiek uitgevee word." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 dae" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-title" +msgstr "Skep 'n rekening" + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-frames.title" +msgstr "Voer as PDF uit" + +#: src/app/main/ui/auth.cljs +msgid "auth.sidebar-tagline" +msgstr "Die oopbron-oplossing vir ontwerp en prototipering." + +msgid "common.share-link.get-link" +msgstr "Kry skakel" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.register" +msgstr "Nog nie 'n rekening nie?" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-ldap-submit" +msgstr "LDAP" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-oidc-submit" +msgstr "OpenID" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "Het verval op %s" + +msgid "dashboard.export-multi" +msgstr "Voer %s Penpot lêers uit" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "Wagwoord moet 'n ander karakter as spasie bevat." + +msgid "common.unpublish" +msgstr "Depubliseer" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 dae" + +msgid "common.share-link.confirm-deletion-link-description" +msgstr "" +"Is jy seker jy wil hierdie skakel verwyder? As jy dit doen, is dit nie meer " +"vir enigiemand beskikbaar nie" + +msgid "dashboard.download-binary-file" +msgstr "Laai Penpot-lêer (.penpot) af" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.confirm-password" +msgstr "Bevestig wagwoord" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.fullname" +msgstr "Volle naam" + +msgid "common.share-link.permissions-hint" +msgstr "Enigiemand met skakel sal toegang hê" + +msgid "common.share-link.permissions-can-comment" +msgstr "Kan kommentaar lewer" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "Die token sal verval op %s" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.management" +msgstr "Spanbestuur" + +msgid "dashboard.download-standard-file" +msgstr "Laai standaardlêer af (.svg + .json)" + +msgid "common.share-link.page-shared" +msgid_plural "common.share-link.page-shared" +msgstr[0] "bladsy gedeel" +msgstr[1] "%s bladsye gedeel" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-subtitle" +msgstr "Dit is gratis, dit is oopbron" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-submit" +msgstr "Meld aan" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.default-team-name" +msgstr "Jou Penpot" + +msgid "common.share-link.permissions-can-inspect" +msgstr "Kan kode inspekteer" + +msgid "common.share-link.team-members" +msgstr "Slegs spanlede" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.terms-privacy-agreement" +msgstr "" +"Wanneer jy 'n nuwe rekening skep, stem jy in tot ons diensbepalings en " +"privaatheidsbeleid." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.new-password" +msgstr "Tik 'n nuwe wagwoord in" + +msgid "common.share-link.title" +msgstr "Deel prototipes" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate" +msgstr "Dupliseer" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.verification-email-sent" +msgstr "Ons het 'n verifikasie-e-pos aan gestuur" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.profile-not-verified" +msgstr "" +"Profiel is nie geverifieer nie, verifieer asseblief profiel voordat jy " +"voortgaan." + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-subtitle" +msgstr "Ons sal vir jou 'n e-pos stuur met instruksies" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.forgot-password" +msgstr "Wagwoord vergeet?" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.recovery-token-sent" +msgstr "Wagwoordherwinningskakel na jou inkassie gestuur." + +msgid "common.publish" +msgstr "Publiseer" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.recovery-submit" +msgstr "Verander jou wagwoord" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.change-email" +msgstr "Verander e-pos" + +msgid "auth.privacy-policy" +msgstr "Privaatheidsbeleid" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.password-changed-successfully" +msgstr "Wagwoord suksesvol verander" + +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "Die naam moet 'n ander karakter as spasie bevat." + +msgid "common.share-link.permissions-pages" +msgstr "Bladsye gedeel" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-account" +msgstr "Skep demo rekening" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.title" +msgstr "Koppelvlak Deurloop" + +msgid "common.share-link.manage-ops" +msgstr "Bestuur toestemmings" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.tutorial-hero.start" +msgstr "Begin die tutoriaal" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-length-hint" +msgstr "Ten minste 8 karakters" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.tutorial-hero.info" +msgstr "" +"Leer die basiese beginsels by Penpot terwyl jy pret het met hierdie " +"praktiese tutoriaal." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "Jy het tot dusver geen tokens nie." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Persoonlike toegangtokens funksioneer soos 'n alternatief vir ons aanmeld-/" +"wagwoord-verifikasiestelsel en kan gebruik word om 'n toepassing toe te laat " +"om toegang tot die interne Penpot API te verkry" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.text" +msgstr "" +"Penpot is bedoel vir spanne. Nooi lede om saam te werk aan projekte en lêers" + +msgid "common.share-link.all-users" +msgstr "Alle Penpot-gebruikers" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Persoonlike toegangstokens" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "Verval op %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "Druk die knoppie \"Genereer nuwe token\" om een te genereer." + +#: src/app/main/ui/dashboard/grid.cljs +#, markdown +msgid "dashboard.empty-placeholder-drafts" +msgstr "" +"Lêers wat by biblioteke gevoeg is, sal hier verskyn. Probeer om jou lêers te " +"deel of voeg by vanaf ons [Biblioteke en sjablone](https://penpot.app/" +"libraries-templates.html)." + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-github-submit" +msgstr "GitHub" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-google-submit" +msgstr "Google" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.delete-team" +msgstr "Verwyder span" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "Toegangstoken is suksesvol geskep." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "Nooit" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.password" +msgstr "Wagwoord" + +msgid "common.share-link.link-copied-success" +msgstr "Skakel suksesvol gekopieer" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.email" +msgstr "E-pos" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.register-submit" +msgstr "Skep 'n rekening" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "Geen verval datum nie" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "Die naam moet hoogstens 250 karakters bevat." + +msgid "dashboard.export-binary-multi" +msgstr "Laai %s Penpot lêers (.penpot) af" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.start" +msgstr "Begin die toer" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate-multi" +msgstr "Dupliseer %s lêers" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.login-here" +msgstr "Meld hier aan" diff --git a/frontend/translations/ar.po b/frontend/translations/ar.po index 1020b5bd3d..9bc152ac5d 100644 --- a/frontend/translations/ar.po +++ b/frontend/translations/ar.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2023-07-02 17:52+0000\n" -"Last-Translator: Amine Gdoura \n" +"PO-Revision-Date: 2024-01-02 16:16+0000\n" +"Last-Translator: Alejandro Alonso \n" "Language-Team: Arabic \n" "Language: ar\n" @@ -10,7 +10,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " "&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" -"X-Generator: Weblate 5.0-dev\n" +"X-Generator: Weblate 5.4-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -47,7 +47,7 @@ msgstr "هل نسيت كلمة السر؟" #: src/app/main/ui/auth/register.cljs msgid "auth.fullname" -msgstr "الاسم بالكامل" +msgstr "الاسم الكامل" #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" @@ -67,7 +67,7 @@ msgstr "Github" #: src/app/main/ui/auth/login.cljs msgid "auth.login-with-gitlab-submit" -msgstr "Gitlab" +msgstr "Gitlabسجل دخولك عن طريق" #: src/app/main/ui/auth/login.cljs msgid "auth.login-with-google-submit" @@ -75,11 +75,11 @@ msgstr "جوجل" #: src/app/main/ui/auth/login.cljs msgid "auth.login-with-ldap-submit" -msgstr "LDAP" +msgstr "LDAPسجل دخولك عن طريق" #: src/app/main/ui/auth/login.cljs msgid "auth.login-with-oidc-submit" -msgstr "OpenID" +msgstr "OpenID سجل دخولك عن طريق" #: src/app/main/ui/auth/recovery.cljs msgid "auth.new-password" @@ -95,11 +95,11 @@ msgstr "تم تغيير كلمة المرور بنجاح" #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.notifications.profile-not-verified" -msgstr "لم يتم التعرف على الحساب الشخصي ، يرجى التحقق قبل المتابعة." +msgstr "لم يتم التأكيد على الحساب الشخصي ، يرجى التحقق قبل المواصلة" #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.notifications.recovery-token-sent" -msgstr "تم إرسال رابط استعادة كلمة المرور إلى صندوق البريد الخاص بك." +msgstr "تم إرسال رمز الاسترداد لاستعادة كلمة المرور إلى صندوق البريد الخاص بك" #: src/app/main/ui/auth/verify_token.cljs msgid "auth.notifications.team-invitation-accepted" @@ -157,7 +157,8 @@ msgstr "شروط الخدمة" #: src/app/main/ui/auth/register.cljs msgid "auth.terms-privacy-agreement" -msgstr "عند إنشاء حساب جديد ، فإنك توافق على شروط الخدمة وسياسة الخصوصية الخاصة بنا." +msgstr "" +"عند إنشاء حساب جديد ، فإنك توافق على شروط الخدمة وسياسة الخصوصية الخاصة بنا." #: src/app/main/ui/auth/register.cljs msgid "auth.verification-email-sent" @@ -400,7 +401,6 @@ msgstr[3] "عدد قليل من الخطوط المضافة" msgstr[4] "تمت إضافة العديد من الخطوط" msgstr[5] "" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "ستتم إضافة أي خط ويب تقوم بتحميله هنا إلى قائمة عائلة الخطوط المتوفرة في " @@ -408,7 +408,6 @@ msgstr "" "عائلة الخطوط على أنها ** عائلة خط واحدة **. يمكنك تحميل الخطوط بالتنسيقات " "التالية: ** TTF و OTF و WOFF ** (ستحتاج إلى تنسيق واحد فقط)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "يجب عليك فقط تحميل الخطوط التي تمتلكها أو لديك ترخيص لاستخدامها في Penpot. " @@ -420,6 +419,14 @@ msgstr "" msgid "dashboard.fonts.upload-all" msgstr "حمل الكل" +msgid "dashboard.fonts.warning-text" +msgstr "" +"لقد اكتشفنا مشكلة محتملة في الخطوط الخاصة بك تتعلق بالمقاييس الرأسية لأنظمة " +"التشغيل المختلفة. للتحقق من ذلك ، يمكنك استخدام خدمات المقاييس العمودية " +"للخطوط مثل [هذه] (https://vertical-metrics.netlify.app/). بالإضافة إلى ذلك " +"، نوصي باستخدام [Transfonter] (https://transfonter.org/) لإنشاء خطوط الويب " +"وإصلاح الأخطاء. " + msgid "dashboard.import" msgstr "استيراد ملفات" @@ -655,6 +662,18 @@ msgstr "الغاء نشر المكتبة" msgid "dashboard.update-settings" msgstr "تحديث الإعدادات" +msgid "dashboard.webhooks.active" +msgstr "نشط" + +msgid "dashboard.webhooks.active.explain" +msgstr "عندما يتم تشغيل هذا الخطاف ، سيتم تسليم تفاصيل الحدث" + +msgid "dashboard.webhooks.content-type" +msgstr "نوع المحتوى" + +msgid "dashboard.webhooks.create" +msgstr "إنشاء الرد التلقائي على الويب" + #: src/app/main/ui/settings.cljs msgid "dashboard.your-account-title" msgstr "حسابك" @@ -706,6 +725,12 @@ msgstr "موفر المصادقة غير معد ومسجل." msgid "errors.auth.unable-to-login" msgstr "يبدوا أنك غير مصرح لك أو أن الجلسة إنتهت." +msgid "errors.bad-font" +msgstr "تعذر تحميل الخط٪ s" + +msgid "errors.bad-font-plural" +msgstr "تعذر تحميل الخطوط٪ s" + #: src/app/main/data/workspace.cljs msgid "errors.clipboard-not-implemented" msgstr "لا يمكن للمتصفح إجراء هذه العملية" @@ -717,7 +742,7 @@ msgstr "البريد الإلكتروني مستخدم بالفعل" #: src/app/main/ui/auth/verify_token.cljs msgid "errors.email-already-validated" -msgstr "تم التحقق من صحة البريد الإلكتروني." +msgstr "متم التحقق من صحة البريد الإلكتروني" msgid "errors.email-as-password" msgstr "لا يمكنك استخدام بريدك الإلكتروني ككلمة مرور" @@ -729,6 +754,11 @@ msgstr "لا يمكنك استخدام بريدك الإلكتروني ككلم msgid "errors.email-has-permanent-bounces" msgstr "يحتوي البريد الإلكتروني «%s» على العديد من تقارير الارتداد الدائم." +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, +#: src/app/main/ui/auth/recovery_request.cljs +msgid "errors.email-invalid" +msgstr "أدخل بريدًا إلكترونيًا صالحًا من فضلك" + #: src/app/main/ui/settings/change_email.cljs msgid "errors.email-invalid-confirmation" msgstr "يجب أن يتطابق البريد الإلكتروني للتأكيد" @@ -736,6 +766,16 @@ msgstr "يجب أن يتطابق البريد الإلكتروني للتأكي msgid "errors.email-spam-or-permanent-bounces" msgstr "تم الإبلاغ عن البريد الإلكتروني «٪ s» كبريد عشوائي أو مرتد بشكل دائم." +#: src/app/main/errors.cljs +msgid "errors.feature-mismatch" +msgstr "" +"يبدو أنك تفتح ملفًا تم تمكين الميزة \"٪ s\" فيه ولكن الواجهة الأمامية لـ " +"penpot لا تدعمه أو تم تعطيله." + +#: src/app/main/errors.cljs +msgid "errors.feature-not-supported" +msgstr "الميزة '٪ s' غير مدعومة." + #: src/app/main/ui/auth/verify_token.cljs, #: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.generic" @@ -782,6 +822,9 @@ msgstr "يجب أن تتطابق كلمة مرور التأكيد" msgid "errors.password-too-short" msgstr "يجب ألا تقل كلمة المرور عن 8 أحرف" +msgid "errors.profile-blocked" +msgstr "هذا الملف الشخصي محظور" + #: src/app/main/ui/auth/recovery_request.cljs, #: src/app/main/ui/settings/change_email.cljs, #: src/app/main/ui/dashboard/team.cljs @@ -813,6 +856,21 @@ msgstr "حدث خطأ غير متوقع." msgid "errors.unexpected-token" msgstr "رمز غير معروف" +msgid "errors.webhooks.connection" +msgstr "خطأ في الاتصال ، عنوان إلكتروني لا يمكن الوصول إليه" + +msgid "errors.webhooks.last-delivery" +msgstr "آخر تسليم لم يكن ناجحًا." + +msgid "errors.webhooks.timeout" +msgstr "نفذ الوقت" + +msgid "errors.webhooks.unexpected" +msgstr "خطأ غير متوقع في التحقق" + +msgid "errors.webhooks.unexpected-status" +msgstr "حالة غير متوقعة٪ s" + #: src/app/main/ui/auth/login.cljs msgid "errors.wrong-credentials" msgstr "يبدو أن اسم المستخدم أو كلمة المرور خاطئة." @@ -853,7 +911,7 @@ msgstr "البريد الإلكتروني" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "اذهب إلى Twitter" +msgstr "اذهب إلى X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -861,7 +919,7 @@ msgstr "هنا للمساعدة في استفساراتك التقنية." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "حساب دعم تويتر" +msgstr "رد عنوان تويتر" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -873,23 +931,23 @@ msgstr "الضبابية" #: src/app/main/ui/inspect/attributes/blur.cljs msgid "inspect.attributes.blur.value" -msgstr "قيمة" +msgstr "تفتيش قيمة الطمس" #: src/app/main/ui/inspect/attributes/common.cljs msgid "inspect.attributes.color.hex" -msgstr "HEX" +msgstr "تفتيش صفات اللون" #: src/app/main/ui/inspect/attributes/common.cljs msgid "inspect.attributes.color.hsla" -msgstr "HSLA" +msgstr "HSLAتتفتيش صفات اللون" #: src/app/main/ui/inspect/attributes/common.cljs msgid "inspect.attributes.color.rgba" -msgstr "RGBA" +msgstr "RGBAفتش اللون" #: src/app/main/ui/inspect/attributes/fill.cljs msgid "inspect.attributes.fill" -msgstr "ملء" +msgstr "املأ" #: src/app/main/ui/inspect/attributes/image.cljs msgid "inspect.attributes.image.download" @@ -934,7 +992,7 @@ msgstr "عرض" #: src/app/main/ui/inspect/attributes/shadow.cljs msgid "inspect.attributes.shadow" -msgstr "ظل" +msgstr "ظلل" #: src/app/main/ui/inspect/attributes/stroke.cljs msgid "inspect.attributes.stroke" @@ -981,6 +1039,10 @@ msgstr "حجم الخط" msgid "inspect.attributes.typography.font-style" msgstr "نوع الخط" +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-weight" +msgstr "وزن الخط" + #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.letter-spacing" msgstr "تباعد الحروف" @@ -1018,6 +1080,9 @@ msgstr "حالة العنوان" msgid "inspect.attributes.typography.text-transform.uppercase" msgstr "الأحرف الكبيرة" +msgid "inspect.empty.help" +msgstr "إذا كنت تريد معرفة المزيد عن فحص التصميم ، فتفضل بزيارة مركز مساعدة لPenpot" + #: src/app/main/ui/inspect/right_sidebar.cljs msgid "inspect.tabs.code" msgstr "شفرة" @@ -1285,7 +1350,6 @@ msgid "labels.no-invitations" msgstr "لا توجد دعوات." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "اضغط على الزر \"دعوة إلى الفريق\" لدعوة المزيد من الأعضاء إلى هذا الفريق." @@ -1835,12 +1899,6 @@ msgstr "دليل المساهمة" msgid "onboarding-v2.welcome.title" msgstr "مرحبًا بك في Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "أنشئ فريقًا لاحقًا" - -msgid "onboarding.choice.team-up.create-team" -msgstr "اسم فريقك" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "بعد تسمية فريقك ، ستتمكن من دعوة الأشخاص للانضمام." @@ -1853,12 +1911,6 @@ msgstr "دعوة أعضاء" msgid "onboarding.choice.team-up.invite-members-info" msgstr "تذكر أن تشمل الجميع. المطورين والمصممين والمديرين ... التنوع يضيف :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "أنشئ فريقًا وادعُه لاحقًا" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "أنشئ فريقًا وأرسل الدعوات" - msgid "onboarding.choice.team-up.roles" msgstr "دعوة مع الدور:" @@ -1874,9 +1926,6 @@ msgstr "سياسة الخصوصية." msgid "onboarding.newsletter.title" msgstr "هل تريد تلقي أخبار Penpot؟" -msgid "onboarding.slide.1.title" -msgstr "اجعل تصميماتك تنبض بالحياة من خلال التفاعلات" - msgid "onboarding.team-modal.create-team" msgstr "أنشئ فريقًا" @@ -1907,7 +1956,7 @@ msgid "onboarding.templates.title" msgstr "إبدأ التصميم" msgid "onboarding.welcome.alt" -msgstr "Penpot" +msgstr "Penpotأهلا بك في" #: src/app/main/ui/auth/recovery.cljs msgid "profile.recovery.go-to-login" @@ -1920,7 +1969,7 @@ msgstr "اذهب إلى تسجيل الدخول" #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, #: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" -msgstr "مختلط" +msgstr "مكرر" # SECTIONS msgid "shortcut-section.basics" @@ -2211,39 +2260,39 @@ msgstr "إذهب إلى لوحة المعلومات" #: src/app/main/ui/dashboard/files.cljs msgid "title.dashboard.files" -msgstr "%s - Penpot" +msgstr "%s - Penpotعنوان ملفات لوحة القيادة" #: src/app/main/ui/dashboard/fonts.cljs msgid "title.dashboard.font-providers" -msgstr "موفرو الخطوط - %s - Penpot" +msgstr "مزودي أسلوب الخط - %s - Penpot" #: src/app/main/ui/dashboard/fonts.cljs msgid "title.dashboard.fonts" -msgstr "الخطوط - %s - Penpot" +msgstr "أسلوب خط لوحة القيادة - %s - Penpot" #: src/app/main/ui/dashboard/projects.cljs msgid "title.dashboard.projects" -msgstr "المشاريع - %s - Penpot" +msgstr "برامج ملفات القيادة - %s - Penpot" #: src/app/main/ui/dashboard/search.cljs msgid "title.dashboard.search" -msgstr "بحث - %s - بينبوت" +msgstr "البحث - %s - Penpot" #: src/app/main/ui/dashboard/libraries.cljs msgid "title.dashboard.shared-libraries" -msgstr "المكتبات المشتركة - %s - Penpot" +msgstr "المكاتب المقسمة - %s - Penpot" #: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/auth.cljs msgid "title.default" -msgstr "Penpot - حرية التصميم لفرق العمل" +msgstr "Penpot - صمم حرية الفرق" #: src/app/main/ui/settings/feedback.cljs msgid "title.settings.feedback" -msgstr "تقديم ملاحظات - Penpot" +msgstr "جواب الرد - Penpot" #: src/app/main/ui/settings/options.cljs msgid "title.settings.options" -msgstr "الإعدادات - Penpot" +msgstr "إعدادات - Penpot" #: src/app/main/ui/settings/password.cljs msgid "title.settings.password" @@ -2251,39 +2300,39 @@ msgstr "كلمة المرور - Penpot" #: src/app/main/ui/settings/profile.cljs msgid "title.settings.profile" -msgstr "الملف الشخصي - Penpot" +msgstr "واجهة الحساب - Penpot" #: src/app/main/ui/dashboard/team.cljs msgid "title.team-members" -msgstr "الأعضاء - %s - Penpot" +msgstr "أعضاء الفريق - %s - Penpot" #: src/app/main/ui/dashboard/team.cljs msgid "title.team-settings" -msgstr "الإعدادات - %s - Penpot" +msgstr "إعدادات - %s - Penpot" #: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs msgid "title.viewer" -msgstr "%s - وضع العرض - Penpot" +msgstr "%s -أسلوب العرض - Penpot" #: src/app/main/ui/workspace.cljs msgid "title.workspace" -msgstr "%s - Penpot" +msgstr "%s -مساحة العمل Penpot" #: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs msgid "viewer.empty-state" -msgstr "لم يتم العثور على لوحات الرسم على الصفحة." +msgstr "No boards found on the page." #: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs msgid "viewer.frame-not-found" -msgstr "لوح الرسم غير موجود." +msgstr "لم يعثر على البورد ." #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.dont-show-interactions" -msgstr "لا تظهر التفاعلات" +msgstr "لا تطهر التفاعلات" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.fullscreen" -msgstr "ملء الشاشة" +msgstr "تكبير الشاشة" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.interactions" @@ -2295,11 +2344,11 @@ msgstr "نسخ الرابط" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.show-interactions" -msgstr "إظهار التفاعلات" +msgstr "أطهر التفاعلات بالنقر" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.show-interactions-on-click" -msgstr "إظهار التفاعلات عند النقر" +msgstr "أطهر التفاعلات بالنقر" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.sitemap" @@ -2307,43 +2356,43 @@ msgstr "خريطة الموقع" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.hcenter" -msgstr "(%s) محاذاة الوسط الأفقي" +msgstr "محاذاة المركز الأفقي (%s)" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.hdistribute" -msgstr "(%s) توزيع التباعد الأفقي" +msgstr "توزيع المسافات الأفقية (%s)" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.hleft" -msgstr "(%s) محاذاة لليسار" +msgstr "محاذاة لليسار (%s)" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.hright" -msgstr "(%s) محاذاة لليمين" +msgstr "محاذاة لليمين (%s)" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.vbottom" -msgstr "(%s) محاذاة للأسفل" +msgstr "محاذاة للأسفل (%s)" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.vcenter" -msgstr "(%s) محاذاة للوسط عموديًا" +msgstr "محاذاة للمركز العمودي (%s)" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.vdistribute" -msgstr "(%s) توزيع التباعد عموديًا" +msgstr "توزيع التباعد الرئسي (%s)" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.vtop" -msgstr "(%s) محاذاة للأعلى" +msgstr "محاذاة أعلى (%s)" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.assets" -msgstr "أصول رقمية" +msgstr "أصول" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.box-filter-all" -msgstr "كل الأصول الرقمية" +msgstr "كل الأصول" #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs @@ -2353,36 +2402,36 @@ msgstr "الألوان" #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.components" -msgstr "المكونات" +msgstr "عناصر" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.create-group" -msgstr "أنشئ مجموعة" +msgstr "إيجاد مجموعة" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.create-group-hint" -msgstr "ستتم تسمية عناصرك تلقائيًا باسم \"المجموعة / العنصر\"" +msgstr "\"ستسمى عناصرك تلقائيا ك\"اسم مجموعة\" \"اسم عنصر" #: src/app/main/ui/workspace/sidebar/sitemap.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" -msgstr "حذف" +msgstr "امسح" #: src/app/main/ui/workspace/sidebar/sitemap.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.duplicate" -msgstr "تكرار" +msgstr "انسخ" #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" -msgstr "تعديل" +msgstr "التحرير" #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.graphics" -msgstr "الرسومات" +msgstr "رسومات" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.group" @@ -2398,7 +2447,7 @@ msgstr "المكتبات" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.not-found" -msgstr "لم يتم العثور على أصول رقمية" +msgstr "الأصل غير موجود" #: src/app/main/ui/workspace/sidebar/sitemap.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs, @@ -2412,30 +2461,26 @@ msgstr "إعادة تسمية المجموعة" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.search" -msgstr "البحث عن الأصول الرقمية" +msgstr "الخث عن أصل" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.selected-count" msgid_plural "workspace.assets.selected-count" -msgstr[0] "0" -msgstr[1] "1" -msgstr[2] "2" -msgstr[3] "بضع" -msgstr[4] "الكثير" -msgstr[5] "غير ذلك" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "متشاركة" +msgstr[0] "%s العناصر المحددة" +msgstr[1] "%s العناصر المحددة" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" +msgstr[5] "" #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" -msgstr "صياغة الحروف" +msgstr "الطباعة" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.font-id" -msgstr "الخط" +msgstr "أسلوب الخط" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.font-size" @@ -2443,11 +2488,11 @@ msgstr "الحجم" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.font-variant-id" -msgstr "متغير" +msgstr "البديل" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.go-to-edit" -msgstr "الذهاب إلى ملف مكتبة الأنماط لتعديله" +msgstr "اذهب إلى تحرير نوع ملف المكتبة" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.letter-spacing" @@ -2455,13 +2500,13 @@ msgstr "تباعد الحروف" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.line-height" -msgstr "ارتفاع الخط" +msgstr "طول الخط" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, #: src/app/main/ui/inspect/attributes/text.cljs, #: src/app/main/ui/inspect/attributes/text.cljs msgid "workspace.assets.typography.sample" -msgstr "مثال" +msgstr "أسلوب خط النص" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.text-transform" @@ -2469,93 +2514,77 @@ msgstr "تحويل النص" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.ungroup" -msgstr "إلغاء التجميع" +msgstr "فك التجميع" #: src/app/main/data/workspace/libraries.cljs, #: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" -msgstr "تدرج خطي" +msgstr "الانحدار الخطي" #: src/app/main/data/workspace/libraries.cljs, #: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.radial" -msgstr "تدرج شعاعي" +msgstr "الانحدار الشعاعي" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-dynamic-alignment" -msgstr "تعطيل المحاذاة الديناميكية" +msgstr "إبطال المحاذاة الدينماكية" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-scale-text" -msgstr "تعطيل مقياس النص" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "تعطيل الانطباق على الشبكة" +msgstr "إبطال المقياس النسبي" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-dynamic-alignment" -msgstr "تفعيل المحاذاة الديناميكية" +msgstr "تككين المحاذاة الدينماكية" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-scale-text" -msgstr "تفعيل مقياس النص" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "الانطباق على الشبكة" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "إخفاء الشبكات" +msgstr "تمكين نص المقياس" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" -msgstr "إخفاء لوحة الألوان" +msgstr "إخفاء لون اللوحة" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-rules" -msgstr "إخفاء القواعد" +msgstr "إخفاء المسطرات" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.select-all" -msgstr "اختر الكل" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "أظهر تخطيط الجدول" +msgstr "حدد الجميع" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" -msgstr "إظهار لوح الألوان" +msgstr "أظهر لون اللوحة" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-rules" -msgstr "إظهار القواعد" +msgstr "أظهر المسطرات" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.save-error" -msgstr "خطأ في الحفظ" +msgstr "خطأ في حفظ الملف" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.saved" -msgstr "حفظ" +msgstr "تم الحفظ" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.saving" -msgstr "جاري الحفظ" +msgstr "حفظ الملف" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.unsaved" -msgstr "التغييرات غير المحفوظة" +msgstr "تغييرات غير محفوظة" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.viewer" -msgstr "عرض الوضع (%s)" +msgstr "وضع العرض (%s)" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.add" -msgstr "أضف" +msgstr "الإضافة" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.colors" @@ -2564,20 +2593,20 @@ msgstr "%s الألوان" #: src/app/main/ui/workspace/colorpicker/libraries.cljs, #: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" -msgstr "مكتبة الملف" +msgstr "مكتبة الملفات" #: src/app/main/ui/workspace/colorpicker/libraries.cljs, #: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" -msgstr "الألوان الحديثة" +msgstr "الألوان المؤخرة" #: src/app/main/ui/workspace/colorpicker.cljs msgid "workspace.libraries.colors.save-color" -msgstr "حفظ نمط اللون" +msgstr "حفظ أسلوب اللون" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.components" -msgstr "%s المكونات" +msgstr "%s العناصر" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.file-library" @@ -2593,47 +2622,47 @@ msgstr "المكتبات في هذا الملف" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.libraries" -msgstr "مكتبات" +msgstr "المكتبات" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.library" -msgstr "مكتبة" +msgstr "المكتبة" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.no-libraries-need-sync" -msgstr "لا توجد مكتبات مشتركة تحتاج إلى تحديث" +msgstr "لا يوجد مكتبات مشتركة تحتاج إلى تحديث" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.no-matches-for" -msgstr "لم يتم العثور على مطابقات ل “%s“" +msgstr "لا يوجد ما يطابق هذا “%s“" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.no-shared-libraries-available" -msgstr "لا توجد مكتبات مشتركة متاحة" +msgstr "لا يوجد مكتبات مشتركة" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.search-shared-libraries" -msgstr "ابحث في المكتبات المشتركة" +msgstr "البحث في المكتبات المشتركة" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.shared-libraries" -msgstr "المكتبات المشتركة" +msgstr "مكتبات مشتركة" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.libraries.text.multiple-typography" -msgstr "طباعة متعددة" +msgstr "كتابات عديدة" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.libraries.text.multiple-typography-tooltip" -msgstr "قم بفك ارتباط كافة الأنماط المطبعية" +msgstr "إلغاء روابط كل الكتابات" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.typography" -msgstr "%s الطباعة" +msgstr "%s الكتابات" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.update" -msgstr "تحديث" +msgstr "التحديث" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.updates" @@ -2641,27 +2670,27 @@ msgstr "التحديثات" #: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "workspace.options.blur-options.title" -msgstr "الضبابية" +msgstr "الطمس" #: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "workspace.options.blur-options.title.group" -msgstr "ضبابية المجموعة" +msgstr "تطميس المجموعة" #: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "workspace.options.blur-options.title.multiple" -msgstr "الضبابية المحددة" +msgstr "تطميس المحدد" #: src/app/main/ui/workspace/sidebar/options/page.cljs msgid "workspace.options.canvas-background" -msgstr "خلفية قماشية" +msgstr "خلفية اللوحة القماشية" #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs msgid "workspace.options.component" -msgstr "المكونات" +msgstr "العنصر" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints" -msgstr "القيود" +msgstr "قيود" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints.bottom" @@ -2669,19 +2698,19 @@ msgstr "أسفل" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints.center" -msgstr "توسيط" +msgstr "وسط" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints.fix-when-scrolling" -msgstr "الإصلاح عند التمرير" +msgstr "أصلح حين التمرير" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints.left" -msgstr "يسار" +msgstr "شمال" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints.leftright" -msgstr "اليسار واليمين" +msgstr "يمين و شمال" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints.right" @@ -2697,7 +2726,7 @@ msgstr "أعلى" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints.topbottom" -msgstr "أعلى وأسفل" +msgstr "أعلى و أسفل" #: src/app/main/ui/workspace/sidebar/options.cljs msgid "workspace.options.design" @@ -2708,11 +2737,6 @@ msgstr "تصميم" msgid "workspace.options.export" msgstr "تصدير" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/inspect/exports.cljs -msgid "workspace.options.export-object" -msgstr "تصدير 0 عنصر" - #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs msgid "workspace.options.export.suffix" msgstr "لاحقة" @@ -2720,23 +2744,23 @@ msgstr "لاحقة" #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, #: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.exporting-object" -msgstr "جارٍ التصدير …" +msgstr "جاري التصدير…" #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.fill" -msgstr "ملء" +msgstr "الملأ" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.auto" -msgstr "آلي" +msgstr "تلقائي" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.column" -msgstr "الأعمدة" +msgstr "أعمدة" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.columns" -msgstr "الأعمدة" +msgstr "أعمدة" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.gutter" @@ -2744,15 +2768,15 @@ msgstr "مزراب" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.height" -msgstr "ارتفاع" +msgstr "طول" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.margin" -msgstr "الهامش" +msgstr "هامش" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.rows" -msgstr "الصفوف" +msgstr "صفوف" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.set-default" @@ -2772,11 +2796,11 @@ msgstr "أسفل" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.type.center" -msgstr "توسيط" +msgstr "وسط" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.type.left" -msgstr "اليسار" +msgstr "شمال" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.type.right" @@ -2784,7 +2808,7 @@ msgstr "يمين" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.type.stretch" -msgstr "التمدد" +msgstr "تمديد" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.type.top" @@ -2792,7 +2816,7 @@ msgstr "أعلى" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.use-default" -msgstr "استخدم الافتراضي" +msgstr "استعمل الإفتراضي" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.width" @@ -2800,7 +2824,7 @@ msgstr "عرض" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.row" -msgstr "الصفوف" +msgstr "صفوف" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.square" @@ -2808,102 +2832,1479 @@ msgstr "مربع" #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" -msgstr "ملء المجموعة" +msgstr "ملأ المجموعة" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.color" -msgstr "لون" +msgstr "اللون" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.color-burn" -msgstr "حرق لوني" +msgstr "احتراق اللون" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.color-dodge" -msgstr "تمويه لوني" +msgstr "انقاص كثافة اللون" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.darken" -msgstr "قاتم" +msgstr "أغمق" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.difference" -msgstr "اختلاف" +msgstr "الفارق" #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.there-are-updates" -msgstr "يوجد تحديثات في المكتبات المشتركة" +msgstr "يوجد تحديثات في المكتبة المشتركة" #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.update" msgstr "تحديث" msgid "workspace.viewport.click-to-close-path" -msgstr "انقر لإغلاق المسار" +msgstr "انقر لتغلق المسار" -#, markdown -msgid "dashboard.fonts.warning-text" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-out" +msgstr "خارج" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.move" +msgstr "تحريك (%s)" + +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "لا يوجد أنماط اللون في مكتبتك" + +msgid "workspace.options.component.copy" +msgstr "النسخ" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interactions" +msgstr "التفاعلات" + +msgid "workspace.undo.entry.multiple.circle" +msgstr "دوائر" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-action" +msgstr "فعل" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-center" +msgstr "المركز السفلي" + +msgid "workspace.shape.menu.transform-to-path" +msgstr "تحويل الى المسار" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-push" +msgstr "دفع" + +msgid "workspace.options.inspect" +msgstr "Inspectفحص" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-top-center" +msgstr "المركز العلوي" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.multiply" +msgstr "تضاعف" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.letter-spacing" +msgstr "تباعد الحروف" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.direction-rtl" +msgstr "RTL" + +msgid "workspace.options.component.create-annotation" +msgstr "إنشاء حاشية" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-auto-width" +msgstr "العرض التلقائي" + +msgid "workspace.options.shadow-options.color" +msgstr "لون الظل" + +msgid "viewer.header.inspect-section" +msgstr "التفحص (%s)" + +msgid "workspace.shape.menu.flatten" +msgstr "تسطيح" + +msgid "workspace.header.menu.hide-pixel-grid" +msgstr "إخفاء تشبيك اللوحة" + +msgid "workspace.undo.entry.multiple.page" +msgstr "صفحات" + +msgid "shortcuts.ungroup" +msgstr "فك التجميع" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.empty" +msgstr "لا يوجد تغييرات في التاريخ الى الآن" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin-simple" +msgstr "هامش بسيط" + +msgid "workspace.undo.entry.multiple.rect" +msgstr "مستطيلات" + +msgid "shortcuts.zoom-selected" +msgstr "كبر المحدد" + +msgid "workspace.options.grid.params.color" +msgstr "لون" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.group" +msgstr "مجموعة" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +msgid "workspace.options.size-presets" +msgstr "الإعدادات المسبقة للحجم" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin" +msgstr "هامش" + +msgid "workspace.options.opacity" +msgstr "العتامة" + +msgid "workspace.options.component.edit-annotation" +msgstr "تحرير حاشية" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.file" +msgstr "الملف" + +msgid "workspace.undo.entry.multiple.media" +msgstr "الأصول الرسومية" + +msgid "workspace.options.show-in-viewer" +msgstr "أظهر في وضع العرض" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.lowercase" +msgstr "أحرف صغيرة" + +msgid "workspace.undo.entry.multiple.group" +msgstr "مجموعات" + +msgid "workspace.shape.menu.create-multiple-components" +msgstr "إنشاء عناصر جديدة" + +msgid "webhooks.last-delivery.success" +msgstr "محاذاة المركز الأفقي" + +msgid "workspace.options.stroke-width" +msgstr "عرض الضرب" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease-out" +msgstr "خفف خارج" + +msgid "workspace.options.x" +msgstr "X محور" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-self" +msgstr "الذات" + +msgid "workspace.shape.menu.path" +msgstr "طريق" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.offsety" +msgstr "Y" + +msgid "workspace.focus.focus-mode" +msgstr "وضع التركيز" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-ms" +msgstr "القوائم" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.right" +msgstr "يمين" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.ellipse" +msgstr "الشكل البيضاوي (%s)" + +msgid "workspace.sidebar.layers.groups" +msgstr "مجموعات" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "مستطيل" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-top-left" +msgstr "أعلى اليسار" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.color-palette" +msgstr "لوحة اللون (%s)" + +msgid "workspace.undo.entry.multiple.frame" +msgstr "لوحة" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.layout-item-min-h" +msgstr "الحد الأدنى للطول" + +msgid "workspace.path.actions.add-node" +msgstr "أضف العقدة (%s)" + +msgid "workspace.options.component.main" +msgstr "أصل" + +msgid "workspace.undo.entry.single.frame" +msgstr "لوحة" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow-starts" +msgstr "جاري التخطيط" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-position" +msgstr "موضع" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.more-colors" +msgstr "المزيد من الألوان" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-bottom" +msgstr "محاذاة الأسفل" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.new" +msgstr "جديد %s" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.create-artboard-from-selection" +msgstr "تحديد محتوى اللوحة" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius-top-right" +msgstr "أعلى اليمين" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-in" +msgstr "داخل" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-toggle-overlay" +msgstr "تبديل التراكب" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow" +msgstr "سهم المثلث" + +msgid "workspace.undo.entry.multiple.curve" +msgstr "منحنيات" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-snap-guides" +msgstr "إبطال الفرقعة للخ\\وط الإرشادية" + +msgid "workspace.path.actions.snap-nodes" +msgstr "كسر العقد (%s)" + +msgid "workspace.undo.entry.multiple.multiple" +msgstr "أشياء" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-slide" +msgstr "انزلاق" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.layout-item-min-w" +msgstr "الحد الأدنى للعرض" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.layout-item-min-h" +msgstr "الحد الأدنو للارتفاع" + +msgid "workspace.sidebar.layers.masks" +msgstr "قناعات" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.screen" +msgstr "شاشة" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "رؤية جميع التغييرات" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.inner-shadow" +msgstr "التظليل الداخلي" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.frame" +msgstr "لوحة (%s)" + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "لا يوجد أنماط الكتابة في مكتبتك" + +#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.paste" +msgstr "معجون" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.selection-fill" +msgstr "اختر نوع الملأ" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-middle" +msgstr "محاذاة الوسط" + +msgid "viewer.breaking-change.description" msgstr "" -"لقد اكتشفنا مشكلة محتملة في الخطوط الخاصة بك تتعلق بالمقاييس الرأسية لأنظمة " -"التشغيل المختلفة. للتحقق من ذلك ، يمكنك استخدام خدمات المقاييس العمودية " -"للخطوط مثل [هذه] (https://vertical-metrics.netlify.app/). بالإضافة إلى ذلك ، " -"نوصي باستخدام [Transfonter] (https://transfonter.org/) لإنشاء خطوط الويب " -"وإصلاح الأخطاء. " +"وصف كسر التغييرThis shareable link is no longer valid. Create a new one or " +"ask the owner for a new one.هذا الرابط القابل للمشاركة لم يعد صالحا.صمم " +"جديدا أو اسأل مالكه للجديد" -msgid "dashboard.webhooks.active" -msgstr "نشط" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-background" +msgstr "إضافة تراكب الخلفية" -msgid "dashboard.webhooks.active.explain" -msgstr "عندما يتم تشغيل هذا الخطاف ، سيتم تسليم تفاصيل الحدث" +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.unknown" +msgstr "انتهت العملية %s" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs -msgid "errors.email-invalid" -msgstr "أدخل بريدًا إلكترونيًا صالحًا من فضلك" +msgid "shortcuts.zoom-lense-increase" +msgstr "زيادة عدسة التكبير" -#: src/app/main/errors.cljs -msgid "errors.feature-mismatch" -msgstr "" -"يبدو أنك تفتح ملفًا تم تمكين الميزة \"٪ s\" فيه ولكن الواجهة الأمامية لـ " -"penpot لا تدعمه أو تم تعطيله." +msgid "workspace.shape.menu.add-grid" +msgstr "إضافة تخطيط التشبيك" -msgid "errors.webhooks.unexpected-status" -msgstr "حالة غير متوقعة٪ s" +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +msgid "workspace.options.exporting-complete" +msgstr "تم التصدير" -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-weight" -msgstr "وزن الخط" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-delay" +msgstr "تأخير" -msgid "inspect.empty.help" -msgstr "" -"إذا كنت تريد معرفة المزيد عن فحص التصميم ، فتفضل بزيارة مركز مساعدة لPenpot" +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.luminosity" +msgstr "لمعان" -msgid "dashboard.webhooks.create" -msgstr "إنشاء الرد التلقائي على الويب" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flip-vertical" +msgstr "التوجيه الرئسي" -msgid "errors.bad-font" -msgstr "تعذر تحميل الخط٪ s" +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.position" +msgstr "الموضع" -msgid "errors.bad-font-plural" -msgstr "تعذر تحميل الخطوط٪ s" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square" +msgstr "مربع" -msgid "dashboard.webhooks.content-type" -msgstr "نوع المحتوى" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "تعديل (%s)" -#: src/app/main/errors.cljs -msgid "errors.feature-not-supported" -msgstr "الميزة '٪ s' غير مدعومة." +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.space-between" +msgstr "التباعد بين" -msgid "errors.profile-blocked" -msgstr "هذا الملف الشخصي محظور" +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.uppercase" +msgstr "الأحرف الكبيرة" -msgid "errors.webhooks.connection" -msgstr "خطأ في الاتصال ، عنوان إلكتروني لا يمكن الوصول إليه" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "تكبير" -msgid "errors.webhooks.last-delivery" -msgstr "آخر تسليم لم يكن ناجحًا." +msgid "workspace.options.component.annotation" +msgstr "حاشية" -msgid "errors.webhooks.timeout" -msgstr "نفذ الوقت" +msgid "shortcuts.toggle-layers" +msgstr "تبديل الطبقات" -msgid "errors.webhooks.unexpected" -msgstr "خطأ غير متوقع في التحقق" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-preserve-scroll" +msgstr "حفظ موضع التمرير" + +msgid "workspace.undo.entry.single.typography" +msgstr "أصل الكتابة" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.selection-color" +msgstr "الألوان المختارة" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.more-lib-colors" +msgstr "المزيد من ألوان المكتبة" + +msgid "workspace.header.menu.disable-snap-pixel-grid" +msgstr "إبطال الفرقعة للبكسل" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease-in" +msgstr "خفف داخل" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.solid" +msgstr "صلب" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.delete" +msgstr "محذوف %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.top" +msgstr "أعلى" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius.single-corners" +msgstr "زوايا مستقلة" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.dotted" +msgstr "منقط" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-artboard-names" +msgstr "إخفاء أسماء البورد" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.backward" +msgstr "أرسل الى الخلف" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.select-a-shape" +msgstr "اختر الشكل أو اللوحة لجر الإتصال الى لوحة أو شكل آخر" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke" +msgstr "ضرب" + +msgid "workspace.layout_grid.editor.title" +msgstr "تحرير التشبيك" + +msgid "workspace.undo.entry.single.page" +msgstr "صفحة" + +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "يجب الا يزيد اسم الويبهوك على 2048 حرفا" + +msgid "workspace.sidebar.layers.images" +msgstr "صور" + +msgid "workspace.header.menu.show-pixel-grid" +msgstr "Show pixel grid" + +msgid "workspace.header.menu.undo" +msgstr "الإلغاء" + +msgid "workspace.undo.entry.single.color" +msgstr "أصل اللون" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.line-height" +msgstr "ارتفاع الخط" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.ungroup" +msgstr "فك التجميع" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-auto-height" +msgstr "الإرتفاع التلقائي" + +msgid "workspace.focus.focus-on" +msgstr "تشغيل التركيز" + +msgid "viewer.header.comments-section" +msgstr "التعليقات (%s)" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.unlock" +msgstr "الغاء القفل" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius-bottom-right" +msgstr "أسفل اليمين" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-overlay" +msgstr "فتح التراكب" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-dissolve" +msgstr "تذوب" + +msgid "workspace.undo.entry.single.rect" +msgstr "مستطيل" + +msgid "shortcuts.toggle-visibility" +msgstr "أظهر\\أخف" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom-full-screen" +msgstr "تكبير الشاشة" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.cut" +msgstr "قطع" + +msgid "workspace.header.menu.disable-scale-content" +msgstr "Disable proportional scale" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +msgid "workspace.options.exporting-object-slow" +msgstr "التصدير بطيء بشكل غير متوقع" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.lock" +msgstr "إغلاق" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.modify" +msgstr "معدل %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin-all" +msgstr "جميع النواحي" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.strikethrough" +msgstr "الإضراب من خلال(%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.reset-overrides" +msgstr "إعادة ضبط التجاوزات" + +msgid "workspace.undo.entry.multiple.typography" +msgstr "أصول الكتابة" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "محاذاة المركز (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.text-palette" +msgstr "الكتابات (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.titlecase" +msgstr "Title case" + +msgid "workspace.sidebar.layers.frames" +msgstr "لوحات" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom-fit" +msgstr "تقليص المقياس للتناسب" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +msgid "workspace.options.exporting-object-error" +msgstr "Export failed" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.hsv" +msgstr "HSV" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.delete-flow-start" +msgstr "حذف المخطط" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-left" +msgstr "اأسفل اليسار" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "الماس" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-offset-effect" +msgstr "تأثير الإزاحة" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.mixed" +msgstr "مختلط" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.rgba" +msgstr "RGBA" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row" +msgstr "الصف" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-navigate-to" +msgstr "الإنتقال الى" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.inner" +msgstr "داخل" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.underline" +msgstr "تسطير (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-overlay-dest" +msgstr "إغلاق التراكب: %s" + +msgid "workspace.header.menu.redo" +msgstr "إعادة" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.detach-instance" +msgstr "فصل المثال" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "محاذاة اليمين (%s)" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-textpalette" +msgstr "أظهر لوخة أسلوب الخط" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flip-horizontal" +msgstr "Flip horizontal" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.hard-light" +msgstr "ضوء الثابت" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.layout-item-max-w" +msgstr "أقصى عرض" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column-reverse" +msgstr "العموج العكسي" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.front" +msgstr "النقل الى الأمام" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-center" +msgstr "مركز" + +msgid "workspace.undo.entry.single.multiple" +msgstr "عنصر" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.add-interaction" +msgstr "انقر على زر +لإضافة التفاعلات" + +msgid "workspace.shape.menu.difference" +msgstr "الفارق" + +msgid "workspace.assets.duplicate-main" +msgstr "انسخ الأصل" + +msgid "workspace.undo.entry.multiple.path" +msgstr "مسارات" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-right" +msgstr "أسفل اليمين" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show-main" +msgstr "أظهر المكون الرئيسي" + +msgid "shortcuts.v-distribute" +msgstr "النشر عموديا" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.grid-title" +msgstr "تشبيك" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.offsetx" +msgstr "X" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.none" +msgstr "لا شيء" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.exclusion" +msgstr "الإستبعاد" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.title" +msgstr "تاريخ" + +msgid "workspace.options.recent-fonts" +msgstr "مؤخر" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-textpalette" +msgstr "إخفاء لوحة أسلوب خط" + +msgid "workspace.undo.entry.single.component" +msgstr "component" + +msgid "workspace.assets.local-library" +msgstr "المكتبة المحلية" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.unmask" +msgstr "كشف القناع" + +msgid "shortcuts.toggle-textpalette" +msgstr "تبديل لوحة النص" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-top" +msgstr "محاذاة أعلى" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-while-pressing" +msgstr "حين الضغط" + +msgid "workspace.sidebar.collapse" +msgstr "انهيار الشريط الجانبي" + +msgid "workspace.options.height" +msgstr "طول" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.update-main" +msgstr "تحديث العنصر الرئيسي" + +msgid "shortcuts.toggle-rules" +msgstr "إظهار\\إخفاء المسطرة" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flow-start" +msgstr "بداية المخطط" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.remove-flex" +msgstr "إزالة تخطيط المنحنى" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow-start" +msgstr "إضافة المخطط" + +msgid "workspace.sidebar.layers.components" +msgstr "العناصر" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease" +msgstr "خفف" + +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/inspect/attributes/svg.cljs +msgid "workspace.sidebar.options.svg-attrs.title" +msgstr "السمات المستوردةSVG" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.comments" +msgstr "تعليقات (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-none" +msgstr "لا شيء" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.lighten" +msgstr "تفتيح" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +msgid "workspace.options.export-multiple" +msgstr "تحديد المصدر" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.hide" +msgstr "إخفاء" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +msgid "workspace.options.retry" +msgstr "أعد المحاولة" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-relative-to" +msgstr "بالنسبة الى" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.update-components-in-bulk" +msgstr "تحديث العناصر الرئيسية" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.sidebar.history" +msgstr "التاريخ (%s)" + +msgid "workspace.options.clip-content" +msgstr "محتوى المقطع" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.path" +msgstr "مسار (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius.all-corners" +msgstr "كل الزوايا" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-top-right" +msgstr "أعلى اليمين" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.select-layer" +msgstr "اختيار الطبقة" + +msgid "workspace.undo.entry.single.image" +msgstr "صورة" + +msgid "workspace.shape.menu.intersection" +msgstr "التفاعلات" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-trigger" +msgstr "مشغل" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.shortcuts" +msgstr "الإختصار (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding" +msgstr "حشوة" + +msgid "title.team-webhooks" +msgstr "خطافات الويب - %s - Penpot" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "تحديث المكتبة" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.none" +msgstr "لا شيء" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-none" +msgstr "(غير مهيء)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing" +msgstr "تخفيف" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "المثلث" + +msgid "workspace.path.actions.draw-nodes" +msgstr "جر العقدة (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row-reverse" +msgstr "الصف العكسي" + +msgid "workspace.undo.entry.single.media" +msgstr "أصل الرسومات" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.packed" +msgstr "معباة" + +msgid "workspace.header.menu.enable-snap-pixel-grid" +msgstr "تمكين الفرقعة للبكسل" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-mouse-enter" +msgstr "إدخال الفأرة" + +msgid "workspace.undo.entry.single.circle" +msgstr "دائرة" + +msgid "viewer.header.interactions-section" +msgstr "التفاعلات (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title.group" +msgstr "ظل المجموعة" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title.group" +msgstr "طبقات المجموعة" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.reset-zoom" +msgstr "إعادة ضبط" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.assets" +msgstr "أصول" + +msgid "workspace.assets.open-library" +msgstr "افتح ملف المكتب" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title-selection" +msgstr "نص الإختيار" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.shared-library" +msgstr "مكتبة مشتركة" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.size" +msgstr "مقاس" + +msgid "workspace.undo.entry.multiple.component" +msgstr "عناصر" + +msgid "workspace.focus.selection" +msgstr "تحديد" + +msgid "workspace.path.actions.merge-nodes" +msgstr "دمج العقد (%s)" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.create-component" +msgstr "إنشاء نعصر" + +msgid "workspace.undo.entry.multiple.color" +msgstr "أصول اللون" + +msgid "workspace.header.menu.enable-scale-content" +msgstr "تمكين المقياس النسبي" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.group-stroke" +msgstr "ضرب المجموعة" + +msgid "workspace.shape.menu.union" +msgstr "اتحاد" + +msgid "workspace.shape.menu.thumbnail-set" +msgstr "تعيين كصورة مصغرة" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom-fill" +msgstr "مقياس الملأ" + +msgid "workspace.sidebar.layers.texts" +msgstr "نصوص" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "محاذاة (%s)" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.dismiss" +msgstr "رفض" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.duplicate" +msgstr "ينسخ" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "مزيد من المعلومات" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.forward" +msgstr "النقل الى الأمام" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-while-hovering" +msgstr "حين التحوم" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show" +msgstr "العرض" + +msgid "workspace.shape.menu.hide-ui" +msgstr "أظهر أو إخف UI" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.center" +msgstr "مركز" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-manual" +msgstr "يدوي" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.sidebar.layers" +msgstr "الطبقات" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-mouse-leave" +msgstr "إخراج الفأرة" + +msgid "workspace.assets.typography.text-styles" +msgstr "أسلوب خط النص" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker" +msgstr "معلم الدائرة" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.layout-item-min-w" +msgstr "الحد الأدنى للعرض" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.gap" +msgstr "فجوة" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker" +msgstr "معلم المربع" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column" +msgstr "عمود" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation" +msgstr "الرسومات المتحركة" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.drop-shadow" +msgstr "إسقاط الظل" + +msgid "workspace.undo.entry.single.curve" +msgstr "منحنى" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.overlay" +msgstr "تراكب" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease-in-out" +msgstr "خفف داخل و خارج" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-snap-guides" +msgstr "الفرقعة للخطوط الإرشادية" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-linear" +msgstr "خطي" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-on-click" +msgstr "بعد النقر" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-navigate-to-dest" +msgstr "انتقال الى: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.spread" +msgstr "الانتشار" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.bottom" +msgstr "أسفل" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-duration" +msgstr "مدة" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.advanced-ops" +msgstr "خيارات متقدمة" + +msgid "shortcuts.toggle-zoom-style" +msgstr "تبديل أسلوب التكبير" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-outside" +msgstr "عطل إذا نقر في الخارج" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.blur" +msgstr "تطميس" + +msgid "workspace.path.actions.separate-nodes" +msgstr "فصل العقد (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.selection-stroke" +msgstr "ضرب الإختيار" + +#: src/app/main/ui/workspace/sidebar/options.cljs +msgid "workspace.options.prototype" +msgstr "النموذج المبدئي" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.use-play-button" +msgstr "استعمل زر التشغيل أعلاه لتشغيل منظر النموذج المبدئي." + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.hue" +msgstr "مسحة" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-artboard-names" +msgstr "أظهر أسماء البورد" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.dashed" +msgstr "متقطع" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.left" +msgstr "شمال" + +msgid "workspace.shape.menu.thumbnail-remove" +msgstr "أطهر الصورة المصغرة" + +msgid "shortcuts.toggle-layout-flex" +msgstr "أضف\\أزل ثني التخطيط" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.space-around" +msgstr "التباعد حول" + +msgid "workspace.options.width" +msgstr "عرض" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.rect" +msgstr "مستطيل (%s)" + +msgid "workspace.undo.entry.single.group" +msgstr "مجموعة" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.layout-item-max-w" +msgstr "أقصى عرض" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.sitemap" +msgstr "خريطة الموقع" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius-bottom-left" +msgstr "أسفل اليسار" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-overlay" +msgstr "إغلاق التراكب" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker" +msgstr "معلم الماس" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.soft-light" +msgstr "ضوء خافت" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding-simple" +msgstr "حشوة بسيطة" + +msgid "workspace.shape.menu.create-annotation" +msgstr "إنشاء تعليق توضيحي" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom-selected" +msgstr "تكبير" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-overlay-dest" +msgstr "فتح التراكب: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-url" +msgstr "رابط مفتوح" + +msgid "workspace.path.actions.delete-node" +msgstr "احذف العقدة (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-destination" +msgstr "وجهة" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.go-main" +msgstr "انتقل الى ملف العنصر الأصلي" + +msgid "shortcuts.undo" +msgstr "الغاء" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "سهم" + +msgid "workspace.path.actions.make-curve" +msgstr "الى المنحنى (%s)" + +msgid "workspace.options.search-font" +msgstr "البخث عن نوع الخط" + +msgid "workspace.path.actions.move-nodes" +msgstr "نقل العقد (%s)" + +msgid "workspace.path.actions.join-nodes" +msgstr "صل العقد (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-url" +msgstr "الرابط المفتوح" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.move" +msgstr "المكونات المعدلة" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title.multiple" +msgstr "الطبقات المحددة" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.direction-ltr" +msgstr "LTR" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-invitations" +msgstr "الدعوات - %s - Penpot" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.delete" +msgstr "حذف" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show-in-assets" +msgstr "العرض في لوحة الاصول" + +msgid "workspace.undo.entry.multiple.shape" +msgstr "أشكال" + +msgid "workspace.options.interaction-auto" +msgstr "تلقائي" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title.multiple" +msgstr "ظلال الإختيار" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-prev-screen" +msgstr "الشاشة السابقة" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.text" +msgstr "نص (%s)" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.copy" +msgstr "انسخ" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.curve" +msgstr "منحنى (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding-all" +msgstr "جميع النواحي" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title" +msgstr "نص" + +msgid "shortcuts.underline" +msgstr "الخط التحتي" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.rgb-complementary" +msgstr "RGB مكملات" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.edit" +msgstr "التحرير" + +msgid "shortcuts.unmask" +msgstr "كشف القناع" + +msgid "workspace.options.y" +msgstr "Y محور" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.round" +msgstr "دائري" + +msgid "shortcuts.toggle-lock" +msgstr "قفل\\فتح" + +msgid "viewer.breaking-change.message" +msgstr "آسف" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title" +msgstr "طبقة" + +msgid "workspace.undo.entry.multiple.text" +msgstr "نصوص" + +msgid "workspace.sidebar.layers.shapes" +msgstr "بسومات" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.add-flow-start" +msgstr "إضافة المخطط" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.show-fill-on-export" +msgstr "أظهر في المصدر" + +msgid "shortcuts.toggle-lock-size" +msgstr "قفل النسب" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.layout-item-max-h" +msgstr "أقصى ارتفاع" + +msgid "workspace.shape.menu.restore-main" +msgstr "استعادة العنصر الرئيسي" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.saturation" +msgstr "التشبع" + +msgid "workspace.sidebar.expand" +msgstr "توسيع الشريط الجانبي" + +#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.mask" +msgstr "قناع" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-toggle-overlay-dest" +msgstr "تبديل التراكب: %s" + +msgid "workspace.path.actions.make-corner" +msgstr "الى الزاوية (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "الدائرة" + +msgid "workspace.options.stroke-color" +msgstr "لون الضرب" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.outer" +msgstr "خارج" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius-top-left" +msgstr "أعلى الشمال" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.normal" +msgstr "عادي" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.preferences" +msgstr "التفضيلات" + +msgid "workspace.shape.menu.exclude" +msgstr "استبعاد" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.rotation" +msgstr "دوران" + +msgid "shortcuts.zoom-lense-decrease" +msgstr "تنقيص عدسة التكبير" + +msgid "workspace.undo.entry.single.shape" +msgstr "شكل" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "workspace.sidebar.sitemap" +msgstr "صفحات" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.view" +msgstr "المنظر" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.detach-instances-in-bulk" +msgstr "فصل الامثلة" + +msgid "workspace.focus.focus-off" +msgstr "تعطيل التركيز" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "رموز الوصول للحساب" + +msgid "workspace.options.radius" +msgstr "نصف القطر" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.back" +msgstr "أرسل الى الخلف" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow" +msgstr "سهم الخط" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.edit" +msgstr "تحرير" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom-fit-all" +msgstr "التكبير لتناسب الجميع" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-after-delay" +msgstr "بعد التأخير" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.layout-item-max-h" +msgstr "أقصى ارتفاع" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title" +msgstr "الظل" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.help-info" +msgstr "المساعدة و المعلومة" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title-group" +msgstr "نص للمجموعة" + +msgid "workspace.undo.entry.single.text" +msgstr "نص" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-fixed" +msgstr "مثبت" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.image" +msgstr "صورة (%s)" + +msgid "workspace.undo.entry.single.path" +msgstr "مسار" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.add-flex" +msgstr "إضافة تخطيط الثني" diff --git a/frontend/translations/bn.po b/frontend/translations/bn.po index b6b4842180..352aaac6ae 100644 --- a/frontend/translations/bn.po +++ b/frontend/translations/bn.po @@ -85,4 +85,4 @@ msgstr "নতুন পাসওয়ার্ড টাইপ করুন" #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.invalid-token-error" -msgstr "রিকভারি টোকেন সঠিক নয়।" \ No newline at end of file +msgstr "রিকভারি টোকেন সঠিক নয়।" diff --git a/frontend/translations/ca.po b/frontend/translations/ca.po index 1c278272e7..b75496da48 100644 --- a/frontend/translations/ca.po +++ b/frontend/translations/ca.po @@ -39,7 +39,8 @@ msgstr "" "Aquest és un servei de PROVA. NO L'UTILITZEU en treballs reals, ja que els " "projectes s'eliminaran periòdicament." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" msgstr "Correu electrònic" @@ -265,7 +266,8 @@ msgstr "Comença la visita" msgid "dasboard.walkthrough-hero.title" msgstr "Passeig per la interfície" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Afegeix a la biblioteca compartida" @@ -295,7 +297,8 @@ msgstr "Baixa el fitxer Penpot (.penpot)" msgid "dashboard.download-standard-file" msgstr "Baixa fitxer estàndard (.svg + .json)" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate" msgstr "Duplica" @@ -304,7 +307,6 @@ msgid "dashboard.duplicate-multi" msgstr "Duplica %s fitxers" #: src/app/main/ui/dashboard/grid.cljs -#, markdown msgid "dashboard.empty-placeholder-drafts" msgstr "" "Encara no hi ha fitxers. Si voleu provar algunes plantilles, podeu anar a " @@ -406,7 +408,6 @@ msgid_plural "dashboard.fonts.fonts-added" msgstr[0] "S'ha afegit 1 tipografia" msgstr[1] "S'han afegit %s tipografies" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "Les tipografies web que pengeu aquí s'afegiran a la llista de famílies " @@ -415,7 +416,6 @@ msgstr "" "sola família tipogràfica**. Podeu pujar tipografies en aquests formats: " "**TTF, OTF i WOFF** (només en cal un)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "Només podeu pujar tipografies de la vostra propietat o de les que tingueu " @@ -468,7 +468,8 @@ msgstr "S'està pujant el fitxer: %s" msgid "dashboard.invite-profile" msgstr "Convida a l'equip" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" msgstr "Abandona l'equip" @@ -494,7 +495,8 @@ msgstr "S'estan carregant els fitxers…" msgid "dashboard.loading-fonts" msgstr "s'estan carregant les tipografies…" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "Mou a" @@ -506,7 +508,8 @@ msgstr "Mou %s fitxers a" msgid "dashboard.move-to-other-team" msgstr "Mou a un altre equip" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" msgstr "+ Fitxer nou" @@ -569,7 +572,8 @@ msgstr "Projectes" msgid "dashboard.remove-account" msgstr "Voleu eliminar el vostre compte?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" msgstr "Elimina de les biblioteques compartides" @@ -613,7 +617,8 @@ msgstr "S'ha duplicat el fitxer" msgid "dashboard.success-duplicate-project" msgstr "S'ha eliminat el projecte" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-file" msgstr "S'ha mogut el fitxer" @@ -649,11 +654,14 @@ msgstr "Resultats de la cerca" msgid "dashboard.type-something" msgstr "Escriviu per cercar resultats" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.unpublish-shared" msgstr "Despublica la biblioteca" -#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/profile.cljs, +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Actualitza la configuració" @@ -669,7 +677,11 @@ msgstr "Correu electrònic" msgid "dashboard.your-name" msgstr "Nom" -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" msgstr "El meu Penpot" @@ -708,7 +720,8 @@ msgstr "Sembla que no esteu autenticat o que la sessió ha caducat." msgid "errors.clipboard-not-implemented" msgstr "El vostre navegador no pot fer aquesta operació" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/change_email.cljs msgid "errors.email-already-exists" msgstr "Aquest correu ja està en ús" @@ -719,7 +732,10 @@ msgstr "Aquest correu ja està validat." msgid "errors.email-as-password" msgstr "No podeu fer servir l'adreça de correu com a contrasenya" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" msgstr "El correu «%s» té molts informes de retorn permanents." @@ -730,7 +746,8 @@ msgstr "El correu de confirmació ha de coincidir" msgid "errors.email-spam-or-permanent-bounces" msgstr "El correu «%s» s'ha marcat com a brossa o rebot permanent." -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.generic" msgstr "Alguna cosa ha anat malament." @@ -759,7 +776,7 @@ msgstr "" "Sembla que el contingut de la imatge no coincideix amb l'extensió del " "fitxer." -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" msgstr "Sembla que la imatge no és vàlida." @@ -780,7 +797,9 @@ msgstr "La contrasenya ha de tenir 8 caràcters com a mínim" msgid "errors.profile-blocked" msgstr "El perfil està bloquejat" -#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.profile-is-muted" msgstr "" "El teu perfil té els missatges de correu silenciats (per informes de correu " @@ -803,7 +822,9 @@ msgstr "" "El propietari no pot abandonar l'equip, heu de reassignar el rol de " "propietat." -#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/data/media.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "errors.unexpected-error" msgstr "S'ha produït un error inesperat." @@ -852,7 +873,7 @@ msgstr "Correu electrònic" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Ves al Twitter" +msgstr "Ves al X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -860,7 +881,7 @@ msgstr "Compte per a ajudar amb dubtes tècnics." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Compte de Twitter d'ajuda" +msgstr "Compte de X d'ajuda" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -914,7 +935,8 @@ msgstr "Alçada" msgid "inspect.attributes.layout.left" msgstr "Esquerra" -#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs +#: src/app/main/ui/inspect/attributes/layout.cljs, +#: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.layout.radius" msgstr "Radi" @@ -934,23 +956,16 @@ msgstr "Amplada" msgid "inspect.attributes.shadow" msgstr "Ombra" -#: src/app/main/ui/inspect/attributes/shadow.cljs -msgid "inspect.attributes.shadow.shorthand.spread" -msgstr "S" - #: src/app/main/ui/inspect/attributes/stroke.cljs msgid "inspect.attributes.stroke" msgstr "Traç" -#, permanent msgid "inspect.attributes.stroke.alignment.center" msgstr "Centre" -#, permanent msgid "inspect.attributes.stroke.alignment.inner" msgstr "Interior" -#, permanent msgid "inspect.attributes.stroke.alignment.outer" msgstr "Exterior" @@ -1078,7 +1093,7 @@ msgstr "Acceptar" msgid "labels.add-custom-font" msgstr "Afegeix tipografia" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "Administració" @@ -1134,7 +1149,8 @@ msgstr "Podeu continuar amb un compte de Penpot" msgid "labels.create" msgstr "Crea" -#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team" msgstr "Crea un equip nou" @@ -1149,7 +1165,8 @@ msgstr "Tipografies personalitzades" msgid "labels.dashboard" msgstr "Tauler" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "Elimina" @@ -1169,7 +1186,10 @@ msgstr "Esborra invitació" msgid "labels.delete-multi-files" msgstr "Elimina %s fitxers" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/files.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.drafts" msgstr "Esborranys" @@ -1180,7 +1200,7 @@ msgstr "Edita" msgid "labels.edit-file" msgstr "Edita'l" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "Editor" @@ -1215,7 +1235,9 @@ msgstr "Tipografies" msgid "labels.github-repo" msgstr "Repositori Github" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "Envia opinions" @@ -1243,7 +1265,8 @@ msgstr "" msgid "labels.internal-error.main-message" msgstr "Error intern" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.invitations" msgstr "Invitacions" @@ -1262,11 +1285,11 @@ msgstr "Inicia sessió o registra'm" msgid "labels.logout" msgstr "Tanca la sessió" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.member" msgstr "Membre" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "Membres" @@ -1274,7 +1297,8 @@ msgstr "Membres" msgid "labels.new-password" msgstr "Contrasenya nova" -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +#: src/app/main/ui/workspace/comments.cljs, +#: src/app/main/ui/dashboard/comments.cljs msgid "labels.no-comments-available" msgstr "No teniu notificacions de comentaris pendents" @@ -1283,7 +1307,6 @@ msgid "labels.no-invitations" msgstr "No hi ha invitacions." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "" "Feu clic al botó «Convida a l'equip» per convidar més membres a aquest " @@ -1331,7 +1354,8 @@ msgstr "o" msgid "labels.owner" msgstr "Propietari" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.password" msgstr "Contrasenya" @@ -1339,7 +1363,8 @@ msgstr "Contrasenya" msgid "labels.pending-invitation" msgstr "Pendent" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.profile" msgstr "Perfil" @@ -1351,7 +1376,8 @@ msgstr "Projectes" msgid "labels.release-notes" msgstr "Notes de la versió" -#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/workspace/libraries.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" msgstr "Elimina" @@ -1359,7 +1385,9 @@ msgstr "Elimina" msgid "labels.remove-member" msgstr "Elimina membre" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.rename" msgstr "Canvia el nom" @@ -1371,7 +1399,7 @@ msgstr "Canvia el nom de l’equip" msgid "labels.resend-invitation" msgstr "Reenvia invitació" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Tornar a intentar-ho" @@ -1401,7 +1429,8 @@ msgstr "Estem de manteniment programat dels nostres sistemes." msgid "labels.service-unavailable.main-message" msgstr "Servei no disponible" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.settings" msgstr "Configuració" @@ -1456,7 +1485,7 @@ msgstr "Visor" msgid "labels.write-new-comment" msgstr "Escriu un comentari nou" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.you" msgstr "(jo)" @@ -1464,22 +1493,25 @@ msgstr "(jo)" msgid "labels.your-account" msgstr "El meu compte" -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "S'està carregant la imatge…" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" msgstr "Afegeix com a biblioteca compartida" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.hint" msgstr "" "Una vegada afegit com a biblioteca compartida, els recursos de la " "biblioteca d'aquest fitxer estaran disponibles per a usar-los entre la " "resta dels fitxers." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.message" msgstr "Afegeix \"%s\" com a biblioteca compartida" @@ -1599,13 +1631,15 @@ msgstr "Segur que voleu eliminar el projecte?" msgid "modals.delete-project-confirm.title" msgstr "Elimina el projecte" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.accept" msgid_plural "modals.delete-shared-confirm.accept" msgstr[0] "Suprimeix el fitxer" msgstr[1] "Suprimeix els fitxers" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "Esteu segur que voleu suprimir aquest fitxer?" @@ -1715,17 +1749,20 @@ msgstr "" msgid "modals.promote-owner-confirm.title" msgstr "Ascendeix a propietari" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" msgstr "Elimina de la biblioteca compartida" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.hint" msgstr "" "Una vegada suprimida com a biblioteca compartida, la biblioteca d'aquest " "fitxer deixarà d'estar disponible per a la resta dels fitxers." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.message" msgstr "Elimina «%s» com a biblioteca compartida" @@ -1733,31 +1770,37 @@ msgstr "Elimina «%s» com a biblioteca compartida" msgid "modals.small-nudge" msgstr "Mínima" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.hint" msgstr "" "S'actualitzaran els components en una llibreria compartida. Això podria " "afectar altres fitxers que els usen." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.message" msgstr "Actualitza els components en una biblioteca compartida" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.accept" msgstr "Actualitza" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.cancel" msgstr "Cancel·la" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.hint" msgstr "" "Esteu a punt d'actualitzar un component d'una biblioteca compartida. Això " "pot afectar altres fitxers que l'usen." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.message" msgstr "Actualitza un component en una biblioteca compartida" @@ -1785,9 +1828,6 @@ msgstr "Guia d'ús" msgid "onboarding-v2.welcome.title" msgstr "Us donem la benvinguda a Penpot!" -msgid "onboarding.choice.team-up.create-team" -msgstr "Nom de l'equip" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "Quan poseu un nom a l'equip, podreu convidar persones a unir-s'hi." @@ -1797,15 +1837,6 @@ msgstr "Introduïu el nom de l'equip" msgid "onboarding.choice.team-up.invite-members" msgstr "Convida membres" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Crea l'equip ara i convida membres en un altre moment" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Crea l'equip i envia les invitacions" - -msgid "onboarding.contrib.desc2.1" -msgstr "Podeu accedir al" - msgid "onboarding.newsletter.accept" msgstr "Sí, subscriu-m'hi" @@ -1820,15 +1851,6 @@ msgstr "Política de privacitat." msgid "onboarding.newsletter.title" msgstr "Voleu rebre les novetats de Penpot?" -msgid "onboarding.slide.0.title" -msgstr "Biblioteques de disseny, estils i components" - -msgid "onboarding.slide.1.title" -msgstr "Doneu vida als vostres dissenys amb interaccions" - -msgid "onboarding.slide.3.alt" -msgstr "Lliurament i codi baix" - msgid "onboarding.team-modal.create-team" msgstr "Crea un equip" @@ -1845,7 +1867,12 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "Vés a l'inici de sessió" -#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "Mixt" @@ -2215,9 +2242,6 @@ msgstr "Activa/desactiva el mode de concentració" msgid "shortcuts.toggle-fullscreen" msgstr "Activa/desactiva la pantalla completa" -msgid "shortcuts.toggle-grid" -msgstr "Mostra/Amaga la graella" - msgid "shortcuts.toggle-history" msgstr "Mostra/Amaga l'historial" @@ -2233,15 +2257,6 @@ msgstr "Bloqueja les proporcions" msgid "shortcuts.toggle-rules" msgstr "Mostra/Amaga les regles" -msgid "shortcuts.toggle-scale-text" -msgstr "Activa/desactiva l'escalat de text" - -msgid "shortcuts.toggle-snap-grid" -msgstr "Ajusta a la graella" - -msgid "shortcuts.toggle-snap-guide" -msgstr "Ajusta a les guies" - msgid "shortcuts.toggle-textpalette" msgstr "Mostra/amaga la paleta de text" @@ -2368,10 +2383,6 @@ msgstr "Interaccions (%s)" msgid "viewer.header.share.copy-link" msgstr "Copia l'enllaç" -#: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.subtitle" -msgstr "Qualsevol persona amb l'enllaç hi tindrà accés" - #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.show-interactions" msgstr "Mostra les interaccions" @@ -2424,11 +2435,13 @@ msgstr "Recursos" msgid "workspace.assets.box-filter-all" msgstr "Tots els recursos" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.colors" msgstr "Colors" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.components" msgstr "Components" @@ -2442,19 +2455,24 @@ msgstr "" "Els elements s'anomenaran automàticament com a \"nom del grup / nom de " "l'element\"" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "Elimina" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.duplicate" msgstr "Duplica" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" msgstr "Edita" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.graphics" msgstr "Gràfics" @@ -2474,7 +2492,9 @@ msgstr "Biblioteques" msgid "workspace.assets.not-found" msgstr "No s'han trobat recursos" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename" msgstr "Canvia el nom" @@ -2492,11 +2512,8 @@ msgid_plural "workspace.assets.selected-count" msgstr[0] "%s element seleccionat" msgstr[1] "%s elements seleccionats" +#: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "COMPARTIT" - -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "Tipografies" @@ -2524,7 +2541,9 @@ msgstr "Espaiat de la lletra" msgid "workspace.assets.typography.line-height" msgstr "Alçada de la línia" -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/inspect/attributes/text.cljs, src/app/main/ui/inspect/attributes/text.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs msgid "workspace.assets.typography.sample" msgstr "Ag" @@ -2548,11 +2567,13 @@ msgstr "Enfocament actiu" msgid "workspace.focus.selection" msgstr "Selecció" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "Degradat lineal" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.radial" msgstr "Degradat radial" @@ -2564,10 +2585,6 @@ msgstr "Desactiva l'alineació dinàmica" msgid "workspace.header.menu.disable-scale-text" msgstr "Desactiva l'escalat del text" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "No ajustis a la quadrícula" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "No ajustis a les guies" @@ -2583,10 +2600,6 @@ msgstr "Activa l'alineació dinàmica" msgid "workspace.header.menu.enable-scale-text" msgstr "Activa l'escalat del text" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Ajusta a la quadrícula" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "Ajusta a les guies" @@ -2598,10 +2611,6 @@ msgstr "Ajusta als píxels" msgid "workspace.header.menu.hide-artboard-names" msgstr "Amaga els noms dels taulers" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Amaga la quadrícula" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Amaga la paleta de colors" @@ -2645,10 +2654,6 @@ msgstr "Selecciona-ho tot" msgid "workspace.header.menu.show-artboard-names" msgstr "Mostra els noms dels taulers" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Mostra la quadrícula" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Mostra la paleta de colors" @@ -2716,7 +2721,8 @@ msgstr "Afegeix" msgid "workspace.libraries.colors" msgstr "%s colors" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" msgstr "Biblioteca del fitxer" @@ -2724,7 +2730,8 @@ msgstr "Biblioteca del fitxer" msgid "workspace.libraries.colors.hsv" msgstr "HSV" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "Colors recents" @@ -2804,9 +2811,6 @@ msgstr "Actualitza" msgid "workspace.libraries.updates" msgstr "ACTUALITZACIONS" -msgid "workspace.library.store" -msgstr "Predeterminades" - #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.add-interaction" msgstr "Feu clic en el botó de + per a afegir interaccions." @@ -2878,15 +2882,18 @@ msgstr "Superior i inferior" msgid "workspace.options.design" msgstr "Disseny" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export" msgstr "Exporta" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-multiple" msgstr "Exporta la selecció" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-object" msgstr "Exporta 1 element" @@ -2894,19 +2901,23 @@ msgstr "Exporta 1 element" msgid "workspace.options.export.suffix" msgstr "Sufix" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-complete" msgstr "Exportació completa" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.exporting-object" msgstr "S'està exportant…" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-error" msgstr "Exportació fallida" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-slow" msgstr "Exportació inesperadament lenta" @@ -3387,7 +3398,8 @@ msgstr "Més llibreries de colors" msgid "workspace.options.opacity" msgstr "Opacitat" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Posició" @@ -3399,12 +3411,12 @@ msgid "workspace.options.radius" msgstr "Radi" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Tots els cantons" +msgid "workspace.options.radius-bottom-left" +msgstr "Inferior esquerra" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Cantons individuals" +msgid "workspace.options.radius-bottom-right" +msgstr "Inferior dreta" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3415,17 +3427,18 @@ msgid "workspace.options.radius-top-right" msgstr "Superior dreta" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Inferior esquerra" +msgid "workspace.options.radius.all-corners" +msgstr "Tots els cantons" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Inferior dreta" +msgid "workspace.options.radius.single-corners" +msgstr "Cantons individuals" msgid "workspace.options.recent-fonts" msgstr "Recent" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" msgstr "Torna-ho a provar" @@ -3500,7 +3513,8 @@ msgstr "Mostra en l'exportació" msgid "workspace.options.show-in-viewer" msgstr "Mostra al visor" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.size" msgstr "Mida" @@ -3582,26 +3596,10 @@ msgstr "Sòlid" msgid "workspace.options.text-options.align-bottom" msgstr "Alinea a baix" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Alinea el centre (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Justifica (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Alinea a l'esquerra (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Alinea al centre" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Alinea a la dreta (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Alinea a dalt" @@ -3638,7 +3636,8 @@ msgstr "Alçada de la línia" msgid "workspace.options.text-options.lowercase" msgstr "Minúscules" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.none" msgstr "Cap" @@ -3646,6 +3645,22 @@ msgstr "Cap" msgid "workspace.options.text-options.strikethrough" msgstr "Ratllat (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Alinea el centre (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Justifica (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Alinea a l'esquerra (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Alinea a la dreta (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Text" @@ -3747,11 +3762,15 @@ msgstr "Elimina" msgid "workspace.shape.menu.delete-flow-start" msgstr "Elimina l'inici del flux" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Desconnecta la instància" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instances-in-bulk" msgstr "Desenganxa les instàncies" @@ -3792,7 +3811,8 @@ msgstr "Porta-ho endavant" msgid "workspace.shape.menu.front" msgstr "Porta-ho a primer pla" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.go-main" msgstr "Vés al fitxer del component principal" @@ -3814,18 +3834,22 @@ msgstr "Intersecció" msgid "workspace.shape.menu.lock" msgstr "Bloca" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.mask" msgstr "Màscara" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.paste" msgstr "Enganxa" msgid "workspace.shape.menu.path" msgstr "Camí" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "Desfés les modificacions" @@ -3837,7 +3861,8 @@ msgstr "Selecciona la capa" msgid "workspace.shape.menu.show" msgstr "Mostra" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-main" msgstr "Vés al component principal" @@ -3865,11 +3890,15 @@ msgstr "Desbloca" msgid "workspace.shape.menu.unmask" msgstr "Desemmascara" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-components-in-bulk" msgstr "Actualitza els components principals" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-main" msgstr "Actualitza el component principal" @@ -3905,7 +3934,8 @@ msgstr "Formes" msgid "workspace.sidebar.layers.texts" msgstr "Textos" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/inspect/attributes/svg.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, +#: src/app/main/ui/inspect/attributes/svg.cljs msgid "workspace.sidebar.options.svg-attrs.title" msgstr "Atributs SVG importats" @@ -4098,3 +4128,51 @@ msgstr "Actualitza" msgid "workspace.viewport.click-to-close-path" msgstr "Feu clic per a tancar el camí" + +#~ msgid "dashboard.newsletter-title" +#~ msgstr "Subscripció al butlletí" + +#~ msgid "feedback.chat-subtitle" +#~ msgstr "Voleu parlar? Xategeu amb nosaltres a Gitter" + +#~ msgid "inspect.attributes.shadow.shorthand.offset-x" +#~ msgstr "X" + +#~ msgid "labels.images" +#~ msgstr "Imatges" + +#~ msgid "labels.skip" +#~ msgstr "Omet" + +#~ msgid "onboarding.contrib.alt" +#~ msgstr "Codi obert" + +#~ msgid "onboarding.contrib.link" +#~ msgstr "projecte a github" + +#~ msgid "onboarding.slide.0.desc1" +#~ msgstr "" +#~ "Creeu interfícies d'usuari boniques en col·laboració amb tots els membres " +#~ "de l'equip." + +#~ msgid "onboarding.slide.1.desc1" +#~ msgstr "Creeu interaccions enriquides per a imitar el comportament del producte." + +#~ msgid "onboarding.slide.2.desc1" +#~ msgstr "" +#~ "Tot l'equip treballant simultàniament amb disseny en temps real i " +#~ "comentaris, idees i opinions sobre els dissenys de forma centralitzada." + +#~ msgid "onboarding.slide.3.desc2" +#~ msgstr "" +#~ "Obteniu i proporcioneu especificacions de codi d'etiquetatge (SVG, HTML) o " +#~ "d'estils (CSS, Less, Stylus...)." + +#~ msgid "viewer.header.share.placeholder" +#~ msgstr "L'enllaç compartit apareixerà aquí" + +#~ msgid "workspace.library.libraries" +#~ msgstr "Biblioteques" + +#~ msgid "workspace.options.blur-options.layer-blur" +#~ msgstr "Capa" diff --git a/frontend/translations/cs.po b/frontend/translations/cs.po index 2b758843c4..b2f2225f4d 100644 --- a/frontend/translations/cs.po +++ b/frontend/translations/cs.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2023-07-01 12:52+0000\n" +"PO-Revision-Date: 2024-01-23 15:02+0000\n" "Last-Translator: \"Amerey.eu\" \n" "Language-Team: Czech \n" @@ -8,8 +8,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -"X-Generator: Weblate 5.0-dev\n" +"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n" +"X-Generator: Weblate 5.4-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -39,7 +39,8 @@ msgstr "" "Toto je DEMO služba, NEPOUŽÍVEJTE ji pro skutečnou práci, projekty budou " "pravidelně mazány." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" msgstr "Email" @@ -264,7 +265,8 @@ msgstr "Začít prohlídku" msgid "dasboard.walkthrough-hero.title" msgstr "Průvodce rozhraním" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Přidat jako sdílenou knihovnu" @@ -294,7 +296,8 @@ msgstr "Stáhnout soubor Penpot (.penpot)" msgid "dashboard.download-standard-file" msgstr "Stáhnout standardní soubor (.svg + .json)" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate" msgstr "Duplikovat" @@ -303,12 +306,11 @@ msgid "dashboard.duplicate-multi" msgstr "Duplikovat %s soubory" #: src/app/main/ui/dashboard/grid.cljs -#, markdown msgid "dashboard.empty-placeholder-drafts" msgstr "" "Zde se zobrazí soubory přidané do knihoven. Zkuste své soubory sdílet nebo " -"je přidat z našich [Libraries & templates](https://penpot.app/libraries-" -"templates.html)." +"je přidat z našich [Libraries & " +"templates](https://penpot.app/libraries-templates.html)." msgid "dashboard.export-binary-multi" msgstr "Stáhnout soubory %s Penpot (.penpot)" @@ -404,7 +406,6 @@ msgstr[0] "Přidáno 1 písmo" msgstr[1] "%s písma přidány" msgstr[2] "%s písem přidáno" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "Jakékoli webové písmo, které sem nahrajete, bude přidáno do seznamu rodin " @@ -413,7 +414,6 @@ msgstr "" "Můžete nahrávat písma v následujících formátech: **TTF, OTF a WOFF** (bude " "potřeba pouze jeden)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "Měli byste nahrávat pouze písma, která vlastníte nebo máte licenci k " @@ -426,6 +426,15 @@ msgstr "" msgid "dashboard.fonts.upload-all" msgstr "Nahrát vše" +msgid "dashboard.fonts.warning-text" +msgstr "" +"Zjistili jsme možný problém ve vašich písmech související s vertikálními " +"metrikami pro různé operační systémy. Chcete-li to zkontrolovat, můžete " +"použít služby vertikálních metrik písem, jako je [toto] " +"(https://vertical-metrics.netlify.app/). Kromě toho doporučujeme použít " +"[Transfonter](https://transfonter.org/) ke generování webových písem a " +"opravě chyb. " + msgid "dashboard.import" msgstr "Importovat Penpot soubory" @@ -435,6 +444,9 @@ msgstr "Ups! Tento soubor se nepodařilo importovat" msgid "dashboard.import.import-error" msgstr "Při importu souboru došlo k problému. Soubor nebyl importován." +msgid "dashboard.import.import-message" +msgstr "Soubory %s byly úspěšně importovány." + msgid "dashboard.import.import-warning" msgstr "Některé soubory obsahovaly neplatné objekty, které byly odstraněny." @@ -463,7 +475,8 @@ msgstr "Nahrávání souboru: %s" msgid "dashboard.invite-profile" msgstr "Pozvat do týmu" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" msgstr "Odejít z týmu" @@ -487,7 +500,8 @@ msgstr "načítání vašich souborů …" msgid "dashboard.loading-fonts" msgstr "načítání vašich písem …" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "Přesunout do" @@ -499,7 +513,8 @@ msgstr "Přesunout soubory %s do" msgid "dashboard.move-to-other-team" msgstr "Přesunout do jiného týmu" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" msgstr "+ Nový soubor" @@ -562,7 +577,8 @@ msgstr "Projekty" msgid "dashboard.remove-account" msgstr "Chcete odstranit svůj účet?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" msgstr "Odstranit jako sdílenou knihovnu" @@ -590,15 +606,24 @@ msgstr "Vyberte téma" msgid "dashboard.show-all-files" msgstr "Zobrazit všechny soubory" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgstr "Váš soubor byl úspěšně smazán" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-delete-project" msgstr "Váš projekt byl úspěšně smazán" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgstr "Váš soubor byl úspěšně duplikován" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-duplicate-project" msgstr "Váš projekt byl úspěšně duplikován" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-file" msgstr "Váš soubor byl úspěšně přesunut" @@ -634,14 +659,46 @@ msgstr "Výsledky vyhledávání" msgid "dashboard.type-something" msgstr "Zadejte výraz pro hledání" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.unpublish-shared" msgstr "Zrušit zveřejnění knihovny" -#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Aktualizovat nastavení" +msgid "dashboard.webhooks.active" +msgstr "Je aktivní" + +msgid "dashboard.webhooks.active.explain" +msgstr "Když je tento webhook spuštěn, budou doručeny podrobnosti o události" + +msgid "dashboard.webhooks.content-type" +msgstr "Typ obsahu" + +msgid "dashboard.webhooks.create" +msgstr "Vytvořit webhook" + +msgid "dashboard.webhooks.create.success" +msgstr "Webhook byl úspěšně vytvořen." + +msgid "dashboard.webhooks.description" +msgstr "" +"Webhooky jsou jednoduchým způsobem, jak umožnit jiným webům a aplikacím, " +"aby byly upozorňovány na určité události v Penpotu. Na každou z vámi " +"poskytnutých adres URL odešleme požadavek POST." + +msgid "dashboard.webhooks.empty.add-one" +msgstr "Chcete-li webhook přidat, stiskněte tlačítko „Přidat webhook“." + +msgid "dashboard.webhooks.empty.no-webhooks" +msgstr "Dosud nebyly vytvořeny žádné webhooky." + +msgid "dashboard.webhooks.update.success" +msgstr "Webhook byl úspěšně aktualizován." + #: src/app/main/ui/settings.cljs msgid "dashboard.your-account-title" msgstr "Váš účet" @@ -654,7 +711,11 @@ msgstr "E-mail" msgid "dashboard.your-name" msgstr "Vaše jméno" -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" msgstr "Váš Penpot" @@ -699,7 +760,8 @@ msgstr "Písma %s se nepodařilo načíst" msgid "errors.clipboard-not-implemented" msgstr "Váš prohlížeč tuto operaci nedokáže provést" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/change_email.cljs msgid "errors.email-already-exists" msgstr "Již použitá e-mailová adresa" @@ -710,10 +772,18 @@ msgstr "E-mail byl již ověřen." msgid "errors.email-as-password" msgstr "Jako heslo nelze použít váš e-mail" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" msgstr "E-mail «%s» má mnoho trvalých zpráv o nedoručitelnosti." +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, +#: src/app/main/ui/auth/recovery_request.cljs +msgid "errors.email-invalid" +msgstr "Zadejte prosím platný email" + #: src/app/main/ui/settings/change_email.cljs msgid "errors.email-invalid-confirmation" msgstr "Potvrzovací e-mail se musí shodovat" @@ -721,7 +791,18 @@ msgstr "Potvrzovací e-mail se musí shodovat" msgid "errors.email-spam-or-permanent-bounces" msgstr "E-mail «%s» byl nahlášen jako spam nebo byl trvale nedostupný." -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/errors.cljs +msgid "errors.feature-mismatch" +msgstr "" +"Vypadá to, že otevíráte soubor, který má povolenou funkci '%s', ale aktuální " +"verze penpotu ji nepodporuje nebo je deaktivovaná." + +#: src/app/main/errors.cljs +msgid "errors.feature-not-supported" +msgstr "Funkce '%s' není podporována." + +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.generic" msgstr "Něco se pokazilo." @@ -740,6 +821,10 @@ msgstr "Tato pozvánka byla pravděpodobně zrušena nebo vypršela její platno msgid "errors.ldap-disabled" msgstr "Ověřování LDAP je vypnuto." +#: src/app/main/errors.cljs +msgid "errors.max-quote-reached" +msgstr "Dosáhli jste '%s' kvóty. Kontaktujte podporu." + #: src/app/main/data/workspace/persistence.cljs msgid "errors.media-too-large" msgstr "Obrázek je příliš velký na to, aby mohl být vložen." @@ -748,7 +833,7 @@ msgstr "Obrázek je příliš velký na to, aby mohl být vložen." msgid "errors.media-type-mismatch" msgstr "Zdá se, že obsah obrázku neodpovídá příponě souboru." -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" msgstr "Zdá se, že toto není platný obrázek." @@ -769,7 +854,9 @@ msgstr "Heslo by mělo mít nejméně 8 znaků" msgid "errors.profile-blocked" msgstr "Profil je zablokován" -#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.profile-is-muted" msgstr "Váš profil má ztlumené e-maily (zprávy o spamu nebo vysoká nedoručitelnost)." @@ -788,7 +875,9 @@ msgstr "Člen, kterého se pokoušíte přiřadit, neexistuje." msgid "errors.team-leave.owner-cant-leave" msgstr "Vlastník nemůže opustit tým, musíte přeřadit roli vlastníka." -#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +#: src/app/main/data/media.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs msgid "errors.unexpected-error" msgstr "Došlo k neočekávané chybě." @@ -796,6 +885,27 @@ msgstr "Došlo k neočekávané chybě." msgid "errors.unexpected-token" msgstr "Neznámý token" +msgid "errors.webhooks.connection" +msgstr "Chyba připojení, adresa URL není dostupná" + +msgid "errors.webhooks.invalid-uri" +msgstr "Adresa URL neprošla ověřením." + +msgid "errors.webhooks.last-delivery" +msgstr "Poslední dodávka nebyla úspěšná." + +msgid "errors.webhooks.ssl-validation" +msgstr "Chyba při ověřování SSL." + +msgid "errors.webhooks.timeout" +msgstr "Timeout" + +msgid "errors.webhooks.unexpected" +msgstr "Při ověřování došlo k neočekávané chybě" + +msgid "errors.webhooks.unexpected-status" +msgstr "Neočekávaný stav %s" + #: src/app/main/ui/auth/login.cljs msgid "errors.wrong-credentials" msgstr "Uživatelské jméno nebo heslo se zdá být chybné." @@ -838,7 +948,7 @@ msgstr "E-mail" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Přejít na Twitter" +msgstr "Přejít na X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -846,37 +956,229 @@ msgstr "Zde vám pomůžeme s vašimi technickými dotazy." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Účet podpory na Twitteru" +msgstr "Účet podpory na Xu" #: src/app/main/ui/settings/password.cljs msgid "generic.error" msgstr "Došlo k chybě" -#: src/app/main/ui/handoff/attributes/image.cljs -msgid "handoff.attributes.image.height" +#: src/app/main/ui/inspect/attributes/blur.cljs +msgid "inspect.attributes.blur" +msgstr "Rozostření" + +#: src/app/main/ui/inspect/attributes/blur.cljs +msgid "inspect.attributes.blur.value" +msgstr "Hodnota" + +#: src/app/main/ui/inspect/attributes/common.cljs +msgid "inspect.attributes.color.hex" +msgstr "HEX" + +#: src/app/main/ui/inspect/attributes/common.cljs +msgid "inspect.attributes.color.hsla" +msgstr "HSLA" + +#: src/app/main/ui/inspect/attributes/common.cljs +msgid "inspect.attributes.color.rgba" +msgstr "RGBA" + +#: src/app/main/ui/inspect/attributes/fill.cljs +msgid "inspect.attributes.fill" +msgstr "Výplň" + +#: src/app/main/ui/inspect/attributes/image.cljs +msgid "inspect.attributes.image.download" +msgstr "Stáhnout zdrojový obrázek" + +#: src/app/main/ui/inspect/attributes/image.cljs +msgid "inspect.attributes.image.height" msgstr "Výška" -#: src/app/main/ui/handoff/attributes/layout.cljs -msgid "handoff.attributes.layout.width" +#: src/app/main/ui/inspect/attributes/image.cljs +msgid "inspect.attributes.image.width" msgstr "Šířka" -#, permanent -msgid "handoff.attributes.stroke.alignment.inner" +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout" +msgstr "Rozložení" + +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.height" +msgstr "Výška" + +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.left" +msgstr "Vlevo" + +#: src/app/main/ui/inspect/attributes/layout.cljs, +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.radius" +msgstr "Poloměr" + +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.rotation" +msgstr "Otáčení" + +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.top" +msgstr "Nahoře" + +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.width" +msgstr "Šířka" + +#: src/app/main/ui/inspect/attributes/shadow.cljs +msgid "inspect.attributes.shadow" +msgstr "Stín" + +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.size" +msgstr "Velikost a poloha" + +#: src/app/main/ui/inspect/attributes/stroke.cljs +msgid "inspect.attributes.stroke" +msgstr "Tah" + +msgid "inspect.attributes.stroke.alignment.center" +msgstr "Střed" + +msgid "inspect.attributes.stroke.alignment.inner" msgstr "Uvnitř" -#: src/app/main/ui/handoff/attributes/text.cljs -msgid "handoff.attributes.typography.font-family" +msgid "inspect.attributes.stroke.alignment.outer" +msgstr "Venku" + +msgid "inspect.attributes.stroke.style.dotted" +msgstr "Tečkovaná" + +msgid "inspect.attributes.stroke.style.mixed" +msgstr "Smíšená" + +msgid "inspect.attributes.stroke.style.none" +msgstr "Žádná" + +msgid "inspect.attributes.stroke.style.solid" +msgstr "Plná" + +#: src/app/main/ui/inspect/attributes/stroke.cljs +msgid "inspect.attributes.stroke.width" +msgstr "Šířka" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography" +msgstr "Typografie" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-family" msgstr "Rodina písem" -msgid "handoff.attributes.typography.text-decoration.underline" -msgstr "Podtrhnout" +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-size" +msgstr "Velikost písma" -msgid "handoff.tabs.code.selected.component" -msgstr "Komponenta" +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-style" +msgstr "Styl písma" -msgid "handoff.tabs.code.selected.rect" +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-weight" +msgstr "Tloušťka písma" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.letter-spacing" +msgstr "Mezery mezi písmeny" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.line-height" +msgstr "Výška řádku" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.text-decoration" +msgstr "Dekorace textu" + +msgid "inspect.attributes.typography.text-decoration.none" +msgstr "Žádná" + +msgid "inspect.attributes.typography.text-decoration.strikethrough" +msgstr "Přeškrtnutí" + +msgid "inspect.attributes.typography.text-decoration.underline" +msgstr "Podtržení" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.text-transform" +msgstr "Transformace textu" + +msgid "inspect.attributes.typography.text-transform.lowercase" +msgstr "Malá písmena" + +msgid "inspect.attributes.typography.text-transform.none" +msgstr "Žádná" + +msgid "inspect.attributes.typography.text-transform.titlecase" +msgstr "První písmena velká" + +msgid "inspect.attributes.typography.text-transform.uppercase" +msgstr "Velká písmena" + +msgid "inspect.empty.help" +msgstr "" +"Pokud se chcete dozvědět více o inspektorovi designu, navštivte centrum " +"nápovědy společnosti Penpot" + +msgid "inspect.empty.more-info" +msgstr "Více informací o inspektorovi" + +msgid "inspect.empty.select" +msgstr "" +"Vyberte tvar, tabuli nebo skupinu, abyste mohli zkontrolovat jejich " +"vlastnosti a kód" + +#: src/app/main/ui/inspect/right_sidebar.cljs +msgid "inspect.tabs.code" +msgstr "Kód" + +msgid "inspect.tabs.code.selected.circle" +msgstr "Kruh" + +msgid "inspect.tabs.code.selected.component" +msgstr "Komponent" + +msgid "inspect.tabs.code.selected.curve" +msgstr "Křivka" + +msgid "inspect.tabs.code.selected.frame" +msgstr "Tabule" + +msgid "inspect.tabs.code.selected.group" +msgstr "Skupina" + +msgid "inspect.tabs.code.selected.image" +msgstr "Obrázek" + +msgid "inspect.tabs.code.selected.mask" +msgstr "Maska" + +#: src/app/main/ui/inspect/right_sidebar.cljs +msgid "inspect.tabs.code.selected.multiple" +msgstr "%s vybráno" + +msgid "inspect.tabs.code.selected.path" +msgstr "Cesta" + +msgid "inspect.tabs.code.selected.rect" msgstr "Obdélník" +msgid "inspect.tabs.code.selected.svg-raw" +msgstr "SVG" + +msgid "inspect.tabs.code.selected.text" +msgstr "Text" + +#: src/app/main/ui/inspect/right_sidebar.cljs +msgid "inspect.tabs.info" +msgstr "Informace" + #: src/app/main/ui/workspace/header.cljs msgid "label.shortcuts" msgstr "Zkratky" @@ -884,10 +1186,13 @@ msgstr "Zkratky" msgid "labels.accept" msgstr "Přijmout" +msgid "labels.active" +msgstr "Aktivní" + msgid "labels.add-custom-font" msgstr "Přidat vlastní písmo" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "Správce" @@ -939,11 +1244,16 @@ msgstr "Pokračovat s" msgid "labels.continue-with-penpot" msgstr "Můžete pokračovat s účtem Penpot" +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.copy-invitation-link" +msgstr "Kopírovat odkaz" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "labels.create" msgstr "Vytvořit" -#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team" msgstr "Vytvořit nový tým" @@ -958,7 +1268,8 @@ msgstr "Vlastní písma" msgid "labels.dashboard" msgstr "Tabule" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "Smazat" @@ -978,7 +1289,10 @@ msgstr "Smazat pozvánku" msgid "labels.delete-multi-files" msgstr "Smazat soubory %s" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/files.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.drafts" msgstr "Pracovní verze" @@ -989,7 +1303,7 @@ msgstr "Upravit" msgid "labels.edit-file" msgstr "Upravit soubor" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "Editor" @@ -1024,7 +1338,9 @@ msgstr "Písma" msgid "labels.github-repo" msgstr "Úložiště Github" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "Dát zpětnou vazbu" @@ -1039,6 +1355,9 @@ msgstr "Centrum nápovědy" msgid "labels.hide-resolved-comments" msgstr "Skrýt vyřešené komentáře" +msgid "labels.inactive" +msgstr "Neaktivní" + msgid "labels.installed-fonts" msgstr "Nainstalovaná písma" @@ -1052,7 +1371,8 @@ msgstr "" msgid "labels.internal-error.main-message" msgstr "Interní chyba" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.invitations" msgstr "Pozvánky" @@ -1071,11 +1391,11 @@ msgstr "Přihlásit se ne registrovat" msgid "labels.logout" msgstr "Odhlásit se" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.member" msgstr "Člen" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "Členové" @@ -1083,7 +1403,8 @@ msgstr "Členové" msgid "labels.new-password" msgstr "Nové heslo" -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +#: src/app/main/ui/workspace/comments.cljs, +#: src/app/main/ui/dashboard/comments.cljs msgid "labels.no-comments-available" msgstr "Vše je aktuální! Zde se zobrazí upozornění na nové komentáře." @@ -1092,7 +1413,6 @@ msgid "labels.no-invitations" msgstr "Nejsou žádné pozvánky." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "" "Chcete-li do tohoto týmu pozvat další členy, stiskněte tlačítko „Pozvat do " @@ -1141,7 +1461,8 @@ msgstr "nebo" msgid "labels.owner" msgstr "Majitel" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.password" msgstr "Heslo" @@ -1161,7 +1482,12 @@ msgstr "Projekty" msgid "labels.release-notes" msgstr "Poznámky k verzi" -#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/workspace.cljs +msgid "labels.reload-file" +msgstr "Znovu načíst soubor" + +#: src/app/main/ui/workspace/libraries.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" msgstr "Odstranit" @@ -1169,7 +1495,9 @@ msgstr "Odstranit" msgid "labels.remove-member" msgstr "Odebrat člena" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.rename" msgstr "Přejmenovat" @@ -1181,7 +1509,7 @@ msgstr "Přejmenovat tým" msgid "labels.resend-invitation" msgstr "Znovu poslat pozvánku" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Zkusit znovu" @@ -1211,7 +1539,8 @@ msgstr "Provádíme plánovanou údržbu našich systémů." msgid "labels.service-unavailable.main-message" msgstr "Služba je nedostupná" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.settings" msgstr "Nastavení" @@ -1241,6 +1570,10 @@ msgstr "Status" msgid "labels.tutorials" msgstr "Tutoriály" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.unpublish-multi-files" +msgstr "Zrušit publikování %s souborů" + #: src/app/main/ui/settings/profile.cljs msgid "labels.update" msgstr "Aktualizovat" @@ -1258,15 +1591,21 @@ msgstr "Nahrát vlastní písma" msgid "labels.uploading" msgstr "Nahrávání…" +msgid "labels.view-only" +msgstr "POUZE ZOBRAZIT" + #: src/app/main/ui/dashboard/team.cljs msgid "labels.viewer" msgstr "Prohlížeč" +msgid "labels.webhooks" +msgstr "Webhooks" + #: src/app/main/ui/comments.cljs msgid "labels.write-new-comment" msgstr "Napsat nový komentář" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.you" msgstr "(vy)" @@ -1274,21 +1613,24 @@ msgstr "(vy)" msgid "labels.your-account" msgstr "Váš účet" -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "Načítání obrázku…" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" msgstr "Přidat jako sdílenou knihovnu" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.hint" msgstr "" "Po přidání jako Sdílené knihovny budou položky této knihovny k dispozici " "pro použití se zbytkem vašich souborů." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.message" msgstr "Přidat „%s“ jako Sdílenou knihovnu" @@ -1318,6 +1660,18 @@ msgstr "Změnit e-mail" msgid "modals.change-email.title" msgstr "Změňte svůj e-mail" +msgid "modals.create-webhook.submit-label" +msgstr "Vytvořit webhook" + +msgid "modals.create-webhook.title" +msgstr "Vytvořit webhook" + +msgid "modals.create-webhook.url.label" +msgstr "Adresa URL datové části" + +msgid "modals.create-webhook.url.placeholder" +msgstr "https://example.com/postreceive" + #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.cancel" msgstr "Zrušit a ponechat si můj účet" @@ -1406,28 +1760,24 @@ msgstr "Opravdu chcete smazat tento projekt?" msgid "modals.delete-project-confirm.title" msgstr "Smazat projekt" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.accept" msgid_plural "modals.delete-shared-confirm.accept" msgstr[0] "Smazat soubor" msgstr[1] "Smazat soubory" msgstr[2] "Smazat soubory" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "Opravdu chcete smazat tento soubor?" msgstr[1] "Opravdu chcete smazat tyto soubory?" msgstr[2] "Opravdu chcete smazat tyto soubory?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Tento soubor obsahuje knihovny, které se v tomto souboru používají:" -msgstr[1] "Tento soubor obsahuje knihovny, které se používají v těchto souborech:" -msgstr[2] "Tento soubor obsahuje knihovny, které se používají v těchto souborech:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" msgstr[0] "Mazání souboru" @@ -1460,6 +1810,21 @@ msgstr "Opravdu chcete tohoto člena smazat z týmu?" msgid "modals.delete-team-member-confirm.title" msgstr "Smazat člena týmu" +msgid "modals.delete-webhook.accept" +msgstr "Smazat webhook" + +msgid "modals.delete-webhook.message" +msgstr "Opravdu chcete tento webhook smazat?" + +msgid "modals.delete-webhook.title" +msgstr "Mazání webhooku" + +msgid "modals.edit-webhook.submit-label" +msgstr "Upravit webhook" + +msgid "modals.edit-webhook.title" +msgstr "Upravit webhook" + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-member-confirm.accept" msgstr "Poslat pozvánku" @@ -1467,6 +1832,11 @@ msgstr "Poslat pozvánku" msgid "modals.invite-member.emails" msgstr "E-maily oddělené čárkou" +msgid "modals.invite-member.repeated-invitation" +msgstr "" +"Některé e-maily jsou od současných členů týmu. Jejich pozvánky nebudou " +"odeslány." + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-team-member.title" msgstr "Pozvat členy do týmu" @@ -1540,17 +1910,20 @@ msgstr "" msgid "modals.promote-owner-confirm.title" msgstr "Nový majitel týmu" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" msgstr "Odebrat jako sdílenou knihovnu" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.hint" msgstr "" "Po odstranění jako Sdílené knihovny přestane být knihovna tohoto souboru k " "dispozici pro použití se zbytkem vašich souborů." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.message" msgstr "Odebrat „%s“ jako sdílenou knihovnu" @@ -1558,68 +1931,58 @@ msgstr "Odebrat „%s“ jako sdílenou knihovnu" msgid "modals.small-nudge" msgstr "Malé posunutí" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Pokud zrušíte jeho publikování, tyto podklady již nebudou dostupné z jiných " -"souborů. Aktiva, která již byla použita, zůstanou v tomto souboru (žádný " -"návrh nebude porušen!)." -msgstr[1] "" -"Pokud jejich publikování zrušíte, tyto podklady již nebudou dostupné z " -"jiných souborů. Aktiva, která již byla použita, zůstanou v tomto souboru (" -"žádný návrh nebude porušen!)." -msgstr[2] "" -"Pokud jejich publikování zrušíte, tyto podklady již nebudou dostupné z " -"jiných souborů. Aktiva, která již byla použita, zůstanou v tomto souboru (" -"žádný návrh nebude porušen!)." +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.accept" +msgstr "Zrušit publikování" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "Opravdu chcete zrušit publikování této knihovny?" msgstr[1] "Opravdu chcete zrušit publikování těchto knihoven?" msgstr[2] "Opravdu chcete zrušit publikování těchto knihoven?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Používá se v tomto souboru:" -msgstr[1] "Používá se v těchto souborech:" -msgstr[2] "Používá se v těchto souborech:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" msgid_plural "modals.unpublish-shared-confirm.title" msgstr[0] "Zrušit publikování knihovny" msgstr[1] "Zrušit publikování knihoven" msgstr[2] "Zrušit publikování knihoven" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.hint" msgstr "" "Chystáte se aktualizovat komponenty ve sdílené knihovně. To může ovlivnit " "další soubory, které jej používají." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.message" msgstr "Aktualizujte komponenty ve sdílené knihovně" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.accept" msgstr "Aktualizovat" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.cancel" msgstr "Zrušit" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.hint" msgstr "" "Chystáte se aktualizovat komponentu ve sdílené knihovně. To může ovlivnit " "další soubory, které ji používají." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.message" msgstr "Aktualizovat komponentu ve sdílené knihovně" @@ -1627,6 +1990,10 @@ msgstr "Aktualizovat komponentu ve sdílené knihovně" msgid "notifications.invitation-email-sent" msgstr "Pozvánka byla úspěšně odeslána" +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-link-copied" +msgstr "Odkaz na pozvánku zkopírován" + #: src/app/main/ui/settings/delete_account.cljs msgid "notifications.profile-deletion-not-allowed" msgstr "Nemůžete smazat svůj profil. Než budete pokračovat, znovu přiřaďte své týmy." @@ -1704,12 +2071,6 @@ msgstr "Průvodce přispíváním" msgid "onboarding-v2.welcome.title" msgstr "Vítejte v Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "Vytvořte tým později" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Název vašeho týmu" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "Po pojmenování svého týmu budete moci pozvat lidi, aby se přidali." @@ -1724,12 +2085,6 @@ msgstr "" "Nezapomeňte zahrnout všechny. Vývojáře, designéry, manažéry... rozmanitost " "se počítá :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Vytvořte tým a pozvěte později" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Vytvořte tým a odešlete pozvánky" - msgid "onboarding.choice.team-up.roles" msgstr "Pozvat s rolí:" @@ -1745,9 +2100,6 @@ msgstr "Zásady ochrany osobních údajů." msgid "onboarding.newsletter.title" msgstr "Chcete dostávat novinky Penpot?" -msgid "onboarding.slide.1.title" -msgstr "Oživte své návrhy pomocí interakce" - msgid "onboarding.team-modal.create-team" msgstr "Vytvořte tým" @@ -1784,7 +2136,12 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "Přejít na přihlášení" -#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "Smíšený" @@ -1838,6 +2195,9 @@ msgstr "Cesty" msgid "shortcut-subsection.shape" msgstr "Tvary" +msgid "shortcut-subsection.text-editor" +msgstr "Texty" + msgid "shortcut-subsection.tools" msgstr "Nástroje" @@ -1856,9 +2216,15 @@ msgstr "Přidat uzel" msgid "shortcuts.align-bottom" msgstr "Zarovnat dolů" +msgid "shortcuts.align-center" +msgstr "Zarovnat na střed" + msgid "shortcuts.align-hcenter" msgstr "Zarovnat vodorovně na střed" +msgid "shortcuts.align-justify" +msgstr "Zarovnat do bloku" + msgid "shortcuts.align-left" msgstr "Zarovnat vlevo" @@ -1874,6 +2240,9 @@ msgstr "Zarovnat na střed svisle" msgid "shortcuts.artboard-selection" msgstr "Vytvořit tabuli z výběru" +msgid "shortcuts.bold" +msgstr "Přepnout tučné písmo" + msgid "shortcuts.bool-difference" msgstr "Rozdíl" @@ -1964,6 +2333,12 @@ msgstr "Překlopit vodorovně" msgid "shortcuts.flip-vertical" msgstr "Překlopit svisle" +msgid "shortcuts.font-size-dec" +msgstr "Zmenšit velikost písma" + +msgid "shortcuts.font-size-inc" +msgstr "Zvětšit velikost písma" + msgid "shortcuts.go-to-drafts" msgstr "Přejít na koncepty" @@ -1988,9 +2363,27 @@ msgstr "Přiblížit" msgid "shortcuts.insert-image" msgstr "Vložit obrázek" +msgid "shortcuts.italic" +msgstr "Přepnout kurzívu" + msgid "shortcuts.join-nodes" msgstr "Propojit uzly" +msgid "shortcuts.letter-spacing-dec" +msgstr "Zmenšit mezery mezi písmeny" + +msgid "shortcuts.letter-spacing-inc" +msgstr "Zvětšit mezery mezi písmeny" + +msgid "shortcuts.line-height-dec" +msgstr "Snížit výšku řádku" + +msgid "shortcuts.line-height-inc" +msgstr "Zvýšit výšku řádku" + +msgid "shortcuts.line-through" +msgstr "Přepnout řádek" + msgid "shortcuts.make-corner" msgstr "Udělat roh" @@ -2078,6 +2471,9 @@ msgstr "Přejít do sekce komentářů" msgid "shortcuts.open-dashboard" msgstr "Přejít na nástěnku" +msgid "shortcuts.open-inspect" +msgstr "Přejděte do sekce inspektor" + msgid "shortcuts.open-interactions" msgstr "Přejít do části interakce" @@ -2108,6 +2504,12 @@ msgstr "Prohledat zkratky" msgid "shortcuts.select-all" msgstr "Vybrat vše" +msgid "shortcuts.select-next" +msgstr "Vybrat další vrstvu" + +msgid "shortcuts.select-prev" +msgstr "Vybrat předchozí vrstvu" + msgid "shortcuts.separate-nodes" msgstr "Rozdělit uzly" @@ -2151,8 +2553,8 @@ msgstr "Přepnout paletu barev" msgid "shortcuts.toggle-focus-mode" msgstr "Přepnout režim soustředění" -msgid "shortcuts.toggle-grid" -msgstr "Zobrazit/skrýt mřížku" +msgid "shortcuts.toggle-fullscreen" +msgstr "Přepnout zobrazení na celou obrazovku" msgid "shortcuts.toggle-history" msgstr "Přepnout historii" @@ -2160,6 +2562,9 @@ msgstr "Přepnout historii" msgid "shortcuts.toggle-layers" msgstr "Přepínání vrstev" +msgid "shortcuts.toggle-layout-flex" +msgstr "Přidat/odebrat flexibilní rozložení" + msgid "shortcuts.toggle-lock" msgstr "Uzamknout vybrané" @@ -2169,21 +2574,18 @@ msgstr "Uzamknout proporce" msgid "shortcuts.toggle-rules" msgstr "Zobrazit/skrýt pravítka" -msgid "shortcuts.toggle-scale-text" -msgstr "Přepnout měřítko textu" - -msgid "shortcuts.toggle-snap-grid" -msgstr "Přichytit k mřížce" - -msgid "shortcuts.toggle-snap-guide" -msgstr "Přichytit k vodicím lištám" - msgid "shortcuts.toggle-textpalette" msgstr "Přepnout paletu textu" +msgid "shortcuts.toggle-visibility" +msgstr "Přepnout viditelnost" + msgid "shortcuts.toggle-zoom-style" msgstr "Přepnout styl přiblížení" +msgid "shortcuts.underline" +msgstr "Přepnout podtržení" + msgid "shortcuts.undo" msgstr "Zpět" @@ -2196,6 +2598,12 @@ msgstr "Zrušit masku" msgid "shortcuts.v-distribute" msgstr "Rozmístit vertikálně" +msgid "shortcuts.zoom-lense-decrease" +msgstr "Zmenšení zoomu" + +msgid "shortcuts.zoom-lense-increase" +msgstr "Zvětšení zoomu" + msgid "shortcuts.zoom-selected" msgstr "Přiblížit na vybrané" @@ -2255,6 +2663,9 @@ msgstr "Členové - %s - Penpot" msgid "title.team-settings" msgstr "Nastavení - %s - Penpot" +msgid "title.team-webhooks" +msgstr "Webhooks - %s - Penpot" + #: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs msgid "title.viewer" msgstr "%s - Režim zobrazení - Penpot" @@ -2288,6 +2699,9 @@ msgstr "Nezobrazovat interakce" msgid "viewer.header.fullscreen" msgstr "Celá obrazovka" +msgid "viewer.header.inspect-section" +msgstr "Zkontrolovat (%s)" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.interactions" msgstr "Interakce" @@ -2311,6 +2725,9 @@ msgstr "Zobrazit interakce po kliknutí" msgid "viewer.header.sitemap" msgstr "Mapa stránek" +msgid "webhooks.last-delivery.success" +msgstr "Poslední doručení proběhlo úspěšně." + #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.hcenter" msgstr "Zarovnat vodorovně na střed (%s)" @@ -2351,11 +2768,13 @@ msgstr "Podklady" msgid "workspace.assets.box-filter-all" msgstr "Všechny podklady" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.colors" msgstr "Barvy" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.components" msgstr "Komponenty" @@ -2369,19 +2788,27 @@ msgstr "" "Vaše položky budou automaticky pojmenovány jako „název skupiny / název " "položky“" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "Delete" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.duplicate" msgstr "Duplikovat" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.duplicate-main" +msgstr "Duplikovat hlavní" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" msgstr "Upravit" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.graphics" msgstr "Grafika" @@ -2404,7 +2831,9 @@ msgstr "místní knihovna" msgid "workspace.assets.not-found" msgstr "Nebyly nalezeny žádné podklady" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename" msgstr "Přejmenovat" @@ -2423,11 +2852,8 @@ msgstr[0] "%s položka vybrána" msgstr[1] "Počet vybraných položek: %s" msgstr[2] "Počet vybraných položek: %s" +#: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "SDÍLENÉ" - -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "Typografie" @@ -2455,10 +2881,15 @@ msgstr "Mezery mezi písmeny" msgid "workspace.assets.typography.line-height" msgstr "Výška řádku" -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/handoff/attributes/text.cljs, src/app/main/ui/handoff/attributes/text.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/handoff/attributes/text.cljs, +#: src/app/main/ui/handoff/attributes/text.cljs msgid "workspace.assets.typography.sample" msgstr "Ag" +msgid "workspace.assets.typography.text-styles" +msgstr "Styly textu" + #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.text-transform" msgstr "Transformace textu" @@ -2479,11 +2910,13 @@ msgstr "Zapnout režim soustředění" msgid "workspace.focus.selection" msgstr "Výběr" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "Lineární přechod" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.radial" msgstr "Radiální přechod" @@ -2491,14 +2924,13 @@ msgstr "Radiální přechod" msgid "workspace.header.menu.disable-dynamic-alignment" msgstr "Zakázat dynamické zarovnání" +msgid "workspace.header.menu.disable-scale-content" +msgstr "Zakázat proporcionální měřítko" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-scale-text" msgstr "Zakázat měřítko textu" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Zakázat přichycení k mřížce" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "Zakázat přichycení k vodicím lištám" @@ -2510,14 +2942,13 @@ msgstr "Zakázat přichycení k pixelu" msgid "workspace.header.menu.enable-dynamic-alignment" msgstr "Povolit dynamické zarovnání" +msgid "workspace.header.menu.enable-scale-content" +msgstr "Povolit proporcionální měřítko" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-scale-text" msgstr "Povolit měřítko textu" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Přichytit k mřížce" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "Přichytit k vodicím lištám" @@ -2529,10 +2960,6 @@ msgstr "Povolit přichycení k pixelu" msgid "workspace.header.menu.hide-artboard-names" msgstr "Skrýt názvy tabulí" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Skrýt mřížky" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Skrýt paletu barev" @@ -2568,6 +2995,9 @@ msgstr "Předvolby" msgid "workspace.header.menu.option.view" msgstr "Pohled" +msgid "workspace.header.menu.redo" +msgstr "Znovu" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.select-all" msgstr "Vybrat vše" @@ -2576,10 +3006,6 @@ msgstr "Vybrat vše" msgid "workspace.header.menu.show-artboard-names" msgstr "Zobrazit názvy tabulí" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Zobrazit mřížku" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Zobrazit paletu barev" @@ -2595,6 +3021,9 @@ msgstr "Zobrazit pravítka" msgid "workspace.header.menu.show-textpalette" msgstr "Zobrazit paletu písem" +msgid "workspace.header.menu.undo" +msgstr "Zpět" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "Resetovat" @@ -2647,7 +3076,8 @@ msgstr "Přidat" msgid "workspace.libraries.colors" msgstr "barvy %s" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" msgstr "Knihovna souborů" @@ -2655,7 +3085,8 @@ msgstr "Knihovna souborů" msgid "workspace.libraries.colors.hsv" msgstr "HSV" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "Nedávné barvy" @@ -2806,15 +3237,18 @@ msgstr "Nahoře a dole" msgid "workspace.options.design" msgstr "Design" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs msgid "workspace.options.export" msgstr "Exportovat" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs msgid "workspace.options.export-multiple" msgstr "Exportovat výběr" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs msgid "workspace.options.export-object" msgid_plural "workspace.options.export-object" msgstr[0] "Exportovat 1 prvek" @@ -2825,19 +3259,23 @@ msgstr[2] "Exportovat %s prvků" msgid "workspace.options.export.suffix" msgstr "Přípona" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-complete" msgstr "Export byl dokončen" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object" msgstr "Exportování…" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-error" msgstr "Export se nezdařil" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-slow" msgstr "Export je nečekaně pomalý" @@ -2955,6 +3393,9 @@ msgstr "Ohraničení skupiny" msgid "workspace.options.height" msgstr "Výška" +msgid "workspace.options.inspect" +msgstr "Inspektor" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-action" msgstr "Akce" @@ -2983,6 +3424,9 @@ msgstr "Push" msgid "workspace.options.interaction-animation-slide" msgstr "Slide" +msgid "workspace.options.interaction-auto" +msgstr "automaticky" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-background" msgstr "Přidejte překrytí pozadí" @@ -3131,6 +3575,10 @@ msgstr "Zachovat pozici posouvání" msgid "workspace.options.interaction-prev-screen" msgstr "Předchozí obrazovka" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-relative-to" +msgstr "Relativní k" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-self" msgstr "já" @@ -3244,13 +3692,57 @@ msgid "workspace.options.layout-item.advanced-ops" msgstr "Rozšířené možnosti" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs -msgid "workspace.options.layout-item.title.layout-min-h" +msgid "workspace.options.layout-item.layout-item-max-h" +msgstr "Maximální výška" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.layout-item-max-w" +msgstr "Maximální šířka" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.layout-item-min-h" msgstr "Minimální výška" +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.layout-item-min-w" +msgstr "Minimální šířka" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.layout-item-max-h" +msgstr "Maximální výška" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.layout-item-max-w" +msgstr "Maximální šířka" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.layout-item-min-h" +msgstr "Minimální výška" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.layout-item-min-w" +msgstr "Minimální šířka" + #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.bottom" msgstr "Dole" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column" +msgstr "Sloupec" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column-reverse" +msgstr "Reverzní sloupec" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row" +msgstr "Řádek" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row-reverse" +msgstr "Reverzní řada" + #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.gap" msgstr "Mezera" @@ -3314,7 +3806,8 @@ msgstr "Více barev knihovny" msgid "workspace.options.opacity" msgstr "Průhlednost" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Pozice" @@ -3326,12 +3819,12 @@ msgid "workspace.options.radius" msgstr "Poloměr" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Všechny rohy" +msgid "workspace.options.radius-bottom-left" +msgstr "Dole vlevo" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Nezávislé rohy" +msgid "workspace.options.radius-bottom-right" +msgstr "Dole vpravo" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3342,17 +3835,18 @@ msgid "workspace.options.radius-top-right" msgstr "Nahoře vpravo" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Dole vlevo" +msgid "workspace.options.radius.all-corners" +msgstr "Všechny rohy" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Dole vpravo" +msgid "workspace.options.radius.single-corners" +msgstr "Nezávislé rohy" msgid "workspace.options.recent-fonts" msgstr "Nedávné" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" msgstr "Opakovat" @@ -3425,7 +3919,8 @@ msgstr "Zobrazit v exportech" msgid "workspace.options.show-in-viewer" msgstr "Zobrazit v režimu zobrazení" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.size" msgstr "Velikost" @@ -3507,26 +4002,10 @@ msgstr "Plný" msgid "workspace.options.text-options.align-bottom" msgstr "Zarovnat dolů" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Zarovnat doprostřed (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Zarovnat (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Zarovnat vlevo (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Zarovnat na střed" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Zarovnat vpravo (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Zarovnat nahoru" @@ -3563,7 +4042,8 @@ msgstr "Výška řádku" msgid "workspace.options.text-options.lowercase" msgstr "Malá písmena" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.none" msgstr "Žádné" @@ -3571,6 +4051,22 @@ msgstr "Žádné" msgid "workspace.options.text-options.strikethrough" msgstr "Přeškrtnutí (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Zarovnat doprostřed (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Zarovnat (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Zarovnat vlevo (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Zarovnat vpravo (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Text" @@ -3638,6 +4134,10 @@ msgstr "Oddělit uzly (%s)" msgid "workspace.path.actions.snap-nodes" msgstr "Přichytit uzly (%s)" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.add-flex" +msgstr "Přidat flexibilní rozložení" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "Poslat na konec" @@ -3670,11 +4170,15 @@ msgstr "Smazat" msgid "workspace.shape.menu.delete-flow-start" msgstr "Smazat počáteční bod" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Odpojit instanci" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instances-in-bulk" msgstr "Odpojit instance" @@ -3715,7 +4219,8 @@ msgstr "Posunout dopředu" msgid "workspace.shape.menu.front" msgstr "Posunout na začátek" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.go-main" msgstr "Přejít na hlavní soubor komponentu" @@ -3737,18 +4242,26 @@ msgstr "Průnik" msgid "workspace.shape.menu.lock" msgstr "Zamknout" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.mask" msgstr "Maska" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.paste" msgstr "Vložit" msgid "workspace.shape.menu.path" msgstr "Cesta" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.remove-flex" +msgstr "Odstranit flexibilní rozložení" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "Obnovit" @@ -3763,11 +4276,13 @@ msgstr "Vybrat vrstvu" msgid "workspace.shape.menu.show" msgstr "Zobrazit" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-in-assets" msgstr "Zobrazit na panelu prostředků" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-main" msgstr "Zobrazit hlavní komponent" @@ -3795,11 +4310,15 @@ msgstr "Odemknout" msgid "workspace.shape.menu.unmask" msgstr "Zrušit masku" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-components-in-bulk" msgstr "Aktualizujte hlavní komponenty" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-main" msgstr "Aktualizujte hlavní komponent" @@ -3841,7 +4360,8 @@ msgstr "Tvary" msgid "workspace.sidebar.layers.texts" msgstr "Texty" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, +#: src/app/main/ui/handoff/attributes/svg.cljs msgid "workspace.sidebar.options.svg-attrs.title" msgstr "Importované atributy SVG" @@ -4035,624 +4555,577 @@ msgstr "Aktualizace" msgid "workspace.viewport.click-to-close-path" msgstr "Kliknutím zavřete cestu" -msgid "dashboard.webhooks.active.explain" -msgstr "Když je tento webhook spuštěn, budou doručeny podrobnosti o události" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "Nikdy" -msgid "dashboard.webhooks.content-type" -msgstr "Typ obsahu" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "Platnost vypršela %s" -msgid "dashboard.webhooks.create" -msgstr "Vytvořit webhook" - -msgid "dashboard.webhooks.description" -msgstr "" -"Webhooky jsou jednoduchým způsobem, jak umožnit jiným webům a aplikacím, aby " -"byly upozorňovány na určité události v Penpotu. Na každou z vámi " -"poskytnutých adres URL odešleme požadavek POST." - -msgid "dashboard.webhooks.empty.add-one" -msgstr "Chcete-li webhook přidat, stiskněte tlačítko „Přidat webhook“." +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "Žádné datum vypršení platnosti" #: src/app/main/errors.cljs -msgid "errors.feature-mismatch" -msgstr "" -"Vypadá to, že otevíráte soubor, který má povolenou funkci '%s', ale vaše " -"rozhraní penpotu ji nepodporuje nebo ji má deaktivovanou." +msgid "errors.version-not-supported" +msgstr "Soubor má nekompatibilní číslo verze" -msgid "errors.webhooks.connection" -msgstr "Chyba připojení, adresa URL není dostupná" +#: src/app/main/errors.cljs +msgid "errors.team-feature-mismatch" +msgstr "Zjištěna nekompatibilní funkce '%s'" -#: src/app/main/ui/inspect/attributes/blur.cljs -msgid "inspect.attributes.blur" -msgstr "Rozostření" +msgid "inspect.attributes.typography.text-transform.unset" +msgstr "Nenastaveno" -#: src/app/main/ui/inspect/attributes/blur.cljs -msgid "inspect.attributes.blur.value" -msgstr "Hodnota" +msgid "labels.discard" +msgstr "Zahodit" -msgid "inspect.attributes.stroke.style.mixed" -msgstr "Smíšená" +msgid "labels.share" +msgstr "Sdílet" -msgid "inspect.attributes.stroke.style.none" -msgstr "Žádná" +msgid "labels.search" +msgstr "Hledat" -msgid "inspect.attributes.stroke.style.solid" -msgstr "Plná" - -msgid "inspect.attributes.typography.text-transform.none" -msgstr "Žádná" - -msgid "inspect.attributes.typography.text-decoration.underline" -msgstr "Podtržení" - -msgid "inspect.attributes.typography.text-transform.titlecase" -msgstr "První písmena velká" - -msgid "inspect.attributes.typography.text-transform.uppercase" -msgstr "Velká písmena" - -msgid "inspect.empty.select" -msgstr "" -"Vyberte tvar, tabuli nebo skupinu, abyste mohli zkontrolovat jejich " -"vlastnosti a kód" - -msgid "inspect.tabs.code.selected.rect" -msgstr "Obdélník" - -msgid "inspect.tabs.code.selected.svg-raw" -msgstr "SVG" - -#: src/app/main/ui/inspect/right_sidebar.cljs -msgid "inspect.tabs.info" -msgstr "Informace" - -msgid "labels.view-only" -msgstr "POUZE ZOBRAZIT" - -msgid "modals.create-webhook.title" -msgstr "Vytvořit webhook" - -msgid "shortcuts.toggle-fullscreen" -msgstr "Přepnout zobrazení na celou obrazovku" - -msgid "shortcuts.toggle-layout-flex" -msgstr "Přidat/odebrat flexibilní rozložení" - -msgid "viewer.header.inspect-section" -msgstr "Zkontrolovat (%s)" - -msgid "webhooks.last-delivery.success" -msgstr "Poslední doručení proběhlo úspěšně." - -msgid "workspace.options.inspect" -msgstr "Inspektor" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs -msgid "workspace.options.layout-item.layout-item-min-w" -msgstr "Minimální šířka" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs -msgid "workspace.options.layout-item.title.layout-item-min-h" -msgstr "Minimální výška" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.column" -msgstr "Sloupec" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "Tato aktualizace je jednorázová." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "Aktualizace %s..." - -#: src/app/main/ui/workspace/context_menu.cljs -msgid "workspace.shape.menu.add-flex" -msgstr "Přidat flexibilní rozložení" - -#: src/app/main/ui/workspace/context_menu.cljs -msgid "workspace.shape.menu.remove-flex" -msgstr "Odstranit flexibilní rozložení" - -msgid "errors.webhooks.invalid-uri" -msgstr "Adresa URL neprošla ověřením." - -msgid "errors.webhooks.ssl-validation" -msgstr "Chyba při ověřování SSL." - -msgid "inspect.tabs.code.selected.text" -msgstr "Text" - -msgid "modals.create-webhook.submit-label" -msgstr "Vytvořit webhook" - -msgid "inspect.attributes.stroke.style.dotted" -msgstr "Tečkovaná" - -msgid "shortcut-subsection.text-editor" -msgstr "Texty" - -msgid "shortcuts.align-center" -msgstr "Zarovnat na střed" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "Vytvořit token" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "" -"Pokud jej smažete, tyto položky již nebudou dostupné z jiných souborů. " -"Položky, které již byly použity, zůstanou v těchto souborech (nebude porušen " -"žádný návrh!)." -msgstr[1] "" -"Pokud je smažete, tyto položky již nebudou dostupné z jiných souborů. " -"Položky, které již byly použity, zůstanou v těchto souborech (nebude porušen " -"žádný návrh!)." -msgstr[2] "" -"Pokud je smažete, tyto položky již nebudou dostupné z jiných souborů. " -"Položky, které již byly použity, zůstanou v těchto souborech (nebude porušen " -"žádný návrh!)." +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "Tato knihovna je aktivována zde: " +msgstr[1] "Tyto knihovny jsou aktivovány zde: " +msgstr[2] "Tyto knihovny jsou aktivovány zde: " -msgid "workspace.assets.duplicate-main" -msgstr "Duplikovat hlavní" +msgid "onboarding.choice.team-up.create-team-and-send-invites" +msgstr "Vytvořte tým a odešlete pozvánky" + +msgid "onboarding.choice.team-up.create-team-without-inviting" +msgstr "Vytvořte tým bez pozvánek" + +msgid "onboarding.choice.team-up.create-team-without-invite" +msgstr "Vytvořte tým" + +msgid "onboarding.choice.team-up.create-team-and-send-invites-description" +msgstr "Budete moci pozvat později" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31-50" + +msgid "onboarding.choice.team-up.continue-creating-team" +msgstr "Pokračujte ve vytváření týmu" + +msgid "onboarding.choice.team-up.start-without-a-team" +msgstr "Začněte bez týmu" + +msgid "onboarding.choice.team-up.start-without-a-team-description" +msgstr "Později budete moci vytvořit tým." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canva" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +msgid "settings.detach" +msgstr "Odpojit" + +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "Název webhooku musí obsahovat maximálně 2048 znaků." + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "Přiblížení" + +msgid "workspace.layout_grid.editor.options.edit-grid" +msgstr "Upravit mřížku" + +msgid "workspace.layout_grid.editor.options.exit" +msgstr "Odejít" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "AKTUALIZACE KNIHOVNY" + +msgid "workspace.options.component.swap" +msgstr "Vyměnit komponent" + +msgid "workspace.options.component.swap.empty" +msgstr "V této knihovně zatím nejsou žádné položky" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-relative-to" -msgstr "Relativní k" +msgid "workspace.options.flows.flow" +msgstr "Flow" -msgid "dashboard.webhooks.active" -msgstr "Je aktivní" +msgid "workspace.top-bar.read-only.done" +msgstr "Hotovo" -msgid "dashboard.webhooks.empty.no-webhooks" -msgstr "Dosud nebyly vytvořeny žádné webhooky." - -msgid "dashboard.webhooks.update.success" -msgstr "Webhook byl úspěšně aktualizován." - -#: src/app/main/errors.cljs -msgid "errors.max-quote-reached" -msgstr "Dosáhli jste '%s' kvóty. Kontaktujte podporu." - -#: src/app/main/ui/inspect/attributes/common.cljs -msgid "inspect.attributes.color.hsla" -msgstr "HSLA" - -#: src/app/main/ui/inspect/attributes/common.cljs -msgid "inspect.attributes.color.rgba" -msgstr "RGBA" - -#: src/app/main/ui/inspect/attributes/image.cljs -msgid "inspect.attributes.image.download" -msgstr "Stáhnout zdrojový obrázek" - -#: src/app/main/ui/inspect/attributes/layout.cljs -msgid "inspect.attributes.layout" -msgstr "Rozložení" - -#: src/app/main/ui/inspect/attributes/layout.cljs -msgid "inspect.attributes.layout.height" -msgstr "Výška" - -#: src/app/main/ui/inspect/attributes/fill.cljs -msgid "inspect.attributes.fill" -msgstr "Výplň" - -msgid "inspect.attributes.typography.text-decoration.none" -msgstr "Žádná" - -#: src/app/main/ui/inspect/right_sidebar.cljs -msgid "inspect.tabs.code" -msgstr "Kód" - -msgid "inspect.tabs.code.selected.circle" -msgstr "Kruh" - -msgid "inspect.tabs.code.selected.component" -msgstr "Komponent" - -msgid "inspect.tabs.code.selected.curve" -msgstr "Křivka" - -msgid "inspect.tabs.code.selected.frame" -msgstr "Tabule" - -msgid "inspect.tabs.code.selected.group" -msgstr "Skupina" - -msgid "inspect.tabs.code.selected.image" +msgid "media.image" msgstr "Obrázek" -msgid "inspect.tabs.code.selected.mask" -msgstr "Maska" +msgid "media.solid" +msgstr "Plná" -#: src/app/main/ui/inspect/right_sidebar.cljs -msgid "inspect.tabs.code.selected.multiple" -msgstr "%s vybráno" +msgid "media.linear" +msgstr "Lineární" -msgid "labels.inactive" -msgstr "Neaktivní" +msgid "media.radial" +msgstr "Radiální" -#: src/app/main/ui/inspect/attributes/layout.cljs -msgid "inspect.attributes.layout.left" -msgstr "Vlevo" +msgid "media.gradient" +msgstr "Přechod" -#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs -msgid "inspect.attributes.layout.radius" -msgstr "Poloměr" +msgid "media.choose-image" +msgstr "Vyberte obrázek" -#: src/app/main/ui/inspect/attributes/layout.cljs -msgid "inspect.attributes.layout.top" -msgstr "Nahoře" - -#, permanent -msgid "inspect.attributes.stroke.alignment.center" -msgstr "Střed" - -#, permanent -msgid "inspect.attributes.stroke.alignment.outer" -msgstr "Venku" - -#: src/app/main/ui/inspect/attributes/stroke.cljs -msgid "inspect.attributes.stroke.width" -msgstr "Šířka" - -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography" -msgstr "Typografie" - -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-family" -msgstr "Rodina písem" - -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-size" -msgstr "Velikost písma" - -msgid "inspect.empty.more-info" -msgstr "Více informací o inspektorovi" - -#: src/app/main/ui/workspace.cljs -msgid "labels.reload-file" -msgstr "Znovu načíst soubor" - -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "labels.unpublish-multi-files" -msgstr "Zrušit publikování %s souborů" - -msgid "labels.webhooks" -msgstr "Webhooks" - -msgid "modals.create-webhook.url.placeholder" -msgstr "https://example.com/postreceive" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "Některé položky v knihovně tohoto souboru se používají zde:" -msgstr[1] "Některá položky v knihovnách těchto souborů se používají zde:" -msgstr[2] "Některá položky v knihovnách těchto souborů se používají zde:" - -msgid "modals.delete-webhook.accept" -msgstr "Smazat webhook" - -msgid "modals.delete-webhook.message" -msgstr "Opravdu chcete tento webhook smazat?" - -msgid "modals.delete-webhook.title" -msgstr "Mazání webhooku" - -msgid "modals.edit-webhook.submit-label" -msgstr "Upravit webhook" - -msgid "modals.edit-webhook.title" -msgstr "Upravit webhook" - -msgid "modals.invite-member.repeated-invitation" -msgstr "" -"Některé e-maily jsou od současných členů týmu. Jejich pozvánky nebudou " -"odeslány." - -#: src/app/main/ui/dashboard/team.cljs -msgid "notifications.invitation-link-copied" -msgstr "Odkaz na pozvánku zkopírován" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "Žádný z prostředků v této knihovně se nepoužívá." -msgstr[1] "Žádné z prostředků v těchto knihovnách se nepoužívá." -msgstr[2] "Žádné z prostředků v těchto knihovnách se nepoužívá." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "Některé položky v této knihovně se používají zde:" -msgstr[1] "Některé položky v těchto knihovnách se používají zde:" -msgstr[2] "Některé položky v těchto knihovnách se používají zde:" - -msgid "shortcuts.align-justify" -msgstr "Zarovnat do bloku" - -msgid "shortcuts.font-size-dec" -msgstr "Zmenšit velikost písma" - -msgid "shortcuts.font-size-inc" -msgstr "Zvětšit velikost písma" - -msgid "shortcuts.italic" -msgstr "Přepnout kurzívu" - -msgid "shortcuts.letter-spacing-dec" -msgstr "Zmenšit mezery mezi písmeny" - -msgid "shortcuts.line-height-dec" -msgstr "Snížit výšku řádku" - -msgid "shortcuts.line-height-inc" -msgstr "Zvýšit výšku řádku" - -msgid "shortcuts.line-through" -msgstr "Přepnout řádek" - -msgid "shortcuts.select-next" -msgstr "Vybrat další vrstvu" - -msgid "shortcuts.select-prev" -msgstr "Vybrat předchozí vrstvu" - -msgid "shortcuts.zoom-lense-decrease" -msgstr "Zmenšení zoomu" - -msgid "shortcuts.zoom-lense-increase" -msgstr "Zvětšení zoomu" - -msgid "shortcuts.underline" -msgstr "Přepnout podtržení" - -msgid "workspace.header.menu.enable-scale-content" -msgstr "Povolit proporcionální měřítko" - -msgid "workspace.header.menu.redo" -msgstr "Znovu" - -msgid "workspace.header.menu.undo" -msgstr "Zpět" - -msgid "workspace.options.interaction-auto" -msgstr "automaticky" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs -msgid "workspace.options.layout-item.layout-item-max-h" -msgstr "Maximální výška" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs -msgid "workspace.options.layout-item.layout-item-max-w" -msgstr "Maximální šířka" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs -msgid "workspace.options.layout-item.title.layout-item-max-h" -msgstr "Maximální výška" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs -msgid "workspace.options.layout-item.title.layout-item-max-w" -msgstr "Maximální šířka" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs -msgid "workspace.options.layout-item.title.layout-item-min-w" -msgstr "Minimální šířka" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.column-reverse" -msgstr "Reverzní sloupec" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.row" -msgstr "Řádek" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.row-reverse" -msgstr "Reverzní řada" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "" -"Chcete-li to zkusit znovu, můžete tento soubor znovu načíst. Pokud problém " -"přetrvává, doporučujeme vám podívat se na seznam a zvážit odstranění " -"poškozené grafiky." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "Některé grafiky nebylo možné aktualizovat." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "Převádí se %s/%s" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "" -"Grafiky knihovny jsou od nynějška komponenty, díky čemuž budou mnohem " -"výkonnější." +msgid "workspace.options.guides.title" +msgstr "Vodítka" +#: src/app/main/ui/auth/register.cljs #, markdown -msgid "dashboard.fonts.warning-text" +msgid "auth.terms-privacy-agreement-md" msgstr "" -"Zjistili jsme možný problém ve vašich písmech související s vertikálními " -"metrikami pro různé operační systémy. Chcete-li to zkontrolovat, můžete " -"použít služby vertikálních metrik písem, jako je [toto] (https://vertical-" -"metrics.netlify.app/). Kromě toho doporučujeme použít " -"[Transfonter](https://transfonter.org/) ke generování webových písem a " -"opravě chyb. " +"Při vytváření nového účtu souhlasíte s našimi [smluvními podmínkami](%s) a [" +"zásadami ochrany soukromí](%s)." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs -msgid "errors.email-invalid" -msgstr "Zadejte prosím platný email" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "Zkopírovaný token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Osobní přístupové tokeny fungují jako alternativa k našemu systému ověřování " +"přihlašovacích údajů/hesel a lze je použít k tomu, aby aplikaci umožnily " +"přístup k internímu rozhraní Penpot API" #: src/app/main/errors.cljs -msgid "errors.feature-not-supported" -msgstr "Funkce '%s' není podporována." - -msgid "dashboard.webhooks.create.success" -msgstr "Webhook byl úspěšně vytvořen." - -msgid "errors.webhooks.last-delivery" -msgstr "Poslední dodávka nebyla úspěšná." - -msgid "inspect.attributes.typography.text-transform.lowercase" -msgstr "Malá písmena" - -msgid "errors.webhooks.unexpected" -msgstr "Při ověřování došlo k neočekávané chybě" - -msgid "inspect.attributes.typography.text-decoration.strikethrough" -msgstr "Přeškrtnutí" - -msgid "errors.webhooks.timeout" -msgstr "Timeout" - -msgid "errors.webhooks.unexpected-status" -msgstr "Neočekávaný stav %s" - -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.text-decoration" -msgstr "Dekorace textu" - -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.text-transform" -msgstr "Transformace textu" - -msgid "inspect.tabs.code.selected.path" -msgstr "Cesta" - -msgid "labels.active" -msgstr "Aktivní" - -msgid "inspect.empty.help" +msgid "errors.file-feature-mismatch" msgstr "" -"Pokud se chcete dozvědět více o inspektorovi designu, navštivte centrum " -"nápovědy společnosti Penpot" +"Zdá se, že existuje nesoulad mezi povolenými funkcemi a funkcemi souboru, " +"který se pokoušíte otevřít. Před otevřením souboru je třeba provést migraci " +"pro '%s'." -#: src/app/main/ui/dashboard/team.cljs -msgid "labels.copy-invitation-link" -msgstr "Kopírovat odkaz" +msgid "errors.validation" +msgstr "Chyba ověření" -msgid "modals.create-webhook.url.label" -msgstr "Adresa URL datové části" +msgid "errors.paste-data-validation" +msgstr "Neplatná data ve schránce" -#: src/app/main/ui/inspect/attributes/layout.cljs -msgid "inspect.attributes.layout.rotation" -msgstr "Otáčení" +msgid "onboarding.choice.team-up.continue-without-a-team" +msgstr "Pokračovat bez týmu" + +msgid "onboarding.choice.team-up.create-team-and-invite" +msgstr "Vytvořte tým a pozvěte" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "Vývojář" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "InVision" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "Zanechte zpětnou vazbu pro můj týmový projekt" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "Další" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "Jiné (upřesněte)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "Pracuji na osobním projektu" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "Předchozí" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "Jak plánujete používat Penpot?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "Jaká je vaše role?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.select-option" +msgstr "Vyberte možnost" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "Jaká je velikost vašeho týmu?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "Vyzkoušejte Penpot, abyste zjistili, zda je vhodný pro tým " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" +msgstr "" +"Vaše zpětná vazba nám pomůže porozumět vašim zvykům a preferencím, abychom " +"mohli i nadále dělat Penpot užitečným nástrojem." + +msgid "shortcuts.text-align-center" +msgstr "Zarovnat na střed" + +msgid "workspace.layout_grid.editor.top-bar.locate" +msgstr "Lokalizovat" + +msgid "workspace.layout_grid.editor.top-bar.done" +msgstr "Hotovo" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "zobrazit všechny změny" + +msgid "workspace.options.component.annotation" +msgstr "Anotace" + +msgid "workspace.shape.menu.create-multiple-components" +msgstr "Vytvořte více komponent" + +#, markdown +msgid "workspace.top-bar.read-only" +msgstr "**Režim kontroly** (Pouze zobrazení)" + +msgid "modals.add-shared-confirm-empty.hint" +msgstr "" +"Vaše knihovna je prázdná. Po přidání jako sdílená knihovna budou položky, " +"které vytvoříte, k dispozici pro použití se zbytkem vašich souborů. Opravdu " +"ji chcete publikovat?" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "Kopírovat token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "Jméno" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "Název může pomoci zjistit, k čemu token slouží" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "Opravdu chcete tento token smazat?" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "Smazat token" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" msgstr[0] "" -"Žádné položky v knihovně tohoto souboru se nepoužívají. Budou odstraněny " -"spolu se souborem." +"Aktiva, která již byla v tomto souboru použita, tam zůstanou (nebude porušen " +"žádný návrh)." msgstr[1] "" -"Žádné položky v knihovně těchto souborů se nepoužívají. Budou odstraněny " -"spolu se soubory." +"Aktiva, která již byla v těchto souborech použita, tam zůstanou (nebude " +"porušen žádný návrh)." msgstr[2] "" -"Žádné položky v knihovně těchto souborů se nepoužívají. Budou odstraněny " -"spolu se soubory." +"Aktiva, která již byla v těchto souborech použita, tam zůstanou (nebude " +"porušen žádný návrh)." -#: src/app/main/ui/inspect/attributes/common.cljs -msgid "inspect.attributes.color.hex" -msgstr "HEX" +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "Název musí obsahovat maximálně 250 znaků." -#: src/app/main/ui/inspect/attributes/image.cljs -msgid "inspect.attributes.image.height" -msgstr "Výška" +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "Název musí obsahovat jiný znak než mezeru." -#: src/app/main/ui/inspect/attributes/image.cljs -msgid "inspect.attributes.image.width" -msgstr "Šířka" +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "Heslo musí obsahovat jiný znak než mezeru." -#: src/app/main/ui/inspect/attributes/layout.cljs -msgid "inspect.attributes.layout.width" -msgstr "Šířka" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "Zatím nemáte žádné tokeny." -#: src/app/main/ui/inspect/attributes/shadow.cljs -msgid "inspect.attributes.shadow" -msgstr "Stín" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "Přístupový token byl úspěšně vytvořen." -#: src/app/main/ui/inspect/attributes/layout.cljs -msgid "inspect.attributes.size" -msgstr "Velikost a poloha" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "Platnost vyprší %s" -#: src/app/main/ui/inspect/attributes/stroke.cljs -msgid "inspect.attributes.stroke" -msgstr "Tah" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 dní" -#, permanent -msgid "inspect.attributes.stroke.alignment.inner" -msgstr "Uvnitř" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 dní" -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-style" -msgstr "Styl písma" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 dní" -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-weight" -msgstr "Tloušťka písma" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 dní" -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.letter-spacing" -msgstr "Mezery mezi písmeny" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "Jméno je povinné" -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.line-height" -msgstr "Výška řádku" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "Platnost tokenu vyprší %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "Token nemá žádné datum vypršení platnosti" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "Přístupové tokeny" + +msgid "workspace.options.component.edit-annotation" +msgstr "Upravit anotaci" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "Začněte pracovat na mém projektu" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "Žádný" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" +msgstr "... drátové modely, cesty a toky uživatelů, navigační stromy atd." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "" -"Pokud jej smažete, tyto položky již nebudou dostupné z jiných souborů. " -"Položky, které již byly použity, zůstanou v tomto souboru (žádný návrh " -"nebude neúplný!)." -msgstr[1] "" -"Pokud je smažete, tyto položky již nebudou dostupné z jiných souborů. " -"Položky, které již byly použity, zůstanou v tomto souboru (žádný návrh " -"nebude neúplný!)." -msgstr[2] "" -"Pokud je smažete, tyto položky již nebudou dostupné z jiných souborů. " -"Položky, které již byly použity, zůstanou v tomto souboru (žádný návrh " -"nebude neúplný!)." +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "Není aktivován v žádném souboru." +msgstr[1] "Nejsou aktivovány v žádném souboru." +msgstr[2] "Nejsou aktivovány v žádném souboru." -msgid "shortcuts.bold" -msgstr "Přepnout tučné písmo" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "Generovat nový token" -msgid "title.team-webhooks" -msgstr "Webhooks - %s - Penpot" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "" +"Pro vygenerování nového tokenu stiskněte tlačítko \"Vygenerovat nový token\"." -msgid "shortcuts.letter-spacing-inc" -msgstr "Zvětšit mezery mezi písmeny" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Osobní přístupové tokeny" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "" -"Pokud zrušíte jeho publikování, tyto podklady již nebudou dostupné z jiných " -"souborů. Položky, které již byl\\ použity, zůstanou v těchto souborech (" -"nebude porušen žádný návrh!)." -msgstr[1] "" -"Pokud zrušíte jejich publikování, tyto podklady již nebudou dostupné z " -"jiných souborů. Položky, které již byl\\ použity, zůstanou v těchto " -"souborech (nebude porušen žádný návrh!)." -msgstr[2] "" -"Pokud zrušíte jejich publikování, tyto podklady již nebudou dostupné z " -"jiných souborů. Položky, které již byl\\ použity, zůstanou v těchto " -"souborech (nebude porušen žádný návrh!)." +msgid "errors.cannot-upload" +msgstr "Nelze nahrát soubor médií." -msgid "workspace.assets.typography.text-styles" -msgstr "Styly textu" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "Datum vypršení platnosti" -msgid "shortcuts.open-inspect" -msgstr "Přejděte do sekce inspektor" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "Smazat token" -msgid "workspace.header.menu.disable-scale-content" -msgstr "Zakázat proporcionální měřítko" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "Vygenerujte přístupový token" -#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs -msgid "workspace.options.layout-item.layout-item-min-h" -msgstr "Minimální výška" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "Designer" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "Zakladatel / viceprezident" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "Pusťme se do toho!" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "Jsem freelancer" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "Produktový nebo projektový manažer" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "Marketing" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "Více než 50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "Sketch" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "Start" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "Před použitím Penpot on-premise si to vyzkoušejte" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "Práce v konceptech" + +msgid "shortcuts.select-parent-layer" +msgstr "Vybrat nadřazenou vrstvu" + +msgid "workspace.options.component.copy" +msgstr "Kopírovat" + +msgid "workspace.options.component.main" +msgstr "Hlavní" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "Kruh" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "Diamant" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "Šipka" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "Obdélník" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "Trojúhelník" + +msgid "workspace.shape.menu.add-grid" +msgstr "Přidat rozvržení mřížky" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "Více informací" + +msgid "modals.delete-component-annotation.message" +msgstr "Opravdu chcete smazat tuto anotaci?" + +msgid "modals.delete-component-annotation.title" +msgstr "Smazat anotaci" + +msgid "modals.publish-empty-library.title" +msgstr "Publikovat prázdnou knihovnu" + +msgid "modals.publish-empty-library.accept" +msgstr "Publikovat" + +msgid "modals.publish-empty-library.message" +msgstr "Vaše knihovna je prázdná. Opravdu to chcete publikovat?" + +msgid "shortcuts.text-align-justify" +msgstr "Zarovnat do bloku" + +msgid "shortcuts.text-align-left" +msgstr "Zarovnat vlevo" + +msgid "shortcuts.text-align-right" +msgstr "Zarovnat vpravo" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "Profil – Přístupové tokeny" + +msgid "workspace.assets.open-library" +msgstr "Otevřete soubor knihovny" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.shared-library" +msgstr "Sdílená knihovna" + +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "Ve vaší knihovně zatím nejsou žádné barevné styly" + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "Ve vaší knihovně zatím nejsou žádné typografické styly" + +msgid "workspace.options.component.create-annotation" +msgstr "Vytvořte anotaci" + +msgid "workspace.shape.menu.create-annotation" +msgstr "Vytvořit anotaci" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.describe-your-experience-working-on" +msgstr "Jak byste nejlépe popsali své zkušenosti s prací na..." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "Zjistěte více o Penpot" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "Získejte kód z mého týmového projektu " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "...branding, ilustrace, marketing atd." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "Hodně" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "... design rozhraní, vizuální aktiva, návrhové systémy atd." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.some" +msgstr "Nějaké" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "S jakým designovým nástrojem máte více zkušeností?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11-30" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2-10" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "Student nebo učitel" + +msgid "workspace.layout_grid.editor.title" +msgstr "Úprava mřížky" + +#: src/app/main/data/common.cljs +msgid "notifications.by-code.upgrade-version" +msgstr "K dispozici je nová verze, obnovte prosím stránku" diff --git a/frontend/translations/da.po b/frontend/translations/da.po index 350d543f14..8e4d96ba6c 100644 --- a/frontend/translations/da.po +++ b/frontend/translations/da.po @@ -454,4 +454,4 @@ msgstr "Skrifttype Udbydere - %s - Penpot" #: src/app/main/ui/dashboard/fonts.cljs msgid "title.dashboard.fonts" -msgstr "Skrifttyper - %s - Penpot" \ No newline at end of file +msgstr "Skrifttyper - %s - Penpot" diff --git a/frontend/translations/de.po b/frontend/translations/de.po index bb713e34ec..fe563da325 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2023-08-19 11:55+0000\n" +"PO-Revision-Date: 2024-01-23 15:01+0000\n" "Last-Translator: Stas Haas \n" "Language-Team: German \n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.0-dev\n" +"X-Generator: Weblate 5.4-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -84,6 +84,14 @@ msgstr "LDAP" msgid "auth.login-with-oidc-submit" msgstr "OpenID" +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "Der Name darf keine Leerzeichen enthalten." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "Der Name darf höchstens 250 Zeichen lang sein." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.new-password" msgstr "Geben Sie ein neues Passwort ein" @@ -118,6 +126,10 @@ msgstr "Passwort" msgid "auth.password-length-hint" msgstr "Mindestens 8 Zeichen" +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "Das Passwort darf keine Leerzeichen enthalten." + msgid "auth.privacy-policy" msgstr "Datenschutzerklärung" @@ -268,6 +280,83 @@ msgstr "Tour starten" msgid "dasboard.walkthrough-hero.title" msgstr "Benutzeroberfläche erkunden" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "Token kopiert" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "Neues Token generieren" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "Der Zugangstoken wurde erfolgreich erstellt." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "" +"Drücken Sie die Schaltfläche \"Neuen Token generieren\", um einen zu " +"generieren." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "Du hast bisher keine Token." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "Der Name ist erforderlich" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 Tage" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 Tage" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 Tage" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 Tage" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "Nie" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "Abgelaufen am %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "Läuft ab am %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "Kein Ablaufdatum" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Persönliche Zugangstoken" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Persönliche Zugangstoken stellen eine Alternative zu unserem " +"Login/Passwort-Authentifizierungssystem dar und können verwendet werden, um " +"einer Anwendung den Zugriff auf die interne Penpot-API zu ermöglichen" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "Der Token läuft am %s ab" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "Der Token hat kein Ablaufdatum" + #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" @@ -411,7 +500,6 @@ msgid_plural "dashboard.fonts.fonts-added" msgstr[0] "1 Schriftart hinzugefügt" msgstr[1] "%s Schriftarten hinzugefügt" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "Jede Webschriftart, die Sie hier hochladen, wird der Liste der Schriftarten " @@ -421,7 +509,6 @@ msgstr "" "den folgenden Formaten hochladen: **TTF, OTF und WOFF** (nur eine wird " "benötigt)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "Sie sollten nur Schriftarten hochladen, die Sie besitzen oder für die Sie " @@ -434,6 +521,15 @@ msgstr "" msgid "dashboard.fonts.upload-all" msgstr "Alle hochladen" +msgid "dashboard.fonts.warning-text" +msgstr "" +"Wir haben ein mögliches Problem in Ihren Schriften festgestellt, das mit " +"den vertikalen Metriken für verschiedene Betriebssysteme zusammenhängt. Um " +"dies zu überprüfen, können Sie Online-Dienste wie " +"[diesen](https://vertical-metrics.netlify.app/) verwenden. Außerdem " +"empfehlen wir die Verwendung von [Transfonter](https://transfonter.org/), " +"um Webfonts zu generieren und Fehler zu beheben. " + msgid "dashboard.import" msgstr "Dateien importieren" @@ -445,6 +541,9 @@ msgstr "" "Beim Importieren der Datei ist ein Fehler aufgetreten. Die Datei wurde " "nicht importiert." +msgid "dashboard.import.import-message" +msgstr "%s Dateien wurden erfolgreich importiert." + msgid "dashboard.import.import-warning" msgstr "Einige Dateien enthielten ungültige Objekte, die entfernt wurden." @@ -606,10 +705,22 @@ msgstr "Theme auswählen" msgid "dashboard.show-all-files" msgstr "Alle Dateien anzeigen" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "Ihre Datei wurde erfolgreich gelöscht" +msgstr[1] "Ihre Dateien wurden erfolgreich gelöscht" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-delete-project" msgstr "Ihr Projekt wurde erfolgreich gelöscht" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "Ihre Datei wurde erfolgreich dupliziert" +msgstr[1] "Ihre Dateien wurden erfolgreich dupliziert" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-duplicate-project" msgstr "Ihr Projekt wurde erfolgreich dupliziert" @@ -751,6 +862,9 @@ msgstr "Die Schriftart %s konnte nicht geladen werden" msgid "errors.bad-font-plural" msgstr "Die Schriftarten %s konnten nicht geladen werden" +msgid "errors.cannot-upload" +msgstr "Die Mediendatei kann nicht hochgeladen werden." + #: src/app/main/data/workspace.cljs msgid "errors.clipboard-not-implemented" msgstr "Ihr Browser kann diese Funktion nicht ausführen" @@ -774,7 +888,8 @@ msgstr "Sie können Ihre E-Mail-Adresse nicht als Passwort verwenden" msgid "errors.email-has-permanent-bounces" msgstr "Die E-Mail-Adresse «%s» hat viele permanente Unzustellbarkeitsberichte." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, +#: src/app/main/ui/auth/recovery_request.cljs msgid "errors.email-invalid" msgstr "Geben Sie bitte eine gültige E-Mail-Adresse ein" @@ -789,8 +904,8 @@ msgstr "Die E-Mail \"%s\" wurde als Spam oder dauerhaft abgelehnt gemeldet." msgid "errors.feature-mismatch" msgstr "" "Es scheint als würden Sie eine Datei öffnen, bei der die Funktion '%s' " -"aktiviert ist. Ihr Penpot-Frontend unterstützt es aber nicht oder hat die " -"Funktion deaktiviert." +"aktiviert ist. Ihr aktuelle Version von Penpot unterstützt es aber nicht " +"oder hat die Funktion deaktiviert." #: src/app/main/errors.cljs msgid "errors.feature-not-supported" @@ -950,7 +1065,7 @@ msgstr "E-Mail" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Zu Twitter wechseln" +msgstr "Zu X wechseln" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -958,7 +1073,7 @@ msgstr "Hier helfen wir Ihnen bei technischen Fragen." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Twitter Support-Konto" +msgstr "X Support-Konto" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -1082,6 +1197,10 @@ msgstr "Schriftgröße" msgid "inspect.attributes.typography.font-style" msgstr "Schriftschnitt" +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-weight" +msgstr "Strichstärke" + #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.letter-spacing" msgstr "Zeichenabstand" @@ -1184,6 +1303,10 @@ msgstr "Tastaturkürzel" msgid "labels.accept" msgstr "Akzeptieren" +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "Zugangstoken" + msgid "labels.active" msgstr "Aktiv" @@ -1287,6 +1410,9 @@ msgstr "Einladung löschen" msgid "labels.delete-multi-files" msgstr "%s Dateien löschen" +msgid "labels.discard" +msgstr "Verwerfen" + #: src/app/main/ui/dashboard/projects.cljs, #: src/app/main/ui/dashboard/sidebar.cljs, #: src/app/main/ui/dashboard/files.cljs, @@ -1413,7 +1539,6 @@ msgid "labels.no-invitations" msgstr "Keine ausstehenden Einladungen." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "" "Drücken Sie auf die Schaltfläche **Personen einladen**, um Personen zu " @@ -1662,6 +1787,30 @@ msgstr "E-Mail-Adresse ändern" msgid "modals.change-email.title" msgstr "Ihre E-Mail-Adresse ändern" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "Token kopieren" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "Ablaufdatum" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "Name" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "Durch den Namen kann man erkennen, wofür der Token verwendet wird" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "Token erzeugen" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "Zugangstoken generieren" + msgid "modals.create-webhook.submit-label" msgstr "Webhook erstellen" @@ -1674,6 +1823,18 @@ msgstr "Payload-URL" msgid "modals.create-webhook.url.placeholder" msgstr "https://example.com/postreceive" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "Token löschen" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "Sind Sie sicher, dass Sie diesen Token löschen möchten?" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "Token löschen" + #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.cancel" msgstr "Abbrechen und mein Konto behalten" @@ -1706,6 +1867,12 @@ msgstr "" msgid "modals.delete-comment-thread.title" msgstr "Konversation löschen" +msgid "modals.delete-component-annotation.message" +msgstr "Sind Sie sicher, dass Sie diese Anmerkung löschen möchten?" + +msgid "modals.delete-component-annotation.title" +msgstr "Anmerkung löschen" + #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-confirm.accept" msgstr "Datei löschen" @@ -1774,28 +1941,10 @@ msgstr[0] "Datei löschen" msgstr[1] "Dateien löschen" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "" -"Wenn Sie es löschen, werden diese Assets nicht mehr in anderen Dateien " -"verfügbar sein. Die bereits verwendeten Assets, bleiben in dieser Datei " -"erhalten (das Design wird dadurch nicht zerstört!)." -msgstr[1] "" -"Wenn Sie die Assets löschen, werden diese Assets nicht mehr in anderen " -"Dateien verfügbar sein. Die bereits verwendeten Assets, bleiben in dieser " -"Datei erhalten (das Design wird dadurch nicht zerstört!)." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "" -"Wenn Sie es löschen, werden diese Assets nicht mehr in anderen Dateien " -"verfügbar sein. Die bereits verwendeten Assets, bleiben in dieser Datei " -"erhalten (das Design wird dadurch nicht zerstört!)." -msgstr[1] "" -"Wenn Sie die Assets löschen, werden diese Assets nicht mehr in anderen " -"Dateien verfügbar sein. Die bereits verwendeten Assets, bleiben in dieser " -"Datei erhalten (das Design wird dadurch nicht zerstört!)." +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "Diese Bibliothek ist hier aktiviert: " +msgstr[1] "Diese Bibliotheken sind hier aktiviert: " #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs @@ -1804,29 +1953,6 @@ msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "Möchten Sie diese Datei wirklich löschen?" msgstr[1] "Möchten Sie diese Dateien wirklich löschen?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" -msgstr[0] "" -"Keines der Assets in der Bibliothek dieser Datei ist in Gebrauch. Sie " -"werden zusammen mit der Datei gelöscht." -msgstr[1] "" -"Keines der Assets in den Bibliotheken dieser Datei ist in Gebrauch. Sie " -"werden zusammen mit der Datei gelöscht." - -#: src/app/main/ui/workspace/header.cljs, -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Einige Assets aus der Bibliothek dieser Datei werden hier verwendet:" -msgstr[1] "Einige Assets aus den Bibliotheken dieser Datei werden hier verwendet:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "Einige Assets aus der Bibliothek dieser Datei werden hier verwendet:" -msgstr[1] "Einige der Assets aus den Bibliotheken dieser Datei werden hier verwendet:" - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" @@ -1882,6 +2008,11 @@ msgstr "Einladung senden" msgid "modals.invite-member.emails" msgstr "E-Mails, durch Komma getrennt" +msgid "modals.invite-member.repeated-invitation" +msgstr "" +"Einige E-Mails stammen von aktuellen Teammitgliedern. Ihre Einladungen " +"werden nicht versendet." + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-team-member.title" msgstr "Mitglieder in das Team einladen" @@ -1951,6 +2082,17 @@ msgstr "Sind Sie sicher, dass Sie diesen Benutzer zum Eigentümer befördern wol msgid "modals.promote-owner-confirm.title" msgstr "Zum Eigentümer befördern" +msgid "modals.publish-empty-library.accept" +msgstr "Veröffentlichen" + +msgid "modals.publish-empty-library.message" +msgstr "" +"Ihre Bibliothek ist leer. Sind Sie sicher, dass Sie es veröffentlichen " +"wollen?" + +msgid "modals.publish-empty-library.title" +msgstr "Leere Bibliothek veröffentlichen" + #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" @@ -1975,28 +2117,10 @@ msgstr "Minimal" #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Wenn Sie die Veröffentlichung aufheben, sind diese Assets nicht mehr in " -"anderen Dateien verfügbar. Bereits verwendete Assets bleiben in dieser Datei " -"erhalten (das Design wird nicht beeinträchtigt!)." -msgstr[1] "" -"Wenn Sie die Veröffentlichung aufheben, sind diese Assets nicht mehr in " -"anderen Dateien verfügbar. Bereits verwendete Assets bleiben in dieser Datei " -"erhalten (das Design wird nicht beeinträchtigt!)." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "" -"Wenn Sie die Veröffentlichung aufheben, sind diese Assets nicht mehr in " -"anderen Dateien verfügbar. Bereits verwendete Assets bleiben in diesen " -"Dateien erhalten (das Design wird nicht beeinträchtigt!)." -msgstr[1] "" -"Wenn Sie die Veröffentlichung aufheben, sind diese Assets nicht mehr in " -"anderen Dateien verfügbar. Bereits verwendete Assets bleiben in diesen " -"Dateien erhalten (das Design wird nicht beeinträchtigt!)." +msgid "modals.unpublish-shared-confirm.accept" +msgid_plural "modals.unpublish-shared-confirm.accept" +msgstr[0] "Veröffentlichung aufheben" +msgstr[1] "Veröffentlichung aufheben" #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs @@ -2005,25 +2129,6 @@ msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "Möchten Sie die Veröffentlichung dieser Bibliothek wirklich aufheben?" msgstr[1] "Möchten Sie die Veröffentlichung dieser Bibliotheken wirklich aufheben?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "Keines der Assets in dieser Bibliothek wird verwendet." -msgstr[1] "Keines der Assets in diesen Bibliotheken wird verwendet." - -#: src/app/main/ui/workspace/header.cljs, -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Einige Assets in dieser Bibliothek werden hier verwendet:" -msgstr[1] "Einige Assets in diesen Bibliotheken werden hier verwendet:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "Einige Assets in dieser Bibliothek werden hier verwendet:" -msgstr[1] "Einige Assets in diesen Bibliotheken werden hier verwendet:" - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" @@ -2067,6 +2172,10 @@ msgstr "" msgid "modals.update-remote-component.message" msgstr "Komponente aus einer geteilten Bibliothek aktualiseren" +#: src/app/main/data/common.cljs +msgid "notifications.by-code.upgrade-version" +msgstr "Eine neue Version ist verfügbar, bitte aktualisieren Sie die Seite" + #: src/app/main/ui/dashboard/team.cljs msgid "notifications.invitation-email-sent" msgstr "Einladung erfolgreich gesendet" @@ -2162,12 +2271,6 @@ msgstr "Leitfaden für Mitwirkende" msgid "onboarding-v2.welcome.title" msgstr "Willkommen bei Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "Team später erstellen" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Ihr Teamname" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "Nach der Bennenung Ihres Teams, können Sie andere Personen einladen." @@ -2182,12 +2285,6 @@ msgstr "" "Denken Sie daran, alle einzubeziehen. Entwickler, Designer, Manager... die " "Vielfalt macht's :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Team erstellen und später einladen" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Team erstellen und Einladungen versenden" - msgid "onboarding.choice.team-up.roles" msgstr "Einladen mit der Rolle:" @@ -2241,6 +2338,10 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "Zur Anmeldung" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "Los geht's!" + #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, @@ -2300,6 +2401,9 @@ msgstr "Pfade" msgid "shortcut-subsection.shape" msgstr "Formen" +msgid "shortcut-subsection.text-editor" +msgstr "Texte" + msgid "shortcut-subsection.tools" msgstr "Werkzeuge" @@ -2318,9 +2422,15 @@ msgstr "Punkt hinzufügen" msgid "shortcuts.align-bottom" msgstr "Unten ausrichten" +msgid "shortcuts.align-center" +msgstr "Zentrieren" + msgid "shortcuts.align-hcenter" msgstr "Horizontal zentrieren" +msgid "shortcuts.align-justify" +msgstr "Blocksatz" + msgid "shortcuts.align-left" msgstr "Linksbündig ausrichten" @@ -2336,6 +2446,9 @@ msgstr "Mittig ausrichten (vertikal)" msgid "shortcuts.artboard-selection" msgstr "Zeichenfläche aus Auswahl erstellen" +msgid "shortcuts.bold" +msgstr "Umschalten auf Fettdruck" + msgid "shortcuts.bool-difference" msgstr "Subtrahieren (Boolesche Operation)" @@ -2426,6 +2539,12 @@ msgstr "Horizontal spiegeln" msgid "shortcuts.flip-vertical" msgstr "Vertikal spiegeln" +msgid "shortcuts.font-size-dec" +msgstr "Schriftgröße verkleinern" + +msgid "shortcuts.font-size-inc" +msgstr "Schriftgröße erhöhen" + msgid "shortcuts.go-to-drafts" msgstr "Zu den Entwürfen" @@ -2450,9 +2569,27 @@ msgstr "Einzoomen" msgid "shortcuts.insert-image" msgstr "Bild einfügen" +msgid "shortcuts.italic" +msgstr "Umschalten auf Kursivdruck" + msgid "shortcuts.join-nodes" msgstr "Punkte verbinden" +msgid "shortcuts.letter-spacing-dec" +msgstr "Buchstabenabstand verringern" + +msgid "shortcuts.letter-spacing-inc" +msgstr "Buchstabenabstand erhöhen" + +msgid "shortcuts.line-height-dec" +msgstr "Zeilenhöhe verringern" + +msgid "shortcuts.line-height-inc" +msgstr "Zeilenhöhe erhöhen" + +msgid "shortcuts.line-through" +msgstr "Durchgestrichen" + msgid "shortcuts.make-corner" msgstr "Zur Ecke umwandeln" @@ -2573,6 +2710,15 @@ msgstr "Tastaturkürzel suchen" msgid "shortcuts.select-all" msgstr "Alles auswählen" +msgid "shortcuts.select-next" +msgstr "Nächste Ebene auswählen" + +msgid "shortcuts.select-parent-layer" +msgstr "Übergeordnete Ebene auswählen" + +msgid "shortcuts.select-prev" +msgstr "Vorherige Ebene auswählen" + msgid "shortcuts.separate-nodes" msgstr "Punkte trennen" @@ -2597,6 +2743,18 @@ msgstr "Mit der Vermessung beginnen" msgid "shortcuts.stop-measure" msgstr "Mit der Vermessung abbrechen" +msgid "shortcuts.text-align-center" +msgstr "Zentriert ausrichten" + +msgid "shortcuts.text-align-justify" +msgstr "Blocksatz" + +msgid "shortcuts.text-align-left" +msgstr "Linksbündig ausrichten" + +msgid "shortcuts.text-align-right" +msgstr "Rechtsbündig ausrichten" + msgid "shortcuts.thumbnail-set" msgstr "Miniaturansichten festlegen" @@ -2619,9 +2777,6 @@ msgstr "Fokusmodus umschalten" msgid "shortcuts.toggle-fullscreen" msgstr "Vollbild aktivieren/deaktivieren" -msgid "shortcuts.toggle-grid" -msgstr "Raster ein-/ausblenden" - msgid "shortcuts.toggle-history" msgstr "Verlauf ein-/ausblenden" @@ -2640,15 +2795,6 @@ msgstr "Seitenverhältnis sperren/entsperren" msgid "shortcuts.toggle-rules" msgstr "Lineale ein-/ausblenden" -msgid "shortcuts.toggle-scale-text" -msgstr "Textskalierung aktivieren/deaktivieren" - -msgid "shortcuts.toggle-snap-grid" -msgstr "Am Raster ausrichten" - -msgid "shortcuts.toggle-snap-guide" -msgstr "An Hilfslinien ausrichten" - msgid "shortcuts.toggle-textpalette" msgstr "Textpalette ein-/ausblenden" @@ -2658,6 +2804,9 @@ msgstr "Elemente ein-/ausblenden" msgid "shortcuts.toggle-zoom-style" msgstr "Zoom-Optionen umschalten" +msgid "shortcuts.underline" +msgstr "Unterstrichen" + msgid "shortcuts.undo" msgstr "Rückgängig" @@ -2670,9 +2819,19 @@ msgstr "Maske entfernen" msgid "shortcuts.v-distribute" msgstr "Vertikal verteilen" +msgid "shortcuts.zoom-lense-decrease" +msgstr "Ansicht mit Zoomwerkzeug verkleinern" + +msgid "shortcuts.zoom-lense-increase" +msgstr "Ansicht mit Zoomwerkzeug vergrößern" + msgid "shortcuts.zoom-selected" msgstr "Zur Auswahl zoomen" +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "Der Name des Webhooks darf höchstens 2048 Zeichen lang sein." + #: src/app/main/ui/dashboard/files.cljs msgid "title.dashboard.files" msgstr "%s - Penpot" @@ -2701,6 +2860,10 @@ msgstr "Gemeinsam genutzte Bibliotheken - %s - Penpot" msgid "title.default" msgstr "Penpot - Gestaltungsfreiheit für Teams" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "Profil - Zugangstokens" + #: src/app/main/ui/settings/feedback.cljs msgid "title.settings.feedback" msgstr "Feedback geben - Penpot" @@ -2867,6 +3030,9 @@ msgstr "Löschen" msgid "workspace.assets.duplicate" msgstr "Duplizieren" +msgid "workspace.assets.duplicate-main" +msgstr "Hauptkomponente duplizieren" + #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" @@ -2896,6 +3062,9 @@ msgstr "lokale Bibliothek" msgid "workspace.assets.not-found" msgstr "Keine Assets gefunden" +msgid "workspace.assets.open-library" +msgstr "Bibliotheksdatei öffnen" + #: src/app/main/ui/workspace/sidebar/sitemap.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs @@ -2916,10 +3085,6 @@ msgid_plural "workspace.assets.selected-count" msgstr[0] "%s Element ausgewählt" msgstr[1] "%s Elemente ausgewählt" -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "GETEILT" - #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" @@ -2992,14 +3157,13 @@ msgstr "Radialer Farbverlauf" msgid "workspace.header.menu.disable-dynamic-alignment" msgstr "Dynamische Ausrichtung deaktivieren" +msgid "workspace.header.menu.disable-scale-content" +msgstr "Proportionale Skalierung deaktivieren" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-scale-text" msgstr "Textskalierung deaktivieren" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Am Raster ausrichten deaktivieren" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "Ausrichten an Hilfslinien deaktivieren" @@ -3011,14 +3175,13 @@ msgstr "Ausrichten am Pixel deaktivieren" msgid "workspace.header.menu.enable-dynamic-alignment" msgstr "Dynamische Ausrichtung aktivieren" +msgid "workspace.header.menu.enable-scale-content" +msgstr "Proportionale Skalierung aktivieren" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-scale-text" msgstr "Textskalierung aktivieren" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Am Raster ausrichten" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "An Hilfslinien ausrichten" @@ -3030,10 +3193,6 @@ msgstr "Ausrichten am Pixel aktivieren" msgid "workspace.header.menu.hide-artboard-names" msgstr "Namen von Zeichenflächen ausblenden" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Raster ausblenden" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Farbpalette ausblenden" @@ -3069,6 +3228,9 @@ msgstr "Einstellungen" msgid "workspace.header.menu.option.view" msgstr "Ansicht" +msgid "workspace.header.menu.redo" +msgstr "Wiederherstellen" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.select-all" msgstr "Alles auswählen" @@ -3077,10 +3239,6 @@ msgstr "Alles auswählen" msgid "workspace.header.menu.show-artboard-names" msgstr "Namen der Zeichenflächen anzeigen" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Raster einblenden" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Farbpalette einblenden" @@ -3096,6 +3254,9 @@ msgstr "Lineale einblenden" msgid "workspace.header.menu.show-textpalette" msgstr "Schriftartenpalette anzeigen" +msgid "workspace.header.menu.undo" +msgstr "Rückgängig" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "Zurücksetzen" @@ -3120,6 +3281,10 @@ msgstr "Ungespeicherte Änderungen" msgid "workspace.header.viewer" msgstr "Ansichtsmodus (%s)" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "Vergrößern" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-fill" msgstr "Füllen - Skalieren zum Füllen" @@ -3148,6 +3313,14 @@ msgstr "Hinzufügen" msgid "workspace.libraries.colors" msgstr "%s Farben" +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "In Ihrer Bibliothek sind noch keine Farbstile vorhanden" + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "In Ihrer Bibliothek sind noch keine Textstile vorhanden" + #: src/app/main/ui/workspace/colorpicker/libraries.cljs, #: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" @@ -3198,6 +3371,10 @@ msgstr "BIBLIOTHEKEN" msgid "workspace.libraries.library" msgstr "BIBLIOTHEK" +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "AKTUALISIERUNGEN DER BIBLIOTHEK" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.no-libraries-need-sync" msgstr "" @@ -3236,6 +3413,10 @@ msgstr "%s Textstile" msgid "workspace.libraries.update" msgstr "Aktualisieren" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "alle Änderungen anzeigen" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.updates" msgstr "AKTUALISIERUNG" @@ -3267,6 +3448,15 @@ msgstr "Inhalt beschneiden" msgid "workspace.options.component" msgstr "Komponente" +msgid "workspace.options.component.annotation" +msgstr "Anmerkung" + +msgid "workspace.options.component.create-annotation" +msgstr "Eine Anmerkung erstellen" + +msgid "workspace.options.component.edit-annotation" +msgstr "Eine Anmerkung bearbeiten" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints" msgstr "Beschränkungen" @@ -3321,7 +3511,8 @@ msgstr "Exportieren" msgid "workspace.options.export-multiple" msgstr "Auswahl exportieren" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-object" msgid_plural "workspace.options.export-object" msgstr[0] "Ein Element exportieren" @@ -3891,12 +4082,12 @@ msgid "workspace.options.radius" msgstr "Radius" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Alle Ecken" +msgid "workspace.options.radius-bottom-left" +msgstr "Unten links" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Ecken einzeln anpassen" +msgid "workspace.options.radius-bottom-right" +msgstr "Unten rechts" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3907,12 +4098,12 @@ msgid "workspace.options.radius-top-right" msgstr "Oben rechts" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Unten links" +msgid "workspace.options.radius.all-corners" +msgstr "Alle Ecken" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Unten rechts" +msgid "workspace.options.radius.single-corners" +msgstr "Ecken einzeln anpassen" msgid "workspace.options.recent-fonts" msgstr "Aktuell" @@ -4012,7 +4203,7 @@ msgstr "Punkt" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.diamond-marker" -msgstr "Diamant" +msgstr "Diamant-Marker" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.line-arrow" @@ -4076,26 +4267,10 @@ msgstr "Durchgezogen" msgid "workspace.options.text-options.align-bottom" msgstr "Unten ausrichten" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Zentrieren (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Ausrichtung in der Breite (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Linksbündig ausrichten (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "An Mitte ausrichten" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Rechtsbündig ausrichten (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Oben ausrichten" @@ -4141,6 +4316,22 @@ msgstr "Keine" msgid "workspace.options.text-options.strikethrough" msgstr "Durchgestrichen (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Zentrieren (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Ausrichtung in der Breite (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Linksbündig ausrichten (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Rechtsbündig ausrichten (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Text" @@ -4210,39 +4401,13 @@ msgstr "Ankerpunkte trennen (%s)" msgid "workspace.path.actions.snap-nodes" msgstr "An Ankerpunkten ausrichten (%s)" -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "" -"Um es erneut zu versuchen, können Sie diese Datei neu laden. Wenn das " -"Problem weiterhin besteht, empfehlen wir Ihnen, einen Blick auf die Liste " -"zu werfen und zu überlegen, ob Sie defekte Grafiken löschen wollen." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "Einige Grafiken konnten nicht aktualisiert werden." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "Konvertieren von %s/%s" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "" -"Von nun an sind Grafiken in der Bibliothek auch Komponenten. Das macht sie " -"viel leistungsfähiger." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "Diese Aktualisierung ist eine einmalige Aktion." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "Aktualisierung von %s..." - #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.add-flex" msgstr "Flex-Layout hinzufügen" +msgid "workspace.shape.menu.add-grid" +msgstr "Grid-Layout hinzufügen" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "In den Hintergrund" @@ -4255,6 +4420,9 @@ msgstr "Eins nach hinten" msgid "workspace.shape.menu.copy" msgstr "Kopieren" +msgid "workspace.shape.menu.create-annotation" +msgstr "Anmerkung erstellen" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.create-artboard-from-selection" msgstr "Auswahl auf Zeichenfläche" @@ -4263,6 +4431,9 @@ msgstr "Auswahl auf Zeichenfläche" msgid "workspace.shape.menu.create-component" msgstr "Komponente erstellen" +msgid "workspace.shape.menu.create-multiple-components" +msgstr "Mehrere Komponenten erstellen" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.cut" msgstr "Ausschneiden" @@ -4649,6 +4820,10 @@ msgstr "Verlauf" msgid "workspace.updates.dismiss" msgstr "Ignorieren" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "Mehr Info" + #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.there-are-updates" msgstr "Es gibt Updates in gemeinsam genutzten Bibliotheken" @@ -4660,105 +4835,343 @@ msgstr "Aktualisieren" msgid "workspace.viewport.click-to-close-path" msgstr "Klicken Sie, um den Pfad zu schließen" -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-duplicate-file" -msgid_plural "dashboard.success-delete-file" -msgstr[0] "Ihre Datei wurde erfolgreich dupliziert" -msgstr[1] "Ihre Dateien wurden erfolgreich dupliziert" +msgid "workspace.options.component.copy" +msgstr "Kopieren" -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-weight" -msgstr "Strichstärke" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "Developer" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "Produkt- oder Projektmanager" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "Marketing" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "Ich arbeite an einem persönlichen Projekt" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "Rechteck" + +msgid "workspace.options.component.main" +msgstr "Main" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "Ich bin ein Freelancer" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "...Branding, Illustrationen, Marketingmaterialien, usw." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.some" +msgstr "Einige" + +msgid "workspace.layout_grid.editor.title" +msgstr "Raster bearbeiten" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "Mehr als 50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "Weiter" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "Wie groß ist Ihr Team?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" +msgstr "... Wireframes, User Journeys & Flows, Navigationsbäume usw." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "Viel" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "Start" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "Diamant" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "Wie wollen Sie Penpot nutzen?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.select-option" +msgstr "Wählen Sie eine Option" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "Designer" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "Keine" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +msgid "settings.detach" +msgstr "Trennen" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "Sketch" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "Dreieck" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.accept" -msgid_plural "modals.unpublish-shared-confirm.accept" -msgstr[0] "Veröffentlichung aufheben" -msgstr[1] "Veröffentlichung aufheben" +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "Es ist in keiner Datei aktiviert." +msgstr[1] "Sie sind in keiner Datei aktiviert." -msgid "shortcuts.font-size-inc" -msgstr "Schriftgröße erhöhen" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "Sonstiges (bitte angeben)" -msgid "shortcut-subsection.text-editor" -msgstr "Texte" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "Testen Sie Penpot, um zu sehen, ob es für das Team geeignet ist " -msgid "shortcuts.align-center" -msgstr "Zentrieren" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "Schüler oder Lehrer" -msgid "shortcuts.font-size-dec" -msgstr "Schriftgröße verkleinern" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2-10" -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-delete-file" -msgid_plural "dashboard.success-delete-file" -msgstr[0] "Ihre Datei wurde erfolgreich gelöscht" -msgstr[1] "Ihre Dateien wurden erfolgreich gelöscht" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "Feedback für mein Team-Projekt hinterlassen" -msgid "workspace.header.menu.undo" -msgstr "Rückgängig" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31-50" -msgid "workspace.header.menu.disable-scale-content" -msgstr "Proportionale Skalierung deaktivieren" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "InVision" -msgid "workspace.header.menu.enable-scale-content" -msgstr "Proportionale Skalierung aktivieren" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canva" -msgid "modals.invite-member.repeated-invitation" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "Mehr über Penpot erfahren" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "Zurück" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "Mit der Arbeit an meinem Projekt beginnen" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "Pfeil" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "Mit welchem Design-Tool haben Sie mehr Erfahrung?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "Was ist Ihre Rolle?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "Gründer/VP" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "Kreis" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11-30" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "... Interface-Design, visuelle Assets, Designsysteme usw." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "Arbeiten an Konzeptideen" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" msgstr "" -"Einige E-Mails stammen von aktuellen Teammitgliedern. Ihre Einladungen " -"werden nicht versendet." +"Ihr Feedback wird uns helfen, Ihre Gewohnheiten und Vorlieben zu verstehen, " +"damit wir Penpot weiterhin zu einem nützlichen und angenehmen Werkzeug " +"machen können." -msgid "shortcuts.letter-spacing-inc" -msgstr "Buchstabenabstand erhöhen" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.describe-your-experience-working-on" +msgstr "" +"Wie würden Sie Ihre Erfahrungen bei der Arbeit an … am besten beschreiben?" -msgid "shortcuts.select-prev" -msgstr "Vorherige Ebene auswählen" +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.shared-library" +msgstr "Geteilte Bibliothek" -msgid "shortcuts.select-next" -msgstr "Nächste Ebene auswählen" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "Testen Sie Penpot, bevor Sie es auf einem eigenen Server verwenden" -msgid "shortcuts.align-justify" -msgstr "Blocksatz" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "Code aus meinem Teamprojekt erhalten " -msgid "shortcuts.bold" -msgstr "Umschalten auf Fettdruck" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" +msgstr[0] "" +"Assets, die bereits in dieser Datei verwendet wurden, bleiben dort erhalten (" +"das Design bleibt erhalten)." +msgstr[1] "" +"Assets, die bereits in diesen Dateien verwendet wurden, bleiben dort " +"erhalten (das Design bleibt erhalten)." -msgid "shortcuts.italic" -msgstr "Umschalten auf Kursivdruck" +msgid "modals.add-shared-confirm-empty.hint" +msgstr "" +"Ihre Bibliothek ist leer. Sobald es als Geteilte Bibliothek hinzugefügt " +"wurde, können die von Ihnen erstellten Assets in den übrigen Dateien " +"verwendet werden. Sind Sie sicher, dass Sie es veröffentlichen möchten?" -msgid "shortcuts.letter-spacing-dec" -msgstr "Buchstabenabstand verringern" +msgid "media.radial" +msgstr "Radial" -msgid "shortcuts.line-height-inc" -msgstr "Zeilenhöhe erhöhen" +msgid "workspace.top-bar.read-only.done" +msgstr "Fertig" -msgid "shortcuts.line-height-dec" -msgstr "Zeilenhöhe verringern" +msgid "media.image" +msgstr "Bild" -msgid "workspace.header.menu.redo" -msgstr "Wiederherstellen" +msgid "media.linear" +msgstr "Linear" + +msgid "media.gradient" +msgstr "Verlauf" + +msgid "media.solid" +msgstr "Einfarbig" + +msgid "media.choose-image" +msgstr "Bild auswählen" + +msgid "workspace.options.guides.title" +msgstr "Hilfslinien" #, markdown -msgid "dashboard.fonts.warning-text" +msgid "workspace.top-bar.read-only" +msgstr "**Inspektionsmodus** (nur Ansicht)" + +#: src/app/main/errors.cljs +msgid "errors.version-not-supported" +msgstr "Die Datei hat eine inkompatible Versionsnummer" + +#: src/app/main/errors.cljs +msgid "errors.team-feature-mismatch" +msgstr "Inkompatible Funktion '%s' erkannt" + +msgid "errors.validation" +msgstr "Validierungsfehler" + +msgid "errors.paste-data-validation" +msgstr "Ungültige Daten in der Zwischenablage" + +msgid "labels.search" +msgstr "Suchen" + +msgid "onboarding.choice.team-up.start-without-a-team" +msgstr "Ohne Team starten" + +msgid "onboarding.choice.team-up.start-without-a-team-description" +msgstr "Sie können später ein Team erstellen." + +msgid "onboarding.choice.team-up.continue-without-a-team" +msgstr "Ohne Team fortsetzen" + +msgid "onboarding.choice.team-up.create-team-and-send-invites" +msgstr "Team erstellen und Einladungen versenden" + +msgid "onboarding.choice.team-up.continue-creating-team" +msgstr "Mit der Erstellung eines Teams fortsetzen" + +msgid "onboarding.choice.team-up.create-team-without-inviting" +msgstr "Team ohne Einladungen erstellen" + +msgid "onboarding.choice.team-up.create-team-and-invite" +msgstr "Team erstellen & einladen" + +msgid "onboarding.choice.team-up.create-team-without-invite" +msgstr "Team erstellen" + +msgid "onboarding.choice.team-up.create-team-and-send-invites-description" +msgstr "Sie können später einladen" + +msgid "workspace.layout_grid.editor.top-bar.done" +msgstr "Fertig" + +msgid "workspace.layout_grid.editor.options.exit" +msgstr "Beenden" + +msgid "workspace.layout_grid.editor.options.edit-grid" +msgstr "Grid bearbeiten" + +msgid "workspace.options.component.swap" +msgstr "Komponente austauschen" + +msgid "workspace.options.component.swap.empty" +msgstr "Es gibt noch keine Assets in dieser Bibliothek" + +#: src/app/main/ui/auth/register.cljs +#, markdown +msgid "auth.terms-privacy-agreement-md" msgstr "" -"Wir haben ein mögliches Problem in Ihren Schriften festgestellt, das mit den " -"vertikalen Metriken für verschiedene Betriebssysteme zusammenhängt. Um dies " -"zu überprüfen, können Sie Online-Dienste wie [diesen](https://vertical-" -"metrics.netlify.app/) verwenden. Außerdem empfehlen wir die Verwendung von " -"[Transfonter](https://transfonter.org/), um Webfonts zu generieren und " -"Fehler zu beheben. " +"Wenn Sie ein neues Konto erstellen, stimmen Sie unseren " +"[Nutzungsbedingungen](%s) und [Datenschutzrichtlinien](%s) zu." -msgid "shortcuts.line-through" -msgstr "Durchgestrichen" +msgid "labels.share" +msgstr "Teilen" -msgid "shortcuts.underline" -msgstr "Unterstrichen" +msgid "inspect.attributes.typography.text-transform.unset" +msgstr "Nicht gesetzt" -msgid "shortcuts.zoom-lense-decrease" -msgstr "Ansicht mit Zoomwerkzeug verkleinern" +msgid "workspace.layout_grid.editor.top-bar.locate" +msgstr "Lokalisieren" -msgid "shortcuts.zoom-lense-increase" -msgstr "Ansicht mit Zoomwerkzeug vergrößern" +#: src/app/main/errors.cljs +msgid "errors.file-feature-mismatch" +msgstr "" +"Es scheint eine Nichtübereinstimmung zwischen den aktivierten Funktionen und " +"den Funktionen der Datei zu geben. Die Migrationen für '%s' müssen " +"durchgeführt werden, bevor die Datei geöffnet werden kann." -msgid "workspace.assets.duplicate-main" -msgstr "Hauptkomponente duplizieren" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow" +msgstr "Flow" diff --git a/frontend/translations/el.po b/frontend/translations/el.po index 36b81f8db2..cc62a2301d 100644 --- a/frontend/translations/el.po +++ b/frontend/translations/el.po @@ -39,7 +39,8 @@ msgstr "" "Αυτή είναι μια υπηρεσία DEMO, ΜΗ ΧΡΗΣΙΜΟΠΟΙΕΙΤΕ για πραγματική εργασία, τα " "έργα θα σβήνονται περιοδικά." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" msgstr "Email" @@ -159,7 +160,8 @@ msgstr "" msgid "auth.verification-email-sent" msgstr "Εχουμε στείλει ενα mail επαλήθευσης " -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Προσθήκη ως Κοινόχρηστη βιβλιοθήκη" @@ -183,7 +185,8 @@ msgstr "Το Penpot σας" msgid "dashboard.delete-team" msgstr "Διαγραφή ομάδας" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate" msgstr "Αντιγραφή" @@ -191,7 +194,8 @@ msgstr "Αντιγραφή" msgid "dashboard.invite-profile" msgstr "Πρόσκληση στην ομάδα" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" msgstr "Αφήστε την ομάδα" @@ -203,7 +207,8 @@ msgstr "Κοινόχρηστες βιβλιοθήκες" msgid "dashboard.loading-files" msgstr "φόρτωση των αρχείων σας …" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "Μετακίνηση" @@ -211,7 +216,8 @@ msgstr "Μετακίνηση" msgid "dashboard.move-to-other-team" msgstr "Μετακίνηση σε άλλη ομάδα" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" msgstr "Νεο αρχείο" @@ -263,7 +269,8 @@ msgstr "Εργα" msgid "dashboard.remove-account" msgstr "Θέλετε να καταργήσετε τον λογαριασμό σας;" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" msgstr "Διαγραφή ως Κοινόχρηστη βιβλιοθήκη" @@ -303,7 +310,8 @@ msgstr "Το έργο σας έχει αναπαραχθεί με επιτυχί msgid "dashboard.success-duplicate-project" msgstr "Το έργο σας έχει αναπαραχθεί με επιτυχία" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-file" msgstr "Το έργο σας μετακινήθηκε με επιτυχία" @@ -335,7 +343,9 @@ msgstr "Αποτελέσματα αναζήτησης" msgid "dashboard.type-something" msgstr "Γράψτε κάτι για αναζήτηση" -#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/profile.cljs, +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Ενημέρωση επιλογών" @@ -351,7 +361,11 @@ msgstr "Email" msgid "dashboard.your-name" msgstr "Το όνομα σου" -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" msgstr "Το Penpot σας" @@ -371,7 +385,8 @@ msgstr "Είσαι σίγουρος;" msgid "errors.clipboard-not-implemented" msgstr "Το πρόγραμμα περιήγησής σας δεν μπορεί να εκτελέσει αυτήν τη λειτουργία" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/change_email.cljs msgid "errors.email-already-exists" msgstr "Το email έχει ήδη χρησιμοποιηθεί" @@ -379,7 +394,10 @@ msgstr "Το email έχει ήδη χρησιμοποιηθεί" msgid "errors.email-already-validated" msgstr "Αυτό το email έχει ήδη επικυρωθεί." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" msgstr "Το email «%s» έχει πολλές μόνιμες αναφορές αναπήδησης." @@ -387,7 +405,8 @@ msgstr "Το email «%s» έχει πολλές μόνιμες αναφορές msgid "errors.email-invalid-confirmation" msgstr "Το email επιβεβαίωσης πρέπει να ταιριάζει" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.generic" msgstr "Έχει συμβεί κάτι λάθος." @@ -405,7 +424,7 @@ msgstr "" "Φαίνεται ότι το περιεχόμενο της εικόνας δεν ταιριάζει με την επέκταση " "αρχείου." -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" msgstr "Φαίνεται ότι δεν είναι έγκυρη εικόνα." @@ -427,7 +446,9 @@ msgstr "Ο κωδικός πρόσβασης πρέπει να είναι του msgid "errors.registration-disabled" msgstr "Η εγγραφή είναι απενεργοποιημένη αυτήν τη στιγμή." -#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/data/media.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "errors.unexpected-error" msgstr "Προέκυψε ένα μη αναμενόμενο σφάλμα." @@ -514,7 +535,8 @@ msgstr "Υψος" msgid "inspect.attributes.layout.left" msgstr "Αριστερά" -#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs +#: src/app/main/ui/inspect/attributes/layout.cljs, +#: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.layout.radius" msgstr "Aκτίνα" @@ -534,23 +556,16 @@ msgstr "Πλάτος" msgid "inspect.attributes.shadow" msgstr "Σκιά " -#: src/app/main/ui/inspect/attributes/shadow.cljs -msgid "inspect.attributes.shadow.shorthand.spread" -msgstr "S" - #: src/app/main/ui/inspect/attributes/stroke.cljs msgid "inspect.attributes.stroke" msgstr "περίγραμμα" -#, permanent msgid "inspect.attributes.stroke.alignment.center" msgstr "Κέντρο" -#, permanent msgid "inspect.attributes.stroke.alignment.inner" msgstr "Μέσα" -#, permanent msgid "inspect.attributes.stroke.alignment.outer" msgstr "Εξω" @@ -665,7 +680,7 @@ msgstr "Πληροφορίες" msgid "labels.accept" msgstr "Αποδέχομαι" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "Διαχειριστής" @@ -695,7 +710,8 @@ msgstr "Σχόλια" msgid "labels.confirm-password" msgstr "Επιβεβαίωση Κωδικού" -#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team" msgstr "Δημιουργήστε μια νέα ομάδα" @@ -703,7 +719,8 @@ msgstr "Δημιουργήστε μια νέα ομάδα" msgid "labels.dashboard" msgstr "πίνακας" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "Διαγραφή" @@ -715,7 +732,10 @@ msgstr "Διαγραφή σχολίου" msgid "labels.delete-comment-thread" msgstr "Διαγραφή νήματος" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/files.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.drafts" msgstr "Πρόχειρα" @@ -723,7 +743,7 @@ msgstr "Πρόχειρα" msgid "labels.edit" msgstr "Edit" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "Editor" @@ -735,7 +755,9 @@ msgstr "Τα σχόλια απενεργοποιήθηκαν" msgid "labels.feedback-sent" msgstr "Εστάλη γνώμη" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "Δώστε μας τη γνώμη σας" @@ -764,7 +786,7 @@ msgstr "Γλώσσα" msgid "labels.logout" msgstr "Αποσύνδεση" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "Μέλη" @@ -772,7 +794,8 @@ msgstr "Μέλη" msgid "labels.new-password" msgstr "Νέος κωδικός πρόσβασης" -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +#: src/app/main/ui/workspace/comments.cljs, +#: src/app/main/ui/dashboard/comments.cljs msgid "labels.no-comments-available" msgstr "Δεν έχετε εκκρεμείς ειδοποιήσεις σχολίων" @@ -810,11 +833,13 @@ msgstr "Μόνο το δικό σου" msgid "labels.owner" msgstr "Ιδιοκτήτης" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.password" msgstr "Κωδικός πρόσβασης" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.profile" msgstr "Προφίλ" @@ -822,11 +847,14 @@ msgstr "Προφίλ" msgid "labels.projects" msgstr "Εργα" -#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/workspace/libraries.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" msgstr "Διαγραφή" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.rename" msgstr "Μετονομασία" @@ -834,7 +862,7 @@ msgstr "Μετονομασία" msgid "labels.rename-team" msgstr "Μετονομασία ομάδας " -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Ξαναδοκιμάσετε" @@ -861,11 +889,12 @@ msgstr "Είμαστε σε προγραμματισμένη συντήρηση msgid "labels.service-unavailable.main-message" msgstr "Η υπηρεσία δεν είναι διαθέσιμη" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.settings" msgstr "Σύνθεση" -#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs +#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.share-prototype" msgstr "Μοιραστείτε το link" @@ -897,22 +926,25 @@ msgstr "Θεατής" msgid "labels.write-new-comment" msgstr "Γράψτε ένα νέο σχόλιο" -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "Φόρτωση εικόνας ..." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" msgstr "Προσθήκη ως Κοινόχρηστη βιβλιοθήκη" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.hint" msgstr "" "Μόλις προστεθεί ως Κοινόχρηστη βιβλιοθήκη, τα στοιχεία αυτής της " "βιβλιοθήκης αρχείων θα είναι διαθέσιμα για χρήση μεταξύ των υπόλοιπων " "αρχείων σας." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.message" msgstr "Προσθήκη “%s” ως Κοινόχρηστη βιβλιοθήκη" @@ -1072,35 +1104,42 @@ msgstr "Είστε σίγουροι ότι θέλετε να προωθήσετ msgid "modals.promote-owner-confirm.title" msgstr "Προώθηση σε κάτοχο" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" msgstr "Κατάργηση ως Κοινόχρηστη βιβλιοθήκη" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.hint" msgstr "" "Μόλις καταργηθεί ως Κοινόχρηστη βιβλιοθήκη, η Βιβλιοθήκη αρχείων αυτού του " "αρχείου θα σταματήσει να είναι διαθέσιμη για χρήση στα υπόλοιπα αρχεία σας." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.message" msgstr "Καταργήστε το “%s” ως Κοινόχρηστη βιβλιοθήκη" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.accept" msgstr "Ενημέρωση στοιχείου" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.cancel" msgstr "Ακύρωση" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.hint" msgstr "" "Πρόκειται να ενημερώσετε ένα στοιχείο σε μια κοινόχρηστη βιβλιοθήκη. Αυτό " "μπορεί να επηρεάσει άλλα αρχεία που το χρησιμοποιούν." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.message" msgstr "Ενημερώστε ένα στοιχείο σε μια κοινόχρηστη βιβλιοθήκη" @@ -1126,7 +1165,12 @@ msgstr "Το email επαλήθευσης εστάλη στο %s. Ελέγξτε msgid "profile.recovery.go-to-login" msgstr "Μεταβείτε στη σύνδεση" -#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "Μικτός " @@ -1150,10 +1194,6 @@ msgstr "Πλήρης οθόνη" msgid "viewer.header.share.copy-link" msgstr "Αντιγραφή link" -#: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.subtitle" -msgstr "Όποιος έχει τον link θα έχει πρόσβαση" - #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.show-interactions" msgstr "Εμφάνιση αλληλεπιδράσεων" @@ -1206,27 +1246,34 @@ msgstr "Περιουσιακά στοιχεία" msgid "workspace.assets.box-filter-all" msgstr "Όλα τα περιουσιακά στοιχεία" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.colors" msgstr "Χρώματα" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.components" msgstr "Συστατικά" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "Διαγραφή" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.duplicate" msgstr "Αντιγραφή" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" msgstr "Επεξεργασία" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.graphics" msgstr "Γραφικά" @@ -1238,7 +1285,9 @@ msgstr "Βιβλιοθήκες" msgid "workspace.assets.not-found" msgstr "Δεν βρέθηκαν στοιχεία" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename" msgstr "Μετονομασία" @@ -1246,11 +1295,8 @@ msgstr "Μετονομασία" msgid "workspace.assets.search" msgstr "Αναζήτηση στοιχείων" +#: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "ΜΟΡΦΗ" - -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "Τυπογραφίες" @@ -1278,7 +1324,9 @@ msgstr "Διάστημα γραμμάτων" msgid "workspace.assets.typography.line-height" msgstr "Υψος γραμμής" -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/inspect/attributes/text.cljs, src/app/main/ui/inspect/attributes/text.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs msgid "workspace.assets.typography.sample" msgstr "Ag" @@ -1286,11 +1334,13 @@ msgstr "Ag" msgid "workspace.assets.typography.text-transform" msgstr "Μετασχηματισμός κειμένου" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "Γραμμική κλίση" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.radial" msgstr "Ακτινική κλίση" @@ -1298,22 +1348,10 @@ msgstr "Ακτινική κλίση" msgid "workspace.header.menu.disable-dynamic-alignment" msgstr "Απενεργοποίηση δυναμικής ευθυγράμμισης" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Απενεργοποιήστε τη σύνδεση στο πλέγμα" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-dynamic-alignment" msgstr "Ενεργοποίηση δυναμικής ευθυγράμμισης" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Σύνδεση στο πλέγμα" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Απόκρυψη πλεγμάτων" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Απόκρυψη παλέτας χρωμάτων" @@ -1326,10 +1364,6 @@ msgstr "Απόκρυψη κανόνες" msgid "workspace.header.menu.select-all" msgstr "Επιλογή όλων" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Εμφάνιση πλέγματος" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Εμφάνιση παλέτας χρωμάτων" @@ -1366,11 +1400,13 @@ msgstr "Προσθήκη" msgid "workspace.libraries.colors" msgstr "%s χρώματα" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" msgstr "Βιβλιοθήκη αρχείων" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "Πρόσφατα χρώματα" @@ -1442,9 +1478,6 @@ msgstr "Ενημέρωση" msgid "workspace.libraries.updates" msgstr "ΕΝΗΜΕΡΩΣΕΙΣ" -msgid "workspace.library.store" -msgstr "Προκαθορισμένες" - #: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "workspace.options.blur-options.title" msgstr "Θολούρα" @@ -1469,11 +1502,13 @@ msgstr "Συστατικό" msgid "workspace.options.design" msgstr "Σχέδιο" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export" msgstr "Εξαγωγή" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-object" msgstr "Εξαγωγή σχήματος" @@ -1481,7 +1516,8 @@ msgstr "Εξαγωγή σχήματος" msgid "workspace.options.export.suffix" msgstr "Κατάληξη" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.exporting-object" msgstr "Εξαγωγή ..." @@ -1653,7 +1689,8 @@ msgstr "στρώματα Ομάδα" msgid "workspace.options.layer-options.title.multiple" msgstr "Επιλεγμένα επίπεδα" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Θέση" @@ -1726,7 +1763,8 @@ msgstr "Ομαδική σκιά" msgid "workspace.options.shadow-options.title.multiple" msgstr "Επιλογή σκιών" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.size" msgstr "Μέγεθος" @@ -1770,26 +1808,10 @@ msgstr "Στερεός" msgid "workspace.options.text-options.align-bottom" msgstr "Στοίχιση κάτω" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Ευθυγράμμιση κέντρο (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Δικαιολόγηση (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Στοίχιση αριστερά (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Στοίχιση στο κέντρο" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Για ευθυγράμμιση προς τα δεξιά (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Ευθυγραμμίστε την κορυφή" @@ -1818,7 +1840,8 @@ msgstr "Υψος γραμμής" msgid "workspace.options.text-options.lowercase" msgstr "Πεζά" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.none" msgstr "Κανένας" @@ -1826,6 +1849,22 @@ msgstr "Κανένας" msgid "workspace.options.text-options.strikethrough" msgstr "Διαγράμμιση (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Ευθυγράμμιση κέντρο (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Δικαιολόγηση (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Στοίχιση αριστερά (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Για ευθυγράμμιση προς τα δεξιά (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Κείμενο" @@ -1880,7 +1919,9 @@ msgstr "Αποκοπή" msgid "workspace.shape.menu.delete" msgstr "Διαγραφή" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Αποσύνδεση παρουσίας" @@ -1920,15 +1961,19 @@ msgstr "Κρύβω" msgid "workspace.shape.menu.lock" msgstr "Κλείδωμα" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.mask" msgstr "Μάσκα" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.paste" msgstr "Επικόλληση" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "Επαναφορά παρακάμψεων" @@ -1956,7 +2001,8 @@ msgstr "Ιστορικό (%s)" msgid "workspace.sidebar.layers" msgstr "στρώσεις" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/inspect/attributes/svg.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, +#: src/app/main/ui/inspect/attributes/svg.cljs msgid "workspace.sidebar.options.svg-attrs.title" msgstr "Εισαγόμενα χαρακτηριστικά SVG" @@ -2141,3 +2187,24 @@ msgstr "Ενημέρωση" msgid "workspace.viewport.click-to-close-path" msgstr "Κάντε κλικ για να κλείσετε τη διαδρομή" + +#~ msgid "feedback.chat-subtitle" +#~ msgstr "Νιώθετε σαν να μιλάτε; Συνομιλήστε μαζί μας στο Gitter" + +#~ msgid "inspect.attributes.shadow.shorthand.offset-x" +#~ msgstr "X" + +#~ msgid "labels.images" +#~ msgstr "εικόνες" + +#~ msgid "viewer.header.share.placeholder" +#~ msgstr "Μοιραστείτε το link θα εμφανιστεί εδώ" + +#~ msgid "workspace.library.libraries" +#~ msgstr "βιβλιοθήκες" + +#~ msgid "workspace.options.blur-options.layer-blur" +#~ msgstr "Στρώμα" + +#~ msgid "workspace.options.text-options.google" +#~ msgstr "Google" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e8bd9cdf12..64d26fe662 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -53,21 +53,17 @@ msgstr "Full Name" msgid "auth.login-here" msgstr "Login here" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs -msgid "auth.name.too-long" -msgstr "The name must contain at most 250 characters." - -#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs -msgid "auth.name.not-all-space" -msgstr "The name must contain some character other than space." - #: src/app/main/ui/auth/login.cljs msgid "auth.login-submit" msgstr "Login" #: src/app/main/ui/auth/login.cljs -msgid "auth.login-title" -msgstr "Great to see you again!" +msgid "auth.login-account-title" +msgstr "Log into my account" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-tagline" +msgstr "Penpot is the free open-source design tool for Design and Code collaboration" #: src/app/main/ui/auth/login.cljs msgid "auth.login-with-github-submit" @@ -89,6 +85,14 @@ msgstr "LDAP" msgid "auth.login-with-oidc-submit" msgstr "OpenID" +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "The name must contain some character other than space." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "The name must contain at most 250 characters." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.new-password" msgstr "Type a new password" @@ -168,15 +172,23 @@ msgid "auth.terms-of-service" msgstr "Terms of service" #: src/app/main/ui/auth/register.cljs +#, markdown +msgid "auth.terms-privacy-agreement-md" +msgstr "" +"When creating a new account, you agree to our [terms of service](%s) and [privacy policy](%s)." + msgid "auth.terms-privacy-agreement" msgstr "" -"When creating a new account, you agree to our terms of service and privacy " -"policy." +"When creating a new account, you agree to ourf terms of service and privacy policy." #: src/app/main/ui/auth/register.cljs msgid "auth.verification-email-sent" msgstr "We've sent a verification email to" +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "...branding, illustrations, marketing pieces, etc." + msgid "common.publish" msgstr "Publish" @@ -275,93 +287,33 @@ msgstr "Start the tour" msgid "dasboard.walkthrough-hero.title" msgstr "Interface Walkthrough" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.add-shared" -msgstr "Add as Shared Library" - -#: src/app/main/ui/settings/profile.cljs -msgid "dashboard.change-email" -msgstr "Change email" - #: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.personal" -msgstr "Personal access tokens" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.personal.description" -msgstr "Personal access tokens function like an alternative to our login/password authentication system and can be used to allow an application to access the internal Penpot API" +msgid "dashboard.access-tokens.copied-success" +msgstr "Copied token" #: src/app/main/ui/settings/access-tokens.cljs msgid "dashboard.access-tokens.create" msgstr "Generate new token" #: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.empty.no-access-tokens" -msgstr "You have no tokens so far." +msgid "dashboard.access-tokens.create.success" +msgstr "Access token created successfully." #: src/app/main/ui/settings/access-tokens.cljs msgid "dashboard.access-tokens.empty.add-one" msgstr "Press the button \"Generate new token\" to generate one." #: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.create-access-token.title" -msgstr "Generate access token" +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "You have no tokens so far." #: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.create-access-token.name.label" -msgstr "Name" +msgid "dashboard.access-tokens.errors-required-name" +msgstr "The name is required" #: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.create-access-token.name.placeholder" -msgstr "The name can help to know what's the token for" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.create-access-token.expiration-date.label" -msgstr "Expiration date" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.create-access-token.submit-label" -msgstr "Create token" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.delete-acces-token.title" -msgstr "Delete token" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.delete-acces-token.message" -msgstr "Are you sure you want to delete this token?" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.delete-acces-token.accept" -msgstr "Delete token" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.create.success" -msgstr "Access token created successfully." - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.expires-on" -msgstr "Expires on %s" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.expired-on" -msgstr "Expired on %s" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.no-expiration" -msgstr "No expiration date" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.create-access-token.copy-token" -msgstr "Copy token" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.copied-success" -msgstr "Copied token" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.expiration-never" -msgstr "Never" +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 days" #: src/app/main/ui/settings/access-tokens.cljs msgid "dashboard.access-tokens.expiration-30-days" @@ -376,12 +328,31 @@ msgid "dashboard.access-tokens.expiration-90-days" msgstr "90 days" #: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.expiration-180-days" -msgstr "180 days" +msgid "dashboard.access-tokens.expiration-never" +msgstr "Never" #: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.errors-required-name" -msgstr "The name is required" +msgid "dashboard.access-tokens.expired-on" +msgstr "Expired on %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "Expires on %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "No expiration date" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Personal access tokens" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Personal access tokens function like an alternative to our login/password " +"authentication system and can be used to allow an application to access the " +"internal Penpot API" #: src/app/main/ui/settings/access-tokens.cljs msgid "dashboard.access-tokens.token-will-expire" @@ -391,6 +362,14 @@ msgstr "The token will expire on %s" msgid "dashboard.access-tokens.token-will-not-expire" msgstr "The token has no expiration date" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.add-shared" +msgstr "Add as Shared Library" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.change-email" +msgstr "Change email" + #: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs msgid "dashboard.copy-suffix" msgstr "(copy)" @@ -559,6 +538,9 @@ msgstr "Import Penpot files" msgid "dashboard.import.analyze-error" msgstr "Oops! We couldn't import this file" +msgid "dashboard.import.analyze-error.components-v2" +msgstr "File with components v2 activated but this team doesn't support it yet." + msgid "dashboard.import.import-error" msgstr "There was a problem importing the file. The file wasn't imported." @@ -816,10 +798,6 @@ msgstr "No webhooks created so far." msgid "dashboard.webhooks.update.success" msgstr "Webhook updated successfully." -#: src/app/main/ui/dashboard/team.cljs -msgid "team.webhooks.max-length" -msgstr "The webhook name must contain at most 2048 characters." - #: src/app/main/ui/settings.cljs msgid "dashboard.your-account-title" msgstr "Your account" @@ -873,6 +851,9 @@ msgstr "The font %s could not be loaded" msgid "errors.bad-font-plural" msgstr "The fonts %s could not be loaded" +msgid "errors.cannot-upload" +msgstr "Cannot upload the media file." + #: src/app/main/data/workspace.cljs msgid "errors.clipboard-not-implemented" msgstr "Your browser cannot do this operation" @@ -907,12 +888,28 @@ msgstr "The email «%s» has been reported as spam or permanently bounce." msgid "errors.feature-mismatch" msgstr "" "Looks like you are opening a file that has the feature '%s' enabled but " -"your penpot frontend does not supports it or has it disabled." +"the current penpot version does not supports it or has it disabled." + +#: src/app/main/errors.cljs +msgid "errors.version-not-supported" +msgstr "" +"File has an incompatible version number" + +#: src/app/main/errors.cljs +msgid "errors.file-feature-mismatch" +msgstr "" +"It seems that there is a mismatch between the enabled features and the " +"features of the file you are trying to open. Migrations for '%s' need " +"to be applied before the file can be opened." #: src/app/main/errors.cljs msgid "errors.feature-not-supported" msgstr "Feature '%s' is not supported." +#: src/app/main/errors.cljs +msgid "errors.team-feature-mismatch" +msgstr "Detected incompatible feature '%s'" + #: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.generic" msgstr "Something wrong has happened." @@ -995,7 +992,7 @@ msgid "errors.webhooks.invalid-uri" msgstr "URL does not pass validation." msgid "errors.webhooks.last-delivery" -msgstr "Last delivery was not successfull." +msgstr "Last delivery was not successful." msgid "errors.webhooks.ssl-validation" msgstr "Error on SSL validation." @@ -1017,8 +1014,12 @@ msgstr "Email or password is incorrect." msgid "errors.wrong-old-password" msgstr "Old password is incorrect" -msgid "errors.cannot-upload" -msgstr "Cannot upload the media file." +msgid "errors.validation" +msgstr "Validation Error" + +msgid "errors.paste-data-validation" +msgstr "Invalid data in clipboard" + #: src/app/main/ui/settings/feedback.cljs msgid "feedback.description" @@ -1054,7 +1055,7 @@ msgstr "Email" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Go to Twitter" +msgstr "Go to X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -1062,7 +1063,7 @@ msgstr "Here to help with your technical queries." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Twitter support account" +msgstr "X support account" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -1223,6 +1224,9 @@ msgstr "Lower Case" msgid "inspect.attributes.typography.text-transform.none" msgstr "None" +msgid "inspect.attributes.typography.text-transform.unset" +msgstr "Unset" + msgid "inspect.attributes.typography.text-transform.titlecase" msgstr "Title Case" @@ -1290,6 +1294,10 @@ msgstr "Shortcuts" msgid "labels.accept" msgstr "Accept" +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "Access tokens" + msgid "labels.active" msgstr "Active" @@ -1356,9 +1364,6 @@ msgstr "Copy link" msgid "labels.create" msgstr "Create" -msgid "labels.discard" -msgstr "Discard" - #: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team" msgstr "Create new team" @@ -1394,6 +1399,9 @@ msgstr "Delete invitation" msgid "labels.delete-multi-files" msgstr "Delete %s files" +msgid "labels.discard" +msgstr "Discard" + #: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "labels.drafts" msgstr "Drafts" @@ -1559,10 +1567,6 @@ msgstr "Owner" msgid "labels.password" msgstr "Password" -#: src/app/main/ui/settings/sidebar.cljs -msgid "labels.access-tokens" -msgstr "Access tokens" - #: src/app/main/ui/dashboard/team.cljs msgid "labels.pending-invitation" msgstr "Pending" @@ -1640,6 +1644,9 @@ msgstr "Settings" msgid "labels.share-prototype" msgstr "Share prototype" +msgid "labels.share" +msgstr "Share" + #: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.shared-libraries" msgstr "Libraries" @@ -1706,10 +1713,19 @@ msgstr "(you)" msgid "labels.your-account" msgstr "Your account" +msgid "labels.search" +msgstr "Search" + #: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "Loading image…" +msgid "modals.add-shared-confirm-empty.hint" +msgstr "" +"Your library is empty. Once added as Shared Library, the assets you create " +"will be available to be used among the rest of your files. Are you sure you " +"want to publish it?" + #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" msgstr "Add as Shared Library" @@ -1748,6 +1764,30 @@ msgstr "Change email" msgid "modals.change-email.title" msgstr "Change your email" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "Copy token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "Expiration date" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "Name" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "The name can help to know what's the token for" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "Create token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "Generate access token" + msgid "modals.create-webhook.submit-label" msgstr "Create webhook" @@ -1760,6 +1800,18 @@ msgstr "Payload URL" msgid "modals.create-webhook.url.placeholder" msgstr "https://example.com/postreceive" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "Delete token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "Are you sure you want to delete this token?" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "Delete token" + #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.cancel" msgstr "Cancel and keep my account" @@ -1790,6 +1842,12 @@ msgstr "" msgid "modals.delete-comment-thread.title" msgstr "Delete conversation" +msgid "modals.delete-component-annotation.message" +msgstr "Are you sure you want to delete this annotation?" + +msgid "modals.delete-component-annotation.title" +msgstr "Delete annotation" + #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-confirm.accept" msgstr "Delete file" @@ -1857,28 +1915,16 @@ msgstr[0] "Delete file" msgstr[1] "Delete files" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "" -"If you delete it, those assets will no longer be available from other " -"files. Assets that have already been used will remain in this file (no " -"design will be broken!)." -msgstr[1] "" -"If you delete them, those assets will no longer be available from other " -"files. Assets that have already been used will remain in this file (no " -"design will be broken!)." +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "It isn't activated in any file." +msgstr[1] "They aren't activated in any file." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "" -"If you delete it, those assets will no longer be available from other " -"files. Assets that have already been used will remain in these files (no " -"design will be broken!)." -msgstr[1] "" -"If you delete them, those assets will no longer be available from other " -"files. Assets that have already been used will remain in these file (no " -"design will be broken!)." +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "This library is activated here: " +msgstr[1] "This libraries are activated here: " #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" @@ -1886,28 +1932,6 @@ msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "Are you sure you want to delete this file?" msgstr[1] "Are you sure you want to delete these files?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" -msgstr[0] "" -"None of the assets in this file's library are in use. They will be deleted " -"along with the file." -msgstr[1] "" -"None of the assets in these file's libraries are in use. They will be " -"deleted along with the files." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Some of the assets in this file's library are in use here:" -msgstr[1] "Some of the assets in these file's libraries are in use here:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "Some of the assets in this file's library are in use here:" -msgstr[1] "Some of the assets in these file's libraries are in use here:" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" @@ -1940,6 +1964,16 @@ msgstr "Are you sure you want to delete this member from the team?" msgid "modals.delete-team-member-confirm.title" msgstr "Delete team member" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" +msgstr[0] "" +"Assets that have already been used in this file will remain there (no " +"design will be broken)." +msgstr[1] "" +"Assets that have already been used in those files will remain there (no " +"design will be broken)." + msgid "modals.delete-webhook.accept" msgstr "Delete webhook" @@ -2040,6 +2074,15 @@ msgstr "" msgid "modals.promote-owner-confirm.title" msgstr "New team owner" +msgid "modals.publish-empty-library.accept" +msgstr "Publish" + +msgid "modals.publish-empty-library.message" +msgstr "Your library is empty. Are you sure you want to publish it?" + +msgid "modals.publish-empty-library.title" +msgstr "Publish empty library" + #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" msgstr "Remove as Shared Library" @@ -2064,29 +2107,10 @@ msgid_plural "modals.unpublish-shared-confirm.accept" msgstr[0] "Unpublish" msgstr[1] "Unpublish" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"If you unpublish it, those assets will no longer be available from other " -"files. Assets that have already been used will remain in this file (no " -"design will be broken!)." -msgstr[1] "" -"If you unpublish them, those assets will no longer be available from other " -"files. Assets that have already been used will remain in this file (no " -"design will be broken!)." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "" -"If you unpublish it, those assets will no longer be available from other " -"files. Assets that have already been used will remain in these files (no " -"design will be broken!)." -msgstr[1] "" -"If you unpublish them, those assets will no longer be available from other " -"files. Assets that have already been used will remain in these file (no " -"design will be broken!)." +msgid "modals.move-shared-confirm.accept" +msgid_plural "modals.move-shared-confirm.accept" +msgstr[0] "Move" +msgstr[1] "Move" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" @@ -2094,23 +2118,10 @@ msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "Are you sure you want to unpublish this library?" msgstr[1] "Are you sure you want to unpublish these libraries?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "None of the assets in this library are in use." -msgstr[1] "None of the assets in these libraries are in use." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Some of the assets in this library are in use here:" -msgstr[1] "Some of the assets in these libraries are in use here:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "Some of the assets in this library are in use here:" -msgstr[1] "Some of the assets in these libraries are in use here:" +msgid "modals.move-shared-confirm.message" +msgid_plural "modals.move-shared-confirm.message" +msgstr[0] "Are you sure you want to move this library?" +msgstr[1] "Are you sure you want to move these libraries?" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" @@ -2118,6 +2129,11 @@ msgid_plural "modals.unpublish-shared-confirm.title" msgstr[0] "Unpublish library" msgstr[1] "Unpublish libraries" +msgid "modals.move-shared-confirm.title" +msgid_plural "modals.move-shared-confirm.title" +msgstr[0] "Move library" +msgstr[1] "Move libraries" + #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.hint" msgstr "" @@ -2146,11 +2162,9 @@ msgstr "" msgid "modals.update-remote-component.message" msgstr "Update a component in a shared library" -msgid "modals.delete-component-annotation.message" -msgstr "Are you sure you want to delete this annotation?" - -msgid "modals.delete-component-annotation.title" -msgstr "Delete annotation" +#: src/app/main/data/common.cljs +msgid "notifications.by-code.upgrade-version" +msgstr "A new version is available, please refresh the page" #: src/app/main/ui/dashboard/team.cljs msgid "notifications.invitation-email-sent" @@ -2262,6 +2276,12 @@ msgstr "Create team and send invites" msgid "onboarding.choice.team-up.create-team-without-inviting" msgstr "Create team without inviting" +msgid "onboarding.choice.team-up.create-team-and-invite" +msgstr "Create team & invite" + +msgid "onboarding.choice.team-up.create-team-without-invite" +msgstr "Create team" + msgid "onboarding.choice.team-up.create-team-and-send-invites-description" msgstr "You'll be able to invite later" @@ -2293,6 +2313,9 @@ msgstr "Want to receive Penpot news?" msgid "onboarding.team-modal.create-team" msgstr "Create a team" +msgid "onboarding.team-modal.team-definition" +msgstr "What's a team?" + msgid "onboarding.team-modal.create-team-desc" msgstr "" "A team allows you to collaborate with other Penpot users working in the " @@ -2326,10 +2349,188 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "Go to login" +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "Which is the design tool you have more experience with?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11-30" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2-10" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31-50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "A lot" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canva" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.describe-your-experience-working-on" +msgstr "How would you best describe your experience working on..." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "Designer" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "Developer" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "Discover more about Penpot" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "Founder/VP" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "I'm a freelancer" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "Get the code from my team project " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "... interface design, visual assets, design systems, etc." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "InVision" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "Leave feedback for my team project" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "Let's get started!" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "Product or Project manager" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "Marketing" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "More than 50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.never-used-one" +msgstr "None" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "Next" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "None" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "Other (specify)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "I’m working in a personal project" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "Previous" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "How are you planning to use Penpot?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "What's your role?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.select-option" +msgstr "Select option" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "Sketch" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.some" +msgstr "Some" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "Start" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "Start to work on my project" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "Student or teacher" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "What's the size of your team?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "Test Penpot to see if it's a fit for team " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "Try out before using Penpot on-premise" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" +msgstr "... wireframes, user journeys & flows, navigation trees, etc." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "Work in concept ideas" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" +msgstr "" +"Your feedback will help us understand what your habits and preferences are " +"so that we can keep making Penpot such a useful and enjoyable tool." + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +msgid "settings.detach" +msgstr "Detach" + #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "Mixed" +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +msgid "settings.select-this-color" +msgstr "Select items using this style" + # SECTIONS msgid "shortcut-section.basics" msgstr "Basics" @@ -2692,6 +2893,9 @@ msgstr "Select all" msgid "shortcuts.select-next" msgstr "Select next layer" +msgid "shortcuts.select-parent-layer" +msgstr "Select parent layer" + msgid "shortcuts.select-prev" msgstr "Select previous layer" @@ -2713,9 +2917,6 @@ msgstr "Snap to pixel grid" msgid "shortcuts.start-editing" msgstr "Start editing" -msgid "shortcuts.select-parent-layer" -msgstr "Select parent layer" - msgid "shortcuts.start-measure" msgstr "Start measurement" @@ -2725,12 +2926,12 @@ msgstr "Stop measurement" msgid "shortcuts.text-align-center" msgstr "Align center" -msgid "shortcuts.text-align-left" -msgstr "Align left" - msgid "shortcuts.text-align-justify" msgstr "Align justify" +msgid "shortcuts.text-align-left" +msgstr "Align left" + msgid "shortcuts.text-align-right" msgstr "Align right" @@ -2756,8 +2957,8 @@ msgstr "Toggle focus mode" msgid "shortcuts.toggle-fullscreen" msgstr "Toggle fullscreen" -msgid "shortcuts.toggle-grid" -msgstr "Show / Hide grid" +msgid "shortcuts.toggle-guides" +msgstr "Show / Hide guides" msgid "shortcuts.toggle-history" msgstr "Toggle history" @@ -2774,21 +2975,24 @@ msgstr "Lock / Unlock" msgid "shortcuts.toggle-lock-size" msgstr "Lock proportions" -msgid "shortcuts.toggle-rules" +msgid "shortcuts.toggle-rulers" msgstr "Show / Hide rulers" -msgid "shortcuts.toggle-scale-text" -msgstr "Toggle scale text" +msgid "shortcuts.scale" +msgstr "Scale" -msgid "shortcuts.toggle-snap-grid" -msgstr "Snap to grid" - -msgid "shortcuts.toggle-snap-guide" +msgid "shortcuts.toggle-snap-guides" msgstr "Snap to guides" +msgid "shortcuts.toggle-snap-ruler-guide" +msgstr "Snap to ruler guides" + msgid "shortcuts.toggle-textpalette" msgstr "Toggle text palette" +msgid "shortcuts.toggle-theme" +msgstr "Change theme" + msgid "shortcuts.toggle-visibility" msgstr "Show / Hide" @@ -2819,8 +3023,12 @@ msgstr "Zoom lense increase" msgid "shortcuts.zoom-selected" msgstr "Zoom to selected" +msgid "shortcuts.toggle-layout-grid" +msgstr "Add/remove grid layout" - +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "The webhook name must contain at most 2048 characters." #: src/app/main/ui/dashboard/files.cljs msgid "title.dashboard.files" @@ -2850,6 +3058,10 @@ msgstr "Shared Libraries - %s - Penpot" msgid "title.default" msgstr "Penpot - Design Freedom for Teams" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "Profile - Access tokens" + #: src/app/main/ui/settings/feedback.cljs msgid "title.settings.feedback" msgstr "Give feedback - Penpot" @@ -2866,10 +3078,6 @@ msgstr "Password - Penpot" msgid "title.settings.profile" msgstr "Profile - Penpot" -#: src/app/main/ui/settings/access-tokens.cljs -msgid "title.settings.access-tokens" -msgstr "Profile - Access tokens" - #: src/app/main/ui/dashboard/team.cljs msgid "title.team-invitations" msgstr "Invitations - %s - Penpot" @@ -2947,7 +3155,7 @@ msgid "viewer.header.sitemap" msgstr "Sitemap" msgid "webhooks.last-delivery.success" -msgstr "Last delivery was successfull." +msgstr "Last delivery was successful." #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.hcenter" @@ -3043,6 +3251,9 @@ msgstr "local library" msgid "workspace.assets.not-found" msgstr "No assets found" +msgid "workspace.assets.open-library" +msgstr "Open library file" + #: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename" msgstr "Rename" @@ -3055,6 +3266,14 @@ msgstr "Rename group" msgid "workspace.assets.search" msgstr "Search assets" +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.filter" +msgstr "Filter" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.sort" +msgstr "Sort" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.selected-count" msgid_plural "workspace.assets.selected-count" @@ -3062,8 +3281,8 @@ msgstr[0] "%s item selected" msgstr[1] "%s items selected" #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "SHARED" +msgid "workspace.assets.shared-library" +msgstr "Shared library" #: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" @@ -3139,14 +3358,14 @@ msgstr "Disable proportional scale" msgid "workspace.header.menu.disable-scale-text" msgstr "Disable scale text" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Disable snap to grid" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "Disable snap to guides" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-snap-ruler-guides" +msgstr "Disable snap to ruler guides" + msgid "workspace.header.menu.disable-snap-pixel-grid" msgstr "Disable snap to pixel" @@ -3161,14 +3380,14 @@ msgstr "Enable proportional scale" msgid "workspace.header.menu.enable-scale-text" msgstr "Enable scale text" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Snap to grid" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "Snap to guides" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-snap-ruler-guides" +msgstr "Snap to ruler guides" + msgid "workspace.header.menu.enable-snap-pixel-grid" msgstr "Enable snap to pixel" @@ -3177,8 +3396,8 @@ msgid "workspace.header.menu.hide-artboard-names" msgstr "Hide board names" #: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Hide grids" +msgid "workspace.header.menu.hide-guides" +msgstr "Hide guides" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" @@ -3227,8 +3446,8 @@ msgid "workspace.header.menu.show-artboard-names" msgstr "Show boards names" #: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Show grid" +msgid "workspace.header.menu.show-guides" +msgstr "Show guides" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" @@ -3248,6 +3467,12 @@ msgstr "Show fonts palette" msgid "workspace.header.menu.undo" msgstr "Undo" +msgid "workspace.header.menu.toggle-light-theme" +msgstr "Switch to light theme" + +msgid "workspace.header.menu.toggle-dark-theme" +msgstr "Switch to dark theme" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "Reset" @@ -3272,6 +3497,10 @@ msgstr "Unsaved changes" msgid "workspace.header.viewer" msgstr "View mode (%s)" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "Zoom" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-fill" msgstr "Fill - Scale to fill" @@ -3292,6 +3521,27 @@ msgstr "Full screen" msgid "workspace.header.zoom-selected" msgstr "Zoom to selected" +msgid "workspace.layout_grid.editor.title" +msgstr "Editing grid" + +msgid "workspace.layout_grid.editor.top-bar.locate" +msgstr "Locate" + +msgid "workspace.layout_grid.editor.top-bar.locate.tooltip" +msgstr "Locate grid layout" + +msgid "workspace.layout_grid.editor.top-bar.done" +msgstr "Done" + +msgid "workspace.layout_grid.editor.options.edit-grid" +msgstr "Edit grid" + +msgid "workspace.layout_grid.editor.options.exit" +msgstr "Exit" + +msgid "workspace.layout_grid.editor.padding.expand" +msgstr "Show 4 sided padding options" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.add" msgstr "Add" @@ -3300,6 +3550,14 @@ msgstr "Add" msgid "workspace.libraries.colors" msgstr "%s colors" +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "There are no color styles in your library yet" + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "There are no typography styles in your library yet" + #: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" msgstr "File library" @@ -3312,14 +3570,6 @@ msgstr "HSV" msgid "workspace.libraries.colors.recent-colors" msgstr "Recent colors" -#: src/app/main/ui/workspace/colorpalette.cljs -msgid "workspace.libraries.colors.empty-palette" -msgstr "There are no color styles in your library yet" - -#: src/app/main/ui/workspace/textpalette.cljs -msgid "workspace.libraries.colors.empty-typography-palette" -msgstr "There are no typography styles in your library yet" - #: src/app/main/ui/workspace/colorpicker.cljs msgid "workspace.libraries.colors.rgb-complementary" msgstr "RGB Complementary" @@ -3356,6 +3606,10 @@ msgstr "LIBRARIES" msgid "workspace.libraries.library" msgstr "LIBRARY" +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "LIBRARY UPDATES" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.no-libraries-need-sync" msgstr "There are no Shared Libraries that need update" @@ -3376,6 +3630,18 @@ msgstr "Search shared libraries" msgid "workspace.libraries.shared-libraries" msgstr "SHARED LIBRARIES" +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.shared-library-btn" +msgstr "Connect library" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.unlink-library-btn" +msgstr "Disconnect library" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.loading" +msgstr "Loading…" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.libraries.text.multiple-typography" msgstr "Multiple typographies" @@ -3392,6 +3658,10 @@ msgstr "%s typographies" msgid "workspace.libraries.update" msgstr "Update" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "see all changes" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.updates" msgstr "UPDATES" @@ -3426,12 +3696,30 @@ msgstr "Component" msgid "workspace.options.component.annotation" msgstr "Annotation" +msgid "workspace.options.component.copy" +msgstr "Copy" + msgid "workspace.options.component.create-annotation" msgstr "Create an annotation" msgid "workspace.options.component.edit-annotation" msgstr "Edit an annotation" +msgid "workspace.options.component.main" +msgstr "Main" + +msgid "workspace.options.component.swap" +msgstr "Swap component" + +msgid "workspace.options.component.swap.empty" +msgstr "There are no assets in this library yet" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs +msgid "workspace.assets.sidebar.components" +msgid_plural "workspace.assets.sidebar.components" +msgstr[0] "1 component" +msgstr[1] "%s components" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints" msgstr "Constraints" @@ -3518,6 +3806,10 @@ msgstr "Fill" msgid "workspace.options.flows.add-flow-start" msgstr "Add flow start" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow" +msgstr "Flow" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.flows.flow-start" msgstr "Flow start" @@ -4049,12 +4341,12 @@ msgid "workspace.options.radius" msgstr "Radius" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "All corners" +msgid "workspace.options.radius-bottom-left" +msgstr "Bottom left" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Independent corners" +msgid "workspace.options.radius-bottom-right" +msgstr "Bottom right" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -4065,12 +4357,12 @@ msgid "workspace.options.radius-top-right" msgstr "Top right" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Bottom left" +msgid "workspace.options.radius.all-corners" +msgstr "All corners" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Bottom right" +msgid "workspace.options.radius.single-corners" +msgstr "Independent corners" msgid "workspace.options.recent-fonts" msgstr "Recent" @@ -4164,14 +4456,26 @@ msgstr "Stroke" msgid "workspace.options.stroke-cap.circle-marker" msgstr "Circle marker" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "Circle" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.diamond-marker" msgstr "Diamond marker" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "Diamond" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.line-arrow" msgstr "Line arrow" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "Arrow" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.none" msgstr "None" @@ -4188,10 +4492,18 @@ msgstr "Square" msgid "workspace.options.stroke-cap.square-marker" msgstr "Square marker" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "Rectangle" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.triangle-arrow" msgstr "Triangle arrow" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "Triangle" + msgid "workspace.options.stroke-color" msgstr "Stroke color" @@ -4230,26 +4542,10 @@ msgstr "Solid" msgid "workspace.options.text-options.align-bottom" msgstr "Align bottom" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Align center (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Justify (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Align left (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Align middle" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Align right (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Align top" @@ -4294,6 +4590,22 @@ msgstr "None" msgid "workspace.options.text-options.strikethrough" msgstr "Strikethrough (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Align center (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Justify (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Align left (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Align right (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Text" @@ -4361,39 +4673,13 @@ msgstr "Separate nodes (%s)" msgid "workspace.path.actions.snap-nodes" msgstr "Snap nodes (%s)" -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "" -"To try it again, you can reload this file. If the problem persists, we " -"suggest you to take a look at the list and consider to delete broken " -"graphics." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "Some graphics could not be updated." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "Converting %s/%s" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "" -"Library Graphics are Components from now on, which will make them much more " -"powerful." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "This update is a one time action." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "Updating %s..." - #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.add-flex" msgstr "Add flex layout" +msgid "workspace.shape.menu.add-grid" +msgstr "Add grid layout" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "Send to back" @@ -4406,6 +4692,9 @@ msgstr "Send backward" msgid "workspace.shape.menu.copy" msgstr "Copy" +msgid "workspace.shape.menu.create-annotation" +msgstr "Create annotation" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.create-artboard-from-selection" msgstr "Selection to board" @@ -4511,6 +4800,10 @@ msgstr "Path" msgid "workspace.shape.menu.remove-flex" msgstr "Remove flex layout" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.remove-grid" +msgstr "Remove grid layout" + #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "Reset overrides" @@ -4534,9 +4827,6 @@ msgstr "Show in assets panel" msgid "workspace.shape.menu.show-main" msgstr "Show main component" -msgid "workspace.shape.menu.create-annotation" -msgstr "Create annotation" - msgid "workspace.shape.menu.thumbnail-remove" msgstr "Remove thumbnail" @@ -4790,6 +5080,10 @@ msgstr "History" msgid "workspace.updates.dismiss" msgstr "Dismiss" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "More info" + #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.there-are-updates" msgstr "There are updates in shared libraries" @@ -4801,178 +5095,76 @@ msgstr "Update" msgid "workspace.viewport.click-to-close-path" msgstr "Click to close the path" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.lets-get-started" -msgstr "Let's get started!" +#, markdown +msgid "workspace.top-bar.view-only" +msgstr "**Inspecting code** (View Only)" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.your-feedback-will-help-us" -msgstr "Your feedback will help us understand what your habits and preferences are so that we can keep making Penpot such a useful and enjoyable tool." +msgid "workspace.top-bar.read-only.done" +msgstr "Done" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.questions-how-are-you-planning-to-use-penpot" -msgstr "How are you planning to use Penpot?" +msgid "media.image" +msgstr "Image" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.discover-more-about-penpot" -msgstr "Discover more about Penpot" +msgid "media.image.short" +msgstr "img" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" -msgstr "Test Penpot to see if it's a fit for team " +msgid "media.solid" +msgstr "Solid" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.start-to-work-on-my-project" -msgstr "Start to work on my project" +msgid "media.linear" +msgstr "Linear" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.get-the-code-from-my-team-project" -msgstr "Get the code from my team project " +msgid "media.radial" +msgstr "Radial" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.leave-feedback-for-my-team-project" -msgstr "Leave feedback for my team project" +msgid "media.gradient" +msgstr "Gradient" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.work-in-concept-ideas" -msgstr "Work in concept ideas" +msgid "media.choose-image" +msgstr "Choose image" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.try-out-before-using-penpot-on-premise" -msgstr "Try out before using Penpot on-premise" +msgid "media.keep-aspect-ratio" +msgstr "Keep aspect ratio" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.select-option" -msgstr "Select option" +msgid "workspace.options.guides.title" +msgstr "Guides" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.previous" -msgstr "Previous" +msgid "workspace.context-menu.grid-track.column.duplicate" +msgstr "Duplicate column" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.next" -msgstr "Next" +msgid "workspace.context-menu.grid-track.column.add-before" +msgstr "Add 1 column to the left" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.start" -msgstr "Start" +msgid "workspace.context-menu.grid-track.column.add-after" +msgstr "Add 1 column to the right" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.describe-your-experience-working-on" -msgstr "How would you best describe your experience working on..." +msgid "workspace.context-menu.grid-track.column.delete" +msgstr "Delete column" -#: src/app/main/ui/onboarding/questions.cljs -msgid "branding-illustrations-marketing-pieces" -msgstr "...branding, illustrations, marketing pieces, etc." +msgid "workspace.context-menu.grid-track.column.delete-shapes" +msgstr "Delete column and shapes" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.none" -msgstr "None" +msgid "workspace.context-menu.grid-track.row.duplicate" +msgstr "Duplicate row" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.some" -msgstr "Some" +msgid "workspace.context-menu.grid-track.row.add-before" +msgstr "Add 1 row above" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.a-lot" -msgstr "A lot" +msgid "workspace.context-menu.grid-track.row.add-after" +msgstr "Add 1 row below" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.interface-design-visual-assets-design-systems" -msgstr "... interface design, visual assets, design systems, etc." +msgid "workspace.context-menu.grid-track.row.delete" +msgstr "Delete row" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.wireframes-user-journeys-flows-navigation-trees" -msgstr "... wireframes, user journeys & flows, navigation trees, etc." +msgid "workspace.context-menu.grid-track.row.delete-shapes" +msgstr "Delete row and shapes" -#: src/app/main/ui/onboarding/questions.cljs -msgid "question.design-tool-more-experienced-with" -msgstr "Which is the design tool you have more experience with?" +msgid "workspace.context-menu.grid-cells.merge" +msgstr "Merge cells" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.figma" -msgstr "Figma" +msgid "workspace.context-menu.grid-cells.area" +msgstr "Create area" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.sketch" -msgstr "Sketch" +msgid "workspace.context-menu.grid-cells.create-board" +msgstr "Create board" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.adobe-xd" -msgstr "Adobe XD" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.canva" -msgstr "Canva" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.invision" -msgstr "InVision" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.never-used-a-tool" -msgstr "I've never used a design tool before" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.other" -msgstr "Other (specify)" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.team-size" -msgstr "What's the size of your team?" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.more-than-50" -msgstr "More than 50" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.31-50" -msgstr "31-50" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.11-30" -msgstr "11-30" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.2-10" -msgstr "2-10" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.freelancer" -msgstr "I'm a freelancer" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.personal-project" -msgstr "I’m working in a personal project" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.role" -msgstr "What's your role?" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.designer" -msgstr "Designer" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.developer" -msgstr "Developer" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.manager" -msgstr "Product or Project manager" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.founder" -msgstr "Founder/VP" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.marketing" -msgstr "Marketing" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.student-teacher" -msgstr "Student or teacher" - -#: src/app/main/data/common.cljs -msgid "notifications.by-code.upgrade-version" -msgstr "A new version is available, please refresh the page" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 8dc32d34d7..18ee3b0300 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1,15 +1,15 @@ msgid "" msgstr "" -"PO-Revision-Date: 2023-03-07 11:37+0000\n" -"Last-Translator: Alvaro Araoz \n" -"Language-Team: Spanish " -"\n" +"PO-Revision-Date: 2024-01-25 12:01+0000\n" +"Last-Translator: Yessenia Villarte Vaca \n" +"Language-Team: Spanish \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.16.2-dev\n" +"X-Generator: Weblate 5.4-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -56,21 +56,17 @@ msgstr "Nombre completo" msgid "auth.login-here" msgstr "Inicia sesión aquí" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs -msgid "auth.name.too-long" -msgstr "El nombre debe contener como máximo 250 caracteres." - -#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs -msgid "auth.name.not-all-space" -msgstr "El nombre debe contener algún carácter diferente de espacio" - #: src/app/main/ui/auth/login.cljs msgid "auth.login-submit" msgstr "Entrar" #: src/app/main/ui/auth/login.cljs -msgid "auth.login-title" -msgstr "¡Un placer verte de nuevo!" +msgid "auth.login-account-title" +msgstr "Entrar en mi cuenta" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-tagline" +msgstr "Penpot es la herramienta de diseño libre y open-source para la colaboración entre Diseño y Código" #: src/app/main/ui/auth/login.cljs msgid "auth.login-with-github-submit" @@ -92,6 +88,14 @@ msgstr "LDAP" msgid "auth.login-with-oidc-submit" msgstr "OpenID" +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "El nombre debe contener algún carácter diferente de espacio" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "El nombre debe contener como máximo 250 caracteres." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.new-password" msgstr "Introduce la nueva contraseña" @@ -173,15 +177,23 @@ msgid "auth.terms-of-service" msgstr "Términos de servicio" #: src/app/main/ui/auth/register.cljs +#, markdown +msgid "auth.terms-privacy-agreement-md" +msgstr "" +"Al crear una nueva cuenta, aceptas nuestros [términos de servicio](%s) y [política de privacidad](%s)." + msgid "auth.terms-privacy-agreement" msgstr "" -"Al crear una nueva cuenta, aceptas nuestros términos de servicio y política " -"de privacidad." +"Al crear una nueva cuenta, aceptas nuestros [términos de servicio](%s) y [política de privacidad](%s)." #: src/app/main/ui/auth/register.cljs msgid "auth.verification-email-sent" msgstr "Hemos enviado un email de verificación a" +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "diseño de marca, ilustraciones, piezas de marketing..." + msgid "common.publish" msgstr "Publicar" @@ -280,95 +292,33 @@ msgstr "Comenzar" msgid "dasboard.walkthrough-hero.title" msgstr "Recorrido por el interfaz" -#: src/app/main/ui/workspace/header.cljs, -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.add-shared" -msgstr "Añadir como Biblioteca Compartida" - -#: src/app/main/ui/settings/profile.cljs -msgid "dashboard.change-email" -msgstr "Cambiar correo" - #: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.personal" -msgstr "Access tokens personales" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.personal.description" -msgstr "Los access tokens personales funcionan como una alternativa a nuestro sistema de autenticación " -"usuario/password y se pueden usar para permitir a otras aplicaciones acceso a la API interna de Penpot" +msgid "dashboard.access-tokens.copied-success" +msgstr "Token copiado" #: src/app/main/ui/settings/access-tokens.cljs msgid "dashboard.access-tokens.create" msgstr "Generar nuevo token" #: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.empty.no-access-tokens" -msgstr "Todavía no tienes ningún token." +msgid "dashboard.access-tokens.create.success" +msgstr "Access token creado con éxito." #: src/app/main/ui/settings/access-tokens.cljs msgid "dashboard.access-tokens.empty.add-one" msgstr "Pulsa el botón \"Generar nuevo token\" para generar uno." #: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.create-access-token.title" -msgstr "Generar access token" +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "Todavía no tienes ningún token." #: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.create-access-token.name.label" -msgstr "Nombre" +msgid "dashboard.access-tokens.errors-required-name" +msgstr "El nombre es obligatorio" #: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.create-access-token.name.placeholder" -msgstr "El nombre te pude ayudar a saber para qué se utiliza el token" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.create-access-token.expiration-date.label" -msgstr "Fecha de expiración" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.create-access-token.submit-label" -msgstr "Crear token" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.delete-acces-token.title" -msgstr "Borrar token" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.delete-acces-token.message" -msgstr "¿Seguro que deseas borrar este token?" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.delete-acces-token.accept" -msgstr "Borrar token" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.create.success" -msgstr "Access token creado con éxito." - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.expires-on" -msgstr "Expira el %s" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.expired-on" -msgstr "Expiró el %s" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.no-expiration" -msgstr "Sin fecha de expiración" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "modals.create-access-token.copy-token" -msgstr "Copiar token" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.copied-success" -msgstr "Token copiado" - -#: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.expiration-never" -msgstr "Nunca" +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 días" #: src/app/main/ui/settings/access-tokens.cljs msgid "dashboard.access-tokens.expiration-30-days" @@ -383,12 +333,31 @@ msgid "dashboard.access-tokens.expiration-90-days" msgstr "90 días" #: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.expiration-180-days" -msgstr "180 días" +msgid "dashboard.access-tokens.expiration-never" +msgstr "Nunca" #: src/app/main/ui/settings/access-tokens.cljs -msgid "dashboard.access-tokens.errors-required-name" -msgstr "El nombre es obligatorio" +msgid "dashboard.access-tokens.expired-on" +msgstr "Expiró el %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "Expira el %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "Sin fecha de expiración" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Access tokens personales" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Los access tokens personales funcionan como una alternativa a nuestro " +"sistema de autenticación usuario/password y se pueden usar para permitir a " +"otras aplicaciones acceso a la API interna de Penpot" #: src/app/main/ui/settings/access-tokens.cljs msgid "dashboard.access-tokens.token-will-expire" @@ -398,6 +367,15 @@ msgstr "El token expirará el %s" msgid "dashboard.access-tokens.token-will-not-expire" msgstr "El token no tiene fecha de expiración" +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.add-shared" +msgstr "Añadir como Biblioteca Compartida" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.change-email" +msgstr "Cambiar correo" + #: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs msgid "dashboard.copy-suffix" msgstr "(copia)" @@ -568,6 +546,9 @@ msgstr "Importar archivos Penpot" msgid "dashboard.import.analyze-error" msgstr "¡Vaya! No hemos podido importar el fichero" +msgid "dashboard.import.analyze-error.components-v2" +msgstr "Fichero exportado con componentes-v2 pero el equipo actual no lo soporta aún." + msgid "dashboard.import.import-error" msgstr "Hubo un problema importando el fichero. No ha podido ser importado." @@ -832,10 +813,6 @@ msgstr "No hay ningún webhook aún." msgid "dashboard.webhooks.update.success" msgstr "Webhook modificado con éxito." -#: src/app/main/ui/dashboard/team.cljs -msgid "team.webhooks.max-length" -msgstr "El nombre del webhook debe contener como máximo 2048 caracteres." - #: src/app/main/ui/settings.cljs msgid "dashboard.your-account-title" msgstr "Tu cuenta" @@ -878,7 +855,7 @@ msgstr "Ok" #: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs msgid "ds.confirm-title" -msgstr "¿Seguro?" +msgstr "¿Está Seguro?" #: src/app/main/ui/auth/login.cljs msgid "errors.auth-provider-not-configured" @@ -893,6 +870,9 @@ msgstr "No se ha podido cargar la fuente %s" msgid "errors.bad-font-plural" msgstr "No se han podido cargar las fuentes %s" +msgid "errors.cannot-upload" +msgstr "No se puede cargar el archivo multimedia." + #: src/app/main/data/workspace.cljs msgid "errors.clipboard-not-implemented" msgstr "Tu navegador no puede realizar esta operación" @@ -907,14 +887,14 @@ msgid "errors.email-already-validated" msgstr "Este correo ya está validado." msgid "errors.email-as-password" -msgstr "No puedes usar tu email como password" +msgstr "No puedes usar tu correo electrónico como contraseña" #: src/app/main/ui/auth/register.cljs, #: src/app/main/ui/auth/recovery_request.cljs, #: src/app/main/ui/settings/change_email.cljs, #: src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" -msgstr "El email «%s» tiene varios reportes de rebote permanente." +msgstr "El correo electrónico «%s» tiene varios reportes de rebote permanente." #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs msgid "errors.email-invalid" @@ -930,9 +910,15 @@ msgstr "El email «%s» tiene reportes de spam o de rebote permanente." #: src/app/main/errors.cljs msgid "errors.feature-mismatch" msgstr "" -"Parece que esta abriendo un fichero con la caracteristica '%s' habilitada " -"pero la aplicacion web de penpot que esta usando no tiene soporte para ella " -"o esta deshabilitada." +"Parece que está abriendo un archivo que tiene la función '%s' habilitada, " +"pero la versión actual de penpot no la admite o la tiene deshabilitada." + +#: src/app/main/errors.cljs +msgid "errors.file-feature-mismatch" +msgstr "" +"Parece que hay discordancia entre las features habilitadas y las features " +"del fichero que se esta intentando abrir. Falta aplicar migraciones para " +"'%s' antes de poder abrir el fichero." #: src/app/main/errors.cljs msgid "errors.feature-not-supported" @@ -1057,9 +1043,6 @@ msgstr "El email o la contraseña son incorrectos." msgid "errors.wrong-old-password" msgstr "La contraseña anterior no es correcta" -msgid "errors.cannot-upload" -msgstr "No se puede subir el fichero" - #: src/app/main/ui/settings/feedback.cljs msgid "feedback.description" msgstr "Descripción" @@ -1095,7 +1078,7 @@ msgstr "Correo electrónico" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Ir a Twitter" +msgstr "Ir a X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -1103,7 +1086,7 @@ msgstr "Cuenta habilitada para responder todas tus dudas técnicas." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Cuenta de Twitter para soporte" +msgstr "Cuenta de X para soporte" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -1262,6 +1245,9 @@ msgstr "Minúsculas" msgid "inspect.attributes.typography.text-transform.none" msgstr "Ninguna" +msgid "inspect.attributes.typography.text-transform.unset" +msgstr "Sin asignar" + msgid "inspect.attributes.typography.text-transform.titlecase" msgstr "Primera en mayúscula" @@ -1331,6 +1317,10 @@ msgstr "Atajos de teclado" msgid "labels.accept" msgstr "Aceptar" +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "Access tokens" + msgid "labels.active" msgstr "Activo" @@ -1607,10 +1597,6 @@ msgstr "Propiedad" msgid "labels.password" msgstr "Contraseña" -#: src/app/main/ui/settings/sidebar.cljs -msgid "labels.access-tokens" -msgstr "Access tokens" - #: src/app/main/ui/dashboard/team.cljs msgid "labels.pending-invitation" msgstr "Pendiente" @@ -1693,6 +1679,9 @@ msgstr "Configuración" msgid "labels.share-prototype" msgstr "Compartir prototipo" +msgid "labels.share" +msgstr "Compartir" + #: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.shared-libraries" msgstr "Bibliotecas" @@ -1759,6 +1748,9 @@ msgstr "(tú)" msgid "labels.your-account" msgstr "Tu cuenta" +msgid "labels.search" +msgstr "Buscar" + #: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "Cargando imagen…" @@ -1804,6 +1796,30 @@ msgstr "Cambiar correo" msgid "modals.change-email.title" msgstr "Cambiar tu correo" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "Copiar token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "Fecha de expiración" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "Nombre" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "El nombre te pude ayudar a saber para qué se utiliza el token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "Crear token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "Generar access token" + msgid "modals.create-webhook.submit-label" msgstr "Crear webhook" @@ -1816,6 +1832,18 @@ msgstr "Payload URL" msgid "modals.create-webhook.url.placeholder" msgstr "https://example.com/postreceive" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "Borrar token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "¿Seguro que deseas borrar este token?" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "Borrar token" + #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.cancel" msgstr "Cancelar y mantener mi cuenta" @@ -1846,6 +1874,12 @@ msgstr "" msgid "modals.delete-comment-thread.title" msgstr "Eliminar conversación" +msgid "modals.delete-component-annotation.message" +msgstr "¿Seguro que quieres borrar esta nota?" + +msgid "modals.delete-component-annotation.title" +msgstr "Borrar nota" + #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-confirm.accept" msgstr "Borrar archivo" @@ -1913,28 +1947,16 @@ msgstr[0] "Borrar archivo" msgstr[1] "Borrar archivos" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "" -"Si lo borras, sus elementos no estarán disponibles para otros archivos. Los " -"elementos que hayan sido utilizados permanecerán en el archivo (¡ningún " -"diseño se romperá!)." -msgstr[1] "" -"Si los borras, sus elementos no estarán disponibles para otros archivos. " -"Los elementos que hayan sido utilizados permanecerán en el archivo (¡ningún " -"diseño se romperá!)." +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "No está activa en ningún fichero." +msgstr[1] "No están activas en ningún fichero." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "" -"Si lo borras, sus elementos no estarán disponibles para otros archivos. Los " -"elementos que hayan sido utilizados permanecerán en los archivo (¡ningún " -"diseño se romperá!)." -msgstr[1] "" -"Si los borras, sus elementos no estarán disponibles para otros archivos. " -"Los elementos que hayan sido utilizados permanecerán en los archivo " -"(¡ningún diseño se romperá!)." +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "Esta biblioteca está activa aquí: " +msgstr[1] "Estas bibliotecas están activas aquí: " #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" @@ -1942,28 +1964,6 @@ msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "¿Seguro que quieres borrar este archivo?" msgstr[1] "¿Seguro que quieres borrar estos archivos?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" -msgstr[0] "" -"Ninguno de los elementos de su biblioteca están en uso. Se borrarán junto " -"con el archivo." -msgstr[1] "" -"Ninguno de los elementos de sus bibliotecas están en uso. Se borrarán junto " -"con los archivos." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Algunos elementos de su biblioteca están siendo usados por:" -msgstr[1] "Algunos elementos de sus bibliotecas están siendo usados por:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "Algunos elementos de su biblioteca están siendo usados por:" -msgstr[1] "Algunos elementos de sus bibliotecas están siendo usados por:" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" @@ -1996,6 +1996,16 @@ msgstr "¿Seguro que quieres eliminar este integrante del equipo?" msgid "modals.delete-team-member-confirm.title" msgstr "Eliminar integrante del equipo" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" +msgstr[0] "" +"Los elementos que hayan sido utilizados en este archivo permanecerán allí " +"(¡ningún diseño se romperá!)." +msgstr[1] "" +"Los elementos que hayan sido utilizados en esos archivos permanecerán allí " +"(¡ningún diseño se romperá!)." + msgid "modals.delete-webhook.accept" msgstr "Borrar webhook" @@ -2096,6 +2106,15 @@ msgstr "" msgid "modals.promote-owner-confirm.title" msgstr "Nueva propiedad del equipo" +msgid "modals.publish-empty-library.accept" +msgstr "Publicar" + +msgid "modals.publish-empty-library.message" +msgstr "Tu biblioteca está vacía. ¿Seguro que quieres publicarla?" + +msgid "modals.publish-empty-library.title" +msgstr "Publicar biblioteca vacía" + #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" @@ -2123,29 +2142,10 @@ msgid_plural "modals.unpublish-shared-confirm.accept" msgstr[0] "Despublicar" msgstr[1] "Despublicar" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Si la despublicas, sus elementos no estarán disponibles para otros " -"archivos. Los elementos que hayan sido utilizados permanecerán en el " -"archivo (¡ningún diseño se romperá!)." -msgstr[1] "" -"Si las despublicas, sus elementos no estarán disponibles para otros " -"archivos. Los elementos que hayan sido utilizados permanecerán en el " -"archivo (¡ningún diseño se romperá!)." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "" -"Si la despublicas, sus elementos no estarán disponibles para otros " -"archivos. Los elementos que hayan sido utilizados permanecerán en los " -"archivo (¡ningún diseño se romperá!)." -msgstr[1] "" -"Si las despublicas, sus elementos no estarán disponibles para otros " -"archivos. Los elementos que hayan sido utilizados permanecerán en los " -"archivo (¡ningún diseño se romperá!)." +msgid "modals.move-shared-confirm.accept" +msgid_plural "modals.move-shared-confirm.accept" +msgstr[0] "Mover" +msgstr[1] "Mover" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" @@ -2153,23 +2153,10 @@ msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "¿Seguro que quieres despublicar esta biblioteca?" msgstr[1] "¿Seguro que quieres despublicar estas bibliotecas?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "Ninguno de los elementos de esta biblioteca están en uso." -msgstr[1] "Ninguno de los elementos de estas bibliotecas están en uso." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Algunos elementos de esta bibioteca están siendo usados por:" -msgstr[1] "Algunos elementos de estas bibiotecas están siendo usados por:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "Algunos elementos de esta bibioteca están siendo usados por:" -msgstr[1] "Algunos elementos de estas bibiotecas están siendo usados por:" +msgid "modals.move-shared-confirm.message" +msgid_plural "modals.move-shared-confirm.message" +msgstr[0] "¿Seguro que quieres mover esta biblioteca?" +msgstr[1] "¿Seguro que quieres mover estas bibliotecas?" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" @@ -2177,6 +2164,11 @@ msgid_plural "modals.unpublish-shared-confirm.title" msgstr[0] "Despublicar biblioteca" msgstr[1] "Despublicar bibliotecas" +msgid "modals.move-shared-confirm.title" +msgid_plural "modals.move-shared-confirm.title" +msgstr[0] "Mover biblioteca" +msgstr[1] "Mover bibliotecas" + #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, #: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.hint" @@ -2211,12 +2203,6 @@ msgstr "" msgid "modals.update-remote-component.message" msgstr "Actualizar un componente en biblioteca" -msgid "modals.delete-component-annotation.message" -msgstr "¿Seguro que quieres borrar esta nota?" - -msgid "modals.delete-component-annotation.title" -msgstr "Borrar nota" - #: src/app/main/ui/dashboard/team.cljs msgid "notifications.invitation-email-sent" msgstr "Invitación enviada con éxito" @@ -2331,6 +2317,12 @@ msgstr "Crear equipo y enviar invitaciones" msgid "onboarding.choice.team-up.create-team-without-inviting" msgstr "Crear equipo sin invitar" +msgid "onboarding.choice.team-up.create-team-and-invite" +msgstr "Crear equipo e invitar" + +msgid "onboarding.choice.team-up.create-team-without-invite" +msgstr "Crear equipo" + msgid "onboarding.choice.team-up.create-team-and-send-invites-description" msgstr "Podrás enviar invitaciones después" @@ -2362,6 +2354,9 @@ msgstr "¿Quieres recibir noticias sobre Penpot?" msgid "onboarding.team-modal.create-team" msgstr "Crea un equipo" +msgid "onboarding.team-modal.team-definition" +msgstr "¿Qué es un equipo?" + msgid "onboarding.team-modal.create-team-desc" msgstr "" "Un equipo permite colaborar en Penpot trabajando en los mismos archivos y " @@ -2395,6 +2390,180 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "Ir al login" +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "¿Cuál es la herramienta de diseño con la que tienes más experiencia?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11-30" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2-10" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31-50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "Mucha" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canva" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.describe-your-experience-working-on" +msgstr "Cuánta experiencia dirías que tienes trabajando con..." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "Diseño" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "Desarrollo" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "Conocer Penpot mejor" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "Dirección" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "Soy freelancer" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "Obtener código de un proyecto" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "diseño de interfaz, visual, sistemas de diseño..." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "InVision" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "Dejar comentarios en un proyecto" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "¡Empecemos!" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "Gestión de producto o proyecto" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "Marketing" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "Más de 50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.never-used-one" +msgstr "Ninguna" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "Siguiente" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "Ninguna" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "Otra (especifica)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "Estoy trabajando en un proyecto personal" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "Anterior" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "¿Qué uso piensas darle a Penpot?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "¿Cuál es tu rol?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.select-option" +msgstr "Selecciona una opción" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "Sketch" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.some" +msgstr "Alguna" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "Comenzar" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "Comenzar a trabajar en mi proyecto" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "Estudiante o profesorado" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "¿De qué tamaño es tu equipo?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "Valorar si Penpot es adecuado para mi equipo" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "Probar Penpot antes de usarlo en una instalación propia" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" +msgstr "prototipos, user journeys, flujos, árboles de navegación..." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "Conceptualizar ideas" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" +msgstr "" +"Tus respuestas nos ayudarán a entender tus hábitos y preferencias, lo que " +"nos ayudará a continuar mejorando Penpot" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +msgid "settings.detach" +msgstr "Desacoplar" + #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, @@ -2404,6 +2573,10 @@ msgstr "Ir al login" msgid "settings.multiple" msgstr "Varios" +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +msgid "settings.select-this-color" +msgstr "Seleccionar elementos que usan este estilo" + # SECTIONS msgid "shortcut-section.basics" msgstr "Básicos" @@ -2766,6 +2939,9 @@ msgstr "Seleccionar todo" msgid "shortcuts.select-next" msgstr "Seleccionar capa siguiente" +msgid "shortcuts.select-parent-layer" +msgstr "Seleccionar capa padre" + msgid "shortcuts.select-prev" msgstr "Seleccionar capa anterior" @@ -2787,30 +2963,27 @@ msgstr "Activar alineación a rejilla de pixel" msgid "shortcuts.start-editing" msgstr "Comenzar edición" -msgid "shortcuts.select-parent-layer" -msgstr "Seleccionar capa padre" - msgid "shortcuts.start-measure" msgstr "Comenzar medida" msgid "shortcuts.stop-measure" msgstr "Terminar medida" -msgid "shortcuts.thumbnail-set" -msgstr "Activar miniaturas" - msgid "shortcuts.text-align-center" msgstr "Alinear al centro" -msgid "shortcuts.text-align-left" -msgstr "Alinear a la izquierda" - msgid "shortcuts.text-align-justify" msgstr "Alinear justificado" +msgid "shortcuts.text-align-left" +msgstr "Alinear a la izquierda" + msgid "shortcuts.text-align-right" msgstr "Alinear a la derecha" +msgid "shortcuts.thumbnail-set" +msgstr "Activar miniaturas" + #: src/app/main/ui/workspace/sidebar/shortcuts.cljs msgid "shortcuts.title" msgstr "Atajos de teclado" @@ -2830,8 +3003,8 @@ msgstr "Mostrar/ocultar focus mode" msgid "shortcuts.toggle-fullscreen" msgstr "Activar/desactivar pantalla completa" -msgid "shortcuts.toggle-grid" -msgstr "Mostrar/ocultar rejilla" +msgid "shortcuts.toggle-guides" +msgstr "Mostrar/ocultar guías" msgid "shortcuts.toggle-history" msgstr "Mostrar/ocultar histórico" @@ -2848,21 +3021,24 @@ msgstr "Bloquear/Desbloquear" msgid "shortcuts.toggle-lock-size" msgstr "Bloquear/Desbloquear proporciones" -msgid "shortcuts.toggle-rules" +msgid "shortcuts.toggle-rulers" msgstr "Mostrar/ocultar reglas" -msgid "shortcuts.toggle-scale-text" -msgstr "Alternar escalado de texto" +msgid "shortcuts.scale" +msgstr "Escalado" -msgid "shortcuts.toggle-snap-grid" -msgstr "Alinear a la rejilla" +msgid "shortcuts.toggle-snap-guides" +msgstr "Alinear a las guías" -msgid "shortcuts.toggle-snap-guide" -msgstr "Alinear a las guias" +msgid "shortcuts.toggle-snap-ruler-guide" +msgstr "Alinear a las guías de reglas" msgid "shortcuts.toggle-textpalette" msgstr "Mostrar/ocultar paleta de textos" +msgid "shortcuts.toggle-theme" +msgstr "Cambiar tema" + msgid "shortcuts.toggle-visibility" msgstr "Mostrar/ocultar elemento" @@ -2893,6 +3069,13 @@ msgstr "Incrementar zoom a objetivo" msgid "shortcuts.zoom-selected" msgstr "Zoom a selección" +msgid "shortcuts.toggle-layout-grid" +msgstr "Añadir/eliminar grid layout" + +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "El nombre del webhook debe contener como máximo 2048 caracteres." + #: src/app/main/ui/dashboard/files.cljs msgid "title.dashboard.files" msgstr "%s - Penpot" @@ -2921,6 +3104,10 @@ msgstr "Bibliotecas Compartidas - %s - Penpot" msgid "title.default" msgstr "Penpot - Diseño Libre para Equipos" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "Perfil - Access tokens" + #: src/app/main/ui/settings/feedback.cljs msgid "title.settings.feedback" msgstr "Danos tu opinión - Penpot" @@ -2937,10 +3124,6 @@ msgstr "Contraseña - Penpot" msgid "title.settings.profile" msgstr "Perfil - Penpot" -#: src/app/main/ui/settings/access-tokens.cljs -msgid "title.settings.access-tokens" -msgstr "Perfil - Access tokens" - #: src/app/main/ui/dashboard/team.cljs msgid "title.team-invitations" msgstr "Invitaciones - %s - Penpot" @@ -3123,6 +3306,9 @@ msgstr "biblioteca local" msgid "workspace.assets.not-found" msgstr "No se encontraron recursos" +msgid "workspace.assets.open-library" +msgstr "Abrir el fichero de la biblioteca" + #: src/app/main/ui/workspace/sidebar/sitemap.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs @@ -3137,6 +3323,14 @@ msgstr "Renombrar grupo" msgid "workspace.assets.search" msgstr "Buscar recursos" +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.filter" +msgstr "Filtrar" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.sort" +msgstr "Ordenar" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.selected-count" msgid_plural "workspace.assets.selected-count" @@ -3144,8 +3338,8 @@ msgstr[0] "%s elemento seleccionado" msgstr[1] "%s elementos seleccionados" #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "COMPARTIDA" +msgid "workspace.assets.shared-library" +msgstr "Biblioteca compartida" #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs @@ -3226,14 +3420,14 @@ msgstr "Desactivar escala proporcional" msgid "workspace.header.menu.disable-scale-text" msgstr "Desactivar escalar texto" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Desactivar alinear a la rejilla" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "Desactivar alinear a las guias" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-snap-ruler-guides" +msgstr "Desactivar alinear a las guias de reglas" + msgid "workspace.header.menu.disable-snap-pixel-grid" msgstr "Desactivar ajuste al pixel" @@ -3248,14 +3442,14 @@ msgstr "Activar escala proporcional" msgid "workspace.header.menu.enable-scale-text" msgstr "Activar escalar texto" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Alinear a la rejilla" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "Alinear a las guias" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-snap-ruler-guides" +msgstr "Alinear a las guias de reglas" + msgid "workspace.header.menu.enable-snap-pixel-grid" msgstr "Activar ajuste al pixel" @@ -3264,8 +3458,8 @@ msgid "workspace.header.menu.hide-artboard-names" msgstr "Ocultar nombres de tableros" #: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Ocultar rejillas" +msgid "workspace.header.menu.hide-guides" +msgstr "Ocultar guías" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" @@ -3314,7 +3508,7 @@ msgid "workspace.header.menu.show-artboard-names" msgstr "Mostrar nombres de tableros" #: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" +msgid "workspace.header.menu.show-g" msgstr "Mostrar rejilla" #: src/app/main/ui/workspace/header.cljs @@ -3335,6 +3529,12 @@ msgstr "Mostrar paleta de textos" msgid "workspace.header.menu.undo" msgstr "Deshacer" +msgid "workspace.header.menu.toggle-light-theme" +msgstr "Cambiar a tema claro" + +msgid "workspace.header.menu.toggle-dark-theme" +msgstr "Cambiar a tema oscuro" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "Restablecer" @@ -3359,6 +3559,10 @@ msgstr "Cambios sin guardar" msgid "workspace.header.viewer" msgstr "Modo de visualización (%s)" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "Zoom" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-fill" msgstr "Escalar para rellenar" @@ -3379,6 +3583,27 @@ msgstr "Pantalla completa" msgid "workspace.header.zoom-selected" msgstr "Zoom a selección" +msgid "workspace.layout_grid.editor.title" +msgstr "Editando rejilla" + +msgid "workspace.layout_grid.editor.top-bar.locate" +msgstr "Mostrar" + +msgid "workspace.layout_grid.editor.top-bar.locate.tooltip" +msgstr "Mostrar grid layout" + +msgid "workspace.layout_grid.editor.top-bar.done" +msgstr "Hecho" + +msgid "workspace.layout_grid.editor.options.edit-grid" +msgstr "Editar rejilla" + +msgid "workspace.layout_grid.editor.options.exit" +msgstr "Salir" + +msgid "workspace.layout_grid.editor.padding.expand" +msgstr "Mostrar el padding a 4 lados" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.add" msgstr "Añadir" @@ -3387,6 +3612,14 @@ msgstr "Añadir" msgid "workspace.libraries.colors" msgstr "%s colores" +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "Todavía no hay estilos de color en tu biblioteca" + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "Todavía no hay tipografías en tu biblioteca" + #: src/app/main/ui/workspace/colorpicker/libraries.cljs, #: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" @@ -3401,14 +3634,6 @@ msgstr "HSV" msgid "workspace.libraries.colors.recent-colors" msgstr "Colores recientes" -#: src/app/main/ui/workspace/colorpalette.cljs -msgid "workspace.libraries.colors.empty-palette" -msgstr "Todavía no hay estilos de color en tu biblioteca" - -#: src/app/main/ui/workspace/textpalette.cljs -msgid "workspace.libraries.colors.empty-typography-palette" -msgstr "Todavía no hay tipografías en tu biblioteca" - #: src/app/main/ui/workspace/colorpicker.cljs msgid "workspace.libraries.colors.rgb-complementary" msgstr "RGB Complementario" @@ -3427,7 +3652,7 @@ msgstr "%s componentes" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.file-library" -msgstr "Biblioteca de este archivo" +msgstr "Biblioteca del archivo" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.graphics" @@ -3445,6 +3670,10 @@ msgstr "BIBLIOTECAS" msgid "workspace.libraries.library" msgstr "BIBLIOTECA" +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "ACTUALIZACIONES DE BIBLIOTECAS" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.no-libraries-need-sync" msgstr "No hay bibliotecas que necesiten ser actualizadas" @@ -3465,6 +3694,14 @@ msgstr "Buscar bibliotecas compartidas" msgid "workspace.libraries.shared-libraries" msgstr "BIBLIOTECAS COMPARTIDAS" +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.shared-library-btn" +msgstr "Conectar biblioteca" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.unlink-library-btn" +msgstr "Desconectar biblioteca" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.libraries.text.multiple-typography" msgstr "Varias tipografías" @@ -3477,6 +3714,12 @@ msgstr "Desvincular todas las tipografías" msgid "workspace.libraries.typography" msgstr "%s tipografías" +#: src/app/main/ui/workspace/sidebar/assets/common.cljs +msgid "workspace.assets.sidebar.components" +msgid_plural "workspace.assets.sidebar.components" +msgstr[0] "1 componente" +msgstr[1] "%s componentes" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.update" msgstr "Actualizar" @@ -3515,12 +3758,24 @@ msgstr "Componente" msgid "workspace.options.component.annotation" msgstr "Nota" +msgid "workspace.options.component.copy" +msgstr "Copia" + msgid "workspace.options.component.create-annotation" msgstr "Crear una nota" msgid "workspace.options.component.edit-annotation" msgstr "Editar una nota" +msgid "workspace.options.component.main" +msgstr "Principal" + +msgid "workspace.options.component.swap" +msgstr "Intercambiar componente" + +msgid "workspace.options.component.swap.empty" +msgstr "Aún no hay recursos en esta biblioteca" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints" msgstr "Restricciones" @@ -3618,6 +3873,10 @@ msgstr "Añadir inicio de flujo" msgid "workspace.options.flows.flow-start" msgstr "Inicio de flujo" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow" +msgstr "Flujo" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.flows.flow-starts" msgstr "Inicios de flujo" @@ -4146,12 +4405,12 @@ msgid "workspace.options.radius" msgstr "Radio" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Todas las esquinas" +msgid "workspace.options.radius-bottom-left" +msgstr "Abajo izquierda" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Esquinas individuales" +msgid "workspace.options.radius-bottom-right" +msgstr "Abajo derecha" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -4162,12 +4421,12 @@ msgid "workspace.options.radius-top-right" msgstr "Arriba derecha" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Abajo izquierda" +msgid "workspace.options.radius.all-corners" +msgstr "Todas las esquinas" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Abajo derecha" +msgid "workspace.options.radius.single-corners" +msgstr "Esquinas individuales" msgid "workspace.options.recent-fonts" msgstr "Recientes" @@ -4265,14 +4524,26 @@ msgstr "Borde" msgid "workspace.options.stroke-cap.circle-marker" msgstr "Marcador círculo" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "Círculo" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.diamond-marker" msgstr "Marcador diamante" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "Diamante" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.line-arrow" msgstr "Flecha de línea" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "Flecha" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.none" msgstr "Ninguno" @@ -4289,10 +4560,18 @@ msgstr "Cuadrado" msgid "workspace.options.stroke-cap.square-marker" msgstr "Marcador cuadrado" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "Rectángulo" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.triangle-arrow" msgstr "Flecha triángulo" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "Triángulo" + msgid "workspace.options.stroke-color" msgstr "Color del trazo" @@ -4331,26 +4610,10 @@ msgstr "Sólido" msgid "workspace.options.text-options.align-bottom" msgstr "Alinear abajo" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Alinear al centro (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Justificar (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Alinear a la izquierda (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Alinear al centro" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Alinear a la derecha (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Alinear arriba" @@ -4396,6 +4659,22 @@ msgstr "Nada" msgid "workspace.options.text-options.strikethrough" msgstr "Tachado (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Alinear al centro (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Justificar (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Alinear a la izquierda (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Alinear a la derecha (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Texto" @@ -4463,39 +4742,13 @@ msgstr "Separar nodos (%s)" msgid "workspace.path.actions.snap-nodes" msgstr "Alinear nodos (%s)" -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "" -"Para intentarlo de nuevo, puedes recargar este archivo. Si el problema " -"persiste, te sugerimos que compruebes la lista y consideres borrar los " -"gráficos que estén mal." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "Algunos gráficos no han podido ser actualizados." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "Convirtiendo %s/%s" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "" -"Desde ahora los gráficos de la librería serán componentes, lo cual los hará " -"mucho más potentes." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "Esta actualización sólo ocurrirá una vez." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "Actualizando %s..." - #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.add-flex" msgstr "Añadir flex layout" +msgid "workspace.shape.menu.add-grid" +msgstr "Añadir grid layout" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "Enviar al fondo" @@ -4508,6 +4761,9 @@ msgstr "Enviar atrás" msgid "workspace.shape.menu.copy" msgstr "Copiar" +msgid "workspace.shape.menu.create-annotation" +msgstr "Crear una nota" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.create-artboard-from-selection" msgstr "Tablero de selección" @@ -4620,6 +4876,10 @@ msgstr "Ruta" msgid "workspace.shape.menu.remove-flex" msgstr "Eliminar flex layout" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.remove-grid" +msgstr "Eliminar grid layout" + #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, #: src/app/main/ui/workspace/context_menu.cljs, #: src/app/main/ui/workspace/context_menu.cljs @@ -4647,9 +4907,6 @@ msgstr "Ver en el panel de recursos" msgid "workspace.shape.menu.show-main" msgstr "Ver componente principal" -msgid "workspace.shape.menu.create-annotation" -msgstr "Crear una nota" - msgid "workspace.shape.menu.thumbnail-remove" msgstr "Quitar miniatura" @@ -4908,6 +5165,10 @@ msgstr "Histórico" msgid "workspace.updates.dismiss" msgstr "Ignorar" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "Más información" + #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.there-are-updates" msgstr "Hay actualizaciones en bibliotecas compartidas" @@ -4919,174 +5180,76 @@ msgstr "Actualizar" msgid "workspace.viewport.click-to-close-path" msgstr "Pulsar para cerrar la ruta" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.lets-get-started" -msgstr "¡Empecemos!" +#, markdown +msgid "workspace.top-bar.view-only" +msgstr "**Inspeccionando código** (View only)" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.your-feedback-will-help-us" -msgstr "Tus respuestas nos ayudarán a entender tus hábitos y preferencias, lo que nos ayudará a continuar mejorando Penpot" +msgid "workspace.top-bar.read-only.done" +msgstr "Hecho" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.questions-how-are-you-planning-to-use-penpot" -msgstr "¿Qué uso piensas darle a Penpot?" +msgid "media.image" +msgstr "Imagen" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.discover-more-about-penpot" -msgstr "Conocer Penpot mejor" +msgid "media.image.short" +msgstr "img" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" -msgstr "Valorar si Penpot es adecuado para mi equipo" +msgid "media.solid" +msgstr "Sólido" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.start-to-work-on-my-project" -msgstr "Comenzar a trabajar en mi proyecto" +msgid "media.linear" +msgstr "Linear" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.get-the-code-from-my-team-project" -msgstr "Obtener código de un proyecto" +msgid "media.radial" +msgstr "Radial" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.leave-feedback-for-my-team-project" -msgstr "Dejar comentarios en un proyecto" +msgid "media.gradient" +msgstr "Gradiente" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.work-in-concept-ideas" -msgstr "Conceptualizar ideas" +msgid "media.choose-image" +msgstr "Elegir imagen" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.try-out-before-using-penpot-on-premise" -msgstr "Probar Penpot antes de usarlo en una instalación propia" +msgid "media.keep-aspect-ratio" +msgstr "Mantener la proporción" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.select-option" -msgstr "Selecciona una opción" +msgid "workspace.options.guides.title" +msgstr "Guías" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.previous" -msgstr "Anterior" +msgid "workspace.context-menu.grid-track.column.duplicate" +msgstr "Duplicar columna" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.next" -msgstr "Siguiente" +msgid "workspace.context-menu.grid-track.column.add-before" +msgstr "Añadir 1 columna a la izquierda" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.start" -msgstr "Comenzar" +msgid "workspace.context-menu.grid-track.column.add-after" +msgstr "Añadir 1 columna a la derecha" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.describe-your-experience-working-on" -msgstr "Cuánta experiencia dirías que tienes trabajando con..." +msgid "workspace.context-menu.grid-track.column.delete" +msgstr "Borrar columna" -#: src/app/main/ui/onboarding/questions.cljs -msgid "branding-illustrations-marketing-pieces" -msgstr "diseño de marca, ilustraciones, piezas de marketing..." +msgid "workspace.context-menu.grid-track.column.delete-shapes" +msgstr "Borrar columna con el contenido" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.none" -msgstr "Ninguna" +msgid "workspace.context-menu.grid-track.row.duplicate" +msgstr "Duplicar fila" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.some" -msgstr "Alguna" +msgid "workspace.context-menu.grid-track.row.add-before" +msgstr "Añadir 1 fila encima" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.a-lot" -msgstr "Mucha" +msgid "workspace.context-menu.grid-track.row.add-after" +msgstr "Añadir 1 fila debajo" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.interface-design-visual-assets-design-systems" -msgstr "diseño de interfaz, visual, sistemas de diseño..." +msgid "workspace.context-menu.grid-track.row.delete" +msgstr "Borrar fila" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.wireframes-user-journeys-flows-navigation-trees" -msgstr "prototipos, user journeys, flujos, árboles de navegación..." +msgid "workspace.context-menu.grid-track.row.delete-shapes" +msgstr "Borrar fila con el contenido" -#: src/app/main/ui/onboarding/questions.cljs -msgid "question.design-tool-more-experienced-with" -msgstr "¿Cuál es la herramienta de diseño con la que tienes más experiencia?" +msgid "workspace.context-menu.grid-cells.merge" +msgstr "Fusionar celdas" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.figma" -msgstr "Figma" +msgid "workspace.context-menu.grid-cells.area" +msgstr "Crear area" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.sketch" -msgstr "Sketch" +msgid "workspace.context-menu.grid-cells.create-board" +msgstr "Crear tablero" -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.adobe-xd" -msgstr "Adobe XD" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.canva" -msgstr "Canva" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.invision" -msgstr "InVision" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.never-used-a-tool" -msgstr "Nunca antes he usado una herramienta de diseño" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.other" -msgstr "Otra (especifica)" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.team-size" -msgstr "¿De qué tamaño es tu equipo?" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.more-than-50" -msgstr "Más de 50" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.31-50" -msgstr "31-50" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.11-30" -msgstr "11-30" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.2-10" -msgstr "2-10" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.freelancer" -msgstr "Soy freelancer" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.personal-project" -msgstr "Estoy trabajando en un proyecto personal" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.role" -msgstr "¿Cuál es tu rol?" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.designer" -msgstr "Diseño" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.developer" -msgstr "Desarrollo" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.manager" -msgstr "Gestión de producto o proyecto" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.founder" -msgstr "Dirección" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.marketing" -msgstr "Marketing" - -#: src/app/main/ui/onboarding/questions.cljs -msgid "questions.student-teacher" -msgstr "Estudiante o profesorado" diff --git a/frontend/translations/es_419.po b/frontend/translations/es_419.po new file mode 100644 index 0000000000..7255239353 --- /dev/null +++ b/frontend/translations/es_419.po @@ -0,0 +1,563 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2024-02-02 13:01+0000\n" +"Last-Translator: Yessenia Villarte Vaca \n" +"Language-Team: Spanish (Latin America) \n" +"Language: es_419\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.4-dev\n" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.confirm-password" +msgstr "Confirmar Contraseña" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-account" +msgstr "Crear cuenta demo" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-profile" +msgstr "¿Solo quieres probarlo?" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.email" +msgstr "Correo electrónico" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.forgot-password" +msgstr "¿Has olvidado tu contraseña?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.login-here" +msgstr "Inicie sesión aquí" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-submit" +msgstr "Iniciar sesión" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-github-submit" +msgstr "GitHub" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-gitlab-submit" +msgstr "GitLab" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-ldap-submit" +msgstr "LDAP" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-oidc-submit" +msgstr "Open ID" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.new-password" +msgstr "Escribe una nueva contraseña" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.invalid-token-error" +msgstr "El token de recuperación no es válido." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.password-changed-successfully" +msgstr "Contraseña cambiada correctamente" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.profile-not-verified" +msgstr "El perfil no está verificado, verifique el perfil antes de continuar." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.password" +msgstr "Contraseña" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-length-hint" +msgstr "Al menos 8 carácteres" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "La contraseña debe contener algún carácter que no sea espacio." + +msgid "auth.privacy-policy" +msgstr "Política de privacidad" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-submit" +msgstr "Recuperar contraseña" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-title" +msgstr "¿Has olvidado tu contraseña?" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.recovery-submit" +msgstr "cambia tu contraseña" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.register" +msgstr "¿No tienes cuenta aún?" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.register-submit" +msgstr "Crea una cuenta" + +#: src/app/main/ui/auth.cljs +msgid "auth.sidebar-tagline" +msgstr "La solución de código abierto para diseño y creación de prototipos." + +msgid "auth.terms-privacy-agreement" +msgstr "" +"Al crear una nueva cuenta, acepta nuestros términos de servicio y política " +"de privacidad." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.verification-email-sent" +msgstr "Hemos enviado un correo electrónico de verificación" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "...marca, ilustraciones, piezas de marketing, etc." + +msgid "common.publish" +msgstr "Publicar" + +msgid "common.share-link.all-users" +msgstr "Todos los usuarios de Penpot" + +msgid "common.share-link.current-tag" +msgstr "(actual)" + +msgid "common.share-link.destroy-link" +msgstr "Borrar enlace" + +msgid "common.share-link.get-link" +msgstr "Conseguir enlace" + +msgid "common.share-link.page-shared" +msgid_plural "common.share-link.page-shared" +msgstr[0] "1 página compartida" +msgstr[1] "%s paginas compartidas" + +msgid "common.share-link.permissions-can-comment" +msgstr "Puedes comentar" + +msgid "common.share-link.permissions-can-inspect" +msgstr "Puedes inspeccionar el código" + +msgid "common.share-link.permissions-hint" +msgstr "Cualquier persona con enlace tendrá acceso" + +msgid "common.share-link.permissions-pages" +msgstr "Páginas compartidas" + +msgid "common.share-link.view-all" +msgstr "Seleccionar todo" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.management" +msgstr "Gestión de equipos" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.tutorial-hero.title" +msgstr "Tutorial práctico" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.start" +msgstr "Iniciar el recorrido" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.title" +msgstr "Tutorial de la interfaz" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "Token copiado" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "Token de acceso creado correctamente." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "No tienes tokens hasta el momento." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 días" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 días" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 días" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 días" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "Nunca" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "Vence el %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "Sin fecha de vencimiento" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "El token caducará el %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "El token no tiene fecha de vencimiento" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.add-shared" +msgstr "Agregar como biblioteca compartida" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.change-email" +msgstr "Cambiar el correo electrónico" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.default-team-name" +msgstr "Tu Penpot" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.delete-team" +msgstr "Eliminar equipo" + +msgid "dashboard.download-binary-file" +msgstr "Descargar el archivo Penpot (.penpot)" + +msgid "dashboard.download-standard-file" +msgstr "Descargar archivo estándar (.svg + .json)" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate" +msgstr "Duplicar" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate-multi" +msgstr "Duplicar %s archivos" + +#: src/app/main/ui/dashboard/grid.cljs +#, markdown +msgid "dashboard.empty-placeholder-drafts" +msgstr "" +"Los archivos agregados a las Bibliotecas aparecerán aquí. Intente compartir " +"sus archivos o agréguelos desde nuestras [Libraries & " +"templates](https://penpot.app/libraries-templates.html)." + +msgid "dashboard.export-binary-multi" +msgstr "Descargar %s archivos Penpot (.penpot)" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.already-have-account" +msgstr "¿Ya tienes una cuenta?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.check-your-email" +msgstr "" +"Revise su correo electrónico y haga clic en el enlace para verificar y " +"comenzar a usar Penpot." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.demo-warning" +msgstr "" +"Este es un servicio DEMO, NO LO UTILICE para trabajos reales, los proyectos " +"se borrarán periódicamente." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.fullname" +msgstr "Nombre completo" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-title" +msgstr "¡Qué bueno verte de nuevo!" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-google-submit" +msgstr "Google" + +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "El nombre debe contener algún carácter distinto al del espacio." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "El nombre debe contener como máximo 250 caracteres." + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.recovery-token-sent" +msgstr "" +"El enlace de recuperación de contraseña ha sido enviado a su bandeja de " +"entrada de su correo electrónico." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "auth.notifications.team-invitation-accepted" +msgstr "Se unió al equipo con éxito" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-subtitle" +msgstr "Le enviaremos un correo electrónico con instrucciones" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-subtitle" +msgstr "Es gratis, es de código abierto" + +msgid "auth.terms-of-service" +msgstr "Términos de servicio" + +#: src/app/main/ui/auth/register.cljs +#, markdown +msgid "auth.terms-privacy-agreement-md" +msgstr "" +"Al crear una nueva cuenta, acepta nuestros [terms of service](%s) y nuestra [" +"privacy policy](%s)." + +msgid "common.share-link.confirm-deletion-link-description" +msgstr "" +"¿Estás seguro de que deseas eliminar este enlace? Si lo haces ya no estará " +"disponible para nadie" + +msgid "common.share-link.link-copied-success" +msgstr "Enlace copiado exitosamente" + +msgid "common.share-link.manage-ops" +msgstr "Administrar permisos" + +msgid "common.share-link.placeholder" +msgstr "El enlace para compartir aparecerá aquí" + +msgid "common.share-link.team-members" +msgstr "Solo miembros del equipo" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-title" +msgstr "Crea una cuenta" + +msgid "common.share-link.title" +msgstr "Compartir prototipos" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.text" +msgstr "" +"Penpot está destinado a equipos. Invite a miembros a trabajar juntos en " +"proyectos y archivos" + +msgid "common.unpublish" +msgstr "Despublicar" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.title" +msgstr "¡En equipo!" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.tutorial-hero.info" +msgstr "" +"Aprenda los conceptos básicos en Penpot mientras se divierte con este " +"tutorial práctico." + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.tutorial-hero.start" +msgstr "Iniciar el tutorial" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.info" +msgstr "Date un paseo por Penpot y conoce sus principales características." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "Presione el botón \"Generar nuevo token\" para generar uno." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "Generar nuevo token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "El nombre es requerido" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "Expirado el %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Tokens de acceso personal" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Los tokens de acceso personal funcionan como una alternativa a nuestro " +"sistema de autenticación de inicio de sesión/contraseña y pueden usarse para " +"permitir que una aplicación acceda a la API interna de Penpot" + +#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs +msgid "dashboard.copy-suffix" +msgstr "(copiar)" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.create-new-team" +msgstr "Crear nuevo equipo" + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-shapes.how-to-link" +msgstr "Información sobre cómo configurar las exportaciones en Penpot." + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-shapes.no-elements" +msgstr "No hay elementos con configuración de exportación." + +msgid "dashboard.export-frames" +msgstr "Exportar tableros como PDF" + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-frames.title" +msgstr "Exportar como PDF" + +msgid "dashboard.export-multi" +msgstr "Exportar %s archivos de Penpot" + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-multiple.selected" +msgstr "%s de %s elementos seleccionados" + +#: src/app/main/ui/workspace/header.cljs +msgid "dashboard.export-shapes" +msgstr "Exportar" + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-shapes.how-to" +msgstr "" +"Puede agregar configuraciones de exportación a elementos desde las " +"propiedades de diseño (en la parte inferior de la barra lateral derecha)." + +msgid "dashboard.export-standard-multi" +msgstr "Descargar %s archivos estándar (.svg + .json)" + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-shapes.title" +msgstr "Selección de exportación" + +msgid "dashboard.export.detail" +msgstr "* Puede incluir componentes, gráficos, colores y/o tipografías." + +msgid "dashboard.export.explain" +msgstr "" +"Uno o más archivos que desea exportar utilizan bibliotecas compartidas. ¿Qué " +"quiere hacer con sus activos*?" + +msgid "dashboard.export.options.all.message" +msgstr "" +"Los archivos con bibliotecas compartidas se incluirán en la exportación, " +"manteniendo su vinculación." + +msgid "dashboard.export.options.all.title" +msgstr "Exportar bibliotecas compartidas" + +msgid "dashboard.export.options.detach.message" +msgstr "" +"Las bibliotecas compartidas no se incluirán en la exportación y no se " +"agregarán activos a la biblioteca. " + +msgid "dashboard.export.options.detach.title" +msgstr "Trate los activos de biblioteca compartidos como objetos básicos" + +msgid "dashboard.export.options.merge.message" +msgstr "" +"Su archivo se exportará con todos los activos externos combinados en la " +"biblioteca de archivos." + +msgid "dashboard.export.options.merge.title" +msgstr "Incluir recursos de biblioteca compartidos en bibliotecas de archivos" + +msgid "dashboard.import.progress.process-typographies" +msgstr "Procesamiento de tipografías" + +msgid "dashboard.import.progress.upload-data" +msgstr "Subiendo datos al servidor (%s/%s)" + +msgid "dashboard.import.progress.upload-media" +msgstr "Subiendo archivo: %s" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.invite-profile" +msgstr "Invitar a la gente" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.leave-team" +msgstr "dejar el equipo" + +msgid "dashboard.libraries-and-templates" +msgstr "Bibliotecas y plantillas" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "dashboard.libraries-title" +msgstr "Bibliotecas" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.loading-files" +msgstr "cargando tus archivos…" + +msgid "dashboard.libraries-and-templates.explore" +msgstr "Explore más de ellos y sepa cómo contribuir" + +msgid "dashboard.libraries-and-templates.import-error" +msgstr "" +"Hubo un problema al importar la plantilla. La plantilla no fue importada." + +msgid "dashboard.loading-fonts" +msgstr "cargando tus fuentes…" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to" +msgstr "Mover a" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-multi" +msgstr "Mover %s archivos a" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-other-team" +msgstr "Pasar a otro equipo" + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +msgid "dashboard.new-file" +msgstr "+ Nuevo archivo" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-file-prefix" +msgstr "Archivo nuevo" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.new-project" +msgstr "+ Nuevo proyecto" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-project-prefix" +msgstr "Nuevo proyecto" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.no-matches-for" +msgstr "No se encontraron coincidencias para \"%s\"" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.no-projects-placeholder" +msgstr "Los proyectos fijados aparecerán aquí" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-changed-successfully" +msgstr "Su dirección de correo electrónico se ha actualizado correctamente" diff --git a/frontend/translations/eu.po b/frontend/translations/eu.po index afa5c75e5e..9436e9d854 100644 --- a/frontend/translations/eu.po +++ b/frontend/translations/eu.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "PO-Revision-Date: 2023-07-01 12:52+0000\n" "Last-Translator: Mikel Larreategi \n" -"Language-Team: Basque \n" +"Language-Team: Basque " +"\n" "Language: eu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" @@ -39,7 +39,8 @@ msgstr "" "Hau PROBAK EGITEKO zerbitzua da. EZ ERABILI benetako lana egiteko, hemengo " "proiektuak noizean behin ezabatu egingo dira." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" msgstr "Posta elektronikoa" @@ -263,7 +264,8 @@ msgstr "Hasi" msgid "dasboard.walkthrough-hero.title" msgstr "Interfazea ezagutu" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Gehitu partekatutako liburutegi bezala" @@ -293,7 +295,8 @@ msgstr "Deskargatu Penpot fitxategia (.penpot)" msgid "dashboard.download-standard-file" msgstr "Deskargatu fitxategi estandarra (.svg + .json)" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate" msgstr "Bikoiztu" @@ -302,12 +305,11 @@ msgid "dashboard.duplicate-multi" msgstr "%s fitxategi bizkoiztu" #: src/app/main/ui/dashboard/grid.cljs -#, markdown msgid "dashboard.empty-placeholder-drafts" msgstr "" "Oh ez! Oraindik ez duzu fitxategirik! Txantiloi batekin proba egin nahi " -"baduzu joan [Liburutegi eta txantiloiak](https://penpot.app/libraries-" -"templates.html) atalera." +"baduzu joan [Liburutegi eta " +"txantiloiak](https://penpot.app/libraries-templates.html) atalera." msgid "dashboard.export-binary-multi" msgstr "Deskargatu %s Penpot fitxategi (.penpot)" @@ -400,7 +402,6 @@ msgid_plural "dashboard.fonts.fonts-added" msgstr[0] "Letra-tipo 1 gehitu da" msgstr[1] "%s letra-tipo gehitu dira" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "Hemen gehitutako edozein letra-tipo pertsonalizatu, talde honen fitxategien " @@ -409,19 +410,27 @@ msgstr "" "honetako letra-tipoak kargatu daitezke: **TTF, OTF and WOFF** (batekin " "nahikoa da)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "Zureak diren edo Penpoten erabiltzeko lizentzia duzun letra-tipoak bakarrik " -"kargatu ditzakezu. Informazio gehiago lortzeko irakurri Edukiaren eskubideen " -"atala: [Penpoten erabilpen baldintzak](https://penpot.app/terms.html). Letra-" -"tipoen lizentzien inguruan irakurtzea ere interesgarria izan daiteke: [letra-" -"tipoen lizentziak](https://www.typography.com/faq)." +"kargatu ditzakezu. Informazio gehiago lortzeko irakurri Edukiaren " +"eskubideen atala: [Penpoten erabilpen " +"baldintzak](https://penpot.app/terms.html). Letra-tipoen lizentzien " +"inguruan irakurtzea ere interesgarria izan daiteke: [letra-tipoen " +"lizentziak](https://www.typography.com/faq)." #: src/app/main/ui/dashboard/fonts.cljs msgid "dashboard.fonts.upload-all" msgstr "Kargatu guztiak" +msgid "dashboard.fonts.warning-text" +msgstr "" +"Zure letra-tipoek sistema eragile desberdinetan metrika bertikalekin " +"arazoak izan ditzaketela detektatu dugu. Zure letra-tipoa nola ikusten den " +"zerbitzu [hau](https://vertical-metrics.netlify.app) erabiliz egiaztatu " +"dezakezu. Gainera, weberako letra-tipoak sortzeko " +"[Transfonter](https://transfonter.org/) erabiltzea gomendatzen dugu. " + msgid "dashboard.import" msgstr "Inportatu Penpot fitxategiak" @@ -431,6 +440,9 @@ msgstr "Ezin izan dugu fitxategia inportatu" msgid "dashboard.import.import-error" msgstr "Errorea gertatu da fitxategia inportatzean. Ezin izan da inportatu." +msgid "dashboard.import.import-message" +msgstr "%s fitxategi ondo inportatu dira." + msgid "dashboard.import.import-warning" msgstr "Fitxategi batzuk inportatu ez diren objektu akasdunak dituzte." @@ -459,7 +471,8 @@ msgstr "Fitxategia bidaltzen: %s" msgid "dashboard.invite-profile" msgstr "Gonbidatu taldera" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" msgstr "Utzi taldea" @@ -483,7 +496,8 @@ msgstr "zure fitxategiak kargatzen…" msgid "dashboard.loading-fonts" msgstr "zure letra-tipoak kargatzen…" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "Mugitu" @@ -495,7 +509,8 @@ msgstr "Mugitu %s fitxategi" msgid "dashboard.move-to-other-team" msgstr "Mugitu beste talde batera" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" msgstr "+ Fitxategi berria" @@ -558,7 +573,8 @@ msgstr "Proiektuak" msgid "dashboard.remove-account" msgstr "Kontua ezabatu nahi duzu?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" msgstr "Ezabatu partekatutako liburutegi gisa" @@ -586,15 +602,24 @@ msgstr "Aukeratu gaia" msgid "dashboard.show-all-files" msgstr "Ikusi fitxategi guztiak" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgstr "Zure fitxategia ondo ezabatu da" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-delete-project" msgstr "Zure proiektua ondo ezabatu da" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgstr "Zure fitxategia ondo bikoiztu da" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-duplicate-project" msgstr "Zure proiektua ondo bikoiztu da" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-file" msgstr "Zure fitxategia ondo mugitu da" @@ -630,14 +655,45 @@ msgstr "Bilaketaren emaitzak" msgid "dashboard.type-something" msgstr "Idatzi bilaktzeko zerbaitu" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.unpublish-shared" msgstr "Atzera bota liburutegia argitaratzea" -#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Eguneratu aukerak" +msgid "dashboard.webhooks.active" +msgstr "Aktibo" + +msgid "dashboard.webhooks.active.explain" +msgstr "Webhook hau aktibatzen denean gertaeraren xehetasunak bidaliko dira" + +msgid "dashboard.webhooks.content-type" +msgstr "Elementu mota" + +msgid "dashboard.webhooks.create" +msgstr "Sortu webhooka" + +msgid "dashboard.webhooks.create.success" +msgstr "Webhooka ondo sortu da." + +msgid "dashboard.webhooks.description" +msgstr "" +"Webhookak beste webgune batzuei Penpoten zerbait gertatu dela jakinarazteko " +"modu bat dira. Adierazitako URLtara POST eskaera bat bidaliko dugu." + +msgid "dashboard.webhooks.empty.add-one" +msgstr "Sakatu \"Sortu webhooka\" botoia bat gehitzeko." + +msgid "dashboard.webhooks.empty.no-webhooks" +msgstr "Ez dago webhookik." + +msgid "dashboard.webhooks.update.success" +msgstr "Webhooka ondo aldatu da." + #: src/app/main/ui/settings.cljs msgid "dashboard.your-account-title" msgstr "Zure kontua" @@ -650,7 +706,11 @@ msgstr "Eposta" msgid "dashboard.your-name" msgstr "Izena" -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" msgstr "Zure Penpot" @@ -695,7 +755,8 @@ msgstr "Ezin izan dira %s letra-tipoak kargatu" msgid "errors.clipboard-not-implemented" msgstr "Zure nabigatzaileak ezin du hori egin" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/change_email.cljs msgid "errors.email-already-exists" msgstr "Eposta helbide hori erabilita dago" @@ -706,10 +767,18 @@ msgstr "Eposta helbide hori egiaztatuta dago." msgid "errors.email-as-password" msgstr "Ezin duzu zure eposta helbidea pasahitz gisa erabiliz" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" msgstr "«%s» helbideak ez ditu mezuak ondo jasotzen, itzuli egiten ditu." +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, +#: src/app/main/ui/auth/recovery_request.cljs +msgid "errors.email-invalid" +msgstr "Mesedez, idatzi eposta helbide zuzen bat" + #: src/app/main/ui/settings/change_email.cljs msgid "errors.email-invalid-confirmation" msgstr "Egiaztapenereko epostak bat etorri behar du aurrekoarekin" @@ -719,7 +788,19 @@ msgstr "" "«%s» helbideak ez ditu mezuak ondo jasotzen, itzuli egiten ditu edo " "spamaren inguruko txostenak ditu." -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/errors.cljs +msgid "errors.feature-mismatch" +msgstr "" +"Badirudi '%s' ezaugarria aktibo duen fitxategi bat irekitzen ari zarela " +"baina zure penpot frontendak ezin du hori egin edo ezaugarri hori " +"desaktibatuta du." + +#: src/app/main/errors.cljs +msgid "errors.feature-not-supported" +msgstr "Ezaugarria ezin da erabili: '%s'." + +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.generic" msgstr "Erroreren bat gertatu da." @@ -738,6 +819,12 @@ msgstr "Gonbidapen hau bertan behera utzi dute edo iraungi egin da." msgid "errors.ldap-disabled" msgstr "LDAP bidez sartzea desgaituta dago." +#: src/app/main/errors.cljs +msgid "errors.max-quote-reached" +msgstr "" +"Kuotaren maximora heldu zara: '%s'. Jarri kontaktuan laguntza " +"zerbitzuarekin." + #: src/app/main/data/workspace/persistence.cljs msgid "errors.media-too-large" msgstr "Irudia handiegia da (5mb baino gutxiago izan behar ditu)." @@ -746,7 +833,7 @@ msgstr "Irudia handiegia da (5mb baino gutxiago izan behar ditu)." msgid "errors.media-type-mismatch" msgstr "Irudiaren edukia eta luzapena bat ez datozela dirudi." -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" msgstr "Irudia zuzena ez dela dirudi." @@ -767,7 +854,9 @@ msgstr "Pasahitzak gutxienez 8 karaktere izan behar ditu" msgid "errors.profile-blocked" msgstr "Profila blokeatuta dago" -#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.profile-is-muted" msgstr "" "Zure profilak ez ditu eposta mezuak jasotzen (spam gisa markatu delako edo " @@ -788,7 +877,9 @@ msgstr "Izendatu nahi duzun kidea ez da existitzen." msgid "errors.team-leave.owner-cant-leave" msgstr "Jabea ezin da taldetik irten, jabetza beste pertsona bati eman behar diozu." -#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/data/media.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "errors.unexpected-error" msgstr "Errore bat gertatut da." @@ -796,6 +887,27 @@ msgstr "Errore bat gertatut da." msgid "errors.unexpected-token" msgstr "Tokena ez da zuzena" +msgid "errors.webhooks.connection" +msgstr "Konexio errorea, URLa ezin da ireki" + +msgid "errors.webhooks.invalid-uri" +msgstr "URLak ez du balidazioa gainditu." + +msgid "errors.webhooks.last-delivery" +msgstr "Errore bat gertatu da azken bidalketan." + +msgid "errors.webhooks.ssl-validation" +msgstr "Errorea gertatu da SSL balidazioan." + +msgid "errors.webhooks.timeout" +msgstr "Denbora muga gainditu da" + +msgid "errors.webhooks.unexpected" +msgstr "Errore ezezaguna balidazioan" + +msgid "errors.webhooks.unexpected-status" +msgstr "Espero ez zen egoera %s" + #: src/app/main/ui/auth/login.cljs msgid "errors.wrong-credentials" msgstr "Izena edo pasahitza ez dira zuzenak." @@ -836,7 +948,7 @@ msgstr "Posta elektronikoa" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Twitterrera joan" +msgstr "Xrera joan" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -844,7 +956,7 @@ msgstr "Zure zalantza teknikoak erantzuteko kontua." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Laguntzarako Twitter kontua" +msgstr "Laguntzarako X kontua" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -898,7 +1010,8 @@ msgstr "Altuera" msgid "inspect.attributes.layout.left" msgstr "Ezkerra" -#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs +#: src/app/main/ui/inspect/attributes/layout.cljs, +#: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.layout.radius" msgstr "Erradioa" @@ -918,19 +1031,20 @@ msgstr "Zabalera" msgid "inspect.attributes.shadow" msgstr "Itzala" +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.size" +msgstr "Tamaina eta posizioa" + #: src/app/main/ui/inspect/attributes/stroke.cljs msgid "inspect.attributes.stroke" msgstr "Ertza" -#, permanent msgid "inspect.attributes.stroke.alignment.center" msgstr "Erdia" -#, permanent msgid "inspect.attributes.stroke.alignment.inner" msgstr "Barnealdea" -#, permanent msgid "inspect.attributes.stroke.alignment.outer" msgstr "Kanpoaldea" @@ -966,6 +1080,10 @@ msgstr "Letra-tipoaren tamaina" msgid "inspect.attributes.typography.font-style" msgstr "Letra-tipoaren estiloa" +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-weight" +msgstr "Letra tipoaren lodiera" + #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.letter-spacing" msgstr "Hizkien tartea" @@ -1003,6 +1121,19 @@ msgstr "Lehenengoa letra larriz" msgid "inspect.attributes.typography.text-transform.uppercase" msgstr "Letra larriz" +msgid "inspect.empty.help" +msgstr "" +"Diseinua ikuskatzeari buruz gehiago jakin nahi baduzu zoaz Penpoten " +"laguntza zentrora" + +msgid "inspect.empty.more-info" +msgstr "Informazio gehiago ikuskatzeari buruz" + +msgid "inspect.empty.select" +msgstr "" +"Aukeratu forma bat, taula bat edo talde bat beren propietateak eta kodea " +"ikuskatzeko" + #: src/app/main/ui/inspect/right_sidebar.cljs msgid "inspect.tabs.code" msgstr "Kodea" @@ -1055,10 +1186,13 @@ msgstr "Lasterteklak" msgid "labels.accept" msgstr "Onartu" +msgid "labels.active" +msgstr "Aktibo" + msgid "labels.add-custom-font" msgstr "Gehitu letra-tipo pertsonalizatua" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "Kudeaketa" @@ -1110,11 +1244,16 @@ msgstr "Honekin jarraitu" msgid "labels.continue-with-penpot" msgstr "Penpot kontu batekin jarraitu dezakezu" +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.copy-invitation-link" +msgstr "Kopiatu esteka" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "labels.create" msgstr "Sortu" -#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team" msgstr "Sortu talde berria" @@ -1129,7 +1268,8 @@ msgstr "Pertsonalizatutako letra-tipoak" msgid "labels.dashboard" msgstr "Lanlekua" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "Ezabatu" @@ -1149,7 +1289,10 @@ msgstr "Ezabatu gonbidapena" msgid "labels.delete-multi-files" msgstr "Ezabatu %s fitxategi" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/files.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.drafts" msgstr "Zirriborroak" @@ -1160,7 +1303,7 @@ msgstr "Editatu" msgid "labels.edit-file" msgstr "Editatu fitxategia" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "Edizioa" @@ -1195,7 +1338,9 @@ msgstr "Letra-tipoak" msgid "labels.github-repo" msgstr "GitHubeko errepositorioa" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "Eman zure iritzia" @@ -1210,6 +1355,9 @@ msgstr "Laguntza zentroa" msgid "labels.hide-resolved-comments" msgstr "Ezkutatu ebatzitzako iruzkinak" +msgid "labels.inactive" +msgstr "Inaktibo" + msgid "labels.installed-fonts" msgstr "Instalatutako letra-tipoak" @@ -1223,7 +1371,8 @@ msgstr "" msgid "labels.internal-error.main-message" msgstr "Barneko errorea" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.invitations" msgstr "Gonbidapenak" @@ -1242,11 +1391,11 @@ msgstr "Sartu edo eman izena" msgid "labels.logout" msgstr "Irten" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.member" msgstr "Kidea" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "Kideak" @@ -1254,7 +1403,8 @@ msgstr "Kideak" msgid "labels.new-password" msgstr "Pasahitz berria" -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +#: src/app/main/ui/workspace/comments.cljs, +#: src/app/main/ui/dashboard/comments.cljs msgid "labels.no-comments-available" msgstr "Ez duzu iruzkinen inguruko jakinarazpenik." @@ -1263,7 +1413,6 @@ msgid "labels.no-invitations" msgstr "Ez dago gonbidapenik." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "Sakatu 'Taldera gonbdiatu' taldekide gehiago izateko." @@ -1307,7 +1456,8 @@ msgstr "edo" msgid "labels.owner" msgstr "Jabea" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.password" msgstr "Pasahitza" @@ -1327,7 +1477,12 @@ msgstr "Proiektuak" msgid "labels.release-notes" msgstr "Bertsioaren oharrak" -#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/workspace.cljs +msgid "labels.reload-file" +msgstr "Birkargatu fitxategia" + +#: src/app/main/ui/workspace/libraries.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" msgstr "Kendu" @@ -1335,7 +1490,9 @@ msgstr "Kendu" msgid "labels.remove-member" msgstr "Kendu kidea" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.rename" msgstr "Berrizendatu" @@ -1347,7 +1504,7 @@ msgstr "Berrizendatu taldea" msgid "labels.resend-invitation" msgstr "Birbidali gonbidapena" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Berriz saiatu" @@ -1377,7 +1534,8 @@ msgstr "Gure sistemaren programatutako mantentze-lanak egiten ari gara." msgid "labels.service-unavailable.main-message" msgstr "Zerbitzua ez dago martxan" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.settings" msgstr "Ezarpenak" @@ -1407,6 +1565,10 @@ msgstr "Egoera" msgid "labels.tutorials" msgstr "Tutorialak" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.unpublish-multi-files" +msgstr "%s fitxategi argitaratzeari utzi" + #: src/app/main/ui/settings/profile.cljs msgid "labels.update" msgstr "Eguneratu" @@ -1424,15 +1586,21 @@ msgstr "Kargatu letra-tipoa" msgid "labels.uploading" msgstr "Kargatzen…" +msgid "labels.view-only" +msgstr "IKUSTEKO BAKARRIK" + #: src/app/main/ui/dashboard/team.cljs msgid "labels.viewer" msgstr "Bistarazlea" +msgid "labels.webhooks" +msgstr "Webhookak" + #: src/app/main/ui/comments.cljs msgid "labels.write-new-comment" msgstr "Idatzi iruzkin berria" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.you" msgstr "(zu)" @@ -1440,21 +1608,24 @@ msgstr "(zu)" msgid "labels.your-account" msgstr "zure kontua" -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "Irudia kargatzen…" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" msgstr "Gehitu partekatutako liburutegi gisa" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.hint" msgstr "" "Partekatutako liburutegi gisa gehitu ostean, fitxategi honetako baliabideak " "beste fitxategietan erabiltzeko bezala egongo dira." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.message" msgstr "Gehitu \"%s\" partekatutako liburutegi gisa" @@ -1484,6 +1655,18 @@ msgstr "Aldatu posta elektronikoa" msgid "modals.change-email.title" msgstr "Aldatu zure posta elektronikoa" +msgid "modals.create-webhook.submit-label" +msgstr "Sortu webhooka" + +msgid "modals.create-webhook.title" +msgstr "Sortu webhooka" + +msgid "modals.create-webhook.url.label" +msgstr "Informazioaren URLa" + +msgid "modals.create-webhook.url.placeholder" +msgstr "https://example.com/postreceive" + #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.cancel" msgstr "Utzi eta mantendu nire kontua" @@ -1576,29 +1759,22 @@ msgstr "Benetan proiektu hau ezabatu egin nahi duzu?" msgid "modals.delete-project-confirm.title" msgstr "Ezabatu proiektua" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.accept" msgid_plural "modals.delete-shared-confirm.accept" msgstr[0] "Ezabatu fitxategia" msgstr[1] "Ezabatu fitxategiak" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "Benetan fitxategi hau ezabatu nahi duzu?" msgstr[1] "Benetan fitxategi hauek ezabatu nahi dituzu?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "" -"Ezabatu nahi duzun fitxategiak, fitxategi honetan erabiltzen den liburutegi " -"bat du:" -msgstr[1] "" -"Ezabatu nahi duzuen fitxategiak, fitxategi hauetan erabiltzen den " -"liburutegi bat du:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" msgstr[0] "Ezabatu fitxategia" @@ -1630,6 +1806,21 @@ msgstr "Benetan kide hau taldetik ezabatu egin nahi duzu?" msgid "modals.delete-team-member-confirm.title" msgstr "Taldekidea ezabatzen" +msgid "modals.delete-webhook.accept" +msgstr "Ezabatu webhooka" + +msgid "modals.delete-webhook.message" +msgstr "Benetan webhook hau ezabatu egin nahi duzu?" + +msgid "modals.delete-webhook.title" +msgstr "Webhooka ezabatzen" + +msgid "modals.edit-webhook.submit-label" +msgstr "Aldatu webhooka" + +msgid "modals.edit-webhook.title" +msgstr "Aldatu webhooka" + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-member-confirm.accept" msgstr "Bidali gonbidapena" @@ -1637,6 +1828,9 @@ msgstr "Bidali gonbidapena" msgid "modals.invite-member.emails" msgstr "Posta elektronikoak, komarekin banatuta" +msgid "modals.invite-member.repeated-invitation" +msgstr "Eposta helbide batzuk jada taldekideenak dira. Ez da gonbidapenik bidaliko." + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-team-member.title" msgstr "Gonbidatu kideak taldera" @@ -1706,17 +1900,20 @@ msgstr "Taldearen jabea zara. Benetan %s taldearen gabe egin nahi duzu?" msgid "modals.promote-owner-confirm.title" msgstr "Taldearen jabe berria" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" msgstr "Ezabatu partekatutako liburutegi gisa" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.hint" msgstr "" "Partekatutako liburutegi gisa ezabatu ostean, fitxategi honen liburutegia " "ezingo da zure beste fitxategietan erabili." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.message" msgstr "Gehitu \"%s\" partekatutako liburutegi bezala" @@ -1724,61 +1921,56 @@ msgstr "Gehitu \"%s\" partekatutako liburutegi bezala" msgid "modals.small-nudge" msgstr "Gutxienekoa" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Argitaratzea atzera botatzen baduzu, elementu horiek ez dira beste " -"fitxategietan agertuko. Elementuak erabiltzen ari bazara fitxategi honetan " -"geldituko dira (diseinurik ez da apurtuko!)." -msgstr[1] "" -"Argitaratzea atzera botatzen baduzu, elementu horiek ez dira beste " -"fitxategietan agertuko. Elementuak erabiltzen ari bazara fitxategi honetan " -"geldituko dira (diseinurik ez da apurtuko!)." +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.accept" +msgstr "Argitaratzea atzera bota" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "Benetan liburutegi honen argitaratzea atzera bota nahi duzu?" msgstr[1] "Benetan liburutegi hauen argitaratzea atzera bota nahi duzu?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Fitxategi honetan erabiltzen ari da:" -msgstr[1] "Fitxategi hauetan erabiltzen ari da:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" msgid_plural "modals.unpublish-shared-confirm.title" msgstr[0] "Liburutegiaren argitaratzea atzera bota" msgstr[1] "Liburutegian argitaratzea atzera bota" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.hint" msgstr "" "Partekatutako liburutegi baten osagaiak eguneratzera zoaz. Honek berau " "darabilten beste fitxategi batzuengan eragina izan dezake." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.message" msgstr "Eguneratu liburutegiaren osagaiak" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.accept" msgstr "Eguneratu" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.cancel" msgstr "Utzi" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.hint" msgstr "" "Partekatutako liburutegi baten osagai bat eguneratzera zoaz. Honek berau " "darabilten beste fitxategi batzuengan eragina izan dezake." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.message" msgstr "Eguneratu liburutegiaren osagaia" @@ -1786,6 +1978,10 @@ msgstr "Eguneratu liburutegiaren osagaia" msgid "notifications.invitation-email-sent" msgstr "Gonbidapena ondo bidali da" +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-link-copied" +msgstr "Gonbidapenaren esteka kopiatu da" + #: src/app/main/ui/settings/delete_account.cljs msgid "notifications.profile-deletion-not-allowed" msgstr "Ezin da zure profila ezabatu. Berresleitu zure taldeak jarraitu aurretik." @@ -1797,8 +1993,8 @@ msgstr "Profila ondo gorde da!" #: src/app/main/ui/settings/change_email.cljs msgid "notifications.validation-email-sent" msgstr "" -"Posta elektronikoa egiaztatzeko mezua ondo bidali da %s helbidera. Egiaztatu " -"zure helbidea!" +"Posta elektronikoa egiaztatzeko mezua ondo bidali da %s helbidera. " +"Egiaztatu zure helbidea!" msgid "onboarding-v2.before-start.desc1" msgstr "" @@ -1870,12 +2066,6 @@ msgstr "Laguntzeko gida" msgid "onboarding-v2.welcome.title" msgstr "Ongi etorri Penpotera!" -msgid "onboarding.choice.team-up.create-later" -msgstr "Sortu taldea beranduago" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Zure taldearen izena" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "Taldeari izena eman ostean, pertsona gehiago gonbidatu ditzakezu." @@ -1890,12 +2080,6 @@ msgstr "" "Ez ahaztu garapeneko, diseinuko, kudeaketako... pertsonak sartzea, " "dibertsitatea ona da :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Sortu taldea eta ondoren gonbidatu" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Sortu taldea eta bidali gonbidapenak" - msgid "onboarding.choice.team-up.roles" msgstr "Gonbidatu rol honekin:" @@ -1949,7 +2133,12 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "Sartu" -#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "Batzuk" @@ -2003,6 +2192,9 @@ msgstr "Bidea" msgid "shortcut-subsection.shape" msgstr "Formak" +msgid "shortcut-subsection.text-editor" +msgstr "Testuak" + msgid "shortcut-subsection.tools" msgstr "Tresnak" @@ -2021,9 +2213,15 @@ msgstr "Gehitu nodoa" msgid "shortcuts.align-bottom" msgstr "Lerrokatu behean" +msgid "shortcuts.align-center" +msgstr "Erdian lerrokatu" + msgid "shortcuts.align-hcenter" msgstr "Lerrokatu erdian horizontalki" +msgid "shortcuts.align-justify" +msgstr "Justifikatuta lerrokatu" + msgid "shortcuts.align-left" msgstr "Lerrokatu ezkerrean" @@ -2039,6 +2237,9 @@ msgstr "Lerrokatu erdian bertikalki" msgid "shortcuts.artboard-selection" msgstr "Sortu arbela hautapenetik" +msgid "shortcuts.bold" +msgstr "Aktibatu/desaktibatu beltza" + msgid "shortcuts.bool-difference" msgstr "Diferentzia" @@ -2129,6 +2330,12 @@ msgstr "Irauli horizontalki" msgid "shortcuts.flip-vertical" msgstr "Irauli bertikalki" +msgid "shortcuts.font-size-dec" +msgstr "Letra tipoaren tamaina txikitu" + +msgid "shortcuts.font-size-inc" +msgstr "Letra tipoaren tamaina handitu" + msgid "shortcuts.go-to-drafts" msgstr "Joan zirriborroetara" @@ -2153,9 +2360,27 @@ msgstr "Zooma handitu" msgid "shortcuts.insert-image" msgstr "Txertatu irudia" +msgid "shortcuts.italic" +msgstr "Aktibatu/desaktibatu etzana" + msgid "shortcuts.join-nodes" msgstr "Elkartu nodoak" +msgid "shortcuts.letter-spacing-dec" +msgstr "Hizkien arteko espazioa txikitu" + +msgid "shortcuts.letter-spacing-inc" +msgstr "Hizkien arteko espazioa handitu" + +msgid "shortcuts.line-height-dec" +msgstr "Lerroen arteko tartea txikitu" + +msgid "shortcuts.line-height-inc" +msgstr "Lerroen arteko tartea handitu" + +msgid "shortcuts.line-through" +msgstr "Aktibatu/desaktibatu marratzea" + msgid "shortcuts.make-corner" msgstr "Bihurtu ertz" @@ -2243,6 +2468,9 @@ msgstr "Iruzkinak" msgid "shortcuts.open-dashboard" msgstr "Joan lan-lekura" +msgid "shortcuts.open-inspect" +msgstr "Ikuskagailura joan" + msgid "shortcuts.open-interactions" msgstr "Joan interakzioetara" @@ -2273,6 +2501,12 @@ msgstr "Bilatu lasterbideak" msgid "shortcuts.select-all" msgstr "Aukeratu guztia" +msgid "shortcuts.select-next" +msgstr "Aukeratu hurrengo geruza" + +msgid "shortcuts.select-prev" +msgstr "Aukeratu aurreko geruza" + msgid "shortcuts.separate-nodes" msgstr "Banatu nodoak" @@ -2319,15 +2553,15 @@ msgstr "Erakutsi/ezkutatu foko-modua" msgid "shortcuts.toggle-fullscreen" msgstr "Aktibatu/desaktibatu pantaila osoa" -msgid "shortcuts.toggle-grid" -msgstr "Erakutsi/ezkutatu sarea" - msgid "shortcuts.toggle-history" msgstr "Erakutsi/Ezkutatu historikoa" msgid "shortcuts.toggle-layers" msgstr "Erakutsi/ezkutatu geruzak" +msgid "shortcuts.toggle-layout-flex" +msgstr "Gehitu/kendu flex diseinua" + msgid "shortcuts.toggle-lock" msgstr "Blokeatu/Desblokeatu" @@ -2337,15 +2571,6 @@ msgstr "Blokeatu/Desblokeatu proportzioak" msgid "shortcuts.toggle-rules" msgstr "Erakutsi/ezkutatu erregelak" -msgid "shortcuts.toggle-scale-text" -msgstr "Erakutsi/Ezkutatu testua eskalatzea" - -msgid "shortcuts.toggle-snap-grid" -msgstr "Lerrokatu sarera" - -msgid "shortcuts.toggle-snap-guide" -msgstr "Lerroatu gidetara" - msgid "shortcuts.toggle-textpalette" msgstr "Erakutsi/Ezkutatu testuen paleta" @@ -2355,6 +2580,9 @@ msgstr "Erakutsi/Ezkutatu elementua" msgid "shortcuts.toggle-zoom-style" msgstr "Erakutsi/Ezkutatu zoomaren estiloa" +msgid "shortcuts.underline" +msgstr "Aktibatu/desaktibatu azpimarraketa" + msgid "shortcuts.undo" msgstr "Desegin" @@ -2367,6 +2595,12 @@ msgstr "Desegin maskara" msgid "shortcuts.v-distribute" msgstr "Banatu bertikalki" +msgid "shortcuts.zoom-lense-decrease" +msgstr "Zooma txikitu" + +msgid "shortcuts.zoom-lense-increase" +msgstr "Zooma handitu" + msgid "shortcuts.zoom-selected" msgstr "Zooma aukeraketara" @@ -2426,6 +2660,9 @@ msgstr "Kideak - %s - Penpot" msgid "title.team-settings" msgstr "Ezarpenak - %s - Penpot" +msgid "title.team-webhooks" +msgstr "Webhookak - %s - Penpot" + #: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs msgid "title.viewer" msgstr "%s - Ikusteko modua - Penpot" @@ -2461,6 +2698,9 @@ msgstr "Ez erakutsi interakzioak" msgid "viewer.header.fullscreen" msgstr "Pantaila osoa" +msgid "viewer.header.inspect-section" +msgstr "Ikuskagailua (%s)" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.interactions" msgstr "Interakzioak" @@ -2484,6 +2724,9 @@ msgstr "Erakutsi interakzioak klik egitean" msgid "viewer.header.sitemap" msgstr "Webgunearen mapa" +msgid "webhooks.last-delivery.success" +msgstr "Azken bidalketa ondo joan da." + #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.hcenter" msgstr "Lerrokatu erdian (%s)" @@ -2524,11 +2767,13 @@ msgstr "Baliabideak" msgid "workspace.assets.box-filter-all" msgstr "Guztiak" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.colors" msgstr "Koloreak" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.components" msgstr "Osagaiak" @@ -2542,19 +2787,27 @@ msgstr "" "Zure elementuak berrizendatu egingo dira: \"taldearen izena / elementuaren " "izena\"" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "Ezabatu" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.duplicate" msgstr "Bikoiztu" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.duplicate-main" +msgstr "Bikoiztu nagusia" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" msgstr "Editatu" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.graphics" msgstr "Grafikoak" @@ -2577,7 +2830,9 @@ msgstr "liburutegi lokala" msgid "workspace.assets.not-found" msgstr "Ez da baliabiderik aurkitu" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename" msgstr "Izena aldatu" @@ -2595,11 +2850,8 @@ msgid_plural "workspace.assets.selected-count" msgstr[0] "%s elementu aukeratuta" msgstr[1] "Ez da elementurik aukeratu" +#: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "PARTEKATUTA" - -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "Letra-tipoak" @@ -2627,10 +2879,15 @@ msgstr "Hizkien tartea" msgid "workspace.assets.typography.line-height" msgstr "Lerroaren altuera" -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/inspect/attributes/text.cljs, src/app/main/ui/inspect/attributes/text.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs msgid "workspace.assets.typography.sample" msgstr "Ag" +msgid "workspace.assets.typography.text-styles" +msgstr "Testuen estiloak" + #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.text-transform" msgstr "Testua eraldatu" @@ -2651,11 +2908,13 @@ msgstr "Fokua gehitu" msgid "workspace.focus.selection" msgstr "Aukeraketa" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "Gradiente lineala" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.radial" msgstr "Gradiente erradiala" @@ -2663,14 +2922,13 @@ msgstr "Gradiente erradiala" msgid "workspace.header.menu.disable-dynamic-alignment" msgstr "Desaktibatu lerrokatze dinamikoa" +msgid "workspace.header.menu.disable-scale-content" +msgstr "Desaktibatu eskala proportzionala" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-scale-text" msgstr "Desaktibatu testu eskala" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Desaktibatu sarera atxikitzea" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "Desaktibatu gidetara atxikitzea" @@ -2682,14 +2940,13 @@ msgstr "Desaktibatu pixelera atxikitzea" msgid "workspace.header.menu.enable-dynamic-alignment" msgstr "Aktibatu lerrokatze dinamikoa" +msgid "workspace.header.menu.enable-scale-content" +msgstr "Aktibatu eskala proportzionala" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-scale-text" msgstr "Aktibatu testua eskalatzea" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Atxikitu sarera" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "Atxikitu gidetara" @@ -2701,10 +2958,6 @@ msgstr "Aktibatu pixelera atxikitzea" msgid "workspace.header.menu.hide-artboard-names" msgstr "Ezkutatu arbelen izenak" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Ezkutatu saretak" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Ezkutatu kolore-paleta" @@ -2740,6 +2993,9 @@ msgstr "Hobespenak" msgid "workspace.header.menu.option.view" msgstr "Ikusi" +msgid "workspace.header.menu.redo" +msgstr "Berregin" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.select-all" msgstr "Guztiak aukeratu" @@ -2748,17 +3004,10 @@ msgstr "Guztiak aukeratu" msgid "workspace.header.menu.show-artboard-names" msgstr "Erakutsi arbelen izenak" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Erakutsi sareta" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Erakutsi kolore-paleta" -msgid "workspace.header.menu.show-pixel-grid" -msgstr "Erakutsi pixelen sarea" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-rules" msgstr "Erakutsi erregelak" @@ -2767,6 +3016,9 @@ msgstr "Erakutsi erregelak" msgid "workspace.header.menu.show-textpalette" msgstr "Erakutsi letra-tipoen paleta" +msgid "workspace.header.menu.undo" +msgstr "Desegin" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "Berrezarri" @@ -2819,7 +3071,8 @@ msgstr "Gehitu" msgid "workspace.libraries.colors" msgstr "%s kolore" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" msgstr "Fitxategien liburutegia" @@ -2827,7 +3080,8 @@ msgstr "Fitxategien liburutegia" msgid "workspace.libraries.colors.hsv" msgstr "HSV" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "Azken koloreak" @@ -2978,15 +3232,18 @@ msgstr "Goian eta behean" msgid "workspace.options.design" msgstr "Diseinua" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export" msgstr "Esportatu" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-multiple" msgstr "Esportatu aukeraketa" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-object" msgid_plural "workspace.options.export-object" msgstr[0] "Esportatu elementu 1" @@ -2996,19 +3253,23 @@ msgstr[1] "Esportatu %s elementu" msgid "workspace.options.export.suffix" msgstr "Aurrizkia" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-complete" msgstr "Esportazioa osatu da" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object" msgstr "Esportazen…" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-error" msgstr "Esportazioak huts egin du" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-slow" msgstr "Esportazio oso geldoa" @@ -3126,6 +3387,9 @@ msgstr "Taldea trazatu" msgid "workspace.options.height" msgstr "Altuera" +msgid "workspace.options.inspect" +msgstr "Ikuskatu" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-action" msgstr "Ekintza" @@ -3154,6 +3418,9 @@ msgstr "Sartu" msgid "workspace.options.interaction-animation-slide" msgstr "Irristatu" +msgid "workspace.options.interaction-auto" +msgstr "automatikoa" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-background" msgstr "Gehitu atzeko planoko geruzi" @@ -3302,6 +3569,10 @@ msgstr "Mantendu scrollaren posizioa" msgid "workspace.options.interaction-prev-screen" msgstr "Aurreko pantaila" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-relative-to" +msgstr "Honekiko erlatiboa" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-self" msgstr "norbera" @@ -3529,7 +3800,8 @@ msgstr "Liburutegiko kolore gehiago" msgid "workspace.options.opacity" msgstr "Opakotasuna" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Posizioa" @@ -3541,12 +3813,12 @@ msgid "workspace.options.radius" msgstr "Erradioa" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Ertz guztiak" +msgid "workspace.options.radius-bottom-left" +msgstr "Behean ezkerrean" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Ertz independenteak" +msgid "workspace.options.radius-bottom-right" +msgstr "Behean eskuman" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3557,17 +3829,18 @@ msgid "workspace.options.radius-top-right" msgstr "Goian eskuman" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Behean ezkerrean" +msgid "workspace.options.radius.all-corners" +msgstr "Ertz guztiak" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Behean eskuman" +msgid "workspace.options.radius.single-corners" +msgstr "Ertz independenteak" msgid "workspace.options.recent-fonts" msgstr "Azkenak" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" msgstr "Berriz saiatu" @@ -3642,7 +3915,8 @@ msgstr "Erakutsi esportazioan" msgid "workspace.options.show-in-viewer" msgstr "Erakutsi ikusteko moduan" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.size" msgstr "Tamaina" @@ -3724,26 +3998,10 @@ msgstr "Solidoa" msgid "workspace.options.text-options.align-bottom" msgstr "Lerrokatu behean" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Lerrokatu erdian (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Justifikatu (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Lerrokatu ezkerrean (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Lerrokatu erdian" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Lerrokatu eskuman (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Lerrokatu goian" @@ -3780,7 +4038,8 @@ msgstr "Lerroaren altuera" msgid "workspace.options.text-options.lowercase" msgstr "Letra xeheak" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.none" msgstr "Bat ere ez" @@ -3788,6 +4047,22 @@ msgstr "Bat ere ez" msgid "workspace.options.text-options.strikethrough" msgstr "Gaineko marra (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Lerrokatu erdian (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Justifikatu (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Lerrokatu ezkerrean (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Lerrokatu eskuman (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Kontsultaren testua" @@ -3855,6 +4130,10 @@ msgstr "Banatu nodoak (%s)" msgid "workspace.path.actions.snap-nodes" msgstr "Atxikitu nodoak (%s)" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.add-flex" +msgstr "Gehitu flex diseinua" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "Atzera bota" @@ -3887,11 +4166,15 @@ msgstr "Ezabatu" msgid "workspace.shape.menu.delete-flow-start" msgstr "Ezabatu fluxuaren hasiera" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Askatu instantzia" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instances-in-bulk" msgstr "Askatu instantziak" @@ -3932,7 +4215,8 @@ msgstr "Ekarri aurrera" msgid "workspace.shape.menu.front" msgstr "Ekarri aurrera" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.go-main" msgstr "Joan osagai nagusiaren fitxategira" @@ -3954,18 +4238,26 @@ msgstr "Ebakidura" msgid "workspace.shape.menu.lock" msgstr "Blokeatu" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.mask" msgstr "Maskara aplikatu" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.paste" msgstr "Itsatsi" msgid "workspace.shape.menu.path" msgstr "Bidea" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.remove-flex" +msgstr "Ezabatu flex diseinua" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "Berrezarri gainidazketak" @@ -3980,11 +4272,13 @@ msgstr "Aukeratu geruza" msgid "workspace.shape.menu.show" msgstr "Erakutsi" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-in-assets" msgstr "Ikusi baliabideen panelean" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-main" msgstr "Erakutsi osagai nagusia" @@ -4012,11 +4306,15 @@ msgstr "Desblokeatu" msgid "workspace.shape.menu.unmask" msgstr "Desegin maskara" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-components-in-bulk" msgstr "Eguneratu osagai nagusiak" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-main" msgstr "Eguneratu osagai nagusia" @@ -4058,7 +4356,8 @@ msgstr "Formak" msgid "workspace.sidebar.layers.texts" msgstr "Testuak" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/inspect/attributes/svg.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, +#: src/app/main/ui/inspect/attributes/svg.cljs msgid "workspace.sidebar.options.svg-attrs.title" msgstr "Inportatutako SVG atributuak" @@ -4251,357 +4550,3 @@ msgstr "Eguneratu" msgid "workspace.viewport.click-to-close-path" msgstr "Egin klik bidea ixteko" - -msgid "errors.webhooks.invalid-uri" -msgstr "URLak ez du balidazioa gainditu." - -msgid "dashboard.webhooks.content-type" -msgstr "Elementu mota" - -#, markdown -msgid "dashboard.fonts.warning-text" -msgstr "" -"Zure letra-tipoek sistema eragile desberdinetan metrika bertikalekin arazoak " -"izan ditzaketela detektatu dugu. Zure letra-tipoa nola ikusten den zerbitzu " -"[hau](https://vertical-metrics.netlify.app) erabiliz egiaztatu dezakezu. " -"Gainera, weberako letra-tipoak sortzeko [Transfonter](https://transfonter." -"org/) erabiltzea gomendatzen dugu. " - -msgid "dashboard.webhooks.active" -msgstr "Aktibo" - -msgid "dashboard.webhooks.description" -msgstr "" -"Webhookak beste webgune batzuei Penpoten zerbait gertatu dela jakinarazteko " -"modu bat dira. Adierazitako URLtara POST eskaera bat bidaliko dugu." - -#: src/app/main/errors.cljs -msgid "errors.feature-not-supported" -msgstr "Ezaugarria ezin da erabili: '%s'." - -msgid "errors.webhooks.unexpected-status" -msgstr "Espero ez zen egoera %s" - -#: src/app/main/errors.cljs -msgid "errors.max-quote-reached" -msgstr "" -"Kuotaren maximora heldu zara: '%s'. Jarri kontaktuan laguntza zerbitzuarekin." - -msgid "errors.webhooks.ssl-validation" -msgstr "Errorea gertatu da SSL balidazioan." - -msgid "errors.webhooks.unexpected" -msgstr "Errore ezezaguna balidazioan" - -#: src/app/main/ui/inspect/attributes/layout.cljs -msgid "inspect.attributes.size" -msgstr "Tamaina eta posizioa" - -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-weight" -msgstr "Letra tipoaren lodiera" - -msgid "inspect.empty.help" -msgstr "" -"Diseinua ikuskatzeari buruz gehiago jakin nahi baduzu zoaz Penpoten laguntza " -"zentrora" - -msgid "inspect.empty.more-info" -msgstr "Informazio gehiago ikuskatzeari buruz" - -msgid "inspect.empty.select" -msgstr "" -"Aukeratu forma bat, taula bat edo talde bat beren propietateak eta kodea " -"ikuskatzeko" - -msgid "labels.view-only" -msgstr "IKUSTEKO BAKARRIK" - -msgid "modals.create-webhook.url.label" -msgstr "Informazioaren URLa" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "" -"Ezabatzen baduzu, bere elementuak ezingo dira beste fitxategietan erabili. " -"Jada erabiltzen ari zaren elementuak fitxategi honetan geldituko dira (ez da " -"diseinurik apurtuko!)." -msgstr[1] "" -"Ezabatzen badituzu, bere elementuak ezingo dira beste fitxategietan erabili. " -"Jada erabiltzen ari zaren elementuak fitxategi honetan geldituko dira (ez da " -"diseinurik apurtuko!)." - -msgid "labels.active" -msgstr "Aktibo" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "" -"Ezabatzen baduzu, bere elementuak ezingo dira beste fitxategietan erabili. " -"Jada erabiltzen ari zaren elementuak fitxategi honetan geldituko dira (ez da " -"diseinurik apurtuko!)." -msgstr[1] "" -"Ezabatzen badituzu, bere elementuak ezingo dira beste fitxategietan erabili. " -"Jada erabiltzen ari zaren elementuak fitxategi honetan geldituko dira (ez da " -"diseinurik apurtuko!)." - -#: src/app/main/ui/dashboard/team.cljs -msgid "labels.copy-invitation-link" -msgstr "Kopiatu esteka" - -#: src/app/main/ui/workspace.cljs -msgid "labels.reload-file" -msgstr "Birkargatu fitxategia" - -msgid "labels.inactive" -msgstr "Inaktibo" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "" -"Argitaratzea atzera botatzen baduzu, elementu horiek ezingo dira beste " -"fitxategietan erabili. Jada erabiltzen diren elementuak fitxategi horietan " -"geldituko dira (diseinurik ez da apurtuko!)." -msgstr[1] "" -"Argitaratzea atzera botatzen baduzu, elementu horiek ezingo dira beste " -"fitxategietan erabili. Jada erabiltzen diren elementuak fitxategi horietan " -"geldituko dira (diseinurik ez da apurtuko!)." - -msgid "shortcuts.align-center" -msgstr "Erdian lerrokatu" - -msgid "shortcuts.toggle-layout-flex" -msgstr "Gehitu/kendu flex diseinua" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "Liburutegi honetako elementuak hemen ari dira erabiltzen:" -msgstr[1] "Liburutegi hauetako elementuak hemen ari dira erabiltzen:" - -msgid "shortcut-subsection.text-editor" -msgstr "Testuak" - -msgid "shortcuts.italic" -msgstr "Aktibatu/desaktibatu etzana" - -msgid "shortcuts.letter-spacing-dec" -msgstr "Hizkien arteko espazioa txikitu" - -msgid "shortcuts.letter-spacing-inc" -msgstr "Hizkien arteko espazioa handitu" - -msgid "workspace.header.menu.redo" -msgstr "Berregin" - -msgid "shortcuts.select-next" -msgstr "Aukeratu hurrengo geruza" - -msgid "workspace.assets.duplicate-main" -msgstr "Bikoiztu nagusia" - -msgid "shortcuts.zoom-lense-increase" -msgstr "Zooma handitu" - -msgid "workspace.header.menu.disable-scale-content" -msgstr "Desaktibatu eskala proportzionala" - -msgid "workspace.header.menu.undo" -msgstr "Desegin" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-relative-to" -msgstr "Honekiko erlatiboa" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "" -"Berriz saiatzeko, fitxategi hau berriz kargatu dezakezu. Hala ere arazoa " -"izaten jarraitzen baduzu, begiratu zerrenda eta ezabatu apurtutako grafikoak." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "" -"Liburutegiko grafikoak osagaiak izango dira orain, horrek ahaltsuago egingo " -"ditu." - -#: src/app/main/ui/workspace/context_menu.cljs -msgid "workspace.shape.menu.remove-flex" -msgstr "Ezabatu flex diseinua" - -msgid "labels.webhooks" -msgstr "Webhookak" - -msgid "title.team-webhooks" -msgstr "Webhookak - %s - Penpot" - -msgid "webhooks.last-delivery.success" -msgstr "Azken bidalketa ondo joan da." - -msgid "dashboard.webhooks.active.explain" -msgstr "Webhook hau aktibatzen denean gertaeraren xehetasunak bidaliko dira" - -msgid "dashboard.webhooks.create" -msgstr "Sortu webhooka" - -msgid "dashboard.webhooks.create.success" -msgstr "Webhooka ondo sortu da." - -msgid "dashboard.webhooks.empty.add-one" -msgstr "Sakatu \"Sortu webhooka\" botoia bat gehitzeko." - -msgid "dashboard.webhooks.empty.no-webhooks" -msgstr "Ez dago webhookik." - -msgid "dashboard.webhooks.update.success" -msgstr "Webhooka ondo aldatu da." - -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs -msgid "errors.email-invalid" -msgstr "Mesedez, idatzi eposta helbide zuzen bat" - -#: src/app/main/errors.cljs -msgid "errors.feature-mismatch" -msgstr "" -"Badirudi '%s' ezaugarria aktibo duen fitxategi bat irekitzen ari zarela " -"baina zure penpot frontendak ezin du hori egin edo ezaugarri hori " -"desaktibatuta du." - -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "labels.unpublish-multi-files" -msgstr "%s fitxategi argitaratzeari utzi" - -msgid "modals.create-webhook.submit-label" -msgstr "Sortu webhooka" - -msgid "modals.create-webhook.title" -msgstr "Sortu webhooka" - -msgid "modals.create-webhook.url.placeholder" -msgstr "https://example.com/postreceive" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" -msgstr[0] "" -"Fitxategi honetako liburutegiko elementuak ez dira inon erabiltzen. " -"Fitxategiarekin batera ezabatuko dira." -msgstr[1] "" -"Fitxategi hautetako liburutegiko elementuak ez dira inon erabiltzen. " -"Fitxategiarekin batera ezabatuko dira." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "Fitxategi honen liburutegiko elementu batzuk hemen erabiltzen ari zara:" -msgstr[1] "" -"Fitxategi hauen liburutegietako elementu batzuk hemen erabiltzen ari zara:" - -msgid "modals.delete-webhook.accept" -msgstr "Ezabatu webhooka" - -msgid "modals.delete-webhook.message" -msgstr "Benetan webhook hau ezabatu egin nahi duzu?" - -msgid "modals.delete-webhook.title" -msgstr "Webhooka ezabatzen" - -msgid "modals.edit-webhook.submit-label" -msgstr "Aldatu webhooka" - -msgid "modals.edit-webhook.title" -msgstr "Aldatu webhooka" - -msgid "modals.invite-member.repeated-invitation" -msgstr "" -"Eposta helbide batzuk jada taldekideenak dira. Ez da gonbidapenik bidaliko." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "Liburutegi honetako elementuak ez dira erabiltzen ari." -msgstr[1] "Liburutegi hauetako elementuak ez dira erabiltzen ari." - -#: src/app/main/ui/dashboard/team.cljs -msgid "notifications.invitation-link-copied" -msgstr "Gonbidapenaren esteka kopiatu da" - -msgid "shortcuts.align-justify" -msgstr "Justifikatuta lerrokatu" - -msgid "shortcuts.bold" -msgstr "Aktibatu/desaktibatu beltza" - -msgid "shortcuts.font-size-dec" -msgstr "Letra tipoaren tamaina txikitu" - -msgid "shortcuts.font-size-inc" -msgstr "Letra tipoaren tamaina handitu" - -msgid "shortcuts.line-height-dec" -msgstr "Lerroen arteko tartea txikitu" - -msgid "shortcuts.line-height-inc" -msgstr "Lerroen arteko tartea handitu" - -msgid "shortcuts.line-through" -msgstr "Aktibatu/desaktibatu marratzea" - -msgid "shortcuts.open-inspect" -msgstr "Ikuskagailura joan" - -msgid "shortcuts.select-prev" -msgstr "Aukeratu aurreko geruza" - -msgid "shortcuts.underline" -msgstr "Aktibatu/desaktibatu azpimarraketa" - -msgid "shortcuts.zoom-lense-decrease" -msgstr "Zooma txikitu" - -msgid "viewer.header.inspect-section" -msgstr "Ikuskagailua (%s)" - -msgid "workspace.assets.typography.text-styles" -msgstr "Testuen estiloak" - -msgid "workspace.header.menu.enable-scale-content" -msgstr "Aktibatu eskala proportzionala" - -msgid "workspace.options.inspect" -msgstr "Ikuskatu" - -msgid "workspace.options.interaction-auto" -msgstr "automatikoa" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "Grafiko batzuk ezin izan dira eguneratu." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "Bihurtzen %s/%s" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "Eguneraketa hau behin bakarrik gertatuko da." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "Eguneratzen %s..." - -#: src/app/main/ui/workspace/context_menu.cljs -msgid "workspace.shape.menu.add-flex" -msgstr "Gehitu flex diseinua" - -msgid "errors.webhooks.last-delivery" -msgstr "Errore bat gertatu da azken bidalketan." - -msgid "errors.webhooks.timeout" -msgstr "Denbora muga gainditu da" - -msgid "errors.webhooks.connection" -msgstr "Konexio errorea, URLa ezin da ireki" diff --git a/frontend/translations/fa.po b/frontend/translations/fa.po index 47aee2b56e..f010b9a74e 100644 --- a/frontend/translations/fa.po +++ b/frontend/translations/fa.po @@ -39,7 +39,8 @@ msgstr "" "این یک سرویس آزمایشی است، برای کار واقعی استفاده نکنید، پروژه‌ها به صورت " "دوره‌ای پاک می‌شوند." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" msgstr "ایمیل" @@ -260,7 +261,8 @@ msgstr "در پنپات قدم بزنید و با ویژگی‌های اصلی msgid "dasboard.walkthrough-hero.start" msgstr "شروع تور" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "افزودن به‌عنوان کتابخانه مشترک" @@ -292,7 +294,8 @@ msgstr "دانلود فایل پنپات (.penpot)" msgid "dashboard.download-standard-file" msgstr "دانلود فایل استاندارد (.svg + .json)" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate" msgstr "تکثیر" @@ -301,7 +304,6 @@ msgid "dashboard.duplicate-multi" msgstr "فایل‌های %s را کپی کنید" #: src/app/main/ui/dashboard/grid.cljs -#, markdown msgid "dashboard.empty-placeholder-drafts" msgstr "" "وای نه! شما هنوز هیچ فایلی ندارید! اگر می‌خواهید چند الگو را امتحان کنید، " @@ -403,7 +405,6 @@ msgid_plural "dashboard.fonts.fonts-added" msgstr[0] "۱ فونت اضافه شد" msgstr[1] "%s فونت اضافه شد" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "هر وب فونتی که در اینجا آپلود کنید به لیست خانواده فونت‌های موجود در " @@ -412,7 +413,6 @@ msgstr "" "فرمت‌های زیر بارگذاری کنید: **TTF، OTF و WOFF** (فقط یکی مورد نیاز خواهد " "بود)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "شما فقط باید فونت‌هایی را که مالک آنها هستید یا مجوز استفاده از آنها را در " @@ -465,7 +465,8 @@ msgstr "در حال آپلود فایل: %s" msgid "dashboard.invite-profile" msgstr "دعوت به تیم" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" msgstr "خروج از تیم" @@ -491,7 +492,8 @@ msgstr "در حال بارگذاری فایل‌های شما …" msgid "dashboard.loading-fonts" msgstr "در حال بارگیری فونت‌های شما …" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "انتقال به" @@ -503,7 +505,8 @@ msgstr "انتقال فایل‌های %s به" msgid "dashboard.move-to-other-team" msgstr "انتقال به تیم دیگر" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" msgstr "+ فایل جدید" @@ -567,7 +570,8 @@ msgstr "پروژه‌ها" msgid "dashboard.remove-account" msgstr "آیا می‌خواهید حساب خود را حذف کنید؟" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs #, fuzzy msgid "dashboard.remove-shared" msgstr "حذف به عنوان کتابخانه مشترک" @@ -615,7 +619,8 @@ msgstr "فایل شما با موفقیت duplicate شد" msgid "dashboard.success-duplicate-project" msgstr "پروژه شما با موفقیت duplicate شد" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-file" msgstr "فایل شما با موفقیت منتقل شد" @@ -652,11 +657,13 @@ msgstr "نتایج جستجو" msgid "dashboard.type-something" msgstr "برای نمایش نتایج جستجو تایپ کنید" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.unpublish-shared" msgstr "لغو انتشار کتابخانه" -#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "به‌روزرسانی تنظیمات" @@ -672,7 +679,11 @@ msgstr "ایمیل" msgid "dashboard.your-name" msgstr "نام شما" -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" msgstr "پن‌پات شما" @@ -721,7 +732,8 @@ msgstr "فونت‌های %s بارگیری نشدند" msgid "errors.clipboard-not-implemented" msgstr "مرورگر شما نمی‌تواند این عملیات را انجام دهد" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/change_email.cljs #, fuzzy msgid "errors.email-already-exists" msgstr "ایمیل قبلا استفاده شده است" @@ -737,7 +749,8 @@ msgstr "شما نمی‌توانید از ایمیل خود به عنوان رم msgid "errors.email-invalid-confirmation" msgstr "ایمیل تأیید باید مطابقت داشته باشد" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs #, fuzzy msgid "errors.generic" msgstr "اشتباهی رخ داده است." @@ -765,7 +778,7 @@ msgstr "تصویر برای درج خیلی بزرگ است." msgid "errors.media-type-mismatch" msgstr "به نظر می‌رسد که محتوای تصویر با پسوند فایل مطابقت ندارد." -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" msgstr "به نظر می‌رسد که این یک تصویر معتبر نیست." @@ -795,7 +808,9 @@ msgstr "عضوی که می‌خواهید اختصاص دهید وجود ندا msgid "errors.team-leave.owner-cant-leave" msgstr "مالک نمی‌تواند تیم را ترک کند، شما باید نقش مالک را مجدداً اختصاص دهید." -#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/data/media.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "errors.unexpected-error" msgstr "یک خطای غیرمنتظره رخ داد." @@ -910,7 +925,8 @@ msgstr "ارتفاع" msgid "inspect.attributes.layout.left" msgstr "چپ" -#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs +#: src/app/main/ui/inspect/attributes/layout.cljs, +#: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.layout.radius" msgstr "گردی" @@ -930,24 +946,17 @@ msgstr "عرض" msgid "inspect.attributes.shadow" msgstr "سایه" -#: src/app/main/ui/inspect/attributes/shadow.cljs -msgid "inspect.attributes.shadow.shorthand.spread" -msgstr "S" - #: src/app/main/ui/inspect/attributes/stroke.cljs #, fuzzy msgid "inspect.attributes.stroke" msgstr "استروک" -#, permanent msgid "inspect.attributes.stroke.alignment.center" msgstr "مرکز" -#, permanent msgid "inspect.attributes.stroke.alignment.inner" msgstr "داخل" -#, permanent msgid "inspect.attributes.stroke.alignment.outer" msgstr "بیرون" @@ -1067,7 +1076,7 @@ msgstr "تایید" msgid "labels.add-custom-font" msgstr "اضافه کردن فونت سفارشی" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "مدیر" @@ -1120,7 +1129,8 @@ msgstr "شما می‌توانید با یک حساب Penpot ادامه دهید msgid "labels.create" msgstr "ایجاد" -#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team" msgstr "ایجاد تیم جدید" @@ -1135,7 +1145,8 @@ msgstr "فونت‌های سفارشی" msgid "labels.dashboard" msgstr "داشبورد" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "حذف" @@ -1156,7 +1167,10 @@ msgstr "حذف دعوت" msgid "labels.delete-multi-files" msgstr "حذف فایل‌های %s" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/files.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.drafts" msgstr "پیش‌نویس‌ها" @@ -1167,7 +1181,7 @@ msgstr "ویرایش" msgid "labels.edit-file" msgstr "ویرایش فایل" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "ویرایشگر" @@ -1203,7 +1217,9 @@ msgstr "فونت‌ها" msgid "labels.github-repo" msgstr "مخزن Github" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "بازخورد بده" @@ -1234,7 +1250,8 @@ msgstr "" msgid "labels.internal-error.main-message" msgstr "خطای داخلی" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.invitations" msgstr "دعوت‌نامه‌ها" @@ -1253,11 +1270,11 @@ msgstr "ورود یا ثبت نام" msgid "labels.logout" msgstr "خروج" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.member" msgstr "عضو" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "اعضا" @@ -1265,7 +1282,8 @@ msgstr "اعضا" msgid "labels.new-password" msgstr "رمزعبور جدید" -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +#: src/app/main/ui/workspace/comments.cljs, +#: src/app/main/ui/dashboard/comments.cljs msgid "labels.no-comments-available" msgstr "هیچ اعلان نظر معلقی ندارید" @@ -1274,7 +1292,6 @@ msgid "labels.no-invitations" msgstr "هیچ دعوتنامه‌ای وجود ندارد." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "دکمه \"دعوت به تیم\" را فشار دهید تا اعضای بیشتری را به این تیم دعوت کنید." @@ -1319,7 +1336,8 @@ msgstr "یا" msgid "labels.owner" msgstr "مالک" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.password" msgstr "کلمه‌عبور" @@ -1339,7 +1357,8 @@ msgstr "پروژه‌ها" msgid "labels.release-notes" msgstr "یادداشت‌های انتشار" -#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/workspace/libraries.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" msgstr "حذف" @@ -1347,7 +1366,9 @@ msgstr "حذف" msgid "labels.remove-member" msgstr "حذف عضو" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.rename" msgstr "تغییرنام" @@ -1359,7 +1380,7 @@ msgstr "تغییر نام تیم" msgid "labels.resend-invitation" msgstr "فرستادن مجدد دعوتنامه" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "امتحان دوباره" @@ -1389,7 +1410,8 @@ msgstr "ما در حال تعمیر و نگهداری برنامه‌ریزی ش msgid "labels.service-unavailable.main-message" msgstr "سرویس در دسترس نیست" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.settings" msgstr "تنظیمات" @@ -1446,7 +1468,7 @@ msgstr "بیننده" msgid "labels.write-new-comment" msgstr "نظر جدید بنویس" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.you" msgstr "(شما)" @@ -1454,22 +1476,25 @@ msgstr "(شما)" msgid "labels.your-account" msgstr "حساب شما" -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "در حال بارگیری تصویر…" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs #, fuzzy msgid "modals.add-shared-confirm.accept" msgstr "افزودن به عنوان کتابخانه مشترک" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.hint" msgstr "" "پس از افزودن به‌عنوان کتابخانه مشترک، دارایی‌های این کتابخانۀ فایل برای " "استفاده در بین بقیه فایل‌های شما در دسترس خواهد بود." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.message" msgstr "افزودن «%s» به عنوان کتابخانه مشترک" @@ -1585,25 +1610,22 @@ msgstr "آیا مطمئن هستید که می‌خواهید این پروژه msgid "modals.delete-project-confirm.title" msgstr "حذف پروژه" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.accept" msgid_plural "modals.delete-shared-confirm.accept" msgstr[0] "حذف فایل" msgstr[1] "حذف فایل‌ها" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "آیا مطمئن هستید که می‌خواهید این فایل را حذف کنید؟" msgstr[1] "آیا مطمئن هستید که می‌خواهید این فایل‌ها را حذف کنید؟" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "این فایل دارای کتابخانه‌هایی است که در این فایل استفاده می‌شوند:" -msgstr[1] "این فایل دارای کتابخانه‌هایی است که در این فایل‌ها استفاده می‌شوند:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" msgstr[0] "درحال حذف فایل" @@ -1712,22 +1734,26 @@ msgstr "" msgid "modals.promote-owner-confirm.title" msgstr "مالک جدید تیم" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" msgstr "حذف به عنوان کتابخانه مشترک" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs #, fuzzy msgid "modals.remove-shared-confirm.hint" msgstr "" "پس از حذف به‌عنوان کتابخانه مشترک، کتابخانه فایل این فایل برای استفاده در " "بین بقیه فایل‌های شما در دسترس نخواهد بود." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.accept" msgstr "به‌روزرسانی" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.cancel" msgstr "لغو" @@ -1753,7 +1779,12 @@ msgstr "پس از نامگذاری تیم خود، می‌توانید افرا msgid "onboarding.welcome.alt" msgstr "Penpot" -#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "مخلوط" @@ -1845,10 +1876,6 @@ msgstr "انتقال" msgid "shortcuts.paste" msgstr "چسباندن" -#, fuzzy -msgid "shortcuts.toggle-grid" -msgstr "نمایش/پنهان کردن شبکه‌بندی" - msgid "shortcuts.toggle-rules" msgstr "نمایش/پنهان کردن خط‌کش‌ها" @@ -1969,10 +1996,6 @@ msgstr "تعاملات (%s)" msgid "viewer.header.share.copy-link" msgstr "کپی کردن لینک" -#: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.subtitle" -msgstr "هر کسی که لینک را داشته باشد دسترسی خواهد داشت" - #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.show-interactions" msgstr "نشان دادن تعاملات" @@ -2021,11 +2044,13 @@ msgstr "دارایی‌ها" msgid "workspace.assets.box-filter-all" msgstr "تمام دارایی‌ها" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.colors" msgstr "رنگ‌ها" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.components" msgstr "کامپوننت‌ها" @@ -2037,19 +2062,24 @@ msgstr "ایجاد یک گروه" msgid "workspace.assets.create-group-hint" msgstr "آیتم‌های شما به طور خودکار به عنوان \"نام گروه / نام آیتم\" نامگذاری می‌شوند" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "حذف" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.duplicate" msgstr "تکثیر" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" msgstr "ویرایش" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.graphics" msgstr "گرافیک" @@ -2061,11 +2091,14 @@ msgstr "گروه" msgid "workspace.assets.libraries" msgstr "کتابخانه‌ها" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename" msgstr "تغییرنام" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "تایپوگرافی‌ها" @@ -2081,7 +2114,9 @@ msgstr "اندازه" msgid "workspace.assets.typography.font-variant-id" msgstr "گونه" -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/handoff/attributes/text.cljs, src/app/main/ui/handoff/attributes/text.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/handoff/attributes/text.cljs, +#: src/app/main/ui/handoff/attributes/text.cljs msgid "workspace.assets.typography.sample" msgstr "مثال" @@ -2230,7 +2265,8 @@ msgstr "بالا" msgid "workspace.options.design" msgstr "طراحی" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export" msgstr "اکسپورت" @@ -2238,7 +2274,8 @@ msgstr "اکسپورت" msgid "workspace.options.export.suffix" msgstr "پسوند" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object" msgstr "درحال گرفتن خروجی…" @@ -2455,7 +2492,8 @@ msgstr "صفحه نمایش" msgid "workspace.options.layer-options.title" msgstr "لایه" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "موقعیت" @@ -2470,7 +2508,8 @@ msgstr "گردی" msgid "workspace.options.recent-fonts" msgstr "اخیر" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" msgstr "تلاش دوباره" @@ -2499,7 +2538,8 @@ msgstr "Y" msgid "workspace.options.shadow-options.title" msgstr "سایه" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.size" msgstr "اندازه" @@ -2548,23 +2588,10 @@ msgstr "خارج" msgid "workspace.options.text-options.align-bottom" msgstr "تراز پایین" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -#, fuzzy -msgid "workspace.options.text-options.text-align-center" -msgstr "تراز در مرکز (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "تراز چپ (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "تراز وسط" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "تراز راست (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "تراز بالا" @@ -2601,10 +2628,24 @@ msgstr "ارتفاع خط" msgid "workspace.options.text-options.lowercase" msgstr "حروف کوچک" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.none" msgstr "هیچ‌یک" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +#, fuzzy +msgid "workspace.options.text-options.text-align-center" +msgstr "تراز در مرکز (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "تراز چپ (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "تراز راست (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "متن" @@ -2711,7 +2752,8 @@ msgstr "جلو بیاورید" msgid "workspace.shape.menu.front" msgstr "به جلو بیاورید" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.go-main" msgstr "به فایل کامپوننت اصلی بروید" @@ -2734,11 +2776,13 @@ msgstr "تقاطع" msgid "workspace.shape.menu.lock" msgstr "قفل" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.mask" msgstr "ماسک" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.paste" msgstr "چسباندن" @@ -2757,12 +2801,14 @@ msgstr "انتخاب لایه" msgid "workspace.shape.menu.show" msgstr "نمایش" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs #, fuzzy msgid "workspace.shape.menu.show-in-assets" msgstr "نمایش در پنل دارایی" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-main" msgstr "نمایش کامپوننت اصلی" @@ -2792,11 +2838,15 @@ msgstr "بازکردن قفل" msgid "workspace.shape.menu.unmask" msgstr "حذف ماسک" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-components-in-bulk" msgstr "به‌روزرسانی کامپوننت‌های اصلی" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-main" msgstr "به‌روزرسانی کامپوننت اصلی" @@ -2835,7 +2885,8 @@ msgstr "شکل‌ها" msgid "workspace.sidebar.layers.texts" msgstr "متن‌ها" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, +#: src/app/main/ui/handoff/attributes/svg.cljs #, fuzzy msgid "workspace.sidebar.options.svg-attrs.title" msgstr "ویژگی‌های SVG ایمپورت شد" @@ -3026,3 +3077,25 @@ msgstr "به‌روزرسانی" msgid "workspace.viewport.click-to-close-path" msgstr "برای بستن مسیر کلیک کنید" + +#~ msgid "dashboard.newsletter-title" +#~ msgstr "اشتراک خبرنامه" + +#, fuzzy +#~ msgid "handoff.attributes.typography.text-transform.titlecase" +#~ msgstr "مورد عنوان" + +#~ msgid "inspect.attributes.shadow.shorthand.offset-x" +#~ msgstr "X" + +#~ msgid "labels.images" +#~ msgstr "تصاویر" + +#~ msgid "labels.skip" +#~ msgstr "رد" + +#~ msgid "viewer.header.share.placeholder" +#~ msgstr "لینک اشتراک‌گذاری در اینجا ظاهر می‌شود" + +#~ msgid "workspace.options.blur-options.background-blur" +#~ msgstr "پس‌زمینه" diff --git a/frontend/translations/fin_FI.po b/frontend/translations/fin_FI.po index 7c0b65df45..2c733dca98 100644 --- a/frontend/translations/fin_FI.po +++ b/frontend/translations/fin_FI.po @@ -257,4 +257,4 @@ msgstr "Käytännön opastus" #: src/app/main/ui/dashboard/projects.cljs #, fuzzy msgid "dasboard.walkthrough-hero.info" -msgstr "Ota opastuskierros Penpotin erilaisista toiminnoista" \ No newline at end of file +msgstr "Ota opastuskierros Penpotin erilaisista toiminnoista" diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 45f079574e..87d06e5739 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -1,15 +1,15 @@ msgid "" msgstr "" -"PO-Revision-Date: 2023-03-31 13:37+0000\n" -"Last-Translator: Aimee \n" -"Language-Team: French " -"\n" +"PO-Revision-Date: 2023-11-16 18:03+0000\n" +"Last-Translator: Swapnil C \n" +"Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" -"X-Generator: Weblate 4.17-dev\n" +"X-Generator: Weblate 5.2\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -54,7 +54,7 @@ msgstr "Nom complet" #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" -msgstr "Se connecter ici" +msgstr "Connectez-vous ici" #: src/app/main/ui/auth/login.cljs msgid "auth.login-submit" @@ -84,6 +84,14 @@ msgstr "LDAP" msgid "auth.login-with-oidc-submit" msgstr "OpenID" +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "Le nom doit contenir au moins un caractère autre que l'espace." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "Le nom ne doit pas contenir plus de 250 caractères." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.new-password" msgstr "Saisissez un nouveau mot de passe" @@ -116,6 +124,10 @@ msgstr "Mot de passe" msgid "auth.password-length-hint" msgstr "Au moins 8 caractères" +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "Le mot de passe doit contenir au moins un caractère autre que l'espace." + msgid "auth.privacy-policy" msgstr "Politique de confidentialité" @@ -264,6 +276,81 @@ msgstr "Commencer le guide" msgid "dasboard.walkthrough-hero.title" msgstr "Démonstration de l'interface" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "Jeton copié" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "Générer un nouveau jeton" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "Jeton d'accès créé avec succès." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "Pressez le bouton \"Générer un nouveau jeton\" pour en générer un." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "Vous n'avez pas encore de jeton." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "Le nom est requis" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 jours" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 jours" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 jours" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 jours" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "Jamais" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "A expiré le %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "Expire le %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "Aucune date d'expiration" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Jetons d'accès personnels" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Les jetons d'accès personnels fonctionnent comme une alternative à notre " +"système d'authentification par login/mot de passe et peuvent être utilisés " +"pour permettre à une application d'accéder à l'API interne de Penpot" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "Le jeton expirera le %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "Le jeton n'a pas de date d'expiration" + #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" @@ -271,7 +358,7 @@ msgstr "Ajouter une Bibliothèque Partagée" #: src/app/main/ui/settings/profile.cljs msgid "dashboard.change-email" -msgstr "Changer adresse e‑mail" +msgstr "Changer l'adresse e‑mail" #: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs msgid "dashboard.copy-suffix" @@ -279,7 +366,7 @@ msgstr "(copie)" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.create-new-team" -msgstr "Créer nouvelle équipe" +msgstr "+ Créer une nouvelle équipe" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.default-team-name" @@ -336,8 +423,8 @@ msgstr "Exporter" #: src/app/main/ui/export.cljs msgid "dashboard.export-shapes.how-to" msgstr "" -"Vous pouvez ajouter des paramètres d'exportation aux éléments depuis les " -"propriétés de design (en bas de la barre latérale de droite)." +"Vous pouvez ajouter des paramètres d'exportation aux formes depuis les " +"propriétés de design (en bas de la barre latérale de droite)" #: src/app/main/ui/export.cljs msgid "dashboard.export-shapes.how-to-link" @@ -345,7 +432,7 @@ msgstr "Information sur comment configurer l'export dans Penpot." #: src/app/main/ui/export.cljs msgid "dashboard.export-shapes.no-elements" -msgstr "Il n'y a pas d'éléments avec des paramètres d'exportation." +msgstr "Aucun élément avec des paramètres d'exportation." #: src/app/main/ui/export.cljs msgid "dashboard.export-shapes.title" @@ -400,7 +487,7 @@ msgstr "Police supprimée" #: src/app/main/ui/dashboard/fonts.cljs msgid "dashboard.fonts.dismiss-all" -msgstr "Rejeter tout" +msgstr "Tout ignorer" msgid "dashboard.fonts.empty-placeholder" msgstr "Les polices personnalisées installées apparaîtront ici." @@ -433,6 +520,17 @@ msgstr "" msgid "dashboard.fonts.upload-all" msgstr "Uploader tout" +#, markdown +msgid "dashboard.fonts.warning-text" +msgstr "" +"Nous avons détecté un problème potentiel dans vos polices de caractères lié " +"aux métriques verticales pour différents systèmes d'exploitation. Pour le " +"vérifier, vous pouvez utiliser des services de métriques verticales de " +"police de caractères comme " +"[celui-ci](https://vertical-metrics.netlify.app/). De plus, nous vous " +"recommandons d'utiliser [Transfonter](https://transfonter.org/) pour " +"générer des polices web et corriger les erreurs. " + msgid "dashboard.import" msgstr "Importer fichiers" @@ -448,7 +546,7 @@ msgid "dashboard.import.import-message" msgstr "%s fichiers ont été importés avec succès." msgid "dashboard.import.import-warning" -msgstr "Certains fichiers contenaient des objets invalides qui ont été supprimés." +msgstr "Certains fichiers contenaient des objets invalides qui ont été enlevés." msgid "dashboard.import.progress.process-colors" msgstr "Traitement des couleurs" @@ -608,10 +706,22 @@ msgstr "Sélectionnez un thème" msgid "dashboard.show-all-files" msgstr "Voir tous les fichiers" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "Votre fichier a été supprimé avec succès" +msgstr[1] "Vos fichiers ont été supprimés avec succès" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-delete-project" msgstr "Votre projet a été supprimé avec succès" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "Votre fichier a été dupliqué avec succès" +msgstr[1] "Vos fichiers ont été dupliqués avec succès" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-duplicate-project" msgstr "Votre projet a été dupliqué avec succès" @@ -663,6 +773,12 @@ msgstr "Retirer la Bibliothèque" msgid "dashboard.update-settings" msgstr "Mettre à jour les paramètres" +msgid "dashboard.webhooks.active" +msgstr "Actif" + +msgid "dashboard.webhooks.active.explain" +msgstr "Quand ce webhook sera activé, les détails de l'évènement seront envoyés" + msgid "dashboard.webhooks.content-type" msgstr "Type de contenu" @@ -672,6 +788,13 @@ msgstr "Créer un webhook" msgid "dashboard.webhooks.create.success" msgstr "Webhook créé avec succès." +msgid "dashboard.webhooks.description" +msgstr "" +"Les webhooks sont une manière simple de permettre à d'autres site web et " +"aux applications d'être notifiés quand certains évènements se produisent " +"dans Penpot. Nous enverrons une requête POST à toutes les URLs que vous " +"avez indiquées." + msgid "dashboard.webhooks.empty.add-one" msgstr "Appuyez sur le bouton « Ajouter un webhook » pour en ajouter un." @@ -738,6 +861,9 @@ msgstr "La police %s n'a pas pu être chargée" msgid "errors.bad-font-plural" msgstr "Les polices %s n'ont pas pu être chargées" +msgid "errors.cannot-upload" +msgstr "Impossible de télécharger le fichier média." + #: src/app/main/data/workspace.cljs msgid "errors.clipboard-not-implemented" msgstr "Votre navigateur ne peut pas effectuer cette opération" @@ -772,6 +898,12 @@ msgstr "L’adresse e‑mail de confirmation doit correspondre" msgid "errors.email-spam-or-permanent-bounces" msgstr "L'e-mail \"%s\" a été signalé comme spam ou a été rejeté." +#: src/app/main/errors.cljs +msgid "errors.feature-mismatch" +msgstr "" +"Il semble que vous ouvrez un fichier qui a la fonctionnalité '%s' activée, " +"mais votre interface Penpot ne la prend pas en charge ou l'a désactivée." + #: src/app/main/errors.cljs msgid "errors.feature-not-supported" msgstr "La fonctionnalité '%s' n'est pas prise en charge." @@ -796,6 +928,12 @@ msgstr "Cette invitation est peut-être été annulée ou a expiré." msgid "errors.ldap-disabled" msgstr "Authentification LDAP désactivée." +#: src/app/main/errors.cljs +msgid "errors.max-quote-reached" +msgstr "" +"Vous avez atteint le quota maximum de '%s'. Veuillez contacter le support " +"technique." + #: src/app/main/data/workspace/persistence.cljs msgid "errors.media-too-large" msgstr "L’image est trop grande." @@ -856,7 +994,7 @@ msgstr "" #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, #: src/app/main/ui/inspect/exports.cljs msgid "errors.unexpected-error" -msgstr "Une erreur inattendue s’est produite" +msgstr "Une erreur inattendue s’est produite." #: src/app/main/ui/auth/verify_token.cljs msgid "errors.unexpected-token" @@ -868,6 +1006,9 @@ msgstr "Erreur de connexion, URL inaccessible" msgid "errors.webhooks.invalid-uri" msgstr "L'URL ne passe pas la validation." +msgid "errors.webhooks.last-delivery" +msgstr "Il y a eu une erreur dans le dernier envoi." + msgid "errors.webhooks.ssl-validation" msgstr "Erreur lors de la validation SSL." @@ -877,6 +1018,9 @@ msgstr "Délai d'attente dépassé" msgid "errors.webhooks.unexpected" msgstr "Erreur inattendue lors de la validation" +msgid "errors.webhooks.unexpected-status" +msgstr "Statut inattendu %s" + #: src/app/main/ui/auth/login.cljs msgid "errors.wrong-credentials" msgstr "E-mail ou mot de passe incorrect." @@ -920,7 +1064,7 @@ msgstr "Email" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Accéder à Twitter" +msgstr "Accéder à X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -928,7 +1072,7 @@ msgstr "Nous sommes là pour répondre à vos questions techniques." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Compte d’assistance Twitter" +msgstr "Compte d’assistance X" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -1052,6 +1196,10 @@ msgstr "Taille de police" msgid "inspect.attributes.typography.font-style" msgstr "Style de police" +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-weight" +msgstr "Graisse de la police" + #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.letter-spacing" msgstr "Interlettrage" @@ -1089,6 +1237,17 @@ msgstr "Premières Lettres en Capitales" msgid "inspect.attributes.typography.text-transform.uppercase" msgstr "Capitales" +msgid "inspect.empty.help" +msgstr "Pour en savoir plus sur l'inspection, visitez le centre d'aide de Penpot" + +msgid "inspect.empty.more-info" +msgstr "Plus d'informations sur l'inspection" + +msgid "inspect.empty.select" +msgstr "" +"Sélectionnez une forme, un plan de travail ou un groupe pour inspecter " +"leurs propriétés et le code" + #: src/app/main/ui/inspect/right_sidebar.cljs msgid "inspect.tabs.code" msgstr "Code" @@ -1141,6 +1300,13 @@ msgstr "Raccourcis" msgid "labels.accept" msgstr "Accepter" +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "Jetons d'accès" + +msgid "labels.active" +msgstr "Activé" + msgid "labels.add-custom-font" msgstr "Ajouter police personnalisée" @@ -1196,6 +1362,10 @@ msgstr "Continuer avec" msgid "labels.continue-with-penpot" msgstr "Vous pouvez continuer avec un compte Penpot" +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.copy-invitation-link" +msgstr "Copier le lien" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "labels.create" msgstr "Créer" @@ -1237,6 +1407,9 @@ msgstr "Supprimer l'invitation" msgid "labels.delete-multi-files" msgstr "Supprimer %s fichiers" +msgid "labels.discard" +msgstr "Rejeter" + #: src/app/main/ui/dashboard/projects.cljs, #: src/app/main/ui/dashboard/sidebar.cljs, #: src/app/main/ui/dashboard/files.cljs, @@ -1303,6 +1476,9 @@ msgstr "Centre d'aide" msgid "labels.hide-resolved-comments" msgstr "Masquer les commentaires résolus" +msgid "labels.inactive" +msgstr "Inactif" + msgid "labels.installed-fonts" msgstr "Polices installées" @@ -1427,6 +1603,10 @@ msgstr "Projets" msgid "labels.release-notes" msgstr "Notes de version" +#: src/app/main/ui/workspace.cljs +msgid "labels.reload-file" +msgstr "Actualiser le fichier" + #: src/app/main/ui/workspace/libraries.cljs, #: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" @@ -1512,6 +1692,10 @@ msgstr "Statut" msgid "labels.tutorials" msgstr "Tutoriels" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.unpublish-multi-files" +msgstr "Dépublier %s fichiers" + #: src/app/main/ui/settings/profile.cljs msgid "labels.update" msgstr "Actualiser" @@ -1529,10 +1713,16 @@ msgstr "Télécharger des polices personnalisées" msgid "labels.uploading" msgstr "Téléchargement…" +msgid "labels.view-only" +msgstr "Lecture seule" + #: src/app/main/ui/dashboard/team.cljs msgid "labels.viewer" msgstr "Spectateur" +msgid "labels.webhooks" +msgstr "Webhooks" + #: src/app/main/ui/comments.cljs msgid "labels.write-new-comment" msgstr "Écrire un nouveau commentaire" @@ -1593,6 +1783,54 @@ msgstr "Changer adresse e‑mail" msgid "modals.change-email.title" msgstr "Changez votre adresse e‑mail" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "Copier le jeton" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "Date d'expiration" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "Nom" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "Le nom aide à savoir comment le jeton sera utilisé" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "Créer un jeton" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "Générer un jeton d'accès" + +msgid "modals.create-webhook.submit-label" +msgstr "Créer un webhook" + +msgid "modals.create-webhook.title" +msgstr "Créer un webhook" + +msgid "modals.create-webhook.url.label" +msgstr "URL de charge utile" + +msgid "modals.create-webhook.url.placeholder" +msgstr "https://example.com/postreceive" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "Supprimer le jeton" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "Confirmez-vous que vous souhaitez supprimer ce jeton ?" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "Supprimer le jeton" + #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.cancel" msgstr "Annuler et conserver mon compte" @@ -1625,6 +1863,12 @@ msgstr "" msgid "modals.delete-comment-thread.title" msgstr "Supprimer une conversation" +msgid "modals.delete-component-annotation.message" +msgstr "Confirmez-vous vouloir supprimer cette note ?" + +msgid "modals.delete-component-annotation.title" +msgstr "Supprimer la note" + #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-confirm.accept" msgstr "Supprimer le fichier" @@ -1692,16 +1936,22 @@ msgstr[0] "Supprimer le fichier" msgstr[1] "Supprimer les fichiers" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.message" -msgid_plural "modals.delete-shared-confirm.message" -msgstr[0] "Êtes-vous sûr de vouloir supprimer ce fichier ?" -msgstr[1] "Êtes-vous sûr de vouloir supprimer ces fichiers ?" +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "Activé dans aucun fichier." +msgstr[1] "Activés dans aucun fichier." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Certaines ressources de la bibliothèque de ce fichier sont utilisées ici :" -msgstr[1] "Certaines ressources des bibliothèques de ce fichier sont utilisées ici :" +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "Cette bibliothèque est active ici : " +msgstr[1] "Ces bibliothèques sont actives ici : " + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.message" +msgid_plural "modals.delete-shared-confirm.message" +msgstr[0] "Vous confirmez vouloir supprimer ce fichier ?" +msgstr[1] "Vous confirmez vouloir supprimer ces fichiers ?" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" @@ -1735,6 +1985,31 @@ msgstr "Êtes‑vous sûr de vouloir supprimer ce membre de l’équipe ?" msgid "modals.delete-team-member-confirm.title" msgstr "Supprimer un membre d’équipe" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" +msgstr[0] "" +"Les éléments déjà utilisés dans ce fichier resteront en place (aucun design " +"ne sera altéré)." +msgstr[1] "" +"Les éléments déjà utilisés dans ces fichiers resteront en place (aucun " +"design ne sera altéré)." + +msgid "modals.delete-webhook.accept" +msgstr "Supprimer le webhook" + +msgid "modals.delete-webhook.message" +msgstr "Vous confirmez vouloir supprimer le webhook ?" + +msgid "modals.delete-webhook.title" +msgstr "Suppression du webhook en cours" + +msgid "modals.edit-webhook.submit-label" +msgstr "Modifier le webhook" + +msgid "modals.edit-webhook.title" +msgstr "Modifier le webhook" + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-member-confirm.accept" msgstr "Envoyer l'invitation" @@ -1742,6 +2017,11 @@ msgstr "Envoyer l'invitation" msgid "modals.invite-member.emails" msgstr "Adresse e-mail, séparées par des virgules" +msgid "modals.invite-member.repeated-invitation" +msgstr "" +"Certains emails appartiennent à des membres actuels de l'équipe. Les " +"invitations ne leur seront pas envoyées." + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-team-member.title" msgstr "Inviter des membres dans l'équipe" @@ -1811,6 +2091,15 @@ msgstr "Êtes‑vous sûr de vouloir promouvoir cette personne propriétaire ? msgid "modals.promote-owner-confirm.title" msgstr "Promouvoir propriétaire" +msgid "modals.publish-empty-library.accept" +msgstr "Publier" + +msgid "modals.publish-empty-library.message" +msgstr "Votre bibliothèque est vide. Voulez-vous la publier quand même ?" + +msgid "modals.publish-empty-library.title" +msgstr "Publier la bibliothèque vide" + #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" @@ -1833,14 +2122,10 @@ msgid "modals.small-nudge" msgstr "Petit nudge" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Si vous annulez la publication, ces ressources seront déplacées vers la " -"bibliothèque locale du fichier." -msgstr[1] "" -"Si vous annulez leur publication, ces ressources seront déplacées vers la " -"bibliothèque locale du fichier." +msgid "modals.unpublish-shared-confirm.accept" +msgid_plural "modals.unpublish-shared-confirm.accept" +msgstr[0] "Dépublier" +msgstr[1] "Dépublier" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" @@ -1848,12 +2133,6 @@ msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "Vous êtes sûr de vouloir retirer cette bibliothèque ?" msgstr[1] "Vous êtes sûr de vouloir retirer ces bibliothèques ?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Certaines ressources de cette bibliothèque sont utilisées ici :" -msgstr[1] "Certaines ressources de ces bibliothèques sont utilisées ici :" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" msgid_plural "modals.unpublish-shared-confirm.title" @@ -1894,7 +2173,11 @@ msgstr "Actualiser le composant d’une bibliothèque" #: src/app/main/ui/dashboard/team.cljs msgid "notifications.invitation-email-sent" -msgstr "E‑mail d'invitation envoyé" +msgstr "E‑mail d'invitation envoyé avec succès" + +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-link-copied" +msgstr "Lien d'invitation copié" #: src/app/main/ui/settings/delete_account.cljs msgid "notifications.profile-deletion-not-allowed" @@ -1986,12 +2269,6 @@ msgstr "Guide du contributeur" msgid "onboarding-v2.welcome.title" msgstr "Bienvenu sur Penpot !" -msgid "onboarding.choice.team-up.create-later" -msgstr "Créer une équipe plus tard" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Le nom de votre équipe" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "" "Après avoir nommé votre équipe, vous pourrez inviter des personnes à la " @@ -2008,12 +2285,6 @@ msgstr "" "N'oubliez pas d'inclure tout le monde. Développeurs, concepteurs, " "gestionnaires... la diversité fait la force :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Créer une équipe et inviter plus tard" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Créer une équipe et envoyer des invitations" - msgid "onboarding.choice.team-up.roles" msgstr "Inviter avec le rôle :" @@ -2115,7 +2386,6 @@ msgstr "Navigation" msgid "shortcut-subsection.navigation-workspace" msgstr "Navigation" -#, fuzzy msgid "shortcut-subsection.panels" msgstr "Panneaux" @@ -2125,6 +2395,9 @@ msgstr "Tracés" msgid "shortcut-subsection.shape" msgstr "Formes" +msgid "shortcut-subsection.text-editor" +msgstr "Textes" + msgid "shortcut-subsection.tools" msgstr "Outils" @@ -2143,9 +2416,15 @@ msgstr "Ajouter un nœud" msgid "shortcuts.align-bottom" msgstr "Aligner en bas" +msgid "shortcuts.align-center" +msgstr "Aligner au centre" + msgid "shortcuts.align-hcenter" msgstr "Aligner horizontalement au centre" +msgid "shortcuts.align-justify" +msgstr "Aligner justifié" + msgid "shortcuts.align-left" msgstr "Aligner à gauche" @@ -2161,6 +2440,9 @@ msgstr "Aligner verticalement au centre" msgid "shortcuts.artboard-selection" msgstr "Créer un plan de travail à partir de la sélection" +msgid "shortcuts.bold" +msgstr "Basculer en gras" + msgid "shortcuts.bool-difference" msgstr "Soustraction booléenne" @@ -2251,6 +2533,12 @@ msgstr "Retourner horizontalement" msgid "shortcuts.flip-vertical" msgstr "Retourner verticalement" +msgid "shortcuts.font-size-dec" +msgstr "Diminuer la taille de la police" + +msgid "shortcuts.font-size-inc" +msgstr "Augmenter la taille de la police" + msgid "shortcuts.go-to-drafts" msgstr "Accéder aux brouillons" @@ -2275,12 +2563,29 @@ msgstr "Zoom avant" msgid "shortcuts.insert-image" msgstr "Insérer une image" +msgid "shortcuts.italic" +msgstr "Basculer en italique" + msgid "shortcuts.join-nodes" msgstr "Joindre les nœuds" -#, fuzzy +msgid "shortcuts.letter-spacing-dec" +msgstr "Diminuer l'espacement entre les lettres" + +msgid "shortcuts.letter-spacing-inc" +msgstr "Augmenter l'espacement entre les lettres" + +msgid "shortcuts.line-height-dec" +msgstr "Diminuer la hauteur de ligne" + +msgid "shortcuts.line-height-inc" +msgstr "Augmenter la hauteur de ligne" + +msgid "shortcuts.line-through" +msgstr "Activer/désactiver texte barré" + msgid "shortcuts.make-corner" -msgstr "Créer un angle" +msgstr "Créer un coin" msgid "shortcuts.make-curve" msgstr "Faire une courbe" @@ -2366,6 +2671,9 @@ msgstr "Accéder aux commentaires des spectateurs" msgid "shortcuts.open-dashboard" msgstr "Accéder au tableau de bord" +msgid "shortcuts.open-inspect" +msgstr "Aller à l'inspecteur" + msgid "shortcuts.open-interactions" msgstr "Accéder aux interactions des spectateurs" @@ -2396,6 +2704,15 @@ msgstr "Rechercher des raccourcis" msgid "shortcuts.select-all" msgstr "Tout sélectionner" +msgid "shortcuts.select-next" +msgstr "Sélectionner le calque suivant" + +msgid "shortcuts.select-parent-layer" +msgstr "Sélectionner le calque parent" + +msgid "shortcuts.select-prev" +msgstr "Sélectionner le calque précédent" + msgid "shortcuts.separate-nodes" msgstr "Séparer les nœuds" @@ -2420,6 +2737,18 @@ msgstr "Commencer la mesure" msgid "shortcuts.stop-measure" msgstr "Arrêter la mesure" +msgid "shortcuts.text-align-center" +msgstr "Aligner au centre" + +msgid "shortcuts.text-align-justify" +msgstr "Aligner justifié" + +msgid "shortcuts.text-align-left" +msgstr "Aligner à gauche" + +msgid "shortcuts.text-align-right" +msgstr "Aligner à droite" + msgid "shortcuts.thumbnail-set" msgstr "Définir les vignettes" @@ -2439,8 +2768,8 @@ msgstr "Activer/désactiver la palette de couleurs" msgid "shortcuts.toggle-focus-mode" msgstr "Activer/désactiver le mode focus" -msgid "shortcuts.toggle-grid" -msgstr "Afficher/masquer la grille" +msgid "shortcuts.toggle-fullscreen" +msgstr "Activer/désactiver le plein écran" msgid "shortcuts.toggle-history" msgstr "Activer/désactiver l'historique" @@ -2448,6 +2777,9 @@ msgstr "Activer/désactiver l'historique" msgid "shortcuts.toggle-layers" msgstr "Activer/désactiver les calques" +msgid "shortcuts.toggle-layout-flex" +msgstr "Ajouter/supprimer flex layout" + msgid "shortcuts.toggle-lock" msgstr "Verrou sélectionné" @@ -2457,16 +2789,6 @@ msgstr "Verrouiller les proportions" msgid "shortcuts.toggle-rules" msgstr "Afficher/masquer les règles" -#, fuzzy -msgid "shortcuts.toggle-scale-text" -msgstr "Mettre le texte à l’échelle" - -msgid "shortcuts.toggle-snap-grid" -msgstr "Aligner sur la grille" - -msgid "shortcuts.toggle-snap-guide" -msgstr "Aligner sur les guides" - msgid "shortcuts.toggle-textpalette" msgstr "Afficher/masquer la palette de texte" @@ -2476,6 +2798,9 @@ msgstr "Afficher/masquer l’élément" msgid "shortcuts.toggle-zoom-style" msgstr "Alterner le style de zoom" +msgid "shortcuts.underline" +msgstr "Activer/désactiver le soulignement" + msgid "shortcuts.undo" msgstr "Annuler" @@ -2491,6 +2816,10 @@ msgstr "Distribuer verticalement" msgid "shortcuts.zoom-selected" msgstr "Zoomer sur la sélection" +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "Le nom du webhook ne peut pas contenir plus de 2048 caractères." + #: src/app/main/ui/dashboard/files.cljs msgid "title.dashboard.files" msgstr "%s - Penpot" @@ -2725,10 +3054,6 @@ msgid_plural "workspace.assets.selected-count" msgstr[0] "%s élément sélectionné" msgstr[1] "%s éléments sélectionnés" -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "PARTAGÉ" - #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" @@ -2802,10 +3127,6 @@ msgstr "Désactiver l’alignement dynamique" msgid "workspace.header.menu.disable-scale-text" msgstr "Désactiver la mise à l'échelle du texte" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Désactiver l’alignement sur la grille" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "Désactiver l’alignement sur les guides" @@ -2821,10 +3142,6 @@ msgstr "Activer l’alignement dynamique" msgid "workspace.header.menu.enable-scale-text" msgstr "Activer le redimensionnement du texte" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Aligner sur la grille" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "Aligner sur les guides" @@ -2836,10 +3153,6 @@ msgstr "Activer l’alignement au pixel" msgid "workspace.header.menu.hide-artboard-names" msgstr "Masquer le nom des plans de travail" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Masquer la grille" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Masquer la palette de couleurs" @@ -2883,10 +3196,6 @@ msgstr "Tout sélectionner" msgid "workspace.header.menu.show-artboard-names" msgstr "Afficher le nom des plans de travail" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Montrer la grille" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Montrer la palette de couleurs" @@ -3018,7 +3327,7 @@ msgstr "BIBLIOTHÈQUES PARTAGÉES" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.libraries.text.multiple-typography" -msgstr "Typographies multiples" +msgstr "Plusieurs typographies" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.libraries.text.multiple-typography-tooltip" @@ -3252,6 +3561,10 @@ msgstr "Action" msgid "workspace.options.interaction-animation-none" msgstr "Aucune" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-outside" +msgstr "Fermer en cliquant a l'extérieur" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-duration" msgstr "Durée" @@ -3469,12 +3782,12 @@ msgid "workspace.options.radius" msgstr "Rayon" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Tous les coins" +msgid "workspace.options.radius-bottom-left" +msgstr "En bas à gauche" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Coins individuels" +msgid "workspace.options.radius-bottom-right" +msgstr "En bas à droite" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3485,12 +3798,12 @@ msgid "workspace.options.radius-top-right" msgstr "En haut à droite" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "En bas à gauche" +msgid "workspace.options.radius.all-corners" +msgstr "Tous les coins" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "En bas à droite" +msgid "workspace.options.radius.single-corners" +msgstr "Coins individuels" #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" @@ -3606,26 +3919,10 @@ msgstr "Solide" msgid "workspace.options.text-options.align-bottom" msgstr "Aligner en bas" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Aligner au centre (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Justifier (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Aligner à gauche (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Aligner verticalement au milieu" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Aligner à droite (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Aligner en haut" @@ -3671,6 +3968,22 @@ msgstr "Aucune" msgid "workspace.options.text-options.strikethrough" msgstr "Barré (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Aligner au centre (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Justifier (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Aligner à gauche (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Aligner à droite (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Texte" @@ -4096,3 +4409,628 @@ msgstr "Actualiser" msgid "workspace.viewport.click-to-close-path" msgstr "Cliquez pour fermer le chemin" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "Développeur" + +#: src/app/main/data/common.cljs +msgid "notifications.by-code.upgrade-version" +msgstr "Une nouvelle version est disponible, merci de rafraîchir la page" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "Beaucoup" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.describe-your-experience-working-on" +msgstr "Comment décririez-vous votre expérience de travail sur..." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2-10" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31-50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canevas" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "En découvrir plus à propos de Penpot" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "Quel est l'outil de design avec lequel vous avez plus d'expérience?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11-30" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "Je travaille sur un projet personnel" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "Quelle est la taille de votre équipe ?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "Comment comptez-vous utiliser Penpot ?" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.shared-library" +msgstr "Bibliothèque partagée" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "Autres (préciser)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "Étudiant ou enseignant" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "Essayer Penpot avant de l'utiliser en local" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "Travailler sur des idées de concept" + +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "Il n'y a pas encore de styles de couleur dans votre bibliothèque" + +msgid "workspace.options.component.copy" +msgstr "Copier" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interactions" +msgstr "Interactions" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-center" +msgstr "Centré en bas" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" +msgstr "" +"Vos retours nous aideront à comprendre vos habitudes et préférences afin que " +"nous puissions continuer à améliorer Penpot." + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-push" +msgstr "Pousser" + +msgid "workspace.options.inspect" +msgstr "Inspecter" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "Gestionnaire de produit ou de projet" + +msgid "workspace.options.component.create-annotation" +msgstr "Créer une note" + +msgid "viewer.header.inspect-section" +msgstr "Inspecter (%s)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "Marketing" + +msgid "workspace.options.opacity" +msgstr "Opacité" + +msgid "workspace.options.component.edit-annotation" +msgstr "Éditer une note" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "Obtenir le code du projet de mon équipe " + +msgid "workspace.options.show-in-viewer" +msgstr "Montrer en mode spectateur" + +msgid "workspace.shape.menu.create-multiple-components" +msgstr "Créer plusieurs composants" + +msgid "webhooks.last-delivery.success" +msgstr "Le dernier envoi a réussi." + +msgid "workspace.options.stroke-width" +msgstr "Largeur du tracé" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease-out" +msgstr "Ease out" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-self" +msgstr "soi" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-ms" +msgstr "ms" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "Rectangle" + +msgid "workspace.options.component.main" +msgstr "Principal" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow-starts" +msgstr "Départs des flux" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-position" +msgstr "Position" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "Je suis freelance" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-in" +msgstr "Dans" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-toggle-overlay" +msgstr "Activer/désactiver la superposition" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow" +msgstr "Flèche triangle" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-slide" +msgstr "Glissement" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "Voir tous les changements" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "...image de marque, illustrations, supports marketing, etc." + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "Il n'y a pas encore de styles typographiques dans votre bibliothèque" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-background" +msgstr "Ajouter une superposition d'arrière-plan" + +msgid "workspace.sidebar.layers.search" +msgstr "Rechercher des calques" + +msgid "shortcuts.zoom-lense-increase" +msgstr "Augmenter le zoom" + +msgid "workspace.shape.menu.add-grid" +msgstr "Ajouter grid layout" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-delay" +msgstr "Délai" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square" +msgstr "Carré" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.space-between" +msgstr "espace entre" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "Zoom" + +msgid "workspace.options.component.annotation" +msgstr "Note" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.more-lib-colors" +msgstr "Plus de couleurs de la bibliothèque" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease-in" +msgstr "Ease in" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.some" +msgstr "Un peu" + +msgid "workspace.layout_grid.editor.title" +msgstr "Édition de la grille" + +msgid "workspace.header.menu.undo" +msgstr "Annuler" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "Plus que 50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "Suivant" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-overlay" +msgstr "Ouvrir la superposition" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-dissolve" +msgstr "Dissolution" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" +msgstr "" +"... wireframes, parcours et flux utilisateurs, arborescence de navigation, " +"etc." + +msgid "workspace.header.menu.disable-scale-content" +msgstr "Désactiver l'échelle proportionnelle" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "Commencer" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.text-palette" +msgstr "Polices (%s)" + +msgid "workspace.sidebar.layers.frames" +msgstr "Plans de travail" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.hsv" +msgstr "HSV" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.delete-flow-start" +msgstr "Supprimer le départ du flux" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "Diamant" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-offset-effect" +msgstr "Effet de décalage" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row" +msgstr "Ligne" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-navigate-to" +msgstr "Naviguer vers" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-overlay-dest" +msgstr "Fermer la superposition : %s" + +msgid "workspace.header.menu.redo" +msgstr "Répéter" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column-reverse" +msgstr "Colonne inversée" + +msgid "workspace.assets.duplicate-main" +msgstr "Dupliquer le principal" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.select-option" +msgstr "Sélectionnez une option" + +msgid "workspace.options.recent-fonts" +msgstr "Récentes" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-while-pressing" +msgstr "En appuyant" + +msgid "workspace.sidebar.collapse" +msgstr "Réduire la barre latérale" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "Designer" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flow-start" +msgstr "Départ du flux" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "Aucune" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "C'est parti !" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.remove-flex" +msgstr "Retirer flex layout" + +msgid "workspace.sidebar.layers.components" +msgstr "Composants" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease" +msgstr "Ease" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +msgid "settings.detach" +msgstr "Détacher" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "Sketch" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-relative-to" +msgstr "Relatif à" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.update-components-in-bulk" +msgstr "Mettre à jour les composants" + +msgid "workspace.options.clip-content" +msgstr "Tronquer le contenu" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.select-layer" +msgstr "Sélectionner le calque" + +msgid "workspace.shape.menu.intersection" +msgstr "Intersection" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding" +msgstr "Marge intérieure" + +msgid "title.team-webhooks" +msgstr "Webhooks - %s - Penpot" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "MISES À JOUR DE LA BIBLIOTHÈQUE" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing" +msgstr "Easing" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "Triangle" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row-reverse" +msgstr "Ligne inversée" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +msgid "workspace.options.export-object" +msgid_plural "workspace.options.export-object" +msgstr[0] "Exporter 1 élément" +msgstr[1] "Exporter %s éléments" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.packed" +msgstr "compacté" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-mouse-enter" +msgstr "Entrée de la souris" + +msgid "workspace.assets.open-library" +msgstr "Ouvrir le fichier de la bibliothèque" + +msgid "workspace.header.menu.enable-scale-content" +msgstr "Activer l'échelle proportionnelle" + +msgid "workspace.shape.menu.thumbnail-set" +msgstr "Définir comme miniature" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "Plus d'information" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-while-hovering" +msgstr "En survolant" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-manual" +msgstr "Manuel" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-mouse-leave" +msgstr "Sortie de la souris" + +msgid "workspace.assets.typography.text-styles" +msgstr "Styles de texte" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker" +msgstr "Marqueur cercle" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.gap" +msgstr "Gap" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker" +msgstr "Marqueur carré" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column" +msgstr "Colonne" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation" +msgstr "Animation" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "Tester Penpot pour voir si ça convient à mon équipe " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "Laisser un commentaire sur mon projet d'équipe" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease-in-out" +msgstr "Ease in out" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-linear" +msgstr "Linéaire" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-on-click" +msgstr "Au clic" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-navigate-to-dest" +msgstr "Naviguer vers : %s" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "InVision" + +msgid "workspace.shape.menu.thumbnail-remove" +msgstr "Retirer la miniature" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.space-around" +msgstr "espace autour" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "Précédent" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-overlay" +msgstr "Fermer la superposition" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker" +msgstr "Marqueur diamant" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding-simple" +msgstr "Marge intérieure simple" + +msgid "workspace.shape.menu.create-annotation" +msgstr "Créer une note" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-overlay-dest" +msgstr "Ouvrir la superposition : %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-destination" +msgstr "Destination" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "Commencer à travailler sur mon projet" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "Flèche" + +msgid "workspace.options.search-font" +msgstr "Rechercher une police" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "Quel est votre rôle ?" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show-in-assets" +msgstr "Afficher dans le panneau des ressources" + +msgid "workspace.options.interaction-auto" +msgstr "automatique" + +msgid "modals.add-shared-confirm-empty.hint" +msgstr "" +"Votre bibliothèque est vide. Une fois ajoutées comme Bibliothèque Partagée, " +"les ressources que vous créez seront utilisables dans vos autres fichiers. " +"Voulez-vous vraiment les publier ?" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.rgb-complementary" +msgstr "Complémentaire en RVB" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.round" +msgstr "Rond" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "Fondateur/Direction" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.show-fill-on-export" +msgstr "Afficher à l'export" + +msgid "workspace.shape.menu.restore-main" +msgstr "Rétablir le composant principal" + +msgid "workspace.sidebar.expand" +msgstr "Ouvrir la barre latérale" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-toggle-overlay-dest" +msgstr "Activer/désactiver la superposition : %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "Cercle" + +msgid "workspace.options.stroke-color" +msgstr "Couleur du tracé" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "... design d'interface, éléments visuels, systèmes de conception, etc." + +msgid "shortcuts.zoom-lense-decrease" +msgstr "Diminuer le zoom" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "Profil - Jetons d'accès" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow" +msgstr "Flèche de ligne" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-after-delay" +msgstr "Après un délai" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.add-flex" +msgstr "Ajouter flex layout" diff --git a/frontend/translations/gl.po b/frontend/translations/gl.po index a0da4a3e23..17030b26a4 100644 --- a/frontend/translations/gl.po +++ b/frontend/translations/gl.po @@ -401,7 +401,6 @@ msgid_plural "dashboard.fonts.fonts-added" msgstr[0] "Engadiuse 1 fonte" msgstr[1] "Engadíronse % fontes" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "Calquera fonte que cargues aquí engadirase na listaxe de familias de fontes " @@ -410,7 +409,6 @@ msgstr "" "Podes cargar fontes cos seguintes formatos: **TTF, OFT e WOFF** (só se " "precisa un)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "Só debes cargar fontes da túa propiedade ou das que teñas licenza para usar " @@ -560,7 +558,8 @@ msgstr "Proxectos" msgid "dashboard.remove-account" msgstr "Queres borrar a túa conta?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" msgstr "Eliminar das bibliotecas compartidas" @@ -604,7 +603,8 @@ msgstr "Duplicouse o ficheiro" msgid "dashboard.success-duplicate-project" msgstr "Duplicouse o proxecto" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-file" msgstr "Moveuse o ficheiro" @@ -640,11 +640,13 @@ msgstr "Resultados da procura" msgid "dashboard.type-something" msgstr "Escribe algo para procurar" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.unpublish-shared" msgstr "Cancelar publicación da Biblioteca" -#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Actualizar configuración" @@ -660,7 +662,11 @@ msgstr "Correo electrónico" msgid "dashboard.your-name" msgstr "Nome" -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" msgstr "O teu Penpot" @@ -769,10 +775,6 @@ msgstr "Ancho" msgid "inspect.attributes.shadow" msgstr "Sombra" -#: src/app/main/ui/inspect/attributes/shadow.cljs -msgid "inspect.attributes.shadow.shorthand.spread" -msgstr "S" - #: src/app/main/ui/inspect/attributes/stroke.cljs msgid "inspect.attributes.stroke" msgstr "Bordo" @@ -1234,10 +1236,6 @@ msgstr "Bibliotecas" msgid "workspace.assets.rename" msgstr "Mudar o nome" -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "COMPARTIDA" - #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" @@ -1491,3 +1489,21 @@ msgstr "Nada" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.edit" msgstr "Editar" + +#~ msgid "dashboard.newsletter-title" +#~ msgstr "Subscrición ao boletín" + +#~ msgid "inspect.attributes.shadow.shorthand.offset-x" +#~ msgstr "X" + +#~ msgid "labels.images" +#~ msgstr "Imaxes" + +#~ msgid "labels.next" +#~ msgstr "Seguinte" + +#~ msgid "labels.start" +#~ msgstr "Comezar" + +#~ msgid "workspace.options.blur-options.background-blur" +#~ msgstr "Fondo" diff --git a/frontend/translations/ha.po b/frontend/translations/ha.po new file mode 100644 index 0000000000..27661a3aa1 --- /dev/null +++ b/frontend/translations/ha.po @@ -0,0 +1,4884 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2024-01-02 16:16+0000\n" +"Last-Translator: Alejandro Alonso \n" +"Language-Team: Hausa \n" +"Language: ha\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.4-dev\n" + +msgid "shortcut-subsection.navigation-dashboard" +msgstr "shawagi" + +msgid "shortcuts.insert-image" +msgstr "sa hoto" + +msgid "shortcuts.bold" +msgstr "fito da shi barobaro" + +msgid "shortcuts.open-viewer" +msgstr "tafi sashin da masu kallo suke hulda" + +msgid "onboarding-v2.before-start.title" +msgstr "kafin ka fara" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-out" +msgstr "fita" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.create-group-hint" +msgstr "" +"abubuwanka za a samu su suna nan take kamar \"sunan kungiya/ sunan abubuwan\"" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.use-default" +msgstr "yi amfani da tsoho" + +#, permanent +msgid "inspect.attributes.stroke.alignment.outer" +msgstr "daga waje" + +#: src/app/main/ui/comments.cljs +msgid "labels.edit" +msgstr "gyara" + +#: src/app/main/ui/inspect/attributes/image.cljs +msgid "inspect.attributes.image.height" +msgstr "tsawo" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography" +msgstr "tsara rubutu" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "jaraba kafin ka yi aiki da fenfot" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.title" +msgstr "cire memban tawaga" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.move" +msgstr "motsa (%s)" + +msgid "shortcuts.make-curve" +msgstr "kirkiri lankwasa" + +msgid "shortcuts.snap-nodes" +msgstr "yanke kauri" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "yin aikin kan tunani mai kyau" + +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "Babu salon kaloli a ma'ajiya yanzu" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.title" +msgid_plural "modals.delete-shared-confirm.title" +msgstr[0] "goge kundi" +msgstr[1] "goge kundaye" + +msgid "dashboard.fonts.empty-placeholder" +msgstr "fonts da ka xora nan za ya bayyana." + +# SUBSECTIONS +msgid "shortcut-subsection.alignment" +msgstr "kwaskwarima" + +msgid "errors.webhooks.unexpected" +msgstr "matsalar da ba zata ba lokacin farfaxowa" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "kwanaki 30" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "settings.multiple" +msgstr "gauraya" + +#: src/app/main/ui/dashboard/team.cljs +msgid "errors.member-is-muted" +msgstr "bayanan da ka nema imel din su ya suma (bayanan matsaloli)." + +msgid "workspace.options.component.copy" +msgstr "Kwafa" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.column" +msgstr "shafi" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.add-shared" +msgstr "tura tamkar fenfot" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interactions" +msgstr "yayin hulda" + +#: src/app/main/ui/auth/login.cljs +msgid "errors.auth-provider-not-configured" +msgstr "manhajar tantancewar ba ta tsaru ba." + +msgid "workspace.undo.entry.multiple.circle" +msgstr "da'ira" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "ka tabbata kana son goge wannan alamar?" + +#: src/app/main/ui/static.cljs +msgid "labels.bad-gateway.desc-message" +msgstr "" +"ka jinkirta kaxan sannan ka qara gwadawa; mu na aiki daidai domin tattala " +"aikinmu." + +#: src/app/main/ui/inspect/attributes/fill.cljs +msgid "inspect.attributes.fill" +msgstr "cika" + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "media.loading" +msgstr "xora hoto …" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "lambobi masu xauke da bayani" + +msgid "shortcuts.open-color-picker" +msgstr "abin daukan kala" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-action" +msgstr "aiki" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "lambar tsaron ba ta da lokacin daina aiki" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +msgid "workspace.options.exporting-object" +msgstr "Ana fitarwa" + +#: src/app/main/ui/workspace/header.cljs +msgid "label.shortcuts" +msgstr "yanke" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.colors" +msgstr "%s kala" + +msgid "shortcuts.delete" +msgstr "goge" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-center" +msgstr "kasa tsakiya" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.in-this-file" +msgstr "Ma'adana a wanna fiyal" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-gitlab-submit" +msgstr "wurin nazari" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-info" +msgstr "bayanin tawaga" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.top" +msgstr "sama" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" +msgstr "" +"bayaninka za ya sanya mu fahimci kwarewarka da xabi'unka ta haka ne za mu " +"mayar maka da fenfot kayan aikin da ka ke jin daxin aiki da shi." + +msgid "shortcuts.move-unit-down" +msgstr "Matsa da sashin kasa" + +msgid "common.share-link.placeholder" +msgstr "hanya mai kyau za ta bayyana a nan" + +msgid "dashboard.import.import-message" +msgid_plural "dashboard.import.import-message" +msgstr[0] "an shigar kundi 1." +msgstr[1] "%s kundaye sun shiga." + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hleft" +msgstr "Daidaita hagu (%s)" + +msgid "labels.accept" +msgstr "karva" + +msgid "workspace.shape.menu.transform-to-path" +msgstr "sauya zuwa hanya" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-title" +msgstr "an manta lambar tsaro?" + +msgid "shortcut-subsection.edit" +msgstr "Tace" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.auto" +msgstr "da kanshi" + +msgid "shortcuts.draw-rect" +msgstr "Ractangula" + +msgid "onboarding.choice.team-up.invite-members-info" +msgstr "" +"ka tuna da kowa. masu qirqira, masu tsarawa, shuwagabanniS... daban-daban ya " +"qara :)" + +msgid "modals.invite-member.repeated-invitation" +msgstr "" +"waxansu imel daga membobin qungiyar na yanzu. ba za a aikawa da gayyatarsu " +"ba ." + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-push" +msgstr "Tura" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "notifications.profile-deletion-not-allowed" +msgstr "ba za ka iya goge kundin ba. ka jira umarnin tawaga ka fin ka ci gaba." + +msgid "workspace.options.inspect" +msgstr "Duba" + +msgid "inspect.attributes.typography.text-decoration.strikethrough" +msgstr "zana layi tsakiyar rubutu" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-top-center" +msgstr "sama tsakiya" + +msgid "shortcuts.move-nodes" +msgstr "matsa a hade" + +#: src/app/main/ui/settings.cljs +msgid "dashboard.your-account-title" +msgstr "gidanka" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.search-placeholder" +msgstr "nema …" + +msgid "shortcuts.redo" +msgstr "gyara" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.multiply" +msgstr "sau" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.left" +msgstr "Hagu" + +msgid "dashboard.webhooks.active" +msgstr "ya na amfani" + +msgid "common.share-link.current-tag" +msgstr "(yanzu)" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs +msgid "errors.email-invalid" +msgstr "sanya imel mai amfani" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.stretch" +msgstr "mikewa" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.letter-spacing" +msgstr "tazarar harafi" + +msgid "shortcuts.bring-backward" +msgstr "komawa baya" + +msgid "shortcuts.show-shortcuts" +msgstr "nuna / boye yanken" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.new-project" +msgstr "+ sabon aiki" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.invalid-token-error" +msgstr "lambar tsaron da ka sanya ba daidai ba ce." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "ma su qirqira" + +msgid "labels.show-comments-list" +msgstr "jerin ire-iren yabo" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.dont-show-interactions" +msgstr "Karka/ki nuna hulda" + +msgid "dashboard.export.options.detach.message" +msgstr "" +"manhajar tura kundi ba ta shiga cikin fitarwa, wani amfaniqarawa a taska. " + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-title" +msgstr "na ji daxin sake haxuwa da kai!" + +msgid "shortcut-subsection.shape" +msgstr "Siffa" + +#: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs +msgid "title.viewer" +msgstr "%s - duba kumburi - Mazubin biruka" + +msgid "dashboard.export.options.merge.title" +msgstr "tura taska ya qunshi bayanan da ke cikin kundin taskoki" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-members" +msgstr "Yan kungiya - %s - Mazubin biruka" + +msgid "shortcuts.align-justify" +msgstr "Tabbataccan tsari" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.direction-rtl" +msgstr "RTL" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "shugaban aiki" + +msgid "workspace.options.component.create-annotation" +msgstr "Kirkiri sharhin rubuta" + +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.rotation" +msgstr "jujjuyawa" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-other-team" +msgstr "tura ma sauran tawaga" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.num-of-members" +msgstr "%s memba" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.admin" +msgstr "shugaba" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-auto-width" +msgstr "sarrafaffen fadi" + +msgid "errors.team-leave.member-does-not-exists" +msgstr "mamban da ka ke son sanyawa ba ya a ciki." + +msgid "workspace.options.shadow-options.color" +msgstr "inuwar kala" + +#, markdown +msgid "dashboard.fonts.warning-text" +msgstr "" +"matsalolin lasisi daga sama zuwa qasa magwajin da ke aiki iri-iri. domin " +"bincikawa za ka iya amfani da aikin ma'aunin sama da qasa [shi ne " +"haka](https://ma'aunin sama da qasa.matattarar bayanai.app/). bugu da qari, " +"mun aminta da amfani da [taransifota](https://taransifota.org/) domin samo " +"webfonts da adana kurakurai. " + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "samo sabuwar lambar tsaro" + +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-email-sent" +msgstr "an aika da saqon" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.select-member-to-promote" +msgstr "zavi memba domin havakawa" + +msgid "shortcuts.draw-text" +msgstr "rubutaccan sako" + +msgid "viewer.header.inspect-section" +msgstr "Duba (%s)" + +msgid "workspace.shape.menu.flatten" +msgstr "mikad da abu" + +msgid "shortcuts.delete-node" +msgstr "fita da ga cikin net wok" + +msgid "onboarding-v2.before-start.desc3" +msgstr "za ka iya kallon koyarwarmu da mutanenmu ke yi." + +msgid "workspace.header.menu.hide-pixel-grid" +msgstr "boye pixel akwati" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "auth.notifications.team-invitation-accepted" +msgstr "ka zama dan tawaga" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.feedback-sent" +msgstr "aika bayani" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.title" +msgstr "haxin guiwa!" + +msgid "workspace.undo.entry.multiple.page" +msgstr "shafi" + +msgid "shortcuts.ungroup" +msgstr "Fita daka rukuni" + +#: src/app/main/ui/static.cljs +msgid "labels.bad-gateway.main-message" +msgstr "akwai matsala" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.hint" +msgstr "" +"idan ba ka cire taskar shirye-shirye ba, kundin taskar na wannan kundin zai " +"tsaya kasancewar za ka iya amfani da shi a cikin kundayenka." + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.empty" +msgstr "babu labaran da su ka canja a yanzu" + +msgid "dashboard.libraries-and-templates" +msgstr "taska da shaidar kamfanoni" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin-simple" +msgstr "Samfarin gefe" + +msgid "workspace.undo.entry.multiple.rect" +msgstr "rectangles" + +#: src/app/main/data/common.cljs +msgid "notifications.by-code.upgrade-version" +msgstr "akwai sabon yayi, fatan za a sabunta fage" + +msgid "shortcuts.go-to-drafts" +msgstr "ta fi rumbu" + +msgid "onboarding.welcome.alt" +msgstr "fenfot" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.bottom" +msgstr "Kasa" + +msgid "shortcuts.zoom-selected" +msgstr "Zuko wanda aka zaba" + +msgid "modals.delete-component-annotation.message" +msgstr "ka tabbata kana son goge wannan bayanin?" + +msgid "inspect.attributes.stroke.style.none" +msgstr "babu" + +msgid "errors.auth.unable-to-login" +msgstr "lokacin ya qare ko ba a tantance ka ba." + +msgid "workspace.options.grid.params.color" +msgstr "Kala" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.top" +msgstr "sama" + +msgid "shortcuts.add-node" +msgstr "kara Girma" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.group" +msgstr "rukuni" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.accept" +msgid_plural "modals.delete-shared-confirm.accept" +msgstr[0] "goge kundi" +msgstr[1] "goge kundaye" + +#: src/app/main/ui/inspect/attributes/stroke.cljs +msgid "inspect.attributes.stroke" +msgstr "yankewa" + +msgid "onboarding.team-modal.create-team-feature-5" +msgstr "100% kyauta!" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +msgid "workspace.options.size-presets" +msgstr "yanayin girman yanayin" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.sitemap" +msgstr "taswirar wuri" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +msgid "labels.no-comments-available" +msgstr "an dakatar da kai duka! Alamar sabon sharhi za ta fito nan." + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.feedback-disabled" +msgstr "kasa samun bayani" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.accept" +msgid_plural "modals.unpublish-shared-confirm.accept" +msgstr[0] "maras wallafa" +msgstr[1] "maras wallafa" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "kasuwanci" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.update" +msgstr "Sabuntawa" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin" +msgstr "gefe" + +msgid "shortcuts.align-hcenter" +msgstr "tsarin tsakiya ko ina" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.show-all-comments" +msgstr "fito da yabo" + +#: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.logout" +msgstr "fita" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "kwanaki 180" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.color-burn" +msgstr "kala konanniya" + +msgid "workspace.options.opacity" +msgstr "dishi dishi" + +msgid "shortcuts.prev-frame" +msgstr "allon da ya gabata" + +msgid "workspace.options.component.edit-annotation" +msgstr "Tace sharhin rubutu" + +msgid "dashboard.import.progress.upload-data" +msgstr "xora bayani akan sabis (%s/%s)" + +msgid "shortcuts.draw-curve" +msgstr "Ratse" + +#: src/app/main/ui/workspace/comments.cljs +msgid "labels.all" +msgstr "duk" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.file" +msgstr "fayil" + +#: src/app/main/ui/inspect/attributes/common.cljs +msgid "inspect.attributes.color.hsla" +msgstr "HSLA" + +#: src/app/main/ui/auth/register.cljs +msgid "errors.registration-disabled" +msgstr "rigitar ba ta yi ba." + +msgid "workspace.undo.entry.multiple.media" +msgstr "kadarar zane" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "sami lambar kowane aiki " + +msgid "workspace.options.show-in-viewer" +msgstr "fito da kaurin sosai" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.lowercase" +msgstr "yanayin kasa" + +#: src/app/main/ui/settings/password.cljs +msgid "errors.password-invalid-confirmation" +msgstr "tabbata labar tsaro ta yi daidai" + +msgid "workspace.undo.entry.multiple.group" +msgstr "rukunis" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.updates" +msgstr "sabuntawa" + +msgid "dashboard.export-frames" +msgstr "Allon fitarwa na PDF" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "modals.delete-page.title" +msgstr "goge shafi" + +#: src/app/main/ui/static.cljs +msgid "labels.service-unavailable.main-message" +msgstr "ba sabis" + +msgid "shortcuts.opacity-2" +msgstr "Saita dishi dishi zuwa kashi 20" + +msgid "workspace.shape.menu.create-multiple-components" +msgstr "kirkiri abubuwa da yawa" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.rename-group" +msgstr "sake sunan kungiyar" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-profile" +msgstr "ka na son gwadawa ne kawai?" + +msgid "webhooks.last-delivery.success" +msgstr "Sakon karshe ya isa." + +msgid "shortcuts.bool-intersection" +msgstr "ma'aunin abubuwa daban daban" + +msgid "workspace.options.stroke-width" +msgstr "gigciye fadin" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease-out" +msgstr "sauki waje" + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.desc-message" +msgstr "babu wannan fagen, ko ba ka da izinin shiga." + +msgid "workspace.options.x" +msgstr "X layi" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.members" +msgstr "mambobi" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-verified-successfully" +msgstr "an tantance adireshinka na imel" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.show-interactions" +msgstr "Nuna hulda" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.hint" +msgstr "" +"idan ka canza wurin mallaka, ba za ka iya sauya matsayin shugaba ba, gazawar " +"wasu dokokin wannan tawaagar. " + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-self" +msgstr "kai/ni" + +msgid "errors.webhooks.invalid-uri" +msgstr "URL bai gyaru ba." + +msgid "modals.create-webhook.url.placeholder" +msgstr "https://misali.com/postreceive" + +msgid "shortcuts.draw-nodes" +msgstr "samar da hanya" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.you" +msgstr "(kai)" + +msgid "dashboard.options" +msgstr "zavi" + +msgid "workspace.shape.menu.path" +msgstr "hanya" + +msgid "onboarding.templates.title" +msgstr "fara tsarawa" + +msgid "labels.go-back" +msgstr "koma baya" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.message" +msgid_plural "modals.unpublish-shared-confirm.message" +msgstr[0] "ka tabbata ka na son rufe taskar nan?" +msgstr[1] "ka tabbata ka na son rufe taskokin nan?" + +#: src/app/main/ui/settings/password.cljs +msgid "errors.wrong-old-password" +msgstr "tsohuwar lambar tsaro ba daidai ba ce" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.unpublish-shared" +msgstr "wallafa taska" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "ina aikin kaina" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.offsety" +msgstr "Y" + +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.size" +msgstr "wurin daxa girma" + +#: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs +msgid "viewer.frame-not-found" +msgstr "Ba'a sami allon ba." + +msgid "workspace.focus.focus-mode" +msgstr "tsarin maida hankali" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-submit" +msgstr "gano lambar tsaro" + +msgid "shortcuts.cut" +msgstr "cire" + +#: src/app/main/ui/confirm.cljs +msgid "ds.component-subtitle" +msgstr "zamanantar da sassa:" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete" +msgstr "goge" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs +msgid "workspace.options.component" +msgstr "Bangarori" + +#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs +msgid "dashboard.copy-suffix" +msgstr "(kwafi)" + +msgid "labels.export" +msgstr "fitarwa" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-ms" +msgstr "ms" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.right" +msgstr "dama" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.cancel" +msgstr "soke" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.save-color" +msgstr "Adana salon kala" + +msgid "shortcuts.opacity-8" +msgstr "seta dishi dishin zuwa kashi 8o" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.ellipse" +msgstr "siffar kwai (%s)" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.search" +msgstr "nemo kadarar" + +msgid "shortcuts.or" +msgstr " ko " + +msgid "shortcuts.opacity-5" +msgstr "Saita dishi dishi zuwa kashi 50" + +msgid "onboarding-v2.newsletter.desc" +msgstr "" +"domin jin daxin fenfot , sai ka biya kuxi domin labarai da ci gaban da ake " +"samu." + +#: src/app/main/ui/workspace/nudge.cljs +msgid "modals.nudge-title" +msgstr "adadin jan hankali" + +msgid "workspace.sidebar.layers.groups" +msgstr "rukuni" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "Rectangle" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.new-password" +msgstr "sabuwar lambar tsaro" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "ana buqatar suna" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-top-left" +msgstr "sama hagu" + +msgid "shortcut-section.dashboard" +msgstr "allon kallo" + +msgid "dashboard.webhooks.active.explain" +msgstr "idan an sami sauyi a nan ake kawo shi" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.create-new-team" +msgstr "yi sabuwar tawaga" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.color-palette" +msgstr "farantin kala (%s)" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "za a iya buxe wannan taskirar a nan: " +msgstr[1] "za a iya buxe taskirorin nan a nan: " + +msgid "shortcuts.move-fast-left" +msgstr "Matsa hagu da sauri" + +msgid "workspace.undo.entry.multiple.frame" +msgstr "allo" + +#: src/app/main/ui/settings/profile.cljs +msgid "labels.update" +msgstr "sabunta" + +msgid "labels.num-of-frames" +msgid_plural "labels.num-of-frames" +msgstr[0] "allo 1" +msgstr[1] "alluna %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.layout-item-min-h" +msgstr "tsaho mafi kankanta" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.message" +msgstr "qara “%s” xakin ajiya" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.loading-files" +msgstr "aikin dora kundaye …" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component-in-bulk.hint" +msgstr "" +"ka kusa sabunta taskar ajiyar shir-shirye. zai iya shafar sauran kundayen da " +"ke amfani da ita." + +msgid "workspace.path.actions.add-node" +msgstr "kara kauri (%s)" + +msgid "workspace.options.component.main" +msgstr "Ainahin" + +#: src/app/main/ui/workspace/sidebar/options.cljs +msgid "workspace.options.design" +msgstr "zane" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.projects-title" +msgstr "aiki" + +msgid "workspace.undo.entry.single.frame" +msgstr "allo" + +msgid "shortcuts.flip-vertical" +msgstr "kifa ta tsaye" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow-starts" +msgstr "gudun farko" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-position" +msgstr "Mataki" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-dynamic-alignment" +msgstr "bada damar daidaitawa mai canjawa" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.more-colors" +msgstr "kaloli masu yawa" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-confirm.accept" +msgstr "goge kundi" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "ni mai zaman kansa ne" + +msgid "shortcuts.paste" +msgstr "manna" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-bottom" +msgstr "jerin kasa" + +msgid "labels.fonts" +msgstr "Font" + +msgid "shortcuts.not-found" +msgstr "babu gajeriyar hanya" + +msgid "common.share-link.destroy-link" +msgstr "tarwatsa hanya" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-close-confirm.hint" +msgstr "" +"ka tabbata memban tawaga kaxai, za ya iya goge tawaga tare da aikinta da " +"kundaye." + +#: src/app/main/ui/settings/password.cljs +msgid "labels.old-password" +msgstr "tsohuwar lambar tsaro" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.new" +msgstr "sabo %s" + +msgid "onboarding-v2.welcome.desc3.title" +msgstr "gudunmawar jagora" + +msgid "shortcuts.toggle-assets" +msgstr "Danna kadara" + +msgid "onboarding-v2.newsletter.updates" +msgstr "" +"aiko man da sabbin abubuwan da aka yi (sabbib fasali, fitowa, gyara...)." + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.create-artboard-from-selection" +msgstr "zababban allo" + +msgid "onboarding.newsletter.title" +msgstr "ka buqatar labaran fenfot?" + +msgid "errors.team-leave.insufficient-members" +msgstr "ba sauran masu fita daga tawaga, ba bu tabbacin gogewa." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "kwafar lambar tsaro" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.fix-when-scrolling" +msgstr "gyara nemowa" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.description" +msgstr "bayani" + +msgid "labels.installed-fonts" +msgstr "sanya fenfot" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius-top-right" +msgstr "saman dama" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-in" +msgstr "ciki" + +msgid "modals.publish-empty-library.title" +msgstr "wallafa taska maras komai" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-toggle-overlay" +msgstr "Juya mai murfi" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.info" +msgstr "bincika manhaja domin sanin manyan sassanta." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow" +msgstr "Triangle mashi" + +msgid "modals.leave-and-reassign.forbidden" +msgstr "" +"ba za ku iya bari ba idan ba wani memba da zai ingata wa mai shi. ku na iya " +"goge tawaga." + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-scale-text" +msgstr "kashe sikelin rubutu" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.rows" +msgstr "jere" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.tutorials" +msgstr "koyarwa" + +msgid "workspace.undo.entry.multiple.curve" +msgstr "kwana" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-snap-guides" +msgstr "kashe tsinkewa zuwa mai jagora" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.already-have-account" +msgstr "ka na da taska?" + +msgid "dashboard.import.progress.process-typographies" +msgstr "kula da rubutu" + +msgid "shortcuts.opacity-1" +msgstr "saita dishi dishi zuwa kashi 10" + +msgid "workspace.path.actions.snap-nodes" +msgstr "tsinke kauri (%s)" + +msgid "shortcuts.bring-front" +msgstr "kawo zuwa gaba" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.typography" +msgstr "rubutun rubutu" + +msgid "common.share-link.view-all" +msgstr "zavi duka" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.give-feedback" +msgstr "bada bayani" + +msgid "shortcuts.bool-exclude" +msgstr "kebentaccan ma'auni" + +msgid "workspace.undo.entry.multiple.multiple" +msgstr "abu" + +msgid "dashboard.export-standard-multi" +msgstr "Sauke %s cikakken kundi (.svg + .json)" + +#: src/app/main/data/workspace/persistence.cljs +msgid "errors.media-too-large" +msgstr "hoton da za ka sanya ya yi girma." + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.invitations" +msgstr "gayyata" + +msgid "shortcut-subsection.navigation-viewer" +msgstr "shawagi" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.check-your-email" +msgstr "" +"bincika taskarka ta imel,ka danna alamar mahaxa domin tabbatarwa,sannan ka " +"fara amfani da fenfot." + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-slide" +msgstr "Ja" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.layout-item-min-w" +msgstr "fadi mafi kankanta" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.layout-item-min-h" +msgstr "tsawo mafi kankanta" + +msgid "workspace.sidebar.layers.masks" +msgstr "takunkumi" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.screen" +msgstr "allo" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "Aga duka canjin" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.inner-shadow" +msgstr "Inuwar ciki" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.frame" +msgstr "allo (%s)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "...branding, illustrations, marketing pieces, etc." + +msgid "onboarding.team-modal.create-team-feature-4" +msgstr "Unlimited members" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs +msgid "workspace.options.export.suffix" +msgstr "Kari na bayan baki" + +msgid "errors.webhooks.connection" +msgstr "hadin bai yi ba, ba a iya samun URL" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.confirm-email" +msgstr "tantance sabon imel" + +msgid "auth.terms-of-service" +msgstr "dokokin aiki" + +msgid "dashboard.export.options.all.message" +msgstr "manhajar tura kundi ta kunshi fitarwa, tattali mahaxarsu." + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.group-fill" +msgstr "Cika rukuni" + +msgid "shortcuts.stop-measure" +msgstr "dena aunawa" + +#: src/app/main/ui/comments.cljs +msgid "labels.write-new-comment" +msgstr "rubuta s abon yabo" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discourse-subtitle1" +msgstr "" +"mun yi farin ciki da samunka a nan. idan ka na da buqatar taimako sai ka " +"tuntuvi na gaba da kai matsayi." + +msgid "dashboard.libraries-and-templates.explore" +msgstr "bincika su da kyau kasan ta yadda za ka bayar da gudunmawa" + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "Babu rubutun rubutu a ma'ajiya yanzu" + +#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.paste" +msgstr "manna" + +msgid "labels.edit-file" +msgstr "gyara kundi" + +msgid "onboarding.templates.subtitle" +msgstr "ga wasu hotunan talla nan." + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.selection-fill" +msgstr "cika zabi" + +msgid "inspect.tabs.code.selected.curve" +msgstr "lankwasa" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.unsaved" +msgstr "Canja canjan da ba'a adana ba" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-middle" +msgstr "jerin tsakiya" + +msgid "viewer.breaking-change.description" +msgstr "" +"Wannan mahadar da aka raba yanxu batada ingaci. Ka/ki kirkiri wata ko ka/ki " +"tamayi maishi a baka/ki sabuwa." + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.color-dodge" +msgstr "kala gudajjiya" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.saving" +msgstr "Adanawa" + +msgid "shortcuts.next-frame" +msgstr "wani tsari" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library" +msgstr "ma'adana" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-background" +msgstr "kara mai rufin bayan" + +msgid "workspace.sidebar.layers.search" +msgstr "nemo shimfida" + +msgid "inspect.attributes.stroke.style.solid" +msgstr "tauri" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.demo-warning" +msgstr "" +"wannan aikin gwaji ne kawai,kar ka yi amfani da shi a aikin gaske,lokaci " +"zuwa lokaci za ya ringa vacewa ne." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "kwanaki 90" + +#: src/app/main/ui/dashboard/search.cljs +msgid "title.dashboard.search" +msgstr "nema - %s - Mazubin biruka" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.unknown" +msgstr "yanayi fiye da %s" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.delete-team-confirm.accept" +msgstr "goge tawaga" + +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.height" +msgstr "tsawo" + +msgid "shortcuts.zoom-lense-increase" +msgstr "Zuko karuwar ido" + +msgid "workspace.shape.menu.add-grid" +msgstr "kara akwatin tsari" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.interactions" +msgstr "Hulda" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/inspect/attributes/text.cljs, src/app/main/ui/inspect/attributes/text.cljs +msgid "workspace.assets.typography.sample" +msgstr "Ag" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "dashboard.fonts.dismiss-all" +msgstr "goge duka" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" +msgstr[0] "" +"duk aikin da aka yi a wannan kundin, nan za a same shi (ba zanen da za a " +"iya tsinkawa)." +msgstr[1] "" +"duk aikin da aka yi a waxancan kundayen, can za a same su (ba zane da za iya " +"tsinkawa)." + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +msgid "workspace.options.exporting-complete" +msgstr "An gama fitarwa" + +msgid "labels.continue-with-penpot" +msgstr "za ka iya ci gaba a idanka na fenfot" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-size" +msgstr "girman salo" + +msgid "shortcuts.hide-ui" +msgstr "fito / boye UI" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-title" +msgstr "qirqiri sabon kundi" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "dashboard.libraries-title" +msgstr "taskoki" + +msgid "inspect.tabs.code.selected.rect" +msgstr "rektangul" + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-frames.title" +msgstr "Fitarwa a PDF" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "labels.create" +msgstr "yi" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-delay" +msgstr "jinkiri" + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-multiple.selected" +msgstr "%s of %s tubullan da aka zava" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.accept" +msgstr "barin tawaga" + +#: src/app/main/ui/components/color_input.cljs +msgid "errors.invalid-color" +msgstr "launin bai yi ba" + +#: src/app/main/ui/workspace/nudge.cljs +msgid "modals.small-nudge" +msgstr "qaramin jan hankali" + +msgid "shortcuts.duplicate" +msgstr "maimaita" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.saved" +msgstr "An adana" + +msgid "shortcuts.create-new-project" +msgstr "samar da sabo abu" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.luminosity" +msgstr "warewa" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.copy-link" +msgstr "Kwafi mahada" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.color" +msgstr "kala" + +msgid "onboarding.team-modal.create-team-feature-1" +msgstr "aiyuka da kundaye da yawa" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs +msgid "shortcuts.title" +msgstr "allon harufa yanke" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flip-vertical" +msgstr "kifa ta kwance" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.position" +msgstr "matsayi" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square" +msgstr "murabba'i" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "tabbatarwa (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.space-between" +msgstr "sarari tsakani" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.open-in-new-tab" +msgstr "buxe kundi a sabon wurin buxewa" + +#: src/app/main/ui/auth.cljs +msgid "auth.sidebar-tagline" +msgstr "samun hanyar magance matsalar zane-zane." + +#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team" +msgstr "yi sabuwar tawaga" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.uppercase" +msgstr "yanayin sama" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.assets" +msgstr "kadara" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "Zuko" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.edit" +msgstr "tace" + +msgid "dashboard.import.import-error" +msgstr "akwi matsala a kundin. ba a shio da kundin ba." + +msgid "onboarding-v2.newsletter.privacy1" +msgstr "mu na kula da sirri, a nan za ka karanta na mu " + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-move-project" +msgstr "an tura aikinka" + +msgid "workspace.options.component.annotation" +msgstr "Yin sharhin rubutu" + +msgid "shortcuts.toggle-layers" +msgstr "Danna shimfida" + +msgid "labels.uploading" +msgstr "ana dorawa…" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-preserve-scroll" +msgstr "adana komawa sama da kasa" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.accept" +msgstr "goge memba" + +msgid "workspace.undo.entry.single.typography" +msgstr "rubutun rubuta kadara" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.selection-color" +msgstr "zababbabbin kaloli" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.more-lib-colors" +msgstr "Ma'ajiyar kaloli masu yawa" + +msgid "inspect.tabs.code.selected.image" +msgstr "hoto" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.create-group" +msgstr "kirkiri rukuni" + +msgid "modals.delete-webhook.accept" +msgstr "goge webhook" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.invite-member-confirm.accept" +msgstr "aika saqon" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.member" +msgstr "mamba" + +msgid "onboarding.newsletter.policy" +msgstr "dokoki." + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +msgid "errors.email-already-exists" +msgstr "an yi amfani da imel" + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "errors.media-type-not-allowed" +msgstr "wannan hoton ba ya aiki." + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.text-transform" +msgstr "masarrafin rubu,manya ko qanana" + +msgid "dashboard.webhooks.create" +msgstr "yin webhook" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.bottom" +msgstr "Kasa" + +msgid "common.share-link.get-link" +msgstr "samun hanya" + +msgid "shortcuts.open-comments" +msgstr "tafi inda 'yan kallo za su bayyana ra'ayi" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.subtitle" +msgstr "" +"bayyana dalilin imel dinka, faiyace idan akwai matsala, an shawarwari ko " +"hasashe. tawaga ko mamba za a kula da kai." + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.title" +msgstr "gogewa %s kundaye" + +msgid "workspace.header.menu.disable-snap-pixel-grid" +msgstr "kashe tsinkewa zuwa pixel" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease-in" +msgstr "sauki ciki" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-family" +msgstr "gidan salo" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.register" +msgstr "ba ka da kunxi har yanzu?" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.password" +msgstr "lambar tsaro" + +#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.remove" +msgstr "cire" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.solid" +msgstr "mai tauri" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +msgid "errors.generic" +msgstr "wata matsala ta faru." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.some" +msgstr "wasu" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.there-are-updates" +msgstr "a kwai na zamani a rababban ma'ajiya" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.leave-team" +msgstr "fita tawaga" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.delete" +msgstr "gogagge %s" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.line-height" +msgstr "tsawon layi" + +#: src/app/main/ui/inspect/right_sidebar.cljs +msgid "inspect.tabs.code" +msgstr "lamba" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.top" +msgstr "sama" + +msgid "shortcuts.bring-back" +msgstr "tura zuwa baya" + +msgid "shortcuts.text-align-justify" +msgstr "jera da inganci" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius.single-corners" +msgstr "kwanar da take cin gashin kanta" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.unexpected-token" +msgstr "tukuicin da ba a san da shi ba" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.dotted" +msgstr "digo digo" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-artboard-names" +msgstr "Boye allom suna" + +msgid "dashboard.export.options.merge.message" +msgstr "" +"za ka iya fitar da kundi tare da haxe muhimman abubuwa, na waje a " +"kunditaskira." + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.backward" +msgstr "tura zuwa baya" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-ldap-submit" +msgstr "LDAP" + +msgid "onboarding-v2.newsletter.privacy2" +msgstr "" +"za mu aika maka da imel mai amfani. za ka iya biya a kowane lokaci za ka iya " +"ta kowace hanyar biyanmu." + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.copy-invitation-link" +msgstr "kwafar hanya" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.rename" +msgstr "sake suna" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.select-a-shape" +msgstr "zabi surar allo, ko rukuni ta hadu da daya allon." + +msgid "onboarding.newsletter.accept" +msgstr "haka, za a biya" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke" +msgstr "gigciye" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.leftright" +msgstr "hagu & dama" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-rules" +msgstr "Nuna ma'auni" + +msgid "shortcuts.toggle-focus-mode" +msgstr "Danna yanayin maida hankali" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +msgid "workspace.options.export" +msgstr "fitarwa" + +msgid "workspace.layout_grid.editor.title" +msgstr "tace akwati" + +msgid "workspace.undo.entry.single.page" +msgstr "shafi" + +msgid "dashboard.export.explain" +msgstr "za ka iya fitar da kundi daya ko fiye ta hanyar tura taska. \"me \"*?" + +msgid "shortcuts.open-interactions" +msgstr "tafi sashin da masu kallo suke hulda" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.message" +msgstr "ka tabbata kana son goge %s kundaye?" + +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "Sunan shafin yanar gizon zai kunshi a mafi yawa haruffa 2048." + +msgid "shortcuts.show-pixel-grid" +msgstr "nuna / boye akwatin pixel" + +msgid "modals.delete-font.title" +msgstr "goge font" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-oidc-submit" +msgstr "shaidar buxewa" + +msgid "inspect.attributes.typography.text-decoration.underline" +msgstr "jan layi" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "gama aiki kan %s" + +#, markdown +msgid "dashboard.fonts.hero-text2" +msgstr "" +"za ka iya xora fonts dinka ne domin ka sami dammar amfani da fenfot. gano " +"akwai qarin abubuwan das u ka dace da dokokin fpntaiki](https://fenfot.app/" +"dokoki.html). Za ka so bayani game da[ffonts](https://www.rubutu.com/faq)." + +msgid "workspace.sidebar.layers.images" +msgstr "hoto" + +msgid "dashboard.webhooks.content-type" +msgstr "irin ra'ayi" + +msgid "workspace.header.menu.show-pixel-grid" +msgstr "Nuna akwatin pixel" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "lokacin daina aiki" + +msgid "shortcuts.h-distribute" +msgstr "raba ta tsaye" + +msgid "workspace.header.menu.undo" +msgstr "Cire" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "fiye da 50" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.show-interactions-on-click" +msgstr "Nuna hulda da an danna" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.file-library" +msgstr "F" + +msgid "dashboard.export-multi" +msgstr "fitar da fenfot %s kundaye" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.email-already-validated" +msgstr "an farfaxo da imel." + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-weight" +msgstr "kaurin salon" + +msgid "shortcuts.artboard-selection" +msgstr "kirkiri allo daga zabi" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.show-all-files" +msgstr "duk kundaye" + +msgid "errors.team-leave.owner-cant-leave" +msgstr "mai abu ba ya barin tawaga, dole adubi matsayin mai abu." + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.left" +msgstr "Hagu" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.select-ui-language" +msgstr "zavavven harshen UI" + +msgid "workspace.undo.entry.single.color" +msgstr "kalar kadara" + +#: src/app/main/data/workspace.cljs +msgid "errors.clipboard-not-implemented" +msgstr "manhajar binciken nan ba ta iya yin wannan aikin" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "dole lambar tsaro ta qunshi wasu alamomi, sannan tazara." + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.line-height" +msgstr "tsahon layi" + +#: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs +msgid "ds.confirm-title" +msgstr "ka tabbata?" + +msgid "labels.search-font" +msgstr "neman font" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.ungroup" +msgstr "raba rukunin" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "na gaba" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-auto-height" +msgstr "sarrafaffan tsaho" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.profile" +msgstr "kundi" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "bayyana yawan tawagarka?" + +msgid "workspace.focus.focus-on" +msgstr "maida hankali" + +msgid "inspect.tabs.code.selected.frame" +msgstr "hukuma" + +msgid "viewer.header.comments-section" +msgstr "Bayyana ra'ayi(%s)" + +msgid "labels.webhooks" +msgstr "Webhooks" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.unlock" +msgstr "bude" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius-bottom-right" +msgstr "kasan dama" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-overlay" +msgstr "bude mai rufi" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-dissolve" +msgstr "narkewa" + +msgid "onboarding-v2.newsletter.news" +msgstr "aiko man da bayanin fenfot (rubutun blog, bidiyon koyarwa, kallo...)." + +msgid "common.unpublish" +msgstr "maras wallafa" + +msgid "workspace.undo.entry.single.rect" +msgstr "rectangle" + +msgid "shortcuts.toggle-visibility" +msgstr "Nuna/boye" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.settings" +msgstr "gyara" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom-full-screen" +msgstr "Cika allon" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.cut" +msgstr "cire" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" +msgstr "... jagora, yawan amfani da shiga, leqe-leqe, dss." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "kwanaki 60" + +msgid "shortcut-section.viewer" +msgstr "Dankallo" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.unpublish-multi-files" +msgstr "kundayen da ba a wallafa ba %s" + +msgid "common.share-link.confirm-deletion-link-description" +msgstr "" +"ka tabbata ka na son rufe wannan hanyar? idan ka rufe ba mai iya sake bi" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.accept" +msgstr "goge aiki" + +#: src/app/main/ui/settings/password.cljs +msgid "generic.error" +msgstr "afkuwar matsala" + +msgid "workspace.header.menu.disable-scale-content" +msgstr "kashe sikelin rabo" + +msgid "dashboard.export.options.detach.title" +msgstr "lura da bayanan da ke cikin manhajar tura kundi" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +msgid "workspace.options.exporting-object-slow" +msgstr "Fitarwa ba tsammani ta sami tsaiko" + +#: src/app/main/ui/workspace/header.cljs +msgid "dashboard.export-shapes" +msgstr "Fitarwa" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.lock" +msgstr "kulle" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.help-center" +msgstr "sashen taimako" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.modify" +msgstr "gyaggyarawa %s" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "da yawa" + +msgid "dashboard.download-binary-file" +msgstr "sauke manhajar fenfot(.manhajar fenfot)" + +msgid "dashboard.fonts.deleted-placeholder" +msgstr "rashin font" + +msgid "shortcuts.opacity-6" +msgstr "Saita dishi dishi zuwa kashi 60" + +#: src/app/main/ui/inspect/attributes/common.cljs +msgid "inspect.attributes.color.hex" +msgstr "HEX" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.expired-invitation" +msgstr "daina aiki" + +msgid "onboarding.team-modal.create-team-desc" +msgstr "" +"tawaga na ba ka damar haduwa da masu amfani da fenfot domin yi aiki daya " +"akan kundaye." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +msgid "errors.email-has-permanent-bounces" +msgstr "imel «%s» na da bayanan matsaloli na dindindin." + +msgid "errors.webhooks.unexpected-status" +msgstr "matsayin da ba zato %s" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.typography" +msgstr "%s Rubutun rubutu" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-email" +msgstr "imel" + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.title" +msgstr "goge tattaunawa" + +#: src/app/main/ui/workspace/nudge.cljs +msgid "modals.big-nudge" +msgstr "jan hankali" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.message" +msgstr "ka tabbata ka na son barin wannan tawagar?" + +msgid "dashboard.webhooks.empty.no-webhooks" +msgstr "ba a gina wani webhooks ba." + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.invite-team-member.title" +msgstr "gayyato membobi zuwa ga tawaga" + +msgid "shortcut-section.workspace" +msgstr "fagen aiki" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.title" +msgstr "barin tawaga" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "fara" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.resend-invitation" +msgstr "sake aika saqon gayyata" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.difference" +msgstr "banbanci" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.hide-resolved-comments" +msgstr "voye saqon da aka buxa" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin-all" +msgstr "gefen duka" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.github-repo" +msgstr "taskar rubuce-rubuce" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.strikethrough" +msgstr "gigciye ta cikinsa (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.darken" +msgstr "kara masa duhu" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.confirm-password" +msgstr "tabbatar da kalmar buxewa" + +msgid "shortcuts.open-workspace" +msgstr "tafi fagen aiki" + +msgid "labels.and" +msgstr "da" + +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.top" +msgstr "sama" + +msgid "shortcut-subsection.main-menu" +msgstr "Babbar kumshiya" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.fullname" +msgstr "cikakken suna" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.reset-overrides" +msgstr "sake saita sokewa" + +msgid "common.share-link.permissions-hint" +msgstr "duk wanda ya mallaki mataki,za ya iya shiga" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.accept" +msgstr "cire taskar shiye-shirye" + +msgid "workspace.undo.entry.multiple.typography" +msgstr "rubutun rubuta kadara" + +msgid "shortcuts.align-center" +msgstr "tsarin tsakiya" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "jerin tsakiya (%s)" + +msgid "dashboard.webhooks.description" +msgstr "" +"Webhooks hanyar sanar da manhajoji da addireshi intanet idan wani ya faru a " +"Penpot. za a tura maka da saqon talla URLs ka samar da." + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.text-palette" +msgstr "rubutub rubutu (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.titlecase" +msgstr "yanayin lakani" + +msgid "workspace.sidebar.layers.frames" +msgstr "allo" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom-fit" +msgstr "dace - ja sikeli ya dace da shi" + +#: src/app/main/ui/dashboard/files.cljs +msgid "title.dashboard.files" +msgstr "%s - Tukunyar aje biro" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +msgid "workspace.options.exporting-object-error" +msgstr "An gaza fitarwa" + +#: src/app/main/ui/alert.cljs +msgid "ds.alert-ok" +msgstr "haka" + +msgid "shortcuts.escape" +msgstr "kubuta" + +msgid "shortcuts.copy" +msgstr "kwafi" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.hsv" +msgstr "HSV" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.delete-flow-start" +msgstr "goge kwararar farko" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-left" +msgstr "kasa hagu" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "mai daraja" + +msgid "dashboard.export.detail" +msgstr "*akwai sassan,hotuna,launuka,da/kozane-zane." + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.hint" +msgstr "" +"idan ka aje a wurin ajiyar tawaga, turken kundin xakin ajiyar za ya kasance " +"za a iya amfani da shi a sauran kundaye." + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-offset-effect" +msgstr "cire tasiri" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to" +msgstr "tura a" + +msgid "modals.create-webhook.title" +msgstr "qirqirar webhook" + +#: src/app/main/ui/inspect/attributes/common.cljs +msgid "inspect.attributes.color.rgba" +msgstr "RGBA" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.mixed" +msgstr "gauraya" + +msgid "shortcuts.detach-component" +msgstr "rarraba abubuwan da su ke a ware" + +msgid "shortcuts.reset-zoom" +msgstr "zake zukowa" + +msgid "onboarding-v2.welcome.desc3" +msgstr "" +"wurin da za ka san yadda za ka hada-hannu da fassara, neman fasali, manyan " +"gudunmawa, magance matsala…" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hright" +msgstr "Daidaita dama (%s)" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.status" +msgstr "daraja" + +msgid "common.share-link.permissions-can-comment" +msgstr "sharhi" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.rgba" +msgstr "RGBA" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row" +msgstr "jerawa" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-navigate-to" +msgstr "kewayawa zuwa" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "ya kake tunanin aiki da fenfot?" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.inner" +msgstr "ciki" + +msgid "onboarding-v2.before-start.desc1" +msgstr "" +"ya kamata kasan akwai kayayyaki da yawa da za su iya taimaka maka ka sami " +"damar fara aiki da fenfot, kamar jagoran mai amfani tasharmu ta youtub." + +msgid "shortcut-subsection.general-viewer" +msgstr "gamayya" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-members" +msgstr "mambobin tawaga" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-file-prefix" +msgstr "sabon kundi" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "lambar tsaron za ta gama aiki %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.underline" +msgstr "ja layi (%s)" + +msgid "onboarding.choice.team-up.create-team-desc" +msgstr "bayan ka yi wa tawagarka suna, za ka iya gaiyato mutane ku hadu." + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-shapes.how-to" +msgstr "" +"za ka iya daidaita kayan zanenka ta hanyar (amfani da madannin qasa sashen " +"dama)." + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.accept" +msgstr "goge fira" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-duplicate-project" +msgstr "an kwafi aikinka" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.cancel" +msgstr "sokewa" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-overlay-dest" +msgstr "kulle mai rufi %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "goge alama" + +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.your-penpot" +msgstr "fenfot na ka" + +msgid "shortcuts.bring-forward" +msgstr "tura gaba" + +msgid "workspace.header.menu.redo" +msgstr "sake" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.detach-instance" +msgstr "raba yanayin abin" + +msgid "shortcuts.opacity-9" +msgstr "Seta dish dishi zuwa 90" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "jera dama (%s)" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-textpalette" +msgstr "Nuna launukan tsarin rubutu" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flip-horizontal" +msgstr "kifa ta tsaye" + +msgid "labels.continue" +msgstr "ci gaba" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.hard-light" +msgstr "Haske mai muya" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.layout-item-max-w" +msgstr "fadi mafi yawa" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.management" +msgstr "shuwagabannin tawaga" + +msgid "inspect.tabs.code.selected.component" +msgstr "bangare" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column-reverse" +msgstr "sake shafi" + +msgid "dashboard.download-standard-file" +msgstr "sauke cikakken kundi(.svg + .json)" + +#: src/app/main/ui/static.cljs +msgid "labels.internal-error.desc-message" +msgstr "" +"an sami matsala. sake gwadawa idan matsalar ba ta kauce ba, tuntubi sashen " +"taimako." + +msgid "labels.font-variants" +msgstr "salo" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.title" +msgstr "kafin ka fita" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.front" +msgstr "kawo zuwa gaba" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-center" +msgstr "tsakiya" + +#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +msgid "errors.unexpected-error" +msgstr "afkuwar kuskuren da ba a zata ba." + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-shared-libraries-available" +msgstr "Babu rababbun ma'adanai wanda aka samu" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.square" +msgstr "murabba'i" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.fullscreen" +msgstr "Cika fuskar" + +msgid "workspace.undo.entry.single.multiple" +msgstr "wani abu" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.role" +msgstr "matsayi" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.add-interaction" +msgstr "Danna wannan madannin + domin saka hulda." + +msgid "workspace.shape.menu.difference" +msgstr "bambanci" + +msgid "workspace.assets.duplicate-main" +msgstr "maimaita ainihin" + +msgid "shortcuts.fit-all" +msgstr "fitar da abubuwan da zai dace da ko wane abu" + +msgid "workspace.undo.entry.multiple.path" +msgstr "hanya" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-right" +msgstr "kasa dama" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.duplicate" +msgstr "maimaita" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show-main" +msgstr "nuna ainihin wurin" + +msgid "onboarding-v2.before-start.desc2.title" +msgstr "xanjagora" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.describe-your-experience-working-on" +msgstr "ta ya za ka bayyana kwarewarka akai..." + +msgid "shortcut-subsection.zoom-viewer" +msgstr "Zukowa" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.remove-shared" +msgstr "cire matattarar kundate" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.select-option" +msgstr "dauki wanda kake so" + +msgid "shortcuts.v-distribute" +msgstr "Rarraba ta tsaye" + +msgid "common.share-link.page-shared" +msgid_plural "common.share-link.page-shared" +msgstr[0] "tura shafi 1" +msgstr[1] "%s an tura shafi" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.grid-title" +msgstr "Akwati" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.offsetx" +msgstr "X" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.twitter-go-to" +msgstr "je ka tiwita" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.none" +msgstr "ba komai" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.exclusion" +msgstr "warewa" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.title" +msgstr "labari" + +msgid "workspace.options.recent-fonts" +msgstr "da dimi dimi" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.new-email" +msgstr "sabon imel" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.search-shared-libraries" +msgstr "Duba rabbaun ma'adanai" + +msgid "shortcuts.text-align-left" +msgstr "jera hagu" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-subtitle" +msgstr "kyauta ne,an buxe hanyar samu" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-textpalette" +msgstr "boye launukan yanayin tsarin rubutu" + +msgid "labels.view-only" +msgstr "gani kaxai" + +msgid "workspace.undo.entry.single.component" +msgstr "bangarori" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.row" +msgstr "layuka" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "dashboard.fonts.upload-all" +msgstr "xora duka" + +msgid "inspect.attributes.typography.text-transform.none" +msgstr "babu" + +msgid "workspace.assets.local-library" +msgstr "dakin karatun gida" + +msgid "shortcuts.move-fast-right" +msgstr "matsa dama da sauri" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.unmask" +msgstr "bude takunkumi" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-submit" +msgstr "shiga ciki" + +msgid "shortcuts.toggle-textpalette" +msgstr "Juya zuwa launukan rubutu" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.update-team" +msgstr "sabunta tawaga" + +#: src/app/main/ui/workspace/sidebar/options/page.cljs +msgid "workspace.options.canvas-background" +msgstr "bayan zane" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-top" +msgstr "jerin sama" + +msgid "shortcuts.font-size-dec" +msgstr "rage girman rubutu" + +msgid "dashboard.webhooks.empty.add-one" +msgstr "danna qasa \"sanya webhook\" qara xaya." + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.default-team-name" +msgstr "manhajar fenfot" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-while-pressing" +msgstr "yayin dannawa" + +msgid "shortcuts.open-dashboard" +msgstr "tafi gaban allon" + +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +msgid "errors.profile-is-muted" +msgstr "bayananka su na da imel maras motsi (baiyana matsaloli)." + +msgid "shortcuts.start-measure" +msgstr "fara aunawa" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.center" +msgstr "Tsakiya" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "yi sabuwar alama" + +msgid "workspace.sidebar.collapse" +msgstr "ruguza a'ajiyar bayani" + +msgid "common.share-link.permissions-can-inspect" +msgstr "iya bincka lamba" + +msgid "workspace.options.height" +msgstr "Tsawo" + +msgid "shortcuts.draw-ellipse" +msgstr "siffar kwai" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "ma su tsarawa" + +#, markdown +msgid "dashboard.fonts.hero-text1" +msgstr "" +"duk wani adireshi da ka xora a nan dangin font ne a wannan tawagar za a sami " +"kundayen da ke xauke da kayan rubutu. Da fontfont iri daya ne ake kasawa**" +"gwaurayen font**. Za ka iya xora font ta waxannan hanyoyin: **TTF, OTF and " +"WOFF** (xaya kawai ake buqata)." + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.drafts" +msgstr "adana" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "title.dashboard.projects" +msgstr "tsare tsare - %s - Mazubin biruka" + +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +msgid "labels.retry" +msgstr "sake gwadawa" + +msgid "modals.delete-webhook.title" +msgstr "gogewa webhook" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.update-main" +msgstr "sabunta ainihin wurin" + +msgid "shortcuts.toggle-rules" +msgstr "Nuna / boye magwaji" + +msgid "shortcuts.draw-path" +msgstr "Hanya" + +#: src/app/main/ui/inspect/right_sidebar.cljs +msgid "inspect.tabs.code.selected.multiple" +msgstr "%s zavavve" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flow-start" +msgstr "fara malala" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "babu" + +msgid "inspect.attributes.typography.text-transform.titlecase" +msgstr "yadda ake rubuta batu" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "za mu fara!" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.remove-flex" +msgstr "cire sassaukan tsari" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "modals.delete-page.body" +msgstr "ka tabbata kana son goge wannan shafin?" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow-start" +msgstr "Gudun farko" + +msgid "shortcuts.move" +msgstr "matsa" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-confirm.message" +msgstr "ka tabbata kana son goge wannan kundin?" + +msgid "shortcuts.opacity-3" +msgstr "Seta dashi dashi zuwa kashi 50" + +msgid "common.share-link.team-members" +msgstr "memba kaxai" + +msgid "workspace.sidebar.layers.components" +msgstr "bangare" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease" +msgstr "sauki" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "sunan zai iya taimakawa wajen sanin menene alama" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.confirm-password" +msgstr "tabbatar da lambar tsaro" + +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/inspect/attributes/svg.cljs +msgid "workspace.sidebar.options.svg-attrs.title" +msgstr "shigo da shi SVG halaye" + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-shapes.how-to-link" +msgstr "bayanin yadda ake fitarwa daga fenfot" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.comments" +msgstr "ra'ayi (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-none" +msgstr "Babu" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.columns" +msgstr "shafi" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.accept" +msgstr "canza wurin mallaka" + +msgid "workspace.viewport.click-to-close-path" +msgstr "latsa kusa da hanya" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" + +msgid "labels.close" +msgstr "rufewa" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.lighten" +msgstr "kara masa haske" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +msgid "workspace.options.export-multiple" +msgstr "Fitar da zababbun" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +msgid "settings.detach" +msgstr "cire" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.hide" +msgstr "boye" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "zana" + +#: src/app/main/ui/settings/change_email.cljs +msgid "errors.email-invalid-confirmation" +msgstr "tabbata imel xinka ya yi daidai" + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-shapes.title" +msgstr "Fitar da zavi" + +msgid "onboarding-v2.welcome.desc2.title" +msgstr "hulxa da kai cikin mutane" + +msgid "dashboard.import.progress.process-page" +msgstr "fejin kasuwar duniya: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.set-default" +msgstr "saita a tsoho" + +#: src/app/main/errors.cljs +msgid "errors.feature-mismatch" +msgstr "" +"tamkar ka buxe wani kundi da ke da muhimmanci '%s' bayar da dama qarin da ka " +"yi ma fenfot xinka bai karbu ba ko ba zai yi aiki ba." + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +msgid "workspace.options.retry" +msgstr "sake" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-palette" +msgstr "nuna launukan kala" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-relative-to" +msgstr "alaka zuwa" + +msgid "errors.webhooks.ssl-validation" +msgstr "kuskure kan farfaxo da SSL." + +msgid "shortcuts.create-component" +msgstr "samar da abubuwa iri- iri" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.update-components-in-bulk" +msgstr "sabunta ainihin wurin" + +msgid "auth.terms-privacy-agreement" +msgstr "" +"lokacin qirqirar kundi, sai ka amincewa da yanayi aikin da " +"qa'idojinmuqa'idoji." + +msgid "modals.delete-font-variant.title" +msgstr "goge salon font" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.sidebar.history" +msgstr "labari (%s)" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-libraries-need-sync" +msgstr "Babu rabben ma'adanai da suke bukatar sabuntawa" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title" +msgstr "Dishi dishi" + +msgid "dashboard.export.options.all.title" +msgstr "fitar da manhajar tura kundi" + +msgid "workspace.options.clip-content" +msgstr "Matse abun ciki" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.editor" +msgstr "maigyara" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.new-password" +msgstr "sanya sabuwar lambar tsaro" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.path" +msgstr "hanya (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius.all-corners" +msgstr "duka kwanar" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-top-right" +msgstr "sama dama" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.sending" +msgstr "aikawa…" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.select-layer" +msgstr "zabi shimfida" + +msgid "workspace.undo.entry.single.image" +msgstr "hoto" + +msgid "workspace.shape.menu.intersection" +msgstr "mahada" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-trigger" +msgstr "Jawo" + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +msgid "dashboard.new-file" +msgstr "+ sabon kundi" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/auth.cljs +msgid "title.default" +msgstr "Mazubin biruka - Tsara yanci ga tawaga" + +#: src/app/main/ui/alert.cljs +msgid "ds.alert-title" +msgstr "natsu" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.shortcuts" +msgstr "yanke (%s)" + +#: src/app/main/ui/inspect/attributes/shadow.cljs +msgid "inspect.attributes.shadow" +msgstr "inuwa" + +msgid "common.share-link.title" +msgstr "fenfot" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding" +msgstr "filla filla" + +msgid "shortcuts.toggle-history" +msgstr "Danna tarihi" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.select-ui-theme" +msgstr "zaven batu" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate" +msgstr "kwafi" + +msgid "title.team-webhooks" +msgstr "gidan yanar gizo - %s - Mazubin biruka" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "sabunta ma'adana" + +#: src/app/main/ui/dashboard/team.cljs +#, markdown +msgid "labels.no-invitations-hint" +msgstr "danna **gayyato mutane** wurin da ake nemo mutane a wannan tawaga." + +msgid "inspect.tabs.code.selected.mask" +msgstr "marfi" + +msgid "modals.edit-webhook.submit-label" +msgstr "gyara webhook" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.none" +msgstr "babu" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-rules" +msgstr "boye ma'auni" + +msgid "dashboard.webhooks.create.success" +msgstr "an gina Webhook." + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.go-to-edit" +msgstr "tafi zuwa salon dakin karatu don a tace" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.confirm" +msgstr "haka, goge asusu" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-matches-for" +msgstr "Babu daidaituwa da aka samu na “%s“" + +msgid "shortcuts.move-unit-right" +msgstr "Matsa da sashin dama" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.projects" +msgstr "aiyuka" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.verification-email-sent" +msgstr "mun aika maka da saqon tantancewa ta imel" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-name" +msgstr "sunanka" + +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout" +msgstr "shiri" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-none" +msgstr "(ba'a saita ba )" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing" +msgstr "saukakawa" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "Triangle" + +msgid "workspace.path.actions.draw-nodes" +msgstr "zane da kauri (%s)" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-scale-text" +msgstr "bada damar sikelin rubutu" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row-reverse" +msgstr "jera sauyin fasali" + +msgid "errors.email-spam-or-permanent-bounces" +msgstr "saqonni marasa amfani na imel «%s»." + +msgid "labels.add-custom-font" +msgstr "inganta font" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "title.dashboard.shared-libraries" +msgstr "Rababban dakin karatu- %s - Mazubin biruka" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +msgid "workspace.options.export-object" +msgid_plural "workspace.options.export-object" +msgstr[0] "Fitarwa 1 Sashi" +msgstr[1] "Fitarwa %s sashi-sashi" + +msgid "errors.bad-font" +msgstr "ba za a iya xora fonts %s ba" + +msgid "shortcuts.align-bottom" +msgstr "tsarin kasa" + +msgid "shortcuts.align-top" +msgstr "tsarin sama" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.message" +msgstr "sabunta sashe a babbar taska" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.profile-not-verified" +msgstr "ba a tantance bayananka ba,sai an tantance a ci gaba." + +#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.radius" +msgstr "tsakiya" + +msgid "workspace.undo.entry.single.media" +msgstr "kadarar zanen hotuna" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.twitter-title" +msgstr "wurin karvar qorafin tiwita" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.packed" +msgstr "cushe" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "suna" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-style" +msgstr "tsarin salo" + +msgid "dashboard.import.analyze-error" +msgstr "kash! mun gaza shigo da kundinka" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-subtitle" +msgstr "za mu aika maka da saqon qa'idoji ta imel" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "ba zai yi aiki a kowane kundi ba." +msgstr[1] "ba za su yi aiki a kowane kundi ba." + +#: src/app/main/ui/settings/password.cljs +msgid "title.settings.password" +msgstr "Nambobin sirri - Mazubin biruka" + +msgid "workspace.header.menu.enable-snap-pixel-grid" +msgstr "Bda damar tsinkewa zuwa akwatin pixel" + +msgid "shortcuts.add-comment" +msgstr "Bayyana Ra'ayi" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-mouse-enter" +msgstr "linzamin kwamfuter ya shiga" + +msgid "workspace.undo.entry.single.circle" +msgstr "da'ira" + +msgid "labels.back" +msgstr "baya" + +msgid "viewer.header.interactions-section" +msgstr "Hulda (%s)" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.forgot-password" +msgstr "ka manta lambar tsaro?" + +msgid "shortcut-subsection.tools" +msgstr "Kayan aiki" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.recovery-token-sent" +msgstr "an aika maka da saqon matakan da za a bi domin dawo da lambar tsaronka." + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title.group" +msgstr "rukunin inuwa" + +msgid "modals.publish-empty-library.message" +msgstr "ba komai a taskarka. ka na son wallafa ta?" + +#: src/app/main/ui/workspace/comments.cljs +msgid "labels.only-yours" +msgstr "naka kawai" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title.group" +msgstr "rukunin shafi" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.reset-zoom" +msgstr "Kara saitawa" + +msgid "shortcuts.opacity-7" +msgstr "Saita dishi dishi zuwa kashi 70" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.assets" +msgstr "kadara" + +#: src/app/main/ui/inspect/attributes/image.cljs +msgid "inspect.attributes.image.width" +msgstr "faxi" + +msgid "shortcuts.snap-pixel-grid" +msgstr "yanke zuwa ga akwatin pixel" + +#: src/app/main/ui/auth/login.cljs +msgid "errors.ldap-disabled" +msgstr "LDAP ya gaza tantancewa." + +msgid "workspace.assets.open-library" +msgstr "bude fiyal din dakin karatu" + +#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +msgid "workspace.gradients.linear" +msgstr "a layi mikakke" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title-selection" +msgstr "zababbun rubutu" + +msgid "modals.delete-component-annotation.title" +msgstr "goge bayani" + +msgid "shortcuts.select-parent-layer" +msgstr "zabi ainihin shafi" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.promote-and-leave" +msgstr "inganta sannan ka bari" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.libraries.text.multiple-typography" +msgstr "Rubutun rubutu dayawa" + +msgid "dashboard.import.progress.upload-media" +msgstr "xora kundaye: %s" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.accept" +msgstr "goge kundaye" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.shared-library" +msgstr "rababben dakin karatu" + +msgid "labels.share-prototype" +msgstr "tura manhajar rubutu" + +msgid "shortcuts.export-shapes" +msgstr "samar da sabbabbin abubuwa" + +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-link-copied" +msgstr "kwafar hanyar gayya" + +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.notifications.password-saved" +msgstr "an tsare maka shaidar tsaro!" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group-name" +msgstr "sunan kungiya" + +msgid "shortcuts.search-placeholder" +msgstr "takaitaccen sako yanken" + +msgid "inspect.tabs.code.selected.svg-raw" +msgstr "SVG" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.size" +msgstr "girma" + +msgid "workspace.undo.entry.multiple.component" +msgstr "bangare" + +msgid "workspace.focus.selection" +msgstr "zaba" + +msgid "workspace.path.actions.merge-nodes" +msgstr "hade kauri (%s)" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.info" +msgstr "" +"za mu aika maka da imel ta wannan imel xin “%s” domin tantance shaidarka." + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.remove-member" +msgstr "cire mamba" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.create-component" +msgstr "kirkiri abubuwa" + +msgid "common.publish" +msgstr "wallafawa" + +msgid "shortcuts.select-next" +msgstr "zabi wani shafi" + +msgid "workspace.undo.entry.multiple.color" +msgstr "kadarar kala" + +#: src/app/main/ui/settings/options.cljs +msgid "title.settings.options" +msgstr "Saiti - Mazubin biruka" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.no-invitations" +msgstr "ba saqon da ba a buxa ba." + +msgid "workspace.header.menu.enable-scale-content" +msgstr "bada damar sikelin rabo" + +msgid "modals.delete-font-variant.message" +msgstr "" +"ka tabbata kana son goge wannan salon font din? ba zai xoru ba idan an yi " +"amfani da shi a kundi." + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.group-stroke" +msgstr "Ja layi a rukuni" + +msgid "workspace.shape.menu.union" +msgstr "hadakan" + +msgid "workspace.shape.menu.thumbnail-set" +msgstr "kara kamar girman babban yatsa" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.recovery-submit" +msgstr "canza lambar tsaro" + +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-cancel" +msgstr "goge" + +#: src/app/main/ui/inspect/attributes/image.cljs +msgid "inspect.attributes.image.download" +msgstr "sauke hanyar hoto" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom-fill" +msgstr "cike - cika sikeli" + +msgid "errors.bad-font-plural" +msgstr "ba za a iya xora fonts %s ba" + +msgid "inspect.empty.help" +msgstr "domin neman qarin bayani game da fenfot a tuntubi sashen agaji" + +msgid "workspace.sidebar.layers.texts" +msgstr "rubutu" + +#: src/app/main/ui/settings/profile.cljs +msgid "title.settings.profile" +msgstr "Karin bayani - Mazubin biruka" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.delete" +msgstr "goge" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "jera hagu (%s)" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.your-account" +msgstr "fagenka" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.dismiss" +msgstr "watsar" + +#: src/app/main/ui/settings/feedback.cljs +msgid "title.settings.feedback" +msgstr "bada martani - Mazubin biruka" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.duplicate" +msgstr "maimaita" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.change-email" +msgstr "canza imel" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.delete-team-confirm.title" +msgstr "gogewar tawaga" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "bayanai masu yawa" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.forward" +msgstr "kawo ta gaba" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.title" +msgstr "imel" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-size" +msgstr "girma" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.message" +msgstr "" +"kai ke da wannan tawagar yanzu. ka tabbata kana son yin %s sabon mai tawaga?" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-palette" +msgstr "farantin launuka" + +msgid "shortcuts.group" +msgstr "rukuni" + +msgid "auth.privacy-policy" +msgstr "matakan kaxaita" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.password-changed-successfully" +msgstr "ka canza lambar tsaro" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-while-hovering" +msgstr "yayin shawagi" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show" +msgstr "nuna" + +msgid "workspace.shape.menu.hide-ui" +msgstr "nuna / boye UI" + +msgid "shortcuts.move-fast-up" +msgstr "Matsa sama da sauri" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-project-prefix" +msgstr "sabon aiki" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.center" +msgstr "tsakiya" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-settings" +msgstr "Saiti - %s - Mazubin biruka" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-manual" +msgstr "Yi da kanka" + +msgid "labels.save" +msgstr "ajiye" + +msgid "dashboard.import.progress.process-media" +msgstr "kammala aiki" + +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-ok" +msgstr "haka" + +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "dole suna ya qumshi waxansu alamimon rubutu, sannan tazara." + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.message" +msgstr "ka tabbata kana son goge wannan aikin?" + +msgid "shortcuts.line-height-inc" +msgstr "kara tsawon layi" + +msgid "onboarding-v2.before-start.desc2" +msgstr "" +"cikakken bayanin yadda za a yi amfani da fenfot. daga rubutu zuwa tsara ko " +"rarraba iri." + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.sidebar.layers" +msgstr "shafi" + +msgid "shortcuts.select-all" +msgstr "zabi duka" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.graphics" +msgstr "%s zane zane" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-mouse-leave" +msgstr "linzamin kwamfutar ya fita" + +msgid "labels.log-or-sign" +msgstr "yi ko shiga" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "idan akwai qari (bayyana)" + +msgid "workspace.assets.typography.text-styles" +msgstr "salon rubutu" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker" +msgstr "da'irar kasuwa" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.searching-for" +msgstr "neman “%s“…" + +msgid "inspect.attributes.typography.text-decoration.none" +msgstr "babu" + +msgid "labels.discard" +msgstr "vatar" + +msgid "shortcuts.font-size-inc" +msgstr "kara gaban yanayi" + +msgid "common.share-link.permissions-pages" +msgstr "tura shafuka" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.layout-item-min-w" +msgstr "fadi mafi kankanta" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hcenter" +msgstr "Daidaita tsakiya a kwance (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.gap" +msgstr "Gibi" + +msgid "inspect.tabs.code.selected.group" +msgstr "qungiya" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker" +msgstr "murabba'in kasuwa" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column" +msgstr "shafi" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment-thread" +msgstr "goge rubutu" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation" +msgstr "hotuna masu motsi" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.drop-shadow" +msgstr "ajiye inuwa" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "jaraba fenfot ka ga ko ta yi daidai da tawaga " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "malami ko dalibi" + +msgid "workspace.undo.entry.single.curve" +msgstr "lankwasa" + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.main-message" +msgstr "alama!" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.overlay" +msgstr "mai rufi" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2-10" + +msgid "onboarding.team-modal.create-team" +msgstr "yin tawaga" + +msgid "dashboard.import.progress.process-colors" +msgstr "aikin rini" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-account" +msgstr "qirqiri taskar gwaji" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.title" +msgstr "ma'aunin karvuwar aiki" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.margin" +msgstr "gefe" + +msgid "onboarding-v2.welcome.title" +msgstr "barka da zuwa fenfot!" + +#, permanent +msgid "inspect.attributes.stroke.alignment.center" +msgstr "tsakiya" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-changed-successfully" +msgstr "ka zamanantar da adireshinka na imel" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "barin aikin tawaga" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.accept" +msgstr "qara yin wurin ajiyar tawaga" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease-in-out" +msgstr "sauki ciki waje" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-snap-guides" +msgstr "Tsinke zuwa mai jagora" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-linear" +msgstr "mikakke" + +#: src/app/main/ui/inspect/attributes/blur.cljs +msgid "inspect.attributes.blur.value" +msgstr "muhimmanci" + +msgid "common.share-link.manage-ops" +msgstr "amincewar shugaba" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.tutorial-hero.start" +msgstr "fara jagoranci" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-on-click" +msgstr "da an danna" + +msgid "onboarding.choice.team-up.invite-members" +msgstr "gayyato mambobi" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hdistribute" +msgstr "rarraba filin kwance (%s)" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-length-hint" +msgstr "aqalla a sami alamoni 8" + +msgid "modals.delete-webhook.message" +msgstr "ka tabbata ka na son goge webhook?" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-confirm.title" +msgstr "goge kundi" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.title" +msgstr "goge aikin" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-navigate-to-dest" +msgstr "kewayawa zywa: %s" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.selected-count" +msgid_plural "workspace.assets.selected-count" +msgstr[0] "%s" +msgstr[1] "%s zababbun abubuwan" + +msgid "dashboard.libraries-and-templates.import-error" +msgstr "akwai matsala wurin shigo da fejin talla. fejin tallar ba ya xauko." + +# SECTIONS +msgid "shortcut-section.basics" +msgstr "shikashikai" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discourse-go-to" +msgstr "je ka taskar fenfot" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.libraries-and-templates" +msgstr "taskoki & allunan talla" + +#: src/app/main/ui/auth/login.cljs +msgid "errors.wrong-credentials" +msgstr "kuskuren imel ko lambar tsaro." + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.spread" +msgstr "bazu" + +msgid "shortcuts.open-inspect" +msgstr "tafi sashin da 'yan kallo za su duba" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.tutorial-hero.info" +msgstr "ka san dokokin fenfot lokacin da ka ke tare da masoya koyarwa." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31-50" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.bottom" +msgstr "kasa" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-duration" +msgstr "tsahon lokaci" + +msgid "shortcuts.go-to-libs" +msgstr "ta fi zuwa rabbabben ma'ajiya" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.advanced-ops" +msgstr "zabi na ci gaba" + +#: src/app/main/ui/settings/change_email.cljs +msgid "notifications.validation-email-sent" +msgstr "an aika da imel din tantancewa %s. bincika imel xinka!" + +msgid "shortcuts.toggle-zoom-style" +msgstr "Danna salon zukowa" + +msgid "shortcut-subsection.zoom-workspace" +msgstr "Zukowa" + +msgid "shortcuts.increase-zoom" +msgstr "zuko ciki" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-outside" +msgstr "rufe yayin matsewa ta waje" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.blur" +msgstr "dishi dishi" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "ba ka da wasu lambobin tsaro yanzu." + +msgid "workspace.path.actions.separate-nodes" +msgstr "raba kauri (%s)" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.title-search" +msgstr "neman sakamako" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "teburin aiki" + +#: src/app/main/ui/workspace.cljs +msgid "labels.reload-file" +msgstr "qara dora kundi" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-id" +msgstr "tsarin haruffa" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.selection-stroke" +msgstr "gigciye zabi" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"lambarka ta shiga na a matsayin zabin hanyar shiga/lambar tsaronmuza a iya " +"amfani tsarin tantancewa,domin shiga manhajar cikin fenfot API" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.title" +msgstr "mallakar tawaga" + +msgid "dashboard.import" +msgstr "shigo da kundin fenfot" + +#: src/app/main/ui/workspace/sidebar/options.cljs +msgid "workspace.options.prototype" +msgstr "samfur" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.right" +msgstr "dama" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-delete-project" +msgstr "ka goge aikinka" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.text" +msgstr "fenfot ta menbobin tawaga ce. kirawo kowa domin yin aiki tarekundaye" + +#: src/app/main/ui/auth/recovery.cljs +msgid "profile.recovery.go-to-login" +msgstr "je ka hanyar shiga" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.theme-change" +msgstr "batun UI" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.use-play-button" +msgstr "yi amfani da madanneta ta sama ki tafi da samfuri." + +msgid "modals.delete-font.message" +msgstr "" +"ka tabbata kana son goge wannan font xin ? ba zai yi aiki ba idan an yi " +"amfani da shi a kundi." + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.hue" +msgstr "Haske haske" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-artboard-names" +msgstr "Nuna sunayen allo" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.save-error" +msgstr "an samu kuskure wajen adanawa" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.dashed" +msgstr "layin raba abu" + +msgid "common.share-link.all-users" +msgstr "duk fenfot" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "mallakar lambobin shiga na sirri" + +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.password-change" +msgstr "sauya lambar tsaro" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canva" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.fill" +msgstr "Cikawa" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.left" +msgstr "hagu" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "qara samun bayanin fenfot" + +msgid "workspace.shape.menu.thumbnail-remove" +msgstr "cire girman babban danyatsa" + +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.width" +msgstr "fadi" + +msgid "inspect.empty.select" +msgstr "zabar zubi, hukumar masu sa ido akan bangarorinsu da lambobinsu" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "gama aiki kan %s" + +msgid "shortcuts.toggle-layout-flex" +msgstr "Tara/fitar da lankwasashhiyar shimfida" + +msgid "labels.or" +msgstr "ko" + +msgid "onboarding.choice.team-up.roles" +msgstr "gayyata tare da bayar da matsayi:" + +msgid "labels.font-providers" +msgstr "ma su fenfot" + +msgid "shortcuts.italic" +msgstr "juya zuwa kwantaccen rubuyu" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.space-around" +msgstr "kewayayyen sarrari" + +msgid "errors.webhooks.timeout" +msgstr "dakatarwa" + +msgid "errors.profile-blocked" +msgstr "bayanan a rufe suke" + +msgid "workspace.options.width" +msgstr "fadi" + +msgid "shortcuts.letter-spacing-dec" +msgstr "rage filin harafin" + +msgid "errors.webhooks.last-delivery" +msgstr "saqon qarshe bai je ba." + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-files" +msgstr "an tura kundayenka" + +msgid "shortcut-subsection.general-dashboard" +msgstr "gamayya" + +#: src/app/main/ui/settings/password.cljs +msgid "errors.password-too-short" +msgstr "lambar tsaro dole ta kai yawan alamu 8" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.title" +msgstr "ka tabbata ka na son goge asusunka?" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.tutorial-hero.title" +msgstr "jagoranci" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.rect" +msgstr "Rectangle (%s)" + +msgid "labels.continue-with" +msgstr "ci gaba da" + +msgid "inspect.attributes.typography.text-transform.lowercase" +msgstr "qananan baqaqe" + +msgid "workspace.undo.entry.single.group" +msgstr "rukuni" + +msgid "inspect.attributes.stroke.style.dotted" +msgstr "xige-xige" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.layout-item-max-w" +msgstr "fadi mafi yawa" + +msgid "shortcuts.align-right" +msgstr "tsarin dama" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "can baya" + +msgid "modals.invite-member.emails" +msgstr "imel, rabawar waqafi" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.sitemap" +msgstr "taswirar wuri" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-dynamic-alignment" +msgstr "kashe daidaitawa mai canjawa" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius-bottom-left" +msgstr "kasan hagu" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.right" +msgstr "Dama" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-overlay" +msgstr "kulle mai rufi" + +msgid "errors.invite-invalid.info" +msgstr "za a iya soke gayyata ko ta ki aiki." + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "dashboard.fonts.fonts-added" +msgid_plural "dashboard.fonts.fonts-added" +msgstr[0] "an qara font 1" +msgstr[1] "%s an qara fonts da yawa" + +msgid "modals.create-webhook.submit-label" +msgstr "qirqirar webhook" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "" +"danna maddanin qasa\"danna qasa\n" +"emo sabuwar lambar tsaro\" samar da wani." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "goge alama" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vtop" +msgstr "Daidaita sama(%s)" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.viewer" +msgstr "ma su kallo" + +msgid "shortcuts.toggle-alignment" +msgstr "Danna a jere" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.invite-profile" +msgstr "neman mutane" + +#: src/app/main/ui/dashboard/grid.cljs +#, markdown +msgid "dashboard.empty-placeholder-drafts" +msgstr "" +"nan za a sami kundayen da aka sanya a taska. gwada sanya na ka kundin \"a " +"taskirarmumaginar kundi](https://manhajar fenfot/taskokin maginar kundaye." +"html)." + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker" +msgstr "darajar kasuwa" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.soft-light" +msgstr "haske mai sauki" + +msgid "shortcuts.decrease-zoom" +msgstr "fito da shi waje" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding-simple" +msgstr "sassaukan ciko" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.gutter" +msgstr "mahada" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group" +msgstr "rukuni" + +msgid "dashboard.webhooks.update.success" +msgstr "sabunta Webhook." + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-github-submit" +msgstr "matattarar manazarta" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.letter-spacing" +msgstr "tazarar harafi" + +msgid "inspect.tabs.code.selected.text" +msgstr "rubutu" + +msgid "shortcuts.opacity-4" +msgstr "Saita dishi dishi zuwa kashi 40" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title.multiple" +msgstr "zabin dishi dishi" + +msgid "shortcuts.align-vcenter" +msgstr "tsarin tsakiya a tsaye" + +msgid "workspace.shape.menu.create-annotation" +msgstr "kirkiri hoto mai motsi" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom-selected" +msgstr "zuko zababbe" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.delete-team-confirm.message" +msgstr "" +"ka tabbata kana son goge wannan tawagar? Duk aiyukanka na kundayen da suka " +"danganci tawagar za su yi gogewar dindindin." + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-overlay-dest" +msgstr "bude mai rufi: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type" +msgstr "Nau'i" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-url" +msgstr "bude URL" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.delete-invitation" +msgstr "goge gayyata" + +msgid "workspace.path.actions.delete-node" +msgstr "goge kauri (%s)" + +msgid "shortcuts.letter-spacing-inc" +msgstr "kara filin harafin" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-destination" +msgstr "makoma" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-google-submit" +msgstr "Gugul" + +msgid "onboarding.newsletter.acceptance-message" +msgstr "" +"an aika maka da saqon buqatar biya, za mu aika maka da saqon imel tabbatar " +"da shi." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.go-main" +msgstr "tafi ainihin wurin fal" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "fara aiki na" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.delete-team" +msgstr "goge tawaga" + +msgid "shortcuts.draw-frame" +msgstr "Allo" + +msgid "shortcuts.text-align-center" +msgstr "jera tsakiya" + +msgid "shortcuts.undo" +msgstr "Cire" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "mashi" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.accept" +msgstr "sabunta" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vcenter" +msgstr "Daidaita a kwance tsakiya (%s)" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.twitter-subtitle1" +msgstr "tambayoyin da ke buqatar amsa." + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.remove-account" +msgstr "ka na son cire taskarka?" + +msgid "labels.font-family" +msgstr "ire-iren font" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.viewer" +msgstr "yanayin kallo (%s)" + +msgid "workspace.path.actions.make-curve" +msgstr "Ta lankwasa (%s)" + +msgid "workspace.options.search-font" +msgstr "nemo jerin harufa" + +msgid "onboarding.team-modal.create-team-feature-3" +msgstr "aiyukan shugaba" + +msgid "workspace.path.actions.move-nodes" +msgstr "tafi da kauri (%s)" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discourse-title" +msgstr "dangin fenfot" + +msgid "workspace.path.actions.join-nodes" +msgstr "hada kauri (%s)" + +msgid "shortcuts.merge-nodes" +msgstr "hada da kauri" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "wane kayan zane ka fi iya aiki da shi?" + +msgid "shortcuts.bool-difference" +msgstr "ma'auni mabanbanci" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-url" +msgstr "URL" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "ka sami lambar tsaron da aka yi." + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.community" +msgstr "matattara" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.move" +msgstr "motsa abun" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title.multiple" +msgstr "dayawa" + +msgid "shortcuts.make-corner" +msgstr "kirkiri kwana" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.show-your-comments" +msgstr "bayyana na ka ra’ayin" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "mi ne ne matsayinka?" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.letter-spacing" +msgstr "fili a tsakanin haruffa" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.direction-ltr" +msgstr "LTR" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.shared-libraries" +msgstr "taskoki" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-invitations" +msgstr "Gayyata - %s - Mazubin biruka" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.pin-unpin" +msgstr "rufewa/buxewa" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.num-of-files" +msgid_plural "labels.num-of-files" +msgstr[0] "kundi 1" +msgstr[1] "kundaye %s" + +msgid "labels.custom-fonts" +msgstr "kwalliya da fenfot" + +#: src/app/main/ui/dashboard/comments.cljs +msgid "labels.comments" +msgstr "yabo" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.height" +msgstr "tsawo" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.delete" +msgstr "goge" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show-in-assets" +msgstr "nuna ta kusuwar kadara" + +msgid "workspace.undo.entry.multiple.shape" +msgstr "siffa" + +#: src/app/main/ui/inspect/right_sidebar.cljs +msgid "inspect.tabs.info" +msgstr "bayani" + +msgid "workspace.options.interaction-auto" +msgstr "da kanshi" + +msgid "onboarding.team-modal.create-team-feature-2" +msgstr "ma su yin shiri dayawa lokaci xaya" + +msgid "modals.add-shared-confirm-empty.hint" +msgstr "" +"xakin ba komai. wurin ajiyar tawaga, turken da ka yi za ya yi aiki a sauran " +"kundaye. ka tabbata kai ne ka ke son wallafa shi?" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.dashboard" +msgstr "dashbod" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title.multiple" +msgstr "inuwar zabi" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.save-settings" +msgstr "wurin ajiyar saiti" + +#: src/app/main/errors.cljs +msgid "errors.max-quote-reached" +msgstr "kammala adadin '%s' madogara. gyara." + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-prev-screen" +msgstr "allon daya gabata" + +msgid "labels.active" +msgstr "mai amfani" + +msgid "shortcuts.text-align-right" +msgstr "jera dama" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.add" +msgstr "tarawa" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.text-decoration" +msgstr "kwalliyar rubutu" + +msgid "dashboard.import.import-warning" +msgstr "wasu kundayen na dauke da abubuwan da ba su da amfani." + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "errors.media-type-mismatch" +msgstr "hoton da aka sanya bai yi daidai da kundin da ake son faxaxawa ba." + +msgid "shortcuts.opacity-0" +msgstr "Saita dishi dishi zuwa kashi 100" + +msgid "shortcuts.clear-undo" +msgstr "goge sake" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.text" +msgstr "rubutu (%s)" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.update" +msgstr "sabintawas" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.copy" +msgstr "kwafi" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.curve" +msgstr "kwana(%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding-all" +msgstr "duka gefan" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title" +msgstr "rubutu" + +#: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs +msgid "viewer.empty-state" +msgstr "Babu allon da aka samu a wannan fejin." + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.file-library" +msgstr "Ma'adanar fiyal" + +msgid "inspect.tabs.code.selected.path" +msgstr "hanya" + +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.left" +msgstr "hagu" + +msgid "shortcut-subsection.navigation-workspace" +msgstr "shawagi" + +msgid "shortcuts.bool-union" +msgstr "ma'auni hadaka" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.submit" +msgstr "sake imel" + +#: src/app/main/errors.cljs +msgid "errors.feature-not-supported" +msgstr "sassa '%s' ba za su yi aiki ba." + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.rename-team" +msgstr "sake suna tawaga" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title.group" +msgstr "rukuni" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-projects" +msgstr "aiyukan tawaga" + +msgid "shortcuts.underline" +msgstr "Danna ta layi a kasa" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "bai yiwuwa" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.rgb-complementary" +msgstr "Jituwar RGB" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.edit" +msgstr "Tace" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.message" +msgstr "ka tabbata kana son cire wannan memban a wannan tawaar?" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.hint" +msgstr "" +"ka kusa sabunta sashe a babbar taska. wannan za ya iya aiki a sauran " +"kundayen da ke amfani da ita." + +#: src/app/main/ui/static.cljs +msgid "labels.internal-error.main-message" +msgstr "kuskuren ciki" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.title" +msgstr "sake imel xinka" + +msgid "shortcut-subsection.modify-layers" +msgstr "gyara shimfida" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.release-notes" +msgstr "bayar da bayani" + +msgid "shortcuts.unmask" +msgstr "Cire takunkumi" + +msgid "workspace.options.y" +msgstr "Y layi" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.message" +msgstr "cire “%s” a taskar shirye-shirye" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.round" +msgstr "da'ira" + +msgid "shortcuts.toggle-lock" +msgstr "Rufe/bude" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.password" +msgstr "lambar tsaro" + +msgid "viewer.breaking-change.message" +msgstr "Sannu!" + +msgid "shortcut-subsection.panels" +msgstr "allon sarrarfav naura" + +msgid "inspect.tabs.code.selected.circle" +msgstr "da'ira" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.info" +msgstr "idan ka cire asusunka za ka iya rasa aikin da ka kammala." + +msgid "dashboard.loading-fonts" +msgstr "xora abin adonka …" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.hint1" +msgstr "" +"kai ne mai wannan tawagar. zabi wani memba da za ya iya inganta wa mai shi " +"kafin ka fita." + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-multi" +msgstr "tura %s kundaye a" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.topbottom" +msgstr "sama & kasa" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vdistribute" +msgstr "rarraba filin tsaye (%s)" + +msgid "shortcuts.separate-nodes" +msgstr "raba kauri" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-close-confirm.message" +msgstr "ka tabbata ka na son fita daga %s tawaga?" + +msgid "shortcut-subsection.path-editor" +msgstr "Hanya" + +msgid "common.share-link.link-copied-success" +msgstr "an samo kwafi" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "mawallafi/VP" + +#: src/app/main/ui/inspect/attributes/stroke.cljs +msgid "inspect.attributes.stroke.width" +msgstr "fadi" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.message" +msgid_plural "modals.delete-shared-confirm.message" +msgstr[0] "ka tabbata kana son goge wannan kundin?" +msgstr[1] "ka tabbata kana son goge waxannan kundayen?" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title" +msgstr "shafi" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.fonts" +msgstr "jerin harufa - %s - Mazubin biruka" + +msgid "workspace.undo.entry.multiple.text" +msgstr "rubutu" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.email" +msgstr "imel" + +msgid "workspace.sidebar.layers.shapes" +msgstr "Siffa" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.recent-colors" +msgstr "kalar yanzu" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.add-flow-start" +msgstr "Kara gudun farko" + +msgid "inspect.attributes.typography.text-transform.uppercase" +msgstr "manyan baqaqe" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.show-fill-on-export" +msgstr "fito da shi a ga" + +msgid "labels.inactive" +msgstr "maras amfani" + +msgid "dashboard.export.title" +msgstr "fitar da kundayr" + +msgid "modals.publish-empty-library.accept" +msgstr "wallafa" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.center" +msgstr "Tsakiya" + +msgid "shortcuts.toggle-lock-size" +msgstr "Rufe rabo" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment" +msgstr "goge yabo" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.layout-item-max-h" +msgstr "tsawo mafi yawa" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.register-submit" +msgstr "qirqiri kundi" + +msgid "onboarding-v2.before-start.desc3.title" +msgstr "koyarwa ta hoto mai motsi" + +msgid "shortcuts.thumbnail-set" +msgstr "saita babban yatsa" + +msgid "workspace.shape.menu.restore-main" +msgstr "saita ainihin wurin" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.no-projects-placeholder" +msgstr "nan za a ga bayanin aiki" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.libraries" +msgstr "ma'adanai" + +msgid "onboarding-v2.welcome.desc1" +msgstr "" +"fenfot ne ke yin kelaidos kamar yadda mutane ke yi, mutane na taimakon " +"junansu. kowa za ya iya hada hannu da:" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "samar da alamar shiga" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "ba lokacin daina amfani" + +msgid "shortcuts.go-to-search" +msgstr "gajeran sako" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "suna dole ya qunshi alamomin rubutu 250." + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.libraries" +msgstr "dakunan karatu" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.components" +msgstr "Bangarori" + +#, permanent +msgid "inspect.attributes.stroke.alignment.inner" +msgstr "daga ciki" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.select-all" +msgstr "zabi duka" + +msgid "labels.upload-custom-fonts" +msgstr "Upload custom fonts" + +msgid "shortcuts.flip-horizontal" +msgstr "kifa shi dai dai" + +msgid "dashboard.import.progress.process-components" +msgstr "aikin sassa" + +#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +msgid "dashboard.update-settings" +msgstr "zamanartarwa wurin gyara" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints" +msgstr "Takura" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.saturation" +msgstr "jikewa" + +msgid "workspace.sidebar.expand" +msgstr "kara yankin ma'agiyar bayani" + +#: src/app/main/ui/inspect/attributes/blur.cljs +msgid "inspect.attributes.blur" +msgstr "xige-xige" + +#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.mask" +msgstr "takunkumik" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.font-providers" +msgstr "samar da jerin harufa - %s - Mazubin biruka" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.num-of-projects" +msgid_plural "labels.num-of-projects" +msgstr[0] "aiki" +msgstr[1] "aiyuka %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.libraries.text.multiple-typography-tooltip" +msgstr "Cire mahadar duka rabutun rubutu" + +msgid "shortcuts.start-editing" +msgstr "fara gyarawa" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-toggle-overlay-dest" +msgstr "Juya mai murfi: %s" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.graphics" +msgstr "zane zane" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.pending-invitation" +msgstr "tukuna" + +msgid "workspace.path.actions.make-corner" +msgstr "ta kwana (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.line-height" +msgstr "tsawon layi" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "da'ira" + +msgid "modals.create-webhook.url.label" +msgstr "farashin URL" + +msgid "workspace.options.stroke-color" +msgstr "gigciye kalar" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11-30" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "kwafar kyauta" + +msgid "dashboard.export-binary-multi" +msgstr "sauke %s kundayen manhajar fenfot(.penpot)" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.components" +msgstr "%s bangarori" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.title" +msgid_plural "modals.unpublish-shared-confirm.title" +msgstr[0] "rufe taska" +msgstr[1] "rufe taskoki" + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-shapes.no-elements" +msgstr "ba wasu abubuwan da ake daidaitawa wajen tsara fitarwa." + +#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +msgid "workspace.gradients.radial" +msgstr "a da'ira" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.cancel" +msgstr "ajiye asusu da soke shi" + +msgid "shortcuts.move-unit-left" +msgstr "Matsa da sashin hagu" + +msgid "inspect.attributes.stroke.style.mixed" +msgstr "gauraya" + +msgid "shortcuts.toggle-colorpalette" +msgstr "Danna launukan kala" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.outer" +msgstr "waje" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius-top-left" +msgstr "saman hagu" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.start" +msgstr "fara koyarwa" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.box-filter-all" +msgstr "duka kadara" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.invite-invalid" +msgstr "gaiyar ba ta yi ba" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.normal" +msgstr "na kullum" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete-multi-files" +msgstr "goge %s kundaye" + +msgid "shortcuts.join-nodes" +msgstr "hada abubuwan" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.preferences" +msgstr "fifiko" + +#: src/app/main/ui/static.cljs +msgid "labels.service-unavailable.desc-message" +msgstr "mu na cikin kula a tsarinka." + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vbottom" +msgstr "Daidaita kasa (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-variant-id" +msgstr "bambanci" + +#: src/app/main/ui/settings/options.cljs +msgid "labels.language" +msgstr "harshe" + +msgid "shortcut-subsection.text-editor" +msgstr "Rubutu" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.shared-libraries" +msgstr "Rabban ma'adanai" + +msgid "workspace.shape.menu.exclude" +msgstr "kebe" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "an goge kundinka" +msgstr[1] "an goge kundayenka" + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.message" +msgstr "ka tabbata ka na son goge fira? duk sharhi a nan za a goge matsaloli." + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.rotation" +msgstr "juyawa" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "... zane-zane, kayan kallo, tsarin qira, etc." + +msgid "shortcuts.move-unit-up" +msgstr "Matsa da sashin samu" + +msgid "labels.upload" +msgstr "xorawa" + +msgid "onboarding-v2.welcome.desc2" +msgstr "" +"wurin da kowa zai iya koyo, fahimtar ta juna a kan fenfot, kasancewarta " +"manyan tawagar fenfot da sauran mutane." + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.rename" +msgstr "sake suna" + +msgid "shortcuts.zoom-lense-decrease" +msgstr "Zuko raguwar ido" + +msgid "modals.edit-webhook.title" +msgstr "gyara webhook" + +msgid "workspace.undo.entry.single.shape" +msgstr "siffa" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "workspace.sidebar.sitemap" +msgstr "shafi" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.width" +msgstr "fadi" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.send" +msgstr "aika" + +#: src/app/main/ui/workspace.cljs +msgid "title.workspace" +msgstr "%s - Mazubin biruka" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.no-matches-for" +msgstr "ba wanda ya yi daidai da “%s“" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.text-transform" +msgstr "canja rubutu" + +msgid "errors.email-as-password" +msgstr "ba za ka iya amfani da imel ba a matsayin lambar tsaro ba" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate-multi" +msgstr "kwafi %s kundaye" + +msgid "shortcuts.line-height-dec" +msgstr "rage tsawon layi" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.view" +msgstr "gani" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.detach-instances-in-bulk" +msgstr "raba yanayin abin" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.scale" +msgstr "maauni" + +msgid "inspect.empty.more-info" +msgstr "qarin bayani a fagen lura" + +#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/options.cljs +msgid "notifications.profile-saved" +msgstr "an ajiye bayanai!" + +msgid "workspace.focus.focus-off" +msgstr "karka maida hankali" + +msgid "shortcuts.toggle-fullscreen" +msgstr "Danna fuskar ta cika duka" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "karin bayani - Shiga alama" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "an kwafi kundinka" +msgstr[1] "an kwafi kundayenka" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.colors" +msgstr "kala" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.owner" +msgstr "mai" + +msgid "shortcuts.select-prev" +msgstr "zabi shafin da ya gabata" + +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-file" +msgstr "an tura aikinka" + +msgid "workspace.options.radius" +msgstr "digon tsakiyar da'ira" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.back" +msgstr "tura zuwa baya" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow" +msgstr "layin mashiw" + +msgid "shortcuts.align-left" +msgstr "tsarin hagu" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.edit" +msgstr "tace" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.ungroup" +msgstr "kashe daga kungiya" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.not-found" +msgstr "ba'a samu kadara ba" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom-fit-all" +msgstr "zuko yayi daidai da ko'ina" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.login-here" +msgstr "shiga nan" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.subject" +msgstr "shugabanci" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-after-delay" +msgstr "Bayan jinkiri" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team.placeholder" +msgstr "shigar da sabon sunan tawaga" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.layout-item-max-h" +msgstr "tsawo mafi yawa" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title" +msgstr "inuwa" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.size" +msgstr "girma" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.help-info" +msgstr "taimako & bayani" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title-group" +msgstr "rukunin rubutu" + +msgid "workspace.undo.entry.single.text" +msgstr "sako" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.type-something" +msgstr "rubuta neman sakamako" + +msgid "shortcuts.mask" +msgstr "takunkumi" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-fixed" +msgstr "dasa" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.image" +msgstr "hoto (%s)" + +msgid "shortcuts.line-through" +msgstr "danna layin duk" + +msgid "workspace.undo.entry.single.path" +msgstr "hanya" + +msgid "errors.cannot-upload" +msgstr "kasa xora xan aiken kundi." + +msgid "onboarding.choice.team-up.create-team-placeholder" +msgstr "sanya sunan tawaga" + +msgid "shortcuts.move-fast-down" +msgstr "Matsa kasa da sauri" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.add-flex" +msgstr "kara sassaukan tsarit" diff --git a/frontend/translations/he.po b/frontend/translations/he.po index 188e55a6c4..eafd1cd44b 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2023-07-24 19:05+0000\n" +"PO-Revision-Date: 2023-10-16 04:09+0000\n" "Last-Translator: Yaron Shahrabani \n" "Language-Team: Hebrew \n" @@ -10,7 +10,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && " "n % 10 == 0) ? 2 : 3));\n" -"X-Generator: Weblate 5.0-dev\n" +"X-Generator: Weblate 5.1-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -81,6 +81,14 @@ msgstr "LDAP" msgid "auth.login-with-oidc-submit" msgstr "‎OpenID Connect" +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "השם חייב להכיל תווים שאינם רווחים." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "השם חייב להכיל 250 תווים לכל היותר." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.new-password" msgstr "נא להקליד סיסמה חדשה" @@ -113,6 +121,10 @@ msgstr "סיסמה" msgid "auth.password-length-hint" msgstr "8 תווים לפחות" +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "הסיסמה חייבת להכיל תווים שאינם רווחים." + msgid "auth.privacy-policy" msgstr "מדיניות פרטיות" @@ -163,6 +175,10 @@ msgstr "יצירת חשבון חדש מהווה את הסכמתך לתנאי ה msgid "auth.verification-email-sent" msgstr "שלחנו הודעת דוא״ל לאימות אל" +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "…מיתוג, איורים, חומרים שיווקיים ועוד." + msgid "common.publish" msgstr "פרסום" @@ -257,6 +273,70 @@ msgstr "התחלת הסיור" msgid "dasboard.walkthrough-hero.title" msgstr "סיור בנבכי מנשק המשתמש" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "האסימון הועתק" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "יצירת אסימון חדש" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "אסימון הגישה נוצר בהצלחה." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "אין לך אסימונים עדיין." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "השם הוא בגדר חובה" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 יום" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 יום" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 יום" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 יום" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "לעולם לא" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "התוקף פג ב־%s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "התוקף יפוג ב־%s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "אין תאריך תפוגה" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "אסימוני כניסה אישיים" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "האסימון יפוג ב־%s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "לאסימון אין תאריך תפוגה" + #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" @@ -390,7 +470,6 @@ msgstr[1] "נוספו 2 גופנים" msgstr[2] "נוספו %s גופנים" msgstr[3] "נוספו %s גופנים" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "כל גופן דפדפן שיועלה כאן יתווסף לרשימת משפחת הגופנים שזמין במאפייני הטקסט " @@ -398,7 +477,6 @@ msgstr "" "גופנים יחידה**. ניתן להעלות גופנים מהסוגים הבאים: **TTF,‏ OTF ו־WOFF** (אחד " "הסוגים יספיק)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "עליך להעלות גופנים בבעלותך או שיש לך רישיון להשתמש בהם ב־Penpot. ניתן למצוא " @@ -410,6 +488,13 @@ msgstr "" msgid "dashboard.fonts.upload-all" msgstr "להעלות הכול" +msgid "dashboard.fonts.warning-text" +msgstr "" +"זיהינו בעיה אפשרית בגודפים שלך ביחס למדדים אנכיים למערכת הפעלה שונות. כדי " +"לבדוק את זה אפשר להשתמש בשירות מדידות אנכיות של גופנים כגון " +"[זה]](https://vertical-metrics.netlify.app/). בנוסף, המלצתנו היא להשתמש " +"ב־[Transfonter](https://transfonter.org/) כדי לייצר גופני רשת ולתקן שגיאות. " + msgid "dashboard.import" msgstr "ייבוא קובצי Penpot" @@ -578,10 +663,26 @@ msgstr "בחירת ערכת עיצוב" msgid "dashboard.show-all-files" msgstr "הצגת כל הקבצים" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "הקובץ שלך נמחק בהצלחה" +msgstr[1] "הקבצים שלך נמחקו בהצלחה" +msgstr[2] "הקבצים שלך נמחקו בהצלחה" +msgstr[3] "הקבצים שלך נמחקו בהצלחה" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-delete-project" msgstr "המיזם שלך נמחק בהצלחה" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "הקובץ שלך שוכפל בהצלחה" +msgstr[1] "הקבצים שלך שוכפלו בהצלחה" +msgstr[2] "הקבצים שלך שוכפלו בהצלחה" +msgstr[3] "הקבצים שלך שוכפלו בהצלחה" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-duplicate-project" msgstr "המיזם שלך שוכפל בהצלחה" @@ -720,6 +821,9 @@ msgstr "לא ניתן לטעון את הגופן %s" msgid "errors.bad-font-plural" msgstr "לא ניתן לטעון את הגופנים %s" +msgid "errors.cannot-upload" +msgstr "לא ניתן להעלות את קובץ המדיה." + #: src/app/main/data/workspace.cljs msgid "errors.clipboard-not-implemented" msgstr "הדפדפן שלך לא יכול לבצע את הפעולה הזאת" @@ -743,7 +847,8 @@ msgstr "אין לך אפשרות להשתמש בכתובת הדוא״ל שלך msgid "errors.email-has-permanent-bounces" msgstr "לכתובת הדוא״ל „%s” יש יותר מדי דוחות החזרה קבועים." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, +#: src/app/main/ui/auth/recovery_request.cljs msgid "errors.email-invalid" msgstr "נא למלא כתובת דוא״ל תקפה בבקשה" @@ -757,8 +862,8 @@ msgstr "כתובת הדוא״ל „%s” דווחה כספאם או שההודע #: src/app/main/errors.cljs msgid "errors.feature-mismatch" msgstr "" -"נראה שניסית לפתוח קובץ בו פעילה היכולת ‚%s’ אבל מנשק ה־Penpot שלך לא תומך בה " -"או שהיא מושבתת." +"נראה שניסית לפתוח קובץ בו פעילה היכולת ‚%s’ אבל מנשק ה־Penpot שלך לא תומך " +"בה או שהיא מושבתת." #: src/app/main/errors.cljs msgid "errors.feature-not-supported" @@ -1037,6 +1142,10 @@ msgstr "גודל גופן" msgid "inspect.attributes.typography.font-style" msgstr "סגנון גופן" +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-weight" +msgstr "משקל גופן" + #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.letter-spacing" msgstr "ריווח תווים" @@ -1135,6 +1244,10 @@ msgstr "קיצורי דרך" msgid "labels.accept" msgstr "מקובל" +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "אסימוני גישה" + msgid "labels.active" msgstr "פעיל" @@ -1238,6 +1351,9 @@ msgstr "מחיקת הזמנה" msgid "labels.delete-multi-files" msgstr "מחיקת %s קבצים" +msgid "labels.discard" +msgstr "התעלמות" + #: src/app/main/ui/dashboard/projects.cljs, #: src/app/main/ui/dashboard/sidebar.cljs, #: src/app/main/ui/dashboard/files.cljs, @@ -1362,7 +1478,6 @@ msgid "labels.no-invitations" msgstr "אין הזמנות ממתינות." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "לחיצה על הכפתור **הזמנת אנשים** תאפשר להזמין אנשים לצוות." @@ -1610,6 +1725,30 @@ msgstr "החלפת כתובת דוא״ל" msgid "modals.change-email.title" msgstr "החלפת כתובת הדוא״ל שלך" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "העתקת אסימון" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "תאריך תפוגה" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "שם" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "השם יכול לסייע לך להבין למה מיועד האסימון" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "יצירת אסימון" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "יצירת אסימון גישה" + msgid "modals.create-webhook.submit-label" msgstr "יצירת התליה" @@ -1622,6 +1761,18 @@ msgstr "כתובת מטען" msgid "modals.create-webhook.url.placeholder" msgstr "https://example.com/postreceive" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "מחיקת אסימון" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "למחוק את האסימון הזה?" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "מחיקת אסימון" + #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.cancel" msgstr "ביטול ושמירה על החשבון שלי" @@ -1715,38 +1866,6 @@ msgstr[1] "מחיקת קבצים" msgstr[2] "מחיקת קבצים" msgstr[3] "מחיקת קבצים" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "" -"מחיקה תוביל להעברת המשאבים לספרייה המקומית של הקובץ הזה. משאבים שאינם " -"בשימוש יאבדו." -msgstr[1] "" -"מחיקה שלהם תוביל להעברת המשאבים לספרייה המקומית של הקובץ הזה. משאבים שאינם " -"בשימוש יאבדו." -msgstr[2] "" -"מחיקה שלהם תוביל להעברת המשאבים לספרייה המקומית של הקובץ הזה. משאבים שאינם " -"בשימוש יאבדו." -msgstr[3] "" -"מחיקה שלהם תוביל להעברת המשאבים לספרייה המקומית של הקובץ הזה. משאבים שאינם " -"בשימוש יאבדו." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "" -"מחיקה תוביל להעברת המשאבים לספרייה המקומית של הקבצים האלה. משאבים שאינם " -"בשימוש יאבדו." -msgstr[1] "" -"מחיקה שלהם תוביל להעברת המשאבים לספרייה המקומית של הקבצים האלה. משאבים " -"שאינם בשימוש יאבדו." -msgstr[2] "" -"מחיקה שלהם תוביל להעברת המשאבים לספרייה המקומית של הקבצים האלה. משאבים " -"שאינם בשימוש יאבדו." -msgstr[3] "" -"מחיקה שלהם תוביל להעברת המשאבים לספרייה המקומית של הקבצים האלה. משאבים " -"שאינם בשימוש יאבדו." - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" @@ -1756,31 +1875,6 @@ msgstr[1] "למחוק את הקבצים?" msgstr[2] "למחוק את הקבצים?" msgstr[3] "למחוק את הקבצים?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" -msgstr[0] "אף אחד מהמשאבים שבספרייה של הקובץ הזה בשימוש. הם יימחקו יחד עם הקובץ." -msgstr[1] "אף אחד מהמשאבים שבספרייה של הקבצים האלה בשימוש. הם יימחקו יחד עם הקבצים." -msgstr[2] "אף אחד מהמשאבים שבספרייה של הקבצים האלה בשימוש. הם יימחקו יחד עם הקבצים." -msgstr[3] "אף אחד מהמשאבים שבספרייה של הקבצים האלה בשימוש. הם יימחקו יחד עם הקבצים." - -#: src/app/main/ui/workspace/header.cljs, -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "בחלק מהמשאבים בספרייה של הקובץ הזה נעשה שימוש כאן:" -msgstr[1] "בחלק מהמשאבים בספרייה של הקבצים האלה נעשה שימוש כאן:" -msgstr[2] "בחלק מהמשאבים בספרייה של הקבצים האלה נעשה שימוש כאן:" -msgstr[3] "בחלק מהמשאבים בספרייה של הקבצים האלה נעשה שימוש כאן:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "בחלק מהמשאבים בספרייה של הקובץ הזה נעשה שימוש כאן:" -msgstr[1] "בחלק מהמשאבים בספרייה של הקבצים האלה נעשה שימוש כאן:" -msgstr[2] "בחלק מהמשאבים בספרייה של הקבצים האלה נעשה שימוש כאן:" -msgstr[3] "בחלק מהמשאבים בספרייה של הקבצים האלה נעשה שימוש כאן:" - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" @@ -1836,6 +1930,9 @@ msgstr "שליחת הזמנה" msgid "modals.invite-member.emails" msgstr "כתובות דוא״ל, מופרדות בפסיקים" +msgid "modals.invite-member.repeated-invitation" +msgstr "חלק מכתובות הדוא״ל הן של חברי צוות נוכחיים. ההזמנות לא תישלחנה אליהם." + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-team-member.title" msgstr "הזמנת חברים לצוות" @@ -1903,6 +2000,15 @@ msgstr "הצוות הזה הוא כרגע בבעלותך. להפוך את %s ל msgid "modals.promote-owner-confirm.title" msgstr "בעלים חדשים לצוות" +msgid "modals.publish-empty-library.accept" +msgstr "פרסום" + +msgid "modals.publish-empty-library.message" +msgstr "הספרייה שלך ריקה. לפרסם אותה בכל זאת?" + +msgid "modals.publish-empty-library.title" +msgstr "פרסום ספרייה ריקה" + #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" @@ -1926,28 +2032,12 @@ msgstr "הינד קטן" #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "ביטול הפרסום יעביר את המשאבים לספרייה המקומית של הקובץ הזה." -msgstr[1] "ביטול הפרסומים יעביר את המשאבים לספרייה המקומית של הקובץ הזה." -msgstr[2] "ביטול הפרסומים יעביר את המשאבים לספרייה המקומית של הקובץ הזה." -msgstr[3] "ביטול הפרסומים יעביר את המשאבים לספרייה המקומית של הקובץ הזה." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "" -"ביטול הפרסום יחסום את הגישה למשאבים מקבצים אחרים. משאבים שכבר היו בשימוש " -"יישארו בקבצים האלה (העיצוב לא יישבר!)." -msgstr[1] "" -"ביטול הפרסום יחסום את הגישה למשאבים מקבצים אחרים. משאבים שכבר היו בשימוש " -"יישארו בקבצים האלה (העיצוב לא יישבר!)." -msgstr[2] "" -"ביטול הפרסום יחסום את הגישה למשאבים מקבצים אחרים. משאבים שכבר היו בשימוש " -"יישארו בקבצים האלה (העיצוב לא יישבר!)." -msgstr[3] "" -"ביטול הפרסום יחסום את הגישה למשאבים מקבצים אחרים. משאבים שכבר היו בשימוש " -"יישארו בקבצים האלה (העיצוב לא יישבר!)." +msgid "modals.unpublish-shared-confirm.accept" +msgid_plural "modals.unpublish-shared-confirm.accept" +msgstr[0] "ביטול פרסום" +msgstr[1] "ביטול פרסום" +msgstr[2] "ביטול פרסום" +msgstr[3] "ביטול פרסום" #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs @@ -1958,31 +2048,6 @@ msgstr[1] "לבטל את פרסום הספריות האלו?" msgstr[2] "לבטל את פרסום הספריות האלו?" msgstr[3] "לבטל את פרסום הספריות האלו?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "אף אחד מהמשאבים שבספרייה הזאת נמצא בשימוש." -msgstr[1] "אף אחד מהמשאבים שבספריות האלה נמצא בשימוש." -msgstr[2] "אף אחד מהמשאבים שבספריות האלה נמצא בשימוש." -msgstr[3] "אף אחד מהמשאבים שבספריות האלה נמצא בשימוש." - -#: src/app/main/ui/workspace/header.cljs, -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "בחלק מהמשאבים בספרייה הזאת נעשה שימוש כאן:" -msgstr[1] "בחלק מהמשאבים בספריות האלה נעשה שימוש כאן:" -msgstr[2] "בחלק מהמשאבים בספריות האלה נעשה שימוש כאן:" -msgstr[3] "בחלק מהמשאבים בספריות האלה נעשה שימוש כאן:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "בחלק מהמשאבים בספרייה הזאת נעשה שימוש כאן:" -msgstr[1] "בחלק מהמשאבים בספריות האלה נעשה שימוש כאן:" -msgstr[2] "בחלק מהמשאבים בספריות האלה נעשה שימוש כאן:" -msgstr[3] "בחלק מהמשאבים בספריות האלה נעשה שימוש כאן:" - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" @@ -2026,6 +2091,10 @@ msgstr "" msgid "modals.update-remote-component.message" msgstr "עדכון רכיב בספריה משותפת" +#: src/app/main/data/common.cljs +msgid "notifications.by-code.upgrade-version" +msgstr "יש גרסה חדשה, נא לרענן את העמוד" + #: src/app/main/ui/dashboard/team.cljs msgid "notifications.invitation-email-sent" msgstr "ההזמנה נשלחה בהצלחה" @@ -2111,12 +2180,6 @@ msgstr "מדריך למתנדבים" msgid "onboarding-v2.welcome.title" msgstr "ברוך בואך ל־Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "ליצירת צוות מאוחר יותר" - -msgid "onboarding.choice.team-up.create-team" -msgstr "שם הצוות שלך" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "לאחר מתן שם לצוות שלך, יתאפשר לך להזמין אנשים להצטרף." @@ -2129,12 +2192,6 @@ msgstr "הזמנת חברים" msgid "onboarding.choice.team-up.invite-members-info" msgstr "רצוי לזכור לכלול את כולם. מפתחים, מעצבים, מנהלים… גיוון מעשיר :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "יצירת צוות והזמנה בהמשך" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "יצירת צוות ושליחת הזמנות" - msgid "onboarding.choice.team-up.roles" msgstr "הזמנה עם התפקיד:" @@ -2186,6 +2243,130 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "מעבר למסך הכניסה" +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "באיזה כלי עיצוב יש לך יותר ניסיון?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11‏-30" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2-‏10" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31‏-50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "הרבה מהם" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canva" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.describe-your-experience-working-on" +msgstr "מה יתאר הכי טוב את אופן השימוש שלך…" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "עיצוב" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "פיתוח" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "היכרות מעמיקה יותר עם Penpot" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "יש לי עסק משלי" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "InVision" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "מתחילים!" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "ניהול מוצר או מיזם" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "שיווק" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "גדול מ־50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "הבאה" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "כלום" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "אחר (נא לפרט)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "זאת עבודה על מיזם פרטי" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "הקודמת" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "מהן התוכניות שלך בנוגע לשימוש ב־Penpot?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "מה התפקיד שלך?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.select-option" +msgstr "בחירת אפשרות" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "Sketch" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.some" +msgstr "חלק מהם" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "התחלה" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "התחלת עבודה על מיזם משלי" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "מה גודל הצוות שלך?" + #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, @@ -2245,6 +2426,9 @@ msgstr "נתיבים" msgid "shortcut-subsection.shape" msgstr "צורות" +msgid "shortcut-subsection.text-editor" +msgstr "טקסטים" + msgid "shortcut-subsection.tools" msgstr "כלים" @@ -2263,9 +2447,15 @@ msgstr "הוספת מפרק" msgid "shortcuts.align-bottom" msgstr "יישור לתחתית" +msgid "shortcuts.align-center" +msgstr "יישור למרכז" + msgid "shortcuts.align-hcenter" msgstr "יישור למרכז אופקית" +msgid "shortcuts.align-justify" +msgstr "יישור משני הצדדים" + msgid "shortcuts.align-left" msgstr "יישור שמאלה" @@ -2281,6 +2471,9 @@ msgstr "יישור למרכז אנכית" msgid "shortcuts.artboard-selection" msgstr "יצירת לוח מהבחירה" +msgid "shortcuts.bold" +msgstr "החלפת מצב מודגש" + msgid "shortcuts.bool-difference" msgstr "הבדל בוליאני" @@ -2371,6 +2564,12 @@ msgstr "היפוך אופקי" msgid "shortcuts.flip-vertical" msgstr "היפוך אנכי" +msgid "shortcuts.font-size-dec" +msgstr "הקטנת גודל הכתב" + +msgid "shortcuts.font-size-inc" +msgstr "הגדלת גודל הכתב" + msgid "shortcuts.go-to-drafts" msgstr "מעבר לטיוטות" @@ -2395,9 +2594,27 @@ msgstr "התקרבות" msgid "shortcuts.insert-image" msgstr "הוספת תמונה" +msgid "shortcuts.italic" +msgstr "החלפת מצב נטוי" + msgid "shortcuts.join-nodes" msgstr "צירוף מפרקים" +msgid "shortcuts.letter-spacing-dec" +msgstr "הקטנת ריווח תווים" + +msgid "shortcuts.letter-spacing-inc" +msgstr "הגדלת ריווח תווים" + +msgid "shortcuts.line-height-dec" +msgstr "הנמכת שורה" + +msgid "shortcuts.line-height-inc" +msgstr "הגבהת שורה" + +msgid "shortcuts.line-through" +msgstr "החלפת מצב קו חוצה" + msgid "shortcuts.make-corner" msgstr "הפיכה לפינה" @@ -2518,6 +2735,15 @@ msgstr "חיפוש בקיצורי הדרך" msgid "shortcuts.select-all" msgstr "בחירה בהכול" +msgid "shortcuts.select-next" +msgstr "בחירת השכבה הבאה" + +msgid "shortcuts.select-parent-layer" +msgstr "בחירת שכבת הורה" + +msgid "shortcuts.select-prev" +msgstr "בחירת השכבה הקודמת" + msgid "shortcuts.separate-nodes" msgstr "הפרדת מפרקים" @@ -2542,6 +2768,18 @@ msgstr "התחלת מדידה" msgid "shortcuts.stop-measure" msgstr "עצירת מדידה" +msgid "shortcuts.text-align-center" +msgstr "יישור למרכז" + +msgid "shortcuts.text-align-justify" +msgstr "פיזור שווה" + +msgid "shortcuts.text-align-left" +msgstr "יישור לשמאל" + +msgid "shortcuts.text-align-right" +msgstr "יישור לימין" + msgid "shortcuts.thumbnail-set" msgstr "הגדרת תמונות ממוזערות" @@ -2564,9 +2802,6 @@ msgstr "החלפת מצב מיקוד" msgid "shortcuts.toggle-fullscreen" msgstr "החלפת מילוי מסך" -msgid "shortcuts.toggle-grid" -msgstr "הצגת/הסתרת רשת" - msgid "shortcuts.toggle-history" msgstr "החלפת הצגת היסטוריה" @@ -2585,21 +2820,18 @@ msgstr "נעילת יחס" msgid "shortcuts.toggle-rules" msgstr "הצגת/הסתרת סרגלים" -msgid "shortcuts.toggle-scale-text" -msgstr "החלפת מצב שינוי קנה מידה של כתב" - -msgid "shortcuts.toggle-snap-grid" -msgstr "הצמדה לרשת" - -msgid "shortcuts.toggle-snap-guide" -msgstr "הצמדה לקווים מנחים" - msgid "shortcuts.toggle-textpalette" msgstr "החלפת לוח טקסט" +msgid "shortcuts.toggle-visibility" +msgstr "החלפת מצב הצגה" + msgid "shortcuts.toggle-zoom-style" msgstr "החלפת סגנון תקריב" +msgid "shortcuts.underline" +msgstr "החלפת מצב קו תחתי" + msgid "shortcuts.undo" msgstr "ביטול" @@ -2612,9 +2844,19 @@ msgstr "ביטול מסכה" msgid "shortcuts.v-distribute" msgstr "פיזור אנכי" +msgid "shortcuts.zoom-lense-decrease" +msgstr "הקטנת עדשת תקריב" + +msgid "shortcuts.zoom-lense-increase" +msgstr "הגדלת עדשת תקריב" + msgid "shortcuts.zoom-selected" msgstr "התמקדות על הנבחר" +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "אורך שם ההתליה הוא עד 2048 תווים." + #: src/app/main/ui/dashboard/files.cljs msgid "title.dashboard.files" msgstr "%s‏ - Penpot" @@ -2643,6 +2885,10 @@ msgstr "ספריות משותפות - %s‏ - Penpot" msgid "title.default" msgstr "Penpot - חופש עיצובי לצוותים" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "פרופיל - אסימוני גישה" + #: src/app/main/ui/settings/feedback.cljs msgid "title.settings.feedback" msgstr "הגשת משוב - Penpot" @@ -2807,6 +3053,9 @@ msgstr "מחיקה" msgid "workspace.assets.duplicate" msgstr "שכפול" +msgid "workspace.assets.duplicate-main" +msgstr "שכפול הראשי" + #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" @@ -2836,6 +3085,9 @@ msgstr "ספרייה מקומית" msgid "workspace.assets.not-found" msgstr "לא נמצאו משאבים" +msgid "workspace.assets.open-library" +msgstr "פתיחת קובץ ספרייה" + #: src/app/main/ui/workspace/sidebar/sitemap.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs @@ -2859,8 +3111,8 @@ msgstr[2] "%s פריטים נבחרו" msgstr[3] "%s פריטים נבחרו" #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "משותף" +msgid "workspace.assets.shared-library" +msgstr "ספרייה משותפת" #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs @@ -2934,14 +3186,13 @@ msgstr "מדרג מעגלי" msgid "workspace.header.menu.disable-dynamic-alignment" msgstr "השבתת יישור דינמי" +msgid "workspace.header.menu.disable-scale-content" +msgstr "השבתת קנה מידה יחסי" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-scale-text" msgstr "השבתת שינוי גודל טקסט" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "השבתת הצמדה לרשת" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "השבתת הצמדה לקווים המנחים" @@ -2953,14 +3204,13 @@ msgstr "השבתת הצמדה לפיקסל" msgid "workspace.header.menu.enable-dynamic-alignment" msgstr "הפעלת יישור דינמי" +msgid "workspace.header.menu.enable-scale-content" +msgstr "הפעלת קנה מידה יחסי" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-scale-text" msgstr "הפעלת שינוי גודל טקסט" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "הצמדה לרשת" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "הצמדה לקווים מנחים" @@ -2972,10 +3222,6 @@ msgstr "הפעלת הצמדה לפיקסל" msgid "workspace.header.menu.hide-artboard-names" msgstr "הסתרת שמות לוחות" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "הסתרת רשתות" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "הסתרת ערכת צבעים" @@ -3011,6 +3257,9 @@ msgstr "העדפות" msgid "workspace.header.menu.option.view" msgstr "תצוגה" +msgid "workspace.header.menu.redo" +msgstr "ביצוע מחדש" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.select-all" msgstr "לבחור הכול" @@ -3019,10 +3268,6 @@ msgstr "לבחור הכול" msgid "workspace.header.menu.show-artboard-names" msgstr "הצגת שמות לוחות" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "הצגת רשת" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "הצגת ערכת צבעים" @@ -3038,6 +3283,9 @@ msgstr "הצגת סרגלים" msgid "workspace.header.menu.show-textpalette" msgstr "הצגת לוח גופנים" +msgid "workspace.header.menu.undo" +msgstr "החזרה" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "איפוס" @@ -3062,6 +3310,10 @@ msgstr "שינויים שלא נשמרו" msgid "workspace.header.viewer" msgstr "מצב תצוגה (%s)" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "תקריב" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-fill" msgstr "מילוי - שינוי גודל כדי למלא" @@ -3090,6 +3342,10 @@ msgstr "הוספה" msgid "workspace.libraries.colors" msgstr "%s צבעים" +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "אין עדיין סגנונות צבע בספרייה שלך" + #: src/app/main/ui/workspace/colorpicker/libraries.cljs, #: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" @@ -3140,6 +3396,10 @@ msgstr "ספריות" msgid "workspace.libraries.library" msgstr "ספרייה" +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "עדכוני ספרייה" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.no-libraries-need-sync" msgstr "אין ספריות משותפות שדורשות עדכון" @@ -3176,6 +3436,10 @@ msgstr "%s טיפוגרפיות" msgid "workspace.libraries.update" msgstr "עדכון" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "הצגת כל השינויים" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.updates" msgstr "עדכונים" @@ -3261,6 +3525,15 @@ msgstr "ייצוא" msgid "workspace.options.export-multiple" msgstr "ייצוא הבחירה" +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs +msgid "workspace.options.export-object" +msgid_plural "workspace.options.export-object" +msgstr[0] "ייצוא רכיב" +msgstr[1] "ייצוא %s רכיבים" +msgstr[2] "ייצוא %s רכיבים" +msgstr[3] "ייצוא %s רכיבים" + #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs msgid "workspace.options.export.suffix" msgstr "סיומת" @@ -3825,12 +4098,12 @@ msgid "workspace.options.radius" msgstr "רדיוס" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "כל הפינות" +msgid "workspace.options.radius-bottom-left" +msgstr "בתחתית משמאל" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "פינות עצמאיות" +msgid "workspace.options.radius-bottom-right" +msgstr "בתחתית מימין" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3841,12 +4114,12 @@ msgid "workspace.options.radius-top-right" msgstr "בראש מימין" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "בתחתית משמאל" +msgid "workspace.options.radius.all-corners" +msgstr "כל הפינות" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "בתחתית מימין" +msgid "workspace.options.radius.single-corners" +msgstr "פינות עצמאיות" msgid "workspace.options.recent-fonts" msgstr "אחרונים" @@ -4008,26 +4281,10 @@ msgstr "אחיד" msgid "workspace.options.text-options.align-bottom" msgstr "יישור לתחתית" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "יישור למרכז (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "יישור לשני הצדדים (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "יישור שמאלה (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "יישור לאמצע" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "יישור ימינה (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "יישור לראש" @@ -4073,6 +4330,22 @@ msgstr "ללא" msgid "workspace.options.text-options.strikethrough" msgstr "קו חוצה (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "יישור למרכז (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "יישור לשני הצדדים (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "יישור שמאלה (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "יישור ימינה (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "טקסט" @@ -4140,36 +4413,13 @@ msgstr "הפרדת מפרקים (%s)" msgid "workspace.path.actions.snap-nodes" msgstr "הצמדת מפרקים (%s)" -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "" -"כדי לנסות שוב, אפשר לרענן את הקובץ הזה. אם הבעיה נמשכת, אנו ממליצים לך " -"להביט ברשימה ולשקול למחוק גרפיקה פגומה." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "לא ניתן לעדכן חלק מהגרפיקה." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "מתבצעת המרה %s/%s" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "גרפיקות ספרייה הן רכיבים מעתה ואילך, מה שהופך אותן להרבה יותר עוצמתיות." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "העדכון הזה הוא חד־פעמי." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "%s מתעדכן…" - #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.add-flex" msgstr "הוספת פריסת flex" +msgid "workspace.shape.menu.add-grid" +msgstr "הוספת פריסת רשת" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "הרחקה" @@ -4190,6 +4440,9 @@ msgstr "בחירה ללוח" msgid "workspace.shape.menu.create-component" msgstr "יצירת רכיב" +msgid "workspace.shape.menu.create-multiple-components" +msgstr "יצירת מגוון רכיבים" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.cut" msgstr "גזירה" @@ -4576,6 +4829,10 @@ msgstr "היסטוריה" msgid "workspace.updates.dismiss" msgstr "התעלמות" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "מידע נוסף" + #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.there-are-updates" msgstr "יש עדכונים בספריות המשותפות" @@ -4587,115 +4844,145 @@ msgstr "עדכון" msgid "workspace.viewport.click-to-close-path" msgstr "לחיצה תסגור את הנתיב" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs -msgid "workspace.options.export-object" -msgid_plural "workspace.options.export-object" -msgstr[0] "ייצוא רכיב" -msgstr[1] "ייצוא %s רכיבים" -msgstr[2] "ייצוא %s רכיבים" -msgstr[3] "ייצוא %s רכיבים" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "אפשר להתנסות לפני שימוש ב־Penpot אצלך בעבודה" -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-weight" -msgstr "משקל גופן" +msgid "dashboard.import.import-message" +msgid_plural "dashboard.import.import-message" +msgstr[0] "קובץ יובא בהצלחה." +msgstr[1] "%s קבצים יובאו בהצלחה." +msgstr[2] "%s קבצים יובאו בהצלחה." +msgstr[3] "%s קבצים יובאו בהצלחה." -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-duplicate-file" -msgid_plural "dashboard.success-delete-file" -msgstr[0] "הקובץ שלך שוכפל בהצלחה" -msgstr[1] "הקבצים שלך שוכפלו בהצלחה" -msgstr[2] "הקבצים שלך שוכפלו בהצלחה" -msgstr[3] "הקבצים שלך שוכפלו בהצלחה" - -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-delete-file" -msgid_plural "dashboard.success-delete-file" -msgstr[0] "הקובץ שלך נמחק בהצלחה" -msgstr[1] "הקבצים שלך נמחקו בהצלחה" -msgstr[2] "הקבצים שלך נמחקו בהצלחה" -msgstr[3] "הקבצים שלך נמחקו בהצלחה" - -msgid "shortcut-subsection.text-editor" -msgstr "טקסטים" +msgid "modals.delete-component-annotation.message" +msgstr "למחוק את הסימון הזה?" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.accept" -msgid_plural "modals.unpublish-shared-confirm.accept" -msgstr[0] "ביטול פרסום" -msgstr[1] "ביטול פרסום" -msgstr[2] "ביטול פרסום" -msgstr[3] "ביטול פרסום" +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "הספרייה הזאת מופעלת כאן: " +msgstr[1] "הספריות האלו מופעלות כאן: " +msgstr[2] "הספריות האלו מופעלות כאן: " +msgstr[3] "הספריות האלו מופעלות כאן: " -msgid "shortcuts.align-center" -msgstr "יישור למרכז" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "לא מופעל באף קובץ." +msgstr[1] "לא מופעלים באף קובץ." +msgstr[2] "לא מופעלים באף קובץ." +msgstr[3] "לא מופעלים באף קובץ." -msgid "shortcuts.align-justify" -msgstr "יישור משני הצדדים" +msgid "modals.delete-component-annotation.title" +msgstr "מחיקת סימון" -msgid "shortcuts.bold" -msgstr "החלפת מצב מודגש" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "לבדוק את Penpot ולראות אם הוא מתאים לצוות שלי " -msgid "shortcuts.font-size-dec" -msgstr "הקטנת גודל הכתב" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "סטודנט/ית או מרצה" -msgid "shortcuts.letter-spacing-dec" -msgstr "הקטנת ריווח תווים" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "להשאיר משוב למיזם הצוותי שלי" -msgid "shortcuts.font-size-inc" -msgstr "הגדלת גודל הכתב" - -msgid "shortcuts.line-height-dec" -msgstr "הנמכת שורה" - -msgid "shortcuts.underline" -msgstr "החלפת מצב קו תחתי" - -msgid "shortcuts.italic" -msgstr "החלפת מצב נטוי" - -msgid "shortcuts.letter-spacing-inc" -msgstr "הגדלת ריווח תווים" - -msgid "shortcuts.line-height-inc" -msgstr "הגבהת שורה" - -msgid "shortcuts.select-prev" -msgstr "בחירת השכבה הקודמת" - -msgid "shortcuts.line-through" -msgstr "החלפת מצב קו חוצה" - -msgid "shortcuts.select-next" -msgstr "בחירת השכבה הבאה" - -msgid "shortcuts.zoom-lense-decrease" -msgstr "הקטנת עדשת תקריב" - -msgid "shortcuts.zoom-lense-increase" -msgstr "הגדלת עדשת תקריב" - -msgid "workspace.header.menu.disable-scale-content" -msgstr "השבתת קנה מידה יחסי" - -msgid "workspace.header.menu.undo" -msgstr "החזרה" - -msgid "workspace.assets.duplicate-main" -msgstr "שכפול הראשי" - -msgid "workspace.header.menu.enable-scale-content" -msgstr "הפעלת קנה מידה יחסי" - -#, markdown -msgid "dashboard.fonts.warning-text" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" msgstr "" -"זיהינו בעיה אפשרית בגודפים שלך ביחס למדדים אנכיים למערכת הפעלה שונות. כדי " -"לבדוק את זה אפשר להשתמש בשירות מדידות אנכיות של גופנים כגון [זה]](https" -"://vertical-metrics.netlify.app/). בנוסף, המלצתנו היא להשתמש " -"ב־[Transfonter](https://transfonter.org/) כדי לייצר גופני רשת ולתקן שגיאות. " +"אסימוני גישה אישיים הם דרך חלופית למערכת אימות הכניסה/סיסמה שלנו ומאפשרים " +"ליישום לגשת ל־API הפנימי של Penpot" -msgid "modals.invite-member.repeated-invitation" -msgstr "חלק מכתובות הדוא״ל הן של חברי צוות נוכחיים. ההזמנות לא תישלחנה אליהם." +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "נא ללחוץ על הכפתור „יצירת אסימון חדש” כדי ליצור אחד חדש." -msgid "workspace.header.menu.redo" -msgstr "ביצוע מחדש" +msgid "modals.add-shared-confirm-empty.hint" +msgstr "" +"הספרייה שלך ריקה. לאחר שנוספה כתיקייה משותפת, הנכסים שנוצרים על ידיך יהיו " +"זמינים לצד שאר הקבצים שלך. לפרסם אותה?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "… עיצוב מנשק, נכסים חזותיים, מערכות עיצוב, וכו׳." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "עבודה עם רעיונות למימוש" + +msgid "workspace.options.component.copy" +msgstr "העתקה" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" +msgstr "" +"המשוב שלך יסייע לנו להבין מה הם ההרגלים וההעדפות שלך כדי שנוכל להמשיך להפוך " +"את Penpot לכלי מהנה ושימושי." + +msgid "workspace.options.component.create-annotation" +msgstr "יצירת הסבר" + +msgid "workspace.options.component.edit-annotation" +msgstr "עריכת הסבר" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "הורדת הקוד מהמיזם הצוותי שלי " + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "מרובע" + +msgid "workspace.options.component.main" +msgstr "ראשי" + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "עדיין אין סוגי טיפוגרפיה בספרייה שלך" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" +msgstr[0] "נכסים שכבר נעשה בהם שימוש בקובץ הזה יישארו שם (אף עיצוב לא ייפגע)." +msgstr[1] "נכסים שכבר נעשה בהם שימוש בקבצים האלה יישארו שם (אף עיצוב לא ייפגע)." +msgstr[2] "נכסים שכבר נעשה בהם שימוש בקבצים האלה יישארו שם (אף עיצוב לא ייפגע)." +msgstr[3] "נכסים שכבר נעשה בהם שימוש בקבצים האלה יישארו שם (אף עיצוב לא ייפגע)." + +msgid "workspace.options.component.annotation" +msgstr "הסבר" + +msgid "workspace.layout_grid.editor.title" +msgstr "רשת עריכה" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" +msgstr "… תרשימי מתאר, סיפורי ותהליכי משתמשים, עצי ניווט ועוד." + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "יהלום" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +msgid "settings.detach" +msgstr "ניתוק" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "משולש" + +msgid "workspace.shape.menu.create-annotation" +msgstr "יצירת הסבר" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "חץ" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "מייסד/סגן נשיא" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "עיגול" diff --git a/frontend/translations/hr.po b/frontend/translations/hr.po index 34a5d24333..a0e38637e1 100644 --- a/frontend/translations/hr.po +++ b/frontend/translations/hr.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "PO-Revision-Date: 2023-07-05 02:17+0000\n" "Last-Translator: Kristijan Žic \n" -"Language-Team: Croatian \n" +"Language-Team: Croatian " +"\n" "Language: hr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" @@ -38,7 +38,8 @@ msgstr "" "Ovo je DEMO usluga. NEMOJ KORISTITI za pravi rad. Projekti će se povremeno " "brisati." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" msgstr "E-mail" @@ -263,7 +264,8 @@ msgstr "Započni obilazak" msgid "dasboard.walkthrough-hero.title" msgstr "Pregledaj sučelje" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Dodaj kao zajedničku biblioteku" @@ -293,7 +295,8 @@ msgstr "Preuzmi Penpot datoteku (.penpot)" msgid "dashboard.download-standard-file" msgstr "Preuzmi standardnu datoteku (.svg + .json)" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate" msgstr "Kopija" @@ -302,7 +305,6 @@ msgid "dashboard.duplicate-multi" msgstr "Kopiraj %s datoteka" #: src/app/main/ui/dashboard/grid.cljs -#, markdown msgid "dashboard.empty-placeholder-drafts" msgstr "" "O ne! Još nemaš datoteka! Ako želiš isprobati neke predloške, idi na " @@ -403,7 +405,6 @@ msgstr[0] "1 font dodan" msgstr[1] "%s fontova dodano" msgstr[2] "%s fontova dodano" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "Svaki web-font koji ovdje preneseš biti će dodan na popis fontova koji je " @@ -411,7 +412,6 @@ msgstr "" "će grupirani kao **jedan font**. Možeš učitati fontove sa sljedećim " "formatima: **TTF, OTF i WOFF** (biti će potreban samo jedan)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "Možeš učitavati samo fontove koje posjeduješ ili imaš licencu za korištenje " @@ -463,7 +463,8 @@ msgstr "Prijenos datoteke: %s" msgid "dashboard.invite-profile" msgstr "Pozovi u tim" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" msgstr "Napusti tim" @@ -487,7 +488,8 @@ msgstr "učitavanje tvojih datoteka…" msgid "dashboard.loading-fonts" msgstr "učitavanje tvojih fontova…" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "Premijesti u" @@ -499,7 +501,8 @@ msgstr "Premijesti %s datoteke u" msgid "dashboard.move-to-other-team" msgstr "Premijesti u drugi tim" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" msgstr "+ Nova datoteka" @@ -562,7 +565,8 @@ msgstr "Projekti" msgid "dashboard.remove-account" msgstr "Želiš li ukloniti svoj račun?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" msgstr "Ukloni kao zajedničku biblioteku" @@ -606,7 +610,8 @@ msgstr "Tvoja datoteka je uspješno duplicirana" msgid "dashboard.success-duplicate-project" msgstr "Tvoj projekt je uspješno dupliciran" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-file" msgstr "Tvoja datoteka je uspješno premještena" @@ -642,11 +647,13 @@ msgstr "Pretraži rezultate" msgid "dashboard.type-something" msgstr "Upiši za rezultate pretraživanja" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.unpublish-shared" msgstr "Poništi objavu biblioteke" -#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Ažuriraj postavke" @@ -662,7 +669,11 @@ msgstr "E-mail" msgid "dashboard.your-name" msgstr "Ime" -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" msgstr "Tvoj Penpot" @@ -702,7 +713,8 @@ msgstr "Čini se da nisi autentificiran/a ili je sesija istekla." msgid "errors.clipboard-not-implemented" msgstr "Tvoj preglednik ne može izvršiti ovu operaciju" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/change_email.cljs msgid "errors.email-already-exists" msgstr "E-mail se već koristi" @@ -713,7 +725,10 @@ msgstr "E-mail je već potvrđen." msgid "errors.email-as-password" msgstr "Ne možeš koristiti svoj e-mail kao lozinku" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" msgstr "E-pmail «%s» ima mnogo trajnih izvješća o odbijanju." @@ -724,7 +739,8 @@ msgstr "E-mail za potvrdu mora odgovarati" msgid "errors.email-spam-or-permanent-bounces" msgstr "E-mail «%s» je prijavljen kao neželjena pošta ili je trajno odbijen." -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.generic" msgstr "Dogodilo se nešto loše." @@ -751,7 +767,7 @@ msgstr "Slika je prevelika za umetanje." msgid "errors.media-type-mismatch" msgstr "Čini se da sadržaj slike ne odgovara ekstenziji datoteke." -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" msgstr "Čini se da ovo nije važeća slika." @@ -769,7 +785,9 @@ msgstr "Lozinka za potvrdu mora odgovarati" msgid "errors.password-too-short" msgstr "Lozinka mora sadržavati najmanje 8 znakova" -#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.profile-is-muted" msgstr "" "Tvoj profil ima isključen e-mail (izvješća o neželjenoj pošti ili veliki " @@ -788,7 +806,9 @@ msgstr "Član kojeg pokušavaš dodijeliti ne postoji." msgid "errors.team-leave.owner-cant-leave" msgstr "Vlasnik ne može napustiti tim, moraš ponovno dodijeliti ulogu vlasnika." -#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/data/media.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "errors.unexpected-error" msgstr "Došlo je do neočekivane pogreške." @@ -836,7 +856,7 @@ msgstr "E-mail" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Idi na Twitter" +msgstr "Idi na X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -844,7 +864,7 @@ msgstr "Ovdje za pomoć za tvoje tehničke upite." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Twitter korisnički račun za podršku" +msgstr "X korisnički račun za podršku" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -898,7 +918,8 @@ msgstr "Visina" msgid "inspect.attributes.layout.left" msgstr "Lijevo" -#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs +#: src/app/main/ui/inspect/attributes/layout.cljs, +#: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.layout.radius" msgstr "Radius" @@ -922,15 +943,12 @@ msgstr "Sjena" msgid "inspect.attributes.stroke" msgstr "Potez" -#, permanent msgid "inspect.attributes.stroke.alignment.center" msgstr "Sredina" -#, permanent msgid "inspect.attributes.stroke.alignment.inner" msgstr "Unutra" -#, permanent msgid "inspect.attributes.stroke.alignment.outer" msgstr "Vani" @@ -1063,7 +1081,7 @@ msgstr "Prihvati" msgid "labels.add-custom-font" msgstr "Dodajte custom font" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "Administrator" @@ -1119,7 +1137,8 @@ msgstr "Možeš nastaviti s Penpot računom" msgid "labels.create" msgstr "Kreiraj" -#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team" msgstr "Kreiraj novi tim" @@ -1134,7 +1153,8 @@ msgstr "Custom fontovi" msgid "labels.dashboard" msgstr "Nadzorna ploča" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "Izbriši" @@ -1154,7 +1174,10 @@ msgstr "Izbriši pozivnicu" msgid "labels.delete-multi-files" msgstr "Izbriši %s datoteka" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/files.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.drafts" msgstr "Nacrti" @@ -1165,7 +1188,7 @@ msgstr "Uredi" msgid "labels.edit-file" msgstr "Uredi datoteku" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "Urednik" @@ -1201,7 +1224,9 @@ msgstr "Fontovi" msgid "labels.github-repo" msgstr "Github repozitorij" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "Daj povratnu informaciju" @@ -1229,7 +1254,8 @@ msgstr "" msgid "labels.internal-error.main-message" msgstr "Interna pogreška" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.invitations" msgstr "Pozivnice" @@ -1248,11 +1274,11 @@ msgstr "Prijava ili registracija" msgid "labels.logout" msgstr "Odjava" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.member" msgstr "Član" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "Članovi" @@ -1260,7 +1286,8 @@ msgstr "Članovi" msgid "labels.new-password" msgstr "Nova lozinka" -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +#: src/app/main/ui/workspace/comments.cljs, +#: src/app/main/ui/dashboard/comments.cljs msgid "labels.no-comments-available" msgstr "Nemaš obavijesti o komentarima na čekanju" @@ -1269,7 +1296,6 @@ msgid "labels.no-invitations" msgstr "Nema pozivnica." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "Pritisni gumb \"Pozovi u tim\" da pozoveš više članova u ovaj tim." @@ -1316,7 +1342,8 @@ msgstr "ili" msgid "labels.owner" msgstr "Vlasnik" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.password" msgstr "Lozinka" @@ -1337,7 +1364,8 @@ msgstr "Projekti" msgid "labels.release-notes" msgstr "Release notes" -#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/workspace/libraries.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" msgstr "Ukloni" @@ -1345,7 +1373,9 @@ msgstr "Ukloni" msgid "labels.remove-member" msgstr "Ukloni člana" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.rename" msgstr "Preimenuj" @@ -1357,7 +1387,7 @@ msgstr "Preimenuj tim" msgid "labels.resend-invitation" msgstr "Ponovno pošalji pozivnicu" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Pokušaj ponovo" @@ -1387,7 +1417,8 @@ msgstr "U programiranom smo održavanju naših sustava." msgid "labels.service-unavailable.main-message" msgstr "Usluga je nedostupna" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.settings" msgstr "Postavke" @@ -1442,7 +1473,7 @@ msgstr "Promatrač" msgid "labels.write-new-comment" msgstr "Napiši novi komentar" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.you" msgstr "(ti)" @@ -1450,21 +1481,24 @@ msgstr "(ti)" msgid "labels.your-account" msgstr "Tvoj korisnički račun" -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "Učitavanje slike…" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" msgstr "Dodaj kao zajedničku biblioteku" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.hint" msgstr "" "Nakon dodavanja kao zajedničku biblioteku, stavke ove biblioteke datoteka " "bit će dostupni za korištenje među ostalim tvojim datotekama." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.message" msgstr "Dodajte “%s” kao zajedničku biblioteku" @@ -1585,28 +1619,24 @@ msgstr "Jesi li siguran/na da želiš izbrisati ovaj projekt?" msgid "modals.delete-project-confirm.title" msgstr "Brisanje projekta" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.accept" msgid_plural "modals.delete-shared-confirm.accept" msgstr[0] "Izbriši datoteku" msgstr[1] "Izbriši datoteke" msgstr[2] "Izbriši datoteke" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "Jesi li siguran/na da želiš izbrisati ovu datoteku?" msgstr[1] "Jesi li siguran/na da želiš izbrisati ove datoteke?" msgstr[2] "Jesi li siguran/na da želiš izbrisati ove datoteke?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Ova datoteka ima biblioteke koje se koriste u ovoj datoteci:" -msgstr[1] "Ova datoteka ima biblioteke koje se koriste u ovim datotekama:" -msgstr[2] "Ova datoteka ima biblioteke koje se koriste u ovim datotekama:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" msgstr[0] "Brisanje datoteke" @@ -1715,11 +1745,13 @@ msgstr "" msgid "modals.promote-owner-confirm.title" msgstr "Novi vlasnik tima" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" msgstr "Ukloni kao zajedničku biblioteku" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs #, fuzzy msgid "modals.remove-shared-confirm.hint" msgstr "" @@ -1727,7 +1759,8 @@ msgstr "" "datoteke više neće biti dostupna za korištenje među tvojim ostalim " "datotekama." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.message" msgstr "Ukloni “%s” kao zajedničku biblioteku" @@ -1735,63 +1768,58 @@ msgstr "Ukloni “%s” kao zajedničku biblioteku" msgid "modals.small-nudge" msgstr "Mali pomak" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.accept" msgstr "Poništi objavu" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "Ako poništiš objavu, stavke u njoj postaju biblioteka ove datoteke." -msgstr[1] "Ako poništiš objavu, stavke u njoj postaju biblioteka ovih datoteka." -msgstr[2] "Ako poništiš objavu, stavke u njoj postaju biblioteka ovih datoteka." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "Jesi li siguran/na da želiš poništiti objavu ove biblioteke?" msgstr[1] "Jesi li siguran/na da želiš poništiti objavu ovih biblioteka?" msgstr[2] "Jesi li siguran/na da želiš poništiti objavu ovih biblioteka?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Koristi se u ovoj datoteci:" -msgstr[1] "Koristi se u ovim datotekama:" -msgstr[2] "Koristi se u ovim datotekama:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" msgid_plural "modals.unpublish-shared-confirm.title" msgstr[0] "Poništi objavu biblioteke" msgstr[1] "Poništi objavu biblioteka" msgstr[2] "Poništi objavu biblioteka" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.hint" msgstr "" "Upravo ćeš ažurirati komponente u zajedničkoj biblioteci. To može utjecati " "na druge datoteke koje ga koriste." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.message" msgstr "Ažuriraj komponente u zajedničkoj biblioteci" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.accept" msgstr "Ažuriraj" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.cancel" msgstr "Poništi" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.hint" msgstr "" "Upravo ćeš ažurirati komponentu u zajedničkoj biblioteci. To može utjecati " "na druge datoteke koje ga koriste." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.message" msgstr "Ažuriraj komponentu u zajedničkoj biblioteci" @@ -1859,12 +1887,6 @@ msgstr "Vodič za doprinos" msgid "onboarding-v2.welcome.title" msgstr "Dobrodošli u Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "Stvori tim kasnije" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Ime tvojeg tima" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "Nakon što imenuješ svoj tim, moći ćeš pozvati ljude da se pridruže." @@ -1879,12 +1901,6 @@ msgstr "" "Ne zaboravi uključiti sve. Programere, dizajnere, menadžere... raznolikost " "se isplati :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Stvori tim i pozovi kasnije" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Stvori tim i pošalji pozivnice" - msgid "onboarding.choice.team-up.roles" msgstr "Pozovi s ulogom:" @@ -1900,9 +1916,6 @@ msgstr "Politika privatnosti." msgid "onboarding.newsletter.title" msgstr "Želiš primati Penpot novostii?" -msgid "onboarding.slide.1.title" -msgstr "Oživi vlastite dizajne interakcijama" - msgid "onboarding.team-modal.create-team" msgstr "Kreiraj tim" @@ -1939,7 +1952,12 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "Idi na prijavu" -#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "Izmješano" @@ -2315,10 +2333,6 @@ msgstr "Promijena fokus moda" msgid "shortcuts.toggle-fullscreen" msgstr "Promijeni cijeli zaslon" -#, fuzzy -msgid "shortcuts.toggle-grid" -msgstr "Promijena \"grida\"" - msgid "shortcuts.toggle-history" msgstr "Promijena povijesti" @@ -2335,20 +2349,12 @@ msgstr "Zaključaj proporcije" msgid "shortcuts.toggle-rules" msgstr "Prikaži/sakrij \"rules\"" -msgid "shortcuts.toggle-scale-text" -msgstr "Promjena mjerila teksta" - -#, fuzzy -msgid "shortcuts.toggle-snap-grid" -msgstr "Poravnanje s \"gridom\"" - -#, fuzzy -msgid "shortcuts.toggle-snap-guide" -msgstr "Pričvrsti na guides" - msgid "shortcuts.toggle-textpalette" msgstr "Promijeni paletu teksta" +msgid "shortcuts.toggle-visibility" +msgstr "Promijeni vidljivost" + msgid "shortcuts.toggle-zoom-style" msgstr "Promijeni stil zooma" @@ -2522,11 +2528,13 @@ msgstr "Stavke" msgid "workspace.assets.box-filter-all" msgstr "Sve stavke" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.colors" msgstr "Boje" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.components" msgstr "Komponente" @@ -2538,19 +2546,24 @@ msgstr "Kreiraj grupu" msgid "workspace.assets.create-group-hint" msgstr "Tvoje stavke će se automatski imenovati kao \"naziv grupe / naziv stavke\"" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "Izbriši" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.duplicate" msgstr "Dupliciraj" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" msgstr "Uredi" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.graphics" msgstr "Grafika" @@ -2573,7 +2586,9 @@ msgstr "lokalna biblioteka" msgid "workspace.assets.not-found" msgstr "Nisu pronađene stavke" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename" msgstr "Preimenuj" @@ -2592,11 +2607,8 @@ msgstr[0] "%s odabrana stavka" msgstr[1] "%s odabranih stavki" msgstr[2] "%s odabranih stavki" +#: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "PODIJELJENO" - -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "Tipografija" @@ -2624,7 +2636,9 @@ msgstr "Razmak između slova" msgid "workspace.assets.typography.line-height" msgstr "Visina linije" -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/inspect/attributes/text.cljs, src/app/main/ui/inspect/attributes/text.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs msgid "workspace.assets.typography.sample" msgstr "Ag" @@ -2648,11 +2662,13 @@ msgstr "Fokus uključen" msgid "workspace.focus.selection" msgstr "Odabir" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "Linearni gradijent" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.radial" msgstr "Radijalni gradijent" @@ -2664,10 +2680,6 @@ msgstr "Onemogući dinamičko poravnanje" msgid "workspace.header.menu.disable-scale-text" msgstr "Onemogući skaliranje teksta" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Onemogući \"snap to grid\"" - #: src/app/main/ui/workspace/header.cljs #, fuzzy msgid "workspace.header.menu.disable-snap-guides" @@ -2685,10 +2697,6 @@ msgstr "Omogući dinamičko poravnanje" msgid "workspace.header.menu.enable-scale-text" msgstr "Omogući skaliranje teksta" -#: src/app/main/ui/workspace/header.cljs -#, fuzzy -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Poravnanje s \"gridom\"" #: src/app/main/ui/workspace/header.cljs #, fuzzy @@ -2703,11 +2711,6 @@ msgstr "Omogući \"snap to pixel\"" msgid "workspace.header.menu.hide-artboard-names" msgstr "Sakrij nazive ploča" -#: src/app/main/ui/workspace/header.cljs -#, fuzzy -msgid "workspace.header.menu.hide-grid" -msgstr "Sakrij \"grid\"" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Sakrij paletu boja" @@ -2752,10 +2755,6 @@ msgstr "Odaberi sve" msgid "workspace.header.menu.show-artboard-names" msgstr "Prikaži nazive ploča" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Prikaži \"grid\"" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Prikaži paletu boja" @@ -2825,7 +2824,8 @@ msgstr "Dodaj" msgid "workspace.libraries.colors" msgstr "%s boje" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" msgstr "Biblioteka datoteka" @@ -2833,7 +2833,8 @@ msgstr "Biblioteka datoteka" msgid "workspace.libraries.colors.hsv" msgstr "HSV" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "Nedavno korištene boje" @@ -2984,15 +2985,18 @@ msgstr "Vrh i dno" msgid "workspace.options.design" msgstr "Dizajn" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export" msgstr "Izvoz" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-multiple" msgstr "Izvezi selektirano" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-object" msgid_plural "workspace.options.export-object" msgstr[0] "Izvezi 1 element" @@ -3003,19 +3007,23 @@ msgstr[2] "Izvezi %s elemenata" msgid "workspace.options.export.suffix" msgstr "Sufiks" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-complete" msgstr "Izvoz završen" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object" msgstr "Izvoz…" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-error" msgstr "Izvoz nije uspio" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-slow" msgstr "Izvoz neočekivano spor" @@ -3540,7 +3548,8 @@ msgstr "Više boja iz biblioteke" msgid "workspace.options.opacity" msgstr "Neprozirnost" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Pozicija" @@ -3552,13 +3561,12 @@ msgid "workspace.options.radius" msgstr "Radius" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Svi kutevi" +msgid "workspace.options.radius-bottom-left" +msgstr "Dolje lijevo" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -#, fuzzy -msgid "workspace.options.radius.single-corners" -msgstr "Jednostruki kutovi" +msgid "workspace.options.radius-bottom-right" +msgstr "Dolje desno" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3569,17 +3577,19 @@ msgid "workspace.options.radius-top-right" msgstr "Gore desno" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Dolje lijevo" +msgid "workspace.options.radius.all-corners" +msgstr "Svi kutevi" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Dolje desno" +#, fuzzy +msgid "workspace.options.radius.single-corners" +msgstr "Jednostruki kutovi" msgid "workspace.options.recent-fonts" msgstr "Nedavni" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" msgstr "Pokušaj ponovo" @@ -3653,7 +3663,8 @@ msgstr "Prikaži u izvozu" msgid "workspace.options.show-in-viewer" msgstr "Prikaži u načinu pregleda" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.size" msgstr "Veličina" @@ -3737,27 +3748,10 @@ msgstr "Čvrsto" msgid "workspace.options.text-options.align-bottom" msgstr "Poravnaj dno" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Poravnaj sredinu (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -#, fuzzy -msgid "workspace.options.text-options.text-align-justify" -msgstr "Složi u blok (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Poravnaj lijevo (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Poravnaj sredinu" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Poravnaj desno (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Poravnaj vrh" @@ -3795,7 +3789,8 @@ msgstr "Visina linije" msgid "workspace.options.text-options.lowercase" msgstr "Mala slova" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.none" msgstr "Nijedan" @@ -3803,6 +3798,23 @@ msgstr "Nijedan" msgid "workspace.options.text-options.strikethrough" msgstr "Precrtanko (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Poravnaj sredinu (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +#, fuzzy +msgid "workspace.options.text-options.text-align-justify" +msgstr "Složi u blok (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Poravnaj lijevo (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Poravnaj desno (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Tekst" @@ -3906,11 +3918,15 @@ msgstr "Izbriši" msgid "workspace.shape.menu.delete-flow-start" msgstr "Izbriši početak flowa" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Odvoji instancu" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instances-in-bulk" msgstr "Odvoji instance" @@ -3954,7 +3970,8 @@ msgstr "Postavi ispred" msgid "workspace.shape.menu.front" msgstr "Postavi naprijed" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.go-main" msgstr "Idi na datoteku glavne komponente" @@ -3976,11 +3993,13 @@ msgstr "Presjek" msgid "workspace.shape.menu.lock" msgstr "Zaključaj" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.mask" msgstr "Maskiraj" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.paste" msgstr "Zalijepi" @@ -3988,7 +4007,9 @@ msgstr "Zalijepi" msgid "workspace.shape.menu.path" msgstr "Path" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs #, fuzzy msgid "workspace.shape.menu.reset-overrides" msgstr "Poništi overrides" @@ -4004,7 +4025,8 @@ msgstr "Označi layer" msgid "workspace.shape.menu.show" msgstr "Prikaži" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-main" msgstr "Prikaži glavnu komponentu" @@ -4033,11 +4055,15 @@ msgstr "Otključaj" msgid "workspace.shape.menu.unmask" msgstr "Ukloni masku" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-components-in-bulk" msgstr "Ažuriraj glavne komponente" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-main" msgstr "Ažuriraj glavnu komponentu" @@ -4075,7 +4101,8 @@ msgstr "Oblici" msgid "workspace.sidebar.layers.texts" msgstr "Tekstovi" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/inspect/attributes/svg.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, +#: src/app/main/ui/inspect/attributes/svg.cljs msgid "workspace.sidebar.options.svg-attrs.title" msgstr "Uvezeni SVG atributi" diff --git a/frontend/translations/id.po b/frontend/translations/id.po index 3d7d8deb69..037737bdd3 100644 --- a/frontend/translations/id.po +++ b/frontend/translations/id.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2023-07-02 17:52+0000\n" -"Last-Translator: Linerly \n" +"PO-Revision-Date: 2023-10-07 12:12+0000\n" +"Last-Translator: Linerly \n" "Language-Team: Indonesian \n" "Language: id\n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 5.0-dev\n" +"X-Generator: Weblate 5.1-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -39,7 +39,8 @@ msgstr "" "Ini layanan DEMO, JANGAN GUNAKAN untuk pekerjaan nyata, proyek-proyek ini " "akan di hapus secara berkala." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" msgstr "Surel" @@ -83,6 +84,14 @@ msgstr "LDAP" msgid "auth.login-with-oidc-submit" msgstr "OpenID Connect" +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "Nama harus berisi beberapa karakter selain spasi." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "Nama harus berisi setidaknya 250 karakter." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.new-password" msgstr "Ketik kata sandi baru" @@ -115,6 +124,10 @@ msgstr "Kata sandi" msgid "auth.password-length-hint" msgstr "Setidaknya 8 karakter" +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "Kata sandi harus berisi beberapa karakter selain spasi." + msgid "auth.privacy-policy" msgstr "Kebijakan privasi" @@ -167,6 +180,10 @@ msgstr "" msgid "auth.verification-email-sent" msgstr "Kami telah mengirimkan surel verifikasi ke" +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "...pencitraan merek, ilustrasi, bagian pemasaran, dll." + msgid "common.publish" msgstr "Terbitkan" @@ -264,7 +281,83 @@ msgstr "Mulai tur" msgid "dasboard.walkthrough-hero.title" msgstr "Panduan Antarmuka" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "Token disalin" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "Buat token baru" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "Token akses berhasil dibuat." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "Tekan tombol \"Buat token baru\" untuk membuat token." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "Anda belum memiliki token." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "Nama diperlukan" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 hari" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 hari" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 hari" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 hari" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "Tidak pernah" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "Telah kedaluwarsa pada %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "Kedaluwarsa pada %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "Tidak ada tanggal kedaluwarsa" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Token akses pribadi" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Token akses pribadi berfungsi sebagai alternatif sistem autentikasi nama " +"pengguna dan kata sandi dan dapat digunakan untuk memperbolehkan sebuah " +"aplikasi untuk mengakses API Penpot internal" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "Token akan kedaluwarsa pada %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "Token tidak memiliki tanggal kedaluwarsa" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Tambahkan sebagai Pustaka Bersama" @@ -294,7 +387,8 @@ msgstr "Unduh berkas Penpot (.penpot)" msgid "dashboard.download-standard-file" msgstr "Unduh berkas standar (.svg + .json)" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate" msgstr "Duplikasi" @@ -303,7 +397,6 @@ msgid "dashboard.duplicate-multi" msgstr "Gandakan % berkas" #: src/app/main/ui/dashboard/grid.cljs -#, markdown msgid "dashboard.empty-placeholder-drafts" msgstr "" "Berkas yang ditambahkan ke Pustaka akan muncul di sini. Coba membagikan " @@ -400,7 +493,6 @@ msgid "dashboard.fonts.fonts-added" msgid_plural "dashboard.fonts.fonts-added" msgstr[0] "%s fon ditambahkan" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "Fon web apa pun yang Anda unggah di sini akan ditambahkan ke daftar " @@ -409,7 +501,6 @@ msgstr "" "fon tunggal**. Anda dapat mengunggah fon dengan format berikut: **TTF, OTF, " "dan WOFF** (hanya satu yang diperlukan)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "Anda seharusnya hanya mengunggah fon yang Anda miliki atau memiliki izin " @@ -422,6 +513,15 @@ msgstr "" msgid "dashboard.fonts.upload-all" msgstr "Unggah semua" +msgid "dashboard.fonts.warning-text" +msgstr "" +"Kami telah mendeteksi masalah yang mungkin ada dalam fon Anda terkair " +"dengan metrik vertikal untuk berbagai sistem operasi. Supaya bisa " +"diperiksa, Anda dapat menggunakan layanan metrik vertikal fon seperti [yang " +"ini](https://vertical-metrics.netlify.app/). Sebagai tambahan, kami juga " +"menyarankan menggunakan [Transfonter](https://transfonter.org/) untuk " +"membuat fon web dan memperbaiki kesalahan. " + msgid "dashboard.import" msgstr "Impor berkas Penpot" @@ -431,6 +531,9 @@ msgstr "Aduh! Kami tidak dapat mengimpor berkas ini" msgid "dashboard.import.import-error" msgstr "Terdapat masalah saat mengimpor berkas. Berkasnya tidak terimpor." +msgid "dashboard.import.import-message" +msgstr "%s berkas telah berhasil diimpor." + msgid "dashboard.import.import-warning" msgstr "Beberapa berkas berisi objek yang tidak valid yang telah dihapus." @@ -459,7 +562,8 @@ msgstr "Mengunggah berkas: %s" msgid "dashboard.invite-profile" msgstr "Undang orang" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" msgstr "Tinggalkan tim" @@ -483,7 +587,8 @@ msgstr "memuat berkas Anda …" msgid "dashboard.loading-fonts" msgstr "memuat fon Anda …" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "Pindahkan ke" @@ -495,7 +600,8 @@ msgstr "Pindahkan %s berkas ke" msgid "dashboard.move-to-other-team" msgstr "Pindahkan ke tim lain" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" msgstr "+ Buat Berkas" @@ -558,7 +664,8 @@ msgstr "Proyek" msgid "dashboard.remove-account" msgstr "Ingin menghapus akun Anda?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" msgstr "Hapus sebagai Pustaka Terbagi" @@ -586,15 +693,26 @@ msgstr "Pilih tema" msgid "dashboard.show-all-files" msgstr "Tampilkan semua berkas" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "Berkas Anda berhasil dihapus" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-delete-project" msgstr "Proyek Anda berhasil dihapus" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "Berkas Anda berhasil digandakan" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-duplicate-project" msgstr "Proyek Anda berhasil digandakan" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-file" msgstr "Berkas Anda berhasil dipindah" @@ -630,11 +748,13 @@ msgstr "Hasil pencarian" msgid "dashboard.type-something" msgstr "Ketik untuk mencari hasil" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.unpublish-shared" msgstr "Batal Penerbitan Pustaka" -#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Perbarui pengaturan" @@ -681,7 +801,11 @@ msgstr "Surel" msgid "dashboard.your-name" msgstr "Nama Anda" -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" msgstr "Penpot Anda" @@ -722,11 +846,15 @@ msgstr "Fon %s tidak dapat dimuat" msgid "errors.bad-font-plural" msgstr "Fon %s tidak dapat dimuat" +msgid "errors.cannot-upload" +msgstr "Tidak dapat mengunggah berkas media." + #: src/app/main/data/workspace.cljs msgid "errors.clipboard-not-implemented" msgstr "Peramban Anda tidak dapat melakukan operasi ini" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/change_email.cljs msgid "errors.email-already-exists" msgstr "Surel sudah digunakan" @@ -737,11 +865,15 @@ msgstr "Surel sudah divalidasi." msgid "errors.email-as-password" msgstr "Anda tidak dapat menggunakan surel Anda sebagai kata sandi" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" msgstr "Surel “%s” memiliki banyak laporan lompatan permanen." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, +#: src/app/main/ui/auth/recovery_request.cljs msgid "errors.email-invalid" msgstr "Silakan menyediakan surel yang valid" @@ -762,7 +894,8 @@ msgstr "" msgid "errors.feature-not-supported" msgstr "Fitur '%s' tidak didukung." -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.generic" msgstr "Sesuatu yang salah terjadi." @@ -793,7 +926,7 @@ msgstr "Gambar terlalu besar untuk disematkan." msgid "errors.media-type-mismatch" msgstr "Serpertinya konten gambar tidak cocok dengan ekstensi berkas." -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" msgstr "Sepertinya ini bukan gambar yang valid." @@ -814,7 +947,9 @@ msgstr "Kata sandi setidaknya 8 karakter" msgid "errors.profile-blocked" msgstr "Profil diblokir" -#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.profile-is-muted" msgstr "Profil Anda membisukan surel (laporan spam atau lompatan tinggi)." @@ -835,7 +970,9 @@ msgstr "" "Pemilik tidak dapat meninggalkan tim, Anda harus memberikan ulang peran " "pemilik." -#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +#: src/app/main/data/media.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs msgid "errors.unexpected-error" msgstr "Sebuah kesalahan tidak terduga terjadi." @@ -907,7 +1044,7 @@ msgstr "Surel" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Pergi ke Twitter" +msgstr "Pergi ke X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -915,7 +1052,7 @@ msgstr "Di sini untuk membantu dengan kueri teknis Anda." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Akun dukungan Twitter" +msgstr "Akun dukungan X" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -969,7 +1106,8 @@ msgstr "Tinggi" msgid "inspect.attributes.layout.left" msgstr "Kiri" -#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs +#: src/app/main/ui/inspect/attributes/layout.cljs, +#: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.layout.radius" msgstr "Radius" @@ -997,15 +1135,12 @@ msgstr "Ukuran dan posisi" msgid "inspect.attributes.stroke" msgstr "Sapuan" -#, permanent msgid "inspect.attributes.stroke.alignment.center" msgstr "Tengah" -#, permanent msgid "inspect.attributes.stroke.alignment.inner" msgstr "Dalam" -#, permanent msgid "inspect.attributes.stroke.alignment.outer" msgstr "Luar" @@ -1041,6 +1176,10 @@ msgstr "Ukuran Fon" msgid "inspect.attributes.typography.font-style" msgstr "Gaya Fon" +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-weight" +msgstr "Berat Fon" + #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.letter-spacing" msgstr "Spasi Huruf" @@ -1141,13 +1280,17 @@ msgstr "Pintasan" msgid "labels.accept" msgstr "Terima" +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "Token akses" + msgid "labels.active" msgstr "Aktif" msgid "labels.add-custom-font" msgstr "Tambahkan fon khusus" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "Admin" @@ -1207,7 +1350,8 @@ msgstr "Salin tautan" msgid "labels.create" msgstr "Buat" -#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team" msgstr "Buat tim baru" @@ -1222,7 +1366,8 @@ msgstr "Fon khusus" msgid "labels.dashboard" msgstr "Dasbor" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "Hapus" @@ -1242,7 +1387,13 @@ msgstr "Hapus undangan" msgid "labels.delete-multi-files" msgstr "Hapus %s berkas" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.discard" +msgstr "Abaikan" + +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/files.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.drafts" msgstr "Draf" @@ -1253,7 +1404,7 @@ msgstr "Sunting" msgid "labels.edit-file" msgstr "Sunting berkas" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "Penyunting" @@ -1288,7 +1439,9 @@ msgstr "Fon" msgid "labels.github-repo" msgstr "Repositori GitHub" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "Berikan masukan" @@ -1319,7 +1472,8 @@ msgstr "" msgid "labels.internal-error.main-message" msgstr "Kesalahan Internal" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.invitations" msgstr "Undangan" @@ -1338,11 +1492,11 @@ msgstr "Masuk atau daftar" msgid "labels.logout" msgstr "Keluar" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.member" msgstr "Anggota" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "Anggota" @@ -1350,7 +1504,8 @@ msgstr "Anggota" msgid "labels.new-password" msgstr "Kata sandi baru" -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +#: src/app/main/ui/workspace/comments.cljs, +#: src/app/main/ui/dashboard/comments.cljs msgid "labels.no-comments-available" msgstr "Anda telah melihat semuanya! Notifikasi komentar baru akan muncul di sini." @@ -1359,7 +1514,6 @@ msgid "labels.no-invitations" msgstr "Tidak ada undangan yang menunggu." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "Tekan tombol **Undang orang** untuk mengundang orang-orang ke tim ini." @@ -1402,7 +1556,8 @@ msgstr "atau" msgid "labels.owner" msgstr "Pemilik" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.password" msgstr "Kata sandi" @@ -1426,7 +1581,8 @@ msgstr "Catatan rilis" msgid "labels.reload-file" msgstr "Muat ulang berkas" -#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/workspace/libraries.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" msgstr "Hapus" @@ -1434,7 +1590,9 @@ msgstr "Hapus" msgid "labels.remove-member" msgstr "Keluarkan anggota" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.rename" msgstr "Ubah nama" @@ -1446,7 +1604,7 @@ msgstr "Ubah nama tim" msgid "labels.resend-invitation" msgstr "Kirim ulang undangan" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Coba lagi" @@ -1476,7 +1634,8 @@ msgstr "Kami dalam pemeliharaan yang telah diprogram untuk sistem kami." msgid "labels.service-unavailable.main-message" msgstr "Layanan Tidak Tersedia" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.settings" msgstr "Pengaturan" @@ -1541,7 +1700,7 @@ msgstr "Kaitan web" msgid "labels.write-new-comment" msgstr "Tulis komentar baru" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.you" msgstr "(Anda)" @@ -1549,21 +1708,24 @@ msgstr "(Anda)" msgid "labels.your-account" msgstr "Akun Anda" -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "Memuat gambar…" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" msgstr "Tambahkan sebagai Pustaka Terbagi" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.hint" msgstr "" "Ketika ditambahkan sebagai Pustaka Terbagi, aset dari pustaka berkas ini " "akan tersedia untuk digunakan di antara berkas Anda yang lain." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.message" msgstr "Tambahkan “%s” sebagai Pustaka Terbagi" @@ -1593,6 +1755,30 @@ msgstr "Ubah surel" msgid "modals.change-email.title" msgstr "Ubah surel Anda" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "Salin token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "Tanggal kedaluwarsa" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "Nama" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "Namanya dapat mengetahui kegunaan tokennya" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "Buat token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "Buat token baru" + msgid "modals.create-webhook.submit-label" msgstr "Buat kaitan web" @@ -1605,6 +1791,18 @@ msgstr "URL Muatan" msgid "modals.create-webhook.url.placeholder" msgstr "https://contoh.co.id/terimapos" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "Hapus token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "Apakah Anda ingin menghapus token ini?" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "Hapus token" + #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.cancel" msgstr "Batalkan dan jaga akun saya" @@ -1637,6 +1835,12 @@ msgstr "" msgid "modals.delete-comment-thread.title" msgstr "Hapus percakapan" +msgid "modals.delete-component-annotation.message" +msgstr "Apakah Anda yakin ingin menghapus anotasi ini?" + +msgid "modals.delete-component-annotation.title" +msgstr "Haus anotasi" + #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-confirm.accept" msgstr "Hapus berkas" @@ -1697,50 +1901,30 @@ msgstr "Apakah Anda yakin ingin menghapus proyek ini?" msgid "modals.delete-project-confirm.title" msgstr "Hapus proyek" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.accept" msgid_plural "modals.delete-shared-confirm.accept" msgstr[0] "Hapus berkas" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "" -"Jika Anda menghapusnya, asetnya tidak akan tersedia lagi dari berkas yang " -"lain. Aset yang telah digunakan akan tetap dalam berkas ini (tidak ada " -"desain yang akan rusak!)." +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "Tidak diaktifkan dalam berkas mana pun." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "" -"Jika Anda menghapusnya, asetnya tidak akan tersedia lagi dari berkas yang " -"lain. Aset yang telah digunakan akan tetap dalam berkas ini (tidak ada " -"desain yang akan rusak!)." +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "Pustaka ini diaktifkan di sini: " -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "Apakah Anda yakin ingin menghapus berkas ini?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" -msgstr[0] "" -"Tidak ada aset berada di pustaka berkas yang digunakan. Mereka akan dihapus " -"termasuk berkasnya." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Beberapa aset di pustaka berkas ini digunakan di sini:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "Beberapa aset di pustaka berkas ini digunakan di sini:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" msgstr[0] "Menghapus berkas" @@ -1771,6 +1955,13 @@ msgstr "Apakah Anda yakin ingin mengeluarkan anggota ini dari tim?" msgid "modals.delete-team-member-confirm.title" msgstr "Keluarkan anggota tim" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" +msgstr[0] "" +"Aset yang telah digunakan dalam berkas akan tetap di sana (tidak ada desain " +"yang akan rusak)." + msgid "modals.delete-webhook.accept" msgstr "Hapus kaitan web" @@ -1793,6 +1984,11 @@ msgstr "Kirim undangan" msgid "modals.invite-member.emails" msgstr "Surel, dipisah dengan koma" +msgid "modals.invite-member.repeated-invitation" +msgstr "" +"Beberapa surel berasal dari anggota tim saat ini. Undangan mereka tidak " +"akan dikirim." + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-team-member.title" msgstr "Undang anggota ke tim" @@ -1866,17 +2062,29 @@ msgstr "" msgid "modals.promote-owner-confirm.title" msgstr "Pemilik tim baru" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.publish-empty-library.accept" +msgstr "Terbitkan" + +msgid "modals.publish-empty-library.message" +msgstr "Pustaka Anda sedang kosong. Apakah Anda ingin menerbitkannya?" + +msgid "modals.publish-empty-library.title" +msgstr "Terbitkan pustaka kosong" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" msgstr "Hapus sebagai Pustaka Terbagi" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.hint" msgstr "" "Ketika dihapus sebagai Pustaka Terbagi, Pustaka Berkas dari berkas ini akan " "tidak lagi tersedia untuk digunakan di antara berkas Anda." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.message" msgstr "Hapus “%s” sebagai Pustaka Terbagi" @@ -1884,75 +2092,62 @@ msgstr "Hapus “%s” sebagai Pustaka Terbagi" msgid "modals.small-nudge" msgstr "Dorongan kecil" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Jika Anda membatalkan penerbitannya, asetnya tidak akan tersedia lagi dari " -"berkas yang lain. Aset yang telah digunakan akan tetap dalam berkas ini (" -"tidak ada desain yang akan rusak!)." +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.accept" +msgid_plural "modals.unpublish-shared-confirm.accept" +msgstr[0] "Batalkan penerbitan" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "" -"Jika Anda membatalkan penerbitannya, asetnya tidak akan tersedia lagi dari " -"berkas yang lain. Aset yang telah digunakan akan tetap dalam berkas ini (" -"tidak ada desain yang akan rusak!)." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "Apakah Anda yakin ingin membatalkan penerbitan pustaka ini?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "Tidak ada aset berada di pustaka ini yang digunakan." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Beberapa aset dalam pustaka ini digunakan di sini:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "Beberapa aset dalam pustaka ini digunakan di sini:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" msgid_plural "modals.unpublish-shared-confirm.title" msgstr[0] "Batalkan penerbitan pustaka" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.hint" msgstr "" "Anda akan memperbarui komponen dalam pustaka terbagi. Ini mungkin " "memengaruhi berkas lain yang menggunakannya." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.message" msgstr "Perbarui komponen dalam pustaka terbagi" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.accept" msgstr "Perbarui" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.cancel" msgstr "Batal" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.hint" msgstr "" "Anda akan memperbarui sebuah komponen dalam sebuah pustaka terbagi. Ini " "mungkin memengaruhi berkas lain yang menggunakannya." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.message" msgstr "Perbarui sebuah komponen dalam sebuah pustaka terbagi" +#: src/app/main/data/common.cljs +msgid "notifications.by-code.upgrade-version" +msgstr "Versi baru sudah tersedia, silakan muat ulang laman" + #: src/app/main/ui/dashboard/team.cljs msgid "notifications.invitation-email-sent" msgstr "Undangan berhasil dikirim" @@ -2045,12 +2240,6 @@ msgstr "Panduan berkontribusi" msgid "onboarding-v2.welcome.title" msgstr "Selamat datang di Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "Buat sebuah tim nanti" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Nama tim Anda" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "" "Setelah memberi nama tim, Anda akan dapat mengundang orang-orang untuk " @@ -2067,12 +2256,6 @@ msgstr "" "Pastikan untuk menyertakan semuanya. Pengembang, pendesain, pengelola... " "keragaman bertambah :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Buat tim dan undang nanti" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Buat tim dang kirim undangan" - msgid "onboarding.choice.team-up.roles" msgstr "Undang dengan peran:" @@ -2126,7 +2309,178 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "Pergi ke log masuk" -#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "Alat desain manakah yang lebih Anda kuasai?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11–30" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2–10" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31–50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "Banyak" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canva" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.describe-your-experience-working-on" +msgstr "Bagaimana cara terbaik Anda menggambarkan pengalaman Anda mengerjakan..." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "Perancang" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "Pengembang" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "Jelajahi lebih lanjut tentang Penpot" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "Pendiri/VP" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "Saya seorang pekerja lepas" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "Dapatkan kode dari proyek tim saya " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "... desain antarmuka, aset visual, sistem desain, dll." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "InVision" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "Tinggalkan masukan untuk proyek tim saya" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "Mari kita mulai!" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "Pengelola Produk atau Proyek" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "Pemasaran" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "Lebih dari 50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "Berikutnya" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "Tidak ada" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "Lainnya (jelaskan)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "Saya mengerjakan proyek pribadi" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "Sebelumnya" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "Apa rencana Anda menggunakan Penpot?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "Apa peran Anda?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.select-option" +msgstr "Pilih opsi" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "Sketch" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.some" +msgstr "Beberapa" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "Mulai" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "Mulai bekerja pada proyek saya" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "Siswa atau Guru" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "Seberapa besar tim Anda?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "Coba Penpot untuk tim Anda " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "Coba sebelum menggunakan Penpot on-premise" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" +msgstr "... gambar rangka, perjalanan & alur pengguna, pohon navigasi, dll." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "Bekerja dalam ide konsep" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" +msgstr "" +"Masukan Anda akan membantu kami mengerti kebiasaan dan preferensi Anda " +"supaya kami dapat membuat Penpot sebuah alat yang berguna dan nyaman." + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "Tercampur" @@ -2180,6 +2534,9 @@ msgstr "Jejak" msgid "shortcut-subsection.shape" msgstr "Bentuk" +msgid "shortcut-subsection.text-editor" +msgstr "Teks" + msgid "shortcut-subsection.tools" msgstr "Peralatan" @@ -2198,9 +2555,15 @@ msgstr "Tambahkan simpul" msgid "shortcuts.align-bottom" msgstr "Sesuaikan ke bawah" +msgid "shortcuts.align-center" +msgstr "Paskan ke tengah" + msgid "shortcuts.align-hcenter" msgstr "Sesuaikan ke tengah secara horizontal" +msgid "shortcuts.align-justify" +msgstr "Paskan secara rata" + msgid "shortcuts.align-left" msgstr "Sesuaikan ke kiri" @@ -2216,6 +2579,9 @@ msgstr "Sesuaikan ke tengah secara vertikal" msgid "shortcuts.artboard-selection" msgstr "Buat papan dari seleksi" +msgid "shortcuts.bold" +msgstr "Tebal" + msgid "shortcuts.bool-difference" msgstr "Perbedaan boolean" @@ -2306,6 +2672,12 @@ msgstr "Balikkan secara horizontal" msgid "shortcuts.flip-vertical" msgstr "Balikkan secara vertikal" +msgid "shortcuts.font-size-dec" +msgstr "Kurangi ukuran fon" + +msgid "shortcuts.font-size-inc" +msgstr "Tambahkan ukuran fon" + msgid "shortcuts.go-to-drafts" msgstr "Pergi ke draf" @@ -2330,9 +2702,27 @@ msgstr "Perbesar" msgid "shortcuts.insert-image" msgstr "Sematkan gambar" +msgid "shortcuts.italic" +msgstr "Miring" + msgid "shortcuts.join-nodes" msgstr "Gabungkan simpul" +msgid "shortcuts.letter-spacing-dec" +msgstr "Kurangi spasi huruf" + +msgid "shortcuts.letter-spacing-inc" +msgstr "Tambahkan spasi huruf" + +msgid "shortcuts.line-height-dec" +msgstr "Kurangi ketinggian baris" + +msgid "shortcuts.line-height-inc" +msgstr "Tambahkan ketinggian baris" + +msgid "shortcuts.line-through" +msgstr "Coret" + msgid "shortcuts.make-corner" msgstr "Buat sudut" @@ -2453,6 +2843,15 @@ msgstr "Cari pintasan" msgid "shortcuts.select-all" msgstr "Pilih semua" +msgid "shortcuts.select-next" +msgstr "Pilih lapisan berikutnya" + +msgid "shortcuts.select-parent-layer" +msgstr "Pilih lapisan induk" + +msgid "shortcuts.select-prev" +msgstr "Pilih lapisan sebelumnya" + msgid "shortcuts.separate-nodes" msgstr "Pisahkan simpul" @@ -2477,6 +2876,18 @@ msgstr "Mulai mengukur" msgid "shortcuts.stop-measure" msgstr "Berhenti mengukur" +msgid "shortcuts.text-align-center" +msgstr "Paskan ke tengah" + +msgid "shortcuts.text-align-justify" +msgstr "Paskan secara rata" + +msgid "shortcuts.text-align-left" +msgstr "Paskan ke kiri" + +msgid "shortcuts.text-align-right" +msgstr "Paskan ke kanan" + msgid "shortcuts.thumbnail-set" msgstr "Tetapkan gambar kecil" @@ -2499,9 +2910,6 @@ msgstr "Alih mode fokus" msgid "shortcuts.toggle-fullscreen" msgstr "Alih layar penuh" -msgid "shortcuts.toggle-grid" -msgstr "Tampilkan/sembunyikan kisi" - msgid "shortcuts.toggle-history" msgstr "Alih riwayat" @@ -2520,21 +2928,18 @@ msgstr "Kunci proporsi" msgid "shortcuts.toggle-rules" msgstr "Tampilkan/sembunyikan penggaris" -msgid "shortcuts.toggle-scale-text" -msgstr "Alih skala teks" - -msgid "shortcuts.toggle-snap-grid" -msgstr "Tancap ke kisi" - -msgid "shortcuts.toggle-snap-guide" -msgstr "Tancap ke pemandu" - msgid "shortcuts.toggle-textpalette" msgstr "Alih palet teks" +msgid "shortcuts.toggle-visibility" +msgstr "Alih keterlihatan" + msgid "shortcuts.toggle-zoom-style" msgstr "Alih gaya zum" +msgid "shortcuts.underline" +msgstr "Garis bawah" + msgid "shortcuts.undo" msgstr "Urungkan" @@ -2547,9 +2952,19 @@ msgstr "Lepaskan topeng" msgid "shortcuts.v-distribute" msgstr "Distribusikan secara vertikal" +msgid "shortcuts.zoom-lense-decrease" +msgstr "Kurangi lensa zum" + +msgid "shortcuts.zoom-lense-increase" +msgstr "Tambahkan lensa zum" + msgid "shortcuts.zoom-selected" msgstr "Zum ke terpilih" +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "Nama webhook berisi sampai 2048 karakter." + #: src/app/main/ui/dashboard/files.cljs msgid "title.dashboard.files" msgstr "%s - Penpot" @@ -2578,6 +2993,10 @@ msgstr "Pustaka Terbagi - %s - Penpot" msgid "title.default" msgstr "Penpot - Kebebasan Desain untuk Tim" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "Profil - Token akses" + #: src/app/main/ui/settings/feedback.cljs msgid "title.settings.feedback" msgstr "Berikan masukan - Penpot" @@ -2713,11 +3132,13 @@ msgstr "Aset" msgid "workspace.assets.box-filter-all" msgstr "Semua aset" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.colors" msgstr "Warna" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.components" msgstr "Komponen" @@ -2729,19 +3150,27 @@ msgstr "Buat sebuah kelompok" msgid "workspace.assets.create-group-hint" msgstr "Butir Anda akan dinamakan \"nama kelompok / nama butir\" secara otomatis" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "Hapus" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.duplicate" msgstr "Gandakan" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.duplicate-main" +msgstr "Gandakan utama" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" msgstr "Sunting" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.graphics" msgstr "Grafis" @@ -2764,7 +3193,12 @@ msgstr "pustaka lokal" msgid "workspace.assets.not-found" msgstr "Tidak ada aset yang ditemukan" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.open-library" +msgstr "Buka berkas pustaka" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename" msgstr "Ubah nama" @@ -2782,10 +3216,11 @@ msgid_plural "workspace.assets.selected-count" msgstr[0] "%s aset dipilih" #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "DIBAGIKAN" +msgid "workspace.assets.shared-library" +msgstr "Pustaka terbagi" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "Tipografi" @@ -2813,7 +3248,9 @@ msgstr "Spasi Huruf" msgid "workspace.assets.typography.line-height" msgstr "Ketinggian Garis" -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/handoff/attributes/text.cljs, src/app/main/ui/handoff/attributes/text.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/handoff/attributes/text.cljs, +#: src/app/main/ui/handoff/attributes/text.cljs msgid "workspace.assets.typography.sample" msgstr "Ag" @@ -2840,11 +3277,13 @@ msgstr "Fokus aktif" msgid "workspace.focus.selection" msgstr "Seleksi" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "Gradien linear" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.radial" msgstr "Gradien radial" @@ -2852,14 +3291,13 @@ msgstr "Gradien radial" msgid "workspace.header.menu.disable-dynamic-alignment" msgstr "Nonaktifkan penyesuaian dinamis" +msgid "workspace.header.menu.disable-scale-content" +msgstr "Nonaktifkan skala proporsional" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-scale-text" msgstr "Nonaktifkan skala teks" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Nonaktifkan tancapan ke kisi" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "Nonaktifkan tancapan ke pemandu" @@ -2871,14 +3309,13 @@ msgstr "Nonaktifkan tancapan ke piksel" msgid "workspace.header.menu.enable-dynamic-alignment" msgstr "Aktifkan penyesuaian dinamis" +msgid "workspace.header.menu.enable-scale-content" +msgstr "Aktifkan skala proporsional" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-scale-text" msgstr "Aktifkan skala teks" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Tancapkan ke kisi" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "Tancapkan ke pemandu" @@ -2890,10 +3327,6 @@ msgstr "Aktifkan tancapkan ke piksel" msgid "workspace.header.menu.hide-artboard-names" msgstr "Sembunyikan nama papan" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Sembunyikan kisi" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Sembunyikan palet warna" @@ -2929,6 +3362,9 @@ msgstr "Preferensi" msgid "workspace.header.menu.option.view" msgstr "Tampilan" +msgid "workspace.header.menu.redo" +msgstr "Ulangi" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.select-all" msgstr "Pilih semua" @@ -2937,10 +3373,6 @@ msgstr "Pilih semua" msgid "workspace.header.menu.show-artboard-names" msgstr "Tampilkan nama papan" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Tampilkan kisi" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Tampilkan palet warna" @@ -2956,6 +3388,9 @@ msgstr "Tampilkan penggaris" msgid "workspace.header.menu.show-textpalette" msgstr "Tampilkan palet fon" +msgid "workspace.header.menu.undo" +msgstr "Urungkan" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "Atur ulang" @@ -2980,6 +3415,10 @@ msgstr "Perubahan belum disimpan" msgid "workspace.header.viewer" msgstr "Mode penampilan (%s)" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "Zum" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-fill" msgstr "Penuhi - Ubah ukuran untuk memenuhi" @@ -3000,6 +3439,9 @@ msgstr "Layar penuh" msgid "workspace.header.zoom-selected" msgstr "Zum ke terpilih" +msgid "workspace.layout_grid.editor.title" +msgstr "Kisi penyuntingan" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.add" msgstr "Tambahkan" @@ -3008,7 +3450,16 @@ msgstr "Tambahkan" msgid "workspace.libraries.colors" msgstr "%s warna" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "Belum ada gaya warna dalam pustaka Anda" + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "Belum ada gaya tipografi dalam pustaka Anda" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" msgstr "Pustaka berkas" @@ -3016,7 +3467,8 @@ msgstr "Pustaka berkas" msgid "workspace.libraries.colors.hsv" msgstr "HSV" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "Warna terkini" @@ -3056,6 +3508,10 @@ msgstr "PUSTAKA" msgid "workspace.libraries.library" msgstr "PUSTAKA" +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "PEMBARUAN PUSTAKA" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.no-libraries-need-sync" msgstr "Tidak ada Pustaka Terbagi yang membutuhkan pembaruan" @@ -3092,6 +3548,10 @@ msgstr "%s tipografi" msgid "workspace.libraries.update" msgstr "Perbarui" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "lihat semua perubahan" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.updates" msgstr "PEMBARUAN" @@ -3123,6 +3583,15 @@ msgstr "Klip konten" msgid "workspace.options.component" msgstr "Komponen" +msgid "workspace.options.component.annotation" +msgstr "Anotasi" + +msgid "workspace.options.component.create-annotation" +msgstr "Buat anotasi" + +msgid "workspace.options.component.edit-annotation" +msgstr "Sunting anotasi" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints" msgstr "Pasangan" @@ -3167,15 +3636,18 @@ msgstr "Atas & Bawah" msgid "workspace.options.design" msgstr "Desain" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs msgid "workspace.options.export" msgstr "Ekspor" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs msgid "workspace.options.export-multiple" msgstr "Ekspor seleksi" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs msgid "workspace.options.export-object" msgid_plural "workspace.options.export-object" msgstr[0] "Ekspor %s elemen" @@ -3184,19 +3656,23 @@ msgstr[0] "Ekspor %s elemen" msgid "workspace.options.export.suffix" msgstr "Akhiran" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-complete" msgstr "Pengeksporan selesai" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object" msgstr "Mengekspor…" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-error" msgstr "Pengeksporan gagal" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-slow" msgstr "Pengeksporan secara tidak terduga lambat" @@ -3652,10 +4128,18 @@ msgstr "Bawah" msgid "workspace.options.layout.direction.column" msgstr "Kolom" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column-reverse" +msgstr "Kolom terbalik" + #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.direction.row" msgstr "Barisan" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row-reverse" +msgstr "Baris terbalik" + #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.gap" msgstr "Celah" @@ -3719,7 +4203,8 @@ msgstr "Lebih banyak warna pustaka" msgid "workspace.options.opacity" msgstr "Opasitas" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Posisi" @@ -3731,12 +4216,12 @@ msgid "workspace.options.radius" msgstr "Radius" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Semua sudut" +msgid "workspace.options.radius-bottom-left" +msgstr "Kiri bawah" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Sudut tersendiri" +msgid "workspace.options.radius-bottom-right" +msgstr "Kanan bawah" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3747,17 +4232,18 @@ msgid "workspace.options.radius-top-right" msgstr "Kanan atas" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Kiri bawah" +msgid "workspace.options.radius.all-corners" +msgstr "Semua sudut" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Kanan bawah" +msgid "workspace.options.radius.single-corners" +msgstr "Sudut tersendiri" msgid "workspace.options.recent-fonts" msgstr "Terkini" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" msgstr "Coba lagi" @@ -3830,7 +4316,8 @@ msgstr "Tampilkan dalam ekspor" msgid "workspace.options.show-in-viewer" msgstr "Tampilkan dalam mode penampil" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.size" msgstr "Ukuran" @@ -3912,26 +4399,10 @@ msgstr "Padat" msgid "workspace.options.text-options.align-bottom" msgstr "Paskan ke bawah" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Paskan ke tengah (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Rata Kiri Kanan (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Paskan ke kiri (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Paskan ke tengah" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Paskan ke kanan (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Paskan ke atas" @@ -3968,7 +4439,8 @@ msgstr "Tinggi garis" msgid "workspace.options.text-options.lowercase" msgstr "Huruf kecil" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.none" msgstr "Tidak ada" @@ -3976,6 +4448,22 @@ msgstr "Tidak ada" msgid "workspace.options.text-options.strikethrough" msgstr "Coret (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Paskan ke tengah (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Rata Kiri Kanan (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Paskan ke kiri (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Paskan ke kanan (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Teks" @@ -4043,39 +4531,13 @@ msgstr "Simpul terpisah (%s)" msgid "workspace.path.actions.snap-nodes" msgstr "Tancap simpul (%s)" -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "" -"Untuk mencoba lagi, Anda dapat memuat ulang berkas ini. Jika masalah tetap " -"ada, kami menyarankan Anda untuk melihat daftar dan mempertimbangkan untuk " -"menghapus grafis yang rusak." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "Beberapa grafis tidak dapat diperbarui." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "Mengubah %s/%s" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "" -"Grafis Pustaka itu Komponen dari sekarang, yang akan membuatnya lebih " -"berdaya." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "Pembaruan ini adalah tindakan satu kali." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "Memperbarui %s..." - #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.add-flex" msgstr "Tambahkan tata letak flex" +msgid "workspace.shape.menu.add-grid" +msgstr "Tambahkan tata letak kisi" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "Kirim ke paling belakang" @@ -4088,6 +4550,9 @@ msgstr "Kirim ke belakang" msgid "workspace.shape.menu.copy" msgstr "Salin" +msgid "workspace.shape.menu.create-annotation" +msgstr "Buat anotasi" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.create-artboard-from-selection" msgstr "Seleksi ke papan" @@ -4096,6 +4561,9 @@ msgstr "Seleksi ke papan" msgid "workspace.shape.menu.create-component" msgstr "Buat komponen" +msgid "workspace.shape.menu.create-multiple-components" +msgstr "Buat beberapa komponen" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.cut" msgstr "Potong" @@ -4108,11 +4576,15 @@ msgstr "Hapus" msgid "workspace.shape.menu.delete-flow-start" msgstr "Hapus awalan alur" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Lepaskan bagian" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instances-in-bulk" msgstr "Lepaskan bagian" @@ -4153,7 +4625,8 @@ msgstr "Bawa ke depan" msgid "workspace.shape.menu.front" msgstr "Bawa ke paling depan" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.go-main" msgstr "Pergi ke berkas komponen utama" @@ -4175,11 +4648,13 @@ msgstr "Persimpangan" msgid "workspace.shape.menu.lock" msgstr "Kunci" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.mask" msgstr "Topeng" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.paste" msgstr "Tempelkan" @@ -4190,7 +4665,9 @@ msgstr "Jalur" msgid "workspace.shape.menu.remove-flex" msgstr "Hapus tata letak flex" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "Atur ulang timpaan" @@ -4205,11 +4682,13 @@ msgstr "Pilih lapisan" msgid "workspace.shape.menu.show" msgstr "Tampilkan" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-in-assets" msgstr "Tampilkan dalam panel aset" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-main" msgstr "Tampilkan komponen utama" @@ -4237,11 +4716,15 @@ msgstr "Buka kunci" msgid "workspace.shape.menu.unmask" msgstr "Buka topeng" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-components-in-bulk" msgstr "Perbarui komponen utama" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-main" msgstr "Perbarui komponen utama" @@ -4283,7 +4766,8 @@ msgstr "Bentuk" msgid "workspace.sidebar.layers.texts" msgstr "Teks" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, +#: src/app/main/ui/handoff/attributes/svg.cljs msgid "workspace.sidebar.options.svg-attrs.title" msgstr "Atribut SVG Diimpor" @@ -4466,6 +4950,10 @@ msgstr "Riwayat" msgid "workspace.updates.dismiss" msgstr "Abaikan" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "Info lebih lanjut" + #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.there-are-updates" msgstr "Ada pembaruan dalam pustaka terbagi" @@ -4477,110 +4965,38 @@ msgstr "Perbarui" msgid "workspace.viewport.click-to-close-path" msgstr "Klik untuk menutup jalur" -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-duplicate-file" -msgid_plural "dashboard.success-delete-file" -msgstr[0] "Berkas Anda berhasil digandakan" +msgid "workspace.options.component.copy" +msgstr "Salin" -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-delete-file" -msgid_plural "dashboard.success-delete-file" -msgstr[0] "Berkas Anda berhasil dihapus" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "Persegi panjang" -msgid "shortcuts.italic" -msgstr "Miring" +msgid "workspace.options.component.main" +msgstr "Utama" -msgid "shortcuts.letter-spacing-dec" -msgstr "Kurangi spasi huruf" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "Berlian" -msgid "shortcuts.letter-spacing-inc" -msgstr "Tambahkan spasi huruf" +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +msgid "settings.detach" +msgstr "Copot" -msgid "shortcuts.line-height-dec" -msgstr "Kurangi ketinggian baris" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "Segitiga" -msgid "shortcuts.line-height-inc" -msgstr "Tambahkan ketinggian baris" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "Tanda panah" -msgid "shortcuts.line-through" -msgstr "Coret" - -msgid "shortcuts.select-next" -msgstr "Pilih lapisan berikutnya" - -msgid "shortcuts.select-prev" -msgstr "Pilih lapisan sebelumnya" - -msgid "shortcuts.zoom-lense-decrease" -msgstr "Kurangi lensa zum" - -msgid "shortcuts.zoom-lense-increase" -msgstr "Tambahkan lensa zum" - -msgid "workspace.header.menu.disable-scale-content" -msgstr "Nonaktifkan skala proporsional" - -msgid "workspace.assets.duplicate-main" -msgstr "Gandakan utama" - -msgid "workspace.header.menu.redo" -msgstr "Ulangi" - -msgid "modals.invite-member.repeated-invitation" +msgid "modals.add-shared-confirm-empty.hint" msgstr "" -"Beberapa surel berasal dari anggota tim saat ini. Undangan mereka tidak akan " -"dikirim." +"Pustaka Anda saat ini kosong. Ketika ditambahkan sebagai Pustaka Terbagi, " +"aset yang Anda buat akan tersedia untuk digunakan bersama dengan berkas " +"Anda. Apakah Anda yakin ingin menerbitkannya?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.accept" -msgid_plural "modals.unpublish-shared-confirm.accept" -msgstr[0] "Batalkan penerbitan" - -msgid "shortcut-subsection.text-editor" -msgstr "Teks" - -msgid "shortcuts.align-center" -msgstr "Paskan ke tengah" - -msgid "shortcuts.align-justify" -msgstr "Paskan secara rata" - -msgid "shortcuts.bold" -msgstr "Tebal" - -msgid "shortcuts.font-size-inc" -msgstr "Tambahkan ukuran fon" - -msgid "shortcuts.underline" -msgstr "Garis bawah" - -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-weight" -msgstr "Berat Fon" - -#, markdown -msgid "dashboard.fonts.warning-text" -msgstr "" -"Kami telah mendeteksi masalah yang mungkin ada dalam fon Anda terkair dengan " -"metrik vertikal untuk berbagai sistem operasi. Supaya bisa diperiksa, Anda " -"dapat menggunakan layanan metrik vertikal fon seperti [yang ini](https" -"://vertical-metrics.netlify.app/). Sebagai tambahan, kami juga menyarankan " -"menggunakan [Transfonter](https://transfonter.org/) untuk membuat fon web " -"dan memperbaiki kesalahan. " - -msgid "shortcuts.font-size-dec" -msgstr "Kurangi ukuran fon" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.column-reverse" -msgstr "Kolom terbalik" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.row-reverse" -msgstr "Baris terbalik" - -msgid "workspace.header.menu.enable-scale-content" -msgstr "Aktifkan skala proporsional" - -msgid "workspace.header.menu.undo" -msgstr "Urungkan" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "Lingkaran" diff --git a/frontend/translations/it.po b/frontend/translations/it.po index 225a7aa423..e8ed1cd3d4 100644 --- a/frontend/translations/it.po +++ b/frontend/translations/it.po @@ -398,7 +398,6 @@ msgid_plural "dashboard.fonts.fonts-added" msgstr[0] "1 font aggiunto" msgstr[1] "%s font aggiunti" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "Qualsiasi font web caricato qui verrà aggiunto alla lista dei font family " @@ -407,7 +406,6 @@ msgstr "" "**singolo font family**. È possibile caricare font con i seguenti " "formati:**TTF, OTF e WOFF**(uno solo di questi è necessario)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "È consigliabile caricare unicamente font di cui si è proprietari o dei " @@ -844,7 +842,7 @@ msgstr "E-mail" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Vai su Twitter" +msgstr "Vai su X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -852,7 +850,7 @@ msgstr "Siamo qui per aiutarti con le tue domande tecniche." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Account di supporto Twitter" +msgstr "Account di supporto X" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -927,10 +925,6 @@ msgstr "Larghezza" msgid "inspect.attributes.shadow" msgstr "Ombra" -#: src/app/main/ui/inspect/attributes/shadow.cljs -msgid "inspect.attributes.shadow.shorthand.spread" -msgstr "S" - #: src/app/main/ui/inspect/attributes/stroke.cljs msgid "inspect.attributes.stroke" msgstr "Contorno" @@ -1279,7 +1273,6 @@ msgid "labels.no-invitations" msgstr "Non ci sono inviti." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "" "Premi il pulsante \"Invita nel team\" per invitare altri membri in questo " @@ -1609,13 +1602,6 @@ msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "Eliminare questo file?" msgstr[1] "Eliminare questi file?" -#: src/app/main/ui/workspace/header.cljs, -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Questo file contiene librerie usate nel file:" -msgstr[1] "Questo file contiene librerie usate nei file:" - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" @@ -1747,17 +1733,6 @@ msgstr "Piccolo scatto" msgid "modals.unpublish-shared-confirm.accept" msgstr "Annulla pubblicazione" -#: src/app/main/ui/workspace/header.cljs, -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Se annulli la pubblicazione, gli elementi diventeranno parte della libreria " -"di questo file." -msgstr[1] "" -"Se annulli la pubblicazione, gli elementi diventeranno parte della libreria " -"di questi file." - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" @@ -1765,13 +1740,6 @@ msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "Annullare la pubblicazione di questa libreria?" msgstr[1] "Annullare la pubblicazione di queste librerie?" -#: src/app/main/ui/workspace/header.cljs, -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "È utilizzata in questo file:" -msgstr[1] "È utilizzata in questi file:" - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" @@ -1876,12 +1844,6 @@ msgstr "Guida alla contribuzione" msgid "onboarding-v2.welcome.title" msgstr "Benvenuti su Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "Crea un team più tardi" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Il nome del tuo team" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "Dopo aver nominato il tuo team, potrai invitare persone ad unirsi ad esso." @@ -1896,18 +1858,9 @@ msgstr "" "Non dimenticarti di includere ogni tipo di persona. Programmatori, " "designers, responsabili... la diversità si somma :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Crea un team e invita più tardi" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Crea un team e manda inviti" - msgid "onboarding.choice.team-up.roles" msgstr "Invita con il ruolo:" -msgid "onboarding.contrib.desc2.1" -msgstr "Puoi accedere al" - msgid "onboarding.newsletter.accept" msgstr "Si, iscrivimi" @@ -1922,9 +1875,35 @@ msgstr "Condizioni sulla Privacy." msgid "onboarding.newsletter.title" msgstr "Vuoi ricevere le news di Pentot?" -msgid "onboarding.slide.0.title" -msgstr "Librerie di design, stili e componenti" - #: src/app/main/ui/dashboard/team.cljs msgid "title.team-invitations" msgstr "Inviti - %s - Penpot" + +#~ msgid "dashboard.newsletter-title" +#~ msgstr "Iscrizione alla newsletter" + +#~ msgid "feedback.chat-subtitle" +#~ msgstr "Hai voglia di parlare? Chatta con noi su Gitter" + +#~ msgid "inspect.attributes.shadow.shorthand.offset-x" +#~ msgstr "X" + +#~ msgid "labels.images" +#~ msgstr "Immagini" + +#~ msgid "labels.skip" +#~ msgstr "Saltare" + +#~ msgid "onboarding.contrib.alt" +#~ msgstr "Open Source" + +#~ msgid "onboarding.contrib.link" +#~ msgstr "progetto su github" + +#~ msgid "onboarding.slide.0.desc1" +#~ msgstr "" +#~ "Crea splendide interfacce utente in collaborazione con tutti i membri del " +#~ "team." + +#~ msgid "onboarding.slide.1.desc1" +#~ msgstr "Crea interazioni complete per imitare al meglio il prodotto finale." diff --git a/frontend/translations/jpn_JP.po b/frontend/translations/jpn_JP.po index 8b44fd5448..e7847f0057 100644 --- a/frontend/translations/jpn_JP.po +++ b/frontend/translations/jpn_JP.po @@ -620,7 +620,7 @@ msgstr "メールアドレス" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Twitterサポートアカウント" +msgstr "Xサポートアカウント" #: src/app/main/ui/settings/password.cljs msgid "generic.error" diff --git a/frontend/translations/ko.po b/frontend/translations/ko.po index 936092eec1..2946e8f69d 100644 --- a/frontend/translations/ko.po +++ b/frontend/translations/ko.po @@ -677,4 +677,4 @@ msgid "shortcuts.group" msgstr "그룹" msgid "shortcuts.h-distribute" -msgstr "가로로 분배하기" \ No newline at end of file +msgstr "가로로 분배하기" diff --git a/frontend/translations/lt.po b/frontend/translations/lt.po index dffcf68aa3..a4cebd6ddc 100644 --- a/frontend/translations/lt.po +++ b/frontend/translations/lt.po @@ -2,15 +2,15 @@ msgid "" msgstr "" "PO-Revision-Date: 2023-08-09 07:04+0000\n" "Last-Translator: Vincas Dundzys \n" -"Language-Team: Lithuanian \n" +"Language-Team: Lithuanian " +"\n" "Language: lt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n % 10 == 1 && (n % 100 < 11 || n % 100 > " -"19)) ? 0 : ((n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19)) ? " -"1 : 2);\n" +"19)) ? 0 : ((n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19)) " +"? 1 : 2);\n" "X-Generator: Weblate 5.0-dev\n" #: src/app/main/ui/auth/register.cljs @@ -170,26 +170,95 @@ msgstr "" msgid "auth.verification-email-sent" msgstr "Išsiuntėme patvirtinimo el. laišką adresu" +msgid "common.publish" +msgstr "Paskelbti" + +msgid "common.share-link.all-users" +msgstr "Visi Penpot vartotojai" + msgid "common.share-link.confirm-deletion-link-description" msgstr "" "Ar tikrai norite pašalinti šią nuorodą? Jei tai padarysite, ji niekam " "nebebus pasiekiama" +msgid "common.share-link.current-tag" +msgstr "(dabartinis)" + +msgid "common.share-link.destroy-link" +msgstr "Naikinti nuorodą" + msgid "common.share-link.get-link" msgstr "Gauti nuorodą" msgid "common.share-link.link-copied-success" msgstr "Nuoroda sėkmingai nukopijuota" +msgid "common.share-link.manage-ops" +msgstr "Valdyti leidimus" + +msgid "common.share-link.page-shared" +msgid_plural "common.share-link.page-shared" +msgstr[0] "Bendrinamas 1 puslapis" +msgstr[1] "Bendrinami % puslapiai" +msgstr[2] "Bendrinama % puslapių" + +msgid "common.share-link.permissions-can-comment" +msgstr "Gali komentuoti" + +msgid "common.share-link.permissions-can-inspect" +msgstr "Gali apžiūrėti kodą" + msgid "common.share-link.permissions-hint" msgstr "Kiekvienas, turintis nuorodą, turės prieigą" +msgid "common.share-link.permissions-pages" +msgstr "Bendrinti puslapiai" + msgid "common.share-link.placeholder" msgstr "Bendrinama nuoroda bus rodoma čia" +msgid "common.share-link.team-members" +msgstr "Tik komandos nariams" + msgid "common.share-link.title" msgstr "Dalinkitės prototipais" +msgid "common.share-link.view-all" +msgstr "Rinktis viską" + +msgid "common.unpublish" +msgstr "Atšaukti paskelbimą" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.management" +msgstr "Komandos valdymas" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.text" +msgstr "" +"Penpot yra skirtas komandoms. Pakvieskite narius bendram darbui su " +"projektais ir failais" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.title" +msgstr "Suburkite komandą!" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.tutorial-hero.info" +msgstr "Išmokite Penpot pagrindus ir mėgaukitės šia pamoka." + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.tutorial-hero.start" +msgstr "Pradėti pamoką" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.info" +msgstr "Panagrinėkite Penpot ir susipažinkite su pagrindinėmis jo savybėmis." + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.start" +msgstr "Pradėkite apžvalgą" + #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Pridėti kaip bendrinamą biblioteką" @@ -227,8 +296,8 @@ msgstr "Dubliuoti %s failus" msgid "dashboard.empty-placeholder-drafts" msgstr "" "Čia bus rodomi prie bibliotekų pridėti failai. Pabandykite bendrinti failus " -"arba pridėti iš mūsų [Bibliotekos ir šablonai] (https://penpot.app/libraries-" -"templates.html)" +"arba pridėti iš mūsų [Bibliotekos ir šablonai] " +"(https://penpot.app/libraries-templates.html)" msgid "dashboard.export-frames" msgstr "Eksportuokite darbalaukius į PDF" @@ -375,76 +444,18 @@ msgstr "Įkeliami duomenys į serverį (%s/%s)" msgid "dashboard.import.progress.upload-media" msgstr "Įkeliamas failas: %s" -msgid "common.share-link.team-members" -msgstr "Tik komandos nariams" +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-file-prefix" +msgstr "Naujas failas" #: src/app/main/ui/dashboard/projects.cljs -msgid "dasboard.team-hero.management" -msgstr "Komandos valdymas" - -#: src/app/main/ui/dashboard/projects.cljs -msgid "dasboard.tutorial-hero.info" -msgstr "Išmokite Penpot pagrindus ir mėgaukitės šia pamoka." - -#: src/app/main/ui/auth/verify_token.cljs -msgid "dashboard.notifications.email-verified-successfully" -msgstr "Jūsų el. pašto adresas buvo sėkmingai patvirtintas" +msgid "dashboard.new-project" +msgstr "+ Naujas projektas" #: src/app/main/data/dashboard.cljs msgid "dashboard.new-project-prefix" msgstr "Naujas projektas" -#: src/app/main/ui/settings/password.cljs -msgid "dashboard.notifications.password-saved" -msgstr "Slaptažodis sėkmingai išsaugotas!" - -#: src/app/main/ui/dashboard/projects.cljs -msgid "dashboard.projects-title" -msgstr "Projektai" - -#: src/app/main/ui/dashboard/team.cljs -msgid "dashboard.num-of-members" -msgstr "%s nariai" - -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.open-in-new-tab" -msgstr "Atidarykite failą naujame skirtuke" - -#: src/app/main/ui/dashboard/project_menu.cljs -msgid "dashboard.pin-unpin" -msgstr "Prisegti/Atsegti" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.remove-shared" -msgstr "Pašalinti kaip bendrinamą biblioteką" - -msgid "dashboard.options" -msgstr "Nustatymai" - -msgid "common.share-link.all-users" -msgstr "Visi Penpot vartotojai" - -msgid "common.share-link.current-tag" -msgstr "(dabartinis)" - -msgid "common.share-link.destroy-link" -msgstr "Naikinti nuorodą" - -msgid "common.share-link.manage-ops" -msgstr "Valdyti leidimus" - -msgid "common.share-link.permissions-can-comment" -msgstr "Gali komentuoti" - -msgid "common.share-link.permissions-can-inspect" -msgstr "Gali apžiūrėti kodą" - -msgid "common.share-link.permissions-pages" -msgstr "Bendrinti puslapiai" - -msgid "common.share-link.view-all" -msgstr "Rinktis viską" - #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.no-matches-for" msgstr "Nerasta jokių atitikmenų pagal \"%s\"" @@ -457,55 +468,44 @@ msgstr "Prisegti projektai bus rodomi čia" msgid "dashboard.notifications.email-changed-successfully" msgstr "Jūsų el. pašto adresas sėkmingai atnaujintas" -#: src/app/main/ui/dashboard/projects.cljs -msgid "dasboard.team-hero.text" -msgstr "" -"Penpot yra skirtas komandoms. Pakvieskite narius bendram darbui su " -"projektais ir failais" +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-verified-successfully" +msgstr "Jūsų el. pašto adresas buvo sėkmingai patvirtintas" -#: src/app/main/ui/dashboard/projects.cljs -msgid "dasboard.team-hero.title" -msgstr "Suburkite komandą!" +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.notifications.password-saved" +msgstr "Slaptažodis sėkmingai išsaugotas!" -#: src/app/main/ui/dashboard/projects.cljs -msgid "dasboard.tutorial-hero.start" -msgstr "Pradėti pamoką" +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.num-of-members" +msgstr "%s nariai" -#: src/app/main/ui/dashboard/projects.cljs -msgid "dasboard.walkthrough-hero.info" -msgstr "Panagrinėkite Penpot ir susipažinkite su pagrindinėmis jo savybėmis." +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.open-in-new-tab" +msgstr "Atidarykite failą naujame skirtuke" -#: src/app/main/ui/dashboard/projects.cljs -msgid "dasboard.walkthrough-hero.start" -msgstr "Pradėkite apžvalgą" - -msgid "common.share-link.page-shared" -msgid_plural "common.share-link.page-shared" -msgstr[0] "Bendrinamas 1 puslapis" -msgstr[1] "Bendrinami % puslapiai" -msgstr[2] "Bendrinama % puslapių" +msgid "dashboard.options" +msgstr "Nustatymai" #: src/app/main/ui/settings/password.cljs msgid "dashboard.password-change" msgstr "Keisti slaptažodį" +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.pin-unpin" +msgstr "Prisegti/Atsegti" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.projects-title" +msgstr "Projektai" + #: src/app/main/ui/settings/profile.cljs msgid "dashboard.remove-account" msgstr "Norite pašalinti paskyrą?" -msgid "common.publish" -msgstr "Paskelbti" - -msgid "common.unpublish" -msgstr "Atšaukti paskelbimą" - -#: src/app/main/data/dashboard.cljs -msgid "dashboard.new-file-prefix" -msgstr "Naujas failas" - -#: src/app/main/ui/dashboard/projects.cljs -msgid "dashboard.new-project" -msgstr "+ Naujas projektas" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.remove-shared" +msgstr "Pašalinti kaip bendrinamą biblioteką" #: src/app/main/ui/settings/options.cljs msgid "dashboard.theme-change" diff --git a/frontend/translations/lv.po b/frontend/translations/lv.po index 44a6c195d0..72c325638a 100644 --- a/frontend/translations/lv.po +++ b/frontend/translations/lv.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2023-06-03 00:50+0000\n" -"Last-Translator: \"Ņikita K.\" \n" +"PO-Revision-Date: 2024-01-06 22:06+0000\n" +"Last-Translator: Edgars Andersons \n" "Language-Team: Latvian \n" "Language: lv\n" @@ -10,17 +10,17 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n % 10 == 0 || n % 100 >= 11 && n % 100 <= " "19) ? 0 : ((n % 10 == 1 && n % 100 != 11) ? 1 : 2);\n" -"X-Generator: Weblate 4.18-dev\n" +"X-Generator: Weblate 5.4-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" -msgstr "Vai jums jau ir konts?" +msgstr "Jau ir konts?" #: src/app/main/ui/auth/register.cljs msgid "auth.check-your-email" msgstr "" -"Pārbaudiet savu e-pastu un noklikšķiniet uz saites, lai apstiprinātu un " -"sāktu lietot Penpot." +"Jāpārbauda savs e-pasts un jānoklikšķina uz saites, lai apstiprinātu un " +"sāktu izmantot Penpot." #: src/app/main/ui/auth/recovery.cljs msgid "auth.confirm-password" @@ -37,20 +37,21 @@ msgstr "Gribat tikai pamēģināt?" #: src/app/main/ui/auth/register.cljs msgid "auth.demo-warning" msgstr "" -"Šis ir DEMO pakalpojums, NEIZMANTOJIET reāliem darbiem, projektus tiks " -"periodiski dzēsti." +"Šis ir IZRĀDĪŠANAS pakalpojums, kas NAV IZMANTOJAMS īstam darbam, jo " +"projekti ik pēc noteikta laika posma tiks izdzēsti." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" -msgstr "E-pasts" +msgstr "E-pasta adrese" #: src/app/main/ui/auth/login.cljs msgid "auth.forgot-password" -msgstr "Aizmirsāt paroli?" +msgstr "Aizmirsta parole?" #: src/app/main/ui/auth/register.cljs msgid "auth.fullname" -msgstr "Vārds un Uzvārds" +msgstr "Pilns vārds" #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" @@ -84,13 +85,21 @@ msgstr "LDAP" msgid "auth.login-with-oidc-submit" msgstr "AtvērtoID (OpenID)" +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "Nosaukumam jāsatur simboli, kas nav atstarpe." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "Nosaukumus nedrīkst pārsniegt 250 simbolus." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.new-password" msgstr "Ierakstiet jaunu paroli" #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.invalid-token-error" -msgstr "Atkopšanas marķieris nav derīgs." +msgstr "Atkopšanas tekstvienība nav derīga." #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.password-changed-successfully" @@ -99,16 +108,16 @@ msgstr "Parole ir veiksmīgi nomainīta" #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.notifications.profile-not-verified" msgstr "" -"Profils nav pārbaudīts, lūdzu, pirms turpiniet, veiciet profila " -"verificēšanu." +"Profils nav apliecināts, lūgums pirms turpināšanas veikt profila " +"apstiprināšanu." #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.notifications.recovery-token-sent" -msgstr "Uz Jūsu e-pastu nosūtīta paroles atkopšanas saite." +msgstr "Paroles atkopšanas saite ir nosūtīta e-pastā." #: src/app/main/ui/auth/verify_token.cljs msgid "auth.notifications.team-invitation-accepted" -msgstr "Jūs esat veiksmīgi pievienojies komandai" +msgstr "Pievienošanās komandai bija veiksmīga" #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs msgid "auth.password" @@ -118,6 +127,10 @@ msgstr "Parole" msgid "auth.password-length-hint" msgstr "Vismaz 8 rakstzīmes" +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "Parolē ir jābūt arī citām rakstzīmēm bez atstarpes." + msgid "auth.privacy-policy" msgstr "Privātuma politika" @@ -127,7 +140,7 @@ msgstr "Atkopt paroli" #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.recovery-request-subtitle" -msgstr "Mēs nosūtīsim jums e-pasta ziņojumu ar norādījumiem" +msgstr "Mēs nosūtīsim e-pasta ziņojumu ar norādēm" #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.recovery-request-title" @@ -155,7 +168,7 @@ msgstr "Izveidot kontu" #: src/app/main/ui/auth.cljs msgid "auth.sidebar-tagline" -msgstr "Atvērtā pirmkoda risinājums projektēšanai un prototipizēšanai." +msgstr "Atvērtā pirmkoda risinājums dizaina izstrādei un modelēšanai." msgid "auth.terms-of-service" msgstr "Pakalpojumu sniegšanas noteikumi" @@ -163,13 +176,17 @@ msgstr "Pakalpojumu sniegšanas noteikumi" #: src/app/main/ui/auth/register.cljs msgid "auth.terms-privacy-agreement" msgstr "" -"Veidojot jaunu kontu, jūs piekrītat mūsu pakalpojumu sniegšanas noteikumiem " -"un konfidencialitātes politikai." +"Ar jauna konta izveidošanu tiek piekrists mūsu pakalpojuma noteikumiem un " +"privātuma nosacījumiem." #: src/app/main/ui/auth/register.cljs msgid "auth.verification-email-sent" msgstr "Mēs esam nosūtījuši apstiprinājuma e-pasta ziņojumu uz" +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "... zīmolrades, ilustrācijām, mārketinga materiāliem utt." + msgid "common.publish" msgstr "Publicēt" @@ -178,8 +195,7 @@ msgstr "Visi Penpot lietotāji" msgid "common.share-link.confirm-deletion-link-description" msgstr "" -"Vai tiešām vēlaties noņemt šo saiti? Noņemot saiti, tā vairs nebūs pieejama " -"nevienam" +"Vai tiešām noņemt šo saiti? Noņemot to, saite vairs nebūs pieejama nevienam" msgid "common.share-link.current-tag" msgstr "(pašreizējais)" @@ -198,30 +214,30 @@ msgstr "Pārvaldīt atļaujas" msgid "common.share-link.page-shared" msgid_plural "common.share-link.page-shared" -msgstr[0] "Nav koplietoto lapu" -msgstr[1] "1 lapa koplietota" -msgstr[2] "%s lapas koplietotas" +msgstr[0] "%s kopīgotu lapu" +msgstr[1] "%s kopīgota lapa" +msgstr[2] "%s kopīgotas lapas" msgid "common.share-link.permissions-can-comment" msgstr "Var komentēt" msgid "common.share-link.permissions-can-inspect" -msgstr "Var skatīties kodu" +msgstr "Var apskatīt kodu" msgid "common.share-link.permissions-hint" msgstr "Ikvienam, kam ir saite, būs piekļuve" msgid "common.share-link.permissions-pages" -msgstr "Koplietotas lapas" +msgstr "Kopīgotas lapas" msgid "common.share-link.placeholder" -msgstr "Šeit tiks parādīta koplietojama saite" +msgstr "Šeit tiks parādīta kopīgojama saite" msgid "common.share-link.team-members" msgstr "Tikai komandas dalībnieki" msgid "common.share-link.title" -msgstr "Prototipu koplietošana" +msgstr "Kopīgot prototipus" msgid "common.share-link.view-all" msgstr "Atlasīt visu" @@ -231,21 +247,22 @@ msgstr "Atcelt publikāciju" #: src/app/main/ui/dashboard/projects.cljs msgid "dasboard.team-hero.management" -msgstr "Komandas vadība" +msgstr "Komandas pārvaldība" #: src/app/main/ui/dashboard/projects.cljs msgid "dasboard.team-hero.text" msgstr "" -"Penpot ir paredzēts komandām. Uzaiciniet dalībniekus strādāt kopā ar " -"projektiem un failiem" +"Penpot ir paredzēta komandām. Jāuzaicina dalībnieki, lai kopā strādātu ar " +"projektiem un datnēm" #: src/app/main/ui/dashboard/projects.cljs msgid "dasboard.team-hero.title" -msgstr "Apvienojaties!" +msgstr "Apvienojieties!" #: src/app/main/ui/dashboard/projects.cljs msgid "dasboard.tutorial-hero.info" -msgstr "Apgūstiet Penpot pamatus, izmantojot šo apmācību un gūstiet prieku." +msgstr "" +"Penpot pamatu apgūšana, kamēr tiek gūts prieks, ar šo praktisko apmācību." #: src/app/main/ui/dashboard/projects.cljs msgid "dasboard.tutorial-hero.start" @@ -257,7 +274,7 @@ msgstr "Praktiskā apmācība" #: src/app/main/ui/dashboard/projects.cljs msgid "dasboard.walkthrough-hero.info" -msgstr "Caurskatiet Penpot un iepazīstieties ar tā galvenajām funkcijām." +msgstr "Caurskati Penpot un iepazīsties ar tās galvenajām iespējām." #: src/app/main/ui/dashboard/projects.cljs msgid "dasboard.walkthrough-hero.start" @@ -267,13 +284,89 @@ msgstr "Sākt iepazīšanos" msgid "dasboard.walkthrough-hero.title" msgstr "Saskarnes caurskate" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "Pilnvara ievietota starpliktuvē" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "Izveidot jaunu pilnvaru" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "Piekļuves pilnvara ir veiksmīgi izveidota." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "Jānospiež poga \"Izveidot jaunu pilnvaru\", lai izveidotu kādu." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "Pagaidām vēl nav pilnvaru." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "Nosaukums ir obligāts" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 dienas" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 dienas" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 dienas" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 dienas" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "Nekad" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "Izbeidzās %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "Derīgs līdz %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "Bez derīguma termiņa" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Privātās piekļuves pilnvaras" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Privātās piekļuves pilnvaras darbojas kā alternatīva mūsu pieteikšanās/" +"paroles autentificēšanas sistēmai, un tās var izmantot, lai ļautu lietotnēm " +"piekļūt iekšējam Penpot API" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "Pilnvara ir derīga līdz %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "Pilnvarai nav derīguma beigu datuma" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Pievienot kā koplietojamu bibliotēku" #: src/app/main/ui/settings/profile.cljs msgid "dashboard.change-email" -msgstr "Mainīt e-pastu" +msgstr "Mainīt e-pasta adresi" #: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs msgid "dashboard.copy-suffix" @@ -285,46 +378,46 @@ msgstr "Izveidot jaunu komandu" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.default-team-name" -msgstr "Jūsu Penpot" +msgstr "Mans Penpot" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.delete-team" msgstr "Dzēst komandu" msgid "dashboard.download-binary-file" -msgstr "Lejupielādēt Penpot failu (.penpot)" +msgstr "Lejupielādēt Penpot datni (.penpot)" msgid "dashboard.download-standard-file" -msgstr "Lejupielādēt standarta failu (.svg + .json)" +msgstr "Lejupielādēt standarta datni (.svg + .json)" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate" -msgstr "Dublēt" +msgstr "Divkāršot" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate-multi" -msgstr "Dublēt failus (%s)" +msgstr "Divkāršot %s datnes" #: src/app/main/ui/dashboard/grid.cljs -#, markdown msgid "dashboard.empty-placeholder-drafts" msgstr "" -"Šeit tiks parādīti Bibliotēkām pievienotie faili. Mēģiniet koplietot failus " -"vai pievienojiet tos no mūsu [Bibliotēkas un veidnes] " -"(https://penpot.app/libraries-templates.html)." +"Šeit tiks parādītas bibliotēkām pievienotās datnes. Mēģini koplietot datnes " +"vai pievienot tās no mūsu [bibliotēkām un veidnēm](https://penpot.app/" +"libraries-templates.html)." msgid "dashboard.export-binary-multi" -msgstr "Lejupielādēt %s Penpot failus (.penpot)" +msgstr "Lejupielādēt %s Penpot datnes (.penpot)" msgid "dashboard.export-frames" -msgstr "Eksportējiet dēļus PDF formātā" +msgstr "Izgūt plātnes kā PDF" #: src/app/main/ui/export.cljs msgid "dashboard.export-frames.title" -msgstr "Eksportēt kā PDF" +msgstr "Izgūt kā PDF" msgid "dashboard.export-multi" -msgstr "Penpot %s failu eksportēšana" +msgstr "Izgūt Penpot %s datnes" #: src/app/main/ui/export.cljs msgid "dashboard.export-multiple.selected" @@ -332,63 +425,65 @@ msgstr "Atlasīti elementi - %s no %s" #: src/app/main/ui/workspace/header.cljs msgid "dashboard.export-shapes" -msgstr "Eksportēt" +msgstr "Izgūt" #: src/app/main/ui/export.cljs msgid "dashboard.export-shapes.how-to" msgstr "" -"Varat pievienot eksportēšanas iestatījumus elementiem no noformējuma " -"rekvizītiem (labās sānjoslas apakšdaļā)." +"Izgūšanas iestatījumus elementiem var pievienot no noformējuma īpašībām (" +"labās sānjoslas apakšā)." #: src/app/main/ui/export.cljs msgid "dashboard.export-shapes.how-to-link" -msgstr "Informācija par to, kā iestatīt eksportu Penpot." +msgstr "Informācija par to, kā iestatīt izgūšanu Penpot." #: src/app/main/ui/export.cljs msgid "dashboard.export-shapes.no-elements" -msgstr "Nav elementu ar eksportēšanas iestatījumiem." +msgstr "Nav elementu ar izgūšanas iestatījumiem." #: src/app/main/ui/export.cljs msgid "dashboard.export-shapes.title" -msgstr "Atlases eksportēša" +msgstr "Izgūt atlasi" msgid "dashboard.export-standard-multi" -msgstr "Lejupielādēt %s standarta failus (. svg +. json)" +msgstr "Lejupielādēt %s standarta datnes (. svg +. json)" msgid "dashboard.export.detail" -msgstr "* var ietvert komponentus, grafikas, krāsas un/vai tipogrāfijas." +msgstr "" +"* var ietvert sastāvdaļas, attēlus, krāsas un/vai burtu stilus un veidus." msgid "dashboard.export.explain" msgstr "" -"Viens vai vairāki faili, kurus vēlaties eksportēt, izmanto koplietojamās " -"bibliotēkas. Ko jūs gribat darīt ar viņu līdzekļiem *?" +"Viena vai vairākas izgūstamās datnes izmanto koplietojamas bibliotēkas. Ko " +"iesākt ar to līdzekļiem*?" msgid "dashboard.export.options.all.message" msgstr "" -"eksportēšanā tiks iekļauti faili ar koplietojamām bibliotēkām, saglabājot " -"to sasaisti." +"izguvē tiks iekļautas datnes ar koplietojamām bibliotēkām, saglabājot to " +"sasaisti." msgid "dashboard.export.options.all.title" -msgstr "Koplietojamo bibliotēku eksportēšana" +msgstr "Izgūt koplietojamās bibliotēkas" msgid "dashboard.export.options.detach.message" msgstr "" -"Koplietojamās bibliotēkas eksportēšanā netiks iekļautas, un bibliotēkai " -"netiks pievienoti līdzekļi. " +"Koplietojamās bibliotēkas netiks iekļautas izguvē, un bibliotēkai netiks " +"pievienoti līdzekļi. " msgid "dashboard.export.options.detach.title" -msgstr "Attiekties pret koplietojamo bibliotēku līdzekļiem, kā pret pamatobjektiem" +msgstr "" +"Attiekties pret koplietojamo bibliotēku līdzekļiem kā pret pamatobjektiem" msgid "dashboard.export.options.merge.message" msgstr "" -"Fails tiks eksportēts ar visiem ārējiem līdzekļiem, kas tiks sapludināti " -"faila bibliotēkā." +"Datne tiks izgūta ar visiem ārējiem līdzekļiem, kas tiks apvienoti datnes " +"bibliotēkā." msgid "dashboard.export.options.merge.title" -msgstr "Failu bibliotēkās iekļaut koplietojamos bibliotēkas līdzekļus" +msgstr "Iekļaut koplietojamos bibliotēkas līdzekļus datņu bibliotēkās" msgid "dashboard.export.title" -msgstr "Eksportēt failus" +msgstr "Izgūt datnes" msgid "dashboard.fonts.deleted-placeholder" msgstr "Fonts izdzēsts" @@ -398,7 +493,7 @@ msgid "dashboard.fonts.dismiss-all" msgstr "Noraidīt visu" msgid "dashboard.fonts.empty-placeholder" -msgstr "Šeit tiks parādīti jūsu augšupielādētie pielāgotie fonti." +msgstr "Šeit tiks parādīti augšupielādētie pielāgotie fonti." #: src/app/main/ui/dashboard/fonts.cljs msgid "dashboard.fonts.fonts-added" @@ -407,44 +502,50 @@ msgstr[0] "Nav pievienoti fonti" msgstr[1] "Fonts pievienots" msgstr[2] "%s fonti pievienoti" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" -"Visi šeit augšupielādētie tīmekļa fonti tiks pievienoti fontu saimes " -"sarakstam, kas pieejams šīs grupas failu teksta rekvizītos. Fonti ar tādu " -"pašu fontu saimes nosaukumu tiks grupēti kā **viena fonta saime**. Fontus " -"var augšupielādēt šādos formātos: **TTF, OTF un WOFF** (būs nepieciešams " -"tikai viens)." +"Visi augšupielādētie tīmekļa fonti tiks pievienoti fontu saimju sarakstam, " +"kas pieejams šīs komandas datņu teksta īpašībās. Fonti ar tādu pašu fontu " +"saimes nosaukumu tiks apkopoti kā **viena fontu saime**. Var augšupielādēt " +"fontus šādos veidolos: **TTF, OTF un WOFF** (būs nepieciešams tikai viens)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" -"Augšupielādējiet tikai tos fontus, kuri jums pieder vai kuriem ir licence " -"lietošanai Penpot. Uzziniet vairāk [Penpot pakalpojumu sniegšanas " -"noteikumi] (https://penpot.app/terms.html). Iespējams, vēlēsities izlasīt " -"arī par [fontu licencēšanu] (https://www.typography.com/faq)." +"Vajadzētu augšupielādēt tikai sev piederošus fontus vai tos, kuriem ir " +"licence to izmantošanai Penpot. Vairāk var uzzināt [Penpot pakalpojuma " +"sniegšanas noteikumos](https://penpot.app/terms.html). Varētu būt noderīgi " +"izlasīt arī par [fontu licencēšanu](https://www.typography.com/faq)." #: src/app/main/ui/dashboard/fonts.cljs msgid "dashboard.fonts.upload-all" msgstr "Augšupielādēt visu" +msgid "dashboard.fonts.warning-text" +msgstr "" +"Esam noteikuši iespējamu sarežģījumu ar fontiem, kas ir saistīta ar " +"vertikālajiem rādītājiem dažādām operētājsistēmām. Lai to pārbaudītu, var " +"izmantot tādus fontu vertikālo rādītāju pakalpojumus kā [šis](https" +"://vertical-metrics.netlify.app/). Turklāt ir ieteicams izmantot " +"[Transfonter](https://transfonter.org/), lai izveidotu tīmekļa fontus un " +"novērstu kļūdas. " + msgid "dashboard.import" -msgstr "Penpot failu importēšana" +msgstr "Ievietot Penpot datnes" msgid "dashboard.import.analyze-error" -msgstr "Oopss! Šo failu nevarēja importēt" +msgstr "Ak vai! Šo datni nevarēja ievietot" msgid "dashboard.import.import-error" -msgstr "Importējot failu, radās problēma. Fails netika importēts." +msgstr "Datnes ievietošanas laikā radās sarežģījumi. Datne netika ievietota." msgid "dashboard.import.import-warning" -msgstr "Dažos failos bija nederīgi objekti, kuri tika noņemti." +msgstr "Dažās datnēs bija nederīgi objekti, kuri tika noņemti." msgid "dashboard.import.progress.process-colors" msgstr "Krāsu apstrāde" msgid "dashboard.import.progress.process-components" -msgstr "Komponenšu apstrāde" +msgstr "Apstrādā sastāvdaļas" msgid "dashboard.import.progress.process-media" msgstr "Multivides apstrāde" @@ -453,19 +554,20 @@ msgid "dashboard.import.progress.process-page" msgstr "%s lapas apstrāde" msgid "dashboard.import.progress.process-typographies" -msgstr "Tipogrāfijas apstrāde" +msgstr "Apstrādā burtu stilus un veidus" msgid "dashboard.import.progress.upload-data" -msgstr "Notiek datu augšupielāde serverī (%s/%s)" +msgstr "Augšupielādē datus serverī (%s/%s)" msgid "dashboard.import.progress.upload-media" -msgstr "Notiek faila augšupielāde: %s" +msgstr "Augšupielādē datni: %s" #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.invite-profile" msgstr "Uzaicināt personas" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" msgstr "Atstāt komandu" @@ -473,10 +575,10 @@ msgid "dashboard.libraries-and-templates" msgstr "Bibliotēkas un veidnes" msgid "dashboard.libraries-and-templates.explore" -msgstr "Izpētiet vairāk un uzziniet, kā dot ieguldījumu" +msgstr "Izpētīt vairāk un uzzinātt, kā sniegt ieguldījumu" msgid "dashboard.libraries-and-templates.import-error" -msgstr "Importējot veidni, radās problēma. Veidne netika importēta." +msgstr "Veidnes ievietošanas laikā radās sarežģījumi. Veidne netika ievietota." #: src/app/main/ui/dashboard/libraries.cljs msgid "dashboard.libraries-title" @@ -484,30 +586,32 @@ msgstr "Bibliotēkas" #: src/app/main/ui/dashboard/grid.cljs msgid "dashboard.loading-files" -msgstr "notiek failu ielāde …" +msgstr "ielādē datnes …" msgid "dashboard.loading-fonts" msgstr "notiek fontu ielāde …" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "Pārvietot uz" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to-multi" -msgstr "Pārvietot %s failus uz" +msgstr "Pārvietot %s datnes uz" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to-other-team" msgstr "Pārvietot uz citu komandu" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" -msgstr "+ Jauns fails" +msgstr "+ Jauna datne" #: src/app/main/data/dashboard.cljs msgid "dashboard.new-file-prefix" -msgstr "Jauns fails" +msgstr "Jauna datne" #: src/app/main/ui/dashboard/projects.cljs msgid "dashboard.new-project" @@ -523,19 +627,19 @@ msgstr "“%s” nav atrasta neviena atbilstība" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.no-projects-placeholder" -msgstr "Šeit būs redzami piesprausti projekti" +msgstr "Šeit būs redzami piespraustie projekti" #: src/app/main/ui/auth/verify_token.cljs msgid "dashboard.notifications.email-changed-successfully" -msgstr "Jūsu e-pasta adrese ir veiksmīgi atjaunināta" +msgstr "E-pasta adrese tika veiksmīgi atjaunināta" #: src/app/main/ui/auth/verify_token.cljs msgid "dashboard.notifications.email-verified-successfully" -msgstr "Jūsu e-pasta adrese ir veiksmīgi pārbaudīta" +msgstr "E-pasta adrese tika veiksmīgi apliecināta" #: src/app/main/ui/settings/password.cljs msgid "dashboard.notifications.password-saved" -msgstr "Parole ir veiksmīgi saglabāta!" +msgstr "Parole ir veiksmīgi saglabāta." #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.num-of-members" @@ -543,7 +647,7 @@ msgstr "Dalībieki: %s" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.open-in-new-tab" -msgstr "Atvērt failu jaunā cilnē" +msgstr "Atvērt datni jaunā cilnē" msgid "dashboard.options" msgstr "Opcijas" @@ -562,9 +666,10 @@ msgstr "Projekti" #: src/app/main/ui/settings/profile.cljs msgid "dashboard.remove-account" -msgstr "Vai vēlaties noņemt savu kontu?" +msgstr "Noņemt savu kontu?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" msgstr "Noņemt kā koplietojamo bibliotēku" @@ -590,41 +695,42 @@ msgstr "Atlasīt dizainu" #: src/app/main/ui/dashboard/grid.cljs msgid "dashboard.show-all-files" -msgstr "Rādīt visus failus" +msgstr "Rādīt visas datnes" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-delete-file" msgid_plural "dashboard.success-delete-file" -msgstr[0] "Neviens fails nav izdzēsts" -msgstr[1] "Jūsu fails ir sekmīgi izdzēsts" -msgstr[2] "Jūsu faili ir sekmīgi izdzēsti" +msgstr[0] "Datnes netika izdzēstas" +msgstr[1] "Datne ir sekmīgi izdzēsta" +msgstr[2] "Datnes ir sekmīgi izdzēstas" #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-delete-project" -msgstr "Jūsu projekts ir veiksmīgi izdzēsts" +msgstr "Projekts tika veiksmīgi izdzēsts" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-duplicate-file" msgid_plural "dashboard.success-delete-file" -msgstr[0] "Neviens fails nav dublēts" -msgstr[1] "Jūsu fails ir veiksmīgi dublēts" -msgstr[2] "Jūsu faili ir veiksmīgi dublēti" +msgstr[0] "Neviena datne netika sekmīgi divkāršota" +msgstr[1] "Datne tika sekmīgi divkāršota" +msgstr[2] "Datnes tika sekmīgi divkāršotas" #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-duplicate-project" -msgstr "Jūsu projekts ir veiksmīgi dublēts" +msgstr "Projekts tika veiksmīgi divkāršots" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-file" -msgstr "Jūsu fails ir veiksmīgi pārvietots" +msgstr "Datne tika sekmīgi pārvietota" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-files" -msgstr "Jūsu faili ir veiksmīgi pārvietoti" +msgstr "Datnes tika veiksmīgi pārvietotas" #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-move-project" -msgstr "Jūsu projekts ir veiksmīgi pārvietots" +msgstr "Projekts tika veiksmīgi pārvietots" #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.team-info" @@ -648,13 +754,15 @@ msgstr "Meklēšanas rezultāti" #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.type-something" -msgstr "Ievadiet, lai meklētu" +msgstr "Jāievada, lai meklētu" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.unpublish-shared" msgstr "Atcelt bibliotēkas publicēšanu" -#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Atjaunināt iestatījumus" @@ -668,41 +776,45 @@ msgid "dashboard.webhooks.content-type" msgstr "Satura tips" msgid "dashboard.webhooks.create" -msgstr "Izveidot tīmekļa āķi" +msgstr "Izveidot tīmekļa aizķeri" msgid "dashboard.webhooks.create.success" -msgstr "Tīmekļa āķis ir veiksmīgi izveidots." +msgstr "Tīmekļa aizķere ir veiksmīgi izveidota." msgid "dashboard.webhooks.description" msgstr "" -"Tīmekļa āķi ir vienkāršs veids, kā ļaut citām tīmekļa vietnēm un lietotnēm " -"saņemt paziņojumus, ja Penpot notiek noteikti notikumi. Mēs nosūtīsim POST " -"pieprasījumu katram jūsu nodrošinātajam URL." +"Tīmekļa aizķeres ir vienkāršs veids, kā ļaut citām tīmekļa vietnēm un " +"lietotnēm saņemt paziņojumus, kad Penpot notiek noteikti notikumi. Mēs " +"nosūtīsim POST pieprasījumu katram norādītajam URL." msgid "dashboard.webhooks.empty.add-one" -msgstr "Nospiediet pogu \"Pievienot tīmekļa āķi\", lai to pievienotu." +msgstr "Jānospiež poga \"Pievienot tīmekļa aizķeri\", lai pievienotu kādu." msgid "dashboard.webhooks.empty.no-webhooks" -msgstr "Līdz šim nav izveidoti tīmekļa āķi." +msgstr "Līdz šim nav izveidota neviena tīmekļa aizķere." msgid "dashboard.webhooks.update.success" -msgstr "Tīmekļa āķis ir sekmīgi atjaunināts." +msgstr "Tīmekļa aizķere ir sekmīgi atjaunināta." #: src/app/main/ui/settings.cljs msgid "dashboard.your-account-title" -msgstr "Jūsu konts" +msgstr "Mans konts" #: src/app/main/ui/settings/profile.cljs msgid "dashboard.your-email" -msgstr "E-pasts" +msgstr "E-pasta adrese" #: src/app/main/ui/settings/profile.cljs msgid "dashboard.your-name" -msgstr "Jūsu vārds" +msgstr "Vārds" -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" -msgstr "Jūsu Penpot" +msgstr "Mans Penpot" #: src/app/main/ui/alert.cljs msgid "ds.alert-ok" @@ -714,7 +826,7 @@ msgstr "Uzmanību" #: src/app/main/ui/confirm.cljs msgid "ds.component-subtitle" -msgstr "Atjaunināmās komponentes:" +msgstr "Atjaunināmās sastāvdaļas:" #: src/app/main/ui/confirm.cljs msgid "ds.confirm-cancel" @@ -733,7 +845,7 @@ msgid "errors.auth-provider-not-configured" msgstr "Autentifikācijas nodrošinātājs nav konfigurēts." msgid "errors.auth.unable-to-login" -msgstr "Šķiet, ka neesat autentificēts vai jums ir beidzies sesijas derīgums." +msgstr "Šķiet, ka neesi autentificēts vai ir beidzies sesijas derīgums." msgid "errors.bad-font" msgstr "Fontu %s nevarēja ielādēt" @@ -741,28 +853,36 @@ msgstr "Fontu %s nevarēja ielādēt" msgid "errors.bad-font-plural" msgstr "Fontus %s nevarēja ielādēt" +msgid "errors.cannot-upload" +msgstr "Nevar augšupielādēt multivides datni." + #: src/app/main/data/workspace.cljs msgid "errors.clipboard-not-implemented" -msgstr "Jūsu pārlūkprogramma nevar veikt šo darbību" +msgstr "Izmantotais pārlūks nevar veikt šo darbību" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/change_email.cljs msgid "errors.email-already-exists" -msgstr "E-pasts jau tiek lietots" +msgstr "E-pasta adrese jau tiek izmantota" #: src/app/main/ui/auth/verify_token.cljs msgid "errors.email-already-validated" -msgstr "E-pasts jau ir pārbaudīts." +msgstr "E-pasta adrese jau ir apliecināta." msgid "errors.email-as-password" msgstr "E-pastu nevar izmantot kā paroli" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" msgstr "E-pastam “%s” ir daudz pastāvīgu atlēcienu atskaišu." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, +#: src/app/main/ui/auth/recovery_request.cljs msgid "errors.email-invalid" -msgstr "Ievadiet derīgu e-pasta adresi, lūdzu" +msgstr "Lūgums ievadīt derīgu e-pasta adresi" #: src/app/main/ui/settings/change_email.cljs msgid "errors.email-invalid-confirmation" @@ -774,14 +894,15 @@ msgstr "E-pasta adrese “%s” ir atzīmēta surogātpasts vai pastāvīgi saņ #: src/app/main/errors.cljs msgid "errors.feature-mismatch" msgstr "" -"Šķiet, ka atverat failu, kurā ir iespējots līdzeklis '%s', bet jūsu Penpot " -"priekšgalsistēma to neatbalsta vai ir atspējota." +"Šķiet, ka tiek atvērta datne, kurā ir iespējota iespēja '%s', bet pašreizējā " +"Penpot versija to neatbalsta vai tā ir atspējota." #: src/app/main/errors.cljs msgid "errors.feature-not-supported" msgstr "Līdzeklis '%s' netiek atbalstīts." -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.generic" msgstr "Noticis kaut kas nelāgs." @@ -794,7 +915,7 @@ msgid "errors.invite-invalid" msgstr "Nederīgs ielūgums" msgid "errors.invite-invalid.info" -msgstr "Iespējams, šis uzaicinājums ir atcelts vai ir beidzies tā derīgums." +msgstr "Iespējams, ka šis uzaicinājums ir atcelts vai ir beidzies tā derīgums." #: src/app/main/ui/auth/login.cljs msgid "errors.ldap-disabled" @@ -802,7 +923,7 @@ msgstr "LDAP autentifikācija ir atspējota." #: src/app/main/errors.cljs msgid "errors.max-quote-reached" -msgstr "Ir sasniegts '%s' limits. Sazinieties ar atbalsta dienestu." +msgstr "Ir sasniegts '%s' ierobežojums. Jāsazinās ar atbalstu." #: src/app/main/data/workspace/persistence.cljs msgid "errors.media-too-large" @@ -810,17 +931,17 @@ msgstr "Attēls ir pārāk liels, lai to ievietotu." #: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-mismatch" -msgstr "Šķiet, ka attēla saturs neatbilst faila paplašinājumam." +msgstr "Šķiet, ka attēla saturs neatbilst datnes paplašinājumam." -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" -msgstr "Šķiet, ka tas nav derīgs attēls." +msgstr "Šķiet, ka šis nav derīgs attēls." #: src/app/main/ui/dashboard/team.cljs msgid "errors.member-is-muted" msgstr "" -"Profilam, kuru uzaicināt, e-pasta ziņojumi ir izslēgti (surogātpasts vai " -"daudz atlēcienu)." +"Uzaicinātajam profilam ir apklusināta e-pasta saņemšana (ziņojumi par " +"surogātpastu vai daudz atlēcienu)." #: src/app/main/ui/settings/password.cljs msgid "errors.password-invalid-confirmation" @@ -828,15 +949,17 @@ msgstr "Apstiprinājuma parolei ir jāsakrīt" #: src/app/main/ui/settings/password.cljs msgid "errors.password-too-short" -msgstr "Parolei jābūt vismaz 8 rakstzīmēm" +msgstr "Parolē ir jābūt vismaz 8 rakstzīmēm" msgid "errors.profile-blocked" msgstr "Profils ir bloķēts" -#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.profile-is-muted" msgstr "" -"Jūsu profila e-pasta ziņojumi ir izslēgti (surogātpasts vai daudz " +"Profila epasta saņemšana ir apklusināta (ziņojumi par surogātpastu vai daudz " "atlēcienu)." #: src/app/main/ui/auth/register.cljs @@ -844,7 +967,9 @@ msgid "errors.registration-disabled" msgstr "Reģistrācija pašlaik ir atspējota." msgid "errors.team-leave.insufficient-members" -msgstr "Nepietiek dalībnieku, lai pamestu komandu, iespējams, vēlaties to izdzēst." +msgstr "" +"Komandā ir nepietiekams dalībnieku skaits, lai to pamestu. Iespējams, ka to " +"ir vēlams izdzēst." msgid "errors.team-leave.member-does-not-exists" msgstr "Dalībnieks, kuru mēģināt piešķirt, nepastāv." @@ -852,13 +977,15 @@ msgstr "Dalībnieks, kuru mēģināt piešķirt, nepastāv." msgid "errors.team-leave.owner-cant-leave" msgstr "Īpašnieks nevar pamest komandu, ir jāpiešķir īpašnieka loma citam." -#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/data/media.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "errors.unexpected-error" -msgstr "Radās neparedzēta kļūda." +msgstr "Atgadījās neparedzēta kļūda." #: src/app/main/ui/auth/verify_token.cljs msgid "errors.unexpected-token" -msgstr "Nezināms marķieris" +msgstr "Nezināma tekstvienība" msgid "errors.webhooks.connection" msgstr "Savienojuma kļūda, URL nav sasniedzams" @@ -876,7 +1003,7 @@ msgid "errors.webhooks.timeout" msgstr "Noilgums" msgid "errors.webhooks.unexpected" -msgstr "Pārbaudes laikā radās neparedzēta kļūda" +msgstr "Pārbaudes laikā atgadījās neparedzēta kļūda" msgid "errors.webhooks.unexpected-status" msgstr "Neparedzēts statuss %s" @@ -900,8 +1027,8 @@ msgstr "Doties uz Penpot forumu" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.discourse-subtitle1" msgstr "" -"Mēs esam priecīgi, ka jūs esat šeit. Ja nepieciešama palīdzība, pirms " -"rakstiet, lūdzu, pameklējiet informāciju." +"Mēs esam priecīgi Tevi šeit redzēt. Ja ir nepieciešama palīdzība, lūgums " +"meklēt pirms ieraksta veikšanas." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.discourse-title" @@ -914,9 +1041,9 @@ msgstr "Temats" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subtitle" msgstr "" -"Lūdzu, aprakstiet e-pasta ziņojuma iemeslu, norādot, vai tā ir problēma, " -"ideja vai šaubas. Jums pēc iespējas ātrāk atbildēs kāds mūsu komandas " -"dalībnieks." +"Lūgums aprakstīt e-pasta ziņojuma iemeslu, norādot, vai tā ir nepilnība, " +"ierosinājums vai šaubas. Kāds mūsu komandas dalībnieks atbildēs pēc iespējas " +"ātrāk." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.title" @@ -924,7 +1051,7 @@ msgstr "E-pasts" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Atvērt Twitter" +msgstr "Atvērt X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -932,7 +1059,7 @@ msgstr "Šeit, lai palīdzētu ar tehniskajiem jautājumiem." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Twitter atbalsta konts" +msgstr "X atbalsta konts" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -986,7 +1113,8 @@ msgstr "Augstums" msgid "inspect.attributes.layout.left" msgstr "Kreisi" -#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs +#: src/app/main/ui/inspect/attributes/layout.cljs, +#: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.layout.radius" msgstr "Rādiuss" @@ -1008,21 +1136,18 @@ msgstr "Ēna" #: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.size" -msgstr "Lielums un novietojums" +msgstr "Izmērs un novietojums" #: src/app/main/ui/inspect/attributes/stroke.cljs msgid "inspect.attributes.stroke" msgstr "Vilkums" -#, permanent msgid "inspect.attributes.stroke.alignment.center" msgstr "Centrs" -#, permanent msgid "inspect.attributes.stroke.alignment.inner" msgstr "Iekšpuse" -#, permanent msgid "inspect.attributes.stroke.alignment.outer" msgstr "Ārpuse" @@ -1044,7 +1169,7 @@ msgstr "Platums" #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography" -msgstr "Tipogrāfija" +msgstr "Burtu stils un veids" #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.font-family" @@ -1058,6 +1183,10 @@ msgstr "Fonta izmērs" msgid "inspect.attributes.typography.font-style" msgstr "Fonta stils" +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-weight" +msgstr "Fonta Treknums" + #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.letter-spacing" msgstr "Burtu atstarpes" @@ -1096,13 +1225,15 @@ msgid "inspect.attributes.typography.text-transform.uppercase" msgstr "Lielie burti" msgid "inspect.empty.help" -msgstr "Ja vēlaties uzzināt vairāk par dizainu, skatiet Penpot palīdzības centru" +msgstr "" +"Ja ir vēlme uzzināt vairāk par dizaina apskati, jāapmeklē Penpot palīdzības " +"centrs" msgid "inspect.empty.more-info" -msgstr "Papildinformācija par pārbaudi" +msgstr "Vairāk informācijas par apskatīšanu" msgid "inspect.empty.select" -msgstr "Atlasiet formu, dēli vai grupu, lai pārbaudītu to rekvizītus un kodu" +msgstr "Jāatlasa apveids, plātne vai kopa, lai apskatītu to īpašības un kodu" #: src/app/main/ui/inspect/right_sidebar.cljs msgid "inspect.tabs.code" @@ -1112,16 +1243,16 @@ msgid "inspect.tabs.code.selected.circle" msgstr "Aplis" msgid "inspect.tabs.code.selected.component" -msgstr "Komponente" +msgstr "Sastāvdaļa" msgid "inspect.tabs.code.selected.curve" msgstr "Līkne" msgid "inspect.tabs.code.selected.frame" -msgstr "Tāfele" +msgstr "Plātne" msgid "inspect.tabs.code.selected.group" -msgstr "Grupa" +msgstr "Kopa" msgid "inspect.tabs.code.selected.image" msgstr "Attēls" @@ -1156,15 +1287,19 @@ msgstr "Saīsnes" msgid "labels.accept" msgstr "Pieņemt" +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "Piekļuves pilnvaras" + msgid "labels.active" msgstr "Aktīvs" msgid "labels.add-custom-font" msgstr "Pievienot pielāgotu fontu" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" -msgstr "Administrators" +msgstr "Pārvaldnieks" #: src/app/main/ui/workspace/comments.cljs msgid "labels.all" @@ -1179,8 +1314,8 @@ msgstr "Atpakaļ" #: src/app/main/ui/static.cljs msgid "labels.bad-gateway.desc-message" msgstr "" -"Izskatās, ka jums mazliet jāpagaida un jāmēģina vēlreiz; mēs veicam nelielu " -"mūsu serveru uzturēšanu." +"Izskatās, ka mazliet jāuzgaida un jāmēģina vēlreiz; mēs veicam nelielus mūsu " +"serveru uzturēšanas darbus." #: src/app/main/ui/static.cljs msgid "labels.bad-gateway.main-message" @@ -1222,22 +1357,24 @@ msgstr "Kopēt saiti" msgid "labels.create" msgstr "Izveidot" -#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team" msgstr "Izveidot jaunu komandu" #: src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team.placeholder" -msgstr "Ievadiet jaunas komandas nosaukumu" +msgstr "Jāievada jaunās komandas nosaukums" msgid "labels.custom-fonts" msgstr "Pielāgotie fonti" #: src/app/main/ui/settings/sidebar.cljs msgid "labels.dashboard" -msgstr "Infopanelis" +msgstr "Informācijas panelis" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "Dzēst" @@ -1255,29 +1392,35 @@ msgstr "Dzēst uzaicinājumu" #: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete-multi-files" -msgstr "%s failu dzēšana" +msgstr "Izdzēst %s datnes" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.discard" +msgstr "Atmest" + +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/files.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.drafts" msgstr "Melnraksti" #: src/app/main/ui/comments.cljs msgid "labels.edit" -msgstr "Rediģēt" +msgstr "Labot" msgid "labels.edit-file" -msgstr "Rediģēt failu" +msgstr "Labot datni" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "Redaktors" #: src/app/main/ui/dashboard/team.cljs msgid "labels.expired-invitation" -msgstr "Beidzies derīguma termiņš" +msgstr "Beidzies derīgums" msgid "labels.export" -msgstr "Eksportēt" +msgstr "Izgūt" #: src/app/main/ui/settings/feedback.cljs msgid "labels.feedback-disabled" @@ -1303,7 +1446,9 @@ msgstr "Fonti" msgid "labels.github-repo" msgstr "GitHub repozitorijs" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "Sniegt atsauksmi" @@ -1327,14 +1472,15 @@ msgstr "Instalētie fonti" #: src/app/main/ui/static.cljs msgid "labels.internal-error.desc-message" msgstr "" -"Notika kaut kas slikts. Lūdzu, mēģiniet vēlreiz un, ja problēma joprojām " -"pastāv, sazinieties ar atbalsta dienestu." +"Notika kaut kas slikts. Lūgums mēģināt vēlreiz un, ja sarežģījumi joprojām " +"pastāv, jāsazinās ar atbalstu." #: src/app/main/ui/static.cljs msgid "labels.internal-error.main-message" msgstr "Iekšēja kļūda" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.invitations" msgstr "Ielūgumi" @@ -1347,25 +1493,26 @@ msgid "labels.libraries-and-templates" msgstr "Bibliotēkas un veidnes" msgid "labels.log-or-sign" -msgstr "Piesakieties vai reģistrējieties" +msgstr "Pieteikties vai reģistrēties" #: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.logout" msgstr "Atteikties" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.member" msgstr "Dalībnieks" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "Dalībnieki" #: src/app/main/ui/settings/password.cljs msgid "labels.new-password" -msgstr "Jauna parole" +msgstr "Jaunā parole" -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +#: src/app/main/ui/workspace/comments.cljs, +#: src/app/main/ui/dashboard/comments.cljs msgid "labels.no-comments-available" msgstr "Jūs esat pieķerti! Šeit tiks parādīti jaunu komentāru paziņojumi." @@ -1376,12 +1523,12 @@ msgstr "Nav gaidošu uzaicinājumu." #: src/app/main/ui/dashboard/team.cljs msgid "labels.no-invitations-hint" msgstr "" -"Noklikšķiniet uz pogas **Uzaicināt personas**, lai uzaicinātu personas uz " -"šo komandu." +"Jānoklikšķina uz pogas **Uzaicināt cilvēkus**, lai šajā komandā uzaicinātu " +"cilvēkus." #: src/app/main/ui/static.cljs msgid "labels.not-found.desc-message" -msgstr "Iespējams, ka šī lapa nepastāv vai jums nav atļauju tai piekļūt." +msgstr "Šī lapa, iespējams, nepastāv, vai arī nav atļauju tai piekļūt." #: src/app/main/ui/static.cljs msgid "labels.not-found.main-message" @@ -1390,15 +1537,15 @@ msgstr "Ups!" #: src/app/main/ui/dashboard/team.cljs msgid "labels.num-of-files" msgid_plural "labels.num-of-files" -msgstr[0] "0 failu" -msgstr[1] "Fails" -msgstr[2] "%s faili" +msgstr[0] "0 datņu" +msgstr[1] "%s datne" +msgstr[2] "%s datnes" msgid "labels.num-of-frames" msgid_plural "labels.num-of-frames" -msgstr[0] "Nav dēļu" -msgstr[1] "1 dēlis" -msgstr[2] "%s dēļi" +msgstr[0] "%s plātņu" +msgstr[1] "%s plātne" +msgstr[2] "%s plātnes" #: src/app/main/ui/dashboard/team.cljs msgid "labels.num-of-projects" @@ -1413,7 +1560,7 @@ msgstr "Vecā parole" #: src/app/main/ui/workspace/comments.cljs msgid "labels.only-yours" -msgstr "Tikai Jūsu" +msgstr "Tikai mans" msgid "labels.or" msgstr "vai" @@ -1422,7 +1569,8 @@ msgstr "vai" msgid "labels.owner" msgstr "Īpašnieks" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.password" msgstr "Parole" @@ -1440,21 +1588,24 @@ msgstr "Projekti" #: src/app/main/ui/settings/sidebar.cljs msgid "labels.release-notes" -msgstr "Izlaides ziņas" +msgstr "Laidiena apraksts" #: src/app/main/ui/workspace.cljs msgid "labels.reload-file" -msgstr "Pārlādēt failu" +msgstr "Pārlādēt datni" -#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/workspace/libraries.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" -msgstr "Dzēst" +msgstr "Noņemt" #: src/app/main/ui/dashboard/team.cljs msgid "labels.remove-member" -msgstr "Dzēst dalībnieku" +msgstr "Noņemt dalībnieku" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.rename" msgstr "Pārdēvēt" @@ -1466,7 +1617,7 @@ msgstr "Pārdēvēt grupu" msgid "labels.resend-invitation" msgstr "Nosūtīt uzaicinājumu vēlreiz" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Pamēģināt vēlreiz" @@ -1496,12 +1647,13 @@ msgstr "Mēs esam ieplānotos sistēmu uzturēšanas darbos." msgid "labels.service-unavailable.main-message" msgstr "Pakalpojums nav pieejams" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.settings" msgstr "Iestatījumi" msgid "labels.share-prototype" -msgstr "Koplietot prototips" +msgstr "Kopīgot prototipu" #: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.shared-libraries" @@ -1516,7 +1668,7 @@ msgstr "Rādīt komentāru sarakstu" #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.show-your-comments" -msgstr "Rādīt tikai jūsu komentārus" +msgstr "Rādīt tikai manas piebildes" #: src/app/main/ui/dashboard/team.cljs msgid "labels.status" @@ -1524,11 +1676,11 @@ msgstr "Statuss" #: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.tutorials" -msgstr "Apmācības" +msgstr "Pamācības" #: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.unpublish-multi-files" -msgstr "%s failu publicēšanas atcelšana" +msgstr "Atcelt %s datņu publicēšanu" #: src/app/main/ui/settings/profile.cljs msgid "labels.update" @@ -1545,7 +1697,7 @@ msgid "labels.upload-custom-fonts" msgstr "Augšupielādēt pielāgotos fontus" msgid "labels.uploading" -msgstr "Notiek augšupielāde…" +msgstr "Augšupielādē…" msgid "labels.view-only" msgstr "TIKAI SKATĪT" @@ -1555,35 +1707,38 @@ msgid "labels.viewer" msgstr "Pārlūks" msgid "labels.webhooks" -msgstr "Tīmekļa āķi" +msgstr "Tīmekļa aizķeres" #: src/app/main/ui/comments.cljs msgid "labels.write-new-comment" msgstr "Rakstīt jaunu komentāru" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.you" msgstr "(Jūs)" #: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.your-account" -msgstr "Jūsu konts" +msgstr "Mans konts" -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "Ielādē attēlu…" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" msgstr "Pievienot kā koplietojamu bibliotēku" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.hint" msgstr "" -"Kad šīs failu bibliotēkas līdzekļi būs pievienoti kā koplietojamā " -"bibliotēka, tie būs pieejami izmantošanai starp pārējiem failiem." +"Tiklīdz šīs datņu bibliotēkas līdzekļi būs pievienot kā koplietojama " +"bibliotēka, tā tie būs pieejami izmantošanai arī pārējās datnēs." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.message" msgstr "Pievienot “%s” kā koplietojamu bibliotēku" @@ -1598,26 +1753,50 @@ msgstr "Pārbaudīt jauno e-pastu" #: src/app/main/ui/settings/change_email.cljs msgid "modals.change-email.info" msgstr "" -"Mēs nosūtīsim jums e-pasta ziņojumu uz jūsu pašreizējo e-pasta ziņojumu " -"“%s”, lai pārbaudītu jūsu identitāti." +"Mēs nosūtīsim e-pasta ziņojumu uz pašreizējo e-pasta adresi “%s”, lai " +"pārbaudītu identitāti." #: src/app/main/ui/settings/change_email.cljs msgid "modals.change-email.new-email" -msgstr "Jauns e-pasts" +msgstr "Jauna e-pasta adrese" #: src/app/main/ui/settings/change_email.cljs msgid "modals.change-email.submit" -msgstr "Mainīt e-pastu" +msgstr "Mainīt e-pasta adresi" #: src/app/main/ui/settings/change_email.cljs msgid "modals.change-email.title" msgstr "E-pasta maiņa" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "Ievietot pilnvaru starpliktuvē" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "Derīguma termiņš" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "Nosaukums" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "Nosaukums var palīdzēt saprast, kam pilnvara ir paredzēta" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "Izveidot pilnvaru" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "Izveidot jaunu piekļuves pilnvaru" + msgid "modals.create-webhook.submit-label" -msgstr "Izveidot tīmekļa āķi" +msgstr "Izveidot tīmekļa aizķeri" msgid "modals.create-webhook.title" -msgstr "Izveidot tīmekļa āķi" +msgstr "Izveidot tīmekļa aizķeri" msgid "modals.create-webhook.url.label" msgstr "Vērtuma URL" @@ -1625,6 +1804,18 @@ msgstr "Vērtuma URL" msgid "modals.create-webhook.url.placeholder" msgstr "https://example.com/postreceive" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "Izdzēst pilnvaru" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "Vai tiešām izdzēst šo pilnvaru?" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "Izdzēst pilnvaru" + #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.cancel" msgstr "Atcelt un paturēt manu kontu" @@ -1635,11 +1826,11 @@ msgstr "Jā, dzēst manu kontu" #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.info" -msgstr "Noņemot kontu, jūs zaudēsit visus pašreizējos projektus un arhīvus." +msgstr "Pēc konta noņemšanas tiks zaudēti visi pašreizējie projekti un arhīvi." #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.title" -msgstr "Vai tiešām vēlaties dzēst savu kontu?" +msgstr "Vai tiešām izdzēst savu kontu?" #: src/app/main/ui/comments.cljs msgid "modals.delete-comment-thread.accept" @@ -1648,56 +1839,60 @@ msgstr "Dzēst sarunu" #: src/app/main/ui/comments.cljs msgid "modals.delete-comment-thread.message" msgstr "" -"Vai tiešām vēlaties izdzēst šo sarunu? Visi komentāri šajā pavedienā tiks " -"dzēsti." +"Vai tiešām izdzēst šo sarunu? Visas šī pavediena piebildes tiks izdzēstas." #: src/app/main/ui/comments.cljs msgid "modals.delete-comment-thread.title" msgstr "Dzēst sarunu" +msgid "modals.delete-component-annotation.message" +msgstr "Vai tiešām izdzēst šo piezīmi?" + +msgid "modals.delete-component-annotation.title" +msgstr "Izdzēst piezīmi" + #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-confirm.accept" -msgstr "Dzēst failu" +msgstr "Izdzēst datni" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-confirm.message" -msgstr "Vai tiešām vēlaties izdzēst šo failu?" +msgstr "Vai tiešām izdzēst šo datni?" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-confirm.title" -msgstr "Faila dzēšana" +msgstr "Izdzēš datni" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-multi-confirm.accept" -msgstr "Failu dzēšana" +msgstr "Izdzēst datnes" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-multi-confirm.message" -msgstr "Vai tiešām vēlaties izdzēst %s failus?" +msgstr "Vai tiešām izdzēst %s datnes?" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-multi-confirm.title" -msgstr "Failu dzēšana (%s)" +msgstr "Izdzēš %s datnes" msgid "modals.delete-font-variant.message" msgstr "" -"Vai tiešām vēlaties izdzēst šo fonta stilu? Tas netiks ielādēts, ja tika " -"izmantots failā." +"Vai tiešām izdzēst šo fontu stilu? Tas netiks ielādēts, ja tiek izmantots " +"datnē." msgid "modals.delete-font-variant.title" msgstr "Fonta stila dzēšana" msgid "modals.delete-font.message" msgstr "" -"Vai tiešām vēlaties izdzēst šo fontu? Tas netiks ielādēts, ja tika " -"izmantots failā." +"Vai tiešām izdzēst šo fontu? Tas netiks ielādēts, ja tiek izmantots datnē." msgid "modals.delete-font.title" msgstr "Fonta dzēšana" #: src/app/main/ui/workspace/sidebar/sitemap.cljs msgid "modals.delete-page.body" -msgstr "Vai tiešām vēlaties izdzēst šo lapu?" +msgstr "Vai tiešām izdzēst šo lapu?" #: src/app/main/ui/workspace/sidebar/sitemap.cljs msgid "modals.delete-page.title" @@ -1709,79 +1904,49 @@ msgstr "Dzēst projektu" #: src/app/main/ui/dashboard/project_menu.cljs msgid "modals.delete-project-confirm.message" -msgstr "Vai tiešām vēlaties dzēst šo projektu?" +msgstr "Vai tiešām izdzēst šo projektu?" #: src/app/main/ui/dashboard/project_menu.cljs msgid "modals.delete-project-confirm.title" msgstr "Dzēst projektu" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.accept" msgid_plural "modals.delete-shared-confirm.accept" -msgstr[0] "Nav failu dzēšanai" -msgstr[1] "Dzēst failu" -msgstr[2] "Dzēst failus" +msgstr[0] "Nav izdzēšamu datņu" +msgstr[1] "Izdzēst datni" +msgstr[2] "Izdzēst datnes" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "Nav līdzekļu dzēšanai." -msgstr[1] "" -"Ja šo izdzēsīsit, šie līdzekļi vairs nebūs pieejami no citiem failiem. Jau " -"izmantotie līdzekļi paliks šajā failā (noformējums netiks grozīts!)." -msgstr[2] "" -"Ja šo izdzēsīsit, šie līdzekļi vairs nebūs pieejami no citiem failiem. Jau " -"izmantotie līdzekļi paliks šajā failā (noformējums netiks grozīts!)." +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "Tie nav aktivēti nevienā datnē." +msgstr[1] "Tas nav aktivēts nevienā datnē." +msgstr[2] "Tie nav aktivēti nevienā datnē." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "Nav līdzekļu dzēšanai." -msgstr[1] "" -"Ja šo izdzēsīsit, šie līdzekļi vairs nebūs pieejami no citiem failiem. Jau " -"izmantotie līdzekļi paliks šajā failā (noformējums netiks grozīts!)." -msgstr[2] "" -"Ja šo izdzēsīsit, šie līdzekļi vairs nebūs pieejami no citiem failiem. Jau " -"izmantotie līdzekļi paliks šajā failā (noformējums netiks grozīts!)." +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "Šīs bibliotēkas ir aktivētas šeit: " +msgstr[1] "Šī bibliotēka ir aktivēta šeit: " +msgstr[2] "Šīs bibliotēkas ir aktivētas šeit: " -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" -msgstr[0] "Nav failu dzēšanai?" -msgstr[1] "Vai tiešām vēlaties izdzēst šo failu?" -msgstr[2] "Vai tiešām vēlaties izdzēst šos failus?" +msgstr[0] "Nav izdzēšamu datņu" +msgstr[1] "Vai tiešām izdzēst šo datni?" +msgstr[2] "Vai tiešām izdzēst šīs datnes?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" -msgstr[0] "Nav bibliotēku līdzekļu dzēšanai." -msgstr[1] "" -"Neviens no šī faila bibliotēku līdzekļiem netiek lietots. Tie tiks izdzēsti " -"kopā ar failiem." -msgstr[2] "" -"Neviens no šo failu bibliotēku līdzekļiem netiek lietots. Tie tiks izdzēsti " -"kopā ar failiem." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Nav bibliotēku līdzekļu dzēšanai:" -msgstr[1] "Daži šī faila bibliotēku līdzekļi tiek izmantoti šeit:" -msgstr[2] "Daži šo failu bibliotēku līdzekļi tiek izmantoti šeit:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "Nav bibliotēku līdzekļu dzēšanai:" -msgstr[1] "Daži šī faila bibliotēku līdzekļi tiek izmantoti šeit:" -msgstr[2] "Daži šo failu bibliotēku līdzekļi tiek izmantoti šeit:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" -msgstr[0] "Nav failu dzēšanai" -msgstr[1] "Faila dzēšana" -msgstr[2] "Failu dzēšana" +msgstr[0] "Nav izdzēšamu datņu" +msgstr[1] "Izdzēš datni" +msgstr[2] "Izdzēš datnes" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.delete-team-confirm.accept" @@ -1790,8 +1955,8 @@ msgstr "Dzēst komandu" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.delete-team-confirm.message" msgstr "" -"Vai tiešām vēlaties dzēst šo komandu? Visi ar komandu saistītie projekti un " -"faili tiks neatgriezeniski dzēsti." +"Vai tiešām izdzēst šo komandu? Visi ar komandu saistītie projekti un datnes " +"tiks neatgriezeniski izdzēstas." #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.delete-team-confirm.title" @@ -1803,26 +1968,26 @@ msgstr "Dzēst dalībnieku" #: src/app/main/ui/dashboard/team.cljs msgid "modals.delete-team-member-confirm.message" -msgstr "Vai tiešām vēlaties izdzēst šo dalībnieku no komandas?" +msgstr "Vai tiešām izdzēst šo dalībnieku no komandas?" #: src/app/main/ui/dashboard/team.cljs msgid "modals.delete-team-member-confirm.title" msgstr "Dzēst komandas dalībnieku" msgid "modals.delete-webhook.accept" -msgstr "Dzēst tīmekļa āķi" +msgstr "Izdzēst tīmekļa aizķeri" msgid "modals.delete-webhook.message" -msgstr "Vai tiešām vēlaties izdzēst šo tīmekļa āķi?" +msgstr "Vai tiešām izdzēst šo tīmekļa aizķeri?" msgid "modals.delete-webhook.title" -msgstr "Notiek tīmekļa āķa dzēšana" +msgstr "Notiek tīmekļa aizķeres izdzēšana" msgid "modals.edit-webhook.submit-label" -msgstr "Rediģēt tīmekļa āķi" +msgstr "Labot tīmekļa aizķeri" msgid "modals.edit-webhook.title" -msgstr "Rediģēt tīmekļa āķi" +msgstr "Labot tīmekļa aizķeri" #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-member-confirm.accept" @@ -1831,6 +1996,11 @@ msgstr "Nosūtīt uzaicinājumu" msgid "modals.invite-member.emails" msgstr "E-pasta ziņojumi, atdalīti ar komatiem" +msgid "modals.invite-member.repeated-invitation" +msgstr "" +"Dažas no e-pasta adresēm ir pašreizējiem komandas dalībniekiem. Ielūgumi " +"viņiem netiks nosūtīti." + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-team-member.title" msgstr "Uzaicināt dalībniekus uz komandu" @@ -1838,23 +2008,23 @@ msgstr "Uzaicināt dalībniekus uz komandu" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-close-confirm.hint" msgstr "" -"Tā kā esat šīs komandas vienīgais dalībnieks, kopā ar tās projektiem un " -"failiem komanda tiks izdzēsta." +"Tā kā esi šīs komandas vienīgais dalībnieks, tā tiks izdzēsta līdz ar tās " +"projektiem un datnēm." #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-close-confirm.message" -msgstr "Vai tiešām vēlaties pamest %s komandu?" +msgstr "Vai tiešām pamest komandu %s?" msgid "modals.leave-and-reassign.forbidden" msgstr "" -"Jūs nevarat pamest komandu, ja nav cita dalībnieka, ko nozīmēt par " -"īpašnieku. Iespējams, ka vēlaties izdzēst grupu." +"Nevar pamest komandu, ja nav cita dalībnieka, ko norādīt kā īpašnieku. " +"Iespējams, ka komandu ir vēlams izdzēst." #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-reassign.hint1" msgstr "" -"Jūs esat šīs komandas īpašnieks. Pirms iziešanas nozīmējiet citu " -"dalībnieku, ko noteikt kā īpašnieku." +"Tu esi šīs komandas īpašnieks. Lūgums pirms pamešanas atlasīt citu " +"dalībnieku, lai to norādītu kā īpašnieku." #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-reassign.promote-and-leave" @@ -1862,7 +2032,7 @@ msgstr "Nozīmēt un pamest" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-reassign.select-member-to-promote" -msgstr "Atlasiet dalībnieku, ko nozīmēt" +msgstr "Atlasīt dalībnieku, ko nozīmēt" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-reassign.title" @@ -1874,7 +2044,7 @@ msgstr "Pamest komandu" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-confirm.message" -msgstr "Vai tiešām vēlaties pamest šo komandu?" +msgstr "Vai tiešām pamest šo komandu?" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-confirm.title" @@ -1886,35 +2056,47 @@ msgstr "Nobīdes apjoms" #: src/app/main/ui/dashboard/team.cljs msgid "modals.promote-owner-confirm.accept" -msgstr "Īpašumtiesību nodošana" +msgstr "Nodot īpašumtiesības" #: src/app/main/ui/dashboard/team.cljs msgid "modals.promote-owner-confirm.hint" msgstr "" -"Ja nodosit īpašumtiesības, jūs mainīsiet savu lomu uz Administrators, " -"zaudējot dažas atļaujas šajā komandā. " +"Ja tiks nodotas īpašumtiesības, ieņemamā loma tiks nomainīta uz " +"\"Pārvaldnieks\", zaudējot dažas atļaujas šajā komandā. " #: src/app/main/ui/dashboard/team.cljs msgid "modals.promote-owner-confirm.message" msgstr "" -"Jūs esat šīs komandas pašreizējais īpašnieks. Vai tiešām vēlaties padarīt " -"%s par jauno komandas īpašnieku?" +"Tu esi šīs komandas pašreizējais īpašnieks. Vai tiešām iecelt %s par jauno " +"komandas īpašnieku?" #: src/app/main/ui/dashboard/team.cljs msgid "modals.promote-owner-confirm.title" msgstr "Jauns komandas īpašnieks" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.publish-empty-library.accept" +msgstr "Publicēt" + +msgid "modals.publish-empty-library.message" +msgstr "Bibliotēka ir tukša. Vai tiešām publicēt to?" + +msgid "modals.publish-empty-library.title" +msgstr "Publicēt tukšu bibliotēku" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" msgstr "Noņemt kā koplietojamo bibliotēku" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.hint" msgstr "" -"Kad fails būs noņemts kā koplietojamā bibliotēka, šī faila failu bibliotēka " -"vairs nebūs pieejama izmantošanai starp pārējiem Jūsu failiem." +"Tiklīdz šīs datnes datņu bibliotēka būs noņemta kā koplietojama bibliotēka, " +"tā pārstās būt pieejama izmantošanai pārējās datnēs." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.message" msgstr "Noņemt “%s” kā koplietojamu bibliotēku" @@ -1922,101 +2104,63 @@ msgstr "Noņemt “%s” kā koplietojamu bibliotēku" msgid "modals.small-nudge" msgstr "Maza nobīde" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.accept" msgid_plural "modals.unpublish-shared-confirm.accept" msgstr[0] "Nav atlases" msgstr[1] "Atcelt publicēšanu" msgstr[2] "Atcelt publicēšanu" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "Nav atlasīto publikāciju." -msgstr[1] "" -"Ja beigsiet publikāciju, šie līdzekļi vairs nebūs pieejami no citiem " -"failiem. Jau izmantotie līdzekļi paliks šajā failā (noformējums netiks " -"grozīts!)." -msgstr[2] "" -"Ja beigsiet publikāciju, šie līdzekļi vairs nebūs pieejami no citiem " -"failiem. Jau izmantotie līdzekļi paliks šajā failā (noformējums netiks " -"grozīts!)." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "Nav atlasīto publikāciju." -msgstr[1] "" -"Ja beigsiet publikāciju, šie līdzekļi vairs nebūs pieejami no citiem " -"failiem. Jau izmantotie līdzekļi paliks šajā failā (noformējums netiks " -"grozīts!)." -msgstr[2] "" -"Ja beigsiet publikāciju, šie līdzekļi vairs nebūs pieejami no citiem " -"failiem. Jau izmantotie līdzekļi paliks šajā failā (noformējums netiks " -"grozīts!)." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" msgid_plural "modals.unpublish-shared-confirm.message" -msgstr[0] "Nav izvēlēto bibliotēku?" -msgstr[1] "Vai tiešām vēlaties atcelt šīs bibliotēkas publicēšanu?" -msgstr[2] "Vai tiešām vēlaties atcelt šo bibliotēku publicēšanu?" +msgstr[0] "Nav izvēlēta neviena bibliotēka" +msgstr[1] "Vai tiešām atcelt šīs bibliotēkas publicēšanu?" +msgstr[2] "Vai tiešām atcelt šo bibliotēku publicēšanu?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "Neviens līdzeklis no šīs bibliotēkas netiek izmantots." -msgstr[1] "Neviens līdzeklis no šīs bibliotēkas netiek izmantots." -msgstr[2] "Neviens līdzeklis no šīm bibliotēkām netiek izmantots." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Daži līdzekļi no šīs bibliotēkās tiek izmantoti šeit:" -msgstr[1] "Daži līdzekļi no šīs bibliotēkās tiek izmantoti šeit:" -msgstr[2] "Daži līdzekļi no šīm bibliotēkām tiek izmantoti šeit:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "Daži līdzekļi no šīs bibliotēkās tiek izmantoti šeit:" -msgstr[1] "Daži līdzekļi no šīs bibliotēkās tiek izmantoti šeit:" -msgstr[2] "Daži līdzekļi no šīm bibliotēkām tiek izmantoti šeit:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" msgid_plural "modals.unpublish-shared-confirm.title" -msgstr[0] "Bibliotēkas publicēšanas atcelšana" -msgstr[1] "Bibliotēkas publicēšanas atcelšana" -msgstr[2] "Bibliotēku publicēšanas atcelšana" +msgstr[0] "Atcelt bibliotēkas publicēšanu" +msgstr[1] "Atcelt bibliotēkas publicēšanu" +msgstr[2] "Atcelt bibliotēku publicēšanu" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.hint" msgstr "" -"Jūs atjaunināt koplietotā bibliotēkā esošās komponentes. Tas var ietekmēt " -"citus failus, kas to izmanto." +"Tiks atjauninātas sastāvdaļas koplietojamā bibliotēkā. Tas var ietekmēt " +"citas datnes, kurās tās ir izmantotas." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.message" -msgstr "Komponenšu atjaunināšana koplietojamā bibliotēkā" +msgstr "Atjaunināt sastāvdaļas koplietojamā bibliotēkā" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.accept" msgstr "Atjaunināt" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.cancel" msgstr "Atcelt" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.hint" msgstr "" -"Jūs atjaunināt koplietotā bibliotēkā esošās komponentes. Tas var ietekmēt " -"citus failus, kas to izmanto." +"Tiks atjaunināta koplietojamas bibliotēkas sastāvdaļa. Tas var ietekmēt " +"citas datnes, kurās tā ir izmantota." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.message" -msgstr "Komponentes atjaunināšana koplietojamā bibliotēkā" +msgstr "Atjaunināt sastāvdaļu koplietojamā bibliotēkā" #: src/app/main/ui/dashboard/team.cljs msgid "notifications.invitation-email-sent" @@ -2028,54 +2172,59 @@ msgstr "Uzaicinājuma saite nokopēta" #: src/app/main/ui/settings/delete_account.cljs msgid "notifications.profile-deletion-not-allowed" -msgstr "Profilu nevar izdzēst. Pirms turpināt, veiciet tiesību piešķiršanu komandās." +msgstr "" +"Profilu nevar izdzēst. Pirms turpināšanas jāpiešķir savas komandas citiem." #: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/options.cljs msgid "notifications.profile-saved" -msgstr "Profils ir veiksmīgi saglabāts!" +msgstr "Profils ir veiksmīgi saglabāts." #: src/app/main/ui/settings/change_email.cljs msgid "notifications.validation-email-sent" -msgstr "Uz %s nosūtīts pārbaudes e-pasta ziņojums. Pārbaudiet savu e-pastu!" +msgstr "" +"Patiesuma pārbaudes e-pasta ziņojums tika nosūtīts uz %s. Jāpārbauda savs " +"e-pasts." msgid "onboarding-v2.before-start.desc1" msgstr "" -"Jums jāzina, ka ir pieejami daudzi resursi, lai palīdzētu Jums sākt darbu " -"ar Penpot, piemēram, Lietotāja rokasgrāmata un mūsu Youtube kanāls." +"Jāņem vērā, ka ir pieejami daudz avotu, kas var palīdzēt uzsākt darbu ar " +"Penpot, piemēram, lietotāja rokasgrāmata un mūsu Youtube kanāls." msgid "onboarding-v2.before-start.desc2" msgstr "" -"Detalizēta informācija par Penpot lietošanu. No prototipa izveides līdz " -"dizaina organizēšanai vai koplietošanai." +"Izvērsta informācija par Penpot izmantošanu. No prototipa izveides līdz " +"dizainu kārtošanai vai kopīgošanai." msgid "onboarding-v2.before-start.desc2.title" msgstr "Lietotāja rokasgrāmata" msgid "onboarding-v2.before-start.desc3" -msgstr "Jūs varat apskatīties mūsu apmācības un mūsu kopienas māteriālus." +msgstr "" +"Ir iespējams apskatīt mūsu pamācības un mūsu kopienas izveidotās pamācības." msgid "onboarding-v2.before-start.desc3.title" -msgstr "Video apmācības" +msgstr "Video pamācības" msgid "onboarding-v2.before-start.title" -msgstr "Pirms sākat" +msgstr "Pirms sākt" msgid "onboarding-v2.newsletter.desc" msgstr "" -"Abonējiet Penpot biļetenu, lai saņemtu jaunāko informāciju par produktu " -"izstrādes gaitu un jaunumiem." +"Abonēt Penpot biļetenu, lai uzzinātu par produkta izstrādes gaitu un " +"jaunumiem." msgid "onboarding-v2.newsletter.news" -msgstr "Sūtiet man ziņas par Penpot (emuāra ziņas, video apmācības, straumēs...)." +msgstr "" +"Sūtīt man jaunumus par Penpot (emuāra ieraksti, video pamācības, " +"straumēšanas...)." msgid "onboarding-v2.newsletter.privacy1" msgstr "Mums rūp privātums, šeit var lasīt mūsu " msgid "onboarding-v2.newsletter.privacy2" msgstr "" -"Mēs jums nosūtīsim tikai attiecīgus e-pasta ziņojumus. Abonementu var " -"anulēt jebkurā laikā, izmantojot abonēšanas atcelšanas saiti jebkurā mūsu " -"biļetenī." +"Mēs nosūtīsim tikai atbilstošus e-pasta ziņojumus. Atteikt abonēšanu var " +"jebkurā laikā ar abonēšanas atteikšanas saiti jebkurā mūsu biļetenā." msgid "onboarding-v2.newsletter.updates" msgstr "" @@ -2084,56 +2233,44 @@ msgstr "" msgid "onboarding-v2.welcome.desc1" msgstr "" -"Penpot ir Atvērtā pirmkoda programmatūra, un to veido Kaleidos, kā arī " -"kopiena, kur daudzi cilvēki jau palīdz viens otram. Visi var sadarboties:" +"Penpot ir Atvērtā pirmkoda lietotne, un to izstrādā Kaleidos, kā arī " +"kopiena, kurā daudz cilvēku jau palīdz cits citam. Visi var sadarboties:" msgid "onboarding-v2.welcome.desc2" msgstr "" "Publiska telpa, lai mācītos, dalītos un apspriestu Penpot, tās tagadni un " -"nākotni ar visu Kopienu un Penpot komandu." +"nākotni ar visu kopienu un Penpot kodola komandu." msgid "onboarding-v2.welcome.desc2.title" msgstr "Dalība Kopienā" msgid "onboarding-v2.welcome.desc3" msgstr "" -"Kur atradīsiet, kā piedalīties ar tulkojumiem, funkciju pieprasījumiem, " -"kļūdām …" +"Kur būs atrodams, kā līdzdarboties pie tulkojumiem, iespēju pieprasījumiem, " +"devuma kodolam, kļūdu meklēšanas…" msgid "onboarding-v2.welcome.desc3.title" -msgstr "Dalības rokasgrāmata" +msgstr "Līdzdalības rokasgrāmata" msgid "onboarding-v2.welcome.title" msgstr "Laipni lūdzam Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "Izveidot komandu vēlāk" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Jūsu komandas nosaukums" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "" -"Pēc savas komandas nosaukšanas jūs varēsiet uzaicināt cilvēkus tai " +"Pēc komandas nosaukuma piešķiršanas varēs uzaicināt cilvēkus tai " "pievienoties." msgid "onboarding.choice.team-up.create-team-placeholder" -msgstr "Ievadiet komandas nosaukumu" +msgstr "Jāievada komandas nosaukums" msgid "onboarding.choice.team-up.invite-members" msgstr "Uzaicināt dalībniekus" msgid "onboarding.choice.team-up.invite-members-info" msgstr "" -"Neaizmirstiet iekļaut visus. Izstrādātāji, dizaineri, menedžeri... viedokļu " +"Jāatceras iekļaut visi. Izstrādātāji, dizaineri, vadītāji... Viedokļu " "dažādībā ir spēks :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Izveidot komandu un uzaicināt vēlāk" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Izveidot komandu un sūtīt uzaicinājumus" - msgid "onboarding.choice.team-up.roles" msgstr "Uzaicināt ar lomu:" @@ -2142,25 +2279,25 @@ msgstr "Jā, abonēt" msgid "onboarding.newsletter.acceptance-message" msgstr "" -"Jūsu abonementa pieprasījums ir nosūtīts, un mēs nosūtīsim jums e-pasta " -"ziņojumu, lai to apstiprinātu." +"Abonēšanas pieprasījums ir nosūtīts, un mēs nosūtīsim e-pasta ziņojumu tā " +"apstiprināšanai." msgid "onboarding.newsletter.policy" msgstr "Konfidencialitātes politika." msgid "onboarding.newsletter.title" -msgstr "Vai vēlaties saņemt Penpot ziņas?" +msgstr "Vai saņemt Penpot jaunumus?" msgid "onboarding.team-modal.create-team" msgstr "Izveidot komandu" msgid "onboarding.team-modal.create-team-desc" msgstr "" -"Komanda ļauj sadarboties ar citiem Penpot lietotājiem, kas strādā tajos " -"pašos failos un projektos." +"Komanda ļauj sadarboties ar citiem Penpot lietotājiem, kas darbojas ar tām " +"pašām datnēm un projektiem." msgid "onboarding.team-modal.create-team-feature-1" -msgstr "Neierobežoti faili un projekti" +msgstr "Neierobežotas datnes un projekti" msgid "onboarding.team-modal.create-team-feature-2" msgstr "Vairāku lietotāju izdevums" @@ -2185,9 +2322,152 @@ msgstr "Penpot" #: src/app/main/ui/auth/recovery.cljs msgid "profile.recovery.go-to-login" -msgstr "Pāriet uz pieteikšanos" +msgstr "Doties uz pieteikšanos" -#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "Ar kuru no šiem rīkiem ir bijusi lielākā pieredze?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11-30" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2-10" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31-50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "Plaša" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canva" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.describe-your-experience-working-on" +msgstr "Kā vislabāk raksturotu savu pieredzi strādājot pie..." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "Atklāt vairāk par Penpot" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "Esmu ārštātnieks(-ce)/Pašnodarbināts(-ā)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "Koda iegūšana no manas grupas projekta " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "... saskarnes dizains, vizuālie līdzekļi, dizaina sistēmas utt." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "InVision" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "Atstāt atsauksmes par manas grupas projektu" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "Ķeramies pie darba!" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "Vairāk nekā 50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "Nākamais" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "Nav" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "Cits (jānorāda)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "Strādāju personīgā projektā" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "Iepriekšējais" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "Kā ir iecerēts izmantot Penpot?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.select-option" +msgstr "Atlasīt iespēju" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "Sketch" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.some" +msgstr "Nelielā" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "Sākt" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "Sākt strādāt pie sava projekta" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "Cik liela ir komanda?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "Izmēģini Penpot, lai saprastu, vai tā der komandai " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "Izmēģināt pirms izvietotas Penpot izmantošanas" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" +msgstr "... struktūrskices, lietotāju ceļi un plūsmas, pārvietošanās koki utt." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "Darbs koncepcijas idejās" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" +msgstr "" +"Atsauksmes palīdzēs mums saprast, kādi ir lietotāju paradumi un izvēles, lai " +"mēs varētu turpināt padarīt Penpot par noderīgu un patīkamu rīku." + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "Jaukts" @@ -2196,7 +2476,7 @@ msgid "shortcut-section.basics" msgstr "Pamati" msgid "shortcut-section.dashboard" -msgstr "Panelis" +msgstr "Informācijas panelis" msgid "shortcut-section.viewer" msgstr "Pārlūks" @@ -2206,10 +2486,10 @@ msgstr "Darbvieta" # SUBSECTIONS msgid "shortcut-subsection.alignment" -msgstr "Līdzinājums" +msgstr "Līdzināšana" msgid "shortcut-subsection.edit" -msgstr "Rediģēt" +msgstr "Labot" msgid "shortcut-subsection.general-dashboard" msgstr "Vispārējs" @@ -2241,6 +2521,9 @@ msgstr "Ceļi" msgid "shortcut-subsection.shape" msgstr "Formas" +msgid "shortcut-subsection.text-editor" +msgstr "Teksti" + msgid "shortcut-subsection.tools" msgstr "Rīki" @@ -2257,25 +2540,34 @@ msgid "shortcuts.add-node" msgstr "Pievienot mezglu" msgid "shortcuts.align-bottom" -msgstr "Līdzināt uz leju" +msgstr "Līdzināt pie apakšas" + +msgid "shortcuts.align-center" +msgstr "Līdzināt vidū" msgid "shortcuts.align-hcenter" -msgstr "Centrēt horizontāli" +msgstr "Līdzināt vidū līmeniski" + +msgid "shortcuts.align-justify" +msgstr "Līdzināt pie abām malām" msgid "shortcuts.align-left" -msgstr "Līdzināt pa kreisi" +msgstr "Līdzināt pie kreisās malas" msgid "shortcuts.align-right" msgstr "Līdzināt pa labi" msgid "shortcuts.align-top" -msgstr "Līdzināt uz augšu" +msgstr "Līdzināt pie augšas" msgid "shortcuts.align-vcenter" -msgstr "Centrēt vertikāli" +msgstr "Līdzināt vidū stateniski" msgid "shortcuts.artboard-selection" -msgstr "Izveidot dēli no atlases" +msgstr "Izveidot plātni no atlases" + +msgid "shortcuts.bold" +msgstr "Pārslēgt treknrakstu" msgid "shortcuts.bool-difference" msgstr "Būla starpība" @@ -2308,7 +2600,7 @@ msgid "shortcuts.copy" msgstr "Kopēt" msgid "shortcuts.create-component" -msgstr "Izveidot komponenti" +msgstr "Izveidot sastāvdaļu" msgid "shortcuts.create-new-project" msgstr "Izveidot jaunu" @@ -2326,7 +2618,7 @@ msgid "shortcuts.delete-node" msgstr "Dzēst mezglu" msgid "shortcuts.detach-component" -msgstr "Atdalīt komponenti" +msgstr "Atdalīt sastāvdaļu" msgid "shortcuts.draw-curve" msgstr "Līkne" @@ -2335,7 +2627,7 @@ msgid "shortcuts.draw-ellipse" msgstr "Elipse" msgid "shortcuts.draw-frame" -msgstr "Dēlis" +msgstr "Plātne" msgid "shortcuts.draw-nodes" msgstr "Zīmēt ceļu" @@ -2350,22 +2642,28 @@ msgid "shortcuts.draw-text" msgstr "Teksts" msgid "shortcuts.duplicate" -msgstr "Dublikāts" +msgstr "Divkāršot" msgid "shortcuts.escape" msgstr "Atcelt" msgid "shortcuts.export-shapes" -msgstr "Eksportēt formas" +msgstr "Izgūt apveidus" msgid "shortcuts.fit-all" -msgstr "Tālummaiņa, lai ietilpinātu visu" +msgstr "Tālummainīt, lai ietilpinātu visu" msgid "shortcuts.flip-horizontal" -msgstr "Apvērst horizontāli" +msgstr "Apvērst līmeniski" msgid "shortcuts.flip-vertical" -msgstr "Apvērst vertikāli" +msgstr "Apvērst stateniski" + +msgid "shortcuts.font-size-dec" +msgstr "Samazināt fonta izmēru" + +msgid "shortcuts.font-size-inc" +msgstr "Palielināt fonta izmēru" msgid "shortcuts.go-to-drafts" msgstr "Doties uz melnrakstiem" @@ -2377,13 +2675,13 @@ msgid "shortcuts.go-to-search" msgstr "Meklēt" msgid "shortcuts.group" -msgstr "Grupa" +msgstr "Apkopot" msgid "shortcuts.h-distribute" -msgstr "Izkliedēt horizontāli" +msgstr "Izkliedēt līmeniski" msgid "shortcuts.hide-ui" -msgstr "Rādīt/paslēpt lietotāja interfeisu" +msgstr "Rādīt / paslēpt lietotāja saskarni" msgid "shortcuts.increase-zoom" msgstr "Tuvināt" @@ -2391,9 +2689,27 @@ msgstr "Tuvināt" msgid "shortcuts.insert-image" msgstr "Ievietot attēlu" +msgid "shortcuts.italic" +msgstr "Pārslēgt slīprakstu" + msgid "shortcuts.join-nodes" msgstr "Savienot mezglus" +msgid "shortcuts.letter-spacing-dec" +msgstr "Samazināt burtstarpu" + +msgid "shortcuts.letter-spacing-inc" +msgstr "Palielināt burtstarpu" + +msgid "shortcuts.line-height-dec" +msgstr "Samazināt līnijas augstumu" + +msgid "shortcuts.line-height-inc" +msgstr "Palielināt līnijas augstumu" + +msgid "shortcuts.line-through" +msgstr "Pārslēgt svītrojumu" + msgid "shortcuts.make-corner" msgstr "Izveidot stūri" @@ -2410,22 +2726,22 @@ msgid "shortcuts.move" msgstr "Pārvietot" msgid "shortcuts.move-fast-down" -msgstr "Ātri pārvietoties uz leju" +msgstr "Strauji pārvietot uz leju" msgid "shortcuts.move-fast-left" -msgstr "Ātri pārvietoties uz kreisi" +msgstr "Strauji pārvietot pa kreisi" msgid "shortcuts.move-fast-right" -msgstr "Ātri pārvietoties pa labi" +msgstr "Strauji pārvietot pa labi" msgid "shortcuts.move-fast-up" -msgstr "Ātri pārvietoties uz augšu" +msgstr "Strauji pārvietot uz augšu" msgid "shortcuts.move-nodes" msgstr "Pārvietot mezglu" msgid "shortcuts.move-unit-down" -msgstr "Pārvietoties lejup" +msgstr "Pārvietot uz leju" msgid "shortcuts.move-unit-left" msgstr "Pārvietot pa kreisi" @@ -2434,10 +2750,10 @@ msgid "shortcuts.move-unit-right" msgstr "Pārvietot pa labi" msgid "shortcuts.move-unit-up" -msgstr "Pārvietoties augšup" +msgstr "Pārvietot uz augšu" msgid "shortcuts.next-frame" -msgstr "Nākamais dēlis" +msgstr "Nākamā plātne" msgid "shortcuts.not-found" msgstr "Saīsnes nav atrastas" @@ -2479,16 +2795,16 @@ msgid "shortcuts.open-comments" msgstr "Atvērt pārlūka komentāru sadaļu" msgid "shortcuts.open-dashboard" -msgstr "Doties uz Infopaneli" +msgstr "Doties uz informācijas paneli" msgid "shortcuts.open-inspect" -msgstr "Doties uz pārlūka izpētes sekciju" +msgstr "Doties uz skatītāja apskatīšanas sadaļu" msgid "shortcuts.open-interactions" -msgstr "Doties uz pārlūka mijiedarbības sadaļu" +msgstr "Doties uz skatītāja mijiedarbības sadaļu" msgid "shortcuts.open-viewer" -msgstr "Doties uz pārlūka mijiedarbības sadaļu" +msgstr "Doties uz skatītāja mijiedarbības sadaļu" msgid "shortcuts.open-workspace" msgstr "Doties uz darbvietu" @@ -2500,7 +2816,7 @@ msgid "shortcuts.paste" msgstr "Ielīmēt" msgid "shortcuts.prev-frame" -msgstr "Iepriekšējais dēlis" +msgstr "Iepriekšējā plātne" msgid "shortcuts.redo" msgstr "Atatsaukt" @@ -2514,6 +2830,15 @@ msgstr "Meklēt saīsnes" msgid "shortcuts.select-all" msgstr "Atlasīt visu" +msgid "shortcuts.select-next" +msgstr "Atlasīt nākamo slāni" + +msgid "shortcuts.select-parent-layer" +msgstr "Atlasīt vecākslāni" + +msgid "shortcuts.select-prev" +msgstr "Atlasīt iepriekšējo slāni" + msgid "shortcuts.separate-nodes" msgstr "Atdalīt mezglus" @@ -2530,7 +2855,7 @@ msgid "shortcuts.snap-pixel-grid" msgstr "Pieķerties pikseļu režģim" msgid "shortcuts.start-editing" -msgstr "Sākt rediģēšanu" +msgstr "Sākt labošanu" msgid "shortcuts.start-measure" msgstr "Sākt mērīšanu" @@ -2538,6 +2863,18 @@ msgstr "Sākt mērīšanu" msgid "shortcuts.stop-measure" msgstr "Beigt mērīšanu" +msgid "shortcuts.text-align-center" +msgstr "Līdzināt vidū" + +msgid "shortcuts.text-align-justify" +msgstr "Līdzināt pie abām malām" + +msgid "shortcuts.text-align-left" +msgstr "Līdzināt pa kreisi" + +msgid "shortcuts.text-align-right" +msgstr "Līdzināt pa labi" + msgid "shortcuts.thumbnail-set" msgstr "Iestatīt sīktēlus" @@ -2546,7 +2883,7 @@ msgid "shortcuts.title" msgstr "Īsinājumtaustiņi" msgid "shortcuts.toggle-alignment" -msgstr "Pārslēgt dinamisko līdzinājumu" +msgstr "Pārslēgt dinamisko līdzināšanu" msgid "shortcuts.toggle-assets" msgstr "Pārslēgt līdzekļus" @@ -2560,9 +2897,6 @@ msgstr "Pārslēgt fokusa režīmu" msgid "shortcuts.toggle-fullscreen" msgstr "Pārslēgt pilnekrāna režīmu" -msgid "shortcuts.toggle-grid" -msgstr "Rādīt/paslēpt režģi" - msgid "shortcuts.toggle-history" msgstr "Pārslēgt vēsturi" @@ -2570,47 +2904,54 @@ msgid "shortcuts.toggle-layers" msgstr "Pārslēgt slāņus" msgid "shortcuts.toggle-layout-flex" -msgstr "Pievienot/noņemt elastīgo izkārtojumu" +msgstr "Pievienot / Noņemt elastīgo izkārtojumu" msgid "shortcuts.toggle-lock" -msgstr "Bloķēt atlasi" +msgstr "Slēgt / Atslēgt" msgid "shortcuts.toggle-lock-size" -msgstr "Bloķēt proporcijas" +msgstr "Slēgt proporcijas" msgid "shortcuts.toggle-rules" msgstr "Rādīt/paslēpt mērjoslas" -msgid "shortcuts.toggle-scale-text" -msgstr "Pārslēgt teksta mērogošanu" - -msgid "shortcuts.toggle-snap-grid" -msgstr "Pieķerties režģim" - -msgid "shortcuts.toggle-snap-guide" -msgstr "Pieķerties vadotnēm" - msgid "shortcuts.toggle-textpalette" msgstr "Pārslēgt teksta paleti" +msgid "shortcuts.toggle-visibility" +msgstr "Pārslēgt redzamību" + msgid "shortcuts.toggle-zoom-style" msgstr "Pārslēgt tālummaiņas stilu" +msgid "shortcuts.underline" +msgstr "Pārslēgt pasvītrojumu" + msgid "shortcuts.undo" msgstr "Atsaukt" msgid "shortcuts.ungroup" -msgstr "Atgrupēt" +msgstr "Atapkopot" msgid "shortcuts.unmask" msgstr "Noņemt masku" msgid "shortcuts.v-distribute" -msgstr "Izkliedēt vertikāli" +msgstr "Izkliedēt stateniski" + +msgid "shortcuts.zoom-lense-decrease" +msgstr "Tālummaiņas samazinājums" + +msgid "shortcuts.zoom-lense-increase" +msgstr "Tālummaiņas palielinājums" msgid "shortcuts.zoom-selected" msgstr "Tālummainīt uz atlasi" +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "Tīmekļa aizķeres nosaukumā drīkst būt ne vairāk kā 2048 rakstzīmes." + #: src/app/main/ui/dashboard/files.cljs msgid "title.dashboard.files" msgstr "%s - Penpot" @@ -2637,7 +2978,11 @@ msgstr "Koplietojamās bibliotēkas - %s - Penpot" #: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/auth.cljs msgid "title.default" -msgstr "Penpot - Dizaina brīvība komandām" +msgstr "Penpot - Modelēšanas brīvība komandām" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "Profils - piekļuves pilnvaras" #: src/app/main/ui/settings/feedback.cljs msgid "title.settings.feedback" @@ -2645,7 +2990,7 @@ msgstr "Sniegt atsauksmes - Penpot" #: src/app/main/ui/settings/options.cljs msgid "title.settings.options" -msgstr "Iestātijumi - Penpot" +msgstr "Iestatījumi - Penpot" #: src/app/main/ui/settings/password.cljs msgid "title.settings.password" @@ -2668,11 +3013,11 @@ msgid "title.team-settings" msgstr "Iestatījumi - %s - Penpot" msgid "title.team-webhooks" -msgstr "Tīkla āķi - %s - Penpot" +msgstr "Tīmekļa aizķeres - %s - Penpot" #: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs msgid "title.viewer" -msgstr "%s - skata režīms - Penpot" +msgstr "%s - skatīšana - Penpot" #: src/app/main/ui/workspace.cljs msgid "title.workspace" @@ -2680,19 +3025,18 @@ msgstr "%s - Penpot" msgid "viewer.breaking-change.description" msgstr "" -"Šī kopīgojamā saite vairs nav derīga. Izveidojiet jaunu vai lūdziet " -"īpašniekam jaunu." +"Šī kopīgojamā saite vairs nav derīga. Jāizveido vai jālūdz īpašniekam jauna." msgid "viewer.breaking-change.message" msgstr "Piedošanu!" #: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs msgid "viewer.empty-state" -msgstr "Lapā nav atrasts neviens dēlis." +msgstr "Lapā nav atrasta neviena plātne." #: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs msgid "viewer.frame-not-found" -msgstr "Dēlis nav atrasts." +msgstr "Plātne netika atrasta." msgid "viewer.header.comments-section" msgstr "Komentāri (%s)" @@ -2706,7 +3050,7 @@ msgid "viewer.header.fullscreen" msgstr "Pilnekrāns" msgid "viewer.header.inspect-section" -msgstr "Pārbaudīt (%s)" +msgstr "Apskatīt (%s)" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.interactions" @@ -2729,18 +3073,18 @@ msgstr "Rādīt mijiedarbības pēc klikšķa" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.sitemap" -msgstr "Tīmekļa vietnes karte" +msgstr "Vietnes karte" msgid "webhooks.last-delivery.success" msgstr "Pēdējā piegāde bija veiksmīga." #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.hcenter" -msgstr "Horizontālā centra līdzināšana (%s)" +msgstr "Līdzināt līmeniskajā vidū (%s)" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.hdistribute" -msgstr "Sadalīt horizontālo atstarpi (%s)" +msgstr "Izlīdzināt līmeniskās atstarpes (%s)" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.hleft" @@ -2752,19 +3096,19 @@ msgstr "Līdzināt pa labi (%s)" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.vbottom" -msgstr "Līdzināt uz leju (%s)" +msgstr "Līdzināt pie apakšas (%s)" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.vcenter" -msgstr "Līdzināt uz vertikālo centru (%s)" +msgstr "Līdzināt stateniskajā vidū (%s)" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.vdistribute" -msgstr "Izkliedēt vertikālo atstarpi (%s)" +msgstr "Izlīdzināt stateniskās atstarpes (%s)" #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.vtop" -msgstr "Līdzināt uz augšu (%s)" +msgstr "Līdzināt pie augšas (%s)" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.assets" @@ -2774,47 +3118,53 @@ msgstr "Līdzekļi" msgid "workspace.assets.box-filter-all" msgstr "Visi līdzekļi" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.colors" msgstr "Krāsas" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.components" -msgstr "Componentes" +msgstr "Sastāvdaļas" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.create-group" -msgstr "Izveidot grupu" +msgstr "Izveidot kopu" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.create-group-hint" msgstr "" -"Jūsu vienumi tiks automātiski nosaukti kā “grupas nosaukums/vienuma " -"nosaukums”" +"Vienumi tiks automātiski nosaukti kā “kopas nosaukums/vienuma nosaukums”" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "Dzēst" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.duplicate" -msgstr "Dublikāts" +msgstr "Divkāršot" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" -msgstr "Rediģēt" +msgstr "Labot" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.graphics" -msgstr "Grafika" +msgstr "Attēli" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.group" -msgstr "Grupa" +msgstr "Kopa" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.group-name" -msgstr "Grupas nosaukums" +msgstr "Kopas nosaukums" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.libraries" @@ -2827,13 +3177,18 @@ msgstr "lokālā bibliotēka" msgid "workspace.assets.not-found" msgstr "Līdzekļi nav atrasti" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.open-library" +msgstr "Atvērt bibliotēkas datni" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename" msgstr "Pārdēvēt" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename-group" -msgstr "Pārdēvēt grupu" +msgstr "Pārdēvēt kopu" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.search" @@ -2847,12 +3202,13 @@ msgstr[1] "atlasīts %s vienums" msgstr[2] "atlasīti %s vienumi" #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "KOPĪGIE" +msgid "workspace.assets.shared-library" +msgstr "Koplietojama bibliotēka" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" -msgstr "Tipogrāfijas" +msgstr "Burtu stili un veidi" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.font-id" @@ -2868,7 +3224,7 @@ msgstr "Fonta variants" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.go-to-edit" -msgstr "Doties uz stilu bibliotēkas failu, lai rediģētu" +msgstr "Doties uz stilu bibliotēkas datni, lai labotu" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.letter-spacing" @@ -2878,7 +3234,9 @@ msgstr "Burtstarpa" msgid "workspace.assets.typography.line-height" msgstr "Rindas augstums" -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/inspect/attributes/text.cljs, src/app/main/ui/inspect/attributes/text.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs msgid "workspace.assets.typography.sample" msgstr "Ag" @@ -2891,59 +3249,59 @@ msgstr "Teksta pārveide" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.ungroup" -msgstr "Atgrupēt" +msgstr "Atapkopot" msgid "workspace.focus.focus-mode" msgstr "Fokusa režīms" msgid "workspace.focus.focus-off" -msgstr "Beigt fokusēties" +msgstr "Izslēgt fokusu" msgid "workspace.focus.focus-on" -msgstr "Fokusēties" +msgstr "Ieslēgt fokusu" msgid "workspace.focus.selection" msgstr "Atlase" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "Lineārais gradients" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.radial" msgstr "Radiālais gradients" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-dynamic-alignment" -msgstr "Atspējot dinamisko līdzinājumu" +msgstr "Atspējot dinamisko līdzināšanu" + +msgid "workspace.header.menu.disable-scale-content" +msgstr "Atspējot proporcionālo mērogu" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-scale-text" msgstr "Deaktivizēt teksta mērogošanu" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Atspējot pieķeršanos režģim" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" -msgstr "Atspējot piesaisti vadotnēm" +msgstr "Atspējot pieķeršanos vadotnēm" msgid "workspace.header.menu.disable-snap-pixel-grid" msgstr "Atspējot pieķeršanos pikselim" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-dynamic-alignment" -msgstr "Iespējot dinamisko līdzinājumu" +msgstr "Iespējot dinamisko līdzināšanu" + +msgid "workspace.header.menu.enable-scale-content" +msgstr "Iespējot proporcionālo mērogu" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-scale-text" msgstr "Aktivizēt teksta mērogošanu" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Pieķerties režģim" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "Pieķerties vadotnēm" @@ -2953,11 +3311,7 @@ msgstr "Iespējot pieķeršanos pikselim" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-artboard-names" -msgstr "Paslēpt dēļu nosaukumus" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Paslēpt režģus" +msgstr "Paslēpt plātņu nosaukumus" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" @@ -2976,11 +3330,11 @@ msgstr "Paslēpt fontu paleti" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.option.edit" -msgstr "Rediģēt" +msgstr "Labot" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.option.file" -msgstr "Fails" +msgstr "Datne" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.option.help-info" @@ -2994,17 +3348,16 @@ msgstr "Izvēles" msgid "workspace.header.menu.option.view" msgstr "Skatīt" +msgid "workspace.header.menu.redo" +msgstr "Atkārtot" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.select-all" -msgstr "Izvēlēties visu" +msgstr "Atlasīt visu" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-artboard-names" -msgstr "Rādīt dēļu nosaukumus" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Rādīt režģi" +msgstr "Rādīt plātņu nosaukumus" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" @@ -3021,9 +3374,12 @@ msgstr "Rādīt mērjoslas" msgid "workspace.header.menu.show-textpalette" msgstr "Rādīt fontu paleti" +msgid "workspace.header.menu.undo" +msgstr "Atsaukt" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" -msgstr "Atiestatīšana" +msgstr "Atiestatīt" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.save-error" @@ -3039,15 +3395,19 @@ msgstr "Saglabāšana" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.unsaved" -msgstr "Nesaglabātās izmaiņas" +msgstr "Nesaglabātas izmaiņas" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.viewer" -msgstr "Skata režīms (%s)" +msgstr "Skatīšana (%s)" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "Tālummaiņa" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-fill" -msgstr "Aizpildījums — aizpildāmais mērogs" +msgstr "Aizpildījums — mērogot, lai aizpildītu" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-fit" @@ -3055,7 +3415,7 @@ msgstr "Ietilpināt — samazināt, lai ietilpinātu" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-fit-all" -msgstr "Tālummaiņa, lai ietilpinātu visu" +msgstr "Tālummainīt, lai ietilpinātu visu" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-full-screen" @@ -3073,17 +3433,27 @@ msgstr "Pievienot" msgid "workspace.libraries.colors" msgstr "%s krāsas" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "Bibliotēkā vēl nav krāsu stilu" + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "Bibliotēkā vēl nav burtu stilu un veidu" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" -msgstr "Failu bibliotēka" +msgstr "Datņu bibliotēka" #: src/app/main/ui/workspace/colorpicker.cljs msgid "workspace.libraries.colors.hsv" msgstr "HSV" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" -msgstr "Nesen lietotās krāsas" +msgstr "Nesen izmantotās krāsas" #: src/app/main/ui/workspace/colorpicker.cljs msgid "workspace.libraries.colors.rgb-complementary" @@ -3099,19 +3469,19 @@ msgstr "Saglabāt krāsu stilu" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.components" -msgstr "%s komponentes" +msgstr "%s sastāvdaļas" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.file-library" -msgstr "Failu bibliotēka" +msgstr "Datņu bibliotēka" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.graphics" -msgstr "%s grafikas" +msgstr "%s attēli" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.in-this-file" -msgstr "BIBLIOTĒKAS ŠAJĀ FAILĀ" +msgstr "BIBLIOTĒKAS ŠAJĀ DATNĒ" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.libraries" @@ -3119,7 +3489,11 @@ msgstr "BIBLIOTĒKAS" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.library" -msgstr "BIBLIOTĒKAS" +msgstr "BIBLIOTĒKA" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "BIBLIOTĒKAS JAUNINĀJUMI" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.no-libraries-need-sync" @@ -3131,7 +3505,7 @@ msgstr "“%s” nav atrasta neviena atbilstība" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.no-shared-libraries-available" -msgstr "Nav pieejamu koplietojamu bibliotēku" +msgstr "Nav pieejamu koplietojamo bibliotēku" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.search-shared-libraries" @@ -3143,27 +3517,31 @@ msgstr "KOPLIETOJAMĀS BIBLIOTĒKAS" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.libraries.text.multiple-typography" -msgstr "Vairākas tipogrāfijas" +msgstr "Vairāki burtu stili un veidi" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.libraries.text.multiple-typography-tooltip" -msgstr "Atsaistīt visas tipogrāfijas" +msgstr "Atsaistīt visus burtu stilus un veidus" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.typography" -msgstr "%s tipogrāfijas" +msgstr "%s burtu stili un veidi" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.update" msgstr "Atjaunināt" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "apskatīt visas izmaiņas" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.updates" msgstr "ATJAUNINĀJUMI" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.add-interaction" -msgstr "Noklikšķiniet uz + pogas, lai pievienotu mijiedarbību." +msgstr "Jāklikšķina uz pogas \"+\", lai pievienotu mijiedarbības." #: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "workspace.options.blur-options.title" @@ -3171,7 +3549,7 @@ msgstr "Aizmiglojums" #: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "workspace.options.blur-options.title.group" -msgstr "Grupas aizmiglojums" +msgstr "Kopas aizmiglojums" #: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "workspace.options.blur-options.title.multiple" @@ -3182,11 +3560,20 @@ msgid "workspace.options.canvas-background" msgstr "Kanvas fons" msgid "workspace.options.clip-content" -msgstr "Izgriešanas saturs" +msgstr "Apcirpt saturu" #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs msgid "workspace.options.component" -msgstr "Komponente" +msgstr "Sastāvdaļa" + +msgid "workspace.options.component.annotation" +msgstr "Piezīme" + +msgid "workspace.options.component.create-annotation" +msgstr "Izveidot piezīmi" + +msgid "workspace.options.component.edit-annotation" +msgstr "Labot piezīmi" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints" @@ -3232,40 +3619,47 @@ msgstr "Augša un apakša" msgid "workspace.options.design" msgstr "Dizains" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export" -msgstr "Eksports" +msgstr "Izguve" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-multiple" -msgstr "Eksportēt atlasi" +msgstr "Izgūt atlasi" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-object" msgid_plural "workspace.options.export-object" -msgstr[0] "%s elementa eksportēšana" -msgstr[1] "%s elementa eksportēšana" -msgstr[2] "%s elementu eksportēšana" +msgstr[0] "Izgūt %s elementus" +msgstr[1] "Izgūt %s elementu" +msgstr[2] "Izgūt %s elementus" #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs msgid "workspace.options.export.suffix" -msgstr "Sufiks" +msgstr "Piedēklis" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-complete" -msgstr "Eksportēšana pabeigta" +msgstr "Izguve pabeigta" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object" -msgstr "Notiek eksportēšana…" +msgstr "Notiek izgūšana…" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-error" -msgstr "Eksportēšana neizdevās" +msgstr "Izgūšana neizdevās" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-slow" -msgstr "Eksportēšana ir negaidīti lēna" +msgstr "Izgūšana ir neparedzēti lēna" #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.fill" @@ -3281,7 +3675,7 @@ msgstr "Plūsmas sākums" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.flows.flow-starts" -msgstr "Plūsma sākās" +msgstr "Plūsma sākas" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.auto" @@ -3356,7 +3750,7 @@ msgstr "Augša" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.use-default" -msgstr "Lietot noklusējumu" +msgstr "Izmantot noklusējumu" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.width" @@ -3372,11 +3766,11 @@ msgstr "Kvadrāts" #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" -msgstr "Grupas aizpildījums" +msgstr "Kopas aizpildījums" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.group-stroke" -msgstr "Grupas kontūra" +msgstr "Kopas vilkums" msgid "workspace.options.height" msgstr "Augstums" @@ -3669,7 +4063,7 @@ msgstr "Slānis" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.title.group" -msgstr "Grupēt slāņus" +msgstr "Apkopot slāņus" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.title.multiple" @@ -3719,10 +4113,18 @@ msgstr "Apakša" msgid "workspace.options.layout.direction.column" msgstr "Kolona" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column-reverse" +msgstr "Apgrieztā kolonna" + #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.direction.row" msgstr "Rinda" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row-reverse" +msgstr "Apgrieztā rinda" + #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.gap" msgstr "Atstarpe" @@ -3786,7 +4188,8 @@ msgstr "Vairāk bibliotēkas krāsu" msgid "workspace.options.opacity" msgstr "Caurspīdīgums" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Novietojums" @@ -3798,12 +4201,12 @@ msgid "workspace.options.radius" msgstr "Rādiuss" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Visi stūri" +msgid "workspace.options.radius-bottom-left" +msgstr "Apakšā pa kreisi" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Atsevišķie stūri" +msgid "workspace.options.radius-bottom-right" +msgstr "Apakšā pa labi" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3814,17 +4217,18 @@ msgid "workspace.options.radius-top-right" msgstr "Augšā pa labi" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Apakšā pa kreisi" +msgid "workspace.options.radius.all-corners" +msgstr "Visi stūri" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Apakšā pa labi" +msgid "workspace.options.radius.single-corners" +msgstr "Atsevišķie stūri" msgid "workspace.options.recent-fonts" msgstr "Pēdējie" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" msgstr "Atkārtot" @@ -3837,7 +4241,8 @@ msgstr "Meklēt fontu" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.select-a-shape" -msgstr "Atlasiet formu, dēli vai grupu, lai vilktu savienojumu uz citu dēli." +msgstr "" +"Jāatlasa apveids, plātne vai kopa, lai vilktu savienojumu uz citu plātni." #: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs msgid "workspace.options.selection-color" @@ -3884,7 +4289,7 @@ msgstr "Ēna" #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.title.group" -msgstr "Grupas ēna" +msgstr "Kopas ēna" #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.title.multiple" @@ -3892,12 +4297,13 @@ msgstr "Atlases ēnas" #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.show-fill-on-export" -msgstr "Rādīt eksportā" +msgstr "Rādīt izguvēs" msgid "workspace.options.show-in-viewer" -msgstr "Rādīt skata režīmā" +msgstr "Rādīt skatītājā" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.size" msgstr "Izmērs" @@ -3977,31 +4383,15 @@ msgstr "Blīvs" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-bottom" -msgstr "Līdzināt lejā" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Līdzināt uz centru (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Izlīdzināt (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Līdzināt pa kreisi (%s)" +msgstr "Līdzināt pie apakšas" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Līdzināt pa vidu (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Līdzināt pa labi (%s)" +msgstr "Līdzināt vidū (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" -msgstr "Līdzināt augšā" +msgstr "Līdzināt pie augšas" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.direction-ltr" @@ -4035,7 +4425,8 @@ msgstr "Rindas augstums" msgid "workspace.options.text-options.lowercase" msgstr "Mazie burti" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.none" msgstr "Nav" @@ -4043,13 +4434,29 @@ msgstr "Nav" msgid "workspace.options.text-options.strikethrough" msgstr "Pārsvītrots (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Līdzināt vidū (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Izlīdzināt (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Līdzināt pa kreisi (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Līdzināt pa labi (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Teksts" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title-group" -msgstr "Grupas teksts" +msgstr "Kopas teksts" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title-selection" @@ -4069,7 +4476,7 @@ msgstr "Lielie burti" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.use-play-button" -msgstr "Lai palaistu prototipa skatu, izmantojiet atskaņošanas pogu pie galvenes." +msgstr "Jāizmanto atskaņosanas poga galvenē, lai palaistu prototipa skatu." msgid "workspace.options.width" msgstr "Platums" @@ -4110,38 +4517,13 @@ msgstr "Atdalīt mezglus (%s)" msgid "workspace.path.actions.snap-nodes" msgstr "Pieķert mezglus (%s)" -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "" -"Lai to mēģinātu vēlreiz, varat atkārtoti ielādēt šo failu. Ja problēma " -"joprojām pastāv, ieteicams apskatīt sarakstu un dzēst bojātās grafikas." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "Dažas grafikas nevar atjaunināt." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "%s/%s pārvēršana" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "" -"Bibliotēkas grafikas turpmāk sauksies Komponentes, kas padarīs tās daudz " -"jaudīgākas." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "Šis atjauninājums ir vienreizēja darbība." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "Notiek %s atjaunināšana..." - #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.add-flex" msgstr "Pievienot elastīgo izkārtojumu" +msgid "workspace.shape.menu.add-grid" +msgstr "Pievienot režģa izkārtojumu" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "Sūtīt atpakaļ" @@ -4154,13 +4536,19 @@ msgstr "Sūtīt uz aizmuguri" msgid "workspace.shape.menu.copy" msgstr "Kopēt" +msgid "workspace.shape.menu.create-annotation" +msgstr "Izveidot piezīmi" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.create-artboard-from-selection" -msgstr "Atlase uz dēli" +msgstr "Atlase uz plātni" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.create-component" -msgstr "Izveidot komponenti" +msgstr "Izveidot sastāvdaļu" + +msgid "workspace.shape.menu.create-multiple-components" +msgstr "Izveidot vairākas sastāvdaļas" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.cut" @@ -4172,13 +4560,17 @@ msgstr "Dzēst" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.delete-flow-start" -msgstr "Dzēst plūsmas sākumu" +msgstr "Izdzēst plūsmas sākumu" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Atvienot instanci" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instances-in-bulk" msgstr "Atvienot instances" @@ -4187,11 +4579,11 @@ msgstr "Starpība" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.duplicate" -msgstr "Dublēšana" +msgstr "Divkāršot" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.edit" -msgstr "Rediģēt" +msgstr "Labot" msgid "workspace.shape.menu.exclude" msgstr "Izslēgt" @@ -4201,11 +4593,11 @@ msgstr "Izklāt" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.flip-horizontal" -msgstr "Apvērst horizontāli" +msgstr "Apvērst līmeniski" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.flip-vertical" -msgstr "Apvērst vertikāli" +msgstr "Apvērst stateniski" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.flow-start" @@ -4219,13 +4611,14 @@ msgstr "Virzīt uz priekšu" msgid "workspace.shape.menu.front" msgstr "Virzīt priekšā" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.go-main" -msgstr "Doties uz galveno komponentu failu" +msgstr "Doties uz galvenās sastāvdaļas datni" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.group" -msgstr "Grupa" +msgstr "Kopa" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.hide" @@ -4239,15 +4632,17 @@ msgstr "Šķēlums" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.lock" -msgstr "Bloķēt" +msgstr "Slēgt" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.mask" msgstr "Maska" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.paste" -msgstr "Ievietot" +msgstr "Ielīmēt" msgid "workspace.shape.menu.path" msgstr "Ceļš" @@ -4256,28 +4651,32 @@ msgstr "Ceļš" msgid "workspace.shape.menu.remove-flex" msgstr "Noņemt elastīgo izkārtojumu" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "Atiestatīt aizvietojumus" msgid "workspace.shape.menu.restore-main" -msgstr "Atjaunot galveno komponenti" +msgstr "Atjaunot galveno sastāvdaļu" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.select-layer" -msgstr "Izvēlēties slāni" +msgstr "Atlasīt slāni" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show" msgstr "Pāradīt" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-in-assets" msgstr "Rādīt līdzekļu panelī" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-main" -msgstr "Rādīt galveno komponenti" +msgstr "Rādīt galveno sastāvdaļu" msgid "workspace.shape.menu.thumbnail-remove" msgstr "Noņemt sīktēlu" @@ -4290,26 +4689,30 @@ msgstr "Transformēt par ceļu" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.ungroup" -msgstr "Atgrupēt" +msgstr "Atapkopot" msgid "workspace.shape.menu.union" msgstr "Apvienot" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.unlock" -msgstr "Atbloķēt" +msgstr "Atslēgt" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.unmask" msgstr "Noņemt masku" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-components-in-bulk" -msgstr "Galveno komponenšu atjaunināšana" +msgstr "Atjaunināt galvenās sastāvdaļas" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-main" -msgstr "Galvenas komponentes atjaunināšana" +msgstr "Atjaunināt galveno sastāvdaļu" msgid "workspace.sidebar.collapse" msgstr "Sakļaut sānjoslu" @@ -4326,13 +4729,13 @@ msgid "workspace.sidebar.layers" msgstr "Slāņi" msgid "workspace.sidebar.layers.components" -msgstr "Komponentes" +msgstr "Sastāvdaļas" msgid "workspace.sidebar.layers.frames" -msgstr "Dēļi" +msgstr "Plātnes" msgid "workspace.sidebar.layers.groups" -msgstr "Grupas" +msgstr "Kopas" msgid "workspace.sidebar.layers.images" msgstr "Attēli" @@ -4349,9 +4752,10 @@ msgstr "Formas" msgid "workspace.sidebar.layers.texts" msgstr "Teksti" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/inspect/attributes/svg.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, +#: src/app/main/ui/inspect/attributes/svg.cljs msgid "workspace.sidebar.options.svg-attrs.title" -msgstr "Importētie SVG atribūti" +msgstr "Ievietotie SVG atribūti" #: src/app/main/ui/workspace/sidebar/sitemap.cljs msgid "workspace.sidebar.sitemap" @@ -4359,7 +4763,7 @@ msgstr "Lapas" #: src/app/main/ui/workspace/header.cljs msgid "workspace.sitemap" -msgstr "Tīmekļa vietnes karte" +msgstr "Vietnes karte" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.assets" @@ -4383,7 +4787,7 @@ msgstr "Elipse (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.frame" -msgstr "Dēlis (%s)" +msgstr "Plātne (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.image" @@ -4411,7 +4815,7 @@ msgstr "Teksts (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.text-palette" -msgstr "Tipogrāfijas (%s)" +msgstr "Burtu stili un veidi (%s)" #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.empty" @@ -4427,7 +4831,7 @@ msgstr "Modificēts %s" #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.entry.move" -msgstr "Pārvietoti objekti" +msgstr "Pārvietotie objekti" msgid "workspace.undo.entry.multiple.circle" msgstr "apļi" @@ -4436,16 +4840,16 @@ msgid "workspace.undo.entry.multiple.color" msgstr "krāsu līdzekļi" msgid "workspace.undo.entry.multiple.component" -msgstr "komponentes" +msgstr "sastāvdaļas" msgid "workspace.undo.entry.multiple.curve" msgstr "līknes" msgid "workspace.undo.entry.multiple.frame" -msgstr "dēļi" +msgstr "plātnes" msgid "workspace.undo.entry.multiple.group" -msgstr "grupas" +msgstr "kopas" msgid "workspace.undo.entry.multiple.media" msgstr "grafiskie līdzekļi" @@ -4469,7 +4873,7 @@ msgid "workspace.undo.entry.multiple.text" msgstr "teksti" msgid "workspace.undo.entry.multiple.typography" -msgstr "tipogrāfijas līdzekļi" +msgstr "burtu stilu un veidu līdzekļi" #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.entry.new" @@ -4482,16 +4886,16 @@ msgid "workspace.undo.entry.single.color" msgstr "krāsas līdzeklis" msgid "workspace.undo.entry.single.component" -msgstr "komponente" +msgstr "sastāvdaļa" msgid "workspace.undo.entry.single.curve" msgstr "līkne" msgid "workspace.undo.entry.single.frame" -msgstr "dēlis" +msgstr "plātne" msgid "workspace.undo.entry.single.group" -msgstr "grupa" +msgstr "kopa" msgid "workspace.undo.entry.single.image" msgstr "attēls" @@ -4518,7 +4922,7 @@ msgid "workspace.undo.entry.single.text" msgstr "teksts" msgid "workspace.undo.entry.single.typography" -msgstr "tipogrāfijas līdzeklis" +msgstr "burtu stila un veida līdzeklis" #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.entry.unknown" @@ -4532,61 +4936,224 @@ msgstr "Vēsture" msgid "workspace.updates.dismiss" msgstr "Izlaist" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "Vairāk informācijas" + #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.there-are-updates" -msgstr "Koplietojamās bibliotēkās ir atjauninājumi" +msgstr "Koplietojamajās bibliotēkās ir atjauninājumi" #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.update" msgstr "Atjaunināt" msgid "workspace.viewport.click-to-close-path" -msgstr "Noklikšķiniet, lai aizvērtu ceļu" +msgstr "Jānoklikšķina, lai aizvērtu ceļu" -msgid "shortcuts.align-center" -msgstr "Centrēt" +#: src/app/main/errors.cljs +msgid "errors.team-feature-mismatch" +msgstr "Noteikta nesaderīga iespēja '%s'" -msgid "shortcuts.align-justify" -msgstr "Līdzināt taisnoti" +msgid "errors.paste-data-validation" +msgstr "Starpliktuvē ir nederīgi dati" -msgid "shortcuts.letter-spacing-dec" -msgstr "Samazināt burtstarpu" +msgid "onboarding.choice.team-up.continue-creating-team" +msgstr "Turpināt komandas izveidošanu" -msgid "shortcuts.letter-spacing-inc" -msgstr "Palielināt burtstarpu" +msgid "onboarding.choice.team-up.start-without-a-team" +msgstr "Uzsākt bez komandas" -msgid "shortcuts.italic" -msgstr "Pārslēgt slīprakstu" +msgid "onboarding.choice.team-up.start-without-a-team-description" +msgstr "Komandu būs iespējams izveidot vēlāk." -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-weight" -msgstr "Fonta Treknums" +msgid "onboarding.choice.team-up.continue-without-a-team" +msgstr "Turpināt bez komandas" + +msgid "onboarding.choice.team-up.create-team-and-send-invites" +msgstr "Izveidot komandu un nosūtīt uzaicinājumus" + +msgid "onboarding.choice.team-up.create-team-and-invite" +msgstr "Izveidot komandu un uzaicināt" + +msgid "onboarding.choice.team-up.create-team-without-invite" +msgstr "Izveidot komandu" + +msgid "onboarding.choice.team-up.create-team-and-send-invites-description" +msgstr "Uzaicināt būs iespējams vēlāk" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "Izstrādātājs" + +msgid "workspace.layout_grid.editor.options.exit" +msgstr "Iziet" + +msgid "media.image" +msgstr "Attēls" + +msgid "media.solid" +msgstr "Viengabalains" + +msgid "media.linear" +msgstr "Līnijveida" + +msgid "media.radial" +msgstr "Radiāls" + +msgid "media.gradient" +msgstr "Pāreja" + +msgid "media.choose-image" +msgstr "Izvēlēties attēlu" + +msgid "workspace.options.guides.title" +msgstr "Vadotnes" + +#: src/app/main/errors.cljs +msgid "errors.version-not-supported" +msgstr "Datnei ir nesaderīgs versijas numurs" + +#: src/app/main/errors.cljs +msgid "errors.file-feature-mismatch" +msgstr "" +"Izskatās, ka ir nesaderība starp iespējotajām iespējām un iespējām datnē, " +"kuru tiek mēģināts atvērt. Jāpiemēro '%s' migrācijas, pirms datne var tikt " +"atvērta." + +msgid "errors.validation" +msgstr "Pārbaudes kļūda" + +msgid "inspect.attributes.typography.text-transform.unset" +msgstr "Atiestatīt" + +msgid "labels.share" +msgstr "Kopīgot" + +msgid "labels.search" +msgstr "Meklēt" + +#: src/app/main/ui/auth/register.cljs +#, markdown +msgid "auth.terms-privacy-agreement-md" +msgstr "" +"Ar jauna konta izveidošanu tiek piekrists mūsu [pakalpojuma noteikumiem](%s) " +"un [privātuma nosacījumiem](%s)." + +msgid "onboarding.choice.team-up.create-team-without-inviting" +msgstr "Izveidot komandu bez uzaicināšanas" + +msgid "workspace.layout_grid.editor.options.edit-grid" +msgstr "Labot režģi" + +msgid "workspace.layout_grid.editor.top-bar.locate" +msgstr "Noteikt atrašanās vietu" + +msgid "workspace.layout_grid.editor.top-bar.done" +msgstr "Darīts" + +msgid "workspace.options.component.swap" +msgstr "Mijmainīt sastāvdaļu" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow" +msgstr "Plūsma" + +msgid "workspace.top-bar.read-only.done" +msgstr "Darīts" + +msgid "workspace.options.component.swap.empty" +msgstr "Šajā bibliotēkā vēl nav līdzekļu" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "Bulta" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "Trijstūris" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "Taisnstūris" #, markdown -msgid "dashboard.fonts.warning-text" +msgid "workspace.top-bar.read-only" +msgstr "**Apskatīšana** (tikai skatīt)" + +msgid "workspace.assets.duplicate-main" +msgstr "Divkāršot galveno" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "Modelētājs" + +msgid "dashboard.import.import-message" +msgid_plural "dashboard.import.import-message" +msgstr[0] "%s datņu tika veiksmīgi ievietotas." +msgstr[1] "%s datne tika veiksmīgi ievietota." +msgstr[2] "%s datnes tika veiksmīgi ievietotas." + +msgid "modals.add-shared-confirm-empty.hint" msgstr "" -"Esam konstatējuši iespējamu problēmu Jūsu fontos, kas saistīta ar vertikālām " -"metrikam dažādām operētājsistēmām. Lai to pārbaudītu, varat izmantot tādus " -"fontu vertikālās metrikas pakalpojumus kā [šis] (https://vertical-metrics." -"netlify.app/). Turklāt ieteicams izmantot [Transfonter] (https://transfonter." -"org/), lai ģenerētu tīmekļa fontus un labotu to kļūdas. " +"Bibliotēka ir tukša. Tiklīdz tā būs pievienota kā koplietojama bibliotēka, " +"izveidotie līdzekļi būs pieejami izmantošanai pārējos failos. Vai tiešām " +"padarīt to pieejamu?" -msgid "modals.invite-member.repeated-invitation" -msgstr "" -"Daži e-pasta ziņojumi ir no pašreizējiem grupas dalībniekiem. Ielūgumi " -"viņiem netiks nosūtīti." +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +msgid "settings.detach" +msgstr "Atdalīt" -msgid "shortcut-subsection.text-editor" -msgstr "Teksti" +msgid "workspace.options.component.copy" +msgstr "Ievietot starpliktuvē" -msgid "shortcuts.bold" -msgstr "Pārslēgt treknrakstu" +msgid "workspace.options.component.main" +msgstr "Galvenais" -msgid "shortcuts.underline" -msgstr "Pārslēgt pasvītrojumu" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "Aplis" -msgid "shortcuts.font-size-dec" -msgstr "Samazināt fonta izmēru" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "Dimants" -msgid "shortcuts.font-size-inc" -msgstr "Palielināt fonta izmēru" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" +msgstr[0] "" +"Līdzekļi, kas jau tiek izmantoti šajā datnē, paliks tajā (dizains netiks " +"salauzts)." +msgstr[1] "" +"Līdzeklis, kas jau tiek izmantots šajā datnē, paliks tajā (dizains netiks " +"salauzts)." +msgstr[2] "" +"Līdzekļi, kas jau tiek izmantoti šajā datnē, paliks tajā (dizains netiks " +"salauzts)." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "Tirgvedība" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "Dibinātājs/viceprezidents" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "Produktu vai projektu vadītājs" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "Kāda ir ieņemamā loma?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "Students vai pasniedzējs" + +#: src/app/main/data/common.cljs +msgid "notifications.by-code.upgrade-version" +msgstr "Ir pieejama jauna versija, lūgums atsvaidzināt lapu" + +msgid "workspace.layout_grid.editor.title" +msgstr "Režģa labošana" diff --git a/frontend/translations/ml.po b/frontend/translations/ml.po index 062e08db4e..e24b0cba05 100644 --- a/frontend/translations/ml.po +++ b/frontend/translations/ml.po @@ -242,4 +242,4 @@ msgid "dashboard.export-shapes" msgstr "എക്സ്പോർട്ട്" msgid "dashboard.export.detail" -msgstr "* ഘടകങ്ങൾ, ഗ്രാഫിക്സ്, നിറങ്ങൾ അല്ലെങ്കിൽ മുദ്രണകലകൾ എന്നിവ ഉൾപ്പെടാം." \ No newline at end of file +msgstr "* ഘടകങ്ങൾ, ഗ്രാഫിക്സ്, നിറങ്ങൾ അല്ലെങ്കിൽ മുദ്രണകലകൾ എന്നിവ ഉൾപ്പെടാം." diff --git a/frontend/translations/ms.po b/frontend/translations/ms.po new file mode 100644 index 0000000000..110f93ecaa --- /dev/null +++ b/frontend/translations/ms.po @@ -0,0 +1,418 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2024-02-09 13:58+0000\n" +"Last-Translator: Revenant \n" +"Language-Team: Malay \n" +"Language: ms\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 5.4-dev\n" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-account" +msgstr "Cipta akaun demo" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-profile" +msgstr "Hanya ingin mencubanya?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.demo-warning" +msgstr "" +"Ini adalah perkhidmatan DEMO, JANGAN GUNAKAN untuk kerja sebenar, projek " +"akan dipadam secara berkala." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.email" +msgstr "E-mel" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.forgot-password" +msgstr "Lupa kata laluan?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.fullname" +msgstr "Nama penuh" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.login-here" +msgstr "Log masuk disini" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-submit" +msgstr "Log masuk" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-title" +msgstr "Gembira dapat berjumpa lagi!" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-github-submit" +msgstr "Github" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-gitlab-submit" +msgstr "GitLab" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-google-submit" +msgstr "Google" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-ldap-submit" +msgstr "LDAP" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-oidc-submit" +msgstr "OpenID Connect" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.invalid-token-error" +msgstr "Token pemulihan adalah tidak sah." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.password-changed-successfully" +msgstr "Kata laluan berjaya ditukar" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.profile-not-verified" +msgstr "Profil tidak disahkan, sila sahkan profil sebelum meneruskan." + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.recovery-token-sent" +msgstr "Pautan pemulihan kata laluan dihantar ke peti masuk anda." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "auth.notifications.team-invitation-accepted" +msgstr "Berjaya menyertai pasukan" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.password" +msgstr "Kata laluan" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-length-hint" +msgstr "Sekurang-kurangnya 8 aksara" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "Kata laluan mesti mengandungi beberapa aksara selain daripada ruang." + +msgid "auth.privacy-policy" +msgstr "Dasar privasi" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-submit" +msgstr "Pulihkan Kata Laluan" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-subtitle" +msgstr "Kami akan menghantar e-mel kepada anda dengan arahan" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-title" +msgstr "Lupa kata laluan?" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.recovery-submit" +msgstr "Tukar kata laluan anda" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.register" +msgstr "Tiada akaun lagi?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-subtitle" +msgstr "Ia percuma dan Sumber Terbuka" + +#: src/app/main/ui/auth.cljs +msgid "auth.sidebar-tagline" +msgstr "Penyelesaian sumber terbuka untuk reka bentuk dan prototaip." + +msgid "auth.terms-of-service" +msgstr "Syarat perkhidmatan" + +#: src/app/main/ui/auth/register.cljs +#, markdown +msgid "auth.terms-privacy-agreement-md" +msgstr "" +"Apabila membuat akaun baharu, anda bersetuju menerima [syarat " +"perkhidmatan](%s) dan [dasar privasi](%s) kami." + +msgid "auth.terms-privacy-agreement" +msgstr "" +"Apabila membuat akaun baharu, anda bersetuju menerima syarat perkhidmatan " +"dan dasar privasi kami." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "... penjenamaan, ilustrasi, bahagian pemasaran, dll." + +msgid "common.publish" +msgstr "Terbitkan" + +msgid "common.share-link.all-users" +msgstr "Semua pengguna Penpot" + +msgid "common.share-link.current-tag" +msgstr "(semasa)" + +msgid "common.share-link.destroy-link" +msgstr "Musnahkan pautan" + +msgid "common.share-link.get-link" +msgstr "Dapatkan pautan" + +msgid "common.share-link.link-copied-success" +msgstr "Pautan berjaya disalin" + +msgid "common.share-link.manage-ops" +msgstr "Urus kebenaran" + +msgid "common.share-link.page-shared" +msgid_plural "common.share-link.page-shared" +msgstr[0] "%s halaman dikongsi" + +msgid "common.share-link.permissions-can-comment" +msgstr "Boleh komen" + +msgid "common.share-link.permissions-hint" +msgstr "Sesiapa yang mempunyai pautan akan mendapat akses" + +msgid "common.share-link.permissions-pages" +msgstr "Halaman dikongsi" + +msgid "common.share-link.placeholder" +msgstr "Pautan boleh kongsi akan dipaparkan di sini" + +msgid "common.share-link.team-members" +msgstr "Hanya ahli pasukan" + +msgid "common.share-link.title" +msgstr "Kongsi prototaip" + +msgid "common.share-link.view-all" +msgstr "Pilih semua" + +msgid "common.unpublish" +msgstr "Nyahterbitkan" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.management" +msgstr "Pengurusan pasukan" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.text" +msgstr "" +"Penpot dibuat untuk pasukan. Jemput ahli untuk bekerjasama dalam projek dan " +"fail" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.title" +msgstr "Berganding bahu!" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.tutorial-hero.info" +msgstr "" +"Pelajari asas-asas di Penpot sambil berseronok dengan tutorial guna tangan " +"ini." + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.tutorial-hero.title" +msgstr "Tutorial guna tangan" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.info" +msgstr "Terokai Penpot untuk mengetahui lebih lanjut tentang ciri utamanya." + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.start" +msgstr "Mulakan jelajah" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.walkthrough-hero.title" +msgstr "Panduan Antara Muka" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "Token disalin" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "Jana token baru" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "Tekan butang \"Jana token baharu\" untuk menjana token." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 hari" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 hari" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 hari" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 hari" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "Tidak pernah" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "Luput pada %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "Tamat tempoh pada %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "Tiada tarikh tamat tempoh" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Token capaian peribadi" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "Token akan luput pada %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "Token tidak mempunyai tarikh luput" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.add-shared" +msgstr "Tambahkan sebagai Perpustakaan kongsi" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.change-email" +msgstr "tukar e-mel" + +#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs +msgid "dashboard.copy-suffix" +msgstr "(salin)" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.create-new-team" +msgstr "Buat pasukan baharu" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.default-team-name" +msgstr "Penpot anda" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.delete-team" +msgstr "Padam pasukan" + +msgid "dashboard.download-binary-file" +msgstr "Muat turun fail Penpot (.penpot)" + +msgid "dashboard.download-standard-file" +msgstr "Muat turun fail standard (.svg + .json)" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate" +msgstr "Pendua" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate-multi" +msgstr "Pendua %s fail" + +#: src/app/main/ui/dashboard/grid.cljs +#, markdown +msgid "dashboard.empty-placeholder-drafts" +msgstr "" +"Fail yang ditambahkan pada Perpustakaan akan dipaparkan di sini. Cuba kongsi " +"fail anda atau tambahkan daripada [Perpustakaan & templat](https://penpot." +"app/libraries-templates.html) kami." + +msgid "dashboard.export-binary-multi" +msgstr "Muat turun %s fail Penpot (.penpot)" + +msgid "dashboard.export-frames" +msgstr "Eksport papan sebagai PDF" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.already-have-account" +msgstr "Sudah mempunyai akaun?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.check-your-email" +msgstr "" +"Semak e-mel anda dan klik pada pautan untuk mengesahkan dan mula menggunakan " +"Penpot." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-title" +msgstr "Cipta akaun" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.confirm-password" +msgstr "Mengesahkan kata laluan" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.verification-email-sent" +msgstr "Kami telah menghantar e-mel pengesahan kepada" + +msgid "common.share-link.confirm-deletion-link-description" +msgstr "" +"Adakah anda pasti untuk mengalih keluar pautan ini? Jika anda melakukannya, " +"ia tidak lagi tersedia untuk sesiapa sahaja" + +msgid "common.share-link.permissions-can-inspect" +msgstr "Boleh memeriksa kod" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.tutorial-hero.start" +msgstr "Mulakan tutorial" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "Token capaian berjaya dihasilkan." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Token capaian peribadi berfungsi seperti alternatif kepada sistem pengesahan " +"log masuk/kata laluan kami dan boleh digunakan untuk membenarkan aplikasi " +"mengakses API dalaman Penpot" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "Anda tidak mempunyai token setakat ini." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "Nama diperlukan" + +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "Nama mesti mengandungi beberapa aksara selain ruang." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "Nama mesti mengandungi paling banyak 250 aksara." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.new-password" +msgstr "Taip kata laluan baharu" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.register-submit" +msgstr "Cipta akaun" + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-frames.title" +msgstr "Eksport sebagai PDF" diff --git a/frontend/translations/my.po b/frontend/translations/my.po index b1fc23594e..9511968215 100644 --- a/frontend/translations/my.po +++ b/frontend/translations/my.po @@ -3,4 +3,4 @@ msgstr "" "X-Generator: Weblate\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" \ No newline at end of file +"Content-Transfer-Encoding: 8bit\n" diff --git a/frontend/translations/nb_NO.po b/frontend/translations/nb_NO.po index de20d80f0f..33b0e4b1d1 100644 --- a/frontend/translations/nb_NO.po +++ b/frontend/translations/nb_NO.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "PO-Revision-Date: 2023-08-22 09:49+0000\n" "Last-Translator: andy \n" -"Language-Team: Norwegian Bokmål \n" +"Language-Team: Norwegian Bokmål " +"\n" "Language: nb_NO\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" @@ -602,14 +602,6 @@ msgstr "Størrelse" msgid "workspace.assets.typography.font-variant-id" msgstr "Variant" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Fest til rutenett" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Vis rutenett" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-rules" msgstr "Vis regler" diff --git a/frontend/translations/nl.po b/frontend/translations/nl.po index 89b48ce9ad..3e7a9bc04a 100644 --- a/frontend/translations/nl.po +++ b/frontend/translations/nl.po @@ -1,274 +1,46 @@ msgid "" msgstr "" -"PO-Revision-Date: 2023-08-28 14:57+0000\n" -"Last-Translator: Sebastiaan Pasma \n" +"PO-Revision-Date: 2023-12-29 21:08+0000\n" +"Last-Translator: Stephan Paternotte \n" "Language-Team: Dutch \n" "Language: nl\n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" +"Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.0.1-dev\n" - -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.notifications.recovery-token-sent" -msgstr "Wachtwoordherstel-link verzonden naar je inbox." +"X-Generator: Weblate 5.4-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" msgstr "Heb je al een account?" -#: src/app/main/ui/auth/recovery.cljs -msgid "auth.confirm-password" -msgstr "Bevestig wachtwoord" - -#: src/app/main/ui/auth/login.cljs -msgid "auth.login-submit" -msgstr "Inloggen" - -#: src/app/main/ui/auth.cljs -msgid "auth.sidebar-tagline" -msgstr "De open-source oplossing voor ontwerp en prototyping." - -#: src/app/main/ui/auth/register.cljs -msgid "auth.demo-warning" -msgstr "" -"Dit is een DEMO-service, GEBRUIK DIT NIET voor echt werk, de projecten " -"zullen regelmatig worden gewist." - -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs -msgid "auth.create-demo-account" -msgstr "Maak een demo-account aan" - -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs -msgid "auth.create-demo-profile" -msgstr "Wil je het gewoon proberen?" - -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.recovery-request-subtitle" -msgstr "We sturen je een e-mail met instructies" - -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.recovery-request-submit" -msgstr "Wachtwoord herstellen" - -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.recovery-request-title" -msgstr "Wachtwoord vergeten?" - -#: src/app/main/ui/auth/recovery.cljs -msgid "auth.recovery-submit" -msgstr "Wijzig je wachtwoord" - -#: src/app/main/ui/auth/login.cljs -msgid "auth.register" -msgstr "Nog geen account?" - -#: src/app/main/ui/auth/register.cljs -msgid "auth.register-subtitle" -msgstr "Het is gratis, het is open source" - -#: src/app/main/ui/auth/register.cljs -msgid "auth.register-title" -msgstr "Account aanmaken" - -#: src/app/main/ui/auth/register.cljs -msgid "auth.verification-email-sent" -msgstr "We hebben een verificatie-e-mail verzonden naar" - -msgid "common.share-link.view-all" -msgstr "Alles selecteren" - -msgid "common.publish" -msgstr "Publiceren" - -msgid "common.share-link.all-users" -msgstr "Alle Penpot-gebruikers" - -msgid "common.share-link.team-members" -msgstr "Alleen teamleden" - -msgid "common.share-link.placeholder" -msgstr "De deelbare link zal hier verschijnen" - -msgid "common.unpublish" -msgstr "Publicatie ongedaan maken" - -#: src/app/main/ui/dashboard/projects.cljs -msgid "dasboard.team-hero.title" -msgstr "Werk samen!" - -#: src/app/main/ui/settings/profile.cljs -msgid "dashboard.change-email" -msgstr "E-mailadres wijzigen" - -#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs -msgid "dashboard.copy-suffix" -msgstr "(kopie)" - -#: src/app/main/ui/dashboard/sidebar.cljs -msgid "dashboard.create-new-team" -msgstr "Nieuw team maken" - -#: src/app/main/ui/dashboard/sidebar.cljs -msgid "dashboard.default-team-name" -msgstr "Jouw Penpot" - -#: src/app/main/ui/dashboard/sidebar.cljs -msgid "dashboard.delete-team" -msgstr "Verwijder team" - -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.duplicate-multi" -msgstr "Dupliceer %s bestanden" - -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.duplicate" -msgstr "Dupliceer" - -msgid "dashboard.export-binary-multi" -msgstr "Download %s Penpot-bestanden (.penpot)" - -#: src/app/main/ui/export.cljs -msgid "dashboard.export-shapes.how-to" -msgstr "" -"Je kunt exportinstellingen toevoegen aan elementen vanuit de " -"ontwerpeigenschappen (onderaan de rechter zijbalk)." - -#: src/app/main/ui/export.cljs -msgid "dashboard.export-shapes.how-to-link" -msgstr "Info over het instellen van exports in Penpot." - -msgid "dashboard.fonts.empty-placeholder" -msgstr "Aangepaste lettertypen die je uploadt, worden hier weergegeven." - -msgid "dashboard.export.options.detach.message" -msgstr "" -"Gedeelde bibliotheken worden niet opgenomen in de export en er worden geen " -"assets aan de bibliotheek toegevoegd. " - -msgid "dashboard.export.options.all.message" -msgstr "" -"Bestanden met gedeelde bibliotheken worden opgenomen in de export en hun " -"koppelingen worden behouden." - -msgid "dashboard.fonts.deleted-placeholder" -msgstr "Lettertype verwijderd" - -#, markdown -msgid "dashboard.fonts.hero-text2" -msgstr "" -"U mag alleen lettertypen uploaden waarvan u de eigenaar bent of waarvoor u " -"een licentie hebt om te gebruiken in Penpot. Lees meer in het gedeelte " -"Inhoudsrechten van [Penpot's Servicevoorwaarden](https://penpot.app/terms." -"html). Misschien wilt u ook meer lezen over [lettertypelicenties](https://www" -".typography.com/faq)." - -#, markdown -msgid "dashboard.fonts.warning-text" -msgstr "" -"We hebben een mogelijk probleem gevonden in uw lettertypen met betrekking " -"tot verticale metriek voor verschillende besturingssystemen. Om het te " -"controleren, kunt u verticale metrische lettertype-services zoals [deze] " -"gebruiken (https://vertical-metrics.netlify.app/). Daarnaast raden we aan " -"[Transfonter](https://transfonter.org/) te gebruiken om webfonts te " -"genereren en soortgelijke fouten op te lossen. " - -msgid "dashboard.import.import-message" -msgid_plural "dashboard.import.import-message" -msgstr[0] "1 bestand is geïmporteerd." -msgstr[1] "%s bestanden zijn geïmporteerd." - -msgid "dashboard.import.import-warning" -msgstr "Sommige bestanden bevatten ongeldige objecten die verwijderd zijn." - -msgid "dashboard.import.progress.process-media" -msgstr "Media aan het verwerken" - -msgid "dashboard.import.progress.process-components" -msgstr "Componenten aan het verwerken" - -msgid "dashboard.import.progress.upload-data" -msgstr "Gegevens uploaden naar server (%s/%s)" - -msgid "dashboard.libraries-and-templates" -msgstr "Bibliotheken & sjablonen" - -#: src/app/main/ui/dashboard/libraries.cljs -msgid "dashboard.libraries-title" -msgstr "Bibliotheken" - -msgid "dashboard.libraries-and-templates.explore" -msgstr "Ontdek er meer van en weet hoe je kunt bijdragen" - -msgid "dashboard.libraries-and-templates.import-error" -msgstr "" -"Er is een probleem opgetreden bij het importeren van het sjabloon. Het " -"sjabloon is niet geïmporteerd." - -#: src/app/main/ui/settings/password.cljs -msgid "dashboard.notifications.password-saved" -msgstr "Wachtwoord succesvol opgeslagen!" - -#: src/app/main/data/dashboard.cljs -msgid "dashboard.new-file-prefix" -msgstr "Nieuw bestand" - -#: src/app/main/ui/dashboard/projects.cljs -msgid "dashboard.new-project" -msgstr "+ Nieuw project" - -#: src/app/main/ui/dashboard/search.cljs -msgid "dashboard.no-matches-for" -msgstr "Geen overeenkomsten gevonden voor \"%s\"" - -#: src/app/main/ui/auth/verify_token.cljs -msgid "dashboard.notifications.email-verified-successfully" -msgstr "Je e-mailadres is geverifieerd" - -#: src/app/main/data/dashboard.cljs -msgid "dashboard.new-project-prefix" -msgstr "Nieuw project" - -#: src/app/main/ui/dashboard/team.cljs -msgid "dashboard.num-of-members" -msgstr "%s leden" - -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.open-in-new-tab" -msgstr "Bestand openen in een nieuw tabblad" - -msgid "dashboard.options" -msgstr "Opties" - -#: src/app/main/ui/dashboard/sidebar.cljs -msgid "dashboard.no-projects-placeholder" -msgstr "Vastgemaakte projecten worden hier weergegeven" - -#: src/app/main/ui/settings/password.cljs -msgid "dashboard.password-change" -msgstr "Verander wachtwoord" - -#: src/app/main/ui/dashboard/project_menu.cljs -msgid "dashboard.pin-unpin" -msgstr "Vastzetten/losmaken" - -#: src/app/main/ui/dashboard/projects.cljs -msgid "dashboard.projects-title" -msgstr "Projecten" - -#: src/app/main/ui/settings/profile.cljs -msgid "dashboard.remove-account" -msgstr "Wil je je account verwijderen?" - #: src/app/main/ui/auth/register.cljs msgid "auth.check-your-email" msgstr "" "Controleer je e-mail en klik op de link om te verifiëren en Penpot te gaan " "gebruiken." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.confirm-password" +msgstr "Wachtwoord bevestigen" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-account" +msgstr "Demo-account aanmaken" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-profile" +msgstr "Wil je het gewoon proberen?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.demo-warning" +msgstr "" +"Dit is een DEMO-service, GEBRUIK DIT NIET voor echt werk, de projecten " +"worden regelmatig gewist." + +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" msgstr "E-mail" @@ -284,6 +56,14 @@ msgstr "Volledige naam" msgid "auth.login-here" msgstr "Hier inloggen" +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-submit" +msgstr "Inloggen" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-title" +msgstr "Goed om je weer te zien!" + #: src/app/main/ui/auth/login.cljs msgid "auth.login-with-github-submit" msgstr "GitHub" @@ -304,9 +84,25 @@ msgstr "LDAP" msgid "auth.login-with-oidc-submit" msgstr "OpenID" -#: src/app/main/ui/auth/login.cljs -msgid "auth.login-title" -msgstr "Goed om je weer te zien!" +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/onboarding/team_choice.cljs, +#: src/app/main/ui/settings/access_tokens.cljs, +#: src/app/main/ui/settings/feedback.cljs, +#: src/app/main/ui/settings/profile.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "De naam mag geen spatie bevatten." + +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/onboarding/team_choice.cljs, +#: src/app/main/ui/settings/access_tokens.cljs, +#: src/app/main/ui/settings/feedback.cljs, +#: src/app/main/ui/settings/profile.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "De naam mag maximaal 250 tekens bevatten." #: src/app/main/ui/auth/recovery.cljs msgid "auth.new-password" @@ -314,7 +110,7 @@ msgstr "Typ een nieuw wachtwoord" #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.invalid-token-error" -msgstr "De hersteltoken is ongeldig." +msgstr "De herstelbewijsstuk is ongeldig." #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.password-changed-successfully" @@ -323,7 +119,11 @@ msgstr "Wachtwoord succesvol gewijzigd" #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.notifications.profile-not-verified" msgstr "" -"Profiel is niet geverifieerd, verifieer het profiel voordat je verder gaat." +"Profiel is niet geverifieerd. Verifieer het profiel voordat je verder gaat." + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.recovery-token-sent" +msgstr "Wachtwoordherstel-link is per e-mail naar je verzonden." #: src/app/main/ui/auth/verify_token.cljs msgid "auth.notifications.team-invitation-accepted" @@ -337,22 +137,72 @@ msgstr "Wachtwoord" msgid "auth.password-length-hint" msgstr "Minimaal 8 tekens" +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "Het wachtwoord mag geen spatie bevatten." + msgid "auth.privacy-policy" msgstr "Privacybeleid" +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-submit" +msgstr "Wachtwoord herstellen" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-subtitle" +msgstr "We sturen je een e-mail met instructies" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-title" +msgstr "Wachtwoord vergeten?" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.recovery-submit" +msgstr "Wachtwoord wijzigen" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.register" +msgstr "Nog geen account?" + #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs msgid "auth.register-submit" msgstr "Account aanmaken" +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-subtitle" +msgstr "Het is gratis, het is open source" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-title" +msgstr "Account aanmaken" + +#: src/app/main/ui/auth.cljs +msgid "auth.sidebar-tagline" +msgstr "De open-source oplossing voor ontwerp en prototyping." + msgid "auth.terms-of-service" msgstr "Gebruiksvoorwaarden" #: src/app/main/ui/auth/register.cljs msgid "auth.terms-privacy-agreement" msgstr "" -"Bij het aanmaken van een nieuw account ga je akkoord met onze " +"Met het aanmaken van een nieuw account ga je akkoord met onze " "gebruiksvoorwaarden en ons privacybeleid." +#: src/app/main/ui/auth/register.cljs +msgid "auth.verification-email-sent" +msgstr "We hebben een verificatie-e-mail verzonden naar" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "…branding, illustraties, marketingstukken, etc." + +msgid "common.publish" +msgstr "Publiceren" + +msgid "common.share-link.all-users" +msgstr "Alle Penpot-gebruikers" + msgid "common.share-link.confirm-deletion-link-description" msgstr "" "Weet je zeker dat je deze link wilt verwijderen? Als je dit doet, is het " @@ -362,7 +212,7 @@ msgid "common.share-link.current-tag" msgstr "(huidig)" msgid "common.share-link.destroy-link" -msgstr "Vernietig link" +msgstr "Link verwijderen" msgid "common.share-link.get-link" msgstr "Link ophalen" @@ -390,9 +240,21 @@ msgstr "Iedereen met de link heeft toegang" msgid "common.share-link.permissions-pages" msgstr "Gedeelde pagina's" +msgid "common.share-link.placeholder" +msgstr "De deelbare link zal hier verschijnen" + +msgid "common.share-link.team-members" +msgstr "Alleen teamleden" + msgid "common.share-link.title" msgstr "Prototypes delen" +msgid "common.share-link.view-all" +msgstr "Alles selecteren" + +msgid "common.unpublish" +msgstr "Publicatie ongedaan maken" + #: src/app/main/ui/dashboard/projects.cljs msgid "dasboard.team-hero.management" msgstr "Teambeheer" @@ -403,19 +265,23 @@ msgstr "" "Penpot is bedoeld voor teams. Nodig leden uit om samen te werken aan " "projecten en bestanden" +#: src/app/main/ui/dashboard/projects.cljs +msgid "dasboard.team-hero.title" +msgstr "Werk samen!" + #: src/app/main/ui/dashboard/projects.cljs msgid "dasboard.tutorial-hero.info" msgstr "" "Leer de basisprincipes van Penpot terwijl je plezier hebt met deze " -"interactieve tutorial." +"interactieve introductie." #: src/app/main/ui/dashboard/projects.cljs msgid "dasboard.tutorial-hero.start" -msgstr "Start de tutorial" +msgstr "Start de introductie" #: src/app/main/ui/dashboard/projects.cljs msgid "dasboard.tutorial-hero.title" -msgstr "Praktische tutorial" +msgstr "Praktische introductie" #: src/app/main/ui/dashboard/projects.cljs msgid "dasboard.walkthrough-hero.info" @@ -430,24 +296,132 @@ msgstr "Start de rondleiding" msgid "dasboard.walkthrough-hero.title" msgstr "Rondleiding door de interface" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "Toegangsbewijs gekopieerd" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "Nieuw toegangsbewijs aanmaken" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "Toegangsbewijs is succesvol aangemaakt." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "" +"Klik op de knop \"Nieuw toegangsbewijs aanmaken\" om er een aan te maken." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "Je hebt nog geen toegangsbewijzen." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "De naam is verplicht" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 dagen" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 dagen" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 dagen" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 dagen" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "Nooit" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "Verlopen op %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "Verloopt op %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "Geen verloopdatum" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Persoonlijke toegangsbewijzen" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Persoonlijke toegangsbewijzen functioneren als alternatief voor ons login/" +"wachtwoord-authenticatiesysteem en kunnen worden gebruikt om een applicatie " +"toegang te geven tot de interne Penpot API" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "Het toegangsbewijs verloopt op %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "Het toegangsbewijs heeft geen verloopdatum" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Toevoegen als gedeelde bibliotheek" +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.change-email" +msgstr "E-mailadres wijzigen" + +#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs +msgid "dashboard.copy-suffix" +msgstr "(kopie)" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.create-new-team" +msgstr "Nieuw team aanmaken" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.default-team-name" +msgstr "Jouw Penpot" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.delete-team" +msgstr "Team verwijderen" + msgid "dashboard.download-binary-file" -msgstr "Download Penpot-bestand (.penpot)" +msgstr "Penpot-bestand downloaden (.penpot)" msgid "dashboard.download-standard-file" -msgstr "Download standaardbestand (.svg + .json)" +msgstr "Standaardbestand downloaden (.svg + .json)" + +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate" +msgstr "Dupliceren" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate-multi" +msgstr "%s bestanden dupliceren" #: src/app/main/ui/dashboard/grid.cljs -#, markdown msgid "dashboard.empty-placeholder-drafts" msgstr "" "Bestanden die aan bibliotheken zijn toegevoegd, worden hier weergegeven. " -"Probeer uw bestanden te delen of toe te voegen vanuit onze [Bibliotheken & " +"Probeer je bestanden te delen of toe te voegen vanuit onze [Bibliotheken & " "sjablonen] (https://penpot.app/libraries-templates.html)." +msgid "dashboard.export-binary-multi" +msgstr "%s Penpot-bestanden downloaden (.penpot)" + msgid "dashboard.export-frames" msgstr "Borden exporteren als PDF" @@ -456,7 +430,7 @@ msgid "dashboard.export-frames.title" msgstr "Exporteren als PDF" msgid "dashboard.export-multi" -msgstr "Exporteer %s Penpot-bestanden" +msgstr "%s Penpot-bestanden exporteren" #: src/app/main/ui/export.cljs msgid "dashboard.export-multiple.selected" @@ -466,6 +440,16 @@ msgstr "%s van %s elementen geselecteerd" msgid "dashboard.export-shapes" msgstr "Exporteren" +#: src/app/main/ui/export.cljs +msgid "dashboard.export-shapes.how-to" +msgstr "" +"Je kunt exportinstellingen toevoegen aan elementen vanuit de " +"ontwerpeigenschappen (onderaan de rechter zijbalk)." + +#: src/app/main/ui/export.cljs +msgid "dashboard.export-shapes.how-to-link" +msgstr "Info over het instellen van exports in Penpot." + #: src/app/main/ui/export.cljs msgid "dashboard.export-shapes.no-elements" msgstr "Er zijn geen elementen met exportinstellingen." @@ -475,21 +459,31 @@ msgid "dashboard.export-shapes.title" msgstr "Selectie exporteren" msgid "dashboard.export-standard-multi" -msgstr "Download %s standaardbestanden (.svg + .json)" +msgstr "%s standaardbestanden downloaden (.svg + .json)" msgid "dashboard.export.detail" -msgstr "* Kan componenten, afbeeldingen, kleuren en/of typografieën bevatten." +msgstr "* Kan componenten, afbeeldingen, kleuren en/of typografie bevatten." msgid "dashboard.export.explain" msgstr "" "Een of meer bestanden die je wilt exporteren maken gebruik van gedeelde " "bibliotheken. Wat wil je doen met hun assets*?" +msgid "dashboard.export.options.all.message" +msgstr "" +"Bestanden met gedeelde bibliotheken worden opgenomen in de export en hun " +"koppelingen worden behouden." + msgid "dashboard.export.options.all.title" msgstr "Gedeelde bibliotheken exporteren" +msgid "dashboard.export.options.detach.message" +msgstr "" +"Gedeelde bibliotheken worden niet opgenomen in de export en er worden geen " +"assets aan de bibliotheek toegevoegd. " + msgid "dashboard.export.options.detach.title" -msgstr "Behandel gedeelde bibliotheek-assets als basisobjecten" +msgstr "Gedeelde bibliotheek-assets als basisobjecten behandelen" msgid "dashboard.export.options.merge.message" msgstr "" @@ -502,20 +496,50 @@ msgstr "Inclusief gedeelde bibliotheek-assets in bestandsbibliotheken" msgid "dashboard.export.title" msgstr "Bestanden exporteren" +msgid "dashboard.fonts.deleted-placeholder" +msgstr "Lettertype ontbreekt" + #: src/app/main/ui/dashboard/fonts.cljs msgid "dashboard.fonts.dismiss-all" msgstr "Alles negeren" +msgid "dashboard.fonts.empty-placeholder" +msgstr "Aangepaste lettertypen die je uploadt, verschijnen hier." + #: src/app/main/ui/dashboard/fonts.cljs msgid "dashboard.fonts.fonts-added" msgid_plural "dashboard.fonts.fonts-added" msgstr[0] "1 lettertype toegevoegd" msgstr[1] "%s lettertypes toegevoegd" +msgid "dashboard.fonts.hero-text1" +msgstr "" +"Elk web-lettertype dat je hier uploadt zal worden toegevoegd aan de " +"lettertypen die beschikbaar zijn in de tekstbestanden van dit team. " +"Lettertypen worden gegroepeerd op familienaam. Je kunt lettertypen uploaden " +"met de volgende formaten: **TTF, OTF en WOFF** (slechts één formaat nodig)." + +msgid "dashboard.fonts.hero-text2" +msgstr "" +"Je mag alleen lettertypen uploaden waarvan je de eigenaar bent of waarvoor " +"je een licentie hebt om te gebruiken in Penpot. Lees meer in de sectie " +"Inhoudsrechten van [Penpot's Servicevoorwaarden](https://penpot.app/terms." +"html). Misschien wil je ook meer lezen over [lettertypelicenties](https://" +"www.typography.com/faq)." + #: src/app/main/ui/dashboard/fonts.cljs msgid "dashboard.fonts.upload-all" msgstr "Alles uploaden" +msgid "dashboard.fonts.warning-text" +msgstr "" +"We hebben een mogelijk probleem gevonden in je lettertypen met betrekking " +"tot verticale metriek voor verschillende besturingssystemen. Om het te " +"controleren, kun je verticale metrische lettertype-services zoals [deze] " +"gebruiken (https://vertical-metrics.netlify.app/). Daarnaast raden we aan " +"[Transfonter](https://transfonter.org/) te gebruiken om web-lettertypen te " +"genereren en soortgelijke fouten op te lossen. " + msgid "dashboard.import" msgstr "Importeer Penpot-bestanden" @@ -527,14 +551,31 @@ msgstr "" "Er is een probleem opgetreden bij het importeren van het bestand. Het " "bestand is niet geïmporteerd." +msgid "dashboard.import.import-message" +msgid_plural "dashboard.import.import-message" +msgstr[0] "1 bestand is geïmporteerd." +msgstr[1] "%s bestanden zijn geïmporteerd." + +msgid "dashboard.import.import-warning" +msgstr "Sommige bestanden bevatten ongeldige objecten die verwijderd zijn." + msgid "dashboard.import.progress.process-colors" msgstr "Kleuren aan het verwerken" +msgid "dashboard.import.progress.process-components" +msgstr "Componenten aan het verwerken" + +msgid "dashboard.import.progress.process-media" +msgstr "Media aan het verwerken" + msgid "dashboard.import.progress.process-page" msgstr "Pagina aan het verwerkten: %s" msgid "dashboard.import.progress.process-typographies" -msgstr "Typografieën aan het verwerken" +msgstr "Typografie verwerken" + +msgid "dashboard.import.progress.upload-data" +msgstr "Gegevens uploaden naar server (%s/%s)" msgid "dashboard.import.progress.upload-media" msgstr "Bestand aan het uploaden: %s" @@ -543,18 +584,35 @@ msgstr "Bestand aan het uploaden: %s" msgid "dashboard.invite-profile" msgstr "Nodig mensen uit" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" msgstr "Team verlaten" +msgid "dashboard.libraries-and-templates" +msgstr "Bibliotheken & sjablonen" + +msgid "dashboard.libraries-and-templates.explore" +msgstr "Ontdek er meer van en weet hoe je kunt bijdragen" + +msgid "dashboard.libraries-and-templates.import-error" +msgstr "" +"Er is een probleem opgetreden bij het importeren van het sjabloon. Het " +"sjabloon is niet geïmporteerd." + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "dashboard.libraries-title" +msgstr "Bibliotheken" + #: src/app/main/ui/dashboard/grid.cljs msgid "dashboard.loading-files" -msgstr "Je bestanden aan het laden …" +msgstr "bestanden laden …" msgid "dashboard.loading-fonts" -msgstr "Je lettertypes worden geladen …" +msgstr "lettertypen laden …" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "Verplaats naar" @@ -566,628 +624,72 @@ msgstr "Verplaats %s bestanden naar" msgid "dashboard.move-to-other-team" msgstr "Verplaats naar ander team" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" msgstr "+ Nieuw bestand" +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-file-prefix" +msgstr "Nieuw bestand" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.new-project" +msgstr "+ Nieuw project" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-project-prefix" +msgstr "Nieuw project" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.no-matches-for" +msgstr "Geen overeenkomsten gevonden voor \"%s\"" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.no-projects-placeholder" +msgstr "Vastgemaakte projecten worden hier weergegeven" + #: src/app/main/ui/auth/verify_token.cljs msgid "dashboard.notifications.email-changed-successfully" msgstr "Je e-mailadres is succesvol bijgewerkt" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-move-file" -msgstr "Je bestand is succesvol verplaatst" - -#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs -msgid "dashboard.update-settings" -msgstr "Instellingen aanpassen" - -msgid "dashboard.webhooks.create.success" -msgstr "Webhook is succesvol aangemaakt." - -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs -msgid "errors.generic" -msgstr "Er ging iets mis." - #: src/app/main/ui/auth/verify_token.cljs -msgid "errors.invite-invalid" -msgstr "Uitnodiging ongeldig" - -msgid "errors.invite-invalid.info" -msgstr "Deze uitnodiging is mogelijk geannuleerd of verlopen." - -msgid "dashboard.webhooks.empty.add-one" -msgstr "Druk op de knop \"Maak webhook\" om er een aan te maken." - -msgid "dashboard.webhooks.empty.no-webhooks" -msgstr "Er zijn nog geen webhooks aangemaakt." - -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs -msgid "errors.media-type-mismatch" -msgstr "" -"Het lijkt erop dat de inhoud van de afbeelding niet overeenkomt met de " -"bestandsextensie." - -msgid "dashboard.webhooks.update.success" -msgstr "Webhook is aangepast." - -#: src/app/main/ui/settings.cljs -msgid "dashboard.your-account-title" -msgstr "Je account" - -#: src/app/main/ui/dashboard/team.cljs -msgid "errors.member-is-muted" -msgstr "" -"Het profiel dat je uitnodigt, heeft e-mails gedempt (spammeldingen of hoge " -"bounces)." +msgid "dashboard.notifications.email-verified-successfully" +msgstr "Je e-mailadres is geverifieerd" #: src/app/main/ui/settings/password.cljs -msgid "errors.password-too-short" -msgstr "Wachtwoord moet minimaal 8 tekens lang zijn" +msgid "dashboard.notifications.password-saved" +msgstr "Wachtwoord succesvol opgeslagen!" -#: src/app/main/ui/settings/profile.cljs -msgid "dashboard.your-email" -msgstr "E-mail" - -#: src/app/main/ui/settings/profile.cljs -msgid "dashboard.your-name" -msgstr "Je naam" - -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.your-penpot" -msgstr "Je Penpot" - -#: src/app/main/ui/alert.cljs -msgid "ds.alert-ok" -msgstr "Ok" - -#: src/app/main/ui/auth/register.cljs -msgid "errors.registration-disabled" -msgstr "De registratie is momenteel uitgeschakeld." - -#: src/app/main/ui/alert.cljs -msgid "ds.alert-title" -msgstr "Waarschuwing" - -#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs -msgid "errors.unexpected-error" -msgstr "Er is een onverwachte fout opgetreden." - -#: src/app/main/ui/confirm.cljs -msgid "ds.component-subtitle" -msgstr "Componenten te updaten:" - -#: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs -msgid "ds.confirm-title" -msgstr "Weet je het zeker?" - -#: src/app/main/ui/confirm.cljs -msgid "ds.confirm-ok" -msgstr "Ok" - -#: src/app/main/ui/dashboard/sidebar.cljs -msgid "labels.cancel" -msgstr "Annuleren" - -msgid "labels.close" -msgstr "Sluiten" - -#: src/app/main/ui/auth/login.cljs -msgid "errors.auth-provider-not-configured" -msgstr "Authenticatie-provider nog niet geconfigureerd." - -msgid "labels.continue-with" -msgstr "Doorgaan met" - -msgid "errors.auth.unable-to-login" -msgstr "Het lijkt erop dat je niet ingelogd bent, of je sessie is verlopen." - -msgid "errors.bad-font" -msgstr "Het lettertype %s kon niet geladen worden" - -msgid "errors.bad-font-plural" -msgstr "De lettertypen %s kunnen niet geladen worden" - -#: src/app/main/ui/settings/sidebar.cljs -msgid "labels.dashboard" -msgstr "Dashboard" - -#: src/app/main/data/workspace.cljs -msgid "errors.clipboard-not-implemented" -msgstr "Je browser kan deze operatie niet uitvoeren" +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.num-of-members" +msgstr "%s leden" #: src/app/main/ui/dashboard/file_menu.cljs -msgid "labels.delete-multi-files" -msgstr "Verwijder %s bestanden" +msgid "dashboard.open-in-new-tab" +msgstr "Bestand openen in een nieuw tabblad" -#: src/app/main/ui/comments.cljs -msgid "labels.edit" -msgstr "Bewerken" - -msgid "labels.edit-file" -msgstr "Bewerk bestand" - -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs -msgid "labels.editor" -msgstr "Editor" - -#: src/app/main/ui/settings/feedback.cljs -msgid "labels.feedback-disabled" -msgstr "Feedback uitgeschakeld" - -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs -msgid "errors.email-already-exists" -msgstr "E-mail is al in gebruik" - -#: src/app/main/ui/auth/verify_token.cljs -msgid "errors.email-already-validated" -msgstr "E-mail is al gevalideerd." - -msgid "errors.email-as-password" -msgstr "Je kan je e-mail niet als wachtwoord gebruiken" - -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs -msgid "errors.email-has-permanent-bounces" -msgstr "Het emailadres «%s» heeft veel permanente bounce-rapporten." - -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs -msgid "errors.email-invalid" -msgstr "Voer een geldig e-mailadres in" - -#: src/app/main/ui/settings/change_email.cljs -msgid "errors.email-invalid-confirmation" -msgstr "Bevestigingsmail moet overeenkomen" - -msgid "labels.installed-fonts" -msgstr "Geïnstalleerde lettertypen" - -#: src/app/main/ui/static.cljs -msgid "labels.internal-error.main-message" -msgstr "Interne fout" - -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs -msgid "labels.invitations" -msgstr "Uitnodigingen" - -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs -msgid "labels.members" -msgstr "Leden" +msgid "dashboard.options" +msgstr "Opties" #: src/app/main/ui/settings/password.cljs -msgid "labels.new-password" -msgstr "Nieuw wachtwoord" - -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs -msgid "labels.no-comments-available" -msgstr "Je bent helemaal bij! Nieuwe reactiemeldingen verschijnen hier." - -#: src/app/main/ui/dashboard/team.cljs -#, markdown -msgid "labels.no-invitations-hint" -msgstr "" -"Klik op de knop **Mensen uitnodigen** om mensen uit te nodigen voor dit team." - -msgid "labels.num-of-frames" -msgid_plural "labels.num-of-frames" -msgstr[0] "1 bord" -msgstr[1] "%s borden" - -#: src/app/main/ui/static.cljs -msgid "labels.not-found.desc-message" -msgstr "Deze pagina bestaat mogelijk niet of je hebt geen toegangsrechten." - -#: src/app/main/ui/dashboard/team.cljs -msgid "labels.num-of-projects" -msgid_plural "labels.num-of-projects" -msgstr[0] "1 project" -msgstr[1] "%s projecten" - -msgid "labels.or" -msgstr "of" - -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs -msgid "labels.owner" -msgstr "Eigenaar" - -#: src/app/main/ui/settings/sidebar.cljs -msgid "labels.profile" -msgstr "Profiel" - -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs -msgid "labels.password" -msgstr "Wachtwoord" - -#: src/app/main/ui/dashboard/team.cljs -msgid "labels.pending-invitation" -msgstr "In behandeling" - -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs -msgid "labels.show-your-comments" -msgstr "Alleen eigen commentaar tonen" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discourse-subtitle1" -msgstr "" -"We zijn blij dat je er bent. Als je hulp nodig hebt, zoek dan eerst voordat " -"je iets plaatst." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.title" -msgstr "E-mail" - -#: src/app/main/ui/inspect/attributes/layout.cljs -msgid "inspect.attributes.layout" -msgstr "Lay-out" - -#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs -msgid "inspect.attributes.layout.radius" -msgstr "Radius" - -#: src/app/main/ui/inspect/attributes/layout.cljs -msgid "inspect.attributes.layout.width" -msgstr "Breedte" - -#: src/app/main/ui/inspect/right_sidebar.cljs -msgid "inspect.tabs.code" -msgstr "Code" - -#: src/app/main/ui/inspect/attributes/stroke.cljs -msgid "inspect.attributes.stroke.width" -msgstr "Breedte" - -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography" -msgstr "Typografie" - -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-family" -msgstr "Fontfamilie" - -msgid "inspect.attributes.typography.text-decoration.strikethrough" -msgstr "Doorhalen" - -msgid "inspect.empty.select" -msgstr "" -"Selecteer een vorm, bord of groep om hun eigenschappen en code te inspecteren" - -msgid "labels.and" -msgstr "en" - -msgid "inspect.tabs.code.selected.svg-raw" -msgstr "SVG" - -msgid "inspect.tabs.code.selected.text" -msgstr "Tekst" - -#: src/app/main/ui/static.cljs -msgid "labels.bad-gateway.desc-message" -msgstr "" -"Het lijkt erop dat je even moet wachten en het opnieuw moet proberen; we " -"voeren klein onderhoud uit aan onze servers." - -#: src/app/main/ui/comments.cljs -msgid "labels.write-new-comment" -msgstr "Schrijf een nieuwe reactie" - -#: src/app/main/ui/dashboard/sidebar.cljs -msgid "labels.your-account" -msgstr "Je account" - -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs -msgid "media.loading" -msgstr "Afbeelding laden…" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.add-shared-confirm.hint" -msgstr "" -"Eenmaal toegevoegd als gedeelde bibliotheek, zijn de items van deze " -"bestandsbibliotheek beschikbaar voor gebruik tussen de rest van uw bestanden." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.add-shared-confirm.message" -msgstr "Voeg \"%s\" toe als gedeelde bibliotheek" - -#: src/app/main/ui/settings/change_email.cljs -msgid "modals.change-email.confirm-email" -msgstr "Controleer nieuwe e-mail" - -#: src/app/main/ui/settings/change_email.cljs -msgid "modals.change-email.info" -msgstr "" -"We sturen je een e-mail naar je huidige e-mailadres \"%s\" om je identiteit " -"te verifiëren." - -#: src/app/main/ui/settings/change_email.cljs -msgid "modals.change-email.new-email" -msgstr "Nieuw e-mailadres" - -#: src/app/main/ui/settings/change_email.cljs -msgid "modals.change-email.submit" -msgstr "Verander e-mailadres" - -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-file-multi-confirm.message" -msgstr "Weet je zeker dat je %s bestanden wilt verwijderen?" - -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-file-multi-confirm.title" -msgstr "Bezig met verwijderen van %s bestanden" - -msgid "modals.delete-font-variant.message" -msgstr "" -"Weet u zeker dat u deze lettertypestijl wilt verwijderen? Het wordt dan niet " -"meer geladen als het in een bestand wordt gebruikt." - -msgid "modals.delete-font-variant.title" -msgstr "Lettertypestijl verwijderen" - -msgid "modals.delete-font.message" -msgstr "" -"Weet u zeker dat u dit lettertype wilt verwijderen? Het wordt dan niet meer " -"geladen als het in een bestand wordt gebruikt." - -#: src/app/main/ui/workspace/sidebar/sitemap.cljs -msgid "modals.delete-page.body" -msgstr "Weet je zeker dat je deze pagina wilt verwijderen?" - -#: src/app/main/ui/workspace/sidebar/sitemap.cljs -msgid "modals.delete-page.title" -msgstr "Verwijder pagina" +msgid "dashboard.password-change" +msgstr "Verander wachtwoord" #: src/app/main/ui/dashboard/project_menu.cljs -msgid "modals.delete-project-confirm.accept" -msgstr "Verwijder project" +msgid "dashboard.pin-unpin" +msgstr "Vastzetten/losmaken" -#: src/app/main/ui/dashboard/project_menu.cljs -msgid "modals.delete-project-confirm.message" -msgstr "Weet je zeker dat je dit project wilt verwijderen?" +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.projects-title" +msgstr "Projecten" -msgid "shortcuts.add-comment" -msgstr "Commentaar" +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.remove-account" +msgstr "Wil je je account verwijderen?" -msgid "shortcuts.add-node" -msgstr "Knooppunt toevoegen" - -msgid "shortcuts.align-bottom" -msgstr "Beneden uitlijnen" - -msgid "shortcuts.align-center" -msgstr "Centreren" - -msgid "shortcuts.align-hcenter" -msgstr "Horizontaal centreren" - -msgid "shortcuts.bold" -msgstr "Schakel vet in" - -msgid "shortcuts.align-justify" -msgstr "Uitlijnen" - -msgid "shortcuts.align-left" -msgstr "Links uitlijnen" - -msgid "shortcuts.align-right" -msgstr "Rechts uitlijnen" - -msgid "shortcuts.align-top" -msgstr "Boven uitlijnen" - -msgid "shortcuts.align-vcenter" -msgstr "Verticaal centreren" - -msgid "shortcuts.artboard-selection" -msgstr "Maak bord van selectie" - -msgid "shortcuts.bool-exclude" -msgstr "Uitsluiten" - -msgid "shortcuts.bool-difference" -msgstr "Aftrekken (Booleaans verschil)" - -msgid "shortcuts.bool-intersection" -msgstr "Booleaanse kruising" - -msgid "shortcuts.bool-union" -msgstr "Verenigen" - -msgid "shortcuts.bring-back" -msgstr "Stuur naar de achtergrond" - -msgid "shortcuts.bring-front" -msgstr "Stuur naar de voorgrond" - -msgid "shortcuts.bring-backward" -msgstr "Stuur naar achteren" - -msgid "shortcuts.bring-forward" -msgstr "Stuur naar voren" - -msgid "shortcuts.clear-undo" -msgstr "Ongedaan maken terugdraaien" - -msgid "shortcuts.copy" -msgstr "Kopieëren" - -msgid "shortcuts.create-new-project" -msgstr "Nieuw project maken" - -msgid "shortcuts.create-component" -msgstr "Component aanmaken" - -msgid "shortcuts.cut" -msgstr "Knippen" - -msgid "shortcuts.decrease-zoom" -msgstr "Uitzoomen" - -msgid "shortcuts.font-size-inc" -msgstr "Vergroot de lettergrootte" - -msgid "modals.delete-webhook.title" -msgstr "Webhook verwijderen" - -msgid "modals.edit-webhook.submit-label" -msgstr "Wijzig webhook" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Als u de publicatie ongedaan maakt, zijn die elementen niet langer " -"beschikbaar vanuit andere bestanden. Elementen die al gebruikt zijn, blijven " -"in dit bestand staan (er gaat geen ontwerp kapot!)." -msgstr[1] "" -"Als u de publicatie ervan ongedaan maakt, zijn die elementen niet langer " -"beschikbaar vanuit andere bestanden. Elementen die al gebruikt zijn, blijven " -"in dit bestand staan (er gaat geen ontwerp kapot!)." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "" -"Als u de publicatie ongedaan maakt, zijn die elementen niet langer " -"beschikbaar vanuit andere bestanden. Elementen die al gebruikt zijn, blijven " -"in deze bestanden staan (er gaat geen ontwerp kapot!)." -msgstr[1] "" -"Als u de publicatie ervan ongedaan maakt, zijn die elementen niet langer " -"beschikbaar vanuit andere bestanden. Elementen die al gebruikt zijn, blijven " -"in deze bestanden staan (er gaat geen ontwerp kapot!)." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "" -"Om het opnieuw te proberen, kunt u dit bestand opnieuw laden. Als het " -"probleem zich blijft voordoen, raden we u aan de lijst te bekijken en te " -"overwegen om kapotte afbeeldingen te verwijderen." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.remove-shared-confirm.hint" -msgstr "" -"Eenmaal verwijderd als gedeelde bibliotheek, is de bestandsbibliotheek van " -"dit bestand niet meer beschikbaar voor gebruik onder de rest van uw " -"bestanden." - -msgid "onboarding-v2.newsletter.privacy2" -msgstr "" -"We sturen u alleen relevante e-mails. U kunt zich op elk moment afmelden via " -"de afmeldlink in al onze nieuwsbrieven." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "Enkele van de elementen in deze bibliotheek worden hier gebruikt:" -msgstr[1] "Enkele van de middelen in deze bibliotheken worden hier gebruikt:" - -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs -msgid "modals.update-remote-component-in-bulk.hint" -msgstr "" -"Je staat op het punt componenten in een gedeelde bibliotheek bij te werken. " -"Dit kan van invloed zijn op andere bestanden die er gebruik van maken." - -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.colors" -msgstr "Kleuren" - -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.components" -msgstr "Componenten" - -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.delete" -msgstr "Verwijderen" - -msgid "workspace.focus.selection" -msgstr "Selectie" - -#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs -msgid "workspace.options.blur-options.title" -msgstr "Vervagen" - -#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.constraints.center" -msgstr "Midden" - -#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.constraints.left" -msgstr "Links" - -#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.constraints.right" -msgstr "Rechts" - -#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.constraints.top" -msgstr "Bovenkant" - -#: src/app/main/ui/workspace/sidebar/options.cljs -msgid "workspace.options.design" -msgstr "Ontwerp" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.gutter" -msgstr "Gutter" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.rows" -msgstr "Rijen" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.size" -msgstr "Grootte" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.type.left" -msgstr "Links" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.square" -msgstr "Vierkant" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.row" -msgstr "Rijen" - -msgid "workspace.options.height" -msgstr "Hoogte" - -msgid "workspace.options.inspect" -msgstr "Inspecteren" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-action" -msgstr "Actie" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-animation" -msgstr "Animatie" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.select-a-shape" -msgstr "" -"Selecteer een vorm, bord of groep om een verbinding naar een ander bord te " -"slepen." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "" -"Bibliotheekafbeeldingen zijn vanaf nu componenten, waardoor ze veel " -"krachtiger worden." - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.use-play-button" -msgstr "" -"Gebruik de afspeelknop in de koptekst om de prototypeweergave uit te voeren." - -#, markdown -msgid "dashboard.fonts.hero-text1" -msgstr "" -"Elke web lettertype die je hier uploadt zal worden toegevoegd aan de fonts " -"die beschikbaar is in de tekstbestanden van dit team. Fonts met dezelfde " -"font family-naam zullen gegroepeerd worden als een font family. Je kunt " -"fonts uploaden met de volgende formats: TTF, OTF en WOFFJ (Je hebt één " -"formaat nodig)." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" msgstr "Verwijder als gedeelde bibliotheek" @@ -1197,7 +699,7 @@ msgstr "Instellingen opslaan" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.search-placeholder" -msgstr "Zoek…" +msgstr "Zoeken…" #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.searching-for" @@ -1213,7 +715,7 @@ msgstr "Thema selecteren" #: src/app/main/ui/dashboard/grid.cljs msgid "dashboard.show-all-files" -msgstr "Toon alle bestanden" +msgstr "Alle bestanden tonen" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-delete-file" @@ -1235,6 +737,11 @@ msgstr[1] "Je bestanden zijn succesvol gedupliceerd" msgid "dashboard.success-duplicate-project" msgstr "Je project is succesvol gedupliceerd" +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-file" +msgstr "Je bestand is succesvol verplaatst" + #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-files" msgstr "Je bestanden zijn succesvol verplaatst" @@ -1267,10 +774,16 @@ msgstr "Zoekresultaten" msgid "dashboard.type-something" msgstr "Typ om te zoeken" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.unpublish-shared" msgstr "Publicatie Bibliotheek ongedaan maken" +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.update-settings" +msgstr "Instellingen bijwerken" + msgid "dashboard.webhooks.active" msgstr "Is actief" @@ -1284,77 +797,218 @@ msgstr "Contenttype" msgid "dashboard.webhooks.create" msgstr "Maak webhook" +msgid "dashboard.webhooks.create.success" +msgstr "Webhook is succesvol aangemaakt." + msgid "dashboard.webhooks.description" msgstr "" "Webhooks zijn een eenvoudige manier om andere websites en apps op de hoogte " "te stellen wanneer bepaalde gebeurtenissen bij Penpot plaatsvinden. We " -"sturen een POST-verzoek naar elke URL die u opgeeft." +"sturen een POST-verzoek naar elke URL die je opgeeft." + +msgid "dashboard.webhooks.empty.add-one" +msgstr "Druk op de knop \"Maak webhook\" om er een aan te maken." + +msgid "dashboard.webhooks.empty.no-webhooks" +msgstr "Er zijn nog geen webhooks aangemaakt." + +msgid "dashboard.webhooks.update.success" +msgstr "Webhook is bijgewerkt." + +#: src/app/main/ui/settings.cljs +msgid "dashboard.your-account-title" +msgstr "Jouw account" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-email" +msgstr "E-mail" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-name" +msgstr "Naam" + +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.your-penpot" +msgstr "Jouw Penpot" + +#: src/app/main/ui/alert.cljs +msgid "ds.alert-ok" +msgstr "Oké" + +#: src/app/main/ui/alert.cljs +msgid "ds.alert-title" +msgstr "Waarschuwing" + +#: src/app/main/ui/confirm.cljs +msgid "ds.component-subtitle" +msgstr "Componenten bijwerken:" #: src/app/main/ui/confirm.cljs msgid "ds.confirm-cancel" msgstr "Annuleren" +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-ok" +msgstr "Oké" + +#: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs +msgid "ds.confirm-title" +msgstr "Weet je het zeker?" + +#: src/app/main/ui/auth/login.cljs +msgid "errors.auth-provider-not-configured" +msgstr "Authenticatie-provider niet geconfigureerd." + +msgid "errors.auth.unable-to-login" +msgstr "" +"Het lijkt erop dat je niet geauthentiseerd bent of dat de sessie is verlopen." + +msgid "errors.bad-font" +msgstr "Het lettertype %s kon niet geladen worden" + +msgid "errors.bad-font-plural" +msgstr "De lettertypen %s konden niet geladen worden" + +msgid "errors.cannot-upload" +msgstr "Kan het mediabestand niet uploaden." + +#: src/app/main/data/workspace.cljs +msgid "errors.clipboard-not-implemented" +msgstr "Je browser kan deze functie niet uitvoeren" + +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/change_email.cljs +msgid "errors.email-already-exists" +msgstr "E-mail is al in gebruik" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.email-already-validated" +msgstr "E-mail is al gevalideerd." + +msgid "errors.email-as-password" +msgstr "Je kan je e-mail niet als wachtwoord gebruiken" + +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs +msgid "errors.email-has-permanent-bounces" +msgstr "Het emailadres «%s» heeft veel permanente bounce-rapporten." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, +#: src/app/main/ui/auth/recovery_request.cljs +msgid "errors.email-invalid" +msgstr "Voer een geldig e-mailadres in" + +#: src/app/main/ui/settings/change_email.cljs +msgid "errors.email-invalid-confirmation" +msgstr "Bevestigingsmail moet overeenkomen" + msgid "errors.email-spam-or-permanent-bounces" msgstr "Het e-mailadres «%s» is gemeld als spam of permanent teruggestuurd." +#: src/app/main/errors.cljs +msgid "errors.feature-mismatch" +msgstr "" +"Het lijkt erop dat je een bestand opent waarin de functie '%s' is " +"ingeschakeld, maar jouw Penpot- versie ondersteunt dit niet of heeft het " +"uitgeschakeld." + #: src/app/main/errors.cljs msgid "errors.feature-not-supported" msgstr "Functie '%s' wordt niet ondersteund." +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +msgid "errors.generic" +msgstr "Er ging iets mis." + #: src/app/main/ui/components/color_input.cljs msgid "errors.invalid-color" msgstr "Ongeldige kleur" +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.invite-invalid" +msgstr "Uitnodiging ongeldig" + +msgid "errors.invite-invalid.info" +msgstr "Deze uitnodiging is mogelijk geannuleerd of verlopen." + #: src/app/main/ui/auth/login.cljs msgid "errors.ldap-disabled" msgstr "LDAP-authenticatie is uitgeschakeld." #: src/app/main/errors.cljs msgid "errors.max-quote-reached" -msgstr "Je hebt de quotum van '%s' bereikt. Contacteer de ondersteuning." +msgstr "Je hebt de limiet van '%s' bereikt. Neem contact op met support." #: src/app/main/data/workspace/persistence.cljs msgid "errors.media-too-large" msgstr "De afbeelding is te groot om in te voegen." -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "errors.media-type-mismatch" +msgstr "" +"Het lijkt erop dat de inhoud van de afbeelding niet overeenkomt met de " +"bestandsextensie." + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" msgstr "Het lijkt erop dat dit geen geldige afbeelding is." +#: src/app/main/ui/dashboard/team.cljs +msgid "errors.member-is-muted" +msgstr "" +"Het profiel dat je uitnodigt, heeft e-mails gedempt (spammeldingen of hoge " +"bounces)." + #: src/app/main/ui/settings/password.cljs msgid "errors.password-invalid-confirmation" msgstr "Bevestigingswachtwoord moet overeenkomen" +#: src/app/main/ui/settings/password.cljs +msgid "errors.password-too-short" +msgstr "Wachtwoord moet minimaal 8 tekens lang zijn" + msgid "errors.profile-blocked" msgstr "Het profiel is geblokkeerd" -#: src/app/main/errors.cljs -msgid "errors.feature-mismatch" -msgstr "" -"Het lijkt erop dat u een bestand opent waarin de functie '%s' is " -"ingeschakeld, maar uw penpot-frontend ondersteunt dit niet of heeft het " -"uitgeschakeld." - -#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.profile-is-muted" msgstr "Je profiel heeft e-mails gedempt (spammeldingen of hoge bounces)." +#: src/app/main/ui/auth/register.cljs +msgid "errors.registration-disabled" +msgstr "De registratie is momenteel uitgeschakeld." + msgid "errors.team-leave.insufficient-members" msgstr "" -"Onvoldoende leden om het team te verlaten, u wilt het team waarschijnlijk " +"Onvoldoende leden om het team te verlaten, je kunt dit team maar beter " "verwijderen." msgid "errors.team-leave.member-does-not-exists" -msgstr "Het lid dat u probeert toe te wijzen, bestaat niet." +msgstr "Het lid dat je probeert toe te wijzen, bestaat niet." msgid "errors.team-leave.owner-cant-leave" msgstr "" -"Eigenaar kan het team niet verlaten, u moet de rol van eigenaar eerst " +"Eigenaar kan het team niet verlaten, je moet de rol van eigenaar eerst " "opnieuw toewijzen." +#: src/app/main/data/media.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs +msgid "errors.unexpected-error" +msgstr "Er is een onverwachte fout opgetreden." + #: src/app/main/ui/auth/verify_token.cljs msgid "errors.unexpected-token" -msgstr "Onbekende token" +msgstr "Onbekend bewijsstuk" msgid "errors.webhooks.connection" msgstr "Verbindingsfout, URL niet bereikbaar" @@ -1379,11 +1033,11 @@ msgstr "Onverwachte status %s" #: src/app/main/ui/auth/login.cljs msgid "errors.wrong-credentials" -msgstr "Email of wachtwoord is incorrect." +msgstr "E-mailadres of wachtwoord is incorrect." #: src/app/main/ui/settings/password.cljs msgid "errors.wrong-old-password" -msgstr "Oud wachtwoord is onjuist" +msgstr "Huidige wachtwoord is onjuist" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.description" @@ -1393,9 +1047,15 @@ msgstr "Omschrijving" msgid "feedback.discourse-go-to" msgstr "Ga naar het Penpot-forum" +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discourse-subtitle1" +msgstr "" +"We zijn blij dat je er bent. Als je hulp nodig hebt, zoek dan eerst voordat " +"je een nieuwe vraag stelt." + #: src/app/main/ui/settings/feedback.cljs msgid "feedback.discourse-title" -msgstr "Penpot-community" +msgstr "Penpot-gemeenschap" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subject" @@ -1404,21 +1064,25 @@ msgstr "Onderwerp" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subtitle" msgstr "" -"Beschrijf de reden van uw e-mail en geef aan of het een probleem, een idee " -"of een twijfel is. Een lid van ons team zal zo snel mogelijk reageren." +"Beschrijf de reden van je e-mail en geef aan of het een probleem, een idee " +"of een twijfel betreft. Een lid van ons team zal zo snel mogelijk reageren." #: src/app/main/ui/settings/feedback.cljs -msgid "feedback.twitter-subtitle1" -msgstr "Hier om te helpen met uw technische vragen." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.twitter-title" -msgstr "X-ondersteuningsaccount" +msgid "feedback.title" +msgstr "E-mail" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" msgstr "Ga naar X" +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.twitter-subtitle1" +msgstr "Hier om te helpen met je technische vragen." + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.twitter-title" +msgstr "X-ondersteuningsaccount" + #: src/app/main/ui/settings/password.cljs msgid "generic.error" msgstr "er is een fout opgetreden" @@ -1459,6 +1123,10 @@ msgstr "Hoogte" msgid "inspect.attributes.image.width" msgstr "Breedte" +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout" +msgstr "Lay-out" + #: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.layout.height" msgstr "Hoogte" @@ -1467,6 +1135,11 @@ msgstr "Hoogte" msgid "inspect.attributes.layout.left" msgstr "Links" +#: src/app/main/ui/inspect/attributes/layout.cljs, +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.radius" +msgstr "Radius" + #: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.layout.rotation" msgstr "Rotatie" @@ -1475,6 +1148,10 @@ msgstr "Rotatie" msgid "inspect.attributes.layout.top" msgstr "Top" +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.layout.width" +msgstr "Breedte" + #: src/app/main/ui/inspect/attributes/shadow.cljs msgid "inspect.attributes.shadow" msgstr "Schaduw" @@ -1483,19 +1160,16 @@ msgstr "Schaduw" msgid "inspect.attributes.size" msgstr "Grootte en positie" -#, permanent +#: src/app/main/ui/inspect/attributes/stroke.cljs +msgid "inspect.attributes.stroke" +msgstr "Streek" + msgid "inspect.attributes.stroke.alignment.center" msgstr "Midden" -#: src/app/main/ui/inspect/attributes/stroke.cljs -msgid "inspect.attributes.stroke" -msgstr "Lijn" - -#, permanent msgid "inspect.attributes.stroke.alignment.inner" msgstr "Binnenkant" -#, permanent msgid "inspect.attributes.stroke.alignment.outer" msgstr "Buitenkant" @@ -1509,7 +1183,19 @@ msgid "inspect.attributes.stroke.style.none" msgstr "Geen" msgid "inspect.attributes.stroke.style.solid" -msgstr "Stevig" +msgstr "Solide" + +#: src/app/main/ui/inspect/attributes/stroke.cljs +msgid "inspect.attributes.stroke.width" +msgstr "Breedte" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography" +msgstr "Typografie" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-family" +msgstr "Lettertype-familie" #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.font-size" @@ -1517,7 +1203,7 @@ msgstr "Lettergrootte" #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.font-style" -msgstr "Lettertypestijl" +msgstr "Lettertype-stijl" #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.font-weight" @@ -1529,7 +1215,7 @@ msgstr "Letterafstand" #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.line-height" -msgstr "Lijnhoogte" +msgstr "Regelafstand" #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.text-decoration" @@ -1538,6 +1224,9 @@ msgstr "Tekst decoratie" msgid "inspect.attributes.typography.text-decoration.none" msgstr "Geen" +msgid "inspect.attributes.typography.text-decoration.strikethrough" +msgstr "Doorhalen" + msgid "inspect.attributes.typography.text-decoration.underline" msgstr "Onderstrepen" @@ -1552,19 +1241,27 @@ msgid "inspect.attributes.typography.text-transform.none" msgstr "Geen" msgid "inspect.attributes.typography.text-transform.titlecase" -msgstr "Title Case" +msgstr "Beginhoofdletters" msgid "inspect.attributes.typography.text-transform.uppercase" -msgstr "Hoofdletters" +msgstr "HOOFDLETTERS" msgid "inspect.empty.help" msgstr "" -"Als u meer wilt weten over ontwerpinspectie, gaat u naar het helpcentrum van " -"Penpot" +"Als je meer wilt weten over ontwerpinspectie, ga dan naar het helpcentrum " +"van Penpot" msgid "inspect.empty.more-info" msgstr "Meer info over inspecteren" +msgid "inspect.empty.select" +msgstr "" +"Selecteer een vorm, bord of groep om hun eigenschappen en code te inspecteren" + +#: src/app/main/ui/inspect/right_sidebar.cljs +msgid "inspect.tabs.code" +msgstr "Code" + msgid "inspect.tabs.code.selected.circle" msgstr "Cirkel" @@ -1572,7 +1269,7 @@ msgid "inspect.tabs.code.selected.component" msgstr "Component" msgid "inspect.tabs.code.selected.curve" -msgstr "Curve" +msgstr "Kromme" msgid "inspect.tabs.code.selected.frame" msgstr "Bord" @@ -1588,7 +1285,7 @@ msgstr "Masker" #: src/app/main/ui/inspect/right_sidebar.cljs msgid "inspect.tabs.code.selected.multiple" -msgstr "%s geselecteerd" +msgstr "%s Geselecteerd" msgid "inspect.tabs.code.selected.path" msgstr "Pad" @@ -1596,62 +1293,11 @@ msgstr "Pad" msgid "inspect.tabs.code.selected.rect" msgstr "Rechthoek" -#: src/app/main/ui/dashboard/comments.cljs -msgid "labels.comments" -msgstr "Commentaar" +msgid "inspect.tabs.code.selected.svg-raw" +msgstr "SVG" -#: src/app/main/ui/dashboard/sidebar.cljs -msgid "labels.community" -msgstr "Community" - -#: src/app/main/ui/settings/password.cljs -msgid "labels.confirm-password" -msgstr "Bevestig wachtwoord" - -msgid "labels.continue" -msgstr "Doorgaan" - -msgid "labels.continue-with-penpot" -msgstr "Je kunt doorgaan met een Penpot account" - -#: src/app/main/ui/dashboard/team.cljs -msgid "labels.copy-invitation-link" -msgstr "Kopieer link" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "labels.create" -msgstr "Aanmaken" - -#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs -msgid "labels.create-team" -msgstr "Maak nieuw team" - -#: src/app/main/ui/dashboard/team_form.cljs -msgid "labels.create-team.placeholder" -msgstr "Nieuwe teamnaam" - -msgid "labels.custom-fonts" -msgstr "Eigen lettertypes" - -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "labels.delete" -msgstr "Verwijder" - -#: src/app/main/ui/comments.cljs -msgid "labels.delete-comment" -msgstr "Verwijder commentaar" - -#: src/app/main/ui/comments.cljs -msgid "labels.delete-comment-thread" -msgstr "Verwijder thread" - -#: src/app/main/ui/dashboard/team.cljs -msgid "labels.delete-invitation" -msgstr "Verwijder uitnodiging" - -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "labels.drafts" -msgstr "Concepten" +msgid "inspect.tabs.code.selected.text" +msgstr "Tekst" #: src/app/main/ui/inspect/right_sidebar.cljs msgid "inspect.tabs.info" @@ -1659,18 +1305,22 @@ msgstr "Informatie" #: src/app/main/ui/workspace/header.cljs msgid "label.shortcuts" -msgstr "Snelkoppelingen" +msgstr "Sneltoetsen" msgid "labels.accept" msgstr "Accepteren" +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "Toegangsbewijzen" + msgid "labels.active" msgstr "Actief" msgid "labels.add-custom-font" -msgstr "Voeg eigen lettertype toe" +msgstr "Eigen lettertype toevoegen" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "Admin" @@ -1678,13 +1328,116 @@ msgstr "Admin" msgid "labels.all" msgstr "Alles" +msgid "labels.and" +msgstr "en" + msgid "labels.back" msgstr "Terug" +#: src/app/main/ui/static.cljs +msgid "labels.bad-gateway.desc-message" +msgstr "" +"Het lijkt erop dat je even moet wachten en het opnieuw moet proberen; we " +"voeren klein onderhoud uit aan onze servers." + #: src/app/main/ui/static.cljs msgid "labels.bad-gateway.main-message" msgstr "Bad Gateway" +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.cancel" +msgstr "Annuleren" + +msgid "labels.close" +msgstr "Sluiten" + +#: src/app/main/ui/dashboard/comments.cljs +msgid "labels.comments" +msgstr "Commentaar" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.community" +msgstr "Gemeenschap" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.confirm-password" +msgstr "Wachtwoord bevestigen" + +msgid "labels.continue" +msgstr "Doorgaan" + +msgid "labels.continue-with" +msgstr "Doorgaan met" + +msgid "labels.continue-with-penpot" +msgstr "Je kunt doorgaan met een Penpot-account" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.copy-invitation-link" +msgstr "Link kopiëren" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "labels.create" +msgstr "Aanmaken" + +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team" +msgstr "Nieuw team aanmaken" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team.placeholder" +msgstr "Nieuwe teamnaam" + +msgid "labels.custom-fonts" +msgstr "Eigen lettertypen" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.dashboard" +msgstr "Dashboard" + +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete" +msgstr "Verwijderen" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment" +msgstr "Commentaar verwijderen" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment-thread" +msgstr "Thread verwijderen" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.delete-invitation" +msgstr "Uitnodiging verwijderen" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete-multi-files" +msgstr "%s bestanden verwijderen" + +msgid "labels.discard" +msgstr "Weggooien" + +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/files.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.drafts" +msgstr "Concepten" + +#: src/app/main/ui/comments.cljs +msgid "labels.edit" +msgstr "Bewerken" + +msgid "labels.edit-file" +msgstr "Bestand bewerken" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.editor" +msgstr "Editor" + #: src/app/main/ui/dashboard/team.cljs msgid "labels.expired-invitation" msgstr "Verlopen" @@ -1692,27 +1445,33 @@ msgstr "Verlopen" msgid "labels.export" msgstr "Exporteren" +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.feedback-disabled" +msgstr "Feedback uitgeschakeld" + #: src/app/main/ui/settings/feedback.cljs msgid "labels.feedback-sent" msgstr "Feedback verstuurd" msgid "labels.font-family" -msgstr "Lettertypefamilie" +msgstr "Lettertype-familie" msgid "labels.font-providers" -msgstr "Lettertypeproviders" +msgstr "Lettertypeaanbieders" msgid "labels.font-variants" msgstr "Stijlen" msgid "labels.fonts" -msgstr "Lettertypes" +msgstr "Lettertypen" #: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.github-repo" -msgstr "GitHub repository" +msgstr "GitHub-repository" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "Geef feedback" @@ -1725,16 +1484,28 @@ msgstr "Helpcentrum" #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.hide-resolved-comments" -msgstr "Verberg opgeloste opmerkingen" +msgstr "Verwerkt commentaar verbergen" msgid "labels.inactive" msgstr "Inactief" +msgid "labels.installed-fonts" +msgstr "Geïnstalleerde lettertypen" + #: src/app/main/ui/static.cljs msgid "labels.internal-error.desc-message" msgstr "" -"Er ging iets mis. Probeer de bewerking opnieuw of neem contact op met de " -"ondersteuning als het probleem zich blijft voordoen." +"Er ging iets mis. Probeer de bewerking opnieuw of neem contact op met " +"support als het probleem zich blijft voordoen." + +#: src/app/main/ui/static.cljs +msgid "labels.internal-error.main-message" +msgstr "Interne fout" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.invitations" +msgstr "Uitnodigingen" #: src/app/main/ui/settings/options.cljs msgid "labels.language" @@ -1751,14 +1522,36 @@ msgstr "Log in of meld je aan" msgid "labels.logout" msgstr "Uitloggen" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.member" msgstr "Lid" +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.members" +msgstr "Leden" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.new-password" +msgstr "Nieuw wachtwoord" + +#: src/app/main/ui/workspace/comments.cljs, +#: src/app/main/ui/dashboard/comments.cljs +msgid "labels.no-comments-available" +msgstr "Je bent helemaal bij! Nieuwe commentaarmeldingen verschijnen hier." + #: src/app/main/ui/dashboard/team.cljs msgid "labels.no-invitations" msgstr "Geen openstaande uitnodigingen." +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.no-invitations-hint" +msgstr "" +"Klik op de knop **Mensen uitnodigen** om mensen uit te nodigen voor dit team." + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.desc-message" +msgstr "Deze pagina bestaat mogelijk niet of je hebt geen toegangsrechten." + #: src/app/main/ui/static.cljs msgid "labels.not-found.main-message" msgstr "Oeps!" @@ -1769,14 +1562,45 @@ msgid_plural "labels.num-of-files" msgstr[0] "1 bestand" msgstr[1] "%s bestanden" +msgid "labels.num-of-frames" +msgid_plural "labels.num-of-frames" +msgstr[0] "1 bord" +msgstr[1] "%s borden" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.num-of-projects" +msgid_plural "labels.num-of-projects" +msgstr[0] "1 project" +msgstr[1] "%s projecten" + #: src/app/main/ui/settings/password.cljs msgid "labels.old-password" -msgstr "Oud wachtwoord" +msgstr "Huidig wachtwoord" #: src/app/main/ui/workspace/comments.cljs msgid "labels.only-yours" msgstr "Alleen van jou" +msgid "labels.or" +msgstr "of" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.owner" +msgstr "Eigenaar" + +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.password" +msgstr "Wachtwoord" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.pending-invitation" +msgstr "In behandeling" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.profile" +msgstr "Profiel" + #: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.projects" msgstr "Projecten" @@ -1789,27 +1613,30 @@ msgstr "Release-opmerkingen" msgid "labels.reload-file" msgstr "Bestand opnieuw laden" -#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/workspace/libraries.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" msgstr "Verwijderen" #: src/app/main/ui/dashboard/team.cljs msgid "labels.remove-member" -msgstr "Verwijder lid" +msgstr "Lid verwijderen" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.rename" -msgstr "Hernoem" +msgstr "Hernoemen" #: src/app/main/ui/dashboard/team_form.cljs msgid "labels.rename-team" -msgstr "Hernoem team" +msgstr "Team hernoemen" #: src/app/main/ui/dashboard/team.cljs msgid "labels.resend-invitation" msgstr "Uitnodiging opnieuw versturen" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Opnieuw proberen" @@ -1839,12 +1666,13 @@ msgstr "We zijn bezig met onderhoud van onze systemen." msgid "labels.service-unavailable.main-message" msgstr "Service niet beschikbaar" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.settings" msgstr "Instellingen" msgid "labels.share-prototype" -msgstr "Deel prototype" +msgstr "Prototype delen" #: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.shared-libraries" @@ -1852,10 +1680,14 @@ msgstr "Bibliotheek" #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.show-all-comments" -msgstr "Toon alle commentaar" +msgstr "Alle commentaar tonen" msgid "labels.show-comments-list" -msgstr "Toon commentaarlijst" +msgstr "Commentaarlijst tonen" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.show-your-comments" +msgstr "Alleen eigen commentaar tonen" #: src/app/main/ui/dashboard/team.cljs msgid "labels.status" @@ -1863,25 +1695,25 @@ msgstr "Status" #: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.tutorials" -msgstr "Tutorials" +msgstr "Introductie" #: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.unpublish-multi-files" -msgstr "Maak de publicatie van %s bestanden ongedaan" +msgstr "Publicatie van %s bestanden ongedaan maken" #: src/app/main/ui/settings/profile.cljs msgid "labels.update" -msgstr "Updaten" +msgstr "Bijwerken" #: src/app/main/ui/dashboard/team_form.cljs msgid "labels.update-team" -msgstr "Bewerk team" +msgstr "Team bijwerken" msgid "labels.upload" msgstr "Uploaden" msgid "labels.upload-custom-fonts" -msgstr "Upload eigen lettertypes" +msgstr "Eigen lettertypen uploaden" msgid "labels.uploading" msgstr "Uploaden…" @@ -1896,33 +1728,112 @@ msgstr "Kijker" msgid "labels.webhooks" msgstr "Webhooks" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/comments.cljs +msgid "labels.write-new-comment" +msgstr "Nieuw commentaar toevoegen" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.you" msgstr "(jij)" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.your-account" +msgstr "Jouw account" + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "media.loading" +msgstr "Afbeelding laden…" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" msgstr "Toevoegen als gedeelde bibliotheek" +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.hint" +msgstr "" +"Eenmaal toegevoegd als gedeelde bibliotheek, zijn de assets van deze " +"bestandsbibliotheek beschikbaar voor gebruik tussen de rest van je bestanden." + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.message" +msgstr "\"%s\" toevoegen als gedeelde bibliotheek" + #: src/app/main/ui/workspace/nudge.cljs msgid "modals.big-nudge" -msgstr "Big nudge" +msgstr "Grote verschuiving" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.confirm-email" +msgstr "Nieuw e-mailadres verifiëren" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.info" +msgstr "" +"We sturen je een e-mail naar je huidige e-mailadres \"%s\" om je identiteit " +"te verifiëren." + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.new-email" +msgstr "Nieuw e-mailadres" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.submit" +msgstr "E-mailadres wijzigen" #: src/app/main/ui/settings/change_email.cljs msgid "modals.change-email.title" -msgstr "Verander je e-mailadres" +msgstr "Je e-mailadres wijzigen" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "Toegangsbewijs kopiëren" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "Vervaldatum" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "Naam" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "De naam helpt je te onthouden waar het toegangsbewijs voor is" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "Toegangsbewijs aanmaken" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "Toegangsbewijs genereren" msgid "modals.create-webhook.submit-label" -msgstr "Maak webhook" +msgstr "Webhook aanmaken" msgid "modals.create-webhook.title" -msgstr "Maak webhook" +msgstr "Webhook aanmaken" msgid "modals.create-webhook.url.label" msgstr "Payload-URL" msgid "modals.create-webhook.url.placeholder" -msgstr "https://voorbeeld.nlpostreceive" +msgstr "https://voorbeeld.nl/postreceive" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "Toegangsbewijs verwijderen" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "Weet je zeker dat je dit toegangsbewijs wilt verwijderen?" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "Toegangsbewijs verwijderen" #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.cancel" @@ -1935,113 +1846,123 @@ msgstr "Ja, verwijder mijn account" #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.info" msgstr "" -"Door je account te verwijderen, verlies je al je huidige projecten en " +"Als je je account verwijdert, verlies je al je huidige projecten en " "archieven." #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.title" msgstr "Weet je zeker dat je je account wilt verwijderen?" +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.accept" +msgstr "Gesprek verwijderen" + #: src/app/main/ui/comments.cljs msgid "modals.delete-comment-thread.message" msgstr "" "Weet je zeker dat je dit gesprek wilt verwijderen? Alle reacties in deze " "thread worden verwijderd." -#: src/app/main/ui/comments.cljs -msgid "modals.delete-comment-thread.accept" -msgstr "Verwijder gesprek" - #: src/app/main/ui/comments.cljs msgid "modals.delete-comment-thread.title" -msgstr "Verwijder gesprek" +msgstr "Gesprek verwijderen" + +msgid "modals.delete-component-annotation.message" +msgstr "Weet je zeker dat je deze aantekening wilt verwijderen?" + +msgid "modals.delete-component-annotation.title" +msgstr "Aantekening verwijderen" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-confirm.accept" -msgstr "Verwijder bestand" +msgstr "Bestand verwijderen" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-confirm.message" -msgstr "Weet u zeker dat u dit bestand wilt verwijderen?" +msgstr "Weet je zeker dat je dit bestand wilt verwijderen?" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-confirm.title" -msgstr "Bestand aan het verwijderen" +msgstr "Bestand verwijderen" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-multi-confirm.accept" -msgstr "Verwijder bestanden" +msgstr "Bestanden verwijderen" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.message" +msgstr "Weet je zeker dat je %s bestanden wilt verwijderen?" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.title" +msgstr "Verwijderen van %s bestanden" + +msgid "modals.delete-font-variant.message" +msgstr "" +"Weet je zeker dat je deze lettertypestijl wilt verwijderen? Het wordt dan " +"niet meer geladen als het in een bestand wordt gebruikt." + +msgid "modals.delete-font-variant.title" +msgstr "Lettertypestijl verwijderen" + +msgid "modals.delete-font.message" +msgstr "" +"Weet je zeker dat je dit lettertype wilt verwijderen? Het wordt dan niet " +"meer geladen als het in een bestand wordt gebruikt." msgid "modals.delete-font.title" msgstr "Lettertype verwijderen" +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "modals.delete-page.body" +msgstr "Weet je zeker dat je deze pagina wilt verwijderen?" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "modals.delete-page.title" +msgstr "Pagina verwijderen" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.accept" +msgstr "Project verwijderen" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.message" +msgstr "Weet je zeker dat je dit project wilt verwijderen?" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "modals.delete-project-confirm.title" -msgstr "Verwijder project" +msgstr "Project verwijderen" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.accept" msgid_plural "modals.delete-shared-confirm.accept" -msgstr[0] "Verwijder bestand" -msgstr[1] "Verwijder bestanden" +msgstr[0] "Bestand verwijderen" +msgstr[1] "Bestanden verwijderen" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "" -"Als u het verwijdert, zijn die middelen niet langer beschikbaar vanuit " -"andere bestanden. Middelen die al gebruikt zijn, blijven in dit bestand " -"staan (er gaat geen ontwerp kapot!)." -msgstr[1] "" -"Als u ze verwijdert, zijn die middelen niet langer beschikbaar vanuit andere " -"bestanden. Middelen die al gebruikt zijn, blijven in dit bestand staan (er " -"gaat geen ontwerp kapot!)." +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "Het is in geen enkel bestand geactiveerd." +msgstr[1] "Ze zijn in geen enkel bestand geactiveerd." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "" -"Als u het verwijdert, zijn die middelen niet langer beschikbaar vanuit " -"andere bestanden. Middelen die al zijn gebruikt, blijven in deze bestanden " -"staan (er wordt geen ontwerp verbroken!)." -msgstr[1] "" -"Als u ze verwijdert, zijn die middelen niet langer beschikbaar vanuit andere " -"bestanden. Middelen die al gebruikt zijn, blijven in dit bestand staan (er " -"gaat geen ontwerp kapot!)." +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "Deze bibliotheek wordt hier geactiveerd: " +msgstr[1] "Deze bibliotheken worden hier geactiveerd: " -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" -msgstr[0] "Weet u zeker dat u dit bestand wilt verwijderen?" -msgstr[1] "Weet u zeker dat u deze bestanden wilt verwijderen?" +msgstr[0] "Weet je zeker dat je dit bestand wilt verwijderen?" +msgstr[1] "Weet je zeker dat je deze bestanden wilt verwijderen?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" -msgstr[0] "" -"Geen van de elementen in de bibliotheek van dit bestand is in gebruik. Ze " -"worden samen met het bestand verwijderd." -msgstr[1] "" -"Geen van de elementen in de bibliotheek van deze bestanden is in gebruik. Ze " -"worden samen met de bestanden verwijderd." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "" -"Sommige elementen in de bibliotheek van dit bestand worden hier gebruikt:" -msgstr[1] "" -"Sommige elementen in de bibliotheek van deze bestanden worden hier gebruikt:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "" -"Sommige elementen in de bibliotheek van dit bestand worden hier gebruikt:" -msgstr[1] "" -"Sommige elementen in de bibliotheek van deze bestanden worden hier gebruikt:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" msgstr[0] "Bestand verwijderen" @@ -2049,7 +1970,7 @@ msgstr[1] "Bestanden verwijderen" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.delete-team-confirm.accept" -msgstr "Verwijder team" +msgstr "Team verwijderen" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.delete-team-confirm.message" @@ -2063,7 +1984,7 @@ msgstr "Team verwijderen" #: src/app/main/ui/dashboard/team.cljs msgid "modals.delete-team-member-confirm.accept" -msgstr "Verwijder lid" +msgstr "Lid verwijderen" #: src/app/main/ui/dashboard/team.cljs msgid "modals.delete-team-member-confirm.message" @@ -2071,53 +1992,108 @@ msgstr "Weet je zeker dat je dit lid van het team wilt verwijderen?" #: src/app/main/ui/dashboard/team.cljs msgid "modals.delete-team-member-confirm.title" -msgstr "Verwijder teamlid" +msgstr "Teamlid verwijderen" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" +msgstr[0] "" +"Assets die al in dit bestand zijn gebruikt, blijven daar staan (er wordt " +"geen ontwerp verbroken)." +msgstr[1] "" +"Assets die al in die bestanden zijn gebruikt, blijven daar staan (er wordt " +"geen ontwerp verbroken)." msgid "modals.delete-webhook.accept" -msgstr "Verwijder webhook" +msgstr "Webhook verwijderen" msgid "modals.delete-webhook.message" msgstr "Weet je zeker dat je deze webhook wilt verwijderen?" +msgid "modals.delete-webhook.title" +msgstr "Webhook verwijderen" + +msgid "modals.edit-webhook.submit-label" +msgstr "Webhook bewerken" + msgid "modals.edit-webhook.title" -msgstr "Wijzig webhook" +msgstr "Webhook bewerken" #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-member-confirm.accept" -msgstr "Stuur uitnodiging" +msgstr "Uitnodiging versturen" msgid "modals.invite-member.emails" msgstr "E-mailadressen, kommagescheiden" msgid "modals.invite-member.repeated-invitation" msgstr "" -"Sommige e-mails zijn van huidige teamleden. Hun uitnodigingen worden niet " -"verzonden." +"Sommige e-mailadressen zijn van bestaande teamleden. Zij krijgen geen nieuwe " +"uitnodigingen." #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-team-member.title" -msgstr "Nodig leden uit voor het team" +msgstr "Leden voor het team uitnodigen" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-close-confirm.hint" msgstr "" -"Aangezien u het enige lid van dit team bent, wordt het team samen met de " +"Aangezien je het enige lid van dit team bent, wordt het team samen met de " "projecten en bestanden verwijderd." #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-close-confirm.message" -msgstr "Weet u zeker dat u het %s team wilt verlaten?" +msgstr "Weet je zeker dat je het %s team wilt verlaten?" msgid "modals.leave-and-reassign.forbidden" msgstr "" "Je kunt het team niet verlaten als er geen ander lid is om tot eigenaar te " -"promoveren. Misschien wilt u het team verwijderen." +"promoveren. Misschien wil je het team verwijderen." #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-reassign.hint1" msgstr "" "Jij bent de eigenaar van dit team. Selecteer een ander lid om tot eigenaar " -"te promoveren voordat u vertrekt." +"te promoveren voordat je vertrekt." + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.promote-and-leave" +msgstr "Promoveren en verlaten" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.select-member-to-promote" +msgstr "Selecteer een lid om te promoveren" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.title" +msgstr "Voordat je gaat" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.accept" +msgstr "Team verlaten" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.message" +msgstr "Weet je zeker dat je dit team wilt verlaten?" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.title" +msgstr "Team verlaten" + +#: src/app/main/ui/workspace/nudge.cljs +msgid "modals.nudge-title" +msgstr "Verschuiving" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.accept" +msgstr "Eigendom overdragen" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.hint" +msgstr "" +"Als je het eigendom overdraagt, verander je je rol in beheerder en verlies " +"je enkele machtigingen voor dit team. " #: src/app/main/ui/dashboard/team.cljs msgid "modals.promote-owner-confirm.message" @@ -2126,732 +2102,232 @@ msgstr "" "eigenaar van het team wilt maken?" #: src/app/main/ui/dashboard/team.cljs -msgid "modals.promote-owner-confirm.hint" +msgid "modals.promote-owner-confirm.title" +msgstr "Nieuwe teameigenaar" + +msgid "modals.publish-empty-library.accept" +msgstr "Publiceren" + +msgid "modals.publish-empty-library.message" +msgstr "Je bibliotheek is leeg. Weet je zeker dat je het wilt publiceren?" + +msgid "modals.publish-empty-library.title" +msgstr "Lege bibliotheek publiceren" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.accept" +msgstr "Als gedeelde bibliotheek verwijderen" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.hint" msgstr "" -"Als u het eigendom overdraagt, verandert u uw rol in beheerder en verliest u " -"enkele machtigingen voor dit team. " +"Eenmaal verwijderd als gedeelde bibliotheek, is de bestandsbibliotheek van " +"dit bestand niet meer beschikbaar voor gebruik onder de rest van je " +"bestanden." -msgid "onboarding.newsletter.acceptance-message" +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.message" +msgstr "\"%s\" als gedeelde bibliotheek verwijderen" + +#: src/app/main/ui/workspace/nudge.cljs +msgid "modals.small-nudge" +msgstr "Kleine verschuiving" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.accept" +msgid_plural "modals.unpublish-shared-confirm.accept" +msgstr[0] "Publicatie ongedaan maken" +msgstr[1] "Publicaties ongedaan maken" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.message" +msgid_plural "modals.unpublish-shared-confirm.message" +msgstr[0] "" +"Weet je zeker dat je de publicatie van deze bibliotheek ongedaan wilt maken?" +msgstr[1] "" +"Weet je zeker dat je de publicatie van deze bibliotheken ongedaan wilt maken?" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.title" +msgid_plural "modals.unpublish-shared-confirm.title" +msgstr[0] "Publicatie bibliotheek ongedaan maken" +msgstr[1] "Publicatie bibliotheken ongedaan maken" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component-in-bulk.hint" msgstr "" -"Uw inschrijvingsverzoek is verzonden, wij sturen u een e-mail ter " -"bevestiging." +"Je staat op het punt om componenten in een gedeelde bibliotheek bij te " +"werken. Dit kan van invloed zijn op andere bestanden die er gebruik van " +"maken." -msgid "onboarding.team-modal.create-team-desc" -msgstr "" -"Met een team kunt u samenwerken met andere Penpot-gebruikers die aan " -"dezelfde bestanden en projecten werken." +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component-in-bulk.message" +msgstr "Componenten in een gedeelde bibliotheek bijwerken" -msgid "shortcuts.detach-component" -msgstr "Component losmaken" +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.accept" +msgstr "Bijwerken" -msgid "shortcuts.draw-curve" -msgstr "Curve" - -msgid "shortcuts.draw-ellipse" -msgstr "Ellips" - -msgid "shortcuts.draw-frame" -msgstr "Bord" - -msgid "shortcuts.draw-nodes" -msgstr "Pad tekenen" - -msgid "shortcuts.draw-path" -msgstr "Pad" - -msgid "shortcuts.draw-rect" -msgstr "Rechthoek" - -msgid "shortcuts.draw-text" -msgstr "Tekst" - -msgid "shortcuts.duplicate" -msgstr "Dupliceren" - -msgid "shortcuts.escape" +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.cancel" msgstr "Annuleren" -msgid "shortcuts.export-shapes" -msgstr "Vormen exporteren" - -msgid "shortcuts.fit-all" -msgstr "Zoom om passend te maken" - -msgid "shortcuts.flip-vertical" -msgstr "Verticaal spiegelen" - -msgid "shortcuts.flip-horizontal" -msgstr "Horizontaal spiegelen" - -msgid "shortcuts.font-size-dec" -msgstr "Verklein de lettergrootte" - -msgid "shortcuts.go-to-drafts" -msgstr "Ga naar concepten" - -msgid "shortcuts.go-to-libs" -msgstr "Ga naar gedeelde bibliotheek" - -msgid "shortcuts.open-workspace" -msgstr "Ga naar workspace" - -msgid "shortcuts.or" -msgstr " of " - -msgid "shortcuts.paste" -msgstr "Plakken" - -msgid "shortcuts.prev-frame" -msgstr "Vorig bord" - -msgid "shortcuts.redo" -msgstr "Opnieuw doen" - -msgid "shortcuts.search-placeholder" -msgstr "Zoek snelkoppelingen" - -msgid "shortcuts.reset-zoom" -msgstr "Zoom resetten" - -msgid "shortcuts.select-all" -msgstr "Selecteer alles" - -msgid "shortcuts.select-next" -msgstr "Selecteer volgende laag" - -msgid "shortcuts.select-prev" -msgstr "Selecteer vorige laag" - -msgid "shortcuts.separate-nodes" -msgstr "Losse knooppunten" - -msgid "shortcuts.show-pixel-grid" -msgstr "Toon/verberg pixelraster" - -msgid "shortcuts.show-shortcuts" -msgstr "Toon/verberg snelkoppelingen" - -msgid "shortcuts.unmask" -msgstr "Masker verwijderen" - -msgid "viewer.breaking-change.description" -msgstr "" -"Deze deelbare link is niet langer geldig. Maak een nieuwe aan of vraag de " -"eigenaar om een nieuwe." - -msgid "viewer.breaking-change.message" -msgstr "Sorry!" - -#: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.interactions" -msgstr "Interacties" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.create-group-hint" -msgstr "Je items krijgen automatisch de naam \"groepsnaam / itemnaam\"" - -#: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.sitemap" -msgstr "Sitemap" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.assets" -msgstr "Elementen" - -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.duplicate" -msgstr "Dupliceren" - -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.edit" -msgstr "Bewerken" - -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.graphics" -msgstr "Graphics" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.group" -msgstr "Groep" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.libraries" -msgstr "Bibliotheken" - -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.rename" -msgstr "Hernoemen" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "GEDEELD" - -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.typography" -msgstr "Typografieën" - -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs -msgid "workspace.assets.typography.font-id" -msgstr "Lettertype" - -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs -msgid "workspace.assets.typography.font-size" -msgstr "Grootte" - -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs -msgid "workspace.assets.typography.font-variant-id" -msgstr "Variant" - -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/inspect/attributes/text.cljs, src/app/main/ui/inspect/attributes/text.cljs -msgid "workspace.assets.typography.sample" -msgstr "Ag" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.ungroup" -msgstr "Degroeperen" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.option.edit" -msgstr "Bewerken" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.option.file" -msgstr "Bestand" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.option.preferences" -msgstr "Voorkeuren" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.option.view" -msgstr "Bekijk" - -msgid "workspace.header.menu.redo" -msgstr "Opnieuw doen" - -msgid "workspace.header.menu.undo" -msgstr "Ongedaan maken" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.reset-zoom" -msgstr "Resetten" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.saved" -msgstr "Opgeslagen" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.saving" -msgstr "Opslaan" - -#: src/app/main/ui/workspace/libraries.cljs -msgid "workspace.libraries.add" -msgstr "Toevoegen" - -#: src/app/main/ui/workspace/colorpicker.cljs -msgid "workspace.libraries.colors.hsv" -msgstr "HSV" - -#: src/app/main/ui/workspace/colorpicker.cljs -msgid "workspace.libraries.colors.rgba" -msgstr "RGBA" - -#: src/app/main/ui/workspace/libraries.cljs -msgid "workspace.libraries.libraries" -msgstr "BIBLIOTHEKEN" - -#: src/app/main/ui/workspace/libraries.cljs -msgid "workspace.libraries.library" -msgstr "BIBLIOTHEEK" - -#: src/app/main/ui/workspace/libraries.cljs -msgid "workspace.libraries.update" -msgstr "Bewerk" - -#: src/app/main/ui/workspace/libraries.cljs -msgid "workspace.libraries.updates" -msgstr "BEWERKINGEN" - -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs -msgid "workspace.options.component" -msgstr "Component" - -#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.constraints" -msgstr "Beperkingen" - -#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.constraints.bottom" -msgstr "Onderkant" - -#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.constraints.scale" -msgstr "Schaal" - -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs -msgid "workspace.options.export" -msgstr "Exporteren" - -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs -msgid "workspace.options.export.suffix" -msgstr "Achtervoegsel" - -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs -msgid "workspace.options.exporting-object" -msgstr "Exporteren…" - -#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs -msgid "workspace.options.fill" -msgstr "Vullen" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.auto" -msgstr "Automatisch" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.column" -msgstr "Kolommen" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.grid-title" -msgstr "Raster" - -msgid "workspace.options.grid.params.color" -msgstr "Kleur" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.columns" -msgstr "Kolommen" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.height" -msgstr "Hoogte" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.margin" -msgstr "Marge" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.type" -msgstr "Type" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.type.bottom" -msgstr "Onderkant" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.type.center" -msgstr "Midden" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.type.right" -msgstr "Rechts" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.type.stretch" -msgstr "Uitrekken" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.type.top" -msgstr "Bovenkant" - -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.width" -msgstr "Breedte" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-animation-dissolve" -msgstr "Ontbinden" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-animation-none" -msgstr "Geen" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-animation-push" -msgstr "Push" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-animation-slide" -msgstr "Schuif" - -msgid "workspace.options.interaction-auto" -msgstr "automatisch" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-delay" -msgstr "Vertraging" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-destination" -msgstr "Bestemming" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-duration" -msgstr "Duur" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-easing" -msgstr "Easing" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-easing-ease" -msgstr "Ease" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-easing-linear" -msgstr "Lineair" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-in" -msgstr "In" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-ms" -msgstr "ms" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-out" -msgstr "Out" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-pos-center" -msgstr "Gecentreerd" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-pos-manual" -msgstr "Handmatig" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-position" -msgstr "Positie" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-self" -msgstr "zelf" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-trigger" -msgstr "Trigger" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-url" -msgstr "URL" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interactions" -msgstr "Interacties" - -msgid "shortcuts.delete" -msgstr "Verwijderen" - -msgid "shortcuts.delete-node" -msgstr "Verwijder knooppunt" - -msgid "onboarding-v2.welcome.desc2" -msgstr "" -"Een openbare ruimte om te leren, te delen en te discussiëren over Penpot, " -"zijn heden en toekomst met de hele gemeenschap en het kernteam van Penpot." - -msgid "onboarding-v2.welcome.desc3" -msgstr "" -"Waar u kunt vinden hoe u kunt samenwerken aan vertalingen, functieverzoeken, " -"kernbijdragen, zoeken naar bugs…" - -msgid "onboarding.choice.team-up.create-team-desc" -msgstr "" -"Nadat je je team een naam hebt gegeven, kun je mensen uitnodigen om lid te " -"worden." - -msgid "onboarding-v2.welcome.desc1" -msgstr "" -"Penpot is Open Source en is gemaakt door zowel Kaleidos als de gemeenschap, " -"waar al veel mensen elkaar helpen. Iedereen kan samenwerken door:" - -msgid "onboarding-v2.before-start.desc1" -msgstr "" -"U moet weten dat er veel bronnen beschikbaar zijn om u op weg te helpen met " -"Penpot, zoals de gebruikershandleiding en ons YouTube-kanaal." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Enkele van de elementen in deze bibliotheek worden hier gebruikt:" -msgstr[1] "Enkele van de elementen in deze biliotheken worden hier gebruikt:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "Geen van de elementen in deze bibliotheek is in gebruik." -msgstr[1] "Geen van de elementen in deze bibliotheken is in gebruik." - -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.hint" msgstr "" "Je staat op het punt een component in een gedeelde bibliotheek bij te " "werken. Dit kan van invloed zijn op andere bestanden die er gebruik van " "maken." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.message" -msgid_plural "modals.unpublish-shared-confirm.message" -msgstr[0] "" -"Weet u zeker dat u de publicatie van deze bibliotheek ongedaan wilt maken?" -msgstr[1] "" -"Weet u zeker dat u de publicatie van deze bibliotheken ongedaan wilt maken?" +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.message" +msgstr "Component in een gedeelde bibliotheek bijwerken" -msgid "onboarding-v2.newsletter.desc" +#: src/app/main/data/common.cljs +msgid "notifications.by-code.upgrade-version" +msgstr "Er is een nieuwe versie beschikbaar, vernieuw de pagina" + +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-email-sent" +msgstr "Uitnodiging succesvol verstuurd" + +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-link-copied" +msgstr "Uitnodigingslink gekopieerd" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "notifications.profile-deletion-not-allowed" msgstr "" -"Abonneer u op de Penpot-nieuwsbrief om op de hoogte te blijven van de " -"voortgang van de productontwikkeling en nieuws." +"Je kunt je profiel niet verwijderen. Wijs je teams opnieuw toe voordat je " +"verder gaat." + +#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/options.cljs +msgid "notifications.profile-saved" +msgstr "Profiel is opgeslagen!" + +#: src/app/main/ui/settings/change_email.cljs +msgid "notifications.validation-email-sent" +msgstr "Verificatie-e-mail verzonden naar %s. Controleer je e-mail!" + +msgid "onboarding-v2.before-start.desc1" +msgstr "" +"Je moet weten dat er veel bronnen beschikbaar zijn om je op weg te helpen " +"met Penpot, zoals de gebruikershandleiding en ons YouTube-kanaal." msgid "onboarding-v2.before-start.desc2" msgstr "" "Gedetailleerde informatie over het gebruik van Penpot. Van prototyping tot " "het organiseren of delen van ontwerpen." +msgid "onboarding-v2.before-start.desc2.title" +msgstr "Gebruikershandleiding" + msgid "onboarding-v2.before-start.desc3" -msgstr "Je kunt onze tutorials en de tutorials van onze community bekijken." - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.add-interaction" -msgstr "Klik op de knop + om interacties toe te voegen." - -#: src/app/main/ui/workspace/libraries.cljs -msgid "workspace.libraries.no-shared-libraries-available" -msgstr "Er zijn geen gedeelde bibliotheken beschikbaar" - -#: src/app/main/ui/dashboard/team.cljs -msgid "notifications.invitation-email-sent" -msgstr "Uitnodiging succesvol verstuurd" - -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs -msgid "modals.update-remote-component.accept" -msgstr "Update" - -#: src/app/main/ui/dashboard/team.cljs -msgid "notifications.invitation-link-copied" -msgstr "Uitnodigingslink gekopieerd" - -#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/options.cljs -msgid "notifications.profile-saved" -msgstr "Profiel is opgeslagen!" +msgstr "Je kunt onze introducties en die van onze gemeenschap bekijken." msgid "onboarding-v2.before-start.desc3.title" -msgstr "Video-tutorials" +msgstr "Video-introducties" msgid "onboarding-v2.before-start.title" msgstr "Voordat je begint" -msgid "shortcut-subsection.zoom-workspace" -msgstr "Zoomen" +msgid "onboarding-v2.newsletter.desc" +msgstr "" +"Abonneer je op de Penpot-nieuwsbrief om op de hoogte te blijven van de " +"voortgang van de productontwikkeling en nieuws." -msgid "shortcut-subsection.zoom-viewer" -msgstr "Zoomen" +msgid "onboarding-v2.newsletter.news" +msgstr "" +"Stuur mij nieuws over Penpot (blogposts, video-introducties, streamings…)." -msgid "shortcuts.italic" -msgstr "Schakel cursief in/uit" +msgid "onboarding-v2.newsletter.privacy1" +msgstr "Wij geven om privacy, lees hier onze " -msgid "shortcuts.opacity-0" -msgstr "Zet dekking op 100%" +msgid "onboarding-v2.newsletter.privacy2" +msgstr "" +"We sturen je alleen relevante e-mails. Je kunt je op elk moment afmelden via " +"de afmeldlink in al onze nieuwsbrieven." -msgid "shortcuts.merge-nodes" -msgstr "Knooppunten samenvoegen" +msgid "onboarding-v2.newsletter.updates" +msgstr "Stuur mij productnieuws (nieuwe functies, releases, correcties…)." -msgid "shortcuts.not-found" -msgstr "Geen sneltoets gevonden" +msgid "onboarding-v2.welcome.desc1" +msgstr "" +"Penpot is Open Source en is gemaakt door zowel Kaleidos als de gemeenschap, " +"waar al veel mensen elkaar helpen. Iedereen kan samenwerken door:" -msgid "shortcuts.move" -msgstr "Verplaats" +msgid "onboarding-v2.welcome.desc2" +msgstr "" +"Een openbare ruimte om te leren, te delen en te discussiëren over Penpot, " +"zijn heden en toekomst met de hele gemeenschap en het kernteam van Penpot." -msgid "shortcuts.toggle-lock-size" -msgstr "Vergrendel proporties" +msgid "onboarding-v2.welcome.desc2.title" +msgstr "Deelnemen aan de Penpot-gemeenschap" -msgid "shortcuts.move-fast-down" -msgstr "Snel naar beneden verplaatsen" +msgid "onboarding-v2.welcome.desc3" +msgstr "" +"Waar je kunt vinden hoe je kunt samenwerken aan vertalingen, " +"functieverzoeken, kernbijdragen, zoeken naar bugs…" -msgid "shortcuts.move-fast-left" -msgstr "Snel naar links verplaatsen" +msgid "onboarding-v2.welcome.desc3.title" +msgstr "Bijdragen" -msgid "shortcuts.ungroup" -msgstr "Degroeperen" +msgid "onboarding-v2.welcome.title" +msgstr "Welkom bij Penpot!" -msgid "shortcuts.v-distribute" -msgstr "Verticaal verdelen" +msgid "onboarding.choice.team-up.create-team-desc" +msgstr "" +"Nadat je je team een naam hebt gegeven, kun je mensen uitnodigen om lid te " +"worden." -msgid "shortcuts.zoom-lense-decrease" -msgstr "Zoomlens verkleinen" +msgid "onboarding.choice.team-up.create-team-placeholder" +msgstr "Voer de naam van het team in" -#: src/app/main/ui/dashboard/projects.cljs -msgid "title.dashboard.projects" -msgstr "Projecten - %s - Penpot" - -#: src/app/main/ui/workspace/sidebar/align.cljs -msgid "workspace.align.vdistribute" -msgstr "Verdeel verticale tussenruimte (%s)" - -#: src/app/main/ui/workspace/sidebar/align.cljs -msgid "workspace.align.vtop" -msgstr "Bovenkant uitlijnen (%s)" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.box-filter-all" -msgstr "Alle elementen" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.create-group" -msgstr "Groep aanmaken" - -msgid "workspace.assets.local-library" -msgstr "Lokale bibliotheek" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.group-name" -msgstr "Groepnaam" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.not-found" -msgstr "Geen elementen gevonden" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.rename-group" -msgstr "Groep hernoemen" - -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs -msgid "workspace.assets.typography.text-transform" -msgstr "Tekst transformeren" - -msgid "workspace.focus.focus-off" -msgstr "Focus uit" - -msgid "workspace.focus.focus-on" -msgstr "Focus aan" - -msgid "workspace.focus.focus-mode" -msgstr "Focusmodus" - -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs -msgid "workspace.gradients.linear" -msgstr "Lineair verloop" - -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs -msgid "workspace.gradients.radial" -msgstr "Radiaal verloop" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-guides" -msgstr "Uitlijnen op hulplijnen" - -msgid "shortcuts.toggle-snap-guide" -msgstr "Uitlijnen op hulplijnen" - -msgid "workspace.header.menu.hide-pixel-grid" -msgstr "Verberg pixelraster" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-rules" -msgstr "Verberg linialen" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-textpalette" -msgstr "Toon lettertype-palet" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "Deze update is een eenmalige actie." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "Sommige afbeeldingen kunnen niet worden bijgewerkt." - -#: src/app/main/ui/workspace/sidebar/history.cljs -msgid "workspace.undo.empty" -msgstr "Er zijn tot nu toe geen wijzigingen in de historie" - -#: src/app/main/data/workspace/libraries.cljs -msgid "workspace.updates.there-are-updates" -msgstr "Er zijn updates in gedeelde bibliotheken" - -#: src/app/main/ui/dashboard/sidebar.cljs -msgid "modals.leave-confirm.message" -msgstr "Weet je zeker dat je dit team wilt verlaten?" - -#: src/app/main/ui/dashboard/sidebar.cljs -msgid "modals.leave-and-reassign.select-member-to-promote" -msgstr "Selecteer een lid om te promoveren" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.remove-shared-confirm.message" -msgstr "Verwijder \"%s\" als gedeelde bibliotheek" - -#: src/app/main/ui/dashboard/sidebar.cljs -msgid "modals.leave-and-reassign.promote-and-leave" -msgstr "Promoveer en verlaat" - -#: src/app/main/ui/dashboard/sidebar.cljs -msgid "modals.leave-and-reassign.title" -msgstr "Voordat je gaat" - -#: src/app/main/ui/dashboard/sidebar.cljs -msgid "modals.leave-confirm.accept" -msgstr "Verlaat team" - -#: src/app/main/ui/dashboard/sidebar.cljs -msgid "modals.leave-confirm.title" -msgstr "Team verlaten" - -#: src/app/main/ui/workspace/nudge.cljs -msgid "modals.nudge-title" -msgstr "Aantal nudges" - -#: src/app/main/ui/dashboard/team.cljs -msgid "modals.promote-owner-confirm.accept" -msgstr "Eigendom overdragen" - -#: src/app/main/ui/dashboard/team.cljs -msgid "modals.promote-owner-confirm.title" -msgstr "Nieuwe teameigenaar" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.remove-shared-confirm.accept" -msgstr "Verwijder als gedeelde bibliotheek" - -#: src/app/main/ui/workspace/nudge.cljs -msgid "modals.small-nudge" -msgstr "Kleine nudge" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.accept" -msgid_plural "modals.unpublish-shared-confirm.accept" -msgstr[0] "Publicatie ongedaan maken" -msgstr[1] "Publicaties ongedaan maken" +msgid "onboarding.choice.team-up.invite-members" +msgstr "Leden uitnodigen" msgid "onboarding.choice.team-up.invite-members-info" msgstr "" "Vergeet niet om iedereen mee te nemen. Ontwikkelaars, ontwerpers, " "managers... diversiteit is alleen maar beter :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Maak een team en nodig later uit" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Maak een team en verstuur uitnodigingen" - msgid "onboarding.choice.team-up.roles" msgstr "Uitnodigen met rol:" msgid "onboarding.newsletter.accept" msgstr "Ja, abonneren" -msgid "onboarding.newsletter.title" -msgstr "Penpot nieuws ontvangen?" +msgid "onboarding.newsletter.acceptance-message" +msgstr "" +"Je inschrijvingsverzoek is verzonden, wij sturen je een e-mail ter " +"bevestiging." msgid "onboarding.newsletter.policy" msgstr "Privacybeleid." +msgid "onboarding.newsletter.title" +msgstr "Wil je Penpot-nieuws ontvangen?" + msgid "onboarding.team-modal.create-team" msgstr "Team aanmaken" +msgid "onboarding.team-modal.create-team-desc" +msgstr "" +"Met een team kun je samenwerken met andere Penpot-gebruikers die aan " +"dezelfde bestanden en projecten werken." + msgid "onboarding.team-modal.create-team-feature-1" msgstr "Oneindig veel bestanden en projecten" @@ -2880,526 +2356,180 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "Ga naar inlogscherm" -msgid "shortcuts.go-to-search" -msgstr "Zoeken" +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "Met welke ontwerptool heb je meer ervaring?" -msgid "shortcuts.group" -msgstr "Groep" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11-30" -msgid "shortcuts.h-distribute" -msgstr "Verdeel horizontaal" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2-10" -msgid "shortcuts.hide-ui" -msgstr "Toon/verberg UI" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31-50" -msgid "shortcuts.increase-zoom" -msgstr "Inzoomen" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "Veel" -msgid "shortcuts.insert-image" -msgstr "Afbeelding invoegen" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" -msgid "shortcuts.join-nodes" -msgstr "Sluit knooppunten aan" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canva" -msgid "shortcuts.letter-spacing-dec" -msgstr "Verklein de letterafstand" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.describe-your-experience-working-on" +msgstr "Hoe zou je je ervaring omschrijven voor het werken aan..." -msgid "shortcuts.letter-spacing-inc" -msgstr "Vergroot de letterafstand" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "Ontwerper" -msgid "shortcuts.line-height-dec" -msgstr "Verklein lijnhoogte" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "Ontwikkelaar" -msgid "shortcuts.line-height-inc" -msgstr "Vergroot lijnhoogte" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "Meer over Penpot ontdekken" -msgid "shortcuts.line-through" -msgstr "Schakel doorstreept in/uit" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" -msgid "shortcuts.make-corner" -msgstr "Maak hoek" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "Oprichter/VP" -msgid "shortcuts.make-curve" -msgstr "Maak curve" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "Ik ben een freelancer" -msgid "shortcuts.mask" -msgstr "Masker" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "Haal de code op van mijn teamproject " -msgid "shortcuts.move-fast-right" -msgstr "Snel naar rechts verplaatsen" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "... interfaceontwerp, visuel assets, design systems, enz." -msgid "shortcuts.move-fast-up" -msgstr "Snel naar boven verplaatsen" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "InVision" -msgid "shortcuts.move-nodes" -msgstr "Verplaats knooppunt" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "Laat feedback achter voor mijn teamproject" -msgid "shortcuts.move-unit-down" -msgstr "Naar beneden verplaatsen" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "Laten we beginnen!" -msgid "shortcuts.move-unit-left" -msgstr "Naar links verplaatsen" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "Product- of projectmanager" -msgid "shortcuts.move-unit-right" -msgstr "Naar rechts verplaatsen" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "Marketing" -msgid "shortcuts.move-unit-up" -msgstr "Naar boven verplaatsen" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "Meer dan 50" -msgid "shortcuts.next-frame" -msgstr "Volgend bord" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "Volgende" -msgid "shortcuts.opacity-1" -msgstr "Zet dekking op 10%" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "Geen" -msgid "shortcuts.opacity-2" -msgstr "Zet dekking op 20%" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "Anders (namelijk…)" -msgid "shortcuts.opacity-3" -msgstr "Zet dekking op 30%" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "Ik werk aan een persoonlijk project" -msgid "shortcuts.opacity-4" -msgstr "Zet dekking op 40%" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "Vorige" -msgid "shortcuts.opacity-5" -msgstr "Zet dekking op 50%" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "Hoe ben je van plan Penpot te gebruiken?" -msgid "shortcuts.opacity-6" -msgstr "Zet dekking op 60%" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "Wat is je rol?" -msgid "shortcuts.opacity-7" -msgstr "Zet dekking op 70%" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.select-option" +msgstr "Selecteer een optie" -msgid "shortcuts.opacity-8" -msgstr "Zet dekking op 80%" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "Sketch" -msgid "shortcuts.opacity-9" -msgstr "Zet dekking op 90%" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.some" +msgstr "Sommige" -msgid "shortcuts.open-color-picker" -msgstr "Kleurkiezer" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "Starten" -msgid "shortcuts.open-comments" -msgstr "Ga naar het commentaargedeelte van de kijker" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "Begin aan mijn project te werken" -msgid "shortcuts.open-dashboard" -msgstr "Ga naar dashboard" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "Student of docent" -msgid "shortcuts.open-inspect" -msgstr "Ga naar de sectie voor het inspecteren van kijkers" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "Hoe groot is je team?" -msgid "shortcuts.open-interactions" -msgstr "Ga naar de kijkersinteracties-sectie" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "Penpot testen om te zien of het geschikt is voor het team " -msgid "shortcuts.open-viewer" -msgstr "Ga naar de kijkersinteracties-sectie" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "Probeer uit voordat je Penpot on-premise gebruikt" -msgid "shortcuts.snap-nodes" -msgstr "Uitlijnen op knooppunten" - -msgid "shortcuts.snap-pixel-grid" -msgstr "Uitlijnen op pixelraster" - -msgid "shortcuts.start-editing" -msgstr "Start met bewerken" - -msgid "shortcuts.start-measure" -msgstr "Begin meting" - -msgid "shortcuts.stop-measure" -msgstr "Stop meting" - -msgid "shortcuts.thumbnail-set" -msgstr "Stel miniaturen in" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs -msgid "shortcuts.title" -msgstr "Toetsenbord sneltoetsen" - -msgid "shortcuts.toggle-alignment" -msgstr "Schakel dynamisch uitlijnen in/uit" - -msgid "shortcuts.toggle-assets" -msgstr "Schakel assets in/uit" - -msgid "shortcuts.toggle-colorpalette" -msgstr "Schakel kleurpalet in/uit" - -msgid "shortcuts.toggle-focus-mode" -msgstr "Schakel focusmodus in/uit" - -msgid "shortcuts.toggle-fullscreen" -msgstr "Schakel Volledig scherm in/uit" - -msgid "shortcuts.toggle-grid" -msgstr "Toon/verberg raster" - -msgid "shortcuts.toggle-history" -msgstr "Schakel historie in/uit" - -msgid "shortcuts.toggle-layers" -msgstr "Schakel lagen in/uit" - -msgid "shortcuts.toggle-layout-flex" -msgstr "Flex layout toevoegen/verwijderen" - -msgid "shortcuts.toggle-lock" -msgstr "Geselecteerde vergrendelen" - -msgid "shortcuts.toggle-rules" -msgstr "Toon/verberg linialen" - -msgid "shortcuts.toggle-scale-text" -msgstr "Schaaltekst wisselen" - -msgid "shortcuts.toggle-snap-grid" -msgstr "Uitlijnen op raster" - -msgid "shortcuts.toggle-textpalette" -msgstr "Schakel tekstpalet in/uit" - -msgid "shortcuts.toggle-visibility" -msgstr "Schakel zichtbaarheid" - -msgid "shortcuts.toggle-zoom-style" -msgstr "Schakel zoomstijl" - -msgid "shortcuts.underline" -msgstr "Schakel onderstreept" - -msgid "shortcuts.undo" -msgstr "Ongedaan maken" - -#: src/app/main/ui/dashboard/fonts.cljs -msgid "title.dashboard.font-providers" -msgstr "Lettertypeaanbieders - %s - Penpot" - -msgid "shortcuts.zoom-lense-increase" -msgstr "Zoomlens vergroten" - -msgid "shortcuts.zoom-selected" -msgstr "Zoomen naar geslecteerde" - -#: src/app/main/ui/dashboard/files.cljs -msgid "title.dashboard.files" -msgstr "%s - Penpot" - -#: src/app/main/ui/dashboard/fonts.cljs -msgid "title.dashboard.fonts" -msgstr "Lettertypes - %s - Penpot" - -#: src/app/main/ui/dashboard/search.cljs -msgid "title.dashboard.search" -msgstr "Zoeken - %s - Penpot" - -#: src/app/main/ui/dashboard/libraries.cljs -msgid "title.dashboard.shared-libraries" -msgstr "Gedeelde bibliotheken - %s - Penpot" - -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/auth.cljs -msgid "title.default" -msgstr "Penpot - Ontwerpvrijheid voor teams" - -#: src/app/main/ui/settings/feedback.cljs -msgid "title.settings.feedback" -msgstr "Feedback geven - Penpot" - -#: src/app/main/ui/settings/options.cljs -msgid "title.settings.options" -msgstr "Instellingen - Penpot" - -#: src/app/main/ui/settings/password.cljs -msgid "title.settings.password" -msgstr "Wachtwoord - Penpot" - -#: src/app/main/ui/settings/profile.cljs -msgid "title.settings.profile" -msgstr "Profiel - Penpot" - -#: src/app/main/ui/dashboard/team.cljs -msgid "title.team-invitations" -msgstr "Uitnodigingen - %s - Penpot" - -#: src/app/main/ui/dashboard/team.cljs -msgid "title.team-settings" -msgstr "Instellingen - %s - Penpot" - -#: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs -msgid "title.viewer" -msgstr "%s - Weergavemodus - Penpot" - -#: src/app/main/ui/dashboard/team.cljs -msgid "title.team-members" -msgstr "Leden - %s - Penpot" - -msgid "title.team-webhooks" -msgstr "Webhooks - %s - Penpot" - -#: src/app/main/ui/workspace.cljs -msgid "title.workspace" -msgstr "%s - Penpot" - -#: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs -msgid "viewer.empty-state" -msgstr "Geen borden gevonden op de pagina." - -#: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs -msgid "viewer.frame-not-found" -msgstr "Bord niet gevonden." - -msgid "viewer.header.comments-section" -msgstr "Commentaar (%s)" - -#: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.dont-show-interactions" -msgstr "Interacties niet tonen" - -#: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.fullscreen" -msgstr "Volledig scherm" - -msgid "viewer.header.inspect-section" -msgstr "Inspecteren (%s)" - -msgid "viewer.header.interactions-section" -msgstr "Interacties (%s)" - -#: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.copy-link" -msgstr "Link kopieëren" - -#: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.show-interactions" -msgstr "Interacties tonen" - -#: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.show-interactions-on-click" -msgstr "Interacties tonen bij aanklikken" - -msgid "webhooks.last-delivery.success" -msgstr "De laatste levering was succesvol." - -#: src/app/main/ui/workspace/sidebar/align.cljs -msgid "workspace.align.hcenter" -msgstr "Horizontaal gecentreerd uitlijnen (%s)" - -#: src/app/main/ui/workspace/sidebar/align.cljs -msgid "workspace.align.hdistribute" -msgstr "Verdeel horizontale tussenruimte (%s)" - -#: src/app/main/ui/workspace/sidebar/align.cljs -msgid "workspace.align.hleft" -msgstr "Links uitlijnen (%s)" - -#: src/app/main/ui/workspace/sidebar/align.cljs -msgid "workspace.align.hright" -msgstr "Rechts uitlijnen (%s)" - -#: src/app/main/ui/workspace/sidebar/align.cljs -msgid "workspace.align.vbottom" -msgstr "Onderkant uitlijnen (%s)" - -#: src/app/main/ui/workspace/sidebar/align.cljs -msgid "workspace.align.vcenter" -msgstr "Verticaal gecentreerd uitlijnen (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs -msgid "workspace.assets.typography.go-to-edit" -msgstr "Ga naar het stijl-bibliotheekbestand om te bewerken" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.selected-count" -msgid_plural "workspace.assets.selected-count" -msgstr[0] "%s item geselecteerd" -msgstr[1] "%s items geselecteerd" - -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.search" -msgstr "Zoek elementen" - -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs -msgid "workspace.assets.typography.letter-spacing" -msgstr "Letterafstand" - -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs -msgid "workspace.assets.typography.line-height" -msgstr "Lijnhoogte" - -msgid "workspace.assets.typography.text-styles" -msgstr "Tekststijlen" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-dynamic-alignment" -msgstr "Schakel dynamische uitlijning uit" - -msgid "workspace.header.menu.disable-scale-content" -msgstr "Schakel proportionele schaal uit" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-scale-text" -msgstr "Schaaltekst uitschakelen" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Uitlijnen op raster uitschakelen" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-guides" -msgstr "Uitlijnen op hulplijnen uitschakelen" - -msgid "workspace.header.menu.disable-snap-pixel-grid" -msgstr "Uitlijnen op pixel uitschakelen" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-dynamic-alignment" -msgstr "Schakel dynamische uitlijning in" - -msgid "workspace.header.menu.enable-scale-content" -msgstr "Schakel proportionele schaal in" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-scale-text" -msgstr "Schaaltekst inschakelen" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Uitlijnen op raster" - -msgid "workspace.header.menu.enable-snap-pixel-grid" -msgstr "Uitlijnen op pixel" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-artboard-names" -msgstr "Verberg bornnamen" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Verberg rasters" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-palette" -msgstr "Verberg kleurpalet" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-textpalette" -msgstr "Verberg lettertype-palet" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.option.help-info" -msgstr "Help & informatie" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.select-all" -msgstr "Alles selecteren" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-artboard-names" -msgstr "Toon bordnamen" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Toon raster" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-palette" -msgstr "Toon kleurpalen" - -msgid "workspace.header.menu.show-pixel-grid" -msgstr "Toon pixelraster" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-rules" -msgstr "Toon linialen" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.save-error" -msgstr "Fout tijdens opslaan" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.unsaved" -msgstr "Niet-opgeslagen wijzigingen" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.viewer" -msgstr "Kijkmodus (%s)" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.zoom-fit" -msgstr "Passend maken - Verkleinen om te passen" - -#: src/app/main/ui/workspace/libraries.cljs -msgid "workspace.libraries.no-libraries-need-sync" -msgstr "Er zijn geen gedeelde bibliotheken die moeten worden bijgewerkt" - -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs -msgid "workspace.options.export-object" -msgid_plural "workspace.options.export-object" -msgstr[0] "Exporteer 1 element" -msgstr[1] "Exporteer %s elementen" - -msgid "onboarding.choice.team-up.invite-members" -msgstr "Leden uitnodigen" - -msgid "onboarding-v2.welcome.desc2.title" -msgstr "Deelnemen aan de Penpotgemeenschap" - -msgid "onboarding-v2.welcome.desc3.title" -msgstr "Bijdragen" - -msgid "onboarding-v2.welcome.title" -msgstr "Welkom bij Penpot!" - -msgid "onboarding.choice.team-up.create-later" -msgstr "Later een team aanmaken" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Jouw teamnaam" - -msgid "onboarding.choice.team-up.create-team-placeholder" -msgstr "Voer de naam van het team in" - -#: src/app/main/ui/settings/delete_account.cljs -msgid "notifications.profile-deletion-not-allowed" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" msgstr "" -"Je kunt je profiel niet verwijderen. Wijs je teams opnieuw toe voordat je " -"verder gaat." +"… draadmodellen, gebruikers journeys en stroomdiagrammen, navigatiebomen, " +"etc." -msgid "onboarding-v2.newsletter.news" +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "Werken in conceptideeën" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" msgstr "" -"Stuur mij nieuws over Penpot (blogposts, video-tutorials, streamings...)." +"Jouw feedback helpt ons te begrijpen wat je gewoonten en voorkeuren zijn, " +"zodat we van Penpot een nuttig en plezierig hulpmiddel kunnen blijven maken." -msgid "onboarding-v2.newsletter.privacy1" -msgstr "Wij geven om privacy, lees hier onze " - -#: src/app/main/ui/settings/change_email.cljs -msgid "notifications.validation-email-sent" -msgstr "Verificatie-e-mail verzonden naar %s. Check je e-mail!" - -msgid "onboarding-v2.newsletter.updates" -msgstr "Stuur mij productupdates (nieuwe functies, releases, fixes...)." - -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs -msgid "modals.update-remote-component.message" -msgstr "Werk een component in een gedeelde bibliotheek bij" - -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs -msgid "modals.update-remote-component-in-bulk.message" -msgstr "Werk componenten in een gedeelde bibliotheek bij" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.title" -msgid_plural "modals.unpublish-shared-confirm.title" -msgstr[0] "Publicatie bibliotheek ongedaan maken" -msgstr[1] "Publicatie bibliotheken ongedaan maken" - -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs -msgid "modals.update-remote-component.cancel" -msgstr "Annuleren" - -msgid "onboarding-v2.before-start.desc2.title" -msgstr "Gebruikershandleiding" - -#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "Gemixt" @@ -3457,148 +2587,896 @@ msgid "shortcut-subsection.text-editor" msgstr "Teksten" msgid "shortcut-subsection.tools" -msgstr "Tools" +msgstr "Hulpmiddelen" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs -msgid "workspace.shape.menu.go-main" -msgstr "Ga naar hoofdcomponent" +msgid "shortcut-subsection.zoom-viewer" +msgstr "Zoomen" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-close-outside" -msgstr "Sluiten als er buiten wordt geklikt" +msgid "shortcut-subsection.zoom-workspace" +msgstr "Zoomen" -msgid "workspace.options.show-in-viewer" -msgstr "Bekijk in Weergavemodus" +msgid "shortcuts.add-comment" +msgstr "Commentaar" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs -msgid "workspace.shape.menu.show-in-assets" -msgstr "Bekijk in elementenpaneel" +msgid "shortcuts.add-node" +msgstr "Knooppunt toevoegen" -#: src/app/main/ui/workspace/libraries.cljs -msgid "workspace.libraries.components" -msgstr "%s componenten" +msgid "shortcuts.align-bottom" +msgstr "Onderaan uitlijnen" -#: src/app/main/ui/workspace/libraries.cljs -msgid "workspace.libraries.search-shared-libraries" -msgstr "Zoek gedeelde bibliotheken" +msgid "shortcuts.align-center" +msgstr "Centreren" -msgid "workspace.options.clip-content" -msgstr "Clip content" +msgid "shortcuts.align-hcenter" +msgstr "Horizontaal centreren" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs -msgid "workspace.options.exporting-complete" -msgstr "Export klaar" +msgid "shortcuts.align-justify" +msgstr "Uitlijnen" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs -msgid "workspace.options.exporting-object-error" -msgstr "Export mislukt" +msgid "shortcuts.align-left" +msgstr "Links uitlijnen" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs -msgid "workspace.options.exporting-object-slow" -msgstr "Export is ontiegelijk traag" +msgid "shortcuts.align-right" +msgstr "Rechts uitlijnen" -#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs -msgid "workspace.options.group-fill" -msgstr "Groep vullen" +msgid "shortcuts.align-top" +msgstr "Bovenaan uitlijnen" -#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs -msgid "workspace.options.group-stroke" -msgstr "Groep lijn" +msgid "shortcuts.align-vcenter" +msgstr "Verticaal centreren" -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.params.use-default" -msgstr "Gebruik standaard" +msgid "shortcuts.artboard-selection" +msgstr "Maak bord van selectie" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-after-delay" -msgstr "Na vertraging" +msgid "shortcuts.bold" +msgstr "Vet in/uitschakelen" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-background" -msgstr "Achtergrondoverlay toevoegen" +msgid "shortcuts.bool-difference" +msgstr "Aftrekken (Booleaans verschil)" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-close-overlay" -msgstr "Overlay sluiten" +msgid "shortcuts.bool-exclude" +msgstr "Uitsluiten" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-close-overlay-dest" -msgstr "Overlay sluiten: %s" +msgid "shortcuts.bool-intersection" +msgstr "Booleaanse kruising" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-easing-ease-in" -msgstr "Ease in" +msgid "shortcuts.bool-union" +msgstr "Booleaanse vereniging" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-navigate-to" -msgstr "Navigeer naar" +msgid "shortcuts.bring-back" +msgstr "Naar de achtergrond" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-mouse-leave" -msgstr "Muis verlaat" +msgid "shortcuts.bring-backward" +msgstr "Naar achteren" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-pos-bottom-left" -msgstr "Linksonder" +msgid "shortcuts.bring-forward" +msgstr "Naar voren" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-none" -msgstr "(niet ingesteld)" +msgid "shortcuts.bring-front" +msgstr "Naar de voorgrond" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-prev-screen" -msgstr "Vorig scherm" +msgid "shortcuts.clear-undo" +msgstr "Ongedaan maken wissen" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-pos-bottom-right" -msgstr "Rechtsonder" +msgid "shortcuts.copy" +msgstr "Kopiëren" -#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs -msgid "workspace.options.layer-options.blend-mode.difference" -msgstr "Verschil" +msgid "shortcuts.create-component" +msgstr "Component aanmaken" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.interaction-while-hovering" -msgstr "Tijdens hover" +msgid "shortcuts.create-new-project" +msgstr "Nieuw project aanmaken" -#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs -msgid "workspace.options.layer-options.blend-mode.color-dodge" -msgstr "Color dodge" +msgid "shortcuts.cut" +msgstr "Knippen" -#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs -msgid "workspace.options.layout-item.layout-item-min-h" -msgstr "Min.Hoogte" +msgid "shortcuts.decrease-zoom" +msgstr "Uitzoomen" -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.column" -msgstr "Kolom" +msgid "shortcuts.delete" +msgstr "Verwijderen" -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.column-reverse" -msgstr "Omgekeerde kolom" +msgid "shortcuts.delete-node" +msgstr "Knooppunt verwijderen" -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.gap" -msgstr "Tussenruimte" +msgid "shortcuts.detach-component" +msgstr "Component losmaken" -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.left" -msgstr "Links" +msgid "shortcuts.draw-curve" +msgstr "Kromme" -#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs -msgid "workspace.options.layout.margin" -msgstr "Marge" +msgid "shortcuts.draw-ellipse" +msgstr "Ellips" -msgid "workspace.viewport.click-to-close-path" -msgstr "Klik om het pad te sluiten" +msgid "shortcuts.draw-frame" +msgstr "Bord" + +msgid "shortcuts.draw-nodes" +msgstr "Pad tekenen" + +msgid "shortcuts.draw-path" +msgstr "Pad" + +msgid "shortcuts.draw-rect" +msgstr "Rechthoek" + +msgid "shortcuts.draw-text" +msgstr "Tekst" + +msgid "shortcuts.duplicate" +msgstr "Dupliceren" + +msgid "shortcuts.escape" +msgstr "Annuleren" + +msgid "shortcuts.export-shapes" +msgstr "Vormen exporteren" + +msgid "shortcuts.fit-all" +msgstr "Passend zoomen" + +msgid "shortcuts.flip-horizontal" +msgstr "Horizontaal spiegelen" + +msgid "shortcuts.flip-vertical" +msgstr "Verticaal spiegelen" + +msgid "shortcuts.font-size-dec" +msgstr "Lettergrootte verkleinen" + +msgid "shortcuts.font-size-inc" +msgstr "Lettergrootte vergroten" + +msgid "shortcuts.go-to-drafts" +msgstr "Ga naar concepten" + +msgid "shortcuts.go-to-libs" +msgstr "Ga naar gedeelde bibliotheek" + +msgid "shortcuts.go-to-search" +msgstr "Zoeken" + +msgid "shortcuts.group" +msgstr "Groeperen" + +msgid "shortcuts.h-distribute" +msgstr "Horizontaal verdelen" + +msgid "shortcuts.hide-ui" +msgstr "UI tonen/verbergen" + +msgid "shortcuts.increase-zoom" +msgstr "Inzoomen" + +msgid "shortcuts.insert-image" +msgstr "Afbeelding invoegen" + +msgid "shortcuts.italic" +msgstr "Cursief in/uitschakelen" + +msgid "shortcuts.join-nodes" +msgstr "Knooppunten verbinden" + +msgid "shortcuts.letter-spacing-dec" +msgstr "Letterafstand verkleinen" + +msgid "shortcuts.letter-spacing-inc" +msgstr "Letterafstand vergroten" + +msgid "shortcuts.line-height-dec" +msgstr "Regelafstand verkleinen" + +msgid "shortcuts.line-height-inc" +msgstr "Regelafstand vergroten" + +msgid "shortcuts.line-through" +msgstr "Doorstrepen in/uitschakelen" + +msgid "shortcuts.make-corner" +msgstr "Hoek maken" + +msgid "shortcuts.make-curve" +msgstr "Kromme maken" + +msgid "shortcuts.mask" +msgstr "Maskeren" + +msgid "shortcuts.merge-nodes" +msgstr "Knooppunten samenvoegen" + +msgid "shortcuts.move" +msgstr "Verplaatsen" + +msgid "shortcuts.move-fast-down" +msgstr "Snel naar beneden verplaatsen" + +msgid "shortcuts.move-fast-left" +msgstr "Snel naar links verplaatsen" + +msgid "shortcuts.move-fast-right" +msgstr "Snel naar rechts verplaatsen" + +msgid "shortcuts.move-fast-up" +msgstr "Snel naar boven verplaatsen" + +msgid "shortcuts.move-nodes" +msgstr "Knooppunt verplaatsen" + +msgid "shortcuts.move-unit-down" +msgstr "Naar beneden verplaatsen" + +msgid "shortcuts.move-unit-left" +msgstr "Naar links verplaatsen" + +msgid "shortcuts.move-unit-right" +msgstr "Naar rechts verplaatsen" + +msgid "shortcuts.move-unit-up" +msgstr "Naar boven verplaatsen" + +msgid "shortcuts.next-frame" +msgstr "Volgend bord" + +msgid "shortcuts.not-found" +msgstr "Geen sneltoets gevonden" + +msgid "shortcuts.opacity-0" +msgstr "Dekking 100%" + +msgid "shortcuts.opacity-1" +msgstr "Dekking 10%" + +msgid "shortcuts.opacity-2" +msgstr "Dekking 20%" + +msgid "shortcuts.opacity-3" +msgstr "Dekking 30%" + +msgid "shortcuts.opacity-4" +msgstr "Dekking 40%" + +msgid "shortcuts.opacity-5" +msgstr "Dekking 50%" + +msgid "shortcuts.opacity-6" +msgstr "Dekking 60%" + +msgid "shortcuts.opacity-7" +msgstr "Dekking 70%" + +msgid "shortcuts.opacity-8" +msgstr "Dekking 80%" + +msgid "shortcuts.opacity-9" +msgstr "Dekking 90%" + +msgid "shortcuts.open-color-picker" +msgstr "Kleurkiezer" + +msgid "shortcuts.open-comments" +msgstr "Ga naar het commentaargedeelte van de kijker" + +msgid "shortcuts.open-dashboard" +msgstr "Ga naar dashboard" + +msgid "shortcuts.open-inspect" +msgstr "Ga naar de sectie voor het inspecteren van kijkers" + +msgid "shortcuts.open-interactions" +msgstr "Ga naar de kijkersinteracties-sectie" + +msgid "shortcuts.open-viewer" +msgstr "Ga naar de kijkersinteracties-sectie" + +msgid "shortcuts.open-workspace" +msgstr "Ga naar werkruimte" + +msgid "shortcuts.or" +msgstr " of " + +msgid "shortcuts.paste" +msgstr "Plakken" + +msgid "shortcuts.prev-frame" +msgstr "Vorig bord" + +msgid "shortcuts.redo" +msgstr "Opnieuw doen" + +msgid "shortcuts.reset-zoom" +msgstr "Zoom herstellen" + +msgid "shortcuts.search-placeholder" +msgstr "Sneltoetsen zoeken" + +msgid "shortcuts.select-all" +msgstr "Alles selecteren" + +msgid "shortcuts.select-next" +msgstr "Volgende laag selecteren" + +msgid "shortcuts.select-parent-layer" +msgstr "Bovenliggende laag selecteren" + +msgid "shortcuts.select-prev" +msgstr "Vorige laag selecteren" + +msgid "shortcuts.separate-nodes" +msgstr "Knooppunten loskoppelen" + +msgid "shortcuts.show-pixel-grid" +msgstr "Pixelraster tonen/verbergen" + +msgid "shortcuts.show-shortcuts" +msgstr "Sneltoetsen tonen/verbergen" + +msgid "shortcuts.snap-nodes" +msgstr "Uitlijnen op knooppunten" + +msgid "shortcuts.snap-pixel-grid" +msgstr "Uitlijnen op pixelraster" + +msgid "shortcuts.start-editing" +msgstr "Start met bewerken" + +msgid "shortcuts.start-measure" +msgstr "Meting starten" + +msgid "shortcuts.stop-measure" +msgstr "Meting beëindigen" + +msgid "shortcuts.text-align-center" +msgstr "Gecentreerd uitlijnen" + +msgid "shortcuts.text-align-justify" +msgstr "Volledig uitvullen" + +msgid "shortcuts.text-align-left" +msgstr "Links uitlijnen" + +msgid "shortcuts.text-align-right" +msgstr "Rechts uitlijnen" + +msgid "shortcuts.thumbnail-set" +msgstr "Miniaturen instellen" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs +msgid "shortcuts.title" +msgstr "Sneltoetsen" + +msgid "shortcuts.toggle-alignment" +msgstr "Dynamisch uitlijnen in/uitschakelen" + +msgid "shortcuts.toggle-assets" +msgstr "Assets in/uitschakelen" + +msgid "shortcuts.toggle-colorpalette" +msgstr "Kleurenpalet in/uitschakelen" + +msgid "shortcuts.toggle-focus-mode" +msgstr "Focusmodus in/uitschakelen" + +msgid "shortcuts.toggle-fullscreen" +msgstr "Volledig scherm in/uitschakelen" + +msgid "shortcuts.toggle-history" +msgstr "Geschiedenis in/uitschakelen" + +msgid "shortcuts.toggle-layers" +msgstr "Lagen in/uitschakelen" + +msgid "shortcuts.toggle-layout-flex" +msgstr "Flex-layout toevoegen/verwijderen" + +msgid "shortcuts.toggle-lock" +msgstr "Vergrendelen/ontgrendelen" + +msgid "shortcuts.toggle-lock-size" +msgstr "Proporties vergrendelen" + +msgid "shortcuts.toggle-rules" +msgstr "Linialen tonen/verbergen" + +msgid "shortcuts.toggle-textpalette" +msgstr "Tekstpalet in/uitschakelen" + +msgid "shortcuts.toggle-visibility" +msgstr "Tonen/verbergen" + +msgid "shortcuts.toggle-zoom-style" +msgstr "Zoomstijl wisselen" + +msgid "shortcuts.underline" +msgstr "Onderstrepen in/uitschakelen" + +msgid "shortcuts.undo" +msgstr "Ongedaan maken" + +msgid "shortcuts.ungroup" +msgstr "Groep opheffen" + +msgid "shortcuts.unmask" +msgstr "Masker verwijderen" + +msgid "shortcuts.v-distribute" +msgstr "Verticaal verdelen" + +msgid "shortcuts.zoom-lense-decrease" +msgstr "Zoomlens verkleinen" + +msgid "shortcuts.zoom-lense-increase" +msgstr "Zoomlens vergroten" + +msgid "shortcuts.zoom-selected" +msgstr "Zoomen naar selectie" + +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "De webhooknaam mag maximaal 2048 tekens bevatten." + +#: src/app/main/ui/dashboard/files.cljs +msgid "title.dashboard.files" +msgstr "%s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.font-providers" +msgstr "Lettertypeaanbieders - %s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.fonts" +msgstr "Lettertypen - %s - Penpot" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "title.dashboard.projects" +msgstr "Projecten - %s - Penpot" + +#: src/app/main/ui/dashboard/search.cljs +msgid "title.dashboard.search" +msgstr "Zoeken - %s - Penpot" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "title.dashboard.shared-libraries" +msgstr "Gedeelde bibliotheken - %s - Penpot" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/auth.cljs +msgid "title.default" +msgstr "Penpot - Ontwerpvrijheid voor teams" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "Profiel - Toegangsbewijzen" + +#: src/app/main/ui/settings/feedback.cljs +msgid "title.settings.feedback" +msgstr "Feedback geven - Penpot" + +#: src/app/main/ui/settings/options.cljs +msgid "title.settings.options" +msgstr "Instellingen - Penpot" + +#: src/app/main/ui/settings/password.cljs +msgid "title.settings.password" +msgstr "Wachtwoord - Penpot" + +#: src/app/main/ui/settings/profile.cljs +msgid "title.settings.profile" +msgstr "Profiel - Penpot" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-invitations" +msgstr "Uitnodigingen - %s - Penpot" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-members" +msgstr "Leden - %s - Penpot" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-settings" +msgstr "Instellingen - %s - Penpot" + +msgid "title.team-webhooks" +msgstr "Webhooks - %s - Penpot" + +#: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs +msgid "title.viewer" +msgstr "%s - Weergavemodus - Penpot" + +#: src/app/main/ui/workspace.cljs +msgid "title.workspace" +msgstr "%s - Penpot" + +msgid "viewer.breaking-change.description" +msgstr "" +"Deze deelbare link is niet langer geldig. Maak een nieuwe aan of vraag de " +"eigenaar om een nieuwe." + +msgid "viewer.breaking-change.message" +msgstr "Sorry!" + +#: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs +msgid "viewer.empty-state" +msgstr "Geen borden gevonden op de pagina." + +#: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs +msgid "viewer.frame-not-found" +msgstr "Bord niet gevonden." + +msgid "viewer.header.comments-section" +msgstr "Commentaar (%s)" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.dont-show-interactions" +msgstr "Interacties niet tonen" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.fullscreen" +msgstr "Volledig scherm" + +msgid "viewer.header.inspect-section" +msgstr "Inspecteren (%s)" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.interactions" +msgstr "Interacties" + +msgid "viewer.header.interactions-section" +msgstr "Interacties (%s)" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.copy-link" +msgstr "Link kopiëren" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.show-interactions" +msgstr "Interacties tonen" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.show-interactions-on-click" +msgstr "Interacties tonen bij aanklikken" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.sitemap" +msgstr "Sitemap" + +msgid "webhooks.last-delivery.success" +msgstr "De laatste levering was succesvol." + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hcenter" +msgstr "Horizontaal centreren (%s)" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hdistribute" +msgstr "Horizontaal verdelen (%s)" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hleft" +msgstr "Links uitlijnen (%s)" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hright" +msgstr "Rechts uitlijnen (%s)" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vbottom" +msgstr "Onderkant uitlijnen (%s)" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vcenter" +msgstr "Verticaal centreren (%s)" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vdistribute" +msgstr "Verticaal verdelen (%s)" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vtop" +msgstr "Bovenkant uitlijnen (%s)" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.assets" +msgstr "Assets" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.box-filter-all" +msgstr "Alle assets" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.colors" +msgstr "Kleuren" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.components" +msgstr "Componenten" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.create-group" +msgstr "Groep aanmaken" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.create-group-hint" +msgstr "Je items krijgen automatisch de naam \"groepsnaam / itemnaam\"" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.delete" +msgstr "Verwijderen" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.duplicate" +msgstr "Dupliceren" msgid "workspace.assets.duplicate-main" msgstr "Hoofdcomponent dupliceren" +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.edit" +msgstr "Bewerken" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.graphics" +msgstr "Graphics" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group" +msgstr "Groeperen" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group-name" +msgstr "Groepsnaam" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.libraries" +msgstr "Bibliotheken" + +msgid "workspace.assets.local-library" +msgstr "Lokale bibliotheek" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.not-found" +msgstr "Geen assets gevonden" + +msgid "workspace.assets.open-library" +msgstr "Open Bibliotheek-bestand" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.rename" +msgstr "Hernoemen" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.rename-group" +msgstr "Groep hernoemen" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.search" +msgstr "Assets zoeken" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.selected-count" +msgid_plural "workspace.assets.selected-count" +msgstr[0] "%s item geselecteerd" +msgstr[1] "%s items geselecteerd" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.shared-library" +msgstr "Gedeelde bibliotheek" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.typography" +msgstr "Typografie" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-id" +msgstr "Lettertype" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-size" +msgstr "Grootte" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-variant-id" +msgstr "Variant" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.go-to-edit" +msgstr "Ga naar het stijl-bibliotheekbestand om te bewerken" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.letter-spacing" +msgstr "Letterafstand" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.line-height" +msgstr "Regelafstand" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "workspace.assets.typography.sample" +msgstr "Ag" + +msgid "workspace.assets.typography.text-styles" +msgstr "Tekststijlen" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.text-transform" +msgstr "Tekst transformeren" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.ungroup" +msgstr "Groep opheffen" + +msgid "workspace.focus.focus-mode" +msgstr "Focusmodus" + +msgid "workspace.focus.focus-off" +msgstr "Focus uit" + +msgid "workspace.focus.focus-on" +msgstr "Focus aan" + +msgid "workspace.focus.selection" +msgstr "Selectie" + +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs +msgid "workspace.gradients.linear" +msgstr "Lineair verloop" + +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs +msgid "workspace.gradients.radial" +msgstr "Radiaal verloop" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-dynamic-alignment" +msgstr "Dynamische uitlijning uitschakelen" + +msgid "workspace.header.menu.disable-scale-content" +msgstr "Proportionele schaal uitschakelen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-scale-text" +msgstr "Tekstschaal uitschakelen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-snap-guides" +msgstr "Uitlijnen op hulplijnen uitschakelen" + +msgid "workspace.header.menu.disable-snap-pixel-grid" +msgstr "Uitlijnen op pixel uitschakelen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-dynamic-alignment" +msgstr "Dynamische uitlijning inschakelen" + +msgid "workspace.header.menu.enable-scale-content" +msgstr "Proportionele schaal inschakelen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-scale-text" +msgstr "Tekstschaal inschakelen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-snap-guides" +msgstr "Uitlijnen op hulplijnen" + +msgid "workspace.header.menu.enable-snap-pixel-grid" +msgstr "Uitlijnen op pixel" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-artboard-names" +msgstr "Bordnamen verbergen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-palette" +msgstr "Kleurenpalet verbergen" + +msgid "workspace.header.menu.hide-pixel-grid" +msgstr "Pixelraster verbergen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-rules" +msgstr "Linialen verbergen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-textpalette" +msgstr "Lettertype-palet verbergen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.edit" +msgstr "Bewerken" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.file" +msgstr "Bestand" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.help-info" +msgstr "Hulp & informatie" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.preferences" +msgstr "Voorkeuren" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.option.view" +msgstr "Beeld" + +msgid "workspace.header.menu.redo" +msgstr "Opnieuw doen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.select-all" +msgstr "Alles selecteren" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-artboard-names" +msgstr "Bordnamen tonen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-palette" +msgstr "Kleurenpalet tonen" + +msgid "workspace.header.menu.show-pixel-grid" +msgstr "Pixelraster tonen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-rules" +msgstr "Linialen tonen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-textpalette" +msgstr "Lettertype-palet tonen" + +msgid "workspace.header.menu.undo" +msgstr "Ongedaan maken" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.reset-zoom" +msgstr "Herstellen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.save-error" +msgstr "Fout tijdens opslaan" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.saved" +msgstr "Opgeslagen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.saving" +msgstr "Opslaan" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.unsaved" +msgstr "Niet-opgeslagen wijzigingen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.viewer" +msgstr "Weergavemodus (%s)" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "Zoomen" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-fill" msgstr "Vullen - Schalen om te vullen" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom-fit" +msgstr "Passend maken - Verkleinen om te passen" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-fit-all" msgstr "Passend zoomen" @@ -3609,17 +3487,38 @@ msgstr "Volledig scherm" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-selected" -msgstr "Zoom naar geselecteerde" +msgstr "Naar selectie zoomen" + +msgid "workspace.layout_grid.editor.title" +msgstr "Raster bewerken" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.add" +msgstr "Toevoegen" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.colors" msgstr "%s kleuren" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "Er zijn nog geen kleurstijlen in je bibliotheek" + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "Er zijn nog geen typografiestijlen in je bibliotheek" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" msgstr "Bestandsbibliotheek" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.hsv" +msgstr "HSV" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "Recente kleuren" @@ -3627,13 +3526,17 @@ msgstr "Recente kleuren" msgid "workspace.libraries.colors.rgb-complementary" msgstr "RGB Complementair" +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.rgba" +msgstr "RGBA" + #: src/app/main/ui/workspace/colorpicker.cljs msgid "workspace.libraries.colors.save-color" msgstr "Kleurstijl opslaan" #: src/app/main/ui/workspace/libraries.cljs -msgid "workspace.libraries.in-this-file" -msgstr "BIBLIOTHEKEN IN DIT BESTAND" +msgid "workspace.libraries.components" +msgstr "%s componenten" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.file-library" @@ -3643,25 +3546,73 @@ msgstr "Bestandsbibliotheek" msgid "workspace.libraries.graphics" msgstr "%s afbeeldingen" +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.in-this-file" +msgstr "BIBLIOTHEKEN IN DIT BESTAND" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.libraries" +msgstr "BIBLIOTHEKEN" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library" +msgstr "BIBLIOTHEEK" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "BIBLIOTHEEK-UPDATES" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-libraries-need-sync" +msgstr "Er zijn geen gedeelde bibliotheken die moeten worden bijgewerkt" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.no-matches-for" msgstr "Geen resultaten gevonden voor “%s“" +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-shared-libraries-available" +msgstr "Er zijn geen gedeelde bibliotheken beschikbaar" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.search-shared-libraries" +msgstr "Gedeelde bibliotheken zoeken" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.shared-libraries" msgstr "GEDEELDE BIBLIOTHEKEN" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.libraries.text.multiple-typography" -msgstr "Meerdere typografieën" +msgstr "Meervoudige typografie" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.libraries.text.multiple-typography-tooltip" -msgstr "Ontkoppel alle typografieën" +msgstr "Alle typografie ontkoppelen" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.typography" -msgstr "%s typografieën" +msgstr "%s typografie" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.update" +msgstr "Bijwerken" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "alle wijzigingen bekijken" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.updates" +msgstr "UPDATES" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.add-interaction" +msgstr "Klik op de knop + om interacties toe te voegen." + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title" +msgstr "Vervagen" #: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "workspace.options.blur-options.title.group" @@ -3675,38 +3626,295 @@ msgstr "Selectie vervagen" msgid "workspace.options.canvas-background" msgstr "Canvasachtergrond" +msgid "workspace.options.clip-content" +msgstr "Content bijsnijden" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs +msgid "workspace.options.component" +msgstr "Component" + +msgid "workspace.options.component.annotation" +msgstr "Aantekening" + +msgid "workspace.options.component.create-annotation" +msgstr "Aantekening maken" + +msgid "workspace.options.component.edit-annotation" +msgstr "Aantekening bewerken" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints" +msgstr "Beperkingen" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.bottom" +msgstr "Onderkant" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.center" +msgstr "Midden" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints.fix-when-scrolling" msgstr "Vastzetten tijdens scrollen" +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.left" +msgstr "Links" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints.leftright" msgstr "Links & Rechts" +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.right" +msgstr "Rechts" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.scale" +msgstr "Schaal" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.top" +msgstr "Bovenkant" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints.topbottom" msgstr "Boven- en onderkant" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options.cljs +msgid "workspace.options.design" +msgstr "Ontwerp" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs +msgid "workspace.options.export" +msgstr "Exporteren" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-multiple" -msgstr "Exporteer selectie" +msgstr "Selectie exporteren" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs +msgid "workspace.options.export-object" +msgid_plural "workspace.options.export-object" +msgstr[0] "1 Element exporteren" +msgstr[1] "%s Elementen exporteren" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs +msgid "workspace.options.export.suffix" +msgstr "Achtervoegsel" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +msgid "workspace.options.exporting-complete" +msgstr "Export klaar" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +msgid "workspace.options.exporting-object" +msgstr "Exporteren…" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +msgid "workspace.options.exporting-object-error" +msgstr "Export mislukt" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +msgid "workspace.options.exporting-object-slow" +msgstr "Export onverwacht traag" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.fill" +msgstr "Vullen" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.flows.add-flow-start" -msgstr "Flow-startpunt toevoegen" +msgstr "Stroomdiagram-startpunt toevoegen" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.flows.flow-start" -msgstr "Flow startpunt" +msgstr "Stroomdiagram-startpunt" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.flows.flow-starts" -msgstr "Flow startpunten" +msgstr "Stroomdiagram-startpunten" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.auto" +msgstr "Automatisch" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.column" +msgstr "Kolommen" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.grid-title" +msgstr "Raster" + +msgid "workspace.options.grid.params.color" +msgstr "Kleur" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.columns" +msgstr "Kolommen" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.gutter" +msgstr "Tussenruimte" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.height" +msgstr "Hoogte" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.margin" +msgstr "Marge" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.rows" +msgstr "Rijen" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.set-default" msgstr "Als standaard instellen" +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.size" +msgstr "Grootte" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type" +msgstr "Type" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.bottom" +msgstr "Onderkant" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.center" +msgstr "Midden" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.left" +msgstr "Links" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.right" +msgstr "Rechts" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.stretch" +msgstr "Uitrekken" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.top" +msgstr "Bovenkant" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.use-default" +msgstr "Standaard gebruiken" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.width" +msgstr "Breedte" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.row" +msgstr "Rijen" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.square" +msgstr "Vierkant" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.group-fill" +msgstr "Groep vullen" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.group-stroke" +msgstr "Groep-streek" + +msgid "workspace.options.height" +msgstr "Hoogte" + +msgid "workspace.options.inspect" +msgstr "Inspecteren" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-action" +msgstr "Actie" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-after-delay" +msgstr "Na vertraging" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation" +msgstr "Animatie" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-dissolve" +msgstr "Ontbinden" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-none" +msgstr "Geen" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-push" +msgstr "Duwen" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-animation-slide" +msgstr "Schuiven" + +msgid "workspace.options.interaction-auto" +msgstr "automatisch" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-background" +msgstr "Achtergrond toevoegen" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-outside" +msgstr "Sluiten als er buiten wordt geklikt" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-overlay" +msgstr "Overlay sluiten" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-overlay-dest" +msgstr "Overlay sluiten: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-delay" +msgstr "Vertraging" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-destination" +msgstr "Bestemming" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-duration" +msgstr "Duur" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing" +msgstr "Easing" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease" +msgstr "Ease" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-ease-in" +msgstr "Ease in" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-easing-ease-in-out" msgstr "Ease in out" @@ -3715,14 +3923,38 @@ msgstr "Ease in out" msgid "workspace.options.interaction-easing-ease-out" msgstr "Ease out" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-easing-linear" +msgstr "Lineair" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-in" +msgstr "In" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-mouse-enter" msgstr "Muis komt binnen" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-mouse-leave" +msgstr "Muis verlaat" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-ms" +msgstr "ms" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-navigate-to" +msgstr "Navigeer naar" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-navigate-to-dest" msgstr "Navigeer naar: %s" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-none" +msgstr "(niet ingesteld)" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-offset-effect" msgstr "Offset-effect" @@ -3743,9 +3975,29 @@ msgstr "Overlay openen: %s" msgid "workspace.options.interaction-open-url" msgstr "URL openen" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-out" +msgstr "Out" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-pos-bottom-center" -msgstr "Middenonder" +msgstr "Midden onder" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-left" +msgstr "Linksonder" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-right" +msgstr "Rechtsonder" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-center" +msgstr "Gecentreerd" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-manual" +msgstr "Handmatig" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-pos-top-center" @@ -3759,38 +4011,74 @@ msgstr "Linksboven" msgid "workspace.options.interaction-pos-top-right" msgstr "Rechtsboven" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-position" +msgstr "Positie" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-preserve-scroll" -msgstr "Behoud de scrollpositie" +msgstr "Scrollpositie behouden" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-prev-screen" +msgstr "Vorig scherm" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-relative-to" msgstr "Relatief tot" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-self" +msgstr "zelf" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-toggle-overlay" -msgstr "Schakel overlay in" +msgstr "Overlay in/uitschakelen" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-toggle-overlay-dest" -msgstr "Schakel overlay in: %s" +msgstr "Overlay in/uitschakelen: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-trigger" +msgstr "Trigger" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-url" +msgstr "URL" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-while-hovering" +msgstr "Tijdens hover" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-while-pressing" msgstr "Tijdens klikken" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interactions" +msgstr "Interacties" + #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.color" msgstr "Kleur" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.color-burn" -msgstr "Color burn" +msgstr "Kleur versterken" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.color-dodge" +msgstr "Kleur verminderen" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.darken" msgstr "Donkerder maken" +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.difference" +msgstr "Verschil" + #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.exclusion" msgstr "Uitsluiting" @@ -3805,16 +4093,12 @@ msgstr "Tint" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.lighten" -msgstr "Verlichten" +msgstr "Lichter maken" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.luminosity" msgstr "Helderheid" -#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs -msgid "workspace.options.layer-options.blend-mode.overlay" -msgstr "Overlay" - #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.multiply" msgstr "Vermenigvuldigen" @@ -3823,6 +4107,10 @@ msgstr "Vermenigvuldigen" msgid "workspace.options.layer-options.blend-mode.normal" msgstr "Normaal" +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.overlay" +msgstr "Overlay" + #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.saturation" msgstr "Verzadiging" @@ -3841,7 +4129,7 @@ msgstr "Laag" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.title.group" -msgstr "Groepslagen" +msgstr "Lagen groeperen" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.title.multiple" @@ -3859,6 +4147,10 @@ msgstr "Max.Hoogte" msgid "workspace.options.layout-item.layout-item-max-w" msgstr "Max.Breedte" +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.layout-item-min-h" +msgstr "Min.Hoogte" + #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs msgid "workspace.options.layout-item.layout-item-min-w" msgstr "Min.Breedte" @@ -3883,68 +4175,69 @@ msgstr "Minimum breedte" msgid "workspace.options.layout.bottom" msgstr "Onderkant" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column" +msgstr "Kolom" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column-reverse" +msgstr "Kolom omkeren" + #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.direction.row" msgstr "Rij" #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.direction.row-reverse" -msgstr "Omgekeerde rij" +msgstr "Rij omkeren" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.gap" +msgstr "Tussenruimte" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.left" +msgstr "Links" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin" +msgstr "Marge" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs msgid "workspace.options.layout.margin-all" msgstr "Alle kanten" +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin-simple" +msgstr "Enkelvoudige marge" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.packed" +msgstr "ingepakt" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding" +msgstr "Opvulling" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding-all" +msgstr "Alle kanten" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding-simple" +msgstr "Enkelvoudige opvulling" + #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.right" msgstr "Rechts" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.position" -msgstr "Positie" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.space-around" +msgstr "ruimte rondom" -#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs -msgid "workspace.options.shadow-options.offsety" -msgstr "Y" - -#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs -msgid "workspace.options.stroke-cap.round" -msgstr "Rond" - -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs -msgid "workspace.shape.menu.update-components-in-bulk" -msgstr "Hoofdcomponenten bijwerken" - -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs -msgid "workspace.shape.menu.paste" -msgstr "Plakken" - -msgid "workspace.shape.menu.restore-main" -msgstr "Hoofdcomponent herstellen" - -msgid "workspace.undo.entry.single.rect" -msgstr "rechthoek" - -msgid "workspace.undo.entry.single.shape" -msgstr "vorm" - -#: src/app/main/data/workspace/libraries.cljs -msgid "workspace.updates.dismiss" -msgstr "Afwijzen" - -msgid "workspace.undo.entry.single.text" -msgstr "tekst" - -msgid "workspace.undo.entry.single.typography" -msgstr "typografie" - -#: src/app/main/ui/workspace/sidebar/history.cljs -msgid "workspace.undo.entry.unknown" -msgstr "Voortgang van %s" - -#: src/app/main/ui/workspace/sidebar/history.cljs -msgid "workspace.undo.title" -msgstr "Geschiedenis" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.space-between" +msgstr "ruimte tussen" #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.top" @@ -3956,10 +4249,15 @@ msgstr "Meer kleuren" #: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs msgid "workspace.options.more-lib-colors" -msgstr "Meer bibliotheek kleuren" +msgstr "Meer bibliotheekkleuren" msgid "workspace.options.opacity" -msgstr "Doorzichtigheid" +msgstr "Dekking" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.position" +msgstr "Positie" #: src/app/main/ui/workspace/sidebar/options.cljs msgid "workspace.options.prototype" @@ -3968,6 +4266,22 @@ msgstr "Prototype" msgid "workspace.options.radius" msgstr "Radius" +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius-bottom-left" +msgstr "Onder links" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius-bottom-right" +msgstr "Onder rechts" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius-top-left" +msgstr "Boven links" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius-top-right" +msgstr "Boven rechts" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius.all-corners" msgstr "Alle hoeken" @@ -3979,7 +4293,8 @@ msgstr "Onafhankelijke hoeken" msgid "workspace.options.recent-fonts" msgstr "Recent" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" msgstr "Opnieuw proberen" @@ -3988,7 +4303,13 @@ msgid "workspace.options.rotation" msgstr "Rotatie" msgid "workspace.options.search-font" -msgstr "Zoek lettertype" +msgstr "Lettertype zoeken" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.select-a-shape" +msgstr "" +"Selecteer een vorm, bord of groep om d.m.v. slepen een verbinding met een " +"ander bord te maken." #: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs msgid "workspace.options.selection-color" @@ -4000,7 +4321,7 @@ msgstr "Selectie vullen" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.selection-stroke" -msgstr "Selectie lijn" +msgstr "Selectie-streek" #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.blur" @@ -4015,12 +4336,16 @@ msgstr "Slagschaduw" #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.inner-shadow" -msgstr "Innerlijke schaduw" +msgstr "Schaduw naar binnen" #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.offsetx" msgstr "X" +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.offsety" +msgstr "Y" + #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.spread" msgstr "Spreiding" @@ -4031,27 +4356,31 @@ msgstr "Schaduw" #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.title.group" -msgstr "Groepsschaduw" +msgstr "Groep voorzien van schaduw" #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.title.multiple" -msgstr "Selectie schaduwen" +msgstr "Selectie voorzien van schaduw" #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.show-fill-on-export" -msgstr "Meenemen in exports" +msgstr "In exports tonen" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.show-in-viewer" +msgstr "In weergavemodus tonen" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.size" -msgstr "Maat" +msgstr "Grootte" #: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs msgid "workspace.options.size-presets" -msgstr "Voorinstellingen voor afmetingen" +msgstr "Groottevoorinstellingen" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke" -msgstr "Lijn" +msgstr "Streek" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.circle-marker" @@ -4059,41 +4388,45 @@ msgstr "Cirkelmarkering" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.diamond-marker" -msgstr "Diamant marker" +msgstr "Ruitmarkering" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.line-arrow" -msgstr "Lijn pijl" +msgstr "Lijn-pijl" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.none" msgstr "Geen" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.round" +msgstr "Rond" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.square" msgstr "Vierkant" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.square-marker" -msgstr "Vierkante markering" +msgstr "Vierkantmarkering" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke-cap.triangle-arrow" -msgstr "Driehoek pijl" +msgstr "Driehoek-pijl" msgid "workspace.options.stroke-color" -msgstr "Lijnkleur" +msgstr "Streekkleur" msgid "workspace.options.stroke-width" -msgstr "Lijndikte" +msgstr "Streekdikte" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke.center" -msgstr "Center" +msgstr "Gecentreerd" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke.dashed" -msgstr "Onderbroken" +msgstr "Gestreept" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke.dotted" @@ -4117,27 +4450,11 @@ msgstr "Solide" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-bottom" -msgstr "Lijn beneden uit" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.align-center" -msgstr "Lijn gecentreerd uit" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.align-justify" -msgstr "Justify (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.align-left" -msgstr "Links uitlijnen (%s)" +msgstr "Onderaan uitlijnen" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Midden uitlijnen (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.align-right" -msgstr "Rechts uitlijnen (%s)" +msgstr "Midden uitlijnen" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -4169,19 +4486,36 @@ msgstr "Letterafstand" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.line-height" -msgstr "Lijnhoogte" +msgstr "Regelafstand" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.lowercase" msgstr "Kleine letters" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.none" msgstr "Geen" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Doorstreept (%s)" +msgstr "Doorhalen (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Centreren (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Uitvullen (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Links uitlijnen (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Rechts uitlijnen (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -4189,11 +4523,11 @@ msgstr "Tekst" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title-group" -msgstr "Groepstekst" +msgstr "Groeptekst" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title-selection" -msgstr "Selectie tekst" +msgstr "Selectietekst" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.titlecase" @@ -4201,11 +4535,16 @@ msgstr "Beginhoofdletters" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Onderstreept (%s)" +msgstr "Onderstrepen (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" -msgstr "Hoofdletters" +msgstr "HOOFDLETTERS" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.use-play-button" +msgstr "" +"Gebruik de afspeelknop in de koptekst om de prototypeweergave uit te voeren." msgid "workspace.options.width" msgstr "Breedte" @@ -4223,7 +4562,7 @@ msgid "workspace.path.actions.delete-node" msgstr "Knooppunt verwijderen (%s)" msgid "workspace.path.actions.draw-nodes" -msgstr "Teken knooppunten (%s)" +msgstr "Knooppunten tekenen (%s)" msgid "workspace.path.actions.join-nodes" msgstr "Knooppunten verbinden (%s)" @@ -4232,32 +4571,27 @@ msgid "workspace.path.actions.make-corner" msgstr "Naar hoek (%s)" msgid "workspace.path.actions.make-curve" -msgstr "Naar curve (%s)" +msgstr "Naar kromme (%s)" msgid "workspace.path.actions.merge-nodes" msgstr "Knooppunten samenvoegen (%s)" msgid "workspace.path.actions.move-nodes" -msgstr "Verplaats knooppunten (%s)" +msgstr "Knooppunten verplaatsen (%s)" msgid "workspace.path.actions.separate-nodes" -msgstr "Verschillende knooppunten (%s)" +msgstr "Knooppunten loskoppelen (%s)" msgid "workspace.path.actions.snap-nodes" -msgstr "Snap knooppunten (%s)" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "%s/%s converteren" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "%s bijwerken..." +msgstr "Uitlijnen op knooppunten (%s)" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.add-flex" msgstr "Flex-indeling toevoegen" +msgid "workspace.shape.menu.add-grid" +msgstr "Rasterindeling toevoegen" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "Naar achtergrond verplaatsen" @@ -4268,7 +4602,10 @@ msgstr "Naar achteren verplaatsen" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.copy" -msgstr "Kopieëren" +msgstr "Kopiëren" + +msgid "workspace.shape.menu.create-annotation" +msgstr "Aantekening maken" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.create-artboard-from-selection" @@ -4276,7 +4613,10 @@ msgstr "Selectie naar bord" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.create-component" -msgstr "Creëer component" +msgstr "Component aanmaken" + +msgid "workspace.shape.menu.create-multiple-components" +msgstr "Meerdere componenten aanmaken" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.cut" @@ -4288,13 +4628,17 @@ msgstr "Verwijderen" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.delete-flow-start" -msgstr "Verwijder flow-start" +msgstr "Stroomdiagram-start verwijderen" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Instantie losmaken" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instances-in-bulk" msgstr "Instanties losmaken" @@ -4317,23 +4661,28 @@ msgstr "Afvlakken" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.flip-horizontal" -msgstr "Draai horizontaal" +msgstr "Horizontaal spiegelen" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.flip-vertical" -msgstr "Draai verticaal" +msgstr "Verticaal spiegelen" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.flow-start" -msgstr "Flow-start" +msgstr "Stroomdiagram-start" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.forward" -msgstr "Breng naar voren" +msgstr "Naar voren brengen" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.front" -msgstr "Breng naar voorgrond" +msgstr "Naar voorgrond brengen" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.go-main" +msgstr "Ga naar hoofdcomponent" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.group" @@ -4344,41 +4693,58 @@ msgid "workspace.shape.menu.hide" msgstr "Verbergen" msgid "workspace.shape.menu.hide-ui" -msgstr "Toon/Verberg gebruikersinterface" +msgstr "Gebruikersinterface tonen/verbergen" msgid "workspace.shape.menu.intersection" msgstr "Kruispunt" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.lock" -msgstr "Vergrendel" +msgstr "Vergrendelen" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.mask" msgstr "Masker" +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.paste" +msgstr "Plakken" + msgid "workspace.shape.menu.path" msgstr "Pad" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.remove-flex" -msgstr "Verwijder flex-indeling" +msgstr "Flex-indeling verwijderen" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "Overschrijvingen opnieuw instellen" +msgid "workspace.shape.menu.restore-main" +msgstr "Hoofdcomponent herstellen" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.select-layer" -msgstr "Selecteer laag" +msgstr "Laag selecteren" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show" -msgstr "Toon" +msgstr "Tonen" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show-in-assets" +msgstr "In paneel Assets tonen" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-main" -msgstr "Toon hoofdcomponent" +msgstr "Hoofdcomponent tonen" msgid "workspace.shape.menu.thumbnail-remove" msgstr "Miniatuur verwijderen" @@ -4391,10 +4757,10 @@ msgstr "Transformeren naar pad" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.ungroup" -msgstr "Degroeperen" +msgstr "Groep opheffen" msgid "workspace.shape.menu.union" -msgstr "Verenig" +msgstr "Vereniging" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.unlock" @@ -4402,17 +4768,25 @@ msgstr "Ontgrendelen" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.unmask" -msgstr "Ontmaskeren" +msgstr "Masker wegnemen" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.update-components-in-bulk" +msgstr "Hoofdcomponenten bijwerken" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-main" msgstr "Hoofdcomponent bijwerken" msgid "workspace.sidebar.collapse" -msgstr "Zijbalk samenvouwen" +msgstr "Zijbalk inklappen" msgid "workspace.sidebar.expand" -msgstr "Vouw zijbalk uit" +msgstr "Zijbalk uitklappen" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.sidebar.history" @@ -4438,7 +4812,7 @@ msgid "workspace.sidebar.layers.masks" msgstr "Maskers" msgid "workspace.sidebar.layers.search" -msgstr "Zoek lagen" +msgstr "Lagen doorzoeken" msgid "workspace.sidebar.layers.shapes" msgstr "Vormen" @@ -4446,7 +4820,8 @@ msgstr "Vormen" msgid "workspace.sidebar.layers.texts" msgstr "Teksten" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/inspect/attributes/svg.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, +#: src/app/main/ui/inspect/attributes/svg.cljs msgid "workspace.sidebar.options.svg-attrs.title" msgstr "Geïmporteerde SVG-kenmerken" @@ -4460,11 +4835,11 @@ msgstr "Sitemap" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.assets" -msgstr "Middelen" +msgstr "Assets" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.color-palette" -msgstr "Kleurpalet (%s)" +msgstr "Kleurenpalet (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.comments" @@ -4472,11 +4847,11 @@ msgstr "Commentaar (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.curve" -msgstr "Curve (%s)" +msgstr "Kromme (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.ellipse" -msgstr "Ovaal (%s)" +msgstr "Ellips (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.frame" @@ -4500,7 +4875,7 @@ msgstr "Rechthoek (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.shortcuts" -msgstr "Snelkoppelingen (%s)" +msgstr "Sneltoetsen (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.text" @@ -4508,7 +4883,11 @@ msgstr "Tekst (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.text-palette" -msgstr "Typografieën (%s)" +msgstr "Typografie (%s)" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.empty" +msgstr "Er zijn tot nu toe geen wijzigingen in de geschiedenis" #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.entry.delete" @@ -4526,13 +4905,13 @@ msgid "workspace.undo.entry.multiple.circle" msgstr "cirkels" msgid "workspace.undo.entry.multiple.color" -msgstr "kleuren" +msgstr "kleur assets" msgid "workspace.undo.entry.multiple.component" msgstr "componenten" msgid "workspace.undo.entry.multiple.curve" -msgstr "curves" +msgstr "krommen" msgid "workspace.undo.entry.multiple.frame" msgstr "bord" @@ -4541,7 +4920,7 @@ msgid "workspace.undo.entry.multiple.group" msgstr "groepen" msgid "workspace.undo.entry.multiple.media" -msgstr "grafische middelen" +msgstr "grafische assets" msgid "workspace.undo.entry.multiple.multiple" msgstr "objecten" @@ -4562,7 +4941,7 @@ msgid "workspace.undo.entry.multiple.text" msgstr "teksten" msgid "workspace.undo.entry.multiple.typography" -msgstr "typografie middelen" +msgstr "typografie-assets" #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.entry.new" @@ -4572,13 +4951,13 @@ msgid "workspace.undo.entry.single.circle" msgstr "cirkel" msgid "workspace.undo.entry.single.color" -msgstr "kleur" +msgstr "kleur asset" msgid "workspace.undo.entry.single.component" msgstr "component" msgid "workspace.undo.entry.single.curve" -msgstr "curve" +msgstr "kromme" msgid "workspace.undo.entry.single.frame" msgstr "bord" @@ -4590,7 +4969,7 @@ msgid "workspace.undo.entry.single.image" msgstr "afbeelding" msgid "workspace.undo.entry.single.media" -msgstr "grafisch middel" +msgstr "grafisch asset" msgid "workspace.undo.entry.single.multiple" msgstr "object" @@ -4601,34 +4980,191 @@ msgstr "pagina" msgid "workspace.undo.entry.single.path" msgstr "pad" +msgid "workspace.undo.entry.single.rect" +msgstr "rechthoek" + +msgid "workspace.undo.entry.single.shape" +msgstr "vorm" + +msgid "workspace.undo.entry.single.text" +msgstr "tekst" + +msgid "workspace.undo.entry.single.typography" +msgstr "typografie asset" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.unknown" +msgstr "Voortgang van %s" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.title" +msgstr "Geschiedenis" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.dismiss" +msgstr "Afwijzen" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "Meer info" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.there-are-updates" +msgstr "Er zijn updates in gedeelde bibliotheken" + #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.update" -msgstr "Bewerken" +msgstr "Bijwerken" -#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs -msgid "workspace.options.layout.margin-simple" -msgstr "Eenvoudige marge" +msgid "workspace.viewport.click-to-close-path" +msgstr "Klik om het pad te sluiten" -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.packed" -msgstr "ingepakt" +msgid "workspace.options.component.copy" +msgstr "Kopiëren" -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.padding" -msgstr "Padding" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "Rechthoek" -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.padding-all" -msgstr "Alle kanten" +msgid "workspace.options.component.main" +msgstr "Hoofd" -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.padding-simple" -msgstr "Eenvoudige opvulling" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "Ruit" -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.space-around" -msgstr "ruimte rondom" +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +msgid "settings.detach" +msgstr "Ontkoppelen" -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.space-between" -msgstr "ruimte tussen" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "Driehoek" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "Pijl" + +msgid "modals.add-shared-confirm-empty.hint" +msgstr "" +"Je bibliotheek is leeg. Eenmaal toegevoegd als Gedeelde Bibliotheek, zijn de " +"assets die je aanmaakt beschikbaar voor gebruik in de rest van je bestanden. " +"Weet je zeker dat je dit wilt publiceren??" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "Cirkel" + +msgid "onboarding.choice.team-up.continue-creating-team" +msgstr "Doorgaan met team aanmaken" + +msgid "workspace.options.guides.title" +msgstr "Hulplijnen" + +msgid "media.choose-image" +msgstr "Afbeelding kiezen" + +#: src/app/main/ui/auth/register.cljs +#, markdown +msgid "auth.terms-privacy-agreement-md" +msgstr "" +"Met het aanmaken van een nieuw account ga je akkoord met onze " +"[servicevoorwaarden] (%s) en [privacybeleid] (%s)." + +msgid "workspace.options.component.swap.empty" +msgstr "Er zijn nog geen assets in deze bibliotheek" + +msgid "media.solid" +msgstr "Solide" + +msgid "workspace.top-bar.read-only.done" +msgstr "Klaar" + +msgid "onboarding.choice.team-up.create-team-and-invite" +msgstr "Team aanmaken & uitnodigen" + +msgid "workspace.layout_grid.editor.top-bar.done" +msgstr "Klaar" + +msgid "onboarding.choice.team-up.continue-without-a-team" +msgstr "Doorgaan zonder team" + +msgid "inspect.attributes.typography.text-transform.unset" +msgstr "Uitschakelen" + +msgid "errors.validation" +msgstr "Validatiefout" + +msgid "workspace.layout_grid.editor.options.exit" +msgstr "Sluiten" + +#: src/app/main/errors.cljs +msgid "errors.version-not-supported" +msgstr "Bestand heeft een incompatibel versienummer" + +msgid "workspace.options.component.swap" +msgstr "Component uitwisselen" + +msgid "onboarding.choice.team-up.create-team-without-inviting" +msgstr "Team aanmaken zonder uitnodigingen" + +#: src/app/main/errors.cljs +msgid "errors.file-feature-mismatch" +msgstr "" +"Het lijkt erop dat er een discrepantie bestaat tussen de ingeschakelde " +"functies en de functies van het bestand dat je probeert te openen. Er moeten " +"migraties voor '%s' worden toegepast voordat het bestand kan worden geopend." + +msgid "onboarding.choice.team-up.create-team-without-invite" +msgstr "Team aanmaken" + +msgid "media.linear" +msgstr "Lineair" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow" +msgstr "Stroomdiagram" + +msgid "labels.search" +msgstr "Zoeken" + +msgid "media.image" +msgstr "Afbeelding" + +msgid "onboarding.choice.team-up.start-without-a-team-description" +msgstr "Je kunt later een team samenstellen." + +msgid "onboarding.choice.team-up.create-team-and-send-invites" +msgstr "Team aanmaken en uitnodigingen versturen" + +msgid "workspace.layout_grid.editor.top-bar.locate" +msgstr "Lokaliseren" + +msgid "media.gradient" +msgstr "Verloop" + +msgid "labels.share" +msgstr "Delen" + +msgid "workspace.layout_grid.editor.options.edit-grid" +msgstr "Raster bewerken" + +msgid "onboarding.choice.team-up.start-without-a-team" +msgstr "Zonder team beginnen" + +msgid "onboarding.choice.team-up.create-team-and-send-invites-description" +msgstr "Je kunt later uitnodigen" + +msgid "media.radial" +msgstr "Radiaal" + +msgid "errors.paste-data-validation" +msgstr "Ongeldige gegevens op klembord" + +#: src/app/main/errors.cljs +msgid "errors.team-feature-mismatch" +msgstr "Incompatibele functie '%s' gedetecteerd" + +#, markdown +msgid "workspace.top-bar.read-only" +msgstr "** Inspectiemodus ** (alleen bekijken)" diff --git a/frontend/translations/pl.po b/frontend/translations/pl.po index c75da91705..839c342182 100644 --- a/frontend/translations/pl.po +++ b/frontend/translations/pl.po @@ -915,7 +915,7 @@ msgstr "Email" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Przejdź do Twittera" +msgstr "Przejdź do Xa" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -923,7 +923,7 @@ msgstr "Służymy pomocą w kwestiach technicznych." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Konto wsparcia na Twitterze" +msgstr "Konto wsparcia na Xze" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -1716,32 +1716,6 @@ msgstr[0] "Usuń plik" msgstr[1] "Usuń pliki" msgstr[2] "Usuń pliki" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "" -"Jeśli go usuniesz, te zasoby zostaną przeniesione do lokalnej biblioteki " -"tego pliku. Wszelkie niewykorzystane zasoby zostaną utracone." -msgstr[1] "" -"Jeśli je usuniesz, te zasoby zostaną przeniesione do lokalnej biblioteki " -"tego pliku. Wszelkie niewykorzystane zasoby zostaną utracone." -msgstr[2] "" -"Jeśli je usuniesz, te zasoby zostaną przeniesione do lokalnej biblioteki " -"tego pliku. Wszelkie niewykorzystane zasoby zostaną utracone." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "" -"Jeśli go usuniesz, te zasoby zostaną przeniesione do lokalnych bibliotek " -"tych plików. Wszelkie niewykorzystane zasoby zostaną utracone." -msgstr[1] "" -"Jeśli je usuniesz, te zasoby zostaną przeniesione do lokalnych bibliotek " -"tych plików. Wszelkie niewykorzystane zasoby zostaną utracone." -msgstr[2] "" -"Jeśli je usuniesz, te zasoby zostaną przeniesione do lokalnych bibliotek " -"tych plików. Wszelkie niewykorzystane zasoby zostaną utracone." - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" @@ -1749,33 +1723,6 @@ msgstr[0] "Czy na pewno chcesz usunąć ten plik?" msgstr[1] "Czy na pewno chcesz usunąć te pliki?" msgstr[2] "Czy na pewno chcesz usunąć te pliki?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" -msgstr[0] "" -"Żaden z zasobów w bibliotece tego pliku nie jest używany. Zostaną one " -"usunięte wraz z plikiem." -msgstr[1] "" -"Żaden z zasobów w bibliotekach tych plików nie jest używany. Zostaną one " -"usunięte wraz z plikami." -msgstr[2] "" -"Żaden z zasobów w bibliotekach tych plików nie jest używany. Zostaną one " -"usunięte wraz z plikami." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Niektóre zasoby z biblioteki tego pliku są tutaj używane:" -msgstr[1] "Niektóre zasoby z biblioteki tych plików są tutaj używane:" -msgstr[2] "Niektóre zasoby z biblioteki tych plików są tutaj używane:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "Niektóre zasoby z biblioteki tego pliku są tutaj używane:" -msgstr[1] "Niektóre zasoby w bibliotekach tych plików są tutaj używane:" -msgstr[2] "Niektóre zasoby w bibliotekach tych plików są tutaj używane:" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" @@ -1929,32 +1876,6 @@ msgstr[0] "Cofnij publikację" msgstr[1] "Cofnij publikacje" msgstr[2] "Cofnij publikacje" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Jeśli cofniesz publikację, te zasoby zostaną przeniesione do lokalnej " -"biblioteki tego pliku." -msgstr[1] "" -"Jeśli cofniesz ich publikację, te zasoby zostaną przeniesione do lokalnej " -"biblioteki tego pliku." -msgstr[2] "" -"Jeśli cofniesz ich publikację, te zasoby zostaną przeniesione do lokalnej " -"biblioteki tego pliku." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "" -"Jeśli cofniesz publikację, te zasoby zostaną przeniesione do lokalnych " -"bibliotek tych plików." -msgstr[1] "" -"Jeśli cofniesz ich publikację, te zasoby zostaną przeniesione do lokalnych " -"bibliotek tych plików." -msgstr[2] "" -"Jeśli cofniesz ich publikację, te zasoby zostaną przeniesione do lokalnych " -"bibliotek tych plików." - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" msgid_plural "modals.unpublish-shared-confirm.message" @@ -1962,27 +1883,6 @@ msgstr[0] "Czy na pewno chcesz cofnąć publikację tej biblioteki?" msgstr[1] "Czy na pewno chcesz cofnąć publikację tych bibliotek?" msgstr[2] "Czy na pewno chcesz cofnąć publikację tych bibliotek?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "Żaden z zasobów w tej bibliotece nie jest używany." -msgstr[1] "Żaden z zasobów w tych bibliotekach nie jest używany." -msgstr[2] "Żaden z zasobów w tych bibliotekach nie jest używany." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Niektóre zasoby z tej biblioteki są tutaj używane:" -msgstr[1] "Niektóre zasoby w tych bibliotekach są tutaj używane:" -msgstr[2] "Niektóre zasoby w tych bibliotekach są tutaj używane:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "Niektóre zasoby z tej biblioteki są tutaj używane:" -msgstr[1] "Niektóre zasoby w tych bibliotekach są tutaj używane:" -msgstr[2] "Niektóre zasoby w tych bibliotekach są tutaj używane:" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" msgid_plural "modals.unpublish-shared-confirm.title" @@ -2111,12 +2011,6 @@ msgstr "Przewodnik współtworzenia" msgid "onboarding-v2.welcome.title" msgstr "Witamy w Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "Utwórz zespół później" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Nazwa twojego zespołu" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "Po nazwaniu swojego zespołu będziesz mógł zapraszać osoby do dołączenia." @@ -2131,12 +2025,6 @@ msgstr "" "Pamiętaj, aby uwzględnić wszystkich. Deweloperzy, projektanci, " "managerowie... różnorodność się sumuje :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Utwórz zespół i zaproś później" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Utwórz zespół i wyślij zaproszenia" - msgid "onboarding.choice.team-up.roles" msgstr "Zaproś z rolą:" @@ -2154,9 +2042,6 @@ msgstr "Polityka prywatności." msgid "onboarding.newsletter.title" msgstr "Chcesz otrzymywać informacje o Penpot?" -msgid "onboarding.slide.1.title" -msgstr "Ożyw swoje projekty dzięki interakcjom" - msgid "onboarding.team-modal.create-team" msgstr "Utwórz zespół" @@ -2566,9 +2451,6 @@ msgstr "Przełącz tryb skupienia" msgid "shortcuts.toggle-fullscreen" msgstr "Przełącz tryb pełnoekranowy" -msgid "shortcuts.toggle-grid" -msgstr "Pokaż/ukryj siatkę" - msgid "shortcuts.toggle-history" msgstr "Przełącz historię" @@ -2587,15 +2469,6 @@ msgstr "Zablokuj proporcje" msgid "shortcuts.toggle-rules" msgstr "Pokaż/ukryj linijki" -msgid "shortcuts.toggle-scale-text" -msgstr "Przełącz skalowanie tekstu" - -msgid "shortcuts.toggle-snap-grid" -msgstr "Przyciągaj do siatki" - -msgid "shortcuts.toggle-snap-guide" -msgstr "Przyciągaj do prowadnic" - msgid "shortcuts.toggle-textpalette" msgstr "Przełącz paletę tekstu" @@ -2852,10 +2725,6 @@ msgstr[0] "Wybrano %s element" msgstr[1] "Wybrano %s elementy" msgstr[2] "Wybrano %s elementów" -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "UDOSTĘPNIONE" - #: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "Typografia" @@ -2927,10 +2796,6 @@ msgstr "Wyłącz wyrównanie dynamiczne" msgid "workspace.header.menu.disable-scale-text" msgstr "Wyłącz skalowanie tekstu" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Wyłącz przyciąganie do siatki" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "Wyłącz przyciąganie do prowadnic" @@ -2946,10 +2811,6 @@ msgstr "Włącz dynamiczne wyrównanie" msgid "workspace.header.menu.enable-scale-text" msgstr "Włącz skalowanie tekstu" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Przyciągaj do siatki" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "Przyciągaj do prowadnic" @@ -2961,10 +2822,6 @@ msgstr "Włącz przyciąganie do piksela" msgid "workspace.header.menu.hide-artboard-names" msgstr "Ukryj nazwy obszarów kompozycji" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Ukryj siatki" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Ukryj paletę kolorów" @@ -3008,10 +2865,6 @@ msgstr "Zaznacz wszystko" msgid "workspace.header.menu.show-artboard-names" msgstr "Pokaz nazwy obszarów kompozycji" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Pokaż siatkę" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Pokaż paletę kolorów" @@ -3725,10 +3578,6 @@ msgstr "Dół" msgid "workspace.options.layout.direction.column" msgstr "Kolumna" -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.reverse-row" -msgstr "Odwróć wiersze" - #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.direction.row" msgstr "Wiersz" @@ -3808,12 +3657,12 @@ msgid "workspace.options.radius" msgstr "Promień" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Wszystkie rogi" +msgid "workspace.options.radius-bottom-left" +msgstr "Dolne lewo" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Poszczególne narożniki" +msgid "workspace.options.radius-bottom-right" +msgstr "Dolne prawo" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3824,12 +3673,12 @@ msgid "workspace.options.radius-top-right" msgstr "Górne prawo" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Dolne lewo" +msgid "workspace.options.radius.all-corners" +msgstr "Wszystkie rogi" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Dolne prawo" +msgid "workspace.options.radius.single-corners" +msgstr "Poszczególne narożniki" msgid "workspace.options.recent-fonts" msgstr "Bieżące" @@ -3991,26 +3840,10 @@ msgstr "Ciągły" msgid "workspace.options.text-options.align-bottom" msgstr "Wyrównaj do dołu" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Wyrównaj do środka (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Wyjustuj (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Wyrównaj do lewej (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Wyrównaj do środka" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Wyrównaj do prawej (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Wyrównaj do góry" @@ -4055,6 +3888,22 @@ msgstr "Brak" msgid "workspace.options.text-options.strikethrough" msgstr "Przekreślenie (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Wyrównaj do środka (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Wyjustuj (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Wyrównaj do lewej (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Wyrównaj do prawej (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Tekst" @@ -4122,35 +3971,6 @@ msgstr "Rozłącz węzły (%s)" msgid "workspace.path.actions.snap-nodes" msgstr "Przyciągnij węzły (%s)" -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "" -"Aby spróbować ponownie, możesz ponownie załadować ten plik. Jeśli problem " -"będzie się powtarzał, sugerujemy przejrzenie listy i rozważenie usunięcia " -"uszkodzonej grafiki." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "Niektórych grafik nie udało się zaktualizować." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "Konwersja %s/%s" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "" -"Grafika biblioteczna jest od teraz komponentami, co sprawi, że będą " -"znacznie potężniejsze." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "Ta aktualizacja jest działaniem jednorazowym." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "Aktualizowanie %s..." - #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.add-flex" msgstr "Dodaj układ flex" diff --git a/frontend/translations/pt_BR.po b/frontend/translations/pt_BR.po index 4b7a6beba0..9610c9f4fc 100644 --- a/frontend/translations/pt_BR.po +++ b/frontend/translations/pt_BR.po @@ -914,15 +914,15 @@ msgstr "E-mail" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Ir ao Twitter" +msgstr "Ir ao X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" -msgstr "Precisa de ajuda com dúvidas mais técnicas? Veja o nosso Twitter." +msgstr "Precisa de ajuda com dúvidas mais técnicas? Veja o nosso X." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Conta de suporte no Twitter" +msgstr "Conta de suporte no X" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -1712,54 +1712,12 @@ msgid_plural "modals.delete-shared-confirm.accept" msgstr[0] "Excluir arquivo" msgstr[1] "Excluir arquivos" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "" -"Se você deletar, este item será movido para a biblioteca local deste " -"arquivo. Itens não utilizados serão perdidos." -msgstr[1] "" -"Se você deletar, estes itens serão movidos para a biblioteca local deste " -"arquivo. Itens não utilizados serão perdidos." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "" -"Se você deletar, este item será movido para as bibliotecas locais deste " -"arquivo. Itens não utilizados serão perdidos." -msgstr[1] "" -"Se você deletar, estes itens serão movidos para as bibliotecas locais deste " -"arquivo. Itens não utilizados serão perdidos." - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "Tem certeza de que deseja excluir este arquivo?" msgstr[1] "Tem certeza de que deseja excluir estes arquivos?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" -msgstr[0] "" -"Nenhum item da biblioteca deste arquivo está em uso. Eles serão deletados " -"junto com o arquivo." -msgstr[1] "" -"Nenhum item das bibliotecas destes arquivos estão em uso. Eles serão " -"deletados junto com os arquivos." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Alguns itens da biblioteca deste arquivo estão sendo usados aqui:" -msgstr[1] "Alguns itens das bibliotecas deste arquivo estão sendo usados aqui:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "Alguns itens da biblioteca deste arquivo estão sendo usados aqui:" -msgstr[1] "Alguns itens das bibliotecas deste arquivo estão sendo usados aqui:" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" @@ -1910,50 +1868,12 @@ msgid_plural "modals.unpublish-shared-confirm.accept" msgstr[0] "Despublicar" msgstr[1] "Despublicar" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Se você cancelar a publicação, os recursos serão movidos para a biblioteca " -"desse arquivo." -msgstr[1] "" -"Se você cancelar as publicações, os recursos serão movidos para a " -"biblioteca desse arquivo." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "" -"Se você cancelar a publicação deste item, os componentes dele serão movidos " -"para a biblioteca local deste arquivo." -msgstr[1] "" -"Se você cancelar a publicação destes itens, os componentes dele serão " -"movidos para a biblioteca local destes arquivos." - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "Tem certeza de que deseja cancelar a publicação desta biblioteca?" msgstr[1] "Tem certeza de que deseja cancelar a publicação dessas bibliotecas?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "Nenhum dos itens desta biblioteca está em uso." -msgstr[1] "Nenhum dos itens destas bibliotecas estão em uso." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Alguns dos recursos dessa biblioteca estão em uso aqui:" -msgstr[1] "Alguns dos recursos dessas bibliotecas estão em uso aqui:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "Alguns itens nesta biblioteca estão em uso aqui:" -msgstr[1] "Alguns itens nestas bibliotecas estão em uso aqui:" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" msgid_plural "modals.unpublish-shared-confirm.title" @@ -2082,12 +2002,6 @@ msgstr "Guia do contribuidor" msgid "onboarding-v2.welcome.title" msgstr "Bem-vindo ao Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "Criar uma equipe depois" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Nome da sua equipe" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "Depois de nomear sua equipe, você poderá convidar pessoas para participar." @@ -2102,12 +2016,6 @@ msgstr "" "Lembre-se de incluir todos. Desenvolvedores, designers, gerentes... a " "diversidade soma :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Crie a equipe e convide membros depois" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Crie uma equipe e envie convites" - msgid "onboarding.choice.team-up.roles" msgstr "Convide com a função:" @@ -2534,9 +2442,6 @@ msgstr "Entrar/Sair do modo de foco" msgid "shortcuts.toggle-fullscreen" msgstr "Entrar/Sair da tela cheia" -msgid "shortcuts.toggle-grid" -msgstr "Mostrar/Esconder grade" - msgid "shortcuts.toggle-history" msgstr "Mostrar/Esconder histórico" @@ -2555,15 +2460,6 @@ msgstr "Fixar proporções" msgid "shortcuts.toggle-rules" msgstr "Mostrar/ocultar réguas" -msgid "shortcuts.toggle-scale-text" -msgstr "Alternar escalonamento de texto" - -msgid "shortcuts.toggle-snap-grid" -msgstr "Aderir a grade" - -msgid "shortcuts.toggle-snap-guide" -msgstr "Aderir as réguas" - msgid "shortcuts.toggle-textpalette" msgstr "Mostrar/Esconder paleta de tipografias" @@ -2819,10 +2715,6 @@ msgid_plural "workspace.assets.selected-count" msgstr[0] "%s item selecionado" msgstr[1] "%s itens selecionados" -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "Compartilhados" - #: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "Tipografias" @@ -2894,10 +2786,6 @@ msgstr "Desabilitar alinhamento dinâmico" msgid "workspace.header.menu.disable-scale-text" msgstr "Desativar escalonamento de texto" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Desativar aderência a grade" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "Desativar aderência as réguas" @@ -2913,10 +2801,6 @@ msgstr "Habilitar alinhamento dinâmico" msgid "workspace.header.menu.enable-scale-text" msgstr "Ativar escalonamento de texto" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Aderir a grade de pixels" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "Aderir as réguas" @@ -2928,10 +2812,6 @@ msgstr "Habilitar aderência a grade de pixels" msgid "workspace.header.menu.hide-artboard-names" msgstr "Esconder nomes das telas" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Ocultar grades" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Esconder paleta de cores" @@ -2975,10 +2855,6 @@ msgstr "Selecionar tudo" msgid "workspace.header.menu.show-artboard-names" msgstr "Mostrar nomes das telas" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Mostrar grade" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Mostrar paleta de cores" @@ -3685,10 +3561,6 @@ msgstr "Inferior" msgid "workspace.options.layout.direction.column" msgstr "Coluna" -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.reverse-row" -msgstr "Linha invertida" - #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.direction.row" msgstr "Linha" @@ -3768,12 +3640,12 @@ msgid "workspace.options.radius" msgstr "Raio" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Todos os cantos" +msgid "workspace.options.radius-bottom-left" +msgstr "Inferior esquerdo" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Cantos individuais" +msgid "workspace.options.radius-bottom-right" +msgstr "Inferior direito" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3784,12 +3656,12 @@ msgid "workspace.options.radius-top-right" msgstr "Superior (a direita)" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Inferior esquerdo" +msgid "workspace.options.radius.all-corners" +msgstr "Todos os cantos" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Inferior direito" +msgid "workspace.options.radius.single-corners" +msgstr "Cantos individuais" msgid "workspace.options.recent-fonts" msgstr "Recente" @@ -3951,26 +3823,10 @@ msgstr "Sólido" msgid "workspace.options.text-options.align-bottom" msgstr "Alinhar a base" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Alinhar ao centro (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Justificar (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Alinhar a esquerda (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Alinhar no meio" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Alinhar a direita (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Alinhar ao topo" @@ -4015,6 +3871,22 @@ msgstr "Nenhum" msgid "workspace.options.text-options.strikethrough" msgstr "Rasurado (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Alinhar ao centro (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Justificar (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Alinhar a esquerda (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Alinhar a direita (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Texto" @@ -4082,35 +3954,6 @@ msgstr "Separar pontos (%s)" msgid "workspace.path.actions.snap-nodes" msgstr "Aderir aos pontos (%s)" -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "" -"Para tentar novamente, recarregue este arquivo. Se o problema persistir, " -"sugerimos olhar a lista e considerar excluir gráficos que não estejam " -"funcionando." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "Alguns gráficos não puderam ser atualizados." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "Convertendo %s/%s" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "" -"A partir de agora os gráficos da biblioteca são Componentes, o que os " -"tornarão bem mais poderosos." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "Essa atualização acontecerá apenas uma vez." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "Atualizando %s..." - #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.add-flex" msgstr "Adicionar Flex Layout" diff --git a/frontend/translations/pt_PT.po b/frontend/translations/pt_PT.po index 707f4d1613..3bd8735be2 100644 --- a/frontend/translations/pt_PT.po +++ b/frontend/translations/pt_PT.po @@ -1,15 +1,15 @@ msgid "" msgstr "" -"PO-Revision-Date: 2022-12-28 23:47+0000\n" -"Last-Translator: Fernando Krik \n" -"Language-Team: Portuguese (Portugal) " -"\n" +"PO-Revision-Date: 2023-12-29 21:08+0000\n" +"Last-Translator: TheScientistPT \n" +"Language-Team: Portuguese (Portugal) \n" "Language: pt_PT\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.15.1-dev\n" +"X-Generator: Weblate 5.4-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -39,7 +39,8 @@ msgstr "" "Este é um serviço de DEMONSTRAÇÃO, NÃO UTILIZES para trabalhos reais. Os " "projetos serão eliminados periodicamente." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" msgstr "E-mail" @@ -83,6 +84,14 @@ msgstr "LDAP" msgid "auth.login-with-oidc-submit" msgstr "OpenID Connect" +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "O nome deve conter pelo menos um caractere que não seja um espaço." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "O nome deve conter um máximo de 250 caracteres." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.new-password" msgstr "Escreve uma nova palavra-passe" @@ -115,6 +124,10 @@ msgstr "Palavra-passe" msgid "auth.password-length-hint" msgstr "Mínimo de 8 caracteres" +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "A palavra-passe deve conter pelo menos um caractere que não seja um espaço." + msgid "auth.privacy-policy" msgstr "Política de privacidade" @@ -152,7 +165,7 @@ msgstr "Cria uma conta" #: src/app/main/ui/auth.cljs msgid "auth.sidebar-tagline" -msgstr "A solução código aberto para design e prototipar." +msgstr "A solução de código aberto para design e prototipagem." msgid "auth.terms-of-service" msgstr "Termos de serviço" @@ -167,6 +180,10 @@ msgstr "" msgid "auth.verification-email-sent" msgstr "Enviámos um email de verificação para" +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "...branding, ilustrações, artigos de marketing, etc." + msgid "common.publish" msgstr "Publicar" @@ -263,7 +280,83 @@ msgstr "Inicia a tour" msgid "dasboard.walkthrough-hero.title" msgstr "Passo a passo na interface" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "Token copiado" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "Gerar novo token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "Token de acesso criado com sucesso." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "Clica no botão \"Gerar novo token\" para gerar um." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "Ainda não tens nenhum token." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "O nome é obrigatório" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 dias" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 dias" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 dias" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 dias" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "Nunca" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "Expirou a %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "Expira a %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "Sem data de expiração" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Tokens de acesso pessoais" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Os tokens de acesso pessoais funcionam como uma alternativa ao nosso sistema " +"de autenticação de login/palavra-passe e podem ser usados para permitir que " +"uma aplicação tenha acesso à API interna do Penpot" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "O token irá expirar a %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "O token não tem data de expiração" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Adicionar como biblioteca partilhada" @@ -293,7 +386,8 @@ msgstr "Descarregar ficheiro Penpot (.penpot)" msgid "dashboard.download-standard-file" msgstr "Descarregar ficheiro standard (svg + json)" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate" msgstr "Duplicar" @@ -302,18 +396,17 @@ msgid "dashboard.duplicate-multi" msgstr "Duplicar %s ficheiros" #: src/app/main/ui/dashboard/grid.cljs -#, markdown msgid "dashboard.empty-placeholder-drafts" msgstr "" "Oh não! Ainda não tens ficheiros! Se quiseres experimentar podes começar " "com os nossos templates em [Libraries & " -"templates](https://penpot.app/libraries-templates.html)" +"templates](https://penpot.app/libraries-templates.html)." msgid "dashboard.export-binary-multi" msgstr "Descarrega %s ficheiros Penpot (.penpot)" msgid "dashboard.export-frames" -msgstr "Exportar pranchetas para PDF…" +msgstr "Exportar pranchetas para PDF" #: src/app/main/ui/export.cljs msgid "dashboard.export-frames.title" @@ -402,7 +495,6 @@ msgid_plural "dashboard.fonts.fonts-added" msgstr[0] "1 tipo de letra adicionado" msgstr[1] "%s tipos de letra adicionados" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "Qualquer web font que carregares aqui será adicionada à família de fontes " @@ -411,7 +503,6 @@ msgstr "" "carregar tipos de letra com os seguintes formatos: **TTF, OTF e WOFF** " "(apenas um será necessário)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "Deves carregar tipos de letra que possuas or tenhas licença para utilizar " @@ -423,6 +514,15 @@ msgstr "" msgid "dashboard.fonts.upload-all" msgstr "Carregar tudo" +#, markdown +msgid "dashboard.fonts.warning-text" +msgstr "" +"Detetámos um possível problema nas tuas fontes relacionado com métricas " +"verticais em sistemas operativos diferentes. Para verificá-lo, podes " +"utilizar serviços como [este](https://vertical-metrics.netlify.app/). Para " +"além disso, recomendamos o uso do [Transfonter](https://transfonter.org/) " +"para gerar fontes web e corrigir erros. " + msgid "dashboard.import" msgstr "Importar ficheiros Penpot" @@ -432,9 +532,6 @@ msgstr "Oops! Não conseguimos importar este ficheiro" msgid "dashboard.import.import-error" msgstr "Ocorreu um problema na importação do ficheiro. O ficheiro não foi importado." -msgid "dashboard.import.import-message" -msgstr "%s ficheiros importados com sucesso." - msgid "dashboard.import.import-warning" msgstr "Alguns ficheiros continham objetos inválidos que foram removidos." @@ -463,7 +560,8 @@ msgstr "A carregar ficheiro: %s" msgid "dashboard.invite-profile" msgstr "Convidar para a equipa" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" msgstr "Sair da equipa" @@ -489,7 +587,8 @@ msgstr "a carregar os teus ficheiros …" msgid "dashboard.loading-fonts" msgstr "a carregar as tuas fontes …" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "Mover para" @@ -501,7 +600,8 @@ msgstr "Mover %s ficheiros para" msgid "dashboard.move-to-other-team" msgstr "Mover para outra equipa" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" msgstr "+ Novo Ficheiro" @@ -523,7 +623,7 @@ msgstr "Não há resultados para \"%s\"" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.no-projects-placeholder" -msgstr "Projetos fixos aparecerão aqui" +msgstr "Projetos afixados aparecerão aqui" #: src/app/main/ui/auth/verify_token.cljs msgid "dashboard.notifications.email-changed-successfully" @@ -554,7 +654,7 @@ msgstr "Alterar palavra-passe" #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.pin-unpin" -msgstr "Fixar/Desafixar" +msgstr "Afixar/Desafixar" #: src/app/main/ui/dashboard/projects.cljs msgid "dashboard.projects-title" @@ -564,7 +664,8 @@ msgstr "Projetos" msgid "dashboard.remove-account" msgstr "Queres remover a tua conta?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" msgstr "Remover como Biblioteca Partilhada" @@ -592,23 +693,16 @@ msgstr "Selecionar tema" msgid "dashboard.show-all-files" msgstr "Mostrar todos os ficheiros" -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-delete-file" -msgstr "O teu ficheiro foi removido com sucesso" - #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-delete-project" msgstr "O teu projeto foi eliminado com sucesso" -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-duplicate-file" -msgstr "O teu ficheiro foi duplicado com sucesso" - #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-duplicate-project" msgstr "O teu projeto foi duplicado com sucesso" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-file" msgstr "O teu ficheiro foi movido com sucesso" @@ -644,14 +738,46 @@ msgstr "Resultados da pesquisa" msgid "dashboard.type-something" msgstr "Escreve para pesquisar resultados" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.unpublish-shared" msgstr "Cancelar publicação da Biblioteca" -#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Atualizar definições" +msgid "dashboard.webhooks.active" +msgstr "Ativo" + +msgid "dashboard.webhooks.active.explain" +msgstr "Quando este webhook for ativado serão enviados detalhes do evento" + +msgid "dashboard.webhooks.content-type" +msgstr "Tipo de conteúdo" + +msgid "dashboard.webhooks.create" +msgstr "Criar webhook" + +msgid "dashboard.webhooks.create.success" +msgstr "Webhook criado com sucesso." + +msgid "dashboard.webhooks.description" +msgstr "" +"Os webhooks são uma forma simples de permitir a outros sites e aplicações " +"serem notificados quando acontecem certas ações no Penpot. Enviaremos um " +"pedido POST para cada um dos URLs que forneceres." + +msgid "dashboard.webhooks.empty.add-one" +msgstr "Clica no botão \"Criar webhook\" para adicionar um." + +msgid "dashboard.webhooks.empty.no-webhooks" +msgstr "Ainda não há nenhum webhook criado." + +msgid "dashboard.webhooks.update.success" +msgstr "Webhook atualizado com sucesso." + #: src/app/main/ui/settings.cljs msgid "dashboard.your-account-title" msgstr "A tua conta" @@ -664,7 +790,11 @@ msgstr "E-mail" msgid "dashboard.your-name" msgstr "O teu nome" -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" msgstr "O teu Penpot" @@ -699,18 +829,21 @@ msgstr "Provedor de autenticação não configurado." msgid "errors.auth.unable-to-login" msgstr "Parece que não estás autenticado ou a sessão expirou." -#, fuzzy msgid "errors.bad-font" msgstr "A fonte %s não pôde ser carregada" msgid "errors.bad-font-plural" -msgstr "A fonte %s não pôde ser carregada" +msgstr "As fontes %s não puderam ser carregadas" + +msgid "errors.cannot-upload" +msgstr "Não foi possível carregar o ficheiro." #: src/app/main/data/workspace.cljs msgid "errors.clipboard-not-implemented" msgstr "O teu browser não pode fazer esta operação" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/change_email.cljs msgid "errors.email-already-exists" msgstr "E-mail já utilizado" @@ -721,10 +854,17 @@ msgstr "E-mail já validado." msgid "errors.email-as-password" msgstr "Não podes utilizar o teu e-mail como palavra-passe" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" msgstr "O e-mail «%s» tem muitos relatórios de rejeição permanentes." +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs +msgid "errors.email-invalid" +msgstr "Por favor introduz um email válido" + #: src/app/main/ui/settings/change_email.cljs msgid "errors.email-invalid-confirmation" msgstr "O e-mail de confirmação deve combinar" @@ -732,7 +872,18 @@ msgstr "O e-mail de confirmação deve combinar" msgid "errors.email-spam-or-permanent-bounces" msgstr "O e-mail «%s» foi denunciado como spam ou devolvido permanentemente." -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/errors.cljs +msgid "errors.feature-mismatch" +msgstr "" +"Parece que estás a abrir um ficheiro que tem a funcionalidade '%s' ativada, " +"mas a versão do teu Penpot não a suporta, ou está desativada." + +#: src/app/main/errors.cljs +msgid "errors.feature-not-supported" +msgstr "A funcionalidade '%s' não é suportada." + +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.generic" msgstr "Ocorreu algo inesperado." @@ -751,6 +902,10 @@ msgstr "Este convite pode ter sido cancelado ou expirado." msgid "errors.ldap-disabled" msgstr "Autenticação LDAP está desativada." +#: src/app/main/errors.cljs +msgid "errors.max-quote-reached" +msgstr "Alcançou o máximo da quota '%s'. Contacte o suporte técnico." + #: src/app/main/data/workspace/persistence.cljs msgid "errors.media-too-large" msgstr "A imagem é demasiado grande para ser inserida." @@ -759,7 +914,7 @@ msgstr "A imagem é demasiado grande para ser inserida." msgid "errors.media-type-mismatch" msgstr "Parece que o conteúdo da imagem não corresponde à extensão do ficheiro." -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" msgstr "Parece que esta não é uma imagem válida." @@ -780,7 +935,9 @@ msgstr "A palavra-passe deverá conter no mínimo 8 caracteres" msgid "errors.profile-blocked" msgstr "O perfil está bloqueado" -#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.profile-is-muted" msgstr "" "O teu perfil tem e-mails silenciados (relatórios de spam ou devoluções " @@ -801,7 +958,9 @@ msgstr "" "O proprietário não pode sair da equipa, deverás retribuir a função de " "proprietário." -#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/data/media.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "errors.unexpected-error" msgstr "Ocorreu um erro inesperado." @@ -809,6 +968,27 @@ msgstr "Ocorreu um erro inesperado." msgid "errors.unexpected-token" msgstr "Token desconhecido" +msgid "errors.webhooks.connection" +msgstr "Erro de conexão, não foi possível alcançar o URL" + +msgid "errors.webhooks.invalid-uri" +msgstr "O URL não passou na validação." + +msgid "errors.webhooks.last-delivery" +msgstr "Último envio sem sucesso." + +msgid "errors.webhooks.ssl-validation" +msgstr "Erro na validação SSL." + +msgid "errors.webhooks.timeout" +msgstr "Timeout" + +msgid "errors.webhooks.unexpected" +msgstr "Erro inesperado ao validar" + +msgid "errors.webhooks.unexpected-status" +msgstr "Estado inesperado %s" + #: src/app/main/ui/auth/login.cljs msgid "errors.wrong-credentials" msgstr "Utilizador ou palavra-passe parecem estar errados." @@ -852,7 +1032,7 @@ msgstr "E-mail" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Ir para o Twitter" +msgstr "Ir para o X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -860,7 +1040,7 @@ msgstr "Aqui para ajudar com as tuas dúvidas técnicas." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Conta de suporte no Twitter" +msgstr "Conta de suporte no X" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -914,7 +1094,8 @@ msgstr "Altura" msgid "inspect.attributes.layout.left" msgstr "Esquerda" -#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs +#: src/app/main/ui/inspect/attributes/layout.cljs, +#: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.layout.radius" msgstr "Raio" @@ -934,23 +1115,20 @@ msgstr "Largura" msgid "inspect.attributes.shadow" msgstr "Sombra" -#: src/app/main/ui/inspect/attributes/shadow.cljs -msgid "inspect.attributes.shadow.shorthand.spread" -msgstr "S" +#: src/app/main/ui/inspect/attributes/layout.cljs +msgid "inspect.attributes.size" +msgstr "Tamanho e posição" #: src/app/main/ui/inspect/attributes/stroke.cljs msgid "inspect.attributes.stroke" msgstr "Traço" -#, permanent msgid "inspect.attributes.stroke.alignment.center" msgstr "Centro" -#, permanent msgid "inspect.attributes.stroke.alignment.inner" msgstr "Interior" -#, permanent msgid "inspect.attributes.stroke.alignment.outer" msgstr "Exterior" @@ -986,6 +1164,10 @@ msgstr "Tamanho da Fonte" msgid "inspect.attributes.typography.font-style" msgstr "Estilo da Fonte" +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-weight" +msgstr "Espessura da fonte" + #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.letter-spacing" msgstr "Espaço entre caracteres" @@ -1023,6 +1205,17 @@ msgstr "Capitalização de Título" msgid "inspect.attributes.typography.text-transform.uppercase" msgstr "Maiúsculas" +msgid "inspect.empty.help" +msgstr "Se quiseres saber mais sobre a inspeção, visita o centro de ajuda do Penpot" + +msgid "inspect.empty.more-info" +msgstr "Mais informações sobre a inspeção" + +msgid "inspect.empty.select" +msgstr "" +"Seleciona uma forma, prancheta, ou grupo para inspecionar os seus atributos " +"e código" + #: src/app/main/ui/inspect/right_sidebar.cljs msgid "inspect.tabs.code" msgstr "Código" @@ -1075,10 +1268,17 @@ msgstr "Atalhos" msgid "labels.accept" msgstr "Aceitar" +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "Tokens de acesso" + +msgid "labels.active" +msgstr "Ativo" + msgid "labels.add-custom-font" msgstr "Adicionar fonte personalizada" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "Administração" @@ -1130,11 +1330,16 @@ msgstr "Continuar com" msgid "labels.continue-with-penpot" msgstr "Podes continuar com uma conta Penpot" +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.copy-invitation-link" +msgstr "Copiar link" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "labels.create" msgstr "Criar" -#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team" msgstr "Criar equipa nova" @@ -1149,7 +1354,8 @@ msgstr "Fonte personalizada" msgid "labels.dashboard" msgstr "Painel" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "Eliminar" @@ -1169,7 +1375,13 @@ msgstr "Eliminar convite" msgid "labels.delete-multi-files" msgstr "Eliminar %s ficheiros" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.discard" +msgstr "Descartar" + +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/files.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.drafts" msgstr "Rascunhos" @@ -1180,7 +1392,7 @@ msgstr "Editar" msgid "labels.edit-file" msgstr "Editar ficheiro" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "Editor" @@ -1215,7 +1427,9 @@ msgstr "Fontes" msgid "labels.github-repo" msgstr "Repositório Github" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "Dá feedback" @@ -1230,6 +1444,9 @@ msgstr "Centro de Ajuda" msgid "labels.hide-resolved-comments" msgstr "Ocultar comentários resolvidos" +msgid "labels.inactive" +msgstr "Inativo" + msgid "labels.installed-fonts" msgstr "Fontes instaladas" @@ -1243,7 +1460,8 @@ msgstr "" msgid "labels.internal-error.main-message" msgstr "Erro interno" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.invitations" msgstr "Convites" @@ -1260,13 +1478,13 @@ msgstr "Iniciar sessão ou registar" #: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.logout" -msgstr "Sair" +msgstr "Terminar sessão" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.member" msgstr "Membro" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "Membros" @@ -1274,16 +1492,16 @@ msgstr "Membros" msgid "labels.new-password" msgstr "Palavra-passe nova" -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +#: src/app/main/ui/workspace/comments.cljs, +#: src/app/main/ui/dashboard/comments.cljs msgid "labels.no-comments-available" -msgstr "Não tens notificações de comentários pendentes" +msgstr "Não tens notificações de comentários pendentes." #: src/app/main/ui/dashboard/team.cljs msgid "labels.no-invitations" msgstr "Não há convites." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "" "Clica no botão \"Convidar para a equipa\" para convidar mais membros para " @@ -1329,7 +1547,8 @@ msgstr "ou" msgid "labels.owner" msgstr "Proprietário" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.password" msgstr "Palavra-passe" @@ -1349,7 +1568,12 @@ msgstr "Projetos" msgid "labels.release-notes" msgstr "Notas de versões" -#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/workspace.cljs +msgid "labels.reload-file" +msgstr "Recarregar ficheiro" + +#: src/app/main/ui/workspace/libraries.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" msgstr "Remover" @@ -1357,7 +1581,9 @@ msgstr "Remover" msgid "labels.remove-member" msgstr "Remover membro" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.rename" msgstr "Renomear" @@ -1369,7 +1595,7 @@ msgstr "Renomear equipa" msgid "labels.resend-invitation" msgstr "Reenviar convite" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Tentar novamente" @@ -1399,7 +1625,8 @@ msgstr "Estamos numa manutenção programada dos nossos sistemas." msgid "labels.service-unavailable.main-message" msgstr "Serviço Indisponível" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.settings" msgstr "Definições" @@ -1429,6 +1656,10 @@ msgstr "Estado" msgid "labels.tutorials" msgstr "Tutoriais" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.unpublish-multi-files" +msgstr "Despublicar %s ficheiros" + #: src/app/main/ui/settings/profile.cljs msgid "labels.update" msgstr "Atualizar" @@ -1446,15 +1677,21 @@ msgstr "Carregar fontes personalizadas" msgid "labels.uploading" msgstr "Carregando…" +msgid "labels.view-only" +msgstr "Somente leitura" + #: src/app/main/ui/dashboard/team.cljs msgid "labels.viewer" msgstr "Visualizador" +msgid "labels.webhooks" +msgstr "Webhooks" + #: src/app/main/ui/comments.cljs msgid "labels.write-new-comment" msgstr "Escrever novo comentário" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.you" msgstr "(tu)" @@ -1462,21 +1699,24 @@ msgstr "(tu)" msgid "labels.your-account" msgstr "A tua conta" -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "A carregar imagem…" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" msgstr "Adicionar como Biblioteca Partilhada" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.hint" msgstr "" "Uma vez adicionados como Biblioteca Partilhada, os recursos na biblioteca " "deste ficheiro estarão disponíveis com o resto dos teus ficheiros." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.message" msgstr "Adicionar \"%s\" como Biblioteca Partilhada" @@ -1506,6 +1746,54 @@ msgstr "Alterar e-mail" msgid "modals.change-email.title" msgstr "Altera o teu e-mail" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "Copiar token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "Data de expiração" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "Nome" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "O nome pode ajudar a sugerir para que o token serve" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "Criar token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "Gerar token de acesso" + +msgid "modals.create-webhook.submit-label" +msgstr "Criar webhook" + +msgid "modals.create-webhook.title" +msgstr "Criar webhook" + +msgid "modals.create-webhook.url.label" +msgstr "URL do payload" + +msgid "modals.create-webhook.url.placeholder" +msgstr "https://example.com/postreceive" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "Apagar token" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "Tens a certeza que queres apagar este token?" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "Apagar token" + #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.cancel" msgstr "Cancelar e manter a minha conta" @@ -1536,6 +1824,12 @@ msgstr "" msgid "modals.delete-comment-thread.title" msgstr "Eliminar conversa" +msgid "modals.delete-component-annotation.message" +msgstr "Tens a certeza que queres apagar esta nota?" + +msgid "modals.delete-component-annotation.title" +msgstr "Apagar nota" + #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-confirm.accept" msgstr "Eliminar ficheiro" @@ -1596,25 +1890,34 @@ msgstr "Tens a certeza de que pretendes eliminar este projeto?" msgid "modals.delete-project-confirm.title" msgstr "Eliminar projeto" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.accept" msgid_plural "modals.delete-shared-confirm.accept" msgstr[0] "Eliminar ficheiro" msgstr[1] "Eliminar ficheiros" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "Não está ativa em nenhum ficheiro." +msgstr[1] "Não estão ativas em nenhum ficheiro." + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "Esta biblioteca está ativa aqui: " +msgstr[1] "Estas bibliotecas estão ativas aqui: " + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "Tens a certeza de que pretendes eliminar este ficheiro?" msgstr[1] "Tens a certeza de que pretendes eliminar estes ficheiros?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Este ficheiro tem bibliotecas utilizadas neste ficheiro:" -msgstr[1] "Este ficheiro tem bibliotecas utilizadas nestes ficheiros:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" msgstr[0] "Eliminando ficheiro" @@ -1646,6 +1949,31 @@ msgstr "Tens a certeza de que pretendes eliminar este membro da equipa?" msgid "modals.delete-team-member-confirm.title" msgstr "Eliminar membro da equipa" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" +msgstr[0] "" +"Os recursos que estiverem a ser usados neste ficheiro continuarão presentes " +"(nenhum design será afetado)." +msgstr[1] "" +"Os recursos que estiverem a ser usados nestes ficheiros continuarão " +"presentes (nenhum design será afetado)." + +msgid "modals.delete-webhook.accept" +msgstr "Apagar webhook" + +msgid "modals.delete-webhook.message" +msgstr "Tens a certeza que queres apagar este webhook?" + +msgid "modals.delete-webhook.title" +msgstr "A apagar webhook" + +msgid "modals.edit-webhook.submit-label" +msgstr "Editar webhook" + +msgid "modals.edit-webhook.title" +msgstr "Editar webhook" + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-member-confirm.accept" msgstr "Enviar convite" @@ -1653,6 +1981,11 @@ msgstr "Enviar convite" msgid "modals.invite-member.emails" msgstr "E-mails, separados por vírgulas" +msgid "modals.invite-member.repeated-invitation" +msgstr "" +"Alguns endereços de email pertencem a membros atuais da equipa. Não serão " +"enviados convites para estes endereços." + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-team-member.title" msgstr "Convidar membros para a equipa" @@ -1726,18 +2059,30 @@ msgstr "" msgid "modals.promote-owner-confirm.title" msgstr "Novo proprietário de equipa" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.publish-empty-library.accept" +msgstr "Publicar" + +msgid "modals.publish-empty-library.message" +msgstr "A tua biblioteca está vazia. Tens a certeza que queres publicá-la?" + +msgid "modals.publish-empty-library.title" +msgstr "Publicar biblioteca vazia" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" msgstr "Remover como Biblioteca Partilhada" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.hint" msgstr "" "Uma vez removida como Biblioteca Partilhada, a Biblioteca de Ficheiros " "deste ficheiro deixarão de estar disponíveis para serem utilizados com o " "resto dos teus ficheiros." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.message" msgstr "Remover \"%s\" como Biblioteca Partilhada" @@ -1745,70 +2090,66 @@ msgstr "Remover \"%s\" como Biblioteca Partilhada" msgid "modals.small-nudge" msgstr "Pequeno deslocamento" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.accept" -msgstr "Cancelar publicação" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Se cancelares a publicação, os recursos nele tornam-se uma biblioteca deste " -"ficheiro." -msgstr[1] "" -"Se cancelares a publicação, os recursos nele tornam-se uma biblioteca " -"destes ficheiros." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "Tens a certeza de que queres cancelar a publicação desta biblioteca?" msgstr[1] "Tens a certeza de que queres cancelar a publicação destas bibliotecas?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Está em uso neste ficheiro:" -msgstr[1] "Está em uso nestes ficheiros:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" msgid_plural "modals.unpublish-shared-confirm.title" msgstr[0] "Cancelar publicação da biblioteca" msgstr[1] "Cancelar publicação das bibliotecas" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.hint" msgstr "" "Estás prestes a atualizar componentes numa biblioteca partilhada. Pode " "afetar outros ficheiros que o utilizam." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.message" msgstr "Atualizar componentes numa biblioteca partilhada" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.accept" msgstr "Atualizar" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.cancel" msgstr "Cancelar" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.hint" msgstr "" "Estás prestes a atualizar componentes numa biblioteca partilhada. Pode " "afetar outros ficheiros que o utilizam." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.message" msgstr "Atualizar componente numa biblioteca partilhada" +#: src/app/main/data/common.cljs +msgid "notifications.by-code.upgrade-version" +msgstr "Está disponível uma nova versão, por favor atualiza a página" + #: src/app/main/ui/dashboard/team.cljs msgid "notifications.invitation-email-sent" msgstr "Convite enviado com sucesso" +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-link-copied" +msgstr "Link de convite copiado" + #: src/app/main/ui/settings/delete_account.cljs msgid "notifications.profile-deletion-not-allowed" msgstr "" @@ -1893,12 +2234,6 @@ msgstr "Guia de Contribuição" msgid "onboarding-v2.welcome.title" msgstr "Bem-vindo ao Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "Criar uma equipa mais tarde" - -msgid "onboarding.choice.team-up.create-team" -msgstr "O nome da tua equipa" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "Depois de nomeares a tua equipa, poderás convidar pessoas para entrar." @@ -1913,18 +2248,9 @@ msgstr "" "Lembra-te em incluir todos. Programadores, designers, gestores... " "acrescenta diversidade :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Criar equipa e convidar mais tarde" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Criar equipa e enviar convites" - msgid "onboarding.choice.team-up.roles" msgstr "Convidar com a função:" -msgid "onboarding.contrib.desc2.1" -msgstr "Podes aceder o" - msgid "onboarding.newsletter.accept" msgstr "Sim, subscreve" @@ -1939,15 +2265,6 @@ msgstr "Política de Privacidade." msgid "onboarding.newsletter.title" msgstr "Queres receber as novidades do Penpot?" -msgid "onboarding.slide.0.title" -msgstr "Bibliotecas de design, estilos e componentes" - -msgid "onboarding.slide.1.title" -msgstr "Dá vida aos teus designs com interações" - -msgid "onboarding.slide.3.alt" -msgstr "Inspect e low code" - msgid "onboarding.team-modal.create-team" msgstr "Cria uma equipa" @@ -1984,7 +2301,179 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "Ir para login" -#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "Com que ferramenta de design tens mais experiência?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11-30" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2-10" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31-50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "Muita" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canva" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.describe-your-experience-working-on" +msgstr "Quanta experiência dirias que tens a trabalhar com..." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "Designer" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "Desenvolvedor(a)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "Descobrir mais sobre o Penpot" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "Direção" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "Sou trabalhador(a) independente (freelancer)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "Obter código de um projeto " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "... design de interfaces, recursos visuais, sistemas de design, etc." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "InVision" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "Deixar comentários num projeto" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "Vamos começar!" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "Gestor(a) de projeto ou de produto" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "Marketing" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "Mais de 50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "Seguinte" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "Nenhuma" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "Outra (especificar)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "Estou a trabalhar num projeto pessoal" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "Anterior" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "Com que objetivo pensas usar o Penpot?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "Qual é o teu cargo?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.select-option" +msgstr "Escolhe uma opção" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "Sketch" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.some" +msgstr "Alguma" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "Começar" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "Começar a trabalhar no meu projeto" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "Estudante ou professor(a)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "De que tamanho é a tua equipa?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "Testar o Penpot para ver se é adequado para a minha equipa " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "Experimentar o Penpot antes de usar num servidor privado" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" +msgstr "...wireframes, jornadas e fluxos de utilizador, árvores de navegação, etc." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "Concetualizar ideias" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" +msgstr "" +"Os teus comentários ajudar-nos-ão a compreender os teus hábitos e " +"preferências para que possamos continuar a tornar o Penpot numa ferramenta " +"fácil e divertida de usar." + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "Misturado" @@ -2038,6 +2527,9 @@ msgstr "Caminhos" msgid "shortcut-subsection.shape" msgstr "Formas" +msgid "shortcut-subsection.text-editor" +msgstr "Textos" + msgid "shortcut-subsection.tools" msgstr "Ferramentas" @@ -2056,14 +2548,20 @@ msgstr "Adicionar nó" msgid "shortcuts.align-bottom" msgstr "Alinhar abaixo" +msgid "shortcuts.align-center" +msgstr "Alinhar ao centro" + msgid "shortcuts.align-hcenter" msgstr "Alinhar ao centro horizontalmente" +msgid "shortcuts.align-justify" +msgstr "Alinhar justificado" + msgid "shortcuts.align-left" msgstr "Alinhar à esquerda" msgid "shortcuts.align-right" -msgstr "Alinhas à direita" +msgstr "Alinhar à direita" msgid "shortcuts.align-top" msgstr "Alinhar topo" @@ -2074,6 +2572,9 @@ msgstr "Alinhar ao centro verticalmente" msgid "shortcuts.artboard-selection" msgstr "Criar prancheta a partir da seleção" +msgid "shortcuts.bold" +msgstr "Alternar negrito" + msgid "shortcuts.bool-difference" msgstr "Diferença booleana" @@ -2164,6 +2665,12 @@ msgstr "Virar horizontalmente" msgid "shortcuts.flip-vertical" msgstr "Virar verticalmente" +msgid "shortcuts.font-size-dec" +msgstr "Decrementar tamanho de fonte" + +msgid "shortcuts.font-size-inc" +msgstr "Incrementar tamanho da fonte" + msgid "shortcuts.go-to-drafts" msgstr "Ir para rascunhos" @@ -2188,9 +2695,27 @@ msgstr "Mais zoom" msgid "shortcuts.insert-image" msgstr "Inserir imagem" +msgid "shortcuts.italic" +msgstr "Alternar itálico" + msgid "shortcuts.join-nodes" msgstr "Unir nós" +msgid "shortcuts.letter-spacing-dec" +msgstr "Decrementar espaçamento de letras" + +msgid "shortcuts.letter-spacing-inc" +msgstr "Incrementar espaçamento de letras" + +msgid "shortcuts.line-height-dec" +msgstr "Decrementar espaçamento entre linhas" + +msgid "shortcuts.line-height-inc" +msgstr "Incrementar espaçamento entre linhas" + +msgid "shortcuts.line-through" +msgstr "Alternar texto rasurado" + msgid "shortcuts.make-corner" msgstr "Fazer canto" @@ -2278,6 +2803,9 @@ msgstr "Ir para secção de comentários" msgid "shortcuts.open-dashboard" msgstr "Ir para painel" +msgid "shortcuts.open-inspect" +msgstr "Ir para secção de inspeção" + msgid "shortcuts.open-interactions" msgstr "Ir para secção de interações" @@ -2308,6 +2836,15 @@ msgstr "Pesquisar atalhos" msgid "shortcuts.select-all" msgstr "Selecionar tudo" +msgid "shortcuts.select-next" +msgstr "Selecionar camada seguinte" + +msgid "shortcuts.select-parent-layer" +msgstr "Selecionar camada pai" + +msgid "shortcuts.select-prev" +msgstr "Selecionar camada anterior" + msgid "shortcuts.separate-nodes" msgstr "Separar nós" @@ -2332,6 +2869,18 @@ msgstr "Iniciar medição" msgid "shortcuts.stop-measure" msgstr "Parar medição" +msgid "shortcuts.text-align-center" +msgstr "Alinhar ao centro" + +msgid "shortcuts.text-align-justify" +msgstr "Alinhar justificado" + +msgid "shortcuts.text-align-left" +msgstr "Alinhar à esquerda" + +msgid "shortcuts.text-align-right" +msgstr "Alinhar à direita" + msgid "shortcuts.thumbnail-set" msgstr "Definir imagem destaque" @@ -2354,15 +2903,15 @@ msgstr "Alternar modo de foco" msgid "shortcuts.toggle-fullscreen" msgstr "Alternar tela cheia" -msgid "shortcuts.toggle-grid" -msgstr "Mostrar/ocultar grade" - msgid "shortcuts.toggle-history" msgstr "Alternar histórico" msgid "shortcuts.toggle-layers" msgstr "Alternar camadas" +msgid "shortcuts.toggle-layout-flex" +msgstr "Adicionar / Remover layout flex" + msgid "shortcuts.toggle-lock" msgstr "Bloquear selecionado" @@ -2372,21 +2921,18 @@ msgstr "Bloquear proporções" msgid "shortcuts.toggle-rules" msgstr "Mostrar/ocultar regras" -msgid "shortcuts.toggle-scale-text" -msgstr "Alternar escala de texto" - -msgid "shortcuts.toggle-snap-grid" -msgstr "Ajustar à grade" - -msgid "shortcuts.toggle-snap-guide" -msgstr "Ajustar às guias" - msgid "shortcuts.toggle-textpalette" msgstr "Alternar paleta de texto" +msgid "shortcuts.toggle-visibility" +msgstr "Mostrar / Ocultar" + msgid "shortcuts.toggle-zoom-style" msgstr "Alternar estilo de zoom" +msgid "shortcuts.underline" +msgstr "Alternar sublinhado" + msgid "shortcuts.undo" msgstr "Desfazer" @@ -2399,9 +2945,19 @@ msgstr "Retirar máscara" msgid "shortcuts.v-distribute" msgstr "Distribuir verticalmente" +msgid "shortcuts.zoom-lense-decrease" +msgstr "Reduzir zoom na lupa" + +msgid "shortcuts.zoom-lense-increase" +msgstr "Aumentar zoom na lupa" + msgid "shortcuts.zoom-selected" msgstr "Zoom para selecionados" +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "O nome do webhook deve conter um máximo de 2048 caracteres." + #: src/app/main/ui/dashboard/files.cljs msgid "title.dashboard.files" msgstr "%s - Penpot" @@ -2430,6 +2986,10 @@ msgstr "Bibliotecas partilhadas - %s - Penpot" msgid "title.default" msgstr "Penpot - Liberdade de Design para Equipas" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "Perfil - Tokens de acesso" + #: src/app/main/ui/settings/feedback.cljs msgid "title.settings.feedback" msgstr "Dá feedback - Penpot" @@ -2458,6 +3018,9 @@ msgstr "Membros - %s - Penpot" msgid "title.team-settings" msgstr "Definições - %s - Penpot" +msgid "title.team-webhooks" +msgstr "Webhooks - %s - Penpot" + #: src/app/main/ui/inspect.cljs, src/app/main/ui/viewer.cljs msgid "title.viewer" msgstr "%s - Modo visualizador - Penpot" @@ -2493,6 +3056,9 @@ msgstr "Não mostrar interações" msgid "viewer.header.fullscreen" msgstr "Tela Cheia" +msgid "viewer.header.inspect-section" +msgstr "Inspecionar (%s)" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.interactions" msgstr "Interações" @@ -2504,10 +3070,6 @@ msgstr "Interações (%s)" msgid "viewer.header.share.copy-link" msgstr "Copiar link" -#: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.subtitle" -msgstr "Qualquer pessoa com o link terá acesso" - #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.show-interactions" msgstr "Mostrar interações" @@ -2520,6 +3082,9 @@ msgstr "Mostrar interações com click" msgid "viewer.header.sitemap" msgstr "Mapa do site" +msgid "webhooks.last-delivery.success" +msgstr "Último envio com sucesso." + #: src/app/main/ui/workspace/sidebar/align.cljs msgid "workspace.align.hcenter" msgstr "Alinhar horizontal ao centro (%s)" @@ -2560,11 +3125,13 @@ msgstr "Recursos" msgid "workspace.assets.box-filter-all" msgstr "Todos os recursos" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.colors" msgstr "Cores" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.components" msgstr "Componentes" @@ -2578,19 +3145,27 @@ msgstr "" "Os teus itens serão nomeados automaticamente como \"nome do grupo / nome do " "item\"" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "Eliminar" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.duplicate" msgstr "Duplicar" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.duplicate-main" +msgstr "Duplicar principal" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" msgstr "Editar" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.graphics" msgstr "Gráficos" @@ -2613,7 +3188,12 @@ msgstr "biblioteca local" msgid "workspace.assets.not-found" msgstr "Recursos não encontrados" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.open-library" +msgstr "Abrir ficheiro de biblioteca" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename" msgstr "Renomear" @@ -2632,10 +3212,11 @@ msgstr[0] "%s item selecionado" msgstr[1] "%s itens selecionados" #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "PARTILHADO" +msgid "workspace.assets.shared-library" +msgstr "Biblioteca partilhada" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "Tipografias" @@ -2663,10 +3244,15 @@ msgstr "Espaço entre letras" msgid "workspace.assets.typography.line-height" msgstr "Altura da Linha" -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/inspect/attributes/text.cljs, src/app/main/ui/inspect/attributes/text.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs msgid "workspace.assets.typography.sample" msgstr "Ag" +msgid "workspace.assets.typography.text-styles" +msgstr "Estilos de texto" + #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.text-transform" msgstr "Transformar Texto" @@ -2687,11 +3273,13 @@ msgstr "Foco ativo" msgid "workspace.focus.selection" msgstr "Seleção" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "Gradiente linear" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.radial" msgstr "Gradiente radial" @@ -2699,14 +3287,13 @@ msgstr "Gradiente radial" msgid "workspace.header.menu.disable-dynamic-alignment" msgstr "Desativar alinhamento dinâmico" +msgid "workspace.header.menu.disable-scale-content" +msgstr "Desativar escala proporcional" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-scale-text" msgstr "Desativar escala de texto" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Desativar ajuste à grade" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "Desativar ajuste às guias" @@ -2718,14 +3305,13 @@ msgstr "Desativar ajuste ao pixel" msgid "workspace.header.menu.enable-dynamic-alignment" msgstr "Ativar alinhamento dinâmico" +msgid "workspace.header.menu.enable-scale-content" +msgstr "Ativar escala proporcional" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-scale-text" msgstr "Ativar escalar texto" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Ajustar à grade" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "Ajustar às guias" @@ -2737,10 +3323,6 @@ msgstr "Ativar ajuste ao pixel" msgid "workspace.header.menu.hide-artboard-names" msgstr "Ocultar nome das pranchetas" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Ocultar grades" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Ocultar paleta de cor" @@ -2776,6 +3358,9 @@ msgstr "Preferências" msgid "workspace.header.menu.option.view" msgstr "Visualização" +msgid "workspace.header.menu.redo" +msgstr "Refazer" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.select-all" msgstr "Selecionar tudo" @@ -2784,10 +3369,6 @@ msgstr "Selecionar tudo" msgid "workspace.header.menu.show-artboard-names" msgstr "Mostrar nomes das pranchetas" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Mostrar grade" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Mostrar paleta de cor" @@ -2803,6 +3384,9 @@ msgstr "Mostrar regras" msgid "workspace.header.menu.show-textpalette" msgstr "Mostrar paleta de texto" +msgid "workspace.header.menu.undo" +msgstr "Desfazer" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "Ampliar em 100%" @@ -2827,6 +3411,10 @@ msgstr "Alterações não guardadas" msgid "workspace.header.viewer" msgstr "Modo de visualização (%s)" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "Zoom" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-fill" msgstr "Ajustar para preencher" @@ -2847,6 +3435,9 @@ msgstr "Tela cheia" msgid "workspace.header.zoom-selected" msgstr "Aumentar para seleção" +msgid "workspace.layout_grid.editor.title" +msgstr "A editar grelha" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.add" msgstr "Adicionar" @@ -2855,7 +3446,16 @@ msgstr "Adicionar" msgid "workspace.libraries.colors" msgstr "%s cores" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "Ainda não existem estilos de cor na tua biblioteca" + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "Ainda não existem tipografias na tua biblioteca" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" msgstr "Biblioteca de ficheiros" @@ -2863,7 +3463,8 @@ msgstr "Biblioteca de ficheiros" msgid "workspace.libraries.colors.hsv" msgstr "HSV" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "Cores recentes" @@ -2903,6 +3504,10 @@ msgstr "BIBLIOTECAS" msgid "workspace.libraries.library" msgstr "BIBLIOTECA" +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "ATUALIZAÇÕES DE BIBLIOTECAS" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.no-libraries-need-sync" msgstr "Não há bibliotecas partilhadas que precisem de atualização" @@ -2939,13 +3544,14 @@ msgstr "%s tipografias" msgid "workspace.libraries.update" msgstr "Atualizar" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "ver todas as alterações" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.updates" msgstr "ATUALIZAÇÕES" -msgid "workspace.library.store" -msgstr "Bibliotecas de lojas" - #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.add-interaction" msgstr "Clica no botão + para adicionar interações." @@ -2973,6 +3579,15 @@ msgstr "Recorte do conteúdo" msgid "workspace.options.component" msgstr "Componente" +msgid "workspace.options.component.annotation" +msgstr "Nota" + +msgid "workspace.options.component.create-annotation" +msgstr "Criar uma nota" + +msgid "workspace.options.component.edit-annotation" +msgstr "Editar uma nota" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints" msgstr "Restrições" @@ -3017,15 +3632,18 @@ msgstr "Topo e Abaixo" msgid "workspace.options.design" msgstr "Design" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export" msgstr "Exportar" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-multiple" msgstr "Exportar seleção" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-object" msgid_plural "workspace.options.export-object" msgstr[0] "Exportar 1 elemento" @@ -3035,19 +3653,23 @@ msgstr[1] "Exportar %s elementos" msgid "workspace.options.export.suffix" msgstr "Sufixo" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-complete" msgstr "Exportação completa" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object" msgstr "A exportar…" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-error" msgstr "A exportação falhou" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-slow" msgstr "Exportação inesperadamente lenta" @@ -3165,6 +3787,9 @@ msgstr "Traço de grupo" msgid "workspace.options.height" msgstr "Altura" +msgid "workspace.options.inspect" +msgstr "Inspecionar" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-action" msgstr "Ação" @@ -3193,6 +3818,9 @@ msgstr "Empurrar" msgid "workspace.options.interaction-animation-slide" msgstr "Deslizar" +msgid "workspace.options.interaction-auto" +msgstr "Automático" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-background" msgstr "Adicionar sobreposição de fundo" @@ -3341,6 +3969,10 @@ msgstr "Preservar posição do scroll" msgid "workspace.options.interaction-prev-screen" msgstr "Ecrã anterior" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-relative-to" +msgstr "Relativa a" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-self" msgstr "auto" @@ -3490,8 +4122,20 @@ msgid "workspace.options.layout.bottom" msgstr "Abaixo" #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.top" -msgstr "Coluna inversa" +msgid "workspace.options.layout.direction.column" +msgstr "Coluna" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column-reverse" +msgstr "Coluna invertida" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row" +msgstr "Linha" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row-reverse" +msgstr "Linha invertida" #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.gap" @@ -3556,7 +4200,8 @@ msgstr "Mais bibliotecas de cor" msgid "workspace.options.opacity" msgstr "Opacidade" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Posição" @@ -3568,12 +4213,12 @@ msgid "workspace.options.radius" msgstr "Raio" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Todos os cantos" +msgid "workspace.options.radius-bottom-left" +msgstr "Inferior esquerdo" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Cantos individuais" +msgid "workspace.options.radius-bottom-right" +msgstr "Inferior direito" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3584,17 +4229,18 @@ msgid "workspace.options.radius-top-right" msgstr "Superior direito" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Inferior esquerdo" +msgid "workspace.options.radius.all-corners" +msgstr "Todos os cantos" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Inferior direito" +msgid "workspace.options.radius.single-corners" +msgstr "Cantos individuais" msgid "workspace.options.recent-fonts" msgstr "Recente" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" msgstr "Repetir" @@ -3669,7 +4315,8 @@ msgstr "Mostrar na exportação" msgid "workspace.options.show-in-viewer" msgstr "Mostrar no modo de visualização" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.size" msgstr "Tamanho" @@ -3751,26 +4398,10 @@ msgstr "Sólido" msgid "workspace.options.text-options.align-bottom" msgstr "Alinhar abaixo" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Alinhar ao centro (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Justificar (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Alinhar à esquerda (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Alinhar ao meio" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Alinhar à direita (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Alinhar ao topo" @@ -3807,7 +4438,8 @@ msgstr "Altura entre linhas" msgid "workspace.options.text-options.lowercase" msgstr "Minúsculas" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.none" msgstr "Nenhum" @@ -3815,6 +4447,22 @@ msgstr "Nenhum" msgid "workspace.options.text-options.strikethrough" msgstr "Rasurado (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Alinhar ao centro (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Justificar (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Alinhar à esquerda (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Alinhar à direita (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Texto" @@ -3849,7 +4497,7 @@ msgid "workspace.options.width" msgstr "Largura" msgid "workspace.options.x" -msgstr "X" +msgstr "Eixo X" msgid "workspace.options.y" msgstr "Y" @@ -3884,6 +4532,13 @@ msgstr "Separar nós (%s)" msgid "workspace.path.actions.snap-nodes" msgstr "Ajustar nós (%s)" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.add-flex" +msgstr "Adicionar layout flex" + +msgid "workspace.shape.menu.add-grid" +msgstr "Adicionar layout em grelha" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "Enviar para trás" @@ -3896,6 +4551,9 @@ msgstr "Mover para trás" msgid "workspace.shape.menu.copy" msgstr "Copiar" +msgid "workspace.shape.menu.create-annotation" +msgstr "Criar nota" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.create-artboard-from-selection" msgstr "Seleção para a prancheta" @@ -3904,6 +4562,9 @@ msgstr "Seleção para a prancheta" msgid "workspace.shape.menu.create-component" msgstr "Criar componente" +msgid "workspace.shape.menu.create-multiple-components" +msgstr "Criar múltiplos componentes" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.cut" msgstr "Recortar" @@ -3916,11 +4577,15 @@ msgstr "Eliminar" msgid "workspace.shape.menu.delete-flow-start" msgstr "Eliminar início de fluxo" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Desprender instância" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instances-in-bulk" msgstr "Desprender instâncias" @@ -3961,7 +4626,8 @@ msgstr "Mover para a frente" msgid "workspace.shape.menu.front" msgstr "Enviar para a frente" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.go-main" msgstr "Ir para ficheiro do componente principal" @@ -3983,18 +4649,26 @@ msgstr "Interseção" msgid "workspace.shape.menu.lock" msgstr "Bloquear" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.mask" msgstr "Máscara" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.paste" msgstr "Colar" msgid "workspace.shape.menu.path" msgstr "Curvas" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.remove-flex" +msgstr "Remover layout flex" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "Anular alterações" @@ -4009,11 +4683,13 @@ msgstr "Selecionar camada" msgid "workspace.shape.menu.show" msgstr "Mostrar" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-in-assets" msgstr "Mostrar recursos no painel" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-main" msgstr "Mostrar componente principal" @@ -4041,11 +4717,15 @@ msgstr "Desbloquear" msgid "workspace.shape.menu.unmask" msgstr "Retirar máscara" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-components-in-bulk" msgstr "Atualizar componentes principais" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-main" msgstr "Atualizar componente principal" @@ -4087,7 +4767,8 @@ msgstr "Formas" msgid "workspace.sidebar.layers.texts" msgstr "Textos" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/inspect/attributes/svg.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, +#: src/app/main/ui/inspect/attributes/svg.cljs msgid "workspace.sidebar.options.svg-attrs.title" msgstr "Importar atributos do SVG" @@ -4270,6 +4951,10 @@ msgstr "Histórico" msgid "workspace.updates.dismiss" msgstr "Ignorar" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "Mais informações" + #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.there-are-updates" msgstr "Existem atualizações nas bibliotecas partilhadas" @@ -4280,3 +4965,225 @@ msgstr "Atualizar" msgid "workspace.viewport.click-to-close-path" msgstr "Clica para fechar o caminho" + +#~ msgid "dashboard.newsletter-title" +#~ msgstr "Subscrição de Newsletter" + +#~ msgid "feedback.chat-subtitle" +#~ msgstr "Sentes vontade de falar? Conversa connosco no Gitter" + +#~ msgid "inspect.attributes.shadow.shorthand.offset-x" +#~ msgstr "X" + +#~ msgid "labels.images" +#~ msgstr "Imagens" + +#~ msgid "labels.skip" +#~ msgstr "Saltar" + +#~ msgid "onboarding.contrib.alt" +#~ msgstr "Código aberto" + +#~ msgid "onboarding.contrib.link" +#~ msgstr "projeto no GitHub" + +#~ msgid "onboarding.slide.0.desc1" +#~ msgstr "Cria interfaces maravilhosas em colaboração com todos os membros da equipa." + +#~ msgid "onboarding.slide.1.desc1" +#~ msgstr "Cria interações ricas para simular o comportamento do produto." + +#~ msgid "onboarding.slide.2.desc1" +#~ msgstr "" +#~ "Todos os membros da equipa a colaborar em tempo real, com comentários, " +#~ "ideias e feedback centralizados diretamente nos designs." + +#~ msgid "onboarding.slide.3.desc2" +#~ msgstr "" +#~ "Recebe e fornece especificações de código como markup (SVG, HTML) ou " +#~ "estilos (CSS, Less, Stylus...)." + +#~ msgid "viewer.header.share.placeholder" +#~ msgstr "Links partilhados aparecerão aqui" + +#~ msgid "workspace.library.libraries" +#~ msgstr "Bibliotecas" + +#~ msgid "workspace.options.blur-options.layer-blur" +#~ msgstr "Camada" + +#~ msgid "workspace.options.layout.direction.left" +#~ msgstr "Linha" + +msgid "workspace.options.component.copy" +msgstr "Cópia" + +msgid "dashboard.import.import-message" +msgid_plural "dashboard.import.import-message" +msgstr[0] "1 ficheiro foi importado com sucesso." +msgstr[1] "%s ficheiros foram importados com sucesso." + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.accept" +msgid_plural "modals.unpublish-shared-confirm.accept" +msgstr[0] "Cancelar publicação" +msgstr[1] "Cancelar publicações" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "Retângulo" + +msgid "workspace.options.component.main" +msgstr "Principal" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "Diamante" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +msgid "settings.detach" +msgstr "Desacoplar" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "Triângulo" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "Seta" + +msgid "modals.add-shared-confirm-empty.hint" +msgstr "" +"A tua biblioteca está vazia. Assim que ela seja adicionada como uma " +"biblioteca partilhada, os recursos que criares nela estarão disponíveis para " +"serem usados nos teus outros ficheiros. Tens a certeza que queres publicá-la?" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "Círculo" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "O teu ficheiro foi apagado com sucesso" +msgstr[1] "Os teus ficheiros foram apagados com sucesso" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "O teu ficheiro foi duplicado com sucesso" +msgstr[1] "Os teus ficheiros foram duplicados com sucesso" + +msgid "onboarding.choice.team-up.continue-creating-team" +msgstr "Continuar criação de equipa" + +msgid "workspace.options.guides.title" +msgstr "Guias" + +msgid "media.choose-image" +msgstr "Escolher imagem" + +#: src/app/main/ui/auth/register.cljs +#, markdown +msgid "auth.terms-privacy-agreement-md" +msgstr "" +"Ao criar uma nova conta, concordas com os nossos [termos de serviço](%s) e [" +"política de privacidade](%s)." + +msgid "workspace.options.component.swap.empty" +msgstr "Ainda não existem recursos nesta biblioteca" + +msgid "media.solid" +msgstr "Sólido" + +msgid "workspace.top-bar.read-only.done" +msgstr "Feito" + +msgid "onboarding.choice.team-up.create-team-and-invite" +msgstr "Criar equipa e convidar" + +msgid "workspace.layout_grid.editor.top-bar.done" +msgstr "Feito" + +msgid "onboarding.choice.team-up.continue-without-a-team" +msgstr "Continuar sem equipa" + +msgid "inspect.attributes.typography.text-transform.unset" +msgstr "Sem atribuição" + +msgid "errors.validation" +msgstr "Erro de validação" + +msgid "workspace.layout_grid.editor.options.exit" +msgstr "Sair" + +#: src/app/main/errors.cljs +msgid "errors.version-not-supported" +msgstr "O ficheiro tem um número de versão incompatível" + +msgid "workspace.options.component.swap" +msgstr "Trocar de componente" + +msgid "onboarding.choice.team-up.create-team-without-inviting" +msgstr "Criar equipa sem convidar" + +#: src/app/main/errors.cljs +msgid "errors.file-feature-mismatch" +msgstr "" +"Parece que existem discrepâncias entre as funcionalidades ativadas e as " +"funcionalidades do ficheiro que estás a tentar abrir. Será necessário " +"aplicar migrações para '%s' antes de poder abrir o ficheiro." + +msgid "onboarding.choice.team-up.create-team-without-invite" +msgstr "Criar equipa" + +msgid "media.linear" +msgstr "Linear" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow" +msgstr "Fluxo" + +msgid "labels.search" +msgstr "Pesquisar" + +msgid "media.image" +msgstr "Imagem" + +msgid "onboarding.choice.team-up.start-without-a-team-description" +msgstr "Poderás criar uma equipa mais tarde." + +msgid "onboarding.choice.team-up.create-team-and-send-invites" +msgstr "Criar equipa e enviar convites" + +msgid "workspace.layout_grid.editor.top-bar.locate" +msgstr "Localizar" + +msgid "media.gradient" +msgstr "Gradiente" + +msgid "labels.share" +msgstr "Partilhar" + +msgid "workspace.layout_grid.editor.options.edit-grid" +msgstr "Editar grelha" + +msgid "onboarding.choice.team-up.start-without-a-team" +msgstr "Começar sem equipa" + +msgid "onboarding.choice.team-up.create-team-and-send-invites-description" +msgstr "Poderás enviar convites mais tarde" + +msgid "media.radial" +msgstr "Radial" + +msgid "errors.paste-data-validation" +msgstr "Dados inválidos na área de transferência" + +#: src/app/main/errors.cljs +msgid "errors.team-feature-mismatch" +msgstr "Funcionalidade incompatível '%s' detetada" + +#, markdown +msgid "workspace.top-bar.read-only" +msgstr "**Modo de inspeção** (Somente leitura)" diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index 3b32d7541e..232fc9a6cd 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2023-07-18 13:02+0000\n" +"PO-Revision-Date: 2023-10-10 10:01+0000\n" "Last-Translator: AlexTECPlayz \n" "Language-Team: Romanian \n" @@ -10,7 +10,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " "20)) ? 1 : 2;\n" -"X-Generator: Weblate 5.0-dev\n" +"X-Generator: Weblate 5.1-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -40,7 +40,8 @@ msgstr "" "Acesta este un DEMO, NU UTILIZAȚI pentru lucrări reale, întrucât proiectele " "vor fi șterse periodic." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" msgstr "Adresă E-mail" @@ -84,6 +85,14 @@ msgstr "LDAP" msgid "auth.login-with-oidc-submit" msgstr "OpenID" +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "Numele trebuie să conțină un caracter altul decât spațiu." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "Numele trebuie să conțină cel mult 250 caractere." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.new-password" msgstr "Introduceți o parolă nouă" @@ -118,6 +127,10 @@ msgstr "Parola" msgid "auth.password-length-hint" msgstr "Cel puțin 8 caractere" +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "Parola trebuie să conțină un caracter altul decât spațiu." + msgid "auth.privacy-policy" msgstr "Politica de Confidențialitate" @@ -170,6 +183,10 @@ msgstr "" msgid "auth.verification-email-sent" msgstr "Am trimis un email de verificare la" +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "... mărci, ilustrații, piese de marketing, etc." + msgid "common.publish" msgstr "Publică" @@ -269,7 +286,83 @@ msgstr "Începeți turul" msgid "dasboard.walkthrough-hero.title" msgstr "Tutorial interfață" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "Jeton copiat" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "Generați jeton nou" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "Jeton de acces creat cu succes." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "Apăsați butonul 'Generați jeton nou' pentru a genera unul." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "Nu aveți încă jetoane." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "Numele este obligatoriu" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 zile" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 zile" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 zile" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 zile" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "Niciodată" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "Expirat pe %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "Expiră pe %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "Fără dată de expirare" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Jeton de acces personal" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Jetoanele de acces personal funcționează ca o alternativă la sistemul " +"nostru de autentificare prin login/parolă și poate fi folosit pentru a " +"permite unei aplicații să acceseze API-ul intern Penpot" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "Jetonul va expira pe %s" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "Jetonul nu are dată de expirare" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Adăugați ca bibliotecă partajată" @@ -299,7 +392,8 @@ msgstr "Descărcați fișierul Penpot (.penpot)" msgid "dashboard.download-standard-file" msgstr "Descărcați fișierul standard (.svg + .json)" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate" msgstr "Duplicat" @@ -308,7 +402,6 @@ msgid "dashboard.duplicate-multi" msgstr "Duplicați %s fișiere" #: src/app/main/ui/dashboard/grid.cljs -#, markdown msgid "dashboard.empty-placeholder-drafts" msgstr "" "Fișierele adăugate la Biblioteci vor apărea aici. Încercați să partajați " @@ -409,7 +502,6 @@ msgstr[0] "1 font adăugat" msgstr[1] "%s fonturi adăugate" msgstr[2] "%s de fonturi adăugate" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "Fonturile încărcate vor fi adăugate la familia de fonturi disponibilă " @@ -417,7 +509,6 @@ msgstr "" "familie de font-uri**. Tipurile de fişiere acceptate: **TTF, OTF și WOFF** " "(se poate urca doar un singur tip)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "Ar trebui să urcați doar fonturi la care aveți drept de folosință sau " @@ -430,6 +521,15 @@ msgstr "" msgid "dashboard.fonts.upload-all" msgstr "Încarcă toate" +msgid "dashboard.fonts.warning-text" +msgstr "" +"Am detectat o posibilă problemă în fonturile dvs. în legătură cu metricile " +"verticale pentru diferite sisteme operaționale. Pentru a o verifica, puteți " +"utiliza servicii de măsurare verticală a fonturilor, cum ar fi " +"[acesta](https://vertical-metrics.netlify.app/). În plus, vă recomandăm să " +"utilizați [Transfonter](https://transfonter.org/) pentru a genera fonturi " +"web și a remedia erorile. " + msgid "dashboard.import" msgstr "Importați fișiere Penpot" @@ -467,7 +567,8 @@ msgstr "Încărcarea fișierului: %s" msgid "dashboard.invite-profile" msgstr "Invitați persoane" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" msgstr "Părăsește echipa" @@ -491,7 +592,8 @@ msgstr "încărcarea fișierelor …" msgid "dashboard.loading-fonts" msgstr "se încarcă fonturile tale…" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "Mută la" @@ -503,7 +605,8 @@ msgstr "Mutați %s fișiere la" msgid "dashboard.move-to-other-team" msgstr "Mutați la altă echipă" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" msgstr "+ Fișier nou" @@ -566,7 +669,8 @@ msgstr "Proiecte" msgid "dashboard.remove-account" msgstr "Doriți să vă ștergeți contul?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" msgstr "Elimină ca şi Colecţie Distribuită" @@ -594,15 +698,30 @@ msgstr "Selectați o temă" msgid "dashboard.show-all-files" msgstr "Afișați toate fișierele" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "Fișierul dumneavoastră a fost șters cu succes" +msgstr[1] "Fișierele dumneavoastră au fost șterse cu succes" +msgstr[2] "Fișierele dumneavoastră au fost șterse cu succes" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-delete-project" msgstr "Proiectul s-a șters cu succes" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "Fișierul dumneavoastră a fost duplicat cu succes" +msgstr[1] "Fișierele dumneavoastră au fost duplicate cu succes" +msgstr[2] "Fișierele dumneavoastră au fost duplicate cu succes" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-duplicate-project" msgstr "Proiectul s-a duplicat cu succes" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-file" msgstr "Fișierul a fost mutat cu succes" @@ -638,11 +757,14 @@ msgstr "Rezultatele căutării" msgid "dashboard.type-something" msgstr "Scrie pentru a începe căutarea" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.unpublish-shared" msgstr "Anulați publicarea bibliotecii" -#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/profile.cljs, +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Actualizare setări" @@ -689,7 +811,11 @@ msgstr "Email" msgid "dashboard.your-name" msgstr "Numele tău" -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" msgstr "Contul Penpot" @@ -730,11 +856,15 @@ msgstr "Fontul %s nu a putut fi încărcat" msgid "errors.bad-font-plural" msgstr "Fonturile %s nu au putut fi încărcate" +msgid "errors.cannot-upload" +msgstr "Fișierul media nu s-a putut încărca." + #: src/app/main/data/workspace.cljs msgid "errors.clipboard-not-implemented" msgstr "Bowser-ul tău nu permite clipboard" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/change_email.cljs msgid "errors.email-already-exists" msgstr "Email deja trimis" @@ -745,11 +875,15 @@ msgstr "Adresa de email este deja validată." msgid "errors.email-as-password" msgstr "Nu vă puteți folosi e-mailul ca parolă" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" msgstr "Adresa de email «%s» are multe rapoarte permanente de respingere." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, +#: src/app/main/ui/auth/recovery_request.cljs msgid "errors.email-invalid" msgstr "Vă rugăm să introduceți un e-mail valid" @@ -770,7 +904,8 @@ msgstr "" msgid "errors.feature-not-supported" msgstr "Funcția \"%s\" nu este acceptată." -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.generic" msgstr "S-a întâmplat ceva în neregulă." @@ -801,7 +936,7 @@ msgstr "Imaginea este prea mare pentru a fi inserată." msgid "errors.media-type-mismatch" msgstr "Se pare că conținutul imaginii nu se potrivește cu extensia de fișier." -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" msgstr "Se pare că aceasta nu este o imagine validă." @@ -822,7 +957,9 @@ msgstr "Parola trebuie să conțină cel puțin 8 caractere" msgid "errors.profile-blocked" msgstr "Profilul este blocat" -#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.profile-is-muted" msgstr "" "Profilul tău conține adrese de email dezactivate (rapoarte spam sau " @@ -845,7 +982,9 @@ msgstr "" "Proprietarul nu poate părăsi echipa, trebuie să reatribuiți rolul de " "proprietar." -#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/data/media.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "errors.unexpected-error" msgstr "A apărut o eroare neașteptată." @@ -916,7 +1055,7 @@ msgstr "Adresă de Email" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Accesați Twitter" +msgstr "Accesați X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -924,7 +1063,7 @@ msgstr "Aici pentru a vă ajuta cu întrebările tehnice." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Cont de asistență Twitter" +msgstr "Cont de asistență X" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -978,7 +1117,8 @@ msgstr "Înălțime" msgid "inspect.attributes.layout.left" msgstr "Stânga" -#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs +#: src/app/main/ui/inspect/attributes/layout.cljs, +#: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.layout.radius" msgstr "Rază" @@ -1006,15 +1146,12 @@ msgstr "Dimensiune și poziție" msgid "inspect.attributes.stroke" msgstr "Linie" -#, permanent msgid "inspect.attributes.stroke.alignment.center" msgstr "Centru" -#, permanent msgid "inspect.attributes.stroke.alignment.inner" msgstr "Interior" -#, permanent msgid "inspect.attributes.stroke.alignment.outer" msgstr "Exterior" @@ -1050,6 +1187,10 @@ msgstr "Dimensiune Font" msgid "inspect.attributes.typography.font-style" msgstr "Stil Font" +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-weight" +msgstr "Greutate Font" + #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.letter-spacing" msgstr "Spațiere" @@ -1152,13 +1293,17 @@ msgstr "Comenzi rapide" msgid "labels.accept" msgstr "Acceptă" +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "Jetoane acces" + msgid "labels.active" msgstr "Activ" msgid "labels.add-custom-font" msgstr "Adăugați font personalizat" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "Administrator" @@ -1216,7 +1361,8 @@ msgstr "Copiați link-ul" msgid "labels.create" msgstr "Creează" -#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team" msgstr "Creează o echipă" @@ -1231,7 +1377,8 @@ msgstr "Fonturi personalizate" msgid "labels.dashboard" msgstr "Administrare" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "Șterge" @@ -1251,7 +1398,13 @@ msgstr "Șterge invitație" msgid "labels.delete-multi-files" msgstr "Șterge %s fișiere" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.discard" +msgstr "Anulați" + +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/files.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.drafts" msgstr "Drafturi" @@ -1262,7 +1415,7 @@ msgstr "Editează" msgid "labels.edit-file" msgstr "Editează fișier" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "Editor" @@ -1297,7 +1450,9 @@ msgstr "Fonturi" msgid "labels.github-repo" msgstr "Repozitoriu Github" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "Lasă un feedback" @@ -1328,7 +1483,8 @@ msgstr "" msgid "labels.internal-error.main-message" msgstr "Eroare internă" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.invitations" msgstr "Invitații" @@ -1347,11 +1503,11 @@ msgstr "Conectați-vă sau înregistrați-vă" msgid "labels.logout" msgstr "Deconectare" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.member" msgstr "Membru" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "Membri" @@ -1359,7 +1515,8 @@ msgstr "Membri" msgid "labels.new-password" msgstr "Parolă nouă" -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +#: src/app/main/ui/workspace/comments.cljs, +#: src/app/main/ui/dashboard/comments.cljs msgid "labels.no-comments-available" msgstr "Sunteți la zi! Notificările de comentarii noi vor apărea aici." @@ -1368,7 +1525,6 @@ msgid "labels.no-invitations" msgstr "Nu există invitații în așteptare." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "" "Faceți clic pe butonul **Invitați persoane** pentru a invita persoane în " @@ -1417,7 +1573,8 @@ msgstr "sau" msgid "labels.owner" msgstr "Autor" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.password" msgstr "Parola" @@ -1425,7 +1582,8 @@ msgstr "Parola" msgid "labels.pending-invitation" msgstr "În curs" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.profile" msgstr "Profil" @@ -1441,7 +1599,8 @@ msgstr "Mențiuni" msgid "labels.reload-file" msgstr "Reîncărcați fișierul" -#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/workspace/libraries.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" msgstr "Elimină" @@ -1449,7 +1608,9 @@ msgstr "Elimină" msgid "labels.remove-member" msgstr "Eliminați membru" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.rename" msgstr "Redenumire" @@ -1461,7 +1622,7 @@ msgstr "Modifică numele echipei" msgid "labels.resend-invitation" msgstr "Retrimite invitația" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Încearcă din nou" @@ -1491,11 +1652,12 @@ msgstr "Momentan suntem în mentenanță." msgid "labels.service-unavailable.main-message" msgstr "Serviciul nu este disponibil" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.settings" msgstr "Setări" -#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs +#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.share-prototype" msgstr "Distribuie link" @@ -1557,7 +1719,7 @@ msgstr "Webhook-uri" msgid "labels.write-new-comment" msgstr "Scrie un comentariu" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.you" msgstr "(tu)" @@ -1565,21 +1727,24 @@ msgstr "(tu)" msgid "labels.your-account" msgstr "Contul tău" -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "Încarcă imaginea…" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" msgstr "Adaugă la Colecții distribuite" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.hint" msgstr "" "O dată adăugat la Colecții distribuite, toate fișierele acestei colecții " "vor deveni disponibile altora." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.message" msgstr "Adaugă “%s” la Colecții Distribuite" @@ -1607,6 +1772,30 @@ msgstr "Schimbă adresa de e-mail" msgid "modals.change-email.title" msgstr "Schimbă-ți adresa de E-mail" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "Copiați jeton" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "Dată de expirare" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "Nume" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "Numele vă poate ajuta să știți pentru ce este folosit jetonul" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "Creați jeton" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "Generați jeton acces" + msgid "modals.create-webhook.submit-label" msgstr "Creați webhook" @@ -1619,6 +1808,18 @@ msgstr "URL a încărcăturii" msgid "modals.create-webhook.url.placeholder" msgstr "https://example.com/postreceive" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "Ștergeți jeton" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "Sunteți sigur că doriți să ștergeți acest jeton?" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "Ștergeți jeton" + #: src/app/main/ui/settings/delete_account.cljs msgid "modals.delete-account.cancel" msgstr "Anulează ștergerea contului" @@ -1649,6 +1850,12 @@ msgstr "" msgid "modals.delete-comment-thread.title" msgstr "Șterge conversație" +msgid "modals.delete-component-annotation.message" +msgstr "Sunteți sigur că doriți să ștergeți această notă?" + +msgid "modals.delete-component-annotation.title" +msgstr "Ștergeți notă" + #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-confirm.accept" msgstr "Șterge fișier" @@ -1709,7 +1916,8 @@ msgstr "Ești sigur că dorești să ștergi acest proiect?" msgid "modals.delete-project-confirm.title" msgstr "Șterge proiect" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.accept" msgid_plural "modals.delete-shared-confirm.accept" msgstr[0] "Șterge fișier" @@ -1717,72 +1925,29 @@ msgstr[1] "Șterge fișiere" msgstr[2] "Șterge fișierele" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "" -"Dacă îl ștergeți, acele elemente nu vor mai fi disponibile din alte fișiere. " -"Elementele care au fost deja utilizate vor rămâne în acest fișier (niciun " -"design nu va fi stricat!)." -msgstr[1] "" -"Dacă le ștergeți, acele elemente nu vor mai fi disponibile din alte fișiere. " -"Elementele care au fost deja utilizate vor rămâne în acest fișier (niciun " -"design nu va fi stricat!)." -msgstr[2] "" -"Dacă le ștergeți, acele elemente nu vor mai fi disponibile din alte fișiere. " -"Elementele care au fost deja utilizate vor rămâne în acest fișier (niciun " -"design nu va fi stricat!)." +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "Nu este activat în niciun fișier." +msgstr[1] "Nu sunt activate în niciun fișier." +msgstr[2] "Nu sunt activate în niciun fișier." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "" -"Dacă îl ștergeți, acele elemente nu vor mai fi disponibile din alte fișiere. " -"Elementele care au fost deja utilizate vor rămâne în acest fișier (niciun " -"design nu va fi stricat!)." -msgstr[1] "" -"Dacă le ștergeți, acele elemente nu vor mai fi disponibile din alte fișiere. " -"Elementele care au fost deja utilizate vor rămâne în acest fișier (niciun " -"design nu va fi stricat!)." -msgstr[2] "" -"Dacă le ștergeți, acele elemente nu vor mai fi disponibile din alte fișiere. " -"Elementele care au fost deja utilizate vor rămâne în acest fișier (niciun " -"design nu va fi stricat!)." +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "Această librărie este activată aici: " +msgstr[1] "Aceste librării sunt activate aici: " +msgstr[2] "Aceste librării sunt activate aici: " -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "Ești sigur că vrei sa ștergi acest fișier?" msgstr[1] "Ești sigur că vrei sa ștergi aceste fișiere?" msgstr[2] "Ești sigur că vrei sa ștergi aceste fișiere?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" -msgstr[0] "" -"Niciunul dintre elementele din biblioteca acestui fișier nu este în uz. Ele " -"vor fi șterse împreună cu fișierul." -msgstr[1] "" -"Niciunul dintre elementele din biblioteca acestor fișiere nu este în uz. " -"Ele vor fi șterse împreună cu fișierele." -msgstr[2] "" -"Niciunul dintre elementele din biblioteca acestor fișiere nu este în uz. " -"Ele vor fi șterse împreună cu fișierele." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Unele elemente din biblioteca acestui fișier sunt în uz aici:" -msgstr[1] "Unele elemente din biblioteca acestor fișiere sunt în uz aici:" -msgstr[2] "Unele elemente din biblioteca acestor fișiere sunt în uz aici:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "Unele dintre elementele din biblioteca acestui fișier sunt utilizate aici:" -msgstr[1] "Unele dintre elementele din biblioteca acestor fișiere sunt utilizate aici:" -msgstr[2] "Unele dintre elementele din biblioteca acestor fișiere sunt utilizate aici:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" msgstr[0] "Se șterge fișierul" @@ -1815,6 +1980,19 @@ msgstr "Ești sigur că dorești să elimini acest membru din echipă?" msgid "modals.delete-team-member-confirm.title" msgstr "Elimină un membru al echipei" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" +msgstr[0] "" +"Obiectele care au fost folosite deja în acest fișier vor rămâne acolo " +"(niciun design nu va fi stricat)." +msgstr[1] "" +"Obiectele care au fost folosite deja în aceste fișiere vor rămâne acolo " +"(niciun design nu va fi stricat)." +msgstr[2] "" +"Obiectele care au fost folosite deja în aceste fișiere vor rămâne acolo " +"(niciun design nu va fi stricat)." + msgid "modals.delete-webhook.accept" msgstr "Ștergeți webhook" @@ -1837,6 +2015,11 @@ msgstr "Trimite invitație" msgid "modals.invite-member.emails" msgstr "E-mailuri, separate prin virgulă" +msgid "modals.invite-member.repeated-invitation" +msgstr "" +"Unele e-mailuri provin de la membri actuali ai echipei. Invitațiile lor nu " +"vor fi trimise." + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-team-member.title" msgstr "Invitați membri în echipă" @@ -1906,17 +2089,29 @@ msgstr "Ești sigur că dorești să promovezi acest utilizator ca deținător a msgid "modals.promote-owner-confirm.title" msgstr "Confirmare promovare" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.publish-empty-library.accept" +msgstr "Publicați" + +msgid "modals.publish-empty-library.message" +msgstr "Librăria dvs. este goală. Sunteți sigur că doriți să o publicați?" + +msgid "modals.publish-empty-library.title" +msgstr "Publicați librărie goală" + +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" msgstr "Elimină din Colecțiile Distribuite" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.hint" msgstr "" "O dată șters din Colecțiile Distribuite, toate fișierele acestei colecții " "nu vor mai fi disponibile altora." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.message" msgstr "Șterge “%s” din Colecții Distribuite" @@ -1924,101 +2119,68 @@ msgstr "Șterge “%s” din Colecții Distribuite" msgid "modals.small-nudge" msgstr "Înghiont mic" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Dacă anulați publicarea, acele elemente nu vor mai fi valabile din alte " -"fișiere. Elementele care au fost deja utilizate vor rămâne în acest fișier (" -"niciun proiect nu va fi distrus!)." -msgstr[1] "" -"Dacă anulați publicarea lor, acele elemente nu vor mai fi valabile din alte " -"fișiere. Elementele care au fost deja utilizate vor rămâne în acest fișier (" -"niciun proiect nu va fi distrus!)." -msgstr[2] "" -"Dacă anulați publicarea lor, acele elemente nu vor mai fi valabile din alte " -"fișiere. Elementele care au fost deja utilizate vor rămâne în acest fișier (" -"niciun proiect nu va fi distrus!)." +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.accept" +msgid_plural "modals.unpublish-shared-confirm.accept" +msgstr[0] "Anulați publicarea" +msgstr[1] "Anulați publicarea" +msgstr[2] "Anulați publicarea" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "" -"Dacă anulați publicarea, acele elemente nu vor mai fi valabile din alte " -"fișiere. Elementele care au fost deja utilizate vor rămâne în acest fișier (" -"niciun proiect nu va fi distrus!)." -msgstr[1] "" -"Dacă anulați publicarea lor, acele elemente nu vor mai fi valabile din alte " -"fișiere. Elementele care au fost deja utilizate vor rămâne în acest fișier (" -"niciun proiect nu va fi distrus!)." -msgstr[2] "" -"Dacă anulați publicarea lor, acele elemente nu vor mai fi valabile din alte " -"fișiere. Elementele care au fost deja utilizate vor rămâne în acest fișier (" -"niciun proiect nu va fi distrus!)." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "Sigur doriți să anulați publicarea acestei biblioteci?" msgstr[1] "Sigur doriți să anulați publicarea acestor biblioteci?" msgstr[2] "Sigur doriți să anulați publicarea acestor biblioteci?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "Niciunul dintre materialele din această bibliotecă nu este utilizat." -msgstr[1] "Niciunul dintre materialele din aceste biblioteci nu este utilizat." -msgstr[2] "Niciunul dintre materialele din aceste biblioteci nu este utilizat." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Unele dintre elementele din această bibliotecă sunt în uz aici:" -msgstr[1] "Unele dintre elementele din aceste biblioteci sunt în uz aici:" -msgstr[2] "Unele dintre elementele din aceste biblioteci sunt în uz aici:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "Unele dintre materialele din această bibliotecă sunt utilizate aici:" -msgstr[1] "Unele dintre materialele din aceste biblioteci sunt utilizate aici:" -msgstr[2] "Unele dintre materialele din aceste biblioteci sunt utilizate aici:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" msgid_plural "modals.unpublish-shared-confirm.title" msgstr[0] "Anulează publicarea bibliotecii" msgstr[1] "Anulează publicarea bibliotecilor" msgstr[2] "Anulează publicarea bibliotecilor" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.hint" msgstr "" "Ești pe cale de a actualiza componentele dintr-o bibliotecă partajată. " "Acest lucru poate afecta alte fișiere care o folosesc." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.message" msgstr "Actualizează componentele într-o bibliotecă partajată" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.accept" msgstr "Actualizare componentă" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.cancel" msgstr "Anulează" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.hint" msgstr "" "Actualizezi o componentă dintr-o colecţie distribuită. Pot fi afectate alte " "fişiere ce o folosesc." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.message" msgstr "Actualizaţi o componentă dintr-o colecţie distribuită" +#: src/app/main/data/common.cljs +msgid "notifications.by-code.upgrade-version" +msgstr "O versiune nouă este valabilă, vă rugăm să reîncărcați pagina" + #: src/app/main/ui/dashboard/team.cljs msgid "notifications.invitation-email-sent" msgstr "Invitaţie trimisă cu succes" @@ -2110,12 +2272,6 @@ msgstr "Ghid de contribuție" msgid "onboarding-v2.welcome.title" msgstr "Bun venit la Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "Creați o echipă mai târziu" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Numele echipei tale" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "După ce îți denumești echipa, vei putea invita oameni să se alăture." @@ -2130,12 +2286,6 @@ msgstr "" "Nu uitați să includeți pe toată lumea. Dezvoltatori, designeri, manageri... " "diversitatea se adaugă :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Creați o echipă și invitați mai târziu" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Creați o echipă și trimiteți invitații" - msgid "onboarding.choice.team-up.roles" msgstr "Invitați cu rolul:" @@ -2189,7 +2339,181 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "Mergi la autentificare" -#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "Care este unealta de design cu care aveți mai multă experiență?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11-30" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2-10" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31-50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "Multe" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canva" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.describe-your-experience-working-on" +msgstr "Cum ați descrie cel mai bine experiența dvs. lucrând pe..." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "Designer" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "Dezvoltator" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "Descoperiți mai multe despre Penpot" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "Fondator/VP" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "Sunt un freelancer" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "Obțineți codul de la proiectul echipei mele " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "... design interfețe, obiecte vizuale, sisteme de design, etc." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "InVision" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "Lăsați feedback pentru proiectul echipei mele" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "Să începem!" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "Manager de produs sau proiect" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "Marketing" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "Mai mult de 50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "Următor" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "Niciuna" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "Alta (specificați)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "Lucrez la un proiect personal" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "Anterior" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "Cum planificați să folosiți Penpot?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "Care este rolul dvs?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.select-option" +msgstr "Selectați opțiune" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "Sketch" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.some" +msgstr "Câteva" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "Începeți" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "Începeți să lucrați în proiectul meu" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "Student sau profesor" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "Care este mărimea echipei dvs?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "Testați Penpot pentru a vedea dacă se potrivește echipei " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "Probați înainte de a folosi Penpot la fața locului" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" +msgstr "" +"... prototipuri, călătorii ale utilizatorilor & fluxuri, arbori de " +"navigație, etc." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "Lucrați în conceptualizarea ideilor" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" +msgstr "" +"Feedback-ul dvs. ne va ajuta să înțelegem care sunt obiceiurile și " +"preferințele dvs. pentru a putea continua să facem Penpot o unealtă " +"folositoare și plăcută." + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "Multiple" @@ -2243,6 +2567,9 @@ msgstr "Drumuri" msgid "shortcut-subsection.shape" msgstr "Forme" +msgid "shortcut-subsection.text-editor" +msgstr "Texte" + msgid "shortcut-subsection.tools" msgstr "Unelte" @@ -2261,9 +2588,15 @@ msgstr "Adaugă nod" msgid "shortcuts.align-bottom" msgstr "Aliniați jos" +msgid "shortcuts.align-center" +msgstr "Aliniere la centru" + msgid "shortcuts.align-hcenter" msgstr "Aliniați centrul orizontal" +msgid "shortcuts.align-justify" +msgstr "Aliniere justificată" + msgid "shortcuts.align-left" msgstr "Aliniați stânga" @@ -2279,6 +2612,9 @@ msgstr "Aliniați centrul vertical" msgid "shortcuts.artboard-selection" msgstr "Creează tablă din selecție" +msgid "shortcuts.bold" +msgstr "Comutare bold" + msgid "shortcuts.bool-difference" msgstr "Diferența booleană" @@ -2369,6 +2705,12 @@ msgstr "Întoarceți pe orizontală" msgid "shortcuts.flip-vertical" msgstr "Întoarceți pe verticală" +msgid "shortcuts.font-size-dec" +msgstr "Reducere dimensiune font" + +msgid "shortcuts.font-size-inc" +msgstr "Creștere dimensiune font" + msgid "shortcuts.go-to-drafts" msgstr "Accesați schițele" @@ -2393,9 +2735,27 @@ msgstr "Măriți" msgid "shortcuts.insert-image" msgstr "Inserați imagine" +msgid "shortcuts.italic" +msgstr "Comutare cursiv" + msgid "shortcuts.join-nodes" msgstr "Uniți noduri" +msgid "shortcuts.letter-spacing-dec" +msgstr "Reducere spațiere litere" + +msgid "shortcuts.letter-spacing-inc" +msgstr "Creștere spațiere litere" + +msgid "shortcuts.line-height-dec" +msgstr "Reducere înălțime linie" + +msgid "shortcuts.line-height-inc" +msgstr "Creștere înălțime linie" + +msgid "shortcuts.line-through" +msgstr "Comutare tăiere" + msgid "shortcuts.make-corner" msgstr "Faceți colț" @@ -2516,6 +2876,15 @@ msgstr "Căutați comenzi rapide" msgid "shortcuts.select-all" msgstr "Selectează tot" +msgid "shortcuts.select-next" +msgstr "Selectare strat următor" + +msgid "shortcuts.select-parent-layer" +msgstr "Selectați strat părinte" + +msgid "shortcuts.select-prev" +msgstr "Selectare strat anterior" + msgid "shortcuts.separate-nodes" msgstr "Noduri separate" @@ -2540,6 +2909,18 @@ msgstr "Începeți măsurarea" msgid "shortcuts.stop-measure" msgstr "Opriți măsurarea" +msgid "shortcuts.text-align-center" +msgstr "Aliniați la centru" + +msgid "shortcuts.text-align-justify" +msgstr "Aliniați justificat" + +msgid "shortcuts.text-align-left" +msgstr "Aliniați la stânga" + +msgid "shortcuts.text-align-right" +msgstr "Aliniați la dreapta" + msgid "shortcuts.thumbnail-set" msgstr "Setați miniaturile" @@ -2562,9 +2943,6 @@ msgstr "Comutați modul de focus" msgid "shortcuts.toggle-fullscreen" msgstr "Comutați la ecran complet" -msgid "shortcuts.toggle-grid" -msgstr "Afișați/ascundeți grila" - msgid "shortcuts.toggle-history" msgstr "Comutați istoricul" @@ -2583,21 +2961,18 @@ msgstr "Blocați proporțiile" msgid "shortcuts.toggle-rules" msgstr "Afișați/ascundeți rigle" -msgid "shortcuts.toggle-scale-text" -msgstr "Comută scalarea textului" - -msgid "shortcuts.toggle-snap-grid" -msgstr "Fixare la grilă" - -msgid "shortcuts.toggle-snap-guide" -msgstr "Fixare la ghiduri" - msgid "shortcuts.toggle-textpalette" msgstr "Comutați paleta de text" +msgid "shortcuts.toggle-visibility" +msgstr "Comutați vizibilitatea" + msgid "shortcuts.toggle-zoom-style" msgstr "Comutați stilul zoomului" +msgid "shortcuts.underline" +msgstr "Comutare subliniere" + msgid "shortcuts.undo" msgstr "Anulați" @@ -2610,9 +2985,19 @@ msgstr "Demascați" msgid "shortcuts.v-distribute" msgstr "Distribuiți vertical" +msgid "shortcuts.zoom-lense-decrease" +msgstr "Reducere obiectiv de zoom" + +msgid "shortcuts.zoom-lense-increase" +msgstr "Creștere obiectiv de zoom" + msgid "shortcuts.zoom-selected" msgstr "Mărește la selecție" +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "Numele webhook-ului trebuie să conțină maxim 2048 caractere." + #: src/app/main/ui/dashboard/files.cljs msgid "title.dashboard.files" msgstr "%s - Penpot" @@ -2641,6 +3026,10 @@ msgstr "Biblioteci Distribuite - %s - Penpot" msgid "title.default" msgstr "Penpot - Libertate în Design pentru Echipe" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "Profil - Jetoane acces" + #: src/app/main/ui/settings/feedback.cljs msgid "title.settings.feedback" msgstr "Oferă feedback - Penpot" @@ -2776,11 +3165,13 @@ msgstr "Obiecte" msgid "workspace.assets.box-filter-all" msgstr "Toate obiectele" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.colors" msgstr "Culori" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.components" msgstr "Componente" @@ -2792,19 +3183,27 @@ msgstr "Creează grup" msgid "workspace.assets.create-group-hint" msgstr "Obiectele vor fi numite automat ca \"nume grup / nume obiect\"" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "Şterge" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.duplicate" msgstr "Duplică" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.duplicate-main" +msgstr "Duplicare principală" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" msgstr "Editează" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.graphics" msgstr "Obiecte grafice" @@ -2827,7 +3226,12 @@ msgstr "biblioteca locală" msgid "workspace.assets.not-found" msgstr "Nu au fost găsite obiecte" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.open-library" +msgstr "Deschideți fișier librărie" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename" msgstr "Redenumeşte" @@ -2847,10 +3251,11 @@ msgstr[1] "%s obiecte selectate" msgstr[2] "%s obiecte selectate" #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "DISTRIBUITE" +msgid "workspace.assets.shared-library" +msgstr "Librărie partajată" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "Tipografii" @@ -2878,7 +3283,9 @@ msgstr "Spaţiere Litere" msgid "workspace.assets.typography.line-height" msgstr "Înălţime linie" -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/inspect/attributes/text.cljs, src/app/main/ui/inspect/attributes/text.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs msgid "workspace.assets.typography.sample" msgstr "Ag" @@ -2905,11 +3312,13 @@ msgstr "Focus pornit" msgid "workspace.focus.selection" msgstr "Selecție" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "Gradient liniar" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.radial" msgstr "Gradient radial" @@ -2917,14 +3326,13 @@ msgstr "Gradient radial" msgid "workspace.header.menu.disable-dynamic-alignment" msgstr "Dezactivează alinierea dinamică" +msgid "workspace.header.menu.disable-scale-content" +msgstr "Dezactivare scară proporțională" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-scale-text" msgstr "Dezactivează dimensionarea textului" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Dezactivați snap-ul la grilă" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "Dezactivați fixarea la ghiduri" @@ -2936,14 +3344,13 @@ msgstr "Dezactivați fixarea la pixel" msgid "workspace.header.menu.enable-dynamic-alignment" msgstr "Aliniere dinamică" +msgid "workspace.header.menu.enable-scale-content" +msgstr "Activare scară proporțională" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-scale-text" msgstr "Activează scalarea textului" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Aliniază per grilă" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "Fixare la ghiduri" @@ -2955,10 +3362,6 @@ msgstr "Activați fixarea la pixel" msgid "workspace.header.menu.hide-artboard-names" msgstr "Ascundeți numele tablelor" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Ascunde grila de ghidaj" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Ascunde paleta de culori" @@ -2994,6 +3397,9 @@ msgstr "Preferințe" msgid "workspace.header.menu.option.view" msgstr "Vezi" +msgid "workspace.header.menu.redo" +msgstr "Refacere" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.select-all" msgstr "Selectează tot" @@ -3002,10 +3408,6 @@ msgstr "Selectează tot" msgid "workspace.header.menu.show-artboard-names" msgstr "Afișați numele tablelor" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Afişează sistemul grid" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Afişează paleta de culori" @@ -3021,6 +3423,9 @@ msgstr "Afişează Liniarul" msgid "workspace.header.menu.show-textpalette" msgstr "Afișați paleta de fonturi" +msgid "workspace.header.menu.undo" +msgstr "Anulare" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "Resetați" @@ -3045,6 +3450,10 @@ msgstr "Modificări nesalvate" msgid "workspace.header.viewer" msgstr "Vizualizare (%s)" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "Zoom" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.zoom-fill" msgstr "Umplere - Scalare pentru a umple" @@ -3065,6 +3474,9 @@ msgstr "Ecran complet" msgid "workspace.header.zoom-selected" msgstr "Zoom la selecție" +msgid "workspace.layout_grid.editor.title" +msgstr "Editare grilă" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.add" msgstr "Adaugă" @@ -3073,7 +3485,16 @@ msgstr "Adaugă" msgid "workspace.libraries.colors" msgstr "%s culori" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "Nu există stiluri de culori în librăria dvs. încă" + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "Nu există stiluri de tipografie în librăria dvs. încă" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" msgstr "Bibliotecă de fișiere" @@ -3081,7 +3502,8 @@ msgstr "Bibliotecă de fișiere" msgid "workspace.libraries.colors.hsv" msgstr "HSV" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "Culori recente" @@ -3121,6 +3543,10 @@ msgstr "BIBLIOTECI" msgid "workspace.libraries.library" msgstr "BIBLIOTECĂ" +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "ACTUALIZĂRI LIBRĂRII" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.no-libraries-need-sync" msgstr "Nu există Biblioteci Distribuite ce necesită update" @@ -3157,6 +3583,10 @@ msgstr "%s tipografii" msgid "workspace.libraries.update" msgstr "Actualizați" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "vedeți toate schimbările" + #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.updates" msgstr "ACTUALIZĂRI" @@ -3188,6 +3618,15 @@ msgstr "Conținutul clipului" msgid "workspace.options.component" msgstr "Componentă" +msgid "workspace.options.component.annotation" +msgstr "Notă" + +msgid "workspace.options.component.create-annotation" +msgstr "Creați o notă" + +msgid "workspace.options.component.edit-annotation" +msgstr "Editați o notă" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.constraints" msgstr "Constrângeri" @@ -3232,31 +3671,45 @@ msgstr "Sus & Jos" msgid "workspace.options.design" msgstr "Design" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export" msgstr "Exportă" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs msgid "workspace.options.export-multiple" msgstr "Exportați selecția" +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs +msgid "workspace.options.export-object" +msgid_plural "workspace.options.export-object" +msgstr[0] "Exportați 1 element" +msgstr[1] "Exportați %s elemente" +msgstr[2] "Exportați %s elemente" + #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs msgid "workspace.options.export.suffix" msgstr "Sufix" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-complete" msgstr "Export finalizat" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.exporting-object" msgstr "Se exportă…" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-error" msgstr "Export eșuat" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-slow" msgstr "Export neașteptat de lent" @@ -3712,10 +4165,18 @@ msgstr "Jos" msgid "workspace.options.layout.direction.column" msgstr "Coloană" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column-reverse" +msgstr "Coloană inversată" + #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.direction.row" msgstr "Rând" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row-reverse" +msgstr "Rând inversat" + #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.gap" msgstr "Spațiu" @@ -3779,7 +4240,8 @@ msgstr "Mai multe culori de bibliotecă" msgid "workspace.options.opacity" msgstr "Opacitate" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Poziţie" @@ -3791,12 +4253,12 @@ msgid "workspace.options.radius" msgstr "Rază" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Toate colţurile" +msgid "workspace.options.radius-bottom-left" +msgstr "Stânga jos" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Colțuri independente" +msgid "workspace.options.radius-bottom-right" +msgstr "Dreapta jos" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3807,17 +4269,18 @@ msgid "workspace.options.radius-top-right" msgstr "Dreapta sus" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Stânga jos" +msgid "workspace.options.radius.all-corners" +msgstr "Toate colţurile" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Dreapta jos" +msgid "workspace.options.radius.single-corners" +msgstr "Colțuri independente" msgid "workspace.options.recent-fonts" msgstr "Recente" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" msgstr "Reîncercați" @@ -3890,7 +4353,8 @@ msgstr "Afișați în exporturi" msgid "workspace.options.show-in-viewer" msgstr "Afișare în modul de vizualizare" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.size" msgstr "Mărime" @@ -3972,26 +4436,10 @@ msgstr "Solid" msgid "workspace.options.text-options.align-bottom" msgstr "Aliniază jos" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Aliniază centru (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Justifică (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Aliniază la stânga (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Aliniază la mijloc" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Aliniază la dreapta (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Aliniază sus" @@ -4028,7 +4476,8 @@ msgstr "Înălţime linii" msgid "workspace.options.text-options.lowercase" msgstr "Minuscule" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.none" msgstr "Nici unul" @@ -4036,6 +4485,22 @@ msgstr "Nici unul" msgid "workspace.options.text-options.strikethrough" msgstr "Barat (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Aliniază centru (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Justifică (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Aliniază la stânga (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Aliniază la dreapta (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Text" @@ -4103,39 +4568,13 @@ msgstr "Separă noduri (%s)" msgid "workspace.path.actions.snap-nodes" msgstr "Trage noduri (%s)" -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "" -"Pentru a încerca din nou, puteți reîncărca acest fișier. Dacă problema " -"persistă, vă sugerăm să aruncați o privire pe listă și să luați în " -"considerare ștergerea graficii rupte." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "Unele elemente grafice nu au putut fi actualizate." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "Se convertește %s/%s" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "" -"Bibliotecile Grafice sunt Componente de acum înainte, ceea ce le va face " -"mult mai puternice." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "Această actualizare este o acțiune unică." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "Actualizare %s..." - #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.add-flex" msgstr "Adăugați aspect flexibil" +msgid "workspace.shape.menu.add-grid" +msgstr "Adăugați aspect grilă" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "Trimite înapoi" @@ -4148,6 +4587,9 @@ msgstr "Trimite în urmă" msgid "workspace.shape.menu.copy" msgstr "Copiază" +msgid "workspace.shape.menu.create-annotation" +msgstr "Creați notă" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.create-artboard-from-selection" msgstr "Selecție la planșă" @@ -4156,6 +4598,9 @@ msgstr "Selecție la planșă" msgid "workspace.shape.menu.create-component" msgstr "Creează componentă" +msgid "workspace.shape.menu.create-multiple-components" +msgstr "Creați componente multiple" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.cut" msgstr "Taie" @@ -4168,11 +4613,15 @@ msgstr "Şterge" msgid "workspace.shape.menu.delete-flow-start" msgstr "Ștergeți începutul fluxului" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Detaşează instanţă" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instances-in-bulk" msgstr "Detașați instanțele" @@ -4213,7 +4662,8 @@ msgstr "Aduceţi înainte" msgid "workspace.shape.menu.front" msgstr "Aduceţi în faţă" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.go-main" msgstr "Mergi la componenta principală" @@ -4235,11 +4685,13 @@ msgstr "Intersecție" msgid "workspace.shape.menu.lock" msgstr "Blochează" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.mask" msgstr "Maschează" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.paste" msgstr "Lipeşte" @@ -4250,7 +4702,9 @@ msgstr "Drum" msgid "workspace.shape.menu.remove-flex" msgstr "Îndepărtați aspect flexibil" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "Resetează suprascrierile" @@ -4265,11 +4719,13 @@ msgstr "Selectați stratul" msgid "workspace.shape.menu.show" msgstr "Afişează" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-in-assets" msgstr "Afișați în panoul de obiecte" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-main" msgstr "Afişează componenta principală" @@ -4297,11 +4753,15 @@ msgstr "Deblochează" msgid "workspace.shape.menu.unmask" msgstr "Demaschează" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-components-in-bulk" msgstr "Actualizați componentele principale" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-main" msgstr "Actualizaţi principala componentă" @@ -4343,7 +4803,8 @@ msgstr "Forme" msgid "workspace.sidebar.layers.texts" msgstr "Texte" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/inspect/attributes/svg.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, +#: src/app/main/ui/inspect/attributes/svg.cljs msgid "workspace.sidebar.options.svg-attrs.title" msgstr "Atribute SVG importate" @@ -4427,7 +4888,7 @@ msgid "workspace.undo.entry.multiple.circle" msgstr "cercuri" msgid "workspace.undo.entry.multiple.color" -msgstr "Culori" +msgstr "Culori obiecte" msgid "workspace.undo.entry.multiple.component" msgstr "componente" @@ -4526,6 +4987,10 @@ msgstr "Istoric" msgid "workspace.updates.dismiss" msgstr "Renunţă" +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "Mai multe informații" + #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.there-are-updates" msgstr "Există actualizări în bibliotecile distribuite" @@ -4537,123 +5002,44 @@ msgstr "Actualizează" msgid "workspace.viewport.click-to-close-path" msgstr "Click pentru a închide calea" -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-delete-file" -msgid_plural "dashboard.success-delete-file" -msgstr[0] "Fișierul dumneavoastră a fost șters cu succes" -msgstr[1] "Fișierele dumneavoastră au fost șterse cu succes" -msgstr[2] "Fișierele dumneavoastră au fost șterse cu succes" +msgid "dashboard.import.import-message" +msgid_plural "dashboard.import.import-message" +msgstr[0] "1 fișier a fost importat cu succes." +msgstr[1] "% fișiere au fost importate cu succes." +msgstr[2] "% de fișiere au fost importate cu succes." -#, markdown -msgid "dashboard.fonts.warning-text" +msgid "workspace.options.component.copy" +msgstr "Copiați" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "Dreptunghi" + +msgid "workspace.options.component.main" +msgstr "Principal" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "Diamant" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +msgid "settings.detach" +msgstr "Detașați" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "Triunghi" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "Săgeată" + +msgid "modals.add-shared-confirm-empty.hint" msgstr "" -"Am detectat o posibilă problemă în fonturile dvs. în legătură cu metricile " -"verticale pentru diferite sisteme operaționale. Pentru a o verifica, puteți " -"utiliza servicii de măsurare verticală a fonturilor, cum ar fi [acesta](https" -"://vertical-metrics.netlify.app/). În plus, vă recomandăm să utilizați " -"[Transfonter](https://transfonter.org/) pentru a genera fonturi web și a " -"remedia erorile. " +"Biblioteca dumneavoastră este goală. Odată adăugate ca bibliotecă partajată, " +"obiectele pe care le creați vor fi disponibile pentru a fi utilizate în " +"celelalte fișiere. Sunteți sigur că doriți să o publicați?" -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-weight" -msgstr "Greutate Font" - -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-duplicate-file" -msgid_plural "dashboard.success-delete-file" -msgstr[0] "Fișierul dumneavoastră a fost duplicat cu succes" -msgstr[1] "Fișierele dumneavoastră au fost duplicate cu succes" -msgstr[2] "Fișierele dumneavoastră au fost duplicate cu succes" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.accept" -msgid_plural "modals.unpublish-shared-confirm.accept" -msgstr[0] "Anulați publicarea" -msgstr[1] "Anulați publicarea" -msgstr[2] "Anulați publicarea" - -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs -msgid "workspace.options.export-object" -msgid_plural "workspace.options.export-object" -msgstr[0] "Exportați 1 element" -msgstr[1] "Exportați %s elemente" -msgstr[2] "Exportați %s elemente" - -msgid "shortcut-subsection.text-editor" -msgstr "Texte" - -msgid "shortcuts.align-justify" -msgstr "Aliniere justificată" - -msgid "workspace.assets.duplicate-main" -msgstr "Duplicare principală" - -msgid "shortcuts.line-height-inc" -msgstr "Creștere înălțime linie" - -msgid "shortcuts.zoom-lense-decrease" -msgstr "Reducere obiectiv de zoom" - -msgid "workspace.header.menu.enable-scale-content" -msgstr "Activare scară proporțională" - -msgid "workspace.header.menu.disable-scale-content" -msgstr "Dezactivare scară proporțională" - -msgid "workspace.header.menu.undo" -msgstr "Anulare" - -msgid "workspace.header.menu.redo" -msgstr "Refacere" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.row-reverse" -msgstr "Rând inversat" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.column-reverse" -msgstr "Coloană inversată" - -msgid "modals.invite-member.repeated-invitation" -msgstr "" -"Unele e-mailuri provin de la membri actuali ai echipei. Invitațiile lor nu " -"vor fi trimise." - -msgid "shortcuts.align-center" -msgstr "Aliniere la centru" - -msgid "shortcuts.bold" -msgstr "Comutare bold" - -msgid "shortcuts.font-size-dec" -msgstr "Reducere dimensiune font" - -msgid "shortcuts.font-size-inc" -msgstr "Creștere dimensiune font" - -msgid "shortcuts.italic" -msgstr "Comutare cursiv" - -msgid "shortcuts.letter-spacing-dec" -msgstr "Reducere spațiere litere" - -msgid "shortcuts.letter-spacing-inc" -msgstr "Creștere spațiere litere" - -msgid "shortcuts.line-height-dec" -msgstr "Reducere înălțime linie" - -msgid "shortcuts.line-through" -msgstr "Comutare tăiere" - -msgid "shortcuts.select-next" -msgstr "Selectare strat următor" - -msgid "shortcuts.select-prev" -msgstr "Selectare strat anterior" - -msgid "shortcuts.underline" -msgstr "Comutare subliniere" - -msgid "shortcuts.zoom-lense-increase" -msgstr "Creștere obiectiv de zoom" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "Cerc" diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index ce5bb56711..5a68e36df1 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2023-06-19 11:49+0000\n" +"PO-Revision-Date: 2024-01-23 15:01+0000\n" "Last-Translator: Stas Haas \n" "Language-Team: Russian \n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Weblate 4.18.1\n" +"X-Generator: Weblate 5.4-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -37,7 +37,8 @@ msgstr "" "Это ДЕМОНСТРАЦИЯ, НЕ ИСПОЛЬЗУЙТЕ для работы, проекты будут периодически " "удаляться." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" msgstr "Эл. почта" @@ -261,7 +262,8 @@ msgstr "Начать тур" msgid "dasboard.walkthrough-hero.title" msgstr "Руководство по интерфейсу" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Добавить как общую библиотеку" @@ -291,7 +293,8 @@ msgstr "Скачать файл Penpot (.penpot)" msgid "dashboard.download-standard-file" msgstr "Скачать стандартный файл (.svg + .json)" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate" msgstr "Дублировать" @@ -300,7 +303,6 @@ msgid "dashboard.duplicate-multi" msgstr "Дублировать файлы (%s)" #: src/app/main/ui/dashboard/grid.cljs -#, markdown msgid "dashboard.empty-placeholder-drafts" msgstr "" "Файлы, добавленные в Библиотеки, появятся здесь. Попробуйте поделиться " @@ -398,7 +400,6 @@ msgid_plural "dashboard.fonts.fonts-added" msgstr[0] "Шрифт добавлен" msgstr[1] "Шрифты добавлены (%s)" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "Любой загружаемый сюда шрифт будет добавлен в семейство шрифтов и доступен " @@ -407,7 +408,6 @@ msgstr "" "загрузки допустимы следующие форматы: **TTF, OTF и WOFF** (используйте один " "из них)." -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "Вам следует загружать только собственные шрифты, или у которых есть " @@ -460,7 +460,8 @@ msgstr "Загрузка файла: %s" msgid "dashboard.invite-profile" msgstr "Пригласить людей" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" msgstr "Покинуть команду" @@ -484,7 +485,8 @@ msgstr "загрузка ваших файлов …" msgid "dashboard.loading-fonts" msgstr "загрузка ваших шрифтов …" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "Переместить" @@ -496,7 +498,8 @@ msgstr "Переместить файлы (%s)" msgid "dashboard.move-to-other-team" msgstr "Перевести в другую команду" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" msgstr "+ Новый файл" @@ -559,7 +562,8 @@ msgstr "Проекты" msgid "dashboard.remove-account" msgstr "Хотите удалить свой аккаунт?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" msgstr "Снять статус общей библиотеки" @@ -587,6 +591,12 @@ msgstr "Выберите тему" msgid "dashboard.show-all-files" msgstr "Показать все файлы" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "Ваш файл успешно удален" +msgstr[1] "Ваши файлы успешно удалены" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-delete-project" msgstr "Ваш проект удалён" @@ -595,7 +605,8 @@ msgstr "Ваш проект удалён" msgid "dashboard.success-duplicate-project" msgstr "Ваш проект продублирован" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-file" msgstr "Ваш файл успешно перемещён" @@ -631,14 +642,26 @@ msgstr "Результаты поиска" msgid "dashboard.type-something" msgstr "Введите для поиска" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.unpublish-shared" msgstr "Снять библиотеку с публикации" -#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/profile.cljs, +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Обновить настройки" +msgid "dashboard.webhooks.active" +msgstr "Активен" + +msgid "dashboard.webhooks.content-type" +msgstr "Тип контента" + +msgid "dashboard.webhooks.create" +msgstr "Создать вебхук" + #: src/app/main/ui/settings.cljs msgid "dashboard.your-account-title" msgstr "Ваш аккаунт" @@ -651,7 +674,11 @@ msgstr "Эл. почта" msgid "dashboard.your-name" msgstr "Ваше имя" -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" msgstr "Ваш Penpot" @@ -696,7 +723,8 @@ msgstr "Шрифты %s не могут быть загружены" msgid "errors.clipboard-not-implemented" msgstr "Ваш браузер не поддерживает эту операцию" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/change_email.cljs msgid "errors.email-already-exists" msgstr "Такая эл. почта уже используется" @@ -707,7 +735,10 @@ msgstr "Эл. почта уже подтверждена." msgid "errors.email-as-password" msgstr "Нельзя указывать в качестве пароля адрес эл. почты" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" msgstr "Эл. почта «%s» постоянно недоступна." @@ -718,7 +749,8 @@ msgstr "Эл. почта для подтверждения должна совп msgid "errors.email-spam-or-permanent-bounces" msgstr "Эл. почта «%s» была отмечена как спам или постоянно недоступна." -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.generic" msgstr "Что-то пошло не так." @@ -741,7 +773,7 @@ msgstr "Изображение слишком большое для вставк msgid "errors.media-type-mismatch" msgstr "Формат медиа не соответует расширению файла." -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" msgstr "Неверное медиа." @@ -760,7 +792,9 @@ msgstr "Пароль должен быть минимум 8 символов" msgid "errors.profile-blocked" msgstr "Профиль заблокирован" -#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.profile-is-muted" msgstr "Ваш адрес электронной почты не доступен." @@ -779,7 +813,9 @@ msgstr "Участник, которого вы пытаетесь назнач msgid "errors.team-leave.owner-cant-leave" msgstr "Нужно переназначить роль владельца перед тем, как покинуть команду." -#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/data/media.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "errors.unexpected-error" msgstr "Произошла ошибка." @@ -829,7 +865,7 @@ msgstr "Эл. почта" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Перейти в Twitter" +msgstr "Перейти в X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -837,7 +873,7 @@ msgstr "Здесь, чтобы помочь с вашими технически #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Аккаунт поддержки в Twitter" +msgstr "Аккаунт поддержки в X" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -891,7 +927,8 @@ msgstr "Высота" msgid "inspect.attributes.layout.left" msgstr "Слева" -#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs +#: src/app/main/ui/inspect/attributes/layout.cljs, +#: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.layout.radius" msgstr "Радиус" @@ -915,15 +952,12 @@ msgstr "Тень" msgid "inspect.attributes.stroke" msgstr "Обводка" -#, permanent msgid "inspect.attributes.stroke.alignment.center" msgstr "Центр" -#, permanent msgid "inspect.attributes.stroke.alignment.inner" msgstr "Внутрь" -#, permanent msgid "inspect.attributes.stroke.alignment.outer" msgstr "Наружу" @@ -1051,7 +1085,7 @@ msgstr "Принять" msgid "labels.add-custom-font" msgstr "Добавить произвольный шрифт" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "Администратор" @@ -1105,7 +1139,8 @@ msgstr "Вы можете продолжить с аккаунтом Penpot" msgid "labels.create" msgstr "Создать" -#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team" msgstr "Создать новую команду" @@ -1120,7 +1155,8 @@ msgstr "Произвольные шрифты" msgid "labels.dashboard" msgstr "Панель управления" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "Удалить" @@ -1140,7 +1176,10 @@ msgstr "Удалить приглашение" msgid "labels.delete-multi-files" msgstr "Удалить файлы (%s)" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/files.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.drafts" msgstr "Черновики" @@ -1151,7 +1190,7 @@ msgstr "Редактировать" msgid "labels.edit-file" msgstr "Редактировать" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "Редактор" @@ -1186,7 +1225,9 @@ msgstr "Шрифты" msgid "labels.github-repo" msgstr "Репозиторий на Github" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "Оставить отзыв" @@ -1212,7 +1253,8 @@ msgstr "Что-то пошло не так. Пожалуйста, повтори msgid "labels.internal-error.main-message" msgstr "Внутренняя ошибка" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.invitations" msgstr "Приглашения" @@ -1231,11 +1273,11 @@ msgstr "Войти или зарегистрироваться" msgid "labels.logout" msgstr "Выйти" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.member" msgstr "Участник" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "Участники" @@ -1243,7 +1285,8 @@ msgstr "Участники" msgid "labels.new-password" msgstr "Новый пароль" -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +#: src/app/main/ui/workspace/comments.cljs, +#: src/app/main/ui/dashboard/comments.cljs msgid "labels.no-comments-available" msgstr "Вы попались! Здесь будут появляться уведомления о новых комментариях." @@ -1252,7 +1295,6 @@ msgid "labels.no-invitations" msgstr "Нет ожидающих приглашений." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "Нажмите кнопку **Пригласить людей**, чтобы пригласить в эту команду." @@ -1296,7 +1338,8 @@ msgstr "или" msgid "labels.owner" msgstr "Владелец" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.password" msgstr "Пароль" @@ -1304,7 +1347,8 @@ msgstr "Пароль" msgid "labels.pending-invitation" msgstr "Ожидание" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.profile" msgstr "Профиль" @@ -1316,7 +1360,8 @@ msgstr "Проекты" msgid "labels.release-notes" msgstr "Примечания к выпуску" -#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/workspace/libraries.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" msgstr "Удалить" @@ -1324,7 +1369,9 @@ msgstr "Удалить" msgid "labels.remove-member" msgstr "Удалить участника" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.rename" msgstr "Переименовать" @@ -1336,7 +1383,7 @@ msgstr "Переименовать команду" msgid "labels.resend-invitation" msgstr "Снова отправить приглашение" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Заново" @@ -1366,11 +1413,12 @@ msgstr "Мы проводим диагностику наших систем." msgid "labels.service-unavailable.main-message" msgstr "Сервис недоступен" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.settings" msgstr "Настройки" -#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs +#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.share-prototype" msgstr "Поделиться ссылкой" @@ -1422,7 +1470,7 @@ msgstr "Наблюдатель" msgid "labels.write-new-comment" msgstr "Написать комментарий" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.you" msgstr "(вы)" @@ -1430,21 +1478,24 @@ msgstr "(вы)" msgid "labels.your-account" msgstr "Ваш аккаунт" -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "Загрузка изображения…" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" msgstr "Добавить как общую библиотеку" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.hint" msgstr "" "При выдаче статуса общей библиотеки, ресурсы этого проекта будут доступны к " "использованию в остальных файлах." -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.message" msgstr "Добавить \"%s\" как общую библиотеку" @@ -1564,25 +1615,22 @@ msgstr "Вы уверены, что хотите удалить этот про msgid "modals.delete-project-confirm.title" msgstr "Удаление проекта" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.accept" msgid_plural "modals.delete-shared-confirm.accept" msgstr[0] "Удалить файл" msgstr[1] "Удалить файлы" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "Вы уверены, что хотите удалить этот файл?" msgstr[1] "Вы уверены, что хотите удалить эти файлы?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Файл содержит библиотеки, которые используются в этом файле:" -msgstr[1] "Файл содержит библиотеки, которые используются в этих файлах:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" msgstr[0] "Удаление файла" @@ -1676,71 +1724,66 @@ msgstr "" msgid "modals.promote-owner-confirm.title" msgstr "Новый владелец команды" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" msgstr "Удалить из общих библиотек" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.hint" msgstr "" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.message" msgstr "" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Если вы отмените публикацию, эти ресурсы будут перемещены в локальную " -"библиотеку этого файла." -msgstr[1] "" -"Если вы отмените их публикацию, эти ресурсы будут перемещены в локальную " -"библиотеку этого файла." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "Вы уверены, что хотите снять с публикации эту библиотеку?" msgstr[1] "Вы уверены, что хотите снять с публикации эти библиотеки?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Некоторые ресурсы этой библиотеки используются здесь:" -msgstr[1] "Некоторые ресурсы этих библиотек используются здесь:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" msgid_plural "modals.unpublish-shared-confirm.title" msgstr[0] "Снять библиотеку с публикации" msgstr[1] "Снять библиотеки с публикации" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.hint" msgstr "" "Вы собираетесь обновить компоненты в общей библиотеке. Это может повлиять " "на другие файлы, которые её используют." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.message" msgstr "Обновить компоненты в общей библиотеке" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.accept" msgstr "Обновить" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.cancel" msgstr "Отменить" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.hint" msgstr "" "Вы собираетесь обновить компонент в общей библиотеке. Это может повлиять на " "другие файлы, которые её используют." -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.message" msgstr "Обновить компонент в общей библиотеке" @@ -1772,12 +1815,6 @@ msgstr "Руководство по участию в проекте" msgid "onboarding-v2.welcome.title" msgstr "Добро пожаловать в Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "Создать команду позже" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Название вашей команды" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "" "После добавления названия команды, вы сможете пригласить людей " @@ -1794,12 +1831,6 @@ msgstr "" "Никого не забудьте. Разработчики, дизайнеры, менеджеры... разнообразие " "развивает :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Создать команду и пригласить позже" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Создать команду и отправить приглашения" - msgid "onboarding.team-modal.create-team" msgstr "Создать команду" @@ -1836,7 +1867,12 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "Перейти к входу" -#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "Смешаный" @@ -1937,9 +1973,6 @@ msgstr "Переключить палитру цветов" msgid "shortcuts.toggle-focus-mode" msgstr "Переключить режим фокуса" -msgid "shortcuts.toggle-grid" -msgstr "Показать/скрыть сетку" - msgid "shortcuts.toggle-history" msgstr "Переключить историю" @@ -2103,11 +2136,13 @@ msgstr "Ресурсы" msgid "workspace.assets.box-filter-all" msgstr "Все ресурсы" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.colors" msgstr "Цвета" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.components" msgstr "Компоненты" @@ -2115,19 +2150,24 @@ msgstr "Компоненты" msgid "workspace.assets.create-group" msgstr "Создать группу" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "Удалить" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.duplicate" msgstr "Дублировать" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" msgstr "Редактировать" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.graphics" msgstr "Графика" @@ -2147,7 +2187,9 @@ msgstr "Библиотеки" msgid "workspace.assets.not-found" msgstr "Ресурсы не найдены" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename" msgstr "Переименовать" @@ -2165,11 +2207,8 @@ msgid_plural "workspace.assets.selected-count" msgstr[0] "Выбран ресурс" msgstr[1] "Выбраны ресурсы (%s)" +#: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "ОБЩИЕ" - -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "Типографика" @@ -2209,26 +2248,14 @@ msgstr "Разгруппировать" msgid "workspace.header.menu.disable-dynamic-alignment" msgstr "Отключить активное выравнивание" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Отключить привязку к сетке" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-dynamic-alignment" msgstr "Включить активное выравнивание" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Привяка к сетке" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-artboard-names" msgstr "Скрыть имена кадров" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Скрыть сетки" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Скрыть палитру цветов" @@ -2260,10 +2287,6 @@ msgstr "" msgid "workspace.header.menu.show-artboard-names" msgstr "Показать имена кадров" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Показать сетку" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Показать палитру цветов" @@ -2288,7 +2311,8 @@ msgstr "Добавить" msgid "workspace.libraries.colors" msgstr "" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" msgstr "Библиотека файлов" @@ -2296,7 +2320,8 @@ msgstr "Библиотека файлов" msgid "workspace.libraries.colors.hsv" msgstr "HSV" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "Недавние цвета" @@ -2388,25 +2413,30 @@ msgstr "Фон холста" msgid "workspace.options.design" msgstr "Дизайн" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export" msgstr "Экспорт" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-object" msgid_plural "workspace.options.export-object" msgstr[0] "Экспорт 1 элемента" msgstr[1] "Экспорт %s элементов" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-complete" msgstr "Экспорт завершён" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.exporting-object" msgstr "Экспортирование…" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-error" msgstr "Экспорт не удался" @@ -2534,7 +2564,8 @@ msgstr "Группировать слои" msgid "workspace.options.layer-options.title.multiple" msgstr "Выделенные слои" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Позиция" @@ -2582,7 +2613,8 @@ msgstr "Тень" msgid "workspace.options.show-in-viewer" msgstr "Показать в режиме просмотра" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.size" msgstr "Размер" @@ -2626,26 +2658,10 @@ msgstr "Сплошной" msgid "workspace.options.text-options.align-bottom" msgstr "Выровнять низ" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Выравнивание по центру (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "Выравнивание по ширине (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Выравнивание по левому краю (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Выравнивание по центру" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Выравнивание по правому краю (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Выравнивание по верхнему краю" @@ -2682,7 +2698,8 @@ msgstr "Высота строки" msgid "workspace.options.text-options.lowercase" msgstr "Нижний регистр" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.none" msgstr "Не задано" @@ -2690,6 +2707,22 @@ msgstr "Не задано" msgid "workspace.options.text-options.strikethrough" msgstr "Перечеркнутый (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Выравнивание по центру (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "Выравнивание по ширине (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Выравнивание по левому краю (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Выравнивание по правому краю (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Текст" @@ -2796,7 +2829,8 @@ msgstr "Исключить" msgid "workspace.shape.menu.flow-start" msgstr "Начало потока" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.go-main" msgstr "Перейти к основному файлу компонента" @@ -2810,7 +2844,9 @@ msgstr "Показать/скрыть UI" msgid "workspace.shape.menu.path" msgstr "Контур" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "Сбросить переопределения" @@ -2825,7 +2861,8 @@ msgstr "Выбрать слой" msgid "workspace.shape.menu.show" msgstr "Показать" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-main" msgstr "Показать основной компонент" @@ -2842,11 +2879,15 @@ msgstr "Преобразовать в контур" msgid "workspace.shape.menu.ungroup" msgstr "Разгруппировать" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-components-in-bulk" msgstr "Обновить основные компоненты" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-main" msgstr "Обновить основной компонент" @@ -2969,17 +3010,25 @@ msgstr "Обновить" msgid "workspace.viewport.click-to-close-path" msgstr "Нажмите для замыкания контура" -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-delete-file" -msgid_plural "dashboard.success-delete-file" -msgstr[0] "Ваш файл успешно удален" -msgstr[1] "Ваши файлы успешно удалены" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 дней" -msgid "dashboard.webhooks.active" -msgstr "Активен" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "Никогда" -msgid "dashboard.webhooks.content-type" -msgstr "Тип контента" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 дней" -msgid "dashboard.webhooks.create" -msgstr "Создать вебхук" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 дней" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 дней" + +msgid "workspace.options.guides.title" +msgstr "Направляющие" diff --git a/frontend/translations/ta.po b/frontend/translations/ta.po index 768888aeff..e3ab123eb3 100644 --- a/frontend/translations/ta.po +++ b/frontend/translations/ta.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "PO-Revision-Date: 2023-06-17 09:51+0000\n" "Last-Translator: \"K.B.Dharun Krishna\" \n" -"Language-Team: Tamil \n" +"Language-Team: Tamil " +"\n" "Language: ta\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" @@ -148,12 +148,6 @@ msgstr "ஒரு கணக்கை உருவாக்கவும்" msgid "auth.register-subtitle" msgstr "இது இலவசம், இது திறந்த மூலமானது" -msgid "common.share-link.link-copied-success" -msgstr "இணைப்பு வெற்றிகரமாக நகலெடுக்கப்பட்டது" - -msgid "common.share-link.manage-ops" -msgstr "அனுமதிகளை நிர்வகிக்கவும்" - #: src/app/main/ui/auth/register.cljs msgid "auth.register-title" msgstr "ஒரு கணக்கை உருவாக்கவும்" @@ -165,15 +159,26 @@ msgstr "வடிவமைப்பு மற்றும் முன்மா msgid "auth.terms-of-service" msgstr "சேவை விதிமுறைகள்" +#: src/app/main/ui/auth/register.cljs +msgid "auth.terms-privacy-agreement" +msgstr "" +"புதிய கணக்கை உருவாக்கும் போது, எங்கள் சேவை விதிமுறைகள் மற்றும் தனியுரிமைக் " +"கொள்கையை ஏற்கிறீர்கள்." + #: src/app/main/ui/auth/register.cljs msgid "auth.verification-email-sent" msgstr "சரிபார்ப்பு மின்னஞ்சலை அனுப்பியுள்ளோம் இந்த முகவரிக்கு" +msgid "common.publish" +msgstr "வெளியிடுங்கள்" + msgid "common.share-link.all-users" msgstr "அனைத்து Penpot பயனர்களும்" -msgid "common.publish" -msgstr "வெளியிடுங்கள்" +msgid "common.share-link.confirm-deletion-link-description" +msgstr "" +"இந்த இணைப்பை நிச்சயமாக அகற்ற விரும்புகிறீர்களா? நீங்கள் அதைச் செய்தால், அது " +"இனி யாருக்கும் கிடைக்காது" msgid "common.share-link.current-tag" msgstr "(தற்போதைய)" @@ -184,6 +189,17 @@ msgstr "இணைப்பை அழிக்கவும்" msgid "common.share-link.get-link" msgstr "இணைப்பைப் பெறுங்கள்" +msgid "common.share-link.link-copied-success" +msgstr "இணைப்பு வெற்றிகரமாக நகலெடுக்கப்பட்டது" + +msgid "common.share-link.manage-ops" +msgstr "அனுமதிகளை நிர்வகிக்கவும்" + +msgid "common.share-link.page-shared" +msgid_plural "common.share-link.page-shared" +msgstr[0] "1 பக்கம் பகிரப்பட்டது" +msgstr[1] "%s பக்கங்கள் பகிரப்பட்டன" + msgid "common.share-link.permissions-can-comment" msgstr "கருத்து தெரிவிக்கலாம்" @@ -195,19 +211,3 @@ msgstr "இணைப்பு உள்ள எவருக்கும் அண msgid "common.share-link.permissions-pages" msgstr "பக்கங்கள் பகிரப்பட்டன" - -#: src/app/main/ui/auth/register.cljs -msgid "auth.terms-privacy-agreement" -msgstr "" -"புதிய கணக்கை உருவாக்கும் போது, எங்கள் சேவை விதிமுறைகள் மற்றும் தனியுரிமைக் " -"கொள்கையை ஏற்கிறீர்கள்." - -msgid "common.share-link.confirm-deletion-link-description" -msgstr "" -"இந்த இணைப்பை நிச்சயமாக அகற்ற விரும்புகிறீர்களா? நீங்கள் அதைச் செய்தால், அது " -"இனி யாருக்கும் கிடைக்காது" - -msgid "common.share-link.page-shared" -msgid_plural "common.share-link.page-shared" -msgstr[0] "1 பக்கம் பகிரப்பட்டது" -msgstr[1] "%s பக்கங்கள் பகிரப்பட்டன" diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index 72458ba80e..f1ef022f9d 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -1,15 +1,15 @@ msgid "" msgstr "" -"PO-Revision-Date: 2023-01-26 16:52+0000\n" +"PO-Revision-Date: 2024-01-28 11:01+0000\n" "Last-Translator: Oğuz Ersen \n" -"Language-Team: Turkish " -"\n" +"Language-Team: Turkish \n" "Language: tr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.16-dev\n" +"X-Generator: Weblate 5.4-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -409,10 +409,10 @@ msgstr[1] "%s yazı tipi eklendi" msgid "dashboard.fonts.hero-text1" msgstr "" "Buraya yüklediğiniz herhangi bir web yazı tipi, bu takımın dosyalarının " -"metin özelliklerinde bulunan yazı tipi ailesi listesine eklenecek. Aynı " -"yazı tipi ailesi adına sahip yazı tipleri, **tek yazı tipi ailesi** olarak " -"gruplandırılacak. Yazı tiplerini şu biçimlerde yükleyebilirsiniz: **TTF, " -"OTF ve WOFF** (yalnızca bir tane gerekli olacak)." +"metin özelliklerinde bulunan yazı tipi ailesi listesine eklenecek. Aynı yazı " +"tipi ailesi adına sahip yazı tipleri, **tek yazı tipi ailesi** olarak " +"gruplandırılacak. Yazı tiplerini şu biçimlerde yükleyebilirsiniz: **TTF, OTF " +"ve WOFF** (yalnızca bir tane gerekli olacak)." msgid "dashboard.fonts.hero-text2" msgstr "" @@ -436,9 +436,6 @@ msgstr "Oops! Bu dosyayı içeri aktaramadık" msgid "dashboard.import.import-error" msgstr "Dosya içeri aktarılırken bir sorun oluştu. Dosya içeri aktarılmadı." -msgid "dashboard.import.import-message" -msgstr "%s dosya başarıyla içeri aktarıldı." - msgid "dashboard.import.import-warning" msgstr "Bazı dosyalar kaldırılmış geçersiz nesneler içeriyordu." @@ -598,17 +595,9 @@ msgstr "Tema seç" msgid "dashboard.show-all-files" msgstr "Tüm dosyaları göster" -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-delete-file" -msgstr "Dosyanız başarıyla silindi" - #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-delete-project" -msgstr "Projen başarıyla silindi" - -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-duplicate-file" -msgstr "Dosyanız başarıyla kopyalandı" +msgstr "Projeniz başarıyla silindi" #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-duplicate-project" @@ -772,7 +761,8 @@ msgstr "E-postanızı parola olarak kullanamazsınız" msgid "errors.email-has-permanent-bounces" msgstr "«%s» adresi için çok fazla geri dönme raporu var." -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, +#: src/app/main/ui/auth/recovery_request.cljs msgid "errors.email-invalid" msgstr "Lütfen geçerli bir e-posta adresi girin" @@ -787,7 +777,7 @@ msgstr "«%s» e-postasının spam veya kalıcı olarak geri döndüğü bildiri msgid "errors.feature-mismatch" msgstr "" "Görünüşe göre '%s' özelliğinin etkin olduğu bir dosyayı açıyorsunuz, ancak " -"penpot ön ucunuz bunu desteklemiyor veya devre dışı bırakıldı." +"şu anki penpot sürümü bunu desteklemiyor veya devre dışı bırakıldı." #: src/app/main/errors.cljs msgid "errors.feature-not-supported" @@ -932,7 +922,7 @@ msgstr "Konu" msgid "feedback.subtitle" msgstr "" "Lütfen bir sorun, fikir ya da kuşkunuzu açıklayarak e-postanızın nedenini " -"belirtin. Ekibimizin bir üyesi en kısa sürede yanıt verecektir." +"belirtin. Takımımızın bir üyesi en kısa sürede yanıt verecektir." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.title" @@ -940,7 +930,7 @@ msgstr "E-posta" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "Twitter'a git" +msgstr "X'a git" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -948,7 +938,7 @@ msgstr "Teknik sorularınıza yardımcı olmak için buradayız." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Twitter destek hesabı" +msgstr "X destek hesabı" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -1023,10 +1013,6 @@ msgstr "Genişlik" msgid "inspect.attributes.shadow" msgstr "Gölge" -#: src/app/main/ui/inspect/attributes/shadow.cljs -msgid "inspect.attributes.shadow.shorthand.spread" -msgstr "S" - #: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.size" msgstr "Boyut ve konum" @@ -1405,7 +1391,6 @@ msgid "labels.no-invitations" msgstr "Bekleyen davetiye yok." #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "" "Kişileri bu takıma davet etmek için **İnsanları davet et** düğmesine " @@ -1761,26 +1746,6 @@ msgid_plural "modals.delete-shared-confirm.accept" msgstr[0] "Dosyayı sil" msgstr[1] "Dosyaları sil" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "" -"Eğer silerseniz, bu varlıklar bu dosyanın yerel kütüphanesine taşınacaktır. " -"Kullanılmayan tüm varlıklar kaybolacaktır." -msgstr[1] "" -"Eğer silerseniz, bu varlıklar bu dosyanın yerel kütüphanesine taşınacaktır. " -"Kullanılmayan tüm varlıklar kaybolacaktır." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "" -"Eğer silerseniz, bu varlıklar bu dosyaların yerel kütüphanelerine " -"taşınacaktır. Kullanılmayan tüm varlıklar kaybolacaktır." -msgstr[1] "" -"Eğer silerseniz, bu varlıklar bu dosyaların yerel kütüphanelerine " -"taşınacaktır. Kullanılmayan tüm varlıklar kaybolacaktır." - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" @@ -1788,29 +1753,6 @@ msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "Bu dosyayı silmek istediğinizden emin misiniz?" msgstr[1] "Bu dosyaları silmek istediğinizden emin misiniz?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" -msgstr[0] "" -"Bu dosyanın kütüphanesindeki varlıkların hiçbiri kullanılmıyor. Dosyayla " -"birlikte silineceklerdir." -msgstr[1] "" -"Bu dosyaların kütüphanesindeki varlıkların hiçbiri kullanılmıyor. " -"Dosyalarla birlikte silineceklerdir." - -#: src/app/main/ui/workspace/header.cljs, -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "Bu dosyanın kütüphanesindeki varlıklardan bazıları burada kullanılıyor:" -msgstr[1] "Bu dosyalarnın kütüphanesindeki varlıklardan bazıları burada kullanılıyor:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "Bu dosyanın kütüphanesindeki varlıklardan bazıları burada kullanılıyor:" -msgstr[1] "Bu dosyalarnın kütüphanesindeki varlıklardan bazıları burada kullanılıyor:" - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" @@ -1830,7 +1772,7 @@ msgstr "" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.delete-team-confirm.title" -msgstr "Takımı sil" +msgstr "Takım siliniyor" #: src/app/main/ui/dashboard/team.cljs msgid "modals.delete-team-member-confirm.accept" @@ -1960,32 +1902,6 @@ msgstr "“%s” Paylaşılan Kütüphanesini Kaldır" msgid "modals.small-nudge" msgstr "Küçük dürtme" -#: src/app/main/ui/workspace/header.cljs, -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.accept" -msgstr "Yayından kaldır" - -#: src/app/main/ui/workspace/header.cljs, -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "" -"Yayından kaldırırsanız, bu varlıklar bu dosyanın yerel kütüphanesine " -"taşınacaktır." -msgstr[1] "" -"Yayından kaldırırsanız, bu varlıklar bu dosyanın yerel kütüphanesine " -"taşınacaktır." - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "" -"Yayından kaldırırsanız, bu varlıklar bu dosyaların yerel kütüphanelerine " -"taşınacaktır." -msgstr[1] "" -"Yayından kaldırırsanız, bu varlıklar bu dosyaların yerel kütüphanelerine " -"taşınacaktır." - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" @@ -1993,25 +1909,6 @@ msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "Bu kütüphaneyi yayından kaldırmak istediğinizden emin misiniz?" msgstr[1] "Bu kütüphaneleri yayından kaldırmak istediğinizden emin misiniz?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "Bu kütüphanedeki varlıkların hiçbiri kullanılmıyor." -msgstr[1] "Bu kütüphanelerdeki varlıkların hiçbiri kullanılmıyor." - -#: src/app/main/ui/workspace/header.cljs, -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "Bu kütüphanedeki varlıklardan bazıları burada kullanılıyor:" -msgstr[1] "Bu kütüphanelerdeki varlıklardan bazıları burada kullanılıyor:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "Bu kütüphanedeki varlıklardan bazıları burada kullanılıyor:" -msgstr[1] "Bu kütüphanelerdeki varlıklardan bazıları burada kullanılıyor:" - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" @@ -2142,12 +2039,6 @@ msgstr "Katkıda bulunma kılavuzu" msgid "onboarding-v2.welcome.title" msgstr "Penpot'a hoş geldiniz!" -msgid "onboarding.choice.team-up.create-later" -msgstr "Daha sonra bir takım oluştur" - -msgid "onboarding.choice.team-up.create-team" -msgstr "Takımınızın adı" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "Takımınızı adlandırdıktan sonra, insanları katılmaya davet edebileceksiniz." @@ -2162,18 +2053,9 @@ msgstr "" "Herkesi dahil etmeyi unutmayın. Geliştiriciler, tasarımcılar, " "yöneticiler... çeşitlilik iyidir :)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "Takım oluştur ve daha sonra davet et" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "Takım oluştur ve davet gönder" - msgid "onboarding.choice.team-up.roles" msgstr "Rol ile davet et:" -msgid "onboarding.contrib.desc2.1" -msgstr "Projeye" - msgid "onboarding.newsletter.accept" msgstr "Evet, abone ol" @@ -2186,15 +2068,6 @@ msgstr "Gizlilik Politikası." msgid "onboarding.newsletter.title" msgstr "Penpot haberlerini almak ister misiniz?" -msgid "onboarding.slide.0.title" -msgstr "Kütüphaneler, biçimler ve bileşenler tasarlayın" - -msgid "onboarding.slide.1.title" -msgstr "Etkileşimlerle tasarımlarınıza hayat verin" - -msgid "onboarding.slide.3.alt" -msgstr "Teslim ve kod özellikleri" - msgid "onboarding.team-modal.create-team" msgstr "Bir takım oluştur" @@ -2218,9 +2091,6 @@ msgstr "Sınırsız üye" msgid "onboarding.team-modal.create-team-feature-5" msgstr "%100 özgür!" -msgid "onboarding.team.start.title" -msgstr "Tasarlamaya başla" - msgid "onboarding.templates.subtitle" msgstr "İşte bazı şablonlar." @@ -2330,7 +2200,7 @@ msgid "shortcuts.artboard-selection" msgstr "Seçimden çalışma yüzeyi oluştur" msgid "shortcuts.bool-difference" -msgstr "Boole fark" +msgstr "Boole farkı" msgid "shortcuts.bool-exclude" msgstr "Boole hariç tut" @@ -2612,9 +2482,6 @@ msgstr "Odak modunu değiştir" msgid "shortcuts.toggle-fullscreen" msgstr "Tam ekranı değiştir" -msgid "shortcuts.toggle-grid" -msgstr "Izgarayı göster/gizle" - msgid "shortcuts.toggle-history" msgstr "Geçmişi değiştir" @@ -2633,15 +2500,6 @@ msgstr "Oranları kilitle" msgid "shortcuts.toggle-rules" msgstr "Cetvelleri göster/gizle" -msgid "shortcuts.toggle-scale-text" -msgstr "Ölçek metnini değiştir" - -msgid "shortcuts.toggle-snap-grid" -msgstr "Izgaraya tuttur" - -msgid "shortcuts.toggle-snap-guide" -msgstr "Kılavuzlara tuttur" - msgid "shortcuts.toggle-textpalette" msgstr "Metin paletini değiştir" @@ -2771,10 +2629,6 @@ msgstr "Etkileşimler (%s)" msgid "viewer.header.share.copy-link" msgstr "Bağlantıyı kopyala" -#: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.share.subtitle" -msgstr "Bağlantıya sahip herkes erişebilecek" - #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.show-interactions" msgstr "Etkileşimleri göster" @@ -2908,10 +2762,6 @@ msgid_plural "workspace.assets.selected-count" msgstr[0] "%s öge seçildi" msgstr[1] "%s öge seçildi" -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "PAYLAŞILDI" - #: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" @@ -2982,15 +2832,11 @@ msgstr "Dairesel degrade" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-dynamic-alignment" -msgstr "Dinamik hizalamayı kapat" +msgstr "Dinamik hizalamayı devre dışı bırak" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-scale-text" -msgstr "Metin ölçeklendirmeyi kapat" - -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "Izgaraya tutturmayı kapat" +msgstr "Metin ölçeklendirmeyi devre dışı bırak" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" @@ -3007,10 +2853,6 @@ msgstr "Dinamik hizalamayı etkinleştir" msgid "workspace.header.menu.enable-scale-text" msgstr "Metin ölçeklendirmeyi etkinleştir" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "Izgaraya tuttur" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "Kılavuzlara tuttur" @@ -3022,10 +2864,6 @@ msgstr "Piksele tutturmayı etkinleştir" msgid "workspace.header.menu.hide-artboard-names" msgstr "Çalışma yüzeyi adlarını gizle" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "Izgaraları gizle" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "Renk paletini gizle" @@ -3069,10 +2907,6 @@ msgstr "Tümünü seç" msgid "workspace.header.menu.show-artboard-names" msgstr "Çalışma yüzeylerinin adlarını göster" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "Izgarayı göster" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "Renk paletini göster" @@ -3230,9 +3064,6 @@ msgstr "Güncelle" msgid "workspace.libraries.updates" msgstr "GÜNCELLEMELER" -msgid "workspace.library.store" -msgstr "Mağaza kütüphaneleri" - #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.add-interaction" msgstr "Etkileşimler eklemek için + düğmesine tıklayın." @@ -3314,11 +3145,6 @@ msgstr "Dışa aktar" msgid "workspace.options.export-multiple" msgstr "Seçimi dışa aktar" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/inspect/exports.cljs -msgid "workspace.options.export-object" -msgstr "1 ögeyi dışa aktar" - #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs msgid "workspace.options.export.suffix" msgstr "Son ek" @@ -3883,12 +3709,12 @@ msgid "workspace.options.radius" msgstr "Yarıçap" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "Tüm köşeler" +msgid "workspace.options.radius-bottom-left" +msgstr "Sol alt" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "Bireysel köşeler" +msgid "workspace.options.radius-bottom-right" +msgstr "Sağ alt" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3899,12 +3725,12 @@ msgid "workspace.options.radius-top-right" msgstr "Sağ üst" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "Sol alt" +msgid "workspace.options.radius.all-corners" +msgstr "Tüm köşeler" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "Sağ alt" +msgid "workspace.options.radius.single-corners" +msgstr "Bireysel köşeler" msgid "workspace.options.recent-fonts" msgstr "Son kullanılanlar" @@ -4068,26 +3894,10 @@ msgstr "Katı" msgid "workspace.options.text-options.align-bottom" msgstr "Alta hizala" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "Ortaya hizala (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "İki yana yasla (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "Sola hizala (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" msgstr "Merkeze hizala" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "Sağa hizala (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" msgstr "Üste hizala" @@ -4133,6 +3943,22 @@ msgstr "Hiçbiri" msgid "workspace.options.text-options.strikethrough" msgstr "Üstü çizili (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "Ortaya hizala (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "İki yana yasla (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "Sola hizala (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "Sağa hizala (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Metin" @@ -4200,38 +4026,9 @@ msgstr "Düğümleri ayır (%s)" msgid "workspace.path.actions.snap-nodes" msgstr "Düğümleri tuttur (%s)" -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "" -"Tekrar denemek için bu dosyayı yeniden yükleyebilirsiniz. Sorun devam " -"ederse, listeye bir göz atmanızı ve bozuk grafikleri silmeyi düşünmenizi " -"öneririz." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "Bazı grafikler güncellenemedi." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "%s/%s dönüştürülüyor" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "" -"Kütüphane Grafikleri bundan böyle Bileşenlerdir ve bu da onları çok daha " -"güçlü kılacaktır." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "Bu güncelleme tek seferlik bir işlemdir." - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "%s güncelleniyor..." - #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.add-flex" -msgstr "Düzen esnekliği ekle" +msgstr "Esnek düzen ekle" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" @@ -4649,3 +4446,750 @@ msgstr "Güncelle" msgid "workspace.viewport.click-to-close-path" msgstr "Yolu kapatmak için tıklayın" + +#~ msgid "dashboard.newsletter-title" +#~ msgstr "Bülten aboneliği" + +#~ msgid "feedback.chat-subtitle" +#~ msgstr "Sohbet etmek ister misin? Glitter'da bizimle sohbet edebilirsin" + +#~ msgid "inspect.attributes.shadow.shorthand.offset-x" +#~ msgstr "X" + +#~ msgid "labels.images" +#~ msgstr "Görseller" + +#~ msgid "labels.skip" +#~ msgstr "Atla" + +#~ msgid "onboarding.contrib.alt" +#~ msgstr "Açık Kaynak" + +#~ msgid "onboarding.contrib.link" +#~ msgstr "github'da erişebilir" + +#~ msgid "onboarding.slide.0.desc1" +#~ msgstr "Tüm takım üyeleriyle iş birliği içinde güzel kullanıcı arayüzleri oluşturun." + +#~ msgid "onboarding.slide.1.desc1" +#~ msgstr "Ürün davranışını taklit etmek için zengin etkileşimler oluşturun." + +#~ msgid "onboarding.slide.2.desc1" +#~ msgstr "" +#~ "Tüm takım üyeleri tasarımlar üzerinde gerçek zamanlı tasarım, çok oyunculu " +#~ "ve merkezi yorumlar, fikirler ve geri bildirimler ile aynı anda çalışır." + +#~ msgid "onboarding.slide.3.desc2" +#~ msgstr "" +#~ "İşaretleme (SVG, HTML) veya biçimler (CSS, Less, Stylus…) gibi kod " +#~ "özellikleri alın ve sağlayın." + +#~ msgid "onboarding.team.create.title" +#~ msgstr "Takım oluştur" + +#~ msgid "onboarding.welcome.title" +#~ msgstr "Penpot'a Hoş Geldiniz" + +#~ msgid "viewer.header.share.placeholder" +#~ msgstr "Paylaşım adresi burada görünecek" + +#~ msgid "workspace.library.libraries" +#~ msgstr "Kütüphaneler" + +#~ msgid "workspace.options.blur-options.layer-blur" +#~ msgstr "Katman" + +#~ msgid "workspace.options.layout-item.min-w" +#~ msgstr "Asgari Genişlik" + +#~ msgid "workspace.options.layout-item.title.min-w" +#~ msgstr "Asgari genişlik" + +msgid "shortcuts.bold" +msgstr "Kalın yazı aç/kapat" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "Penpot'u fiziksel bir sunucuda kullanmadan önce deneyin" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "Konsept fikirler üzerinde çalışmak" + +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "Kütüphanenizde henüz renk stili yok" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30 gün" + +msgid "workspace.options.component.copy" +msgstr "Kopyala" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" +msgstr "" +"Geri bildiriminiz, Penpot'u kullanışlı ve eğlenceli bir araç haline " +"getirmeye devam edebilmemiz için alışkanlıklarınızı ve tercihlerinizi " +"anlamamıza yardımcı olacaktır." + +msgid "modals.invite-member.repeated-invitation" +msgstr "" +"Bazı e-posta adresleri mevcut takım üyelerine aittir. Davetleri " +"gönderilmeyecektir." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "Geliştirici" + +msgid "shortcuts.align-justify" +msgstr "İki yana yasla" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "Ürün veya Proje yöneticisi" + +msgid "workspace.options.component.create-annotation" +msgstr "Bir açıklama oluştur" + +#, markdown +msgid "dashboard.fonts.warning-text" +msgstr "" +"İşletim sistemlerinin farklı dikey metriklerine ilişkin olarak yazı " +"tiplerinizde olası bir sorun tespit ettik. Bu durumu kontrol etmek için [" +"bunun gibi](https://vertical-metrics.netlify.app/) yazı tipi dikey metrik " +"hizmetlerini kullanabilirsiniz. Ayrıca web yazı tipleri oluşturmak ve " +"hataları düzeltmek için [Transfonter](https://transfonter.org/) kullanmanızı " +"öneririz. " + +#: src/app/main/data/common.cljs +msgid "notifications.by-code.upgrade-version" +msgstr "Yeni bir sürüm mevcut, lütfen sayfayı yenileyin" + +msgid "modals.delete-component-annotation.message" +msgstr "Bu açıklamayı silmek istediğinize emin misiniz?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "Pazarlama" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180 gün" + +msgid "workspace.options.component.edit-annotation" +msgstr "Bir açıklamayı düzenle" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "Takım projemin kodunu al " + +msgid "workspace.shape.menu.create-multiple-components" +msgstr "Çoklu bileşen oluştur" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "Kendi projem üzerinde çalışıyorum" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "Dikdörtgen" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "İsim gereklidir" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "Bu kütüphane burada etkinleştirildi: " +msgstr[1] "Bu kütüphaneler burada etkinleştirildiler: " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "Serbest çalışıyorum" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "Belirteç kopyalandı" + +msgid "modals.publish-empty-library.title" +msgstr "Boş kütüphaneyi yayınla" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "tüm değişiklikleri gör" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "...marka çalışması, çizimler, pazarlama materyalleri, vb." + +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "Kütüphanenizde henüz tipografi stili yok" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90 gün" + +msgid "shortcuts.zoom-lense-increase" +msgstr "Görüntüyü büyült" + +msgid "workspace.shape.menu.add-grid" +msgstr "Izgara düzeni ekle" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" +msgstr[0] "" +"Bu dosyada daha önce kullanılmış olan varlıklar orada kalmaya devam edecek (" +"hiçbir tasarım bozulmayacak)." +msgstr[1] "" +"Bu dosyalarda daha önce kullanılmış olan varlıklar orada kalmaya devam " +"edecek (hiçbir tasarım bozulmayacak)." + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "Yakınlaştırma" + +msgid "workspace.options.component.annotation" +msgstr "Açıklama" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.some" +msgstr "Biraz" + +msgid "shortcuts.text-align-justify" +msgstr "İki yana yasla" + +msgid "workspace.layout_grid.editor.title" +msgstr "Düzenleme ızgarası" + +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "Webhook adı en fazla 2048 karakter içermelidir." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "%s tarihinde sona erdi" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "Sona erme tarihi" + +msgid "workspace.header.menu.undo" +msgstr "Geri al" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "50'den fazla" + +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-weight" +msgstr "Yazı Tipi Kalınlığı" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "Parola boşluk dışında bir karakter içermelidir." + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "Sonraki" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "Takımınızın büyüklüğü nedir?" + +msgid "shortcuts.toggle-visibility" +msgstr "Göster / Gizle" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" +msgstr "" +"... taslak çizimler, kullanıcı deneyimi yol haritası ve akışları, gezinme " +"menüsü, vb." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60 gün" + +msgid "workspace.header.menu.disable-scale-content" +msgstr "Orantılı ölçeklendirmeyi devre dışı bırak" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "Çok fazla" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "Başla" + +msgid "shortcuts.align-center" +msgstr "Ortala" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "Elmas" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "Penpot'u nasıl kullanmayı planlıyorsunuz?" + +msgid "workspace.header.menu.redo" +msgstr "Tekrarla" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.describe-your-experience-working-on" +msgstr "... üzerinde çalışma deneyiminizi en iyi nasıl tarif edersiniz?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.select-option" +msgstr "Bir seçenek belirleyin" + +msgid "shortcuts.text-align-left" +msgstr "Sola hizala" + +msgid "shortcuts.font-size-dec" +msgstr "Yazı boyutunu azalt" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "Tasarımcı" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "Hiçbiri" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "Haydi başlayalım!" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +msgid "settings.detach" +msgstr "Çıkar" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "Sketch" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library-updates" +msgstr "KÜTÜPHANE GÜNCELLEMELERİ" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "Üçgen" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "İsim" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "Hiçbir dosyada etkinleştirilmedi." +msgstr[1] "Hiçbir dosyada etkinleştirilmediler." + +msgid "modals.publish-empty-library.message" +msgstr "Kütüphaneniz boş. Yine de yayınlamak istediğinizden emin misiniz?" + +msgid "workspace.assets.open-library" +msgstr "Kütüphane dosyasını aç" + +msgid "modals.delete-component-annotation.title" +msgstr "Açıklamayı sil" + +msgid "shortcuts.select-parent-layer" +msgstr "Ana katmanı seç" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.shared-library" +msgstr "Paylaşılan kütüphane" + +msgid "shortcuts.select-next" +msgstr "Sonraki katmanı seç" + +msgid "workspace.header.menu.enable-scale-content" +msgstr "Orantılı ölçeklendirmeyi etkinleştir" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "Daha fazla bilgi" + +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "İsim boşluk dışında bir karakter içermelidir." + +msgid "shortcuts.line-height-inc" +msgstr "Satır yüksekliğini artır" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "Diğer (lütfen belirtiniz)" + +msgid "labels.discard" +msgstr "At" + +msgid "shortcuts.font-size-inc" +msgstr "Yazı boyutunu artır" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "Penpot'un takımınız için uygun olup olmadığını görmek için test edin " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "Öğrenci veya öğretmen" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2-10" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "Takımımın projesi için geri bildirim bırakın" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31-50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "InVision" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canva" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "Penpot'u daha fazla keşfedin" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "%s tarihinde sona eriyor" + +msgid "shortcuts.italic" +msgstr "İtalik yazı aç/kapat" + +msgid "shortcuts.letter-spacing-dec" +msgstr "Harf aralığını azalt" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "Önceki" + +msgid "workspace.shape.menu.create-annotation" +msgstr "Açıklama oluştur" + +msgid "shortcuts.letter-spacing-inc" +msgstr "Harf aralığını artır" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "Projem üzerinde çalışmaya başla" + +msgid "shortcuts.text-align-center" +msgstr "Ortaya hizala" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "Ok" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "Hangi tasarım aracını daha iyi kullanıyorsunuz?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "Göreviniz nedir?" + +msgid "modals.add-shared-confirm-empty.hint" +msgstr "" +"Kütüphaneniz boş. Paylaşılan Kütüphane olarak eklendiğinde, oluşturduğunuz " +"varlıklar diğer dosyalarınız arasında kullanılabilir olacak. Yayınlamak " +"istediğinizden emin misiniz?" + +msgid "shortcuts.text-align-right" +msgstr "Sağa hizala" + +msgid "shortcuts.underline" +msgstr "Alt çizgiyi aç/kapat" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "Asla" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "Kurucu/Başkan Yardımcısı" + +msgid "modals.publish-empty-library.accept" +msgstr "Yayınla" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "Süresiz" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "İsim en fazla 250 karakter içermelidir." + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "Daire" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11-30" + +msgid "shortcut-subsection.text-editor" +msgstr "Metinler" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "... kullanıcı arayüzü tasarımı, görsel öğeler, tasarım sistemleri, vb." + +msgid "shortcuts.zoom-lense-decrease" +msgstr "Görüntüyü küçült" + +msgid "shortcuts.line-height-dec" +msgstr "Satır yüksekliğini azalt" + +msgid "shortcuts.select-prev" +msgstr "Önceki katmanı seç" + +msgid "shortcuts.line-through" +msgstr "Üstü çizili yazı aç/kapat" + +msgid "errors.cannot-upload" +msgstr "Medya dosyası yüklenemedi." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "Bu belirteci silmek istediğinizden emin misiniz?" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "Erişim belirteçleri" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "Belirtecin sona erme tarihi yok" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "Yeni belirteç oluştur" + +msgid "workspace.options.component.main" +msgstr "Ana bileşen" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "Belirtecin süresi %s tarihinde sona erecek" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "Belirteci sil" + +msgid "workspace.assets.duplicate-main" +msgstr "Ana bileşeni çoğalt" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "Belirteç oluştur" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "Adı, belirtecin ne için olduğunu bilmenize yardımcı olabilir" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "Şu ana kadar hiç belirteciniz yok." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "" +"Kişisel erişim belirteçleri, oturum açma/parola kimlik doğrulama sistemimize " +"alternatif olarak işlev görür ve bir uygulamanın dahili Penpot API'sine " +"erişmesine izin vermek için kullanılabilir" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "Kişisel erişim belirteçleri" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "Bir belirteç oluşturmak için \"Yeni belirteç oluştur\" düğmesine basın." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "Belirteci sil" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "Erişim belirteci başarıyla oluşturuldu." + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "Erişim belirteci oluştur" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "Belirteci kopyala" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "Profil - Erişim belirteçleri" + +#: src/app/main/ui/auth/register.cljs +#, markdown +msgid "auth.terms-privacy-agreement-md" +msgstr "" +"Bir hesap oluştururken, [koşullarımızı](%s) ve [gizlilik politikamızı](%s) " +"kabul etmiş sayılırsınız." + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "Dosyanız başarıyla silindi" +msgstr[1] "Dosyalarınız başarıyla silindi" + +#: src/app/main/errors.cljs +msgid "errors.version-not-supported" +msgstr "Dosyanın uyumsuz bir sürüm numarası var" + +#: src/app/main/errors.cljs +msgid "errors.file-feature-mismatch" +msgstr "" +"Etkinleştirilen özellikler ile açmaya çalıştığınız dosyanın özellikleri " +"arasında bir uyumsuzluk var gibi görünüyor. Dosyanın açılabilmesi için önce " +"'%s' için geçişlerin uygulanması gerekiyor." + +msgid "errors.validation" +msgstr "Doğrulama Hatası" + +msgid "errors.paste-data-validation" +msgstr "Panoda geçersiz veri" + +msgid "inspect.attributes.typography.text-transform.unset" +msgstr "Ayarlanmadı" + +msgid "labels.share" +msgstr "Paylaş" + +msgid "labels.search" +msgstr "Ara" + +msgid "onboarding.choice.team-up.start-without-a-team" +msgstr "Takım olmadan başlayın" + +msgid "onboarding.choice.team-up.continue-creating-team" +msgstr "Takım oluşturmaya devam edin" + +msgid "onboarding.choice.team-up.start-without-a-team-description" +msgstr "Daha sonra bir takım oluşturabileceksiniz." + +msgid "onboarding.choice.team-up.continue-without-a-team" +msgstr "Takım olmadan devam edin" + +msgid "onboarding.choice.team-up.create-team-and-send-invites" +msgstr "Takım oluşturun ve davet gönderin" + +msgid "onboarding.choice.team-up.create-team-without-inviting" +msgstr "Davet etmeden takım oluşturun" + +msgid "onboarding.choice.team-up.create-team-and-invite" +msgstr "Takım oluşturun ve davet edin" + +msgid "onboarding.choice.team-up.create-team-without-invite" +msgstr "Takım oluşturun" + +msgid "onboarding.choice.team-up.create-team-and-send-invites-description" +msgstr "Daha sonra davet edebileceksiniz" + +msgid "workspace.layout_grid.editor.top-bar.locate" +msgstr "Bul" + +msgid "workspace.layout_grid.editor.top-bar.done" +msgstr "Bitti" + +msgid "workspace.layout_grid.editor.options.edit-grid" +msgstr "Izgarayı düzenle" + +msgid "workspace.layout_grid.editor.options.exit" +msgstr "Çıkış" + +msgid "workspace.options.component.swap" +msgstr "Bileşeni değiştir" + +msgid "workspace.options.component.swap.empty" +msgstr "Bu kütüphanede henüz varlık yok" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow" +msgstr "Akış" + +#, markdown +msgid "workspace.top-bar.read-only" +msgstr "**İnceleme modu** (Yalnızca görüntüle)" + +msgid "workspace.top-bar.read-only.done" +msgstr "Bitti" + +msgid "media.image" +msgstr "Görsel" + +msgid "media.solid" +msgstr "Katı" + +msgid "media.linear" +msgstr "Doğrusal" + +msgid "media.radial" +msgstr "Işınsal" + +msgid "media.gradient" +msgstr "Değişim" + +msgid "media.choose-image" +msgstr "Görsel seç" + +msgid "workspace.options.guides.title" +msgstr "Kılavuzlar" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.accept" +msgid_plural "modals.unpublish-shared-confirm.accept" +msgstr[0] "Yayından kaldır" +msgstr[1] "Yayından kaldır" + +msgid "dashboard.import.import-message" +msgid_plural "dashboard.import.import-message" +msgstr[0] "1 dosya başarıyla içeri aktarıldı." +msgstr[1] "%s dosya başarıyla içeri aktarıldı." + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "Dosyanız başarıyla kopyalandı" +msgstr[1] "Dosyalarınız başarıyla kopyalandı" + +#: src/app/main/errors.cljs +msgid "errors.team-feature-mismatch" +msgstr "Uyumsuz '%s' özelliği algılandı" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +msgid "workspace.options.export-object" +msgid_plural "workspace.options.export-object" +msgstr[0] "1 ögeyi dışa aktar" +msgstr[1] "%s ögeyi dışa aktar" diff --git a/frontend/translations/ukr_UA.po b/frontend/translations/ukr_UA.po index 6b9fec16a2..75c2e4e1c0 100644 --- a/frontend/translations/ukr_UA.po +++ b/frontend/translations/ukr_UA.po @@ -707,10 +707,6 @@ msgstr "Бібліотеки" msgid "workspace.assets.rename" msgstr "Перейменувати" -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "СПІЛЬНІ" - #: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "Типографіка" diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po index 3ac71e4d0c..c7f736a1fa 100644 --- a/frontend/translations/zh_CN.po +++ b/frontend/translations/zh_CN.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2023-06-09 01:52+0000\n" -"Last-Translator: 王世阳 \n" +"PO-Revision-Date: 2024-01-12 23:06+0000\n" +"Last-Translator: Geek Squirrel \n" "Language-Team: Chinese (Simplified) \n" "Language: zh_CN\n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Weblate 4.18-dev\n" +"X-Generator: Weblate 5.4-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -35,7 +35,8 @@ msgstr "只是想试试?" msgid "auth.demo-warning" msgstr "这是一个演示服务,请【不要】用于真实工作,这些项目将被周期性地抹除。" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" msgstr "电子邮件" @@ -253,7 +254,8 @@ msgstr "开始浏览" msgid "dasboard.walkthrough-hero.title" msgstr "界面浏览" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "添加为共享库" @@ -283,7 +285,8 @@ msgstr "下载Penpot文件 (.penpot)" msgid "dashboard.download-standard-file" msgstr "下载标准文件(.svg + .json)" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate" msgstr "复制" @@ -292,7 +295,6 @@ msgid "dashboard.duplicate-multi" msgstr "复制 %s 个文件" #: src/app/main/ui/dashboard/grid.cljs -#, markdown msgid "dashboard.empty-placeholder-drafts" msgstr "" "添加到库的文件将出现在这里。尝试分享你的文件或从我们的[库和模板](https://penpot.app/libraries-templates." @@ -381,13 +383,11 @@ msgid_plural "dashboard.fonts.fonts-added" msgstr[0] "1 个字体添加成功" msgstr[1] "%s 个字体添加成功" -#, markdown msgid "dashboard.fonts.hero-text1" msgstr "" "你在此上传的任何网络字体文件,将会被添加至本团队下文件的字体属性中的可用字体族列表中。拥有相同字体族名称的字体文件,将会按照字体族进行分组。你可以上传以" "下格式的字体文件:**TTF,OTF和WOFF**(你只需要上传其中一种即可)。" -#, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "你应当只向Penpot上传你所拥有的字体,或是你持有使用许可的字体。点击[Penpot服务条例](https://penpot.app/terms." @@ -397,6 +397,12 @@ msgstr "" msgid "dashboard.fonts.upload-all" msgstr "全部上传" +msgid "dashboard.fonts.warning-text" +msgstr "" +"我们在你的字体中检测到一个可能的问题,与不同操作系统的垂直度量有关。为了检查它,你可以使用字体垂直度量服务,如[这个](https://" +"vertical-metrics.netlify.app/)。此外,我们建议使用[Transfonter](https://transfonter." +"org/)来生成网络字体并修复错误。 " + msgid "dashboard.import" msgstr "导入文件" @@ -406,6 +412,9 @@ msgstr "文件无法导入" msgid "dashboard.import.import-error" msgstr "文件导入过程中出现未知问题,导入失败。" +msgid "dashboard.import.import-message" +msgstr "%s 个文件导入成功。" + msgid "dashboard.import.import-warning" msgstr "一些包含无效对象的文档已被移除。" @@ -434,7 +443,8 @@ msgstr "文件上传中" msgid "dashboard.invite-profile" msgstr "邀请people" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" msgstr "退出团队" @@ -458,7 +468,8 @@ msgstr "正在加载文档…" msgid "dashboard.loading-fonts" msgstr "正在加载字体…" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "移动到" @@ -470,7 +481,8 @@ msgstr "移动 %s 个文件到" msgid "dashboard.move-to-other-team" msgstr "移动到其他团队" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" msgstr "+ 新文档" @@ -533,7 +545,8 @@ msgstr "项目" msgid "dashboard.remove-account" msgstr "希望注销您的账号?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" msgstr "不再作为共享库" @@ -561,15 +574,28 @@ msgstr "选择界面主题" msgid "dashboard.show-all-files" msgstr "显示全部文档" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "您的文件已被成功删除" +msgstr[1] "您的文件已被成功删除" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-delete-project" msgstr "成功删除了项目" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "您的文件已被成功复制" +msgstr[1] "您的文件已被成功复制" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-duplicate-project" msgstr "成功创建了项目副本" -#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-file" msgstr "成功移动了文件" @@ -605,11 +631,14 @@ msgstr "搜索结果" msgid "dashboard.type-something" msgstr "输入关键词进行搜索" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.unpublish-shared" msgstr "取消发布库" -#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/profile.cljs, +#: src/app/main/ui/settings/password.cljs, +#: src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "保存设置" @@ -617,7 +646,7 @@ msgid "dashboard.webhooks.active" msgstr "处于活跃状态" msgid "dashboard.webhooks.active.explain" -msgstr "当这个钩子被触发时事件的细节将被传递" +msgstr "当这个webhook被触发时,事件细节将被传递" msgid "dashboard.webhooks.content-type" msgstr "内容类型" @@ -652,7 +681,11 @@ msgstr "电子邮件" msgid "dashboard.your-name" msgstr "你的姓名" -#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/libraries.cljs, +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" msgstr "你的Penpot" @@ -697,7 +730,8 @@ msgstr "无法加载%s等字体" msgid "errors.clipboard-not-implemented" msgstr "你的浏览器不支持该操作" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/change_email.cljs msgid "errors.email-already-exists" msgstr "电子邮件已被占用" @@ -708,11 +742,15 @@ msgstr "电子邮件已经验证通过。" msgid "errors.email-as-password" msgstr "密码不能为邮箱地址" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/register.cljs, +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" msgstr "电子邮件“%s”收到了非常多的永久退信报告。" -#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, +#: src/app/main/ui/auth/recovery_request.cljs msgid "errors.email-invalid" msgstr "请输入有效的电子邮件" @@ -725,13 +763,15 @@ msgstr "此邮箱[%s]已被标记为垃圾邮件或已被永久拉黑。" #: src/app/main/errors.cljs msgid "errors.feature-mismatch" -msgstr "看起来你正在打开一个启用了'%s'功能的文件,但你的penpot前台不支持它或禁用它。" +msgstr "看起来你正在打开一个启用了'%s'功能的文件,但当前penpot版本并不支持该功能或已" +"将其禁用。" #: src/app/main/errors.cljs msgid "errors.feature-not-supported" msgstr "不支持功能“%s”。" -#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/verify_token.cljs, +#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.generic" msgstr "发生了某种错误。" @@ -762,7 +802,7 @@ msgstr "图片尺寸过大,故无法插入。" msgid "errors.media-type-mismatch" msgstr "图片内容好像与文档扩展名不匹配。" -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" msgstr "该图片好像不可用。" @@ -781,7 +821,9 @@ msgstr "密码最少需要8位字符" msgid "errors.profile-blocked" msgstr "个人资料已被屏蔽" -#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/auth/recovery_request.cljs, +#: src/app/main/ui/settings/change_email.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "errors.profile-is-muted" msgstr "你设置了邮件免打扰(报告垃圾邮件或者多次退信)。" @@ -798,7 +840,9 @@ msgstr "您尝试分配的成员不存在。" msgid "errors.team-leave.owner-cant-leave" msgstr "所有者不能离开团队,您必须转让所有者角色。" -#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/data/media.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "errors.unexpected-error" msgstr "发生了意料之外的错误。" @@ -927,7 +971,8 @@ msgstr "高" msgid "inspect.attributes.layout.left" msgstr "左" -#: src/app/main/ui/inspect/attributes/layout.cljs, src/app/main/ui/inspect/attributes/layout.cljs +#: src/app/main/ui/inspect/attributes/layout.cljs, +#: src/app/main/ui/inspect/attributes/layout.cljs msgid "inspect.attributes.layout.radius" msgstr "圆角" @@ -955,15 +1000,12 @@ msgstr "尺寸和位置" msgid "inspect.attributes.stroke" msgstr "边框" -#, permanent msgid "inspect.attributes.stroke.alignment.center" msgstr "居中" -#, permanent msgid "inspect.attributes.stroke.alignment.inner" msgstr "内部" -#, permanent msgid "inspect.attributes.stroke.alignment.outer" msgstr "外部" @@ -999,6 +1041,10 @@ msgstr "字号" msgid "inspect.attributes.typography.font-style" msgstr "文字风格" +#: src/app/main/ui/inspect/attributes/text.cljs +msgid "inspect.attributes.typography.font-weight" +msgstr "字体重量" + #: src/app/main/ui/inspect/attributes/text.cljs msgid "inspect.attributes.typography.letter-spacing" msgstr "字距" @@ -1103,7 +1149,7 @@ msgstr "激活" msgid "labels.add-custom-font" msgstr "添加自定义字体" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "管理员" @@ -1161,7 +1207,8 @@ msgstr "复制链接" msgid "labels.create" msgstr "创建" -#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +#: src/app/main/ui/dashboard/team_form.cljs, +#: src/app/main/ui/dashboard/team_form.cljs msgid "labels.create-team" msgstr "创建新团队" @@ -1176,7 +1223,8 @@ msgstr "自定义字体" msgid "labels.dashboard" msgstr "面板" -#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "删除" @@ -1196,7 +1244,10 @@ msgstr "删除邀请" msgid "labels.delete-multi-files" msgstr "删除%s个文件" -#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/projects.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/files.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.drafts" msgstr "草稿" @@ -1207,7 +1258,7 @@ msgstr "编辑" msgid "labels.edit-file" msgstr "编辑文档" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "编辑者" @@ -1242,7 +1293,9 @@ msgstr "字体" msgid "labels.github-repo" msgstr "Github仓库" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "提交反馈" @@ -1271,7 +1324,8 @@ msgstr "发生了一些不妙的事。请尝试重新操作。如果问题仍然 msgid "labels.internal-error.main-message" msgstr "内部错误" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.invitations" msgstr "邀请" @@ -1290,11 +1344,11 @@ msgstr "登录或注册" msgid "labels.logout" msgstr "登出" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.member" msgstr "成员" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "成员" @@ -1302,7 +1356,8 @@ msgstr "成员" msgid "labels.new-password" msgstr "新密码" -#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +#: src/app/main/ui/workspace/comments.cljs, +#: src/app/main/ui/dashboard/comments.cljs msgid "labels.no-comments-available" msgstr "你们都赶上了! 新的评论通知将出现在这里。" @@ -1311,7 +1366,6 @@ msgid "labels.no-invitations" msgstr "没有待处理的邀请。" #: src/app/main/ui/dashboard/team.cljs -#, markdown msgid "labels.no-invitations-hint" msgstr "单击“**邀请他人**”按钮以邀请人员加入此团队。" @@ -1355,7 +1409,8 @@ msgstr "或" msgid "labels.owner" msgstr "所有者" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.password" msgstr "密码" @@ -1363,7 +1418,8 @@ msgstr "密码" msgid "labels.pending-invitation" msgstr "待办" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.profile" msgstr "个人资料" @@ -1379,7 +1435,8 @@ msgstr "发布说明" msgid "labels.reload-file" msgstr "重新加载文件" -#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +#: src/app/main/ui/workspace/libraries.cljs, +#: src/app/main/ui/dashboard/team.cljs msgid "labels.remove" msgstr "移除" @@ -1387,7 +1444,9 @@ msgstr "移除" msgid "labels.remove-member" msgstr "删除成员" -#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/dashboard/sidebar.cljs, +#: src/app/main/ui/dashboard/project_menu.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "labels.rename" msgstr "重命名" @@ -1399,7 +1458,7 @@ msgstr "重命名团队" msgid "labels.resend-invitation" msgstr "重新发送邀请" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "重试" @@ -1429,7 +1488,8 @@ msgstr "我们正在进行系统的程序维护。" msgid "labels.service-unavailable.main-message" msgstr "服务不可用" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, +#: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.settings" msgstr "设置" @@ -1494,7 +1554,7 @@ msgstr "Webhooks" msgid "labels.write-new-comment" msgstr "写一条新评论" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.you" msgstr "(你)" @@ -1502,19 +1562,22 @@ msgstr "(你)" msgid "labels.your-account" msgstr "你的账户" -#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" msgstr "正在加载图片…" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" msgstr "添加为共享库" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.hint" msgstr "一旦添加为共享库,此文档库中的素材就可被用于你的其他文档中。" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.message" msgstr "将“%s”添加为共享库" @@ -1600,7 +1663,7 @@ msgstr "删除文件" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-multi-confirm.message" -msgstr "你确定要删除这%s个文件吗?" +msgstr "你确定要删除这%s个文件?" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-multi-confirm.title" @@ -1638,53 +1701,22 @@ msgstr "你确定想要删除这个项目?" msgid "modals.delete-project-confirm.title" msgstr "删除项目" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.accept" msgid_plural "modals.delete-shared-confirm.accept" msgstr[0] "删除文件" msgstr[1] "批量删除文件" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint" -msgid_plural "modals.delete-shared-confirm.hint" -msgstr[0] "如果你删除它,这些资产将不再能从其他文件中获得。已经使用过的资产将保留在这个" -"文件中(没有设计会被破坏!)。" -msgstr[1] "如果你删除它们,这些资产将不再能从其他文件中获得。已经使用过的资产将保留在这" -"个文件中(没有设计会被破坏!)。" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.hint-many" -msgid_plural "modals.delete-shared-confirm.hint-many" -msgstr[0] "如果你删除它,这些资产将不再能从其他文件中获得。已经使用过的资产将保留在这些" -"文件中(没有设计会被破坏!)。" -msgstr[1] "如果你删除它们,这些资产将不再能从其他文件中获得。已经使用过的资产将保留在这" -"些文件中(没有设计会被破坏!)。" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" msgid_plural "modals.delete-shared-confirm.message" msgstr[0] "你是否确认要删除这个文件?" msgstr[1] "你是否确认要删除这些文件?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.no-files-message" -msgid_plural "modals.delete-shared-confirm.no-files-message" -msgstr[0] "此文件库中的任何资源均未在使用中。它们将与文件一起删除。" -msgstr[1] "这些文件库中的所有资源均未在使用中。它们将与文件一起删除。" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message" -msgid_plural "modals.delete-shared-confirm.scd-message" -msgstr[0] "此处使用此文件库中的某些资源:" -msgstr[1] "这些文件库中的一些资源正在这里使用:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.delete-shared-confirm.scd-message-many" -msgid_plural "modals.delete-shared-confirm.scd-message-many" -msgstr[0] "此处使用此文件库中的某些资源:" -msgstr[1] "这些文件库中的一些资源正在这里使用:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.title" msgid_plural "modals.delete-shared-confirm.title" msgstr[0] "删除文件" @@ -1736,6 +1768,9 @@ msgstr "发送邀请" msgid "modals.invite-member.emails" msgstr "电子邮件,以逗号分隔" +msgid "modals.invite-member.repeated-invitation" +msgstr "有些电子邮件是来自当前的团队成员。他们的邀请将不会被发送。" + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-team-member.title" msgstr "邀请成员加入团队" @@ -1799,15 +1834,18 @@ msgstr "您是此团队的所有者,你确定想要将所有者转让给该成 msgid "modals.promote-owner-confirm.title" msgstr "新增团队所有者" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" msgstr "不再作为共享库" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.hint" msgstr "一旦不再作为共享库,该文档库就不能继续用于你的其他文档中。" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.message" msgstr "不再将“%s”作为共享库" @@ -1815,73 +1853,54 @@ msgstr "不再将“%s”作为共享库" msgid "modals.small-nudge" msgstr "小幅微调" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint" -msgid_plural "modals.unpublish-shared-confirm.hint" -msgstr[0] "如果你取消发布,这些资产将不再能从其他文件中获得。已经使用过的资产将保留在这" -"个文件中(没有设计会被破坏!)。" -msgstr[1] "如果你取消发布它们,这些资产将不再能从其他文件中获得。已经使用过的资产将保留" -"在这个文件中(没有设计会被破坏!)。" +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.accept" +msgid_plural "modals.unpublish-shared-confirm.accept" +msgstr[0] "取消发布" +msgstr[1] "取消发布" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.hint-many" -msgid_plural "modals.unpublish-shared-confirm.hint-many" -msgstr[0] "如果你取消发布,这些资产将不再能从其他文件中获得。已经使用过的资产将保留在这" -"些文件中(没有设计会被破坏!)。" -msgstr[1] "如果你取消发布它们,这些资产将不再能从其他文件中获得。已经使用过的资产将保留" -"在这些文件中(没有设计会被破坏!)。" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" msgid_plural "modals.unpublish-shared-confirm.message" msgstr[0] "你是否确认取消发布这个库?" msgstr[1] "你是否确认取消发布这些库?" -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.no-files-message" -msgid_plural "modals.unpublish-shared-confirm.no-files-message" -msgstr[0] "此库中的所有资源均未在使用中。" -msgstr[1] "这些库中的所有资源均未在使用中。" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message" -msgid_plural "modals.unpublish-shared-confirm.scd-message" -msgstr[0] "此处使用此库中的某些资源:" -msgstr[1] "此处使用了这些库中的一些资源:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.scd-message-many" -msgid_plural "modals.unpublish-shared-confirm.scd-message-many" -msgstr[0] "此处使用此库中的某些资源:" -msgstr[1] "此处使用了这些库中的一些资源:" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#: src/app/main/ui/workspace/header.cljs, +#: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.title" msgid_plural "modals.unpublish-shared-confirm.title" msgstr[0] "取消发布库" msgstr[1] "批量取消发布库" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.hint" msgstr "你即将更新共享库中的组件,这可能会影响使用这些组件的其他文档。" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component-in-bulk.message" msgstr "更新共享库组件" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.accept" msgstr "更新组件" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.cancel" msgstr "取消" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.hint" msgstr "你即将更新共享库中的一个组件。这可能会对使用该组件的其他文档产生影响。" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.message" msgstr "更新共享库中的一个组件" @@ -1956,12 +1975,6 @@ msgstr "贡献指南" msgid "onboarding-v2.welcome.title" msgstr "欢迎来到Penpot!" -msgid "onboarding.choice.team-up.create-later" -msgstr "稍后创建团队" - -msgid "onboarding.choice.team-up.create-team" -msgstr "团队名称" - msgid "onboarding.choice.team-up.create-team-desc" msgstr "命名团队后,您将能够邀请他人加入。" @@ -1974,12 +1987,6 @@ msgstr "邀请成员" msgid "onboarding.choice.team-up.invite-members-info" msgstr "记得将开发人员、设计师、经理……等各类人员都加进来:)" -msgid "onboarding.choice.team-up.invite-members-skip" -msgstr "创建团队并稍后邀请" - -msgid "onboarding.choice.team-up.invite-members-submit" -msgstr "创建团队并发送邀请" - msgid "onboarding.choice.team-up.roles" msgstr "邀请角色:" @@ -2029,7 +2036,12 @@ msgstr "Penpot" msgid "profile.recovery.go-to-login" msgstr "去登录" -#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "混合" @@ -2083,6 +2095,9 @@ msgstr "路径" msgid "shortcut-subsection.shape" msgstr "形状" +msgid "shortcut-subsection.text-editor" +msgstr "文本" + msgid "shortcut-subsection.tools" msgstr "工具" @@ -2101,14 +2116,20 @@ msgstr "添加节点" msgid "shortcuts.align-bottom" msgstr "底部对齐" +msgid "shortcuts.align-center" +msgstr "居中对齐" + msgid "shortcuts.align-hcenter" msgstr "水平居中对齐" +msgid "shortcuts.align-justify" +msgstr "两端对齐" + msgid "shortcuts.align-left" -msgstr "左对齐" +msgstr "靠左对齐" msgid "shortcuts.align-right" -msgstr "右对齐" +msgstr "靠右对齐" msgid "shortcuts.align-top" msgstr "顶部对齐" @@ -2119,6 +2140,9 @@ msgstr "垂直居中对齐" msgid "shortcuts.artboard-selection" msgstr "以所选内容创建画板" +msgid "shortcuts.bold" +msgstr "切换粗体" + msgid "shortcuts.bool-difference" msgstr "布尔差" @@ -2209,6 +2233,12 @@ msgstr "水平翻转" msgid "shortcuts.flip-vertical" msgstr "垂直翻转" +msgid "shortcuts.font-size-dec" +msgstr "缩小字体大小" + +msgid "shortcuts.font-size-inc" +msgstr "增加字体大小" + msgid "shortcuts.go-to-drafts" msgstr "前往草稿" @@ -2233,9 +2263,27 @@ msgstr "放大" msgid "shortcuts.insert-image" msgstr "插入图片" +msgid "shortcuts.italic" +msgstr "切换斜体" + msgid "shortcuts.join-nodes" msgstr "链接节点" +msgid "shortcuts.letter-spacing-dec" +msgstr "减少字母间距" + +msgid "shortcuts.letter-spacing-inc" +msgstr "减少字母间距" + +msgid "shortcuts.line-height-dec" +msgstr "减少行高" + +msgid "shortcuts.line-height-inc" +msgstr "增加行高" + +msgid "shortcuts.line-through" +msgstr "切换删除线" + msgid "shortcuts.make-corner" msgstr "制作圆角" @@ -2356,6 +2404,12 @@ msgstr "搜索快捷方式" msgid "shortcuts.select-all" msgstr "选择所有" +msgid "shortcuts.select-next" +msgstr "选择下一个图层" + +msgid "shortcuts.select-prev" +msgstr "选择上一个图层" + msgid "shortcuts.separate-nodes" msgstr "分离节点" @@ -2402,9 +2456,6 @@ msgstr "切换焦点模式" msgid "shortcuts.toggle-fullscreen" msgstr "切换全屏" -msgid "shortcuts.toggle-grid" -msgstr "显示/隐藏网格" - msgid "shortcuts.toggle-history" msgstr "切换历史" @@ -2423,21 +2474,18 @@ msgstr "锁定比例" msgid "shortcuts.toggle-rules" msgstr "显示/隐藏规则" -msgid "shortcuts.toggle-scale-text" -msgstr "切换缩放文本" - -msgid "shortcuts.toggle-snap-grid" -msgstr "网络对齐" - -msgid "shortcuts.toggle-snap-guide" -msgstr "辅助线对齐" - msgid "shortcuts.toggle-textpalette" msgstr "切换文本调色板" +msgid "shortcuts.toggle-visibility" +msgstr "切换可见度" + msgid "shortcuts.toggle-zoom-style" msgstr "切换缩放样式" +msgid "shortcuts.underline" +msgstr "切换下划线" + msgid "shortcuts.undo" msgstr "回退" @@ -2450,6 +2498,12 @@ msgstr "取消遮罩" msgid "shortcuts.v-distribute" msgstr "垂直分布" +msgid "shortcuts.zoom-lense-decrease" +msgstr "变焦镜头缩小" + +msgid "shortcuts.zoom-lense-increase" +msgstr "变焦镜头放大" + msgid "shortcuts.zoom-selected" msgstr "缩放到选定对象" @@ -2614,11 +2668,13 @@ msgstr "素材" msgid "workspace.assets.box-filter-all" msgstr "所有素材" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.colors" msgstr "颜色" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.components" msgstr "组件" @@ -2630,19 +2686,27 @@ msgstr "创建组" msgid "workspace.assets.create-group-hint" msgstr "这些物件将按照“组名/物件名”的格式自动命名" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "删除" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.duplicate" msgstr "创建副本" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.duplicate-main" +msgstr "重复主体" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.edit" msgstr "编辑" -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.graphics" msgstr "图形" @@ -2665,7 +2729,9 @@ msgstr "本地库" msgid "workspace.assets.not-found" msgstr "未找到素材" -#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs, +#: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename" msgstr "重命名" @@ -2683,11 +2749,8 @@ msgid_plural "workspace.assets.selected-count" msgstr[0] "已选中%s个物件" msgstr[1] "已选中%s个物件" +#: src/app/main/ui/workspace/sidebar/assets.cljs, #: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "共享的" - -#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "排版" @@ -2715,7 +2778,9 @@ msgstr "字距" msgid "workspace.assets.typography.line-height" msgstr "行高" -#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/inspect/attributes/text.cljs, src/app/main/ui/inspect/attributes/text.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs, +#: src/app/main/ui/inspect/attributes/text.cljs msgid "workspace.assets.typography.sample" msgstr "Ag" @@ -2742,11 +2807,13 @@ msgstr "关注" msgid "workspace.focus.selection" msgstr "选择" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "线性渐变" -#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, +#: src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.radial" msgstr "放射渐变" @@ -2754,14 +2821,13 @@ msgstr "放射渐变" msgid "workspace.header.menu.disable-dynamic-alignment" msgstr "禁用动态对齐" +msgid "workspace.header.menu.disable-scale-content" +msgstr "禁用比例尺" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-scale-text" msgstr "禁用缩放文本" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.disable-snap-grid" -msgstr "禁用吸附到网格" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-guides" msgstr "禁用与参考线对齐" @@ -2773,14 +2839,13 @@ msgstr "禁用像素对齐" msgid "workspace.header.menu.enable-dynamic-alignment" msgstr "启用动态对齐" +msgid "workspace.header.menu.enable-scale-content" +msgstr "启用比例尺" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-scale-text" msgstr "启用缩放文本" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.enable-snap-grid" -msgstr "吸附到网格" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-guides" msgstr "与参考线对齐" @@ -2792,10 +2857,6 @@ msgstr "启用像素对齐" msgid "workspace.header.menu.hide-artboard-names" msgstr "隐藏画板名称" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.hide-grid" -msgstr "隐藏网格" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-palette" msgstr "隐藏调色盘" @@ -2831,6 +2892,9 @@ msgstr "首选项" msgid "workspace.header.menu.option.view" msgstr "视图" +msgid "workspace.header.menu.redo" +msgstr "重做" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.select-all" msgstr "全选" @@ -2839,10 +2903,6 @@ msgstr "全选" msgid "workspace.header.menu.show-artboard-names" msgstr "显示画板名称" -#: src/app/main/ui/workspace/header.cljs -msgid "workspace.header.menu.show-grid" -msgstr "显示网格" - #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-palette" msgstr "显示调色盘" @@ -2858,6 +2918,9 @@ msgstr "显示标尺" msgid "workspace.header.menu.show-textpalette" msgstr "显示字体调色板" +msgid "workspace.header.menu.undo" +msgstr "撤销" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "重置" @@ -2910,7 +2973,8 @@ msgstr "添加" msgid "workspace.libraries.colors" msgstr "%s种颜色" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.file-library" msgstr "文档库" @@ -2918,7 +2982,8 @@ msgstr "文档库" msgid "workspace.libraries.colors.hsv" msgstr "HSV" -#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, +#: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "最近使用的颜色" @@ -3069,31 +3134,44 @@ msgstr "上下固定" msgid "workspace.options.design" msgstr "设计" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export" msgstr "导出" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.export-multiple" msgstr "导出已选择" +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs +msgid "workspace.options.export-object" +msgid_plural "workspace.options.export-object" +msgstr[0] "输出1个元素" +msgstr[1] "输出%s元素" + #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs msgid "workspace.options.export.suffix" msgstr "后缀" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-complete" msgstr "导出完成" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs msgid "workspace.options.exporting-object" msgstr "正在导出…" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-error" msgstr "导出失败" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-slow" msgstr "导出速度意外缓慢" @@ -3549,10 +3627,18 @@ msgstr "底部" msgid "workspace.options.layout.direction.column" msgstr "列" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.column-reverse" +msgstr "反向列" + #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.direction.row" msgstr "行" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.row-reverse" +msgstr "反向行" + #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs msgid "workspace.options.layout.gap" msgstr "差距" @@ -3616,7 +3702,8 @@ msgstr "更多共享库颜色" msgid "workspace.options.opacity" msgstr "不透明度" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "位置" @@ -3628,12 +3715,12 @@ msgid "workspace.options.radius" msgstr "圆角" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.all-corners" -msgstr "所有角" +msgid "workspace.options.radius-bottom-left" +msgstr "左下角" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius.single-corners" -msgstr "独立的角" +msgid "workspace.options.radius-bottom-right" +msgstr "右下角" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius-top-left" @@ -3644,17 +3731,18 @@ msgid "workspace.options.radius-top-right" msgstr "右上角" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-left" -msgstr "左下角" +msgid "workspace.options.radius.all-corners" +msgstr "所有角" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs -msgid "workspace.options.radius-bottom-right" -msgstr "右下角" +msgid "workspace.options.radius.single-corners" +msgstr "独立的角" msgid "workspace.options.recent-fonts" msgstr "最近的" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, +#: src/app/main/ui/inspect/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" msgstr "重试" @@ -3727,7 +3815,8 @@ msgstr "在导出中显示" msgid "workspace.options.show-in-viewer" msgstr "在预览模式显示" -#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.size" msgstr "尺寸" @@ -3809,25 +3898,9 @@ msgstr "实线" msgid "workspace.options.text-options.align-bottom" msgstr "底部对齐" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "居中对齐 (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-justify" -msgstr "整理 (%s)" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-left" -msgstr "靠左对齐 (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "居中对齐" - -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-right" -msgstr "靠右对齐 (%s)" +msgstr "垂直居中" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3865,7 +3938,8 @@ msgstr "行高" msgid "workspace.options.text-options.lowercase" msgstr "小写" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.none" msgstr "无" @@ -3873,6 +3947,22 @@ msgstr "无" msgid "workspace.options.text-options.strikethrough" msgstr "删除线 (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "水平居中 (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-justify" +msgstr "两端对齐 (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-left" +msgstr "靠左对齐 (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-right" +msgstr "靠右对齐 (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "文本" @@ -3940,30 +4030,6 @@ msgstr "拆分节点(%s)" msgid "workspace.path.actions.snap-nodes" msgstr "对接节点 (%s)" -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-hint" -msgstr "要重试,您可以重新加载此文件。如果问题仍然存在,我们建议您查看列表并考虑删除损坏的图形。" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.error-msg" -msgstr "某些图形无法更新。" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.progress" -msgstr "转换%s/%s" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text1" -msgstr "从现在开始,库图形是组件,这将使它们更加强大。" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.text2" -msgstr "此更新是一次性操作。" - -#: src/app/main/ui/workspace.cljs -msgid "workspace.remove-graphics.title" -msgstr "正在更新 %s..." - #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.add-flex" msgstr "添加弹性布局" @@ -4000,11 +4066,15 @@ msgstr "删除" msgid "workspace.shape.menu.delete-flow-start" msgstr "删除流程起点" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "解绑实例" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instances-in-bulk" msgstr "解绑实例" @@ -4045,7 +4115,8 @@ msgstr "向上移动一层" msgid "workspace.shape.menu.front" msgstr "移至顶层" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.go-main" msgstr "前往主组件文档" @@ -4067,11 +4138,13 @@ msgstr "差集" msgid "workspace.shape.menu.lock" msgstr "锁定" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.mask" msgstr "蒙板" -#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.paste" msgstr "粘贴" @@ -4082,7 +4155,9 @@ msgstr "路径" msgid "workspace.shape.menu.remove-flex" msgstr "删除弹性布局" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "还原自定义选项" @@ -4097,11 +4172,13 @@ msgstr "选择图层" msgid "workspace.shape.menu.show" msgstr "显示" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-in-assets" msgstr "在素材面板中显示" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show-main" msgstr "显示主组件" @@ -4129,11 +4206,15 @@ msgstr "取消锁定" msgid "workspace.shape.menu.unmask" msgstr "取消蒙版" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-components-in-bulk" msgstr "更新主要组件" -#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, +#: src/app/main/ui/workspace/context_menu.cljs, +#: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-main" msgstr "更新主组件" @@ -4175,7 +4256,8 @@ msgstr "形状" msgid "workspace.sidebar.layers.texts" msgstr "文本" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/inspect/attributes/svg.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, +#: src/app/main/ui/inspect/attributes/svg.cljs msgid "workspace.sidebar.options.svg-attrs.title" msgstr "已导入SVG属性" @@ -4369,115 +4451,523 @@ msgstr "更新" msgid "workspace.viewport.click-to-close-path" msgstr "单击以闭合路径" -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-delete-file" -msgid_plural "dashboard.success-delete-file" -msgstr[0] "您的文件已被成功删除" -msgstr[1] "您的文件已被成功删除" +msgid "workspace.layout_grid.editor.top-bar.locate" +msgstr "定位" -#: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-weight" -msgstr "字体重量" +msgid "workspace.layout_grid.editor.top-bar.done" +msgstr "完成" -msgid "shortcuts.bold" -msgstr "切换粗体" +msgid "workspace.layout_grid.editor.options.edit-grid" +msgstr "编辑网格" -msgid "shortcuts.letter-spacing-inc" -msgstr "减少字母间距" +msgid "workspace.layout_grid.editor.options.exit" +msgstr "退出" -msgid "shortcuts.font-size-inc" -msgstr "增加字体大小" +#: src/app/main/ui/workspace/textpalette.cljs +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "你的库中还没有排版风格" -msgid "shortcuts.letter-spacing-dec" -msgstr "减少字母间距" +msgid "workspace.options.component.swap.empty" +msgstr "你的库中还没有素材" -msgid "shortcuts.underline" -msgstr "切换下划线" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow" +msgstr "流程" -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.column-reverse" -msgstr "反向列" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "钻石" -msgid "workspace.header.menu.redo" -msgstr "重做" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "箭头" -#: src/app/main/ui/dashboard/file_menu.cljs -msgid "dashboard.success-duplicate-file" -msgid_plural "dashboard.success-delete-file" -msgstr[0] "您的文件已被成功复制" -msgstr[1] "您的文件已被成功复制" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "矩形" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs -msgid "workspace.options.export-object" -msgid_plural "workspace.options.export-object" -msgstr[0] "输出1个元素" -msgstr[1] "输出%s元素" - -#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs -msgid "modals.unpublish-shared-confirm.accept" -msgid_plural "modals.unpublish-shared-confirm.accept" -msgstr[0] "取消发布" -msgstr[1] "取消发布" - -msgid "shortcuts.line-height-inc" -msgstr "增加行高" - -msgid "shortcuts.line-through" -msgstr "切换删除线" - -msgid "shortcuts.align-justify" -msgstr "两端对齐" - -msgid "workspace.assets.duplicate-main" -msgstr "重复主体" - -msgid "workspace.header.menu.undo" -msgstr "撤销" - -#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs -msgid "workspace.options.layout.direction.row-reverse" -msgstr "反向行" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "三角形" #, markdown -msgid "dashboard.fonts.warning-text" -msgstr "" -"我们在你的字体中检测到一个可能的问题,与不同操作系统的垂直度量有关。为了检查" -"它,你可以使用字体垂直度量服务,如[这个](https://vertical-metrics.netlify." -"app/)。此外,我们建议使用[Transfonter](https://transfonter.org/" -")来生成网络字体并修复错误。 " +msgid "workspace.top-bar.read-only" +msgstr "**检查模式**(不可编辑)" -msgid "modals.invite-member.repeated-invitation" -msgstr "有些电子邮件是来自当前的团队成员。他们的邀请将不会被发送。" +msgid "workspace.top-bar.read-only.done" +msgstr "完成" -msgid "shortcuts.font-size-dec" -msgstr "缩小字体大小" +msgid "media.solid" +msgstr "纯色" -msgid "shortcuts.italic" -msgstr "切换斜体" +msgid "media.linear" +msgstr "线性" -msgid "shortcuts.select-prev" -msgstr "选择上一个图层" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.no-access-tokens" +msgstr "你目前还没有令牌。" -msgid "shortcut-subsection.text-editor" -msgstr "文本" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.errors-required-name" +msgstr "名称是必填项" -msgid "shortcuts.align-center" -msgstr "居中对齐" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-180-days" +msgstr "180天" -msgid "shortcuts.line-height-dec" -msgstr "减少行高" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-30-days" +msgstr "30天" -msgid "shortcuts.select-next" -msgstr "选择下一个图层" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-60-days" +msgstr "60天" -msgid "workspace.header.menu.disable-scale-content" -msgstr "禁用比例尺" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-90-days" +msgstr "90天" -msgid "workspace.header.menu.enable-scale-content" -msgstr "启用比例尺" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expiration-never" +msgstr "从不" -msgid "shortcuts.zoom-lense-increase" -msgstr "变焦镜头放大" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expired-on" +msgstr "已经于%s到期" -msgid "shortcuts.zoom-lense-decrease" -msgstr "变焦镜头缩小" +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal" +msgstr "个人访问令牌" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-expire" +msgstr "令牌将于%s到期" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.expires-on" +msgstr "将于%s到期" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.no-expiration" +msgstr "无到期时限" + +#: src/app/main/errors.cljs +msgid "errors.version-not-supported" +msgstr "文件具有不兼容的版本号" + +#: src/app/main/errors.cljs +msgid "errors.file-feature-mismatch" +msgstr "看起来当前启用的功能,与正在打开的文件所依赖的功能不匹配。在打开文件前,需要" +"应用对“%s”修改。" + +#: src/app/main/errors.cljs +msgid "errors.team-feature-mismatch" +msgstr "检测到不兼容功能“%s”" + +msgid "errors.validation" +msgstr "验证错误" + +msgid "errors.paste-data-validation" +msgstr "剪切板中为无效数据" + +msgid "inspect.attributes.typography.text-transform.unset" +msgstr "取消设置" + +msgid "labels.share" +msgstr "分享" + +msgid "labels.search" +msgstr "搜索" + +msgid "modals.add-shared-confirm-empty.hint" +msgstr "你的库是空白的。一旦添加为共享库,此文档库中的素材就可被用于你的其他文档中。" +"你确定要发布它吗?" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.copy-token" +msgstr "复制令牌" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.expiration-date.label" +msgstr "到期时间" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.label" +msgstr "名称" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.name.placeholder" +msgstr "名称可以帮你记住令牌的用途" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.submit-label" +msgstr "创建令牌" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.create-access-token.title" +msgstr "生成访问令牌" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.title" +msgstr "删除令牌" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.message" +msgstr "你确定想要删除这个令牌吗?" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "该库被以下文档使用: " +msgstr[1] "这些库被以下文档使用: " + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "" +msgstr[1] "" + +msgid "modals.publish-empty-library.accept" +msgstr "发布" + +msgid "modals.publish-empty-library.message" +msgstr "你的库是空白的。你确定想要发布它?" + +msgid "modals.publish-empty-library.title" +msgstr "发布空白库" + +#: src/app/main/data/common.cljs +msgid "notifications.by-code.upgrade-version" +msgstr "有新版本可用,请刷新页面" + +msgid "onboarding.choice.team-up.continue-creating-team" +msgstr "继续创建团队" + +msgid "onboarding.choice.team-up.start-without-a-team-description" +msgstr "你可以稍后再创建团队。" + +msgid "onboarding.choice.team-up.start-without-a-team" +msgstr "以个人身份开始" + +msgid "onboarding.choice.team-up.continue-without-a-team" +msgstr "以个人身份继续" + +msgid "onboarding.choice.team-up.create-team-and-send-invites" +msgstr "创建团队并发送邀请" + +msgid "onboarding.choice.team-up.create-team-without-inviting" +msgstr "创建团队但暂不邀请" + +msgid "onboarding.choice.team-up.create-team-and-invite" +msgstr "创建团队 & 邀请" + +msgid "onboarding.choice.team-up.create-team-without-invite" +msgstr "创建团队" + +msgid "onboarding.choice.team-up.create-team-and-send-invites-description" +msgstr "你可以稍后再邀请成员" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.interface-design-visual-assets-design-systems" +msgstr "...界面设计,视觉素材,设计系统等。" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.none" +msgstr "无" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.other" +msgstr "其他(请注明)" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.personal-project" +msgstr "我在做个人项目" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.previous" +msgstr "前一项" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.questions-how-are-you-planning-to-use-penpot" +msgstr "你计划用Penpot做什么?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.sketch" +msgstr "Sketch" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start-to-work-on-my-project" +msgstr "开始着手我的项目" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.student-teacher" +msgstr "学生/教师" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.start" +msgstr "开始" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.test-penpot-to-see-if-its-a-fit-for-team" +msgstr "试用Penpot,来看它是否适合团队 " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.try-out-before-using-penpot-on-premise" +msgstr "在本地部署Penpot前进行试用" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.work-in-concept-ideas" +msgstr "从事概念构想的工作" + +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "Webhook的名称最多包含2048个字符。" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "title.settings.access-tokens" +msgstr "个人资料 — 访问令牌" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.token-will-not-expire" +msgstr "令牌无到期时限" + +msgid "workspace.shape.menu.add-grid" +msgstr "添加网格布局" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.leave-feedback-for-my-team-project" +msgstr "给我的团队项目做反馈" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "branding-illustrations-marketing-pieces" +msgstr "品牌设计、插图、营销物料等。" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.a-lot" +msgstr "非常多" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.wireframes-user-journeys-flows-navigation-trees" +msgstr "...线框图,用户轨迹和用户流程,导航树等。" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "question.design-tool-more-experienced-with" +msgstr "你最熟悉哪个设计工具?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.invision" +msgstr "InVision" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.31-50" +msgstr "31-50" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.more-than-50" +msgstr "50以上" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.freelancer" +msgstr "我是一名自由职业者" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.2-10" +msgstr "2-10" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.copied-success" +msgstr "已复制令牌" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create" +msgstr "生成新令牌" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.empty.add-one" +msgstr "点击“生成新令牌”按钮来生成一个。" + +msgid "labels.discard" +msgstr "丢弃" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.founder" +msgstr "创始人/副总裁" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.figma" +msgstr "Figma" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.get-the-code-from-my-team-project" +msgstr "从我的团队项目获得邀请码 " + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.lets-get-started" +msgstr "让我们开始吧!" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.marketing" +msgstr "市场营销" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.next" +msgstr "下一项" + +msgid "shortcuts.text-align-center" +msgstr "水平居中" + +msgid "shortcuts.text-align-left" +msgstr "靠左对齐" + +msgid "shortcuts.text-align-justify" +msgstr "两端对齐" + +msgid "workspace.options.component.annotation" +msgstr "注释" + +msgid "workspace.options.component.copy" +msgstr "复制" + +msgid "workspace.options.component.create-annotation" +msgstr "创建注释" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "圆形" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.more-info" +msgstr "更多信息" + +msgid "modals.delete-component-annotation.message" +msgstr "你确定想要删除这个注释?" + +msgid "workspace.shape.menu.create-annotation" +msgstr "创建注释" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.adobe-xd" +msgstr "Adobe XD" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.canva" +msgstr "Canva" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "modals.delete-acces-token.accept" +msgstr "删除令牌" + +msgid "modals.delete-component-annotation.title" +msgstr "删除注释" + +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.empty-palette" +msgstr "你的库中还没有颜色风格" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.11-30" +msgstr "11-30" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.designer" +msgstr "设计师" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.developer" +msgstr "开发者" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.discover-more-about-penpot" +msgstr "深入了解Penpot的精彩之处" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.manager" +msgstr "产品经理/项目经理" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.role" +msgstr "你是哪种身份?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.team-size" +msgstr "你的团队有多少人?" + +#: src/app/main/ui/onboarding/questions.cljs +msgid "questions.your-feedback-will-help-us" +msgstr "你的反馈将帮助我们更好地理解你的习惯和偏好,以便我们不断改进Penpot,使其成为" +"一个有用且好用的工具。" + +msgid "shortcuts.select-parent-layer" +msgstr "选择上级图层" + +msgid "shortcuts.text-align-right" +msgstr "靠右对齐" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.libraries.update.see-all-changes" +msgstr "查看所有修改" + +msgid "workspace.assets.open-library" +msgstr "打开库文档" + +msgid "workspace.options.component.edit-annotation" +msgstr "编辑注释" + +msgid "workspace.shape.menu.create-multiple-components" +msgstr "创建多个组件" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.shared-library" +msgstr "共享库" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.zoom" +msgstr "缩放" + +msgid "workspace.layout_grid.editor.title" +msgstr "编辑网格" + +msgid "media.radial" +msgstr "径向" + +msgid "media.gradient" +msgstr "渐变" + +msgid "media.choose-image" +msgstr "选择图片" + +msgid "media.image" +msgstr "图片" + +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "姓名必须包含一些空格以外的字符。" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "姓名最多包含250个字符。" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "密码必须包含一些空格以外的字符。" + +#: src/app/main/ui/auth/register.cljs +#, markdown +msgid "auth.terms-privacy-agreement-md" +msgstr "创建新账号,即代表你同意我们的[服务条例](%s)和[隐私政策](%s)。" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.create.success" +msgstr "成功创建访问令牌。" + +#: src/app/main/ui/settings/access-tokens.cljs +msgid "dashboard.access-tokens.personal.description" +msgstr "个人访问令牌可被理解为密码认证的代替选项,常用于允许(第三方)应用访问Penpot" +"内部API" + +msgid "errors.cannot-upload" +msgstr "无法上传该媒体文件。" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.access-tokens" +msgstr "访问令牌" diff --git a/frontend/translations/zh_Hant.po b/frontend/translations/zh_Hant.po index b4506552f8..ad1fe9a148 100644 --- a/frontend/translations/zh_Hant.po +++ b/frontend/translations/zh_Hant.po @@ -847,7 +847,7 @@ msgstr "電子郵件" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-go-to" -msgstr "前往Twitter" +msgstr "前往X" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-subtitle1" @@ -855,7 +855,7 @@ msgstr "協助解你的決技術問題。" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.twitter-title" -msgstr "Twitter支援帳戶" +msgstr "X支援帳戶" #: src/app/main/ui/settings/password.cljs msgid "generic.error" @@ -1584,9 +1584,6 @@ msgstr "切換調色板" msgid "shortcuts.toggle-focus-mode" msgstr "切換專注模式" -msgid "shortcuts.toggle-grid" -msgstr "顯示/隱藏網格" - msgid "shortcuts.toggle-history" msgstr "切換歷史記錄" @@ -1715,10 +1712,6 @@ msgstr "檔案庫" msgid "workspace.assets.rename" msgstr "重新命名" -#: src/app/main/ui/workspace/sidebar/assets.cljs -msgid "workspace.assets.shared" -msgstr "共用" - #: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.typography" msgstr "字體排版設計" @@ -2116,10 +2109,6 @@ msgstr "外面" msgid "workspace.options.stroke.solid" msgstr "實線" -#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs -msgid "workspace.options.text-options.text-align-center" -msgstr "置中 (%s)" - #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.direction-ltr" msgstr "左至右" @@ -2136,6 +2125,10 @@ msgstr "小寫" msgid "workspace.options.text-options.none" msgstr "無" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-align-center" +msgstr "置中 (%s)" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "文字" @@ -2328,4 +2321,4 @@ msgid "workspace.updates.update" msgstr "更新" msgid "workspace.viewport.click-to-close-path" -msgstr "點擊以關閉路徑" \ No newline at end of file +msgstr "點擊以關閉路徑" diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000000..119e8e7aa1 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from "vite"; +import { configDefaults } from 'vitest/config' + +import { resolve } from "path"; + +// https://vitejs.dev/config/ +export default defineConfig({ + test: { + exclude: [...configDefaults.exclude, 'target/**', 'resources/**'], + environment: 'jsdom' + }, + + resolve: { + alias: { + "@target": resolve(__dirname, "./target/storybook"), + }, + }, +}); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 29d69bb78e..d4531a5c9f 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1,5961 +1,16069 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/runtime-corejs3@^7.16.5": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.18.6.tgz#6f02c5536911f4b445946a2179554b95c8838635" - integrity sha512-cOu5wH2JFBgMjje+a+fz2JNIWU4GzYpl05oSob3UDvBEh6EuIn+TXFHMmBbhSb+k/4HMzgKCQfEEDArAWNF9Cw== - dependencies: - core-js-pure "^3.20.2" - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.21.0": - version "7.21.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200" - integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q== - dependencies: - regenerator-runtime "^0.13.11" - -"@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" - integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ== - dependencies: - regenerator-runtime "^0.13.11" - -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== - -"@cypress/request@^2.88.10": - version "2.88.10" - resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce" - integrity sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - http-signature "~1.3.6" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^8.3.2" - -"@cypress/xvfb@^1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" - integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== - dependencies: - debug "^3.1.0" - lodash.once "^4.1.1" - -"@dabh/diagnostics@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" - integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== - dependencies: - colorspace "1.1.x" - enabled "2.0.x" - kuler "^2.0.0" - -"@gulp-sourcemaps/identity-map@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz#a6e8b1abec8f790ec6be2b8c500e6e68037c0019" - integrity sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q== - dependencies: - acorn "^6.4.1" - normalize-path "^3.0.0" - postcss "^7.0.16" - source-map "^0.6.0" - through2 "^3.0.1" - -"@gulp-sourcemaps/map-sources@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz#890ae7c5d8c877f6d384860215ace9d7ec945bda" - integrity sha1-iQrnxdjId/bThIYCFazp1+yUW9o= - dependencies: - normalize-path "^2.0.1" - through2 "^2.0.3" - -"@sentry/browser@^6.17.4": - version "6.17.9" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.17.9.tgz#62eac0cc3c7c788df6b4677fe9882d3974d84027" - integrity sha512-RsC8GBZmZ3YfBTaIOJ06RlFp5zG7BkUoquNJmf4YhRUZeihT9osrn8qUYGFWSV/UduwKUIlSGJA/rATWWhwPRQ== - dependencies: - "@sentry/core" "6.17.9" - "@sentry/types" "6.17.9" - "@sentry/utils" "6.17.9" - tslib "^1.9.3" - -"@sentry/core@6.17.9": - version "6.17.9" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.17.9.tgz#1c09f1f101207952566349a1921d46db670c8f62" - integrity sha512-14KalmTholGUtgdh9TklO+jUpyQ/D3OGkhlH1rnGQGoJgFy2eYm+s+MnUEMxFdGIUCz5kOteuNqYZxaDmFagpQ== - dependencies: - "@sentry/hub" "6.17.9" - "@sentry/minimal" "6.17.9" - "@sentry/types" "6.17.9" - "@sentry/utils" "6.17.9" - tslib "^1.9.3" - -"@sentry/hub@6.17.9": - version "6.17.9" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.17.9.tgz#f2c355088a49045e49feafb5356ca5d6e1e31d3c" - integrity sha512-34EdrweWDbBV9EzEFIXcO+JeoyQmKzQVJxpTKZoJA6PUwf2NrndaUdjlkDEtBEzjuLUTxhLxtOzEsYs1O6RVcg== - dependencies: - "@sentry/types" "6.17.9" - "@sentry/utils" "6.17.9" - tslib "^1.9.3" - -"@sentry/minimal@6.17.9": - version "6.17.9" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.17.9.tgz#0edca978097b3f56463ede028395d40adbf2ae84" - integrity sha512-T3PMCHcKk6lkZq6zKgANrYJJxXBXKOe+ousV1Fas1rVBMv7dtKfsa4itqQHszcW9shusPDiaQKIJ4zRLE5LKmg== - dependencies: - "@sentry/hub" "6.17.9" - "@sentry/types" "6.17.9" - tslib "^1.9.3" - -"@sentry/tracing@^6.17.4": - version "6.17.9" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.17.9.tgz#d4a6d96d88f10c9cd496e5b32f44d6e67d4c5dc7" - integrity sha512-5Rb/OS4ryNJLvz2nv6wyjwhifjy6veqaF9ffLrwFYij/WDy7m62ASBblxgeiI3fbPLX0aBRFWIJAq1vko26+AQ== - dependencies: - "@sentry/hub" "6.17.9" - "@sentry/minimal" "6.17.9" - "@sentry/types" "6.17.9" - "@sentry/utils" "6.17.9" - tslib "^1.9.3" - -"@sentry/types@6.17.9": - version "6.17.9" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.17.9.tgz#d579c33cde0301adaf8ff4762479ad017bf0dffa" - integrity sha512-xuulX6qUCL14ayEOh/h6FUIvZtsi1Bx34dSOaWDrjXUOJHJAM7214uiqW1GZxPJ13YuaUIubjTSfDmSQ9CBzTw== - -"@sentry/utils@6.17.9": - version "6.17.9" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.17.9.tgz#425fe9af4e2d6114c2e9aaede75ccb6ddf91fbda" - integrity sha512-4eo9Z3JlJCGlGrQRbtZWL+L9NnlUXgTbfK3Lk7oO8D1ev8R5b5+iE6tZHTvU5rQRcq6zu+POT+tK5u9oxc/rnQ== - dependencies: - "@sentry/types" "6.17.9" - tslib "^1.9.3" - -"@types/node@*": - version "17.0.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.18.tgz#3b4fed5cfb58010e3a2be4b6e74615e4847f1074" - integrity sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA== - -"@types/node@^14.14.31": - version "14.18.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.12.tgz#0d4557fd3b94497d793efd4e7d92df2f83b4ef24" - integrity sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A== - -"@types/q@^1.5.1": - version "1.5.5" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" - integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== - -"@types/sinonjs__fake-timers@8.1.1": - version "8.1.1" - resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" - integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== - -"@types/sizzle@^2.3.2": - version "2.3.3" - resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" - integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== - -"@types/yauzl@^2.9.1": - version "2.9.2" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a" - integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA== - dependencies: - "@types/node" "*" - -"@xmldom/xmldom@^0.7.5": - version "0.7.5" - resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d" - integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A== - -abbrev@1, abbrev@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -acorn@^6.4.1: - version "6.4.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" - integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv@^6.12.3: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-colors@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" - integrity sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA== - dependencies: - ansi-wrap "^0.1.0" - -ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== - -ansi-escapes@^4.3.0: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-gray@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" - integrity sha1-KWLPVOyXksSFEKPetSRDaGHvclE= - dependencies: - ansi-wrap "0.1.0" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-wrap@0.1.0, ansi-wrap@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" - integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= - -any-promise@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -append-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1" - integrity sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE= - dependencies: - buffer-equal "^1.0.0" - -arch@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" - integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== - -archy@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" - integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-filter@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/arr-filter/-/arr-filter-1.1.2.tgz#43fdddd091e8ef11aa4c45d9cdc18e2dff1711ee" - integrity sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4= - dependencies: - make-iterator "^1.0.0" - -arr-flatten@^1.0.1, arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-map@^2.0.0, arr-map@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/arr-map/-/arr-map-2.0.2.tgz#3a77345ffc1cf35e2a91825601f9e58f2e24cac4" - integrity sha1-Onc0X/wc814qkYJWAfnljy4kysQ= - dependencies: - make-iterator "^1.0.0" - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-each@^1.0.0, array-each@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" - integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8= - -array-initial@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/array-initial/-/array-initial-1.1.0.tgz#2fa74b26739371c3947bd7a7adc73be334b3d795" - integrity sha1-L6dLJnOTccOUe9enrcc74zSz15U= - dependencies: - array-slice "^1.0.0" - is-number "^4.0.0" - -array-last@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/array-last/-/array-last-1.3.0.tgz#7aa77073fec565ddab2493f5f88185f404a9d336" - integrity sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg== - dependencies: - is-number "^4.0.0" - -array-slice@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" - integrity sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w== - -array-sort@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-sort/-/array-sort-1.0.0.tgz#e4c05356453f56f53512a7d1d6123f2c54c0a88a" - integrity sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg== - dependencies: - default-compare "^1.0.0" - get-value "^2.0.6" - kind-of "^5.0.2" - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -asap@~2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= - -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -assert@^1.1.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - -async-done@^1.2.0, async-done@^1.2.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/async-done/-/async-done-1.3.2.tgz#5e15aa729962a4b07414f528a88cdf18e0b290a2" - integrity sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.2" - process-nextick-args "^2.0.0" - stream-exhaust "^1.0.1" - -async-each@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" - integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== - -async-settle@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/async-settle/-/async-settle-1.0.0.tgz#1d0a914bb02575bec8a8f3a74e5080f72b2c0c6b" - integrity sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs= - dependencies: - async-done "^1.2.2" - -async@^3.2.0, async@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" - integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -autoprefixer@^10.4.13: - version "10.4.14" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d" - integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== - dependencies: - browserslist "^4.21.5" - caniuse-lite "^1.0.30001464" - fraction.js "^4.2.0" - normalize-range "^0.1.2" - picocolors "^1.0.0" - postcss-value-parser "^4.2.0" - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== - -bach@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880" - integrity sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA= - dependencies: - arr-filter "^1.1.1" - arr-flatten "^1.0.1" - arr-map "^2.0.0" - array-each "^1.0.0" - array-initial "^1.0.0" - array-last "^1.1.1" - async-done "^1.2.2" - async-settle "^1.0.0" - now-and-later "^2.0.0" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.0.2, base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bintrees@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8" - integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw== - -blob-util@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" - integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== - -bluebird@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -bn.js@^5.0.0, bn.js@^5.1.1: - version "5.2.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" - integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== - -boolbase@^1.0.0, boolbase@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -braces@^2.3.1, braces@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -brorand@^1.0.1, brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.3" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserslist@^4.21.5: - version "4.21.9" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.9.tgz#e11bdd3c313d7e2a9e87e8b4b0c7872b13897635" - integrity sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg== - dependencies: - caniuse-lite "^1.0.30001503" - electron-to-chromium "^1.4.431" - node-releases "^2.0.12" - update-browserslist-db "^1.0.11" - -buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= - -buffer-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" - integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -buffer@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= - -bytes@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -cachedir@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" - integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001503: - version "1.0.30001512" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001512.tgz#7450843fb581c39f290305a83523c7a9ef0d4cb4" - integrity sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw== - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -chalk@^2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -check-more-types@^2.24.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" - integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= - -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.2: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chokidar@^2.0.0: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - -ci-info@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" - integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw== - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -clean-css@^4.x: - version "4.2.4" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178" - integrity sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A== - dependencies: - source-map "~0.6.0" - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-table3@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8" - integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA== - dependencies: - string-width "^4.2.0" - optionalDependencies: - colors "1.4.0" - -cli-truncate@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" - integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== - dependencies: - slice-ansi "^3.0.0" - string-width "^4.2.0" - -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - -clone-buffer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" - integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= - -clone-stats@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" - integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= - -clone@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" - integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= - -cloneable-readable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.3.tgz#120a00cb053bfb63a222e709f9683ea2e11d8cec" - integrity sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ== - dependencies: - inherits "^2.0.1" - process-nextick-args "^2.0.0" - readable-stream "^2.3.5" - -clsx@^1.0.4: - version "1.2.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" - integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== - -coa@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" - integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== - dependencies: - "@types/q" "^1.5.1" - chalk "^2.4.1" - q "^1.1.2" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-map@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-map/-/collection-map-1.0.0.tgz#aea0f06f8d26c780c2b75494385544b2255af18c" - integrity sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw= - dependencies: - arr-map "^2.0.2" - for-own "^1.0.0" - make-iterator "^1.0.0" - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0, color-convert@^1.9.3: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@^1.0.0, color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^1.6.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" - integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - -color@^3.1.3: - version "3.2.1" - resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" - integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== - dependencies: - color-convert "^1.9.3" - color-string "^1.6.0" - -colorette@^2.0.16: - version "2.0.16" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" - integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== - -colors@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" - integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== - -colorspace@1.1.x: - version "1.1.4" - resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" - integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== - dependencies: - color "^3.1.3" - text-hex "1.0.x" - -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@^2.19.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== - -common-tags@^1.8.0: - version "1.8.2" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" - integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -concat-stream@^1.6.0, concat-stream@^1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -concat-with-sourcemaps@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz#d4ea93f05ae25790951b99e7b3b09e3908a4082e" - integrity sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg== - dependencies: - source-map "^0.6.1" - -config-chain@^1.1.13: - version "1.1.13" - resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" - integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - -console-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= - -content-type@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -convert-source-map@^1.0.0, convert-source-map@^1.5.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" - integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== - dependencies: - safe-buffer "~5.1.1" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -copy-props@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/copy-props/-/copy-props-2.0.5.tgz#03cf9ae328d4ebb36f8f1d804448a6af9ee3f2d2" - integrity sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw== - dependencies: - each-props "^1.3.2" - is-plain-object "^5.0.0" - -core-js-pure@^3.20.2: - version "3.21.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.21.1.tgz#8c4d1e78839f5f46208de7230cebfb72bc3bdb51" - integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ== - -core-js@^3.6.4: - version "3.21.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94" - integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig== - -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -cross-fetch@^3.0.4: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - -cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -cross-spawn@^7.0.0: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -css-select-base-adapter@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" - integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== - -css-select@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" - integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== - dependencies: - boolbase "^1.0.0" - css-what "^3.2.1" - domutils "^1.7.0" - nth-check "^1.0.2" - -css-selector-parser@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.4.1.tgz#03f9cb8a81c3e5ab2c51684557d5aaf6d2569759" - integrity sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g== - -css-tree@1.0.0-alpha.37: - version "1.0.0-alpha.37" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" - integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== - dependencies: - mdn-data "2.0.4" - source-map "^0.6.1" - -css-tree@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - -css-what@^3.2.1: - version "3.4.2" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" - integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== - -css@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" - integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== - dependencies: - inherits "^2.0.4" - source-map "^0.6.1" - source-map-resolve "^0.6.0" - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssmin@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/cssmin/-/cssmin-0.4.3.tgz#c9194077e0ebdacd691d5f59015b9d819f38d015" - integrity sha1-yRlAd+Dr2s1pHV9ZAVudgZ840BU= - -csso@^4.0.2: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== - dependencies: - css-tree "^1.1.2" - -cssom@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" - integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== - -csstype@^3.0.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" - integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== - -cypress-file-upload@^5.0.8: - version "5.0.8" - resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1" - integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g== - -cypress@^10.3.0: - version "10.3.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.3.0.tgz#fae8d32f0822fcfb938e79c7c31ef344794336ae" - integrity sha512-txkQWKzvBVnWdCuKs5Xc08gjpO89W2Dom2wpZgT9zWZT5jXxqPIxqP/NC1YArtkpmp3fN5HW8aDjYBizHLUFvg== - dependencies: - "@cypress/request" "^2.88.10" - "@cypress/xvfb" "^1.2.4" - "@types/node" "^14.14.31" - "@types/sinonjs__fake-timers" "8.1.1" - "@types/sizzle" "^2.3.2" - arch "^2.2.0" - blob-util "^2.0.2" - bluebird "^3.7.2" - buffer "^5.6.0" - cachedir "^2.3.0" - chalk "^4.1.0" - check-more-types "^2.24.0" - cli-cursor "^3.1.0" - cli-table3 "~0.6.1" - commander "^5.1.0" - common-tags "^1.8.0" - dayjs "^1.10.4" - debug "^4.3.2" - enquirer "^2.3.6" - eventemitter2 "^6.4.3" - execa "4.1.0" - executable "^4.1.1" - extract-zip "2.0.1" - figures "^3.2.0" - fs-extra "^9.1.0" - getos "^3.2.1" - is-ci "^3.0.0" - is-installed-globally "~0.4.0" - lazy-ass "^1.6.0" - listr2 "^3.8.3" - lodash "^4.17.21" - log-symbols "^4.0.0" - minimist "^1.2.6" - ospath "^1.2.2" - pretty-bytes "^5.6.0" - proxy-from-env "1.0.0" - request-progress "^3.0.0" - semver "^7.3.2" - supports-color "^8.1.1" - tmp "~0.2.1" - untildify "^4.0.0" - yauzl "^2.10.0" - -d@1, d@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" - integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== - dependencies: - es5-ext "^0.10.50" - type "^1.0.1" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -date-fns@^2.30.0: - version "2.30.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" - integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== - dependencies: - "@babel/runtime" "^7.21.0" - -dayjs@^1.10.4: - version "1.10.7" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" - integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== - -debug-fabulous@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/debug-fabulous/-/debug-fabulous-1.1.0.tgz#af8a08632465224ef4174a9f06308c3c2a1ebc8e" - integrity sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg== - dependencies: - debug "3.X" - memoizee "0.4.X" - object-assign "4.X" - -debug@3.X, debug@^3.1.0, debug@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^4.1.1, debug@^4.3.2: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - -decamelize@^1.1.1, decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -default-compare@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f" - integrity sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ== - dependencies: - kind-of "^5.0.2" - -default-resolution@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684" - integrity sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ= - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -detect-file@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" - integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= - -detect-newline@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" - integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dom-helpers@^5.1.3: - version "5.2.1" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" - integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== - dependencies: - "@babel/runtime" "^7.8.7" - csstype "^3.0.2" - -dom-serializer@0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== - dependencies: - domelementtype "^2.0.1" - entities "^2.0.0" - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -domelementtype@1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domelementtype@^2.0.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" - integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== - -domutils@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -draft-js@^0.11.7: - version "0.11.7" - resolved "https://registry.yarnpkg.com/draft-js/-/draft-js-0.11.7.tgz#be293aaa255c46d8a6647f3860aa4c178484a206" - integrity sha512-ne7yFfN4sEL82QPQEn80xnADR8/Q6ALVworbC5UOSzOvjffmYfFsr3xSZtxbIirti14R7Y33EZC5rivpLgIbsg== - dependencies: - fbjs "^2.0.0" - immutable "~3.7.4" - object-assign "^4.1.1" - -duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -each-props@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/each-props/-/each-props-1.3.2.tgz#ea45a414d16dd5cfa419b1a81720d5ca06892333" - integrity sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA== - dependencies: - is-plain-object "^2.0.1" - object.defaults "^1.1.0" - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -editorconfig@^0.15.3: - version "0.15.3" - resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" - integrity sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g== - dependencies: - commander "^2.19.0" - lru-cache "^4.1.5" - semver "^5.6.0" - sigmund "^1.0.1" - -electron-to-chromium@^1.4.431: - version "1.4.451" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.451.tgz#12b63ee5c82cbbc7b4ddd91e90f5a0dfc10de26e" - integrity sha512-YYbXHIBxAHe3KWvGOJOuWa6f3tgow44rBW+QAuwVp2DvGqNZeE//K2MowNdWS7XE8li5cgQDrX1LdBr41LufkA== - -elliptic@^6.5.3: - version "6.5.4" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -enabled@2.0.x: - version "2.0.0" - resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" - integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== - -encoding@^0.1.13: - version "0.1.13" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" - integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== - dependencies: - iconv-lite "^0.6.2" - -end-of-stream@^1.0.0, end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enquirer@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== - dependencies: - ansi-colors "^4.1.1" - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -error-ex@^1.2.0, error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.17.2, es-abstract@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" - integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^1.1.1" - get-symbol-description "^1.0.0" - has "^1.0.3" - has-symbols "^1.0.2" - internal-slot "^1.0.3" - is-callable "^1.2.4" - is-negative-zero "^2.0.1" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.1" - is-string "^1.0.7" - is-weakref "^1.0.1" - object-inspect "^1.11.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.1" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: - version "0.10.53" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" - integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== - dependencies: - es6-iterator "~2.0.3" - es6-symbol "~3.1.3" - next-tick "~1.0.0" - -es6-iterator@^2.0.1, es6-iterator@^2.0.3, es6-iterator@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" - integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= - dependencies: - d "1" - es5-ext "^0.10.35" - es6-symbol "^3.1.1" - -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-symbol@^3.1.1, es6-symbol@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" - integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== - dependencies: - d "^1.0.1" - ext "^1.1.2" - -es6-weak-map@^2.0.1, es6-weak-map@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" - integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== - dependencies: - d "1" - es5-ext "^0.10.46" - es6-iterator "^2.0.3" - es6-symbol "^3.1.1" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -event-emitter@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" - integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= - dependencies: - d "1" - es5-ext "~0.10.14" - -eventemitter2@^6.4.3: - version "6.4.5" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.5.tgz#97380f758ae24ac15df8353e0cc27f8b95644655" - integrity sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw== - -events@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -execa@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" - integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== - dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" - -executable@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" - integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== - dependencies: - pify "^2.2.0" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-tilde@^2.0.0, expand-tilde@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" - integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= - dependencies: - homedir-polyfill "^1.0.1" - -ext@^1.1.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/ext/-/ext-1.6.0.tgz#3871d50641e874cc172e2b53f919842d19db4c52" - integrity sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg== - dependencies: - type "^2.5.0" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@^3.0.0, extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -extract-zip@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" - integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - -extract-zip@^1.6.5: - version "1.7.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== - dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" - yauzl "^2.10.0" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - -fancy-log@^1.3.2, fancy-log@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" - integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== - dependencies: - ansi-gray "^0.1.1" - color-support "^1.1.3" - parse-node-version "^1.0.0" - time-stamp "^1.0.0" - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz#e6a754cc8f15e58987aa9cbd27af66fd6f4e5af9" - integrity sha1-5qdUzI8V5YmHqpy9J69m/W9OWvk= - -fbjs-css-vars@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" - integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== - -fbjs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-2.0.0.tgz#01fb812138d7e31831ed3e374afe27b9169ef442" - integrity sha512-8XA8ny9ifxrAWlyhAbexXcs3rRMtxWcs3M0lctLfB49jRDHiaxj+Mo0XxbwE7nKZYzgCFoq64FS+WFd4IycPPQ== - dependencies: - core-js "^3.6.4" - cross-fetch "^3.0.4" - fbjs-css-vars "^1.0.0" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.18" - -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= - dependencies: - pend "~1.2.0" - -fecha@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce" - integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== - -figures@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -findup-sync@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" - integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw= - dependencies: - detect-file "^1.0.0" - is-glob "^3.1.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - -findup-sync@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" - integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== - dependencies: - detect-file "^1.0.0" - is-glob "^4.0.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - -fined@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fined/-/fined-1.2.0.tgz#d00beccf1aa2b475d16d423b0238b713a2c4a37b" - integrity sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng== - dependencies: - expand-tilde "^2.0.2" - is-plain-object "^2.0.3" - object.defaults "^1.1.0" - object.pick "^1.2.0" - parse-filepath "^1.0.1" - -flagged-respawn@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41" - integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q== - -flush-write-stream@^1.0.2: - version "1.1.1" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" - integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== - dependencies: - inherits "^2.0.3" - readable-stream "^2.3.6" - -fn.name@1.x.x: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" - integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== - -for-in@^1.0.1, for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -for-own@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" - integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= - dependencies: - for-in "^1.0.1" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -fraction.js@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" - integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fs-extra@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" - integrity sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA= - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" - -fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-mkdirp-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz#0b7815fc3201c6a69e14db98ce098c16935259eb" - integrity sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes= - dependencies: - graceful-fs "^4.1.11" - through2 "^2.0.3" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@^1.2.7: - version "1.2.13" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" - integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -generic-names@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-4.0.0.tgz#0bd8a2fd23fe8ea16cbd0a279acd69c06933d9a3" - integrity sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A== - dependencies: - loader-utils "^3.2.0" - -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" - integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - -get-stream@^5.0.0, get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -getos@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" - integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== - dependencies: - async "^3.2.0" - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - -gettext-parser@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/gettext-parser/-/gettext-parser-5.1.2.tgz#ebecc29a905334a48680a57b2d64da920f781cd4" - integrity sha512-TaCShmFIQDvic6Ao+LFvFSPyl/9sjua3zNHMfmjfzzEeK3NIPbBSbNdPihJ+vG476td+ylrVk0ZyjJaAy9CiwQ== - dependencies: - content-type "^1.0.4" - encoding "^0.1.13" - readable-stream "^3.6.0" - safe-buffer "^5.2.1" - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-stream@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" - integrity sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ= - dependencies: - extend "^3.0.0" - glob "^7.1.1" - glob-parent "^3.1.0" - is-negated-glob "^1.0.0" - ordered-read-streams "^1.0.0" - pumpify "^1.3.5" - readable-stream "^2.1.5" - remove-trailing-separator "^1.0.1" - to-absolute-glob "^2.0.0" - unique-stream "^2.0.2" - -glob-watcher@^5.0.3: - version "5.0.5" - resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-5.0.5.tgz#aa6bce648332924d9a8489be41e3e5c52d4186dc" - integrity sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw== - dependencies: - anymatch "^2.0.0" - async-done "^1.2.0" - chokidar "^2.0.0" - is-negated-glob "^1.0.0" - just-debounce "^1.0.0" - normalize-path "^3.0.0" - object.defaults "^1.1.0" - -glob@^7.1.1, glob@^7.1.3, glob@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -global-dirs@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" - integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA== - dependencies: - ini "2.0.0" - -global-modules@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" - integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== - dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" - -global-prefix@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" - integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= - dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" - -glogg@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.2.tgz#2d7dd702beda22eb3bffadf880696da6d846313f" - integrity sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA== - dependencies: - sparkles "^1.0.0" - -graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0: - version "4.2.9" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" - integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== - -gulp-cli@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/gulp-cli/-/gulp-cli-2.3.0.tgz#ec0d380e29e52aa45e47977f0d32e18fd161122f" - integrity sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A== - dependencies: - ansi-colors "^1.0.1" - archy "^1.0.0" - array-sort "^1.0.0" - color-support "^1.1.3" - concat-stream "^1.6.0" - copy-props "^2.0.1" - fancy-log "^1.3.2" - gulplog "^1.0.0" - interpret "^1.4.0" - isobject "^3.0.1" - liftoff "^3.1.0" - matchdep "^2.0.0" - mute-stdout "^1.0.0" - pretty-hrtime "^1.0.0" - replace-homedir "^1.0.0" - semver-greatest-satisfied-range "^1.1.0" - v8flags "^3.2.0" - yargs "^7.1.0" - -gulp-concat@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/gulp-concat/-/gulp-concat-2.6.1.tgz#633d16c95d88504628ad02665663cee5a4793353" - integrity sha1-Yz0WyV2IUEYorQJmVmPO5aR5M1M= - dependencies: - concat-with-sourcemaps "^1.0.0" - through2 "^2.0.0" - vinyl "^2.0.0" - -gulp-gzip@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/gulp-gzip/-/gulp-gzip-1.4.2.tgz#0422a94014248655b5b1a9eea1c2abee1d4f4337" - integrity sha512-ZIxfkUwk2XmZPTT9pPHrHUQlZMyp9nPhg2sfoeN27mBGpi7OaHnOD+WCN41NXjfJQ69lV1nQ9LLm1hYxx4h3UQ== - dependencies: - ansi-colors "^1.0.1" - bytes "^3.0.0" - fancy-log "^1.3.2" - plugin-error "^1.0.0" - stream-to-array "^2.3.0" - through2 "^2.0.3" - -gulp-mustache@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/gulp-mustache/-/gulp-mustache-5.0.0.tgz#5ebc8bbb36a0e657391b341f11325579d4502b07" - integrity sha512-8tk0R1Fd+l6+e/t954e3UheFo25dKkTapPLD1sWoSroPXfIPxyHVgbhfH5VJGqeXl3te5GOwPtfcxxZJ+PYoFg== - dependencies: - escape-string-regexp "^2.0.0" - mustache "^4.0.1" - plugin-error "^1.0.0" - replace-ext "^1.0.0" - through2 "^3.0.1" - -gulp-postcss@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/gulp-postcss/-/gulp-postcss-9.0.1.tgz#d43caa2f2ce1018f889f7c1296faf82e9928b66f" - integrity sha512-9QUHam5JyXwGUxaaMvoFQVT44tohpEFpM8xBdPfdwTYGM0AItS1iTQz0MpsF8Jroh7GF5Jt2GVPaYgvy8qD2Fw== - dependencies: - fancy-log "^1.3.3" - plugin-error "^1.0.1" - postcss-load-config "^3.0.0" - vinyl-sourcemaps-apply "^0.2.1" - -gulp-rename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-2.0.0.tgz#9bbc3962b0c0f52fc67cd5eaff6c223ec5b9cf6c" - integrity sha512-97Vba4KBzbYmR5VBs9mWmK+HwIf5mj+/zioxfZhOKeXtx5ZjBk57KFlePf5nxq9QsTtFl0ejnHE3zTC9MHXqyQ== - -gulp-sass@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/gulp-sass/-/gulp-sass-5.1.0.tgz#bb3d9094f39a260f62a8d0a6797b95ab826f9663" - integrity sha512-7VT0uaF+VZCmkNBglfe1b34bxn/AfcssquLKVDYnCDJ3xNBaW7cUuI3p3BQmoKcoKFrs9jdzUxyb+u+NGfL4OQ== - dependencies: - lodash.clonedeep "^4.5.0" - picocolors "^1.0.0" - plugin-error "^1.0.1" - replace-ext "^2.0.0" - strip-ansi "^6.0.1" - vinyl-sourcemaps-apply "^0.2.1" - -gulp-sourcemaps@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz#2e154e1a2efed033c0e48013969e6f30337b2743" - integrity sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ== - dependencies: - "@gulp-sourcemaps/identity-map" "^2.0.1" - "@gulp-sourcemaps/map-sources" "^1.0.0" - acorn "^6.4.1" - convert-source-map "^1.0.0" - css "^3.0.0" - debug-fabulous "^1.0.0" - detect-newline "^2.0.0" - graceful-fs "^4.0.0" - source-map "^0.6.0" - strip-bom-string "^1.0.0" - through2 "^2.0.0" - -gulp-svg-sprite@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/gulp-svg-sprite/-/gulp-svg-sprite-1.5.0.tgz#292694c6af8570093f62cba09092ec8e5241d322" - integrity sha512-xLepqh1DjCSNm+secZsxmoWKNTXAXCC6Tglop0e2oOoiIqwBWOvep5Y+qvqRP9L3dn8qBxKkqGot8aTheNGYoQ== - dependencies: - plugin-error "^1.0.1" - svg-sprite "^1.5.0" - through2 "^2.0.3" - -gulp@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/gulp/-/gulp-4.0.2.tgz#543651070fd0f6ab0a0650c6a3e6ff5a7cb09caa" - integrity sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA== - dependencies: - glob-watcher "^5.0.3" - gulp-cli "^2.2.0" - undertaker "^1.2.1" - vinyl-fs "^3.0.0" - -gulplog@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5" - integrity sha1-4oxNRdBey77YGDY86PnFkmIp/+U= - dependencies: - glogg "^1.0.0" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - -has-bigints@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-symbols@^1.0.1, has-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== - -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hasha@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1" - integrity sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE= - dependencies: - is-stream "^1.0.1" - pinkie-promise "^2.0.0" - -highlight.js@^11.8.0: - version "11.8.0" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.8.0.tgz#966518ea83257bae2e7c9a48596231856555bb65" - integrity sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg== - -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" - -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -http-signature@~1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" - integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== - dependencies: - assert-plus "^1.0.0" - jsprim "^2.0.2" - sshpk "^1.14.1" - -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= - -human-signals@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" - integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== - -iconv-lite@^0.6.2: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -icss-utils@^5.0.0, icss-utils@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" - integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== - -ieee754@^1.1.13, ieee754@^1.1.4: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore-by-default@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" - integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== - -immediate@~3.0.5: - version "3.0.6" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" - integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= - -immutable@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" - integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw== - -immutable@~3.7.4: - version "3.7.6" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b" - integrity sha1-E7TTyxK++hVIKib+Gy665kAHHks= - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - -ini@^1.3.4: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -internal-slot@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" - integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== - dependencies: - get-intrinsic "^1.1.0" - has "^1.0.3" - side-channel "^1.0.4" - -interpret@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - -is-absolute@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" - integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== - dependencies: - is-relative "^1.0.0" - is-windows "^1.0.1" - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - dependencies: - binary-extensions "^1.0.0" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-callable@^1.1.4, is-callable@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" - integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== - -is-ci@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" - integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== - dependencies: - ci-info "^3.2.0" - -is-core-module@^2.8.1: - version "2.8.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" - integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== - dependencies: - has "^1.0.3" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-date-object@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-installed-globally@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" - integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== - dependencies: - global-dirs "^3.0.0" - is-path-inside "^3.0.2" - -is-negated-glob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" - integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI= - -is-negative-zero@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" - integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== - -is-number-object@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" - integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== - dependencies: - has-tostringtag "^1.0.0" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-number@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" - integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-path-inside@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-plain-object@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" - integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== - -is-promise@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" - integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== - -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-relative@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" - integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== - dependencies: - is-unc-path "^1.0.0" - -is-shared-array-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" - integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== - -is-stream@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-unc-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" - integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== - dependencies: - unc-path-regex "^0.1.2" - -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - -is-utf8@^0.2.0, is-utf8@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-valid-glob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" - integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= - -is-weakref@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== - dependencies: - call-bind "^1.0.2" - -is-windows@^1.0.1, is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -js-beautify@^1.14.7: - version "1.14.8" - resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.14.8.tgz#e0c570c15b5445b006de6d9a3e70fb62f9e408e9" - integrity sha512-4S7HFeI9YfRvRgKnEweohs0tgJj28InHVIj4Nl8Htf96Y6pHg3+tJrmo4ucAM9f7l4SHbFI3IvFAZ2a1eQPbyg== - dependencies: - config-chain "^1.1.13" - editorconfig "^0.15.3" - glob "^8.1.0" - nopt "^6.0.0" - -"js-tokens@^3.0.0 || ^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1, js-yaml@^3.14.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -jsonfile@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" - integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug= - optionalDependencies: - graceful-fs "^4.1.6" - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - -jsprim@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" - integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - -jszip@^3.10.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" - integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== - dependencies: - lie "~3.3.0" - pako "~1.0.2" - readable-stream "~2.3.6" - setimmediate "^1.0.5" - -just-debounce@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.1.0.tgz#2f81a3ad4121a76bc7cb45dbf704c0d76a8e5ddf" - integrity sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ== - -kew@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" - integrity sha1-edk9LTM2PW/dKXCzNdkUGtWR15s= - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0, kind-of@^5.0.2: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -klaw@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" - integrity sha1-QIhDO0azsbolnXh4XY6W9zugJDk= - optionalDependencies: - graceful-fs "^4.1.9" - -kuler@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" - integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== - -last-run@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/last-run/-/last-run-1.1.1.tgz#45b96942c17b1c79c772198259ba943bebf8ca5b" - integrity sha1-RblpQsF7HHnHchmCWbqUO+v4yls= - dependencies: - default-resolution "^2.0.0" - es6-weak-map "^2.0.1" - -lazy-ass@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" - integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= - -lazystream@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" - integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== - dependencies: - readable-stream "^2.0.5" - -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - -lead@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42" - integrity sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI= - dependencies: - flush-write-stream "^1.0.2" - -lie@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" - integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== - dependencies: - immediate "~3.0.5" - -liftoff@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" - integrity sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog== - dependencies: - extend "^3.0.0" - findup-sync "^3.0.0" - fined "^1.0.1" - flagged-respawn "^1.0.0" - is-plain-object "^2.0.4" - object.map "^1.0.0" - rechoir "^0.6.2" - resolve "^1.1.7" - -lilconfig@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" - integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA== - -listr2@^3.8.3: - version "3.14.0" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" - integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== - dependencies: - cli-truncate "^2.1.0" - colorette "^2.0.16" - log-update "^4.0.0" - p-map "^4.0.0" - rfdc "^1.3.0" - rxjs "^7.5.1" - through "^2.3.8" - wrap-ansi "^7.0.0" - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - -loader-utils@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" - integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== - -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - -lodash.once@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= - -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log-symbols@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - -log-update@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" - integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== - dependencies: - ansi-escapes "^4.3.0" - cli-cursor "^3.1.0" - slice-ansi "^4.0.0" - wrap-ansi "^6.2.0" - -logform@^2.3.2, logform@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/logform/-/logform-2.4.0.tgz#131651715a17d50f09c2a2c1a524ff1a4164bcfe" - integrity sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw== - dependencies: - "@colors/colors" "1.5.0" - fecha "^4.2.0" - ms "^2.1.1" - safe-stable-stringify "^2.3.1" - triple-beam "^1.3.0" - -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lru-cache@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -lru-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" - integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM= - dependencies: - es5-ext "~0.10.2" - -luxon@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48" - integrity sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg== - -make-iterator@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" - integrity sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw== - dependencies: - kind-of "^6.0.2" - -map-cache@^0.2.0, map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-stream@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" - integrity sha1-ih8HiW2CsQkmvTdEokIACfiJdKg= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -marked@^4.2.5: - version "4.2.5" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.5.tgz#979813dfc1252cc123a79b71b095759a32f42a5d" - integrity sha512-jPueVhumq7idETHkb203WDD4fMA3yV9emQ5vLwop58lu8bTclMghBWcYAavlDqIEMaisADinV1TooIFCfqOsYQ== - -matchdep@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e" - integrity sha1-xvNINKDY28OzfCfui7yyfHd1WC4= - dependencies: - findup-sync "^2.0.0" - micromatch "^3.0.4" - resolve "^1.4.0" - stack-trace "0.0.10" - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== - -mdn-data@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" - integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== - -memoizee@0.4.X: - version "0.4.15" - resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72" - integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ== - dependencies: - d "^1.0.1" - es5-ext "^0.10.53" - es6-weak-map "^2.0.3" - event-emitter "^0.3.5" - is-promise "^2.2.2" - lru-queue "^0.1.0" - next-tick "^1.1.0" - timers-ext "^0.1.7" - -memorystream@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" - integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI= - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -mime-db@1.51.0: - version "1.51.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" - integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.34" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" - integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== - dependencies: - mime-db "1.51.0" - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - -minimatch@^3.0.4, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -mousetrap@^1.6.5: - version "1.6.5" - resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" - integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -mustache@^4.0.1, mustache@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" - integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== - -mute-stdout@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mute-stdout/-/mute-stdout-1.0.1.tgz#acb0300eb4de23a7ddeec014e3e96044b3472331" - integrity sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg== - -nan@^2.12.1: - version "2.15.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" - integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== - -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -next-tick@1, next-tick@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" - integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== - -next-tick@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" - integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -node-fetch@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -node-libs-browser@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - -node-releases@^2.0.12: - version "2.0.12" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.12.tgz#35627cc224a23bfb06fb3380f2b3afaaa7eb1039" - integrity sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ== - -nodemon@^2.0.20: - version "2.0.22" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.22.tgz#182c45c3a78da486f673d6c1702e00728daf5258" - integrity sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ== - dependencies: - chokidar "^3.5.2" - debug "^3.2.7" - ignore-by-default "^1.0.1" - minimatch "^3.1.2" - pstree.remy "^1.1.8" - semver "^5.7.1" - simple-update-notifier "^1.0.7" - supports-color "^5.5.0" - touch "^3.1.0" - undefsafe "^2.0.5" - -nopt@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" - integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== - dependencies: - abbrev "^1.0.0" - -nopt@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== - dependencies: - abbrev "1" - -normalize-package-data@^2.3.2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-path@^2.0.1, normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== - -now-and-later@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c" - integrity sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ== - dependencies: - once "^1.3.2" - -npm-run-all@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba" - integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ== - dependencies: - ansi-styles "^3.2.1" - chalk "^2.4.1" - cross-spawn "^6.0.5" - memorystream "^0.3.1" - minimatch "^3.0.4" - pidtree "^0.3.0" - read-pkg "^3.0.0" - shell-quote "^1.6.1" - string.prototype.padend "^3.0.0" - -npm-run-path@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -nth-check@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" - integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== - dependencies: - boolbase "~1.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@4.X, object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-inspect@^1.11.0, object-inspect@^1.9.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" - integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.assign@^4.0.4, object.assign@^4.1.0, object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -object.defaults@^1.0.0, object.defaults@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf" - integrity sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8= - dependencies: - array-each "^1.0.1" - array-slice "^1.0.0" - for-own "^1.0.0" - isobject "^3.0.0" - -object.getownpropertydescriptors@^2.1.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz#b223cf38e17fefb97a63c10c91df72ccb386df9e" - integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - -object.map@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37" - integrity sha1-z4Plncj8wK1fQlDh94s7gb2AHTc= - dependencies: - for-own "^1.0.0" - make-iterator "^1.0.0" - -object.pick@^1.2.0, object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -object.reduce@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object.reduce/-/object.reduce-1.0.1.tgz#6fe348f2ac7fa0f95ca621226599096825bb03ad" - integrity sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60= - dependencies: - for-own "^1.0.0" - make-iterator "^1.0.0" - -object.values@^1.1.0: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" - integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - -once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -one-time@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" - integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== - dependencies: - fn.name "1.x.x" - -onetime@^5.1.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -opentype.js@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-1.3.4.tgz#1c0e72e46288473cc4a4c6a2dc60fd7fe6020d77" - integrity sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw== - dependencies: - string.prototype.codepointat "^0.2.1" - tiny-inflate "^1.0.3" - -ordered-read-streams@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" - integrity sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4= - dependencies: - readable-stream "^2.0.1" - -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= - -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= - dependencies: - lcid "^1.0.0" - -ospath@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" - integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -pako@~1.0.2, pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.6" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - -parse-filepath@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" - integrity sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE= - dependencies: - is-absolute "^1.0.0" - map-cache "^0.2.0" - path-root "^0.1.1" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-node-version@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" - integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== - -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-root-regex@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" - integrity sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0= - -path-root@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" - integrity sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc= - dependencies: - path-root-regex "^0.1.0" - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - -pbkdf2@^3.0.3: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" - integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -phantomjs-prebuilt@^2.1.16: - version "2.1.16" - resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef" - integrity sha1-79ISpKOWbTZHaE6ouniFSb4q7+8= - dependencies: - es6-promise "^4.0.3" - extract-zip "^1.6.5" - fs-extra "^1.0.0" - hasha "^2.2.0" - kew "^0.7.0" - progress "^1.1.8" - request "^2.81.0" - request-progress "^2.0.1" - which "^1.2.10" - -picocolors@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" - integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pidtree@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a" - integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA== - -pify@^2.0.0, pify@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -plugin-error@^1.0.0, plugin-error@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" - integrity sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA== - dependencies: - ansi-colors "^1.0.1" - arr-diff "^4.0.0" - arr-union "^3.1.0" - extend-shallow "^3.0.2" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -postcss-clean@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/postcss-clean/-/postcss-clean-1.2.2.tgz#7b44303dcecbc2b29a70ed18d22af427203fa256" - integrity sha512-DpuMWW19Dd2K9KY4wknMz3khq9q2yZYa2U37bnhzdtBdBv0ggIfUj5T2XD3ir6gKVlDkb5OtOqw1iQJWq6qvpw== - dependencies: - clean-css "^4.x" - postcss "^6.x" - -postcss-load-config@^3.0.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.3.tgz#21935b2c43b9a86e6581a576ca7ee1bde2bd1d23" - integrity sha512-5EYgaM9auHGtO//ljHH+v/aC/TQ5LHXtL7bQajNAUBKUVKiYE8rYpFms7+V26D9FncaGe2zwCoPQsFKb5zF/Hw== - dependencies: - lilconfig "^2.0.4" - yaml "^1.10.2" - -postcss-modules-extract-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" - integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== - -postcss-modules-local-by-default@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" - integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== - dependencies: - icss-utils "^5.0.0" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== - dependencies: - postcss-selector-parser "^6.0.4" - -postcss-modules-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" - integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== - dependencies: - icss-utils "^5.0.0" - -postcss-modules@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-6.0.0.tgz#cac283dbabbbdc2558c45391cbd0e2df9ec50118" - integrity sha512-7DGfnlyi/ju82BRzTIjWS5C4Tafmzl3R79YP/PASiocj+aa6yYphHhhKUOEoXQToId5rgyFgJ88+ccOUydjBXQ== - dependencies: - generic-names "^4.0.0" - icss-utils "^5.1.0" - lodash.camelcase "^4.3.0" - postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.0" - postcss-modules-scope "^3.0.0" - postcss-modules-values "^4.0.0" - string-hash "^1.1.1" - -postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: - version "6.0.11" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" - integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss@^6.x: - version "6.0.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" - integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== - dependencies: - chalk "^2.4.1" - source-map "^0.6.1" - supports-color "^5.4.0" - -postcss@^7.0.16: - version "7.0.39" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" - integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== - dependencies: - picocolors "^0.2.1" - source-map "^0.6.1" - -postcss@^8.4.20: - version "8.4.24" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.24.tgz#f714dba9b2284be3cc07dbd2fc57ee4dc972d2df" - integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -prettier@^2.8.1: - version "2.8.8" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== - -pretty-bytes@^5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" - integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== - -pretty-hrtime@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" - integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= - -prettysize@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prettysize/-/prettysize-2.0.0.tgz#902c02480d865d9cc0813011c9feb4fa02ce6996" - integrity sha512-VVtxR7sOh0VsG8o06Ttq5TrI1aiZKmC+ClSn4eBPaNf4SHr5lzbYW+kYGX3HocBL/MfpVrRfFZ9V3vCbLaiplg== - -process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -progress@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" - integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= - -promise@^7.1.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== - dependencies: - asap "~2.0.3" - -prop-types@^15.7.2: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - -proto-list@~1.2.1: - version "1.2.4" - resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" - integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== - -proxy-from-env@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" - integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== - -psl@^1.1.28: - version "1.8.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" - integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== - -pstree.remy@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" - integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pumpify@^1.3.5: - version "1.5.1" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" - integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== - dependencies: - duplexify "^3.6.0" - inherits "^2.0.3" - pump "^2.0.0" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -q@^1.1.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= - -qs@~6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" - integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== - -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomcolor@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/randomcolor/-/randomcolor-0.6.2.tgz#7a57362ae1a1278439aeed2c15e5deb8ea33f56d" - integrity sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A== - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -react-dom@~17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" - -react-is@^16.13.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-lifecycles-compat@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-virtualized@^9.22.3: - version "9.22.5" - resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.5.tgz#bfb96fed519de378b50d8c0064b92994b3b91620" - integrity sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ== - dependencies: - "@babel/runtime" "^7.7.2" - clsx "^1.0.4" - dom-helpers "^5.1.3" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-lifecycles-compat "^3.0.4" - -react@~17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" - -"readable-stream@2 || 3", readable-stream@^3.4.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -readline-sync@^1.4.7: - version "1.4.10" - resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b" - integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw== - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= - dependencies: - resolve "^1.1.6" - -regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -remove-bom-buffer@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53" - integrity sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ== - dependencies: - is-buffer "^1.1.5" - is-utf8 "^0.2.1" - -remove-bom-stream@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz#05f1a593f16e42e1fb90ebf59de8e569525f9523" - integrity sha1-BfGlk/FuQuH7kOv1nejlaVJflSM= - dependencies: - remove-bom-buffer "^3.0.0" - safe-buffer "^5.1.0" - through2 "^2.0.3" - -remove-trailing-separator@^1.0.1, remove-trailing-separator@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -repeat-element@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" - integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== - -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -replace-ext@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" - integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== - -replace-ext@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-2.0.0.tgz#9471c213d22e1bcc26717cd6e50881d88f812b06" - integrity sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug== - -replace-homedir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/replace-homedir/-/replace-homedir-1.0.0.tgz#e87f6d513b928dde808260c12be7fec6ff6e798c" - integrity sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw= - dependencies: - homedir-polyfill "^1.0.1" - is-absolute "^1.0.0" - remove-trailing-separator "^1.1.0" - -request-progress@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08" - integrity sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg= - dependencies: - throttleit "^1.0.0" - -request-progress@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" - integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= - dependencies: - throttleit "^1.0.0" - -request@^2.81.0: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resolve-dir@^1.0.0, resolve-dir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" - integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= - dependencies: - expand-tilde "^2.0.0" - global-modules "^1.0.0" - -resolve-options@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131" - integrity sha1-MrueOcBtZzONyTeMDW1gdFZq0TE= - dependencies: - value-or-function "^3.0.0" - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.4.0: - version "1.22.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" - integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== - dependencies: - is-core-module "^2.8.1" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -rfdc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== - -rimraf@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -rxjs@^7.5.1: - version "7.5.4" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.4.tgz#3d6bd407e6b7ce9a123e76b1e770dc5761aa368d" - integrity sha512-h5M3Hk78r6wAheJF0a5YahB1yRQKCsZ4MsGdZ5O9ETbVtjPcScGfrMmoOq7EBsCRzd4BDkvDJ7ogP8Sz5tTFiQ== - dependencies: - tslib "^2.1.0" - -rxjs@~7.8.1: - version "7.8.1" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" - integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== - dependencies: - tslib "^2.1.0" - -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -safe-stable-stringify@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" - integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg== - -"safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sass@^1.57.1: - version "1.57.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.57.1.tgz#dfafd46eb3ab94817145e8825208ecf7281119b5" - integrity sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw== - dependencies: - chokidar ">=3.0.0 <4.0.0" - immutable "^4.0.0" - source-map-js ">=0.6.2 <2.0.0" - -sax@^1.2.4, sax@~1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -semver-greatest-satisfied-range@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz#13e8c2658ab9691cb0cd71093240280d36f77a5b" - integrity sha1-E+jCZYq5aRywzXEJMkAoDTb3els= - dependencies: - sver-compat "^1.5.0" - -"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^7.3.2: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - -semver@~7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setimmediate@^1.0.4, setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -shadow-cljs-jar@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b" - integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== - -shadow-cljs@2.20.16: - version "2.20.16" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.20.16.tgz#32e83586fcc91a246b7fb622349135ad84ca1a19" - integrity sha512-k33ssZppDkBSYIfKswiKOX/8bNTnHbHoTmwf3KBPXBQkDPptPR3FeedmWtS5vKWnucFTe9DObhM2exKocErIxw== - dependencies: - node-libs-browser "^2.2.1" - readline-sync "^1.4.7" - shadow-cljs-jar "1.3.2" - source-map-support "^0.4.15" - which "^1.3.1" - ws "^7.4.6" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shell-quote@^1.6.1: - version "1.7.3" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" - integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== - -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - -sigmund@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - integrity sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g== - -signal-exit@^3.0.2: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= - dependencies: - is-arrayish "^0.3.1" - -simple-update-notifier@^1.0.7: - version "1.1.0" - resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz#67694c121de354af592b347cdba798463ed49c82" - integrity sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg== - dependencies: - semver "~7.0.0" - -slice-ansi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" - integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-resolve@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" - integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== - dependencies: - source-map "^0.5.6" - -source-map-support@^0.5.21: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" - integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== - -source-map@^0.5.1, source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -sparkles@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" - integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== - -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.11" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" - integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -sshpk@^1.14.1, sshpk@^1.7.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - -stack-trace@0.0.10, stack-trace@0.0.x: - version "0.0.10" - resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" - integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-exhaust@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/stream-exhaust/-/stream-exhaust-1.0.2.tgz#acdac8da59ef2bc1e17a2c0ccf6c320d120e555d" - integrity sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw== - -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== - -stream-to-array@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353" - integrity sha1-u/azn19D7DC8cbq8s3VXrOzzQ1M= - dependencies: - any-promise "^1.1.0" - -string-hash@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" - integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A== - -string-width@^1.0.1, string-width@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string.prototype.codepointat@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc" - integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg== - -string.prototype.padend@^3.0.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.3.tgz#997a6de12c92c7cb34dc8a201a6c53d9bd88a5f1" - integrity sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - -string.prototype.trimend@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string_decoder@^1.0.0, string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-bom-string@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" - integrity sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI= - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -sver-compat@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8" - integrity sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg= - dependencies: - es6-iterator "^2.0.1" - es6-symbol "^3.1.1" - -svg-sprite@^1.5.0: - version "1.5.4" - resolved "https://registry.yarnpkg.com/svg-sprite/-/svg-sprite-1.5.4.tgz#974fd4734ea00d9951ce335a453ab2b66551e2d1" - integrity sha512-3jeqAmQS4c4rAMzsQNBXo3+J/x65JIxaFl15wTyvrJdT/G0DzXd67oTMBxz5+lCb8ETsWjM6ZAyr/+R+BwXzag== - dependencies: - "@xmldom/xmldom" "^0.7.5" - async "^3.2.3" - css-selector-parser "^1.4.1" - cssmin "^0.4.3" - cssom "^0.5.0" - glob "^7.2.0" - js-yaml "^3.14.1" - lodash "^4.17.21" - mkdirp "^0.5.5" - mustache "^4.2.0" - phantomjs-prebuilt "^2.1.16" - prettysize "^2.0.0" - svgo "^1.3.2" - vinyl "^2.2.1" - winston "^3.5.1" - xpath "^0.0.32" - yargs "^15.4.1" - -svgo@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" - integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== - dependencies: - chalk "^2.4.1" - coa "^2.0.2" - css-select "^2.0.0" - css-select-base-adapter "^0.1.1" - css-tree "1.0.0-alpha.37" - csso "^4.0.2" - js-yaml "^3.13.1" - mkdirp "~0.5.1" - object.values "^1.1.0" - sax "~1.2.4" - stable "^0.1.8" - unquote "~1.1.1" - util.promisify "~1.0.0" - -tdigest@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.2.tgz#96c64bac4ff10746b910b0e23b515794e12faced" - integrity sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA== - dependencies: - bintrees "1.0.2" - -text-hex@1.0.x: - version "1.0.0" - resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" - integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== - -throttleit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" - integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= - -through2-filter@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" - integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA== - dependencies: - through2 "~2.0.0" - xtend "~4.0.0" - -through2@^2.0.0, through2@^2.0.3, through2@~2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -through2@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4" - integrity sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ== - dependencies: - inherits "^2.0.4" - readable-stream "2 || 3" - -through@^2.3.8: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -time-stamp@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" - integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= - -timers-browserify@^2.0.4: - version "2.0.12" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" - integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== - dependencies: - setimmediate "^1.0.4" - -timers-ext@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6" - integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ== - dependencies: - es5-ext "~0.10.46" - next-tick "1" - -tiny-inflate@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" - integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== - -tmp@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" - integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== - dependencies: - rimraf "^3.0.0" - -to-absolute-glob@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" - integrity sha1-GGX0PZ50sIItufFFt4z/fQ98hJs= - dependencies: - is-absolute "^1.0.0" - is-negated-glob "^1.0.0" - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -to-through@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-through/-/to-through-2.0.0.tgz#fc92adaba072647bc0b67d6b03664aa195093af6" - integrity sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY= - dependencies: - through2 "^2.0.3" - -touch@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" - integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== - dependencies: - nopt "~1.0.10" - -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= - -triple-beam@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" - integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== - -tslib@^1.9.3: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.1.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== - -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -type@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" - integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== - -type@^2.5.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/type/-/type-2.6.0.tgz#3ca6099af5981d36ca86b78442973694278a219f" - integrity sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ== - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -ua-parser-js@^0.7.18: - version "0.7.31" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" - integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== - -ua-parser-js@^1.0.32: - version "1.0.35" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.35.tgz#c4ef44343bc3db0a3cbefdf21822f1b1fc1ab011" - integrity sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA== - -unbox-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" - integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== - dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.1" - has-symbols "^1.0.2" - which-boxed-primitive "^1.0.2" - -unc-path-regex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" - integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= - -undefsafe@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" - integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== - -undertaker-registry@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/undertaker-registry/-/undertaker-registry-1.0.1.tgz#5e4bda308e4a8a2ae584f9b9a4359a499825cc50" - integrity sha1-XkvaMI5KiirlhPm5pDWaSZglzFA= - -undertaker@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/undertaker/-/undertaker-1.3.0.tgz#363a6e541f27954d5791d6fa3c1d321666f86d18" - integrity sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg== - dependencies: - arr-flatten "^1.0.1" - arr-map "^2.0.0" - bach "^1.0.0" - collection-map "^1.0.0" - es6-weak-map "^2.0.1" - fast-levenshtein "^1.0.0" - last-run "^1.1.0" - object.defaults "^1.0.0" - object.reduce "^1.0.0" - undertaker-registry "^1.0.0" - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -unique-stream@^2.0.2: - version "2.3.1" - resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac" - integrity sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A== - dependencies: - json-stable-stringify-without-jsonify "^1.0.1" - through2-filter "^3.0.0" - -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - -unquote@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" - integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -untildify@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" - integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== - -upath@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - -update-browserslist-db@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" - integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util.promisify@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" - integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.2" - has-symbols "^1.0.1" - object.getownpropertydescriptors "^2.1.0" - -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== - dependencies: - inherits "2.0.3" - -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -v8flags@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" - integrity sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg== - dependencies: - homedir-polyfill "^1.0.1" - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -value-or-function@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813" - integrity sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM= - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -vinyl-fs@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7" - integrity sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng== - dependencies: - fs-mkdirp-stream "^1.0.0" - glob-stream "^6.1.0" - graceful-fs "^4.0.0" - is-valid-glob "^1.0.0" - lazystream "^1.0.0" - lead "^1.0.0" - object.assign "^4.0.4" - pumpify "^1.3.5" - readable-stream "^2.3.3" - remove-bom-buffer "^3.0.0" - remove-bom-stream "^1.2.0" - resolve-options "^1.1.0" - through2 "^2.0.0" - to-through "^2.0.0" - value-or-function "^3.0.0" - vinyl "^2.0.0" - vinyl-sourcemap "^1.1.0" - -vinyl-sourcemap@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz#92a800593a38703a8cdb11d8b300ad4be63b3e16" - integrity sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY= - dependencies: - append-buffer "^1.0.2" - convert-source-map "^1.5.0" - graceful-fs "^4.1.6" - normalize-path "^2.1.1" - now-and-later "^2.0.0" - remove-bom-buffer "^3.0.0" - vinyl "^2.0.0" - -vinyl-sourcemaps-apply@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" - integrity sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU= - dependencies: - source-map "^0.5.1" - -vinyl@^2.0.0, vinyl@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974" - integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw== - dependencies: - clone "^2.1.1" - clone-buffer "^1.0.0" - clone-stats "^1.0.0" - cloneable-readable "^1.0.0" - remove-trailing-separator "^1.0.1" - replace-ext "^1.0.0" - -vm-browserify@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" - integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.10, which@^1.2.14, which@^1.2.9, which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -winston-transport@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa" - integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q== - dependencies: - logform "^2.3.2" - readable-stream "^3.6.0" - triple-beam "^1.3.0" - -winston@^3.5.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/winston/-/winston-3.6.0.tgz#be32587a099a292b88c49fac6fa529d478d93fb6" - integrity sha512-9j8T75p+bcN6D00sF/zjFVmPp+t8KMPB1MzbbzYjeN9VWxdsYnTB40TkbNUEXAmILEfChMvAMgidlX64OG3p6w== - dependencies: - "@dabh/diagnostics" "^2.0.2" - async "^3.2.3" - is-stream "^2.0.0" - logform "^2.4.0" - one-time "^1.0.0" - readable-stream "^3.4.0" - safe-stable-stringify "^2.3.1" - stack-trace "0.0.x" - triple-beam "^1.3.0" - winston-transport "^4.5.0" - -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -ws@^7.4.6: - version "7.5.7" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" - integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== - -xpath@^0.0.32: - version "0.0.32" - resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.32.tgz#1b73d3351af736e17ec078d6da4b8175405c48af" - integrity sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw== - -xregexp@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-5.1.1.tgz#6d3fe18819e3143aaf52f9284d34f49a59583ebb" - integrity sha512-fKXeVorD+CzWvFs7VBuKTYIW63YD1e1osxwQ8caZ6o1jg6pDAbABDG54LCIq0j5cy7PjRvGIq6sef9DYPXpncg== - dependencies: - "@babel/runtime-corejs3" "^7.16.5" - -xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^3.2.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" - integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== - -y18n@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.1.tgz#7ede329c1d8cdbbe209bd25cdb990e9b1ebbb394" - integrity sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA== - dependencies: - camelcase "^3.0.0" - object.assign "^4.1.0" - -yargs@^15.4.1: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" - -yargs@^7.1.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.2.tgz#63a0a5d42143879fdbb30370741374e0641d55db" - integrity sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA== - dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "^5.0.1" - -yauzl@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@ampproject/remapping@npm:^2.2.0": + version: 2.2.1 + resolution: "@ampproject/remapping@npm:2.2.1" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.0" + "@jridgewell/trace-mapping": "npm:^0.3.9" + checksum: 92ce5915f8901d8c7cd4f4e6e2fe7b9fd335a29955b400caa52e0e5b12ca3796ada7c2f10e78c9c5b0f9c2539dff0ffea7b19850a56e1487aa083531e1e46d43 + languageName: node + linkType: hard + +"@aw-web-design/x-default-browser@npm:1.4.126": + version: 1.4.126 + resolution: "@aw-web-design/x-default-browser@npm:1.4.126" + dependencies: + default-browser-id: "npm:3.0.0" + bin: + x-default-browser: bin/x-default-browser.js + checksum: 634c7fad7a5f4df86e3fcd3a11e50034fcb6f6302281569727574cbda7532850063cb34ec328384a686ab0812f297bf301a5e2450bc7b93b5f80a006b1f2dfd7 + languageName: node + linkType: hard + +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/code-frame@npm:7.23.5" + dependencies: + "@babel/highlight": "npm:^7.23.4" + chalk: "npm:^2.4.2" + checksum: a10e843595ddd9f97faa99917414813c06214f4d9205294013e20c70fbdf4f943760da37dec1d998bf3e6fc20fa2918a47c0e987a7e458663feb7698063ad7c6 + languageName: node + linkType: hard + +"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9, @babel/compat-data@npm:^7.23.3, @babel/compat-data@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/compat-data@npm:7.23.5" + checksum: 081278ed46131a890ad566a59c61600a5f9557bd8ee5e535890c8548192532ea92590742fd74bd9db83d74c669ef8a04a7e1c85cdea27f960233e3b83c3a957c + languageName: node + linkType: hard + +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.23.0, @babel/core@npm:^7.23.2, @babel/core@npm:^7.23.3": + version: 7.23.5 + resolution: "@babel/core@npm:7.23.5" + dependencies: + "@ampproject/remapping": "npm:^2.2.0" + "@babel/code-frame": "npm:^7.23.5" + "@babel/generator": "npm:^7.23.5" + "@babel/helper-compilation-targets": "npm:^7.22.15" + "@babel/helper-module-transforms": "npm:^7.23.3" + "@babel/helpers": "npm:^7.23.5" + "@babel/parser": "npm:^7.23.5" + "@babel/template": "npm:^7.22.15" + "@babel/traverse": "npm:^7.23.5" + "@babel/types": "npm:^7.23.5" + convert-source-map: "npm:^2.0.0" + debug: "npm:^4.1.0" + gensync: "npm:^1.0.0-beta.2" + json5: "npm:^2.2.3" + semver: "npm:^6.3.1" + checksum: 311a512a870ee330a3f9a7ea89e5df790b2b5af0b1bd98b10b4edc0de2ac440f0df4d69ea2c0ee38a4b89041b9a495802741d93603be7d4fd834ec8bb6970bd2 + languageName: node + linkType: hard + +"@babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/generator@npm:7.23.5" + dependencies: + "@babel/types": "npm:^7.23.5" + "@jridgewell/gen-mapping": "npm:^0.3.2" + "@jridgewell/trace-mapping": "npm:^0.3.17" + jsesc: "npm:^2.5.1" + checksum: 14c6e874f796c4368e919bed6003bb0adc3ce837760b08f9e646d20aeb5ae7d309723ce6e4f06bcb4a2b5753145446c8e4425851380f695e40e71e1760f49e7b + languageName: node + linkType: hard + +"@babel/helper-annotate-as-pure@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" + dependencies: + "@babel/types": "npm:^7.22.5" + checksum: 5a80dc364ddda26b334bbbc0f6426cab647381555ef7d0cd32eb284e35b867c012ce6ce7d52a64672ed71383099c99d32765b3d260626527bb0e3470b0f58e45 + languageName: node + linkType: hard + +"@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.22.15" + dependencies: + "@babel/types": "npm:^7.22.15" + checksum: 2535e3824ca6337f65786bbac98e562f71699f25532cecd196f027d7698b4967a96953d64e36567956658ad1a05ccbdc62d1ba79ee751c79f4f1d2d3ecc2e01c + languageName: node + linkType: hard + +"@babel/helper-compilation-targets@npm:^7.22.15, @babel/helper-compilation-targets@npm:^7.22.6": + version: 7.22.15 + resolution: "@babel/helper-compilation-targets@npm:7.22.15" + dependencies: + "@babel/compat-data": "npm:^7.22.9" + "@babel/helper-validator-option": "npm:^7.22.15" + browserslist: "npm:^4.21.9" + lru-cache: "npm:^5.1.1" + semver: "npm:^6.3.1" + checksum: 45b9286861296e890f674a3abb199efea14a962a27d9b8adeb44970a9fd5c54e73a9e342e8414d2851cf4f98d5994537352fbce7b05ade32e9849bbd327f9ff1 + languageName: node + linkType: hard + +"@babel/helper-create-class-features-plugin@npm:^7.22.15, @babel/helper-create-class-features-plugin@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/helper-create-class-features-plugin@npm:7.23.5" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-function-name": "npm:^7.23.0" + "@babel/helper-member-expression-to-functions": "npm:^7.23.0" + "@babel/helper-optimise-call-expression": "npm:^7.22.5" + "@babel/helper-replace-supers": "npm:^7.22.20" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" + "@babel/helper-split-export-declaration": "npm:^7.22.6" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: a29bd03725630dcf2f094b7e3fe45c63984e63a5d092ceffec2da9d95c108afcc073863d6e9c0fb944d07f3cde5ebac4bba833473ca96af5e949f7d471154901 + languageName: node + linkType: hard + +"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.22.15, @babel/helper-create-regexp-features-plugin@npm:^7.22.5": + version: 7.22.15 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.22.15" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + regexpu-core: "npm:^5.3.1" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 8eba4c1b7b94a83e7a82df5c3e504584ff0ba6ab8710a67ecc2c434a7fb841a29c2f5c94d2de51f25446119a1df538fa90b37bd570db22ddd5e7147fe98277c6 + languageName: node + linkType: hard + +"@babel/helper-define-polyfill-provider@npm:^0.4.3": + version: 0.4.3 + resolution: "@babel/helper-define-polyfill-provider@npm:0.4.3" + dependencies: + "@babel/helper-compilation-targets": "npm:^7.22.6" + "@babel/helper-plugin-utils": "npm:^7.22.5" + debug: "npm:^4.1.1" + lodash.debounce: "npm:^4.0.8" + resolve: "npm:^1.14.2" + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 0007035157e0d32ee9cb4ca319b89d6f3705523383efe52a59eb3d4dfa2ed08c5147e49c10a6e6d69c15221d89c76c8e5875475d6710fb44a5c37b8e69388e40 + languageName: node + linkType: hard + +"@babel/helper-environment-visitor@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-environment-visitor@npm:7.22.20" + checksum: e762c2d8f5d423af89bd7ae9abe35bd4836d2eb401af868a63bbb63220c513c783e25ef001019418560b3fdc6d9a6fb67e6c0b650bcdeb3a2ac44b5c3d2bdd94 + languageName: node + linkType: hard + +"@babel/helper-function-name@npm:^7.22.5, @babel/helper-function-name@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-function-name@npm:7.23.0" + dependencies: + "@babel/template": "npm:^7.22.15" + "@babel/types": "npm:^7.23.0" + checksum: d771dd1f3222b120518176733c52b7cadac1c256ff49b1889dbbe5e3fed81db855b8cc4e40d949c9d3eae0e795e8229c1c8c24c0e83f27cfa6ee3766696c6428 + languageName: node + linkType: hard + +"@babel/helper-hoist-variables@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-hoist-variables@npm:7.22.5" + dependencies: + "@babel/types": "npm:^7.22.5" + checksum: 60a3077f756a1cd9f14eb89f0037f487d81ede2b7cfe652ea6869cd4ec4c782b0fb1de01b8494b9a2d2050e3d154d7d5ad3be24806790acfb8cbe2073bf1e208 + languageName: node + linkType: hard + +"@babel/helper-member-expression-to-functions@npm:^7.22.15, @babel/helper-member-expression-to-functions@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-member-expression-to-functions@npm:7.23.0" + dependencies: + "@babel/types": "npm:^7.23.0" + checksum: b810daddf093ffd0802f1429052349ed9ea08ef7d0c56da34ffbcdecbdafac86f95bdea2fe30e0e0e629febc7dd41b56cb5eacc10d1a44336d37b755dac31fa4 + languageName: node + linkType: hard + +"@babel/helper-module-imports@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/helper-module-imports@npm:7.22.15" + dependencies: + "@babel/types": "npm:^7.22.15" + checksum: 4e0d7fc36d02c1b8c8b3006dfbfeedf7a367d3334a04934255de5128115ea0bafdeb3e5736a2559917f0653e4e437400d54542da0468e08d3cbc86d3bbfa8f30 + languageName: node + linkType: hard + +"@babel/helper-module-transforms@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/helper-module-transforms@npm:7.23.3" + dependencies: + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-module-imports": "npm:^7.22.15" + "@babel/helper-simple-access": "npm:^7.22.5" + "@babel/helper-split-export-declaration": "npm:^7.22.6" + "@babel/helper-validator-identifier": "npm:^7.22.20" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 211e1399d0c4993671e8e5c2b25383f08bee40004ace5404ed4065f0e9258cc85d99c1b82fd456c030ce5cfd4d8f310355b54ef35de9924eabfc3dff1331d946 + languageName: node + linkType: hard + +"@babel/helper-optimise-call-expression@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-optimise-call-expression@npm:7.22.5" + dependencies: + "@babel/types": "npm:^7.22.5" + checksum: 31b41a764fc3c585196cf5b776b70cf4705c132e4ce9723f39871f215f2ddbfb2e28a62f9917610f67c8216c1080482b9b05f65dd195dae2a52cef461f2ac7b8 + languageName: node + linkType: hard + +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": + version: 7.22.5 + resolution: "@babel/helper-plugin-utils@npm:7.22.5" + checksum: d2c4bfe2fa91058bcdee4f4e57a3f4933aed7af843acfd169cd6179fab8d13c1d636474ecabb2af107dc77462c7e893199aa26632bac1c6d7e025a17cbb9d20d + languageName: node + linkType: hard + +"@babel/helper-remap-async-to-generator@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-remap-async-to-generator@npm:7.22.20" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-wrap-function": "npm:^7.22.20" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: aa93aa74250b636d477e8d863fbe59d4071f8c2654841b7ac608909e480c1cf3ff7d7af5a4038568829ad09d810bb681668cbe497d9c89ba5c352793dc9edf1e + languageName: node + linkType: hard + +"@babel/helper-replace-supers@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-replace-supers@npm:7.22.20" + dependencies: + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-member-expression-to-functions": "npm:^7.22.15" + "@babel/helper-optimise-call-expression": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 6b0858811ad46873817c90c805015d63300e003c5a85c147a17d9845fa2558a02047c3cc1f07767af59014b2dd0fa75b503e5bc36e917f360e9b67bb6f1e79f4 + languageName: node + linkType: hard + +"@babel/helper-simple-access@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-simple-access@npm:7.22.5" + dependencies: + "@babel/types": "npm:^7.22.5" + checksum: f0cf81a30ba3d09a625fd50e5a9069e575c5b6719234e04ee74247057f8104beca89ed03e9217b6e9b0493434cedc18c5ecca4cea6244990836f1f893e140369 + languageName: node + linkType: hard + +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.22.5" + dependencies: + "@babel/types": "npm:^7.22.5" + checksum: ab7fa2aa709ab49bb8cd86515a1e715a3108c4bb9a616965ba76b43dc346dee66d1004ccf4d222b596b6224e43e04cbc5c3a34459501b388451f8c589fbc3691 + languageName: node + linkType: hard + +"@babel/helper-split-export-declaration@npm:^7.22.6": + version: 7.22.6 + resolution: "@babel/helper-split-export-declaration@npm:7.22.6" + dependencies: + "@babel/types": "npm:^7.22.5" + checksum: d83e4b623eaa9622c267d3c83583b72f3aac567dc393dda18e559d79187961cb29ae9c57b2664137fc3d19508370b12ec6a81d28af73a50e0846819cb21c6e44 + languageName: node + linkType: hard + +"@babel/helper-string-parser@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/helper-string-parser@npm:7.23.4" + checksum: f348d5637ad70b6b54b026d6544bd9040f78d24e7ec245a0fc42293968181f6ae9879c22d89744730d246ce8ec53588f716f102addd4df8bbc79b73ea10004ac + languageName: node + linkType: hard + +"@babel/helper-validator-identifier@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-validator-identifier@npm:7.22.20" + checksum: dcad63db345fb110e032de46c3688384b0008a42a4845180ce7cd62b1a9c0507a1bed727c4d1060ed1a03ae57b4d918570259f81724aaac1a5b776056f37504e + languageName: node + linkType: hard + +"@babel/helper-validator-option@npm:^7.22.15, @babel/helper-validator-option@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/helper-validator-option@npm:7.23.5" + checksum: af45d5c0defb292ba6fd38979e8f13d7da63f9623d8ab9ededc394f67eb45857d2601278d151ae9affb6e03d5d608485806cd45af08b4468a0515cf506510e94 + languageName: node + linkType: hard + +"@babel/helper-wrap-function@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-wrap-function@npm:7.22.20" + dependencies: + "@babel/helper-function-name": "npm:^7.22.5" + "@babel/template": "npm:^7.22.15" + "@babel/types": "npm:^7.22.19" + checksum: 97b5f42ff4d305318ff2f99a5f59d3e97feff478333b2d893c4f85456d3c66372070f71d7bf9141f598c8cf2741c49a15918193633c427a88d170d98eb8c46eb + languageName: node + linkType: hard + +"@babel/helpers@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/helpers@npm:7.23.5" + dependencies: + "@babel/template": "npm:^7.22.15" + "@babel/traverse": "npm:^7.23.5" + "@babel/types": "npm:^7.23.5" + checksum: a37e2728eb4378a4888e5d614e28de7dd79b55ac8acbecd0e5c761273e2a02a8f33b34b1932d9069db55417ace2937cbf8ec37c42f1030ce6d228857d7ccaa4f + languageName: node + linkType: hard + +"@babel/highlight@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/highlight@npm:7.23.4" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.22.20" + chalk: "npm:^2.4.2" + js-tokens: "npm:^4.0.0" + checksum: fbff9fcb2f5539289c3c097d130e852afd10d89a3a08ac0b5ebebbc055cc84a4bcc3dcfed463d488cde12dd0902ef1858279e31d7349b2e8cee43913744bda33 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/parser@npm:7.23.5" + bin: + parser: ./bin/babel-parser.js + checksum: 3356aa90d7bafb4e2c7310e7c2c3d443c4be4db74913f088d3d577a1eb914ea4188e05fd50a47ce907a27b755c4400c4e3cbeee73dbeb37761f6ca85954f5a20 + languageName: node + linkType: hard + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 356a4e9fc52d7ca761ce6857fc58e2295c2785d22565760e6a5680be86c6e5883ab86e0ba25ef572882c01713d3a31ae6cfa3e3222cdb95e6026671dab1fa415 + languageName: node + linkType: hard + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" + "@babel/plugin-transform-optional-chaining": "npm:^7.23.3" + peerDependencies: + "@babel/core": ^7.13.0 + checksum: a8785f099d55ca71ed89815e0f3a636a80c16031f80934cfec17c928d096ee0798964733320c8b145ef36ba429c5e19d5107b06231e0ab6777cfb0f01adfdc23 + languageName: node + linkType: hard + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.23.3" + dependencies: + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 0f43b74741d50e637ba4dcef2786621126fe4da6ccf4ee2e94423ee23f6a04ecd91d458e59764c43e4968be139e5197ee43be8a2fea2c09f0b202a3391e548cc + languageName: node + linkType: hard + +"@babel/plugin-proposal-private-property-in-object@npm:7.21.0-placeholder-for-preset-env.2": + version: 7.21.0-placeholder-for-preset-env.2 + resolution: "@babel/plugin-proposal-private-property-in-object@npm:7.21.0-placeholder-for-preset-env.2" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e605e0070da087f6c35579499e65801179a521b6842c15181a1e305c04fded2393f11c1efd09b087be7f8b083d1b75e8f3efcbc1292b4f60d3369e14812cff63 + languageName: node + linkType: hard + +"@babel/plugin-syntax-async-generators@npm:^7.8.4": + version: 7.8.4 + resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d13efb282838481348c71073b6be6245b35d4f2f964a8f71e4174f235009f929ef7613df25f8d2338e2d3e44bc4265a9f8638c6aaa136d7a61fe95985f9725c8 + languageName: node + linkType: hard + +"@babel/plugin-syntax-class-properties@npm:^7.12.13": + version: 7.12.13 + resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.12.13" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 95168fa186416195280b1264fb18afcdcdcea780b3515537b766cb90de6ce042d42dd6a204a39002f794ae5845b02afb0fd4861a3308a861204a55e68310a120 + languageName: node + linkType: hard + +"@babel/plugin-syntax-class-static-block@npm:^7.14.5": + version: 7.14.5 + resolution: "@babel/plugin-syntax-class-static-block@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 4464bf9115f4a2d02ce1454411baf9cfb665af1da53709c5c56953e5e2913745b0fcce82982a00463d6facbdd93445c691024e310b91431a1e2f024b158f6371 + languageName: node + linkType: hard + +"@babel/plugin-syntax-dynamic-import@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-dynamic-import@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 9c50927bf71adf63f60c75370e2335879402648f468d0172bc912e303c6a3876927d8eb35807331b57f415392732ed05ab9b42c68ac30a936813ab549e0246c5 + languageName: node + linkType: hard + +"@babel/plugin-syntax-export-namespace-from@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-export-namespace-from@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 5100d658ba563829700cd8d001ddc09f4c0187b1a13de300d729c5b3e87503f75a6d6c99c1794182f7f1a9f546ee009df4f15a0ce36376e206ed0012fa7cdc24 + languageName: node + linkType: hard + +"@babel/plugin-syntax-flow@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-syntax-flow@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 8a5e1e8b6a3728a2c8fe6d70c09a43642e737d9c0485e1b041cd3a6021ef05376ec3c9137be3b118c622ba09b5770d26fdc525473f8d06d4ab9e46de2783dd0a + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-assertions@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7db8b59f75667bada2293353bb66b9d5651a673b22c72f47da9f5c46e719142481601b745f9822212fd7522f92e26e8576af37116f85dae1b5e5967f80d0faab + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-attributes@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 99b40d33d79205a8e04bb5dea56fd72906ffc317513b20ca7319e7683e18fce8ea2eea5e9171056f92b979dc0ab1e31b2cb5171177a5ba61e05b54fe7850a606 + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-meta@npm:^7.10.4": + version: 7.10.4 + resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 0b08b5e4c3128523d8e346f8cfc86824f0da2697b1be12d71af50a31aff7a56ceb873ed28779121051475010c28d6146a6bfea8518b150b71eeb4e46190172ee + languageName: node + linkType: hard + +"@babel/plugin-syntax-json-strings@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-json-strings@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e98f31b2ec406c57757d115aac81d0336e8434101c224edd9a5c93cefa53faf63eacc69f3138960c8b25401315af03df37f68d316c151c4b933136716ed6906e + languageName: node + linkType: hard + +"@babel/plugin-syntax-jsx@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-syntax-jsx@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 563bb7599b868773f1c7c1d441ecc9bc53aeb7832775da36752c926fc402a1fa5421505b39e724f71eb217c13e4b93117e081cac39723b0e11dac4c897f33c3e + languageName: node + linkType: hard + +"@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4": + version: 7.10.4 + resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 2594cfbe29411ad5bc2ad4058de7b2f6a8c5b86eda525a993959438615479e59c012c14aec979e538d60a584a1a799b60d1b8942c3b18468cb9d99b8fd34cd0b + languageName: node + linkType: hard + +"@babel/plugin-syntax-nullish-coalescing-operator@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-nullish-coalescing-operator@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 2024fbb1162899094cfc81152449b12bd0cc7053c6d4bda8ac2852545c87d0a851b1b72ed9560673cbf3ef6248257262c3c04aabf73117215c1b9cc7dd2542ce + languageName: node + linkType: hard + +"@babel/plugin-syntax-numeric-separator@npm:^7.10.4": + version: 7.10.4 + resolution: "@babel/plugin-syntax-numeric-separator@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: c55a82b3113480942c6aa2fcbe976ff9caa74b7b1109ff4369641dfbc88d1da348aceb3c31b6ed311c84d1e7c479440b961906c735d0ab494f688bf2fd5b9bb9 + languageName: node + linkType: hard + +"@babel/plugin-syntax-object-rest-spread@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-object-rest-spread@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ee1eab52ea6437e3101a0a7018b0da698545230015fc8ab129d292980ec6dff94d265e9e90070e8ae5fed42f08f1622c14c94552c77bcac784b37f503a82ff26 + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-catch-binding@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-catch-binding@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 27e2493ab67a8ea6d693af1287f7e9acec206d1213ff107a928e85e173741e1d594196f99fec50e9dde404b09164f39dec5864c767212154ffe1caa6af0bc5af + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-chaining@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-chaining@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 46edddf2faa6ebf94147b8e8540dfc60a5ab718e2de4d01b2c0bdf250a4d642c2bd47cbcbb739febcb2bf75514dbcefad3c52208787994b8d0f8822490f55e81 + languageName: node + linkType: hard + +"@babel/plugin-syntax-private-property-in-object@npm:^7.14.5": + version: 7.14.5 + resolution: "@babel/plugin-syntax-private-property-in-object@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 69822772561706c87f0a65bc92d0772cea74d6bc0911537904a676d5ff496a6d3ac4e05a166d8125fce4a16605bace141afc3611074e170a994e66e5397787f3 + languageName: node + linkType: hard + +"@babel/plugin-syntax-top-level-await@npm:^7.14.5": + version: 7.14.5 + resolution: "@babel/plugin-syntax-top-level-await@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 14bf6e65d5bc1231ffa9def5f0ef30b19b51c218fcecaa78cd1bdf7939dfdf23f90336080b7f5196916368e399934ce5d581492d8292b46a2fb569d8b2da106f + languageName: node + linkType: hard + +"@babel/plugin-syntax-typescript@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-syntax-typescript@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 4d6e9cdb9d0bfb9bd9b220fc951d937fce2ca69135ec121153572cebe81d86abc9a489208d6b69ee5f10cadcaeffa10d0425340a5029e40e14a6025021b90948 + languageName: node + linkType: hard + +"@babel/plugin-syntax-unicode-sets-regex@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-syntax-unicode-sets-regex@npm:7.18.6" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.18.6" + "@babel/helper-plugin-utils": "npm:^7.18.6" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 9144e5b02a211a4fb9a0ce91063f94fbe1004e80bde3485a0910c9f14897cf83fabd8c21267907cff25db8e224858178df0517f14333cfcf3380ad9a4139cb50 + languageName: node + linkType: hard + +"@babel/plugin-transform-arrow-functions@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-arrow-functions@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: b128315c058f5728d29b0b78723659b11de88247ea4d0388f0b935cddf60a80c40b9067acf45cbbe055bd796928faef152a09d9e4a0695465aca4394d9f109ca + languageName: node + linkType: hard + +"@babel/plugin-transform-async-generator-functions@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.23.4" + dependencies: + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-remap-async-to-generator": "npm:^7.22.20" + "@babel/plugin-syntax-async-generators": "npm:^7.8.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f2eef4de609975a3f7da7832576b5ffc93e43c80f87e1a99e886b0f8591096cfc4c37e2d5f52fdeaa2a9c09a25a59f3e621159abaca75d3193922a5c0e4cbe0c + languageName: node + linkType: hard + +"@babel/plugin-transform-async-to-generator@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-async-to-generator@npm:7.23.3" + dependencies: + "@babel/helper-module-imports": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-remap-async-to-generator": "npm:^7.22.20" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: da3ffd413eef02a8e2cfee3e0bb0d5fc0fcb795c187bc14a5a8e8874cdbdc43bbf00089c587412d7752d97efc5967c3c18ff5398e3017b9a14a06126f017e7e9 + languageName: node + linkType: hard + +"@babel/plugin-transform-block-scoped-functions@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 82c12a11277528184a979163de7189ceb00129f60dd930b0d5313454310bf71205f302fb2bf0430247161c8a22aaa9fb9eec1459f9f7468206422c191978fd59 + languageName: node + linkType: hard + +"@babel/plugin-transform-block-scoping@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-block-scoping@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 83006804dddf980ab1bcd6d67bc381e24b58c776507c34f990468f820d0da71dba3697355ca4856532fa2eeb2a1e3e73c780f03760b5507a511cbedb0308e276 + languageName: node + linkType: hard + +"@babel/plugin-transform-class-properties@npm:^7.22.5, @babel/plugin-transform-class-properties@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-class-properties@npm:7.23.3" + dependencies: + "@babel/helper-create-class-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bca30d576f539eef216494b56d610f1a64aa9375de4134bc021d9660f1fa735b1d7cc413029f22abc0b7cb737e3a57935c8ae9d8bd1730921ccb1deebce51bfd + languageName: node + linkType: hard + +"@babel/plugin-transform-class-static-block@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-class-static-block@npm:7.23.4" + dependencies: + "@babel/helper-create-class-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-class-static-block": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.12.0 + checksum: fdca96640ef29d8641a7f8de106f65f18871b38cc01c0f7b696d2b49c76b77816b30a812c08e759d06dd10b4d9b3af6b5e4ac22a2017a88c4077972224b77ab0 + languageName: node + linkType: hard + +"@babel/plugin-transform-classes@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/plugin-transform-classes@npm:7.23.5" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + "@babel/helper-compilation-targets": "npm:^7.22.15" + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-function-name": "npm:^7.23.0" + "@babel/helper-optimise-call-expression": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-replace-supers": "npm:^7.22.20" + "@babel/helper-split-export-declaration": "npm:^7.22.6" + globals: "npm:^11.1.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 07988f52b4893151887d1ea6ff79e5fe834078c5731bd09babd5659edbbae21ea4e2de326a02443a63fd776b4c945da6177f07875b56fe66e0b7899e830a9e92 + languageName: node + linkType: hard + +"@babel/plugin-transform-computed-properties@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-computed-properties@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/template": "npm:^7.22.15" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3ca8a006f8e652b58c21ecb84df1d01a73f0a96b1d216fd09a890b235dd90cb966b152b603b88f7e850ae238644b1636ce5c30b7c029c0934b43383932372e4a + languageName: node + linkType: hard + +"@babel/plugin-transform-destructuring@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-destructuring@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 717e9a62c1b0c93c507f87b4eaf839ec08d3c3147f14d74ae240d8749488d9762a8b3950132be620a069bde70f4b3e4ee9867b226c973fcc40f3cdec975cde71 + languageName: node + linkType: hard + +"@babel/plugin-transform-dotall-regex@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-dotall-regex@npm:7.23.3" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 6c89286d1277c2a63802a453c797c87c1203f89e4c25115f7b6620f5fce15d8c8d37af613222f6aa497aa98773577a6ec8752e79e13d59bc5429270677ea010b + languageName: node + linkType: hard + +"@babel/plugin-transform-duplicate-keys@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-duplicate-keys@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7e2640e4e6adccd5e7b0615b6e9239d7c98363e21c52086ea13759dfa11cf7159b255fc5331c2de435639ea8eb6acefae115ae0d797a3d19d12587652f8052a5 + languageName: node + linkType: hard + +"@babel/plugin-transform-dynamic-import@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-dynamic-import@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 19ae4a4a2ca86d35224734c41c48b2aa6a13139f3cfa1cbd18c0e65e461de8b65687dec7e52b7a72bb49db04465394c776aa1b13a2af5dc975b2a0cde3dcab67 + languageName: node + linkType: hard + +"@babel/plugin-transform-exponentiation-operator@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.23.3" + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 5c33ee6a1bdc52fcdf0807f445b27e3fbdce33008531885e65a699762327565fffbcfde8395be7f21bcb22d582e425eddae45650c986462bb84ba68f43687516 + languageName: node + linkType: hard + +"@babel/plugin-transform-export-namespace-from@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 38bf04f851e36240bbe83ace4169da626524f4107bfb91f05b4ad93a5fb6a36d5b3d30b8883c1ba575ccfc1bac7938e90ca2e3cb227f7b3f4a9424beec6fd4a7 + languageName: node + linkType: hard + +"@babel/plugin-transform-flow-strip-types@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-flow-strip-types@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-flow": "npm:^7.23.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 9ab627f9668fc1f95564b26bffd6706f86205960d9ccc168236752fbef65dbe10aa0ce74faae12f48bb3b72ec7f38ef2a78b4874c222c1e85754e981639f3b33 + languageName: node + linkType: hard + +"@babel/plugin-transform-for-of@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-for-of@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 8a36202cfee312ba80e509c7c2131e6773524e572b4dc64a8ee95bd912634fdeb5ea91c6c7747ee30e03562d0f0d333f88ed7dbb929b36b60b8d74189189e12f + languageName: node + linkType: hard + +"@babel/plugin-transform-function-name@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-function-name@npm:7.23.3" + dependencies: + "@babel/helper-compilation-targets": "npm:^7.22.15" + "@babel/helper-function-name": "npm:^7.23.0" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 89cb9747802118048115cf92a8f310752f02030549b26f008904990cbdc86c3d4a68e07ca3b5c46de8a46ed4df2cb576ac222c74c56de67253d2a3ddc2956083 + languageName: node + linkType: hard + +"@babel/plugin-transform-json-strings@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-json-strings@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-json-strings": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 39e82223992a9ad857722ae051291935403852ad24b0dd64c645ca1c10517b6bf9822377d88643fed8b3e61a4e3f7e5ae41cf90eb07c40a786505d47d5970e54 + languageName: node + linkType: hard + +"@babel/plugin-transform-literals@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-literals@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 8292106b106201464c2bfdd5c014fe6a9ca1c0256eb0a8031deb20081e21906fe68b156186f77d993c23eeab6d8d6f5f66e8895eec7ed97ce6de5dbcafbcd7f4 + languageName: node + linkType: hard + +"@babel/plugin-transform-logical-assignment-operators@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 87b034dd13143904e405887e6125d76c27902563486efc66b7d9a9d8f9406b76c6ac42d7b37224014af5783d7edb465db0cdecd659fa3227baad0b3a6a35deff + languageName: node + linkType: hard + +"@babel/plugin-transform-member-expression-literals@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-member-expression-literals@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 687f24f3ec60b627fef6e87b9e2770df77f76727b9d5f54fa4c84a495bb24eb4a20f1a6240fa22d339d45aac5eaeb1b39882e941bfd00cf498f9c53478d1ec88 + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-amd@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-modules-amd@npm:7.23.3" + dependencies: + "@babel/helper-module-transforms": "npm:^7.23.3" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 9f7ec036f7cfc588833a4dd117a44813b64aa4c1fd5bfb6c78f60198c1d290938213090c93a46f97a68a2490fad909e21a82b2472e95da74d108c125df21c8d5 + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-commonjs@npm:^7.23.0, @babel/plugin-transform-modules-commonjs@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.23.3" + dependencies: + "@babel/helper-module-transforms": "npm:^7.23.3" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-simple-access": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 5c8840c5c9ecba39367ae17c973ed13dbc43234147b77ae780eec65010e2a9993c5d717721b23e8179f7cf49decdd325c509b241d69cfbf92aa647a1d8d5a37d + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-systemjs@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.23.3" + dependencies: + "@babel/helper-hoist-variables": "npm:^7.22.5" + "@babel/helper-module-transforms": "npm:^7.23.3" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-validator-identifier": "npm:^7.22.20" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 0d55280a276510222c8896bf4e581acb84824aa5b14c824f7102242ad6bc5104aaffe5ab22fe4d27518f4ae2811bd59c36d0c0bfa695157f9cfce33f0517a069 + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-umd@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-modules-umd@npm:7.23.3" + dependencies: + "@babel/helper-module-transforms": "npm:^7.23.3" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f0d2f890a15b4367d0d8f160bed7062bdb145c728c24e9bfbc1211c7925aae5df72a88df3832c92dd2011927edfed4da1b1249e4c78402e893509316c0c2caa6 + languageName: node + linkType: hard + +"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.22.5" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: b0b072bef303670b5a98307bc37d1ac326cb7ad40ea162b89a03c2ffc465451be7ef05be95cb81ed28bfeb29670dc98fe911f793a67bceab18b4cb4c81ef48f3 + languageName: node + linkType: hard + +"@babel/plugin-transform-new-target@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-new-target@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f489b9e1f17b42b2ba6312d58351e757cb23a8409f64f2bb6af4c09d015359588a5d68943b20756f141d0931a94431c782f3ed1225228a930a04b07be0c31b04 + languageName: node + linkType: hard + +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.11, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bce490d22da5c87ff27fffaff6ad5a4d4979b8d7b72e30857f191e9c1e1824ba73bb8d7081166289369e388f94f0ce5383a593b1fc84d09464a062c75f824b0b + languageName: node + linkType: hard + +"@babel/plugin-transform-numeric-separator@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-numeric-separator": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e34902da4f5588dc4812c92cb1f6a5e3e3647baf7b4623e30942f551bf1297621abec4e322ebfa50b320c987c0f34d9eb4355b3d289961d9035e2126e3119c12 + languageName: node + linkType: hard + +"@babel/plugin-transform-object-rest-spread@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.23.4" + dependencies: + "@babel/compat-data": "npm:^7.23.3" + "@babel/helper-compilation-targets": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" + "@babel/plugin-transform-parameters": "npm:^7.23.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: b56017992ffe7fcd1dd9a9da67c39995a141820316266bcf7d77dc912980d228ccbd3f36191d234f5cc389b09157b5d2a955e33e8fb368319534affd1c72b262 + languageName: node + linkType: hard + +"@babel/plugin-transform-object-super@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-object-super@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-replace-supers": "npm:^7.22.20" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: a6856fd8c0afbe5b3318c344d4d201d009f4051e2f6ff6237ff2660593e93c5997a58772b13d639077c3e29ced3440247b29c496cd77b13af1e7559a70009775 + languageName: node + linkType: hard + +"@babel/plugin-transform-optional-catch-binding@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 4ef61812af0e4928485e28301226ce61139a8b8cea9e9a919215ebec4891b9fea2eb7a83dc3090e2679b7d7b2c8653da601fbc297d2addc54a908b315173991e + languageName: node + linkType: hard + +"@babel/plugin-transform-optional-chaining@npm:^7.23.0, @babel/plugin-transform-optional-chaining@npm:^7.23.3, @babel/plugin-transform-optional-chaining@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" + "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 305b773c29ad61255b0e83ec1e92b2f7af6aa58be4cba1e3852bddaa14f7d2afd7b4438f41c28b179d6faac7eb8d4fb5530a17920294f25d459b8f84406bfbfb + languageName: node + linkType: hard + +"@babel/plugin-transform-parameters@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-parameters@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: a8d4cbe0f6ba68d158f5b4215c63004fc37a1fdc539036eb388a9792017c8496ea970a1932ccb929308f61e53dc56676ed01d8df6f42bc0a85c7fd5ba82482b7 + languageName: node + linkType: hard + +"@babel/plugin-transform-private-methods@npm:^7.22.5, @babel/plugin-transform-private-methods@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-private-methods@npm:7.23.3" + dependencies: + "@babel/helper-create-class-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 745a655edcd111b7f91882b921671ca0613079760d8c9befe336b8a9bc4ce6bb49c0c08941831c950afb1b225b4b2d3eaac8842e732db095b04db38efd8c34f4 + languageName: node + linkType: hard + +"@babel/plugin-transform-private-property-in-object@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.23.4" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + "@babel/helper-create-class-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 8d31b28f24204b4d13514cd3a8f3033abf575b1a6039759ddd6e1d82dd33ba7281f9bc85c9f38072a665d69bfa26dc40737eefaf9d397b024654a483d2357bf5 + languageName: node + linkType: hard + +"@babel/plugin-transform-property-literals@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-property-literals@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: b2549f23f90cf276c2e3058c2225c3711c2ad1c417e336d3391199445a9776dd791b83be47b2b9a7ae374b40652d74b822387e31fa5267a37bf49c122e1a9747 + languageName: node + linkType: hard + +"@babel/plugin-transform-react-jsx-self@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-react-jsx-self@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 6b586508fc58998483d4ee93a7e784c4f4d2350e2633739cf1990b7ad172e13906f72382fdaf7f07b4e3c7e7555342634d392bdeb1a079bb64762c6368ca9a32 + languageName: node + linkType: hard + +"@babel/plugin-transform-react-jsx-source@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-react-jsx-source@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: a3aad7cf738e9bfaddc26cdbb83bb9684c2e689d26fb0793d772af0c8da0cd25bb02523d192fbc6946c32143e56b472c1d33fa82466b3f2d3346e1ce8fe83cf6 + languageName: node + linkType: hard + +"@babel/plugin-transform-regenerator@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-regenerator@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + regenerator-transform: "npm:^0.15.2" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3b0e989ae5db78894ee300b24e07fbcec490c39ab48629c519377581cf94e90308f4ddc10a8914edc9f403e2d3ac7a7ae0ae09003629d852da03e2ba846299c6 + languageName: node + linkType: hard + +"@babel/plugin-transform-reserved-words@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-reserved-words@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 4e6d61f6c9757592661cfbd2c39c4f61551557b98cb5f0995ef10f5540f67e18dde8a42b09716d58943b6e4b7ef5c9bcf19902839e7328a4d49149e0fecdbfcd + languageName: node + linkType: hard + +"@babel/plugin-transform-shorthand-properties@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-shorthand-properties@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: c423c66fec0b6503f50561741754c84366ef9e9818442c8881fbaa90cc363fd137084b9431cdc00ed2f1fd8c8a1a5982c4a7e1f2af3769db4caf2ac7ea55d4f0 + languageName: node + linkType: hard + +"@babel/plugin-transform-spread@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-spread@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: a348e4ae47e4ceeceb760506ec7bf835ccc18a2cf70ec74ebfbe41bc172fa2412b05b7d1b86836f8aee375e41a04ff20486074778d0e2d19d668b33dc52e9dbb + languageName: node + linkType: hard + +"@babel/plugin-transform-sticky-regex@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-sticky-regex@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: cd15c407906b41e4b924ea151e455c11274dba050771ee7154ad88a1a274140ac5e84efc8d08c4379f2f0cec8a09e4a0a3b2a3a954ba6a67d9fb35df1c714c56 + languageName: node + linkType: hard + +"@babel/plugin-transform-template-literals@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-template-literals@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 9b5f43788b9ffcb8f2b445a16b1aa40fcf23cb0446a4649445f098ec6b4cb751f243a535da623d59fefe48f4c40552f5621187a61811779076bab26863e3373d + languageName: node + linkType: hard + +"@babel/plugin-transform-typeof-symbol@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-typeof-symbol@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 50e81d84c6059878be2a0e41e0d790cab10882cfb8fa85e8c2665ccb0b3cd7233f49197f17427bc7c1b36c80e07076640ecf1b641888d78b9cb91bc16478d84a + languageName: node + linkType: hard + +"@babel/plugin-transform-typescript@npm:^7.23.3": + version: 7.23.5 + resolution: "@babel/plugin-transform-typescript@npm:7.23.5" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + "@babel/helper-create-class-features-plugin": "npm:^7.23.5" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-typescript": "npm:^7.23.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 75d6689bfdf4c9462b5fb21107c295717c9bedffe5eae8b22b0a65c9603660683d55e020df83825de13792358043bd939f48efc2b3a293b5210a608076c94934 + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-escapes@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-unicode-escapes@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f1ed54742dc982666f471df5d087cfda9c6dbf7842bec2d0f7893ed359b142a38c0210358f297ab5c7a3e11ec0dfb0e523de2e2edf48b62f257aaadd5f068866 + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-property-regex@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.23.3" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: dca5702d43fac70351623a12e4dfa454fd028a67498888522b644fd1a02534fabd440106897e886ebcc6ce6a39c58094ca29953b6f51bc67372aa8845a5ae49f + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-regex@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-unicode-regex@npm:7.23.3" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: df824dcca2f6e731f61d69103e87d5dd974d8a04e46e28684a4ba935ae633d876bded09b8db890fd72d0caf7b9638e2672b753671783613cc78d472951e2df8c + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-sets-regex@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.23.3" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 30fe1d29af8395a867d40a63a250ca89072033d9bc7d4587eeebeaf4ad7f776aab83064321bfdb1d09d7e29a1d392852361f4f60a353f0f4d1a3b435dcbf256b + languageName: node + linkType: hard + +"@babel/preset-env@npm:^7.23.2": + version: 7.23.5 + resolution: "@babel/preset-env@npm:7.23.5" + dependencies: + "@babel/compat-data": "npm:^7.23.5" + "@babel/helper-compilation-targets": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-validator-option": "npm:^7.23.5" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.23.3" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.23.3" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.23.3" + "@babel/plugin-proposal-private-property-in-object": "npm:7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators": "npm:^7.8.4" + "@babel/plugin-syntax-class-properties": "npm:^7.12.13" + "@babel/plugin-syntax-class-static-block": "npm:^7.14.5" + "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" + "@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3" + "@babel/plugin-syntax-import-assertions": "npm:^7.23.3" + "@babel/plugin-syntax-import-attributes": "npm:^7.23.3" + "@babel/plugin-syntax-import-meta": "npm:^7.10.4" + "@babel/plugin-syntax-json-strings": "npm:^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" + "@babel/plugin-syntax-numeric-separator": "npm:^7.10.4" + "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" + "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" + "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" + "@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5" + "@babel/plugin-syntax-top-level-await": "npm:^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6" + "@babel/plugin-transform-arrow-functions": "npm:^7.23.3" + "@babel/plugin-transform-async-generator-functions": "npm:^7.23.4" + "@babel/plugin-transform-async-to-generator": "npm:^7.23.3" + "@babel/plugin-transform-block-scoped-functions": "npm:^7.23.3" + "@babel/plugin-transform-block-scoping": "npm:^7.23.4" + "@babel/plugin-transform-class-properties": "npm:^7.23.3" + "@babel/plugin-transform-class-static-block": "npm:^7.23.4" + "@babel/plugin-transform-classes": "npm:^7.23.5" + "@babel/plugin-transform-computed-properties": "npm:^7.23.3" + "@babel/plugin-transform-destructuring": "npm:^7.23.3" + "@babel/plugin-transform-dotall-regex": "npm:^7.23.3" + "@babel/plugin-transform-duplicate-keys": "npm:^7.23.3" + "@babel/plugin-transform-dynamic-import": "npm:^7.23.4" + "@babel/plugin-transform-exponentiation-operator": "npm:^7.23.3" + "@babel/plugin-transform-export-namespace-from": "npm:^7.23.4" + "@babel/plugin-transform-for-of": "npm:^7.23.3" + "@babel/plugin-transform-function-name": "npm:^7.23.3" + "@babel/plugin-transform-json-strings": "npm:^7.23.4" + "@babel/plugin-transform-literals": "npm:^7.23.3" + "@babel/plugin-transform-logical-assignment-operators": "npm:^7.23.4" + "@babel/plugin-transform-member-expression-literals": "npm:^7.23.3" + "@babel/plugin-transform-modules-amd": "npm:^7.23.3" + "@babel/plugin-transform-modules-commonjs": "npm:^7.23.3" + "@babel/plugin-transform-modules-systemjs": "npm:^7.23.3" + "@babel/plugin-transform-modules-umd": "npm:^7.23.3" + "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.22.5" + "@babel/plugin-transform-new-target": "npm:^7.23.3" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.23.4" + "@babel/plugin-transform-numeric-separator": "npm:^7.23.4" + "@babel/plugin-transform-object-rest-spread": "npm:^7.23.4" + "@babel/plugin-transform-object-super": "npm:^7.23.3" + "@babel/plugin-transform-optional-catch-binding": "npm:^7.23.4" + "@babel/plugin-transform-optional-chaining": "npm:^7.23.4" + "@babel/plugin-transform-parameters": "npm:^7.23.3" + "@babel/plugin-transform-private-methods": "npm:^7.23.3" + "@babel/plugin-transform-private-property-in-object": "npm:^7.23.4" + "@babel/plugin-transform-property-literals": "npm:^7.23.3" + "@babel/plugin-transform-regenerator": "npm:^7.23.3" + "@babel/plugin-transform-reserved-words": "npm:^7.23.3" + "@babel/plugin-transform-shorthand-properties": "npm:^7.23.3" + "@babel/plugin-transform-spread": "npm:^7.23.3" + "@babel/plugin-transform-sticky-regex": "npm:^7.23.3" + "@babel/plugin-transform-template-literals": "npm:^7.23.3" + "@babel/plugin-transform-typeof-symbol": "npm:^7.23.3" + "@babel/plugin-transform-unicode-escapes": "npm:^7.23.3" + "@babel/plugin-transform-unicode-property-regex": "npm:^7.23.3" + "@babel/plugin-transform-unicode-regex": "npm:^7.23.3" + "@babel/plugin-transform-unicode-sets-regex": "npm:^7.23.3" + "@babel/preset-modules": "npm:0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2: "npm:^0.4.6" + babel-plugin-polyfill-corejs3: "npm:^0.8.5" + babel-plugin-polyfill-regenerator: "npm:^0.5.3" + core-js-compat: "npm:^3.31.0" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 2a0e1274dec045186e131c6433659b75492583290e8d41633c616f6bff829cb2e4b2f9a57f556283a54db3bd6aa697911e56a36f607911a29b731c445a5b5a06 + languageName: node + linkType: hard + +"@babel/preset-flow@npm:^7.22.15": + version: 7.23.3 + resolution: "@babel/preset-flow@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-validator-option": "npm:^7.22.15" + "@babel/plugin-transform-flow-strip-types": "npm:^7.23.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 1cf109925791f2af679f03289848d27596b4f27cb0ad4ee74a8dd4c1cbecc119bdef3b45cbbe12489bc9bdf61163f94c1c0bf6013cc58c325f1cc99edc01bda9 + languageName: node + linkType: hard + +"@babel/preset-modules@npm:0.1.6-no-external-plugins": + version: 0.1.6-no-external-plugins + resolution: "@babel/preset-modules@npm:0.1.6-no-external-plugins" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.0.0" + "@babel/types": "npm:^7.4.4" + esutils: "npm:^2.0.2" + peerDependencies: + "@babel/core": ^7.0.0-0 || ^8.0.0-0 <8.0.0 + checksum: 9d02f70d7052446c5f3a4fb39e6b632695fb6801e46d31d7f7c5001f7c18d31d1ea8369212331ca7ad4e7877b73231f470b0d559162624128f1b80fe591409e6 + languageName: node + linkType: hard + +"@babel/preset-typescript@npm:^7.23.0": + version: 7.23.3 + resolution: "@babel/preset-typescript@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-validator-option": "npm:^7.22.15" + "@babel/plugin-syntax-jsx": "npm:^7.23.3" + "@babel/plugin-transform-modules-commonjs": "npm:^7.23.3" + "@babel/plugin-transform-typescript": "npm:^7.23.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e72b654c7f0f08b35d7e1c0e3a59c0c13037f295c425760b8b148aa7dde01e6ddd982efc525710f997a1494fafdd55cb525738c016609e7e4d703d02014152b7 + languageName: node + linkType: hard + +"@babel/register@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/register@npm:7.22.15" + dependencies: + clone-deep: "npm:^4.0.1" + find-cache-dir: "npm:^2.0.0" + make-dir: "npm:^2.1.0" + pirates: "npm:^4.0.5" + source-map-support: "npm:^0.5.16" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 895cc773c3b3eae909478ea2a9735ef6edd634b04b4aaaad2ce576fd591c2b3c70ff8c90423e769a291bee072186e7e4801480c1907e31ba3053c6cdba5571cb + languageName: node + linkType: hard + +"@babel/regjsgen@npm:^0.8.0": + version: 0.8.0 + resolution: "@babel/regjsgen@npm:0.8.0" + checksum: 4f3ddd8c7c96d447e05c8304c1d5ba3a83fcabd8a716bc1091c2f31595cdd43a3a055fff7cb5d3042b8cb7d402d78820fcb4e05d896c605a7d8bcf30f2424c4a + languageName: node + linkType: hard + +"@babel/runtime-corejs3@npm:^7.16.5": + version: 7.23.5 + resolution: "@babel/runtime-corejs3@npm:7.23.5" + dependencies: + core-js-pure: "npm:^3.30.2" + regenerator-runtime: "npm:^0.14.0" + checksum: 9bbad4ae7efea21e2c92ddee70b42ce9773a56e044cfc16267f9610b38ee531c87b465d84d39433fca93f7f567b47d5e40383e3d2cfe85dbeceea7fba8a52cc8 + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": + version: 7.23.5 + resolution: "@babel/runtime@npm:7.23.5" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: ca679cc91bb7e424bc2db87bb58cc3b06ade916b9adb21fbbdc43e54cdaacb3eea201ceba2a0464b11d2eb65b9fe6a6ffcf4d7521fa52994f19be96f1af14788 + languageName: node + linkType: hard + +"@babel/template@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/template@npm:7.22.15" + dependencies: + "@babel/code-frame": "npm:^7.22.13" + "@babel/parser": "npm:^7.22.15" + "@babel/types": "npm:^7.22.15" + checksum: 9312edd37cf1311d738907003f2aa321a88a42ba223c69209abe4d7111db019d321805504f606c7fd75f21c6cf9d24d0a8223104cd21ebd207e241b6c551f454 + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/traverse@npm:7.23.5" + dependencies: + "@babel/code-frame": "npm:^7.23.5" + "@babel/generator": "npm:^7.23.5" + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-function-name": "npm:^7.23.0" + "@babel/helper-hoist-variables": "npm:^7.22.5" + "@babel/helper-split-export-declaration": "npm:^7.22.6" + "@babel/parser": "npm:^7.23.5" + "@babel/types": "npm:^7.23.5" + debug: "npm:^4.1.0" + globals: "npm:^11.1.0" + checksum: c5ea793080ca6719b0a1612198fd25e361cee1f3c14142d7a518d2a1eeb5c1d21f7eec1b26c20ea6e1ddd8ed12ab50b960ff95ffd25be353b6b46e1b54d6f825 + languageName: node + linkType: hard + +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.5, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": + version: 7.23.5 + resolution: "@babel/types@npm:7.23.5" + dependencies: + "@babel/helper-string-parser": "npm:^7.23.4" + "@babel/helper-validator-identifier": "npm:^7.22.20" + to-fast-properties: "npm:^2.0.0" + checksum: 7dd5e2f59828ed046ad0b06b039df2524a8b728d204affb4fc08da2502b9dd3140b1356b5166515d229dc811539a8b70dcd4bc507e06d62a89f4091a38d0b0fb + languageName: node + linkType: hard + +"@base2/pretty-print-object@npm:1.0.1": + version: 1.0.1 + resolution: "@base2/pretty-print-object@npm:1.0.1" + checksum: 98f77ea185a30c854897feb2a68fe51be8451a1a0b531bac61a5dd67033926a0ba0c9be6e0f819b8cb72ca349b3e7648bf81c12fd21df0b45219c75a3a75784b + languageName: node + linkType: hard + +"@bufbuild/protobuf@npm:^1.0.0": + version: 1.7.2 + resolution: "@bufbuild/protobuf@npm:1.7.2" + checksum: 37a968b7d314c1f2e2b996bb287c72dbeaacd5bc0d92e2f706437a51c4e483ff85b97994428e252d6acf99bd7b16435471413ae3af1bd9b416d72ab3f0decd22 + languageName: node + linkType: hard + +"@colors/colors@npm:1.5.0": + version: 1.5.0 + resolution: "@colors/colors@npm:1.5.0" + checksum: eb42729851adca56d19a08e48d5a1e95efd2a32c55ae0323de8119052be0510d4b7a1611f2abcbf28c044a6c11e6b7d38f99fccdad7429300c37a8ea5fb95b44 + languageName: node + linkType: hard + +"@colors/colors@npm:1.6.0, @colors/colors@npm:^1.6.0": + version: 1.6.0 + resolution: "@colors/colors@npm:1.6.0" + checksum: 9328a0778a5b0db243af54455b79a69e3fb21122d6c15ef9e9fcc94881d8d17352d8b2b2590f9bdd46fac5c2d6c1636dcfc14358a20c70e22daf89e1a759b629 + languageName: node + linkType: hard + +"@dabh/diagnostics@npm:^2.0.2": + version: 2.0.3 + resolution: "@dabh/diagnostics@npm:2.0.3" + dependencies: + colorspace: "npm:1.1.x" + enabled: "npm:2.0.x" + kuler: "npm:^2.0.0" + checksum: a5133df8492802465ed01f2f0a5784585241a1030c362d54a602ed1839816d6c93d71dde05cf2ddb4fd0796238c19774406bd62fa2564b637907b495f52425fe + languageName: node + linkType: hard + +"@discoveryjs/json-ext@npm:^0.5.3": + version: 0.5.7 + resolution: "@discoveryjs/json-ext@npm:0.5.7" + checksum: e10f1b02b78e4812646ddf289b7d9f2cb567d336c363b266bd50cd223cf3de7c2c74018d91cd2613041568397ef3a4a2b500aba588c6e5bd78c38374ba68f38c + languageName: node + linkType: hard + +"@emotion/use-insertion-effect-with-fallbacks@npm:^1.0.0": + version: 1.0.1 + resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.0.1" + peerDependencies: + react: ">=16.8.0" + checksum: a15b2167940e3a908160687b73fc4fcd81e59ab45136b6967f02c7c419d9a149acd22a416b325c389642d4f1c3d33cf4196cad6b618128b55b7c74f6807a240b + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/android-arm64@npm:0.18.20" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/android-arm64@npm:0.19.8" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/android-arm@npm:0.18.20" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/android-arm@npm:0.19.8" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/android-x64@npm:0.18.20" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/android-x64@npm:0.19.8" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/darwin-arm64@npm:0.18.20" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/darwin-arm64@npm:0.19.8" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/darwin-x64@npm:0.18.20" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/darwin-x64@npm:0.19.8" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/freebsd-arm64@npm:0.18.20" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/freebsd-arm64@npm:0.19.8" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/freebsd-x64@npm:0.18.20" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/freebsd-x64@npm:0.19.8" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-arm64@npm:0.18.20" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-arm64@npm:0.19.8" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-arm@npm:0.18.20" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-arm@npm:0.19.8" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-ia32@npm:0.18.20" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-ia32@npm:0.19.8" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-loong64@npm:0.18.20" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-loong64@npm:0.19.8" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-mips64el@npm:0.18.20" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-mips64el@npm:0.19.8" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-ppc64@npm:0.18.20" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-ppc64@npm:0.19.8" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-riscv64@npm:0.18.20" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-riscv64@npm:0.19.8" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-s390x@npm:0.18.20" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-s390x@npm:0.19.8" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/linux-x64@npm:0.18.20" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-x64@npm:0.19.8" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/netbsd-x64@npm:0.18.20" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/netbsd-x64@npm:0.19.8" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/openbsd-x64@npm:0.18.20" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/openbsd-x64@npm:0.19.8" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/sunos-x64@npm:0.18.20" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/sunos-x64@npm:0.19.8" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/win32-arm64@npm:0.18.20" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/win32-arm64@npm:0.19.8" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/win32-ia32@npm:0.18.20" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/win32-ia32@npm:0.19.8" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.18.20": + version: 0.18.20 + resolution: "@esbuild/win32-x64@npm:0.18.20" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/win32-x64@npm:0.19.8" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@fal-works/esbuild-plugin-global-externals@npm:^2.1.2": + version: 2.1.2 + resolution: "@fal-works/esbuild-plugin-global-externals@npm:2.1.2" + checksum: 2c84a8e6121b00ac8e4eb2469ab8f188142db2f1927391758e5d0142cb684b7eb0fad0c9d6caf358616eb2a77af2c067e08b9ec8e05749b415fc4dd0ef96d0fe + languageName: node + linkType: hard + +"@floating-ui/core@npm:^1.4.2": + version: 1.5.0 + resolution: "@floating-ui/core@npm:1.5.0" + dependencies: + "@floating-ui/utils": "npm:^0.1.3" + checksum: bca811cefd09c3f56c4cf58c3e94826c1ce4a0b40124e9030ddca2ef1cc68b4ddc5ba5b4d7cc94c9555aea6876d2428a77a2ae261fe5b39c79df247a9518b053 + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^1.5.1": + version: 1.5.3 + resolution: "@floating-ui/dom@npm:1.5.3" + dependencies: + "@floating-ui/core": "npm:^1.4.2" + "@floating-ui/utils": "npm:^0.1.3" + checksum: e5f30b911f939e40003851077bba441f269ae689bdc43c674bee43aa98fc6b7a5f59be432d27b7be599b1e4ab7b15c752875ea777a89cff01d157e593b78b25b + languageName: node + linkType: hard + +"@floating-ui/react-dom@npm:^2.0.0": + version: 2.0.4 + resolution: "@floating-ui/react-dom@npm:2.0.4" + dependencies: + "@floating-ui/dom": "npm:^1.5.1" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 5d597a7939e484428452cee775884f6c14055783d811a1abedf03151eb8825ecf42a544553efecdc502f30ca2a6b3e6630485367c39473d259e74f5f1331bc0a + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.1.3": + version: 0.1.6 + resolution: "@floating-ui/utils@npm:0.1.6" + checksum: 0a089db0e0526b89e83cb0a773a903517db5c9067cd473febfd8fa91a3a2ccbc3a835234796c1bb528def21dbb67be50e28d9c473cb58a6d90679d7e549b9c0c + languageName: node + linkType: hard + +"@gulp-sourcemaps/identity-map@npm:^2.0.1": + version: 2.0.1 + resolution: "@gulp-sourcemaps/identity-map@npm:2.0.1" + dependencies: + acorn: "npm:^6.4.1" + normalize-path: "npm:^3.0.0" + postcss: "npm:^7.0.16" + source-map: "npm:^0.6.0" + through2: "npm:^3.0.1" + checksum: 1102181f6a34eb569b8001a5c10c5583c4a52d6cfeadeee37fdee508fe6bb8966399d208596b56948c18b0c5e0c8dfa59de42e7645a2d22d171b322c4a8fe933 + languageName: node + linkType: hard + +"@gulp-sourcemaps/map-sources@npm:^1.0.0": + version: 1.0.0 + resolution: "@gulp-sourcemaps/map-sources@npm:1.0.0" + dependencies: + normalize-path: "npm:^2.0.1" + through2: "npm:^2.0.3" + checksum: 7b5bf8b52aacf656b8e727f2f4e5f6b37de7abc2c679e9f19a94ee1bbd3a8116df49ca31b64fba9471131983ab953c6b8ffab100a7af4b73bd6fd46a058b5f54 + languageName: node + linkType: hard + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + languageName: node + linkType: hard + +"@istanbuljs/load-nyc-config@npm:^1.0.0": + version: 1.1.0 + resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" + dependencies: + camelcase: "npm:^5.3.1" + find-up: "npm:^4.1.0" + get-package-type: "npm:^0.1.0" + js-yaml: "npm:^3.13.1" + resolve-from: "npm:^5.0.0" + checksum: dd2a8b094887da5a1a2339543a4933d06db2e63cbbc2e288eb6431bd832065df0c099d091b6a67436e71b7d6bf85f01ce7c15f9253b4cbebcc3b9a496165ba42 + languageName: node + linkType: hard + +"@istanbuljs/schema@npm:^0.1.2": + version: 0.1.3 + resolution: "@istanbuljs/schema@npm:0.1.3" + checksum: 61c5286771676c9ca3eb2bd8a7310a9c063fb6e0e9712225c8471c582d157392c88f5353581c8c9adbe0dff98892317d2fdfc56c3499aa42e0194405206a963a + languageName: node + linkType: hard + +"@jest/schemas@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/schemas@npm:29.6.3" + dependencies: + "@sinclair/typebox": "npm:^0.27.8" + checksum: b329e89cd5f20b9278ae1233df74016ebf7b385e0d14b9f4c1ad18d096c4c19d1e687aa113a9c976b16ec07f021ae53dea811fb8c1248a50ac34fbe009fdf6be + languageName: node + linkType: hard + +"@jest/transform@npm:^29.3.1": + version: 29.7.0 + resolution: "@jest/transform@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@jest/types": "npm:^29.6.3" + "@jridgewell/trace-mapping": "npm:^0.3.18" + babel-plugin-istanbul: "npm:^6.1.1" + chalk: "npm:^4.0.0" + convert-source-map: "npm:^2.0.0" + fast-json-stable-stringify: "npm:^2.1.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + pirates: "npm:^4.0.4" + slash: "npm:^3.0.0" + write-file-atomic: "npm:^4.0.2" + checksum: 7f4a7f73dcf45dfdf280c7aa283cbac7b6e5a904813c3a93ead7e55873761fc20d5c4f0191d2019004fac6f55f061c82eb3249c2901164ad80e362e7a7ede5a6 + languageName: node + linkType: hard + +"@jest/types@npm:^27.5.1": + version: 27.5.1 + resolution: "@jest/types@npm:27.5.1" + dependencies: + "@types/istanbul-lib-coverage": "npm:^2.0.0" + "@types/istanbul-reports": "npm:^3.0.0" + "@types/node": "npm:*" + "@types/yargs": "npm:^16.0.0" + chalk: "npm:^4.0.0" + checksum: 4598b302398db0eb77168b75a6c58148ea02cc9b9f21c5d1bbe985c1c9257110a5653cf7b901c3cab87fba231e3fed83633687f1c0903b4bc6939ab2a8452504 + languageName: node + linkType: hard + +"@jest/types@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/types@npm:29.6.3" + dependencies: + "@jest/schemas": "npm:^29.6.3" + "@types/istanbul-lib-coverage": "npm:^2.0.0" + "@types/istanbul-reports": "npm:^3.0.0" + "@types/node": "npm:*" + "@types/yargs": "npm:^17.0.8" + chalk: "npm:^4.0.0" + checksum: ea4e493dd3fb47933b8ccab201ae573dcc451f951dc44ed2a86123cd8541b82aa9d2b1031caf9b1080d6673c517e2dcc25a44b2dc4f3fbc37bfc965d444888c0 + languageName: node + linkType: hard + +"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.3.0": + version: 0.3.0 + resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.3.0" + dependencies: + glob: "npm:^7.2.0" + glob-promise: "npm:^4.2.0" + magic-string: "npm:^0.27.0" + react-docgen-typescript: "npm:^2.2.2" + peerDependencies: + typescript: ">= 4.3.x" + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 31098ad8fcc2440437534599c111d9f2951dd74821e8ba46c521b969bae4c918d830b7bb0484efbad29a51711bb62d3bc623d5a1ed5b1695b5b5594ea9dd4ca0 + languageName: node + linkType: hard + +"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2": + version: 0.3.3 + resolution: "@jridgewell/gen-mapping@npm:0.3.3" + dependencies: + "@jridgewell/set-array": "npm:^1.0.1" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + "@jridgewell/trace-mapping": "npm:^0.3.9" + checksum: 376fc11cf5a967318ba3ddd9d8e91be528eab6af66810a713c49b0c3f8dc67e9949452c51c38ab1b19aa618fb5e8594da5a249977e26b1e7fea1ee5a1fcacc74 + languageName: node + linkType: hard + +"@jridgewell/resolve-uri@npm:^3.1.0": + version: 3.1.1 + resolution: "@jridgewell/resolve-uri@npm:3.1.1" + checksum: 0dbc9e29bc640bbbdc5b9876d2859c69042bfcf1423c1e6421bcca53e826660bff4e41c7d4bcb8dbea696404231a6f902f76ba41835d049e20f2dd6cffb713bf + languageName: node + linkType: hard + +"@jridgewell/set-array@npm:^1.0.1": + version: 1.1.2 + resolution: "@jridgewell/set-array@npm:1.1.2" + checksum: bc7ab4c4c00470de4e7562ecac3c0c84f53e7ee8a711e546d67c47da7febe7c45cd67d4d84ee3c9b2c05ae8e872656cdded8a707a283d30bd54fbc65aef821ab + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.13, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15": + version: 1.4.15 + resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" + checksum: 0c6b5ae663087558039052a626d2d7ed5208da36cfd707dcc5cea4a07cfc918248403dcb5989a8f7afaf245ce0573b7cc6fd94c4a30453bd10e44d9363940ba5 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.9": + version: 0.3.20 + resolution: "@jridgewell/trace-mapping@npm:0.3.20" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 0ea0b2675cf513ec44dc25605616a3c9b808b9832e74b5b63c44260d66b58558bba65764f81928fc1033ead911f8718dca1134049c3e7a93937faf436671df31 + languageName: node + linkType: hard + +"@juggle/resize-observer@npm:^3.3.1": + version: 3.4.0 + resolution: "@juggle/resize-observer@npm:3.4.0" + checksum: 12930242357298c6f2ad5d4ec7cf631dfb344ca7c8c830ab7f64e6ac11eb1aae486901d8d880fd08fb1b257800c160a0da3aee1e7ed9adac0ccbb9b7c5d93347 + languageName: node + linkType: hard + +"@mdx-js/react@npm:^2.1.5": + version: 2.3.0 + resolution: "@mdx-js/react@npm:2.3.0" + dependencies: + "@types/mdx": "npm:^2.0.0" + "@types/react": "npm:>=16" + peerDependencies: + react: ">=16" + checksum: 6d647115703dbe258f7fe372499fa8c6fe17a053ff0f2a208111c9973a71ae738a0ed376770445d39194d217e00e1a015644b24f32c2f7cb4f57988de0649b15 + languageName: node + linkType: hard + +"@ndelangen/get-tarball@npm:^3.0.7": + version: 3.0.9 + resolution: "@ndelangen/get-tarball@npm:3.0.9" + dependencies: + gunzip-maybe: "npm:^1.4.2" + pump: "npm:^3.0.0" + tar-fs: "npm:^2.1.1" + checksum: d66e76c6c990745d691c85d1dfa7f3dfd181405bb52c295baf4d1838b847d40c686e24602ea0ab1cdeb14d409db59f6bb9e2f96f56fe53da275da9cccf778e27 + languageName: node + linkType: hard + +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": "npm:2.0.5" + run-parallel: "npm:^1.1.9" + checksum: 732c3b6d1b1e967440e65f284bd06e5821fedf10a1bea9ed2bb75956ea1f30e08c44d3def9d6a230666574edbaf136f8cfd319c14fd1f87c66e6a44449afb2eb + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 88dafe5e3e29a388b07264680dc996c17f4bda48d163a9d4f5c1112979f0ce8ec72aa7116122c350b4e7976bc5566dc3ddb579be1ceaacc727872eb4ed93926d + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:^1.2.3": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": "npm:2.1.5" + fastq: "npm:^1.6.0" + checksum: db9de047c3bb9b51f9335a7bb46f4fcfb6829fb628318c12115fbaf7d369bfce71c15b103d1fc3b464812d936220ee9bc1c8f762d032c9f6be9acc99249095b1 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^2.0.0": + version: 2.2.0 + resolution: "@npmcli/agent@npm:2.2.0" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.1" + checksum: 7b89590598476dda88e79c473766b67c682aae6e0ab0213491daa6083dcc0c171f86b3868f5506f22c09aa5ea69ad7dfb78f4bf39a8dca375d89a42f408645b3 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.0 + resolution: "@npmcli/fs@npm:3.1.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 162b4a0b8705cd6f5c2470b851d1dc6cd228c86d2170e1769d738c1fbb69a87160901411c3c035331e9e99db72f1f1099a8b734bf1637cc32b9a5be1660e4e1e + languageName: node + linkType: hard + +"@one-ini/wasm@npm:0.1.1": + version: 0.1.1 + resolution: "@one-ini/wasm@npm:0.1.1" + checksum: 54700e055037f1a63bfcc86d24822203b25759598c2c3e295d1435130a449108aebc119c9c2e467744767dbe0b6ab47a182c61aa1071ba7368f5e20ab197ba65 + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + languageName: node + linkType: hard + +"@radix-ui/number@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/number@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + checksum: 42e4870cd14459da6da03e43c7507dc4c807ed787a87bda52912a0d1d6d5013326b697c18c9625fc6a2cf0af2b45d9c86747985b45358fd92ab646b983978e3c + languageName: node + linkType: hard + +"@radix-ui/primitive@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/primitive@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + checksum: 912216455537db3ca77f3e7f70174fb2b454fbd4a37a0acb7cfadad9ab6131abdfb787472242574460a3c301edf45738340cc84f6717982710082840fde7d916 + languageName: node + linkType: hard + +"@radix-ui/react-arrow@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-arrow@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-primitive": "npm:1.0.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: c931f6d7e0bac50fd1654a0303a303aff74a68a13a33a851a43a7c88677b53a92ca6557920b9105144a3002f899ce888437d20ddd7803a5c716edac99587626d + languageName: node + linkType: hard + +"@radix-ui/react-collection@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-collection@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-context": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-slot": "npm:1.0.2" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: cefa56383d7451ca79e4bd5a29aaeef6c205a04297213efd149aaead82fc8cde4fb8298e20e6b3613e5696e43f814fb4489805428f6604834fb31f73c6725fa8 + languageName: node + linkType: hard + +"@radix-ui/react-compose-refs@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-compose-refs@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: be06f8dab35b5a1bffa7a5982fb26218ddade1acb751288333e3b89d7b4a7dfb5a6371be83876dac0ec2ebe0866d295e8618b778608e1965342986ea448040ec + languageName: node + linkType: hard + +"@radix-ui/react-context@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-context@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 3de5761b32cc70cd61715527f29d8c699c01ab28c195ced972ccbc7025763a373a68f18c9f948c7a7b922e469fd2df7fee5f7536e3f7bad44ffc06d959359333 + languageName: node + linkType: hard + +"@radix-ui/react-direction@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-direction@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: b1a45b4d1d5070ca3b5864b920f6c6210c962bdb519abb62b38b1baef9d06737dc3d8ecdb61860b7504a735235a539652f5977c7299ec021da84e6b0f64d988a + languageName: node + linkType: hard + +"@radix-ui/react-dismissable-layer@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-dismissable-layer@npm:1.0.4" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/primitive": "npm:1.0.1" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + "@radix-ui/react-use-escape-keydown": "npm:1.0.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: a7b9695092cd4109a7b4a4a66b7f634c42d4f39aa0893621a8ee5e8bc90f8ae27e741df66db726c341a60d2115e3f813520fee1f5cc4fb05d77914b4ade3819f + languageName: node + linkType: hard + +"@radix-ui/react-focus-guards@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-focus-guards@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: d5fd4e5aa9d9a87c8ad490b3b4992d6f1d9eddf18e56df2a2bcf8744c4332b275d73377fd193df3e6ba0ad9608dc497709beca5c64de2b834d5f5350b3c9a272 + languageName: node + linkType: hard + +"@radix-ui/react-focus-scope@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-focus-scope@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: bfff46919666c122f5b812ee427494ae8408c0eebee30337bd2ce0eedf539f0feaa242f790304ef9df15425b837010ffc6061ce467bedd2c5fd9373bee2b95da + languageName: node + linkType: hard + +"@radix-ui/react-id@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-id@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-use-layout-effect": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: e2859ca58bea171c956098ace7ecf615cf9432f58a118b779a14720746b3adcf0351c36c75de131548672d3cd290ca238198acbd33b88dc4706f98312e9317ad + languageName: node + linkType: hard + +"@radix-ui/react-popper@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-popper@npm:1.1.2" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@floating-ui/react-dom": "npm:^2.0.0" + "@radix-ui/react-arrow": "npm:1.0.3" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-context": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + "@radix-ui/react-use-layout-effect": "npm:1.0.1" + "@radix-ui/react-use-rect": "npm:1.0.1" + "@radix-ui/react-use-size": "npm:1.0.1" + "@radix-ui/rect": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 4bd069b79f7046af2c0967b8e43f727cd09834cbd6df1e3d5a943c4f83428ff8b646882737fdf7593c22e261a1d13768a5c020138d79503862ae2e1729081bba + languageName: node + linkType: hard + +"@radix-ui/react-portal@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-portal@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-primitive": "npm:1.0.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: baf295bbbf09ead37b64ee1dc025a6a540960f5e60552766d78f6065504c67d4bcf49fad5e2073617d9a3011daafad625aa3bd1da7a886c704833b22a49e888f + languageName: node + linkType: hard + +"@radix-ui/react-primitive@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-primitive@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-slot": "npm:1.0.2" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 67a66ff8898a5e7739eda228ab6f5ce808858da1dce967014138d87e72b6bbfc93dc1467c706d98d1a2b93bf0b6e09233d1a24d31c78227b078444c1a69c42be + languageName: node + linkType: hard + +"@radix-ui/react-roving-focus@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-roving-focus@npm:1.0.4" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/primitive": "npm:1.0.1" + "@radix-ui/react-collection": "npm:1.0.3" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-context": "npm:1.0.1" + "@radix-ui/react-direction": "npm:1.0.1" + "@radix-ui/react-id": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + "@radix-ui/react-use-controllable-state": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 61e3ddfd1647e64fba855434ff41e8e7ba707244fe8841f78c450fbdce525383b64259279475615d030dbf1625cbffd8eeebee72d91bf6978794f5dbcf887fc0 + languageName: node + linkType: hard + +"@radix-ui/react-select@npm:^1.2.2": + version: 1.2.2 + resolution: "@radix-ui/react-select@npm:1.2.2" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/number": "npm:1.0.1" + "@radix-ui/primitive": "npm:1.0.1" + "@radix-ui/react-collection": "npm:1.0.3" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-context": "npm:1.0.1" + "@radix-ui/react-direction": "npm:1.0.1" + "@radix-ui/react-dismissable-layer": "npm:1.0.4" + "@radix-ui/react-focus-guards": "npm:1.0.1" + "@radix-ui/react-focus-scope": "npm:1.0.3" + "@radix-ui/react-id": "npm:1.0.1" + "@radix-ui/react-popper": "npm:1.1.2" + "@radix-ui/react-portal": "npm:1.0.3" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-slot": "npm:1.0.2" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + "@radix-ui/react-use-controllable-state": "npm:1.0.1" + "@radix-ui/react-use-layout-effect": "npm:1.0.1" + "@radix-ui/react-use-previous": "npm:1.0.1" + "@radix-ui/react-visually-hidden": "npm:1.0.3" + aria-hidden: "npm:^1.1.1" + react-remove-scroll: "npm:2.5.5" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 888fffa703a8f79b45c01d5f03ad9aae66250ddfff827bbba4f222c4d0720aa2f01a3e4b6bd80acabaf5e2fa7ad79de9e9dfd14831f7f4c24337d4d8dfb58ccc + languageName: node + linkType: hard + +"@radix-ui/react-separator@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-separator@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-primitive": "npm:1.0.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 87bcde47343f2bc4439a0dc34381f557905d9b3c1e8c5a0d32ceea62a8ef84f3abf671c5cb29309fc87759ad41d39af619ba546cf54109d64c8746e3ca683de3 + languageName: node + linkType: hard + +"@radix-ui/react-slot@npm:1.0.2": + version: 1.0.2 + resolution: "@radix-ui/react-slot@npm:1.0.2" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-compose-refs": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 3af6ea4891e6fa8091e666802adffe7718b3cd390a10fa9229a5f40f8efded9f3918ea01b046103d93923d41cc32119505ebb6bde76cad07a87b6cf4f2119347 + languageName: node + linkType: hard + +"@radix-ui/react-toggle-group@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-toggle-group@npm:1.0.4" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/primitive": "npm:1.0.1" + "@radix-ui/react-context": "npm:1.0.1" + "@radix-ui/react-direction": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-roving-focus": "npm:1.0.4" + "@radix-ui/react-toggle": "npm:1.0.3" + "@radix-ui/react-use-controllable-state": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 4f4761965022759ac0950ac026029b64049e1f18ef07a01ddde788b7606efcb262c9ae3a418de0c0756bf7285182ed0d268502c6f17ba86d2ff27eee5507bbf7 + languageName: node + linkType: hard + +"@radix-ui/react-toggle@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-toggle@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/primitive": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-use-controllable-state": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 9b487dad213ea7e70b0aa205e7c6f790a6f2bf394c39912e22dbe003403fd0d24a41c2efd31695fc31ab7bac286f28253dbb2fc5202cacd572ebf909f1fdc86c + languageName: node + linkType: hard + +"@radix-ui/react-toolbar@npm:^1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-toolbar@npm:1.0.4" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/primitive": "npm:1.0.1" + "@radix-ui/react-context": "npm:1.0.1" + "@radix-ui/react-direction": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-roving-focus": "npm:1.0.4" + "@radix-ui/react-separator": "npm:1.0.3" + "@radix-ui/react-toggle-group": "npm:1.0.4" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 3ed7ebe22ef2e8369e08bb59776671a7b8c413628249c338b8db86b4b9ac40127b4201d5bd4a9c23ea1fd21464769b4fa427d3ebcda3a7fcdbd45b256b5a753a + languageName: node + linkType: hard + +"@radix-ui/react-use-callback-ref@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-callback-ref@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 331b432be1edc960ca148637ae6087220873ee828ceb13bd155926ef8f49e862812de5b379129f6aaefcd11be53715f3237e6caa9a33d9c0abfff43f3ba58938 + languageName: node + linkType: hard + +"@radix-ui/react-use-controllable-state@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-controllable-state@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 29b069dbf09e48bca321af6272574ad0fc7283174e7d092731a10663fe00c0e6b4bde5e1b5ea67725fe48dcbe8026e7ff0d69d42891c62cbb9ca408498171fbe + languageName: node + linkType: hard + +"@radix-ui/react-use-escape-keydown@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-use-escape-keydown@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 3c94c78902dcb40b60083ee2184614f45c95a189178f52d89323b467bd04bcf5fdb1bc4d43debecd7f0b572c3843c7e04edbcb56f40a4b4b43936fb2770fb8ad + languageName: node + linkType: hard + +"@radix-ui/react-use-layout-effect@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-layout-effect@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 13cd0c38395c5838bc9a18238020d3bcf67fb340039e6d1cbf438be1b91d64cf6900b78121f3dc9219faeb40dcc7b523ce0f17e4a41631655690e5a30a40886a + languageName: node + linkType: hard + +"@radix-ui/react-use-previous@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-previous@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: f5fbc602108668484a4ed506b7842482222d1d03094362e26abb7fdd593eee8794fc47d85b3524fb9d00884801c89a6eefd0bed0971eba1ec189c637b6afd398 + languageName: node + linkType: hard + +"@radix-ui/react-use-rect@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-rect@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/rect": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 94c5ab31dfd3678c0cb77a30025e82b3a287577c1a8674b0d703a36d27434bc9c59790e0bebf57ed153f0b8e0d8c3b9675fc9787b9eac525a09abcda8fa9e7eb + languageName: node + linkType: hard + +"@radix-ui/react-use-size@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-size@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-use-layout-effect": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: b109a4b3781781c4dc641a1173f0a6fcb0b0f7b2d7cdba5848a46070c9fb4e518909a46c20a3c2efbc78737c64859c59ead837f2940e8c8394d1c503ef58773b + languageName: node + linkType: hard + +"@radix-ui/react-visually-hidden@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-visually-hidden@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-primitive": "npm:1.0.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 0cbc12c2156b3fa0e40090cafd8525ce84c16a6b5a038a8e8fc7cbb16ed6da9ab369593962c57a18c41a16ec8713e0195c68ea34072ef1ca254ed4d4c0770bb4 + languageName: node + linkType: hard + +"@radix-ui/rect@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/rect@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + checksum: 4c5159661340acc31b11e1f2ebd87a1521d39bfa287544dd2cd75b399539a4b625d38a1501c90ceae21fcca18ed164b0c3735817ff140ae334098192c110e571 + languageName: node + linkType: hard + +"@resvg/resvg-js-android-arm-eabi@npm:2.6.0": + version: 2.6.0 + resolution: "@resvg/resvg-js-android-arm-eabi@npm:2.6.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@resvg/resvg-js-android-arm64@npm:2.6.0": + version: 2.6.0 + resolution: "@resvg/resvg-js-android-arm64@npm:2.6.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@resvg/resvg-js-darwin-arm64@npm:2.6.0": + version: 2.6.0 + resolution: "@resvg/resvg-js-darwin-arm64@npm:2.6.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@resvg/resvg-js-darwin-x64@npm:2.6.0": + version: 2.6.0 + resolution: "@resvg/resvg-js-darwin-x64@npm:2.6.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@resvg/resvg-js-linux-arm-gnueabihf@npm:2.6.0": + version: 2.6.0 + resolution: "@resvg/resvg-js-linux-arm-gnueabihf@npm:2.6.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@resvg/resvg-js-linux-arm64-gnu@npm:2.6.0": + version: 2.6.0 + resolution: "@resvg/resvg-js-linux-arm64-gnu@npm:2.6.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@resvg/resvg-js-linux-arm64-musl@npm:2.6.0": + version: 2.6.0 + resolution: "@resvg/resvg-js-linux-arm64-musl@npm:2.6.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@resvg/resvg-js-linux-x64-gnu@npm:2.6.0": + version: 2.6.0 + resolution: "@resvg/resvg-js-linux-x64-gnu@npm:2.6.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@resvg/resvg-js-linux-x64-musl@npm:2.6.0": + version: 2.6.0 + resolution: "@resvg/resvg-js-linux-x64-musl@npm:2.6.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@resvg/resvg-js-win32-arm64-msvc@npm:2.6.0": + version: 2.6.0 + resolution: "@resvg/resvg-js-win32-arm64-msvc@npm:2.6.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@resvg/resvg-js-win32-ia32-msvc@npm:2.6.0": + version: 2.6.0 + resolution: "@resvg/resvg-js-win32-ia32-msvc@npm:2.6.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@resvg/resvg-js-win32-x64-msvc@npm:2.6.0": + version: 2.6.0 + resolution: "@resvg/resvg-js-win32-x64-msvc@npm:2.6.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@resvg/resvg-js@npm:^2.1.0": + version: 2.6.0 + resolution: "@resvg/resvg-js@npm:2.6.0" + dependencies: + "@resvg/resvg-js-android-arm-eabi": "npm:2.6.0" + "@resvg/resvg-js-android-arm64": "npm:2.6.0" + "@resvg/resvg-js-darwin-arm64": "npm:2.6.0" + "@resvg/resvg-js-darwin-x64": "npm:2.6.0" + "@resvg/resvg-js-linux-arm-gnueabihf": "npm:2.6.0" + "@resvg/resvg-js-linux-arm64-gnu": "npm:2.6.0" + "@resvg/resvg-js-linux-arm64-musl": "npm:2.6.0" + "@resvg/resvg-js-linux-x64-gnu": "npm:2.6.0" + "@resvg/resvg-js-linux-x64-musl": "npm:2.6.0" + "@resvg/resvg-js-win32-arm64-msvc": "npm:2.6.0" + "@resvg/resvg-js-win32-ia32-msvc": "npm:2.6.0" + "@resvg/resvg-js-win32-x64-msvc": "npm:2.6.0" + dependenciesMeta: + "@resvg/resvg-js-android-arm-eabi": + optional: true + "@resvg/resvg-js-android-arm64": + optional: true + "@resvg/resvg-js-darwin-arm64": + optional: true + "@resvg/resvg-js-darwin-x64": + optional: true + "@resvg/resvg-js-linux-arm-gnueabihf": + optional: true + "@resvg/resvg-js-linux-arm64-gnu": + optional: true + "@resvg/resvg-js-linux-arm64-musl": + optional: true + "@resvg/resvg-js-linux-x64-gnu": + optional: true + "@resvg/resvg-js-linux-x64-musl": + optional: true + "@resvg/resvg-js-win32-arm64-msvc": + optional: true + "@resvg/resvg-js-win32-ia32-msvc": + optional: true + "@resvg/resvg-js-win32-x64-msvc": + optional: true + checksum: 1d2bffc2d25008aa2cda9fbfc6728538e182a941e46af6239d2f63f920b0c3619c71bb192047ca77cc604aa9dd1198fe382c611327fa8611050526ddfec8caaa + languageName: node + linkType: hard + +"@rollup/pluginutils@npm:^5.0.2": + version: 5.1.0 + resolution: "@rollup/pluginutils@npm:5.1.0" + dependencies: + "@types/estree": "npm:^1.0.0" + estree-walker: "npm:^2.0.2" + picomatch: "npm:^2.3.1" + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: c7bed15711f942d6fdd3470fef4105b73991f99a478605e13d41888963330a6f9e32be37e6ddb13f012bc7673ff5e54f06f59fd47109436c1c513986a8a7612d + languageName: node + linkType: hard + +"@rollup/rollup-android-arm-eabi@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.6.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-android-arm64@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-android-arm64@npm:4.6.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-arm64@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.6.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-x64@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.6.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-gnueabihf@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.6.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-gnu@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.6.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-musl@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.6.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-gnu@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.6.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-musl@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.6.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.6.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-ia32-msvc@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.6.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.6.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@sinclair/typebox@npm:^0.27.8": + version: 0.27.8 + resolution: "@sinclair/typebox@npm:0.27.8" + checksum: ef6351ae073c45c2ac89494dbb3e1f87cc60a93ce4cde797b782812b6f97da0d620ae81973f104b43c9b7eaa789ad20ba4f6a1359f1cc62f63729a55a7d22d4e + languageName: node + linkType: hard + +"@storybook/addon-actions@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-actions@npm:7.6.17" + dependencies: + "@storybook/core-events": "npm:7.6.17" + "@storybook/global": "npm:^5.0.0" + "@types/uuid": "npm:^9.0.1" + dequal: "npm:^2.0.2" + polished: "npm:^4.2.2" + uuid: "npm:^9.0.0" + checksum: 91d20a7c35fff6a0b2aa33f2c1171d457c68fb9d955da12629d6f75d931d5aa3756837e413ab7bb928c4cc4b48dcc5cdd63510e6028e7bd8fc8c82d93be967d0 + languageName: node + linkType: hard + +"@storybook/addon-backgrounds@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-backgrounds@npm:7.6.17" + dependencies: + "@storybook/global": "npm:^5.0.0" + memoizerific: "npm:^1.11.3" + ts-dedent: "npm:^2.0.0" + checksum: 43518d762efa8dd140d029541e8e2bb748173a8428e3de67287ca132525e33e443282a2b06f3b381250d9557ada9ea3a07039aa69cf3de6b04aec02027fb9943 + languageName: node + linkType: hard + +"@storybook/addon-controls@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-controls@npm:7.6.17" + dependencies: + "@storybook/blocks": "npm:7.6.17" + lodash: "npm:^4.17.21" + ts-dedent: "npm:^2.0.0" + checksum: da66466b801064a916e059ce127efb2ab074a5c80fb65b568ac361d09fe55e0e993cd5400d6b0361bdfd783725e59449bbd30f87643964fa0db8e02a5f9550fd + languageName: node + linkType: hard + +"@storybook/addon-docs@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-docs@npm:7.6.17" + dependencies: + "@jest/transform": "npm:^29.3.1" + "@mdx-js/react": "npm:^2.1.5" + "@storybook/blocks": "npm:7.6.17" + "@storybook/client-logger": "npm:7.6.17" + "@storybook/components": "npm:7.6.17" + "@storybook/csf-plugin": "npm:7.6.17" + "@storybook/csf-tools": "npm:7.6.17" + "@storybook/global": "npm:^5.0.0" + "@storybook/mdx2-csf": "npm:^1.0.0" + "@storybook/node-logger": "npm:7.6.17" + "@storybook/postinstall": "npm:7.6.17" + "@storybook/preview-api": "npm:7.6.17" + "@storybook/react-dom-shim": "npm:7.6.17" + "@storybook/theming": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + fs-extra: "npm:^11.1.0" + remark-external-links: "npm:^8.0.0" + remark-slug: "npm:^6.0.0" + ts-dedent: "npm:^2.0.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: b43666832f1657f4dfac976ac8b8071995d65860a29f1ac66b80adb69a0d02f0d1d70684d94ddb76f0957f003b94b4252599e19f1e6a4342686598bbb40280ae + languageName: node + linkType: hard + +"@storybook/addon-essentials@npm:^7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-essentials@npm:7.6.17" + dependencies: + "@storybook/addon-actions": "npm:7.6.17" + "@storybook/addon-backgrounds": "npm:7.6.17" + "@storybook/addon-controls": "npm:7.6.17" + "@storybook/addon-docs": "npm:7.6.17" + "@storybook/addon-highlight": "npm:7.6.17" + "@storybook/addon-measure": "npm:7.6.17" + "@storybook/addon-outline": "npm:7.6.17" + "@storybook/addon-toolbars": "npm:7.6.17" + "@storybook/addon-viewport": "npm:7.6.17" + "@storybook/core-common": "npm:7.6.17" + "@storybook/manager-api": "npm:7.6.17" + "@storybook/node-logger": "npm:7.6.17" + "@storybook/preview-api": "npm:7.6.17" + ts-dedent: "npm:^2.0.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 3208790b219e88fadc634aa00134eb3f0da9d2c05cd84e733d07e201177c58bccb85879ee4c26441a35b1e7fd318111dd668fdd8b3e57b37da512a658d4f50e9 + languageName: node + linkType: hard + +"@storybook/addon-highlight@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-highlight@npm:7.6.17" + dependencies: + "@storybook/global": "npm:^5.0.0" + checksum: 5f16a648a38257bdd66f592b519cc6b4ecf36c50d0cb01696f1c42c6c9fa2b44b7056b64d611579f2ec4764787b6bd34ea6b9ebddb01b0e562b3eb8100b1cf96 + languageName: node + linkType: hard + +"@storybook/addon-interactions@npm:^7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-interactions@npm:7.6.17" + dependencies: + "@storybook/global": "npm:^5.0.0" + "@storybook/types": "npm:7.6.17" + jest-mock: "npm:^27.0.6" + polished: "npm:^4.2.2" + ts-dedent: "npm:^2.2.0" + checksum: f0910e8db378f502270747508c42174bdb75671620d24868264638a2693c60b35f088e4c06cb2239a69f4aa176f8dc8cf9e215f872d5aeefec933643225b66b8 + languageName: node + linkType: hard + +"@storybook/addon-links@npm:^7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-links@npm:7.6.17" + dependencies: + "@storybook/csf": "npm:^0.1.2" + "@storybook/global": "npm:^5.0.0" + ts-dedent: "npm:^2.0.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + checksum: c95aa5629a948f07a260430fecb8bed283a1bcfa97d8925b5edf3d4eab46155c85dec1814a00db4206a6de8323803b3d8bf74665c97caf34bb229a403f5b03d7 + languageName: node + linkType: hard + +"@storybook/addon-measure@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-measure@npm:7.6.17" + dependencies: + "@storybook/global": "npm:^5.0.0" + tiny-invariant: "npm:^1.3.1" + checksum: 18c26fd08c6b369ae74cbea4447ae0791efb6968875223b12e84021cf1c7a48496d56c35c6b1de03603081b650c3e4b54530b8704b68467bc667cbf550623ef9 + languageName: node + linkType: hard + +"@storybook/addon-onboarding@npm:^1.0.11": + version: 1.0.11 + resolution: "@storybook/addon-onboarding@npm:1.0.11" + dependencies: + "@storybook/telemetry": "npm:^7.1.0" + react-confetti: "npm:^6.1.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: ca3de3eb85fb6d04309dbc07c26956777c064bb5032fb99aca3e43361b0816ac4326183aac99204d795fdc2010aa69c4978353c70a42926e9da0819343fcd2a0 + languageName: node + linkType: hard + +"@storybook/addon-outline@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-outline@npm:7.6.17" + dependencies: + "@storybook/global": "npm:^5.0.0" + ts-dedent: "npm:^2.0.0" + checksum: 840a554504c457e3dee273266ba90a7f36b7488a72644d046f0233c305d7fe3a0773848d104a3dc7d6efafc3e1b41a3fc4d6cdd7a37b3a3fe75a03fcde206efb + languageName: node + linkType: hard + +"@storybook/addon-toolbars@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-toolbars@npm:7.6.17" + checksum: af4453848c29ab8edb0cf6ca42ff14750841eaf3b523920620e42c27c0f07574a83c0dfe75f6a0de1846178aafb6833d59cef7faa7268777c24ed490da647814 + languageName: node + linkType: hard + +"@storybook/addon-viewport@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-viewport@npm:7.6.17" + dependencies: + memoizerific: "npm:^1.11.3" + checksum: d888954f45ab358189cf0172e1c9b8a1bd2b68aa99d5d6518abe7fc355bbfeb91cc1c21c64e461994f5987652d05944aaa270366e22475eaeccadc701419b0d7 + languageName: node + linkType: hard + +"@storybook/blocks@npm:7.6.17, @storybook/blocks@npm:^7.6.17": + version: 7.6.17 + resolution: "@storybook/blocks@npm:7.6.17" + dependencies: + "@storybook/channels": "npm:7.6.17" + "@storybook/client-logger": "npm:7.6.17" + "@storybook/components": "npm:7.6.17" + "@storybook/core-events": "npm:7.6.17" + "@storybook/csf": "npm:^0.1.2" + "@storybook/docs-tools": "npm:7.6.17" + "@storybook/global": "npm:^5.0.0" + "@storybook/manager-api": "npm:7.6.17" + "@storybook/preview-api": "npm:7.6.17" + "@storybook/theming": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + "@types/lodash": "npm:^4.14.167" + color-convert: "npm:^2.0.1" + dequal: "npm:^2.0.2" + lodash: "npm:^4.17.21" + markdown-to-jsx: "npm:^7.1.8" + memoizerific: "npm:^1.11.3" + polished: "npm:^4.2.2" + react-colorful: "npm:^5.1.2" + telejson: "npm:^7.2.0" + tocbot: "npm:^4.20.1" + ts-dedent: "npm:^2.0.0" + util-deprecate: "npm:^1.0.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: f38233c935679345b4893d3d75b38ca8e74f3749b1f42a2356b61754bf1886cde8565546cdf53217335c8318506c56954aee7cc23c627b06f2d8c3b842d5d12b + languageName: node + linkType: hard + +"@storybook/builder-manager@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/builder-manager@npm:7.6.17" + dependencies: + "@fal-works/esbuild-plugin-global-externals": "npm:^2.1.2" + "@storybook/core-common": "npm:7.6.17" + "@storybook/manager": "npm:7.6.17" + "@storybook/node-logger": "npm:7.6.17" + "@types/ejs": "npm:^3.1.1" + "@types/find-cache-dir": "npm:^3.2.1" + "@yarnpkg/esbuild-plugin-pnp": "npm:^3.0.0-rc.10" + browser-assert: "npm:^1.2.1" + ejs: "npm:^3.1.8" + esbuild: "npm:^0.18.0" + esbuild-plugin-alias: "npm:^0.2.1" + express: "npm:^4.17.3" + find-cache-dir: "npm:^3.0.0" + fs-extra: "npm:^11.1.0" + process: "npm:^0.11.10" + util: "npm:^0.12.4" + checksum: 1b2ca77f7f3bf3c72890e949cfadc45d633fee7315ebcabfc1d6e23cd259db93114cbd9b9197597057f90c5fd60b3e72b0782a284a4f80c6efdd15f118b2c594 + languageName: node + linkType: hard + +"@storybook/builder-vite@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/builder-vite@npm:7.6.17" + dependencies: + "@storybook/channels": "npm:7.6.17" + "@storybook/client-logger": "npm:7.6.17" + "@storybook/core-common": "npm:7.6.17" + "@storybook/csf-plugin": "npm:7.6.17" + "@storybook/node-logger": "npm:7.6.17" + "@storybook/preview": "npm:7.6.17" + "@storybook/preview-api": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + "@types/find-cache-dir": "npm:^3.2.1" + browser-assert: "npm:^1.2.1" + es-module-lexer: "npm:^0.9.3" + express: "npm:^4.17.3" + find-cache-dir: "npm:^3.0.0" + fs-extra: "npm:^11.1.0" + magic-string: "npm:^0.30.0" + rollup: "npm:^2.25.0 || ^3.3.0" + peerDependencies: + "@preact/preset-vite": "*" + typescript: ">= 4.3.x" + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + vite-plugin-glimmerx: "*" + peerDependenciesMeta: + "@preact/preset-vite": + optional: true + typescript: + optional: true + vite-plugin-glimmerx: + optional: true + checksum: eaa70e474240efd44adfdc8e7f6f57c3c1daddc966c221da981a0191fad322d78b279e954e03f20369eaa8223d11267f0a101ed3e9c16a3f7096f76fafc7388e + languageName: node + linkType: hard + +"@storybook/channels@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/channels@npm:7.6.17" + dependencies: + "@storybook/client-logger": "npm:7.6.17" + "@storybook/core-events": "npm:7.6.17" + "@storybook/global": "npm:^5.0.0" + qs: "npm:^6.10.0" + telejson: "npm:^7.2.0" + tiny-invariant: "npm:^1.3.1" + checksum: 7109b67a60c656d22deb1b9b44bf0e26b565044de6ccf63589b0e52188931e2eaa11b78f7a0e1b59396f654537f79ac4264c715417d467aca602a6e80495f49e + languageName: node + linkType: hard + +"@storybook/channels@npm:7.6.6": + version: 7.6.6 + resolution: "@storybook/channels@npm:7.6.6" + dependencies: + "@storybook/client-logger": "npm:7.6.6" + "@storybook/core-events": "npm:7.6.6" + "@storybook/global": "npm:^5.0.0" + qs: "npm:^6.10.0" + telejson: "npm:^7.2.0" + tiny-invariant: "npm:^1.3.1" + checksum: 081666ebe90e1710ed1cd8eb0cae01ff1a307d448c83f83a51d4ff9d55fa54063460024f6d6464ffb0713be37471120a2d60a9981dfcd786cf6a628487c525c2 + languageName: node + linkType: hard + +"@storybook/cli@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/cli@npm:7.6.17" + dependencies: + "@babel/core": "npm:^7.23.2" + "@babel/preset-env": "npm:^7.23.2" + "@babel/types": "npm:^7.23.0" + "@ndelangen/get-tarball": "npm:^3.0.7" + "@storybook/codemod": "npm:7.6.17" + "@storybook/core-common": "npm:7.6.17" + "@storybook/core-events": "npm:7.6.17" + "@storybook/core-server": "npm:7.6.17" + "@storybook/csf-tools": "npm:7.6.17" + "@storybook/node-logger": "npm:7.6.17" + "@storybook/telemetry": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + "@types/semver": "npm:^7.3.4" + "@yarnpkg/fslib": "npm:2.10.3" + "@yarnpkg/libzip": "npm:2.3.0" + chalk: "npm:^4.1.0" + commander: "npm:^6.2.1" + cross-spawn: "npm:^7.0.3" + detect-indent: "npm:^6.1.0" + envinfo: "npm:^7.7.3" + execa: "npm:^5.0.0" + express: "npm:^4.17.3" + find-up: "npm:^5.0.0" + fs-extra: "npm:^11.1.0" + get-npm-tarball-url: "npm:^2.0.3" + get-port: "npm:^5.1.1" + giget: "npm:^1.0.0" + globby: "npm:^11.0.2" + jscodeshift: "npm:^0.15.1" + leven: "npm:^3.1.0" + ora: "npm:^5.4.1" + prettier: "npm:^2.8.0" + prompts: "npm:^2.4.0" + puppeteer-core: "npm:^2.1.1" + read-pkg-up: "npm:^7.0.1" + semver: "npm:^7.3.7" + strip-json-comments: "npm:^3.0.1" + tempy: "npm:^1.0.1" + ts-dedent: "npm:^2.0.0" + util-deprecate: "npm:^1.0.2" + bin: + getstorybook: ./bin/index.js + sb: ./bin/index.js + checksum: 8d8d426a1eca5d58a4cafa8418a1c8a41736e21a89c66307d18cea98c583976d672ae0773ab53e4e38f110dad2db788bd5d8daef3970ae14834db205818713ef + languageName: node + linkType: hard + +"@storybook/client-logger@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/client-logger@npm:7.6.17" + dependencies: + "@storybook/global": "npm:^5.0.0" + checksum: 77ebd176e65171b10b94f65ce7f10ed8c78e162b54462f5b87604f568e747f1604b4eb62ff7a601bf02d7e72b32e373fb980dd9c688a655706e74c025ebb82f3 + languageName: node + linkType: hard + +"@storybook/client-logger@npm:7.6.6": + version: 7.6.6 + resolution: "@storybook/client-logger@npm:7.6.6" + dependencies: + "@storybook/global": "npm:^5.0.0" + checksum: c7d1c8ef8d885c1b82b27a7ef45d75b33cb5a8805dc978240b82e4d319165e690a28b296fb9d364a0450be2fad478e49e99b898294c520082f7f9890dda8f1a6 + languageName: node + linkType: hard + +"@storybook/codemod@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/codemod@npm:7.6.17" + dependencies: + "@babel/core": "npm:^7.23.2" + "@babel/preset-env": "npm:^7.23.2" + "@babel/types": "npm:^7.23.0" + "@storybook/csf": "npm:^0.1.2" + "@storybook/csf-tools": "npm:7.6.17" + "@storybook/node-logger": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + "@types/cross-spawn": "npm:^6.0.2" + cross-spawn: "npm:^7.0.3" + globby: "npm:^11.0.2" + jscodeshift: "npm:^0.15.1" + lodash: "npm:^4.17.21" + prettier: "npm:^2.8.0" + recast: "npm:^0.23.1" + checksum: b8428203dfa551ea34b34659e5231cdc03eeb0fba2c53f801794b732515b173131bbe3df14dff9a540c18d3dfdafa7f94d11dbf34bf4dbaf03a47dd7c80d09ae + languageName: node + linkType: hard + +"@storybook/components@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/components@npm:7.6.17" + dependencies: + "@radix-ui/react-select": "npm:^1.2.2" + "@radix-ui/react-toolbar": "npm:^1.0.4" + "@storybook/client-logger": "npm:7.6.17" + "@storybook/csf": "npm:^0.1.2" + "@storybook/global": "npm:^5.0.0" + "@storybook/theming": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + memoizerific: "npm:^1.11.3" + use-resize-observer: "npm:^9.1.0" + util-deprecate: "npm:^1.0.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 199421d7668a3afcce9375c567443704778b4288bed16a39f02e5c1aaa9892b4ffba829b47d5a3fa8328521f6e0c26e5e7e7beed898cc0f8f835a99ec8f125a6 + languageName: node + linkType: hard + +"@storybook/core-client@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/core-client@npm:7.6.17" + dependencies: + "@storybook/client-logger": "npm:7.6.17" + "@storybook/preview-api": "npm:7.6.17" + checksum: 3342367bce219d46ac0c5b494688ae86aeb5c4006d98749dec2e30518850bc76a8b255611e9151f043d5141d11deb781b972c8610e98565cab4112dc86b7c1d5 + languageName: node + linkType: hard + +"@storybook/core-common@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/core-common@npm:7.6.17" + dependencies: + "@storybook/core-events": "npm:7.6.17" + "@storybook/node-logger": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + "@types/find-cache-dir": "npm:^3.2.1" + "@types/node": "npm:^18.0.0" + "@types/node-fetch": "npm:^2.6.4" + "@types/pretty-hrtime": "npm:^1.0.0" + chalk: "npm:^4.1.0" + esbuild: "npm:^0.18.0" + esbuild-register: "npm:^3.5.0" + file-system-cache: "npm:2.3.0" + find-cache-dir: "npm:^3.0.0" + find-up: "npm:^5.0.0" + fs-extra: "npm:^11.1.0" + glob: "npm:^10.0.0" + handlebars: "npm:^4.7.7" + lazy-universal-dotenv: "npm:^4.0.0" + node-fetch: "npm:^2.0.0" + picomatch: "npm:^2.3.0" + pkg-dir: "npm:^5.0.0" + pretty-hrtime: "npm:^1.0.3" + resolve-from: "npm:^5.0.0" + ts-dedent: "npm:^2.0.0" + checksum: 5be46d8f2d97dcde4a45de688278baed78185b44895825fe2f9423b70410fa88214a9709f40e7656cebe218a2c57cfa9979228e9f2b522eb47cf5af825d1133d + languageName: node + linkType: hard + +"@storybook/core-common@npm:7.6.6": + version: 7.6.6 + resolution: "@storybook/core-common@npm:7.6.6" + dependencies: + "@storybook/core-events": "npm:7.6.6" + "@storybook/node-logger": "npm:7.6.6" + "@storybook/types": "npm:7.6.6" + "@types/find-cache-dir": "npm:^3.2.1" + "@types/node": "npm:^18.0.0" + "@types/node-fetch": "npm:^2.6.4" + "@types/pretty-hrtime": "npm:^1.0.0" + chalk: "npm:^4.1.0" + esbuild: "npm:^0.18.0" + esbuild-register: "npm:^3.5.0" + file-system-cache: "npm:2.3.0" + find-cache-dir: "npm:^3.0.0" + find-up: "npm:^5.0.0" + fs-extra: "npm:^11.1.0" + glob: "npm:^10.0.0" + handlebars: "npm:^4.7.7" + lazy-universal-dotenv: "npm:^4.0.0" + node-fetch: "npm:^2.0.0" + picomatch: "npm:^2.3.0" + pkg-dir: "npm:^5.0.0" + pretty-hrtime: "npm:^1.0.3" + resolve-from: "npm:^5.0.0" + ts-dedent: "npm:^2.0.0" + checksum: 19c7eefc93d8884f204cf7b2a8f232de531783789d55f243de2a54f5813fc7eee9f93aa30d36434b05579ad6e812c7fb99c2fdf8cd58c368761d91fa4031d8a8 + languageName: node + linkType: hard + +"@storybook/core-events@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/core-events@npm:7.6.17" + dependencies: + ts-dedent: "npm:^2.0.0" + checksum: ab6410da3a456a61138b4a760a28b74bb9dc6f4c81de0d5ff7760b1853c6a437f8a0d05301c291f45503575d60c3be4805db4178f649eccd32c5ffd98a790250 + languageName: node + linkType: hard + +"@storybook/core-events@npm:7.6.6": + version: 7.6.6 + resolution: "@storybook/core-events@npm:7.6.6" + dependencies: + ts-dedent: "npm:^2.0.0" + checksum: 5d43c14374015bbf653009bb0fcc99690ace861af6130074c38bf2e2baaf8415ab9381261f1d058e4890a6151d827df00e4a59b9d593ecb06cca7b0af0cd7abe + languageName: node + linkType: hard + +"@storybook/core-server@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/core-server@npm:7.6.17" + dependencies: + "@aw-web-design/x-default-browser": "npm:1.4.126" + "@discoveryjs/json-ext": "npm:^0.5.3" + "@storybook/builder-manager": "npm:7.6.17" + "@storybook/channels": "npm:7.6.17" + "@storybook/core-common": "npm:7.6.17" + "@storybook/core-events": "npm:7.6.17" + "@storybook/csf": "npm:^0.1.2" + "@storybook/csf-tools": "npm:7.6.17" + "@storybook/docs-mdx": "npm:^0.1.0" + "@storybook/global": "npm:^5.0.0" + "@storybook/manager": "npm:7.6.17" + "@storybook/node-logger": "npm:7.6.17" + "@storybook/preview-api": "npm:7.6.17" + "@storybook/telemetry": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + "@types/detect-port": "npm:^1.3.0" + "@types/node": "npm:^18.0.0" + "@types/pretty-hrtime": "npm:^1.0.0" + "@types/semver": "npm:^7.3.4" + better-opn: "npm:^3.0.2" + chalk: "npm:^4.1.0" + cli-table3: "npm:^0.6.1" + compression: "npm:^1.7.4" + detect-port: "npm:^1.3.0" + express: "npm:^4.17.3" + fs-extra: "npm:^11.1.0" + globby: "npm:^11.0.2" + ip: "npm:^2.0.1" + lodash: "npm:^4.17.21" + open: "npm:^8.4.0" + pretty-hrtime: "npm:^1.0.3" + prompts: "npm:^2.4.0" + read-pkg-up: "npm:^7.0.1" + semver: "npm:^7.3.7" + telejson: "npm:^7.2.0" + tiny-invariant: "npm:^1.3.1" + ts-dedent: "npm:^2.0.0" + util: "npm:^0.12.4" + util-deprecate: "npm:^1.0.2" + watchpack: "npm:^2.2.0" + ws: "npm:^8.2.3" + checksum: b56077bea18c22151adb72c96efb1717034314b08bba5cae12b1f8a0e4135773f5c1e334ad3523dfeb578078b2d41a6091e2b0a992a110ca1859fdd89b1a4702 + languageName: node + linkType: hard + +"@storybook/csf-plugin@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/csf-plugin@npm:7.6.17" + dependencies: + "@storybook/csf-tools": "npm:7.6.17" + unplugin: "npm:^1.3.1" + checksum: 720ecbd2e845f6d6d575b8fb5b05a085ddba1eb486318a9b7d6f2ea6646fe3e62d7c9589e18aab15ce0a715c653c9d24b2e0f38117e92845e636f0410a85f76d + languageName: node + linkType: hard + +"@storybook/csf-tools@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/csf-tools@npm:7.6.17" + dependencies: + "@babel/generator": "npm:^7.23.0" + "@babel/parser": "npm:^7.23.0" + "@babel/traverse": "npm:^7.23.2" + "@babel/types": "npm:^7.23.0" + "@storybook/csf": "npm:^0.1.2" + "@storybook/types": "npm:7.6.17" + fs-extra: "npm:^11.1.0" + recast: "npm:^0.23.1" + ts-dedent: "npm:^2.0.0" + checksum: 827458c97de27127a026d6f4592ad8760f27b69dc1082251710b8067b0616bf2c6b9c13b12cbf12a8162a6528d92ca81839cf78d0d10d09978d3ccdedaca7bce + languageName: node + linkType: hard + +"@storybook/csf-tools@npm:7.6.6": + version: 7.6.6 + resolution: "@storybook/csf-tools@npm:7.6.6" + dependencies: + "@babel/generator": "npm:^7.23.0" + "@babel/parser": "npm:^7.23.0" + "@babel/traverse": "npm:^7.23.2" + "@babel/types": "npm:^7.23.0" + "@storybook/csf": "npm:^0.1.2" + "@storybook/types": "npm:7.6.6" + fs-extra: "npm:^11.1.0" + recast: "npm:^0.23.1" + ts-dedent: "npm:^2.0.0" + checksum: 8a94ca5eb2fa06e81434d4aff61b2002765a332764690ae29a571358910ccd9d46e5464b980b70162fe420975fdaf68ebc5008fa3caa145bb87bc5008f935388 + languageName: node + linkType: hard + +"@storybook/csf@npm:^0.1.2": + version: 0.1.2 + resolution: "@storybook/csf@npm:0.1.2" + dependencies: + type-fest: "npm:^2.19.0" + checksum: b51a55292e5d2af8b1d135a28ecaa94f8860ddfedcb393adfa2cca1ee23853156066f737d8be1cb5412f572781aa525dc0b2f6e4a6f6ce805489f0149efe837c + languageName: node + linkType: hard + +"@storybook/docs-mdx@npm:^0.1.0": + version: 0.1.0 + resolution: "@storybook/docs-mdx@npm:0.1.0" + checksum: e4d510f0452a7a3cb09d9617920c18b974f836299dfba38d6b2e62fbfea418d71f340b6c280a87201b1336a7221c7cc16b47794c1f8e81d01dcfa1f599343085 + languageName: node + linkType: hard + +"@storybook/docs-tools@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/docs-tools@npm:7.6.17" + dependencies: + "@storybook/core-common": "npm:7.6.17" + "@storybook/preview-api": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + "@types/doctrine": "npm:^0.0.3" + assert: "npm:^2.1.0" + doctrine: "npm:^3.0.0" + lodash: "npm:^4.17.21" + checksum: 38473d0ce609cee38df5a8f3ad34a23ce6050e06b492cab51052ba67a2c6ecece532e0dee9f5e3cc5dee3d7105233289d05465a7ae0f5cb94fd2bbda1c267d38 + languageName: node + linkType: hard + +"@storybook/global@npm:^5.0.0": + version: 5.0.0 + resolution: "@storybook/global@npm:5.0.0" + checksum: 8f1b61dcdd3a89584540896e659af2ecc700bc740c16909a7be24ac19127ea213324de144a141f7caf8affaed017d064fea0618d453afbe027cf60f54b4a6d0b + languageName: node + linkType: hard + +"@storybook/manager-api@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/manager-api@npm:7.6.17" + dependencies: + "@storybook/channels": "npm:7.6.17" + "@storybook/client-logger": "npm:7.6.17" + "@storybook/core-events": "npm:7.6.17" + "@storybook/csf": "npm:^0.1.2" + "@storybook/global": "npm:^5.0.0" + "@storybook/router": "npm:7.6.17" + "@storybook/theming": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + dequal: "npm:^2.0.2" + lodash: "npm:^4.17.21" + memoizerific: "npm:^1.11.3" + store2: "npm:^2.14.2" + telejson: "npm:^7.2.0" + ts-dedent: "npm:^2.0.0" + checksum: 475d0e0d37a72087c6b4f4e0bfe6ad648c27b5ea34951580b2e339f883d697ac7c4d99926db544a7c58b0aba959ad2d70129d7a7cee4bafaccd3810329a51e03 + languageName: node + linkType: hard + +"@storybook/manager@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/manager@npm:7.6.17" + checksum: e703466e95b0fca58963ac0abec188164e6bce904471171dd360c0d63ead0183a5b242db034af63157acd42d38348984e5fe4e6414af6190234c4d5d41608cee + languageName: node + linkType: hard + +"@storybook/mdx2-csf@npm:^1.0.0": + version: 1.1.0 + resolution: "@storybook/mdx2-csf@npm:1.1.0" + checksum: ba4496a51efae35edb3e509e488cd16066ccf0768d2dc527bbc2650d0bc0f630540985205772d63d1711d1a5dae66136a919077c90fa2ac7a02a13de43446baa + languageName: node + linkType: hard + +"@storybook/node-logger@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/node-logger@npm:7.6.17" + checksum: 7b91f10812b8ea4e8716c3b133c5a78ac419e6bcd6a6ab80117cee25287aa973c1710a74a882238697499a1eca6521c4171f4f2d2e8651fb8ef6e28b7ee167fe + languageName: node + linkType: hard + +"@storybook/node-logger@npm:7.6.6": + version: 7.6.6 + resolution: "@storybook/node-logger@npm:7.6.6" + checksum: 2f6929c9260d2523953aa63faec5dc477a37bf18f5d0c20aff7711f4d39e428eb27c80a4a982a5842759d1531219e41a00635b2219fae999d8ec11354eab3aee + languageName: node + linkType: hard + +"@storybook/postinstall@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/postinstall@npm:7.6.17" + checksum: 62038e1feacfa5b9acc85afd1cdcbee3c9d780c8dbb6d2eb8cf7bfbb6a14d989fa61351958f512415761d5190075367f1f3641e104c0cec0a2c8dd056617dea6 + languageName: node + linkType: hard + +"@storybook/preview-api@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/preview-api@npm:7.6.17" + dependencies: + "@storybook/channels": "npm:7.6.17" + "@storybook/client-logger": "npm:7.6.17" + "@storybook/core-events": "npm:7.6.17" + "@storybook/csf": "npm:^0.1.2" + "@storybook/global": "npm:^5.0.0" + "@storybook/types": "npm:7.6.17" + "@types/qs": "npm:^6.9.5" + dequal: "npm:^2.0.2" + lodash: "npm:^4.17.21" + memoizerific: "npm:^1.11.3" + qs: "npm:^6.10.0" + synchronous-promise: "npm:^2.0.15" + ts-dedent: "npm:^2.0.0" + util-deprecate: "npm:^1.0.2" + checksum: b4357ee0c1f9b05feee051d0c0ed3343972277f12d9d033fcc59acfb18d336cecc4a5f0b23998011af4a92c8126e785b2931dbdbdf79787aac5756a01c32aee0 + languageName: node + linkType: hard + +"@storybook/preview@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/preview@npm:7.6.17" + checksum: b4a2394c4622ff7291ba1b161d537902c53ed52ae3511c65e10c934b04463f6e7e55487b88889800acab55ea1c0aa33ea2a207786f3e06eda4617787f859da6b + languageName: node + linkType: hard + +"@storybook/react-dom-shim@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/react-dom-shim@npm:7.6.17" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 20558c58f9f0a3a00c5a1bbf2aa3517e3d318e6528f503129c99fb9ee4b604a225e79725f67e01e6e99d5d8c7db0614575dcc89af7768381afe59c976cb7cfc0 + languageName: node + linkType: hard + +"@storybook/react-vite@npm:^7.6.17": + version: 7.6.17 + resolution: "@storybook/react-vite@npm:7.6.17" + dependencies: + "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.3.0" + "@rollup/pluginutils": "npm:^5.0.2" + "@storybook/builder-vite": "npm:7.6.17" + "@storybook/react": "npm:7.6.17" + "@vitejs/plugin-react": "npm:^3.0.1" + magic-string: "npm:^0.30.0" + react-docgen: "npm:^7.0.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + checksum: 2b45d09f17421d102b7599c55495b9c1688012f9761492493abf55dcfe8c23d65a4465ed6d5f96bb8e41475bbca103f4e0a285f65df85e17f8e82dce673b77dc + languageName: node + linkType: hard + +"@storybook/react@npm:7.6.17, @storybook/react@npm:^7.6.17": + version: 7.6.17 + resolution: "@storybook/react@npm:7.6.17" + dependencies: + "@storybook/client-logger": "npm:7.6.17" + "@storybook/core-client": "npm:7.6.17" + "@storybook/docs-tools": "npm:7.6.17" + "@storybook/global": "npm:^5.0.0" + "@storybook/preview-api": "npm:7.6.17" + "@storybook/react-dom-shim": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + "@types/escodegen": "npm:^0.0.6" + "@types/estree": "npm:^0.0.51" + "@types/node": "npm:^18.0.0" + acorn: "npm:^7.4.1" + acorn-jsx: "npm:^5.3.1" + acorn-walk: "npm:^7.2.0" + escodegen: "npm:^2.1.0" + html-tags: "npm:^3.1.0" + lodash: "npm:^4.17.21" + prop-types: "npm:^15.7.2" + react-element-to-jsx-string: "npm:^15.0.0" + ts-dedent: "npm:^2.0.0" + type-fest: "npm:~2.19" + util-deprecate: "npm:^1.0.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + typescript: "*" + peerDependenciesMeta: + typescript: + optional: true + checksum: 747bb48413865701716652b9587c8c5b07cc51cb1d54125a69a4ec355f24fdcfc3a9d925a0b6268786875e97addf435e10efe737450e50eea1d19408049674e6 + languageName: node + linkType: hard + +"@storybook/router@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/router@npm:7.6.17" + dependencies: + "@storybook/client-logger": "npm:7.6.17" + memoizerific: "npm:^1.11.3" + qs: "npm:^6.10.0" + checksum: 8e5f354bd835319ca3c7f3ea8248914e7c22dee5815b1bdcbdbf6a9dc018f608683e482013767004105bc726d42c71f001a6c8d10c2177a511e6c0e093b7cf2d + languageName: node + linkType: hard + +"@storybook/telemetry@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/telemetry@npm:7.6.17" + dependencies: + "@storybook/client-logger": "npm:7.6.17" + "@storybook/core-common": "npm:7.6.17" + "@storybook/csf-tools": "npm:7.6.17" + chalk: "npm:^4.1.0" + detect-package-manager: "npm:^2.0.1" + fetch-retry: "npm:^5.0.2" + fs-extra: "npm:^11.1.0" + read-pkg-up: "npm:^7.0.1" + checksum: 2d13afef0fd73982c1efec1598583ed592bd608bbc61f9c4d96c47be9202d80043041764e00ea3b10b0636417cfbfe7b3d13c6898187a09554c8a696f89ac226 + languageName: node + linkType: hard + +"@storybook/telemetry@npm:^7.1.0": + version: 7.6.6 + resolution: "@storybook/telemetry@npm:7.6.6" + dependencies: + "@storybook/client-logger": "npm:7.6.6" + "@storybook/core-common": "npm:7.6.6" + "@storybook/csf-tools": "npm:7.6.6" + chalk: "npm:^4.1.0" + detect-package-manager: "npm:^2.0.1" + fetch-retry: "npm:^5.0.2" + fs-extra: "npm:^11.1.0" + read-pkg-up: "npm:^7.0.1" + checksum: b9e55909ab06a14f7836ff33a0e12a4531a0ae8770a149a15fbc59d5ed4ce73c77c9b5ac37828f863a4dc7821cadb5466a0c907e7215f2b6f6e4b6733be6fc1f + languageName: node + linkType: hard + +"@storybook/testing-library@npm:^0.2.2": + version: 0.2.2 + resolution: "@storybook/testing-library@npm:0.2.2" + dependencies: + "@testing-library/dom": "npm:^9.0.0" + "@testing-library/user-event": "npm:^14.4.0" + ts-dedent: "npm:^2.2.0" + checksum: 3179c74148c92267ea449068ce9fb00bf960dbf06654354de7869428415d16dc730a0d58b5adca7619d21e5a058ae0bf713e34c09be8bca574388ec0106c5068 + languageName: node + linkType: hard + +"@storybook/theming@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/theming@npm:7.6.17" + dependencies: + "@emotion/use-insertion-effect-with-fallbacks": "npm:^1.0.0" + "@storybook/client-logger": "npm:7.6.17" + "@storybook/global": "npm:^5.0.0" + memoizerific: "npm:^1.11.3" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: f18c52b236554056a97d9df23c5ecf186ffe2ef22eae3812a961b5d9beff96c2a05134ce2a39ad246c2b4ae0d5904a4e7148f7eb3d38d9c7b676d6d0a6c30595 + languageName: node + linkType: hard + +"@storybook/types@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/types@npm:7.6.17" + dependencies: + "@storybook/channels": "npm:7.6.17" + "@types/babel__core": "npm:^7.0.0" + "@types/express": "npm:^4.7.0" + file-system-cache: "npm:2.3.0" + checksum: 7de04987b44b2d78d9e6ff39b54ece657b1d5266cc180a6b1a192ab394f893f8352578d9c8d0d2327e21689843a1c314f08e05eec18992d78a8d9347b0bcc72a + languageName: node + linkType: hard + +"@storybook/types@npm:7.6.6": + version: 7.6.6 + resolution: "@storybook/types@npm:7.6.6" + dependencies: + "@storybook/channels": "npm:7.6.6" + "@types/babel__core": "npm:^7.0.0" + "@types/express": "npm:^4.7.0" + file-system-cache: "npm:2.3.0" + checksum: e0f657336ad9d554715a362119e550707129611ee31809b3d5a081d20830f331cf40bdf1471d667d7824f17ae2cd34f75b69dca8c2e443b09266d228d7937f2a + languageName: node + linkType: hard + +"@testing-library/dom@npm:^9.0.0": + version: 9.3.3 + resolution: "@testing-library/dom@npm:9.3.3" + dependencies: + "@babel/code-frame": "npm:^7.10.4" + "@babel/runtime": "npm:^7.12.5" + "@types/aria-query": "npm:^5.0.1" + aria-query: "npm:5.1.3" + chalk: "npm:^4.1.0" + dom-accessibility-api: "npm:^0.5.9" + lz-string: "npm:^1.5.0" + pretty-format: "npm:^27.0.2" + checksum: c3bbd67503634fd955233dc172531640656701fe35ecb9a83f85e5965874b786452f5e7c26b4f8b3b4fc4379f3a80193c74425b57843ba191f4845e22b0ac483 + languageName: node + linkType: hard + +"@testing-library/user-event@npm:^14.4.0": + version: 14.5.1 + resolution: "@testing-library/user-event@npm:14.5.1" + peerDependencies: + "@testing-library/dom": ">=7.21.4" + checksum: 1e00d6ead23377885b906db6e46e259161a0efb4138f7527481d7435f3c8f65cb7e3eab2900e2ac1886fa6dd03416e773a3a60dea87a9a2086a7127dee315f6f + languageName: node + linkType: hard + +"@trysound/sax@npm:0.2.0": + version: 0.2.0 + resolution: "@trysound/sax@npm:0.2.0" + checksum: 44907308549ce775a41c38a815f747009ac45929a45d642b836aa6b0a536e4978d30b8d7d680bbd116e9dd73b7dbe2ef0d1369dcfc2d09e83ba381e485ecbe12 + languageName: node + linkType: hard + +"@types/aria-query@npm:^5.0.1": + version: 5.0.4 + resolution: "@types/aria-query@npm:5.0.4" + checksum: dc667bc6a3acc7bba2bccf8c23d56cb1f2f4defaa704cfef595437107efaa972d3b3db9ec1d66bc2711bfc35086821edd32c302bffab36f2e79b97f312069f08 + languageName: node + linkType: hard + +"@types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.18.0, @types/babel__core@npm:^7.20.4": + version: 7.20.5 + resolution: "@types/babel__core@npm:7.20.5" + dependencies: + "@babel/parser": "npm:^7.20.7" + "@babel/types": "npm:^7.20.7" + "@types/babel__generator": "npm:*" + "@types/babel__template": "npm:*" + "@types/babel__traverse": "npm:*" + checksum: bdee3bb69951e833a4b811b8ee9356b69a61ed5b7a23e1a081ec9249769117fa83aaaf023bb06562a038eb5845155ff663e2d5c75dd95c1d5ccc91db012868ff + languageName: node + linkType: hard + +"@types/babel__generator@npm:*": + version: 7.6.7 + resolution: "@types/babel__generator@npm:7.6.7" + dependencies: + "@babel/types": "npm:^7.0.0" + checksum: 2427203864ef231857e102eeb32b731a419164863983119cdd4dac9f1503c2831eb4262d05ade95d4574aa410b94c16e54e36a616758452f685a34881f4596d9 + languageName: node + linkType: hard + +"@types/babel__template@npm:*": + version: 7.4.4 + resolution: "@types/babel__template@npm:7.4.4" + dependencies: + "@babel/parser": "npm:^7.1.0" + "@babel/types": "npm:^7.0.0" + checksum: cc84f6c6ab1eab1427e90dd2b76ccee65ce940b778a9a67be2c8c39e1994e6f5bbc8efa309f6cea8dc6754994524cd4d2896558df76d92e7a1f46ecffee7112b + languageName: node + linkType: hard + +"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.18.0": + version: 7.20.4 + resolution: "@types/babel__traverse@npm:7.20.4" + dependencies: + "@babel/types": "npm:^7.20.7" + checksum: e76cb4974c7740fd61311152dc497e7b05c1c46ba554aab875544ab0a7457f343cafcad34ba8fb2ff543ab0e012ef2d3fa0c13f1a4e9a4cd9c4c703c7a2a8d62 + languageName: node + linkType: hard + +"@types/body-parser@npm:*": + version: 1.19.5 + resolution: "@types/body-parser@npm:1.19.5" + dependencies: + "@types/connect": "npm:*" + "@types/node": "npm:*" + checksum: aebeb200f25e8818d8cf39cd0209026750d77c9b85381cdd8deeb50913e4d18a1ebe4b74ca9b0b4d21952511eeaba5e9fbbf739b52731a2061e206ec60d568df + languageName: node + linkType: hard + +"@types/connect@npm:*": + version: 3.4.38 + resolution: "@types/connect@npm:3.4.38" + dependencies: + "@types/node": "npm:*" + checksum: 2e1cdba2c410f25649e77856505cd60223250fa12dff7a503e492208dbfdd25f62859918f28aba95315251fd1f5e1ffbfca1e25e73037189ab85dd3f8d0a148c + languageName: node + linkType: hard + +"@types/cross-spawn@npm:^6.0.2": + version: 6.0.6 + resolution: "@types/cross-spawn@npm:6.0.6" + dependencies: + "@types/node": "npm:*" + checksum: e3d476bb6b3a54a8934a97fe6ee4bd13e2e5eb29073929a4be76a52466602ffaea420b20774ffe8503f9fa24f3ae34817e95e7f625689fb0d1c10404f5b2889c + languageName: node + linkType: hard + +"@types/detect-port@npm:^1.3.0": + version: 1.3.5 + resolution: "@types/detect-port@npm:1.3.5" + checksum: d8dd9d0e643106a2263f530b24ffdc3409d9391c50fc5e404018ba3633947aa3777db7fb094aeb0f49a13cc998aae8889747ad9edaa02b13a2de2385f37106ef + languageName: node + linkType: hard + +"@types/doctrine@npm:^0.0.3": + version: 0.0.3 + resolution: "@types/doctrine@npm:0.0.3" + checksum: 566dcdc988c97ff01d14493ceb2223643347f07cf0a88c86cd7cb7c2821cfc837fd39295e6809a29614fdfdc6c4e981408155ca909b2e5da5d947af939b6c966 + languageName: node + linkType: hard + +"@types/doctrine@npm:^0.0.9": + version: 0.0.9 + resolution: "@types/doctrine@npm:0.0.9" + checksum: cdaca493f13c321cf0cacd1973efc0ae74569633145d9e6fc1128f32217a6968c33bea1f858275239fe90c98f3be57ec8f452b416a9ff48b8e8c1098b20fa51c + languageName: node + linkType: hard + +"@types/ejs@npm:^3.1.1": + version: 3.1.5 + resolution: "@types/ejs@npm:3.1.5" + checksum: 13d994cf0323d7e0ad33b9384914ccd3b4cd8bf282eced3649b1621b66ee7c784ac2d120a9d7b1f43d6f873518248fb8c3221b06a649b847860b9c2389a0b0ed + languageName: node + linkType: hard + +"@types/emscripten@npm:^1.39.6": + version: 1.39.10 + resolution: "@types/emscripten@npm:1.39.10" + checksum: c9adde9307d54efb5152931bfe99966fbe12fbd4d07663fb5cdc4cc1bd3a1f030882d50d4a27875b7b2d9713d160609e67b72e92177a021c9f4699ee5ac41035 + languageName: node + linkType: hard + +"@types/escodegen@npm:^0.0.6": + version: 0.0.6 + resolution: "@types/escodegen@npm:0.0.6" + checksum: bbef189319c7b0386486bc7224369f118c7aedf35cc13e40ae5879b9ab4f848936f31e8eea50e71d4de72d4b7a77d9e6e9e5ceec4406c648fbc0077ede634ed5 + languageName: node + linkType: hard + +"@types/estree@npm:^0.0.51": + version: 0.0.51 + resolution: "@types/estree@npm:0.0.51" + checksum: a70c60d5e634e752fcd45b58c9c046ef22ad59ede4bc93ad5193c7e3b736ebd6bcd788ade59d9c3b7da6eeb0939235f011d4c59bb4fc04d8c346b76035099dd1 + languageName: node + linkType: hard + +"@types/estree@npm:^1.0.0": + version: 1.0.5 + resolution: "@types/estree@npm:1.0.5" + checksum: b3b0e334288ddb407c7b3357ca67dbee75ee22db242ca7c56fe27db4e1a31989cb8af48a84dd401deb787fe10cc6b2ab1ee82dc4783be87ededbe3d53c79c70d + languageName: node + linkType: hard + +"@types/express-serve-static-core@npm:^4.17.33": + version: 4.17.41 + resolution: "@types/express-serve-static-core@npm:4.17.41" + dependencies: + "@types/node": "npm:*" + "@types/qs": "npm:*" + "@types/range-parser": "npm:*" + "@types/send": "npm:*" + checksum: dc166cbf4475c00a81fbcab120bf7477c527184be11ae149df7f26d9c1082114c68f8d387a2926fe80291b06477c8bbd9231ff4f5775de328e887695aefce269 + languageName: node + linkType: hard + +"@types/express@npm:^4.7.0": + version: 4.17.21 + resolution: "@types/express@npm:4.17.21" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^4.17.33" + "@types/qs": "npm:*" + "@types/serve-static": "npm:*" + checksum: 12e562c4571da50c7d239e117e688dc434db1bac8be55613294762f84fd77fbd0658ccd553c7d3ab02408f385bc93980992369dd30e2ecd2c68c358e6af8fabf + languageName: node + linkType: hard + +"@types/find-cache-dir@npm:^3.2.1": + version: 3.2.1 + resolution: "@types/find-cache-dir@npm:3.2.1" + checksum: 68059aec88ef776a689c1711a881fd91a9ce1b03dd5898ea1d2ac5d77d7b0235f21fdf210f380c13deca8b45e4499841a63aaf31fd2123af687f2c6b472f41ce + languageName: node + linkType: hard + +"@types/glob@npm:^7.1.3": + version: 7.2.0 + resolution: "@types/glob@npm:7.2.0" + dependencies: + "@types/minimatch": "npm:*" + "@types/node": "npm:*" + checksum: a8eb5d5cb5c48fc58c7ca3ff1e1ddf771ee07ca5043da6e4871e6757b4472e2e73b4cfef2644c38983174a4bc728c73f8da02845c28a1212f98cabd293ecae98 + languageName: node + linkType: hard + +"@types/graceful-fs@npm:^4.1.3": + version: 4.1.9 + resolution: "@types/graceful-fs@npm:4.1.9" + dependencies: + "@types/node": "npm:*" + checksum: 235d2fc69741448e853333b7c3d1180a966dd2b8972c8cbcd6b2a0c6cd7f8d582ab2b8e58219dbc62cce8f1b40aa317ff78ea2201cdd8249da5025adebed6f0b + languageName: node + linkType: hard + +"@types/http-errors@npm:*": + version: 2.0.4 + resolution: "@types/http-errors@npm:2.0.4" + checksum: 494670a57ad4062fee6c575047ad5782506dd35a6b9ed3894cea65830a94367bd84ba302eb3dde331871f6d70ca287bfedb1b2cf658e6132cd2cbd427ab56836 + languageName: node + linkType: hard + +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0": + version: 2.0.6 + resolution: "@types/istanbul-lib-coverage@npm:2.0.6" + checksum: 3948088654f3eeb45363f1db158354fb013b362dba2a5c2c18c559484d5eb9f6fd85b23d66c0a7c2fcfab7308d0a585b14dadaca6cc8bf89ebfdc7f8f5102fb7 + languageName: node + linkType: hard + +"@types/istanbul-lib-report@npm:*": + version: 3.0.3 + resolution: "@types/istanbul-lib-report@npm:3.0.3" + dependencies: + "@types/istanbul-lib-coverage": "npm:*" + checksum: 247e477bbc1a77248f3c6de5dadaae85ff86ac2d76c5fc6ab1776f54512a745ff2a5f791d22b942e3990ddbd40f3ef5289317c4fca5741bedfaa4f01df89051c + languageName: node + linkType: hard + +"@types/istanbul-reports@npm:^3.0.0": + version: 3.0.4 + resolution: "@types/istanbul-reports@npm:3.0.4" + dependencies: + "@types/istanbul-lib-report": "npm:*" + checksum: 1647fd402aced5b6edac87274af14ebd6b3a85447ef9ad11853a70fd92a98d35f81a5d3ea9fcb5dbb5834e800c6e35b64475e33fcae6bfa9acc70d61497c54ee + languageName: node + linkType: hard + +"@types/lodash@npm:^4.14.167": + version: 4.14.202 + resolution: "@types/lodash@npm:4.14.202" + checksum: 6064d43c8f454170841bd67c8266cc9069d9e570a72ca63f06bceb484cb4a3ee60c9c1f305c1b9e3a87826049fd41124b8ef265c4dd08b00f6766609c7fe9973 + languageName: node + linkType: hard + +"@types/mdx@npm:^2.0.0": + version: 2.0.10 + resolution: "@types/mdx@npm:2.0.10" + checksum: a2a5d71967c44c650e883eaaeb61db9c0758b9c1d675e04b7a3cfeeaee6efd5044dc9c78d780aa3fe408a2f85680bf3b723c92a1772bb6c2da35ef346d766de2 + languageName: node + linkType: hard + +"@types/mime-types@npm:^2.1.0": + version: 2.1.4 + resolution: "@types/mime-types@npm:2.1.4" + checksum: a10d57881d14a053556b3d09292de467968d965b0a06d06732c748da39b3aa569270b5b9f32529fd0e9ac1e5f3b91abb894f5b1996373254a65cb87903c86622 + languageName: node + linkType: hard + +"@types/mime@npm:*": + version: 3.0.4 + resolution: "@types/mime@npm:3.0.4" + checksum: db478bc0f99e40f7b3e01d356a9bdf7817060808a294978111340317bcd80ca35382855578c5b60fbc84ae449674bd9bb38427b18417e1f8f19e4f72f8b242cd + languageName: node + linkType: hard + +"@types/mime@npm:^1": + version: 1.3.5 + resolution: "@types/mime@npm:1.3.5" + checksum: c2ee31cd9b993804df33a694d5aa3fa536511a49f2e06eeab0b484fef59b4483777dbb9e42a4198a0809ffbf698081fdbca1e5c2218b82b91603dfab10a10fbc + languageName: node + linkType: hard + +"@types/minimatch@npm:*": + version: 5.1.2 + resolution: "@types/minimatch@npm:5.1.2" + checksum: 83cf1c11748891b714e129de0585af4c55dd4c2cafb1f1d5233d79246e5e1e19d1b5ad9e8db449667b3ffa2b6c80125c429dbee1054e9efb45758dbc4e118562 + languageName: node + linkType: hard + +"@types/node-fetch@npm:^2.6.4": + version: 2.6.9 + resolution: "@types/node-fetch@npm:2.6.9" + dependencies: + "@types/node": "npm:*" + form-data: "npm:^4.0.0" + checksum: b15b6d518ea4dd4a21cf328e9df0a88b2e5b76f3455ddfeb9063a3b97087c50b15ab195a869dadbbeb09d08dcc915557fb6a4f72b4fe79ee42e215fce3d9b0db + languageName: node + linkType: hard + +"@types/node@npm:*": + version: 20.10.0 + resolution: "@types/node@npm:20.10.0" + dependencies: + undici-types: "npm:~5.26.4" + checksum: f379e57d9d28cb5f3d8eab943de0c54a0ca2f95ee356e1fe2a1a4fa718b740103ae522c50ce107cffd52c3642ef3244cfc55bf5369081dd6c48369c8587b21ae + languageName: node + linkType: hard + +"@types/node@npm:^18.0.0": + version: 18.18.13 + resolution: "@types/node@npm:18.18.13" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 5f1840f26b4c00e6b4945be678644a46e6689ef10d9d7795d587b76059045b99a14ca6075264296e6e91d73e098fe83df9580881278d9a6ce394b368d9c76700 + languageName: node + linkType: hard + +"@types/node@npm:^20.11.20": + version: 20.11.20 + resolution: "@types/node@npm:20.11.20" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 8e8de211e6d54425c603388a9b5cc9c434101985d0a1c88aabbf65d10df2b1fccd71855c20e61ae8a75c7aea56cb0f64e722cf7914cff1247d0b62ce21996ac4 + languageName: node + linkType: hard + +"@types/normalize-package-data@npm:^2.4.0": + version: 2.4.4 + resolution: "@types/normalize-package-data@npm:2.4.4" + checksum: aef7bb9b015883d6f4119c423dd28c4bdc17b0e8a0ccf112c78b4fe0e91fbc4af7c6204b04bba0e199a57d2f3fbbd5b4a14bf8739bf9d2a39b2a0aad545e0f86 + languageName: node + linkType: hard + +"@types/pretty-hrtime@npm:^1.0.0": + version: 1.0.3 + resolution: "@types/pretty-hrtime@npm:1.0.3" + checksum: e4c22475c588be982b398dee9ac0b05b21078bc26581819290a4901c5b269bcaa04cae0e61e012d412e811b0897c9dab316db064208914df2f0ed0960fc5306b + languageName: node + linkType: hard + +"@types/prop-types@npm:*": + version: 15.7.11 + resolution: "@types/prop-types@npm:15.7.11" + checksum: e53423cf9d510515ef8b47ff42f4f1b65a7b7b37c8704e2dbfcb9a60defe0c0e1f3cb1acfdeb466bad44ca938d7c79bffdd51b48ffb659df2432169d0b27a132 + languageName: node + linkType: hard + +"@types/qs@npm:*, @types/qs@npm:^6.9.5": + version: 6.9.10 + resolution: "@types/qs@npm:6.9.10" + checksum: 6be12e5f062d1b41eb037d59bf9cb65bc9410cedd5e6da832dfd7c8e2b3f4c91e81c9b90b51811140770e5052c6c4e8361181bd9437ddcd4515dc128b7c00353 + languageName: node + linkType: hard + +"@types/range-parser@npm:*": + version: 1.2.7 + resolution: "@types/range-parser@npm:1.2.7" + checksum: 361bb3e964ec5133fa40644a0b942279ed5df1949f21321d77de79f48b728d39253e5ce0408c9c17e4e0fd95ca7899da36841686393b9f7a1e209916e9381a3c + languageName: node + linkType: hard + +"@types/react@npm:>=16": + version: 18.2.39 + resolution: "@types/react@npm:18.2.39" + dependencies: + "@types/prop-types": "npm:*" + "@types/scheduler": "npm:*" + csstype: "npm:^3.0.2" + checksum: e91a5419d7615ab4fcaa7cb3ad2bd372093676e86c082748ab36ac394d3ed560070482d092a5488f74d6b1d913369e4dabf6eb287debed4d70cd3eb7dc135542 + languageName: node + linkType: hard + +"@types/resolve@npm:^1.20.2": + version: 1.20.6 + resolution: "@types/resolve@npm:1.20.6" + checksum: a9b0549d816ff2c353077365d865a33655a141d066d0f5a3ba6fd4b28bc2f4188a510079f7c1f715b3e7af505a27374adce2a5140a3ece2a059aab3d6e1a4244 + languageName: node + linkType: hard + +"@types/scheduler@npm:*": + version: 0.16.8 + resolution: "@types/scheduler@npm:0.16.8" + checksum: f86de504945b8fc41b1f391f847444d542e2e4067cf7e5d9bfeb5d2d2393d3203b1161bc0ef3b1e104d828dabfb60baf06e8d2c27e27ff7e8258e6e618d8c4ec + languageName: node + linkType: hard + +"@types/semver@npm:^7.3.4": + version: 7.5.6 + resolution: "@types/semver@npm:7.5.6" + checksum: 196dc32db5f68cbcde2e6a42bb4aa5cbb100fa2b7bd9c8c82faaaf3e03fbe063e205dbb4f03c7cdf53da2edb70a0d34c9f2e601b54281b377eb8dc1743226acd + languageName: node + linkType: hard + +"@types/send@npm:*": + version: 0.17.4 + resolution: "@types/send@npm:0.17.4" + dependencies: + "@types/mime": "npm:^1" + "@types/node": "npm:*" + checksum: 7f17fa696cb83be0a104b04b424fdedc7eaba1c9a34b06027239aba513b398a0e2b7279778af521f516a397ced417c96960e5f50fcfce40c4bc4509fb1a5883c + languageName: node + linkType: hard + +"@types/serve-static@npm:*": + version: 1.15.5 + resolution: "@types/serve-static@npm:1.15.5" + dependencies: + "@types/http-errors": "npm:*" + "@types/mime": "npm:*" + "@types/node": "npm:*" + checksum: 811d1a2f7e74a872195e7a013bcd87a2fb1edf07eaedcb9dcfd20c1eb4bc56ad4ea0d52141c13192c91ccda7c8aeb8a530d8a7e60b9c27f5990d7e62e0fecb03 + languageName: node + linkType: hard + +"@types/triple-beam@npm:^1.3.2": + version: 1.3.5 + resolution: "@types/triple-beam@npm:1.3.5" + checksum: d5d7f25da612f6d79266f4f1bb9c1ef8f1684e9f60abab251e1261170631062b656ba26ff22631f2760caeafd372abc41e64867cde27fba54fafb73a35b9056a + languageName: node + linkType: hard + +"@types/unist@npm:^2.0.0": + version: 2.0.10 + resolution: "@types/unist@npm:2.0.10" + checksum: 5f247dc2229944355209ad5c8e83cfe29419fa7f0a6d557421b1985a1500444719cc9efcc42c652b55aab63c931813c88033e0202c1ac684bcd4829d66e44731 + languageName: node + linkType: hard + +"@types/uuid@npm:^9.0.1": + version: 9.0.7 + resolution: "@types/uuid@npm:9.0.7" + checksum: b329ebd4f9d1d8e08d4f2cc211be4922d70d1149f73d5772630e4a3acfb5170c6d37b3d7a39a0412f1a56e86e8a844c7f297c798b082f90380608bf766688787 + languageName: node + linkType: hard + +"@types/yargs-parser@npm:*": + version: 21.0.3 + resolution: "@types/yargs-parser@npm:21.0.3" + checksum: e71c3bd9d0b73ca82e10bee2064c384ab70f61034bbfb78e74f5206283fc16a6d85267b606b5c22cb2a3338373586786fed595b2009825d6a9115afba36560a0 + languageName: node + linkType: hard + +"@types/yargs@npm:^16.0.0": + version: 16.0.9 + resolution: "@types/yargs@npm:16.0.9" + dependencies: + "@types/yargs-parser": "npm:*" + checksum: be24bd9a56c97ddb2964c1c18f5b9fe8271a50e100dc6945989901aae58f7ce6fb8f3a591c749a518401b6301358dbd1997e83c36138a297094feae7f9ac8211 + languageName: node + linkType: hard + +"@types/yargs@npm:^17.0.8": + version: 17.0.32 + resolution: "@types/yargs@npm:17.0.32" + dependencies: + "@types/yargs-parser": "npm:*" + checksum: 2095e8aad8a4e66b86147415364266b8d607a3b95b4239623423efd7e29df93ba81bb862784a6e08664f645cc1981b25fd598f532019174cd3e5e1e689e1cccf + languageName: node + linkType: hard + +"@vitejs/plugin-react@npm:^4.2.0": + version: 4.2.0 + resolution: "@vitejs/plugin-react@npm:4.2.0" + dependencies: + "@babel/core": "npm:^7.23.3" + "@babel/plugin-transform-react-jsx-self": "npm:^7.23.3" + "@babel/plugin-transform-react-jsx-source": "npm:^7.23.3" + "@types/babel__core": "npm:^7.20.4" + react-refresh: "npm:^0.14.0" + peerDependencies: + vite: ^4.2.0 || ^5.0.0 + checksum: b6bd9b2a49d58e96bd2576abc4d816c862a51e3d394c8a42ea507cac434279193529a567fce7026e16a65ca2cdb3e6f1cdfeb3ec9751fde235e74564de693939 + languageName: node + linkType: hard + +"@vitest/expect@npm:1.3.1": + version: 1.3.1 + resolution: "@vitest/expect@npm:1.3.1" + dependencies: + "@vitest/spy": "npm:1.3.1" + "@vitest/utils": "npm:1.3.1" + chai: "npm:^4.3.10" + checksum: ea66a1e912d896a481a27631b68089b885af7e8ed62ba8aaa119c37a9beafe6c094fd672775a20e6e23460af66e294f9ca259e6e0562708d1b7724eaaf53c7bb + languageName: node + linkType: hard + +"@vitest/runner@npm:1.3.1": + version: 1.3.1 + resolution: "@vitest/runner@npm:1.3.1" + dependencies: + "@vitest/utils": "npm:1.3.1" + p-limit: "npm:^5.0.0" + pathe: "npm:^1.1.1" + checksum: d732de2368d2bc32cbc27f0bbc5477f6e36088ddfb873c036935a45b1b252ebc529b932cf5cd944eed9b692243acebef828f6d3218583cb8a6817a8270712050 + languageName: node + linkType: hard + +"@vitest/snapshot@npm:1.3.1": + version: 1.3.1 + resolution: "@vitest/snapshot@npm:1.3.1" + dependencies: + magic-string: "npm:^0.30.5" + pathe: "npm:^1.1.1" + pretty-format: "npm:^29.7.0" + checksum: cad0844270852c6d53c1ca6b7ca279034880d2140837ff245d5bd2376f4356cc924929c58dc69bcf9fad83ba934d4a06000c908971cc24b5d7a9ec2656b72d29 + languageName: node + linkType: hard + +"@vitest/spy@npm:1.3.1": + version: 1.3.1 + resolution: "@vitest/spy@npm:1.3.1" + dependencies: + tinyspy: "npm:^2.2.0" + checksum: efc42f679d2a51fc6583ca3136ccd47581cb27c923ed3cb0500f5dee9aac99b681bfdd400c16ef108f2e0761daa642bc190816a6411931a2aba99ebf8b213dd4 + languageName: node + linkType: hard + +"@vitest/utils@npm:1.3.1": + version: 1.3.1 + resolution: "@vitest/utils@npm:1.3.1" + dependencies: + diff-sequences: "npm:^29.6.3" + estree-walker: "npm:^3.0.3" + loupe: "npm:^2.3.7" + pretty-format: "npm:^29.7.0" + checksum: d604c8ad3b1aee30d4dcd889098f591407bfe18547ff96485b1d1ed54eff58219c756a9544a7fbd4e37886863abacd7a89a76334cb3ea7f84c3d496bb757db23 + languageName: node + linkType: hard + +"@xmldom/xmldom@npm:^0.8.3": + version: 0.8.10 + resolution: "@xmldom/xmldom@npm:0.8.10" + checksum: c7647c442502720182b0d65b17d45d2d95317c1c8c497626fe524bda79b4fb768a9aa4fae2da919f308e7abcff7d67c058b102a9d641097e9a57f0b80187851f + languageName: node + linkType: hard + +"@yarnpkg/esbuild-plugin-pnp@npm:^3.0.0-rc.10": + version: 3.0.0-rc.15 + resolution: "@yarnpkg/esbuild-plugin-pnp@npm:3.0.0-rc.15" + dependencies: + tslib: "npm:^2.4.0" + peerDependencies: + esbuild: ">=0.10.0" + checksum: 5095bc316862971add31ca1fadb0095b6ad15f25120f6ab3a06086bb6a7be93c2f3c45bff80d5976689fc89b0e9bf82bd3d410e205c852739874d32d050c4e57 + languageName: node + linkType: hard + +"@yarnpkg/fslib@npm:2.10.3": + version: 2.10.3 + resolution: "@yarnpkg/fslib@npm:2.10.3" + dependencies: + "@yarnpkg/libzip": "npm:^2.3.0" + tslib: "npm:^1.13.0" + checksum: c4fbbed99e801f17c381204e9699d9ea4fb51b14e99968985f477bdbc7b02b61e026860173f3f46bd60d9f46ae6a06f420a3edb3c02c3a45ae83779095928094 + languageName: node + linkType: hard + +"@yarnpkg/libzip@npm:2.3.0, @yarnpkg/libzip@npm:^2.3.0": + version: 2.3.0 + resolution: "@yarnpkg/libzip@npm:2.3.0" + dependencies: + "@types/emscripten": "npm:^1.39.6" + tslib: "npm:^1.13.0" + checksum: 0c2361ccb002e28463ed98541f3bdaab54f52aad6a2080666c2a9ea605ebd9cdfb7b0340b1db6f105820d05bcb803cdfb3ce755a8f6034657298c291bf884f81 + languageName: node + linkType: hard + +"abbrev@npm:1": + version: 1.1.1 + resolution: "abbrev@npm:1.1.1" + checksum: 3f762677702acb24f65e813070e306c61fafe25d4b2583f9dfc935131f774863f3addd5741572ed576bd69cabe473c5af18e1e108b829cb7b6b4747884f726e6 + languageName: node + linkType: hard + +"abbrev@npm:^2.0.0": + version: 2.0.0 + resolution: "abbrev@npm:2.0.0" + checksum: f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 + languageName: node + linkType: hard + +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: "npm:^5.0.0" + checksum: 90ccc50f010250152509a344eb2e71977fbf8db0ab8f1061197e3275ddf6c61a41a6edfd7b9409c664513131dd96e962065415325ef23efa5db931b382d24ca5 + languageName: node + linkType: hard + +"accepts@npm:~1.3.5, accepts@npm:~1.3.8": + version: 1.3.8 + resolution: "accepts@npm:1.3.8" + dependencies: + mime-types: "npm:~2.1.34" + negotiator: "npm:0.6.3" + checksum: 3a35c5f5586cfb9a21163ca47a5f77ac34fa8ceb5d17d2fa2c0d81f41cbd7f8c6fa52c77e2c039acc0f4d09e71abdc51144246900f6bef5e3c4b333f77d89362 + languageName: node + linkType: hard + +"acorn-jsx@npm:^5.3.1": + version: 5.3.2 + resolution: "acorn-jsx@npm:5.3.2" + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 4c54868fbef3b8d58927d5e33f0a4de35f59012fe7b12cf9dfbb345fb8f46607709e1c4431be869a23fb63c151033d84c4198fa9f79385cec34fcb1dd53974c1 + languageName: node + linkType: hard + +"acorn-walk@npm:^7.2.0": + version: 7.2.0 + resolution: "acorn-walk@npm:7.2.0" + checksum: ff99f3406ed8826f7d6ef6ac76b7608f099d45a1ff53229fa267125da1924188dbacf02e7903dfcfd2ae4af46f7be8847dc7d564c73c4e230dfb69c8ea8e6b4c + languageName: node + linkType: hard + +"acorn-walk@npm:^8.3.2": + version: 8.3.2 + resolution: "acorn-walk@npm:8.3.2" + checksum: 7e2a8dad5480df7f872569b9dccff2f3da7e65f5353686b1d6032ab9f4ddf6e3a2cb83a9b52cf50b1497fd522154dda92f0abf7153290cc79cd14721ff121e52 + languageName: node + linkType: hard + +"acorn@npm:^6.4.1": + version: 6.4.2 + resolution: "acorn@npm:6.4.2" + bin: + acorn: bin/acorn + checksum: 52a72d5d785fa64a95880f2951021a38954f8f69a4944dfeab6fb1449b0f02293eae109a56d55b58ff31a90a00d16a804658a12db8ef834c20b3d1201fe5ba5b + languageName: node + linkType: hard + +"acorn@npm:^7.4.1": + version: 7.4.1 + resolution: "acorn@npm:7.4.1" + bin: + acorn: bin/acorn + checksum: bd0b2c2b0f334bbee48828ff897c12bd2eb5898d03bf556dcc8942022cec795ac5bb5b6b585e2de687db6231faf07e096b59a361231dd8c9344d5df5f7f0e526 + languageName: node + linkType: hard + +"acorn@npm:^8.10.0, acorn@npm:^8.11.2": + version: 8.11.2 + resolution: "acorn@npm:8.11.2" + bin: + acorn: bin/acorn + checksum: a3ed76c761b75ec54b1ec3068fb7f113a182e95aea7f322f65098c2958d232e3d211cb6dac35ff9c647024b63714bc528a26d54a925d1fef2c25585b4c8e4017 + languageName: node + linkType: hard + +"address@npm:^1.0.1": + version: 1.2.2 + resolution: "address@npm:1.2.2" + checksum: 1c8056b77fb124456997b78ed682ecc19d2fd7ea8bd5850a2aa8c3e3134c913847c57bcae418622efd32ba858fa1e242a40a251ac31da0515664fc0ac03a047d + languageName: node + linkType: hard + +"agent-base@npm:5": + version: 5.1.1 + resolution: "agent-base@npm:5.1.1" + checksum: 3baa3f01072c16e3955ce7802166e576cde9831af82b262aae1c780af49c0c84e82e64ba9ef9e7d1704fe29e9f0096a78a4f998ec137360fee3cb95186f97161 + languageName: node + linkType: hard + +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0": + version: 7.1.0 + resolution: "agent-base@npm:7.1.0" + dependencies: + debug: "npm:^4.3.4" + checksum: fc974ab57ffdd8421a2bc339644d312a9cca320c20c3393c9d8b1fd91731b9bbabdb985df5fc860f5b79d81c3e350daa3fcb31c5c07c0bb385aafc817df004ce + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: "npm:^2.0.0" + indent-string: "npm:^4.0.0" + checksum: a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 + languageName: node + linkType: hard + +"animate.css@npm:^4.1.1": + version: 4.1.1 + resolution: "animate.css@npm:4.1.1" + checksum: 28fcf5a5f502e4c12186846d22aa1cd63b835955160a97116930c78bff8a89135aa5c57f94010252a29456ada7cfc8ed8791cac02521ec6402befaf883937159 + languageName: node + linkType: hard + +"ansi-colors@npm:^1.0.1": + version: 1.1.0 + resolution: "ansi-colors@npm:1.1.0" + dependencies: + ansi-wrap: "npm:^0.1.0" + checksum: c5f3ae4710ed564ca173cd2cf3e85a3bf8dabb7b20688f84299caaf0a4af01e6b7825b32739336c9437492058d3b07d90ef42e3e6223fbba3dc9d52f63e29056 + languageName: node + linkType: hard + +"ansi-gray@npm:^0.1.1": + version: 0.1.1 + resolution: "ansi-gray@npm:0.1.1" + dependencies: + ansi-wrap: "npm:0.1.0" + checksum: f15a0c069f81a343afe2af5e111624603ce9e6059996d44a0338d7e44b88171a05dc975debdf4df01a86e62395027ae0615499a1e4adfefbebd417061b506079 + languageName: node + linkType: hard + +"ansi-regex@npm:^2.0.0": + version: 2.1.1 + resolution: "ansi-regex@npm:2.1.1" + checksum: 78cebaf50bce2cb96341a7230adf28d804611da3ce6bf338efa7b72f06cc6ff648e29f80cd95e582617ba58d5fdbec38abfeed3500a98bce8381a9daec7c548b + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: cbe16dbd2c6b2735d1df7976a7070dd277326434f0212f43abf6d87674095d247968209babdaad31bb00882fa68807256ba9be340eec2f1004de14ca75f52a08 + languageName: node + linkType: hard + +"ansi-styles@npm:^3.2.1": + version: 3.2.1 + resolution: "ansi-styles@npm:3.2.1" + dependencies: + color-convert: "npm:^1.9.0" + checksum: ece5a8ef069fcc5298f67e3f4771a663129abd174ea2dfa87923a2be2abf6cd367ef72ac87942da00ce85bd1d651d4cd8595aebdb1b385889b89b205860e977b + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 + languageName: node + linkType: hard + +"ansi-styles@npm:^5.0.0": + version: 5.2.0 + resolution: "ansi-styles@npm:5.2.0" + checksum: 9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: 5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c + languageName: node + linkType: hard + +"ansi-wrap@npm:0.1.0, ansi-wrap@npm:^0.1.0": + version: 0.1.0 + resolution: "ansi-wrap@npm:0.1.0" + checksum: 1e0a53ae0d1a3fc5ceeb5d1868cb5b0a61543a1ff11f3efc51bab7923cc01fe8180db1f9250ce5003b425c53f568bcf3c2dea9d90b5c1cd0a1dae13f76c601dd + languageName: node + linkType: hard + +"any-promise@npm:^1.1.0": + version: 1.3.0 + resolution: "any-promise@npm:1.3.0" + checksum: 60f0298ed34c74fef50daab88e8dab786036ed5a7fad02e012ab57e376e0a0b4b29e83b95ea9b5e7d89df762f5f25119b83e00706ecaccb22cfbacee98d74889 + languageName: node + linkType: hard + +"anymatch@npm:^2.0.0": + version: 2.0.0 + resolution: "anymatch@npm:2.0.0" + dependencies: + micromatch: "npm:^3.1.4" + normalize-path: "npm:^2.1.1" + checksum: a0d745e52f0233048724b9c9d7b1d8a650f7a50151a0f1d2cce1857b09fd096052d334f8c570cc88596edef8249ae778f767db94025cd00f81e154a37bb7e34e + languageName: node + linkType: hard + +"anymatch@npm:^3.0.3, anymatch@npm:~3.1.2": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac + languageName: node + linkType: hard + +"app-root-dir@npm:^1.0.2": + version: 1.0.2 + resolution: "app-root-dir@npm:1.0.2" + checksum: 0225e4be7788968a82bb76df9b14b0d7f212a5c12e8c625cdc34f80548780bcbfc5f3287d0806dddd83bf9dbf9ce302e76b2887cd3a6f4be52b79df7f3aa9e7c + languageName: node + linkType: hard + +"append-buffer@npm:^1.0.2": + version: 1.0.2 + resolution: "append-buffer@npm:1.0.2" + dependencies: + buffer-equal: "npm:^1.0.0" + checksum: 909c34059ddd418ddd7c5a050b2891f971eafd17ffdcf4b39411fcb6ecb780db3e147a17dd8c4482381ee2c3a3447689d6e2ef5529dd9c1f9bb630b763a5aab5 + languageName: node + linkType: hard + +"archy@npm:^1.0.0": + version: 1.0.0 + resolution: "archy@npm:1.0.0" + checksum: 200c849dd1c304ea9914827b0555e7e1e90982302d574153e28637db1a663c53de62bad96df42d50e8ce7fc18d05e3437d9aa8c4b383803763755f0956c7d308 + languageName: node + linkType: hard + +"argparse@npm:^1.0.7": + version: 1.0.10 + resolution: "argparse@npm:1.0.10" + dependencies: + sprintf-js: "npm:~1.0.2" + checksum: b2972c5c23c63df66bca144dbc65d180efa74f25f8fd9b7d9a0a6c88ae839db32df3d54770dcb6460cf840d232b60695d1a6b1053f599d84e73f7437087712de + languageName: node + linkType: hard + +"argparse@npm:^2.0.1": + version: 2.0.1 + resolution: "argparse@npm:2.0.1" + checksum: c5640c2d89045371c7cedd6a70212a04e360fd34d6edeae32f6952c63949e3525ea77dbec0289d8213a99bbaeab5abfa860b5c12cf88a2e6cf8106e90dd27a7e + languageName: node + linkType: hard + +"aria-hidden@npm:^1.1.1": + version: 1.2.3 + resolution: "aria-hidden@npm:1.2.3" + dependencies: + tslib: "npm:^2.0.0" + checksum: 46b07b7273167ad3fc2625f1ecbb43f8e6f73115c66785cbb5dcf1e2508133a43b6419d610c39676ceaeb563239efbd8974d5c0187695db8b3e8c3e11f549c2d + languageName: node + linkType: hard + +"aria-query@npm:5.1.3": + version: 5.1.3 + resolution: "aria-query@npm:5.1.3" + dependencies: + deep-equal: "npm:^2.0.5" + checksum: edcbc8044c4663d6f88f785e983e6784f98cb62b4ba1e9dd8d61b725d0203e4cfca38d676aee984c31f354103461102a3d583aa4fbe4fd0a89b679744f4e5faf + languageName: node + linkType: hard + +"arr-diff@npm:^4.0.0": + version: 4.0.0 + resolution: "arr-diff@npm:4.0.0" + checksum: 67b80067137f70c89953b95f5c6279ad379c3ee39f7143578e13bd51580a40066ee2a55da066e22d498dce10f68c2d70056d7823f972fab99dfbf4c78d0bc0f7 + languageName: node + linkType: hard + +"arr-filter@npm:^1.1.1": + version: 1.1.2 + resolution: "arr-filter@npm:1.1.2" + dependencies: + make-iterator: "npm:^1.0.0" + checksum: 66b7f29957e9e1ce02f8de6802c588cca21124335c875849ac5ef306188be7adcce6d978e3349ce05abb35420cdb7988a818020e1b16471ad83b48e2cf58ad3a + languageName: node + linkType: hard + +"arr-flatten@npm:^1.0.1, arr-flatten@npm:^1.1.0": + version: 1.1.0 + resolution: "arr-flatten@npm:1.1.0" + checksum: bef53be02ed3bc58f202b3861a5b1eb6e1ae4fecf39c3ad4d15b1e0433f941077d16e019a33312d820844b0661777322acbb7d0c447b04d9bdf7d6f9c532548a + languageName: node + linkType: hard + +"arr-map@npm:^2.0.0, arr-map@npm:^2.0.2": + version: 2.0.2 + resolution: "arr-map@npm:2.0.2" + dependencies: + make-iterator: "npm:^1.0.0" + checksum: b91d095a194455f779f929de86bb815671f1602c7f344426334ddc819a8a684cde76f61ed572fd5553d23711ccba04da542f204ecb0b81c28bbe70d9793497fc + languageName: node + linkType: hard + +"arr-union@npm:^3.1.0": + version: 3.1.0 + resolution: "arr-union@npm:3.1.0" + checksum: 7d5aa05894e54aa93c77c5726c1dd5d8e8d3afe4f77983c0aa8a14a8a5cbe8b18f0cf4ecaa4ac8c908ef5f744d2cbbdaa83fd6e96724d15fea56cfa7f5efdd51 + languageName: node + linkType: hard + +"array-buffer-byte-length@npm:^1.0.0": + version: 1.0.0 + resolution: "array-buffer-byte-length@npm:1.0.0" + dependencies: + call-bind: "npm:^1.0.2" + is-array-buffer: "npm:^3.0.1" + checksum: 12f84f6418b57a954caa41654e5e63e019142a4bbb2c6829ba86d1ba65d31ccfaf1461d1743556fd32b091fac34ff44d9dfbdb001402361c45c373b2c86f5c20 + languageName: node + linkType: hard + +"array-each@npm:^1.0.0, array-each@npm:^1.0.1": + version: 1.0.1 + resolution: "array-each@npm:1.0.1" + checksum: b5951ac450b560849143722d6785672ae71f5e9b061f11e7e2f775513a952e583e8bcedbba538a08049e235f5583756efec440fc6740a9b47b411cb487f65a9b + languageName: node + linkType: hard + +"array-flatten@npm:1.1.1": + version: 1.1.1 + resolution: "array-flatten@npm:1.1.1" + checksum: 806966c8abb2f858b08f5324d9d18d7737480610f3bd5d3498aaae6eb5efdc501a884ba019c9b4a8f02ff67002058749d05548fd42fa8643f02c9c7f22198b91 + languageName: node + linkType: hard + +"array-initial@npm:^1.0.0": + version: 1.1.0 + resolution: "array-initial@npm:1.1.0" + dependencies: + array-slice: "npm:^1.0.0" + is-number: "npm:^4.0.0" + checksum: 2a895b8aed2d782b953c4281ed09d67a465ed1c62e2264c7ee3e1a39c72b3790bac21d6ffa62f0ce606f18a99195c50fd4cd36cc725b501ee49c81fd2441ead5 + languageName: node + linkType: hard + +"array-last@npm:^1.1.1": + version: 1.3.0 + resolution: "array-last@npm:1.3.0" + dependencies: + is-number: "npm:^4.0.0" + checksum: bb620e744fab80b104a5eddfa828eb915451ffc23b737e76b2ecfbbef42e1a9557ca85d280cde10c5d12b4627d15857e7312a2f20d9ecc45f1e52d745a591438 + languageName: node + linkType: hard + +"array-slice@npm:^1.0.0": + version: 1.1.0 + resolution: "array-slice@npm:1.1.0" + checksum: dfefd705905f428b6c4cace2a787f308b5a64db5411e33cdf8ff883b6643f1703e48ac152b74eea482f8f6765fdf78b5277e2bad7840be2b4d5c23777db3266f + languageName: node + linkType: hard + +"array-sort@npm:^1.0.0": + version: 1.0.0 + resolution: "array-sort@npm:1.0.0" + dependencies: + default-compare: "npm:^1.0.0" + get-value: "npm:^2.0.6" + kind-of: "npm:^5.0.2" + checksum: 10fe9186fcf25e019e28a8a5d0375f6f5b71f48983266ae64ae06f7c55e1ccd7aea6ecf78c77829e7859e2da240398c45e4d046fd9f45935485d08c9fd45eb53 + languageName: node + linkType: hard + +"array-union@npm:^2.1.0": + version: 2.1.0 + resolution: "array-union@npm:2.1.0" + checksum: 429897e68110374f39b771ec47a7161fc6a8fc33e196857c0a396dc75df0b5f65e4d046674db764330b6bb66b39ef48dd7c53b6a2ee75cfb0681e0c1a7033962 + languageName: node + linkType: hard + +"array-unique@npm:^0.3.2": + version: 0.3.2 + resolution: "array-unique@npm:0.3.2" + checksum: dbf4462cdba8a4b85577be07705210b3d35be4b765822a3f52962d907186617638ce15e0603a4fefdcf82f4cbbc9d433f8cbbd6855148a68872fa041b6474121 + languageName: node + linkType: hard + +"arraybuffer.prototype.slice@npm:^1.0.2": + version: 1.0.2 + resolution: "arraybuffer.prototype.slice@npm:1.0.2" + dependencies: + array-buffer-byte-length: "npm:^1.0.0" + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + get-intrinsic: "npm:^1.2.1" + is-array-buffer: "npm:^3.0.2" + is-shared-array-buffer: "npm:^1.0.2" + checksum: 96b6e40e439678ffb7fa266398510074d33c3980fbb475490b69980cca60adec3b0777047ef377068a29862157f83edef42efc64ce48ce38977d04d68de5b7fb + languageName: node + linkType: hard + +"asap@npm:~2.0.3": + version: 2.0.6 + resolution: "asap@npm:2.0.6" + checksum: c6d5e39fe1f15e4b87677460bd66b66050cd14c772269cee6688824c1410a08ab20254bb6784f9afb75af9144a9f9a7692d49547f4d19d715aeb7c0318f3136d + languageName: node + linkType: hard + +"asn1.js@npm:^5.2.0": + version: 5.4.1 + resolution: "asn1.js@npm:5.4.1" + dependencies: + bn.js: "npm:^4.0.0" + inherits: "npm:^2.0.1" + minimalistic-assert: "npm:^1.0.0" + safer-buffer: "npm:^2.1.0" + checksum: b577232fa6069cc52bb128e564002c62b2b1fe47f7137bdcd709c0b8495aa79cee0f8cc458a831b2d8675900eea0d05781b006be5e1aa4f0ae3577a73ec20324 + languageName: node + linkType: hard + +"assert@npm:^1.1.1": + version: 1.5.1 + resolution: "assert@npm:1.5.1" + dependencies: + object.assign: "npm:^4.1.4" + util: "npm:^0.10.4" + checksum: 836688b928b68b7fc5bbc165443e16a62623d57676a1e8a980a0316f9ae86e5e0a102c63470491bf55a8545e75766303640c0c7ad1cf6bfa5450130396043bbd + languageName: node + linkType: hard + +"assert@npm:^2.0.0, assert@npm:^2.1.0": + version: 2.1.0 + resolution: "assert@npm:2.1.0" + dependencies: + call-bind: "npm:^1.0.2" + is-nan: "npm:^1.3.2" + object-is: "npm:^1.1.5" + object.assign: "npm:^4.1.4" + util: "npm:^0.12.5" + checksum: 7271a5da883c256a1fa690677bf1dd9d6aa882139f2bed1cd15da4f9e7459683e1da8e32a203d6cc6767e5e0f730c77a9532a87b896b4b0af0dd535f668775f0 + languageName: node + linkType: hard + +"assertion-error@npm:^1.1.0": + version: 1.1.0 + resolution: "assertion-error@npm:1.1.0" + checksum: 25456b2aa333250f01143968e02e4884a34588a8538fbbf65c91a637f1dbfb8069249133cd2f4e530f10f624d206a664e7df30207830b659e9f5298b00a4099b + languageName: node + linkType: hard + +"assign-symbols@npm:^1.0.0": + version: 1.0.0 + resolution: "assign-symbols@npm:1.0.0" + checksum: 29a654b8a6da6889a190d0d0efef4b1bfb5948fa06cbc245054aef05139f889f2f7c75b989917e3fde853fc4093b88048e4de8578a73a76f113d41bfd66e5775 + languageName: node + linkType: hard + +"ast-types@npm:^0.16.1": + version: 0.16.1 + resolution: "ast-types@npm:0.16.1" + dependencies: + tslib: "npm:^2.0.1" + checksum: abcc49e42eb921a7ebc013d5bec1154651fb6dbc3f497541d488859e681256901b2990b954d530ba0da4d0851271d484f7057d5eff5e07cb73e8b10909f711bf + languageName: node + linkType: hard + +"async-done@npm:^1.2.0, async-done@npm:^1.2.2": + version: 1.3.2 + resolution: "async-done@npm:1.3.2" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.2" + process-nextick-args: "npm:^2.0.0" + stream-exhaust: "npm:^1.0.1" + checksum: 0c11985b49e7915f2de2333a12722e415ba0d46e8285f699b610b11cd54ee8c59056e8ae6e7ed2c88e4cc2235173895fe4a67c610b3105cc58d821f4ce72fb35 + languageName: node + linkType: hard + +"async-each@npm:^1.0.1": + version: 1.0.6 + resolution: "async-each@npm:1.0.6" + checksum: d4e45e8f077e20e015952c065ceae75f82b30ee2d4a8e56a5c454ae44331aaa009d8c94fe043ba254c177bffae9f6ebeefebb7daf9f7ce4d27fac0274dc328ae + languageName: node + linkType: hard + +"async-limiter@npm:~1.0.0": + version: 1.0.1 + resolution: "async-limiter@npm:1.0.1" + checksum: 0693d378cfe86842a70d4c849595a0bb50dc44c11649640ca982fa90cbfc74e3cc4753b5a0847e51933f2e9c65ce8e05576e75e5e1fd963a086e673735b35969 + languageName: node + linkType: hard + +"async-settle@npm:^1.0.0": + version: 1.0.0 + resolution: "async-settle@npm:1.0.0" + dependencies: + async-done: "npm:^1.2.2" + checksum: cae0911fa77078472d5f8889a1dbd60bd35a69b0a5ed0b4bd0cdb7ac57935c08c6b16242eaa0149c7a920553d5efba4512ef1175f6ed0b66f374d61c01373a36 + languageName: node + linkType: hard + +"async@npm:^3.2.3, async@npm:^3.2.4": + version: 3.2.5 + resolution: "async@npm:3.2.5" + checksum: 1408287b26c6db67d45cb346e34892cee555b8b59e6c68e6f8c3e495cad5ca13b4f218180e871f3c2ca30df4ab52693b66f2f6ff43644760cab0b2198bda79c1 + languageName: node + linkType: hard + +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d + languageName: node + linkType: hard + +"atob@npm:^2.1.2": + version: 2.1.2 + resolution: "atob@npm:2.1.2" + bin: + atob: bin/atob.js + checksum: ada635b519dc0c576bb0b3ca63a73b50eefacf390abb3f062558342a8d68f2db91d0c8db54ce81b0d89de3b0f000de71f3ae7d761fd7d8cc624278fe443d6c7e + languageName: node + linkType: hard + +"autoprefixer@npm:^10.4.17": + version: 10.4.17 + resolution: "autoprefixer@npm:10.4.17" + dependencies: + browserslist: "npm:^4.22.2" + caniuse-lite: "npm:^1.0.30001578" + fraction.js: "npm:^4.3.7" + normalize-range: "npm:^0.1.2" + picocolors: "npm:^1.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.1.0 + bin: + autoprefixer: bin/autoprefixer + checksum: 1d21cc8edb7bf993682094ceed03a32c18f5293f071182a64c2c6defb44bbe91d576ad775d2347469a81997b80cea0bbc4ad3eeb5b12710f9feacf2e6c04bb51 + languageName: node + linkType: hard + +"available-typed-arrays@npm:^1.0.5": + version: 1.0.5 + resolution: "available-typed-arrays@npm:1.0.5" + checksum: c4df567ca72d2754a6cbad20088f5f98b1065b3360178169fa9b44ea101af62c0f423fc3854fa820fd6895b6b9171b8386e71558203103ff8fc2ad503fdcc660 + languageName: node + linkType: hard + +"babel-core@npm:^7.0.0-bridge.0": + version: 7.0.0-bridge.0 + resolution: "babel-core@npm:7.0.0-bridge.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f57576e30267be4607d163b7288031d332cf9200ea35efe9fb33c97f834e304376774c28c1f9d6928d6733fcde7041e4010f1248a0519e7730c590d4b07b9608 + languageName: node + linkType: hard + +"babel-plugin-istanbul@npm:^6.1.1": + version: 6.1.1 + resolution: "babel-plugin-istanbul@npm:6.1.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.0.0" + "@istanbuljs/load-nyc-config": "npm:^1.0.0" + "@istanbuljs/schema": "npm:^0.1.2" + istanbul-lib-instrument: "npm:^5.0.4" + test-exclude: "npm:^6.0.0" + checksum: 1075657feb705e00fd9463b329921856d3775d9867c5054b449317d39153f8fbcebd3e02ebf00432824e647faff3683a9ca0a941325ef1afe9b3c4dd51b24beb + languageName: node + linkType: hard + +"babel-plugin-polyfill-corejs2@npm:^0.4.6": + version: 0.4.6 + resolution: "babel-plugin-polyfill-corejs2@npm:0.4.6" + dependencies: + "@babel/compat-data": "npm:^7.22.6" + "@babel/helper-define-polyfill-provider": "npm:^0.4.3" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 64a98811f343492aa6970ab253760194e389c0417e5b830522f944009c1f0c78e1251975fd1b9869cd48cc4623111b20a3389cf6732a1d10ba0d19de6fa5114f + languageName: node + linkType: hard + +"babel-plugin-polyfill-corejs3@npm:^0.8.5": + version: 0.8.6 + resolution: "babel-plugin-polyfill-corejs3@npm:0.8.6" + dependencies: + "@babel/helper-define-polyfill-provider": "npm:^0.4.3" + core-js-compat: "npm:^3.33.1" + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 97d974c1dfbefdf27866e21a1ac757f6ab1626379b544d6f8ddb05f7bfa02173f8347b6140295b0f770394549f9321775d3048e466a9a02b99b88ad5f0346858 + languageName: node + linkType: hard + +"babel-plugin-polyfill-regenerator@npm:^0.5.3": + version: 0.5.3 + resolution: "babel-plugin-polyfill-regenerator@npm:0.5.3" + dependencies: + "@babel/helper-define-polyfill-provider": "npm:^0.4.3" + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: cc32313b9ebbf1d7bedc33524a861136b9e5d3b6e9be317ac360a1c2a59ae5ed1b465a6c68b2715cdefb089780ddfb0c11f4a148e49827a947beee76e43da598 + languageName: node + linkType: hard + +"bach@npm:^1.0.0": + version: 1.2.0 + resolution: "bach@npm:1.2.0" + dependencies: + arr-filter: "npm:^1.1.1" + arr-flatten: "npm:^1.0.1" + arr-map: "npm:^2.0.0" + array-each: "npm:^1.0.0" + array-initial: "npm:^1.0.0" + array-last: "npm:^1.1.1" + async-done: "npm:^1.2.2" + async-settle: "npm:^1.0.0" + now-and-later: "npm:^2.0.0" + checksum: 0f2615664960f73fc38d1738206a861266b8b9d1ef5e95dccd7e2d8f2b8e93c718ec7717cb35d4229d2a4ed9909c3830b64bca892451a6bcf07fa572e1e0758c + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + languageName: node + linkType: hard + +"base64-js@npm:^1.0.2, base64-js@npm:^1.3.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf + languageName: node + linkType: hard + +"base@npm:^0.11.1": + version: 0.11.2 + resolution: "base@npm:0.11.2" + dependencies: + cache-base: "npm:^1.0.1" + class-utils: "npm:^0.3.5" + component-emitter: "npm:^1.2.1" + define-property: "npm:^1.0.0" + isobject: "npm:^3.0.1" + mixin-deep: "npm:^1.2.0" + pascalcase: "npm:^0.1.1" + checksum: 30a2c0675eb52136b05ef496feb41574d9f0bb2d6d677761da579c00a841523fccf07f1dbabec2337b5f5750f428683b8ca60d89e56a1052c4ae1c0cd05de64d + languageName: node + linkType: hard + +"better-opn@npm:^3.0.2": + version: 3.0.2 + resolution: "better-opn@npm:3.0.2" + dependencies: + open: "npm:^8.0.4" + checksum: 911ef25d44da75aabfd2444ce7a4294a8000ebcac73068c04a60298b0f7c7506b60421aa4cd02ac82502fb42baaff7e4892234b51e6923eded44c5a11185f2f5 + languageName: node + linkType: hard + +"big-integer@npm:^1.6.44": + version: 1.6.52 + resolution: "big-integer@npm:1.6.52" + checksum: 9604224b4c2ab3c43c075d92da15863077a9f59e5d4205f4e7e76acd0cd47e8d469ec5e5dba8d9b32aa233951893b29329ca56ac80c20ce094b4a647a66abae0 + languageName: node + linkType: hard + +"binary-extensions@npm:^1.0.0": + version: 1.13.1 + resolution: "binary-extensions@npm:1.13.1" + checksum: 2d616938ac23d828ec3fbe0dea429b566fd2c137ddc38f166f16561ccd58029deac3fa9fddb489ab13d679c8fb5f1bd0e82824041299e5e39d8dd3cc68fbb9f9 + languageName: node + linkType: hard + +"binary-extensions@npm:^2.0.0": + version: 2.2.0 + resolution: "binary-extensions@npm:2.2.0" + checksum: d73d8b897238a2d3ffa5f59c0241870043aa7471335e89ea5e1ff48edb7c2d0bb471517a3e4c5c3f4c043615caa2717b5f80a5e61e07503d51dc85cb848e665d + languageName: node + linkType: hard + +"bindings@npm:^1.5.0": + version: 1.5.0 + resolution: "bindings@npm:1.5.0" + dependencies: + file-uri-to-path: "npm:1.0.0" + checksum: 3dab2491b4bb24124252a91e656803eac24292473e56554e35bbfe3cc1875332cfa77600c3bac7564049dc95075bf6fcc63a4609920ff2d64d0fe405fcf0d4ba + languageName: node + linkType: hard + +"bintrees@npm:1.0.2": + version: 1.0.2 + resolution: "bintrees@npm:1.0.2" + checksum: 132944b20c93c1a8f97bf8aa25980a76c6eb4291b7f2df2dbcd01cb5b417c287d3ee0847c7260c9f05f3d5a4233aaa03dec95114e97f308abe9cc3f72bed4a44 + languageName: node + linkType: hard + +"bl@npm:^4.0.3, bl@npm:^4.1.0": + version: 4.1.0 + resolution: "bl@npm:4.1.0" + dependencies: + buffer: "npm:^5.5.0" + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.4.0" + checksum: 02847e1d2cb089c9dc6958add42e3cdeaf07d13f575973963335ac0fdece563a50ac770ac4c8fa06492d2dd276f6cc3b7f08c7cd9c7a7ad0f8d388b2a28def5f + languageName: node + linkType: hard + +"bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.9": + version: 4.12.0 + resolution: "bn.js@npm:4.12.0" + checksum: 9736aaa317421b6b3ed038ff3d4491935a01419ac2d83ddcfebc5717385295fcfcf0c57311d90fe49926d0abbd7a9dbefdd8861e6129939177f7e67ebc645b21 + languageName: node + linkType: hard + +"bn.js@npm:^5.0.0, bn.js@npm:^5.2.1": + version: 5.2.1 + resolution: "bn.js@npm:5.2.1" + checksum: bed3d8bd34ec89dbcf9f20f88bd7d4a49c160fda3b561c7bb227501f974d3e435a48fb9b61bc3de304acab9215a3bda0803f7017ffb4d0016a0c3a740a283caa + languageName: node + linkType: hard + +"body-parser@npm:1.20.1": + version: 1.20.1 + resolution: "body-parser@npm:1.20.1" + dependencies: + bytes: "npm:3.1.2" + content-type: "npm:~1.0.4" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + http-errors: "npm:2.0.0" + iconv-lite: "npm:0.4.24" + on-finished: "npm:2.4.1" + qs: "npm:6.11.0" + raw-body: "npm:2.5.1" + type-is: "npm:~1.6.18" + unpipe: "npm:1.0.0" + checksum: a202d493e2c10a33fb7413dac7d2f713be579c4b88343cd814b6df7a38e5af1901fc31044e04de176db56b16d9772aa25a7723f64478c20f4d91b1ac223bf3b8 + languageName: node + linkType: hard + +"boolbase@npm:^1.0.0": + version: 1.0.0 + resolution: "boolbase@npm:1.0.0" + checksum: e4b53deb4f2b85c52be0e21a273f2045c7b6a6ea002b0e139c744cb6f95e9ec044439a52883b0d74dedd1ff3da55ed140cfdddfed7fb0cccbed373de5dce1bcf + languageName: node + linkType: hard + +"bplist-parser@npm:^0.2.0": + version: 0.2.0 + resolution: "bplist-parser@npm:0.2.0" + dependencies: + big-integer: "npm:^1.6.44" + checksum: ce79c69e0f6efe506281e7c84e3712f7d12978991675b6e3a58a295b16f13ca81aa9b845c335614a545e0af728c8311b6aa3142af76ba1cb616af9bbac5c4a9f + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.11 + resolution: "brace-expansion@npm:1.1.11" + dependencies: + balanced-match: "npm:^1.0.0" + concat-map: "npm:0.0.1" + checksum: 695a56cd058096a7cb71fb09d9d6a7070113c7be516699ed361317aca2ec169f618e28b8af352e02ab4233fb54eb0168460a40dc320bab0034b36ab59aaad668 + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: "npm:^1.0.0" + checksum: b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f + languageName: node + linkType: hard + +"braces@npm:^2.3.1, braces@npm:^2.3.2": + version: 2.3.2 + resolution: "braces@npm:2.3.2" + dependencies: + arr-flatten: "npm:^1.1.0" + array-unique: "npm:^0.3.2" + extend-shallow: "npm:^2.0.1" + fill-range: "npm:^4.0.0" + isobject: "npm:^3.0.1" + repeat-element: "npm:^1.1.2" + snapdragon: "npm:^0.8.1" + snapdragon-node: "npm:^2.0.1" + split-string: "npm:^3.0.2" + to-regex: "npm:^3.0.1" + checksum: 72b27ea3ea2718f061c29e70fd6e17606e37c65f5801abddcf0b0052db1de7d60f3bf92cfc220ab57b44bd0083a5f69f9d03b3461d2816cfe9f9398207acc728 + languageName: node + linkType: hard + +"braces@npm:^3.0.2, braces@npm:~3.0.2": + version: 3.0.2 + resolution: "braces@npm:3.0.2" + dependencies: + fill-range: "npm:^7.0.1" + checksum: 321b4d675791479293264019156ca322163f02dc06e3c4cab33bb15cd43d80b51efef69b0930cfde3acd63d126ebca24cd0544fa6f261e093a0fb41ab9dda381 + languageName: node + linkType: hard + +"brorand@npm:^1.0.1, brorand@npm:^1.1.0": + version: 1.1.0 + resolution: "brorand@npm:1.1.0" + checksum: 6f366d7c4990f82c366e3878492ba9a372a73163c09871e80d82fb4ae0d23f9f8924cb8a662330308206e6b3b76ba1d528b4601c9ef73c2166b440b2ea3b7571 + languageName: node + linkType: hard + +"browser-assert@npm:^1.2.1": + version: 1.2.1 + resolution: "browser-assert@npm:1.2.1" + checksum: 902abf999f92c9c951fdb6d7352c09eea9a84706258699655f7e7906e42daa06a1ae286398a755872740e05a6a71c43c5d1a0c0431d67a8cdb66e5d859a3fc0c + languageName: node + linkType: hard + +"browserify-aes@npm:^1.0.0, browserify-aes@npm:^1.0.4": + version: 1.2.0 + resolution: "browserify-aes@npm:1.2.0" + dependencies: + buffer-xor: "npm:^1.0.3" + cipher-base: "npm:^1.0.0" + create-hash: "npm:^1.1.0" + evp_bytestokey: "npm:^1.0.3" + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + checksum: 967f2ae60d610b7b252a4cbb55a7a3331c78293c94b4dd9c264d384ca93354c089b3af9c0dd023534efdc74ffbc82510f7ad4399cf82bc37bc07052eea485f18 + languageName: node + linkType: hard + +"browserify-cipher@npm:^1.0.0": + version: 1.0.1 + resolution: "browserify-cipher@npm:1.0.1" + dependencies: + browserify-aes: "npm:^1.0.4" + browserify-des: "npm:^1.0.0" + evp_bytestokey: "npm:^1.0.0" + checksum: aa256dcb42bc53a67168bbc94ab85d243b0a3b56109dee3b51230b7d010d9b78985ffc1fb36e145c6e4db151f888076c1cfc207baf1525d3e375cbe8187fe27d + languageName: node + linkType: hard + +"browserify-des@npm:^1.0.0": + version: 1.0.2 + resolution: "browserify-des@npm:1.0.2" + dependencies: + cipher-base: "npm:^1.0.1" + des.js: "npm:^1.0.0" + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.1.2" + checksum: 943eb5d4045eff80a6cde5be4e5fbb1f2d5002126b5a4789c3c1aae3cdddb1eb92b00fb92277f512288e5c6af330730b1dbabcf7ce0923e749e151fcee5a074d + languageName: node + linkType: hard + +"browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.1.0": + version: 4.1.0 + resolution: "browserify-rsa@npm:4.1.0" + dependencies: + bn.js: "npm:^5.0.0" + randombytes: "npm:^2.0.1" + checksum: fb2b5a8279d8a567a28d8ee03fb62e448428a906bab5c3dc9e9c3253ace551b5ea271db15e566ac78f1b1d71b243559031446604168b9235c351a32cae99d02a + languageName: node + linkType: hard + +"browserify-sign@npm:^4.0.0": + version: 4.2.2 + resolution: "browserify-sign@npm:4.2.2" + dependencies: + bn.js: "npm:^5.2.1" + browserify-rsa: "npm:^4.1.0" + create-hash: "npm:^1.2.0" + create-hmac: "npm:^1.1.7" + elliptic: "npm:^6.5.4" + inherits: "npm:^2.0.4" + parse-asn1: "npm:^5.1.6" + readable-stream: "npm:^3.6.2" + safe-buffer: "npm:^5.2.1" + checksum: 4d1292e5c165d93455630515003f0e95eed9239c99e2d373920c5b56903d16296a3d23cd4bdc4d298f55ad9b83714a9e63bc4839f1166c303349a16e84e9b016 + languageName: node + linkType: hard + +"browserify-zlib@npm:^0.1.4": + version: 0.1.4 + resolution: "browserify-zlib@npm:0.1.4" + dependencies: + pako: "npm:~0.2.0" + checksum: 0cde7ca5d33d43125649330fd75c056397e53731956a2593c4a2529f4e609a8e6abdb2b8e1921683abf5645375b92cfb2a21baa42fe3c9fc3e2556d32043af93 + languageName: node + linkType: hard + +"browserify-zlib@npm:^0.2.0": + version: 0.2.0 + resolution: "browserify-zlib@npm:0.2.0" + dependencies: + pako: "npm:~1.0.5" + checksum: 9ab10b6dc732c6c5ec8ebcbe5cb7fe1467f97402c9b2140113f47b5f187b9438f93a8e065d8baf8b929323c18324fbf1105af479ee86d9d36cab7d7ef3424ad9 + languageName: node + linkType: hard + +"browserslist@npm:^4.21.9, browserslist@npm:^4.22.1": + version: 4.22.1 + resolution: "browserslist@npm:4.22.1" + dependencies: + caniuse-lite: "npm:^1.0.30001541" + electron-to-chromium: "npm:^1.4.535" + node-releases: "npm:^2.0.13" + update-browserslist-db: "npm:^1.0.13" + bin: + browserslist: cli.js + checksum: 6810f2d63f171d0b7b8d38cf091708e00cb31525501810a507839607839320d66e657293b0aa3d7f051ecbc025cb07390a90c037682c1d05d12604991e41050b + languageName: node + linkType: hard + +"browserslist@npm:^4.22.2": + version: 4.23.0 + resolution: "browserslist@npm:4.23.0" + dependencies: + caniuse-lite: "npm:^1.0.30001587" + electron-to-chromium: "npm:^1.4.668" + node-releases: "npm:^2.0.14" + update-browserslist-db: "npm:^1.0.13" + bin: + browserslist: cli.js + checksum: 8e9cc154529062128d02a7af4d8adeead83ca1df8cd9ee65a88e2161039f3d68a4d40fea7353cab6bae4c16182dec2fdd9a1cf7dc2a2935498cee1af0e998943 + languageName: node + linkType: hard + +"bser@npm:2.1.1": + version: 2.1.1 + resolution: "bser@npm:2.1.1" + dependencies: + node-int64: "npm:^0.4.0" + checksum: 24d8dfb7b6d457d73f32744e678a60cc553e4ec0e9e1a01cf614b44d85c3c87e188d3cc78ef0442ce5032ee6818de20a0162ba1074725c0d08908f62ea979227 + languageName: node + linkType: hard + +"buffer-builder@npm:^0.2.0": + version: 0.2.0 + resolution: "buffer-builder@npm:0.2.0" + checksum: e50c3a379f4acaea75ade1ee3e8c07ed6d7c5dfc3f98adbcf0159bfe1a4ce8ca1fe3689e861fcdb3fcef0012ebd4345a6112a5b8a1185295452bb66d7b6dc8a1 + languageName: node + linkType: hard + +"buffer-crc32@npm:~0.2.3": + version: 0.2.13 + resolution: "buffer-crc32@npm:0.2.13" + checksum: cb0a8ddf5cf4f766466db63279e47761eb825693eeba6a5a95ee4ec8cb8f81ede70aa7f9d8aeec083e781d47154290eb5d4d26b3f7a465ec57fb9e7d59c47150 + languageName: node + linkType: hard + +"buffer-equal@npm:^1.0.0": + version: 1.0.1 + resolution: "buffer-equal@npm:1.0.1" + checksum: 578f03cc9458f9151f68478ab80ebee99a4203de0647a47b491aa3d5fb821938cb4139119a2dae1a1ef9ed5506e0eee4d6a37178efbf2e2e0ee3a9886898fffd + languageName: node + linkType: hard + +"buffer-from@npm:^1.0.0": + version: 1.1.2 + resolution: "buffer-from@npm:1.1.2" + checksum: 124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 + languageName: node + linkType: hard + +"buffer-xor@npm:^1.0.3": + version: 1.0.3 + resolution: "buffer-xor@npm:1.0.3" + checksum: fd269d0e0bf71ecac3146187cfc79edc9dbb054e2ee69b4d97dfb857c6d997c33de391696d04bdd669272751fa48e7872a22f3a6c7b07d6c0bc31dbe02a4075c + languageName: node + linkType: hard + +"buffer@npm:^4.3.0": + version: 4.9.2 + resolution: "buffer@npm:4.9.2" + dependencies: + base64-js: "npm:^1.0.2" + ieee754: "npm:^1.1.4" + isarray: "npm:^1.0.0" + checksum: dc443d7e7caab23816b58aacdde710b72f525ad6eecd7d738fcaa29f6d6c12e8d9c13fed7219fd502be51ecf0615f5c077d4bdc6f9308dde2e53f8e5393c5b21 + languageName: node + linkType: hard + +"buffer@npm:^5.5.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.1.13" + checksum: 27cac81cff434ed2876058d72e7c4789d11ff1120ef32c9de48f59eab58179b66710c488987d295ae89a228f835fc66d088652dffeb8e3ba8659f80eb091d55e + languageName: node + linkType: hard + +"buffer@npm:^6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.2.1" + checksum: 2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0 + languageName: node + linkType: hard + +"builtin-status-codes@npm:^3.0.0": + version: 3.0.0 + resolution: "builtin-status-codes@npm:3.0.0" + checksum: c37bbba11a34c4431e56bd681b175512e99147defbe2358318d8152b3a01df7bf25e0305873947e5b350073d5ef41a364a22b37e48f1fb6d2fe6d5286a0f348c + languageName: node + linkType: hard + +"bytes@npm:3.0.0": + version: 3.0.0 + resolution: "bytes@npm:3.0.0" + checksum: 91d42c38601c76460519ffef88371caacaea483a354c8e4b8808e7b027574436a5713337c003ea3de63ee4991c2a9a637884fdfe7f761760d746929d9e8fec60 + languageName: node + linkType: hard + +"bytes@npm:3.1.2, bytes@npm:^3.0.0": + version: 3.1.2 + resolution: "bytes@npm:3.1.2" + checksum: 76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e + languageName: node + linkType: hard + +"cac@npm:^6.7.14": + version: 6.7.14 + resolution: "cac@npm:6.7.14" + checksum: 4ee06aaa7bab8981f0d54e5f5f9d4adcd64058e9697563ce336d8a3878ed018ee18ebe5359b2430eceae87e0758e62ea2019c3f52ae6e211b1bd2e133856cd10 + languageName: node + linkType: hard + +"cacache@npm:^18.0.0": + version: 18.0.1 + resolution: "cacache@npm:18.0.1" + dependencies: + "@npmcli/fs": "npm:^3.1.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^10.0.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^4.0.0" + ssri: "npm:^10.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^3.0.0" + checksum: a31666805a80a8b16ad3f85faf66750275a9175a3480896f4f6d31b5d53ef190484fabd71bdb6d2ea5603c717fbef09f4af03d6a65b525c8ef0afaa44c361866 + languageName: node + linkType: hard + +"cache-base@npm:^1.0.1": + version: 1.0.1 + resolution: "cache-base@npm:1.0.1" + dependencies: + collection-visit: "npm:^1.0.0" + component-emitter: "npm:^1.2.1" + get-value: "npm:^2.0.6" + has-value: "npm:^1.0.0" + isobject: "npm:^3.0.1" + set-value: "npm:^2.0.0" + to-object-path: "npm:^0.3.0" + union-value: "npm:^1.0.0" + unset-value: "npm:^1.0.0" + checksum: a7142e25c73f767fa520957dcd179b900b86eac63b8cfeaa3b2a35e18c9ca5968aa4e2d2bed7a3e7efd10f13be404344cfab3a4156217e71f9bdb95940bb9c8c + languageName: node + linkType: hard + +"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.4, call-bind@npm:^1.0.5": + version: 1.0.5 + resolution: "call-bind@npm:1.0.5" + dependencies: + function-bind: "npm:^1.1.2" + get-intrinsic: "npm:^1.2.1" + set-function-length: "npm:^1.1.1" + checksum: a6172c168fd6dacf744fcde745099218056bd755c50415b592655dcd6562157ed29f130f56c3f6db2250f67e4bd62e5c218cdc56d7bfd76e0bda50770fce2d10 + languageName: node + linkType: hard + +"camelcase@npm:^3.0.0": + version: 3.0.0 + resolution: "camelcase@npm:3.0.0" + checksum: 98871bb40b936430beca49490d325759f8d8ade32bea538ee63c20b17b326abb6bbd3e1d84daf63d9332b2fc7637f28696bf76da59180b1247051b955cb1da12 + languageName: node + linkType: hard + +"camelcase@npm:^5.3.1": + version: 5.3.1 + resolution: "camelcase@npm:5.3.1" + checksum: 92ff9b443bfe8abb15f2b1513ca182d16126359ad4f955ebc83dc4ddcc4ef3fdd2c078bc223f2673dc223488e75c99b16cc4d056624374b799e6a1555cf61b23 + languageName: node + linkType: hard + +"caniuse-lite@npm:^1.0.30001541": + version: 1.0.30001565 + resolution: "caniuse-lite@npm:1.0.30001565" + checksum: b400e0364651a700e39d59449ca6c65b26e2caceecc4b93ae54a01ed1f62d2a7e1333b1dc640d95fbe620ffa5be38fe4dbacd880cd7a1f42fc72bb8de9a2d0c9 + languageName: node + linkType: hard + +"caniuse-lite@npm:^1.0.30001578, caniuse-lite@npm:^1.0.30001587": + version: 1.0.30001589 + resolution: "caniuse-lite@npm:1.0.30001589" + checksum: 20debfb949413f603011bc7dacaf050010778bc4f8632c86fafd1bd0c43180c95ae7c31f6c82348f6309e5e221934e327c3607a216e3f09640284acf78cd6d4d + languageName: node + linkType: hard + +"chai@npm:^4.3.10": + version: 4.3.10 + resolution: "chai@npm:4.3.10" + dependencies: + assertion-error: "npm:^1.1.0" + check-error: "npm:^1.0.3" + deep-eql: "npm:^4.1.3" + get-func-name: "npm:^2.0.2" + loupe: "npm:^2.3.6" + pathval: "npm:^1.1.1" + type-detect: "npm:^4.0.8" + checksum: c887d24f67be6fb554c7ebbde3bb0568697a8833d475e4768296916891ba143f25fc079f6eb34146f3dd5a3279d34c1f387c32c9a6ab288e579f948d9ccf53fe + languageName: node + linkType: hard + +"chalk@npm:^2.4.1, chalk@npm:^2.4.2": + version: 2.4.2 + resolution: "chalk@npm:2.4.2" + dependencies: + ansi-styles: "npm:^3.2.1" + escape-string-regexp: "npm:^1.0.5" + supports-color: "npm:^5.3.0" + checksum: e6543f02ec877732e3a2d1c3c3323ddb4d39fbab687c23f526e25bd4c6a9bf3b83a696e8c769d078e04e5754921648f7821b2a2acfd16c550435fd630026e073 + languageName: node + linkType: hard + +"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.2": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: "npm:^4.1.0" + supports-color: "npm:^7.1.0" + checksum: 4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880 + languageName: node + linkType: hard + +"check-error@npm:^1.0.3": + version: 1.0.3 + resolution: "check-error@npm:1.0.3" + dependencies: + get-func-name: "npm:^2.0.2" + checksum: 94aa37a7315c0e8a83d0112b5bfb5a8624f7f0f81057c73e4707729cdd8077166c6aefb3d8e2b92c63ee130d4a2ff94bad46d547e12f3238cc1d78342a973841 + languageName: node + linkType: hard + +"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.5.2, chokidar@npm:^3.5.3": + version: 3.5.3 + resolution: "chokidar@npm:3.5.3" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 1076953093e0707c882a92c66c0f56ba6187831aa51bb4de878c1fec59ae611a3bf02898f190efec8e77a086b8df61c2b2a3ea324642a0558bdf8ee6c5dc9ca1 + languageName: node + linkType: hard + +"chokidar@npm:^2.0.0": + version: 2.1.8 + resolution: "chokidar@npm:2.1.8" + dependencies: + anymatch: "npm:^2.0.0" + async-each: "npm:^1.0.1" + braces: "npm:^2.3.2" + fsevents: "npm:^1.2.7" + glob-parent: "npm:^3.1.0" + inherits: "npm:^2.0.3" + is-binary-path: "npm:^1.0.0" + is-glob: "npm:^4.0.0" + normalize-path: "npm:^3.0.0" + path-is-absolute: "npm:^1.0.0" + readdirp: "npm:^2.2.1" + upath: "npm:^1.1.1" + dependenciesMeta: + fsevents: + optional: true + checksum: 5631cc00080224f9482cf5418dcbea111aec02fa8d81a8cfe37e47b9cf36089e071de52d503647e3a821a01426a40adc926ba899f657af86a51b8f8d4eef12a7 + languageName: node + linkType: hard + +"chownr@npm:^1.1.1": + version: 1.1.4 + resolution: "chownr@npm:1.1.4" + checksum: ed57952a84cc0c802af900cf7136de643d3aba2eecb59d29344bc2f3f9bf703a301b9d84cdc71f82c3ffc9ccde831b0d92f5b45f91727d6c9da62f23aef9d9db + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: 594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 + languageName: node + linkType: hard + +"ci-info@npm:^3.2.0": + version: 3.9.0 + resolution: "ci-info@npm:3.9.0" + checksum: 6f0109e36e111684291d46123d491bc4e7b7a1934c3a20dea28cba89f1d4a03acd892f5f6a81ed3855c38647e285a150e3c9ba062e38943bef57fee6c1554c3a + languageName: node + linkType: hard + +"cipher-base@npm:^1.0.0, cipher-base@npm:^1.0.1, cipher-base@npm:^1.0.3": + version: 1.0.4 + resolution: "cipher-base@npm:1.0.4" + dependencies: + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + checksum: d8d005f8b64d8a77b3d3ce531301ae7b45902c9cab4ec8b66bdbd2bf2a1d9fceb9a2133c293eb3c060b2d964da0f14c47fb740366081338aa3795dd1faa8984b + languageName: node + linkType: hard + +"class-utils@npm:^0.3.5": + version: 0.3.6 + resolution: "class-utils@npm:0.3.6" + dependencies: + arr-union: "npm:^3.1.0" + define-property: "npm:^0.2.5" + isobject: "npm:^3.0.0" + static-extend: "npm:^0.1.1" + checksum: d44f4afc7a3e48dba4c2d3fada5f781a1adeeff371b875c3b578bc33815c6c29d5d06483c2abfd43a32d35b104b27b67bfa39c2e8a422fa858068bd756cfbd42 + languageName: node + linkType: hard + +"clean-css@npm:^4.x": + version: 4.2.4 + resolution: "clean-css@npm:4.2.4" + dependencies: + source-map: "npm:~0.6.0" + checksum: 0e41795fdc9d65e5e17a3b0016d90bf2a653e3a680829b5bcebdbab48604cfe36d96d8af6346338d2c2aca8aa9af024ac4fb752ac3eb5b71bef68a34a129b58a + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 + languageName: node + linkType: hard + +"cli-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "cli-cursor@npm:3.1.0" + dependencies: + restore-cursor: "npm:^3.1.0" + checksum: 92a2f98ff9037d09be3dfe1f0d749664797fb674bf388375a2207a1203b69d41847abf16434203e0089212479e47a358b13a0222ab9fccfe8e2644a7ccebd111 + languageName: node + linkType: hard + +"cli-spinners@npm:^2.5.0": + version: 2.9.2 + resolution: "cli-spinners@npm:2.9.2" + checksum: 907a1c227ddf0d7a101e7ab8b300affc742ead4b4ebe920a5bf1bc6d45dce2958fcd195eb28fa25275062fe6fa9b109b93b63bc8033396ed3bcb50297008b3a3 + languageName: node + linkType: hard + +"cli-table3@npm:^0.6.1": + version: 0.6.3 + resolution: "cli-table3@npm:0.6.3" + dependencies: + "@colors/colors": "npm:1.5.0" + string-width: "npm:^4.2.0" + dependenciesMeta: + "@colors/colors": + optional: true + checksum: 39e580cb346c2eaf1bd8f4ff055ae644e902b8303c164a1b8894c0dc95941f92e001db51f49649011be987e708d9fa3183ccc2289a4d376a057769664048cc0c + languageName: node + linkType: hard + +"cliui@npm:^3.2.0": + version: 3.2.0 + resolution: "cliui@npm:3.2.0" + dependencies: + string-width: "npm:^1.0.1" + strip-ansi: "npm:^3.0.1" + wrap-ansi: "npm:^2.0.0" + checksum: 07b121fac7fd33ff8dbf3523f0d3dca0329d4e457e57dee54502aa5f27a33cbd9e66aa3e248f0260d8a1431b65b2bad8f510cd97fb8ab6a8e0506310a92e18d5 + languageName: node + linkType: hard + +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^7.0.0" + checksum: 4bda0f09c340cbb6dfdc1ed508b3ca080f12992c18d68c6be4d9cf51756033d5266e61ec57529e610dacbf4da1c634423b0c1b11037709cc6b09045cbd815df5 + languageName: node + linkType: hard + +"clone-buffer@npm:^1.0.0": + version: 1.0.0 + resolution: "clone-buffer@npm:1.0.0" + checksum: d813f4d12651bc4951d5e4869e2076d34ccfc3b23d0aae4e2e20e5a5e97bc7edbba84038356d222c54b25e3a83b5f45e8b637c18c6bd1794b2f1b49114122c50 + languageName: node + linkType: hard + +"clone-deep@npm:^4.0.1": + version: 4.0.1 + resolution: "clone-deep@npm:4.0.1" + dependencies: + is-plain-object: "npm:^2.0.4" + kind-of: "npm:^6.0.2" + shallow-clone: "npm:^3.0.0" + checksum: 637753615aa24adf0f2d505947a1bb75e63964309034a1cf56ba4b1f30af155201edd38d26ffe26911adaae267a3c138b344a4947d39f5fc1b6d6108125aa758 + languageName: node + linkType: hard + +"clone-stats@npm:^1.0.0": + version: 1.0.0 + resolution: "clone-stats@npm:1.0.0" + checksum: bb1e05991e034e1eb104173c25bb652ea5b2b4dad5a49057a857e00f8d1da39de3bd689128a25bab8cbdfbea8ae8f6066030d106ed5c299a7d92be7967c50217 + languageName: node + linkType: hard + +"clone@npm:^1.0.2": + version: 1.0.4 + resolution: "clone@npm:1.0.4" + checksum: 2176952b3649293473999a95d7bebfc9dc96410f6cbd3d2595cf12fd401f63a4bf41a7adbfd3ab2ff09ed60cb9870c58c6acdd18b87767366fabfc163700f13b + languageName: node + linkType: hard + +"clone@npm:^2.1.1": + version: 2.1.2 + resolution: "clone@npm:2.1.2" + checksum: ed0601cd0b1606bc7d82ee7175b97e68d1dd9b91fd1250a3617b38d34a095f8ee0431d40a1a611122dcccb4f93295b4fdb94942aa763392b5fe44effa50c2d5e + languageName: node + linkType: hard + +"cloneable-readable@npm:^1.0.0": + version: 1.1.3 + resolution: "cloneable-readable@npm:1.1.3" + dependencies: + inherits: "npm:^2.0.1" + process-nextick-args: "npm:^2.0.0" + readable-stream: "npm:^2.3.5" + checksum: 52db2904dcfcd117e4e9605b69607167096c954352eff0fcded0a16132c9cfc187b36b5db020bee2dc1b3a968ca354f8b30aef3d8b4ea74e3ea83a81d43e47bb + languageName: node + linkType: hard + +"clsx@npm:^1.0.4": + version: 1.2.1 + resolution: "clsx@npm:1.2.1" + checksum: 34dead8bee24f5e96f6e7937d711978380647e936a22e76380290e35486afd8634966ce300fc4b74a32f3762c7d4c0303f442c3e259f4ce02374eb0c82834f27 + languageName: node + linkType: hard + +"code-point-at@npm:^1.0.0": + version: 1.1.0 + resolution: "code-point-at@npm:1.1.0" + checksum: 33f6b234084e46e6e369b6f0b07949392651b4dde70fc6a592a8d3dafa08d5bb32e3981a02f31f6fc323a26bc03a4c063a9d56834848695bda7611c2417ea2e6 + languageName: node + linkType: hard + +"collection-map@npm:^1.0.0": + version: 1.0.0 + resolution: "collection-map@npm:1.0.0" + dependencies: + arr-map: "npm:^2.0.2" + for-own: "npm:^1.0.0" + make-iterator: "npm:^1.0.0" + checksum: 9fdda135961199d00401f1c72b2cb87d5ed1c120a98d0244a6199c1167b0f51ce88ae392300d2518c9930671bd2db85b5c47521e0bc54f7745872139a5b16964 + languageName: node + linkType: hard + +"collection-visit@npm:^1.0.0": + version: 1.0.0 + resolution: "collection-visit@npm:1.0.0" + dependencies: + map-visit: "npm:^1.0.0" + object-visit: "npm:^1.0.0" + checksum: add72a8d1c37cb90e53b1aaa2c31bf1989bfb733f0b02ce82c9fa6828c7a14358dba2e4f8e698c02f69e424aeccae1ffb39acdeaf872ade2f41369e84a2fcf8a + languageName: node + linkType: hard + +"color-convert@npm:^1.9.0, color-convert@npm:^1.9.3": + version: 1.9.3 + resolution: "color-convert@npm:1.9.3" + dependencies: + color-name: "npm:1.1.3" + checksum: 5ad3c534949a8c68fca8fbc6f09068f435f0ad290ab8b2f76841b9e6af7e0bb57b98cb05b0e19fe33f5d91e5a8611ad457e5f69e0a484caad1f7487fd0e8253c + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: 37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 + languageName: node + linkType: hard + +"color-name@npm:1.1.3": + version: 1.1.3 + resolution: "color-name@npm:1.1.3" + checksum: 566a3d42cca25b9b3cd5528cd7754b8e89c0eb646b7f214e8e2eaddb69994ac5f0557d9c175eb5d8f0ad73531140d9c47525085ee752a91a2ab15ab459caf6d6 + languageName: node + linkType: hard + +"color-name@npm:^1.0.0, color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 + languageName: node + linkType: hard + +"color-string@npm:^1.6.0": + version: 1.9.1 + resolution: "color-string@npm:1.9.1" + dependencies: + color-name: "npm:^1.0.0" + simple-swizzle: "npm:^0.2.2" + checksum: b0bfd74c03b1f837f543898b512f5ea353f71630ccdd0d66f83028d1f0924a7d4272deb278b9aef376cacf1289b522ac3fb175e99895283645a2dc3a33af2404 + languageName: node + linkType: hard + +"color-support@npm:^1.1.3": + version: 1.1.3 + resolution: "color-support@npm:1.1.3" + bin: + color-support: bin.js + checksum: 8ffeaa270a784dc382f62d9be0a98581db43e11eee301af14734a6d089bd456478b1a8b3e7db7ca7dc5b18a75f828f775c44074020b51c05fc00e6d0992b1cc6 + languageName: node + linkType: hard + +"color@npm:^3.1.3": + version: 3.2.1 + resolution: "color@npm:3.2.1" + dependencies: + color-convert: "npm:^1.9.3" + color-string: "npm:^1.6.0" + checksum: 39345d55825884c32a88b95127d417a2c24681d8b57069413596d9fcbb721459ef9d9ec24ce3e65527b5373ce171b73e38dbcd9c830a52a6487e7f37bf00e83c + languageName: node + linkType: hard + +"colorette@npm:^2.0.20": + version: 2.0.20 + resolution: "colorette@npm:2.0.20" + checksum: e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40 + languageName: node + linkType: hard + +"colorspace@npm:1.1.x": + version: 1.1.4 + resolution: "colorspace@npm:1.1.4" + dependencies: + color: "npm:^3.1.3" + text-hex: "npm:1.0.x" + checksum: af5f91ff7f8e146b96e439ac20ed79b197210193bde721b47380a75b21751d90fa56390c773bb67c0aedd34ff85091883a437ab56861c779bd507d639ba7e123 + languageName: node + linkType: hard + +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: "npm:~1.0.0" + checksum: 0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5 + languageName: node + linkType: hard + +"commander@npm:^10.0.0": + version: 10.0.1 + resolution: "commander@npm:10.0.1" + checksum: 53f33d8927758a911094adadda4b2cbac111a5b377d8706700587650fd8f45b0bbe336de4b5c3fe47fd61f420a3d9bd452b6e0e6e5600a7e74d7bf0174f6efe3 + languageName: node + linkType: hard + +"commander@npm:^6.2.1": + version: 6.2.1 + resolution: "commander@npm:6.2.1" + checksum: 85748abd9d18c8bc88febed58b98f66b7c591d9b5017cad459565761d7b29ca13b7783ea2ee5ce84bf235897333706c4ce29adf1ce15c8252780e7000e2ce9ea + languageName: node + linkType: hard + +"commander@npm:^7.2.0": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 8d690ff13b0356df7e0ebbe6c59b4712f754f4b724d4f473d3cc5b3fdcf978e3a5dc3078717858a2ceb50b0f84d0660a7f22a96cdc50fb877d0c9bb31593d23a + languageName: node + linkType: hard + +"commondir@npm:^1.0.1": + version: 1.0.1 + resolution: "commondir@npm:1.0.1" + checksum: 33a124960e471c25ee19280c9ce31ccc19574b566dc514fe4f4ca4c34fa8b0b57cf437671f5de380e11353ea9426213fca17687dd2ef03134fea2dbc53809fd6 + languageName: node + linkType: hard + +"component-emitter@npm:^1.2.1": + version: 1.3.1 + resolution: "component-emitter@npm:1.3.1" + checksum: e4900b1b790b5e76b8d71b328da41482118c0f3523a516a41be598dc2785a07fd721098d9bf6e22d89b19f4fa4e1025160dc00317ea111633a3e4f75c2b86032 + languageName: node + linkType: hard + +"compressible@npm:~2.0.16": + version: 2.0.18 + resolution: "compressible@npm:2.0.18" + dependencies: + mime-db: "npm:>= 1.43.0 < 2" + checksum: 8a03712bc9f5b9fe530cc5a79e164e665550d5171a64575d7dcf3e0395d7b4afa2d79ab176c61b5b596e28228b350dd07c1a2a6ead12fd81d1b6cd632af2fef7 + languageName: node + linkType: hard + +"compression@npm:^1.7.4": + version: 1.7.4 + resolution: "compression@npm:1.7.4" + dependencies: + accepts: "npm:~1.3.5" + bytes: "npm:3.0.0" + compressible: "npm:~2.0.16" + debug: "npm:2.6.9" + on-headers: "npm:~1.0.2" + safe-buffer: "npm:5.1.2" + vary: "npm:~1.1.2" + checksum: 138db836202a406d8a14156a5564fb1700632a76b6e7d1546939472895a5304f2b23c80d7a22bf44c767e87a26e070dbc342ea63bb45ee9c863354fa5556bbbc + languageName: node + linkType: hard + +"concat-map@npm:0.0.1": + version: 0.0.1 + resolution: "concat-map@npm:0.0.1" + checksum: c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f + languageName: node + linkType: hard + +"concat-stream@npm:^1.6.0, concat-stream@npm:^1.6.2": + version: 1.6.2 + resolution: "concat-stream@npm:1.6.2" + dependencies: + buffer-from: "npm:^1.0.0" + inherits: "npm:^2.0.3" + readable-stream: "npm:^2.2.2" + typedarray: "npm:^0.0.6" + checksum: 2e9864e18282946dabbccb212c5c7cec0702745e3671679eb8291812ca7fd12023f7d8cb36493942a62f770ac96a7f90009dc5c82ad69893438371720fa92617 + languageName: node + linkType: hard + +"concat-with-sourcemaps@npm:^1.0.0": + version: 1.1.0 + resolution: "concat-with-sourcemaps@npm:1.1.0" + dependencies: + source-map: "npm:^0.6.1" + checksum: d30cec83a320d20d7e9482a4d011fa84319a0a8f9107acb632c48493d608be3a2b879608866d9edba2ce304ee52bc798138c26ad16eda6fbe7ec5e7bec99a683 + languageName: node + linkType: hard + +"concurrently@npm:^8.2.2": + version: 8.2.2 + resolution: "concurrently@npm:8.2.2" + dependencies: + chalk: "npm:^4.1.2" + date-fns: "npm:^2.30.0" + lodash: "npm:^4.17.21" + rxjs: "npm:^7.8.1" + shell-quote: "npm:^1.8.1" + spawn-command: "npm:0.0.2" + supports-color: "npm:^8.1.1" + tree-kill: "npm:^1.2.2" + yargs: "npm:^17.7.2" + bin: + conc: dist/bin/concurrently.js + concurrently: dist/bin/concurrently.js + checksum: 0e9683196fe9c071d944345d21d8f34aa6c0cc50c0dd897e95619f2f1c9eb4871dca851b2569da17888235b7335b4c821ca19deed35bebcd9a131ee5d247f34c + languageName: node + linkType: hard + +"config-chain@npm:^1.1.13": + version: 1.1.13 + resolution: "config-chain@npm:1.1.13" + dependencies: + ini: "npm:^1.3.4" + proto-list: "npm:~1.2.1" + checksum: 39d1df18739d7088736cc75695e98d7087aea43646351b028dfabd5508d79cf6ef4c5bcd90471f52cd87ae470d1c5490c0a8c1a292fbe6ee9ff688061ea0963e + languageName: node + linkType: hard + +"console-browserify@npm:^1.1.0": + version: 1.2.0 + resolution: "console-browserify@npm:1.2.0" + checksum: 89b99a53b7d6cee54e1e64fa6b1f7ac24b844b4019c5d39db298637e55c1f4ffa5c165457ad984864de1379df2c8e1886cbbdac85d9dbb6876a9f26c3106f226 + languageName: node + linkType: hard + +"constants-browserify@npm:^1.0.0": + version: 1.0.0 + resolution: "constants-browserify@npm:1.0.0" + checksum: ab49b1d59a433ed77c964d90d19e08b2f77213fb823da4729c0baead55e3c597f8f97ebccfdfc47bd896d43854a117d114c849a6f659d9986420e97da0f83ac5 + languageName: node + linkType: hard + +"content-disposition@npm:0.5.4": + version: 0.5.4 + resolution: "content-disposition@npm:0.5.4" + dependencies: + safe-buffer: "npm:5.2.1" + checksum: bac0316ebfeacb8f381b38285dc691c9939bf0a78b0b7c2d5758acadad242d04783cee5337ba7d12a565a19075af1b3c11c728e1e4946de73c6ff7ce45f3f1bb + languageName: node + linkType: hard + +"content-type@npm:^1.0.5, content-type@npm:~1.0.4": + version: 1.0.5 + resolution: "content-type@npm:1.0.5" + checksum: b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af + languageName: node + linkType: hard + +"convert-source-map@npm:^1.0.0, convert-source-map@npm:^1.5.0": + version: 1.9.0 + resolution: "convert-source-map@npm:1.9.0" + checksum: 281da55454bf8126cbc6625385928c43479f2060984180c42f3a86c8b8c12720a24eac260624a7d1e090004028d2dee78602330578ceec1a08e27cb8bb0a8a5b + languageName: node + linkType: hard + +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0" + checksum: 8f2f7a27a1a011cc6cc88cc4da2d7d0cfa5ee0369508baae3d98c260bb3ac520691464e5bbe4ae7cdf09860c1d69ecc6f70c63c6e7c7f7e3f18ec08484dc7d9b + languageName: node + linkType: hard + +"cookie-signature@npm:1.0.6": + version: 1.0.6 + resolution: "cookie-signature@npm:1.0.6" + checksum: b36fd0d4e3fef8456915fcf7742e58fbfcc12a17a018e0eb9501c9d5ef6893b596466f03b0564b81af29ff2538fd0aa4b9d54fe5ccbfb4c90ea50ad29fe2d221 + languageName: node + linkType: hard + +"cookie@npm:0.5.0": + version: 0.5.0 + resolution: "cookie@npm:0.5.0" + checksum: c01ca3ef8d7b8187bae434434582288681273b5a9ed27521d4d7f9f7928fe0c920df0decd9f9d3bbd2d14ac432b8c8cf42b98b3bdd5bfe0e6edddeebebe8b61d + languageName: node + linkType: hard + +"copy-descriptor@npm:^0.1.0": + version: 0.1.1 + resolution: "copy-descriptor@npm:0.1.1" + checksum: 161f6760b7348c941007a83df180588fe2f1283e0867cc027182734e0f26134e6cc02de09aa24a95dc267b2e2025b55659eef76c8019df27bc2d883033690181 + languageName: node + linkType: hard + +"copy-props@npm:^2.0.1": + version: 2.0.5 + resolution: "copy-props@npm:2.0.5" + dependencies: + each-props: "npm:^1.3.2" + is-plain-object: "npm:^5.0.0" + checksum: 7011a7bff2d8bbf08ae1f2a0e2e3015b57a14fa5ed9bfa393efe1573c2ac92a94caf9d4f93db4329e9da332f7f91aa7b8fa0dbae1c890009ecf602ec34d298c9 + languageName: node + linkType: hard + +"core-js-compat@npm:^3.31.0, core-js-compat@npm:^3.33.1": + version: 3.33.3 + resolution: "core-js-compat@npm:3.33.3" + dependencies: + browserslist: "npm:^4.22.1" + checksum: 779997ac791b7f7d01f21312c7b83fff2babb1f632d21fd6cfd8e9c737442475bcb660fade7e1cd7642b5c9593685bc2188089bf86b31d671e8e05e28ee30e58 + languageName: node + linkType: hard + +"core-js-pure@npm:^3.30.2": + version: 3.33.3 + resolution: "core-js-pure@npm:3.33.3" + checksum: 97cf39cc013f6a4f77700762de36b495228b3c087fc04b61e86bfbfb475595529966cabbcf37e738e3a468c486e815c85118d120cc6fc4960da08a14caf69826 + languageName: node + linkType: hard + +"core-util-is@npm:~1.0.0": + version: 1.0.3 + resolution: "core-util-is@npm:1.0.3" + checksum: 90a0e40abbddfd7618f8ccd63a74d88deea94e77d0e8dbbea059fa7ebebb8fbb4e2909667fe26f3a467073de1a542ebe6ae4c73a73745ac5833786759cd906c9 + languageName: node + linkType: hard + +"create-ecdh@npm:^4.0.0": + version: 4.0.4 + resolution: "create-ecdh@npm:4.0.4" + dependencies: + bn.js: "npm:^4.1.0" + elliptic: "npm:^6.5.3" + checksum: 77b11a51360fec9c3bce7a76288fc0deba4b9c838d5fb354b3e40c59194d23d66efe6355fd4b81df7580da0661e1334a235a2a5c040b7569ba97db428d466e7f + languageName: node + linkType: hard + +"create-hash@npm:^1.1.0, create-hash@npm:^1.1.2, create-hash@npm:^1.2.0": + version: 1.2.0 + resolution: "create-hash@npm:1.2.0" + dependencies: + cipher-base: "npm:^1.0.1" + inherits: "npm:^2.0.1" + md5.js: "npm:^1.3.4" + ripemd160: "npm:^2.0.1" + sha.js: "npm:^2.4.0" + checksum: d402e60e65e70e5083cb57af96d89567954d0669e90550d7cec58b56d49c4b193d35c43cec8338bc72358198b8cbf2f0cac14775b651e99238e1cf411490f915 + languageName: node + linkType: hard + +"create-hmac@npm:^1.1.0, create-hmac@npm:^1.1.4, create-hmac@npm:^1.1.7": + version: 1.1.7 + resolution: "create-hmac@npm:1.1.7" + dependencies: + cipher-base: "npm:^1.0.3" + create-hash: "npm:^1.1.0" + inherits: "npm:^2.0.1" + ripemd160: "npm:^2.0.0" + safe-buffer: "npm:^5.0.1" + sha.js: "npm:^2.4.8" + checksum: 24332bab51011652a9a0a6d160eed1e8caa091b802335324ae056b0dcb5acbc9fcf173cf10d128eba8548c3ce98dfa4eadaa01bd02f44a34414baee26b651835 + languageName: node + linkType: hard + +"cross-fetch@npm:^3.1.5": + version: 3.1.8 + resolution: "cross-fetch@npm:3.1.8" + dependencies: + node-fetch: "npm:^2.6.12" + checksum: 4c5e022ffe6abdf380faa6e2373c0c4ed7ef75e105c95c972b6f627c3f083170b6886f19fb488a7fa93971f4f69dcc890f122b0d97f0bf5f41ca1d9a8f58c8af + languageName: node + linkType: hard + +"cross-spawn@npm:^6.0.5": + version: 6.0.5 + resolution: "cross-spawn@npm:6.0.5" + dependencies: + nice-try: "npm:^1.0.4" + path-key: "npm:^2.0.1" + semver: "npm:^5.5.0" + shebang-command: "npm:^1.2.0" + which: "npm:^1.2.9" + checksum: e05544722e9d7189b4292c66e42b7abeb21db0d07c91b785f4ae5fefceb1f89e626da2703744657b287e86dcd4af57b54567cef75159957ff7a8a761d9055012 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + languageName: node + linkType: hard + +"crypto-browserify@npm:^3.11.0": + version: 3.12.0 + resolution: "crypto-browserify@npm:3.12.0" + dependencies: + browserify-cipher: "npm:^1.0.0" + browserify-sign: "npm:^4.0.0" + create-ecdh: "npm:^4.0.0" + create-hash: "npm:^1.1.0" + create-hmac: "npm:^1.1.0" + diffie-hellman: "npm:^5.0.0" + inherits: "npm:^2.0.1" + pbkdf2: "npm:^3.0.3" + public-encrypt: "npm:^4.0.0" + randombytes: "npm:^2.0.0" + randomfill: "npm:^1.0.3" + checksum: 0c20198886576050a6aa5ba6ae42f2b82778bfba1753d80c5e7a090836890dc372bdc780986b2568b4fb8ed2a91c958e61db1f0b6b1cc96af4bd03ffc298ba92 + languageName: node + linkType: hard + +"crypto-random-string@npm:^2.0.0": + version: 2.0.0 + resolution: "crypto-random-string@npm:2.0.0" + checksum: 288589b2484fe787f9e146f56c4be90b940018f17af1b152e4dde12309042ff5a2bf69e949aab8b8ac253948381529cc6f3e5a2427b73643a71ff177fa122b37 + languageName: node + linkType: hard + +"css-select@npm:^4.1.3": + version: 4.3.0 + resolution: "css-select@npm:4.3.0" + dependencies: + boolbase: "npm:^1.0.0" + css-what: "npm:^6.0.1" + domhandler: "npm:^4.3.1" + domutils: "npm:^2.8.0" + nth-check: "npm:^2.0.1" + checksum: a489d8e5628e61063d5a8fe0fa1cc7ae2478cb334a388a354e91cf2908154be97eac9fa7ed4dffe87a3e06cf6fcaa6016553115335c4fd3377e13dac7bd5a8e1 + languageName: node + linkType: hard + +"css-selector-parser@npm:^1.4.1": + version: 1.4.1 + resolution: "css-selector-parser@npm:1.4.1" + checksum: 4a89a7b61072cf0e4d09e8abbb9a77bc661232b6fe6a6fe51ba775757bae0e3fc462b0db4c9a857da55afb89a1c1746a7b2ec1200f639c539556ebdc758b0101 + languageName: node + linkType: hard + +"css-tree@npm:^1.1.2, css-tree@npm:^1.1.3": + version: 1.1.3 + resolution: "css-tree@npm:1.1.3" + dependencies: + mdn-data: "npm:2.0.14" + source-map: "npm:^0.6.1" + checksum: 499a507bfa39b8b2128f49736882c0dd636b0cd3370f2c69f4558ec86d269113286b7df469afc955de6a68b0dba00bc533e40022a73698081d600072d5d83c1c + languageName: node + linkType: hard + +"css-what@npm:^6.0.1": + version: 6.1.0 + resolution: "css-what@npm:6.1.0" + checksum: a09f5a6b14ba8dcf57ae9a59474722e80f20406c53a61e9aedb0eedc693b135113ffe2983f4efc4b5065ae639442e9ae88df24941ef159c218b231011d733746 + languageName: node + linkType: hard + +"css@npm:^3.0.0": + version: 3.0.0 + resolution: "css@npm:3.0.0" + dependencies: + inherits: "npm:^2.0.4" + source-map: "npm:^0.6.1" + source-map-resolve: "npm:^0.6.0" + checksum: c17cb4a46a39c11b00225f1314158a892828af34cdf3badc7e88084882e9f414e4902a1d59231c0854f310af30bde343fd8a9e79c6001426fe88af45d3312fe2 + languageName: node + linkType: hard + +"cssesc@npm:^3.0.0": + version: 3.0.0 + resolution: "cssesc@npm:3.0.0" + bin: + cssesc: bin/cssesc + checksum: 6bcfd898662671be15ae7827120472c5667afb3d7429f1f917737f3bf84c4176003228131b643ae74543f17a394446247df090c597bb9a728cce298606ed0aa7 + languageName: node + linkType: hard + +"csso@npm:^4.2.0": + version: 4.2.0 + resolution: "csso@npm:4.2.0" + dependencies: + css-tree: "npm:^1.1.2" + checksum: f8c6b1300efaa0f8855a7905ae3794a29c6496e7f16a71dec31eb6ca7cfb1f058a4b03fd39b66c4deac6cb06bf6b4ba86da7b67d7320389cb9994d52b924b903 + languageName: node + linkType: hard + +"cssom@npm:^0.5.0": + version: 0.5.0 + resolution: "cssom@npm:0.5.0" + checksum: 8c4121c243baf0678c65dcac29b201ff0067dfecf978de9d5c83b2ff127a8fdefd2bfd54577f5ad8c80ed7d2c8b489ae01c82023545d010c4ecb87683fb403dd + languageName: node + linkType: hard + +"cssstyle@npm:^4.0.1": + version: 4.0.1 + resolution: "cssstyle@npm:4.0.1" + dependencies: + rrweb-cssom: "npm:^0.6.0" + checksum: cadf9a8b23e11f4c6d63f21291096a0b0be868bd4ab9c799daa2c5b18330e39e5281605f01da906e901b42f742df0f3b3645af6465e83377ff7d15a88ee432a0 + languageName: node + linkType: hard + +"csstype@npm:^3.0.2": + version: 3.1.2 + resolution: "csstype@npm:3.1.2" + checksum: 32c038af259897c807ac738d9eab16b3d86747c72b09d5c740978e06f067f9b7b1737e1b75e407c7ab1fe1543dc95f20e202b4786aeb1b8d3bdf5d5ce655e6c6 + languageName: node + linkType: hard + +"d@npm:1, d@npm:^1.0.1": + version: 1.0.1 + resolution: "d@npm:1.0.1" + dependencies: + es5-ext: "npm:^0.10.50" + type: "npm:^1.0.1" + checksum: 1fedcb3b956a461f64d86b94b347441beff5cef8910b6ac4ec509a2c67eeaa7093660a98b26601ac91f91260238add73bdf25867a9c0cb783774642bc4c1523f + languageName: node + linkType: hard + +"data-urls@npm:^5.0.0": + version: 5.0.0 + resolution: "data-urls@npm:5.0.0" + dependencies: + whatwg-mimetype: "npm:^4.0.0" + whatwg-url: "npm:^14.0.0" + checksum: 1b894d7d41c861f3a4ed2ae9b1c3f0909d4575ada02e36d3d3bc584bdd84278e20709070c79c3b3bff7ac98598cb191eb3e86a89a79ea4ee1ef360e1694f92ad + languageName: node + linkType: hard + +"date-fns@npm:^2.30.0": + version: 2.30.0 + resolution: "date-fns@npm:2.30.0" + dependencies: + "@babel/runtime": "npm:^7.21.0" + checksum: e4b521fbf22bc8c3db332bbfb7b094fd3e7627de0259a9d17c7551e2d2702608a7307a449206065916538e384f37b181565447ce2637ae09828427aed9cb5581 + languageName: node + linkType: hard + +"date-fns@npm:^3.3.1": + version: 3.3.1 + resolution: "date-fns@npm:3.3.1" + checksum: e04ff79244010e03b912d791cd3250af5f18866ce868604958d76bd87e5fb0b79f0a810b8e7066248452b41779b288c4fd21de1cac2cd4b6d384e9dd931c9674 + languageName: node + linkType: hard + +"debug-fabulous@npm:^1.0.0": + version: 1.1.0 + resolution: "debug-fabulous@npm:1.1.0" + dependencies: + debug: "npm:3.X" + memoizee: "npm:0.4.X" + object-assign: "npm:4.X" + checksum: 3f1213b786c677311540a7ae9625210b24fed368caeb9f41d298eae5ae348063df5fb1e2e5aef0519c9529103c19f926844ff478893f987cab9b6c65694c6516 + languageName: node + linkType: hard + +"debug@npm:2.6.9, debug@npm:^2.2.0, debug@npm:^2.3.3, debug@npm:^2.6.9": + version: 2.6.9 + resolution: "debug@npm:2.6.9" + dependencies: + ms: "npm:2.0.0" + checksum: 121908fb839f7801180b69a7e218a40b5a0b718813b886b7d6bdb82001b931c938e2941d1e4450f33a1b1df1da653f5f7a0440c197f29fbf8a6e9d45ff6ef589 + languageName: node + linkType: hard + +"debug@npm:3.X": + version: 3.2.7 + resolution: "debug@npm:3.2.7" + dependencies: + ms: "npm:^2.1.1" + checksum: 37d96ae42cbc71c14844d2ae3ba55adf462ec89fd3a999459dec3833944cd999af6007ff29c780f1c61153bcaaf2c842d1e4ce1ec621e4fc4923244942e4a02a + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.4": + version: 4.3.4 + resolution: "debug@npm:4.3.4" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: cedbec45298dd5c501d01b92b119cd3faebe5438c3917ff11ae1bff86a6c722930ac9c8659792824013168ba6db7c4668225d845c633fbdafbbf902a6389f736 + languageName: node + linkType: hard + +"decamelize@npm:^1.1.1": + version: 1.2.0 + resolution: "decamelize@npm:1.2.0" + checksum: 85c39fe8fbf0482d4a1e224ef0119db5c1897f8503bcef8b826adff7a1b11414972f6fef2d7dec2ee0b4be3863cf64ac1439137ae9e6af23a3d8dcbe26a5b4b2 + languageName: node + linkType: hard + +"decimal.js@npm:^10.4.3": + version: 10.4.3 + resolution: "decimal.js@npm:10.4.3" + checksum: 6d60206689ff0911f0ce968d40f163304a6c1bc739927758e6efc7921cfa630130388966f16bf6ef6b838cb33679fbe8e7a78a2f3c478afce841fd55ac8fb8ee + languageName: node + linkType: hard + +"decode-uri-component@npm:^0.2.0": + version: 0.2.2 + resolution: "decode-uri-component@npm:0.2.2" + checksum: 1f4fa54eb740414a816b3f6c24818fbfcabd74ac478391e9f4e2282c994127db02010ce804f3d08e38255493cfe68608b3f5c8e09fd6efc4ae46c807691f7a31 + languageName: node + linkType: hard + +"deep-eql@npm:^4.1.3": + version: 4.1.3 + resolution: "deep-eql@npm:4.1.3" + dependencies: + type-detect: "npm:^4.0.0" + checksum: ff34e8605d8253e1bf9fe48056e02c6f347b81d9b5df1c6650a1b0f6f847b4a86453b16dc226b34f853ef14b626e85d04e081b022e20b00cd7d54f079ce9bbdd + languageName: node + linkType: hard + +"deep-equal@npm:^2.0.5": + version: 2.2.3 + resolution: "deep-equal@npm:2.2.3" + dependencies: + array-buffer-byte-length: "npm:^1.0.0" + call-bind: "npm:^1.0.5" + es-get-iterator: "npm:^1.1.3" + get-intrinsic: "npm:^1.2.2" + is-arguments: "npm:^1.1.1" + is-array-buffer: "npm:^3.0.2" + is-date-object: "npm:^1.0.5" + is-regex: "npm:^1.1.4" + is-shared-array-buffer: "npm:^1.0.2" + isarray: "npm:^2.0.5" + object-is: "npm:^1.1.5" + object-keys: "npm:^1.1.1" + object.assign: "npm:^4.1.4" + regexp.prototype.flags: "npm:^1.5.1" + side-channel: "npm:^1.0.4" + which-boxed-primitive: "npm:^1.0.2" + which-collection: "npm:^1.0.1" + which-typed-array: "npm:^1.1.13" + checksum: a48244f90fa989f63ff5ef0cc6de1e4916b48ea0220a9c89a378561960814794a5800c600254482a2c8fd2e49d6c2e196131dc983976adb024c94a42dfe4949f + languageName: node + linkType: hard + +"default-browser-id@npm:3.0.0": + version: 3.0.0 + resolution: "default-browser-id@npm:3.0.0" + dependencies: + bplist-parser: "npm:^0.2.0" + untildify: "npm:^4.0.0" + checksum: 8db3ab882eb3e1e8b59d84c8641320e6c66d8eeb17eb4bb848b7dd549b1e6fd313988e4a13542e95fbaeff03f6e9dedc5ad191ad4df7996187753eb0d45c00b7 + languageName: node + linkType: hard + +"default-compare@npm:^1.0.0": + version: 1.0.0 + resolution: "default-compare@npm:1.0.0" + dependencies: + kind-of: "npm:^5.0.2" + checksum: 718f6f76c327c26509697ded2b642dbe526589c98ba6316a90b6564f5084d05cf07fc38addd452d8eed9c22fb598eea5ecc52b130f602975c608e61c70251ff2 + languageName: node + linkType: hard + +"default-resolution@npm:^2.0.0": + version: 2.0.0 + resolution: "default-resolution@npm:2.0.0" + checksum: 162c538be2dbecd09f7303a34303f97ca1684232e1cd7dd58a97cf472d3874b92ed2fba52c01cada47f595136007dec4dfdb368a7e1c043872407b97a00772ad + languageName: node + linkType: hard + +"defaults@npm:^1.0.3": + version: 1.0.4 + resolution: "defaults@npm:1.0.4" + dependencies: + clone: "npm:^1.0.2" + checksum: 9cfbe498f5c8ed733775db62dfd585780387d93c17477949e1670bfcfb9346e0281ce8c4bf9f4ac1fc0f9b851113bd6dc9e41182ea1644ccd97de639fa13c35a + languageName: node + linkType: hard + +"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.1": + version: 1.1.1 + resolution: "define-data-property@npm:1.1.1" + dependencies: + get-intrinsic: "npm:^1.2.1" + gopd: "npm:^1.0.1" + has-property-descriptors: "npm:^1.0.0" + checksum: 77ef6e0bceb515e05b5913ab635a84d537cee84f8a7c37c77fdcb31fc5b80f6dbe81b33375e4b67d96aa04e6a0d8d4ea099e431d83f089af8d93adfb584bcb94 + languageName: node + linkType: hard + +"define-lazy-prop@npm:^2.0.0": + version: 2.0.0 + resolution: "define-lazy-prop@npm:2.0.0" + checksum: db6c63864a9d3b7dc9def55d52764968a5af296de87c1b2cc71d8be8142e445208071953649e0386a8cc37cfcf9a2067a47207f1eb9ff250c2a269658fdae422 + languageName: node + linkType: hard + +"define-properties@npm:^1.1.3, define-properties@npm:^1.1.4, define-properties@npm:^1.2.0": + version: 1.2.1 + resolution: "define-properties@npm:1.2.1" + dependencies: + define-data-property: "npm:^1.0.1" + has-property-descriptors: "npm:^1.0.0" + object-keys: "npm:^1.1.1" + checksum: 88a152319ffe1396ccc6ded510a3896e77efac7a1bfbaa174a7b00414a1747377e0bb525d303794a47cf30e805c2ec84e575758512c6e44a993076d29fd4e6c3 + languageName: node + linkType: hard + +"define-property@npm:^0.2.5": + version: 0.2.5 + resolution: "define-property@npm:0.2.5" + dependencies: + is-descriptor: "npm:^0.1.0" + checksum: 9986915c0893818dedc9ca23eaf41370667762fd83ad8aa4bf026a28563120dbaacebdfbfbf2b18d3b929026b9c6ee972df1dbf22de8fafb5fe6ef18361e4750 + languageName: node + linkType: hard + +"define-property@npm:^1.0.0": + version: 1.0.0 + resolution: "define-property@npm:1.0.0" + dependencies: + is-descriptor: "npm:^1.0.0" + checksum: d7cf09db10d55df305f541694ed51dafc776ad9bb8a24428899c9f2d36b11ab38dce5527a81458d1b5e7c389f8cbe803b4abad6e91a0037a329d153b84fc975e + languageName: node + linkType: hard + +"define-property@npm:^2.0.2": + version: 2.0.2 + resolution: "define-property@npm:2.0.2" + dependencies: + is-descriptor: "npm:^1.0.2" + isobject: "npm:^3.0.1" + checksum: f91a08ad008fa764172a2c072adc7312f10217ade89ddaea23018321c6d71b2b68b8c229141ed2064179404e345c537f1a2457c379824813695b51a6ad3e4969 + languageName: node + linkType: hard + +"defu@npm:^6.1.2": + version: 6.1.3 + resolution: "defu@npm:6.1.3" + checksum: 60d0d9a6e328148d5313fe0239ba3777701291f35570b52562454653d953fec5281b084514540f8d3b60d61bad9e39b52e95b3c0451631ded220ad8fdc893455 + languageName: node + linkType: hard + +"del@npm:^6.0.0": + version: 6.1.1 + resolution: "del@npm:6.1.1" + dependencies: + globby: "npm:^11.0.1" + graceful-fs: "npm:^4.2.4" + is-glob: "npm:^4.0.1" + is-path-cwd: "npm:^2.2.0" + is-path-inside: "npm:^3.0.2" + p-map: "npm:^4.0.0" + rimraf: "npm:^3.0.2" + slash: "npm:^3.0.0" + checksum: 8a095c5ccade42c867a60252914ae485ec90da243d735d1f63ec1e64c1cfbc2b8810ad69a29ab6326d159d4fddaa2f5bad067808c42072351ec458efff86708f + languageName: node + linkType: hard + +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19 + languageName: node + linkType: hard + +"depd@npm:2.0.0": + version: 2.0.0 + resolution: "depd@npm:2.0.0" + checksum: 58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c + languageName: node + linkType: hard + +"dequal@npm:^2.0.2": + version: 2.0.3 + resolution: "dequal@npm:2.0.3" + checksum: f98860cdf58b64991ae10205137c0e97d384c3a4edc7f807603887b7c4b850af1224a33d88012009f150861cbee4fa2d322c4cc04b9313bee312e47f6ecaa888 + languageName: node + linkType: hard + +"des.js@npm:^1.0.0": + version: 1.1.0 + resolution: "des.js@npm:1.1.0" + dependencies: + inherits: "npm:^2.0.1" + minimalistic-assert: "npm:^1.0.0" + checksum: 671354943ad67493e49eb4c555480ab153edd7cee3a51c658082fcde539d2690ed2a4a0b5d1f401f9cde822edf3939a6afb2585f32c091f2d3a1b1665cd45236 + languageName: node + linkType: hard + +"destroy@npm:1.2.0": + version: 1.2.0 + resolution: "destroy@npm:1.2.0" + checksum: bd7633942f57418f5a3b80d5cb53898127bcf53e24cdf5d5f4396be471417671f0fee48a4ebe9a1e9defbde2a31280011af58a57e090ff822f589b443ed4e643 + languageName: node + linkType: hard + +"detect-file@npm:^1.0.0": + version: 1.0.0 + resolution: "detect-file@npm:1.0.0" + checksum: c782a5f992047944c39d337c82f5d1d21d65d1378986d46c354df9d9ec6d5f356bca0182969c11b08b9b8a7af8727b3c2d5a9fad0b022be4a3bf4c216f63ed07 + languageName: node + linkType: hard + +"detect-indent@npm:^6.1.0": + version: 6.1.0 + resolution: "detect-indent@npm:6.1.0" + checksum: dd83cdeda9af219cf77f5e9a0dc31d828c045337386cfb55ce04fad94ba872ee7957336834154f7647b89b899c3c7acc977c57a79b7c776b506240993f97acc7 + languageName: node + linkType: hard + +"detect-newline@npm:^2.0.0": + version: 2.1.0 + resolution: "detect-newline@npm:2.1.0" + checksum: cb75c36c59da87115f49fe4aa22507f6c5271bac94c63a056af5d9dea2919208de57b6f0fb4543d6cf635965d10b42729d443589caa302cc76e1fa9f48e55f05 + languageName: node + linkType: hard + +"detect-node-es@npm:^1.1.0": + version: 1.1.0 + resolution: "detect-node-es@npm:1.1.0" + checksum: e562f00de23f10c27d7119e1af0e7388407eb4b06596a25f6d79a360094a109ff285de317f02b090faae093d314cf6e73ac3214f8a5bb3a0def5bece94557fbe + languageName: node + linkType: hard + +"detect-package-manager@npm:^2.0.1": + version: 2.0.1 + resolution: "detect-package-manager@npm:2.0.1" + dependencies: + execa: "npm:^5.1.1" + checksum: 56ffd65228d1ff3ead5ea7f8ab951a517a29270de27510b790c9a8b77d4f36efbd61493e170ca77ee3dc13cbb5218583ce65b78ad14a59dc48565c9bcbbf3c71 + languageName: node + linkType: hard + +"detect-port@npm:^1.3.0": + version: 1.5.1 + resolution: "detect-port@npm:1.5.1" + dependencies: + address: "npm:^1.0.1" + debug: "npm:4" + bin: + detect: bin/detect-port.js + detect-port: bin/detect-port.js + checksum: f2b204ad3a9f8e8b53fea35fcc97469f31a8e3e786a2f59fbc886397e33b5f130c5f964bf001b9a64d990047c3824f6a439308461ff19801df04ab48a754639e + languageName: node + linkType: hard + +"dettle@npm:^1.0.1": + version: 1.0.1 + resolution: "dettle@npm:1.0.1" + checksum: 116a101aff93b2e1d5e505adbe53c4b898d924bc16f12f5ac629055ed8a8a19c86f916b834b178b7bfb352dd601bbfe01e49ccd56144a5a2f780f4bd374ef112 + languageName: node + linkType: hard + +"diff-sequences@npm:^29.6.3": + version: 29.6.3 + resolution: "diff-sequences@npm:29.6.3" + checksum: 32e27ac7dbffdf2fb0eb5a84efd98a9ad084fbabd5ac9abb8757c6770d5320d2acd172830b28c4add29bb873d59420601dfc805ac4064330ce59b1adfd0593b2 + languageName: node + linkType: hard + +"diffie-hellman@npm:^5.0.0": + version: 5.0.3 + resolution: "diffie-hellman@npm:5.0.3" + dependencies: + bn.js: "npm:^4.1.0" + miller-rabin: "npm:^4.0.0" + randombytes: "npm:^2.0.0" + checksum: ce53ccafa9ca544b7fc29b08a626e23a9b6562efc2a98559a0c97b4718937cebaa9b5d7d0a05032cc9c1435e9b3c1532b9e9bf2e0ede868525922807ad6e1ecf + languageName: node + linkType: hard + +"dir-glob@npm:^3.0.1": + version: 3.0.1 + resolution: "dir-glob@npm:3.0.1" + dependencies: + path-type: "npm:^4.0.0" + checksum: dcac00920a4d503e38bb64001acb19df4efc14536ada475725e12f52c16777afdee4db827f55f13a908ee7efc0cb282e2e3dbaeeb98c0993dd93d1802d3bf00c + languageName: node + linkType: hard + +"doctrine@npm:^3.0.0": + version: 3.0.0 + resolution: "doctrine@npm:3.0.0" + dependencies: + esutils: "npm:^2.0.2" + checksum: c96bdccabe9d62ab6fea9399fdff04a66e6563c1d6fb3a3a063e8d53c3bb136ba63e84250bbf63d00086a769ad53aef92d2bd483f03f837fc97b71cbee6b2520 + languageName: node + linkType: hard + +"dom-accessibility-api@npm:^0.5.9": + version: 0.5.16 + resolution: "dom-accessibility-api@npm:0.5.16" + checksum: b2c2eda4fae568977cdac27a9f0c001edf4f95a6a6191dfa611e3721db2478d1badc01db5bb4fa8a848aeee13e442a6c2a4386d65ec65a1436f24715a2f8d053 + languageName: node + linkType: hard + +"dom-helpers@npm:^5.1.3": + version: 5.2.1 + resolution: "dom-helpers@npm:5.2.1" + dependencies: + "@babel/runtime": "npm:^7.8.7" + csstype: "npm:^3.0.2" + checksum: f735074d66dd759b36b158fa26e9d00c9388ee0e8c9b16af941c38f014a37fc80782de83afefd621681b19ac0501034b4f1c4a3bff5caa1b8667f0212b5e124c + languageName: node + linkType: hard + +"dom-serializer@npm:^1.0.1": + version: 1.4.1 + resolution: "dom-serializer@npm:1.4.1" + dependencies: + domelementtype: "npm:^2.0.1" + domhandler: "npm:^4.2.0" + entities: "npm:^2.0.0" + checksum: 67d775fa1ea3de52035c98168ddcd59418356943b5eccb80e3c8b3da53adb8e37edb2cc2f885802b7b1765bf5022aec21dfc32910d7f9e6de4c3148f095ab5e0 + languageName: node + linkType: hard + +"domain-browser@npm:^1.1.1": + version: 1.2.0 + resolution: "domain-browser@npm:1.2.0" + checksum: a955f482f4b4710fbd77c12a33e77548d63603c30c80f61a80519f27e3db1ba8530b914584cc9e9365d2038753d6b5bd1f4e6c81e432b007b0ec95b8b5e69b1b + languageName: node + linkType: hard + +"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0": + version: 2.3.0 + resolution: "domelementtype@npm:2.3.0" + checksum: 686f5a9ef0fff078c1412c05db73a0dce096190036f33e400a07e2a4518e9f56b1e324f5c576a0a747ef0e75b5d985c040b0d51945ce780c0dd3c625a18cd8c9 + languageName: node + linkType: hard + +"domhandler@npm:^4.2.0, domhandler@npm:^4.3.1": + version: 4.3.1 + resolution: "domhandler@npm:4.3.1" + dependencies: + domelementtype: "npm:^2.2.0" + checksum: 5c199c7468cb052a8b5ab80b13528f0db3d794c64fc050ba793b574e158e67c93f8336e87fd81e9d5ee43b0e04aea4d8b93ed7be4899cb726a1601b3ba18538b + languageName: node + linkType: hard + +"domutils@npm:^2.8.0": + version: 2.8.0 + resolution: "domutils@npm:2.8.0" + dependencies: + dom-serializer: "npm:^1.0.1" + domelementtype: "npm:^2.2.0" + domhandler: "npm:^4.2.0" + checksum: d58e2ae01922f0dd55894e61d18119924d88091837887bf1438f2327f32c65eb76426bd9384f81e7d6dcfb048e0f83c19b222ad7101176ad68cdc9c695b563db + languageName: node + linkType: hard + +"dotenv-expand@npm:^10.0.0": + version: 10.0.0 + resolution: "dotenv-expand@npm:10.0.0" + checksum: 298f5018e29cfdcb0b5f463ba8e8627749103fbcf6cf81c561119115754ed582deee37b49dfc7253028aaba875ab7aea5fa90e5dac88e511d009ab0e6677924e + languageName: node + linkType: hard + +"dotenv@npm:^16.0.0": + version: 16.3.1 + resolution: "dotenv@npm:16.3.1" + checksum: b95ff1bbe624ead85a3cd70dbd827e8e06d5f05f716f2d0cbc476532d54c7c9469c3bc4dd93ea519f6ad711cb522c00ac9a62b6eb340d5affae8008facc3fbd7 + languageName: node + linkType: hard + +"draft-js@git+https://github.com/penpot/draft-js.git": + version: 0.11.7 + resolution: "draft-js@https://github.com/penpot/draft-js.git#commit=3119afbfa3efb80da6a7b232b0ae873a31e7acc0" + dependencies: + fbjs: "npm:^3.0.4" + immutable: "npm:~3.7.4" + object-assign: "npm:^4.1.1" + peerDependencies: + react: ">=0.14.0" + react-dom: ">=0.14.0" + checksum: 46f3dd133b174feeefe2f8cbd7b943385448727c375d0d75dc49651979cfd89d2a64347283749bc75dd789b095ce6747122c5822328f4ea15ba02ca5663ffb4b + languageName: node + linkType: hard + +"duplexify@npm:^3.5.0, duplexify@npm:^3.6.0": + version: 3.7.1 + resolution: "duplexify@npm:3.7.1" + dependencies: + end-of-stream: "npm:^1.0.0" + inherits: "npm:^2.0.1" + readable-stream: "npm:^2.0.0" + stream-shift: "npm:^1.0.0" + checksum: 59d1440c1b4e3a4db35ae96933392703ce83518db1828d06b9b6322920d6cbbf0b7159e88be120385fe459e77f1eb0c7622f26e9ec1f47c9ff05c2b35747dbd3 + languageName: node + linkType: hard + +"each-props@npm:^1.3.2": + version: 1.3.2 + resolution: "each-props@npm:1.3.2" + dependencies: + is-plain-object: "npm:^2.0.1" + object.defaults: "npm:^1.1.0" + checksum: 458eacb5703bd3d7a65c13427639980b0e7feb2a171c41af55808a04927394289c520fc0629e2d37a4fcbbb5ab3bb0c45c36ed4a86887e33c276fa823bcdc549 + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 + languageName: node + linkType: hard + +"editorconfig@npm:^1.0.4": + version: 1.0.4 + resolution: "editorconfig@npm:1.0.4" + dependencies: + "@one-ini/wasm": "npm:0.1.1" + commander: "npm:^10.0.0" + minimatch: "npm:9.0.1" + semver: "npm:^7.5.3" + bin: + editorconfig: bin/editorconfig + checksum: ed6985959d7b34a56e1c09bef118758c81c969489b768d152c93689fce8403b0452462e934f665febaba3478eebc0fd41c0a36100783eaadf6d926c4abc87a3d + languageName: node + linkType: hard + +"ee-first@npm:1.1.1": + version: 1.1.1 + resolution: "ee-first@npm:1.1.1" + checksum: b5bb125ee93161bc16bfe6e56c6b04de5ad2aa44234d8f644813cc95d861a6910903132b05093706de2b706599367c4130eb6d170f6b46895686b95f87d017b7 + languageName: node + linkType: hard + +"ejs@npm:^3.1.8": + version: 3.1.9 + resolution: "ejs@npm:3.1.9" + dependencies: + jake: "npm:^10.8.5" + bin: + ejs: bin/cli.js + checksum: f0e249c79128810f5f6d5cbf347fc906d86bb9384263db0b2a9004aea649f2bc2d112736de5716c509c80afb4721c47281bd5b57c757d3b63f1bf5ac5f885893 + languageName: node + linkType: hard + +"electron-to-chromium@npm:^1.4.535": + version: 1.4.596 + resolution: "electron-to-chromium@npm:1.4.596" + checksum: 6e05fdbe0a77beda4eaad646c83143ccf4dcec5b15da7dc641bbd872ef62acff4cb31e1febf4bafdbe8e1f61720c2ff738690acce0b8dac980a331802633befd + languageName: node + linkType: hard + +"electron-to-chromium@npm:^1.4.668": + version: 1.4.681 + resolution: "electron-to-chromium@npm:1.4.681" + checksum: 5b2558dfb8bb82c20fb5fa1d9bbe06a3add47431dc3e1e4815e997be6ad387787047d9e534ed96839a9e7012520a5281c865158b09db41d10c029af003f05f94 + languageName: node + linkType: hard + +"elliptic@npm:^6.5.3, elliptic@npm:^6.5.4": + version: 6.5.4 + resolution: "elliptic@npm:6.5.4" + dependencies: + bn.js: "npm:^4.11.9" + brorand: "npm:^1.1.0" + hash.js: "npm:^1.0.0" + hmac-drbg: "npm:^1.0.1" + inherits: "npm:^2.0.4" + minimalistic-assert: "npm:^1.0.1" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: 5f361270292c3b27cf0843e84526d11dec31652f03c2763c6c2b8178548175ff5eba95341dd62baff92b2265d1af076526915d8af6cc9cb7559c44a62f8ca6e2 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 + languageName: node + linkType: hard + +"enabled@npm:2.0.x": + version: 2.0.0 + resolution: "enabled@npm:2.0.0" + checksum: 3b2c2af9bc7f8b9e291610f2dde4a75cf6ee52a68f4dd585482fbdf9a55d65388940e024e56d40bb03e05ef6671f5f53021fa8b72a20e954d7066ec28166713f + languageName: node + linkType: hard + +"encodeurl@npm:~1.0.2": + version: 1.0.2 + resolution: "encodeurl@npm:1.0.2" + checksum: f6c2387379a9e7c1156c1c3d4f9cb7bb11cf16dd4c1682e1f6746512564b053df5781029b6061296832b59fb22f459dbe250386d217c2f6e203601abb2ee0bec + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: 36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + languageName: node + linkType: hard + +"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": + version: 1.4.4 + resolution: "end-of-stream@npm:1.4.4" + dependencies: + once: "npm:^1.4.0" + checksum: 870b423afb2d54bb8d243c63e07c170409d41e20b47eeef0727547aea5740bd6717aca45597a9f2745525667a6b804c1e7bede41f856818faee5806dd9ff3975 + languageName: node + linkType: hard + +"entities@npm:^2.0.0": + version: 2.2.0 + resolution: "entities@npm:2.2.0" + checksum: 7fba6af1f116300d2ba1c5673fc218af1961b20908638391b4e1e6d5850314ee2ac3ec22d741b3a8060479911c99305164aed19b6254bde75e7e6b1b2c3f3aa3 + languageName: node + linkType: hard + +"entities@npm:^4.4.0": + version: 4.5.0 + resolution: "entities@npm:4.5.0" + checksum: 5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + +"envinfo@npm:^7.7.3": + version: 7.11.0 + resolution: "envinfo@npm:7.11.0" + bin: + envinfo: dist/cli.js + checksum: 4415b9c1ca32cdf92ce126136b9965eeac2efd6ab7e5278c06e8f86d048edad87ef4084710313a6d938ef9bc084ab17e1caee16339d731d230f3e2650f3aaf4d + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + languageName: node + linkType: hard + +"error-ex@npm:^1.2.0, error-ex@npm:^1.3.1": + version: 1.3.2 + resolution: "error-ex@npm:1.3.2" + dependencies: + is-arrayish: "npm:^0.2.1" + checksum: ba827f89369b4c93382cfca5a264d059dfefdaa56ecc5e338ffa58a6471f5ed93b71a20add1d52290a4873d92381174382658c885ac1a2305f7baca363ce9cce + languageName: node + linkType: hard + +"es-abstract@npm:^1.22.1": + version: 1.22.3 + resolution: "es-abstract@npm:1.22.3" + dependencies: + array-buffer-byte-length: "npm:^1.0.0" + arraybuffer.prototype.slice: "npm:^1.0.2" + available-typed-arrays: "npm:^1.0.5" + call-bind: "npm:^1.0.5" + es-set-tostringtag: "npm:^2.0.1" + es-to-primitive: "npm:^1.2.1" + function.prototype.name: "npm:^1.1.6" + get-intrinsic: "npm:^1.2.2" + get-symbol-description: "npm:^1.0.0" + globalthis: "npm:^1.0.3" + gopd: "npm:^1.0.1" + has-property-descriptors: "npm:^1.0.0" + has-proto: "npm:^1.0.1" + has-symbols: "npm:^1.0.3" + hasown: "npm:^2.0.0" + internal-slot: "npm:^1.0.5" + is-array-buffer: "npm:^3.0.2" + is-callable: "npm:^1.2.7" + is-negative-zero: "npm:^2.0.2" + is-regex: "npm:^1.1.4" + is-shared-array-buffer: "npm:^1.0.2" + is-string: "npm:^1.0.7" + is-typed-array: "npm:^1.1.12" + is-weakref: "npm:^1.0.2" + object-inspect: "npm:^1.13.1" + object-keys: "npm:^1.1.1" + object.assign: "npm:^4.1.4" + regexp.prototype.flags: "npm:^1.5.1" + safe-array-concat: "npm:^1.0.1" + safe-regex-test: "npm:^1.0.0" + string.prototype.trim: "npm:^1.2.8" + string.prototype.trimend: "npm:^1.0.7" + string.prototype.trimstart: "npm:^1.0.7" + typed-array-buffer: "npm:^1.0.0" + typed-array-byte-length: "npm:^1.0.0" + typed-array-byte-offset: "npm:^1.0.0" + typed-array-length: "npm:^1.0.4" + unbox-primitive: "npm:^1.0.2" + which-typed-array: "npm:^1.1.13" + checksum: da31ec43b1c8eb47ba8a17693cac143682a1078b6c3cd883ce0e2062f135f532e93d873694ef439670e1f6ca03195118f43567ba6f33fb0d6c7daae750090236 + languageName: node + linkType: hard + +"es-get-iterator@npm:^1.1.3": + version: 1.1.3 + resolution: "es-get-iterator@npm:1.1.3" + dependencies: + call-bind: "npm:^1.0.2" + get-intrinsic: "npm:^1.1.3" + has-symbols: "npm:^1.0.3" + is-arguments: "npm:^1.1.1" + is-map: "npm:^2.0.2" + is-set: "npm:^2.0.2" + is-string: "npm:^1.0.7" + isarray: "npm:^2.0.5" + stop-iteration-iterator: "npm:^1.0.0" + checksum: ebd11effa79851ea75d7f079405f9d0dc185559fd65d986c6afea59a0ff2d46c2ed8675f19f03dce7429d7f6c14ff9aede8d121fbab78d75cfda6a263030bac0 + languageName: node + linkType: hard + +"es-module-lexer@npm:^0.9.3": + version: 0.9.3 + resolution: "es-module-lexer@npm:0.9.3" + checksum: be77d73aee709fdc68d22b9938da81dfee3bc45e8d601629258643fe5bfdab253d6e2540035e035cfa8cf52a96366c1c19b46bcc23b4507b1d44e5907d2e7f6c + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.0.1": + version: 2.0.2 + resolution: "es-set-tostringtag@npm:2.0.2" + dependencies: + get-intrinsic: "npm:^1.2.2" + has-tostringtag: "npm:^1.0.0" + hasown: "npm:^2.0.0" + checksum: 176d6bd1be31dd0145dcceee62bb78d4a5db7f81db437615a18308a6f62bcffe45c15081278413455e8cf0aad4ea99079de66f8de389605942dfdacbad74c2d5 + languageName: node + linkType: hard + +"es-to-primitive@npm:^1.2.1": + version: 1.2.1 + resolution: "es-to-primitive@npm:1.2.1" + dependencies: + is-callable: "npm:^1.1.4" + is-date-object: "npm:^1.0.1" + is-symbol: "npm:^1.0.2" + checksum: 0886572b8dc075cb10e50c0af62a03d03a68e1e69c388bd4f10c0649ee41b1fbb24840a1b7e590b393011b5cdbe0144b776da316762653685432df37d6de60f1 + languageName: node + linkType: hard + +"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.46, es5-ext@npm:^0.10.50, es5-ext@npm:^0.10.53, es5-ext@npm:~0.10.14, es5-ext@npm:~0.10.2, es5-ext@npm:~0.10.46": + version: 0.10.62 + resolution: "es5-ext@npm:0.10.62" + dependencies: + es6-iterator: "npm:^2.0.3" + es6-symbol: "npm:^3.1.3" + next-tick: "npm:^1.1.0" + checksum: 72dfbec5e4bce24754be9f2c2a1c67c01de3fe000103c115f52891f6a51f44a59674c40a1f6bd2390fcd43987746dccb76efafea91c7bb6295bdca8d63ba3db4 + languageName: node + linkType: hard + +"es6-iterator@npm:^2.0.1, es6-iterator@npm:^2.0.3": + version: 2.0.3 + resolution: "es6-iterator@npm:2.0.3" + dependencies: + d: "npm:1" + es5-ext: "npm:^0.10.35" + es6-symbol: "npm:^3.1.1" + checksum: 91f20b799dba28fb05bf623c31857fc1524a0f1c444903beccaf8929ad196c8c9ded233e5ac7214fc63a92b3f25b64b7f2737fcca8b1f92d2d96cf3ac902f5d8 + languageName: node + linkType: hard + +"es6-symbol@npm:^3.1.1, es6-symbol@npm:^3.1.3": + version: 3.1.3 + resolution: "es6-symbol@npm:3.1.3" + dependencies: + d: "npm:^1.0.1" + ext: "npm:^1.1.2" + checksum: 22982f815f00df553a89f4fb74c5048fed85df598482b4bd38dbd173174247949c72982a7d7132a58b147525398400e5f182db59b0916cb49f1e245fb0e22233 + languageName: node + linkType: hard + +"es6-weak-map@npm:^2.0.1, es6-weak-map@npm:^2.0.3": + version: 2.0.3 + resolution: "es6-weak-map@npm:2.0.3" + dependencies: + d: "npm:1" + es5-ext: "npm:^0.10.46" + es6-iterator: "npm:^2.0.3" + es6-symbol: "npm:^3.1.1" + checksum: 460932be9542473dbbddd183e21c15a66cfec1b2c17dae2b514e190d6fb2896b7eb683783d4b36da036609d2e1c93d2815f21b374dfccaf02a8978694c2f7b67 + languageName: node + linkType: hard + +"esbuild-plugin-alias@npm:^0.2.1": + version: 0.2.1 + resolution: "esbuild-plugin-alias@npm:0.2.1" + checksum: a67bc6bc2744fc8637f7321f00c1f00e4fae86c182662421738ebfabf3ad344967b9c667185c6c34d9edd5b289807d34bfdceef94620e94e0a45683534af69e0 + languageName: node + linkType: hard + +"esbuild-register@npm:^3.5.0": + version: 3.5.0 + resolution: "esbuild-register@npm:3.5.0" + dependencies: + debug: "npm:^4.3.4" + peerDependencies: + esbuild: ">=0.12 <1" + checksum: 9ccd0573cb66018e4cce3c1416eed0f5f3794c7026ce469a94e2f8761335abed8e363fc8e8bb036ab9ad7e579bb4296b8568a04ae5626596c123576b0d9c9bde + languageName: node + linkType: hard + +"esbuild@npm:^0.18.0": + version: 0.18.20 + resolution: "esbuild@npm:0.18.20" + dependencies: + "@esbuild/android-arm": "npm:0.18.20" + "@esbuild/android-arm64": "npm:0.18.20" + "@esbuild/android-x64": "npm:0.18.20" + "@esbuild/darwin-arm64": "npm:0.18.20" + "@esbuild/darwin-x64": "npm:0.18.20" + "@esbuild/freebsd-arm64": "npm:0.18.20" + "@esbuild/freebsd-x64": "npm:0.18.20" + "@esbuild/linux-arm": "npm:0.18.20" + "@esbuild/linux-arm64": "npm:0.18.20" + "@esbuild/linux-ia32": "npm:0.18.20" + "@esbuild/linux-loong64": "npm:0.18.20" + "@esbuild/linux-mips64el": "npm:0.18.20" + "@esbuild/linux-ppc64": "npm:0.18.20" + "@esbuild/linux-riscv64": "npm:0.18.20" + "@esbuild/linux-s390x": "npm:0.18.20" + "@esbuild/linux-x64": "npm:0.18.20" + "@esbuild/netbsd-x64": "npm:0.18.20" + "@esbuild/openbsd-x64": "npm:0.18.20" + "@esbuild/sunos-x64": "npm:0.18.20" + "@esbuild/win32-arm64": "npm:0.18.20" + "@esbuild/win32-ia32": "npm:0.18.20" + "@esbuild/win32-x64": "npm:0.18.20" + dependenciesMeta: + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 473b1d92842f50a303cf948a11ebd5f69581cd254d599dd9d62f9989858e0533f64e83b723b5e1398a5b488c0f5fd088795b4235f65ecaf4f007d4b79f04bc88 + languageName: node + linkType: hard + +"esbuild@npm:^0.19.3": + version: 0.19.8 + resolution: "esbuild@npm:0.19.8" + dependencies: + "@esbuild/android-arm": "npm:0.19.8" + "@esbuild/android-arm64": "npm:0.19.8" + "@esbuild/android-x64": "npm:0.19.8" + "@esbuild/darwin-arm64": "npm:0.19.8" + "@esbuild/darwin-x64": "npm:0.19.8" + "@esbuild/freebsd-arm64": "npm:0.19.8" + "@esbuild/freebsd-x64": "npm:0.19.8" + "@esbuild/linux-arm": "npm:0.19.8" + "@esbuild/linux-arm64": "npm:0.19.8" + "@esbuild/linux-ia32": "npm:0.19.8" + "@esbuild/linux-loong64": "npm:0.19.8" + "@esbuild/linux-mips64el": "npm:0.19.8" + "@esbuild/linux-ppc64": "npm:0.19.8" + "@esbuild/linux-riscv64": "npm:0.19.8" + "@esbuild/linux-s390x": "npm:0.19.8" + "@esbuild/linux-x64": "npm:0.19.8" + "@esbuild/netbsd-x64": "npm:0.19.8" + "@esbuild/openbsd-x64": "npm:0.19.8" + "@esbuild/sunos-x64": "npm:0.19.8" + "@esbuild/win32-arm64": "npm:0.19.8" + "@esbuild/win32-ia32": "npm:0.19.8" + "@esbuild/win32-x64": "npm:0.19.8" + dependenciesMeta: + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 57d7d0bc40965bdd9d4c2d76d7f9b8890c59d764e2e3820d3b01af03b6187a90efc0acf05ec900d66672c15760d7377bd22d9330d302fecc492b27065c6941a6 + languageName: node + linkType: hard + +"escalade@npm:^3.1.1": + version: 3.1.1 + resolution: "escalade@npm:3.1.1" + checksum: afd02e6ca91ffa813e1108b5e7756566173d6bc0d1eb951cb44d6b21702ec17c1cf116cfe75d4a2b02e05acb0b808a7a9387d0d1ca5cf9c04ad03a8445c3e46d + languageName: node + linkType: hard + +"escape-html@npm:~1.0.3": + version: 1.0.3 + resolution: "escape-html@npm:1.0.3" + checksum: 524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^1.0.5": + version: 1.0.5 + resolution: "escape-string-regexp@npm:1.0.5" + checksum: a968ad453dd0c2724e14a4f20e177aaf32bb384ab41b674a8454afe9a41c5e6fe8903323e0a1052f56289d04bd600f81278edf140b0fcc02f5cac98d0f5b5371 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^2.0.0": + version: 2.0.0 + resolution: "escape-string-regexp@npm:2.0.0" + checksum: 2530479fe8db57eace5e8646c9c2a9c80fa279614986d16dcc6bcaceb63ae77f05a851ba6c43756d816c61d7f4534baf56e3c705e3e0d884818a46808811c507 + languageName: node + linkType: hard + +"escodegen@npm:^2.1.0": + version: 2.1.0 + resolution: "escodegen@npm:2.1.0" + dependencies: + esprima: "npm:^4.0.1" + estraverse: "npm:^5.2.0" + esutils: "npm:^2.0.2" + source-map: "npm:~0.6.1" + dependenciesMeta: + source-map: + optional: true + bin: + escodegen: bin/escodegen.js + esgenerate: bin/esgenerate.js + checksum: e1450a1f75f67d35c061bf0d60888b15f62ab63aef9df1901cffc81cffbbb9e8b3de237c5502cf8613a017c1df3a3003881307c78835a1ab54d8c8d2206e01d3 + languageName: node + linkType: hard + +"esprima@npm:^4.0.0, esprima@npm:^4.0.1, esprima@npm:~4.0.0": + version: 4.0.1 + resolution: "esprima@npm:4.0.1" + bin: + esparse: ./bin/esparse.js + esvalidate: ./bin/esvalidate.js + checksum: ad4bab9ead0808cf56501750fd9d3fb276f6b105f987707d059005d57e182d18a7c9ec7f3a01794ebddcca676773e42ca48a32d67a250c9d35e009ca613caba3 + languageName: node + linkType: hard + +"estraverse@npm:^5.2.0": + version: 5.3.0 + resolution: "estraverse@npm:5.3.0" + checksum: 1ff9447b96263dec95d6d67431c5e0771eb9776427421260a3e2f0fdd5d6bd4f8e37a7338f5ad2880c9f143450c9b1e4fc2069060724570a49cf9cf0312bd107 + languageName: node + linkType: hard + +"estree-walker@npm:^2.0.2": + version: 2.0.2 + resolution: "estree-walker@npm:2.0.2" + checksum: 53a6c54e2019b8c914dc395890153ffdc2322781acf4bd7d1a32d7aedc1710807bdcd866ac133903d5629ec601fbb50abe8c2e5553c7f5a0afdd9b6af6c945af + languageName: node + linkType: hard + +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d + languageName: node + linkType: hard + +"esutils@npm:^2.0.2": + version: 2.0.3 + resolution: "esutils@npm:2.0.3" + checksum: 9a2fe69a41bfdade834ba7c42de4723c97ec776e40656919c62cbd13607c45e127a003f05f724a1ea55e5029a4cf2de444b13009f2af71271e42d93a637137c7 + languageName: node + linkType: hard + +"etag@npm:~1.8.1": + version: 1.8.1 + resolution: "etag@npm:1.8.1" + checksum: 12be11ef62fb9817314d790089a0a49fae4e1b50594135dcb8076312b7d7e470884b5100d249b28c18581b7fd52f8b485689ffae22a11ed9ec17377a33a08f84 + languageName: node + linkType: hard + +"event-emitter@npm:^0.3.5": + version: 0.3.5 + resolution: "event-emitter@npm:0.3.5" + dependencies: + d: "npm:1" + es5-ext: "npm:~0.10.14" + checksum: 75082fa8ffb3929766d0f0a063bfd6046bd2a80bea2666ebaa0cfd6f4a9116be6647c15667bea77222afc12f5b4071b68d393cf39fdaa0e8e81eda006160aff0 + languageName: node + linkType: hard + +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 0255d9f936215fd206156fd4caa9e8d35e62075d720dc7d847e89b417e5e62cf1ce6c9b4e0a1633a9256de0efefaf9f8d26924b1f3c8620cffb9db78e7d3076b + languageName: node + linkType: hard + +"events@npm:^3.0.0, events@npm:^3.3.0": + version: 3.3.0 + resolution: "events@npm:3.3.0" + checksum: d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 + languageName: node + linkType: hard + +"eventsource-parser@npm:^1.1.2": + version: 1.1.2 + resolution: "eventsource-parser@npm:1.1.2" + checksum: b38948bc81ae6c2a8b9c88383d4f8c2bfbaf23955827a9af68d39bc0550ae83cc400b197e814bea9aef6e0cdc9bae5afd95787418ee3d9ad01ffc4774cf1b84a + languageName: node + linkType: hard + +"evp_bytestokey@npm:^1.0.0, evp_bytestokey@npm:^1.0.3": + version: 1.0.3 + resolution: "evp_bytestokey@npm:1.0.3" + dependencies: + md5.js: "npm:^1.3.4" + node-gyp: "npm:latest" + safe-buffer: "npm:^5.1.1" + checksum: 77fbe2d94a902a80e9b8f5a73dcd695d9c14899c5e82967a61b1fc6cbbb28c46552d9b127cff47c45fcf684748bdbcfa0a50410349109de87ceb4b199ef6ee99 + languageName: node + linkType: hard + +"execa@npm:^5.0.0, execa@npm:^5.1.1": + version: 5.1.1 + resolution: "execa@npm:5.1.1" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^6.0.0" + human-signals: "npm:^2.1.0" + is-stream: "npm:^2.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^4.0.1" + onetime: "npm:^5.1.2" + signal-exit: "npm:^3.0.3" + strip-final-newline: "npm:^2.0.0" + checksum: c8e615235e8de4c5addf2fa4c3da3e3aa59ce975a3e83533b4f6a71750fb816a2e79610dc5f1799b6e28976c9ae86747a36a606655bf8cb414a74d8d507b304f + languageName: node + linkType: hard + +"execa@npm:^8.0.1": + version: 8.0.1 + resolution: "execa@npm:8.0.1" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^8.0.1" + human-signals: "npm:^5.0.0" + is-stream: "npm:^3.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^5.1.0" + onetime: "npm:^6.0.0" + signal-exit: "npm:^4.1.0" + strip-final-newline: "npm:^3.0.0" + checksum: 2c52d8775f5bf103ce8eec9c7ab3059909ba350a5164744e9947ed14a53f51687c040a250bda833f906d1283aa8803975b84e6c8f7a7c42f99dc8ef80250d1af + languageName: node + linkType: hard + +"expand-brackets@npm:^2.1.4": + version: 2.1.4 + resolution: "expand-brackets@npm:2.1.4" + dependencies: + debug: "npm:^2.3.3" + define-property: "npm:^0.2.5" + extend-shallow: "npm:^2.0.1" + posix-character-classes: "npm:^0.1.0" + regex-not: "npm:^1.0.0" + snapdragon: "npm:^0.8.1" + to-regex: "npm:^3.0.1" + checksum: 3e2fb95d2d7d7231486493fd65db913927b656b6fcdfcce41e139c0991a72204af619ad4acb1be75ed994ca49edb7995ef241dbf8cf44dc3c03d211328428a87 + languageName: node + linkType: hard + +"expand-tilde@npm:^2.0.0, expand-tilde@npm:^2.0.2": + version: 2.0.2 + resolution: "expand-tilde@npm:2.0.2" + dependencies: + homedir-polyfill: "npm:^1.0.1" + checksum: 205a60497422746d1c3acbc1d65bd609b945066f239a2b785e69a7a651ac4cbeb4e08555b1ea0023abbe855e6fcb5bbf27d0b371367fdccd303d4fb2b4d66845 + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 + languageName: node + linkType: hard + +"express@npm:^4.17.3": + version: 4.18.2 + resolution: "express@npm:4.18.2" + dependencies: + accepts: "npm:~1.3.8" + array-flatten: "npm:1.1.1" + body-parser: "npm:1.20.1" + content-disposition: "npm:0.5.4" + content-type: "npm:~1.0.4" + cookie: "npm:0.5.0" + cookie-signature: "npm:1.0.6" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + finalhandler: "npm:1.2.0" + fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" + merge-descriptors: "npm:1.0.1" + methods: "npm:~1.1.2" + on-finished: "npm:2.4.1" + parseurl: "npm:~1.3.3" + path-to-regexp: "npm:0.1.7" + proxy-addr: "npm:~2.0.7" + qs: "npm:6.11.0" + range-parser: "npm:~1.2.1" + safe-buffer: "npm:5.2.1" + send: "npm:0.18.0" + serve-static: "npm:1.15.0" + setprototypeof: "npm:1.2.0" + statuses: "npm:2.0.1" + type-is: "npm:~1.6.18" + utils-merge: "npm:1.0.1" + vary: "npm:~1.1.2" + checksum: 75af556306b9241bc1d7bdd40c9744b516c38ce50ae3210658efcbf96e3aed4ab83b3432f06215eae5610c123bc4136957dc06e50dfc50b7d4d775af56c4c59c + languageName: node + linkType: hard + +"ext@npm:^1.1.2": + version: 1.7.0 + resolution: "ext@npm:1.7.0" + dependencies: + type: "npm:^2.7.2" + checksum: a8e5f34e12214e9eee3a4af3b5c9d05ba048f28996450975b369fc86e5d0ef13b6df0615f892f5396a9c65d616213c25ec5b0ad17ef42eac4a500512a19da6c7 + languageName: node + linkType: hard + +"extend-shallow@npm:^2.0.1": + version: 2.0.1 + resolution: "extend-shallow@npm:2.0.1" + dependencies: + is-extendable: "npm:^0.1.0" + checksum: ee1cb0a18c9faddb42d791b2d64867bd6cfd0f3affb711782eb6e894dd193e2934a7f529426aac7c8ddb31ac5d38000a00aa2caf08aa3dfc3e1c8ff6ba340bd9 + languageName: node + linkType: hard + +"extend-shallow@npm:^3.0.0, extend-shallow@npm:^3.0.2": + version: 3.0.2 + resolution: "extend-shallow@npm:3.0.2" + dependencies: + assign-symbols: "npm:^1.0.0" + is-extendable: "npm:^1.0.1" + checksum: f39581b8f98e3ad94995e33214fff725b0297cf09f2725b6f624551cfb71e0764accfd0af80becc0182af5014d2a57b31b85ec999f9eb8a6c45af81752feac9a + languageName: node + linkType: hard + +"extend@npm:^3.0.0": + version: 3.0.2 + resolution: "extend@npm:3.0.2" + checksum: 73bf6e27406e80aa3e85b0d1c4fd987261e628064e170ca781125c0b635a3dabad5e05adbf07595ea0cf1e6c5396cacb214af933da7cbaf24fe75ff14818e8f9 + languageName: node + linkType: hard + +"extglob@npm:^2.0.4": + version: 2.0.4 + resolution: "extglob@npm:2.0.4" + dependencies: + array-unique: "npm:^0.3.2" + define-property: "npm:^1.0.0" + expand-brackets: "npm:^2.1.4" + extend-shallow: "npm:^2.0.1" + fragment-cache: "npm:^0.2.1" + regex-not: "npm:^1.0.0" + snapdragon: "npm:^0.8.1" + to-regex: "npm:^3.0.1" + checksum: e1a891342e2010d046143016c6c03d58455c2c96c30bf5570ea07929984ee7d48fad86b363aee08f7a8a638f5c3a66906429b21ecb19bc8e90df56a001cd282c + languageName: node + linkType: hard + +"extract-zip@npm:^1.6.6": + version: 1.7.0 + resolution: "extract-zip@npm:1.7.0" + dependencies: + concat-stream: "npm:^1.6.2" + debug: "npm:^2.6.9" + mkdirp: "npm:^0.5.4" + yauzl: "npm:^2.10.0" + bin: + extract-zip: cli.js + checksum: 333f1349ee678d47268315f264dbfcd7003747d25640441e186e87c66efd7129f171f1bcfe8ff1151a24da19d5f8602daff002ee24145dc65516bc9a8e40ee08 + languageName: node + linkType: hard + +"fancy-log@npm:^1.3.2": + version: 1.3.3 + resolution: "fancy-log@npm:1.3.3" + dependencies: + ansi-gray: "npm:^0.1.1" + color-support: "npm:^1.1.3" + parse-node-version: "npm:^1.0.0" + time-stamp: "npm:^1.0.0" + checksum: 2fd9070191c8671065fbe3523d283b4a4eb240e37121e99b3b3260b2ea2934961b166cf48dcadeb6cdce877039e27499f1403808b455bd29b1b66060a03eb041 + languageName: node + linkType: hard + +"fancy-log@npm:^2.0.0": + version: 2.0.0 + resolution: "fancy-log@npm:2.0.0" + dependencies: + color-support: "npm:^1.1.3" + checksum: a6e116f3346756a7363eea343b551c1375d2cd2afc2a92d224feb78589b6b3cff85db9dc5d5df89792a0f7c1e17f731f52cb3d2807302f0516422be6269b95a8 + languageName: node + linkType: hard + +"fast-glob@npm:^3.2.9": + version: 3.3.2 + resolution: "fast-glob@npm:3.3.2" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.4" + checksum: 42baad7b9cd40b63e42039132bde27ca2cb3a4950d0a0f9abe4639ea1aa9d3e3b40f98b1fe31cbc0cc17b664c9ea7447d911a152fa34ec5b72977b125a6fc845 + languageName: node + linkType: hard + +"fast-json-stable-stringify@npm:^2.1.0": + version: 2.1.0 + resolution: "fast-json-stable-stringify@npm:2.1.0" + checksum: 7f081eb0b8a64e0057b3bb03f974b3ef00135fbf36c1c710895cd9300f13c94ba809bb3a81cf4e1b03f6e5285610a61abbd7602d0652de423144dfee5a389c9b + languageName: node + linkType: hard + +"fast-levenshtein@npm:^1.0.0": + version: 1.1.4 + resolution: "fast-levenshtein@npm:1.1.4" + checksum: 667ff83888eefb3f9d1e0bc6b1a67e40212784d0f4049d8607a1cf01cc7e0b71047bad23f9e1403e1e43de8f1180e23a0352ddb6bc502a18d2065dff5ccbcdc8 + languageName: node + linkType: hard + +"fastq@npm:^1.6.0": + version: 1.15.0 + resolution: "fastq@npm:1.15.0" + dependencies: + reusify: "npm:^1.0.4" + checksum: 5ce4f83afa5f88c9379e67906b4d31bc7694a30826d6cc8d0f0473c966929017fda65c2174b0ec89f064ede6ace6c67f8a4fe04cef42119b6a55b0d465554c24 + languageName: node + linkType: hard + +"fb-watchman@npm:^2.0.0": + version: 2.0.2 + resolution: "fb-watchman@npm:2.0.2" + dependencies: + bser: "npm:2.1.1" + checksum: feae89ac148adb8f6ae8ccd87632e62b13563e6fb114cacb5265c51f585b17e2e268084519fb2edd133872f1d47a18e6bfd7e5e08625c0d41b93149694187581 + languageName: node + linkType: hard + +"fbjs-css-vars@npm:^1.0.0": + version: 1.0.2 + resolution: "fbjs-css-vars@npm:1.0.2" + checksum: dfb64116b125a64abecca9e31477b5edb9a2332c5ffe74326fe36e0a72eef7fc8a49b86adf36c2c293078d79f4524f35e80f5e62546395f53fb7c9e69821f54f + languageName: node + linkType: hard + +"fbjs@npm:^3.0.4": + version: 3.0.5 + resolution: "fbjs@npm:3.0.5" + dependencies: + cross-fetch: "npm:^3.1.5" + fbjs-css-vars: "npm:^1.0.0" + loose-envify: "npm:^1.0.0" + object-assign: "npm:^4.1.0" + promise: "npm:^7.1.1" + setimmediate: "npm:^1.0.5" + ua-parser-js: "npm:^1.0.35" + checksum: 66d0a2fc9a774f9066e35ac2ac4bf1245931d27f3ac287c7d47e6aa1fc152b243c2109743eb8f65341e025621fb51a12038fadb9fd8fda2e3ddae04ebab06f91 + languageName: node + linkType: hard + +"fd-slicer@npm:~1.1.0": + version: 1.1.0 + resolution: "fd-slicer@npm:1.1.0" + dependencies: + pend: "npm:~1.2.0" + checksum: 304dd70270298e3ffe3bcc05e6f7ade2511acc278bc52d025f8918b48b6aa3b77f10361bddfadfe2a28163f7af7adbdce96f4d22c31b2f648ba2901f0c5fc20e + languageName: node + linkType: hard + +"fecha@npm:^4.2.0": + version: 4.2.3 + resolution: "fecha@npm:4.2.3" + checksum: 0e895965959cf6a22bb7b00f0bf546f2783836310f510ddf63f463e1518d4c96dec61ab33fdfd8e79a71b4856a7c865478ce2ee8498d560fe125947703c9b1cf + languageName: node + linkType: hard + +"fetch-retry@npm:^5.0.2": + version: 5.0.6 + resolution: "fetch-retry@npm:5.0.6" + checksum: 349f50db631039630e915f70c763469cb696f3ac92ca6f63823109334a2bc62f63670b8c5a5c7e0195c39df517e60ef385cc5264f4c4904d0c6707d371fa8999 + languageName: node + linkType: hard + +"file-system-cache@npm:2.3.0": + version: 2.3.0 + resolution: "file-system-cache@npm:2.3.0" + dependencies: + fs-extra: "npm:11.1.1" + ramda: "npm:0.29.0" + checksum: 43de19f0db32e6546bb7abeecb1d6ea83c1eca23b38905c9415a29f6219cc9d6d87b0c1a6aca92c46a0f1bc276241a339f2f68b8aa0ca5c2eb64b6e1e3e4da01 + languageName: node + linkType: hard + +"file-uri-to-path@npm:1.0.0": + version: 1.0.0 + resolution: "file-uri-to-path@npm:1.0.0" + checksum: 3b545e3a341d322d368e880e1c204ef55f1d45cdea65f7efc6c6ce9e0c4d22d802d5629320eb779d006fe59624ac17b0e848d83cc5af7cd101f206cb704f5519 + languageName: node + linkType: hard + +"filelist@npm:^1.0.4": + version: 1.0.4 + resolution: "filelist@npm:1.0.4" + dependencies: + minimatch: "npm:^5.0.1" + checksum: 426b1de3944a3d153b053f1c0ebfd02dccd0308a4f9e832ad220707a6d1f1b3c9784d6cadf6b2f68f09a57565f63ebc7bcdc913ccf8012d834f472c46e596f41 + languageName: node + linkType: hard + +"fill-range@npm:^4.0.0": + version: 4.0.0 + resolution: "fill-range@npm:4.0.0" + dependencies: + extend-shallow: "npm:^2.0.1" + is-number: "npm:^3.0.0" + repeat-string: "npm:^1.6.1" + to-regex-range: "npm:^2.1.0" + checksum: ccd57b7c43d7e28a1f8a60adfa3c401629c08e2f121565eece95e2386ebc64dedc7128d8c3448342aabf19db0c55a34f425f148400c7a7be9a606ba48749e089 + languageName: node + linkType: hard + +"fill-range@npm:^7.0.1": + version: 7.0.1 + resolution: "fill-range@npm:7.0.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: 7cdad7d426ffbaadf45aeb5d15ec675bbd77f7597ad5399e3d2766987ed20bda24d5fac64b3ee79d93276f5865608bb22344a26b9b1ae6c4d00bd94bf611623f + languageName: node + linkType: hard + +"finalhandler@npm:1.2.0": + version: 1.2.0 + resolution: "finalhandler@npm:1.2.0" + dependencies: + debug: "npm:2.6.9" + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + on-finished: "npm:2.4.1" + parseurl: "npm:~1.3.3" + statuses: "npm:2.0.1" + unpipe: "npm:~1.0.0" + checksum: 64b7e5ff2ad1fcb14931cd012651631b721ce657da24aedb5650ddde9378bf8e95daa451da43398123f5de161a81e79ff5affe4f9f2a6d2df4a813d6d3e254b7 + languageName: node + linkType: hard + +"find-cache-dir@npm:^2.0.0": + version: 2.1.0 + resolution: "find-cache-dir@npm:2.1.0" + dependencies: + commondir: "npm:^1.0.1" + make-dir: "npm:^2.0.0" + pkg-dir: "npm:^3.0.0" + checksum: 556117fd0af14eb88fb69250f4bba9e905e7c355c6136dff0e161b9cbd1f5285f761b778565a278da73a130f42eccc723d7ad4c002ae547ed1d698d39779dabb + languageName: node + linkType: hard + +"find-cache-dir@npm:^3.0.0": + version: 3.3.2 + resolution: "find-cache-dir@npm:3.3.2" + dependencies: + commondir: "npm:^1.0.1" + make-dir: "npm:^3.0.2" + pkg-dir: "npm:^4.1.0" + checksum: 92747cda42bff47a0266b06014610981cfbb71f55d60f2c8216bc3108c83d9745507fb0b14ecf6ab71112bed29cd6fb1a137ee7436179ea36e11287e3159e587 + languageName: node + linkType: hard + +"find-up@npm:^1.0.0": + version: 1.1.2 + resolution: "find-up@npm:1.1.2" + dependencies: + path-exists: "npm:^2.0.0" + pinkie-promise: "npm:^2.0.0" + checksum: 51e35c62d9b7efe82d7d5cce966bfe10c2eaa78c769333f8114627e3a8a4a4f50747f5f50bff50b1094cbc6527776f0d3b9ff74d3561ef714a5290a17c80c2bc + languageName: node + linkType: hard + +"find-up@npm:^3.0.0": + version: 3.0.0 + resolution: "find-up@npm:3.0.0" + dependencies: + locate-path: "npm:^3.0.0" + checksum: 2c2e7d0a26db858e2f624f39038c74739e38306dee42b45f404f770db357947be9d0d587f1cac72d20c114deb38aa57316e879eb0a78b17b46da7dab0a3bd6e3 + languageName: node + linkType: hard + +"find-up@npm:^4.0.0, find-up@npm:^4.1.0": + version: 4.1.0 + resolution: "find-up@npm:4.1.0" + dependencies: + locate-path: "npm:^5.0.0" + path-exists: "npm:^4.0.0" + checksum: 0406ee89ebeefa2d507feb07ec366bebd8a6167ae74aa4e34fb4c4abd06cf782a3ce26ae4194d70706f72182841733f00551c209fe575cb00bd92104056e78c1 + languageName: node + linkType: hard + +"find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" + dependencies: + locate-path: "npm:^6.0.0" + path-exists: "npm:^4.0.0" + checksum: 062c5a83a9c02f53cdd6d175a37ecf8f87ea5bbff1fdfb828f04bfa021441bc7583e8ebc0872a4c1baab96221fb8a8a275a19809fb93fbc40bd69ec35634069a + languageName: node + linkType: hard + +"findup-sync@npm:^2.0.0": + version: 2.0.0 + resolution: "findup-sync@npm:2.0.0" + dependencies: + detect-file: "npm:^1.0.0" + is-glob: "npm:^3.1.0" + micromatch: "npm:^3.0.4" + resolve-dir: "npm:^1.0.1" + checksum: 359e0382679718e49a022eca71d217cf0175fb2d0fba2d538f12b7add164d778b78b624375e959a3a78da1ede593e6cc288f4e7e81e0fcd0adf8746636b64608 + languageName: node + linkType: hard + +"findup-sync@npm:^3.0.0": + version: 3.0.0 + resolution: "findup-sync@npm:3.0.0" + dependencies: + detect-file: "npm:^1.0.0" + is-glob: "npm:^4.0.0" + micromatch: "npm:^3.0.4" + resolve-dir: "npm:^1.0.1" + checksum: ff6f37328a7629775db2abf0fcd40e7c117baf37f23006f206c18bcd9ca0ce99d8c24ae86df540370ec76c1080ab59fe82cb71d2c7c1ad819ccccee726af7e92 + languageName: node + linkType: hard + +"fined@npm:^1.0.1": + version: 1.2.0 + resolution: "fined@npm:1.2.0" + dependencies: + expand-tilde: "npm:^2.0.2" + is-plain-object: "npm:^2.0.3" + object.defaults: "npm:^1.1.0" + object.pick: "npm:^1.2.0" + parse-filepath: "npm:^1.0.1" + checksum: 412f78bc35c450c9888844012f2a53c00c919453cab1d480e24243f12c2ca6479edee88014088351755bafd3eec56336938cbd7362c986491dffefd4ad9741f5 + languageName: node + linkType: hard + +"flagged-respawn@npm:^1.0.0": + version: 1.0.1 + resolution: "flagged-respawn@npm:1.0.1" + checksum: 4ded739606afa331d60e530cd94ea7948e3bacab8de1c084be3bbb5e37ecceec207eef1ba8fc88d14d1b975c771ac1efc1517d800027b4e05613c6c797211178 + languageName: node + linkType: hard + +"flow-parser@npm:0.*": + version: 0.222.0 + resolution: "flow-parser@npm:0.222.0" + checksum: 5576d961ba4f331168c97291a58f6afbf335dd134f0d8e34758a6f2f8276afcf504f010466b81a3420fe8c6291d9d768cd42c6bed511f2dae18d485b30dbae7e + languageName: node + linkType: hard + +"flush-write-stream@npm:^1.0.2": + version: 1.1.1 + resolution: "flush-write-stream@npm:1.1.1" + dependencies: + inherits: "npm:^2.0.3" + readable-stream: "npm:^2.3.6" + checksum: 2cd4f65b728d5f388197a03dafabc6a5e4f0c2ed1a2d912e288f7aa1c2996dd90875e55b50cf32c78dca55ad2e2dfae5d3db09b223838388033d87cf5920dd87 + languageName: node + linkType: hard + +"fn.name@npm:1.x.x": + version: 1.1.0 + resolution: "fn.name@npm:1.1.0" + checksum: 8ad62aa2d4f0b2a76d09dba36cfec61c540c13a0fd72e5d94164e430f987a7ce6a743112bbeb14877c810ef500d1f73d7f56e76d029d2e3413f20d79e3460a9a + languageName: node + linkType: hard + +"for-each@npm:^0.3.3": + version: 0.3.3 + resolution: "for-each@npm:0.3.3" + dependencies: + is-callable: "npm:^1.1.3" + checksum: 22330d8a2db728dbf003ec9182c2d421fbcd2969b02b4f97ec288721cda63eb28f2c08585ddccd0f77cb2930af8d958005c9e72f47141dc51816127a118f39aa + languageName: node + linkType: hard + +"for-in@npm:^1.0.1, for-in@npm:^1.0.2": + version: 1.0.2 + resolution: "for-in@npm:1.0.2" + checksum: 42bb609d564b1dc340e1996868b67961257fd03a48d7fdafd4f5119530b87f962be6b4d5b7e3a3fc84c9854d149494b1d358e0b0ce9837e64c4c6603a49451d6 + languageName: node + linkType: hard + +"for-own@npm:^1.0.0": + version: 1.0.0 + resolution: "for-own@npm:1.0.0" + dependencies: + for-in: "npm:^1.0.1" + checksum: ca475bc22935edf923631e9e23588edcbed33a30f0c81adc98e2c7df35db362ec4f4b569bc69051c7cfc309dfc223818c09a2f52ccd9ed77b71931c913a43a13 + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: "npm:^7.0.0" + signal-exit: "npm:^4.0.1" + checksum: 9700a0285628abaeb37007c9a4d92bd49f67210f09067638774338e146c8e9c825c5c877f072b2f75f41dc6a2d0be8664f79ffc03f6576649f54a84fb9b47de0 + languageName: node + linkType: hard + +"form-data@npm:^4.0.0": + version: 4.0.0 + resolution: "form-data@npm:4.0.0" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + mime-types: "npm:^2.1.12" + checksum: cb6f3ac49180be03ff07ba3ff125f9eba2ff0b277fb33c7fc47569fc5e616882c5b1c69b9904c4c4187e97dd0419dd03b134174756f296dec62041e6527e2c6e + languageName: node + linkType: hard + +"forwarded@npm:0.2.0": + version: 0.2.0 + resolution: "forwarded@npm:0.2.0" + checksum: 9b67c3fac86acdbc9ae47ba1ddd5f2f81526fa4c8226863ede5600a3f7c7416ef451f6f1e240a3cc32d0fd79fcfe6beb08fd0da454f360032bde70bf80afbb33 + languageName: node + linkType: hard + +"fraction.js@npm:^4.3.7": + version: 4.3.7 + resolution: "fraction.js@npm:4.3.7" + checksum: df291391beea9ab4c263487ffd9d17fed162dbb736982dee1379b2a8cc94e4e24e46ed508c6d278aded9080ba51872f1bc5f3a5fd8d7c74e5f105b508ac28711 + languageName: node + linkType: hard + +"fragment-cache@npm:^0.2.1": + version: 0.2.1 + resolution: "fragment-cache@npm:0.2.1" + dependencies: + map-cache: "npm:^0.2.2" + checksum: 5891d1c1d1d5e1a7fb3ccf28515c06731476fa88f7a50f4ede8a0d8d239a338448e7f7cc8b73db48da19c229fa30066104fe6489862065a4f1ed591c42fbeabf + languageName: node + linkType: hard + +"fresh@npm:0.5.2": + version: 0.5.2 + resolution: "fresh@npm:0.5.2" + checksum: c6d27f3ed86cc5b601404822f31c900dd165ba63fff8152a3ef714e2012e7535027063bc67ded4cb5b3a49fa596495d46cacd9f47d6328459cf570f08b7d9e5a + languageName: node + linkType: hard + +"frontend@workspace:.": + version: 0.0.0-use.local + resolution: "frontend@workspace:." + dependencies: + "@storybook/addon-essentials": "npm:^7.6.17" + "@storybook/addon-interactions": "npm:^7.6.17" + "@storybook/addon-links": "npm:^7.6.17" + "@storybook/addon-onboarding": "npm:^1.0.11" + "@storybook/blocks": "npm:^7.6.17" + "@storybook/react": "npm:^7.6.17" + "@storybook/react-vite": "npm:^7.6.17" + "@storybook/testing-library": "npm:^0.2.2" + "@types/node": "npm:^20.11.20" + animate.css: "npm:^4.1.1" + autoprefixer: "npm:^10.4.17" + concurrently: "npm:^8.2.2" + date-fns: "npm:^3.3.1" + draft-js: "git+https://github.com/penpot/draft-js.git" + eventsource-parser: "npm:^1.1.2" + fancy-log: "npm:^2.0.0" + gettext-parser: "npm:^8.0.0" + gulp: "npm:4.0.2" + gulp-concat: "npm:^2.6.1" + gulp-gzip: "npm:^1.4.2" + gulp-mustache: "npm:^5.0.0" + gulp-postcss: "npm:^10.0.0" + gulp-rename: "npm:^2.0.0" + gulp-sass: "npm:^5.1.0" + gulp-sourcemaps: "npm:^3.0.0" + gulp-svg-sprite: "npm:^2.0.3" + highlight.js: "npm:^11.9.0" + js-beautify: "npm:^1.15.1" + jsdom: "npm:^24.0.0" + jszip: "npm:^3.10.1" + luxon: "npm:^3.4.4" + map-stream: "npm:0.0.7" + marked: "npm:^12.0.0" + mkdirp: "npm:^3.0.1" + mousetrap: "npm:^1.6.5" + mustache: "npm:^4.2.0" + nodemon: "npm:^3.1.0" + npm-run-all: "npm:^4.1.5" + opentype.js: "npm:^1.3.4" + p-limit: "npm:^5.0.0" + postcss: "npm:^8.4.35" + postcss-clean: "npm:^1.2.2" + postcss-modules: "npm:^6.0.0" + prettier: "npm:^3.2.5" + pretty-time: "npm:^1.1.0" + prop-types: "npm:^15.8.1" + randomcolor: "npm:^0.6.2" + react: "npm:^18.2.0" + react-dom: "npm:^18.2.0" + react-virtualized: "npm:^9.22.5" + rimraf: "npm:^5.0.5" + rxjs: "npm:8.0.0-alpha.14" + sass: "npm:^1.71.1" + sass-embedded: "npm:^1.71.1" + sax: "npm:^1.3.0" + shadow-cljs: "npm:2.27.4" + source-map-support: "npm:^0.5.21" + storybook: "npm:^7.6.17" + svg-sprite: "npm:^2.0.2" + tdigest: "npm:^0.1.2" + typescript: "npm:^5.3.3" + ua-parser-js: "npm:^1.0.37" + vite: "npm:^5.1.4" + vitest: "npm:^1.3.1" + watcher: "npm:^2.3.0" + workerpool: "npm:^9.1.0" + xregexp: "npm:^5.1.1" + languageName: unknown + linkType: soft + +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: a0cde99085f0872f4d244e83e03a46aa387b74f5a5af750896c6b05e9077fac00e9932fdf5aef84f2f16634cd473c63037d7a512576da7d5c2b9163d1909f3a8 + languageName: node + linkType: hard + +"fs-extra@npm:11.1.1": + version: 11.1.1 + resolution: "fs-extra@npm:11.1.1" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: a2480243d7dcfa7d723c5f5b24cf4eba02a6ccece208f1524a2fbde1c629492cfb9a59e4b6d04faff6fbdf71db9fdc8ef7f396417a02884195a625f5d8dc9427 + languageName: node + linkType: hard + +"fs-extra@npm:^11.1.0": + version: 11.2.0 + resolution: "fs-extra@npm:11.2.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: d77a9a9efe60532d2e790e938c81a02c1b24904ef7a3efb3990b835514465ba720e99a6ea56fd5e2db53b4695319b644d76d5a0e9988a2beef80aa7b1da63398 + languageName: node + linkType: hard + +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + languageName: node + linkType: hard + +"fs-mkdirp-stream@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-mkdirp-stream@npm:1.0.0" + dependencies: + graceful-fs: "npm:^4.1.11" + through2: "npm:^2.0.3" + checksum: c1a6a8913e6cda1741e1d146d05baa21fe6a91802b836b3a0ae1b0654269b5097727d77d97cf5f242317b2c5e44831f834fd3bb36853a2083494d94523221a39 + languageName: node + linkType: hard + +"fs.realpath@npm:^1.0.0": + version: 1.0.0 + resolution: "fs.realpath@npm:1.0.0" + checksum: 444cf1291d997165dfd4c0d58b69f0e4782bfd9149fd72faa4fe299e68e0e93d6db941660b37dd29153bf7186672ececa3b50b7e7249477b03fdf850f287c948 + languageName: node + linkType: hard + +"fsevents@npm:^1.2.7": + version: 1.2.13 + resolution: "fsevents@npm:1.2.13" + dependencies: + bindings: "npm:^1.5.0" + nan: "npm:^2.12.1" + checksum: 4427ff08db9ee7327f2c3ad58ec56f9096a917eed861bfffaa2e2be419479cdf37d00750869ab9ecbf5f59f32ad999bd59577d73fc639193e6c0ce52bb253e02 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A^1.2.7#optional!builtin": + version: 1.2.13 + resolution: "fsevents@patch:fsevents@npm%3A1.2.13#optional!builtin::version=1.2.13&hash=d11327" + dependencies: + bindings: "npm:^1.5.0" + nan: "npm:^2.12.1" + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 + languageName: node + linkType: hard + +"function.prototype.name@npm:^1.1.6": + version: 1.1.6 + resolution: "function.prototype.name@npm:1.1.6" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + functions-have-names: "npm:^1.2.3" + checksum: 9eae11294905b62cb16874adb4fc687927cda3162285e0ad9612e6a1d04934005d46907362ea9cdb7428edce05a2f2c3dabc3b2d21e9fd343e9bb278230ad94b + languageName: node + linkType: hard + +"functions-have-names@npm:^1.2.3": + version: 1.2.3 + resolution: "functions-have-names@npm:1.2.3" + checksum: 33e77fd29bddc2d9bb78ab3eb854c165909201f88c75faa8272e35899e2d35a8a642a15e7420ef945e1f64a9670d6aa3ec744106b2aa42be68ca5114025954ca + languageName: node + linkType: hard + +"generic-names@npm:^4.0.0": + version: 4.0.0 + resolution: "generic-names@npm:4.0.0" + dependencies: + loader-utils: "npm:^3.2.0" + checksum: 4e2be864535fadceed4e803fefc1df7f85447d9479d51e611a8a43a2c96533422b62c8fae84d9eb10cc21ee3de569a8c29d5ba68978ae930cccc9cb43b9a36d1 + languageName: node + linkType: hard + +"gensync@npm:^1.0.0-beta.2": + version: 1.0.0-beta.2 + resolution: "gensync@npm:1.0.0-beta.2" + checksum: 782aba6cba65b1bb5af3b095d96249d20edbe8df32dbf4696fd49be2583faf676173bf4809386588828e4dd76a3354fcbeb577bab1c833ccd9fc4577f26103f8 + languageName: node + linkType: hard + +"get-caller-file@npm:^1.0.1": + version: 1.0.3 + resolution: "get-caller-file@npm:1.0.3" + checksum: 763dcee2de8ff60ae7e13a4bad8306205a2fbe108e555686344ddd9ef211b8bebfe459d3a739669257014c59e7cc1e7a44003c21af805c1214673e6a45f06c51 + languageName: node + linkType: hard + +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde + languageName: node + linkType: hard + +"get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": + version: 2.0.2 + resolution: "get-func-name@npm:2.0.2" + checksum: 89830fd07623fa73429a711b9daecdb304386d237c71268007f788f113505ef1d4cc2d0b9680e072c5082490aec9df5d7758bf5ac6f1c37062855e8e3dc0b9df + languageName: node + linkType: hard + +"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2": + version: 1.2.2 + resolution: "get-intrinsic@npm:1.2.2" + dependencies: + function-bind: "npm:^1.1.2" + has-proto: "npm:^1.0.1" + has-symbols: "npm:^1.0.3" + hasown: "npm:^2.0.0" + checksum: 4e7fb8adc6172bae7c4fe579569b4d5238b3667c07931cd46b4eee74bbe6ff6b91329bec311a638d8e60f5b51f44fe5445693c6be89ae88d4b5c49f7ff12db0b + languageName: node + linkType: hard + +"get-nonce@npm:^1.0.0": + version: 1.0.1 + resolution: "get-nonce@npm:1.0.1" + checksum: 2d7df55279060bf0568549e1ffc9b84bc32a32b7541675ca092dce56317cdd1a59a98dcc4072c9f6a980779440139a3221d7486f52c488e69dc0fd27b1efb162 + languageName: node + linkType: hard + +"get-npm-tarball-url@npm:^2.0.3": + version: 2.1.0 + resolution: "get-npm-tarball-url@npm:2.1.0" + checksum: af779fa5b9c89a3deaf9640630a23368f5ba6a028a1179872aaf581a59485fb2c2c6bd9b94670de228cfc5f23600c89a01e594879085f7fb4dddf820a63105b8 + languageName: node + linkType: hard + +"get-package-type@npm:^0.1.0": + version: 0.1.0 + resolution: "get-package-type@npm:0.1.0" + checksum: e34cdf447fdf1902a1f6d5af737eaadf606d2ee3518287abde8910e04159368c268568174b2e71102b87b26c2020486f126bfca9c4fb1ceb986ff99b52ecd1be + languageName: node + linkType: hard + +"get-port@npm:^5.1.1": + version: 5.1.1 + resolution: "get-port@npm:5.1.1" + checksum: 2873877a469b24e6d5e0be490724a17edb39fafc795d1d662e7bea951ca649713b4a50117a473f9d162312cb0e946597bd0e049ed2f866e79e576e8e213d3d1c + languageName: node + linkType: hard + +"get-stream@npm:^6.0.0": + version: 6.0.1 + resolution: "get-stream@npm:6.0.1" + checksum: 49825d57d3fd6964228e6200a58169464b8e8970489b3acdc24906c782fb7f01f9f56f8e6653c4a50713771d6658f7cfe051e5eb8c12e334138c9c918b296341 + languageName: node + linkType: hard + +"get-stream@npm:^8.0.1": + version: 8.0.1 + resolution: "get-stream@npm:8.0.1" + checksum: 5c2181e98202b9dae0bb4a849979291043e5892eb40312b47f0c22b9414fc9b28a3b6063d2375705eb24abc41ecf97894d9a51f64ff021511b504477b27b4290 + languageName: node + linkType: hard + +"get-symbol-description@npm:^1.0.0": + version: 1.0.0 + resolution: "get-symbol-description@npm:1.0.0" + dependencies: + call-bind: "npm:^1.0.2" + get-intrinsic: "npm:^1.1.1" + checksum: 23bc3b44c221cdf7669a88230c62f4b9e30393b61eb21ba4400cb3e346801bd8f95fe4330ee78dbae37aecd874646d53e3e76a17a654d0c84c77f6690526d6bb + languageName: node + linkType: hard + +"get-value@npm:^2.0.3, get-value@npm:^2.0.6": + version: 2.0.6 + resolution: "get-value@npm:2.0.6" + checksum: f069c132791b357c8fc4adfe9e2929b0a2c6e95f98ca7bc6fcbc27f8a302e552f86b4ae61ec56d9e9ac2544b93b6a39743d479866a37b43fcc104088ba74f0d9 + languageName: node + linkType: hard + +"gettext-parser@npm:^8.0.0": + version: 8.0.0 + resolution: "gettext-parser@npm:8.0.0" + dependencies: + content-type: "npm:^1.0.5" + encoding: "npm:^0.1.13" + readable-stream: "npm:^4.5.2" + safe-buffer: "npm:^5.2.1" + checksum: b329981791afeded45c010a5b59f980b199b53a29cfd064d09a38e9b7a9678b34666bc505cff6888984ce70ab6a7bb7d1df3e95f8c1310e21b18edac28a05160 + languageName: node + linkType: hard + +"giget@npm:^1.0.0": + version: 1.1.3 + resolution: "giget@npm:1.1.3" + dependencies: + colorette: "npm:^2.0.20" + defu: "npm:^6.1.2" + https-proxy-agent: "npm:^7.0.2" + mri: "npm:^1.2.0" + node-fetch-native: "npm:^1.4.0" + pathe: "npm:^1.1.1" + tar: "npm:^6.2.0" + bin: + giget: dist/cli.mjs + checksum: 7f3d3628f4c488ab543e2edcd93b6899b2486a0afc2caab748ad65714d631f5cdfc9cf00404ed21b390c070cf5214037dffb593cd667c54b97adc6a1c657cdf9 + languageName: node + linkType: hard + +"github-slugger@npm:^1.0.0": + version: 1.5.0 + resolution: "github-slugger@npm:1.5.0" + checksum: 116f99732925f939cbfd6f2e57db1aa7e111a460db0d103e3b3f2fce6909d44311663d4542350706cad806345b9892358cc3b153674f88eeae77f43380b3bfca + languageName: node + linkType: hard + +"glob-parent@npm:^3.1.0": + version: 3.1.0 + resolution: "glob-parent@npm:3.1.0" + dependencies: + is-glob: "npm:^3.1.0" + path-dirname: "npm:^1.0.0" + checksum: bfa89ce5ae1dfea4c2ece7b61d2ea230d87fcbec7472915cfdb3f4caf688a91ecb0dc86ae39b1e17505adce7e64cae3b971d64dc66091f3a0131169fd631b00d + languageName: node + linkType: hard + +"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: "npm:^4.0.1" + checksum: cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee + languageName: node + linkType: hard + +"glob-promise@npm:^4.2.0": + version: 4.2.2 + resolution: "glob-promise@npm:4.2.2" + dependencies: + "@types/glob": "npm:^7.1.3" + peerDependencies: + glob: ^7.1.6 + checksum: 3eb01bed2901539365df6a4d27800afb8788840647d01f9bf3500b3de756597f2ff4b8c823971ace34db228c83159beca459dc42a70968d4e9c8200ed2cc96bd + languageName: node + linkType: hard + +"glob-stream@npm:^6.1.0": + version: 6.1.0 + resolution: "glob-stream@npm:6.1.0" + dependencies: + extend: "npm:^3.0.0" + glob: "npm:^7.1.1" + glob-parent: "npm:^3.1.0" + is-negated-glob: "npm:^1.0.0" + ordered-read-streams: "npm:^1.0.0" + pumpify: "npm:^1.3.5" + readable-stream: "npm:^2.1.5" + remove-trailing-separator: "npm:^1.0.1" + to-absolute-glob: "npm:^2.0.0" + unique-stream: "npm:^2.0.2" + checksum: 6b2653f2aafe99f17c0348de34dc0782cc20c3425ade4d4e354ef125b6e049e71cb4a209c6ea624a6a72bf99e0d7a25f1c2f04f81e42b0b8091b48d210fc48f5 + languageName: node + linkType: hard + +"glob-to-regexp@npm:^0.4.1": + version: 0.4.1 + resolution: "glob-to-regexp@npm:0.4.1" + checksum: 0486925072d7a916f052842772b61c3e86247f0a80cc0deb9b5a3e8a1a9faad5b04fb6f58986a09f34d3e96cd2a22a24b7e9882fb1cf904c31e9a310de96c429 + languageName: node + linkType: hard + +"glob-watcher@npm:^5.0.3": + version: 5.0.5 + resolution: "glob-watcher@npm:5.0.5" + dependencies: + anymatch: "npm:^2.0.0" + async-done: "npm:^1.2.0" + chokidar: "npm:^2.0.0" + is-negated-glob: "npm:^1.0.0" + just-debounce: "npm:^1.0.0" + normalize-path: "npm:^3.0.0" + object.defaults: "npm:^1.1.0" + checksum: 40649b8aa37ff6f09559303574eb0b5871ed16bdbaa1f335de7b0bfbaa096765c45111908ec8bcf65436870c59d1934377e720024848c532f900bc046c8d8c58 + languageName: node + linkType: hard + +"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.3, glob@npm:^10.3.7": + version: 10.3.10 + resolution: "glob@npm:10.3.10" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^2.3.5" + minimatch: "npm:^9.0.1" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry: "npm:^1.10.1" + bin: + glob: dist/esm/bin.mjs + checksum: 13d8a1feb7eac7945f8c8480e11cd4a44b24d26503d99a8d8ac8d5aefbf3e9802a2b6087318a829fad04cb4e829f25c5f4f1110c68966c498720dd261c7e344d + languageName: node + linkType: hard + +"glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.2.0, glob@npm:^7.2.3": + version: 7.2.3 + resolution: "glob@npm:7.2.3" + dependencies: + fs.realpath: "npm:^1.0.0" + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:^3.1.1" + once: "npm:^1.3.0" + path-is-absolute: "npm:^1.0.0" + checksum: 65676153e2b0c9095100fe7f25a778bf45608eeb32c6048cf307f579649bcc30353277b3b898a3792602c65764e5baa4f643714dfbdfd64ea271d210c7a425fe + languageName: node + linkType: hard + +"global-modules@npm:^1.0.0": + version: 1.0.0 + resolution: "global-modules@npm:1.0.0" + dependencies: + global-prefix: "npm:^1.0.1" + is-windows: "npm:^1.0.1" + resolve-dir: "npm:^1.0.0" + checksum: 7d91ecf78d4fcbc966b2d89c1400df273afea795bc8cadf39857ee1684e442065621fd79413ff5fcd9e90c6f1b2dc0123e644fa0b7811f987fd54c6b9afad858 + languageName: node + linkType: hard + +"global-prefix@npm:^1.0.1": + version: 1.0.2 + resolution: "global-prefix@npm:1.0.2" + dependencies: + expand-tilde: "npm:^2.0.2" + homedir-polyfill: "npm:^1.0.1" + ini: "npm:^1.3.4" + is-windows: "npm:^1.0.1" + which: "npm:^1.2.14" + checksum: d8037e300f1dc04d5d410d16afa662e71bfad22dcceba6c9727bb55cc273b8988ca940b3402f62e5392fd261dd9924a9a73a865ef2000219461f31f3fc86be06 + languageName: node + linkType: hard + +"globals@npm:^11.1.0": + version: 11.12.0 + resolution: "globals@npm:11.12.0" + checksum: 758f9f258e7b19226bd8d4af5d3b0dcf7038780fb23d82e6f98932c44e239f884847f1766e8fa9cc5635ccb3204f7fa7314d4408dd4002a5e8ea827b4018f0a1 + languageName: node + linkType: hard + +"globalthis@npm:^1.0.3": + version: 1.0.3 + resolution: "globalthis@npm:1.0.3" + dependencies: + define-properties: "npm:^1.1.3" + checksum: 0db6e9af102a5254630351557ac15e6909bc7459d3e3f6b001e59fe784c96d31108818f032d9095739355a88467459e6488ff16584ee6250cd8c27dec05af4b0 + languageName: node + linkType: hard + +"globby@npm:^11.0.1, globby@npm:^11.0.2": + version: 11.1.0 + resolution: "globby@npm:11.1.0" + dependencies: + array-union: "npm:^2.1.0" + dir-glob: "npm:^3.0.1" + fast-glob: "npm:^3.2.9" + ignore: "npm:^5.2.0" + merge2: "npm:^1.4.1" + slash: "npm:^3.0.0" + checksum: b39511b4afe4bd8a7aead3a27c4ade2b9968649abab0a6c28b1a90141b96ca68ca5db1302f7c7bd29eab66bf51e13916b8e0a3d0ac08f75e1e84a39b35691189 + languageName: node + linkType: hard + +"glogg@npm:^1.0.0": + version: 1.0.2 + resolution: "glogg@npm:1.0.2" + dependencies: + sparkles: "npm:^1.0.0" + checksum: ebe04ac32f646943f1f8a260a324832489e745b66ca64381a9d19847f9e8cc74527445868e7dde696145950939ddeca76784dc6d99fa41158876ea59ae14a493 + languageName: node + linkType: hard + +"gopd@npm:^1.0.1": + version: 1.0.1 + resolution: "gopd@npm:1.0.1" + dependencies: + get-intrinsic: "npm:^1.1.3" + checksum: 505c05487f7944c552cee72087bf1567debb470d4355b1335f2c262d218ebbff805cd3715448fe29b4b380bae6912561d0467233e4165830efd28da241418c63 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.0.0, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + languageName: node + linkType: hard + +"gulp-cli@npm:^2.2.0": + version: 2.3.0 + resolution: "gulp-cli@npm:2.3.0" + dependencies: + ansi-colors: "npm:^1.0.1" + archy: "npm:^1.0.0" + array-sort: "npm:^1.0.0" + color-support: "npm:^1.1.3" + concat-stream: "npm:^1.6.0" + copy-props: "npm:^2.0.1" + fancy-log: "npm:^1.3.2" + gulplog: "npm:^1.0.0" + interpret: "npm:^1.4.0" + isobject: "npm:^3.0.1" + liftoff: "npm:^3.1.0" + matchdep: "npm:^2.0.0" + mute-stdout: "npm:^1.0.0" + pretty-hrtime: "npm:^1.0.0" + replace-homedir: "npm:^1.0.0" + semver-greatest-satisfied-range: "npm:^1.1.0" + v8flags: "npm:^3.2.0" + yargs: "npm:^7.1.0" + bin: + gulp: bin/gulp.js + checksum: 77adb21dd60ac8ef53624413613c92a010e132bdee8f45f356e0174db72b5a164256de3da5f17f138f57fedc50482b4e433a6839e2af5e79e2f72be11eda3d14 + languageName: node + linkType: hard + +"gulp-concat@npm:^2.6.1": + version: 2.6.1 + resolution: "gulp-concat@npm:2.6.1" + dependencies: + concat-with-sourcemaps: "npm:^1.0.0" + through2: "npm:^2.0.0" + vinyl: "npm:^2.0.0" + checksum: dabe4ff20a6015b7fc9456ea8f453795ec9e1ad121fe3fe755a994a9a51181f433f301158f06b65ebb24c7188be4011123db4fa4c6227e55b3865301e3ed339a + languageName: node + linkType: hard + +"gulp-gzip@npm:^1.4.2": + version: 1.4.2 + resolution: "gulp-gzip@npm:1.4.2" + dependencies: + ansi-colors: "npm:^1.0.1" + bytes: "npm:^3.0.0" + fancy-log: "npm:^1.3.2" + plugin-error: "npm:^1.0.0" + stream-to-array: "npm:^2.3.0" + through2: "npm:^2.0.3" + checksum: b9ccbadd7672d533cbda5fc4eceb6b2bce92e1c60f2cc66920510e0c7c466df61ca3eec02e55de6edf3c690436a62faf61c9379364dbf89357f62330a1c9c302 + languageName: node + linkType: hard + +"gulp-mustache@npm:^5.0.0": + version: 5.0.0 + resolution: "gulp-mustache@npm:5.0.0" + dependencies: + escape-string-regexp: "npm:^2.0.0" + mustache: "npm:^4.0.1" + plugin-error: "npm:^1.0.0" + replace-ext: "npm:^1.0.0" + through2: "npm:^3.0.1" + checksum: b4c8decdd10d3f40c329428306aefefe1b1c800b63a90d0b4cf44eab846cd49770356357658b9186805ba6983494c51749ecbb5937ef0a134802956cf8b92226 + languageName: node + linkType: hard + +"gulp-postcss@npm:^10.0.0": + version: 10.0.0 + resolution: "gulp-postcss@npm:10.0.0" + dependencies: + fancy-log: "npm:^2.0.0" + plugin-error: "npm:^2.0.1" + postcss-load-config: "npm:^5.0.0" + vinyl-sourcemaps-apply: "npm:^0.2.1" + peerDependencies: + postcss: ^8.0.0 + checksum: 042c2111879acf29a65f001b232326135b1bb15d95ebdb6074815b642aaa76555edb59f26011b975a973b2039b3ea58d0b14c6cf7c8bc566830bf57cda8dc825 + languageName: node + linkType: hard + +"gulp-rename@npm:^2.0.0": + version: 2.0.0 + resolution: "gulp-rename@npm:2.0.0" + checksum: 59f0e467544ddfbeccc208944cb57801e61cd262ca8db595fb520ce6c56bb265b0f873a3872b7ef6b580e5f3f2bd38d0f382fa271d80f73f845e7d56eb124613 + languageName: node + linkType: hard + +"gulp-sass@npm:^5.1.0": + version: 5.1.0 + resolution: "gulp-sass@npm:5.1.0" + dependencies: + lodash.clonedeep: "npm:^4.5.0" + picocolors: "npm:^1.0.0" + plugin-error: "npm:^1.0.1" + replace-ext: "npm:^2.0.0" + strip-ansi: "npm:^6.0.1" + vinyl-sourcemaps-apply: "npm:^0.2.1" + checksum: 6eaacf92519ef9963cec83d4c716ae174aad0bb6427794058c86d6300559016633a97903457aaeb5485de98c710af86d4941e8b20c498f7b7cba14f93ba90065 + languageName: node + linkType: hard + +"gulp-sourcemaps@npm:^3.0.0": + version: 3.0.0 + resolution: "gulp-sourcemaps@npm:3.0.0" + dependencies: + "@gulp-sourcemaps/identity-map": "npm:^2.0.1" + "@gulp-sourcemaps/map-sources": "npm:^1.0.0" + acorn: "npm:^6.4.1" + convert-source-map: "npm:^1.0.0" + css: "npm:^3.0.0" + debug-fabulous: "npm:^1.0.0" + detect-newline: "npm:^2.0.0" + graceful-fs: "npm:^4.0.0" + source-map: "npm:^0.6.0" + strip-bom-string: "npm:^1.0.0" + through2: "npm:^2.0.0" + checksum: 3129ff26b21b0d5df49b1d6db86f02b530baa3933c6e46b567e8756f8f3cf321967d5e8bf5d4b9b4129ce2b8d33394e3ed05acb8ee2c4b0943a1920453721f72 + languageName: node + linkType: hard + +"gulp-svg-sprite@npm:^2.0.3": + version: 2.0.3 + resolution: "gulp-svg-sprite@npm:2.0.3" + dependencies: + plugin-error: "npm:^2.0.1" + svg-sprite: "npm:^2.0.2" + checksum: a755423a35ca4985cf2aa03874ef5529c61012f99b3ee3443a06aa4dd0fe649ea6ee269c4329d1130c8e3997ddb056ce81776072b072da44872b1bed3b733dca + languageName: node + linkType: hard + +"gulp@npm:4.0.2": + version: 4.0.2 + resolution: "gulp@npm:4.0.2" + dependencies: + glob-watcher: "npm:^5.0.3" + gulp-cli: "npm:^2.2.0" + undertaker: "npm:^1.2.1" + vinyl-fs: "npm:^3.0.0" + bin: + gulp: ./bin/gulp.js + checksum: 5433fa64680b68b1e747868cc68563c3ab4a3b3a60e63fc930de3e8fc71ac1c3ce7ea9657a3e306103fe39961ea156fdb9a1af37764aa8f450ac5c8fed2fa98d + languageName: node + linkType: hard + +"gulplog@npm:^1.0.0": + version: 1.0.0 + resolution: "gulplog@npm:1.0.0" + dependencies: + glogg: "npm:^1.0.0" + checksum: a693c2f54a96af82ee6d467b18a11ba041dc7c422486e6dfa0a88f470a76bad944dda597c625cc7cfff5e39b7701f2ade7aebb08eb8163da66354c2f88fa67c1 + languageName: node + linkType: hard + +"gunzip-maybe@npm:^1.4.2": + version: 1.4.2 + resolution: "gunzip-maybe@npm:1.4.2" + dependencies: + browserify-zlib: "npm:^0.1.4" + is-deflate: "npm:^1.0.0" + is-gzip: "npm:^1.0.0" + peek-stream: "npm:^1.1.0" + pumpify: "npm:^1.3.3" + through2: "npm:^2.0.3" + bin: + gunzip-maybe: bin.js + checksum: 42798a8061759885c2084e1804e51313d14f2dc9cf6c137e222953ec802f914e592d6f9dbf6ad67f4e78eb036e86db017d9c7c93bb23e90cd5ae09326296ed77 + languageName: node + linkType: hard + +"handlebars@npm:^4.7.7": + version: 4.7.8 + resolution: "handlebars@npm:4.7.8" + dependencies: + minimist: "npm:^1.2.5" + neo-async: "npm:^2.6.2" + source-map: "npm:^0.6.1" + uglify-js: "npm:^3.1.4" + wordwrap: "npm:^1.0.0" + dependenciesMeta: + uglify-js: + optional: true + bin: + handlebars: bin/handlebars + checksum: 7aff423ea38a14bb379316f3857fe0df3c5d66119270944247f155ba1f08e07a92b340c58edaa00cfe985c21508870ee5183e0634dcb53dd405f35c93ef7f10d + languageName: node + linkType: hard + +"has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2": + version: 1.0.2 + resolution: "has-bigints@npm:1.0.2" + checksum: 724eb1485bfa3cdff6f18d95130aa190561f00b3fcf9f19dc640baf8176b5917c143b81ec2123f8cddb6c05164a198c94b13e1377c497705ccc8e1a80306e83b + languageName: node + linkType: hard + +"has-flag@npm:^3.0.0": + version: 3.0.0 + resolution: "has-flag@npm:3.0.0" + checksum: 1c6c83b14b8b1b3c25b0727b8ba3e3b647f99e9e6e13eb7322107261de07a4c1be56fc0d45678fc376e09772a3a1642ccdaf8fc69bdf123b6c086598397ce473 + languageName: node + linkType: hard + +"has-flag@npm:^4.0.0": + version: 4.0.0 + resolution: "has-flag@npm:4.0.0" + checksum: 2e789c61b7888d66993e14e8331449e525ef42aac53c627cc53d1c3334e768bcb6abdc4f5f0de1478a25beec6f0bd62c7549058b7ac53e924040d4f301f02fd1 + languageName: node + linkType: hard + +"has-property-descriptors@npm:^1.0.0": + version: 1.0.1 + resolution: "has-property-descriptors@npm:1.0.1" + dependencies: + get-intrinsic: "npm:^1.2.2" + checksum: d62ba94b40150b00d621bc64a6aedb5bf0ee495308b4b7ed6bac856043db3cdfb1db553ae81cec91c9d2bd82057ff0e94145e7fa25d5aa5985ed32e0921927f6 + languageName: node + linkType: hard + +"has-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "has-proto@npm:1.0.1" + checksum: c8a8fe411f810b23a564bd5546a8f3f0fff6f1b692740eb7a2fdc9df716ef870040806891e2f23ff4653f1083e3895bf12088703dd1a0eac3d9202d3a4768cd0 + languageName: node + linkType: hard + +"has-symbols@npm:^1.0.2, has-symbols@npm:^1.0.3": + version: 1.0.3 + resolution: "has-symbols@npm:1.0.3" + checksum: e6922b4345a3f37069cdfe8600febbca791c94988c01af3394d86ca3360b4b93928bbf395859158f88099cb10b19d98e3bbab7c9ff2c1bd09cf665ee90afa2c3 + languageName: node + linkType: hard + +"has-tostringtag@npm:^1.0.0": + version: 1.0.0 + resolution: "has-tostringtag@npm:1.0.0" + dependencies: + has-symbols: "npm:^1.0.2" + checksum: 1cdba76b7d13f65198a92b8ca1560ba40edfa09e85d182bf436d928f3588a9ebd260451d569f0ed1b849c4bf54f49c862aa0d0a77f9552b1855bb6deb526c011 + languageName: node + linkType: hard + +"has-value@npm:^0.3.1": + version: 0.3.1 + resolution: "has-value@npm:0.3.1" + dependencies: + get-value: "npm:^2.0.3" + has-values: "npm:^0.1.4" + isobject: "npm:^2.0.0" + checksum: 7a7c2e9d07bc9742c81806150adb154d149bc6155267248c459cd1ce2a64b0759980d26213260e4b7599c8a3754551179f155ded88d0533a0d2bc7bc29028432 + languageName: node + linkType: hard + +"has-value@npm:^1.0.0": + version: 1.0.0 + resolution: "has-value@npm:1.0.0" + dependencies: + get-value: "npm:^2.0.6" + has-values: "npm:^1.0.0" + isobject: "npm:^3.0.0" + checksum: 17cdccaf50f8aac80a109dba2e2ee5e800aec9a9d382ef9deab66c56b34269e4c9ac720276d5ffa722764304a1180ae436df077da0dd05548cfae0209708ba4d + languageName: node + linkType: hard + +"has-values@npm:^0.1.4": + version: 0.1.4 + resolution: "has-values@npm:0.1.4" + checksum: a8f00ad862c20289798c35243d5bd0b0a97dd44b668c2204afe082e0265f2d0bf3b89fc8cc0ef01a52b49f10aa35cf85c336ee3a5f1cac96ed490f5e901cdbf2 + languageName: node + linkType: hard + +"has-values@npm:^1.0.0": + version: 1.0.0 + resolution: "has-values@npm:1.0.0" + dependencies: + is-number: "npm:^3.0.0" + kind-of: "npm:^4.0.0" + checksum: a6f2a1cc6b2e43eacc68e62e71ad6890def7f4b13d2ef06b4ad3ee156c23e470e6df144b9b467701908e17633411f1075fdff0cab45fb66c5e0584d89b25f35e + languageName: node + linkType: hard + +"hash-base@npm:^3.0.0": + version: 3.1.0 + resolution: "hash-base@npm:3.1.0" + dependencies: + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.6.0" + safe-buffer: "npm:^5.2.0" + checksum: 663eabcf4173326fbb65a1918a509045590a26cc7e0964b754eef248d281305c6ec9f6b31cb508d02ffca383ab50028180ce5aefe013e942b44a903ac8dc80d0 + languageName: node + linkType: hard + +"hash.js@npm:^1.0.0, hash.js@npm:^1.0.3": + version: 1.1.7 + resolution: "hash.js@npm:1.1.7" + dependencies: + inherits: "npm:^2.0.3" + minimalistic-assert: "npm:^1.0.1" + checksum: 41ada59494eac5332cfc1ce6b7ebdd7b88a3864a6d6b08a3ea8ef261332ed60f37f10877e0c825aaa4bddebf164fbffa618286aeeec5296675e2671cbfa746c4 + languageName: node + linkType: hard + +"hasown@npm:^2.0.0": + version: 2.0.0 + resolution: "hasown@npm:2.0.0" + dependencies: + function-bind: "npm:^1.1.2" + checksum: 5d415b114f410661208c95e7ab4879f1cc2765b8daceff4dc8718317d1cb7b9ffa7c5d1eafd9a4389c9aab7445d6ea88e05f3096cb1e529618b55304956b87fc + languageName: node + linkType: hard + +"highlight.js@npm:^11.9.0": + version: 11.9.0 + resolution: "highlight.js@npm:11.9.0" + checksum: 27cfa8717dc9d200aecbdb383eb122d5f45ce715d2f468583785d36fbfe5076ce033abb02486dc13b407171721cda6f474ed3f3a5a8e8c3d91367fa5f51ee374 + languageName: node + linkType: hard + +"hmac-drbg@npm:^1.0.1": + version: 1.0.1 + resolution: "hmac-drbg@npm:1.0.1" + dependencies: + hash.js: "npm:^1.0.3" + minimalistic-assert: "npm:^1.0.0" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: f3d9ba31b40257a573f162176ac5930109816036c59a09f901eb2ffd7e5e705c6832bedfff507957125f2086a0ab8f853c0df225642a88bf1fcaea945f20600d + languageName: node + linkType: hard + +"homedir-polyfill@npm:^1.0.1": + version: 1.0.3 + resolution: "homedir-polyfill@npm:1.0.3" + dependencies: + parse-passwd: "npm:^1.0.0" + checksum: 3c099844f94b8b438f124bd5698bdcfef32b2d455115fb8050d7148e7f7b95fc89ba9922586c491f0e1cdebf437b1053c84ecddb8d596e109e9ac69c5b4a9e27 + languageName: node + linkType: hard + +"hosted-git-info@npm:^2.1.4": + version: 2.8.9 + resolution: "hosted-git-info@npm:2.8.9" + checksum: 317cbc6b1bbbe23c2a40ae23f3dafe9fa349ce42a89a36f930e3f9c0530c179a3882d2ef1e4141a4c3674d6faaea862138ec55b43ad6f75e387fda2483a13c70 + languageName: node + linkType: hard + +"html-encoding-sniffer@npm:^4.0.0": + version: 4.0.0 + resolution: "html-encoding-sniffer@npm:4.0.0" + dependencies: + whatwg-encoding: "npm:^3.1.1" + checksum: 523398055dc61ac9b34718a719cb4aa691e4166f29187e211e1607de63dc25ac7af52ca7c9aead0c4b3c0415ffecb17326396e1202e2e86ff4bca4c0ee4c6140 + languageName: node + linkType: hard + +"html-tags@npm:^3.1.0": + version: 3.3.1 + resolution: "html-tags@npm:3.3.1" + checksum: 680165e12baa51bad7397452d247dbcc5a5c29dac0e6754b1187eee3bf26f514bc1907a431dd2f7eb56207611ae595ee76a0acc8eaa0d931e72c791dd6463d79 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc + languageName: node + linkType: hard + +"http-errors@npm:2.0.0": + version: 2.0.0 + resolution: "http-errors@npm:2.0.0" + dependencies: + depd: "npm:2.0.0" + inherits: "npm:2.0.4" + setprototypeof: "npm:1.2.0" + statuses: "npm:2.0.1" + toidentifier: "npm:1.0.1" + checksum: fc6f2715fe188d091274b5ffc8b3657bd85c63e969daa68ccb77afb05b071a4b62841acb7a21e417b5539014dff2ebf9550f0b14a9ff126f2734a7c1387f8e19 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.0 + resolution: "http-proxy-agent@npm:7.0.0" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: a11574ff39436cee3c7bc67f259444097b09474605846ddd8edf0bf4ad8644be8533db1aa463426e376865047d05dc22755e638632819317c0c2f1b2196657c8 + languageName: node + linkType: hard + +"https-browserify@npm:^1.0.0": + version: 1.0.0 + resolution: "https-browserify@npm:1.0.0" + checksum: e17b6943bc24ea9b9a7da5714645d808670af75a425f29baffc3284962626efdc1eb3aa9bbffaa6e64028a6ad98af5b09fabcb454a8f918fb686abfdc9e9b8ae + languageName: node + linkType: hard + +"https-proxy-agent@npm:^4.0.0": + version: 4.0.0 + resolution: "https-proxy-agent@npm:4.0.0" + dependencies: + agent-base: "npm:5" + debug: "npm:4" + checksum: fbba3e037ec04e1850e867064a763b86dd884baae9c5f4ad380504e321068c9e9b5de79cf2f3a28ede7c36036dce905b58d9f51703c5b3884d887114f4887f77 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2": + version: 7.0.2 + resolution: "https-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:4" + checksum: 7735eb90073db087e7e79312e3d97c8c04baf7ea7ca7b013382b6a45abbaa61b281041a98f4e13c8c80d88f843785bcc84ba189165b4b4087b1e3496ba656d77 + languageName: node + linkType: hard + +"human-signals@npm:^2.1.0": + version: 2.1.0 + resolution: "human-signals@npm:2.1.0" + checksum: 695edb3edfcfe9c8b52a76926cd31b36978782062c0ed9b1192b36bebc75c4c87c82e178dfcb0ed0fc27ca59d434198aac0bd0be18f5781ded775604db22304a + languageName: node + linkType: hard + +"human-signals@npm:^5.0.0": + version: 5.0.0 + resolution: "human-signals@npm:5.0.0" + checksum: 5a9359073fe17a8b58e5a085e9a39a950366d9f00217c4ff5878bd312e09d80f460536ea6a3f260b5943a01fe55c158d1cea3fc7bee3d0520aeef04f6d915c82 + languageName: node + linkType: hard + +"iconv-lite@npm:0.4.24": + version: 0.4.24 + resolution: "iconv-lite@npm:0.4.24" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3" + checksum: c6886a24cc00f2a059767440ec1bc00d334a89f250db8e0f7feb4961c8727118457e27c495ba94d082e51d3baca378726cd110aaf7ded8b9bbfd6a44760cf1d4 + languageName: node + linkType: hard + +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + languageName: node + linkType: hard + +"icss-utils@npm:^5.0.0, icss-utils@npm:^5.1.0": + version: 5.1.0 + resolution: "icss-utils@npm:5.1.0" + peerDependencies: + postcss: ^8.1.0 + checksum: 39c92936fabd23169c8611d2b5cc39e39d10b19b0d223352f20a7579f75b39d5f786114a6b8fc62bee8c5fed59ba9e0d38f7219a4db383e324fb3061664b043d + languageName: node + linkType: hard + +"ieee754@npm:^1.1.13, ieee754@npm:^1.1.4, ieee754@npm:^1.2.1": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb + languageName: node + linkType: hard + +"ignore-by-default@npm:^1.0.1": + version: 1.0.1 + resolution: "ignore-by-default@npm:1.0.1" + checksum: 9ab6e70e80f7cc12735def7ecb5527cfa56ab4e1152cd64d294522827f2dcf1f6d85531241537dc3713544e88dd888f65cb3c49c7b2cddb9009087c75274e533 + languageName: node + linkType: hard + +"ignore@npm:^5.2.0": + version: 5.3.0 + resolution: "ignore@npm:5.3.0" + checksum: dc06bea5c23aae65d0725a957a0638b57e235ae4568dda51ca142053ed2c352de7e3bc93a69b2b32ac31966a1952e9a93c5ef2e2ab7c6b06aef9808f6b55b571 + languageName: node + linkType: hard + +"immediate@npm:~3.0.5": + version: 3.0.6 + resolution: "immediate@npm:3.0.6" + checksum: f8ba7ede69bee9260241ad078d2d535848745ff5f6995c7c7cb41cfdc9ccc213f66e10fa5afb881f90298b24a3f7344b637b592beb4f54e582770cdce3f1f039 + languageName: node + linkType: hard + +"immutable@npm:^4.0.0": + version: 4.3.4 + resolution: "immutable@npm:4.3.4" + checksum: c15b9f0fa7b3c9315725cb00704fddad59f0e668a7379c39b9a528a8386140ee9effb015ae51a5b423e05c59d15fc0b38c970db6964ad6b3e05d0761db68441f + languageName: node + linkType: hard + +"immutable@npm:~3.7.4": + version: 3.7.6 + resolution: "immutable@npm:3.7.6" + checksum: efe2bbb2620aa897afbb79545b9eda4dd3dc072e05ae7004895a7efb43187e4265612a88f8723f391eb1c87c46c52fd11e2d1968e42404450c63e49558d7ca4e + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + languageName: node + linkType: hard + +"inflight@npm:^1.0.4": + version: 1.0.6 + resolution: "inflight@npm:1.0.6" + dependencies: + once: "npm:^1.3.0" + wrappy: "npm:1" + checksum: 7faca22584600a9dc5b9fca2cd5feb7135ac8c935449837b315676b4c90aa4f391ec4f42240178244b5a34e8bede1948627fda392ca3191522fc46b34e985ab2 + languageName: node + linkType: hard + +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 + languageName: node + linkType: hard + +"inherits@npm:2.0.3": + version: 2.0.3 + resolution: "inherits@npm:2.0.3" + checksum: 6e56402373149ea076a434072671f9982f5fad030c7662be0332122fe6c0fa490acb3cc1010d90b6eff8d640b1167d77674add52dfd1bb85d545cf29e80e73e7 + languageName: node + linkType: hard + +"ini@npm:^1.3.4": + version: 1.3.8 + resolution: "ini@npm:1.3.8" + checksum: ec93838d2328b619532e4f1ff05df7909760b6f66d9c9e2ded11e5c1897d6f2f9980c54dd638f88654b00919ce31e827040631eab0a3969e4d1abefa0719516a + languageName: node + linkType: hard + +"internal-slot@npm:^1.0.4, internal-slot@npm:^1.0.5": + version: 1.0.6 + resolution: "internal-slot@npm:1.0.6" + dependencies: + get-intrinsic: "npm:^1.2.2" + hasown: "npm:^2.0.0" + side-channel: "npm:^1.0.4" + checksum: aa37cafc8ffbf513a340de58f40d5017b4949d99722d7e4f0e24b182455bdd258000d4bb1d7b4adcf9f8979b97049b99fe9defa9db8e18a78071d2637ac143fb + languageName: node + linkType: hard + +"interpret@npm:^1.4.0": + version: 1.4.0 + resolution: "interpret@npm:1.4.0" + checksum: 08c5ad30032edeec638485bc3f6db7d0094d9b3e85e0f950866600af3c52e9fd69715416d29564731c479d9f4d43ff3e4d302a178196bdc0e6837ec147640450 + languageName: node + linkType: hard + +"invariant@npm:^2.2.4": + version: 2.2.4 + resolution: "invariant@npm:2.2.4" + dependencies: + loose-envify: "npm:^1.0.0" + checksum: 5af133a917c0bcf65e84e7f23e779e7abc1cd49cb7fdc62d00d1de74b0d8c1b5ee74ac7766099fb3be1b05b26dfc67bab76a17030d2fe7ea2eef867434362dfc + languageName: node + linkType: hard + +"invert-kv@npm:^1.0.0": + version: 1.0.0 + resolution: "invert-kv@npm:1.0.0" + checksum: 9ccef12ada8494c56175cc0380b4cea18b6c0a368436f324a30e43a332db90bdfb83cd3a7987b71df359cdf931ce45b7daf35b677da56658565d61068e4bc20b + languageName: node + linkType: hard + +"ip@npm:^2.0.0": + version: 2.0.0 + resolution: "ip@npm:2.0.0" + checksum: 8d186cc5585f57372847ae29b6eba258c68862055e18a75cc4933327232cb5c107f89800ce29715d542eef2c254fbb68b382e780a7414f9ee7caf60b7a473958 + languageName: node + linkType: hard + +"ip@npm:^2.0.1": + version: 2.0.1 + resolution: "ip@npm:2.0.1" + checksum: cab8eb3e88d0abe23e4724829621ec4c4c5cb41a7f936a2e626c947128c1be16ed543448d42af7cca95379f9892bfcacc1ccd8d09bc7e8bea0e86d492ce33616 + languageName: node + linkType: hard + +"ipaddr.js@npm:1.9.1": + version: 1.9.1 + resolution: "ipaddr.js@npm:1.9.1" + checksum: 0486e775047971d3fdb5fb4f063829bac45af299ae0b82dcf3afa2145338e08290563a2a70f34b732d795ecc8311902e541a8530eeb30d75860a78ff4e94ce2a + languageName: node + linkType: hard + +"is-absolute-url@npm:^3.0.0": + version: 3.0.3 + resolution: "is-absolute-url@npm:3.0.3" + checksum: 04c415974c32e73a83d3a21a9bea18fc4e2c14fbe6bbd64832cf1e67a75ade2af0e900f552f0b8a447f1305f5ffc9d143ccd8d005dbe715d198c359d342b86f0 + languageName: node + linkType: hard + +"is-absolute@npm:^1.0.0": + version: 1.0.0 + resolution: "is-absolute@npm:1.0.0" + dependencies: + is-relative: "npm:^1.0.0" + is-windows: "npm:^1.0.1" + checksum: 422302ce879d4f3ca6848499b6f3ddcc8fd2dc9f3e9cad3f6bcedff58cdfbbbd7f4c28600fffa7c59a858f1b15c27fb6cfe1d5275e58a36d2bf098a44ef5abc4 + languageName: node + linkType: hard + +"is-accessor-descriptor@npm:^1.0.1": + version: 1.0.1 + resolution: "is-accessor-descriptor@npm:1.0.1" + dependencies: + hasown: "npm:^2.0.0" + checksum: d034034074c5ffeb6c868e091083182279db1a956f49f8d1494cecaa0f8b99d706556ded2a9b20d9aa290549106eef8204d67d8572902e06dcb1add6db6b524d + languageName: node + linkType: hard + +"is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.1": + version: 1.1.1 + resolution: "is-arguments@npm:1.1.1" + dependencies: + call-bind: "npm:^1.0.2" + has-tostringtag: "npm:^1.0.0" + checksum: 5ff1f341ee4475350adfc14b2328b38962564b7c2076be2f5bac7bd9b61779efba99b9f844a7b82ba7654adccf8e8eb19d1bb0cc6d1c1a085e498f6793d4328f + languageName: node + linkType: hard + +"is-array-buffer@npm:^3.0.1, is-array-buffer@npm:^3.0.2": + version: 3.0.2 + resolution: "is-array-buffer@npm:3.0.2" + dependencies: + call-bind: "npm:^1.0.2" + get-intrinsic: "npm:^1.2.0" + is-typed-array: "npm:^1.1.10" + checksum: 40ed13a5f5746ac3ae2f2e463687d9b5a3f5fd0086f970fb4898f0253c2a5ec2e3caea2d664dd8f54761b1c1948609702416921a22faebe160c7640a9217c80e + languageName: node + linkType: hard + +"is-arrayish@npm:^0.2.1": + version: 0.2.1 + resolution: "is-arrayish@npm:0.2.1" + checksum: e7fb686a739068bb70f860b39b67afc62acc62e36bb61c5f965768abce1873b379c563e61dd2adad96ebb7edf6651111b385e490cf508378959b0ed4cac4e729 + languageName: node + linkType: hard + +"is-arrayish@npm:^0.3.1": + version: 0.3.2 + resolution: "is-arrayish@npm:0.3.2" + checksum: f59b43dc1d129edb6f0e282595e56477f98c40278a2acdc8b0a5c57097c9eff8fe55470493df5775478cf32a4dc8eaf6d3a749f07ceee5bc263a78b2434f6a54 + languageName: node + linkType: hard + +"is-bigint@npm:^1.0.1": + version: 1.0.4 + resolution: "is-bigint@npm:1.0.4" + dependencies: + has-bigints: "npm:^1.0.1" + checksum: eb9c88e418a0d195ca545aff2b715c9903d9b0a5033bc5922fec600eb0c3d7b1ee7f882dbf2e0d5a6e694e42391be3683e4368737bd3c4a77f8ac293e7773696 + languageName: node + linkType: hard + +"is-binary-path@npm:^1.0.0": + version: 1.0.1 + resolution: "is-binary-path@npm:1.0.1" + dependencies: + binary-extensions: "npm:^1.0.0" + checksum: 16e456fa3782eaf3d8e28d382b750507e3d54ff6694df8a1b2c6498da321e2ead311de9c42e653d8fb3213de72bac204b5f97e4a110cda8a72f17b1c1b4eb643 + languageName: node + linkType: hard + +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: "npm:^2.0.0" + checksum: a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 + languageName: node + linkType: hard + +"is-boolean-object@npm:^1.1.0": + version: 1.1.2 + resolution: "is-boolean-object@npm:1.1.2" + dependencies: + call-bind: "npm:^1.0.2" + has-tostringtag: "npm:^1.0.0" + checksum: 6090587f8a8a8534c0f816da868bc94f32810f08807aa72fa7e79f7e11c466d281486ffe7a788178809c2aa71fe3e700b167fe80dd96dad68026bfff8ebf39f7 + languageName: node + linkType: hard + +"is-buffer@npm:^1.1.5": + version: 1.1.6 + resolution: "is-buffer@npm:1.1.6" + checksum: ae18aa0b6e113d6c490ad1db5e8df9bdb57758382b313f5a22c9c61084875c6396d50bbf49315f5b1926d142d74dfb8d31b40d993a383e0a158b15fea7a82234 + languageName: node + linkType: hard + +"is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7": + version: 1.2.7 + resolution: "is-callable@npm:1.2.7" + checksum: ceebaeb9d92e8adee604076971dd6000d38d6afc40bb843ea8e45c5579b57671c3f3b50d7f04869618242c6cee08d1b67806a8cb8edaaaf7c0748b3720d6066f + languageName: node + linkType: hard + +"is-core-module@npm:^2.13.0": + version: 2.13.1 + resolution: "is-core-module@npm:2.13.1" + dependencies: + hasown: "npm:^2.0.0" + checksum: 2cba9903aaa52718f11c4896dabc189bab980870aae86a62dc0d5cedb546896770ee946fb14c84b7adf0735f5eaea4277243f1b95f5cefa90054f92fbcac2518 + languageName: node + linkType: hard + +"is-data-descriptor@npm:^1.0.1": + version: 1.0.1 + resolution: "is-data-descriptor@npm:1.0.1" + dependencies: + hasown: "npm:^2.0.0" + checksum: ad3acc372e3227f87eb8cdba112c343ca2a67f1885aecf64f02f901cb0858a1fc9488ad42135ab102e9d9e71a62b3594740790bb103a9ba5da830a131a89e3e8 + languageName: node + linkType: hard + +"is-date-object@npm:^1.0.1, is-date-object@npm:^1.0.5": + version: 1.0.5 + resolution: "is-date-object@npm:1.0.5" + dependencies: + has-tostringtag: "npm:^1.0.0" + checksum: eed21e5dcc619c48ccef804dfc83a739dbb2abee6ca202838ee1bd5f760fe8d8a93444f0d49012ad19bb7c006186e2884a1b92f6e1c056da7fd23d0a9ad5992e + languageName: node + linkType: hard + +"is-deflate@npm:^1.0.0": + version: 1.0.0 + resolution: "is-deflate@npm:1.0.0" + checksum: 35f7ffcbef3549dd8a4d8df5dc09b4f4656a0fc88326e8b5201cda54114a9c2d8efb689d87c16f3f35c95bd71dcf13dc790d62b7504745b42c53ab4b40238f5a + languageName: node + linkType: hard + +"is-descriptor@npm:^0.1.0": + version: 0.1.7 + resolution: "is-descriptor@npm:0.1.7" + dependencies: + is-accessor-descriptor: "npm:^1.0.1" + is-data-descriptor: "npm:^1.0.1" + checksum: f5960b9783f508aec570465288cb673d4b3cc4aae4e6de970c3afd9a8fc1351edcb85d78b2cce2ec5251893a423f73263cab3bb94cf365a8d71b5d510a116392 + languageName: node + linkType: hard + +"is-descriptor@npm:^1.0.0, is-descriptor@npm:^1.0.2": + version: 1.0.3 + resolution: "is-descriptor@npm:1.0.3" + dependencies: + is-accessor-descriptor: "npm:^1.0.1" + is-data-descriptor: "npm:^1.0.1" + checksum: b4ee667ea787d3a0be4e58536087fd0587de2b0b6672fbfe288f5b8d831ac4b79fd987f31d6c2d4e5543a42c97a87428bc5215ce292a1a47070147793878226f + languageName: node + linkType: hard + +"is-docker@npm:^2.0.0, is-docker@npm:^2.1.1": + version: 2.2.1 + resolution: "is-docker@npm:2.2.1" + bin: + is-docker: cli.js + checksum: e828365958d155f90c409cdbe958f64051d99e8aedc2c8c4cd7c89dcf35329daed42f7b99346f7828df013e27deb8f721cf9408ba878c76eb9e8290235fbcdcc + languageName: node + linkType: hard + +"is-extendable@npm:^0.1.0, is-extendable@npm:^0.1.1": + version: 0.1.1 + resolution: "is-extendable@npm:0.1.1" + checksum: dd5ca3994a28e1740d1e25192e66eed128e0b2ff161a7ea348e87ae4f616554b486854de423877a2a2c171d5f7cd6e8093b91f54533bc88a59ee1c9838c43879 + languageName: node + linkType: hard + +"is-extendable@npm:^1.0.1": + version: 1.0.1 + resolution: "is-extendable@npm:1.0.1" + dependencies: + is-plain-object: "npm:^2.0.4" + checksum: 1d6678a5be1563db6ecb121331c819c38059703f0179f52aa80c242c223ee9c6b66470286636c0e63d7163e4d905c0a7d82a096e0b5eaeabb51b9f8d0af0d73f + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.0, is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: 5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^1.0.0": + version: 1.0.0 + resolution: "is-fullwidth-code-point@npm:1.0.0" + dependencies: + number-is-nan: "npm:^1.0.0" + checksum: 12acfcf16142f2d431bf6af25d68569d3198e81b9799b4ae41058247aafcc666b0127d64384ea28e67a746372611fcbe9b802f69175287aba466da3eddd5ba0f + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + languageName: node + linkType: hard + +"is-generator-function@npm:^1.0.7": + version: 1.0.10 + resolution: "is-generator-function@npm:1.0.10" + dependencies: + has-tostringtag: "npm:^1.0.0" + checksum: df03514df01a6098945b5a0cfa1abff715807c8e72f57c49a0686ad54b3b74d394e2d8714e6f709a71eb00c9630d48e73ca1796c1ccc84ac95092c1fecc0d98b + languageName: node + linkType: hard + +"is-glob@npm:^3.1.0": + version: 3.1.0 + resolution: "is-glob@npm:3.1.0" + dependencies: + is-extglob: "npm:^2.1.0" + checksum: ba816a35dcf5285de924a8a4654df7b183a86381d73ea3bbf3df3cc61b3ba61fdddf90ee205709a2235b210ee600ee86e5e8600093cf291a662607fd032e2ff4 + languageName: node + linkType: hard + +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:~4.0.1": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: "npm:^2.1.1" + checksum: 17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a + languageName: node + linkType: hard + +"is-gzip@npm:^1.0.0": + version: 1.0.0 + resolution: "is-gzip@npm:1.0.0" + checksum: cbc1db080c636a6fb0f7346e3076f8276a29a9d8b52ae67c1971a8131c43f308e98ed227d1a6f49970e6c6ebabee0568e60aed7a3579dd4e1817cddf2faaf9b7 + languageName: node + linkType: hard + +"is-interactive@npm:^1.0.0": + version: 1.0.0 + resolution: "is-interactive@npm:1.0.0" + checksum: dd47904dbf286cd20aa58c5192161be1a67138485b9836d5a70433b21a45442e9611b8498b8ab1f839fc962c7620667a50535fdfb4a6bc7989b8858645c06b4d + languageName: node + linkType: hard + +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d + languageName: node + linkType: hard + +"is-map@npm:^2.0.1, is-map@npm:^2.0.2": + version: 2.0.2 + resolution: "is-map@npm:2.0.2" + checksum: 119ff9137a37fd131a72fab3f4ab8c9d6a24b0a1ee26b4eff14dc625900d8675a97785eea5f4174265e2006ed076cc24e89f6e57ebd080a48338d914ec9168a5 + languageName: node + linkType: hard + +"is-nan@npm:^1.3.2": + version: 1.3.2 + resolution: "is-nan@npm:1.3.2" + dependencies: + call-bind: "npm:^1.0.0" + define-properties: "npm:^1.1.3" + checksum: 8bfb286f85763f9c2e28ea32e9127702fe980ffd15fa5d63ade3be7786559e6e21355d3625dd364c769c033c5aedf0a2ed3d4025d336abf1b9241e3d9eddc5b0 + languageName: node + linkType: hard + +"is-negated-glob@npm:^1.0.0": + version: 1.0.0 + resolution: "is-negated-glob@npm:1.0.0" + checksum: f9d4fb2effd7a6d0e4770463e4cf708fbff2d5b660ab2043e5703e21e3234dfbe9974fdd8c08eb80f9898d5dd3d21b020e8d07fce387cd394a79991f01cd8d1c + languageName: node + linkType: hard + +"is-negative-zero@npm:^2.0.2": + version: 2.0.2 + resolution: "is-negative-zero@npm:2.0.2" + checksum: eda024c158f70f2017f3415e471b818d314da5ef5be68f801b16314d4a4b6304a74cbed778acf9e2f955bb9c1c5f2935c1be0c7c99e1ad12286f45366217b6a3 + languageName: node + linkType: hard + +"is-number-object@npm:^1.0.4": + version: 1.0.7 + resolution: "is-number-object@npm:1.0.7" + dependencies: + has-tostringtag: "npm:^1.0.0" + checksum: aad266da1e530f1804a2b7bd2e874b4869f71c98590b3964f9d06cc9869b18f8d1f4778f838ecd2a11011bce20aeecb53cb269ba916209b79c24580416b74b1b + languageName: node + linkType: hard + +"is-number@npm:^3.0.0": + version: 3.0.0 + resolution: "is-number@npm:3.0.0" + dependencies: + kind-of: "npm:^3.0.2" + checksum: e639c54640b7f029623df24d3d103901e322c0c25ea5bde97cd723c2d0d4c05857a8364ab5c58d963089dbed6bf1d0ffe975cb6aef917e2ad0ccbca653d31b4f + languageName: node + linkType: hard + +"is-number@npm:^4.0.0": + version: 4.0.0 + resolution: "is-number@npm:4.0.0" + checksum: bb17a331f357eb59a7f8db848086c41886715b2ea1db03f284a99d14001cda094083a5b6a7b343b5bcf410ccef668a70bc626d07bc2032cc4ab46dd264cea244 + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811 + languageName: node + linkType: hard + +"is-path-cwd@npm:^2.2.0": + version: 2.2.0 + resolution: "is-path-cwd@npm:2.2.0" + checksum: afce71533a427a759cd0329301c18950333d7589533c2c90205bd3fdcf7b91eb92d1940493190567a433134d2128ec9325de2fd281e05be1920fbee9edd22e0a + languageName: node + linkType: hard + +"is-path-inside@npm:^3.0.2": + version: 3.0.3 + resolution: "is-path-inside@npm:3.0.3" + checksum: cf7d4ac35fb96bab6a1d2c3598fe5ebb29aafb52c0aaa482b5a3ed9d8ba3edc11631e3ec2637660c44b3ce0e61a08d54946e8af30dec0b60a7c27296c68ffd05 + languageName: node + linkType: hard + +"is-plain-object@npm:5.0.0, is-plain-object@npm:^5.0.0": + version: 5.0.0 + resolution: "is-plain-object@npm:5.0.0" + checksum: 893e42bad832aae3511c71fd61c0bf61aa3a6d853061c62a307261842727d0d25f761ce9379f7ba7226d6179db2a3157efa918e7fe26360f3bf0842d9f28942c + languageName: node + linkType: hard + +"is-plain-object@npm:^2.0.1, is-plain-object@npm:^2.0.3, is-plain-object@npm:^2.0.4": + version: 2.0.4 + resolution: "is-plain-object@npm:2.0.4" + dependencies: + isobject: "npm:^3.0.1" + checksum: f050fdd5203d9c81e8c4df1b3ff461c4bc64e8b5ca383bcdde46131361d0a678e80bcf00b5257646f6c636197629644d53bd8e2375aea633de09a82d57e942f4 + languageName: node + linkType: hard + +"is-potential-custom-element-name@npm:^1.0.1": + version: 1.0.1 + resolution: "is-potential-custom-element-name@npm:1.0.1" + checksum: b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9 + languageName: node + linkType: hard + +"is-promise@npm:^2.2.2": + version: 2.2.2 + resolution: "is-promise@npm:2.2.2" + checksum: 2dba959812380e45b3df0fb12e7cb4d4528c989c7abb03ececb1d1fd6ab1cbfee956ca9daa587b9db1d8ac3c1e5738cf217bdb3dfd99df8c691be4c00ae09069 + languageName: node + linkType: hard + +"is-regex@npm:^1.1.4": + version: 1.1.4 + resolution: "is-regex@npm:1.1.4" + dependencies: + call-bind: "npm:^1.0.2" + has-tostringtag: "npm:^1.0.0" + checksum: bb72aae604a69eafd4a82a93002058c416ace8cde95873589a97fc5dac96a6c6c78a9977d487b7b95426a8f5073969124dd228f043f9f604f041f32fcc465fc1 + languageName: node + linkType: hard + +"is-relative@npm:^1.0.0": + version: 1.0.0 + resolution: "is-relative@npm:1.0.0" + dependencies: + is-unc-path: "npm:^1.0.0" + checksum: 61157c4be8594dd25ac6f0ef29b1218c36667259ea26698367a4d9f39ff9018368bc365c490b3c79be92dfb1e389e43c4b865c95709e7b3bc72c5932f751fb60 + languageName: node + linkType: hard + +"is-set@npm:^2.0.1, is-set@npm:^2.0.2": + version: 2.0.2 + resolution: "is-set@npm:2.0.2" + checksum: 5f8bd1880df8c0004ce694e315e6e1e47a3452014be792880bb274a3b2cdb952fdb60789636ca6e084c7947ca8b7ae03ccaf54c93a7fcfed228af810559e5432 + languageName: node + linkType: hard + +"is-shared-array-buffer@npm:^1.0.2": + version: 1.0.2 + resolution: "is-shared-array-buffer@npm:1.0.2" + dependencies: + call-bind: "npm:^1.0.2" + checksum: cfeee6f171f1b13e6cbc6f3b6cc44e192b93df39f3fcb31aa66ffb1d2df3b91e05664311659f9701baba62f5e98c83b0673c628e7adc30f55071c4874fcdccec + languageName: node + linkType: hard + +"is-stream@npm:^2.0.0": + version: 2.0.1 + resolution: "is-stream@npm:2.0.1" + checksum: 7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5 + languageName: node + linkType: hard + +"is-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "is-stream@npm:3.0.0" + checksum: eb2f7127af02ee9aa2a0237b730e47ac2de0d4e76a4a905a50a11557f2339df5765eaea4ceb8029f1efa978586abe776908720bfcb1900c20c6ec5145f6f29d8 + languageName: node + linkType: hard + +"is-string@npm:^1.0.5, is-string@npm:^1.0.7": + version: 1.0.7 + resolution: "is-string@npm:1.0.7" + dependencies: + has-tostringtag: "npm:^1.0.0" + checksum: 905f805cbc6eedfa678aaa103ab7f626aac9ebbdc8737abb5243acaa61d9820f8edc5819106b8fcd1839e33db21de9f0116ae20de380c8382d16dc2a601921f6 + languageName: node + linkType: hard + +"is-symbol@npm:^1.0.2, is-symbol@npm:^1.0.3": + version: 1.0.4 + resolution: "is-symbol@npm:1.0.4" + dependencies: + has-symbols: "npm:^1.0.2" + checksum: 9381dd015f7c8906154dbcbf93fad769de16b4b961edc94f88d26eb8c555935caa23af88bda0c93a18e65560f6d7cca0fd5a3f8a8e1df6f1abbb9bead4502ef7 + languageName: node + linkType: hard + +"is-typed-array@npm:^1.1.10, is-typed-array@npm:^1.1.12, is-typed-array@npm:^1.1.3, is-typed-array@npm:^1.1.9": + version: 1.1.12 + resolution: "is-typed-array@npm:1.1.12" + dependencies: + which-typed-array: "npm:^1.1.11" + checksum: 9863e9cc7223c6fc1c462a2c3898a7beff6b41b1ee0fabb03b7d278ae7de670b5bcbc8627db56bb66ed60902fa37d53fe5cce0fd2f7d73ac64fe5da6f409b6ae + languageName: node + linkType: hard + +"is-unc-path@npm:^1.0.0": + version: 1.0.0 + resolution: "is-unc-path@npm:1.0.0" + dependencies: + unc-path-regex: "npm:^0.1.2" + checksum: ac1b78f9b748196e3be3d0e722cd4b0f98639247a130a8f2473a58b29baf63fdb1b1c5a12c830660c5ee6ef0279c5418ca8e346f98cbe1a29e433d7ae531d42e + languageName: node + linkType: hard + +"is-unicode-supported@npm:^0.1.0": + version: 0.1.0 + resolution: "is-unicode-supported@npm:0.1.0" + checksum: 00cbe3455c3756be68d2542c416cab888aebd5012781d6819749fefb15162ff23e38501fe681b3d751c73e8ff561ac09a5293eba6f58fdf0178462ce6dcb3453 + languageName: node + linkType: hard + +"is-utf8@npm:^0.2.0, is-utf8@npm:^0.2.1": + version: 0.2.1 + resolution: "is-utf8@npm:0.2.1" + checksum: 3ed45e5b4ddfa04ed7e32c63d29c61b980ecd6df74698f45978b8c17a54034943bcbffb6ae243202e799682a66f90fef526f465dd39438745e9fe70794c1ef09 + languageName: node + linkType: hard + +"is-valid-glob@npm:^1.0.0": + version: 1.0.0 + resolution: "is-valid-glob@npm:1.0.0" + checksum: 73aef3a2dc218b677362c876d1bc69699e10cfb50ecae6ac5fa946d7f5bb783721e81d9383bd120e4fb7bcfaa7ebe1edab0b707fd93051cc6e04f90f02d689b6 + languageName: node + linkType: hard + +"is-weakmap@npm:^2.0.1": + version: 2.0.1 + resolution: "is-weakmap@npm:2.0.1" + checksum: 9c9fec9efa7bf5030a4a927f33fff2a6976b93646259f92b517d3646c073cc5b98283a162ce75c412b060a46de07032444b530f0a4c9b6e012ef8f1741c3a987 + languageName: node + linkType: hard + +"is-weakref@npm:^1.0.2": + version: 1.0.2 + resolution: "is-weakref@npm:1.0.2" + dependencies: + call-bind: "npm:^1.0.2" + checksum: 1545c5d172cb690c392f2136c23eec07d8d78a7f57d0e41f10078aa4f5daf5d7f57b6513a67514ab4f073275ad00c9822fc8935e00229d0a2089e1c02685d4b1 + languageName: node + linkType: hard + +"is-weakset@npm:^2.0.1": + version: 2.0.2 + resolution: "is-weakset@npm:2.0.2" + dependencies: + call-bind: "npm:^1.0.2" + get-intrinsic: "npm:^1.1.1" + checksum: ef5136bd446ae4603229b897f73efd0720c6ab3ec6cc05c8d5c4b51aa9f95164713c4cad0a22ff1fedf04865ff86cae4648bc1d5eead4b6388e1150525af1cc1 + languageName: node + linkType: hard + +"is-windows@npm:^1.0.1, is-windows@npm:^1.0.2": + version: 1.0.2 + resolution: "is-windows@npm:1.0.2" + checksum: b32f418ab3385604a66f1b7a3ce39d25e8881dee0bd30816dc8344ef6ff9df473a732bcc1ec4e84fe99b2f229ae474f7133e8e93f9241686cfcf7eebe53ba7a5 + languageName: node + linkType: hard + +"is-wsl@npm:^2.2.0": + version: 2.2.0 + resolution: "is-wsl@npm:2.2.0" + dependencies: + is-docker: "npm:^2.0.0" + checksum: a6fa2d370d21be487c0165c7a440d567274fbba1a817f2f0bfa41cc5e3af25041d84267baa22df66696956038a43973e72fca117918c91431920bdef490fa25e + languageName: node + linkType: hard + +"isarray@npm:1.0.0, isarray@npm:^1.0.0, isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: 18b5be6669be53425f0b84098732670ed4e727e3af33bc7f948aac01782110eb9a18b3b329c5323bcdd3acdaae547ee077d3951317e7f133bff7105264b3003d + languageName: node + linkType: hard + +"isarray@npm:^2.0.5": + version: 2.0.5 + resolution: "isarray@npm:2.0.5" + checksum: 4199f14a7a13da2177c66c31080008b7124331956f47bca57dd0b6ea9f11687aa25e565a2c7a2b519bc86988d10398e3049a1f5df13c9f6b7664154690ae79fd + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + languageName: node + linkType: hard + +"isobject@npm:^2.0.0": + version: 2.1.0 + resolution: "isobject@npm:2.1.0" + dependencies: + isarray: "npm:1.0.0" + checksum: c4cafec73b3b2ee11be75dff8dafd283b5728235ac099b07d7873d5182553a707768e208327bbc12931b9422d8822280bf88d894a0024ff5857b3efefb480e7b + languageName: node + linkType: hard + +"isobject@npm:^3.0.0, isobject@npm:^3.0.1": + version: 3.0.1 + resolution: "isobject@npm:3.0.1" + checksum: 03344f5064a82f099a0cd1a8a407f4c0d20b7b8485e8e816c39f249e9416b06c322e8dec5b842b6bb8a06de0af9cb48e7bc1b5352f0fadc2f0abac033db3d4db + languageName: node + linkType: hard + +"istanbul-lib-coverage@npm:^3.2.0": + version: 3.2.2 + resolution: "istanbul-lib-coverage@npm:3.2.2" + checksum: 6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^5.0.4": + version: 5.2.1 + resolution: "istanbul-lib-instrument@npm:5.2.1" + dependencies: + "@babel/core": "npm:^7.12.3" + "@babel/parser": "npm:^7.14.7" + "@istanbuljs/schema": "npm:^0.1.2" + istanbul-lib-coverage: "npm:^3.2.0" + semver: "npm:^6.3.0" + checksum: 8a1bdf3e377dcc0d33ec32fe2b6ecacdb1e4358fd0eb923d4326bb11c67622c0ceb99600a680f3dad5d29c66fc1991306081e339b4d43d0b8a2ab2e1d910a6ee + languageName: node + linkType: hard + +"jackspeak@npm:^2.3.5": + version: 2.3.6 + resolution: "jackspeak@npm:2.3.6" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: f01d8f972d894cd7638bc338e9ef5ddb86f7b208ce177a36d718eac96ec86638a6efa17d0221b10073e64b45edc2ce15340db9380b1f5d5c5d000cbc517dc111 + languageName: node + linkType: hard + +"jake@npm:^10.8.5": + version: 10.8.7 + resolution: "jake@npm:10.8.7" + dependencies: + async: "npm:^3.2.3" + chalk: "npm:^4.0.2" + filelist: "npm:^1.0.4" + minimatch: "npm:^3.1.2" + bin: + jake: bin/cli.js + checksum: 89326d01a8bc110d02d973729a66394c79a34b34461116f5c530a2a2dbc30265683fe6737928f75df9178e9d369ff1442f5753fb983d525e740eefdadc56a103 + languageName: node + linkType: hard + +"jest-haste-map@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-haste-map@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/graceful-fs": "npm:^4.1.3" + "@types/node": "npm:*" + anymatch: "npm:^3.0.3" + fb-watchman: "npm:^2.0.0" + fsevents: "npm:^2.3.2" + graceful-fs: "npm:^4.2.9" + jest-regex-util: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + walker: "npm:^1.0.8" + dependenciesMeta: + fsevents: + optional: true + checksum: 2683a8f29793c75a4728787662972fedd9267704c8f7ef9d84f2beed9a977f1cf5e998c07b6f36ba5603f53cb010c911fe8cd0ac9886e073fe28ca66beefd30c + languageName: node + linkType: hard + +"jest-mock@npm:^27.0.6": + version: 27.5.1 + resolution: "jest-mock@npm:27.5.1" + dependencies: + "@jest/types": "npm:^27.5.1" + "@types/node": "npm:*" + checksum: 6ad58454b37ee3f726930b07efbf40a7c79d2d2d9c7b226708b4b550bc0904de93bcacf714105d11952a5c0bc855e5d59145c8c9dbbb4e69b46e7367abf53b52 + languageName: node + linkType: hard + +"jest-regex-util@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-regex-util@npm:29.6.3" + checksum: 4e33fb16c4f42111159cafe26397118dcfc4cf08bc178a67149fb05f45546a91928b820894572679d62559839d0992e21080a1527faad65daaae8743a5705a3b + languageName: node + linkType: hard + +"jest-util@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-util@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + graceful-fs: "npm:^4.2.9" + picomatch: "npm:^2.2.3" + checksum: bc55a8f49fdbb8f51baf31d2a4f312fb66c9db1483b82f602c9c990e659cdd7ec529c8e916d5a89452ecbcfae4949b21b40a7a59d4ffc0cd813a973ab08c8150 + languageName: node + linkType: hard + +"jest-worker@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-worker@npm:29.7.0" + dependencies: + "@types/node": "npm:*" + jest-util: "npm:^29.7.0" + merge-stream: "npm:^2.0.0" + supports-color: "npm:^8.0.0" + checksum: 5570a3a005b16f46c131968b8a5b56d291f9bbb85ff4217e31c80bd8a02e7de799e59a54b95ca28d5c302f248b54cbffde2d177c2f0f52ffcee7504c6eabf660 + languageName: node + linkType: hard + +"js-beautify@npm:^1.15.1": + version: 1.15.1 + resolution: "js-beautify@npm:1.15.1" + dependencies: + config-chain: "npm:^1.1.13" + editorconfig: "npm:^1.0.4" + glob: "npm:^10.3.3" + js-cookie: "npm:^3.0.5" + nopt: "npm:^7.2.0" + bin: + css-beautify: js/bin/css-beautify.js + html-beautify: js/bin/html-beautify.js + js-beautify: js/bin/js-beautify.js + checksum: 4140dd95537143eb429b6c8e47e21310f16c032d97a03163c6c7c0502bc663242a5db08d3ad941b87f24a142ce4f9190c556d2340bcd056545326377dfae5362 + languageName: node + linkType: hard + +"js-cookie@npm:^3.0.5": + version: 3.0.5 + resolution: "js-cookie@npm:3.0.5" + checksum: 04a0e560407b4489daac3a63e231d35f4e86f78bff9d792011391b49c59f721b513411cd75714c418049c8dc9750b20fcddad1ca5a2ca616c3aca4874cce5b3a + languageName: node + linkType: hard + +"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": + version: 4.0.0 + resolution: "js-tokens@npm:4.0.0" + checksum: e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed + languageName: node + linkType: hard + +"js-tokens@npm:^8.0.2": + version: 8.0.3 + resolution: "js-tokens@npm:8.0.3" + checksum: b50ba7d926b087ad31949d8155c7bc84374e0785019b17bdddeb2c4f98f5dea04ba464651fe23a8be4f7d15f50d06ce8bb536087b24ce3ebfbaea4a1dc5869f0 + languageName: node + linkType: hard + +"js-yaml@npm:^3.13.1": + version: 3.14.1 + resolution: "js-yaml@npm:3.14.1" + dependencies: + argparse: "npm:^1.0.7" + esprima: "npm:^4.0.0" + bin: + js-yaml: bin/js-yaml.js + checksum: 6746baaaeac312c4db8e75fa22331d9a04cccb7792d126ed8ce6a0bbcfef0cedaddd0c5098fade53db067c09fe00aa1c957674b4765610a8b06a5a189e46433b + languageName: node + linkType: hard + +"js-yaml@npm:^4.1.0": + version: 4.1.0 + resolution: "js-yaml@npm:4.1.0" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: 184a24b4eaacfce40ad9074c64fd42ac83cf74d8c8cd137718d456ced75051229e5061b8633c3366b8aada17945a7a356b337828c19da92b51ae62126575018f + languageName: node + linkType: hard + +"jscodeshift@npm:^0.15.1": + version: 0.15.1 + resolution: "jscodeshift@npm:0.15.1" + dependencies: + "@babel/core": "npm:^7.23.0" + "@babel/parser": "npm:^7.23.0" + "@babel/plugin-transform-class-properties": "npm:^7.22.5" + "@babel/plugin-transform-modules-commonjs": "npm:^7.23.0" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.22.11" + "@babel/plugin-transform-optional-chaining": "npm:^7.23.0" + "@babel/plugin-transform-private-methods": "npm:^7.22.5" + "@babel/preset-flow": "npm:^7.22.15" + "@babel/preset-typescript": "npm:^7.23.0" + "@babel/register": "npm:^7.22.15" + babel-core: "npm:^7.0.0-bridge.0" + chalk: "npm:^4.1.2" + flow-parser: "npm:0.*" + graceful-fs: "npm:^4.2.4" + micromatch: "npm:^4.0.4" + neo-async: "npm:^2.5.0" + node-dir: "npm:^0.1.17" + recast: "npm:^0.23.3" + temp: "npm:^0.8.4" + write-file-atomic: "npm:^2.3.0" + peerDependencies: + "@babel/preset-env": ^7.1.6 + peerDependenciesMeta: + "@babel/preset-env": + optional: true + bin: + jscodeshift: bin/jscodeshift.js + checksum: 334de6ffa776a68b3f59f2f18a285ea977f3339d85e3517f3854761e65769ffa7e453c35cde320fc969106d573df39bd3fb08b23db54ae17c1b1516e5bf05742 + languageName: node + linkType: hard + +"jsdom@npm:^24.0.0": + version: 24.0.0 + resolution: "jsdom@npm:24.0.0" + dependencies: + cssstyle: "npm:^4.0.1" + data-urls: "npm:^5.0.0" + decimal.js: "npm:^10.4.3" + form-data: "npm:^4.0.0" + html-encoding-sniffer: "npm:^4.0.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.2" + is-potential-custom-element-name: "npm:^1.0.1" + nwsapi: "npm:^2.2.7" + parse5: "npm:^7.1.2" + rrweb-cssom: "npm:^0.6.0" + saxes: "npm:^6.0.0" + symbol-tree: "npm:^3.2.4" + tough-cookie: "npm:^4.1.3" + w3c-xmlserializer: "npm:^5.0.0" + webidl-conversions: "npm:^7.0.0" + whatwg-encoding: "npm:^3.1.1" + whatwg-mimetype: "npm:^4.0.0" + whatwg-url: "npm:^14.0.0" + ws: "npm:^8.16.0" + xml-name-validator: "npm:^5.0.0" + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + checksum: 7b35043d7af39ad6dcaef0fa5679d8c8a94c6c9b6cc4a79222b7c9987d57ab7150c50856684ae56b473ab28c7d82aec0fb7ca19dcbd4c3f46683c807d717a3af + languageName: node + linkType: hard + +"jsesc@npm:^2.5.1": + version: 2.5.2 + resolution: "jsesc@npm:2.5.2" + bin: + jsesc: bin/jsesc + checksum: dbf59312e0ebf2b4405ef413ec2b25abb5f8f4d9bc5fb8d9f90381622ebca5f2af6a6aa9a8578f65903f9e33990a6dc798edd0ce5586894bf0e9e31803a1de88 + languageName: node + linkType: hard + +"jsesc@npm:~0.5.0": + version: 0.5.0 + resolution: "jsesc@npm:0.5.0" + bin: + jsesc: bin/jsesc + checksum: f93792440ae1d80f091b65f8ceddf8e55c4bb7f1a09dee5dcbdb0db5612c55c0f6045625aa6b7e8edb2e0a4feabd80ee48616dbe2d37055573a84db3d24f96d9 + languageName: node + linkType: hard + +"json-parse-better-errors@npm:^1.0.1": + version: 1.0.2 + resolution: "json-parse-better-errors@npm:1.0.2" + checksum: 2f1287a7c833e397c9ddd361a78638e828fc523038bb3441fd4fc144cfd2c6cd4963ffb9e207e648cf7b692600f1e1e524e965c32df5152120910e4903a47dcb + languageName: node + linkType: hard + +"json-parse-even-better-errors@npm:^2.3.0": + version: 2.3.1 + resolution: "json-parse-even-better-errors@npm:2.3.1" + checksum: 140932564c8f0b88455432e0f33c4cb4086b8868e37524e07e723f4eaedb9425bdc2bafd71bd1d9765bd15fd1e2d126972bc83990f55c467168c228c24d665f3 + languageName: node + linkType: hard + +"json-stable-stringify-without-jsonify@npm:^1.0.1": + version: 1.0.1 + resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" + checksum: cb168b61fd4de83e58d09aaa6425ef71001bae30d260e2c57e7d09a5fd82223e2f22a042dedaab8db23b7d9ae46854b08bb1f91675a8be11c5cffebef5fb66a5 + languageName: node + linkType: hard + +"json5@npm:^2.2.3": + version: 2.2.3 + resolution: "json5@npm:2.2.3" + bin: + json5: lib/cli.js + checksum: 5a04eed94810fa55c5ea138b2f7a5c12b97c3750bc63d11e511dcecbfef758003861522a070c2272764ee0f4e3e323862f386945aeb5b85b87ee43f084ba586c + languageName: node + linkType: hard + +"jsonc-parser@npm:^3.2.0": + version: 3.2.0 + resolution: "jsonc-parser@npm:3.2.0" + checksum: 5a12d4d04dad381852476872a29dcee03a57439574e4181d91dca71904fcdcc5e8e4706c0a68a2c61ad9810e1e1c5806b5100d52d3e727b78f5cdc595401045b + languageName: node + linkType: hard + +"jsonfile@npm:^6.0.1": + version: 6.1.0 + resolution: "jsonfile@npm:6.1.0" + dependencies: + graceful-fs: "npm:^4.1.6" + universalify: "npm:^2.0.0" + dependenciesMeta: + graceful-fs: + optional: true + checksum: 4f95b5e8a5622b1e9e8f33c96b7ef3158122f595998114d1e7f03985649ea99cb3cd99ce1ed1831ae94c8c8543ab45ebd044207612f31a56fd08462140e46865 + languageName: node + linkType: hard + +"jszip@npm:^3.10.1": + version: 3.10.1 + resolution: "jszip@npm:3.10.1" + dependencies: + lie: "npm:~3.3.0" + pako: "npm:~1.0.2" + readable-stream: "npm:~2.3.6" + setimmediate: "npm:^1.0.5" + checksum: 58e01ec9c4960383fb8b38dd5f67b83ccc1ec215bf74c8a5b32f42b6e5fb79fada5176842a11409c4051b5b94275044851814a31076bf49e1be218d3ef57c863 + languageName: node + linkType: hard + +"just-debounce@npm:^1.0.0": + version: 1.1.0 + resolution: "just-debounce@npm:1.1.0" + checksum: 462ce68eef6068414bd70dfb1f43ff4e1911330b6473fcdd9d52482fbf544e7666572ca35b6b2297c3baaa944e682ddae8a1b2eb3fc75bb8978ea8192b7f6705 + languageName: node + linkType: hard + +"kind-of@npm:^3.0.2, kind-of@npm:^3.0.3, kind-of@npm:^3.2.0": + version: 3.2.2 + resolution: "kind-of@npm:3.2.2" + dependencies: + is-buffer: "npm:^1.1.5" + checksum: 7e34bc29d4b02c997f92f080de34ebb92033a96736bbb0bb2410e033a7e5ae6571f1fa37b2d7710018f95361473b816c604234197f4f203f9cf149d8ef1574d9 + languageName: node + linkType: hard + +"kind-of@npm:^4.0.0": + version: 4.0.0 + resolution: "kind-of@npm:4.0.0" + dependencies: + is-buffer: "npm:^1.1.5" + checksum: d6c44c75ee36898142dfc7106afbd50593216c37f96acb81a7ab33ca1a6938ce97d5692b8fc8fccd035f83811a9d97749d68771116441a48eedd0b68e2973165 + languageName: node + linkType: hard + +"kind-of@npm:^5.0.2": + version: 5.1.0 + resolution: "kind-of@npm:5.1.0" + checksum: fe85b7a2ed4b4d5a12e16e01d00d5c336e1760842fe0da38283605b9880c984288935e87b13138909e4d23d2d197a1d492f7393c6638d2c0fab8a900c4fb0392 + languageName: node + linkType: hard + +"kind-of@npm:^6.0.2": + version: 6.0.3 + resolution: "kind-of@npm:6.0.3" + checksum: 61cdff9623dabf3568b6445e93e31376bee1cdb93f8ba7033d86022c2a9b1791a1d9510e026e6465ebd701a6dd2f7b0808483ad8838341ac52f003f512e0b4c4 + languageName: node + linkType: hard + +"kleur@npm:^3.0.3": + version: 3.0.3 + resolution: "kleur@npm:3.0.3" + checksum: cd3a0b8878e7d6d3799e54340efe3591ca787d9f95f109f28129bdd2915e37807bf8918bb295ab86afb8c82196beec5a1adcaf29042ce3f2bd932b038fe3aa4b + languageName: node + linkType: hard + +"kuler@npm:^2.0.0": + version: 2.0.0 + resolution: "kuler@npm:2.0.0" + checksum: 0a4e99d92ca373f8f74d1dc37931909c4d0d82aebc94cf2ba265771160fc12c8df34eaaac80805efbda367e2795cb1f1dd4c3d404b6b1cf38aec94035b503d2d + languageName: node + linkType: hard + +"last-run@npm:^1.1.0": + version: 1.1.1 + resolution: "last-run@npm:1.1.1" + dependencies: + default-resolution: "npm:^2.0.0" + es6-weak-map: "npm:^2.0.1" + checksum: dd468d32839d1f548e0b30b76fbb015aa01c1a10bcddedfe39d7f06612e91292899411aaecd6c420a024c368d853fa8845613f7b304b3d173892be07872a4a9c + languageName: node + linkType: hard + +"lazy-universal-dotenv@npm:^4.0.0": + version: 4.0.0 + resolution: "lazy-universal-dotenv@npm:4.0.0" + dependencies: + app-root-dir: "npm:^1.0.2" + dotenv: "npm:^16.0.0" + dotenv-expand: "npm:^10.0.0" + checksum: 3bc4fe649c46c4a20561ca1fd10cd1df641d2c6c42c61af6c65a5fe0546cb548f449e13e6c7440be445c9fe5b4973c25f499e7d899b8704b7b9bd0ec85bbfe2d + languageName: node + linkType: hard + +"lazystream@npm:^1.0.0": + version: 1.0.1 + resolution: "lazystream@npm:1.0.1" + dependencies: + readable-stream: "npm:^2.0.5" + checksum: ea4e509a5226ecfcc303ba6782cc269be8867d372b9bcbd625c88955df1987ea1a20da4643bf9270336415a398d33531ebf0d5f0d393b9283dc7c98bfcbd7b69 + languageName: node + linkType: hard + +"lcid@npm:^1.0.0": + version: 1.0.0 + resolution: "lcid@npm:1.0.0" + dependencies: + invert-kv: "npm:^1.0.0" + checksum: 87fb32196c3c80458778f34f71c042e114f3134a3c86c0d60ee9c94f0750e467d7ca0c005a5224ffd9d49a6e449b5e5c31e1544f1827765a0ba8747298f5980e + languageName: node + linkType: hard + +"lead@npm:^1.0.0": + version: 1.0.0 + resolution: "lead@npm:1.0.0" + dependencies: + flush-write-stream: "npm:^1.0.2" + checksum: 355fa4cce74a62cec9d4dc4520a8a6a3bd0472e88e070208a895aa1d144bd5f35a099e0f0d4938f4bc909b6a40fb64cc389e0ec32cc86471540e7a643ffe0519 + languageName: node + linkType: hard + +"leven@npm:^3.1.0": + version: 3.1.0 + resolution: "leven@npm:3.1.0" + checksum: cd778ba3fbab0f4d0500b7e87d1f6e1f041507c56fdcd47e8256a3012c98aaee371d4c15e0a76e0386107af2d42e2b7466160a2d80688aaa03e66e49949f42df + languageName: node + linkType: hard + +"lie@npm:~3.3.0": + version: 3.3.0 + resolution: "lie@npm:3.3.0" + dependencies: + immediate: "npm:~3.0.5" + checksum: 56dd113091978f82f9dc5081769c6f3b947852ecf9feccaf83e14a123bc630c2301439ce6182521e5fbafbde88e88ac38314327a4e0493a1bea7e0699a7af808 + languageName: node + linkType: hard + +"liftoff@npm:^3.1.0": + version: 3.1.0 + resolution: "liftoff@npm:3.1.0" + dependencies: + extend: "npm:^3.0.0" + findup-sync: "npm:^3.0.0" + fined: "npm:^1.0.1" + flagged-respawn: "npm:^1.0.0" + is-plain-object: "npm:^2.0.4" + object.map: "npm:^1.0.0" + rechoir: "npm:^0.6.2" + resolve: "npm:^1.1.7" + checksum: f4cf3f09a3c368e8ee349dc11cfaae39898d1063e803a05ebacf34949eab20dd9448c682684f5933fdef0b3a61b4e56ae2dbd089689cbb892690d7e368330c20 + languageName: node + linkType: hard + +"lilconfig@npm:^3.0.0": + version: 3.1.1 + resolution: "lilconfig@npm:3.1.1" + checksum: 311b559794546894e3fe176663427326026c1c644145be9e8041c58e268aa9328799b8dfe7e4dd8c6a4ae305feae95a1c9e007db3569f35b42b6e1bc8274754c + languageName: node + linkType: hard + +"lines-and-columns@npm:^1.1.6": + version: 1.2.4 + resolution: "lines-and-columns@npm:1.2.4" + checksum: 3da6ee62d4cd9f03f5dc90b4df2540fb85b352081bee77fe4bbcd12c9000ead7f35e0a38b8d09a9bb99b13223446dd8689ff3c4959807620726d788701a83d2d + languageName: node + linkType: hard + +"load-json-file@npm:^1.0.0": + version: 1.1.0 + resolution: "load-json-file@npm:1.1.0" + dependencies: + graceful-fs: "npm:^4.1.2" + parse-json: "npm:^2.2.0" + pify: "npm:^2.0.0" + pinkie-promise: "npm:^2.0.0" + strip-bom: "npm:^2.0.0" + checksum: 2a5344c2d88643735a938fdca8582c0504e1c290577faa74f56b9cc187fa443832709a15f36e5771f779ec0878215a03abc8faf97ec57bb86092ceb7e0caef22 + languageName: node + linkType: hard + +"load-json-file@npm:^4.0.0": + version: 4.0.0 + resolution: "load-json-file@npm:4.0.0" + dependencies: + graceful-fs: "npm:^4.1.2" + parse-json: "npm:^4.0.0" + pify: "npm:^3.0.0" + strip-bom: "npm:^3.0.0" + checksum: 6b48f6a0256bdfcc8970be2c57f68f10acb2ee7e63709b386b2febb6ad3c86198f840889cdbe71d28f741cbaa2f23a7771206b138cd1bdd159564511ca37c1d5 + languageName: node + linkType: hard + +"loader-utils@npm:^3.2.0": + version: 3.2.1 + resolution: "loader-utils@npm:3.2.1" + checksum: d3e1f217d160e8e894a0385a33500d4ce14065e8ffb250f5a81ae65bc2c3baa50625ec34182ba4417b46b4ac6725aed64429e1104d6401e074af2aa1dd018394 + languageName: node + linkType: hard + +"local-pkg@npm:^0.5.0": + version: 0.5.0 + resolution: "local-pkg@npm:0.5.0" + dependencies: + mlly: "npm:^1.4.2" + pkg-types: "npm:^1.0.3" + checksum: f61cbd00d7689f275558b1a45c7ff2a3ddf8472654123ed880215677b9adfa729f1081e50c27ffb415cdb9fa706fb755fec5e23cdd965be375c8059e87ff1cc9 + languageName: node + linkType: hard + +"locate-path@npm:^3.0.0": + version: 3.0.0 + resolution: "locate-path@npm:3.0.0" + dependencies: + p-locate: "npm:^3.0.0" + path-exists: "npm:^3.0.0" + checksum: 3db394b7829a7fe2f4fbdd25d3c4689b85f003c318c5da4052c7e56eed697da8f1bce5294f685c69ff76e32cba7a33629d94396976f6d05fb7f4c755c5e2ae8b + languageName: node + linkType: hard + +"locate-path@npm:^5.0.0": + version: 5.0.0 + resolution: "locate-path@npm:5.0.0" + dependencies: + p-locate: "npm:^4.1.0" + checksum: 33a1c5247e87e022f9713e6213a744557a3e9ec32c5d0b5efb10aa3a38177615bf90221a5592674857039c1a0fd2063b82f285702d37b792d973e9e72ace6c59 + languageName: node + linkType: hard + +"locate-path@npm:^6.0.0": + version: 6.0.0 + resolution: "locate-path@npm:6.0.0" + dependencies: + p-locate: "npm:^5.0.0" + checksum: d3972ab70dfe58ce620e64265f90162d247e87159b6126b01314dd67be43d50e96a50b517bce2d9452a79409c7614054c277b5232377de50416564a77ac7aad3 + languageName: node + linkType: hard + +"lodash.camelcase@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.camelcase@npm:4.3.0" + checksum: fcba15d21a458076dd309fce6b1b4bf611d84a0ec252cb92447c948c533ac250b95d2e00955801ebc367e5af5ed288b996d75d37d2035260a937008e14eaf432 + languageName: node + linkType: hard + +"lodash.clonedeep@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.clonedeep@npm:4.5.0" + checksum: 2caf0e4808f319d761d2939ee0642fa6867a4bbf2cfce43276698828380756b99d4c4fa226d881655e6ac298dd453fe12a5ec8ba49861777759494c534936985 + languageName: node + linkType: hard + +"lodash.debounce@npm:^4.0.8": + version: 4.0.8 + resolution: "lodash.debounce@npm:4.0.8" + checksum: 762998a63e095412b6099b8290903e0a8ddcb353ac6e2e0f2d7e7d03abd4275fe3c689d88960eb90b0dde4f177554d51a690f22a343932ecbc50a5d111849987 + languageName: node + linkType: hard + +"lodash.escape@npm:^4.0.1": + version: 4.0.1 + resolution: "lodash.escape@npm:4.0.1" + checksum: 90ade409cec05b6869090476952fdfb84d4d87b1ff4a0e03ebd590f980d9a1248d93ba14579f10d80c6429e4d6af13ba137c28db64cae6dadb71442e54a3ad2b + languageName: node + linkType: hard + +"lodash.merge@npm:^4.6.2": + version: 4.6.2 + resolution: "lodash.merge@npm:4.6.2" + checksum: 402fa16a1edd7538de5b5903a90228aa48eb5533986ba7fa26606a49db2572bf414ff73a2c9f5d5fd36b31c46a5d5c7e1527749c07cbcf965ccff5fbdf32c506 + languageName: node + linkType: hard + +"lodash.trim@npm:^4.5.1": + version: 4.5.1 + resolution: "lodash.trim@npm:4.5.1" + checksum: 5e81316d8fb02ff63c92d73cc737cc264ea49114a0f42e083ceb7c39679be30cd3497c0c1f6a3b79c36e322fa0b52dd619fc26a9fb693cc5201988d967d5292c + languageName: node + linkType: hard + +"lodash.trimstart@npm:^4.5.1": + version: 4.5.1 + resolution: "lodash.trimstart@npm:4.5.1" + checksum: de0d4b1da63fb98cdae1e9ce02e845e5791b0cae78285f0dc6a8029878e7663e586bf6660ab49c5c12302432583f5f7ec2effe735645b208cbf758eba5f9f0d8 + languageName: node + linkType: hard + +"lodash@npm:^4.17.21": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c + languageName: node + linkType: hard + +"log-symbols@npm:^4.1.0": + version: 4.1.0 + resolution: "log-symbols@npm:4.1.0" + dependencies: + chalk: "npm:^4.1.0" + is-unicode-supported: "npm:^0.1.0" + checksum: 67f445a9ffa76db1989d0fa98586e5bc2fd5247260dafb8ad93d9f0ccd5896d53fb830b0e54dade5ad838b9de2006c826831a3c528913093af20dff8bd24aca6 + languageName: node + linkType: hard + +"logform@npm:^2.3.2, logform@npm:^2.4.0": + version: 2.6.0 + resolution: "logform@npm:2.6.0" + dependencies: + "@colors/colors": "npm:1.6.0" + "@types/triple-beam": "npm:^1.3.2" + fecha: "npm:^4.2.0" + ms: "npm:^2.1.1" + safe-stable-stringify: "npm:^2.3.1" + triple-beam: "npm:^1.3.0" + checksum: 6e02f8617a03155b2fce451bacf777a2c01da16d32c4c745b3ec85be6c3f2602f2a4953a8bd096441cb4c42c447b52318541d6b6bc335dce903cb9ad77a1749f + languageName: node + linkType: hard + +"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": + version: 1.4.0 + resolution: "loose-envify@npm:1.4.0" + dependencies: + js-tokens: "npm:^3.0.0 || ^4.0.0" + bin: + loose-envify: cli.js + checksum: 655d110220983c1a4b9c0c679a2e8016d4b67f6e9c7b5435ff5979ecdb20d0813f4dec0a08674fcbdd4846a3f07edbb50a36811fd37930b94aaa0d9daceb017e + languageName: node + linkType: hard + +"loupe@npm:^2.3.6, loupe@npm:^2.3.7": + version: 2.3.7 + resolution: "loupe@npm:2.3.7" + dependencies: + get-func-name: "npm:^2.0.1" + checksum: 71a781c8fc21527b99ed1062043f1f2bb30bdaf54fa4cf92463427e1718bc6567af2988300bc243c1f276e4f0876f29e3cbf7b58106fdc186915687456ce5bf4 + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.1.0 + resolution: "lru-cache@npm:10.1.0" + checksum: 778bc8b2626daccd75f24c4b4d10632496e21ba064b126f526c626fbdbc5b28c472013fccd45d7646b9e1ef052444824854aed617b59cd570d01a8b7d651fc1e + languageName: node + linkType: hard + +"lru-cache@npm:^5.1.1": + version: 5.1.1 + resolution: "lru-cache@npm:5.1.1" + dependencies: + yallist: "npm:^3.0.2" + checksum: 89b2ef2ef45f543011e38737b8a8622a2f8998cddf0e5437174ef8f1f70a8b9d14a918ab3e232cb3ba343b7abddffa667f0b59075b2b80e6b4d63c3de6127482 + languageName: node + linkType: hard + +"lru-cache@npm:^6.0.0": + version: 6.0.0 + resolution: "lru-cache@npm:6.0.0" + dependencies: + yallist: "npm:^4.0.0" + checksum: cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 + languageName: node + linkType: hard + +"lru-queue@npm:^0.1.0": + version: 0.1.0 + resolution: "lru-queue@npm:0.1.0" + dependencies: + es5-ext: "npm:~0.10.2" + checksum: 83517032b46843601c4528be65e8aaf85f5a7860a9cfa3e4f2b5591da436e7cd748d95b450c91434c4ffb75d3ae4c069ddbdd9f71ada56a99a00c03088c51b4d + languageName: node + linkType: hard + +"luxon@npm:^3.4.4": + version: 3.4.4 + resolution: "luxon@npm:3.4.4" + checksum: 02e26a0b039c11fd5b75e1d734c8f0332c95510f6a514a9a0991023e43fb233884da02d7f966823ffb230632a733fc86d4a4b1e63c3fbe00058b8ee0f8c728af + languageName: node + linkType: hard + +"lz-string@npm:^1.5.0": + version: 1.5.0 + resolution: "lz-string@npm:1.5.0" + bin: + lz-string: bin/bin.js + checksum: 36128e4de34791838abe979b19927c26e67201ca5acf00880377af7d765b38d1c60847e01c5ec61b1a260c48029084ab3893a3925fd6e48a04011364b089991b + languageName: node + linkType: hard + +"magic-string@npm:^0.27.0": + version: 0.27.0 + resolution: "magic-string@npm:0.27.0" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.4.13" + checksum: cddacfea14441ca57ae8a307bc3cf90bac69efaa4138dd9a80804cffc2759bf06f32da3a293fb13eaa96334b7d45b7768a34f1d226afae25d2f05b05a3bb37d8 + languageName: node + linkType: hard + +"magic-string@npm:^0.30.0, magic-string@npm:^0.30.5": + version: 0.30.5 + resolution: "magic-string@npm:0.30.5" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.4.15" + checksum: 38ac220ca7539e96da7ea2f38d85796bdf5c69b6bcae728c4bc2565084e6dc326b9174ee9770bea345cf6c9b3a24041b767167874fab5beca874d2356a9d1520 + languageName: node + linkType: hard + +"make-dir@npm:^2.0.0, make-dir@npm:^2.1.0": + version: 2.1.0 + resolution: "make-dir@npm:2.1.0" + dependencies: + pify: "npm:^4.0.1" + semver: "npm:^5.6.0" + checksum: ada869944d866229819735bee5548944caef560d7a8536ecbc6536edca28c72add47cc4f6fc39c54fb25d06b58da1f8994cf7d9df7dadea047064749efc085d8 + languageName: node + linkType: hard + +"make-dir@npm:^3.0.2": + version: 3.1.0 + resolution: "make-dir@npm:3.1.0" + dependencies: + semver: "npm:^6.0.0" + checksum: 56aaafefc49c2dfef02c5c95f9b196c4eb6988040cf2c712185c7fe5c99b4091591a7fc4d4eafaaefa70ff763a26f6ab8c3ff60b9e75ea19876f49b18667ecaa + languageName: node + linkType: hard + +"make-fetch-happen@npm:^13.0.0": + version: 13.0.0 + resolution: "make-fetch-happen@npm:13.0.0" + dependencies: + "@npmcli/agent": "npm:^2.0.0" + cacache: "npm:^18.0.0" + http-cache-semantics: "npm:^4.1.1" + is-lambda: "npm:^1.0.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^3.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.3" + promise-retry: "npm:^2.0.1" + ssri: "npm:^10.0.0" + checksum: 43b9f6dcbc6fe8b8604cb6396957c3698857a15ba4dbc38284f7f0e61f248300585ef1eb8cc62df54e9c724af977e45b5cdfd88320ef7f53e45070ed3488da55 + languageName: node + linkType: hard + +"make-iterator@npm:^1.0.0": + version: 1.0.1 + resolution: "make-iterator@npm:1.0.1" + dependencies: + kind-of: "npm:^6.0.2" + checksum: 84b77d72e4af589a4e6069a9e0265ff55e63162b528aa085149060b7bf4e858c700892b95a073feaf517988cac75ca2e8d9ceb14243718b2f268dc4f4a90ff0a + languageName: node + linkType: hard + +"makeerror@npm:1.0.12": + version: 1.0.12 + resolution: "makeerror@npm:1.0.12" + dependencies: + tmpl: "npm:1.0.5" + checksum: b0e6e599780ce6bab49cc413eba822f7d1f0dfebd1c103eaa3785c59e43e22c59018323cf9e1708f0ef5329e94a745d163fcbb6bff8e4c6742f9be9e86f3500c + languageName: node + linkType: hard + +"map-cache@npm:^0.2.0, map-cache@npm:^0.2.2": + version: 0.2.2 + resolution: "map-cache@npm:0.2.2" + checksum: 05e3eb005c1b80b9f949ca007687640e8c5d0fc88dc45c3c3ab4902a3bec79d66a58f3e3b04d6985d90cd267c629c7b46c977e9c34433e8c11ecfcbb9f0fa290 + languageName: node + linkType: hard + +"map-or-similar@npm:^1.5.0": + version: 1.5.0 + resolution: "map-or-similar@npm:1.5.0" + checksum: 33c6ccfdc272992e33e4e99a69541a3e7faed9de3ac5bc732feb2500a9ee71d3f9d098980a70b7746e7eeb7f859ff7dfb8aa9b5ecc4e34170a32ab78cfb18def + languageName: node + linkType: hard + +"map-stream@npm:0.0.7": + version: 0.0.7 + resolution: "map-stream@npm:0.0.7" + checksum: 77da244656dad5013bd147b0eef6f0343a212f14761332b97364fe348d4d70f0b8a0903457d6fc88772ec7c3d4d048b24f8db3aa5c0f77a8ce8bf2391473b8ec + languageName: node + linkType: hard + +"map-visit@npm:^1.0.0": + version: 1.0.0 + resolution: "map-visit@npm:1.0.0" + dependencies: + object-visit: "npm:^1.0.0" + checksum: fb3475e5311939a6147e339999113db607adc11c7c3cd3103e5e9dbf502898416ecba6b1c7c649c6d4d12941de00cee58b939756bdf20a9efe7d4fa5a5738b73 + languageName: node + linkType: hard + +"markdown-to-jsx@npm:^7.1.8": + version: 7.3.2 + resolution: "markdown-to-jsx@npm:7.3.2" + peerDependencies: + react: ">= 0.14.0" + checksum: 191b9a9defeed02e12dd340cebf279f577266dac7b34574fa44ce4d64ee8536f9967d455b8303c853f84413feb473118290a6160d8221eeaf3b9e4961b8980e3 + languageName: node + linkType: hard + +"marked@npm:^12.0.0": + version: 12.0.0 + resolution: "marked@npm:12.0.0" + bin: + marked: bin/marked.js + checksum: 485c0d2a1b59f7d305435d2d65aac477eee8e47ccd686e06c35145b7186c399fd741543f7c0bb02e67d53b3cc0341f491d967ca40a5c3aa49c6cc466e1f5d872 + languageName: node + linkType: hard + +"matchdep@npm:^2.0.0": + version: 2.0.0 + resolution: "matchdep@npm:2.0.0" + dependencies: + findup-sync: "npm:^2.0.0" + micromatch: "npm:^3.0.4" + resolve: "npm:^1.4.0" + stack-trace: "npm:0.0.10" + checksum: 0a44d235d1edc84fe37cf8b07f55bb6b9f10480bb754f21692421b04e020a9b5f8f9f2e138119e4eac219027328daa4d9cae7dc4c38e08f23cf29cc5dfb8a727 + languageName: node + linkType: hard + +"md5.js@npm:^1.3.4": + version: 1.3.5 + resolution: "md5.js@npm:1.3.5" + dependencies: + hash-base: "npm:^3.0.0" + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.1.2" + checksum: b7bd75077f419c8e013fc4d4dada48be71882e37d69a44af65a2f2804b91e253441eb43a0614423a1c91bb830b8140b0dc906bc797245e2e275759584f4efcc5 + languageName: node + linkType: hard + +"mdast-util-definitions@npm:^4.0.0": + version: 4.0.0 + resolution: "mdast-util-definitions@npm:4.0.0" + dependencies: + unist-util-visit: "npm:^2.0.0" + checksum: d81bb0b702f99878c8e8e4f66dd7f6f673ab341f061b3d9487ba47dad28b584e02f16b4c42df23714eaac8a7dd8544ba7d77308fad8d4a9fd0ac92e2a7f56be9 + languageName: node + linkType: hard + +"mdast-util-to-string@npm:^1.0.0": + version: 1.1.0 + resolution: "mdast-util-to-string@npm:1.1.0" + checksum: 5dad9746ec0839792a8a35f504564e8d2b8c30013652410306c111963d33f1ee7b5477aa64ed77b64e13216363a29395809875ffd80e2031a08614657628a121 + languageName: node + linkType: hard + +"mdn-data@npm:2.0.14": + version: 2.0.14 + resolution: "mdn-data@npm:2.0.14" + checksum: 67241f8708c1e665a061d2b042d2d243366e93e5bf1f917693007f6d55111588b952dcbfd3ea9c2d0969fb754aad81b30fdcfdcc24546495fc3b24336b28d4bd + languageName: node + linkType: hard + +"media-typer@npm:0.3.0": + version: 0.3.0 + resolution: "media-typer@npm:0.3.0" + checksum: d160f31246907e79fed398470285f21bafb45a62869dc469b1c8877f3f064f5eabc4bcc122f9479b8b605bc5c76187d7871cf84c4ee3ecd3e487da1993279928 + languageName: node + linkType: hard + +"memoizee@npm:0.4.X": + version: 0.4.15 + resolution: "memoizee@npm:0.4.15" + dependencies: + d: "npm:^1.0.1" + es5-ext: "npm:^0.10.53" + es6-weak-map: "npm:^2.0.3" + event-emitter: "npm:^0.3.5" + is-promise: "npm:^2.2.2" + lru-queue: "npm:^0.1.0" + next-tick: "npm:^1.1.0" + timers-ext: "npm:^0.1.7" + checksum: 297e65cd8256bdf24c48f5e158da80d4c9688db0d6e65c5dcc13fa768e782ddeb71aec36925359931b5efef0efc6666b5bb2af6deb3de63d4258a3821ed16fce + languageName: node + linkType: hard + +"memoizerific@npm:^1.11.3": + version: 1.11.3 + resolution: "memoizerific@npm:1.11.3" + dependencies: + map-or-similar: "npm:^1.5.0" + checksum: 661bf69b7afbfad57f0208f0c63324f4c96087b480708115b78ee3f0237d86c7f91347f6db31528740b2776c2e34c709bcb034e1e910edee2270c9603a0a469e + languageName: node + linkType: hard + +"memorystream@npm:^0.3.1": + version: 0.3.1 + resolution: "memorystream@npm:0.3.1" + checksum: 4bd164657711d9747ff5edb0508b2944414da3464b7fe21ac5c67cf35bba975c4b446a0124bd0f9a8be54cfc18faf92e92bd77563a20328b1ccf2ff04e9f39b9 + languageName: node + linkType: hard + +"merge-descriptors@npm:1.0.1": + version: 1.0.1 + resolution: "merge-descriptors@npm:1.0.1" + checksum: b67d07bd44cfc45cebdec349bb6e1f7b077ee2fd5beb15d1f7af073849208cb6f144fe403e29a36571baf3f4e86469ac39acf13c318381e958e186b2766f54ec + languageName: node + linkType: hard + +"merge-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-stream@npm:2.0.0" + checksum: 867fdbb30a6d58b011449b8885601ec1690c3e41c759ecd5a9d609094f7aed0096c37823ff4a7190ef0b8f22cc86beb7049196ff68c016e3b3c671d0dac91ce5 + languageName: node + linkType: hard + +"merge2@npm:^1.3.0, merge2@npm:^1.4.1": + version: 1.4.1 + resolution: "merge2@npm:1.4.1" + checksum: 254a8a4605b58f450308fc474c82ac9a094848081bf4c06778200207820e5193726dc563a0d2c16468810516a5c97d9d3ea0ca6585d23c58ccfff2403e8dbbeb + languageName: node + linkType: hard + +"methods@npm:~1.1.2": + version: 1.1.2 + resolution: "methods@npm:1.1.2" + checksum: bdf7cc72ff0a33e3eede03708c08983c4d7a173f91348b4b1e4f47d4cdbf734433ad971e7d1e8c77247d9e5cd8adb81ea4c67b0a2db526b758b2233d7814b8b2 + languageName: node + linkType: hard + +"micromatch@npm:^3.0.4, micromatch@npm:^3.1.10, micromatch@npm:^3.1.4": + version: 3.1.10 + resolution: "micromatch@npm:3.1.10" + dependencies: + arr-diff: "npm:^4.0.0" + array-unique: "npm:^0.3.2" + braces: "npm:^2.3.1" + define-property: "npm:^2.0.2" + extend-shallow: "npm:^3.0.2" + extglob: "npm:^2.0.4" + fragment-cache: "npm:^0.2.1" + kind-of: "npm:^6.0.2" + nanomatch: "npm:^1.2.9" + object.pick: "npm:^1.3.0" + regex-not: "npm:^1.0.0" + snapdragon: "npm:^0.8.1" + to-regex: "npm:^3.0.2" + checksum: 531a32e7ac92bef60657820202be71b63d0f945c08a69cc4c239c0b19372b751483d464a850a2e3a5ff6cc9060641e43d44c303af104c1a27493d137d8af017f + languageName: node + linkType: hard + +"micromatch@npm:^4.0.4": + version: 4.0.5 + resolution: "micromatch@npm:4.0.5" + dependencies: + braces: "npm:^3.0.2" + picomatch: "npm:^2.3.1" + checksum: 3d6505b20f9fa804af5d8c596cb1c5e475b9b0cd05f652c5b56141cf941bd72adaeb7a436fda344235cef93a7f29b7472efc779fcdb83b478eab0867b95cdeff + languageName: node + linkType: hard + +"miller-rabin@npm:^4.0.0": + version: 4.0.1 + resolution: "miller-rabin@npm:4.0.1" + dependencies: + bn.js: "npm:^4.0.0" + brorand: "npm:^1.0.1" + bin: + miller-rabin: bin/miller-rabin + checksum: 26b2b96f6e49dbcff7faebb78708ed2f5f9ae27ac8cbbf1d7c08f83cf39bed3d418c0c11034dce997da70d135cc0ff6f3a4c15dc452f8e114c11986388a64346 + languageName: node + linkType: hard + +"mime-db@npm:1.52.0, mime-db@npm:>= 1.43.0 < 2": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa + languageName: node + linkType: hard + +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.25, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: "npm:1.52.0" + checksum: 82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2 + languageName: node + linkType: hard + +"mime@npm:1.6.0": + version: 1.6.0 + resolution: "mime@npm:1.6.0" + bin: + mime: cli.js + checksum: b92cd0adc44888c7135a185bfd0dddc42c32606401c72896a842ae15da71eb88858f17669af41e498b463cd7eb998f7b48939a25b08374c7924a9c8a6f8a81b0 + languageName: node + linkType: hard + +"mime@npm:^2.0.3": + version: 2.6.0 + resolution: "mime@npm:2.6.0" + bin: + mime: cli.js + checksum: a7f2589900d9c16e3bdf7672d16a6274df903da958c1643c9c45771f0478f3846dcb1097f31eb9178452570271361e2149310931ec705c037210fc69639c8e6c + languageName: node + linkType: hard + +"mimic-fn@npm:^2.1.0": + version: 2.1.0 + resolution: "mimic-fn@npm:2.1.0" + checksum: b26f5479d7ec6cc2bce275a08f146cf78f5e7b661b18114e2506dd91ec7ec47e7a25bf4360e5438094db0560bcc868079fb3b1fb3892b833c1ecbf63f80c95a4 + languageName: node + linkType: hard + +"mimic-fn@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-fn@npm:4.0.0" + checksum: de9cc32be9996fd941e512248338e43407f63f6d497abe8441fa33447d922e927de54d4cc3c1a3c6d652857acd770389d5a3823f311a744132760ce2be15ccbf + languageName: node + linkType: hard + +"min-indent@npm:^1.0.1": + version: 1.0.1 + resolution: "min-indent@npm:1.0.1" + checksum: 7e207bd5c20401b292de291f02913230cb1163abca162044f7db1d951fa245b174dc00869d40dd9a9f32a885ad6a5f3e767ee104cf278f399cb4e92d3f582d5c + languageName: node + linkType: hard + +"minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": + version: 1.0.1 + resolution: "minimalistic-assert@npm:1.0.1" + checksum: 96730e5601cd31457f81a296f521eb56036e6f69133c0b18c13fe941109d53ad23a4204d946a0d638d7f3099482a0cec8c9bb6d642604612ce43ee536be3dddd + languageName: node + linkType: hard + +"minimalistic-crypto-utils@npm:^1.0.1": + version: 1.0.1 + resolution: "minimalistic-crypto-utils@npm:1.0.1" + checksum: 790ecec8c5c73973a4fbf2c663d911033e8494d5fb0960a4500634766ab05d6107d20af896ca2132e7031741f19888154d44b2408ada0852446705441383e9f8 + languageName: node + linkType: hard + +"minimatch@npm:9.0.1": + version: 9.0.1 + resolution: "minimatch@npm:9.0.1" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: aa043eb8822210b39888a5d0d28df0017b365af5add9bd522f180d2a6962de1cbbf1bdeacdb1b17f410dc3336bc8d76fb1d3e814cdc65d00c2f68e01f0010096 + languageName: node + linkType: hard + +"minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 + languageName: node + linkType: hard + +"minimatch@npm:^5.0.1": + version: 5.1.6 + resolution: "minimatch@npm:5.1.6" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 3defdfd230914f22a8da203747c42ee3c405c39d4d37ffda284dac5e45b7e1f6c49aa8be606509002898e73091ff2a3bbfc59c2c6c71d4660609f63aa92f98e3 + languageName: node + linkType: hard + +"minimatch@npm:^9.0.1": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 85f407dcd38ac3e180f425e86553911d101455ca3ad5544d6a7cec16286657e4f8a9aa6695803025c55e31e35a91a2252b5dc8e7d527211278b8b65b4dbd5eac + languageName: node + linkType: hard + +"minimist@npm:^1.2.5, minimist@npm:^1.2.6": + version: 1.2.8 + resolution: "minimist@npm:1.2.8" + checksum: 19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.4 + resolution: "minipass-fetch@npm:3.0.4" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.1.2" + dependenciesMeta: + encoding: + optional: true + checksum: 1b63c1f3313e88eeac4689f1b71c9f086598db9a189400e3ee960c32ed89e06737fa23976c9305c2d57464fb3fcdc12749d3378805c9d6176f5569b0d0ee8a75 + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3": + version: 7.0.4 + resolution: "minipass@npm:7.0.4" + checksum: 6c7370a6dfd257bf18222da581ba89a5eaedca10e158781232a8b5542a90547540b4b9b7e7f490e4cda43acfbd12e086f0453728ecf8c19e0ef6921bc5958ac5 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: 64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 + languageName: node + linkType: hard + +"mixin-deep@npm:^1.2.0": + version: 1.3.2 + resolution: "mixin-deep@npm:1.3.2" + dependencies: + for-in: "npm:^1.0.2" + is-extendable: "npm:^1.0.1" + checksum: cb39ffb73c377222391af788b4c83d1a6cecb2d9fceb7015384f8deb46e151a9b030c21ef59a79cb524d4557e3f74c7248ab948a62a6e7e296b42644863d183b + languageName: node + linkType: hard + +"mkdirp-classic@npm:^0.5.2": + version: 0.5.3 + resolution: "mkdirp-classic@npm:0.5.3" + checksum: 95371d831d196960ddc3833cc6907e6b8f67ac5501a6582f47dfae5eb0f092e9f8ce88e0d83afcae95d6e2b61a01741ba03714eeafb6f7a6e9dcc158ac85b168 + languageName: node + linkType: hard + +"mkdirp@npm:^0.5.4": + version: 0.5.6 + resolution: "mkdirp@npm:0.5.6" + dependencies: + minimist: "npm:^1.2.6" + bin: + mkdirp: bin/cmd.js + checksum: e2e2be789218807b58abced04e7b49851d9e46e88a2f9539242cc8a92c9b5c3a0b9bab360bd3014e02a140fc4fbc58e31176c408b493f8a2a6f4986bd7527b01 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: 46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf + languageName: node + linkType: hard + +"mkdirp@npm:^3.0.1": + version: 3.0.1 + resolution: "mkdirp@npm:3.0.1" + bin: + mkdirp: dist/cjs/src/bin.js + checksum: 9f2b975e9246351f5e3a40dcfac99fcd0baa31fbfab615fe059fb11e51f10e4803c63de1f384c54d656e4db31d000e4767e9ef076a22e12a641357602e31d57d + languageName: node + linkType: hard + +"mlly@npm:^1.2.0, mlly@npm:^1.4.2": + version: 1.4.2 + resolution: "mlly@npm:1.4.2" + dependencies: + acorn: "npm:^8.10.0" + pathe: "npm:^1.1.1" + pkg-types: "npm:^1.0.3" + ufo: "npm:^1.3.0" + checksum: 905e3a704c7d3bcaad55f31d6efe9f680eab5be053ab7f8b299b8dbc027041f741fa6a93db9a3c461be2552632f3831b6c43c50af530f5fb2e9cd6273bc9d642 + languageName: node + linkType: hard + +"mousetrap@npm:^1.6.5": + version: 1.6.5 + resolution: "mousetrap@npm:1.6.5" + checksum: 5c361bdbbff3966fd58d70f39b9fe1f8e32c78f3ce65989d83af7aad32a3a95313ce835a8dd8a55cb5de9eeb7c1f0c2b9048631a3073b5606241589e8fc0ba53 + languageName: node + linkType: hard + +"mri@npm:^1.2.0": + version: 1.2.0 + resolution: "mri@npm:1.2.0" + checksum: a3d32379c2554cf7351db6237ddc18dc9e54e4214953f3da105b97dc3babe0deb3ffe99cf409b38ea47cc29f9430561ba6b53b24ab8f9ce97a4b50409e4a50e7 + languageName: node + linkType: hard + +"ms@npm:2.0.0": + version: 2.0.0 + resolution: "ms@npm:2.0.0" + checksum: f8fda810b39fd7255bbdc451c46286e549794fcc700dc9cd1d25658bbc4dc2563a5de6fe7c60f798a16a60c6ceb53f033cb353f493f0cf63e5199b702943159d + languageName: node + linkType: hard + +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc + languageName: node + linkType: hard + +"ms@npm:2.1.3, ms@npm:^2.1.1": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 + languageName: node + linkType: hard + +"mustache@npm:^4.0.1, mustache@npm:^4.2.0": + version: 4.2.0 + resolution: "mustache@npm:4.2.0" + bin: + mustache: bin/mustache + checksum: 1f8197e8a19e63645a786581d58c41df7853da26702dbc005193e2437c98ca49b255345c173d50c08fe4b4dbb363e53cb655ecc570791f8deb09887248dd34a2 + languageName: node + linkType: hard + +"mute-stdout@npm:^1.0.0": + version: 1.0.1 + resolution: "mute-stdout@npm:1.0.1" + checksum: 5b6a20ee77cbe9e61fa52cfb1f2ddf1c21d49a0c874a2f6a24bbff962031084a0694a0258e92b33c0f492229416031c1a53d4dcffc902b981daff2379fd40903 + languageName: node + linkType: hard + +"nan@npm:^2.12.1": + version: 2.18.0 + resolution: "nan@npm:2.18.0" + dependencies: + node-gyp: "npm:latest" + checksum: 9209d80134fdb98c0afe35c1372d2b930a0a8d3c52706cb5e4257a27e9845c375f7a8daedadadec8d6403ca2eebb3b37d362ff5d1ec03249462abf65fef2a148 + languageName: node + linkType: hard + +"nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" + bin: + nanoid: bin/nanoid.cjs + checksum: e3fb661aa083454f40500473bb69eedb85dc160e763150b9a2c567c7e9ff560ce028a9f833123b618a6ea742e311138b591910e795614a629029e86e180660f3 + languageName: node + linkType: hard + +"nanomatch@npm:^1.2.9": + version: 1.2.13 + resolution: "nanomatch@npm:1.2.13" + dependencies: + arr-diff: "npm:^4.0.0" + array-unique: "npm:^0.3.2" + define-property: "npm:^2.0.2" + extend-shallow: "npm:^3.0.2" + fragment-cache: "npm:^0.2.1" + is-windows: "npm:^1.0.2" + kind-of: "npm:^6.0.2" + object.pick: "npm:^1.3.0" + regex-not: "npm:^1.0.0" + snapdragon: "npm:^0.8.1" + to-regex: "npm:^3.0.1" + checksum: 0f5cefa755ca2e20c86332821995effb24acb79551ddaf51c1b9112628cad234a0d8fd9ac6aa56ad1f8bfad6ff6ae86e851acb960943249d9fa44b091479953a + languageName: node + linkType: hard + +"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: 3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 + languageName: node + linkType: hard + +"neo-async@npm:^2.5.0, neo-async@npm:^2.6.2": + version: 2.6.2 + resolution: "neo-async@npm:2.6.2" + checksum: c2f5a604a54a8ec5438a342e1f356dff4bc33ccccdb6dc668d94fe8e5eccfc9d2c2eea6064b0967a767ba63b33763f51ccf2cd2441b461a7322656c1f06b3f5d + languageName: node + linkType: hard + +"next-tick@npm:1, next-tick@npm:^1.1.0": + version: 1.1.0 + resolution: "next-tick@npm:1.1.0" + checksum: 3ba80dd805fcb336b4f52e010992f3e6175869c8d88bf4ff0a81d5d66e6049f89993463b28211613e58a6b7fe93ff5ccbba0da18d4fa574b96289e8f0b577f28 + languageName: node + linkType: hard + +"nice-try@npm:^1.0.4": + version: 1.0.5 + resolution: "nice-try@npm:1.0.5" + checksum: 95568c1b73e1d0d4069a3e3061a2102d854513d37bcfda73300015b7ba4868d3b27c198d1dbbd8ebdef4112fc2ed9e895d4a0f2e1cce0bd334f2a1346dc9205f + languageName: node + linkType: hard + +"node-dir@npm:^0.1.17": + version: 0.1.17 + resolution: "node-dir@npm:0.1.17" + dependencies: + minimatch: "npm:^3.0.2" + checksum: 16222e871708c405079ff8122d4a7e1d522c5b90fc8f12b3112140af871cfc70128c376e845dcd0044c625db0d2efebd2d852414599d240564db61d53402b4c1 + languageName: node + linkType: hard + +"node-fetch-native@npm:^1.4.0": + version: 1.4.1 + resolution: "node-fetch-native@npm:1.4.1" + checksum: ab298a42ebf3b1b6c6a8cbc53d8ba703895f55171ed743b0828c2a87d461642d8053143864915a69d41cc01013db86406da105fff6c0a05a00d8caf5c279549c + languageName: node + linkType: hard + +"node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.12": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 10.0.1 + resolution: "node-gyp@npm:10.0.1" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^13.0.0" + nopt: "npm:^7.0.0" + proc-log: "npm:^3.0.0" + semver: "npm:^7.3.5" + tar: "npm:^6.1.2" + which: "npm:^4.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: abddfff7d873312e4ed4a5fb75ce893a5c4fb69e7fcb1dfa71c28a6b92a7f1ef6b62790dffb39181b5a82728ba8f2f32d229cf8cbe66769fe02cea7db4a555aa + languageName: node + linkType: hard + +"node-int64@npm:^0.4.0": + version: 0.4.0 + resolution: "node-int64@npm:0.4.0" + checksum: a6a4d8369e2f2720e9c645255ffde909c0fbd41c92ea92a5607fc17055955daac99c1ff589d421eee12a0d24e99f7bfc2aabfeb1a4c14742f6c099a51863f31a + languageName: node + linkType: hard + +"node-libs-browser@npm:^2.2.1": + version: 2.2.1 + resolution: "node-libs-browser@npm:2.2.1" + dependencies: + assert: "npm:^1.1.1" + browserify-zlib: "npm:^0.2.0" + buffer: "npm:^4.3.0" + console-browserify: "npm:^1.1.0" + constants-browserify: "npm:^1.0.0" + crypto-browserify: "npm:^3.11.0" + domain-browser: "npm:^1.1.1" + events: "npm:^3.0.0" + https-browserify: "npm:^1.0.0" + os-browserify: "npm:^0.3.0" + path-browserify: "npm:0.0.1" + process: "npm:^0.11.10" + punycode: "npm:^1.2.4" + querystring-es3: "npm:^0.2.0" + readable-stream: "npm:^2.3.3" + stream-browserify: "npm:^2.0.1" + stream-http: "npm:^2.7.2" + string_decoder: "npm:^1.0.0" + timers-browserify: "npm:^2.0.4" + tty-browserify: "npm:0.0.0" + url: "npm:^0.11.0" + util: "npm:^0.11.0" + vm-browserify: "npm:^1.0.1" + checksum: 0e05321a6396408903ed642231d2bca7dd96492d074c7af161ba06a63c95378bd3de50b4105eccbbc02d93ba3da69f0ff5e624bc2a8c92ca462ceb6a403e7986 + languageName: node + linkType: hard + +"node-releases@npm:^2.0.13": + version: 2.0.13 + resolution: "node-releases@npm:2.0.13" + checksum: 2fb44bf70fc949d27f3a48a7fd1a9d1d603ddad4ccd091f26b3fb8b1da976605d919330d7388ccd55ca2ade0dc8b2e12841ba19ef249c8bb29bf82532d401af7 + languageName: node + linkType: hard + +"node-releases@npm:^2.0.14": + version: 2.0.14 + resolution: "node-releases@npm:2.0.14" + checksum: 199fc93773ae70ec9969bc6d5ac5b2bbd6eb986ed1907d751f411fef3ede0e4bfdb45ceb43711f8078bea237b6036db8b1bf208f6ff2b70c7d615afd157f3ab9 + languageName: node + linkType: hard + +"nodemon@npm:^3.1.0": + version: 3.1.0 + resolution: "nodemon@npm:3.1.0" + dependencies: + chokidar: "npm:^3.5.2" + debug: "npm:^4" + ignore-by-default: "npm:^1.0.1" + minimatch: "npm:^3.1.2" + pstree.remy: "npm:^1.1.8" + semver: "npm:^7.5.3" + simple-update-notifier: "npm:^2.0.0" + supports-color: "npm:^5.5.0" + touch: "npm:^3.1.0" + undefsafe: "npm:^2.0.5" + bin: + nodemon: bin/nodemon.js + checksum: 3aeb50105ecae31ce4d0a5cd464011d4aa0dc15419e39ac0fd203d784e38940e1436f4ed96adbaa0f9614ee0644f91e3cf38f2afae8d3918ae7afc51c7e2116b + languageName: node + linkType: hard + +"nopt@npm:^7.0.0, nopt@npm:^7.2.0": + version: 7.2.0 + resolution: "nopt@npm:7.2.0" + dependencies: + abbrev: "npm:^2.0.0" + bin: + nopt: bin/nopt.js + checksum: 9bd7198df6f16eb29ff16892c77bcf7f0cc41f9fb5c26280ac0def2cf8cf319f3b821b3af83eba0e74c85807cc430a16efe0db58fe6ae1f41e69519f585b6aff + languageName: node + linkType: hard + +"nopt@npm:~1.0.10": + version: 1.0.10 + resolution: "nopt@npm:1.0.10" + dependencies: + abbrev: "npm:1" + bin: + nopt: ./bin/nopt.js + checksum: ddfbd892116a125fd68849ef564dd5b1f0a5ba0dbbf18782e9499e2efad8f4d3790635b47c6b5d3f7e014069e7b3ce5b8112687e9ae093fcd2678188c866fe28 + languageName: node + linkType: hard + +"normalize-package-data@npm:^2.3.2, normalize-package-data@npm:^2.5.0": + version: 2.5.0 + resolution: "normalize-package-data@npm:2.5.0" + dependencies: + hosted-git-info: "npm:^2.1.4" + resolve: "npm:^1.10.0" + semver: "npm:2 || 3 || 4 || 5" + validate-npm-package-license: "npm:^3.0.1" + checksum: 357cb1646deb42f8eb4c7d42c4edf0eec312f3628c2ef98501963cc4bbe7277021b2b1d977f982b2edce78f5a1014613ce9cf38085c3df2d76730481357ca504 + languageName: node + linkType: hard + +"normalize-path@npm:^2.0.1, normalize-path@npm:^2.1.1": + version: 2.1.1 + resolution: "normalize-path@npm:2.1.1" + dependencies: + remove-trailing-separator: "npm:^1.0.1" + checksum: db814326ff88057437233361b4c7e9cac7b54815b051b57f2d341ce89b1d8ec8cbd43e7fa95d7652b3b69ea8fcc294b89b8530d556a84d1bdace94229e1e9a8b + languageName: node + linkType: hard + +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": + version: 3.0.0 + resolution: "normalize-path@npm:3.0.0" + checksum: e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 + languageName: node + linkType: hard + +"normalize-range@npm:^0.1.2": + version: 0.1.2 + resolution: "normalize-range@npm:0.1.2" + checksum: bf39b73a63e0a42ad1a48c2bd1bda5a07ede64a7e2567307a407674e595bcff0fa0d57e8e5f1e7fa5e91000797c7615e13613227aaaa4d6d6e87f5bd5cc95de6 + languageName: node + linkType: hard + +"now-and-later@npm:^2.0.0": + version: 2.0.1 + resolution: "now-and-later@npm:2.0.1" + dependencies: + once: "npm:^1.3.2" + checksum: a3b123b6a7378f300cf45b381efb69b7d085a4151dceeca8442e7e08aa50f6e44d15af114261dca201e19be85f9e25dd61ad74aab62ad3675210bfc60f1f19f5 + languageName: node + linkType: hard + +"npm-run-all@npm:^4.1.5": + version: 4.1.5 + resolution: "npm-run-all@npm:4.1.5" + dependencies: + ansi-styles: "npm:^3.2.1" + chalk: "npm:^2.4.1" + cross-spawn: "npm:^6.0.5" + memorystream: "npm:^0.3.1" + minimatch: "npm:^3.0.4" + pidtree: "npm:^0.3.0" + read-pkg: "npm:^3.0.0" + shell-quote: "npm:^1.6.1" + string.prototype.padend: "npm:^3.0.0" + bin: + npm-run-all: bin/npm-run-all/index.js + run-p: bin/run-p/index.js + run-s: bin/run-s/index.js + checksum: 736ee39bd35454d3efaa4a2e53eba6c523e2e17fba21a18edcce6b221f5cab62000bef16bb6ae8aff9e615831e6b0eb25ab51d52d60e6fa6f4ea880e4c6d31f4 + languageName: node + linkType: hard + +"npm-run-path@npm:^4.0.1": + version: 4.0.1 + resolution: "npm-run-path@npm:4.0.1" + dependencies: + path-key: "npm:^3.0.0" + checksum: 6f9353a95288f8455cf64cbeb707b28826a7f29690244c1e4bb61ec573256e021b6ad6651b394eb1ccfd00d6ec50147253aba2c5fe58a57ceb111fad62c519ac + languageName: node + linkType: hard + +"npm-run-path@npm:^5.1.0": + version: 5.1.0 + resolution: "npm-run-path@npm:5.1.0" + dependencies: + path-key: "npm:^4.0.0" + checksum: ff6d77514489f47fa1c3b1311d09cd4b6d09a874cc1866260f9dea12cbaabda0436ed7f8c2ee44d147bf99a3af29307c6f63b0f83d242b0b6b0ab25dff2629e3 + languageName: node + linkType: hard + +"nth-check@npm:^2.0.1": + version: 2.1.1 + resolution: "nth-check@npm:2.1.1" + dependencies: + boolbase: "npm:^1.0.0" + checksum: 5fee7ff309727763689cfad844d979aedd2204a817fbaaf0e1603794a7c20db28548d7b024692f953557df6ce4a0ee4ae46cd8ebd9b36cfb300b9226b567c479 + languageName: node + linkType: hard + +"number-is-nan@npm:^1.0.0": + version: 1.0.1 + resolution: "number-is-nan@npm:1.0.1" + checksum: cb97149006acc5cd512c13c1838223abdf202e76ddfa059c5e8e7507aff2c3a78cd19057516885a2f6f5b576543dc4f7b6f3c997cc7df53ae26c260855466df5 + languageName: node + linkType: hard + +"nwsapi@npm:^2.2.7": + version: 2.2.7 + resolution: "nwsapi@npm:2.2.7" + checksum: 44be198adae99208487a1c886c0a3712264f7bbafa44368ad96c003512fed2753d4e22890ca1e6edb2690c3456a169f2a3c33bfacde1905cf3bf01c7722464db + languageName: node + linkType: hard + +"object-assign@npm:4.X, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: 1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414 + languageName: node + linkType: hard + +"object-copy@npm:^0.1.0": + version: 0.1.0 + resolution: "object-copy@npm:0.1.0" + dependencies: + copy-descriptor: "npm:^0.1.0" + define-property: "npm:^0.2.5" + kind-of: "npm:^3.0.3" + checksum: 79314b05e9d626159a04f1d913f4c4aba9eae8848511cf5f4c8e3b04bb3cc313b65f60357f86462c959a14c2d58380fedf89b6b32ecec237c452a5ef3900a293 + languageName: node + linkType: hard + +"object-inspect@npm:^1.13.1, object-inspect@npm:^1.9.0": + version: 1.13.1 + resolution: "object-inspect@npm:1.13.1" + checksum: fad603f408e345c82e946abdf4bfd774260a5ed3e5997a0b057c44153ac32c7271ff19e3a5ae39c858da683ba045ccac2f65245c12763ce4e8594f818f4a648d + languageName: node + linkType: hard + +"object-is@npm:^1.1.5": + version: 1.1.5 + resolution: "object-is@npm:1.1.5" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.1.3" + checksum: 8c263fb03fc28f1ffb54b44b9147235c5e233dc1ca23768e7d2569740b5d860154d7cc29a30220fe28ed6d8008e2422aefdebfe987c103e1c5d190cf02d9d886 + languageName: node + linkType: hard + +"object-keys@npm:^1.1.1": + version: 1.1.1 + resolution: "object-keys@npm:1.1.1" + checksum: b11f7ccdbc6d406d1f186cdadb9d54738e347b2692a14439ca5ac70c225fa6db46db809711b78589866d47b25fc3e8dee0b4c722ac751e11180f9380e3d8601d + languageName: node + linkType: hard + +"object-visit@npm:^1.0.0": + version: 1.0.1 + resolution: "object-visit@npm:1.0.1" + dependencies: + isobject: "npm:^3.0.0" + checksum: 086b475bda24abd2318d2b187c3e928959b89f5cb5883d6fe5a42d03719b61fc18e765f658de9ac8730e67ba9ff26d61e73d991215948ff9ecefe771e0071029 + languageName: node + linkType: hard + +"object.assign@npm:^4.0.4, object.assign@npm:^4.1.0, object.assign@npm:^4.1.4": + version: 4.1.4 + resolution: "object.assign@npm:4.1.4" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.1.4" + has-symbols: "npm:^1.0.3" + object-keys: "npm:^1.1.1" + checksum: 2f286118c023e557757620e647b02e7c88d3d417e0c568fca0820de8ec9cca68928304854d5b03e99763eddad6e78a6716e2930f7e6372e4b9b843f3fd3056f3 + languageName: node + linkType: hard + +"object.defaults@npm:^1.0.0, object.defaults@npm:^1.1.0": + version: 1.1.0 + resolution: "object.defaults@npm:1.1.0" + dependencies: + array-each: "npm:^1.0.1" + array-slice: "npm:^1.0.0" + for-own: "npm:^1.0.0" + isobject: "npm:^3.0.0" + checksum: 9ed5c41ce500c2dce2e6f8baa71b0e73b013dcd57c02e545dd85b46e52140af707e2b05c31f6126209f8b15709f10817ddbe6fb5c13f8d873d811694f28ee3fd + languageName: node + linkType: hard + +"object.map@npm:^1.0.0": + version: 1.0.1 + resolution: "object.map@npm:1.0.1" + dependencies: + for-own: "npm:^1.0.0" + make-iterator: "npm:^1.0.0" + checksum: f5dff48d3aa6604e8c1983c988a1314b8858181cbedc1671a83c8db6f247a97f31a7acb7ec1b85a72a785149bc34ffbd284d953d902fef7a3c19e2064959a0aa + languageName: node + linkType: hard + +"object.pick@npm:^1.2.0, object.pick@npm:^1.3.0": + version: 1.3.0 + resolution: "object.pick@npm:1.3.0" + dependencies: + isobject: "npm:^3.0.1" + checksum: cd316ec986e49895a28f2df9182de9cdeee57cd2a952c122aacc86344c28624fe002d9affc4f48b5014ec7c033da9942b08821ddb44db8c5bac5b3ec54bdc31e + languageName: node + linkType: hard + +"object.reduce@npm:^1.0.0": + version: 1.0.1 + resolution: "object.reduce@npm:1.0.1" + dependencies: + for-own: "npm:^1.0.0" + make-iterator: "npm:^1.0.0" + checksum: d3c10543bf939f7475e61f90784613fec60c6a3b92a45e2d7a88b1fe297c1466edd0148a102cbbb9eb14a48bafecb698917af5b76895f434e6a715e78397f5fc + languageName: node + linkType: hard + +"on-finished@npm:2.4.1": + version: 2.4.1 + resolution: "on-finished@npm:2.4.1" + dependencies: + ee-first: "npm:1.1.1" + checksum: 46fb11b9063782f2d9968863d9cbba33d77aa13c17f895f56129c274318b86500b22af3a160fe9995aa41317efcd22941b6eba747f718ced08d9a73afdb087b4 + languageName: node + linkType: hard + +"on-headers@npm:~1.0.2": + version: 1.0.2 + resolution: "on-headers@npm:1.0.2" + checksum: f649e65c197bf31505a4c0444875db0258e198292f34b884d73c2f751e91792ef96bb5cf89aa0f4fecc2e4dc662461dda606b1274b0e564f539cae5d2f5fc32f + languageName: node + linkType: hard + +"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.3.2, once@npm:^1.4.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: 5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 + languageName: node + linkType: hard + +"one-time@npm:^1.0.0": + version: 1.0.0 + resolution: "one-time@npm:1.0.0" + dependencies: + fn.name: "npm:1.x.x" + checksum: 6e4887b331edbb954f4e915831cbec0a7b9956c36f4feb5f6de98c448ac02ff881fd8d9b55a6b1b55030af184c6b648f340a76eb211812f4ad8c9b4b8692fdaa + languageName: node + linkType: hard + +"onetime@npm:^5.1.0, onetime@npm:^5.1.2": + version: 5.1.2 + resolution: "onetime@npm:5.1.2" + dependencies: + mimic-fn: "npm:^2.1.0" + checksum: ffcef6fbb2692c3c40749f31ea2e22677a876daea92959b8a80b521d95cca7a668c884d8b2045d1d8ee7d56796aa405c405462af112a1477594cc63531baeb8f + languageName: node + linkType: hard + +"onetime@npm:^6.0.0": + version: 6.0.0 + resolution: "onetime@npm:6.0.0" + dependencies: + mimic-fn: "npm:^4.0.0" + checksum: 4eef7c6abfef697dd4479345a4100c382d73c149d2d56170a54a07418c50816937ad09500e1ed1e79d235989d073a9bade8557122aee24f0576ecde0f392bb6c + languageName: node + linkType: hard + +"open@npm:^8.0.4, open@npm:^8.4.0": + version: 8.4.2 + resolution: "open@npm:8.4.2" + dependencies: + define-lazy-prop: "npm:^2.0.0" + is-docker: "npm:^2.1.1" + is-wsl: "npm:^2.2.0" + checksum: bb6b3a58401dacdb0aad14360626faf3fb7fba4b77816b373495988b724fb48941cad80c1b65d62bb31a17609b2cd91c41a181602caea597ca80dfbcc27e84c9 + languageName: node + linkType: hard + +"opentype.js@npm:^1.3.4": + version: 1.3.4 + resolution: "opentype.js@npm:1.3.4" + dependencies: + string.prototype.codepointat: "npm:^0.2.1" + tiny-inflate: "npm:^1.0.3" + bin: + ot: bin/ot + checksum: 7de1c175c439a648f32f5113c619cdcd7ae09096cb791b5b1b8b24afdaf76ccdf8938297dab61ded48e032e8ebb54bcf444eedef562498f7e241dbebcf3da2f6 + languageName: node + linkType: hard + +"ora@npm:^5.4.1": + version: 5.4.1 + resolution: "ora@npm:5.4.1" + dependencies: + bl: "npm:^4.1.0" + chalk: "npm:^4.1.0" + cli-cursor: "npm:^3.1.0" + cli-spinners: "npm:^2.5.0" + is-interactive: "npm:^1.0.0" + is-unicode-supported: "npm:^0.1.0" + log-symbols: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + wcwidth: "npm:^1.0.1" + checksum: 10ff14aace236d0e2f044193362b22edce4784add08b779eccc8f8ef97195cae1248db8ec1ec5f5ff076f91acbe573f5f42a98c19b78dba8c54eefff983cae85 + languageName: node + linkType: hard + +"ordered-read-streams@npm:^1.0.0": + version: 1.0.1 + resolution: "ordered-read-streams@npm:1.0.1" + dependencies: + readable-stream: "npm:^2.0.1" + checksum: 6243667adbcea69527cfebd1e483f0d06109dea578e4bbd6f185acfd1c3cc5f059b887fe600ba3084498924b9566405c0595819e02caf9ce88bc604e90b652b8 + languageName: node + linkType: hard + +"os-browserify@npm:^0.3.0": + version: 0.3.0 + resolution: "os-browserify@npm:0.3.0" + checksum: 6ff32cb1efe2bc6930ad0fd4c50e30c38010aee909eba8d65be60af55efd6cbb48f0287e3649b4e3f3a63dce5a667b23c187c4293a75e557f0d5489d735bcf52 + languageName: node + linkType: hard + +"os-locale@npm:^1.4.0": + version: 1.4.0 + resolution: "os-locale@npm:1.4.0" + dependencies: + lcid: "npm:^1.0.0" + checksum: 302173159d562000ddf982ed75c493a0d861e91372c9e1b13aab21590ff2e1ba264a41995b29be8dc5278a6127ffcd2ad5591779e8164a570fc5fa6c0787b057 + languageName: node + linkType: hard + +"p-limit@npm:^2.0.0, p-limit@npm:^2.2.0": + version: 2.3.0 + resolution: "p-limit@npm:2.3.0" + dependencies: + p-try: "npm:^2.0.0" + checksum: 8da01ac53efe6a627080fafc127c873da40c18d87b3f5d5492d465bb85ec7207e153948df6b9cbaeb130be70152f874229b8242ee2be84c0794082510af97f12 + languageName: node + linkType: hard + +"p-limit@npm:^3.0.2": + version: 3.1.0 + resolution: "p-limit@npm:3.1.0" + dependencies: + yocto-queue: "npm:^0.1.0" + checksum: 9db675949dbdc9c3763c89e748d0ef8bdad0afbb24d49ceaf4c46c02c77d30db4e0652ed36d0a0a7a95154335fab810d95c86153105bb73b3a90448e2bb14e1a + languageName: node + linkType: hard + +"p-limit@npm:^5.0.0": + version: 5.0.0 + resolution: "p-limit@npm:5.0.0" + dependencies: + yocto-queue: "npm:^1.0.0" + checksum: 574e93b8895a26e8485eb1df7c4b58a1a6e8d8ae41b1750cc2cc440922b3d306044fc6e9a7f74578a883d46802d9db72b30f2e612690fcef838c173261b1ed83 + languageName: node + linkType: hard + +"p-locate@npm:^3.0.0": + version: 3.0.0 + resolution: "p-locate@npm:3.0.0" + dependencies: + p-limit: "npm:^2.0.0" + checksum: 7b7f06f718f19e989ce6280ed4396fb3c34dabdee0df948376483032f9d5ec22fdf7077ec942143a75827bb85b11da72016497fc10dac1106c837ed593969ee8 + languageName: node + linkType: hard + +"p-locate@npm:^4.1.0": + version: 4.1.0 + resolution: "p-locate@npm:4.1.0" + dependencies: + p-limit: "npm:^2.2.0" + checksum: 1b476ad69ad7f6059744f343b26d51ce091508935c1dbb80c4e0a2f397ffce0ca3a1f9f5cd3c7ce19d7929a09719d5c65fe70d8ee289c3f267cd36f2881813e9 + languageName: node + linkType: hard + +"p-locate@npm:^5.0.0": + version: 5.0.0 + resolution: "p-locate@npm:5.0.0" + dependencies: + p-limit: "npm:^3.0.2" + checksum: 2290d627ab7903b8b70d11d384fee714b797f6040d9278932754a6860845c4d3190603a0772a663c8cb5a7b21d1b16acb3a6487ebcafa9773094edc3dfe6009a + languageName: node + linkType: hard + +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: 592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 + languageName: node + linkType: hard + +"p-try@npm:^2.0.0": + version: 2.2.0 + resolution: "p-try@npm:2.2.0" + checksum: c36c19907734c904b16994e6535b02c36c2224d433e01a2f1ab777237f4d86e6289fd5fd464850491e940379d4606ed850c03e0f9ab600b0ebddb511312e177f + languageName: node + linkType: hard + +"pako@npm:~0.2.0": + version: 0.2.9 + resolution: "pako@npm:0.2.9" + checksum: 79c1806ebcf325b60ae599e4d7227c2e346d7b829dc20f5cf24cef07c934079dc3a61c5b3c8278a2f7a190c4a613e343ea11e5302dbe252efd11712df4b6b041 + languageName: node + linkType: hard + +"pako@npm:~1.0.2, pako@npm:~1.0.5": + version: 1.0.11 + resolution: "pako@npm:1.0.11" + checksum: 86dd99d8b34c3930345b8bbeb5e1cd8a05f608eeb40967b293f72fe469d0e9c88b783a8777e4cc7dc7c91ce54c5e93d88ff4b4f060e6ff18408fd21030d9ffbe + languageName: node + linkType: hard + +"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.6": + version: 5.1.6 + resolution: "parse-asn1@npm:5.1.6" + dependencies: + asn1.js: "npm:^5.2.0" + browserify-aes: "npm:^1.0.0" + evp_bytestokey: "npm:^1.0.0" + pbkdf2: "npm:^3.0.3" + safe-buffer: "npm:^5.1.1" + checksum: 4ed1d9b9e120c5484d29d67bb90171aac0b73422bc016d6294160aea983275c28a27ab85d862059a36a86a97dd31b7ddd97486802ca9fac67115fe3409e9dcbd + languageName: node + linkType: hard + +"parse-filepath@npm:^1.0.1": + version: 1.0.2 + resolution: "parse-filepath@npm:1.0.2" + dependencies: + is-absolute: "npm:^1.0.0" + map-cache: "npm:^0.2.0" + path-root: "npm:^0.1.1" + checksum: 37bbd225fa864257246777efbdf72a9305c4ae12110bf467d11994e93f8be60dd309dcef68124a2c78c5d3b4e64e1c36fcc2560e2ea93fd97767831e7a446805 + languageName: node + linkType: hard + +"parse-json@npm:^2.2.0": + version: 2.2.0 + resolution: "parse-json@npm:2.2.0" + dependencies: + error-ex: "npm:^1.2.0" + checksum: 7a90132aa76016f518a3d5d746a21b3f1ad0f97a68436ed71b6f995b67c7151141f5464eea0c16c59aec9b7756519a0e3007a8f98cf3714632d509ec07736df6 + languageName: node + linkType: hard + +"parse-json@npm:^4.0.0": + version: 4.0.0 + resolution: "parse-json@npm:4.0.0" + dependencies: + error-ex: "npm:^1.3.1" + json-parse-better-errors: "npm:^1.0.1" + checksum: 8d80790b772ccb1bcea4e09e2697555e519d83d04a77c2b4237389b813f82898943a93ffff7d0d2406203bdd0c30dcf95b1661e3a53f83d0e417f053957bef32 + languageName: node + linkType: hard + +"parse-json@npm:^5.0.0": + version: 5.2.0 + resolution: "parse-json@npm:5.2.0" + dependencies: + "@babel/code-frame": "npm:^7.0.0" + error-ex: "npm:^1.3.1" + json-parse-even-better-errors: "npm:^2.3.0" + lines-and-columns: "npm:^1.1.6" + checksum: 77947f2253005be7a12d858aedbafa09c9ae39eb4863adf330f7b416ca4f4a08132e453e08de2db46459256fb66afaac5ee758b44fe6541b7cdaf9d252e59585 + languageName: node + linkType: hard + +"parse-node-version@npm:^1.0.0": + version: 1.0.1 + resolution: "parse-node-version@npm:1.0.1" + checksum: 999cd3d7da1425c2e182dce82b226c6dc842562d3ed79ec47f5c719c32a7f6c1a5352495b894fc25df164be7f2ede4224758255da9902ddef81f2b77ba46bb2c + languageName: node + linkType: hard + +"parse-passwd@npm:^1.0.0": + version: 1.0.0 + resolution: "parse-passwd@npm:1.0.0" + checksum: 1c05c05f95f184ab9ca604841d78e4fe3294d46b8e3641d305dcc28e930da0e14e602dbda9f3811cd48df5b0e2e27dbef7357bf0d7c40e41b18c11c3a8b8d17b + languageName: node + linkType: hard + +"parse5@npm:^7.1.2": + version: 7.1.2 + resolution: "parse5@npm:7.1.2" + dependencies: + entities: "npm:^4.4.0" + checksum: 297d7af8224f4b5cb7f6617ecdae98eeaed7f8cbd78956c42785e230505d5a4f07cef352af10d3006fa5c1544b76b57784d3a22d861ae071bbc460c649482bf4 + languageName: node + linkType: hard + +"parseurl@npm:~1.3.3": + version: 1.3.3 + resolution: "parseurl@npm:1.3.3" + checksum: 90dd4760d6f6174adb9f20cf0965ae12e23879b5f5464f38e92fce8073354341e4b3b76fa3d878351efe7d01e617121955284cfd002ab087fba1a0726ec0b4f5 + languageName: node + linkType: hard + +"pascalcase@npm:^0.1.1": + version: 0.1.1 + resolution: "pascalcase@npm:0.1.1" + checksum: 48dfe90618e33810bf58211d8f39ad2c0262f19ad6354da1ba563935b5f429f36409a1fb9187c220328f7a4dc5969917f8e3e01ee089b5f1627b02aefe39567b + languageName: node + linkType: hard + +"path-browserify@npm:0.0.1": + version: 0.0.1 + resolution: "path-browserify@npm:0.0.1" + checksum: 3d59710cddeea06509d91935196185900f3d9d29376dff68ff0e146fbd41d0fb304e983d0158f30cabe4dd2ffcc6a7d3d977631994ee984c88e66aed50a1ccd3 + languageName: node + linkType: hard + +"path-dirname@npm:^1.0.0": + version: 1.0.2 + resolution: "path-dirname@npm:1.0.2" + checksum: 71e59be2bada7c91f62b976245fd421b7cb01fde3207fe53a82d8880621ad04fd8b434e628c9cf4e796259fc168a107d77cd56837725267c5b2c58cefe2c4e1b + languageName: node + linkType: hard + +"path-exists@npm:^2.0.0": + version: 2.1.0 + resolution: "path-exists@npm:2.1.0" + dependencies: + pinkie-promise: "npm:^2.0.0" + checksum: 87352f1601c085d5a6eb202f60e5c016c1b790bd0bc09398af446ed3f5c4510b4531ff99cf8acac2d91868886e792927b4292f768b35a83dce12588fb7cbb46e + languageName: node + linkType: hard + +"path-exists@npm:^3.0.0": + version: 3.0.0 + resolution: "path-exists@npm:3.0.0" + checksum: 17d6a5664bc0a11d48e2b2127d28a0e58822c6740bde30403f08013da599182289c56518bec89407e3f31d3c2b6b296a4220bc3f867f0911fee6952208b04167 + languageName: node + linkType: hard + +"path-exists@npm:^4.0.0": + version: 4.0.0 + resolution: "path-exists@npm:4.0.0" + checksum: 8c0bd3f5238188197dc78dced15207a4716c51cc4e3624c44fc97acf69558f5ebb9a2afff486fe1b4ee148e0c133e96c5e11a9aa5c48a3006e3467da070e5e1b + languageName: node + linkType: hard + +"path-is-absolute@npm:^1.0.0": + version: 1.0.1 + resolution: "path-is-absolute@npm:1.0.1" + checksum: 127da03c82172a2a50099cddbf02510c1791fc2cc5f7713ddb613a56838db1e8168b121a920079d052e0936c23005562059756d653b7c544c53185efe53be078 + languageName: node + linkType: hard + +"path-key@npm:^2.0.1": + version: 2.0.1 + resolution: "path-key@npm:2.0.1" + checksum: dd2044f029a8e58ac31d2bf34c34b93c3095c1481942960e84dd2faa95bbb71b9b762a106aead0646695330936414b31ca0bd862bf488a937ad17c8c5d73b32b + languageName: node + linkType: hard + +"path-key@npm:^3.0.0, path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + languageName: node + linkType: hard + +"path-key@npm:^4.0.0": + version: 4.0.0 + resolution: "path-key@npm:4.0.0" + checksum: 794efeef32863a65ac312f3c0b0a99f921f3e827ff63afa5cb09a377e202c262b671f7b3832a4e64731003fa94af0263713962d317b9887bd1e0c48a342efba3 + languageName: node + linkType: hard + +"path-parse@npm:^1.0.7": + version: 1.0.7 + resolution: "path-parse@npm:1.0.7" + checksum: 11ce261f9d294cc7a58d6a574b7f1b935842355ec66fba3c3fd79e0f036462eaf07d0aa95bb74ff432f9afef97ce1926c720988c6a7451d8a584930ae7de86e1 + languageName: node + linkType: hard + +"path-root-regex@npm:^0.1.0": + version: 0.1.2 + resolution: "path-root-regex@npm:0.1.2" + checksum: 27651a234f280c70d982dd25c35550f74a4284cde6b97237aab618cb4b5745682d18cdde1160617bb4a4b6b8aec4fbc911c4a2ad80d01fa4c7ee74dae7af2337 + languageName: node + linkType: hard + +"path-root@npm:^0.1.1": + version: 0.1.1 + resolution: "path-root@npm:0.1.1" + dependencies: + path-root-regex: "npm:^0.1.0" + checksum: aed5cd290df84c46c7730f6a363e95e47a23929b51ab068a3818d69900da3e89dc154cdfd0c45c57b2e02f40c094351bc862db70c2cb00b7e6bd47039a227813 + languageName: node + linkType: hard + +"path-scurry@npm:^1.10.1": + version: 1.10.1 + resolution: "path-scurry@npm:1.10.1" + dependencies: + lru-cache: "npm:^9.1.1 || ^10.0.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: e5dc78a7348d25eec61ab166317e9e9c7b46818aa2c2b9006c507a6ff48c672d011292d9662527213e558f5652ce0afcc788663a061d8b59ab495681840c0c1e + languageName: node + linkType: hard + +"path-to-regexp@npm:0.1.7": + version: 0.1.7 + resolution: "path-to-regexp@npm:0.1.7" + checksum: 50a1ddb1af41a9e68bd67ca8e331a705899d16fb720a1ea3a41e310480948387daf603abb14d7b0826c58f10146d49050a1291ba6a82b78a382d1c02c0b8f905 + languageName: node + linkType: hard + +"path-type@npm:^1.0.0": + version: 1.1.0 + resolution: "path-type@npm:1.1.0" + dependencies: + graceful-fs: "npm:^4.1.2" + pify: "npm:^2.0.0" + pinkie-promise: "npm:^2.0.0" + checksum: 2b8c348cb52bbc0c0568afa10a0a5d8f6233adfe5ae75feb56064f6aed6324ab74185c61c2545f4e52ca08acdc76005f615da4e127ed6eecb866002cf491f350 + languageName: node + linkType: hard + +"path-type@npm:^3.0.0": + version: 3.0.0 + resolution: "path-type@npm:3.0.0" + dependencies: + pify: "npm:^3.0.0" + checksum: 1332c632f1cac15790ebab8dd729b67ba04fc96f81647496feb1c2975d862d046f41e4b975dbd893048999b2cc90721f72924ad820acc58c78507ba7141a8e56 + languageName: node + linkType: hard + +"path-type@npm:^4.0.0": + version: 4.0.0 + resolution: "path-type@npm:4.0.0" + checksum: 666f6973f332f27581371efaf303fd6c272cc43c2057b37aa99e3643158c7e4b2626549555d88626e99ea9e046f82f32e41bbde5f1508547e9a11b149b52387c + languageName: node + linkType: hard + +"pathe@npm:^1.1.0, pathe@npm:^1.1.1": + version: 1.1.1 + resolution: "pathe@npm:1.1.1" + checksum: 3ae5a0529c3415d91c3ac9133f52cffea54a0dd46892fe059f4b80faf36fd207957d4594bdc87043b65d0761b1e5728f81f46bafff3b5302da4e2e48889b8c0e + languageName: node + linkType: hard + +"pathval@npm:^1.1.1": + version: 1.1.1 + resolution: "pathval@npm:1.1.1" + checksum: f63e1bc1b33593cdf094ed6ff5c49c1c0dc5dc20a646ca9725cc7fe7cd9995002d51d5685b9b2ec6814342935748b711bafa840f84c0bb04e38ff40a335c94dc + languageName: node + linkType: hard + +"pbkdf2@npm:^3.0.3": + version: 3.1.2 + resolution: "pbkdf2@npm:3.1.2" + dependencies: + create-hash: "npm:^1.1.2" + create-hmac: "npm:^1.1.4" + ripemd160: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + sha.js: "npm:^2.4.8" + checksum: 5a30374e87d33fa080a92734d778cf172542cc7e41b96198c4c88763997b62d7850de3fbda5c3111ddf79805ee7c1da7046881c90ac4920b5e324204518b05fd + languageName: node + linkType: hard + +"peek-stream@npm:^1.1.0": + version: 1.1.3 + resolution: "peek-stream@npm:1.1.3" + dependencies: + buffer-from: "npm:^1.0.0" + duplexify: "npm:^3.5.0" + through2: "npm:^2.0.3" + checksum: 3c35d1951b8640036f93b1b5628a90f849e49ca4f2e6aba393ff4978413931d9c491c83f71a92f878d5ea4c670af0bba04dfcfb79b310ead22601db7c1420e36 + languageName: node + linkType: hard + +"pend@npm:~1.2.0": + version: 1.2.0 + resolution: "pend@npm:1.2.0" + checksum: 8a87e63f7a4afcfb0f9f77b39bb92374afc723418b9cb716ee4257689224171002e07768eeade4ecd0e86f1fa3d8f022994219fb45634f2dbd78c6803e452458 + languageName: node + linkType: hard + +"picocolors@npm:^0.2.1": + version: 0.2.1 + resolution: "picocolors@npm:0.2.1" + checksum: 98a83c77912c80aea0fc518aec184768501bfceafa490714b0f43eda9c52e372b844ce0a591e822bbfe5df16dcf366be7cbdb9534d39cf54a80796340371ee17 + languageName: node + linkType: hard + +"picocolors@npm:^1.0.0": + version: 1.0.0 + resolution: "picocolors@npm:1.0.0" + checksum: 20a5b249e331c14479d94ec6817a182fd7a5680debae82705747b2db7ec50009a5f6648d0621c561b0572703f84dbef0858abcbd5856d3c5511426afcb1961f7 + languageName: node + linkType: hard + +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.0, picomatch@npm:^2.3.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be + languageName: node + linkType: hard + +"pidtree@npm:^0.3.0": + version: 0.3.1 + resolution: "pidtree@npm:0.3.1" + bin: + pidtree: bin/pidtree.js + checksum: cd69b0182f749f45ab48584e3442c48c5dc4512502c18d5b0147a33b042c41a4db4269b9ce2f7c48f11833ee5e79d81f5ebc6f7bf8372d4ea55726f60dc505a1 + languageName: node + linkType: hard + +"pify@npm:^2.0.0": + version: 2.3.0 + resolution: "pify@npm:2.3.0" + checksum: 551ff8ab830b1052633f59cb8adc9ae8407a436e06b4a9718bcb27dc5844b83d535c3a8512b388b6062af65a98c49bdc0dd523d8b2617b188f7c8fee457158dc + languageName: node + linkType: hard + +"pify@npm:^3.0.0": + version: 3.0.0 + resolution: "pify@npm:3.0.0" + checksum: fead19ed9d801f1b1fcd0638a1ac53eabbb0945bf615f2f8806a8b646565a04a1b0e7ef115c951d225f042cca388fdc1cd3add46d10d1ed6951c20bd2998af10 + languageName: node + linkType: hard + +"pify@npm:^4.0.1": + version: 4.0.1 + resolution: "pify@npm:4.0.1" + checksum: 6f9d404b0d47a965437403c9b90eca8bb2536407f03de165940e62e72c8c8b75adda5516c6b9b23675a5877cc0bcac6bdfb0ef0e39414cd2476d5495da40e7cf + languageName: node + linkType: hard + +"pinkie-promise@npm:^2.0.0": + version: 2.0.1 + resolution: "pinkie-promise@npm:2.0.1" + dependencies: + pinkie: "npm:^2.0.0" + checksum: 11b5e5ce2b090c573f8fad7b517cbca1bb9a247587306f05ae71aef6f9b2cd2b923c304aa9663c2409cfde27b367286179f1379bc4ec18a3fbf2bb0d473b160a + languageName: node + linkType: hard + +"pinkie@npm:^2.0.0": + version: 2.0.4 + resolution: "pinkie@npm:2.0.4" + checksum: 25228b08b5597da42dc384221aa0ce56ee0fbf32965db12ba838e2a9ca0193c2f0609c45551ee077ccd2060bf109137fdb185b00c6d7e0ed7e35006d20fdcbc6 + languageName: node + linkType: hard + +"pirates@npm:^4.0.4, pirates@npm:^4.0.5": + version: 4.0.6 + resolution: "pirates@npm:4.0.6" + checksum: 00d5fa51f8dded94d7429700fb91a0c1ead00ae2c7fd27089f0c5b63e6eca36197fe46384631872690a66f390c5e27198e99006ab77ae472692ab9c2ca903f36 + languageName: node + linkType: hard + +"pkg-dir@npm:^3.0.0": + version: 3.0.0 + resolution: "pkg-dir@npm:3.0.0" + dependencies: + find-up: "npm:^3.0.0" + checksum: 902a3d0c1f8ac43b1795fa1ba6ffeb37dfd53c91469e969790f6ed5e29ff2bdc50b63ba6115dc056d2efb4a040aa2446d512b3804bdafdf302f734fb3ec21847 + languageName: node + linkType: hard + +"pkg-dir@npm:^4.1.0": + version: 4.2.0 + resolution: "pkg-dir@npm:4.2.0" + dependencies: + find-up: "npm:^4.0.0" + checksum: c56bda7769e04907a88423feb320babaed0711af8c436ce3e56763ab1021ba107c7b0cafb11cde7529f669cfc22bffcaebffb573645cbd63842ea9fb17cd7728 + languageName: node + linkType: hard + +"pkg-dir@npm:^5.0.0": + version: 5.0.0 + resolution: "pkg-dir@npm:5.0.0" + dependencies: + find-up: "npm:^5.0.0" + checksum: 793a496d685dc55bbbdbbb22d884535c3b29241e48e3e8d37e448113a71b9e42f5481a61fdc672d7322de12fbb2c584dd3a68bf89b18fffce5c48a390f911bc5 + languageName: node + linkType: hard + +"pkg-types@npm:^1.0.3": + version: 1.0.3 + resolution: "pkg-types@npm:1.0.3" + dependencies: + jsonc-parser: "npm:^3.2.0" + mlly: "npm:^1.2.0" + pathe: "npm:^1.1.0" + checksum: 7f692ff2005f51b8721381caf9bdbc7f5461506ba19c34f8631660a215c8de5e6dca268f23a319dd180b8f7c47a0dc6efea14b376c485ff99e98d810b8f786c4 + languageName: node + linkType: hard + +"plugin-error@npm:^1.0.0, plugin-error@npm:^1.0.1": + version: 1.0.1 + resolution: "plugin-error@npm:1.0.1" + dependencies: + ansi-colors: "npm:^1.0.1" + arr-diff: "npm:^4.0.0" + arr-union: "npm:^3.1.0" + extend-shallow: "npm:^3.0.2" + checksum: 9b0ef44f8d2749013dfeb4a86c8082f2f277bf72e0c694c30dd504d0b329f321db91fe9d9cb0f7e8579f7ffa4260b7792827bc5ef4f87d6bcc0fc691de3d91a1 + languageName: node + linkType: hard + +"plugin-error@npm:^2.0.1": + version: 2.0.1 + resolution: "plugin-error@npm:2.0.1" + dependencies: + ansi-colors: "npm:^1.0.1" + checksum: f4ad7de56dd72455f01257680733c51884c3d7f74fbc111483da72489d52fa86a52e45d64ef89b869efce78a5ae17737ff89738d9bd5508d6220968ccd02b308 + languageName: node + linkType: hard + +"polished@npm:^4.2.2": + version: 4.2.2 + resolution: "polished@npm:4.2.2" + dependencies: + "@babel/runtime": "npm:^7.17.8" + checksum: 1d054d1fea18ac7d921ca91504ffcf1ef0f505eda6acbfec6e205a98ebfea80b658664995deb35907dabc5f75f287dc2894812503a8aed28285bb91f25cf7400 + languageName: node + linkType: hard + +"posix-character-classes@npm:^0.1.0": + version: 0.1.1 + resolution: "posix-character-classes@npm:0.1.1" + checksum: cce88011548a973b4af58361cd8f5f7b5a6faff8eef0901565802f067bcabf82597e920d4c97c22068464be3cbc6447af589f6cc8a7d813ea7165be60a0395bc + languageName: node + linkType: hard + +"postcss-clean@npm:^1.2.2": + version: 1.2.2 + resolution: "postcss-clean@npm:1.2.2" + dependencies: + clean-css: "npm:^4.x" + postcss: "npm:^6.x" + checksum: fad77df4dd82b9415e8f5aae81448abf5709c121c1b85825ab16468e61cf0a8928ff1330e202a8d69b43847b58fb3a7354699ff09130f2f6bb90afa9f99c4887 + languageName: node + linkType: hard + +"postcss-load-config@npm:^5.0.0": + version: 5.0.3 + resolution: "postcss-load-config@npm:5.0.3" + dependencies: + lilconfig: "npm:^3.0.0" + yaml: "npm:^2.3.4" + peerDependencies: + jiti: ">=1.21.0" + postcss: ">=8.0.9" + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + checksum: decb5363cead7dc72f664a7943f1cd88e252107b289261f50925101e864c7bb80a5c479e876609d8146c1ab6b52b961abb91cbb41768edc416eb9729555f0643 + languageName: node + linkType: hard + +"postcss-modules-extract-imports@npm:^3.0.0": + version: 3.0.0 + resolution: "postcss-modules-extract-imports@npm:3.0.0" + peerDependencies: + postcss: ^8.1.0 + checksum: f8879d66d8162fb7a3fcd916d37574006c584ea509107b1cfb798a5e090175ef9470f601e46f0a305070d8ff2500e07489a5c1ac381c29a1dc1120e827ca7943 + languageName: node + linkType: hard + +"postcss-modules-local-by-default@npm:^4.0.0": + version: 4.0.3 + resolution: "postcss-modules-local-by-default@npm:4.0.3" + dependencies: + icss-utils: "npm:^5.0.0" + postcss-selector-parser: "npm:^6.0.2" + postcss-value-parser: "npm:^4.1.0" + peerDependencies: + postcss: ^8.1.0 + checksum: be49b86efbfb921f42287e227584aac91af9826fc1083db04958ae283dfe215ca539421bfba71f9da0f0b10651f28e95a64b5faca7166f578a1933b8646051f7 + languageName: node + linkType: hard + +"postcss-modules-scope@npm:^3.0.0": + version: 3.0.0 + resolution: "postcss-modules-scope@npm:3.0.0" + dependencies: + postcss-selector-parser: "npm:^6.0.4" + peerDependencies: + postcss: ^8.1.0 + checksum: 60af503910363689568c2c3701cb019a61b58b3d739391145185eec211bea5d50ccb6ecbe6955b39d856088072fd50ea002e40a52b50e33b181ff5c41da0308a + languageName: node + linkType: hard + +"postcss-modules-values@npm:^4.0.0": + version: 4.0.0 + resolution: "postcss-modules-values@npm:4.0.0" + dependencies: + icss-utils: "npm:^5.0.0" + peerDependencies: + postcss: ^8.1.0 + checksum: dd18d7631b5619fb9921b198c86847a2a075f32e0c162e0428d2647685e318c487a2566cc8cc669fc2077ef38115cde7a068e321f46fb38be3ad49646b639dbc + languageName: node + linkType: hard + +"postcss-modules@npm:^6.0.0": + version: 6.0.0 + resolution: "postcss-modules@npm:6.0.0" + dependencies: + generic-names: "npm:^4.0.0" + icss-utils: "npm:^5.1.0" + lodash.camelcase: "npm:^4.3.0" + postcss-modules-extract-imports: "npm:^3.0.0" + postcss-modules-local-by-default: "npm:^4.0.0" + postcss-modules-scope: "npm:^3.0.0" + postcss-modules-values: "npm:^4.0.0" + string-hash: "npm:^1.1.1" + peerDependencies: + postcss: ^8.0.0 + checksum: 24b5240597472e75d115a6eb242cf6f94c284ee443c4572383092ac12246593887e0cec408f47cd57292485f1482b0936b77c25756bc8324dbd1035336a89ff4 + languageName: node + linkType: hard + +"postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4": + version: 6.0.13 + resolution: "postcss-selector-parser@npm:6.0.13" + dependencies: + cssesc: "npm:^3.0.0" + util-deprecate: "npm:^1.0.2" + checksum: 51f099b27f7c7198ea1826470ef0adfa58b3bd3f59b390fda123baa0134880a5fa9720137b6009c4c1373357b144f700b0edac73335d0067422063129371444e + languageName: node + linkType: hard + +"postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0": + version: 4.2.0 + resolution: "postcss-value-parser@npm:4.2.0" + checksum: f4142a4f56565f77c1831168e04e3effd9ffcc5aebaf0f538eee4b2d465adfd4b85a44257bb48418202a63806a7da7fe9f56c330aebb3cac898e46b4cbf49161 + languageName: node + linkType: hard + +"postcss@npm:^6.x": + version: 6.0.23 + resolution: "postcss@npm:6.0.23" + dependencies: + chalk: "npm:^2.4.1" + source-map: "npm:^0.6.1" + supports-color: "npm:^5.4.0" + checksum: 45d45184ffbb9d510e7585d9441af9a1a771a56b7553b1d598544e54acdfd31df439a95d5f00a6dc57b85b76d0c8925fec18614b1cc795887c845c3965e32e63 + languageName: node + linkType: hard + +"postcss@npm:^7.0.16": + version: 7.0.39 + resolution: "postcss@npm:7.0.39" + dependencies: + picocolors: "npm:^0.2.1" + source-map: "npm:^0.6.1" + checksum: fd27ee808c0d02407582cccfad4729033e2b439d56cd45534fb39aaad308bb35a290f3b7db5f2394980e8756f9381b458a625618550808c5ff01a125f51efc53 + languageName: node + linkType: hard + +"postcss@npm:^8.4.32": + version: 8.4.32 + resolution: "postcss@npm:8.4.32" + dependencies: + nanoid: "npm:^3.3.7" + picocolors: "npm:^1.0.0" + source-map-js: "npm:^1.0.2" + checksum: 39308a9195fa34d4dbdd7b58a896cff0c7809f84f7a4ac1b95b68ca86c9138a395addff33075668ed3983d41b90aac05754c445237a9365eb1c3a5602ebd03ad + languageName: node + linkType: hard + +"postcss@npm:^8.4.35": + version: 8.4.35 + resolution: "postcss@npm:8.4.35" + dependencies: + nanoid: "npm:^3.3.7" + picocolors: "npm:^1.0.0" + source-map-js: "npm:^1.0.2" + checksum: e8dd04e48001eb5857abc9475365bf08f4e508ddf9bc0b8525449a95d190f10d025acebc5b56ac2e94b3c7146790e4ae78989bb9633cb7ee20d1cc9b7dc909b2 + languageName: node + linkType: hard + +"prettier@npm:^2.8.0": + version: 2.8.8 + resolution: "prettier@npm:2.8.8" + bin: + prettier: bin-prettier.js + checksum: 463ea8f9a0946cd5b828d8cf27bd8b567345cf02f56562d5ecde198b91f47a76b7ac9eae0facd247ace70e927143af6135e8cf411986b8cb8478784a4d6d724a + languageName: node + linkType: hard + +"prettier@npm:^3.2.5": + version: 3.2.5 + resolution: "prettier@npm:3.2.5" + bin: + prettier: bin/prettier.cjs + checksum: ea327f37a7d46f2324a34ad35292af2ad4c4c3c3355da07313339d7e554320f66f65f91e856add8530157a733c6c4a897dc41b577056be5c24c40f739f5ee8c6 + languageName: node + linkType: hard + +"pretty-format@npm:^27.0.2": + version: 27.5.1 + resolution: "pretty-format@npm:27.5.1" + dependencies: + ansi-regex: "npm:^5.0.1" + ansi-styles: "npm:^5.0.0" + react-is: "npm:^17.0.1" + checksum: 0cbda1031aa30c659e10921fa94e0dd3f903ecbbbe7184a729ad66f2b6e7f17891e8c7d7654c458fa4ccb1a411ffb695b4f17bbcd3fe075fabe181027c4040ed + languageName: node + linkType: hard + +"pretty-format@npm:^29.7.0": + version: 29.7.0 + resolution: "pretty-format@npm:29.7.0" + dependencies: + "@jest/schemas": "npm:^29.6.3" + ansi-styles: "npm:^5.0.0" + react-is: "npm:^18.0.0" + checksum: edc5ff89f51916f036c62ed433506b55446ff739358de77207e63e88a28ca2894caac6e73dcb68166a606e51c8087d32d400473e6a9fdd2dbe743f46c9c0276f + languageName: node + linkType: hard + +"pretty-hrtime@npm:^1.0.0, pretty-hrtime@npm:^1.0.3": + version: 1.0.3 + resolution: "pretty-hrtime@npm:1.0.3" + checksum: 67cb3fc283a72252b49ac488647e6a01b78b7aa1b8f2061834aa1650691229081518ef3ca940f77f41cc8a8f02ba9eeb74b843481596670209e493062f2e89e0 + languageName: node + linkType: hard + +"pretty-time@npm:^1.1.0": + version: 1.1.0 + resolution: "pretty-time@npm:1.1.0" + checksum: ba9d7af19cd43838fb2b147654990949575e400dc2cc24bf71ec4a6c4033a38ba8172b1014b597680c6d4d3c075e94648b2c13a7206c5f0c90b711c7388726f3 + languageName: node + linkType: hard + +"prettysize@npm:^2.0.0": + version: 2.0.0 + resolution: "prettysize@npm:2.0.0" + checksum: b5ff8d54844a133d09b582540b731d721af4b86c3d8a9322f204e9e4cb08f891d076ad29acf1ad4091a0515920dd8bf26c96435dcf6ce248131ca4a3f8a1ec89 + languageName: node + linkType: hard + +"proc-log@npm:^3.0.0": + version: 3.0.0 + resolution: "proc-log@npm:3.0.0" + checksum: f66430e4ff947dbb996058f6fd22de2c66612ae1a89b097744e17fb18a4e8e7a86db99eda52ccf15e53f00b63f4ec0b0911581ff2aac0355b625c8eac509b0dc + languageName: node + linkType: hard + +"process-nextick-args@npm:^2.0.0, process-nextick-args@npm:~2.0.0": + version: 2.0.1 + resolution: "process-nextick-args@npm:2.0.1" + checksum: bec089239487833d46b59d80327a1605e1c5287eaad770a291add7f45fda1bb5e28b38e0e061add0a1d0ee0984788ce74fa394d345eed1c420cacf392c554367 + languageName: node + linkType: hard + +"process@npm:^0.11.10": + version: 0.11.10 + resolution: "process@npm:0.11.10" + checksum: 40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3 + languageName: node + linkType: hard + +"progress@npm:^2.0.1": + version: 2.0.3 + resolution: "progress@npm:2.0.3" + checksum: 1697e07cb1068055dbe9fe858d242368ff5d2073639e652b75a7eb1f2a1a8d4afd404d719de23c7b48481a6aa0040686310e2dac2f53d776daa2176d3f96369c + languageName: node + linkType: hard + +"promise-make-naked@npm:^2.1.1": + version: 2.1.1 + resolution: "promise-make-naked@npm:2.1.1" + checksum: 97bc0a3eeae59f75e8716d5f511edb4ed7558fa304f93407a7c9de3645a19135abfc87d4bca0b570619d3314fa87db67ea3463c4a5068c4bbe7f8889c6883f1d + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + languageName: node + linkType: hard + +"promise@npm:^7.1.1": + version: 7.3.1 + resolution: "promise@npm:7.3.1" + dependencies: + asap: "npm:~2.0.3" + checksum: 742e5c0cc646af1f0746963b8776299701ad561ce2c70b49365d62c8db8ea3681b0a1bf0d4e2fe07910bf72f02d39e51e8e73dc8d7503c3501206ac908be107f + languageName: node + linkType: hard + +"prompts@npm:^2.4.0": + version: 2.4.2 + resolution: "prompts@npm:2.4.2" + dependencies: + kleur: "npm:^3.0.3" + sisteransi: "npm:^1.0.5" + checksum: 16f1ac2977b19fe2cf53f8411cc98db7a3c8b115c479b2ca5c82b5527cd937aa405fa04f9a5960abeb9daef53191b53b4d13e35c1f5d50e8718c76917c5f1ea4 + languageName: node + linkType: hard + +"prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": + version: 15.8.1 + resolution: "prop-types@npm:15.8.1" + dependencies: + loose-envify: "npm:^1.4.0" + object-assign: "npm:^4.1.1" + react-is: "npm:^16.13.1" + checksum: 59ece7ca2fb9838031d73a48d4becb9a7cc1ed10e610517c7d8f19a1e02fa47f7c27d557d8a5702bec3cfeccddc853579832b43f449e54635803f277b1c78077 + languageName: node + linkType: hard + +"proto-list@npm:~1.2.1": + version: 1.2.4 + resolution: "proto-list@npm:1.2.4" + checksum: b9179f99394ec8a68b8afc817690185f3b03933f7b46ce2e22c1930dc84b60d09f5ad222beab4e59e58c6c039c7f7fcf620397235ef441a356f31f9744010e12 + languageName: node + linkType: hard + +"proxy-addr@npm:~2.0.7": + version: 2.0.7 + resolution: "proxy-addr@npm:2.0.7" + dependencies: + forwarded: "npm:0.2.0" + ipaddr.js: "npm:1.9.1" + checksum: c3eed999781a35f7fd935f398b6d8920b6fb00bbc14287bc6de78128ccc1a02c89b95b56742bf7cf0362cc333c61d138532049c7dedc7a328ef13343eff81210 + languageName: node + linkType: hard + +"proxy-from-env@npm:^1.0.0": + version: 1.1.0 + resolution: "proxy-from-env@npm:1.1.0" + checksum: fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b + languageName: node + linkType: hard + +"psl@npm:^1.1.33": + version: 1.9.0 + resolution: "psl@npm:1.9.0" + checksum: 6a3f805fdab9442f44de4ba23880c4eba26b20c8e8e0830eff1cb31007f6825dace61d17203c58bfe36946842140c97a1ba7f67bc63ca2d88a7ee052b65d97ab + languageName: node + linkType: hard + +"pstree.remy@npm:^1.1.8": + version: 1.1.8 + resolution: "pstree.remy@npm:1.1.8" + checksum: 30f78c88ce6393cb3f7834216cb6e282eb83c92ccb227430d4590298ab2811bc4a4745f850a27c5178e79a8f3e316591de0fec87abc19da648c2b3c6eb766d14 + languageName: node + linkType: hard + +"public-encrypt@npm:^4.0.0": + version: 4.0.3 + resolution: "public-encrypt@npm:4.0.3" + dependencies: + bn.js: "npm:^4.1.0" + browserify-rsa: "npm:^4.0.0" + create-hash: "npm:^1.1.0" + parse-asn1: "npm:^5.0.0" + randombytes: "npm:^2.0.1" + safe-buffer: "npm:^5.1.2" + checksum: 6c2cc19fbb554449e47f2175065d6b32f828f9b3badbee4c76585ac28ae8641aafb9bb107afc430c33c5edd6b05dbe318df4f7d6d7712b1093407b11c4280700 + languageName: node + linkType: hard + +"pump@npm:^2.0.0": + version: 2.0.1 + resolution: "pump@npm:2.0.1" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: f1fe8960f44d145f8617ea4c67de05392da4557052980314c8f85081aee26953bdcab64afad58a2b1df0e8ff7203e3710e848cbe81a01027978edc6e264db355 + languageName: node + linkType: hard + +"pump@npm:^3.0.0": + version: 3.0.0 + resolution: "pump@npm:3.0.0" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: bbdeda4f747cdf47db97428f3a135728669e56a0ae5f354a9ac5b74556556f5446a46f720a8f14ca2ece5be9b4d5d23c346db02b555f46739934cc6c093a5478 + languageName: node + linkType: hard + +"pumpify@npm:^1.3.3, pumpify@npm:^1.3.5": + version: 1.5.1 + resolution: "pumpify@npm:1.5.1" + dependencies: + duplexify: "npm:^3.6.0" + inherits: "npm:^2.0.3" + pump: "npm:^2.0.0" + checksum: 0bcabf9e3dbf2d0cc1f9b84ac80d3c75386111caf8963bfd98817a1e2192000ac0ccc804ca6ccd5b2b8430fdb71347b20fb2f014fe3d41adbacb1b502a841c45 + languageName: node + linkType: hard + +"punycode@npm:^1.2.4, punycode@npm:^1.4.1": + version: 1.4.1 + resolution: "punycode@npm:1.4.1" + checksum: 354b743320518aef36f77013be6e15da4db24c2b4f62c5f1eb0529a6ed02fbaf1cb52925785f6ab85a962f2b590d9cd5ad730b70da72b5f180e2556b8bd3ca08 + languageName: node + linkType: hard + +"punycode@npm:^2.1.1, punycode@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: 14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 + languageName: node + linkType: hard + +"puppeteer-core@npm:^2.1.1": + version: 2.1.1 + resolution: "puppeteer-core@npm:2.1.1" + dependencies: + "@types/mime-types": "npm:^2.1.0" + debug: "npm:^4.1.0" + extract-zip: "npm:^1.6.6" + https-proxy-agent: "npm:^4.0.0" + mime: "npm:^2.0.3" + mime-types: "npm:^2.1.25" + progress: "npm:^2.0.1" + proxy-from-env: "npm:^1.0.0" + rimraf: "npm:^2.6.1" + ws: "npm:^6.1.0" + checksum: 29a73c2327e208e6528bac05f841b3340ee1a8d7bd59e7b235c9d8b3c0bf266804ad1aa901a0e4a1d66ce4202646f242988c3c5c4dfb105e9ad082bf4aae69be + languageName: node + linkType: hard + +"qs@npm:6.11.0": + version: 6.11.0 + resolution: "qs@npm:6.11.0" + dependencies: + side-channel: "npm:^1.0.4" + checksum: 4e4875e4d7c7c31c233d07a448e7e4650f456178b9dd3766b7cfa13158fdb24ecb8c4f059fa91e820dc6ab9f2d243721d071c9c0378892dcdad86e9e9a27c68f + languageName: node + linkType: hard + +"qs@npm:^6.10.0, qs@npm:^6.11.2": + version: 6.11.2 + resolution: "qs@npm:6.11.2" + dependencies: + side-channel: "npm:^1.0.4" + checksum: 4f95d4ff18ed480befcafa3390022817ffd3087fc65f146cceb40fc5edb9fa96cb31f648cae2fa96ca23818f0798bd63ad4ca369a0e22702fcd41379b3ab6571 + languageName: node + linkType: hard + +"querystring-es3@npm:^0.2.0": + version: 0.2.1 + resolution: "querystring-es3@npm:0.2.1" + checksum: 476938c1adb45c141f024fccd2ffd919a3746e79ed444d00e670aad68532977b793889648980e7ca7ff5ffc7bfece623118d0fbadcaf217495eeb7059ae51580 + languageName: node + linkType: hard + +"querystringify@npm:^2.1.1": + version: 2.2.0 + resolution: "querystringify@npm:2.2.0" + checksum: 3258bc3dbdf322ff2663619afe5947c7926a6ef5fb78ad7d384602974c467fadfc8272af44f5eb8cddd0d011aae8fabf3a929a8eee4b86edcc0a21e6bd10f9aa + languageName: node + linkType: hard + +"queue-microtask@npm:^1.2.2": + version: 1.2.3 + resolution: "queue-microtask@npm:1.2.3" + checksum: 900a93d3cdae3acd7d16f642c29a642aea32c2026446151f0778c62ac089d4b8e6c986811076e1ae180a694cedf077d453a11b58ff0a865629a4f82ab558e102 + languageName: node + linkType: hard + +"ramda@npm:0.29.0": + version: 0.29.0 + resolution: "ramda@npm:0.29.0" + checksum: b00eaaf1c62b06a99affa1d583e256bd65ad27ab9d0ef512f55d7d93b842e7cd244a4a09179f61fdd8548362e409323867a2b0477cbd0626b5644eb6ac7c53da + languageName: node + linkType: hard + +"randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5": + version: 2.1.0 + resolution: "randombytes@npm:2.1.0" + dependencies: + safe-buffer: "npm:^5.1.0" + checksum: 50395efda7a8c94f5dffab564f9ff89736064d32addf0cc7e8bf5e4166f09f8ded7a0849ca6c2d2a59478f7d90f78f20d8048bca3cdf8be09d8e8a10790388f3 + languageName: node + linkType: hard + +"randomcolor@npm:^0.6.2": + version: 0.6.2 + resolution: "randomcolor@npm:0.6.2" + checksum: 845b8b4a31ff4a6fe590930217db93ca5741b0eb7b363c564d5db72d52449383bd4cc44b77b8ae64cf4b70118ef7b8b9062134743e9ec7a6798d6a6f84318a2a + languageName: node + linkType: hard + +"randomfill@npm:^1.0.3": + version: 1.0.4 + resolution: "randomfill@npm:1.0.4" + dependencies: + randombytes: "npm:^2.0.5" + safe-buffer: "npm:^5.1.0" + checksum: 11aeed35515872e8f8a2edec306734e6b74c39c46653607f03c68385ab8030e2adcc4215f76b5e4598e028c4750d820afd5c65202527d831d2a5f207fe2bc87c + languageName: node + linkType: hard + +"range-parser@npm:~1.2.1": + version: 1.2.1 + resolution: "range-parser@npm:1.2.1" + checksum: 96c032ac2475c8027b7a4e9fe22dc0dfe0f6d90b85e496e0f016fbdb99d6d066de0112e680805075bd989905e2123b3b3d002765149294dce0c1f7f01fcc2ea0 + languageName: node + linkType: hard + +"raw-body@npm:2.5.1": + version: 2.5.1 + resolution: "raw-body@npm:2.5.1" + dependencies: + bytes: "npm:3.1.2" + http-errors: "npm:2.0.0" + iconv-lite: "npm:0.4.24" + unpipe: "npm:1.0.0" + checksum: 5dad5a3a64a023b894ad7ab4e5c7c1ce34d3497fc7138d02f8c88a3781e68d8a55aa7d4fd3a458616fa8647cc228be314a1c03fb430a07521de78b32c4dd09d2 + languageName: node + linkType: hard + +"react-colorful@npm:^5.1.2": + version: 5.6.1 + resolution: "react-colorful@npm:5.6.1" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 48eb73cf71e10841c2a61b6b06ab81da9fffa9876134c239bfdebcf348ce2a47e56b146338e35dfb03512c85966bfc9a53844fc56bc50154e71f8daee59ff6f0 + languageName: node + linkType: hard + +"react-confetti@npm:^6.1.0": + version: 6.1.0 + resolution: "react-confetti@npm:6.1.0" + dependencies: + tween-functions: "npm:^1.2.0" + peerDependencies: + react: ^16.3.0 || ^17.0.1 || ^18.0.0 + checksum: 5b4eb23eef564695f6db1d25b294ed31d5fa21ff4092c6a38e641f85cd10e3e0b50014366e3ac0f7cf772e73faaecd14614e5b11a5531336fa769dda8068ab59 + languageName: node + linkType: hard + +"react-docgen-typescript@npm:^2.2.2": + version: 2.2.2 + resolution: "react-docgen-typescript@npm:2.2.2" + peerDependencies: + typescript: ">= 4.3.x" + checksum: d31a061a21b5d4b67d4af7bc742541fd9e16254bd32861cd29c52565bc2175f40421a3550d52b6a6b0d0478e7cc408558eb0060a0bdd2957b02cfceeb0ee1e88 + languageName: node + linkType: hard + +"react-docgen@npm:^7.0.0": + version: 7.0.1 + resolution: "react-docgen@npm:7.0.1" + dependencies: + "@babel/core": "npm:^7.18.9" + "@babel/traverse": "npm:^7.18.9" + "@babel/types": "npm:^7.18.9" + "@types/babel__core": "npm:^7.18.0" + "@types/babel__traverse": "npm:^7.18.0" + "@types/doctrine": "npm:^0.0.9" + "@types/resolve": "npm:^1.20.2" + doctrine: "npm:^3.0.0" + resolve: "npm:^1.22.1" + strip-indent: "npm:^4.0.0" + checksum: 870c1193211f14497bf7a96137f96840dc058842ca75ff7251d91e88c3c71d7a41d5f1a124cc1b53bfbf1f2b6b58bfccc4dd6e22592814a5155d3894953274be + languageName: node + linkType: hard + +"react-dom@npm:^18.2.0": + version: 18.2.0 + resolution: "react-dom@npm:18.2.0" + dependencies: + loose-envify: "npm:^1.1.0" + scheduler: "npm:^0.23.0" + peerDependencies: + react: ^18.2.0 + checksum: 66dfc5f93e13d0674e78ef41f92ed21dfb80f9c4ac4ac25a4b51046d41d4d2186abc915b897f69d3d0ebbffe6184e7c5876f2af26bfa956f179225d921be713a + languageName: node + linkType: hard + +"react-element-to-jsx-string@npm:^15.0.0": + version: 15.0.0 + resolution: "react-element-to-jsx-string@npm:15.0.0" + dependencies: + "@base2/pretty-print-object": "npm:1.0.1" + is-plain-object: "npm:5.0.0" + react-is: "npm:18.1.0" + peerDependencies: + react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 + react-dom: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 + checksum: 0d60a0ea758529c32a706d0c69d70b69fb94de3c46442fffdee34f08f51ffceddbb5395b41dfd1565895653e9f60f98ca525835be9d5db1f16d6b22be12f4cd4 + languageName: node + linkType: hard + +"react-is@npm:18.1.0": + version: 18.1.0 + resolution: "react-is@npm:18.1.0" + checksum: 558874e4c3bd9805a9294426e090919ee6901be3ab07f80b997c36b5a01a8d691112802e7438d146f6c82fd6495d8c030f276ef05ec3410057f8740a8d723f8c + languageName: node + linkType: hard + +"react-is@npm:^16.13.1": + version: 16.13.1 + resolution: "react-is@npm:16.13.1" + checksum: 33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1 + languageName: node + linkType: hard + +"react-is@npm:^17.0.1": + version: 17.0.2 + resolution: "react-is@npm:17.0.2" + checksum: 2bdb6b93fbb1820b024b496042cce405c57e2f85e777c9aabd55f9b26d145408f9f74f5934676ffdc46f3dcff656d78413a6e43968e7b3f92eea35b3052e9053 + languageName: node + linkType: hard + +"react-is@npm:^18.0.0": + version: 18.2.0 + resolution: "react-is@npm:18.2.0" + checksum: 6eb5e4b28028c23e2bfcf73371e72cd4162e4ac7ab445ddae2afe24e347a37d6dc22fae6e1748632cd43c6d4f9b8f86dcf26bf9275e1874f436d129952528ae0 + languageName: node + linkType: hard + +"react-lifecycles-compat@npm:^3.0.4": + version: 3.0.4 + resolution: "react-lifecycles-compat@npm:3.0.4" + checksum: 1d0df3c85af79df720524780f00c064d53a9dd1899d785eddb7264b378026979acbddb58a4b7e06e7d0d12aa1494fd5754562ee55d32907b15601068dae82c27 + languageName: node + linkType: hard + +"react-refresh@npm:^0.14.0": + version: 0.14.0 + resolution: "react-refresh@npm:0.14.0" + checksum: b8ae07ad153357d77830928a7f1fc2df837aabefee907fa273ba04c7643f3b860e986f1d4b7ada9b721c8d79b8c24b5b911a314a1a2398b105f1b13d19ea2b8d + languageName: node + linkType: hard + +"react-remove-scroll-bar@npm:^2.3.3": + version: 2.3.4 + resolution: "react-remove-scroll-bar@npm:2.3.4" + dependencies: + react-style-singleton: "npm:^2.2.1" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 2262750dc1022c56d2c79e8d865c00045881c57bcaca74810ae8adac35cfdf723ff7d6b3b0e95c85eb9a0cff90bb4b1e0af801bd703ce8c0a2e35ab14ff1babb + languageName: node + linkType: hard + +"react-remove-scroll@npm:2.5.5": + version: 2.5.5 + resolution: "react-remove-scroll@npm:2.5.5" + dependencies: + react-remove-scroll-bar: "npm:^2.3.3" + react-style-singleton: "npm:^2.2.1" + tslib: "npm:^2.1.0" + use-callback-ref: "npm:^1.3.0" + use-sidecar: "npm:^1.1.2" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 4952657e6a7b9d661d4ad4dfcef81b9c7fa493e35164abff99c35c0b27b3d172ef7ad70c09416dc44dd14ff2e6b38a5ec7da27e27e90a15cbad36b8fd2fd8054 + languageName: node + linkType: hard + +"react-style-singleton@npm:^2.2.1": + version: 2.2.1 + resolution: "react-style-singleton@npm:2.2.1" + dependencies: + get-nonce: "npm:^1.0.0" + invariant: "npm:^2.2.4" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 6d66f3bdb65e1ec79089f80314da97c9a005087a04ee034255a5de129a4c0d9fd0bf99fa7bf642781ac2dc745ca687aae3de082bd8afdd0d117bc953241e15ad + languageName: node + linkType: hard + +"react-virtualized@npm:^9.22.5": + version: 9.22.5 + resolution: "react-virtualized@npm:9.22.5" + dependencies: + "@babel/runtime": "npm:^7.7.2" + clsx: "npm:^1.0.4" + dom-helpers: "npm:^5.1.3" + loose-envify: "npm:^1.4.0" + prop-types: "npm:^15.7.2" + react-lifecycles-compat: "npm:^3.0.4" + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 + checksum: b0444b472f317dce61119c07426c5e9ebfe5125d049996678da922717715a1aa83df755aa36877f4b1718aa2e181d22f15ebb807ee356418c56f922f865628c1 + languageName: node + linkType: hard + +"react@npm:^18.2.0": + version: 18.2.0 + resolution: "react@npm:18.2.0" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: b562d9b569b0cb315e44b48099f7712283d93df36b19a39a67c254c6686479d3980b7f013dc931f4a5a3ae7645eae6386b4aa5eea933baa54ecd0f9acb0902b8 + languageName: node + linkType: hard + +"read-pkg-up@npm:^1.0.1": + version: 1.0.1 + resolution: "read-pkg-up@npm:1.0.1" + dependencies: + find-up: "npm:^1.0.0" + read-pkg: "npm:^1.0.0" + checksum: 36c4fc8bd73edf77a4eeb497b6e43010819ea4aef64cbf8e393439fac303398751c5a299feab84e179a74507e3a1416e1ed033a888b1dac3463bf46d1765f7ac + languageName: node + linkType: hard + +"read-pkg-up@npm:^7.0.1": + version: 7.0.1 + resolution: "read-pkg-up@npm:7.0.1" + dependencies: + find-up: "npm:^4.1.0" + read-pkg: "npm:^5.2.0" + type-fest: "npm:^0.8.1" + checksum: 82b3ac9fd7c6ca1bdc1d7253eb1091a98ff3d195ee0a45386582ce3e69f90266163c34121e6a0a02f1630073a6c0585f7880b3865efcae9c452fa667f02ca385 + languageName: node + linkType: hard + +"read-pkg@npm:^1.0.0": + version: 1.1.0 + resolution: "read-pkg@npm:1.1.0" + dependencies: + load-json-file: "npm:^1.0.0" + normalize-package-data: "npm:^2.3.2" + path-type: "npm:^1.0.0" + checksum: 51fce9f7066787dc7688ea7014324cedeb9f38daa7dace4f1147d526f22354a07189ef728710bc97e27fcf5ed3a03b68ad8b60afb4251984640b6f09c180d572 + languageName: node + linkType: hard + +"read-pkg@npm:^3.0.0": + version: 3.0.0 + resolution: "read-pkg@npm:3.0.0" + dependencies: + load-json-file: "npm:^4.0.0" + normalize-package-data: "npm:^2.3.2" + path-type: "npm:^3.0.0" + checksum: 65acf2df89fbcd506b48b7ced56a255ba00adf7ecaa2db759c86cc58212f6fd80f1f0b7a85c848551a5d0685232e9b64f45c1fd5b48d85df2761a160767eeb93 + languageName: node + linkType: hard + +"read-pkg@npm:^5.2.0": + version: 5.2.0 + resolution: "read-pkg@npm:5.2.0" + dependencies: + "@types/normalize-package-data": "npm:^2.4.0" + normalize-package-data: "npm:^2.5.0" + parse-json: "npm:^5.0.0" + type-fest: "npm:^0.6.0" + checksum: b51a17d4b51418e777029e3a7694c9bd6c578a5ab99db544764a0b0f2c7c0f58f8a6bc101f86a6fceb8ba6d237d67c89acf6170f6b98695d0420ddc86cf109fb + languageName: node + linkType: hard + +"readable-stream@npm:2 || 3, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7 + languageName: node + linkType: hard + +"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.1.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.5, readable-stream@npm:^2.3.6, readable-stream@npm:~2.3.6": + version: 2.3.8 + resolution: "readable-stream@npm:2.3.8" + dependencies: + core-util-is: "npm:~1.0.0" + inherits: "npm:~2.0.3" + isarray: "npm:~1.0.0" + process-nextick-args: "npm:~2.0.0" + safe-buffer: "npm:~5.1.1" + string_decoder: "npm:~1.1.1" + util-deprecate: "npm:~1.0.1" + checksum: 7efdb01f3853bc35ac62ea25493567bf588773213f5f4a79f9c365e1ad13bab845ac0dae7bc946270dc40c3929483228415e92a3fc600cc7e4548992f41ee3fa + languageName: node + linkType: hard + +"readable-stream@npm:^4.5.2": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" + dependencies: + abort-controller: "npm:^3.0.0" + buffer: "npm:^6.0.3" + events: "npm:^3.3.0" + process: "npm:^0.11.10" + string_decoder: "npm:^1.3.0" + checksum: a2c80e0e53aabd91d7df0330929e32d0a73219f9477dbbb18472f6fdd6a11a699fc5d172a1beff98d50eae4f1496c950ffa85b7cc2c4c196963f289a5f39275d + languageName: node + linkType: hard + +"readdirp@npm:^2.2.1": + version: 2.2.1 + resolution: "readdirp@npm:2.2.1" + dependencies: + graceful-fs: "npm:^4.1.11" + micromatch: "npm:^3.1.10" + readable-stream: "npm:^2.0.2" + checksum: 770d177372ff2212d382d425d55ca48301fcbf3231ab3827257bbcca7ff44fb51fe4af6acc2dda8512dc7f29da390e9fbea5b2b3fc724b86e85cc828395b7797 + languageName: node + linkType: hard + +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: "npm:^2.2.1" + checksum: 6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b + languageName: node + linkType: hard + +"readline-sync@npm:^1.4.7": + version: 1.4.10 + resolution: "readline-sync@npm:1.4.10" + checksum: 0a4d0fe4ad501f8f005a3c9cbf3cc0ae6ca2ced93e9a1c7c46f226bdfcb6ef5d3f437ae7e9d2e1098ee13524a3739c830e4c8dbc7f543a693eecd293e41093a3 + languageName: node + linkType: hard + +"recast@npm:^0.23.1, recast@npm:^0.23.3": + version: 0.23.4 + resolution: "recast@npm:0.23.4" + dependencies: + assert: "npm:^2.0.0" + ast-types: "npm:^0.16.1" + esprima: "npm:~4.0.0" + source-map: "npm:~0.6.1" + tslib: "npm:^2.0.1" + checksum: d719633be8029e28f23b8191d4a525c5dbdac721792ab3cb5e9dfcf1694fb93f3c147b186916195a9c7fa0711f1e4990ba457cdcee02faed3899d4a80da1bd1f + languageName: node + linkType: hard + +"rechoir@npm:^0.6.2": + version: 0.6.2 + resolution: "rechoir@npm:0.6.2" + dependencies: + resolve: "npm:^1.1.6" + checksum: 22c4bb32f4934a9468468b608417194f7e3ceba9a508512125b16082c64f161915a28467562368eeb15dc16058eb5b7c13a20b9eb29ff9927d1ebb3b5aa83e84 + languageName: node + linkType: hard + +"regenerate-unicode-properties@npm:^10.1.0": + version: 10.1.1 + resolution: "regenerate-unicode-properties@npm:10.1.1" + dependencies: + regenerate: "npm:^1.4.2" + checksum: 89adb5ee5ba081380c78f9057c02e156a8181969f6fcca72451efc45612e0c3df767b4333f8d8479c274d9c6fe52ec4854f0d8a22ef95dccbe87da8e5f2ac77d + languageName: node + linkType: hard + +"regenerate@npm:^1.4.2": + version: 1.4.2 + resolution: "regenerate@npm:1.4.2" + checksum: f73c9eba5d398c818edc71d1c6979eaa05af7a808682749dd079f8df2a6d91a9b913db216c2c9b03e0a8ba2bba8701244a93f45211afbff691c32c7b275db1b8 + languageName: node + linkType: hard + +"regenerator-runtime@npm:^0.14.0": + version: 0.14.0 + resolution: "regenerator-runtime@npm:0.14.0" + checksum: e25f062c1a183f81c99681691a342760e65c55e8d3a4d4fe347ebe72433b123754b942b70b622959894e11f8a9131dc549bd3c9a5234677db06a4af42add8d12 + languageName: node + linkType: hard + +"regenerator-transform@npm:^0.15.2": + version: 0.15.2 + resolution: "regenerator-transform@npm:0.15.2" + dependencies: + "@babel/runtime": "npm:^7.8.4" + checksum: 7cfe6931ec793269701994a93bab89c0cc95379191fad866270a7fea2adfec67ea62bb5b374db77058b60ba4509319d9b608664d0d288bd9989ca8dbd08fae90 + languageName: node + linkType: hard + +"regex-not@npm:^1.0.0, regex-not@npm:^1.0.2": + version: 1.0.2 + resolution: "regex-not@npm:1.0.2" + dependencies: + extend-shallow: "npm:^3.0.2" + safe-regex: "npm:^1.1.0" + checksum: a0f8d6045f63b22e9759db10e248369c443b41cedd7dba0922d002b66c2734bc2aef0d98c4d45772d1f756245f4c5203856b88b9624bba2a58708858a8d485d6 + languageName: node + linkType: hard + +"regexp.prototype.flags@npm:^1.5.1": + version: 1.5.1 + resolution: "regexp.prototype.flags@npm:1.5.1" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + set-function-name: "npm:^2.0.0" + checksum: 1de7d214c0a726c7c874a7023e47b0e27b9f7fdb64175bfe1861189de1704aaeca05c3d26c35aa375432289b99946f3cf86651a92a8f7601b90d8c226a23bcd8 + languageName: node + linkType: hard + +"regexpu-core@npm:^5.3.1": + version: 5.3.2 + resolution: "regexpu-core@npm:5.3.2" + dependencies: + "@babel/regjsgen": "npm:^0.8.0" + regenerate: "npm:^1.4.2" + regenerate-unicode-properties: "npm:^10.1.0" + regjsparser: "npm:^0.9.1" + unicode-match-property-ecmascript: "npm:^2.0.0" + unicode-match-property-value-ecmascript: "npm:^2.1.0" + checksum: 7945d5ab10c8bbed3ca383d4274687ea825aee4ab93a9c51c6e31e1365edd5ea807f6908f800ba017b66c462944ba68011164e7055207747ab651f8111ef3770 + languageName: node + linkType: hard + +"regjsparser@npm:^0.9.1": + version: 0.9.1 + resolution: "regjsparser@npm:0.9.1" + dependencies: + jsesc: "npm:~0.5.0" + bin: + regjsparser: bin/parser + checksum: fe44fcf19a99fe4f92809b0b6179530e5ef313ff7f87df143b08ce9a2eb3c4b6189b43735d645be6e8f4033bfb015ed1ca54f0583bc7561bed53fd379feb8225 + languageName: node + linkType: hard + +"remark-external-links@npm:^8.0.0": + version: 8.0.0 + resolution: "remark-external-links@npm:8.0.0" + dependencies: + extend: "npm:^3.0.0" + is-absolute-url: "npm:^3.0.0" + mdast-util-definitions: "npm:^4.0.0" + space-separated-tokens: "npm:^1.0.0" + unist-util-visit: "npm:^2.0.0" + checksum: 5f0affc97e18ad3247e3b29449f4df98be5a75950cf0f0f13dd1755c4ef1065f9ab44626bba34d913d32bb92afd6f06a8e2f8068e83b48337f0b7a5d1f0cecfe + languageName: node + linkType: hard + +"remark-slug@npm:^6.0.0": + version: 6.1.0 + resolution: "remark-slug@npm:6.1.0" + dependencies: + github-slugger: "npm:^1.0.0" + mdast-util-to-string: "npm:^1.0.0" + unist-util-visit: "npm:^2.0.0" + checksum: 7cc2857936fce9c9c00b9c7d70de46d594cedf93bd8560fd006164dee7aacccdf472654ee35b33f4fb4bd0af882d89998c6d0c9088c2e95702a9fc15ebae002a + languageName: node + linkType: hard + +"remove-bom-buffer@npm:^3.0.0": + version: 3.0.0 + resolution: "remove-bom-buffer@npm:3.0.0" + dependencies: + is-buffer: "npm:^1.1.5" + is-utf8: "npm:^0.2.1" + checksum: 5179a73424893880709fff54ba2160d6175abfb587031a4cdf16f43acb5952d219fe342a40ea45a4d2ef40cd7af19722b0ba6447a6605b42b6c0674eff320896 + languageName: node + linkType: hard + +"remove-bom-stream@npm:^1.2.0": + version: 1.2.0 + resolution: "remove-bom-stream@npm:1.2.0" + dependencies: + remove-bom-buffer: "npm:^3.0.0" + safe-buffer: "npm:^5.1.0" + through2: "npm:^2.0.3" + checksum: c5f34d3308c7864579838a3741a08983bd47d3bac5e6f9e4f498c1eccdc6784805ce52aec1700c420eff09d05184e6c96bb6a3380cf18aadce6dd3d4138399cb + languageName: node + linkType: hard + +"remove-trailing-separator@npm:^1.0.1, remove-trailing-separator@npm:^1.1.0": + version: 1.1.0 + resolution: "remove-trailing-separator@npm:1.1.0" + checksum: 3568f9f8f5af3737b4aee9e6e1e8ec4be65a92da9cb27f989e0893714d50aa95ed2ff02d40d1fa35e1b1a234dc9c2437050ef356704a3999feaca6667d9e9bfc + languageName: node + linkType: hard + +"repeat-element@npm:^1.1.2": + version: 1.1.4 + resolution: "repeat-element@npm:1.1.4" + checksum: 81aa8d82bc845780803ef52df3533fa399974b99df571d0bb86e91f0ffca9ee4b9c4e8e5e72af087938cc28d2aef93d106a6d01da685d72ce96455b90a9f9f69 + languageName: node + linkType: hard + +"repeat-string@npm:^1.6.1": + version: 1.6.1 + resolution: "repeat-string@npm:1.6.1" + checksum: 87fa21bfdb2fbdedc44b9a5b118b7c1239bdd2c2c1e42742ef9119b7d412a5137a1d23f1a83dc6bb686f4f27429ac6f542e3d923090b44181bafa41e8ac0174d + languageName: node + linkType: hard + +"replace-ext@npm:^1.0.0": + version: 1.0.1 + resolution: "replace-ext@npm:1.0.1" + checksum: 9a9c3d68d0d31f20533ed23e9f6990cff8320cf357eebfa56c0d7b63746ae9f2d6267f3321e80e0bffcad854f710fc9a48dbcf7615579d767db69e9cd4a43168 + languageName: node + linkType: hard + +"replace-ext@npm:^2.0.0": + version: 2.0.0 + resolution: "replace-ext@npm:2.0.0" + checksum: 52cb1006f83c5f07ef2c76b070c58bdeca1b67beded57d60593d1af8cd8ee731501d0433645cea8e9a4bf57a7018f47c9a3928c0463496cad1946fa85907aa47 + languageName: node + linkType: hard + +"replace-homedir@npm:^1.0.0": + version: 1.0.0 + resolution: "replace-homedir@npm:1.0.0" + dependencies: + homedir-polyfill: "npm:^1.0.1" + is-absolute: "npm:^1.0.0" + remove-trailing-separator: "npm:^1.1.0" + checksum: 7ccdc91dde3dfbf284eb5841291fa634f61a4534d4a1faf3de7ada14db27675cf76b0246c29bf11544ebb8a9e75b401cb5185fa17a9b9a0bd4910f8ff1a25073 + languageName: node + linkType: hard + +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99 + languageName: node + linkType: hard + +"require-main-filename@npm:^1.0.1": + version: 1.0.1 + resolution: "require-main-filename@npm:1.0.1" + checksum: 1ab87efb72a0e223a667154e92f29ca753fd42eb87f22db142b91c86d134e29ecf18af929111ccd255fd340b57d84a9d39489498d8dfd5136b300ded30a5f0b6 + languageName: node + linkType: hard + +"requires-port@npm:^1.0.0": + version: 1.0.0 + resolution: "requires-port@npm:1.0.0" + checksum: b2bfdd09db16c082c4326e573a82c0771daaf7b53b9ce8ad60ea46aa6e30aaf475fe9b164800b89f93b748d2c234d8abff945d2551ba47bf5698e04cd7713267 + languageName: node + linkType: hard + +"resolve-dir@npm:^1.0.0, resolve-dir@npm:^1.0.1": + version: 1.0.1 + resolution: "resolve-dir@npm:1.0.1" + dependencies: + expand-tilde: "npm:^2.0.0" + global-modules: "npm:^1.0.0" + checksum: 8197ed13e4a51d9cd786ef6a09fc83450db016abe7ef3311ca39389b3e508d77c26fe0cf0483a9b407b8caa2764bb5ccc52cf6a017ded91492a416475a56066f + languageName: node + linkType: hard + +"resolve-from@npm:^5.0.0": + version: 5.0.0 + resolution: "resolve-from@npm:5.0.0" + checksum: b21cb7f1fb746de8107b9febab60095187781137fd803e6a59a76d421444b1531b641bba5857f5dc011974d8a5c635d61cec49e6bd3b7fc20e01f0fafc4efbf2 + languageName: node + linkType: hard + +"resolve-options@npm:^1.1.0": + version: 1.1.0 + resolution: "resolve-options@npm:1.1.0" + dependencies: + value-or-function: "npm:^3.0.0" + checksum: 2f55cbe96ef8260771fc52a4335bb4a04e0d7b52e616c2538a0eb48fd8335a932a3bfc67356a21db965e4bc3e4be869e7925d475c8fb556adf771cc5409fbf3d + languageName: node + linkType: hard + +"resolve-url@npm:^0.2.1": + version: 0.2.1 + resolution: "resolve-url@npm:0.2.1" + checksum: c285182cfcddea13a12af92129ce0569be27fb0074ffaefbd3ba3da2eac2acecdfc996d435c4982a9fa2b4708640e52837c9153a5ab9255886a00b0b9e8d2a54 + languageName: node + linkType: hard + +"resolve@npm:^1.1.6, resolve@npm:^1.1.7, resolve@npm:^1.10.0, resolve@npm:^1.14.2, resolve@npm:^1.22.1, resolve@npm:^1.4.0": + version: 1.22.8 + resolution: "resolve@npm:1.22.8" + dependencies: + is-core-module: "npm:^2.13.0" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 07e179f4375e1fd072cfb72ad66d78547f86e6196c4014b31cb0b8bb1db5f7ca871f922d08da0fbc05b94e9fd42206f819648fa3b5b873ebbc8e1dc68fec433a + languageName: node + linkType: hard + +"resolve@patch:resolve@npm%3A^1.1.6#optional!builtin, resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.4.0#optional!builtin": + version: 1.22.8 + resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" + dependencies: + is-core-module: "npm:^2.13.0" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 0446f024439cd2e50c6c8fa8ba77eaa8370b4180f401a96abf3d1ebc770ac51c1955e12764cde449fde3fff480a61f84388e3505ecdbab778f4bef5f8212c729 + languageName: node + linkType: hard + +"restore-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "restore-cursor@npm:3.1.0" + dependencies: + onetime: "npm:^5.1.0" + signal-exit: "npm:^3.0.2" + checksum: 8051a371d6aa67ff21625fa94e2357bd81ffdc96267f3fb0fc4aaf4534028343836548ef34c240ffa8c25b280ca35eb36be00b3cb2133fa4f51896d7e73c6b4f + languageName: node + linkType: hard + +"ret@npm:~0.1.10": + version: 0.1.15 + resolution: "ret@npm:0.1.15" + checksum: 01f77cad0f7ea4f955852c03d66982609893edc1240c0c964b4c9251d0f9fb6705150634060d169939b096d3b77f4c84d6b6098a5b5d340160898c8581f1f63f + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + languageName: node + linkType: hard + +"reusify@npm:^1.0.4": + version: 1.0.4 + resolution: "reusify@npm:1.0.4" + checksum: c19ef26e4e188f408922c46f7ff480d38e8dfc55d448310dfb518736b23ed2c4f547fb64a6ed5bdba92cd7e7ddc889d36ff78f794816d5e71498d645ef476107 + languageName: node + linkType: hard + +"rimraf@npm:^2.6.1": + version: 2.7.1 + resolution: "rimraf@npm:2.7.1" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: ./bin.js + checksum: 4eef73d406c6940927479a3a9dee551e14a54faf54b31ef861250ac815172bade86cc6f7d64a4dc5e98b65e4b18a2e1c9ff3b68d296be0c748413f092bb0dd40 + languageName: node + linkType: hard + +"rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "rimraf@npm:3.0.2" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: bin.js + checksum: 9cb7757acb489bd83757ba1a274ab545eafd75598a9d817e0c3f8b164238dd90eba50d6b848bd4dcc5f3040912e882dc7ba71653e35af660d77b25c381d402e8 + languageName: node + linkType: hard + +"rimraf@npm:^5.0.5": + version: 5.0.5 + resolution: "rimraf@npm:5.0.5" + dependencies: + glob: "npm:^10.3.7" + bin: + rimraf: dist/esm/bin.mjs + checksum: d50dbe724f33835decd88395b25ed35995077c60a50ae78ded06e0185418914e555817aad1b4243edbff2254548c2f6ad6f70cc850040bebb4da9e8cc016f586 + languageName: node + linkType: hard + +"rimraf@npm:~2.6.2": + version: 2.6.3 + resolution: "rimraf@npm:2.6.3" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: ./bin.js + checksum: f1e646f8c567795f2916aef7aadf685b543da6b9a53e482bb04b07472c7eef2b476045ba1e29f401c301c66b630b22b815ab31fdd60c5e1ae6566ff523debf45 + languageName: node + linkType: hard + +"ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1": + version: 2.0.2 + resolution: "ripemd160@npm:2.0.2" + dependencies: + hash-base: "npm:^3.0.0" + inherits: "npm:^2.0.1" + checksum: f6f0df78817e78287c766687aed4d5accbebc308a8e7e673fb085b9977473c1f139f0c5335d353f172a915bb288098430755d2ad3c4f30612f4dd0c901cd2c3a + languageName: node + linkType: hard + +"rollup@npm:^2.25.0 || ^3.3.0": + version: 3.29.4 + resolution: "rollup@npm:3.29.4" + dependencies: + fsevents: "npm:~2.3.2" + dependenciesMeta: + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 65eddf84bf389ea8e4d4c1614b1c6a298d08f8ae785c0c087e723a879190c8aaddbab4aa3b8a0524551b9036750c9f8bfea27b377798accfd2ba5084ceff5aaa + languageName: node + linkType: hard + +"rollup@npm:^4.2.0": + version: 4.6.0 + resolution: "rollup@npm:4.6.0" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.6.0" + "@rollup/rollup-android-arm64": "npm:4.6.0" + "@rollup/rollup-darwin-arm64": "npm:4.6.0" + "@rollup/rollup-darwin-x64": "npm:4.6.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.6.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.6.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.6.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.6.0" + "@rollup/rollup-linux-x64-musl": "npm:4.6.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.6.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.6.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.6.0" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 27e5cb87633a480d951121a631cfcff1e5ca0539069f88abd3dd75df6b4cc4cadfb532ee84e1bf7d7a7f318ad57dc5e516c77331703761dd71ed57c0a57e0c3e + languageName: node + linkType: hard + +"rrweb-cssom@npm:^0.6.0": + version: 0.6.0 + resolution: "rrweb-cssom@npm:0.6.0" + checksum: 3d9d90d53c2349ea9c8509c2690df5a4ef930c9cf8242aeb9425d4046f09d712bb01047e00da0e1c1dab5db35740b3d78fd45c3e7272f75d3724a563f27c30a3 + languageName: node + linkType: hard + +"run-parallel@npm:^1.1.9": + version: 1.2.0 + resolution: "run-parallel@npm:1.2.0" + dependencies: + queue-microtask: "npm:^1.2.2" + checksum: 200b5ab25b5b8b7113f9901bfe3afc347e19bb7475b267d55ad0eb86a62a46d77510cb0f232507c9e5d497ebda569a08a9867d0d14f57a82ad5564d991588b39 + languageName: node + linkType: hard + +"rxjs@npm:8.0.0-alpha.14": + version: 8.0.0-alpha.14 + resolution: "rxjs@npm:8.0.0-alpha.14" + checksum: 14e4b487455c0940e3d87985cc3f2917ec88ce8cd3ede06071eb071a841d729549d43cab198757863838a4e22c1d7d3e9816f08d37357fa7e3506bdd6f8af56e + languageName: node + linkType: hard + +"rxjs@npm:^7.4.0, rxjs@npm:^7.8.1": + version: 7.8.1 + resolution: "rxjs@npm:7.8.1" + dependencies: + tslib: "npm:^2.1.0" + checksum: 3c49c1ecd66170b175c9cacf5cef67f8914dcbc7cd0162855538d365c83fea631167cacb644b3ce533b2ea0e9a4d0b12175186985f89d75abe73dbd8f7f06f68 + languageName: node + linkType: hard + +"safe-array-concat@npm:^1.0.1": + version: 1.0.1 + resolution: "safe-array-concat@npm:1.0.1" + dependencies: + call-bind: "npm:^1.0.2" + get-intrinsic: "npm:^1.2.1" + has-symbols: "npm:^1.0.3" + isarray: "npm:^2.0.5" + checksum: 4b15ce5fce5ce4d7e744a63592cded88d2f27806ed229eadb2e42629cbcd40e770f7478608e75f455e7fe341acd8c0a01bdcd7146b10645ea7411c5e3c1d1dd8 + languageName: node + linkType: hard + +"safe-buffer@npm:5.1.2, safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": + version: 5.1.2 + resolution: "safe-buffer@npm:5.1.2" + checksum: 780ba6b5d99cc9a40f7b951d47152297d0e260f0df01472a1b99d4889679a4b94a13d644f7dbc4f022572f09ae9005fa2fbb93bbbd83643316f365a3e9a45b21 + languageName: node + linkType: hard + +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: 6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 + languageName: node + linkType: hard + +"safe-regex-test@npm:^1.0.0": + version: 1.0.0 + resolution: "safe-regex-test@npm:1.0.0" + dependencies: + call-bind: "npm:^1.0.2" + get-intrinsic: "npm:^1.1.3" + is-regex: "npm:^1.1.4" + checksum: 14a81a7e683f97b2d6e9c8be61fddcf8ed7a02f4e64a825515f96bb1738eb007145359313741d2704d28b55b703a0f6300c749dde7c1dbc13952a2b85048ede2 + languageName: node + linkType: hard + +"safe-regex@npm:^1.1.0": + version: 1.1.0 + resolution: "safe-regex@npm:1.1.0" + dependencies: + ret: "npm:~0.1.10" + checksum: 547d58aa5184cbef368fd5ed5f28d20f911614748c5da6b35f53fd6626396707587251e6e3d1e3010fd3ff1212e413841b8825eaa5f317017ca62a30899af31a + languageName: node + linkType: hard + +"safe-stable-stringify@npm:^2.3.1": + version: 2.4.3 + resolution: "safe-stable-stringify@npm:2.4.3" + checksum: 81dede06b8f2ae794efd868b1e281e3c9000e57b39801c6c162267eb9efda17bd7a9eafa7379e1f1cacd528d4ced7c80d7460ad26f62ada7c9e01dec61b2e768 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + languageName: node + linkType: hard + +"sass-embedded-android-arm64@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-android-arm64@npm:1.71.1" + bin: + sass: dart-sass/sass + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-android-arm@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-android-arm@npm:1.71.1" + bin: + sass: dart-sass/sass + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"sass-embedded-android-ia32@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-android-ia32@npm:1.71.1" + bin: + sass: dart-sass/sass + conditions: os=android & cpu=ia32 + languageName: node + linkType: hard + +"sass-embedded-android-x64@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-android-x64@npm:1.71.1" + bin: + sass: dart-sass/sass + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded-darwin-arm64@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-darwin-arm64@npm:1.71.1" + bin: + sass: dart-sass/sass + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-darwin-x64@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-darwin-x64@npm:1.71.1" + bin: + sass: dart-sass/sass + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded-linux-arm64@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-linux-arm64@npm:1.71.1" + bin: + sass: dart-sass/sass + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-linux-arm@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-linux-arm@npm:1.71.1" + bin: + sass: dart-sass/sass + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"sass-embedded-linux-ia32@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-linux-ia32@npm:1.71.1" + bin: + sass: dart-sass/sass + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"sass-embedded-linux-musl-arm64@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-linux-musl-arm64@npm:1.71.1" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-linux-musl-arm@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-linux-musl-arm@npm:1.71.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"sass-embedded-linux-musl-ia32@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-linux-musl-ia32@npm:1.71.1" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"sass-embedded-linux-musl-x64@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-linux-musl-x64@npm:1.71.1" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded-linux-x64@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-linux-x64@npm:1.71.1" + bin: + sass: dart-sass/sass + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded-win32-ia32@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-win32-ia32@npm:1.71.1" + bin: + sass: dart-sass/sass.bat + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"sass-embedded-win32-x64@npm:1.71.1": + version: 1.71.1 + resolution: "sass-embedded-win32-x64@npm:1.71.1" + bin: + sass: dart-sass/sass.bat + conditions: os=win32 & (cpu=arm64 | cpu=x64) + languageName: node + linkType: hard + +"sass-embedded@npm:^1.71.1": + version: 1.71.1 + resolution: "sass-embedded@npm:1.71.1" + dependencies: + "@bufbuild/protobuf": "npm:^1.0.0" + buffer-builder: "npm:^0.2.0" + immutable: "npm:^4.0.0" + rxjs: "npm:^7.4.0" + sass-embedded-android-arm: "npm:1.71.1" + sass-embedded-android-arm64: "npm:1.71.1" + sass-embedded-android-ia32: "npm:1.71.1" + sass-embedded-android-x64: "npm:1.71.1" + sass-embedded-darwin-arm64: "npm:1.71.1" + sass-embedded-darwin-x64: "npm:1.71.1" + sass-embedded-linux-arm: "npm:1.71.1" + sass-embedded-linux-arm64: "npm:1.71.1" + sass-embedded-linux-ia32: "npm:1.71.1" + sass-embedded-linux-musl-arm: "npm:1.71.1" + sass-embedded-linux-musl-arm64: "npm:1.71.1" + sass-embedded-linux-musl-ia32: "npm:1.71.1" + sass-embedded-linux-musl-x64: "npm:1.71.1" + sass-embedded-linux-x64: "npm:1.71.1" + sass-embedded-win32-ia32: "npm:1.71.1" + sass-embedded-win32-x64: "npm:1.71.1" + supports-color: "npm:^8.1.1" + varint: "npm:^6.0.0" + dependenciesMeta: + sass-embedded-android-arm: + optional: true + sass-embedded-android-arm64: + optional: true + sass-embedded-android-ia32: + optional: true + sass-embedded-android-x64: + optional: true + sass-embedded-darwin-arm64: + optional: true + sass-embedded-darwin-x64: + optional: true + sass-embedded-linux-arm: + optional: true + sass-embedded-linux-arm64: + optional: true + sass-embedded-linux-ia32: + optional: true + sass-embedded-linux-musl-arm: + optional: true + sass-embedded-linux-musl-arm64: + optional: true + sass-embedded-linux-musl-ia32: + optional: true + sass-embedded-linux-musl-x64: + optional: true + sass-embedded-linux-x64: + optional: true + sass-embedded-win32-ia32: + optional: true + sass-embedded-win32-x64: + optional: true + checksum: 637b00398b92b88db6b6dc8906d1c6e42c6907cd26afbda05ff3cdc19360eb2efeeaa8591c995f14e05aa8a08314bf7af219a4cbe1172a95365ca6b442b799d5 + languageName: node + linkType: hard + +"sass@npm:^1.71.1": + version: 1.71.1 + resolution: "sass@npm:1.71.1" + dependencies: + chokidar: "npm:>=3.0.0 <4.0.0" + immutable: "npm:^4.0.0" + source-map-js: "npm:>=0.6.2 <2.0.0" + bin: + sass: sass.js + checksum: 59d79a6e106747746792b0c71908ae0aecdaf9b794d5724ee64e5249412f0d8ebe7ee2bf12946618848f14f949c4f6b530d82da3e62ab31c71198c6f73002130 + languageName: node + linkType: hard + +"sax@npm:^1.3.0": + version: 1.3.0 + resolution: "sax@npm:1.3.0" + checksum: 599dbe0ba9d8bd55e92d920239b21d101823a6cedff71e542589303fa0fa8f3ece6cf608baca0c51be846a2e88365fac94a9101a9c341d94b98e30c4deea5bea + languageName: node + linkType: hard + +"saxes@npm:^6.0.0": + version: 6.0.0 + resolution: "saxes@npm:6.0.0" + dependencies: + xmlchars: "npm:^2.2.0" + checksum: 3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74 + languageName: node + linkType: hard + +"scheduler@npm:^0.23.0": + version: 0.23.0 + resolution: "scheduler@npm:0.23.0" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: b777f7ca0115e6d93e126ac490dbd82642d14983b3079f58f35519d992fa46260be7d6e6cede433a92db70306310c6f5f06e144f0e40c484199e09c1f7be53dd + languageName: node + linkType: hard + +"semver-greatest-satisfied-range@npm:^1.1.0": + version: 1.1.0 + resolution: "semver-greatest-satisfied-range@npm:1.1.0" + dependencies: + sver-compat: "npm:^1.5.0" + checksum: 66f3a52a9ca788523452e5efc70373ca497abc24945eadc5b52b6d06c2821ec201018d1c9eab8d53e8630e9c5457eb4d428828b7ca068b95c41be4cc6ca3a6d6 + languageName: node + linkType: hard + +"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.5.0, semver@npm:^5.6.0": + version: 5.7.2 + resolution: "semver@npm:5.7.2" + bin: + semver: bin/semver + checksum: e4cf10f86f168db772ae95d86ba65b3fd6c5967c94d97c708ccb463b778c2ee53b914cd7167620950fc07faf5a564e6efe903836639e512a1aa15fbc9667fa25 + languageName: node + linkType: hard + +"semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1": + version: 6.3.1 + resolution: "semver@npm:6.3.1" + bin: + semver: bin/semver.js + checksum: e3d79b609071caa78bcb6ce2ad81c7966a46a7431d9d58b8800cfa9cb6a63699b3899a0e4bcce36167a284578212d9ae6942b6929ba4aa5015c079a67751d42d + languageName: node + linkType: hard + +"semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.3": + version: 7.5.4 + resolution: "semver@npm:7.5.4" + dependencies: + lru-cache: "npm:^6.0.0" + bin: + semver: bin/semver.js + checksum: 5160b06975a38b11c1ab55950cb5b8a23db78df88275d3d8a42ccf1f29e55112ac995b3a26a522c36e3b5f76b0445f1eef70d696b8c7862a2b4303d7b0e7609e + languageName: node + linkType: hard + +"send@npm:0.18.0": + version: 0.18.0 + resolution: "send@npm:0.18.0" + dependencies: + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" + mime: "npm:1.6.0" + ms: "npm:2.1.3" + on-finished: "npm:2.4.1" + range-parser: "npm:~1.2.1" + statuses: "npm:2.0.1" + checksum: 0eb134d6a51fc13bbcb976a1f4214ea1e33f242fae046efc311e80aff66c7a43603e26a79d9d06670283a13000e51be6e0a2cb80ff0942eaf9f1cd30b7ae736a + languageName: node + linkType: hard + +"serve-static@npm:1.15.0": + version: 1.15.0 + resolution: "serve-static@npm:1.15.0" + dependencies: + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + parseurl: "npm:~1.3.3" + send: "npm:0.18.0" + checksum: fa9f0e21a540a28f301258dfe1e57bb4f81cd460d28f0e973860477dd4acef946a1f41748b5bd41c73b621bea2029569c935faa38578fd34cd42a9b4947088ba + languageName: node + linkType: hard + +"set-blocking@npm:^2.0.0": + version: 2.0.0 + resolution: "set-blocking@npm:2.0.0" + checksum: 9f8c1b2d800800d0b589de1477c753492de5c1548d4ade52f57f1d1f5e04af5481554d75ce5e5c43d4004b80a3eb714398d6907027dc0534177b7539119f4454 + languageName: node + linkType: hard + +"set-function-length@npm:^1.1.1": + version: 1.1.1 + resolution: "set-function-length@npm:1.1.1" + dependencies: + define-data-property: "npm:^1.1.1" + get-intrinsic: "npm:^1.2.1" + gopd: "npm:^1.0.1" + has-property-descriptors: "npm:^1.0.0" + checksum: a29e255c116c29e3323b851c4f46c58c91be9bb8b065f191e2ea1807cb2c839df56e3175732a498e0c6d54626ba6b6fef896bf699feb7ab70c42dc47eb247c95 + languageName: node + linkType: hard + +"set-function-name@npm:^2.0.0": + version: 2.0.1 + resolution: "set-function-name@npm:2.0.1" + dependencies: + define-data-property: "npm:^1.0.1" + functions-have-names: "npm:^1.2.3" + has-property-descriptors: "npm:^1.0.0" + checksum: 6be7d3e15be47f4db8a5a563a35c60b5e7c4af91cc900e8972ffad33d3aaa227900faa55f60121cdb04b85866a734bb7fe4cd91f654c632861cc86121a48312a + languageName: node + linkType: hard + +"set-value@npm:^2.0.0, set-value@npm:^2.0.1": + version: 2.0.1 + resolution: "set-value@npm:2.0.1" + dependencies: + extend-shallow: "npm:^2.0.1" + is-extendable: "npm:^0.1.1" + is-plain-object: "npm:^2.0.3" + split-string: "npm:^3.0.1" + checksum: 4c40573c4f6540456e4b38b95f570272c4cfbe1d12890ad4057886da8535047cd772dfadf5b58e2e87aa244dfb4c57e3586f6716b976fc47c5144b6b09e1811b + languageName: node + linkType: hard + +"setimmediate@npm:^1.0.4, setimmediate@npm:^1.0.5": + version: 1.0.5 + resolution: "setimmediate@npm:1.0.5" + checksum: 5bae81bfdbfbd0ce992893286d49c9693c82b1bcc00dcaaf3a09c8f428fdeacf4190c013598b81875dfac2b08a572422db7df779a99332d0fce186d15a3e4d49 + languageName: node + linkType: hard + +"setprototypeof@npm:1.2.0": + version: 1.2.0 + resolution: "setprototypeof@npm:1.2.0" + checksum: 68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc + languageName: node + linkType: hard + +"sha.js@npm:^2.4.0, sha.js@npm:^2.4.8": + version: 2.4.11 + resolution: "sha.js@npm:2.4.11" + dependencies: + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + bin: + sha.js: ./bin.js + checksum: b7a371bca8821c9cc98a0aeff67444a03d48d745cb103f17228b96793f455f0eb0a691941b89ea1e60f6359207e36081d9be193252b0f128e0daf9cfea2815a5 + languageName: node + linkType: hard + +"shadow-cljs-jar@npm:1.3.4": + version: 1.3.4 + resolution: "shadow-cljs-jar@npm:1.3.4" + checksum: c5548bb5f2bda5e0a90df6f42e4ec3a07ed4c72cdebb87619e8d9a2167bb3d4b60d6f6a305a3e15cbfb379d5fdbe2a989a0e7059b667cfb3911bc198a4489e94 + languageName: node + linkType: hard + +"shadow-cljs@npm:2.27.4": + version: 2.27.4 + resolution: "shadow-cljs@npm:2.27.4" + dependencies: + node-libs-browser: "npm:^2.2.1" + readline-sync: "npm:^1.4.7" + shadow-cljs-jar: "npm:1.3.4" + source-map-support: "npm:^0.4.15" + which: "npm:^1.3.1" + ws: "npm:^7.4.6" + bin: + shadow-cljs: cli/runner.js + checksum: bae23e71df9c2b2979259a0cde8747c923ee295f58ab4637c9d6b103d82542b40ef39172d4be2dbb94af2e6458a177d1ec96c1eb1e73b1d8f3a4ddb5eaaba7d4 + languageName: node + linkType: hard + +"shallow-clone@npm:^3.0.0": + version: 3.0.1 + resolution: "shallow-clone@npm:3.0.1" + dependencies: + kind-of: "npm:^6.0.2" + checksum: 7bab09613a1b9f480c85a9823aebec533015579fa055ba6634aa56ba1f984380670eaf33b8217502931872aa1401c9fcadaa15f9f604d631536df475b05bcf1e + languageName: node + linkType: hard + +"shebang-command@npm:^1.2.0": + version: 1.2.0 + resolution: "shebang-command@npm:1.2.0" + dependencies: + shebang-regex: "npm:^1.0.0" + checksum: 7b20dbf04112c456b7fc258622dafd566553184ac9b6938dd30b943b065b21dabd3776460df534cc02480db5e1b6aec44700d985153a3da46e7db7f9bd21326d + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + languageName: node + linkType: hard + +"shebang-regex@npm:^1.0.0": + version: 1.0.0 + resolution: "shebang-regex@npm:1.0.0" + checksum: 9abc45dee35f554ae9453098a13fdc2f1730e525a5eb33c51f096cc31f6f10a4b38074c1ebf354ae7bffa7229506083844008dfc3bb7818228568c0b2dc1fff2 + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + languageName: node + linkType: hard + +"shell-quote@npm:^1.6.1, shell-quote@npm:^1.8.1": + version: 1.8.1 + resolution: "shell-quote@npm:1.8.1" + checksum: 8cec6fd827bad74d0a49347057d40dfea1e01f12a6123bf82c4649f3ef152fc2bc6d6176e6376bffcd205d9d0ccb4f1f9acae889384d20baff92186f01ea455a + languageName: node + linkType: hard + +"side-channel@npm:^1.0.4": + version: 1.0.4 + resolution: "side-channel@npm:1.0.4" + dependencies: + call-bind: "npm:^1.0.0" + get-intrinsic: "npm:^1.0.2" + object-inspect: "npm:^1.9.0" + checksum: 054a5d23ee35054b2c4609b9fd2a0587760737782b5d765a9c7852264710cc39c6dcb56a9bbd6c12cd84071648aea3edb2359d2f6e560677eedadce511ac1da5 + languageName: node + linkType: hard + +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 + languageName: node + linkType: hard + +"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: 25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + languageName: node + linkType: hard + +"simple-swizzle@npm:^0.2.2": + version: 0.2.2 + resolution: "simple-swizzle@npm:0.2.2" + dependencies: + is-arrayish: "npm:^0.3.1" + checksum: df5e4662a8c750bdba69af4e8263c5d96fe4cd0f9fe4bdfa3cbdeb45d2e869dff640beaaeb1ef0e99db4d8d2ec92f85508c269f50c972174851bc1ae5bd64308 + languageName: node + linkType: hard + +"simple-update-notifier@npm:^2.0.0": + version: 2.0.0 + resolution: "simple-update-notifier@npm:2.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 2a00bd03bfbcbf8a737c47ab230d7920f8bfb92d1159d421bdd194479f6d01ebc995d13fbe13d45dace23066a78a3dc6642999b4e3b38b847e6664191575b20c + languageName: node + linkType: hard + +"sisteransi@npm:^1.0.5": + version: 1.0.5 + resolution: "sisteransi@npm:1.0.5" + checksum: 230ac975cca485b7f6fe2b96a711aa62a6a26ead3e6fb8ba17c5a00d61b8bed0d7adc21f5626b70d7c33c62ff4e63933017a6462942c719d1980bb0b1207ad46 + languageName: node + linkType: hard + +"slash@npm:^3.0.0": + version: 3.0.0 + resolution: "slash@npm:3.0.0" + checksum: e18488c6a42bdfd4ac5be85b2ced3ccd0224773baae6ad42cfbb9ec74fc07f9fa8396bd35ee638084ead7a2a0818eb5e7151111544d4731ce843019dab4be47b + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + languageName: node + linkType: hard + +"snapdragon-node@npm:^2.0.1": + version: 2.1.1 + resolution: "snapdragon-node@npm:2.1.1" + dependencies: + define-property: "npm:^1.0.0" + isobject: "npm:^3.0.0" + snapdragon-util: "npm:^3.0.1" + checksum: 7616e6a1ca054afe3ad8defda17ebe4c73b0800d2e0efd635c44ee1b286f8ac7900517314b5330862ce99b28cd2782348ee78bae573ff0f55832ad81d9657f3f + languageName: node + linkType: hard + +"snapdragon-util@npm:^3.0.1": + version: 3.0.1 + resolution: "snapdragon-util@npm:3.0.1" + dependencies: + kind-of: "npm:^3.2.0" + checksum: 4441856d343399ba7f37f79681949d51b922e290fcc07e7bc94655a50f584befa4fb08f40c3471cd160e004660161964d8ff140cba49baa59aa6caba774240e3 + languageName: node + linkType: hard + +"snapdragon@npm:^0.8.1": + version: 0.8.2 + resolution: "snapdragon@npm:0.8.2" + dependencies: + base: "npm:^0.11.1" + debug: "npm:^2.2.0" + define-property: "npm:^0.2.5" + extend-shallow: "npm:^2.0.1" + map-cache: "npm:^0.2.2" + source-map: "npm:^0.5.6" + source-map-resolve: "npm:^0.5.0" + use: "npm:^3.1.0" + checksum: dfdac1f73d47152d72fc07f4322da09bbddfa31c1c9c3ae7346f252f778c45afa5b03e90813332f02f04f6de8003b34a168c456f8bb719024d092f932520ffca + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.1": + version: 8.0.2 + resolution: "socks-proxy-agent@npm:8.0.2" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:^4.3.4" + socks: "npm:^2.7.1" + checksum: a842402fc9b8848a31367f2811ca3cd14c4106588b39a0901cd7a69029998adfc6456b0203617c18ed090542ad0c24ee4e9d4c75a0c4b75071e214227c177eb7 + languageName: node + linkType: hard + +"socks@npm:^2.7.1": + version: 2.7.1 + resolution: "socks@npm:2.7.1" + dependencies: + ip: "npm:^2.0.0" + smart-buffer: "npm:^4.2.0" + checksum: 43f69dbc9f34fc8220bc51c6eea1c39715ab3cfdb115d6e3285f6c7d1a603c5c75655668a5bbc11e3c7e2c99d60321fb8d7ab6f38cda6a215fadd0d6d0b52130 + languageName: node + linkType: hard + +"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.2": + version: 1.0.2 + resolution: "source-map-js@npm:1.0.2" + checksum: 32f2dfd1e9b7168f9a9715eb1b4e21905850f3b50cf02cf476e47e4eebe8e6b762b63a64357896aa29b37e24922b4282df0f492e0d2ace572b43d15525976ff8 + languageName: node + linkType: hard + +"source-map-resolve@npm:^0.5.0": + version: 0.5.3 + resolution: "source-map-resolve@npm:0.5.3" + dependencies: + atob: "npm:^2.1.2" + decode-uri-component: "npm:^0.2.0" + resolve-url: "npm:^0.2.1" + source-map-url: "npm:^0.4.0" + urix: "npm:^0.1.0" + checksum: 410acbe93882e058858d4c1297be61da3e1533f95f25b95903edddc1fb719654e705663644677542d1fb78a66390238fad1a57115fc958a0724cf9bb509caf57 + languageName: node + linkType: hard + +"source-map-resolve@npm:^0.6.0": + version: 0.6.0 + resolution: "source-map-resolve@npm:0.6.0" + dependencies: + atob: "npm:^2.1.2" + decode-uri-component: "npm:^0.2.0" + checksum: bc2a94af3d2417196195eecf0130925b3558726726504a7c7bd1b9e383c4a789fa3f4616c4c673cf8bd7930ddd2e80481f203422282aeae342dbd56b91995188 + languageName: node + linkType: hard + +"source-map-support@npm:^0.4.15": + version: 0.4.18 + resolution: "source-map-support@npm:0.4.18" + dependencies: + source-map: "npm:^0.5.6" + checksum: cd9f0309c1632b1e01a7715a009e0b036d565f3af8930fa8cda2a06aeec05ad1d86180e743b7e1f02cc3c97abe8b6d8de7c3878c2d8e01e86e17f876f7ecf98e + languageName: node + linkType: hard + +"source-map-support@npm:^0.5.16, source-map-support@npm:^0.5.21": + version: 0.5.21 + resolution: "source-map-support@npm:0.5.21" + dependencies: + buffer-from: "npm:^1.0.0" + source-map: "npm:^0.6.0" + checksum: 9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d + languageName: node + linkType: hard + +"source-map-url@npm:^0.4.0": + version: 0.4.1 + resolution: "source-map-url@npm:0.4.1" + checksum: f8af0678500d536c7f643e32094d6718a4070ab4ca2d2326532512cfbe2d5d25a45849b4b385879326f2d7523bb3b686d0360dd347a3cda09fd89a5c28d4bc58 + languageName: node + linkType: hard + +"source-map@npm:^0.5.1, source-map@npm:^0.5.6": + version: 0.5.7 + resolution: "source-map@npm:0.5.7" + checksum: 904e767bb9c494929be013017380cbba013637da1b28e5943b566031e29df04fba57edf3f093e0914be094648b577372bd8ad247fa98cfba9c600794cd16b599 + languageName: node + linkType: hard + +"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.0, source-map@npm:~0.6.1": + version: 0.6.1 + resolution: "source-map@npm:0.6.1" + checksum: ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + languageName: node + linkType: hard + +"space-separated-tokens@npm:^1.0.0": + version: 1.1.5 + resolution: "space-separated-tokens@npm:1.1.5" + checksum: 3ee0a6905f89e1ffdfe474124b1ade9fe97276a377a0b01350bc079b6ec566eb5b219e26064cc5b7f3899c05bde51ffbc9154290b96eaf82916a1e2c2c13ead9 + languageName: node + linkType: hard + +"sparkles@npm:^1.0.0": + version: 1.0.1 + resolution: "sparkles@npm:1.0.1" + checksum: 2327c06d259f1cf3c56e627f22119f89230479fb1007711c971cb6d9c4ed53850a8533d0d7bfca94e120340ba610bd255f0976779717413c6fc147cc0fc1ae6e + languageName: node + linkType: hard + +"spawn-command@npm:0.0.2": + version: 0.0.2 + resolution: "spawn-command@npm:0.0.2" + checksum: b22f2d71239e6e628a400831861ba747750bbb40c0a53323754cf7b84330b73d81e40ff1f9055e6d1971818679510208a9302e13d9ff3b32feb67e74d7a1b3ef + languageName: node + linkType: hard + +"spdx-correct@npm:^3.0.0": + version: 3.2.0 + resolution: "spdx-correct@npm:3.2.0" + dependencies: + spdx-expression-parse: "npm:^3.0.0" + spdx-license-ids: "npm:^3.0.0" + checksum: 49208f008618b9119208b0dadc9208a3a55053f4fd6a0ae8116861bd22696fc50f4142a35ebfdb389e05ccf2de8ad142573fefc9e26f670522d899f7b2fe7386 + languageName: node + linkType: hard + +"spdx-exceptions@npm:^2.1.0": + version: 2.3.0 + resolution: "spdx-exceptions@npm:2.3.0" + checksum: 83089e77d2a91cb6805a5c910a2bedb9e50799da091f532c2ba4150efdef6e53f121523d3e2dc2573a340dc0189e648b03157097f65465b3a0c06da1f18d7e8a + languageName: node + linkType: hard + +"spdx-expression-parse@npm:^3.0.0": + version: 3.0.1 + resolution: "spdx-expression-parse@npm:3.0.1" + dependencies: + spdx-exceptions: "npm:^2.1.0" + spdx-license-ids: "npm:^3.0.0" + checksum: 6f8a41c87759fa184a58713b86c6a8b028250f158159f1d03ed9d1b6ee4d9eefdc74181c8ddc581a341aa971c3e7b79e30b59c23b05d2436d5de1c30bdef7171 + languageName: node + linkType: hard + +"spdx-license-ids@npm:^3.0.0": + version: 3.0.16 + resolution: "spdx-license-ids@npm:3.0.16" + checksum: 7d88b8f01308948bb3ea69c066448f2776cf3d35a410d19afb836743086ced1566f6824ee8e6d67f8f25aa81fa86d8076a666c60ac4528caecd55e93edb5114e + languageName: node + linkType: hard + +"split-string@npm:^3.0.1, split-string@npm:^3.0.2": + version: 3.1.0 + resolution: "split-string@npm:3.1.0" + dependencies: + extend-shallow: "npm:^3.0.0" + checksum: 72d7cd625445c7af215130e1e2bc183013bb9dd48a074eda1d35741e2b0dcb355e6df5b5558a62543a24dcec37dd1d6eb7a6228ff510d3c9de0f3dc1d1da8a70 + languageName: node + linkType: hard + +"sprintf-js@npm:~1.0.2": + version: 1.0.3 + resolution: "sprintf-js@npm:1.0.3" + checksum: ecadcfe4c771890140da5023d43e190b7566d9cf8b2d238600f31bec0fc653f328da4450eb04bd59a431771a8e9cc0e118f0aa3974b683a4981b4e07abc2a5bb + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.5 + resolution: "ssri@npm:10.0.5" + dependencies: + minipass: "npm:^7.0.3" + checksum: b091f2ae92474183c7ac5ed3f9811457e1df23df7a7e70c9476eaa9a0c4a0c8fc190fb45acefbf023ca9ee864dd6754237a697dc52a0fb182afe65d8e77443d8 + languageName: node + linkType: hard + +"stable@npm:^0.1.8": + version: 0.1.8 + resolution: "stable@npm:0.1.8" + checksum: df74b5883075076e78f8e365e4068ecd977af6c09da510cfc3148a303d4b87bc9aa8f7c48feb67ed4ef970b6140bd9eabba2129e28024aa88df5ea0114cba39d + languageName: node + linkType: hard + +"stack-trace@npm:0.0.10, stack-trace@npm:0.0.x": + version: 0.0.10 + resolution: "stack-trace@npm:0.0.10" + checksum: 9ff3dabfad4049b635a85456f927a075c9d0c210e3ea336412d18220b2a86cbb9b13ec46d6c37b70a302a4ea4d49e30e5d4944dd60ae784073f1cde778ac8f4b + languageName: node + linkType: hard + +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 + languageName: node + linkType: hard + +"static-extend@npm:^0.1.1": + version: 0.1.2 + resolution: "static-extend@npm:0.1.2" + dependencies: + define-property: "npm:^0.2.5" + object-copy: "npm:^0.1.0" + checksum: 284f5865a9e19d079f1badbcd70d5f9f82e7a08393f818a220839cd5f71729e89105e1c95322bd28e833161d484cee671380ca443869ae89578eef2bf55c0653 + languageName: node + linkType: hard + +"statuses@npm:2.0.1": + version: 2.0.1 + resolution: "statuses@npm:2.0.1" + checksum: 34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0 + languageName: node + linkType: hard + +"std-env@npm:^3.5.0": + version: 3.6.0 + resolution: "std-env@npm:3.6.0" + checksum: a540b8cb011bef4bf5905e1e28f24ce37124f9d001c69224ee0025d3600144e6847bac62cd38fbd98148ab4d26ab0682b9b4d42bc863cd1cca0b9807f18aadba + languageName: node + linkType: hard + +"stop-iteration-iterator@npm:^1.0.0": + version: 1.0.0 + resolution: "stop-iteration-iterator@npm:1.0.0" + dependencies: + internal-slot: "npm:^1.0.4" + checksum: c4158d6188aac510d9e92925b58709207bd94699e9c31186a040c80932a687f84a51356b5895e6dc72710aad83addb9411c22171832c9ae0e6e11b7d61b0dfb9 + languageName: node + linkType: hard + +"store2@npm:^2.14.2": + version: 2.14.2 + resolution: "store2@npm:2.14.2" + checksum: 2f27c3eaa7207b81410e170e7c41379816d22c1566308a9d97fbf853c4facff531fcb2a85f085c7503c578736570972f747c26018ebeaba7d1341fb82a7b6d52 + languageName: node + linkType: hard + +"storybook@npm:^7.6.17": + version: 7.6.17 + resolution: "storybook@npm:7.6.17" + dependencies: + "@storybook/cli": "npm:7.6.17" + bin: + sb: ./index.js + storybook: ./index.js + checksum: 256b8ff26b69f622889488605e786c0742350a901037139dd469ec20f2e7031c326d65f2a202a5ee7baa407ff407a6746af2f01d91c0c617eda2013679a65271 + languageName: node + linkType: hard + +"stream-browserify@npm:^2.0.1": + version: 2.0.2 + resolution: "stream-browserify@npm:2.0.2" + dependencies: + inherits: "npm:~2.0.1" + readable-stream: "npm:^2.0.2" + checksum: 485562bd5d962d633ae178449029c6fa2611052e356bdb5668f768544aa4daa94c4f9a97de718f3f30ad98f3cb98a5f396252bb3855aff153c138f79c0e8f6ac + languageName: node + linkType: hard + +"stream-exhaust@npm:^1.0.1": + version: 1.0.2 + resolution: "stream-exhaust@npm:1.0.2" + checksum: e8b84a9496ba8ecfde7549e682803a98c8dc983b60cb27726797c9c2627d0b4b2cb95d7016f015465e97ea77e9e41fccce2105ecf2c87451597e3a34405a72b3 + languageName: node + linkType: hard + +"stream-http@npm:^2.7.2": + version: 2.8.3 + resolution: "stream-http@npm:2.8.3" + dependencies: + builtin-status-codes: "npm:^3.0.0" + inherits: "npm:^2.0.1" + readable-stream: "npm:^2.3.6" + to-arraybuffer: "npm:^1.0.0" + xtend: "npm:^4.0.0" + checksum: fbe7d327a29216bbabe88d3819bb8f7a502f11eeacf3212579e5af1f76fa7283f6ffa66134ab7d80928070051f571d1029e85f65ce3369fffd4c4df3669446c4 + languageName: node + linkType: hard + +"stream-shift@npm:^1.0.0": + version: 1.0.1 + resolution: "stream-shift@npm:1.0.1" + checksum: b63a0d178cde34b920ad93e2c0c9395b840f408d36803b07c61416edac80ef9e480a51910e0ceea0d679cec90921bcd2cccab020d3a9fa6c73a98b0fbec132fd + languageName: node + linkType: hard + +"stream-to-array@npm:^2.3.0": + version: 2.3.0 + resolution: "stream-to-array@npm:2.3.0" + dependencies: + any-promise: "npm:^1.1.0" + checksum: 19d66e4e3c12e0aadd8755027edf7d90b696bd978eec5111a5cd2b67befa8851afd8c1b618121c3059850165c4ee4afc307f84869cf6db7fb24708d3523958f8 + languageName: node + linkType: hard + +"string-hash@npm:^1.1.1": + version: 1.1.3 + resolution: "string-hash@npm:1.1.3" + checksum: 179725d7706b49fbbc0a4901703a2d8abec244140879afd5a17908497e586a6b07d738f6775450aefd9f8dd729e4a0abd073fbc6fa3bd020b7a1d2369614af88 + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: 1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + languageName: node + linkType: hard + +"string-width@npm:^1.0.1, string-width@npm:^1.0.2": + version: 1.0.2 + resolution: "string-width@npm:1.0.2" + dependencies: + code-point-at: "npm:^1.0.0" + is-fullwidth-code-point: "npm:^1.0.0" + strip-ansi: "npm:^3.0.0" + checksum: c558438baed23a9ab9370bb6a939acbdb2b2ffc517838d651aad0f5b2b674fb85d460d9b1d0b6a4c210dffd09e3235222d89a5bd4c0c1587f78b2bb7bc00c65e + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca + languageName: node + linkType: hard + +"string.prototype.codepointat@npm:^0.2.1": + version: 0.2.1 + resolution: "string.prototype.codepointat@npm:0.2.1" + checksum: 83c4d2f83b6f3f8f377e0b36628b74a9efcaf5a725e6fb6354f15f30f0399c8f4b08956df883877b2b0f50dd2e644ed7e8b1f6d45bdee2dc5b3f4248796607fa + languageName: node + linkType: hard + +"string.prototype.padend@npm:^3.0.0": + version: 3.1.5 + resolution: "string.prototype.padend@npm:3.1.5" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + checksum: 94ba0d7a463c225d0337ebe4f5c150577d6d09fe56c798f77cd2b11f8d7c9b7b05e65b3c2a273f03529a3f155edb2d78b9c06b7a91f964f89796010a6cbc1dfa + languageName: node + linkType: hard + +"string.prototype.trim@npm:^1.2.8": + version: 1.2.8 + resolution: "string.prototype.trim@npm:1.2.8" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + checksum: 4f76c583908bcde9a71208ddff38f67f24c9ec8093631601666a0df8b52fad44dad2368c78895ce83eb2ae8e7068294cc96a02fc971ab234e4d5c9bb61ea4e34 + languageName: node + linkType: hard + +"string.prototype.trimend@npm:^1.0.7": + version: 1.0.7 + resolution: "string.prototype.trimend@npm:1.0.7" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + checksum: 53c24911c7c4d8d65f5ef5322de23a3d5b6b4db73273e05871d5ab4571ae5638f38f7f19d71d09116578fb060e5a145cc6a208af2d248c8baf7a34f44d32ce57 + languageName: node + linkType: hard + +"string.prototype.trimstart@npm:^1.0.7": + version: 1.0.7 + resolution: "string.prototype.trimstart@npm:1.0.7" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + checksum: 0bcf391b41ea16d4fda9c9953d0a7075171fe090d33b4cf64849af94944c50862995672ac03e0c5dba2940a213ad7f53515a668dac859ce22a0276289ae5cf4f + languageName: node + linkType: hard + +"string_decoder@npm:^1.0.0, string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: "npm:~5.2.0" + checksum: 810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d + languageName: node + linkType: hard + +"string_decoder@npm:~1.1.1": + version: 1.1.1 + resolution: "string_decoder@npm:1.1.1" + dependencies: + safe-buffer: "npm:~5.1.0" + checksum: b4f89f3a92fd101b5653ca3c99550e07bdf9e13b35037e9e2a1c7b47cec4e55e06ff3fc468e314a0b5e80bfbaf65c1ca5a84978764884ae9413bec1fc6ca924e + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: 1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + languageName: node + linkType: hard + +"strip-ansi@npm:^3.0.0, strip-ansi@npm:^3.0.1": + version: 3.0.1 + resolution: "strip-ansi@npm:3.0.1" + dependencies: + ansi-regex: "npm:^2.0.0" + checksum: f6e7fbe8e700105dccf7102eae20e4f03477537c74b286fd22cfc970f139002ed6f0d9c10d0e21aa9ed9245e0fa3c9275930e8795c5b947da136e4ecb644a70f + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 + languageName: node + linkType: hard + +"strip-bom-string@npm:^1.0.0": + version: 1.0.0 + resolution: "strip-bom-string@npm:1.0.0" + checksum: 5c5717e2643225aa6a6d659d34176ab2657037f1fe2423ac6fcdb488f135e14fef1022030e426d8b4d0989e09adbd5c3288d5d3b9c632abeefd2358dfc512bca + languageName: node + linkType: hard + +"strip-bom@npm:^2.0.0": + version: 2.0.0 + resolution: "strip-bom@npm:2.0.0" + dependencies: + is-utf8: "npm:^0.2.0" + checksum: 4fcbb248af1d5c1f2d710022b7d60245077e7942079bfb7ef3fc8c1ae78d61e96278525ba46719b15ab12fced5c7603777105bc898695339d7c97c64d300ed0b + languageName: node + linkType: hard + +"strip-bom@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-bom@npm:3.0.0" + checksum: 51201f50e021ef16672593d7434ca239441b7b760e905d9f33df6e4f3954ff54ec0e0a06f100d028af0982d6f25c35cd5cda2ce34eaebccd0250b8befb90d8f1 + languageName: node + linkType: hard + +"strip-final-newline@npm:^2.0.0": + version: 2.0.0 + resolution: "strip-final-newline@npm:2.0.0" + checksum: bddf8ccd47acd85c0e09ad7375409d81653f645fda13227a9d459642277c253d877b68f2e5e4d819fe75733b0e626bac7e954c04f3236f6d196f79c94fa4a96f + languageName: node + linkType: hard + +"strip-final-newline@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-final-newline@npm:3.0.0" + checksum: a771a17901427bac6293fd416db7577e2bc1c34a19d38351e9d5478c3c415f523f391003b42ed475f27e33a78233035df183525395f731d3bfb8cdcbd4da08ce + languageName: node + linkType: hard + +"strip-indent@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-indent@npm:4.0.0" + dependencies: + min-indent: "npm:^1.0.1" + checksum: 6b1fb4e22056867f5c9e7a6f3f45922d9a2436cac758607d58aeaac0d3b16ec40b1c43317de7900f1b8dd7a4107352fa47fb960f2c23566538c51e8585c8870e + languageName: node + linkType: hard + +"strip-json-comments@npm:^3.0.1": + version: 3.1.1 + resolution: "strip-json-comments@npm:3.1.1" + checksum: 9681a6257b925a7fa0f285851c0e613cc934a50661fa7bb41ca9cbbff89686bb4a0ee366e6ecedc4daafd01e83eee0720111ab294366fe7c185e935475ebcecd + languageName: node + linkType: hard + +"strip-literal@npm:^2.0.0": + version: 2.0.0 + resolution: "strip-literal@npm:2.0.0" + dependencies: + js-tokens: "npm:^8.0.2" + checksum: 63a6e4224ac7088ff93fd19fc0f6882705020da2f0767dbbecb929cbf9d49022e72350420f47be635866823608da9b9a5caf34f518004721895b6031199fc3c8 + languageName: node + linkType: hard + +"stubborn-fs@npm:^1.2.5": + version: 1.2.5 + resolution: "stubborn-fs@npm:1.2.5" + checksum: 0676befd9901d4dd4e162700fa0396f11d523998589cd6b61b06d1021db811dc4c1e6713869748c6cfa49d58beb9b6f0dc5b6aca6b075811b949e1602ce1e26f + languageName: node + linkType: hard + +"supports-color@npm:^5.3.0, supports-color@npm:^5.4.0, supports-color@npm:^5.5.0": + version: 5.5.0 + resolution: "supports-color@npm:5.5.0" + dependencies: + has-flag: "npm:^3.0.0" + checksum: 6ae5ff319bfbb021f8a86da8ea1f8db52fac8bd4d499492e30ec17095b58af11f0c55f8577390a749b1c4dde691b6a0315dab78f5f54c9b3d83f8fb5905c1c05 + languageName: node + linkType: hard + +"supports-color@npm:^7.1.0": + version: 7.2.0 + resolution: "supports-color@npm:7.2.0" + dependencies: + has-flag: "npm:^4.0.0" + checksum: afb4c88521b8b136b5f5f95160c98dee7243dc79d5432db7efc27efb219385bbc7d9427398e43dd6cc730a0f87d5085ce1652af7efbe391327bc0a7d0f7fc124 + languageName: node + linkType: hard + +"supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": + version: 8.1.1 + resolution: "supports-color@npm:8.1.1" + dependencies: + has-flag: "npm:^4.0.0" + checksum: ea1d3c275dd604c974670f63943ed9bd83623edc102430c05adb8efc56ba492746b6e95386e7831b872ec3807fd89dd8eb43f735195f37b5ec343e4234cc7e89 + languageName: node + linkType: hard + +"supports-preserve-symlinks-flag@npm:^1.0.0": + version: 1.0.0 + resolution: "supports-preserve-symlinks-flag@npm:1.0.0" + checksum: 6c4032340701a9950865f7ae8ef38578d8d7053f5e10518076e6554a9381fa91bd9c6850193695c141f32b21f979c985db07265a758867bac95de05f7d8aeb39 + languageName: node + linkType: hard + +"sver-compat@npm:^1.5.0": + version: 1.5.0 + resolution: "sver-compat@npm:1.5.0" + dependencies: + es6-iterator: "npm:^2.0.1" + es6-symbol: "npm:^3.1.1" + checksum: 23388477ff4c6ef45c66d3a1347cb392cfcfd2e995ab15b56fd0f4fed1c263730f71816ffb62a433d37aa467939a3b9ccc9542bf2d0ef1f38fdd23cd77842dcb + languageName: node + linkType: hard + +"svg-sprite@npm:^2.0.2": + version: 2.0.2 + resolution: "svg-sprite@npm:2.0.2" + dependencies: + "@resvg/resvg-js": "npm:^2.1.0" + "@xmldom/xmldom": "npm:^0.8.3" + async: "npm:^3.2.4" + css-selector-parser: "npm:^1.4.1" + csso: "npm:^4.2.0" + cssom: "npm:^0.5.0" + glob: "npm:^7.2.3" + js-yaml: "npm:^4.1.0" + lodash.escape: "npm:^4.0.1" + lodash.merge: "npm:^4.6.2" + lodash.trim: "npm:^4.5.1" + lodash.trimstart: "npm:^4.5.1" + mustache: "npm:^4.2.0" + prettysize: "npm:^2.0.0" + svgo: "npm:^2.8.0" + vinyl: "npm:^2.2.1" + winston: "npm:^3.8.2" + xpath: "npm:^0.0.32" + yargs: "npm:^17.5.1" + bin: + svg-sprite: bin/svg-sprite.js + checksum: 0dc5cca43eef365bc92f065313b0a6d27dab20a525e573979f0837b1a5ad0559a44330ceacfebc7b15e235e82cf1c16618108f99acdf6ae7654cd2a84d583cd8 + languageName: node + linkType: hard + +"svgo@npm:^2.8.0": + version: 2.8.0 + resolution: "svgo@npm:2.8.0" + dependencies: + "@trysound/sax": "npm:0.2.0" + commander: "npm:^7.2.0" + css-select: "npm:^4.1.3" + css-tree: "npm:^1.1.3" + csso: "npm:^4.2.0" + picocolors: "npm:^1.0.0" + stable: "npm:^0.1.8" + bin: + svgo: bin/svgo + checksum: 0741f5d5cad63111a90a0ce7a1a5a9013f6d293e871b75efe39addb57f29a263e45294e485a4d2ff9cc260a5d142c8b5937b2234b4ef05efdd2706fb2d360ecc + languageName: node + linkType: hard + +"symbol-tree@npm:^3.2.4": + version: 3.2.4 + resolution: "symbol-tree@npm:3.2.4" + checksum: dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509 + languageName: node + linkType: hard + +"synchronous-promise@npm:^2.0.15": + version: 2.0.17 + resolution: "synchronous-promise@npm:2.0.17" + checksum: 1babe643d8417789ef6e5a2f3d4b8abcda2de236acd09bbe2c98f6be82c0a2c92ed21a6e4f934845fa8de18b1435a9cba1e8c3d945032e8a532f076224c024b1 + languageName: node + linkType: hard + +"tar-fs@npm:^2.1.1": + version: 2.1.1 + resolution: "tar-fs@npm:2.1.1" + dependencies: + chownr: "npm:^1.1.1" + mkdirp-classic: "npm:^0.5.2" + pump: "npm:^3.0.0" + tar-stream: "npm:^2.1.4" + checksum: 871d26a934bfb7beeae4c4d8a09689f530b565f79bd0cf489823ff0efa3705da01278160da10bb006d1a793fa0425cf316cec029b32a9159eacbeaff4965fb6d + languageName: node + linkType: hard + +"tar-stream@npm:^2.1.4": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: "npm:^4.0.3" + end-of-stream: "npm:^1.4.1" + fs-constants: "npm:^1.0.0" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + checksum: 2f4c910b3ee7196502e1ff015a7ba321ec6ea837667220d7bcb8d0852d51cb04b87f7ae471008a6fb8f5b1a1b5078f62f3a82d30c706f20ada1238ac797e7692 + languageName: node + linkType: hard + +"tar@npm:^6.1.11, tar@npm:^6.1.2, tar@npm:^6.2.0": + version: 6.2.0 + resolution: "tar@npm:6.2.0" + dependencies: + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: 02ca064a1a6b4521fef88c07d389ac0936730091f8c02d30ea60d472e0378768e870769ab9e986d87807bfee5654359cf29ff4372746cc65e30cbddc352660d8 + languageName: node + linkType: hard + +"tdigest@npm:^0.1.2": + version: 0.1.2 + resolution: "tdigest@npm:0.1.2" + dependencies: + bintrees: "npm:1.0.2" + checksum: 10187b8144b112fcdfd3a5e4e9068efa42c990b1e30cd0d4f35ee8f58f16d1b41bc587e668fa7a6f6ca31308961cbd06cd5d4a4ae1dc388335902ae04f7d57df + languageName: node + linkType: hard + +"telejson@npm:^7.2.0": + version: 7.2.0 + resolution: "telejson@npm:7.2.0" + dependencies: + memoizerific: "npm:^1.11.3" + checksum: d26e6cc93e54bfdcdb207b49905508c5db45862e811a2e2193a735409e47b14530e1c19351618a3e03ad2fd4ffc3759364fcd72851aba2df0300fab574b6151c + languageName: node + linkType: hard + +"temp-dir@npm:^2.0.0": + version: 2.0.0 + resolution: "temp-dir@npm:2.0.0" + checksum: b1df969e3f3f7903f3426861887ed76ba3b495f63f6d0c8e1ce22588679d9384d336df6064210fda14e640ed422e2a17d5c40d901f60e161c99482d723f4d309 + languageName: node + linkType: hard + +"temp@npm:^0.8.4": + version: 0.8.4 + resolution: "temp@npm:0.8.4" + dependencies: + rimraf: "npm:~2.6.2" + checksum: 7f071c963031bfece37e13c5da11e9bb451e4ddfc4653e23e327a2f91594102dc826ef6a693648e09a6e0eb856f507967ec759ae55635e0878091eccf411db37 + languageName: node + linkType: hard + +"tempy@npm:^1.0.1": + version: 1.0.1 + resolution: "tempy@npm:1.0.1" + dependencies: + del: "npm:^6.0.0" + is-stream: "npm:^2.0.0" + temp-dir: "npm:^2.0.0" + type-fest: "npm:^0.16.0" + unique-string: "npm:^2.0.0" + checksum: 864a1cf1b5536dc21e84ae45dbbc3ba4dd2c7ec1674d895f99c349cf209df959a53d797ca38d0b2cf69c7684d565fde5cfc67faaa63b7208ffb21d454b957472 + languageName: node + linkType: hard + +"test-exclude@npm:^6.0.0": + version: 6.0.0 + resolution: "test-exclude@npm:6.0.0" + dependencies: + "@istanbuljs/schema": "npm:^0.1.2" + glob: "npm:^7.1.4" + minimatch: "npm:^3.0.4" + checksum: 019d33d81adff3f9f1bfcff18125fb2d3c65564f437d9be539270ee74b994986abb8260c7c2ce90e8f30162178b09dbbce33c6389273afac4f36069c48521f57 + languageName: node + linkType: hard + +"text-hex@npm:1.0.x": + version: 1.0.0 + resolution: "text-hex@npm:1.0.0" + checksum: 57d8d320d92c79d7c03ffb8339b825bb9637c2cbccf14304309f51d8950015c44464b6fd1b6820a3d4821241c68825634f09f5a2d9d501e84f7c6fd14376860d + languageName: node + linkType: hard + +"through2-filter@npm:^3.0.0": + version: 3.0.0 + resolution: "through2-filter@npm:3.0.0" + dependencies: + through2: "npm:~2.0.0" + xtend: "npm:~4.0.0" + checksum: 741d9144dbbafca3a4a75fc55a0c062641ac464071118cef2213f35f0a961e3331795c802d5bef915060d07cebd29e6c7079e656845145de4db63c74054b4156 + languageName: node + linkType: hard + +"through2@npm:^2.0.0, through2@npm:^2.0.3, through2@npm:~2.0.0": + version: 2.0.5 + resolution: "through2@npm:2.0.5" + dependencies: + readable-stream: "npm:~2.3.6" + xtend: "npm:~4.0.1" + checksum: cbfe5b57943fa12b4f8c043658c2a00476216d79c014895cef1ac7a1d9a8b31f6b438d0e53eecbb81054b93128324a82ecd59ec1a4f91f01f7ac113dcb14eade + languageName: node + linkType: hard + +"through2@npm:^3.0.1": + version: 3.0.2 + resolution: "through2@npm:3.0.2" + dependencies: + inherits: "npm:^2.0.4" + readable-stream: "npm:2 || 3" + checksum: 8ea17efa2ce5b78ef5c52d08e29d0dbdad9c321c2add5192bba3434cae25b2319bf9cdac1c54c3bfbd721438a30565ca6f3f19eb79f62341dafc5a12429d2ccc + languageName: node + linkType: hard + +"time-stamp@npm:^1.0.0": + version: 1.1.0 + resolution: "time-stamp@npm:1.1.0" + checksum: 99340b52a6ab3ce805c30c1884baee06251c54ef37d852979edf2b2b1d649664fc1ced50e0c7df90f8deb3dc28cb310af3e6002c2b63966c68f488e0bac3e5c5 + languageName: node + linkType: hard + +"timers-browserify@npm:^2.0.4": + version: 2.0.12 + resolution: "timers-browserify@npm:2.0.12" + dependencies: + setimmediate: "npm:^1.0.4" + checksum: 98e84db1a685bc8827c117a8bc62aac811ad56a995d07938fc7ed8cdc5bf3777bfe2d4e5da868847194e771aac3749a20f6cdd22091300fe889a76fe214a4641 + languageName: node + linkType: hard + +"timers-ext@npm:^0.1.7": + version: 0.1.7 + resolution: "timers-ext@npm:0.1.7" + dependencies: + es5-ext: "npm:~0.10.46" + next-tick: "npm:1" + checksum: fc43c6a01f52875e57d301ae9ec47b3021c6d9b96de5bc6e4e5fc4a3d2b25ebaab69faf6fe85520efbef0ad784537748f88f7efd7b6b2bf0a177c8bc7a66ca7c + languageName: node + linkType: hard + +"tiny-inflate@npm:^1.0.3": + version: 1.0.3 + resolution: "tiny-inflate@npm:1.0.3" + checksum: fab687537254f6ec44c9a2e880048fe70da3542aba28f73cda3e74c95cabf342a339372f2a6c032e322324f01accc03ca26c04ba2bad9b3eb8cf3ee99bba7f9b + languageName: node + linkType: hard + +"tiny-invariant@npm:^1.3.1": + version: 1.3.1 + resolution: "tiny-invariant@npm:1.3.1" + checksum: 5b87c1d52847d9452b60d0dcb77011b459044e0361ca8253bfe7b43d6288106e12af926adb709a6fc28900e3864349b91dad9a4ac93c39aa15f360b26c2ff4db + languageName: node + linkType: hard + +"tiny-readdir@npm:^2.2.0": + version: 2.4.0 + resolution: "tiny-readdir@npm:2.4.0" + dependencies: + promise-make-naked: "npm:^2.1.1" + checksum: 0fd05eb677a9bf25f6ace33ad2eeaeb8555303321e18cd22c7a96391f099c1dd900d745738a1c6ba276540b1dc117f72fbbf60cc47bf1c7a73840745e3ea42f8 + languageName: node + linkType: hard + +"tinybench@npm:^2.5.1": + version: 2.5.1 + resolution: "tinybench@npm:2.5.1" + checksum: 9c55ef25ce1689c3e2fdb89cacbf27dada4d04f846cac70023fe97fc35d2122816d8bbc5b20253e071d13688cf006355d59f0096d22958b818e1e2fe60e5165b + languageName: node + linkType: hard + +"tinypool@npm:^0.8.2": + version: 0.8.2 + resolution: "tinypool@npm:0.8.2" + checksum: 8998626614172fc37c394e9a14e701dc437727fc6525488a4d4fd42044a4b2b59d6f076d750cbf5c699f79c58dd4e40599ab09e2f1ae0df4b23516b98c9c3055 + languageName: node + linkType: hard + +"tinyspy@npm:^2.2.0": + version: 2.2.0 + resolution: "tinyspy@npm:2.2.0" + checksum: 8c7b70748dd8590e85d52741db79243746c15bc03c92d75c23160a762142db577e7f53e360ba7300e321b12bca5c42dd2522a8dbeec6ba3830302573dd8516bc + languageName: node + linkType: hard + +"tmpl@npm:1.0.5": + version: 1.0.5 + resolution: "tmpl@npm:1.0.5" + checksum: f935537799c2d1922cb5d6d3805f594388f75338fe7a4a9dac41504dd539704ca4db45b883b52e7b0aa5b2fd5ddadb1452bf95cd23a69da2f793a843f9451cc9 + languageName: node + linkType: hard + +"to-absolute-glob@npm:^2.0.0": + version: 2.0.2 + resolution: "to-absolute-glob@npm:2.0.2" + dependencies: + is-absolute: "npm:^1.0.0" + is-negated-glob: "npm:^1.0.0" + checksum: 7c5384222d6bd8f68d105bcc618794dfc3433de74eea195da172f27e107e8b2e1e1991e4adaf837f65e04623e4b03d90e19fd48aaeecfc89b6f642da2510c4d5 + languageName: node + linkType: hard + +"to-arraybuffer@npm:^1.0.0": + version: 1.0.1 + resolution: "to-arraybuffer@npm:1.0.1" + checksum: 2460bd95524f4845a751e4f8bf9937f9f3dcd1651f104e1512868782f858f8302c1cf25bbc30794bc1b3ff65c4e135158377302f2abaff43a2d8e3c38dfe098c + languageName: node + linkType: hard + +"to-fast-properties@npm:^2.0.0": + version: 2.0.0 + resolution: "to-fast-properties@npm:2.0.0" + checksum: b214d21dbfb4bce3452b6244b336806ffea9c05297148d32ebb428d5c43ce7545bdfc65a1ceb58c9ef4376a65c0cb2854d645f33961658b3e3b4f84910ddcdd7 + languageName: node + linkType: hard + +"to-object-path@npm:^0.3.0": + version: 0.3.0 + resolution: "to-object-path@npm:0.3.0" + dependencies: + kind-of: "npm:^3.0.2" + checksum: 731832a977614c03a770363ad2bd9e9c82f233261861724a8e612bb90c705b94b1a290a19f52958e8e179180bb9b71121ed65e245691a421467726f06d1d7fc3 + languageName: node + linkType: hard + +"to-regex-range@npm:^2.1.0": + version: 2.1.1 + resolution: "to-regex-range@npm:2.1.1" + dependencies: + is-number: "npm:^3.0.0" + repeat-string: "npm:^1.6.1" + checksum: 440d82dbfe0b2e24f36dd8a9467240406ad1499fc8b2b0f547372c22ed1d092ace2a3eb522bb09bfd9c2f39bf1ca42eb78035cf6d2b8c9f5c78da3abc96cd949 + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: "npm:^7.0.0" + checksum: 487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892 + languageName: node + linkType: hard + +"to-regex@npm:^3.0.1, to-regex@npm:^3.0.2": + version: 3.0.2 + resolution: "to-regex@npm:3.0.2" + dependencies: + define-property: "npm:^2.0.2" + extend-shallow: "npm:^3.0.2" + regex-not: "npm:^1.0.2" + safe-regex: "npm:^1.1.0" + checksum: 99d0b8ef397b3f7abed4bac757b0f0bb9f52bfd39167eb7105b144becfaa9a03756892352d01ac6a911f0c1ceef9f81db68c46899521a3eed054082042796120 + languageName: node + linkType: hard + +"to-through@npm:^2.0.0": + version: 2.0.0 + resolution: "to-through@npm:2.0.0" + dependencies: + through2: "npm:^2.0.3" + checksum: f8a7b0b38c51bcc018c38e6867588ac72120bd62232250b49a0fc209bd53ed66461ff85dc50b398c8e3686aa3e61165bce1dce4e89930f2f973b0fd3f64e4d2c + languageName: node + linkType: hard + +"tocbot@npm:^4.20.1": + version: 4.23.0 + resolution: "tocbot@npm:4.23.0" + checksum: e112c569913600b98a451fc114ba293c7365e529b6b22c34a4ba244a22a359d79aaafb0c752022b1c9a6e2d04692e436700c0b95d534da9ee31c0f310e96761b + languageName: node + linkType: hard + +"toidentifier@npm:1.0.1": + version: 1.0.1 + resolution: "toidentifier@npm:1.0.1" + checksum: 93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1 + languageName: node + linkType: hard + +"touch@npm:^3.1.0": + version: 3.1.0 + resolution: "touch@npm:3.1.0" + dependencies: + nopt: "npm:~1.0.10" + bin: + nodetouch: ./bin/nodetouch.js + checksum: dacb4a639401b83b0a40b56c0565e01096e5ecf38b22a4840d9eeb642a5bea136c6a119e4543f9b172349a5ee343b10cda0880eb47f7d7ddfd6eac59dcf53244 + languageName: node + linkType: hard + +"tough-cookie@npm:^4.1.3": + version: 4.1.3 + resolution: "tough-cookie@npm:4.1.3" + dependencies: + psl: "npm:^1.1.33" + punycode: "npm:^2.1.1" + universalify: "npm:^0.2.0" + url-parse: "npm:^1.5.3" + checksum: 4fc0433a0cba370d57c4b240f30440c848906dee3180bb6e85033143c2726d322e7e4614abb51d42d111ebec119c4876ed8d7247d4113563033eebbc1739c831 + languageName: node + linkType: hard + +"tr46@npm:^5.0.0": + version: 5.0.0 + resolution: "tr46@npm:5.0.0" + dependencies: + punycode: "npm:^2.3.1" + checksum: 1521b6e7bbc8adc825c4561480f9fe48eb2276c81335eed9fa610aa4c44a48a3221f78b10e5f18b875769eb3413e30efbf209ed556a17a42aa8d690df44b7bee + languageName: node + linkType: hard + +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11 + languageName: node + linkType: hard + +"tree-kill@npm:^1.2.2": + version: 1.2.2 + resolution: "tree-kill@npm:1.2.2" + bin: + tree-kill: cli.js + checksum: 7b1b7c7f17608a8f8d20a162e7957ac1ef6cd1636db1aba92f4e072dc31818c2ff0efac1e3d91064ede67ed5dc57c565420531a8134090a12ac10cf792ab14d2 + languageName: node + linkType: hard + +"triple-beam@npm:^1.3.0": + version: 1.4.1 + resolution: "triple-beam@npm:1.4.1" + checksum: 4bf1db71e14fe3ff1c3adbe3c302f1fdb553b74d7591a37323a7badb32dc8e9c290738996cbb64f8b10dc5a3833645b5d8c26221aaaaa12e50d1251c9aba2fea + languageName: node + linkType: hard + +"ts-dedent@npm:^2.0.0, ts-dedent@npm:^2.2.0": + version: 2.2.0 + resolution: "ts-dedent@npm:2.2.0" + checksum: 175adea838468cc2ff7d5e97f970dcb798bbcb623f29c6088cb21aa2880d207c5784be81ab1741f56b9ac37840cbaba0c0d79f7f8b67ffe61c02634cafa5c303 + languageName: node + linkType: hard + +"tslib@npm:^1.13.0": + version: 1.14.1 + resolution: "tslib@npm:1.14.1" + checksum: 69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 + languageName: node + linkType: hard + +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0": + version: 2.6.2 + resolution: "tslib@npm:2.6.2" + checksum: e03a8a4271152c8b26604ed45535954c0a45296e32445b4b87f8a5abdb2421f40b59b4ca437c4346af0f28179780d604094eb64546bee2019d903d01c6c19bdb + languageName: node + linkType: hard + +"tty-browserify@npm:0.0.0": + version: 0.0.0 + resolution: "tty-browserify@npm:0.0.0" + checksum: c0c68206565f1372e924d5cdeeff1a0d9cc729833f1da98c03d78be8f939e5f61a107bd0ab77d1ef6a47d62bb0e48b1081fbea273acf404959e22fd3891439c5 + languageName: node + linkType: hard + +"tween-functions@npm:^1.2.0": + version: 1.2.0 + resolution: "tween-functions@npm:1.2.0" + checksum: 7e59295b8b0ee4132ed2fe335f56a9db5c87056dad6b6fd3011be72239fd20398003ddb4403bc98ad9f5c94468890830f64016edbbde35581faf95b32cda8305 + languageName: node + linkType: hard + +"type-detect@npm:^4.0.0, type-detect@npm:^4.0.8": + version: 4.0.8 + resolution: "type-detect@npm:4.0.8" + checksum: 8fb9a51d3f365a7de84ab7f73b653534b61b622aa6800aecdb0f1095a4a646d3f5eb295322127b6573db7982afcd40ab492d038cf825a42093a58b1e1353e0bd + languageName: node + linkType: hard + +"type-fest@npm:^0.16.0": + version: 0.16.0 + resolution: "type-fest@npm:0.16.0" + checksum: 6b4d846534e7bcb49a6160b068ffaed2b62570d989d909ac3f29df5ef1e993859f890a4242eebe023c9e923f96adbcb3b3e88a198c35a1ee9a731e147a6839c3 + languageName: node + linkType: hard + +"type-fest@npm:^0.6.0": + version: 0.6.0 + resolution: "type-fest@npm:0.6.0" + checksum: 0c585c26416fce9ecb5691873a1301b5aff54673c7999b6f925691ed01f5b9232db408cdbb0bd003d19f5ae284322523f44092d1f81ca0a48f11f7cf0be8cd38 + languageName: node + linkType: hard + +"type-fest@npm:^0.8.1": + version: 0.8.1 + resolution: "type-fest@npm:0.8.1" + checksum: dffbb99329da2aa840f506d376c863bd55f5636f4741ad6e65e82f5ce47e6914108f44f340a0b74009b0cb5d09d6752ae83203e53e98b1192cf80ecee5651636 + languageName: node + linkType: hard + +"type-fest@npm:^2.19.0, type-fest@npm:~2.19": + version: 2.19.0 + resolution: "type-fest@npm:2.19.0" + checksum: a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb + languageName: node + linkType: hard + +"type-is@npm:~1.6.18": + version: 1.6.18 + resolution: "type-is@npm:1.6.18" + dependencies: + media-typer: "npm:0.3.0" + mime-types: "npm:~2.1.24" + checksum: a23daeb538591b7efbd61ecf06b6feb2501b683ffdc9a19c74ef5baba362b4347e42f1b4ed81f5882a8c96a3bfff7f93ce3ffaf0cbbc879b532b04c97a55db9d + languageName: node + linkType: hard + +"type@npm:^1.0.1": + version: 1.2.0 + resolution: "type@npm:1.2.0" + checksum: 444660849aaebef8cbb9bc43b28ec2068952064cfce6a646f88db97aaa2e2d6570c5629cd79238b71ba23aa3f75146a0b96e24e198210ee0089715a6f8889bf7 + languageName: node + linkType: hard + +"type@npm:^2.7.2": + version: 2.7.2 + resolution: "type@npm:2.7.2" + checksum: 84c2382788fe24e0bc3d64c0c181820048f672b0f06316aa9c7bdb373f8a09f8b5404f4e856bc4539fb931f2f08f2adc4c53f6c08c9c0314505d70c29a1289e1 + languageName: node + linkType: hard + +"typed-array-buffer@npm:^1.0.0": + version: 1.0.0 + resolution: "typed-array-buffer@npm:1.0.0" + dependencies: + call-bind: "npm:^1.0.2" + get-intrinsic: "npm:^1.2.1" + is-typed-array: "npm:^1.1.10" + checksum: ebad66cdf00c96b1395dffc7873169cf09801fca5954507a484f41f253feb1388d815db297b0b3bb8ce7421eac6f7ff45e2ec68450a3d68408aa4ae02fcf3a6c + languageName: node + linkType: hard + +"typed-array-byte-length@npm:^1.0.0": + version: 1.0.0 + resolution: "typed-array-byte-length@npm:1.0.0" + dependencies: + call-bind: "npm:^1.0.2" + for-each: "npm:^0.3.3" + has-proto: "npm:^1.0.1" + is-typed-array: "npm:^1.1.10" + checksum: 6696435d53ce0e704ff6760c57ccc35138aec5f87859e03eb2a3246336d546feae367952dbc918116f3f0dffbe669734e3cbd8960283c2fa79aac925db50d888 + languageName: node + linkType: hard + +"typed-array-byte-offset@npm:^1.0.0": + version: 1.0.0 + resolution: "typed-array-byte-offset@npm:1.0.0" + dependencies: + available-typed-arrays: "npm:^1.0.5" + call-bind: "npm:^1.0.2" + for-each: "npm:^0.3.3" + has-proto: "npm:^1.0.1" + is-typed-array: "npm:^1.1.10" + checksum: 4036ce007ae9752931bed3dd61e0d6de2a3e5f6a5a85a05f3adb35388d2c0728f9b1a1e638d75579f168e49c289bfb5417f00e96d4ab081f38b647fc854ff7a5 + languageName: node + linkType: hard + +"typed-array-length@npm:^1.0.4": + version: 1.0.4 + resolution: "typed-array-length@npm:1.0.4" + dependencies: + call-bind: "npm:^1.0.2" + for-each: "npm:^0.3.3" + is-typed-array: "npm:^1.1.9" + checksum: c5163c0103d07fefc8a2ad0fc151f9ca9a1f6422098c00f695d55f9896e4d63614cd62cf8d8a031c6cee5f418e8980a533796597174da4edff075b3d275a7e23 + languageName: node + linkType: hard + +"typedarray@npm:^0.0.6": + version: 0.0.6 + resolution: "typedarray@npm:0.0.6" + checksum: 6005cb31df50eef8b1f3c780eb71a17925f3038a100d82f9406ac2ad1de5eb59f8e6decbdc145b3a1f8e5836e17b0c0002fb698b9fe2516b8f9f9ff602d36412 + languageName: node + linkType: hard + +"typescript@npm:^5.3.3": + version: 5.3.3 + resolution: "typescript@npm:5.3.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: e33cef99d82573624fc0f854a2980322714986bc35b9cb4d1ce736ed182aeab78e2cb32b385efa493b2a976ef52c53e20d6c6918312353a91850e2b76f1ea44f + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.3.3#optional!builtin": + version: 5.3.3 + resolution: "typescript@patch:typescript@npm%3A5.3.3#optional!builtin::version=5.3.3&hash=e012d7" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 1d0a5f4ce496c42caa9a30e659c467c5686eae15d54b027ee7866744952547f1be1262f2d40de911618c242b510029d51d43ff605dba8fb740ec85ca2d3f9500 + languageName: node + linkType: hard + +"ua-parser-js@npm:^1.0.35, ua-parser-js@npm:^1.0.37": + version: 1.0.37 + resolution: "ua-parser-js@npm:1.0.37" + checksum: dac8cf82a55b2e097bd2286954e01454c4cfcf23c9d9b56961ce94bda3cec5a38ca536e6e84c20a4000a9d4b4a4abcbd98ec634ccebe21be36595ea3069126e4 + languageName: node + linkType: hard + +"ufo@npm:^1.3.0": + version: 1.3.2 + resolution: "ufo@npm:1.3.2" + checksum: 180f3dfcdf319b54fe0272780841c93cb08a024fc2ee5f95e63285c2a3c42d8b671cd3641e9a53aafccf100cf8466aa8c040ddfa0efea1fc1968c9bfb250a661 + languageName: node + linkType: hard + +"uglify-js@npm:^3.1.4": + version: 3.17.4 + resolution: "uglify-js@npm:3.17.4" + bin: + uglifyjs: bin/uglifyjs + checksum: 8b7fcdca69deb284fed7d2025b73eb747ce37f9aca6af53422844f46427152d5440601b6e2a033e77856a2f0591e4167153d5a21b68674ad11f662034ec13ced + languageName: node + linkType: hard + +"unbox-primitive@npm:^1.0.2": + version: 1.0.2 + resolution: "unbox-primitive@npm:1.0.2" + dependencies: + call-bind: "npm:^1.0.2" + has-bigints: "npm:^1.0.2" + has-symbols: "npm:^1.0.3" + which-boxed-primitive: "npm:^1.0.2" + checksum: 81ca2e81134167cc8f75fa79fbcc8a94379d6c61de67090986a2273850989dd3bae8440c163121b77434b68263e34787a675cbdcb34bb2f764c6b9c843a11b66 + languageName: node + linkType: hard + +"unc-path-regex@npm:^0.1.2": + version: 0.1.2 + resolution: "unc-path-regex@npm:0.1.2" + checksum: bf9c781c4e2f38e6613ea17a51072e4b416840fbe6eeb244597ce9b028fac2fb6cfd3dde1f14111b02c245e665dc461aab8168ecc30b14364d02caa37f812996 + languageName: node + linkType: hard + +"undefsafe@npm:^2.0.5": + version: 2.0.5 + resolution: "undefsafe@npm:2.0.5" + checksum: 96c0466a5fbf395917974a921d5d4eee67bca4b30d3a31ce7e621e0228c479cf893e783a109af6e14329b52fe2f0cb4108665fad2b87b0018c0df6ac771261d5 + languageName: node + linkType: hard + +"undertaker-registry@npm:^1.0.0": + version: 1.0.1 + resolution: "undertaker-registry@npm:1.0.1" + checksum: 55b60fac04e7fda61d544c33c3d71e9d20aaa91026ca4833041fcd4c2de890f1a798320a8eb3dc3e9a0af68dd2fc7b26087ed6a48fd1163ac1dfacd3936a11fe + languageName: node + linkType: hard + +"undertaker@npm:^1.2.1": + version: 1.3.0 + resolution: "undertaker@npm:1.3.0" + dependencies: + arr-flatten: "npm:^1.0.1" + arr-map: "npm:^2.0.0" + bach: "npm:^1.0.0" + collection-map: "npm:^1.0.0" + es6-weak-map: "npm:^2.0.1" + fast-levenshtein: "npm:^1.0.0" + last-run: "npm:^1.1.0" + object.defaults: "npm:^1.0.0" + object.reduce: "npm:^1.0.0" + undertaker-registry: "npm:^1.0.0" + checksum: 3442616fca45767e667de467a690803751d57f952807643e29cf017d8bfdc5be2bbd13c888a0f0c0f64bf1583417ee13736605b899d482e0fec8dbe43dfa9ce8 + languageName: node + linkType: hard + +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501 + languageName: node + linkType: hard + +"unicode-canonical-property-names-ecmascript@npm:^2.0.0": + version: 2.0.0 + resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0" + checksum: 0fe812641bcfa3ae433025178a64afb5d9afebc21a922dafa7cba971deebb5e4a37350423890750132a85c936c290fb988146d0b1bd86838ad4897f4fc5bd0de + languageName: node + linkType: hard + +"unicode-match-property-ecmascript@npm:^2.0.0": + version: 2.0.0 + resolution: "unicode-match-property-ecmascript@npm:2.0.0" + dependencies: + unicode-canonical-property-names-ecmascript: "npm:^2.0.0" + unicode-property-aliases-ecmascript: "npm:^2.0.0" + checksum: 4d05252cecaf5c8e36d78dc5332e03b334c6242faf7cf16b3658525441386c0a03b5f603d42cbec0f09bb63b9fd25c9b3b09667aee75463cac3efadae2cd17ec + languageName: node + linkType: hard + +"unicode-match-property-value-ecmascript@npm:^2.1.0": + version: 2.1.0 + resolution: "unicode-match-property-value-ecmascript@npm:2.1.0" + checksum: f5b9499b9e0ffdc6027b744d528f17ec27dd7c15da03254ed06851feec47e0531f20d410910c8a49af4a6a190f4978413794c8d75ce112950b56d583b5d5c7f2 + languageName: node + linkType: hard + +"unicode-property-aliases-ecmascript@npm:^2.0.0": + version: 2.1.0 + resolution: "unicode-property-aliases-ecmascript@npm:2.1.0" + checksum: 50ded3f8c963c7785e48c510a3b7c6bc4e08a579551489aa0349680a35b1ceceec122e33b2b6c1b579d0be2250f34bb163ac35f5f8695fe10bbc67fb757f0af8 + languageName: node + linkType: hard + +"union-value@npm:^1.0.0": + version: 1.0.1 + resolution: "union-value@npm:1.0.1" + dependencies: + arr-union: "npm:^3.1.0" + get-value: "npm:^2.0.6" + is-extendable: "npm:^0.1.1" + set-value: "npm:^2.0.1" + checksum: 8758d880cb9545f62ce9cfb9b791b2b7a206e0ff5cc4b9d7cd6581da2c6839837fbb45e639cf1fd8eef3cae08c0201b614b7c06dd9f5f70d9dbe7c5fe2fbf592 + languageName: node + linkType: hard + +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: "npm:^4.0.0" + checksum: 6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 + languageName: node + linkType: hard + +"unique-stream@npm:^2.0.2": + version: 2.3.1 + resolution: "unique-stream@npm:2.3.1" + dependencies: + json-stable-stringify-without-jsonify: "npm:^1.0.1" + through2-filter: "npm:^3.0.0" + checksum: 4827c5f249d1d760076d64e087d18618104ce5511112c85150b0dd76cea5ddd5a5fd143559597d07b519c2a19abd83f5cdaac3a30204d66cff63e986dd4cd18c + languageName: node + linkType: hard + +"unique-string@npm:^2.0.0": + version: 2.0.0 + resolution: "unique-string@npm:2.0.0" + dependencies: + crypto-random-string: "npm:^2.0.0" + checksum: 11820db0a4ba069d174bedfa96c588fc2c96b083066fafa186851e563951d0de78181ac79c744c1ed28b51f9d82ac5b8196ff3e4560d0178046ef455d8c2244b + languageName: node + linkType: hard + +"unist-util-is@npm:^4.0.0": + version: 4.1.0 + resolution: "unist-util-is@npm:4.1.0" + checksum: 21ca3d7bacc88853b880b19cb1b133a056c501617d7f9b8cce969cd8b430ed7e1bc416a3a11b02540d5de6fb86807e169d00596108a459d034cf5faec97c055e + languageName: node + linkType: hard + +"unist-util-visit-parents@npm:^3.0.0": + version: 3.1.1 + resolution: "unist-util-visit-parents@npm:3.1.1" + dependencies: + "@types/unist": "npm:^2.0.0" + unist-util-is: "npm:^4.0.0" + checksum: 231c80c5ba8e79263956fcaa25ed2a11ad7fe77ac5ba0d322e9d51bbc4238501e3bb52f405e518bcdc5471e27b33eff520db0aa4a3b1feb9fb6e2de6ae385d49 + languageName: node + linkType: hard + +"unist-util-visit@npm:^2.0.0": + version: 2.0.3 + resolution: "unist-util-visit@npm:2.0.3" + dependencies: + "@types/unist": "npm:^2.0.0" + unist-util-is: "npm:^4.0.0" + unist-util-visit-parents: "npm:^3.0.0" + checksum: 7b11303d82271ca53a2ced2d56c87a689dd518596c99ff4a11cdff750f5cc5c0e4b64b146bd2363557cb29443c98713bfd1e8dc6d1c3f9d474b9eb1f23a60888 + languageName: node + linkType: hard + +"universalify@npm:^0.2.0": + version: 0.2.0 + resolution: "universalify@npm:0.2.0" + checksum: cedbe4d4ca3967edf24c0800cfc161c5a15e240dac28e3ce575c689abc11f2c81ccc6532c8752af3b40f9120fb5e454abecd359e164f4f6aa44c29cd37e194fe + languageName: node + linkType: hard + +"universalify@npm:^2.0.0": + version: 2.0.1 + resolution: "universalify@npm:2.0.1" + checksum: 73e8ee3809041ca8b818efb141801a1004e3fc0002727f1531f4de613ea281b494a40909596dae4a042a4fb6cd385af5d4db2e137b1362e0e91384b828effd3a + languageName: node + linkType: hard + +"unpipe@npm:1.0.0, unpipe@npm:~1.0.0": + version: 1.0.0 + resolution: "unpipe@npm:1.0.0" + checksum: 193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c + languageName: node + linkType: hard + +"unplugin@npm:^1.3.1": + version: 1.5.1 + resolution: "unplugin@npm:1.5.1" + dependencies: + acorn: "npm:^8.11.2" + chokidar: "npm:^3.5.3" + webpack-sources: "npm:^3.2.3" + webpack-virtual-modules: "npm:^0.6.0" + checksum: 08cee7d100de3b8697d33eaa32405d821d0f51600640ce79e26f4258ddedcd9ac4c022f0453d6d978e9f75fd939ba9553440827987b2b03078f7087f4bef7c96 + languageName: node + linkType: hard + +"unset-value@npm:^1.0.0": + version: 1.0.0 + resolution: "unset-value@npm:1.0.0" + dependencies: + has-value: "npm:^0.3.1" + isobject: "npm:^3.0.0" + checksum: 68a796dde4a373afdbf017de64f08490a3573ebee549136da0b3a2245299e7f65f647ef70dc13c4ac7f47b12fba4de1646fa0967a365638578fedce02b9c0b1f + languageName: node + linkType: hard + +"untildify@npm:^4.0.0": + version: 4.0.0 + resolution: "untildify@npm:4.0.0" + checksum: d758e624c707d49f76f7511d75d09a8eda7f2020d231ec52b67ff4896bcf7013be3f9522d8375f57e586e9a2e827f5641c7e06ee46ab9c435fc2b2b2e9de517a + languageName: node + linkType: hard + +"upath@npm:^1.1.1": + version: 1.2.0 + resolution: "upath@npm:1.2.0" + checksum: 3746f24099bf69dbf8234cecb671e1016e1f6b26bd306de4ff8966fb0bc463fa1014ffc48646b375de1ab573660e3a0256f6f2a87218b2dfa1779a84ef6992fa + languageName: node + linkType: hard + +"update-browserslist-db@npm:^1.0.13": + version: 1.0.13 + resolution: "update-browserslist-db@npm:1.0.13" + dependencies: + escalade: "npm:^3.1.1" + picocolors: "npm:^1.0.0" + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: e52b8b521c78ce1e0c775f356cd16a9c22c70d25f3e01180839c407a5dc787fb05a13f67560cbaf316770d26fa99f78f1acd711b1b54a4f35d4820d4ea7136e6 + languageName: node + linkType: hard + +"urix@npm:^0.1.0": + version: 0.1.0 + resolution: "urix@npm:0.1.0" + checksum: 264f1b29360c33c0aec5fb9819d7e28f15d1a3b83175d2bcc9131efe8583f459f07364957ae3527f1478659ec5b2d0f1ad401dfb625f73e4d424b3ae35fc5fc0 + languageName: node + linkType: hard + +"url-parse@npm:^1.5.3": + version: 1.5.10 + resolution: "url-parse@npm:1.5.10" + dependencies: + querystringify: "npm:^2.1.1" + requires-port: "npm:^1.0.0" + checksum: bd5aa9389f896974beb851c112f63b466505a04b4807cea2e5a3b7092f6fbb75316f0491ea84e44f66fed55f1b440df5195d7e3a8203f64fcefa19d182f5be87 + languageName: node + linkType: hard + +"url@npm:^0.11.0": + version: 0.11.3 + resolution: "url@npm:0.11.3" + dependencies: + punycode: "npm:^1.4.1" + qs: "npm:^6.11.2" + checksum: 7546b878ee7927cfc62ca21dbe2dc395cf70e889c3488b2815bf2c63355cb3c7db555128176a01b0af6cccf265667b6fd0b4806de00cb71c143c53986c08c602 + languageName: node + linkType: hard + +"use-callback-ref@npm:^1.3.0": + version: 1.3.0 + resolution: "use-callback-ref@npm:1.3.0" + dependencies: + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 8a0867ffd441f358c66d79567970a745cc78ac2f98840a81c1fa749a525e8716116c645497d886a815e1dcf40ad81a107ebd6a7d15fd9ab5925c44a994a1d89a + languageName: node + linkType: hard + +"use-resize-observer@npm:^9.1.0": + version: 9.1.0 + resolution: "use-resize-observer@npm:9.1.0" + dependencies: + "@juggle/resize-observer": "npm:^3.3.1" + peerDependencies: + react: 16.8.0 - 18 + react-dom: 16.8.0 - 18 + checksum: 6ccdeb09fe20566ec182b1635a22f189e13d46226b74610432590e69b31ef5d05d069badc3306ebd0d2bb608743b17981fb535763a1d7dc2c8ae462ee8e5999c + languageName: node + linkType: hard + +"use-sidecar@npm:^1.1.2": + version: 1.1.2 + resolution: "use-sidecar@npm:1.1.2" + dependencies: + detect-node-es: "npm:^1.1.0" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 89f0018fd9aee1fc17c85ac18c4bf8944d460d453d0d0e04ddbc8eaddf3fa591e9c74a1f8a438a1bff368a7a2417fab380bdb3df899d2194c4375b0982736de0 + languageName: node + linkType: hard + +"use@npm:^3.1.0": + version: 3.1.1 + resolution: "use@npm:3.1.1" + checksum: 75b48673ab80d5139c76922630d5a8a44e72ed58dbaf54dee1b88352d10e1c1c1fc332066c782d8ae9a56503b85d3dc67ff6d2ffbd9821120466d1280ebb6d6e + languageName: node + linkType: hard + +"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942 + languageName: node + linkType: hard + +"util@npm:^0.10.4": + version: 0.10.4 + resolution: "util@npm:0.10.4" + dependencies: + inherits: "npm:2.0.3" + checksum: d29f6893e406b63b088ce9924da03201df89b31490d4d011f1c07a386ea4b3dbe907464c274023c237da470258e1805d806c7e4009a5974cd6b1d474b675852a + languageName: node + linkType: hard + +"util@npm:^0.11.0": + version: 0.11.1 + resolution: "util@npm:0.11.1" + dependencies: + inherits: "npm:2.0.3" + checksum: 8e9d1a85e661c8a8d9883d821aedbff3f8d9c3accd85357020905386ada5653b20389fc3591901e2a0bde64f8dc86b28c3f990114aa5a38eaaf30b455fa3cdf6 + languageName: node + linkType: hard + +"util@npm:^0.12.4, util@npm:^0.12.5": + version: 0.12.5 + resolution: "util@npm:0.12.5" + dependencies: + inherits: "npm:^2.0.3" + is-arguments: "npm:^1.0.4" + is-generator-function: "npm:^1.0.7" + is-typed-array: "npm:^1.1.3" + which-typed-array: "npm:^1.1.2" + checksum: c27054de2cea2229a66c09522d0fa1415fb12d861d08523a8846bf2e4cbf0079d4c3f725f09dcb87493549bcbf05f5798dce1688b53c6c17201a45759e7253f3 + languageName: node + linkType: hard + +"utils-merge@npm:1.0.1": + version: 1.0.1 + resolution: "utils-merge@npm:1.0.1" + checksum: 02ba649de1b7ca8854bfe20a82f1dfbdda3fb57a22ab4a8972a63a34553cf7aa51bc9081cf7e001b035b88186d23689d69e71b510e610a09a4c66f68aa95b672 + languageName: node + linkType: hard + +"uuid@npm:^9.0.0": + version: 9.0.1 + resolution: "uuid@npm:9.0.1" + bin: + uuid: dist/bin/uuid + checksum: 1607dd32ac7fc22f2d8f77051e6a64845c9bce5cd3dd8aa0070c074ec73e666a1f63c7b4e0f4bf2bc8b9d59dc85a15e17807446d9d2b17c8485fbc2147b27f9b + languageName: node + linkType: hard + +"v8flags@npm:^3.2.0": + version: 3.2.0 + resolution: "v8flags@npm:3.2.0" + dependencies: + homedir-polyfill: "npm:^1.0.1" + checksum: aa0149384c1b75eee60f9e4339dbcc891d5a2154f51dbe41feb35a2227e88c0f30701234676c47b7887414c6a95bce23783931eeed52126842b7ba3a75984da7 + languageName: node + linkType: hard + +"validate-npm-package-license@npm:^3.0.1": + version: 3.0.4 + resolution: "validate-npm-package-license@npm:3.0.4" + dependencies: + spdx-correct: "npm:^3.0.0" + spdx-expression-parse: "npm:^3.0.0" + checksum: 7b91e455a8de9a0beaa9fe961e536b677da7f48c9a493edf4d4d4a87fd80a7a10267d438723364e432c2fcd00b5650b5378275cded362383ef570276e6312f4f + languageName: node + linkType: hard + +"value-or-function@npm:^3.0.0": + version: 3.0.0 + resolution: "value-or-function@npm:3.0.0" + checksum: 78a75b44543bb70ea3eee1804bbb101558f422335e3b62ed8864deeb85295efab1b109f607c3806b13c2fc48630d93f6c564b2796377a01a6302d355323ecebe + languageName: node + linkType: hard + +"varint@npm:^6.0.0": + version: 6.0.0 + resolution: "varint@npm:6.0.0" + checksum: 737fc37088a62ed3bd21466e318d21ca7ac4991d0f25546f518f017703be4ed0f9df1c5559f1dd533dddba4435a1b758fd9230e4772c1a930ef72b42f5c750fd + languageName: node + linkType: hard + +"vary@npm:~1.1.2": + version: 1.1.2 + resolution: "vary@npm:1.1.2" + checksum: f15d588d79f3675135ba783c91a4083dcd290a2a5be9fcb6514220a1634e23df116847b1cc51f66bfb0644cf9353b2abb7815ae499bab06e46dd33c1a6bf1f4f + languageName: node + linkType: hard + +"vinyl-fs@npm:^3.0.0": + version: 3.0.3 + resolution: "vinyl-fs@npm:3.0.3" + dependencies: + fs-mkdirp-stream: "npm:^1.0.0" + glob-stream: "npm:^6.1.0" + graceful-fs: "npm:^4.0.0" + is-valid-glob: "npm:^1.0.0" + lazystream: "npm:^1.0.0" + lead: "npm:^1.0.0" + object.assign: "npm:^4.0.4" + pumpify: "npm:^1.3.5" + readable-stream: "npm:^2.3.3" + remove-bom-buffer: "npm:^3.0.0" + remove-bom-stream: "npm:^1.2.0" + resolve-options: "npm:^1.1.0" + through2: "npm:^2.0.0" + to-through: "npm:^2.0.0" + value-or-function: "npm:^3.0.0" + vinyl: "npm:^2.0.0" + vinyl-sourcemap: "npm:^1.1.0" + checksum: c7e52624b8a32fd5164210d0ce45050ddfcd535ac0b172c59138a402ca730bd1083ee78e43dc71d8ee21475869e9c080ff212e98926a2b980eb3aa644a561777 + languageName: node + linkType: hard + +"vinyl-sourcemap@npm:^1.1.0": + version: 1.1.0 + resolution: "vinyl-sourcemap@npm:1.1.0" + dependencies: + append-buffer: "npm:^1.0.2" + convert-source-map: "npm:^1.5.0" + graceful-fs: "npm:^4.1.6" + normalize-path: "npm:^2.1.1" + now-and-later: "npm:^2.0.0" + remove-bom-buffer: "npm:^3.0.0" + vinyl: "npm:^2.0.0" + checksum: 5945250fbc04ed8be348f27adfcf842d310f2e4eea88c4821b48768d12bc8407c332c26b0eeabc63f5808843a2859d902020572bdc42e625a9d049a298d8cf68 + languageName: node + linkType: hard + +"vinyl-sourcemaps-apply@npm:^0.2.1": + version: 0.2.1 + resolution: "vinyl-sourcemaps-apply@npm:0.2.1" + dependencies: + source-map: "npm:^0.5.1" + checksum: 141c66335eb98f40e2c31418cda57b33ef5378480c73c8416fd88e44655212160119a629f740d1b1969e84481c5e01d3e3f861c38ed16a0cf2afcc112a466f7d + languageName: node + linkType: hard + +"vinyl@npm:^2.0.0, vinyl@npm:^2.2.1": + version: 2.2.1 + resolution: "vinyl@npm:2.2.1" + dependencies: + clone: "npm:^2.1.1" + clone-buffer: "npm:^1.0.0" + clone-stats: "npm:^1.0.0" + cloneable-readable: "npm:^1.0.0" + remove-trailing-separator: "npm:^1.0.1" + replace-ext: "npm:^1.0.0" + checksum: e7073fe5a3e10bbd5a3abe7ccf3351ed1b784178576b09642c08b0ef4056265476610aabd29eabfaaf456ada45f05f4112a35687d502f33aab33b025fc6ec38f + languageName: node + linkType: hard + +"vite-node@npm:1.3.1": + version: 1.3.1 + resolution: "vite-node@npm:1.3.1" + dependencies: + cac: "npm:^6.7.14" + debug: "npm:^4.3.4" + pathe: "npm:^1.1.1" + picocolors: "npm:^1.0.0" + vite: "npm:^5.0.0" + bin: + vite-node: vite-node.mjs + checksum: b50665ef224f3527f856ab88a0cfabab36dd6e2dd1e3edca8f8f25d5d33754e1050495472c2c82147d0dcf7c5280971dae2f37a531c10f3941d8d3344e34ce0b + languageName: node + linkType: hard + +"vite@npm:^5.0.0": + version: 5.0.10 + resolution: "vite@npm:5.0.10" + dependencies: + esbuild: "npm:^0.19.3" + fsevents: "npm:~2.3.3" + postcss: "npm:^8.4.32" + rollup: "npm:^4.2.0" + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: d666b2760d2a7ea1d0d35f67c042053e562144f80554be4e4dc58e607fd5f62193cd203d73ab2e315df66830d8b9d9a2e3509d0208bdef1b2e92e0a5c364df84 + languageName: node + linkType: hard + +"vite@npm:^5.1.4": + version: 5.1.4 + resolution: "vite@npm:5.1.4" + dependencies: + esbuild: "npm:^0.19.3" + fsevents: "npm:~2.3.3" + postcss: "npm:^8.4.35" + rollup: "npm:^4.2.0" + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: 8f04c8bed33f266bde27f432412456a3b893b51fe1857f0b8cd259100b376c1393a7927db1dd6344a4376baed72ed179ec5b0428aef2ae8508f1f28f95acb908 + languageName: node + linkType: hard + +"vitest@npm:^1.3.1": + version: 1.3.1 + resolution: "vitest@npm:1.3.1" + dependencies: + "@vitest/expect": "npm:1.3.1" + "@vitest/runner": "npm:1.3.1" + "@vitest/snapshot": "npm:1.3.1" + "@vitest/spy": "npm:1.3.1" + "@vitest/utils": "npm:1.3.1" + acorn-walk: "npm:^8.3.2" + chai: "npm:^4.3.10" + debug: "npm:^4.3.4" + execa: "npm:^8.0.1" + local-pkg: "npm:^0.5.0" + magic-string: "npm:^0.30.5" + pathe: "npm:^1.1.1" + picocolors: "npm:^1.0.0" + std-env: "npm:^3.5.0" + strip-literal: "npm:^2.0.0" + tinybench: "npm:^2.5.1" + tinypool: "npm:^0.8.2" + vite: "npm:^5.0.0" + vite-node: "npm:1.3.1" + why-is-node-running: "npm:^2.2.2" + peerDependencies: + "@edge-runtime/vm": "*" + "@types/node": ^18.0.0 || >=20.0.0 + "@vitest/browser": 1.3.1 + "@vitest/ui": 1.3.1 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 66d312a3dc12e67bba22d31332d939e89cd17d38531893c7b13b8826704564031c1dde795df2799b855660572c19a595301e920710c7775d072ee6332502efc5 + languageName: node + linkType: hard + +"vm-browserify@npm:^1.0.1": + version: 1.1.2 + resolution: "vm-browserify@npm:1.1.2" + checksum: 0cc1af6e0d880deb58bc974921320c187f9e0a94f25570fca6b1bd64e798ce454ab87dfd797551b1b0cc1849307421aae0193cedf5f06bdb5680476780ee344b + languageName: node + linkType: hard + +"w3c-xmlserializer@npm:^5.0.0": + version: 5.0.0 + resolution: "w3c-xmlserializer@npm:5.0.0" + dependencies: + xml-name-validator: "npm:^5.0.0" + checksum: 8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b + languageName: node + linkType: hard + +"walker@npm:^1.0.8": + version: 1.0.8 + resolution: "walker@npm:1.0.8" + dependencies: + makeerror: "npm:1.0.12" + checksum: a17e037bccd3ca8a25a80cb850903facdfed0de4864bd8728f1782370715d679fa72e0a0f5da7c1c1379365159901e5935f35be531229da53bbfc0efdabdb48e + languageName: node + linkType: hard + +"watcher@npm:^2.3.0": + version: 2.3.0 + resolution: "watcher@npm:2.3.0" + dependencies: + dettle: "npm:^1.0.1" + stubborn-fs: "npm:^1.2.5" + tiny-readdir: "npm:^2.2.0" + checksum: 7b1e47321ddf96882ebee6f619211b085f98bc0c3bceb94a58938e8d8d209f83283b30b645bdae148e063c3bc165eeafd73e3a14bdb7c3bfe519bd7536172257 + languageName: node + linkType: hard + +"watchpack@npm:^2.2.0": + version: 2.4.0 + resolution: "watchpack@npm:2.4.0" + dependencies: + glob-to-regexp: "npm:^0.4.1" + graceful-fs: "npm:^4.1.2" + checksum: c5e35f9fb9338d31d2141d9835643c0f49b5f9c521440bb648181059e5940d93dd8ed856aa8a33fbcdd4e121dad63c7e8c15c063cf485429cd9d427be197fe62 + languageName: node + linkType: hard + +"wcwidth@npm:^1.0.1": + version: 1.0.1 + resolution: "wcwidth@npm:1.0.1" + dependencies: + defaults: "npm:^1.0.3" + checksum: 5b61ca583a95e2dd85d7078400190efd452e05751a64accb8c06ce4db65d7e0b0cde9917d705e826a2e05cc2548f61efde115ffa374c3e436d04be45c889e5b4 + languageName: node + linkType: hard + +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: 5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db + languageName: node + linkType: hard + +"webidl-conversions@npm:^7.0.0": + version: 7.0.0 + resolution: "webidl-conversions@npm:7.0.0" + checksum: 228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4 + languageName: node + linkType: hard + +"webpack-sources@npm:^3.2.3": + version: 3.2.3 + resolution: "webpack-sources@npm:3.2.3" + checksum: 2ef63d77c4fad39de4a6db17323d75eb92897b32674e97d76f0a1e87c003882fc038571266ad0ef581ac734cbe20952912aaa26155f1905e96ce251adbb1eb4e + languageName: node + linkType: hard + +"webpack-virtual-modules@npm:^0.6.0": + version: 0.6.1 + resolution: "webpack-virtual-modules@npm:0.6.1" + checksum: 696bdc1acf3806374bdeb4b9b9856b79ee70b31e92f325dfab9b8c8c7e14bb6ddffa9f895a214770c4fb8fea45a21f34ca64310f74e877292a90f4a9966c9c2f + languageName: node + linkType: hard + +"whatwg-encoding@npm:^3.1.1": + version: 3.1.1 + resolution: "whatwg-encoding@npm:3.1.1" + dependencies: + iconv-lite: "npm:0.6.3" + checksum: 273b5f441c2f7fda3368a496c3009edbaa5e43b71b09728f90425e7f487e5cef9eb2b846a31bd760dd8077739c26faf6b5ca43a5f24033172b003b72cf61a93e + languageName: node + linkType: hard + +"whatwg-mimetype@npm:^4.0.0": + version: 4.0.0 + resolution: "whatwg-mimetype@npm:4.0.0" + checksum: a773cdc8126b514d790bdae7052e8bf242970cebd84af62fb2f35a33411e78e981f6c0ab9ed1fe6ec5071b09d5340ac9178e05b52d35a9c4bcf558ba1b1551df + languageName: node + linkType: hard + +"whatwg-url@npm:^14.0.0": + version: 14.0.0 + resolution: "whatwg-url@npm:14.0.0" + dependencies: + tr46: "npm:^5.0.0" + webidl-conversions: "npm:^7.0.0" + checksum: ac32e9ba9d08744605519bbe9e1371174d36229689ecc099157b6ba102d4251a95e81d81f3d80271eb8da182eccfa65653f07f0ab43ea66a6934e643fd091ba9 + languageName: node + linkType: hard + +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: 1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5 + languageName: node + linkType: hard + +"which-boxed-primitive@npm:^1.0.2": + version: 1.0.2 + resolution: "which-boxed-primitive@npm:1.0.2" + dependencies: + is-bigint: "npm:^1.0.1" + is-boolean-object: "npm:^1.1.0" + is-number-object: "npm:^1.0.4" + is-string: "npm:^1.0.5" + is-symbol: "npm:^1.0.3" + checksum: 0a62a03c00c91dd4fb1035b2f0733c341d805753b027eebd3a304b9cb70e8ce33e25317add2fe9b5fea6f53a175c0633ae701ff812e604410ddd049777cd435e + languageName: node + linkType: hard + +"which-collection@npm:^1.0.1": + version: 1.0.1 + resolution: "which-collection@npm:1.0.1" + dependencies: + is-map: "npm:^2.0.1" + is-set: "npm:^2.0.1" + is-weakmap: "npm:^2.0.1" + is-weakset: "npm:^2.0.1" + checksum: 249f913e1758ed2f06f00706007d87dc22090a80591a56917376e70ecf8fc9ab6c41d98e1c87208bb9648676f65d4b09c0e4d23c56c7afb0f0a73a27d701df5d + languageName: node + linkType: hard + +"which-module@npm:^1.0.0": + version: 1.0.0 + resolution: "which-module@npm:1.0.0" + checksum: ce5088fb12dae0b6d5997b6221342943ff6275c3b2cd9c569f04ec23847c71013d254c6127d531010dccc22c0fc0f8dce2b6ecf6898941a60b576adb2018af22 + languageName: node + linkType: hard + +"which-typed-array@npm:^1.1.11, which-typed-array@npm:^1.1.13, which-typed-array@npm:^1.1.2": + version: 1.1.13 + resolution: "which-typed-array@npm:1.1.13" + dependencies: + available-typed-arrays: "npm:^1.0.5" + call-bind: "npm:^1.0.4" + for-each: "npm:^0.3.3" + gopd: "npm:^1.0.1" + has-tostringtag: "npm:^1.0.0" + checksum: 9f5f1c42918df3d5b91c4315ed0051d5d874370998bf095c9ae0df374f0881f85094e3c384b8fb08ab7b4d4f54ba81c0aff75da6226e7c0589b83dfbec1cd4c9 + languageName: node + linkType: hard + +"which@npm:^1.2.14, which@npm:^1.2.9, which@npm:^1.3.1": + version: 1.3.1 + resolution: "which@npm:1.3.1" + dependencies: + isexe: "npm:^2.0.0" + bin: + which: ./bin/which + checksum: e945a8b6bbf6821aaaef7f6e0c309d4b615ef35699576d5489b4261da9539f70393c6b2ce700ee4321c18f914ebe5644bc4631b15466ffbaad37d83151f6af59 + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + languageName: node + linkType: hard + +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" + dependencies: + isexe: "npm:^3.1.1" + bin: + node-which: bin/which.js + checksum: 449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a + languageName: node + linkType: hard + +"why-is-node-running@npm:^2.2.2": + version: 2.2.2 + resolution: "why-is-node-running@npm:2.2.2" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 805d57eb5d33f0fb4e36bae5dceda7fd8c6932c2aeb705e30003970488f1a2bc70029ee64be1a0e1531e2268b11e65606e88e5b71d667ea745e6dc48fc9014bd + languageName: node + linkType: hard + +"winston-transport@npm:^4.5.0": + version: 4.6.0 + resolution: "winston-transport@npm:4.6.0" + dependencies: + logform: "npm:^2.3.2" + readable-stream: "npm:^3.6.0" + triple-beam: "npm:^1.3.0" + checksum: 43f7f03dfbaeb2a37ddcfadf5f03a6802c77fb8800a384e9aeecce8d233272ed8f18c50f377045a7e154fd6c951e31c9af1bbcd7a3db9246518af42b6f961cc1 + languageName: node + linkType: hard + +"winston@npm:^3.8.2": + version: 3.11.0 + resolution: "winston@npm:3.11.0" + dependencies: + "@colors/colors": "npm:^1.6.0" + "@dabh/diagnostics": "npm:^2.0.2" + async: "npm:^3.2.3" + is-stream: "npm:^2.0.0" + logform: "npm:^2.4.0" + one-time: "npm:^1.0.0" + readable-stream: "npm:^3.4.0" + safe-stable-stringify: "npm:^2.3.1" + stack-trace: "npm:0.0.x" + triple-beam: "npm:^1.3.0" + winston-transport: "npm:^4.5.0" + checksum: 7e1f8919cbdc62cfe46e6204d79a83e1364696ef61111483f3ecf204988922383fe74192c5bc9f89df9b47caf24c2d34f5420ef6f3b693f8d1286b46432e97be + languageName: node + linkType: hard + +"wordwrap@npm:^1.0.0": + version: 1.0.0 + resolution: "wordwrap@npm:1.0.0" + checksum: 7ed2e44f3c33c5c3e3771134d2b0aee4314c9e49c749e37f464bf69f2bcdf0cbf9419ca638098e2717cff4875c47f56a007532f6111c3319f557a2ca91278e92 + languageName: node + linkType: hard + +"workerpool@npm:^9.1.0": + version: 9.1.0 + resolution: "workerpool@npm:9.1.0" + checksum: 32d0807962be58a98ec22f5630be4a90f779f5faab06d5b4f000d32c11c8d5feb66be9bc5c73fdc49c91519e391db55c9e2e63392854b3df945744b2436a7efd + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + languageName: node + linkType: hard + +"wrap-ansi@npm:^2.0.0": + version: 2.1.0 + resolution: "wrap-ansi@npm:2.1.0" + dependencies: + string-width: "npm:^1.0.1" + strip-ansi: "npm:^3.0.1" + checksum: 1a47367eef192fc9ecaf00238bad5de8987c3368082b619ab36c5e2d6d7b0a2aef95a2ca65840be598c56ced5090a3ba487956c7aee0cac7c45017502fa980fb + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 + languageName: node + linkType: hard + +"write-file-atomic@npm:^2.3.0": + version: 2.4.3 + resolution: "write-file-atomic@npm:2.4.3" + dependencies: + graceful-fs: "npm:^4.1.11" + imurmurhash: "npm:^0.1.4" + signal-exit: "npm:^3.0.2" + checksum: 8cb4bba0c1ab814a9b127844da0db4fb8c5e06ddbe6317b8b319377c73b283673036c8b9360120062898508b9428d81611cf7fa97584504a00bc179b2a580b92 + languageName: node + linkType: hard + +"write-file-atomic@npm:^4.0.2": + version: 4.0.2 + resolution: "write-file-atomic@npm:4.0.2" + dependencies: + imurmurhash: "npm:^0.1.4" + signal-exit: "npm:^3.0.7" + checksum: a2c282c95ef5d8e1c27b335ae897b5eca00e85590d92a3fd69a437919b7b93ff36a69ea04145da55829d2164e724bc62202cdb5f4b208b425aba0807889375c7 + languageName: node + linkType: hard + +"ws@npm:^6.1.0": + version: 6.2.2 + resolution: "ws@npm:6.2.2" + dependencies: + async-limiter: "npm:~1.0.0" + checksum: d628a1e95668a296644b4f51ce5debb43d9f1d89ebb2e32fef205a685b9439378eb824d60ce3a40bbc3bad0e887d84a56b343f2076f48d74f17c4c0800c42967 + languageName: node + linkType: hard + +"ws@npm:^7.4.6": + version: 7.5.9 + resolution: "ws@npm:7.5.9" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: aec4ef4eb65821a7dde7b44790f8699cfafb7978c9b080f6d7a98a7f8fc0ce674c027073a78574c94786ba7112cc90fa2cc94fc224ceba4d4b1030cff9662494 + languageName: node + linkType: hard + +"ws@npm:^8.16.0": + version: 8.16.0 + resolution: "ws@npm:8.16.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: a7783bb421c648b1e622b423409cb2a58ac5839521d2f689e84bc9dc41d59379c692dd405b15a997ea1d4c0c2e5314ad707332d0c558f15232d2bc07c0b4618a + languageName: node + linkType: hard + +"ws@npm:^8.2.3": + version: 8.14.2 + resolution: "ws@npm:8.14.2" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 35b4c2da048b8015c797fd14bcb5a5766216ce65c8a5965616a5440ca7b6c3681ee3cbd0ea0c184a59975556e9d58f2002abf8485a14d11d3371770811050a16 + languageName: node + linkType: hard + +"xml-name-validator@npm:^5.0.0": + version: 5.0.0 + resolution: "xml-name-validator@npm:5.0.0" + checksum: 3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5 + languageName: node + linkType: hard + +"xmlchars@npm:^2.2.0": + version: 2.2.0 + resolution: "xmlchars@npm:2.2.0" + checksum: b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593 + languageName: node + linkType: hard + +"xpath@npm:^0.0.32": + version: 0.0.32 + resolution: "xpath@npm:0.0.32" + checksum: 3743ab91a8ec1b5eac1f27ddf2fbf696fcde8ce487215becde1502b85a309dcd1b0baeaac1ee7a730aea4787d049b67ae89e8aedbe03a5a07a71e62ec296d9de + languageName: node + linkType: hard + +"xregexp@npm:^5.1.1": + version: 5.1.1 + resolution: "xregexp@npm:5.1.1" + dependencies: + "@babel/runtime-corejs3": "npm:^7.16.5" + checksum: ae007c7898afd808e7664931228dc4bd38e65ebc24c66318416a038b4351cc73cc9b3b9cea1ab5ffd97933bf9b75afbf848f36e91d22b2416d6bd7d6fcfd2ee6 + languageName: node + linkType: hard + +"xtend@npm:^4.0.0, xtend@npm:~4.0.0, xtend@npm:~4.0.1": + version: 4.0.2 + resolution: "xtend@npm:4.0.2" + checksum: 366ae4783eec6100f8a02dff02ac907bf29f9a00b82ac0264b4d8b832ead18306797e283cf19de776538babfdcb2101375ec5646b59f08c52128ac4ab812ed0e + languageName: node + linkType: hard + +"y18n@npm:^3.2.1": + version: 3.2.2 + resolution: "y18n@npm:3.2.2" + checksum: 08dc1880f6f766057ed25cd61ef0c7dab3db93639db9a7487a84f75dac7a349dface8dff8d1d8b7bdf50969fcd69ab858ab26b06968b4e4b12ee60d195233c46 + languageName: node + linkType: hard + +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249 + languageName: node + linkType: hard + +"yallist@npm:^3.0.2": + version: 3.1.1 + resolution: "yallist@npm:3.1.1" + checksum: c66a5c46bc89af1625476f7f0f2ec3653c1a1791d2f9407cfb4c2ba812a1e1c9941416d71ba9719876530e3340a99925f697142989371b72d93b9ee628afd8c1 + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + languageName: node + linkType: hard + +"yaml@npm:^2.3.4": + version: 2.4.0 + resolution: "yaml@npm:2.4.0" + bin: + yaml: bin.mjs + checksum: 97ab0b5a0714c92e4dd75120a6a63e470b0adc282afae0a701bf38f8c42cbf6429fcd6aca883e3a63c68936ab841862e6c69e2d66d355c3e4fc7cfd346af2108 + languageName: node + linkType: hard + +"yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2 + languageName: node + linkType: hard + +"yargs-parser@npm:^5.0.1": + version: 5.0.1 + resolution: "yargs-parser@npm:5.0.1" + dependencies: + camelcase: "npm:^3.0.0" + object.assign: "npm:^4.1.0" + checksum: 94f24930da4eb80c6ba1c308e1d187a6cab6206f08cda8654b3ebbd0d20f689c113a5111898e90c5885fdc39ce68de5a59aca703f2578335af644ba8f239166f + languageName: node + linkType: hard + +"yargs@npm:^17.5.1, yargs@npm:^17.7.2": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05 + languageName: node + linkType: hard + +"yargs@npm:^7.1.0": + version: 7.1.2 + resolution: "yargs@npm:7.1.2" + dependencies: + camelcase: "npm:^3.0.0" + cliui: "npm:^3.2.0" + decamelize: "npm:^1.1.1" + get-caller-file: "npm:^1.0.1" + os-locale: "npm:^1.4.0" + read-pkg-up: "npm:^1.0.1" + require-directory: "npm:^2.1.1" + require-main-filename: "npm:^1.0.1" + set-blocking: "npm:^2.0.0" + string-width: "npm:^1.0.2" + which-module: "npm:^1.0.0" + y18n: "npm:^3.2.1" + yargs-parser: "npm:^5.0.1" + checksum: ca7dc99af8335a731cf8255bc5a8a782e0d591de21ff7e4abc8f65ad58acab638a5760f368d64a8dcc4f8cb2978ecba45f459c8c5368203cf6339be7e395cdb5 + languageName: node + linkType: hard + +"yauzl@npm:^2.10.0": + version: 2.10.0 + resolution: "yauzl@npm:2.10.0" + dependencies: + buffer-crc32: "npm:~0.2.3" + fd-slicer: "npm:~1.1.0" + checksum: f265002af7541b9ec3589a27f5fb8f11cf348b53cc15e2751272e3c062cd73f3e715bc72d43257de71bbaecae446c3f1b14af7559e8ab0261625375541816422 + languageName: node + linkType: hard + +"yocto-queue@npm:^0.1.0": + version: 0.1.0 + resolution: "yocto-queue@npm:0.1.0" + checksum: dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f + languageName: node + linkType: hard + +"yocto-queue@npm:^1.0.0": + version: 1.0.0 + resolution: "yocto-queue@npm:1.0.0" + checksum: 856117aa15cf5103d2a2fb173f0ab4acb12b4b4d0ed3ab249fdbbf612e55d1cadfd27a6110940e24746fb0a78cf640b522cc8bca76f30a3b00b66e90cf82abe0 + languageName: node + linkType: hard diff --git a/package.json b/package.json new file mode 100644 index 0000000000..94f4829985 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "penpot", + "version": "1.20.0", + "license": "MPL-2.0", + "author": "Kaleidos INC", + "private": true, + "packageManager": "yarn@4.0.2", + "repository": { + "type": "git", + "url": "https://github.com/penpot/penpot" + }, + "type": "module", + "scripts": { + "fmt:clj:check": "cljfmt check --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/", + "fmt:clj": "cljfmt fix --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/", + "lint:clj:common": "clj-kondo --parallel=true --lint common/src", + "lint:clj:frontend": "clj-kondo --parallel=true --lint frontend/src", + "lint:clj:backend": "clj-kondo --parallel=true --lint backend/src", + "lint:clj:exporter": "clj-kondo --parallel=true --lint exporter/src", + "lint:clj": "yarn run lint:clj:common && yarn run lint:clj:frontend && yarn run lint:clj:backend && yarn run lint:clj:exporter" + } +} diff --git a/version.txt b/version.txt index 843f863534..227cea2156 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.19.4 +2.0.0 diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000000..9a4b9536a2 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,12 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"penpot@workspace:.": + version: 0.0.0-use.local + resolution: "penpot@workspace:." + languageName: unknown + linkType: soft