mirror of
https://github.com/penpot/penpot.git
synced 2026-01-21 12:50:11 -05:00
Compare commits
13 Commits
azazeln28-
...
hiru-add-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
094ad897ab | ||
|
|
b2c45f6e1d | ||
|
|
7a377258d3 | ||
|
|
dd4a18a3ff | ||
|
|
bff005a951 | ||
|
|
776c62051d | ||
|
|
952f622ce9 | ||
|
|
a6c6f97f47 | ||
|
|
88424eb54a | ||
|
|
de9a21121a | ||
|
|
cea10308b7 | ||
|
|
5223c9c881 | ||
|
|
be62fa10c4 |
2
.github/workflows/build-tag.yml
vendored
2
.github/workflows/build-tag.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
TEXT: |
|
||||
🐳 *[PENPOT] Docker image available: {{ github.ref_name }}*
|
||||
🐳 *[PENPOT] Docker image available: ${{ github.ref_name }}*
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@infra
|
||||
|
||||
|
||||
2
.github/workflows/commit-checker.yml
vendored
2
.github/workflows/commit-checker.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Check Commit Type
|
||||
uses: gsactions/commit-message-checker@v2
|
||||
with:
|
||||
pattern: '^(((:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s[A-Z].*[^.])|(Merge|Revert).+[^.])$'
|
||||
pattern: '^(((:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s[A-Z].*[^.])|(Merge|Revert|Reapply).+[^.])$'
|
||||
flags: 'gm'
|
||||
error: 'Commit should match CONTRIBUTING.md guideline'
|
||||
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request
|
||||
|
||||
13
CHANGES.md
13
CHANGES.md
@@ -1,5 +1,18 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.14.0 (Unreleased)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
|
||||
## 2.13.0 (Unreleased)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
@@ -1766,6 +1766,59 @@
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "0017-remove-unneeded-objects-from-components"
|
||||
[data _]
|
||||
;; Some components have an `:objects` attribute, despite not being
|
||||
;; deleted. This migration removes it.
|
||||
(letfn [(check-component [component]
|
||||
(if (and (not (:deleted component))
|
||||
(contains? component :objects))
|
||||
(dissoc component :objects)
|
||||
component))]
|
||||
(d/update-when data :components d/update-vals check-component)))
|
||||
|
||||
(defmethod migrate-data "0018-sync-component-id-with-near-main"
|
||||
[data _]
|
||||
(let [libs (some-> (:libs data) deref)]
|
||||
(letfn [(fix-shape
|
||||
[data page shape]
|
||||
(if (and (ctk/subcopy-head? shape)
|
||||
(nil? (ctk/get-swap-slot shape)))
|
||||
(let [file {:id (:id data) :data data}
|
||||
ref-shape (ctf/find-ref-shape file page libs shape {:include-deleted? true :with-context? true})]
|
||||
(if (and (some? ref-shape)
|
||||
(or (not= (:component-id shape) (:component-id ref-shape))
|
||||
(not= (:component-file shape) (:component-file ref-shape))))
|
||||
(cond-> shape
|
||||
(some? (:component-id ref-shape))
|
||||
(assoc :component-id (:component-id ref-shape))
|
||||
|
||||
(nil? (:component-id ref-shape))
|
||||
(dissoc :component-id)
|
||||
|
||||
(some? (:component-file ref-shape))
|
||||
(assoc :component-file (:component-file ref-shape))
|
||||
|
||||
(nil? (:component-file ref-shape))
|
||||
(dissoc :component-file))
|
||||
shape))
|
||||
shape))
|
||||
|
||||
(update-page
|
||||
[data page]
|
||||
(d/update-when page :objects d/update-vals (partial fix-shape data page)))
|
||||
|
||||
(fix-data [data]
|
||||
(loop [current-data data
|
||||
iteration 0]
|
||||
(let [next-data (update current-data :pages-index d/update-vals (partial update-page current-data))]
|
||||
(if (or (= current-data next-data)
|
||||
(> iteration 20)) ;; safety bound
|
||||
next-data
|
||||
(recur next-data (inc iteration))))))]
|
||||
(fix-data data))))
|
||||
|
||||
|
||||
(def available-migrations
|
||||
(into (d/ordered-set)
|
||||
["legacy-2"
|
||||
@@ -1839,4 +1892,6 @@
|
||||
"0014-clear-components-nil-objects"
|
||||
"0015-fix-text-attrs-blank-strings"
|
||||
"0015-clean-shadow-color"
|
||||
"0016-copy-fills-from-position-data-to-text-node"]))
|
||||
"0016-copy-fills-from-position-data-to-text-node"
|
||||
"0017-remove-unneeded-objects-from-components"
|
||||
"0018-sync-component-id-with-near-main"]))
|
||||
|
||||
@@ -333,6 +333,31 @@
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :component-id-mismatch
|
||||
[_ {:keys [shape page-id args] :as error} file-data _]
|
||||
(let [repair-shape
|
||||
(fn [shape]
|
||||
; Set the component-id and component-file to the ones of the near main
|
||||
(log/debug :hint (str " -> set component-id to " (:component-id args)))
|
||||
(log/debug :hint (str " -> set component-file to " (:component-file args)))
|
||||
(cond-> shape
|
||||
(some? (:component-id args))
|
||||
(assoc :component-id (:component-id args))
|
||||
|
||||
(nil? (:component-id args))
|
||||
(dissoc :component-id)
|
||||
|
||||
(some? (:component-file args))
|
||||
(assoc :component-file (:component-file args))
|
||||
|
||||
(nil? (:component-file args))
|
||||
(dissoc :component-file)))]
|
||||
|
||||
(log/dbg :hint "repairing shape :component-id-mismatch" :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-is-head
|
||||
[_ {:keys [shape page-id args] :as error} file-data _]
|
||||
(let [repair-shape
|
||||
@@ -499,7 +524,7 @@
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :component-nil-objects-not-allowed
|
||||
[_ {:keys [shape] :as error} file-data _]
|
||||
[_ {component :shape} file-data _] ; in this error the :shape argument is the component
|
||||
(let [repair-component
|
||||
(fn [component]
|
||||
; Remove the objects key, or set it to {} if the component is deleted
|
||||
@@ -511,10 +536,26 @@
|
||||
(log/debug :hint " -> remove :objects")
|
||||
(dissoc component :objects))))]
|
||||
|
||||
(log/dbg :hint "repairing component :component-nil-objects-not-allowed" :id (:id shape) :name (:name shape))
|
||||
(log/dbg :hint "repairing component :component-nil-objects-not-allowed" :id (:id component) :name (:name component))
|
||||
(-> (pcb/empty-changes nil)
|
||||
(pcb/with-library-data file-data)
|
||||
(pcb/update-component (:id shape) repair-component))))
|
||||
(pcb/update-component (:id component) repair-component))))
|
||||
|
||||
(defmethod repair-error :non-deleted-component-cannot-have-objects
|
||||
[_ {component :shape} file-data _] ; in this error the :shape argument is the component
|
||||
(let [repair-component
|
||||
(fn [component]
|
||||
; Remove the :objects field
|
||||
(if-not (:deleted component)
|
||||
(do
|
||||
(log/debug :hint " -> remove :objects")
|
||||
(dissoc component :objects))
|
||||
component))]
|
||||
|
||||
(log/dbg :hint "repairing component :non-deleted-component-cannot-have-objects" :id (:id component) :name (:name component))
|
||||
(-> (pcb/empty-changes nil)
|
||||
(pcb/with-library-data file-data)
|
||||
(pcb/update-component (:id component) repair-component))))
|
||||
|
||||
(defmethod repair-error :invalid-text-touched
|
||||
[_ {:keys [shape page-id] :as error} file-data _]
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
:ref-shape-is-head
|
||||
:ref-shape-is-not-head
|
||||
:shape-ref-in-main
|
||||
:component-id-mismatch
|
||||
:root-main-not-allowed
|
||||
:nested-main-not-allowed
|
||||
:root-copy-not-allowed
|
||||
@@ -59,6 +60,7 @@
|
||||
:not-head-copy-not-allowed
|
||||
:not-component-not-allowed
|
||||
:component-nil-objects-not-allowed
|
||||
:non-deleted-component-cannot-have-objects
|
||||
:instance-head-not-frame
|
||||
:invalid-text-touched
|
||||
:misplaced-slot
|
||||
@@ -326,6 +328,20 @@
|
||||
:component-file (:component-file ref-shape)
|
||||
:component-id (:component-id ref-shape)))))
|
||||
|
||||
(defn- check-ref-component-id
|
||||
"Validate that if the copy has not been swwpped, the component-id and component-file are
|
||||
the same as in the referenced shape in the near main."
|
||||
[shape file page libraries]
|
||||
(when (nil? (ctk/get-swap-slot shape))
|
||||
(when-let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true)]
|
||||
(when (or (not= (:component-id shape) (:component-id ref-shape))
|
||||
(not= (:component-file shape) (:component-file ref-shape)))
|
||||
(report-error :component-id-mismatch
|
||||
"Nested copy component-id and component-file must be the same as the near main"
|
||||
shape file page
|
||||
:component-id (:component-id ref-shape)
|
||||
:component-file (:component-file ref-shape))))))
|
||||
|
||||
(defn- check-empty-swap-slot
|
||||
"Validate that this shape does not have any swap slot."
|
||||
[shape file page]
|
||||
@@ -418,6 +434,7 @@
|
||||
(check-component-not-main-head shape file page libraries)
|
||||
(check-component-not-root shape file page)
|
||||
(check-valid-touched shape file page)
|
||||
(check-ref-component-id shape file page libraries)
|
||||
;; 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
|
||||
@@ -648,6 +665,13 @@
|
||||
"Component main not allowed inside other component"
|
||||
main-instance file component-page))))
|
||||
|
||||
(defn- check-not-objects
|
||||
[component file]
|
||||
(when (d/not-empty? (:objects component))
|
||||
(report-error :non-deleted-component-cannot-have-objects
|
||||
"A non-deleted component cannot have shapes inside"
|
||||
component file nil)))
|
||||
|
||||
(defn- check-component
|
||||
"Validate semantic coherence of a component. Report all errors found."
|
||||
[component file]
|
||||
@@ -656,7 +680,8 @@
|
||||
"Objects list cannot be nil"
|
||||
component file nil))
|
||||
(when-not (:deleted component)
|
||||
(check-main-inside-main component file))
|
||||
(check-main-inside-main component file)
|
||||
(check-not-objects component file))
|
||||
(when (:deleted component)
|
||||
(check-component-duplicate-swap-slot component file)
|
||||
(check-ref-cycles component file))
|
||||
|
||||
@@ -1769,6 +1769,23 @@
|
||||
(pcb/update-shapes changes [(:id dest-shape)] ctk/unhead-shape {:ignore-touched true})
|
||||
changes))
|
||||
|
||||
(defn- check-swapped-main
|
||||
[changes dest-shape origin-shape]
|
||||
;; Only for direct updates (from main to copy). Check if the main shape
|
||||
;; has been swapped. If so, the new component-id and component-file must
|
||||
;; be put into the copy.
|
||||
(if (and (= (:shape-ref dest-shape) (:id origin-shape))
|
||||
(ctk/instance-head? dest-shape)
|
||||
(ctk/instance-head? origin-shape)
|
||||
(or (not= (:component-id dest-shape) (:component-id origin-shape))
|
||||
(not= (:component-file dest-shape) (:component-file origin-shape))))
|
||||
(pcb/update-shapes changes [(:id dest-shape)]
|
||||
#(assoc %
|
||||
:component-id (:component-id origin-shape)
|
||||
:component-file (:component-file origin-shape))
|
||||
{:ignore-touched true})
|
||||
changes))
|
||||
|
||||
(defn- update-attrs
|
||||
"The main function that implements the attribute sync algorithm. Copy
|
||||
attributes that have changed in the origin shape to the dest shape.
|
||||
@@ -1811,6 +1828,8 @@
|
||||
:always
|
||||
(check-detached-main dest-shape origin-shape)
|
||||
:always
|
||||
(check-swapped-main dest-shape origin-shape)
|
||||
:always
|
||||
(generate-update-tokens container dest-shape origin-shape touched omit-touched? nil))
|
||||
|
||||
(let [attr-group (get ctk/sync-attrs attr)
|
||||
|
||||
@@ -132,3 +132,94 @@ Some naming conventions:
|
||||
(if-let [last-period (str/last-index-of s ".")]
|
||||
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
|
||||
[s ""]))
|
||||
|
||||
;; Tree building functions --------------------------------------------------
|
||||
|
||||
"Build tree structure from flat list of paths"
|
||||
|
||||
"`build-tree-root` is the main function to build the tree."
|
||||
|
||||
"Receives a list of segments with 'name' properties representing paths,
|
||||
and a separator string."
|
||||
"E.g segments = [{... :name 'one/two/three'} {... :name 'one/two/four'} {... :name 'one/five'}]"
|
||||
|
||||
"Transforms into a tree structure like:
|
||||
[{:name 'one'
|
||||
:path 'one'
|
||||
:depth 0
|
||||
:leaf nil
|
||||
:children-fn (fn [] [{:name 'two'
|
||||
:path 'one.two'
|
||||
:depth 1
|
||||
:leaf nil
|
||||
:children-fn (fn [] [{... :name 'three'} {... :name 'four'}])}
|
||||
{:name 'five'
|
||||
:path 'one.five'
|
||||
:depth 1
|
||||
:leaf {... :name 'five'}
|
||||
...}])}]"
|
||||
|
||||
(defn- sort-by-children
|
||||
"Sorts segments so that those with children come first."
|
||||
[segments separator]
|
||||
(sort-by (fn [segment]
|
||||
(let [path (split-path (:name segment) :separator separator)
|
||||
path-length (count path)]
|
||||
(if (= path-length 1)
|
||||
1
|
||||
0)))
|
||||
segments))
|
||||
|
||||
(defn- group-by-first-segment
|
||||
"Groups segments by their first path segment and update segment name."
|
||||
[segments separator]
|
||||
(reduce (fn [acc segment]
|
||||
(let [[first-segment & remaining-segments] (split-path (:name segment) :separator separator)
|
||||
rest-path (when (seq remaining-segments) (join-path remaining-segments :separator separator :with-spaces? false))]
|
||||
(update acc first-segment (fnil conj [])
|
||||
(if rest-path
|
||||
(assoc segment :name rest-path)
|
||||
segment))))
|
||||
{}
|
||||
segments))
|
||||
|
||||
(defn- sort-and-group-segments
|
||||
"Sorts elements and groups them by their first path segment."
|
||||
[segments separator]
|
||||
(let [sorted (sort-by-children segments separator)
|
||||
grouped (group-by-first-segment sorted separator)]
|
||||
grouped))
|
||||
|
||||
(defn- build-tree-node
|
||||
"Builds a single tree node with lazy children."
|
||||
[segment-name remaining-segments separator parent-path depth]
|
||||
(let [current-path (if parent-path
|
||||
(str parent-path "." segment-name)
|
||||
segment-name)
|
||||
|
||||
is-leaf? (and (seq remaining-segments)
|
||||
(every? (fn [segment]
|
||||
(let [remaining-segment-name (first (split-path (:name segment) :separator separator))]
|
||||
(= segment-name remaining-segment-name)))
|
||||
remaining-segments))
|
||||
|
||||
leaf-segment (when is-leaf? (first remaining-segments))
|
||||
node {:name segment-name
|
||||
:path current-path
|
||||
:depth depth
|
||||
:leaf leaf-segment
|
||||
:children-fn (when-not is-leaf?
|
||||
(fn []
|
||||
(let [grouped-elements (sort-and-group-segments remaining-segments separator)]
|
||||
(mapv (fn [[child-segment-name remaining-child-segments]]
|
||||
(build-tree-node child-segment-name remaining-child-segments separator current-path (inc depth)))
|
||||
grouped-elements))))}]
|
||||
node))
|
||||
|
||||
(defn build-tree-root
|
||||
"Builds the root level of the tree."
|
||||
[segments separator]
|
||||
(let [grouped-elements (sort-and-group-segments segments separator)]
|
||||
(mapv (fn [[segment-name remaining-segments]]
|
||||
(build-tree-node segment-name remaining-segments separator nil 0))
|
||||
grouped-elements)))
|
||||
|
||||
@@ -274,7 +274,7 @@
|
||||
file-id
|
||||
{file-id file}
|
||||
file-id))]
|
||||
(thf/apply-changes file changes)))
|
||||
(thf/apply-changes file changes :validate? false)))
|
||||
|
||||
(defn swap-component
|
||||
"Swap the specified shape by the component specified by component-tag"
|
||||
@@ -305,12 +305,13 @@
|
||||
[changes nil])
|
||||
|
||||
|
||||
file' (thf/apply-changes file changes)]
|
||||
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
|
||||
(when new-shape-label
|
||||
(thi/rm-id! (:id new-shape))
|
||||
(thi/set-id! new-shape-label (:id new-shape)))
|
||||
(if propagate-fn
|
||||
(propagate-fn file')
|
||||
(-> (propagate-fn file')
|
||||
(thf/validate-file!))
|
||||
file')))
|
||||
|
||||
(defn swap-component-in-shape [file shape-tag component-tag & {:keys [page-label propagate-fn]}]
|
||||
@@ -339,9 +340,10 @@
|
||||
(assoc shape :fills (ths/sample-fills-color :fill-color color)))
|
||||
(:objects page)
|
||||
{})
|
||||
file' (thf/apply-changes file changes)]
|
||||
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
|
||||
(if propagate-fn
|
||||
(propagate-fn file')
|
||||
(-> (propagate-fn file')
|
||||
(thf/validate-file!))
|
||||
file')))
|
||||
|
||||
(defn update-bottom-color
|
||||
@@ -357,9 +359,10 @@
|
||||
(assoc shape :fills (ths/sample-fills-color :fill-color color)))
|
||||
(:objects page)
|
||||
{})
|
||||
file' (thf/apply-changes file changes)]
|
||||
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
|
||||
(if propagate-fn
|
||||
(propagate-fn file')
|
||||
(-> (propagate-fn file')
|
||||
(thf/validate-file!))
|
||||
file')))
|
||||
|
||||
(defn reset-overrides [file shape & {:keys [page-label propagate-fn]}]
|
||||
@@ -374,9 +377,10 @@
|
||||
{file-id file}
|
||||
(ctn/make-container container :page)
|
||||
(:id shape)))
|
||||
file' (thf/apply-changes file changes)]
|
||||
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
|
||||
(if propagate-fn
|
||||
(propagate-fn file')
|
||||
(-> (propagate-fn file')
|
||||
(thf/validate-file!))
|
||||
file')))
|
||||
|
||||
(defn reset-overrides-in-first-child [file shape-tag & {:keys [page-label propagate-fn]}]
|
||||
@@ -398,9 +402,10 @@
|
||||
#{(-> (ths/get-shape file shape-tag :page-label page-label)
|
||||
:id)}
|
||||
{})
|
||||
file' (thf/apply-changes file changes)]
|
||||
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
|
||||
(if propagate-fn
|
||||
(propagate-fn file')
|
||||
(-> (propagate-fn file')
|
||||
(thf/validate-file!))
|
||||
file')))
|
||||
|
||||
(defn duplicate-shape [file shape-tag & {:keys [page-label propagate-fn]}]
|
||||
@@ -419,8 +424,9 @@
|
||||
(:id file)) ;; file-id
|
||||
(cll/generate-duplicate-changes-update-indices (:objects page) ;; objects
|
||||
#{(:id shape)}))
|
||||
file' (thf/apply-changes file changes)]
|
||||
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
|
||||
(if propagate-fn
|
||||
(propagate-fn file')
|
||||
(-> (propagate-fn file')
|
||||
(thf/validate-file!))
|
||||
file')))
|
||||
|
||||
|
||||
@@ -54,12 +54,14 @@
|
||||
([file] (validate-file! file {}))
|
||||
([file libraries]
|
||||
(cfv/validate-file-schema! file)
|
||||
(cfv/validate-file! file libraries)))
|
||||
(cfv/validate-file! file libraries)
|
||||
file))
|
||||
|
||||
(defn apply-changes
|
||||
[file changes]
|
||||
[file changes & {:keys [validate?] :or {validate? true}}]
|
||||
(let [file' (ctf/update-file-data file #(cfc/process-changes % (:redo-changes changes) true))]
|
||||
(validate-file! file')
|
||||
(when validate?
|
||||
(validate-file! file'))
|
||||
file'))
|
||||
|
||||
(defn apply-undo-changes
|
||||
|
||||
@@ -146,12 +146,15 @@
|
||||
"Check if some attribute is one that is involved in component syncrhonization.
|
||||
Note that design tokens also are involved, although they go by an alternate
|
||||
route and thus they are not part of :sync-attrs.
|
||||
Also when detaching a nested copy it also needs to trigger a synchronization,
|
||||
even though :shape-ref is not a synced attribute per se"
|
||||
Also when detaching a nested copy or it also needs to trigger a synchronization,
|
||||
even though :shape-ref or :component-id are not synced attribute per se"
|
||||
[attr]
|
||||
(or (get sync-attrs attr)
|
||||
(= :shape-ref attr)
|
||||
(= :applied-tokens attr)))
|
||||
(= :applied-tokens attr)
|
||||
(= :component-id attr)
|
||||
(= :component-file attr)
|
||||
(= :component-root attr)))
|
||||
|
||||
(defn instance-root?
|
||||
"Check if this shape is the head of a top instance."
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
(some? objects)
|
||||
(assoc :objects objects)
|
||||
|
||||
(nil? objects)
|
||||
(dissoc :objects)
|
||||
|
||||
(some? modified-at)
|
||||
(assoc :modified-at modified-at)
|
||||
|
||||
|
||||
@@ -465,9 +465,10 @@
|
||||
page
|
||||
{(:id file) file}
|
||||
(thi/id :nested-h-ellipse))
|
||||
file' (-> (thf/apply-changes file changes)
|
||||
file' (-> (thf/apply-changes file changes :validate? false)
|
||||
(tho/propagate-component-changes :c-board-with-ellipse)
|
||||
(tho/propagate-component-changes :c-big-board))
|
||||
(tho/propagate-component-changes :c-big-board)
|
||||
(thf/validate-file!))
|
||||
|
||||
;; ==== Get
|
||||
nested2-h-ellipse (ths/get-shape file' :nested-h-ellipse)
|
||||
|
||||
@@ -64,9 +64,8 @@
|
||||
|
||||
(reset-all-overrides [file]
|
||||
(-> file
|
||||
(tho/reset-overrides-in-first-child :frame-board-1 :page-label :page-1)
|
||||
(tho/reset-overrides-in-first-child :copy-board-1 :page-label :page-2)
|
||||
(propagate-all-component-changes)))
|
||||
(tho/reset-overrides-in-first-child :frame-board-1 :page-label :page-1 :propagate-fn propagate-all-component-changes)
|
||||
(tho/reset-overrides-in-first-child :copy-board-1 :page-label :page-2 :propagate-fn propagate-all-component-changes)))
|
||||
|
||||
(fill-colors [file]
|
||||
[(tho/bottom-fill-color file :frame-ellipse-1 :page-label :page-1)
|
||||
|
||||
@@ -56,10 +56,9 @@
|
||||
|
||||
(reset-all-overrides [file]
|
||||
(-> file
|
||||
(tho/reset-overrides (ths/get-shape file :copy-simple-1))
|
||||
(tho/reset-overrides (ths/get-shape file :copy-frame-composed-1))
|
||||
(tho/reset-overrides (ths/get-shape file :composed-1-composed-2-copy))
|
||||
(propagate-all-component-changes)))
|
||||
(tho/reset-overrides (ths/get-shape file :copy-simple-1 :propagate-fn propagate-all-component-changes))
|
||||
(tho/reset-overrides (ths/get-shape file :copy-frame-composed-1 :propagate-fn propagate-all-component-changes))
|
||||
(tho/reset-overrides (ths/get-shape file :composed-1-composed-2-copy :propagate-fn propagate-all-component-changes))))
|
||||
|
||||
(fill-colors [file]
|
||||
[(tho/bottom-fill-color file :frame-simple-1)
|
||||
|
||||
@@ -6,20 +6,12 @@
|
||||
|
||||
(ns common-tests.logic.swap-as-override-test
|
||||
(:require
|
||||
[app.common.files.changes :as ch]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.logic.libraries :as cll]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.data :as d]
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.compositions :as tho]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
@@ -27,23 +19,40 @@
|
||||
(defn- setup []
|
||||
(-> (thf/sample-file :file1)
|
||||
|
||||
(tho/add-simple-component :component-1 :frame-component-1 :child-component-1 :child-params {:name "child-component-1" :type :rect :fills (ths/sample-fills-color :fill-color "#111111")})
|
||||
(tho/add-simple-component :component-2 :frame-component-2 :child-component-2 :child-params {:name "child-component-2" :type :rect :fills (ths/sample-fills-color :fill-color "#222222")})
|
||||
(tho/add-simple-component :component-3 :frame-component-3 :child-component-3 :child-params {:name "child-component-3" :type :rect :fills (ths/sample-fills-color :fill-color "#333333")})
|
||||
(tho/add-simple-component :component-1 :frame-component-1 :child-component-1
|
||||
:root-params {:name "component-1"}
|
||||
:child-params {:name "child-component-1"
|
||||
:type :rect
|
||||
:fills (ths/sample-fills-color :fill-color "#111111")})
|
||||
(tho/add-simple-component :component-2 :frame-component-2 :child-component-2
|
||||
:root-params {:name "component-2"}
|
||||
:child-params {:name "child-component-2"
|
||||
:type :rect
|
||||
:fills (ths/sample-fills-color :fill-color "#222222")})
|
||||
(tho/add-simple-component :component-3 :frame-component-3 :child-component-3
|
||||
:root-params {:name "component-3"}
|
||||
:child-params {:name "child-component-3"
|
||||
:type :rect
|
||||
:fills (ths/sample-fills-color :fill-color "#333333")})
|
||||
|
||||
(tho/add-frame :frame-icon-and-text)
|
||||
(thc/instantiate-component :component-1 :copy-component-1 :parent-label :frame-icon-and-text :children-labels [:component-1-icon-and-text])
|
||||
(tho/add-frame :frame-icon-and-text :name "copy-component-1")
|
||||
(thc/instantiate-component :component-1 :copy-component-1
|
||||
:parent-label :frame-icon-and-text
|
||||
:children-labels [:component-1-icon-and-text])
|
||||
(ths/add-sample-shape :text
|
||||
{:type :text
|
||||
:name "icon+text"
|
||||
:parent-label :frame-icon-and-text})
|
||||
(thc/make-component :icon-and-text :frame-icon-and-text)
|
||||
|
||||
(tho/add-frame :frame-panel)
|
||||
(thc/instantiate-component :icon-and-text :copy-icon-and-text :parent-label :frame-panel :children-labels [:icon-and-text-panel])
|
||||
(tho/add-frame :frame-panel :name "icon-and-text")
|
||||
(thc/instantiate-component :icon-and-text :copy-icon-and-text
|
||||
:parent-label :frame-panel
|
||||
:children-labels [:icon-and-text-panel])
|
||||
(thc/make-component :panel :frame-panel)
|
||||
|
||||
(thc/instantiate-component :panel :copy-panel :children-labels [:copy-icon-and-text-panel])))
|
||||
(thc/instantiate-component :panel :copy-panel
|
||||
:children-labels [:copy-icon-and-text-panel])))
|
||||
|
||||
(defn- propagate-all-component-changes [file]
|
||||
(-> file
|
||||
|
||||
@@ -58,10 +58,10 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
|
||||
async waitForTextSpan(nth = 0) {
|
||||
if (!nth) {
|
||||
return this.page.waitForSelector('[data-itype="span"]');
|
||||
return this.page.waitForSelector('[data-itype="inline"]');
|
||||
}
|
||||
return this.page.waitForSelector(
|
||||
`[data-itype="span"]:nth-child(${nth})`,
|
||||
`[data-itype="inline"]:nth-child(${nth})`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ const setupEmptyTokensFile = async (page, options = {}) => {
|
||||
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
|
||||
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
|
||||
tokenSetItems: workspacePage.tokenSetItems,
|
||||
tokensSidebar: workspacePage.tokensSidebar,
|
||||
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
|
||||
tokenContextMenuForSet: workspacePage.tokenContextMenuForSet,
|
||||
};
|
||||
@@ -110,15 +111,12 @@ const checkInputFieldWithError = async (
|
||||
).toBeVisible();
|
||||
};
|
||||
|
||||
const checkInputFieldWithoutError = async (
|
||||
tokenThemeUpdateCreateModal,
|
||||
inputLocator,
|
||||
) => {
|
||||
const checkInputFieldWithoutError = async (inputLocator) => {
|
||||
expect(await inputLocator.getAttribute("aria-invalid")).toBeNull();
|
||||
expect(await inputLocator.getAttribute("aria-describedby")).toBeNull();
|
||||
};
|
||||
|
||||
async function testTokenCreationFlow(
|
||||
const testTokenCreationFlow = async (
|
||||
page,
|
||||
{
|
||||
tokenLabel,
|
||||
@@ -132,7 +130,7 @@ async function testTokenCreationFlow(
|
||||
resolvedValueText,
|
||||
secondResolvedValueText,
|
||||
},
|
||||
) {
|
||||
) => {
|
||||
const invalidValueError = "Invalid token value";
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
const selfReferenceError = "Token has self reference";
|
||||
@@ -242,7 +240,45 @@ async function testTokenCreationFlow(
|
||||
await expect(
|
||||
tokensTabPanel.getByRole("button", { name: "my-token-2" }),
|
||||
).toBeEnabled();
|
||||
}
|
||||
};
|
||||
|
||||
const unfoldTokenTree = async (tokensTabPanel, type, tokenName) => {
|
||||
const tokenSegments = tokenName.split(".");
|
||||
const tokenFolderTree = tokenSegments.slice(0, -1);
|
||||
const tokenLeafName = tokenSegments.pop();
|
||||
|
||||
const typeParentWrapper = tokensTabPanel.getByTestId(`section-${type}`);
|
||||
const typeSectionButton = typeParentWrapper
|
||||
.getByRole("button", {
|
||||
name: type,
|
||||
})
|
||||
.first();
|
||||
|
||||
const isSectionExpanded =
|
||||
await typeSectionButton.getAttribute("aria-expanded");
|
||||
|
||||
if (isSectionExpanded === "false") {
|
||||
await typeSectionButton.click();
|
||||
}
|
||||
|
||||
for (const segment of tokenFolderTree) {
|
||||
const segmentButton = typeParentWrapper
|
||||
.getByRole("listitem")
|
||||
.getByRole("button", { name: segment })
|
||||
.first();
|
||||
|
||||
const isExpanded = await segmentButton.getAttribute("aria-expanded");
|
||||
if (isExpanded === "false") {
|
||||
await segmentButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
await expect(
|
||||
typeParentWrapper.getByRole("button", {
|
||||
name: tokenLeafName,
|
||||
}),
|
||||
).toBeEnabled();
|
||||
};
|
||||
|
||||
test.describe("Tokens: Tokens Tab", () => {
|
||||
test("Clicking tokens tab button opens tokens sidebar tab", async ({
|
||||
@@ -398,15 +434,12 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
const selfReferenceError = "Token has self reference";
|
||||
const missingReferenceError = "Missing token references";
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
const addTokenButton = tokensTabPanel.getByRole("button", {
|
||||
name: `Add Token: Color`,
|
||||
});
|
||||
|
||||
await addTokenButton.click();
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "Add Token: Color" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
// Placeholder checks
|
||||
@@ -471,38 +504,34 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
await expect(submitButton).toBeEnabled();
|
||||
await submitButton.click();
|
||||
|
||||
await expect(
|
||||
tokensTabPanel.getByRole("button", {
|
||||
name: "color.primary",
|
||||
}),
|
||||
).toBeEnabled();
|
||||
await unfoldTokenTree(tokensSidebar, "color", "color.primary");
|
||||
|
||||
// Create token referencing the previous one with keyboard
|
||||
|
||||
await tokensTabPanel
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "Add Token: Color" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
await nameField.click();
|
||||
await nameField.fill("color.secondary");
|
||||
await nameField.fill("secondary");
|
||||
await nameField.press("Tab");
|
||||
|
||||
await valueField.click();
|
||||
await valueField.fill("{color.primary}");
|
||||
|
||||
await expect(submitButton).toBeEnabled();
|
||||
await nameField.press("Enter");
|
||||
await submitButton.press("Enter");
|
||||
|
||||
await expect(
|
||||
tokensTabPanel.getByRole("button", {
|
||||
name: "color.secondary",
|
||||
tokensSidebar.getByRole("button", {
|
||||
name: "secondary",
|
||||
}),
|
||||
).toBeEnabled();
|
||||
|
||||
// Tokens tab panel should have two tokens with the color red / #ff0000
|
||||
await expect(
|
||||
tokensTabPanel.getByRole("button", { name: "#ff0000" }),
|
||||
tokensSidebar.getByRole("button", { name: "#ff0000" }),
|
||||
).toHaveCount(2);
|
||||
|
||||
// Global set has been auto created and is active
|
||||
@@ -518,7 +547,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
).toHaveAttribute("aria-checked", "true");
|
||||
|
||||
// Check color picker
|
||||
await tokensTabPanel
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "Add Token: Color" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
@@ -1079,7 +1108,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFile(page, {flags: ["enable-token-shadow"]});
|
||||
await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -1507,24 +1536,15 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
test("User edits token and auto created set show up in the sidebar", async ({
|
||||
page,
|
||||
}) => {
|
||||
const {
|
||||
workspacePage,
|
||||
tokensUpdateCreateModal,
|
||||
tokenThemesSetsSidebar,
|
||||
tokensSidebar,
|
||||
tokenContextMenuForToken,
|
||||
} = await setupTokensFile(page);
|
||||
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
const tokensColorGroup = tokensSidebar.getByRole("button", {
|
||||
name: "Color 92",
|
||||
});
|
||||
await expect(tokensColorGroup).toBeVisible();
|
||||
await tokensColorGroup.click();
|
||||
await unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
|
||||
|
||||
const colorToken = tokensSidebar.getByRole("button", {
|
||||
name: "colors.blue.100",
|
||||
name: "100",
|
||||
});
|
||||
await expect(colorToken).toBeVisible();
|
||||
await colorToken.click({ button: "right" });
|
||||
@@ -1541,8 +1561,10 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
|
||||
await unfoldTokenTree(tokensSidebar, "color", "colors.blue.100.changed");
|
||||
|
||||
const colorTokenChanged = tokensSidebar.getByRole("button", {
|
||||
name: "colors.blue.100.changed",
|
||||
name: "changed",
|
||||
});
|
||||
await expect(colorTokenChanged).toBeVisible();
|
||||
});
|
||||
@@ -1633,11 +1655,10 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
});
|
||||
|
||||
test("User creates grouped color token", async ({ page }) => {
|
||||
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
await tokensTabPanel
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "Add Token: Color" })
|
||||
.click();
|
||||
|
||||
@@ -1649,7 +1670,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
||||
|
||||
await nameField.click();
|
||||
await nameField.fill("color.dark.primary");
|
||||
await nameField.fill("dark.primary");
|
||||
|
||||
await valueField.click();
|
||||
await valueField.fill("red");
|
||||
@@ -1660,7 +1681,9 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
await expect(submitButton).toBeEnabled();
|
||||
await submitButton.click();
|
||||
|
||||
await expect(tokensTabPanel.getByLabel("color.dark.primary")).toBeEnabled();
|
||||
await unfoldTokenTree(tokensSidebar, "color", "dark.primary");
|
||||
|
||||
await expect(tokensSidebar.getByLabel("primary")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("User cant create regular token with value missing", async ({
|
||||
@@ -1676,7 +1699,6 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
@@ -1686,7 +1708,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
|
||||
// Fill in name but leave value empty
|
||||
await nameField.click();
|
||||
await nameField.fill("color.primary");
|
||||
await nameField.fill("primary");
|
||||
|
||||
// Submit button should remain disabled when value is empty
|
||||
await expect(submitButton).toBeDisabled();
|
||||
@@ -1704,7 +1726,6 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
.click();
|
||||
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
||||
|
||||
await valueField.click();
|
||||
@@ -1754,15 +1775,10 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
const tokensColorGroup = tokensSidebar.getByRole("button", {
|
||||
name: "Color 92",
|
||||
});
|
||||
|
||||
await expect(tokensColorGroup).toBeVisible();
|
||||
await tokensColorGroup.click();
|
||||
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
|
||||
|
||||
const colorToken = tokensSidebar.getByRole("button", {
|
||||
name: "colors.blue.100",
|
||||
name: "100",
|
||||
});
|
||||
|
||||
await colorToken.click({ button: "right" });
|
||||
@@ -1782,15 +1798,10 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
const tokensColorGroup = tokensSidebar.getByRole("button", {
|
||||
name: "Color 92",
|
||||
});
|
||||
await expect(tokensColorGroup).toBeVisible();
|
||||
|
||||
await tokensColorGroup.click();
|
||||
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
|
||||
|
||||
const colorToken = tokensSidebar.getByRole("button", {
|
||||
name: "colors.blue.100",
|
||||
name: "100",
|
||||
});
|
||||
await expect(colorToken).toBeVisible();
|
||||
await colorToken.click({ button: "right" });
|
||||
@@ -1803,8 +1814,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
});
|
||||
|
||||
test("User fold/unfold color tokens", async ({ page }) => {
|
||||
const { tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFile(page);
|
||||
const { tokensSidebar } = await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
@@ -1814,8 +1824,10 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
await expect(tokensColorGroup).toBeVisible();
|
||||
await tokensColorGroup.click();
|
||||
|
||||
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
|
||||
|
||||
const colorToken = tokensSidebar.getByRole("button", {
|
||||
name: "colors.blue.100",
|
||||
name: "100",
|
||||
});
|
||||
await expect(colorToken).toBeVisible();
|
||||
await tokensColorGroup.click();
|
||||
@@ -2218,13 +2230,10 @@ test.describe("Tokens: Apply token", () => {
|
||||
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||
await tokensTabButton.click();
|
||||
|
||||
await tokensSidebar
|
||||
.getByRole("button")
|
||||
.filter({ hasText: "Color" })
|
||||
.click();
|
||||
unfoldTokenTree(tokensSidebar, "color", "colors.black");
|
||||
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "colors.black" })
|
||||
.getByRole("button", { name: "black" })
|
||||
.click({ button: "right" });
|
||||
await tokenContextMenuForToken.getByText("Fill").click();
|
||||
|
||||
@@ -2462,7 +2471,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("shadow.primary");
|
||||
await nameField.fill("primary");
|
||||
|
||||
// User adds first shadow with a color from the color ramp
|
||||
const firstShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||
@@ -2709,9 +2718,11 @@ test.describe("Tokens: Apply token", () => {
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
|
||||
unfoldTokenTree(tokensSidebar, "shadow", "primary");
|
||||
|
||||
// Verify token appears in sidebar
|
||||
const shadowToken = tokensSidebar.getByRole("button", {
|
||||
name: "shadow.primary",
|
||||
name: "primary",
|
||||
});
|
||||
await expect(shadowToken).toBeEnabled();
|
||||
|
||||
|
||||
49
frontend/src/app/main/ui/ds/layers/layer_button.cljs
Normal file
49
frontend/src/app/main/ui/ds/layers/layer_button.cljs
Normal file
@@ -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
|
||||
|
||||
(ns app.main.ui.ds.layers.layer-button
|
||||
(:require-macros
|
||||
[app.main.style :as stl])
|
||||
(:require
|
||||
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema:layer-button
|
||||
[:map
|
||||
[:label :string]
|
||||
[:description {:optional true} [:maybe :string]]
|
||||
[:class {:optional true} :string]
|
||||
[:expandable {:optional true} :boolean]
|
||||
[:expanded {:optional true} :boolean]
|
||||
[:icon {:optional true} :string]
|
||||
[:on-toggle-expand fn?]])
|
||||
|
||||
(mf/defc layer-button*
|
||||
{::mf/schema schema:layer-button}
|
||||
[{:keys [label description class is-expandable expanded icon on-toggle-expand children] :rest props}]
|
||||
(let [button-props (mf/spread-props props
|
||||
{:class [class (stl/css-case :layer-button true
|
||||
:layer-button--expandable is-expandable
|
||||
:layer-button--expanded expanded)]
|
||||
:type "button"
|
||||
:on-click on-toggle-expand})]
|
||||
[:div {:class (stl/css :layer-button-wrapper)}
|
||||
[:> "button" button-props
|
||||
[:div {:class (stl/css :layer-button-content)}
|
||||
(when is-expandable
|
||||
(if expanded
|
||||
[:> icon* {:icon-id i/arrow-down :class (stl/css :folder-node-icon)}]
|
||||
[:> icon* {:icon-id i/arrow-right :class (stl/css :folder-node-icon)}]))
|
||||
(when icon
|
||||
[:> icon* {:icon-id icon :class (stl/css :layer-button-icon)}])
|
||||
[:span {:class (stl/css :layer-button-name)}
|
||||
label]
|
||||
(when description
|
||||
[:span {:class (stl/css :layer-button-description)}
|
||||
description])
|
||||
[:span {:class (stl/css :layer-button-quantity)}]]]
|
||||
[:div {:class (stl/css :layer-button-actions)}
|
||||
children]]))
|
||||
56
frontend/src/app/main/ui/ds/layers/layer_button.scss
Normal file
56
frontend/src/app/main/ui/ds/layers/layer_button.scss
Normal file
@@ -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 "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "ds/colors.scss" as *;
|
||||
|
||||
.layer-button-wrapper {
|
||||
--layer-button-block-size: #{$sz-32};
|
||||
--layer-button-background: var(--color-background-primary);
|
||||
--layer-button-text: var(--color-foreground-secondary);
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
block-size: var(--layer-button-block-size);
|
||||
|
||||
background: var(--layer-button-background);
|
||||
color: var(--layer-button-text);
|
||||
}
|
||||
|
||||
.layer-button {
|
||||
@include use-typography("body-small");
|
||||
|
||||
appearance: none;
|
||||
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.layer-button--expanded {
|
||||
& .layer-button-name {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.layer-button-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.layer-button-description {
|
||||
padding: var(--sp-xs);
|
||||
background-color: var(--color-background-tertiary);
|
||||
border-radius: $br-6;
|
||||
}
|
||||
@@ -346,19 +346,17 @@
|
||||
{:value (:id variant)
|
||||
:key (pr-str variant)
|
||||
:label (:name variant)})))
|
||||
variant-options (if (or (= font-variant-id :multiple) (= font-variant-id "mixed"))
|
||||
variant-options (if (= font-variant-id :multiple)
|
||||
(conj basic-variant-options
|
||||
{:value ""
|
||||
:key :multiple-variants
|
||||
:label "--"})
|
||||
basic-variant-options)
|
||||
font-variant-value (attr->string font-variant-id)
|
||||
font-variant-value (if (= font-variant-value "mixed") "" font-variant-value)]
|
||||
basic-variant-options)]
|
||||
|
||||
;; TODO Add disabled mode
|
||||
[:& select
|
||||
{:class (stl/css :font-variant-select)
|
||||
:default-value font-variant-value
|
||||
:default-value (attr->string font-variant-id)
|
||||
:options variant-options
|
||||
:on-change on-font-variant-change
|
||||
:on-blur on-blur}])]]]))
|
||||
|
||||
@@ -44,6 +44,39 @@
|
||||
[(seq (array/sort! empty))
|
||||
(seq (array/sort! filled))]))))
|
||||
|
||||
(mf/defc selected-set-info*
|
||||
{::mf/private true}
|
||||
[{:keys [tokens-lib selected-token-set-id]}]
|
||||
(let [selected-token-set
|
||||
(mf/with-memo [tokens-lib]
|
||||
(when selected-token-set-id
|
||||
(some-> tokens-lib (ctob/get-set selected-token-set-id))))
|
||||
|
||||
active-token-sets-names
|
||||
(mf/with-memo [tokens-lib]
|
||||
(some-> tokens-lib (ctob/get-active-themes-set-names)))
|
||||
|
||||
token-set-active?
|
||||
(mf/use-fn
|
||||
(mf/deps active-token-sets-names)
|
||||
(fn [name]
|
||||
(contains? active-token-sets-names name)))]
|
||||
[:div {:class (stl/css :sets-header-container)}
|
||||
[:> text* {:as "span"
|
||||
:typography "headline-small"
|
||||
:class (stl/css :sets-header)}
|
||||
(tr "workspace.tokens.tokens-section-title" (ctob/get-name selected-token-set))]
|
||||
[:div {:class (stl/css :sets-header-status) :title (tr "workspace.tokens.inactive-set-description")}
|
||||
;; NOTE: when no set in tokens-lib, the selected-token-set-id
|
||||
;; will be `nil`, so for properly hide the inactive message we
|
||||
;; check that at least `selected-token-set-id` has a value
|
||||
(when (and (some? selected-token-set-id)
|
||||
(not (token-set-active? (ctob/get-name selected-token-set))))
|
||||
[:*
|
||||
[:> icon* {:class (stl/css :sets-header-status-icon) :icon-id i/eye-off}]
|
||||
[:> text* {:as "span" :typography "body-small" :class (stl/css :sets-header-status-text)}
|
||||
(tr "workspace.tokens.inactive-set")]])]]))
|
||||
|
||||
(mf/defc tokens-section*
|
||||
{::mf/private true}
|
||||
[{:keys [tokens-lib active-tokens resolved-active-tokens]}]
|
||||
@@ -65,9 +98,7 @@
|
||||
selected-token-set-id
|
||||
(mf/deref refs/selected-token-set-id)
|
||||
|
||||
selected-token-set
|
||||
(when selected-token-set-id
|
||||
(some-> tokens-lib (ctob/get-set selected-token-set-id)))
|
||||
|
||||
|
||||
;; If we have not selected any set explicitly we just
|
||||
;; select the first one from the list of sets
|
||||
@@ -92,15 +123,9 @@
|
||||
tokens)]
|
||||
(ctob/group-by-type tokens)))
|
||||
|
||||
active-token-sets-names
|
||||
(mf/with-memo [tokens-lib]
|
||||
(some-> tokens-lib (ctob/get-active-themes-set-names)))
|
||||
|
||||
token-set-active?
|
||||
(mf/use-fn
|
||||
(mf/deps active-token-sets-names)
|
||||
(fn [name]
|
||||
(contains? active-token-sets-names name)))
|
||||
|
||||
|
||||
|
||||
[empty-group filled-group]
|
||||
(mf/with-memo [tokens-by-type]
|
||||
@@ -118,34 +143,27 @@
|
||||
|
||||
[:*
|
||||
[:& token-context-menu]
|
||||
[:div {:class (stl/css :sets-header-container)}
|
||||
[:> text* {:as "span" :typography "headline-small" :class (stl/css :sets-header)} (tr "workspace.tokens.tokens-section-title" (ctob/get-name selected-token-set))]
|
||||
[:div {:class (stl/css :sets-header-status) :title (tr "workspace.tokens.inactive-set-description")}
|
||||
;; NOTE: when no set in tokens-lib, the selected-token-set-id
|
||||
;; will be `nil`, so for properly hide the inactive message we
|
||||
;; check that at least `selected-token-set-id` has a value
|
||||
(when (and (some? selected-token-set-id)
|
||||
(not (token-set-active? (ctob/get-name selected-token-set))))
|
||||
[:*
|
||||
[:> icon* {:class (stl/css :sets-header-status-icon) :icon-id i/eye-off}]
|
||||
[:> text* {:as "span" :typography "body-small" :class (stl/css :sets-header-status-text)}
|
||||
(tr "workspace.tokens.inactive-set")]])]]
|
||||
|
||||
[:& selected-set-info* {:tokens-lib tokens-lib
|
||||
:selected-token-set-id selected-token-set-id}]
|
||||
|
||||
(for [type filled-group]
|
||||
(let [tokens (get tokens-by-type type)]
|
||||
[:> token-group* {:key (name type)
|
||||
:is-open (get open-status type false)
|
||||
:tokens tokens
|
||||
:is-expanded (get open-status type false)
|
||||
:type type
|
||||
:selected-ids selected
|
||||
:selected-shapes selected-shapes
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens resolved-active-tokens
|
||||
:tokens tokens}]))
|
||||
:tokens-lib tokens-lib
|
||||
:selected-token-set-id selected-token-set-id}]))
|
||||
|
||||
(for [type empty-group]
|
||||
[:> token-group* {:key (name type)
|
||||
:tokens []
|
||||
:type type
|
||||
:selected-shapes selected-shapes
|
||||
:is-selected-inside-layout :is-selected-inside-layout
|
||||
:active-theme-tokens resolved-active-tokens
|
||||
:tokens []}])]))
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens resolved-active-tokens}])]))
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
(ns app.main.ui.workspace.tokens.management.group
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
@@ -16,51 +19,70 @@
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.workspace.sidebar.assets.common :as cmm]
|
||||
[app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]]
|
||||
[app.main.ui.ds.layers.layer-button :refer [layer-button*]]
|
||||
[app.main.ui.workspace.tokens.management.token-tree :refer [token-tree*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
|
||||
(defn token-section-icon
|
||||
[type]
|
||||
(case type
|
||||
:border-radius "corner-radius"
|
||||
:color "drop"
|
||||
:boolean "boolean-difference"
|
||||
:font-family "text-font-family"
|
||||
:font-size "text-font-size"
|
||||
:letter-spacing "text-letterspacing"
|
||||
:text-case "text-mixed"
|
||||
:text-decoration "text-underlined"
|
||||
:font-weight "text-font-weight"
|
||||
:typography "text-typography"
|
||||
:opacity "percentage"
|
||||
:number "number"
|
||||
:rotation "rotation"
|
||||
:spacing "padding-extended"
|
||||
:string "text-mixed"
|
||||
:stroke-width "stroke-size"
|
||||
:dimensions "expand"
|
||||
:sizing "expand"
|
||||
:shadow "drop-shadow"
|
||||
:border-radius i/corner-radius
|
||||
:color i/drop
|
||||
:boolean i/boolean-difference
|
||||
:font-family i/text-font-family
|
||||
:font-size i/text-font-size
|
||||
:letter-spacing i/text-letterspacing
|
||||
:text-case i/text-mixed
|
||||
:text-decoration i/text-underlined
|
||||
:font-weight i/text-font-weight
|
||||
:typography i/text-typography
|
||||
:opacity i/percentage
|
||||
:number i/number
|
||||
:rotation i/rotation
|
||||
:spacing i/padding-extended
|
||||
:string i/text-mixed
|
||||
:stroke-width i/stroke-size
|
||||
:dimensions i/expand
|
||||
:sizing i/expand
|
||||
:shadow i/drop-shadow
|
||||
"add"))
|
||||
|
||||
(def ^:private schema:token-group
|
||||
[:map
|
||||
[:type :keyword]
|
||||
[:tokens :any]
|
||||
[:selected-shapes :any]
|
||||
[:is-selected-inside-layout {:optional true} [:maybe :boolean]]
|
||||
[:active-theme-tokens {:optional true} :any]
|
||||
[:selected-token-set-id {:optional true} :any]
|
||||
[:tokens-lib {:optional true} :any]
|
||||
[:on-token-pill-click {:optional true} fn?]
|
||||
[:on-context-menu {:optional true} fn?]])
|
||||
|
||||
(mf/defc token-group*
|
||||
{::mf/private true}
|
||||
[{:keys [type tokens selected-shapes is-selected-inside-layout active-theme-tokens is-open selected-ids]}]
|
||||
{::mf/schema schema:token-group}
|
||||
[{:keys [type tokens selected-shapes is-selected-inside-layout active-theme-tokens selected-token-set-id tokens-lib is-expanded selected-ids]}]
|
||||
(let [{:keys [modal title]}
|
||||
(get dwta/token-properties type)
|
||||
editing-ref (mf/deref refs/workspace-editor-state)
|
||||
not-editing? (empty? editing-ref)
|
||||
|
||||
is-expanded (d/nilv is-expanded false)
|
||||
|
||||
can-edit?
|
||||
(mf/use-ctx ctx/can-edit?)
|
||||
|
||||
is-selected-inside-layout (d/nilv is-selected-inside-layout false)
|
||||
|
||||
tokens
|
||||
(mf/with-memo [tokens]
|
||||
(vec (sort-by :name tokens)))
|
||||
|
||||
expandable? (d/nilv (seq tokens) false)
|
||||
|
||||
on-context-menu
|
||||
(mf/use-fn
|
||||
(fn [event token]
|
||||
@@ -73,8 +95,8 @@
|
||||
|
||||
on-toggle-open-click
|
||||
(mf/use-fn
|
||||
(mf/deps is-open type)
|
||||
#(st/emit! (dwtl/set-token-type-section-open type (not is-open))))
|
||||
(mf/deps is-expanded type)
|
||||
#(st/emit! (dwtl/set-token-type-section-open type (not is-expanded))))
|
||||
|
||||
on-popover-open-click
|
||||
(mf/use-fn
|
||||
@@ -96,33 +118,36 @@
|
||||
(mf/use-fn
|
||||
(mf/deps not-editing? selected-ids)
|
||||
(fn [event token]
|
||||
(dom/stop-propagation event)
|
||||
(when (and not-editing? (seq selected-shapes) (not= (:type token) :number))
|
||||
(st/emit! (dwta/toggle-token {:token token
|
||||
:shape-ids selected-ids})))))]
|
||||
(let [token (ctob/get-token tokens-lib selected-token-set-id (:id token))]
|
||||
(dom/stop-propagation event)
|
||||
(when (and not-editing? (seq selected-shapes) (not= (:type token) :number))
|
||||
(st/emit! (dwta/toggle-token {:token token
|
||||
:shape-ids selected-ids}))))))]
|
||||
|
||||
[:div {:on-click on-toggle-open-click :class (stl/css :token-section-wrapper)}
|
||||
[:> cmm/asset-section* {:icon (token-section-icon type)
|
||||
:title title
|
||||
:section :tokens
|
||||
:assets-count (count tokens)
|
||||
:is-open is-open}
|
||||
[:> cmm/asset-section-block* {:role :title-button}
|
||||
(when can-edit?
|
||||
[:> icon-button* {:on-click on-popover-open-click
|
||||
:variant "ghost"
|
||||
:icon i/add
|
||||
:id (str "add-token-button-" title)
|
||||
:aria-label (tr "workspace.tokens.add-token" title)}])]
|
||||
(when is-open
|
||||
[:> cmm/asset-section-block* {:role :content}
|
||||
[:div {:class (stl/css :token-pills-wrapper)}
|
||||
(for [token tokens]
|
||||
[:> token-pill*
|
||||
{:key (:name token)
|
||||
:token token
|
||||
:selected-shapes selected-shapes
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens active-theme-tokens
|
||||
:on-click on-token-pill-click
|
||||
:on-context-menu on-context-menu}])]])]]))
|
||||
[:div {:class (stl/css :token-section-wrapper)
|
||||
:data-testid (dm/str "section-" (name type))}
|
||||
[:> layer-button* {:label title
|
||||
:expanded is-expanded
|
||||
:description (when expandable? (dm/str (count tokens)))
|
||||
:is-expandable expandable?
|
||||
:aria-expanded is-expanded
|
||||
:aria-controls (dm/str "token-tree-" (name type))
|
||||
:on-toggle-expand on-toggle-open-click
|
||||
:icon (token-section-icon type)}
|
||||
(when can-edit?
|
||||
[:> icon-button* {:id (str "add-token-button-" title)
|
||||
:icon "add"
|
||||
:aria-label (tr "workspace.tokens.add-token" title)
|
||||
:variant "ghost"
|
||||
:on-click on-popover-open-click
|
||||
:class (stl/css :token-section-icon)}])]
|
||||
(when is-expanded
|
||||
[:> token-tree* {:tokens tokens
|
||||
:id (dm/str "token-tree-" (name type))
|
||||
:tokens-lib tokens-lib
|
||||
:selected-shapes selected-shapes
|
||||
:active-theme-tokens active-theme-tokens
|
||||
:selected-token-set-id selected-token-set-id
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:on-token-pill-click on-token-pill-click
|
||||
:on-context-menu on-context-menu}])]))
|
||||
|
||||
@@ -1,11 +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
|
||||
|
||||
.token-pills-wrapper {
|
||||
display: flex;
|
||||
gap: var(--sp-xs);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -307,10 +307,9 @@
|
||||
:class (stl/css :token-pill-icon)}])
|
||||
|
||||
(if contains-path?
|
||||
(let [[first-part last-part] (cpn/split-by-last-period name)]
|
||||
(let [[_ last-part] (cpn/split-by-last-period name)]
|
||||
[:span {:class (stl/css :divided-name-wrapper)
|
||||
:aria-label name}
|
||||
[:span {:class (stl/css :first-name-wrapper)} first-part]
|
||||
[:span {:class (stl/css :last-name-wrapper)} last-part]])
|
||||
[:span {:class (stl/css :name-wrapper)
|
||||
:aria-label name}
|
||||
|
||||
@@ -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.main.ui.workspace.tokens.management.token-tree
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.path-names :as cpn]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.ui.ds.layers.layer-button :refer [layer-button*]]
|
||||
[app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema:folder-node
|
||||
[:map
|
||||
[:node :any]
|
||||
[:selected-shapes :any]
|
||||
[:is-selected-inside-layout {:optional true} :boolean]
|
||||
[:active-theme-tokens {:optional true} :any]
|
||||
[:selected-token-set-id {:optional true} :any]
|
||||
[:tokens-lib {:optional true} :any]
|
||||
[:on-token-pill-click {:optional true} fn?]
|
||||
[:on-context-menu {:optional true} fn?]])
|
||||
|
||||
(mf/defc folder-node*
|
||||
{::mf/schema schema:folder-node}
|
||||
[{:keys [node selected-shapes is-selected-inside-layout active-theme-tokens selected-token-set-id tokens-lib on-token-pill-click on-context-menu]}]
|
||||
(let [expanded* (mf/use-state false)
|
||||
expanded (deref expanded*)
|
||||
swap-folder-expanded #(swap! expanded* not)]
|
||||
[:li {:class (stl/css :folder-node)}
|
||||
[:> layer-button* {:label (:name node)
|
||||
:expanded expanded
|
||||
:aria-expanded expanded
|
||||
:aria-controls (str "folder-children-" (:path node))
|
||||
:is-expandable (not (:leaf node))
|
||||
:on-toggle-expand swap-folder-expanded}]
|
||||
(when expanded
|
||||
(let [children-fn (:children-fn node)]
|
||||
[:div {:class (stl/css :folder-children-wrapper)
|
||||
:id (str "folder-children-" (:path node))}
|
||||
(when children-fn
|
||||
(let [children (children-fn)]
|
||||
(for [child children]
|
||||
(if (not (:leaf child))
|
||||
[:ul {:class (stl/css :node-parent)}
|
||||
[:> folder-node* {:key (:path child)
|
||||
:node child
|
||||
:selected-shapes selected-shapes
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens active-theme-tokens
|
||||
:on-token-pill-click on-token-pill-click
|
||||
:on-context-menu on-context-menu
|
||||
:tokens-lib tokens-lib
|
||||
:selected-token-set-id selected-token-set-id}]]
|
||||
(let [id (:id (:leaf child))
|
||||
token (ctob/get-token tokens-lib selected-token-set-id id)]
|
||||
[:> token-pill*
|
||||
{:key id
|
||||
:token token
|
||||
:selected-shapes selected-shapes
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens active-theme-tokens
|
||||
:on-click on-token-pill-click
|
||||
:on-context-menu on-context-menu}])))))]))]))
|
||||
|
||||
(def ^:private schema:token-tree
|
||||
[:map
|
||||
[:tokens :any]
|
||||
[:selected-shapes :any]
|
||||
[:is-selected-inside-layout {:optional true} :boolean]
|
||||
[:active-theme-tokens {:optional true} :any]
|
||||
[:selected-token-set-id {:optional true} :any]
|
||||
[:tokens-lib {:optional true} :any]
|
||||
[:on-token-pill-click {:optional true} fn?]
|
||||
[:on-context-menu {:optional true} fn?]])
|
||||
|
||||
(mf/defc token-tree*
|
||||
{::mf/schema schema:token-tree}
|
||||
[{:keys [tokens selected-shapes is-selected-inside-layout active-theme-tokens tokens-lib selected-token-set-id on-token-pill-click on-context-menu]}]
|
||||
(let [separator "."
|
||||
tree (mf/use-memo
|
||||
(mf/deps tokens)
|
||||
(fn []
|
||||
(cpn/build-tree-root tokens separator)))]
|
||||
[:div {:class (stl/css :token-tree-wrapper)}
|
||||
(for [node tree]
|
||||
[:ul {:class (stl/css :node-parent)
|
||||
:key (:path node)
|
||||
:style {:--node-depth (inc (:depth node))}}
|
||||
(if (:leaf node)
|
||||
(let [token (ctob/get-token tokens-lib selected-token-set-id (get-in node [:leaf :id]))]
|
||||
[:> token-pill*
|
||||
{:token token
|
||||
:selected-shapes selected-shapes
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens active-theme-tokens
|
||||
:on-click on-token-pill-click
|
||||
:on-context-menu on-context-menu}])
|
||||
;; Render segment folder
|
||||
[:> folder-node* {:node node
|
||||
:selected-shapes selected-shapes
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens active-theme-tokens
|
||||
:on-token-pill-click on-token-pill-click
|
||||
:on-context-menu on-context-menu
|
||||
:tokens-lib tokens-lib
|
||||
:selected-token-set-id selected-token-set-id}])])]))
|
||||
@@ -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
|
||||
|
||||
@use "ds/_borders.scss" as *;
|
||||
|
||||
.token-tree-wrapper {
|
||||
padding-block-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.node-parent {
|
||||
--node-spacing: var(--sp-l);
|
||||
--node-depth: 0;
|
||||
|
||||
margin-block-end: 0;
|
||||
padding-inline-start: calc(var(--node-spacing) * var(--node-depth));
|
||||
}
|
||||
|
||||
.folder-children-wrapper:has(> button) {
|
||||
margin-inline-start: var(--sp-s);
|
||||
padding-inline-start: var(--sp-s);
|
||||
border-inline-start: $b-2 solid var(--color-background-quaternary);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: var(--sp-xs);
|
||||
|
||||
& .node-parent {
|
||||
flex: 1 0 100%;
|
||||
|
||||
&:last-of-type {
|
||||
margin-block-end: var(--sp-s);
|
||||
}
|
||||
}
|
||||
& .token-pill {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
@@ -23,15 +23,15 @@
|
||||
[node]
|
||||
(is-element node "br"))
|
||||
|
||||
(defn is-text-span-child
|
||||
(defn is-inline-child
|
||||
[node]
|
||||
(or (is-line-break node)
|
||||
(is-text-node node)))
|
||||
|
||||
(defn get-text-span-text
|
||||
(defn get-inline-text
|
||||
[element]
|
||||
(when-not (is-text-span-child (.-firstChild element))
|
||||
(throw (js/TypeError. "Invalid text span child")))
|
||||
(when-not (is-inline-child (.-firstChild element))
|
||||
(throw (js/TypeError. "Invalid inline child")))
|
||||
(if (is-line-break (.-firstChild element))
|
||||
""
|
||||
(.-textContent element)))
|
||||
@@ -54,7 +54,7 @@
|
||||
(assoc acc key (if (value-empty? value) (get defaults key) value))))
|
||||
{} attrs)))
|
||||
|
||||
(defn get-text-span-styles
|
||||
(defn get-inline-styles
|
||||
[element]
|
||||
(get-attrs-from-styles element txt/text-node-attrs (txt/get-default-text-attrs)))
|
||||
|
||||
@@ -66,18 +66,18 @@
|
||||
[element]
|
||||
(get-attrs-from-styles element txt/root-attrs txt/default-root-attrs))
|
||||
|
||||
(defn create-text-span
|
||||
(defn create-inline
|
||||
[element]
|
||||
(let [text (get-text-span-text element)]
|
||||
(let [text (get-inline-text element)]
|
||||
(d/merge {:text text
|
||||
:key (.-id element)}
|
||||
(get-text-span-styles element))))
|
||||
(get-inline-styles element))))
|
||||
|
||||
(defn create-paragraph
|
||||
[element]
|
||||
(d/merge {:type "paragraph"
|
||||
:key (.-id element)
|
||||
:children (mapv create-text-span (.-children element))}
|
||||
:children (mapv create-inline (.-children element))}
|
||||
(get-paragraph-styles element)))
|
||||
|
||||
(defn create-root
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
[root]
|
||||
(get-styles-from-attrs root txt/root-attrs txt/default-text-attrs))
|
||||
|
||||
(defn get-text-span-styles
|
||||
(defn get-inline-styles
|
||||
[inline paragraph]
|
||||
(let [node (if (= "" (:text inline)) paragraph inline)
|
||||
styles (get-styles-from-attrs node txt/text-node-attrs txt/default-text-attrs)]
|
||||
@@ -104,7 +104,7 @@
|
||||
(when text
|
||||
(.replace text (js/RegExp "/" "g") "/\u200B")))
|
||||
|
||||
(defn get-text-span-children
|
||||
(defn get-inline-children
|
||||
[inline paragraph]
|
||||
[(if (and (= "" (:text inline))
|
||||
(= 1 (count (:children paragraph))))
|
||||
@@ -119,14 +119,14 @@
|
||||
[paragraph]
|
||||
(some #(not= "" (:text % "")) (:children paragraph)))
|
||||
|
||||
(defn create-text-span
|
||||
(defn create-inline
|
||||
[inline paragraph]
|
||||
(create-element
|
||||
"span"
|
||||
{:id (or (:key inline) (create-random-key))
|
||||
:data {:itype "span"}
|
||||
:style (get-text-span-styles inline paragraph)}
|
||||
(get-text-span-children inline paragraph)))
|
||||
:data {:itype "inline"}
|
||||
:style (get-inline-styles inline paragraph)}
|
||||
(get-inline-children inline paragraph)))
|
||||
|
||||
(defn create-paragraph
|
||||
[paragraph]
|
||||
@@ -135,7 +135,7 @@
|
||||
{:id (or (:key paragraph) (create-random-key))
|
||||
:data {:itype "paragraph"}
|
||||
:style (get-paragraph-styles paragraph)}
|
||||
(mapv #(create-text-span % paragraph) (:children paragraph))))
|
||||
(mapv #(create-inline % paragraph) (:children paragraph))))
|
||||
|
||||
(defn create-root
|
||||
[root]
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"@vitest/browser": "^1.6.0",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"canvas": "^3.2.1",
|
||||
"esbuild": "^0.24.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"playwright": "^1.45.1",
|
||||
|
||||
@@ -160,7 +160,7 @@ export class TextEditor extends EventTarget {
|
||||
if (this.#element.ariaAutoComplete) this.#element.ariaAutoComplete = false;
|
||||
if (!this.#element.ariaMultiLine) this.#element.ariaMultiLine = true;
|
||||
this.#element.dataset.itype = "editor";
|
||||
if (options?.shouldUpdatePositionOnScroll) {
|
||||
if (options.shouldUpdatePositionOnScroll) {
|
||||
this.#updatePositionFromCanvas();
|
||||
}
|
||||
}
|
||||
@@ -186,7 +186,7 @@ export class TextEditor extends EventTarget {
|
||||
"stylechange",
|
||||
this.#onStyleChange,
|
||||
);
|
||||
if (options?.shouldUpdatePositionOnScroll) {
|
||||
if (options.shouldUpdatePositionOnScroll) {
|
||||
window.addEventListener("scroll", this.#onScroll);
|
||||
}
|
||||
addEventListeners(this.#element, this.#events, {
|
||||
@@ -218,7 +218,7 @@ export class TextEditor extends EventTarget {
|
||||
|
||||
// Disposes the rest of event listeners.
|
||||
removeEventListeners(this.#element, this.#events);
|
||||
if (this.#options?.shouldUpdatePositionOnScroll) {
|
||||
if (this.#options.shouldUpdatePositionOnScroll) {
|
||||
window.removeEventListener("scroll", this.#onScroll);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { getFills } from "./Color.js";
|
||||
|
||||
/* @vitest-environment jsdom */
|
||||
describe("Color", () => {
|
||||
test("getFills", () => {
|
||||
expect(getFills("#aa0000")).toBe(
|
||||
'[["^ ","~:fill-color","#aa0000","~:fill-opacity",1]]',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -31,9 +31,9 @@ describe("Content", () => {
|
||||
inertElement.style,
|
||||
);
|
||||
expect(contentFragment).toBeInstanceOf(DocumentFragment);
|
||||
expect(contentFragment.children).toHaveLength(2);
|
||||
expect(contentFragment.children).toHaveLength(1);
|
||||
expect(contentFragment.firstElementChild).toBeInstanceOf(HTMLDivElement);
|
||||
expect(contentFragment.firstElementChild.children).toHaveLength(1);
|
||||
expect(contentFragment.firstElementChild.children).toHaveLength(2);
|
||||
expect(contentFragment.firstElementChild.firstElementChild).toBeInstanceOf(
|
||||
HTMLSpanElement,
|
||||
);
|
||||
@@ -43,7 +43,6 @@ describe("Content", () => {
|
||||
expect(contentFragment.textContent).toBe("Hello, World!");
|
||||
});
|
||||
|
||||
/*
|
||||
test("mapContentFragmentFromHTML should return a valid content for the editor (multiple paragraphs)", () => {
|
||||
const paragraphs = [
|
||||
"Lorem ipsum",
|
||||
@@ -52,12 +51,11 @@ describe("Content", () => {
|
||||
];
|
||||
const inertElement = document.createElement("div");
|
||||
const contentFragment = mapContentFragmentFromHTML(
|
||||
"<div>Lorem ipsum</div><div>Dolor sit amet</div><div>Sed iaculis blandit odio ornare sagittis.</div>",
|
||||
"<div>Lorem ipsum</div><div>Dolor sit amet</div><div><br/></div><div>Sed iaculis blandit odio ornare sagittis.</div>",
|
||||
inertElement.style,
|
||||
);
|
||||
console.log()
|
||||
expect(contentFragment).toBeInstanceOf(DocumentFragment);
|
||||
expect(contentFragment.children).toHaveLength(5);
|
||||
expect(contentFragment.children).toHaveLength(3);
|
||||
for (let index = 0; index < contentFragment.children.length; index++) {
|
||||
expect(contentFragment.children.item(index)).toBeInstanceOf(
|
||||
HTMLDivElement,
|
||||
@@ -76,7 +74,6 @@ describe("Content", () => {
|
||||
"Lorem ipsumDolor sit ametSed iaculis blandit odio ornare sagittis.",
|
||||
);
|
||||
});
|
||||
*/
|
||||
|
||||
test("mapContentFragmentFromString should return a valid content for the editor", () => {
|
||||
const contentFragment = mapContentFragmentFromString("Hello, \nWorld!");
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import {
|
||||
isEditor,
|
||||
TYPE,
|
||||
TAG,
|
||||
} from "./Editor.js";
|
||||
|
||||
/* @vitest-environment jsdom */
|
||||
describe("Editor", () => {
|
||||
test("isEditor should return true", () => {
|
||||
const element = document.createElement(TAG)
|
||||
element.dataset.itype = TYPE;
|
||||
expect(isEditor(element)).toBeTruthy();
|
||||
});
|
||||
|
||||
test("isEditor should return false when element is null", () => {
|
||||
expect(isEditor(null)).toBeFalsy();
|
||||
});
|
||||
|
||||
test("isEditor should return false when the tag is not valid", () => {
|
||||
const element = document.createElement("span");
|
||||
expect(isEditor(element)).toBeFalsy();
|
||||
});
|
||||
|
||||
test("isEditor should return false when the itype is not valid", () => {
|
||||
const element = document.createElement(TAG);
|
||||
element.dataset.itype = "whatever";
|
||||
expect(isEditor(element)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -49,8 +49,7 @@ describe("Element", () => {
|
||||
},
|
||||
allowedStyles: [["text-decoration"]],
|
||||
});
|
||||
// FIXME:
|
||||
// expect(element.style.getPropertyValue("text-decoration")).toBe("underline");
|
||||
expect(element.style.textDecoration).toBe("underline");
|
||||
});
|
||||
|
||||
test("createElement should create a new element with a child", () => {
|
||||
|
||||
@@ -88,23 +88,14 @@ describe("Paragraph", () => {
|
||||
|
||||
test("isParagraphEnd should return true on an empty paragraph", () => {
|
||||
const paragraph = createEmptyParagraph();
|
||||
expect(isParagraphEnd(paragraph.firstElementChild.firstChild, 0)).toBe(true);
|
||||
expect(isParagraphEnd(paragraph.firstChild.firstChild, 0)).toBe(true);
|
||||
});
|
||||
|
||||
test("isParagraphEnd should return true on a paragraph", () => {
|
||||
const paragraph = createParagraph([
|
||||
createTextSpan(new Text("Hello, World!")),
|
||||
]);
|
||||
expect(isParagraphEnd(paragraph.firstElementChild.firstChild, 13)).toBe(true);
|
||||
});
|
||||
|
||||
test("isParagraphEnd should return false on a paragrah where the focus offset is inside", () => {
|
||||
const paragraph = createParagraph([
|
||||
createTextSpan(new Text("Lorem ipsum sit")),
|
||||
createTextSpan(new Text("amet")),
|
||||
]);
|
||||
console.log(paragraph.innerHTML)
|
||||
expect(isParagraphEnd(paragraph.firstElementChild.firstChild, 15)).toBe(false);
|
||||
expect(isParagraphEnd(paragraph.firstChild.firstChild, 13)).toBe(true);
|
||||
});
|
||||
|
||||
test("splitParagraph should split a paragraph", () => {
|
||||
|
||||
@@ -30,11 +30,10 @@ describe("Root", () => {
|
||||
test("setRootStyles should apply only the styles of root to the root", () => {
|
||||
const emptyRoot = createEmptyRoot();
|
||||
setRootStyles(emptyRoot, {
|
||||
"--vertical-align": "top",
|
||||
"font-size": "25px",
|
||||
["--vertical-align"]: "top",
|
||||
["font-size"]: "25px",
|
||||
});
|
||||
// FIXME:
|
||||
// expect(emptyRoot.style.getPropertyValue("--vertical-align")).toBe("top");
|
||||
expect(emptyRoot.style.getPropertyValue("--vertical-align")).toBe("top");
|
||||
// We expect this style to be empty because we don't apply it
|
||||
// to the root.
|
||||
expect(emptyRoot.style.getPropertyValue("font-size")).toBe("");
|
||||
|
||||
@@ -243,9 +243,6 @@ export function normalizeStyles(
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function setStyle(element, styleName, styleValue, styleUnit) {
|
||||
if (styleValue === "mixed")
|
||||
return element;
|
||||
|
||||
if (
|
||||
styleName.startsWith("--") &&
|
||||
typeof styleValue !== "string" &&
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("Style", () => {
|
||||
"font-size": "32px",
|
||||
display: "none",
|
||||
});
|
||||
expect(element.style.display).toBe("");
|
||||
expect(element.style.display).toBe("none");
|
||||
expect(element.style.fontSize).toBe("");
|
||||
expect(element.style.textDecoration).toBe("");
|
||||
});
|
||||
@@ -32,13 +32,13 @@ describe("Style", () => {
|
||||
setStyles(a, [["display"]], {
|
||||
display: "none",
|
||||
});
|
||||
expect(a.style.display).toBe("");
|
||||
expect(a.style.display).toBe("none");
|
||||
expect(a.style.fontSize).toBe("");
|
||||
expect(a.style.textDecoration).toBe("");
|
||||
|
||||
const b = document.createElement("div");
|
||||
setStyles(b, [["display"]], a.style);
|
||||
expect(b.style.display).toBe("");
|
||||
expect(b.style.display).toBe("none");
|
||||
expect(b.style.fontSize).toBe("");
|
||||
expect(b.style.textDecoration).toBe("");
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ import { setStyles, mergeStyles } from "./Style.js";
|
||||
import { createRandomId } from "./Element.js";
|
||||
|
||||
export const TAG = "SPAN";
|
||||
export const TYPE = "span";
|
||||
export const TYPE = "inline";
|
||||
export const QUERY = `[data-itype="${TYPE}"]`;
|
||||
export const STYLES = [
|
||||
["--typography-ref-id"],
|
||||
|
||||
@@ -18,7 +18,7 @@ import { createLineBreak } from "./LineBreak.js";
|
||||
describe("TextSpan", () => {
|
||||
test("createTextSpan should throw when passed an invalid child", () => {
|
||||
expect(() => createTextSpan("Hello, World!")).toThrowError(
|
||||
"Invalid text span child",
|
||||
"Invalid textSpan child",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("TextSpan", () => {
|
||||
|
||||
test("getTextSpanLength throws when the passed node is not an textSpan", () => {
|
||||
const textSpan = document.createElement("div");
|
||||
expect(() => getTextSpanLength(textSpan)).toThrowError("Invalid text span");
|
||||
expect(() => getTextSpanLength(textSpan)).toThrowError("Invalid textSpan");
|
||||
});
|
||||
|
||||
test("getTextSpanLength returns the length of the textSpan content", () => {
|
||||
|
||||
@@ -277,29 +277,22 @@ export class SelectionController extends EventTarget {
|
||||
this.#applyDefaultStylesToCurrentStyle();
|
||||
const root = startNode.parentElement.parentElement.parentElement;
|
||||
this.#applyStylesFromElementToCurrentStyle(root);
|
||||
if (startNode === endNode) {
|
||||
const paragraph = startNode.parentElement.parentElement;
|
||||
// FIXME: I don't like this approximation. Having to iterate nodes twice
|
||||
// is bad for performance. I think we need another way of "computing"
|
||||
// the cascade.
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(
|
||||
startNode,
|
||||
endNode,
|
||||
)) {
|
||||
const paragraph = textNode.parentElement.parentElement;
|
||||
this.#applyStylesFromElementToCurrentStyle(paragraph);
|
||||
const textSpan = startNode.parentElement;
|
||||
this.#applyStylesFromElementToCurrentStyle(textSpan);
|
||||
} else {
|
||||
// FIXME: I don't like this approximation. Having to iterate nodes twice
|
||||
// is bad for performance. I think we need another way of "computing"
|
||||
// the cascade.
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(
|
||||
startNode,
|
||||
endNode,
|
||||
)) {
|
||||
const paragraph = textNode.parentElement.parentElement;
|
||||
this.#applyStylesFromElementToCurrentStyle(paragraph);
|
||||
}
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(
|
||||
startNode,
|
||||
endNode,
|
||||
)) {
|
||||
const textSpan = textNode.parentElement;
|
||||
this.#mergeStylesFromElementToCurrentStyle(textSpan);
|
||||
}
|
||||
}
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(
|
||||
startNode,
|
||||
endNode,
|
||||
)) {
|
||||
const textSpan = textNode.parentElement;
|
||||
this.#mergeStylesFromElementToCurrentStyle(textSpan);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -1699,8 +1692,7 @@ export class SelectionController extends EventTarget {
|
||||
* @param {RemoveSelectedOptions} [options]
|
||||
*/
|
||||
removeSelected(options) {
|
||||
if (this.isCollapsed)
|
||||
return;
|
||||
if (this.isCollapsed) return;
|
||||
|
||||
const affectedTextSpans = new Set();
|
||||
const affectedParagraphs = new Set();
|
||||
@@ -1709,6 +1701,7 @@ export class SelectionController extends EventTarget {
|
||||
let nextNode = null;
|
||||
|
||||
let { startNode, endNode, startOffset, endOffset } = this.getRanges();
|
||||
|
||||
if (this.shouldHandleCompleteDeletion(startNode, endNode)) {
|
||||
return this.handleCompleteContentDeletion();
|
||||
}
|
||||
@@ -1767,8 +1760,6 @@ export class SelectionController extends EventTarget {
|
||||
affectedParagraphs.add(paragraph);
|
||||
|
||||
let shouldRemoveNodeCompletely = false;
|
||||
const isEndNode = currentNode === endNode;
|
||||
|
||||
if (currentNode === startNode) {
|
||||
if (startOffset === 0) {
|
||||
// We should remove this node completely.
|
||||
@@ -1777,11 +1768,11 @@ export class SelectionController extends EventTarget {
|
||||
// We should remove this node partially.
|
||||
currentNode.nodeValue = currentNode.nodeValue.slice(0, startOffset);
|
||||
}
|
||||
} else if (isEndNode) {
|
||||
} else if (currentNode === endNode) {
|
||||
if (
|
||||
isLineBreak(endNode) ||
|
||||
(isTextNode(endNode) &&
|
||||
endOffset >= (endNode.nodeValue?.length || 0))
|
||||
endOffset === (endNode.nodeValue?.length || 0))
|
||||
) {
|
||||
// We should remove this node completely.
|
||||
shouldRemoveNodeCompletely = true;
|
||||
@@ -1794,7 +1785,8 @@ export class SelectionController extends EventTarget {
|
||||
shouldRemoveNodeCompletely = true;
|
||||
}
|
||||
|
||||
console.log("shouldRemoveNodeCompletely", shouldRemoveNodeCompletely);
|
||||
this.#textNodeIterator.nextNode();
|
||||
|
||||
// Realizamos el borrado del nodo actual.
|
||||
if (shouldRemoveNodeCompletely) {
|
||||
currentNode.remove();
|
||||
@@ -1806,18 +1798,14 @@ export class SelectionController extends EventTarget {
|
||||
textSpan.remove();
|
||||
}
|
||||
|
||||
if (paragraph !== startParagraph
|
||||
&& paragraph.children.length === 0) {
|
||||
if (paragraph !== startParagraph && paragraph.children.length === 0) {
|
||||
paragraph.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Break immediately after processing endNode, before advancing iterator
|
||||
if (isEndNode) {
|
||||
if (currentNode === endNode) {
|
||||
break;
|
||||
}
|
||||
|
||||
this.#textNodeIterator.nextNode();
|
||||
} while (this.#textNodeIterator.currentNode);
|
||||
|
||||
if (startParagraph !== endParagraph) {
|
||||
@@ -1829,7 +1817,6 @@ export class SelectionController extends EventTarget {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("textSpans", startTextSpan, endTextSpan);
|
||||
if (
|
||||
startTextSpan.childNodes.length === 0 &&
|
||||
endTextSpan.childNodes.length > 0
|
||||
@@ -1867,28 +1854,16 @@ export class SelectionController extends EventTarget {
|
||||
return this.collapse(startNode, startOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object with ranges.
|
||||
*
|
||||
* @returns {}
|
||||
*/
|
||||
getRanges() {
|
||||
let startNode = getClosestTextNode(this.#range.startContainer);
|
||||
let endNode = getClosestTextNode(this.#range.endContainer);
|
||||
|
||||
let startOffset = this.#range.startOffset;
|
||||
let endOffset = this.#range.endOffset;
|
||||
let endOffset = this.#range.startOffset + this.#range.toString().length;
|
||||
|
||||
return { startNode, endNode, startOffset, endOffset };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we should remove the complete root.
|
||||
*
|
||||
* @param {*} startNode
|
||||
* @param {*} endNode
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldHandleCompleteDeletion(startNode, endNode) {
|
||||
const root = this.#textEditor.root;
|
||||
return (
|
||||
|
||||
@@ -35,16 +35,6 @@ function focus(
|
||||
}
|
||||
|
||||
describe("SelectionController", () => {
|
||||
test("`options` should return the Options object kept by the SelectionController", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithText("");
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(
|
||||
textEditorMock,
|
||||
selection
|
||||
);
|
||||
expect(selectionController.options).toBe({});
|
||||
});
|
||||
|
||||
test("`selection` should return the Selection object kept by the SelectionController", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithText("");
|
||||
const selection = document.getSelection();
|
||||
@@ -1059,48 +1049,6 @@ describe("SelectionController", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("`removeSelected` should remove only the selected text from two paragraphs", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([createTextSpan(new Text("Lorem ipsum"))]),
|
||||
createParagraph([createTextSpan(new Text("dolor sit amet"))]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(textEditorMock, selection);
|
||||
focus(
|
||||
selection,
|
||||
textEditorMock,
|
||||
root.firstElementChild.firstElementChild.firstChild,
|
||||
6,
|
||||
root.lastElementChild.firstElementChild.firstChild,
|
||||
9,
|
||||
);
|
||||
selectionController.removeSelected();
|
||||
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.children).toHaveLength(1);
|
||||
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.firstChild.children).toHaveLength(2);
|
||||
expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
|
||||
expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
|
||||
HTMLSpanElement,
|
||||
);
|
||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe("span");
|
||||
expect(textEditorMock.root.textContent).toBe("Lorem amet");
|
||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||
Text,
|
||||
);
|
||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||
"Lorem ",
|
||||
);
|
||||
expect(textEditorMock.root.firstChild.lastChild.firstChild).toBeInstanceOf(
|
||||
Text,
|
||||
);
|
||||
expect(textEditorMock.root.firstChild.lastChild.firstChild.nodeValue).toBe(
|
||||
" amet",
|
||||
);
|
||||
});
|
||||
|
||||
test("`removeSelected` and `removeBackwardParagraph`", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
||||
|
||||
@@ -48,7 +48,7 @@ export class StyleDeclaration {
|
||||
}
|
||||
|
||||
item(index) {
|
||||
return Array.from(this.#items.keys()).at(index);
|
||||
return Array.from(this.#items).at(index).name;
|
||||
}
|
||||
|
||||
removeProperty(name) {
|
||||
|
||||
@@ -29,23 +29,4 @@ describe("StyleDeclaration", () => {
|
||||
expect(styleDeclaration.getPropertyValue("line-height")).toBe("");
|
||||
expect(styleDeclaration.getPropertyPriority("line-height")).toBe("");
|
||||
});
|
||||
|
||||
test("Iterate styles", () => {
|
||||
const properties = [
|
||||
["line-height", "1.2"],
|
||||
["--variable", "hola"],
|
||||
];
|
||||
|
||||
const styleDeclaration = new StyleDeclaration();
|
||||
for (const [name,value] of properties) {
|
||||
styleDeclaration.setProperty(name, value);
|
||||
}
|
||||
for (let index = 0; index < styleDeclaration.length; index++) {
|
||||
const name = styleDeclaration.item(index);
|
||||
const value = styleDeclaration.getPropertyValue(name);
|
||||
const [expectedName, expectedValue] = properties[index];
|
||||
expect(name).toBe(expectedName);
|
||||
expect(value).toBe(expectedValue);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -462,6 +462,8 @@ class TextEditorPlayground {
|
||||
// Number of text leaves in the paragraph.
|
||||
view.setUint32(0, paragraph.leaves.length, true);
|
||||
|
||||
console.log("lineHeight", paragraph.lineHeight);
|
||||
|
||||
// Serialize paragraph attributes
|
||||
view.setUint8(4, paragraph.textAlign, true); // text-align: left
|
||||
view.setUint8(5, paragraph.textDirection, true); // text-direction: LTR
|
||||
|
||||
@@ -515,7 +515,6 @@ __metadata:
|
||||
"@vitest/browser": "npm:^1.6.0"
|
||||
"@vitest/coverage-v8": "npm:^1.6.0"
|
||||
"@vitest/ui": "npm:^1.6.0"
|
||||
canvas: "npm:^3.2.1"
|
||||
esbuild: "npm:^0.24.0"
|
||||
jsdom: "npm:^25.0.0"
|
||||
playwright: "npm:^1.45.1"
|
||||
@@ -903,24 +902,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base64-js@npm:^1.3.1":
|
||||
version: 1.5.1
|
||||
resolution: "base64-js@npm:1.5.1"
|
||||
checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bl@npm:^4.0.3":
|
||||
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: 10c0/02847e1d2cb089c9dc6958add42e3cdeaf07d13f575973963335ac0fdece563a50ac770ac4c8fa06492d2dd276f6cc3b7f08c7cd9c7a7ad0f8d388b2a28def5f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^1.1.7":
|
||||
version: 1.1.11
|
||||
resolution: "brace-expansion@npm:1.1.11"
|
||||
@@ -949,16 +930,6 @@ __metadata:
|
||||
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: 10c0/27cac81cff434ed2876058d72e7c4789d11ff1120ef32c9de48f59eab58179b66710c488987d295ae89a228f835fc66d088652dffeb8e3ba8659f80eb091d55e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cac@npm:^6.7.14":
|
||||
version: 6.7.14
|
||||
resolution: "cac@npm:6.7.14"
|
||||
@@ -986,17 +957,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"canvas@npm:^3.2.1":
|
||||
version: 3.2.1
|
||||
resolution: "canvas@npm:3.2.1"
|
||||
dependencies:
|
||||
node-addon-api: "npm:^7.0.0"
|
||||
node-gyp: "npm:latest"
|
||||
prebuild-install: "npm:^7.1.3"
|
||||
checksum: 10c0/c0fd572a8b28e075b40a42b523bdf05e985feaeb18b56085432bfb91a3b905af48f89ec73ed4e795de892cb13f7332ceb0c78cf84c64281c41c29995665b89c8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chai@npm:^4.3.10":
|
||||
version: 4.4.1
|
||||
resolution: "chai@npm:4.4.1"
|
||||
@@ -1021,13 +981,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chownr@npm:^1.1.1":
|
||||
version: 1.1.4
|
||||
resolution: "chownr@npm:1.1.4"
|
||||
checksum: 10c0/ed57952a84cc0c802af900cf7136de643d3aba2eecb59d29344bc2f3f9bf703a301b9d84cdc71f82c3ffc9ccde831b0d92f5b45f91727d6c9da62f23aef9d9db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chownr@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "chownr@npm:2.0.0"
|
||||
@@ -1130,15 +1083,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decompress-response@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "decompress-response@npm:6.0.0"
|
||||
dependencies:
|
||||
mimic-response: "npm:^3.1.0"
|
||||
checksum: 10c0/bd89d23141b96d80577e70c54fb226b2f40e74a6817652b80a116d7befb8758261ad073a8895648a29cc0a5947021ab66705cb542fa9c143c82022b27c5b175e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"deep-eql@npm:^4.1.3":
|
||||
version: 4.1.4
|
||||
resolution: "deep-eql@npm:4.1.4"
|
||||
@@ -1148,13 +1092,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"deep-extend@npm:^0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "deep-extend@npm:0.6.0"
|
||||
checksum: 10c0/1c6b0abcdb901e13a44c7d699116d3d4279fdb261983122a3783e7273844d5f2537dc2e1c454a23fcf645917f93fbf8d07101c1d03c015a87faa662755212566
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"delayed-stream@npm:~1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "delayed-stream@npm:1.0.0"
|
||||
@@ -1162,13 +1099,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-libc@npm:^2.0.0":
|
||||
version: 2.1.2
|
||||
resolution: "detect-libc@npm:2.1.2"
|
||||
checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"diff-sequences@npm:^29.6.3":
|
||||
version: 29.6.3
|
||||
resolution: "diff-sequences@npm:29.6.3"
|
||||
@@ -1206,15 +1136,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1":
|
||||
version: 1.4.5
|
||||
resolution: "end-of-stream@npm:1.4.5"
|
||||
dependencies:
|
||||
once: "npm:^1.4.0"
|
||||
checksum: 10c0/b0701c92a10b89afb1cb45bf54a5292c6f008d744eb4382fa559d54775ff31617d1d7bc3ef617575f552e24fad2c7c1a1835948c66b3f3a4be0a6c1f35c883d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"entities@npm:^4.4.0":
|
||||
version: 4.5.0
|
||||
resolution: "entities@npm:4.5.0"
|
||||
@@ -1425,13 +1346,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"expand-template@npm:^2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "expand-template@npm:2.0.3"
|
||||
checksum: 10c0/1c9e7afe9acadf9d373301d27f6a47b34e89b3391b1ef38b7471d381812537ef2457e620ae7f819d2642ce9c43b189b3583813ec395e2938319abe356a9b2f51
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"exponential-backoff@npm:^3.1.1":
|
||||
version: 3.1.1
|
||||
resolution: "exponential-backoff@npm:3.1.1"
|
||||
@@ -1505,13 +1419,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs-constants@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "fs-constants@npm:1.0.0"
|
||||
checksum: 10c0/a0cde99085f0872f4d244e83e03a46aa387b74f5a5af750896c6b05e9077fac00e9932fdf5aef84f2f16634cd473c63037d7a512576da7d5c2b9163d1909f3a8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs-minipass@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "fs-minipass@npm:2.1.0"
|
||||
@@ -1589,13 +1496,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"github-from-package@npm:0.0.0":
|
||||
version: 0.0.0
|
||||
resolution: "github-from-package@npm:0.0.0"
|
||||
checksum: 10c0/737ee3f52d0a27e26332cde85b533c21fcdc0b09fb716c3f8e522cfaa9c600d4a631dec9fcde179ec9d47cca89017b7848ed4d6ae6b6b78f936c06825b1fcc12
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob-parent@npm:^5.1.2":
|
||||
version: 5.1.2
|
||||
resolution: "glob-parent@npm:5.1.2"
|
||||
@@ -1708,13 +1608,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ieee754@npm:^1.1.13":
|
||||
version: 1.2.1
|
||||
resolution: "ieee754@npm:1.2.1"
|
||||
checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"imurmurhash@npm:^0.1.4":
|
||||
version: 0.1.4
|
||||
resolution: "imurmurhash@npm:0.1.4"
|
||||
@@ -1739,20 +1632,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:^2.0.4":
|
||||
"inherits@npm:2":
|
||||
version: 2.0.4
|
||||
resolution: "inherits@npm:2.0.4"
|
||||
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ini@npm:~1.3.0":
|
||||
version: 1.3.8
|
||||
resolution: "ini@npm:1.3.8"
|
||||
checksum: 10c0/ec93838d2328b619532e4f1ff05df7909760b6f66d9c9e2ded11e5c1897d6f2f9980c54dd638f88654b00919ce31e827040631eab0a3969e4d1abefa0719516a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ip-address@npm:^9.0.5":
|
||||
version: 9.0.5
|
||||
resolution: "ip-address@npm:9.0.5"
|
||||
@@ -2050,13 +1936,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mimic-response@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "mimic-response@npm:3.1.0"
|
||||
checksum: 10c0/0d6f07ce6e03e9e4445bee655202153bdb8a98d67ee8dc965ac140900d7a2688343e6b4c9a72cfc9ef2f7944dfd76eef4ab2482eb7b293a68b84916bac735362
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1":
|
||||
version: 3.1.2
|
||||
resolution: "minimatch@npm:3.1.2"
|
||||
@@ -2075,13 +1954,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimist@npm:^1.2.0, minimist@npm:^1.2.3":
|
||||
version: 1.2.8
|
||||
resolution: "minimist@npm:1.2.8"
|
||||
checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minipass-collect@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "minipass-collect@npm:2.0.1"
|
||||
@@ -2166,13 +2038,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3":
|
||||
version: 0.5.3
|
||||
resolution: "mkdirp-classic@npm:0.5.3"
|
||||
checksum: 10c0/95371d831d196960ddc3833cc6907e6b8f67ac5501a6582f47dfae5eb0f092e9f8ce88e0d83afcae95d6e2b61a01741ba03714eeafb6f7a6e9dcc158ac85b168
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mkdirp@npm:^1.0.3":
|
||||
version: 1.0.4
|
||||
resolution: "mkdirp@npm:1.0.4"
|
||||
@@ -2217,13 +2082,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"napi-build-utils@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "napi-build-utils@npm:2.0.0"
|
||||
checksum: 10c0/5833aaeb5cc5c173da47a102efa4680a95842c13e0d9cc70428bd3ee8d96bb2172f8860d2811799b5daa5cbeda779933601492a2028a6a5351c6d0fcf6de83db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"negotiator@npm:^0.6.3":
|
||||
version: 0.6.3
|
||||
resolution: "negotiator@npm:0.6.3"
|
||||
@@ -2231,24 +2089,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-abi@npm:^3.3.0":
|
||||
version: 3.87.0
|
||||
resolution: "node-abi@npm:3.87.0"
|
||||
dependencies:
|
||||
semver: "npm:^7.3.5"
|
||||
checksum: 10c0/41cfc361edd1b0711d412ca9e1a475180c5b897868bd5583df7ff73e30e6044cc7de307df36c2257203320f17fadf7e82dfdf5a9f6fd510a8578e3fe3ed67ebb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-addon-api@npm:^7.0.0":
|
||||
version: 7.1.1
|
||||
resolution: "node-addon-api@npm:7.1.1"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10c0/fb32a206276d608037fa1bcd7e9921e177fe992fc610d098aa3128baca3c0050fc1e014fa007e9b3874cf865ddb4f5bd9f43ccb7cbbbe4efaff6a83e920b17e9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp@npm:latest":
|
||||
version: 10.1.0
|
||||
resolution: "node-gyp@npm:10.1.0"
|
||||
@@ -2296,7 +2136,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0":
|
||||
"once@npm:^1.3.0":
|
||||
version: 1.4.0
|
||||
resolution: "once@npm:1.4.0"
|
||||
dependencies:
|
||||
@@ -2453,28 +2293,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prebuild-install@npm:^7.1.3":
|
||||
version: 7.1.3
|
||||
resolution: "prebuild-install@npm:7.1.3"
|
||||
dependencies:
|
||||
detect-libc: "npm:^2.0.0"
|
||||
expand-template: "npm:^2.0.3"
|
||||
github-from-package: "npm:0.0.0"
|
||||
minimist: "npm:^1.2.3"
|
||||
mkdirp-classic: "npm:^0.5.3"
|
||||
napi-build-utils: "npm:^2.0.0"
|
||||
node-abi: "npm:^3.3.0"
|
||||
pump: "npm:^3.0.0"
|
||||
rc: "npm:^1.2.7"
|
||||
simple-get: "npm:^4.0.0"
|
||||
tar-fs: "npm:^2.0.0"
|
||||
tunnel-agent: "npm:^0.6.0"
|
||||
bin:
|
||||
prebuild-install: bin.js
|
||||
checksum: 10c0/25919a42b52734606a4036ab492d37cfe8b601273d8dfb1fa3c84e141a0a475e7bad3ab848c741d2f810cef892fcf6059b8c7fe5b29f98d30e0c29ad009bedff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prettier@npm:^3.3.3":
|
||||
version: 3.3.3
|
||||
resolution: "prettier@npm:3.3.3"
|
||||
@@ -2526,16 +2344,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pump@npm:^3.0.0":
|
||||
version: 3.0.3
|
||||
resolution: "pump@npm:3.0.3"
|
||||
dependencies:
|
||||
end-of-stream: "npm:^1.1.0"
|
||||
once: "npm:^1.3.1"
|
||||
checksum: 10c0/ada5cdf1d813065bbc99aa2c393b8f6beee73b5de2890a8754c9f488d7323ffd2ca5f5a0943b48934e3fcbd97637d0337369c3c631aeb9614915db629f1c75c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"punycode@npm:^2.1.1, punycode@npm:^2.3.1":
|
||||
version: 2.3.1
|
||||
resolution: "punycode@npm:2.3.1"
|
||||
@@ -2557,20 +2365,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rc@npm:^1.2.7":
|
||||
version: 1.2.8
|
||||
resolution: "rc@npm:1.2.8"
|
||||
dependencies:
|
||||
deep-extend: "npm:^0.6.0"
|
||||
ini: "npm:~1.3.0"
|
||||
minimist: "npm:^1.2.0"
|
||||
strip-json-comments: "npm:~2.0.1"
|
||||
bin:
|
||||
rc: ./cli.js
|
||||
checksum: 10c0/24a07653150f0d9ac7168e52943cc3cb4b7a22c0e43c7dff3219977c2fdca5a2760a304a029c20811a0e79d351f57d46c9bde216193a0f73978496afc2b85b15
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-is@npm:^18.0.0":
|
||||
version: 18.3.1
|
||||
resolution: "react-is@npm:18.3.1"
|
||||
@@ -2578,17 +2372,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0":
|
||||
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: 10c0/e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"requires-port@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "requires-port@npm:1.0.0"
|
||||
@@ -2696,13 +2479,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0":
|
||||
version: 5.2.1
|
||||
resolution: "safe-buffer@npm:5.2.1"
|
||||
checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"safer-buffer@npm:>= 2.1.2 < 3.0.0":
|
||||
version: 2.1.2
|
||||
resolution: "safer-buffer@npm:2.1.2"
|
||||
@@ -2758,24 +2534,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"simple-concat@npm:^1.0.0":
|
||||
version: 1.0.1
|
||||
resolution: "simple-concat@npm:1.0.1"
|
||||
checksum: 10c0/62f7508e674414008910b5397c1811941d457dfa0db4fd5aa7fa0409eb02c3609608dfcd7508cace75b3a0bf67a2a77990711e32cd213d2c76f4fd12ee86d776
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"simple-get@npm:^4.0.0":
|
||||
version: 4.0.1
|
||||
resolution: "simple-get@npm:4.0.1"
|
||||
dependencies:
|
||||
decompress-response: "npm:^6.0.0"
|
||||
once: "npm:^1.3.1"
|
||||
simple-concat: "npm:^1.0.0"
|
||||
checksum: 10c0/b0649a581dbca741babb960423248899203165769747142033479a7dc5e77d7b0fced0253c731cd57cf21e31e4d77c9157c3069f4448d558ebc96cf9e1eebcf0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sirv@npm:^2.0.4":
|
||||
version: 2.0.4
|
||||
resolution: "sirv@npm:2.0.4"
|
||||
@@ -2874,15 +2632,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string_decoder@npm:^1.1.1":
|
||||
version: 1.3.0
|
||||
resolution: "string_decoder@npm:1.3.0"
|
||||
dependencies:
|
||||
safe-buffer: "npm:~5.2.0"
|
||||
checksum: 10c0/810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d
|
||||
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"
|
||||
@@ -2908,13 +2657,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strip-json-comments@npm:~2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "strip-json-comments@npm:2.0.1"
|
||||
checksum: 10c0/b509231cbdee45064ff4f9fd73609e2bcc4e84a4d508e9dd0f31f70356473fde18abfb5838c17d56fb236f5a06b102ef115438de0600b749e818a35fbbc48c43
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strip-literal@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "strip-literal@npm:2.1.0"
|
||||
@@ -2940,31 +2682,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar-fs@npm:^2.0.0":
|
||||
version: 2.1.4
|
||||
resolution: "tar-fs@npm:2.1.4"
|
||||
dependencies:
|
||||
chownr: "npm:^1.1.1"
|
||||
mkdirp-classic: "npm:^0.5.2"
|
||||
pump: "npm:^3.0.0"
|
||||
tar-stream: "npm:^2.1.4"
|
||||
checksum: 10c0/decb25acdc6839182c06ec83cba6136205bda1db984e120c8ffd0d80182bc5baa1d916f9b6c5c663ea3f9975b4dd49e3c6bb7b1707cbcdaba4e76042f43ec84c
|
||||
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: 10c0/2f4c910b3ee7196502e1ff015a7ba321ec6ea837667220d7bcb8d0852d51cb04b87f7ae471008a6fb8f5b1a1b5078f62f3a82d30c706f20ada1238ac797e7692
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar@npm:^6.1.11, tar@npm:^6.1.2":
|
||||
version: 6.2.1
|
||||
resolution: "tar@npm:6.2.1"
|
||||
@@ -3055,15 +2772,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tunnel-agent@npm:^0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "tunnel-agent@npm:0.6.0"
|
||||
dependencies:
|
||||
safe-buffer: "npm:^5.0.1"
|
||||
checksum: 10c0/4c7a1b813e7beae66fdbf567a65ec6d46313643753d0beefb3c7973d66fcec3a1e7f39759f0a0b4465883499c6dc8b0750ab8b287399af2e583823e40410a17a
|
||||
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"
|
||||
@@ -3120,13 +2828,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"util-deprecate@npm:^1.0.1":
|
||||
version: 1.0.2
|
||||
resolution: "util-deprecate@npm:1.0.2"
|
||||
checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite-node@npm:1.6.0":
|
||||
version: 1.6.0
|
||||
resolution: "vite-node@npm:1.6.0"
|
||||
|
||||
Reference in New Issue
Block a user