mirror of
https://github.com/penpot/penpot.git
synced 2025-07-21 13:47:13 +02:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
c0919aff51
110 changed files with 1849 additions and 1757 deletions
|
@ -203,7 +203,7 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [selected (wsh/lookup-selected state)]
|
||||
(rx/of (group-shapes nil selected))))))
|
||||
(rx/of (group-shapes nil selected :change-selection? true))))))
|
||||
|
||||
(defn ungroup-shapes
|
||||
[ids & {:keys [change-selection?] :or {change-selection? false}}]
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
|
@ -131,8 +132,7 @@
|
|||
(let [new-interaction (-> ctsi/default-interaction
|
||||
(ctsi/set-destination destination)
|
||||
(assoc :position-relative-to (:id shape)))]
|
||||
(update shape :interactions
|
||||
ctsi/add-interaction new-interaction)))))
|
||||
(cls/add-new-interaction shape new-interaction)))))
|
||||
(when (and (not (connected-frame? objects (:id frame)))
|
||||
(nil? flow))
|
||||
(rx/of (add-flow (:id frame))))))))))
|
||||
|
|
|
@ -97,7 +97,7 @@
|
|||
(when cls
|
||||
(cond
|
||||
(true? v) cls
|
||||
(false? v) nil
|
||||
(false? v) ""
|
||||
:else `(if ~v ~cls ""))))))
|
||||
(interpose " ")))
|
||||
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
[app.main.ui.frame-preview :as frame-preview]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.messages :as msgs]
|
||||
[app.main.ui.onboarding :refer [onboarding-modal]]
|
||||
[app.main.ui.onboarding.newsletter :refer [onboarding-newsletter]]
|
||||
[app.main.ui.onboarding.questions :refer [questions-modal]]
|
||||
[app.main.ui.onboarding.team-choice :refer [onboarding-team-modal]]
|
||||
[app.main.ui.releases :refer [release-notes-modal]]
|
||||
[app.main.ui.static :as static]
|
||||
[app.util.dom :as dom]
|
||||
|
@ -96,19 +98,43 @@
|
|||
#_[:& app.main.ui.onboarding/onboarding-modal]
|
||||
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
|
||||
(when-let [props (get profile :props)]
|
||||
(cond
|
||||
(and (not (:onboarding-viewed props))
|
||||
(contains? cf/flags :onboarding))
|
||||
[:& onboarding-modal {}]
|
||||
(let [show-question-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :onboarding-questions)))
|
||||
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(:onboarding-viewed props)
|
||||
(not= (:release-notes-viewed props) (:main cf/version))
|
||||
(not= "0.0" (:main cf/version)))
|
||||
[:& release-notes-modal {:version (:main cf/version)}]))
|
||||
show-newsletter-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :newsletter-updates))
|
||||
(contains? props :onboarding-questions))
|
||||
|
||||
show-team-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
(not (contains? props :onboarding-team-id))
|
||||
(contains? props :newsletter-updates))
|
||||
|
||||
show-release-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(:onboarding-viewed props)
|
||||
(not= (:release-notes-viewed props) (:main cf/version))
|
||||
(not= "0.0" (:main cf/version)))]
|
||||
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
|
||||
show-newsletter-modal?
|
||||
[:& onboarding-newsletter]
|
||||
|
||||
show-team-modal?
|
||||
[:& onboarding-team-modal]
|
||||
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}])))
|
||||
|
||||
[:& dashboard-page {:route route :profile profile}]]
|
||||
|
||||
:viewer
|
||||
(let [{:keys [query-params path-params]} route
|
||||
{:keys [index share-id section page-id interactions-mode frame-id]
|
||||
|
|
|
@ -44,6 +44,9 @@
|
|||
{::mf/props :obj}
|
||||
[{:keys [route]}]
|
||||
(let [section (dm/get-in route [:data :name])
|
||||
show-login-icon (and
|
||||
(not= section :auth-register-validate)
|
||||
(not= section :auth-register-success))
|
||||
params (:query-params route)
|
||||
error (:error params)]
|
||||
|
||||
|
@ -55,8 +58,9 @@
|
|||
(st/emit! (du/show-redirect-error error))))
|
||||
|
||||
[:main {:class (stl/css :auth-section)}
|
||||
[:h1 {:class (stl/css :logo-container)}
|
||||
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
|
||||
(when show-login-icon
|
||||
[:h1 {:class (stl/css :logo-container)}
|
||||
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]])
|
||||
[:div {:class (stl/css :login-illustration)}
|
||||
i/login-illustration]
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: $s-120;
|
||||
height: $s-96;
|
||||
margin-block-end: $s-52;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,14 +10,22 @@
|
|||
width: 100%;
|
||||
padding-block-end: 0;
|
||||
display: grid;
|
||||
gap: $s-24;
|
||||
gap: $s-12;
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-12;
|
||||
margin-top: $s-12;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-title-wrapper {
|
||||
width: 100%;
|
||||
padding-block-end: 0;
|
||||
display: grid;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-color: var(--modal-separator-backogrund-color);
|
||||
margin: 0;
|
||||
|
|
|
@ -169,7 +169,7 @@
|
|||
[:& fm/input
|
||||
{:name :email
|
||||
:type "email"
|
||||
:label (tr "auth.email")
|
||||
:label (tr "auth.work-email")
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
:form form}
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/input {:name :email
|
||||
:label (tr "auth.email")
|
||||
:label (tr "auth.work-email")
|
||||
:type "text"
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/input {:type "text"
|
||||
:name :email
|
||||
:label (tr "auth.email")
|
||||
:label (tr "auth.work-email")
|
||||
:data-test "email-input"
|
||||
:show-success? true
|
||||
:class (stl/css :form-field)}]]
|
||||
|
@ -134,11 +134,11 @@
|
|||
(mf/defc register-page
|
||||
{::mf/props :obj}
|
||||
[{:keys [params]}]
|
||||
[:div {:class (stl/css :auth-form-wrapper)}
|
||||
[:div {:class (stl/css :auth-form-wrapper :register-form)}
|
||||
[:h1 {:class (stl/css :auth-title)
|
||||
:data-test "registration-title"} (tr "auth.register-title")]
|
||||
[:p {:class (stl/css :auth-tagline)}
|
||||
(tr "auth.login-tagline")]
|
||||
(tr "auth.register-tagline")]
|
||||
|
||||
(when (contains? cf/flags :demo-warning)
|
||||
[:& login/demo-warning])
|
||||
|
@ -229,11 +229,11 @@
|
|||
(mf/html
|
||||
[:& tr-html
|
||||
{:tag-name "div"
|
||||
:label "auth.terms-privacy-agreement-md"
|
||||
:label "auth.terms-and-privacy-agreement"
|
||||
:params [cf/terms-of-service-uri cf/privacy-policy-uri]}])]
|
||||
[:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)}
|
||||
[:& fm/input {:name :accept-terms-and-privacy
|
||||
:class "check-primary"
|
||||
:class (stl/css :checkbox-terms-and-privacy)
|
||||
:type "checkbox"
|
||||
:default-checked false
|
||||
:label terms-label}]]))
|
||||
|
@ -247,11 +247,12 @@
|
|||
(mf/defc register-validate-page
|
||||
[{:keys [params]}]
|
||||
[:div {:class (stl/css :auth-form-wrapper)}
|
||||
[:h1 {:class (stl/css :auth-title)
|
||||
:data-test "register-title"} (tr "auth.register-title")]
|
||||
[:div {:class (stl/css :auth-subtitle)} (tr "auth.register-subtitle")]
|
||||
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:h1 {:class (stl/css :logo-container)}
|
||||
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
|
||||
[:div {:class (stl/css :auth-title-wrapper)}
|
||||
[:h2 {:class (stl/css :auth-title)
|
||||
:data-test "register-title"} (tr "auth.register-account-title")]
|
||||
[:div {:class (stl/css :auth-subtitle)} (tr "auth.register-account-tagline")]]
|
||||
|
||||
[:& register-validate-form {:params params}]
|
||||
|
||||
|
@ -264,7 +265,11 @@
|
|||
(mf/defc register-success-page
|
||||
[{:keys [params]}]
|
||||
[:div {:class (stl/css :auth-form-wrapper :register-success)}
|
||||
[:div {:class (stl/css :notification-icon)} i/icon-verify]
|
||||
[:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]
|
||||
[:h1 {:class (stl/css :logo-container)}
|
||||
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
|
||||
[:div {:class (stl/css :auth-title-wrapper)}
|
||||
[:h2 {:class (stl/css :auth-title)}
|
||||
(tr "auth.check-mail")]
|
||||
[:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]]
|
||||
[:div {:class (stl/css :notification-text-email)} (:email params "")]
|
||||
[:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]])
|
||||
|
|
|
@ -8,15 +8,24 @@
|
|||
@use "./common.scss";
|
||||
|
||||
.accept-terms-and-privacy-wrapper {
|
||||
margin: $s-16 0;
|
||||
:global(a) {
|
||||
color: $df-secondary;
|
||||
font-weight: $fw700;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-terms-and-privacy {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.register-form {
|
||||
gap: $s-24;
|
||||
}
|
||||
|
||||
.register-success {
|
||||
padding-bottom: $s-32;
|
||||
gap: $s-24;
|
||||
.auth-title {
|
||||
@include medTitleTipography;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
|
@ -30,9 +39,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
.notification-text-email,
|
||||
.notification-text {
|
||||
font-size: $fs-16;
|
||||
color: var(--notification-foreground-color-default);
|
||||
margin-bottom: $s-16;
|
||||
@include bodyMediumTypography;
|
||||
color: var(--title-foreground-color);
|
||||
}
|
||||
|
||||
.notification-text-email {
|
||||
@include medTitleTipography;
|
||||
font-size: $fs-20;
|
||||
color: var(--register-confirmation-color);
|
||||
margin-inline: $s-36;
|
||||
}
|
||||
|
||||
.logo-btn {
|
||||
height: $s-40;
|
||||
svg {
|
||||
width: $s-120;
|
||||
height: $s-40;
|
||||
fill: var(--main-icon-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: $s-120;
|
||||
margin-block-end: $s-24;
|
||||
}
|
||||
|
|
|
@ -181,6 +181,7 @@
|
|||
[:*
|
||||
[:div
|
||||
{:class (stl/css :floating-thread-bubble)
|
||||
:data-testid "floating-thread-bubble"
|
||||
:style {:top (str pos-y "px")
|
||||
:left (str pos-x "px")}
|
||||
:on-click dom/stop-propagation}
|
||||
|
@ -435,9 +436,9 @@
|
|||
[:* {:key (dm/str (:id item))}
|
||||
[:& comment-item {:comment item
|
||||
:users users
|
||||
:origin origin}]])
|
||||
[:div {:ref ref}]]
|
||||
[:& reply-form {:thread thread}]])))
|
||||
:origin origin}]])]
|
||||
[:& reply-form {:thread thread}]
|
||||
[:div {:ref ref}]])))
|
||||
|
||||
(defn use-buble
|
||||
[zoom {:keys [position frame-id]}]
|
||||
|
@ -558,6 +559,7 @@
|
|||
:on-pointer-move on-pointer-move*
|
||||
:on-click on-click*
|
||||
:on-lost-pointer-capture on-lost-pointer-capture
|
||||
:data-testid "floating-thread-bubble"
|
||||
:class (stl/css-case
|
||||
:floating-thread-bubble true
|
||||
:resolved (:is-resolved thread)
|
||||
|
|
|
@ -236,6 +236,7 @@
|
|||
.reply-form {
|
||||
textarea {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
line-height: 1.45;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
|
|
@ -117,7 +117,7 @@
|
|||
[:*
|
||||
(cond
|
||||
(some? label)
|
||||
[:label {:class (stl/css-case :input-with-label (not is-checkbox?)
|
||||
[:label {:class (stl/css-case :input-with-label-form (not is-checkbox?)
|
||||
:input-label is-text?
|
||||
:radio-label is-radio?
|
||||
:checkbox-label is-checkbox?)
|
||||
|
@ -214,7 +214,7 @@
|
|||
[:span {:class (stl/css :hint)} hint])]))
|
||||
|
||||
(mf/defc select
|
||||
[{:keys [options disabled form default dropdown-class] :as props
|
||||
[{:keys [options disabled form default dropdown-class select-class] :as props
|
||||
:or {default ""}}]
|
||||
(let [input-name (get props :name)
|
||||
form (or form (mf/use-ctx form-ctx))
|
||||
|
@ -230,6 +230,7 @@
|
|||
{:default-value value
|
||||
:disabled disabled
|
||||
:options options
|
||||
:class select-class
|
||||
:dropdown-class dropdown-class
|
||||
:on-change handle-change}]]))
|
||||
|
||||
|
@ -297,6 +298,71 @@
|
|||
:value value'
|
||||
:checked checked?}]]))]))
|
||||
|
||||
(mf/defc image-radio-buttons
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [form (or (unchecked-get props "form")
|
||||
(mf/use-ctx form-ctx))
|
||||
name (unchecked-get props "name")
|
||||
image (unchecked-get props "image")
|
||||
img-height (unchecked-get props "img-height")
|
||||
img-width (unchecked-get props "img-width")
|
||||
current-value (or (dm/get-in @form [:data name] "")
|
||||
(unchecked-get props "value"))
|
||||
on-change (unchecked-get props "on-change")
|
||||
options (unchecked-get props "options")
|
||||
trim? (unchecked-get props "trim")
|
||||
class (unchecked-get props "class")
|
||||
encode-fn (d/nilv (unchecked-get props "encode-fn") identity)
|
||||
decode-fn (d/nilv (unchecked-get props "decode-fn") identity)
|
||||
|
||||
on-change'
|
||||
(mf/use-fn
|
||||
(mf/deps on-change form name)
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-value decode-fn)]
|
||||
(when (some? form)
|
||||
(swap! form assoc-in [:touched name] true)
|
||||
(fm/on-input-change form name value trim?))
|
||||
|
||||
(when (fn? on-change)
|
||||
(on-change name value)))))]
|
||||
|
||||
[:div {:class (if image
|
||||
class
|
||||
(dm/str class " " (stl/css :custom-radio)))}
|
||||
(for [{:keys [image icon value label area]} options]
|
||||
(let [icon? (some? icon)
|
||||
value' (encode-fn value)
|
||||
checked? (= value current-value)
|
||||
key (str/ffmt "%-%" (d/name name) (d/name value'))]
|
||||
|
||||
[:label {:for key
|
||||
:key key
|
||||
:style {:grid-area area}
|
||||
:class (stl/css-case :radio-label-image true
|
||||
:global/checked checked?)}
|
||||
(cond
|
||||
icon?
|
||||
[:span {:class (stl/css :icon-inside)
|
||||
:style {:height img-height
|
||||
:width img-width}} icon]
|
||||
|
||||
:else
|
||||
[:span {:style {:background-image (str/ffmt "url(%)" image)
|
||||
:height img-height
|
||||
:width img-width}
|
||||
:class (stl/css :image-inside)}])
|
||||
|
||||
[:span {:class (stl/css :image-text)} label]
|
||||
[:input {:on-change on-change'
|
||||
:type "radio"
|
||||
:class (stl/css :radio-input)
|
||||
:id key
|
||||
:name name
|
||||
:value value'
|
||||
:checked checked?}]]))]))
|
||||
|
||||
(mf/defc submit-button*
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [on-click children label form class name disabled] :as props}]
|
||||
|
@ -378,6 +444,7 @@
|
|||
:no-padding (pos? (count @items))
|
||||
:invalid (and (some? valid-item-fn)
|
||||
touched?
|
||||
(not (str/empty? @value))
|
||||
(not (valid-item-fn @value)))))
|
||||
|
||||
on-focus
|
||||
|
|
|
@ -38,10 +38,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.input-with-label {
|
||||
.input-with-label-form {
|
||||
@include flexColumn;
|
||||
gap: $s-8;
|
||||
@include bodySmallTypography;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
height: 100%;
|
||||
|
@ -55,6 +54,7 @@
|
|||
color: var(--input-foreground-color-active);
|
||||
margin-top: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 $s-8;
|
||||
|
||||
|
@ -64,6 +64,7 @@
|
|||
border-radius: $br-8;
|
||||
}
|
||||
}
|
||||
|
||||
// Input autofill
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
|
@ -169,6 +170,10 @@
|
|||
border-color: var(--input-checkbox-border-color-hover);
|
||||
}
|
||||
}
|
||||
a {
|
||||
// Need for terms and conditions links on register checkbox
|
||||
color: var(--link-foreground-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -367,7 +372,7 @@
|
|||
height: fit-content;
|
||||
border-radius: $br-8;
|
||||
padding: $s-8;
|
||||
color: var(--input-foreground-color);
|
||||
color: var(--input-foreground-color-rest);
|
||||
border: $s-1 solid transparent;
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
|
@ -393,14 +398,12 @@
|
|||
border-radius: $br-circle;
|
||||
}
|
||||
|
||||
.radio-label.with-image {
|
||||
.radio-label-image {
|
||||
@include smallTitleTipography;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 0px;
|
||||
justify-items: center;
|
||||
gap: 0;
|
||||
height: $s-116;
|
||||
width: $s-92;
|
||||
border-radius: $br-8;
|
||||
margin: 0;
|
||||
border: 1px solid var(--color-background-tertiary);
|
||||
|
@ -413,22 +416,29 @@
|
|||
outline: none;
|
||||
border: $s-1 solid var(--input-border-color-active);
|
||||
}
|
||||
.image-text {
|
||||
color: var(--input-foreground-color-rest);
|
||||
display: grid;
|
||||
align-self: center;
|
||||
margin-bottom: $s-16;
|
||||
padding-inline: $s-8;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.image-inside {
|
||||
width: $s-60;
|
||||
height: $s-48;
|
||||
background-size: $s-48;
|
||||
margin: $s-16;
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.icon-inside {
|
||||
width: $s-60;
|
||||
height: $s-48;
|
||||
margin: $s-16;
|
||||
@include flexCenter;
|
||||
svg {
|
||||
width: $s-60;
|
||||
height: $s-48;
|
||||
width: 40px;
|
||||
height: 60px;
|
||||
stroke: var(--icon-foreground);
|
||||
fill: none;
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
|
||||
.input-wrapper {
|
||||
@extend .input-with-label;
|
||||
@include bodySmallTypography;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
}
|
||||
.file-name-edit {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.file-name-label {
|
||||
|
|
|
@ -540,5 +540,6 @@
|
|||
|
||||
.email-input {
|
||||
@extend .input-base;
|
||||
@include bodySmallTypography;
|
||||
height: auto;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
|
||||
.group-name-input {
|
||||
@extend .input-element-label;
|
||||
@include bodySmallTypography;
|
||||
margin-bottom: $s-8;
|
||||
label {
|
||||
@include flexColumn;
|
||||
|
|
|
@ -93,6 +93,7 @@
|
|||
}
|
||||
.input-wrapper {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,161 +0,0 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.onboarding
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.onboarding.newsletter]
|
||||
[app.main.ui.onboarding.questions]
|
||||
[app.main.ui.onboarding.team-choice]
|
||||
[app.main.ui.onboarding.templates]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.timers :as tm]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; --- ONBOARDING LIGHTBOX
|
||||
|
||||
(defn send-event
|
||||
[event-name]
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name event-name
|
||||
::ev/origin "dashboard"})))
|
||||
|
||||
|
||||
(mf/defc onboarding-welcome
|
||||
[{:keys [next] :as props}]
|
||||
(let [go-next
|
||||
(fn []
|
||||
(send-event "onboarding-step1-continue")
|
||||
(next))]
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-left)}
|
||||
[:img {:src "images/welcomeilustration.svg"
|
||||
:border "0"
|
||||
:alt (tr "onboarding.welcome.alt")}]]
|
||||
[:div {:class (stl/css :modal-right)}
|
||||
[:div {:class (stl/css :release)}
|
||||
"Version " (:main cf/version)]
|
||||
[:h1 {:class (stl/css :modal-title)
|
||||
:data-test "onboarding-welcome"}
|
||||
(tr "onboarding-v2.welcome.title")]
|
||||
[:p {:class (stl/css :modal-text)}
|
||||
(tr "onboarding-v2.welcome.desc1")]
|
||||
|
||||
[:div {:class (stl/css :text-wrapper)}
|
||||
[:div {:class (stl/css :property-title)}
|
||||
[:a {:href "https://community.penpot.app/"
|
||||
:target "_blank"
|
||||
:on-click #(send-event "onboarding-community-link")}
|
||||
(tr "onboarding-v2.welcome.desc2.title")]]
|
||||
[:div {:class (stl/css :property-description)}
|
||||
(tr "onboarding-v2.welcome.desc2")]]
|
||||
|
||||
[:div {:class (stl/css :text-wrapper)}
|
||||
[:div {:class (stl/css :property-title)}
|
||||
[:a {:href "https://help.penpot.app/contributing-guide/"
|
||||
:target "_blank" :on-click #(send-event "onboarding-contributing-link")}
|
||||
(tr "onboarding-v2.welcome.desc3.title")]]
|
||||
[:div {:class (stl/css :property-description)}
|
||||
(tr "onboarding-v2.welcome.desc3")]]
|
||||
|
||||
[:button {:on-click go-next
|
||||
:class (stl/css :accept-btn)
|
||||
:data-test "onboarding-next-btn"}
|
||||
(tr "labels.continue")]]]))
|
||||
|
||||
(mf/defc onboarding-before-start
|
||||
[{:keys [next] :as props}]
|
||||
(let [go-next
|
||||
(fn []
|
||||
(send-event "onboarding-step2-continue")
|
||||
(next))]
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-left)}
|
||||
[:img {:src "images/beforeyoustartilustration.svg"
|
||||
:border "0"
|
||||
:alt (tr "onboarding.welcome.alt")}]]
|
||||
[:div {:class (stl/css :modal-right)}
|
||||
[:div {:class (stl/css :release)}
|
||||
"Version " (:main cf/version)]
|
||||
|
||||
[:h1 {:class (stl/css :modal-title)
|
||||
:data-test "onboarding-welcome"}
|
||||
(tr "onboarding-v2.before-start.title")]
|
||||
[:p {:class (stl/css :modal-text)}
|
||||
(tr "onboarding-v2.before-start.desc1")]
|
||||
|
||||
[:div {:class (stl/css :text-wrapper)}
|
||||
[:div {:class (stl/css :property-title)}
|
||||
[:a {:class (stl/css :modal-link)
|
||||
:href "https://help.penpot.app/user-guide/"
|
||||
:target "_blank"
|
||||
:on-click #(send-event "onboarding-user-guide-link")}
|
||||
(tr "onboarding-v2.before-start.desc2.title")]]
|
||||
[:div {:class (stl/css :property-description)}
|
||||
(tr "onboarding-v2.before-start.desc2")]]
|
||||
|
||||
[:div {:class (stl/css :text-wrapper)}
|
||||
[:div {:class (stl/css :property-title)}
|
||||
[:a {:class (stl/css :modal-link)
|
||||
:href "https://www.youtube.com/c/Penpot"
|
||||
:target "_blank"
|
||||
:on-click #(send-event "onboarding-video-tutorials-link")}
|
||||
(tr "onboarding-v2.before-start.desc3.title")]]
|
||||
[:div {:class (stl/css :property-description)}
|
||||
(tr "onboarding-v2.before-start.desc3")]]
|
||||
|
||||
|
||||
[:button {:on-click go-next
|
||||
:class (stl/css :accept-btn)
|
||||
:data-test "onboarding-next-btn"}
|
||||
(tr "labels.continue")]]]))
|
||||
|
||||
(mf/defc onboarding-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :onboarding}
|
||||
[_]
|
||||
(let [slide (mf/use-state :start)
|
||||
klass (mf/use-state "fadeInDown")
|
||||
|
||||
navigate
|
||||
(mf/use-fn #(reset! slide %))
|
||||
|
||||
skip
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (modal/hide)
|
||||
(du/mark-onboarding-as-viewed))
|
||||
(cond
|
||||
(contains? cf/flags :onboarding-questions)
|
||||
(modal/show! {:type :onboarding-questions})
|
||||
|
||||
(contains? cf/flags :onboarding-newsletter)
|
||||
(modal/show! {:type :onboarding-newsletter})
|
||||
|
||||
(contains? cf/flags :onboarding-team)
|
||||
(modal/show! {:type :onboarding-team}))))
|
||||
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
|
||||
(mf/with-effect [@slide]
|
||||
(when (not= :start @slide)
|
||||
(reset! klass "fadeIn"))
|
||||
(let [sem (tm/schedule 300 #(reset! klass nil))]
|
||||
(fn []
|
||||
(reset! klass nil)
|
||||
(tm/dispose! sem))))
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated {:class (dm/str @klass " " (stl/css :animated))}
|
||||
(case @slide
|
||||
:start [:& onboarding-welcome {:next #(navigate :opensource)}]
|
||||
:opensource [:& onboarding-before-start {:next skip}])]]))
|
|
@ -1,86 +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
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: $s-32;
|
||||
padding-inline: $s-100;
|
||||
padding-block-start: $s-100;
|
||||
padding-block-end: $s-72;
|
||||
margin: 0;
|
||||
width: $s-960;
|
||||
height: $s-632;
|
||||
max-width: $s-960;
|
||||
max-height: $s-632;
|
||||
}
|
||||
|
||||
.modal-left {
|
||||
width: $s-240;
|
||||
margin-block-end: $s-64;
|
||||
img {
|
||||
width: $s-240;
|
||||
height: 100%;
|
||||
border-radius: $br-8 0 0 $br-8;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-right {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: $s-40 auto auto auto $s-32;
|
||||
gap: $s-24;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.release {
|
||||
@include bodySmallTypography;
|
||||
position: absolute;
|
||||
top: calc(-1 * $s-28);
|
||||
right: 0;
|
||||
padding: $s-8;
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include bigTitleTipography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.modal-text,
|
||||
.property-description {
|
||||
@include bodyLargeTypography;
|
||||
margin: 0;
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.modal-link {
|
||||
@include bodyLargeTypography;
|
||||
color: var(--modal-link-foreground-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.text-wrapper {
|
||||
@include flexColumn;
|
||||
}
|
||||
|
||||
.property-title a {
|
||||
@include medTitleTipography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.accept-btn {
|
||||
@extend .modal-accept-btn;
|
||||
justify-self: flex-end;
|
||||
}
|
|
@ -8,39 +8,52 @@
|
|||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.main.data.events :as-alias ev]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc onboarding-newsletter
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :onboarding-newsletter}
|
||||
[]
|
||||
(let [message (tr "onboarding.newsletter.acceptance-message")
|
||||
newsletter-updates (mf/use-state false)
|
||||
newsletter-news (mf/use-state false)
|
||||
toggle
|
||||
(mf/use-callback
|
||||
(fn [option]
|
||||
(swap! option not)))
|
||||
(let [state* (mf/use-state #(do {:newsletter-updates false
|
||||
:newsletter-news false}))
|
||||
state (deref state*)
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [attr (-> (dom/get-current-target event)
|
||||
(dom/get-data "attr")
|
||||
(keyword))]
|
||||
(swap! state* update attr not))))
|
||||
|
||||
accept
|
||||
(mf/use-callback
|
||||
(mf/deps @newsletter-updates @newsletter-news)
|
||||
on-next
|
||||
(mf/use-fn
|
||||
(mf/deps state)
|
||||
(fn []
|
||||
(st/emit! (when (or @newsletter-updates @newsletter-news)
|
||||
(msg/success message))
|
||||
(modal/show {:type :onboarding-team})
|
||||
(du/update-profile-props {:newsletter-updates @newsletter-updates :newsletter-news @newsletter-news}))))
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
(when (or (:newsletter-updates state)
|
||||
(:newsletter-news state))
|
||||
(st/emit! (msg/success (tr "onboarding.newsletter.acceptance-message"))))
|
||||
|
||||
(let [params (-> state
|
||||
(assoc ::ev/name "onboarding-step")
|
||||
(assoc :label "newsletter:subscriptions")
|
||||
(assoc :step 6))]
|
||||
(st/emit! (ptk/data-event ::ev/event params)
|
||||
(du/update-profile-props state)))))
|
||||
|
||||
onboarding-a-b-test?
|
||||
(cf/external-feature-flag "signup-background" "test")]
|
||||
|
||||
[:div {:class (stl/css-case
|
||||
:modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated.fadeInDown {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-left)}
|
||||
[:img {:src "images/deco-newsletter.png"
|
||||
|
@ -50,30 +63,34 @@
|
|||
[:h2 {:class (stl/css :modal-title)
|
||||
:data-test "onboarding-newsletter-title"}
|
||||
(tr "onboarding.newsletter.title")]
|
||||
|
||||
[:p {:class (stl/css :modal-text)}
|
||||
(tr "onboarding-v2.newsletter.desc")]
|
||||
|
||||
|
||||
[:div {:class (stl/css :newsletter-options)}
|
||||
[:div {:class (stl/css :input-wrapper)}
|
||||
[:label {:for "newsletter-updates"}
|
||||
[:span {:class (stl/css-case :global/checked @newsletter-updates)}
|
||||
(when @newsletter-updates
|
||||
i/status-tick)]
|
||||
[:span {:class (stl/css-case :global/checked (:newsletter-updates state))}
|
||||
i/status-tick]
|
||||
|
||||
(tr "onboarding-v2.newsletter.updates")
|
||||
[:input {:type "checkbox"
|
||||
:id "newsletter-updates"
|
||||
:on-change #(toggle newsletter-updates)}]]]
|
||||
:data-attr "newsletter-updates"
|
||||
:value (:newsletter-updates state)
|
||||
:on-change on-change}]]]
|
||||
|
||||
[:div {:class (stl/css :input-wrapper)}
|
||||
[:label {:for "newsletter-news"}
|
||||
[:span {:class (stl/css-case :global/checked @newsletter-news)}
|
||||
(when @newsletter-news
|
||||
i/status-tick)]
|
||||
[:span {:class (stl/css-case :global/checked (:newsletter-news state))}
|
||||
i/status-tick]
|
||||
|
||||
(tr "onboarding-v2.newsletter.news")
|
||||
[:input {:type "checkbox"
|
||||
:id "newsletter-news"
|
||||
:on-change #(toggle newsletter-news)}]]]]
|
||||
:data-attr "newsletter-news"
|
||||
:value (:newsletter-news state)
|
||||
:on-change on-change}]]]]
|
||||
|
||||
[:p {:class (stl/css :modal-text)}
|
||||
(tr "onboarding-v2.newsletter.privacy1")
|
||||
|
@ -84,5 +101,6 @@
|
|||
[:p {:class (stl/css :modal-text)}
|
||||
(tr "onboarding-v2.newsletter.privacy2")]
|
||||
|
||||
[:button {:on-click accept
|
||||
:class (stl/css :accept-btn)} (tr "labels.continue")]]]]))
|
||||
[:button {:on-click on-next
|
||||
:class (stl/css :accept-btn)}
|
||||
(tr "labels.continue")]]]]))
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.events :as-alias ev]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
|
@ -19,223 +19,430 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc step-container
|
||||
[{:keys [form step on-next on-prev children class] :as props}]
|
||||
{::mf/props :obj}
|
||||
[{:keys [form step on-next on-prev children class label]}]
|
||||
|
||||
[:& fm/form {:form form :on-submit on-next :class (dm/str class " " (stl/css :form-wrapper))}
|
||||
[:div {:class (stl/css :paginator)} (str/ffmt "%/4" step)]
|
||||
(let [on-next*
|
||||
(mf/use-fn
|
||||
(mf/deps on-next step label)
|
||||
(fn [form event]
|
||||
(let [params (-> (:clean-data @form)
|
||||
(assoc :label label)
|
||||
(assoc :step step)
|
||||
(assoc ::ev/name "onboarding-step"))]
|
||||
(st/emit! (ptk/data-event ::ev/event params))
|
||||
(on-next form event))))]
|
||||
|
||||
children
|
||||
[:& fm/form {:form form
|
||||
:on-submit on-next*
|
||||
:class (dm/str class " " (stl/css :form-wrapper))}
|
||||
[:div {:class (stl/css :paginator)} (str/ffmt "%/5" step)]
|
||||
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
children
|
||||
|
||||
(when on-prev
|
||||
[:button {:class (stl/css :prev-button)
|
||||
:on-click on-prev} (tr "questions.previous")])
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:label (if (< step 4) (tr "questions.next") (tr "questions.start"))
|
||||
:class (stl/css :next-button)}]]])
|
||||
(when (some? on-prev)
|
||||
[:button {:class (stl/css :prev-button)
|
||||
:on-click on-prev}
|
||||
(tr "questions.previous")])
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:label (if (< step 5)
|
||||
(tr "questions.next")
|
||||
(tr "questions.start"))
|
||||
:class (stl/css :next-button)}]]]))
|
||||
|
||||
(s/def ::questions-form-step-1
|
||||
(s/keys :req-un [::planning]))
|
||||
|
||||
(mf/defc step-1
|
||||
[{:keys [on-next form] :as props}]
|
||||
[:& step-container {:form form :step 1 :on-next on-next :class (stl/css :step-1)}
|
||||
[:img {:class (stl/css :header-image)
|
||||
:src "images/form/use-for-1.png" :alt (tr "questions.lets-get-started")}]
|
||||
[:h1 {:class (stl/css :modal-title)} (tr "questions.lets-get-started")]
|
||||
[:p {:class (stl/css :modal-text)} (tr "questions.your-feedback-will-help-us")]
|
||||
|
||||
[:div {:class (stl/css :modal-question)}
|
||||
[:h3 {:class (stl/css :modal-subtitle)} (tr "questions.questions-how-are-you-planning-to-use-penpot")]
|
||||
[:& fm/select
|
||||
{:options [{:label (tr "questions.select-option")
|
||||
:value "" :key "questions-how-are-you-planning-to-use-penpot"
|
||||
:disabled true}
|
||||
{:label (tr "questions.discover-more-about-penpot")
|
||||
:value "discover-more-about-penpot"
|
||||
:key "discover-more-about-penpot"}
|
||||
{:label (tr "questions.test-penpot-to-see-if-its-a-fit-for-team")
|
||||
:value "test-penpot-to-see-if-its-a-fit-for-team"
|
||||
:key "test-penpot-to-see-if-its-a-fit-for-team"}
|
||||
{:label (tr "questions.start-to-work-on-my-project")
|
||||
:value "start-to-work-on-my-project"
|
||||
:key "start-to-work-on-my-project"}
|
||||
{:label (tr "questions.get-the-code-from-my-team-project")
|
||||
:value "get-the-code-from-my-team-project"
|
||||
:key "get-the-code-from-my-team-project"}
|
||||
{:label (tr "questions.leave-feedback-for-my-team-project")
|
||||
:value "leave-feedback-for-my-team-project"
|
||||
:key "leave-feedback-for-my-team-project"}
|
||||
{:label (tr "questions.work-in-concept-ideas")
|
||||
:value "work-in-concept-ideas"
|
||||
:key "work-in-concept-ideas"}
|
||||
{:label (tr "questions.try-out-before-using-penpot-on-premise")
|
||||
:value "try-out-before-using-penpot-on-premise"
|
||||
:key "try-out-before-using-penpot-on-premise"}]
|
||||
:default ""
|
||||
:name :planning
|
||||
:dropdown-class (stl/css :question-dropdown)}]]])
|
||||
|
||||
(s/def ::questions-form-step-2
|
||||
(s/keys :req-un [::experience-branding-illustrations-marketing-pieces
|
||||
::experience-interface-design-visual-assets-design-systems
|
||||
::experience-interface-wireframes-user-journeys-flows-navigation-trees]))
|
||||
|
||||
(mf/defc step-2
|
||||
[{:keys [on-next on-prev form] :as props}]
|
||||
[:& step-container {:form form :step 2 :on-next on-next :on-prev on-prev :class (stl/css :step-2)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
(tr "questions.describe-your-experience-working-on")]
|
||||
|
||||
[:div {:class (stl/css-case :modal-question true
|
||||
:question-centered true)}
|
||||
[:div {:class (stl/css-case :modal-subtitle true
|
||||
:centered true)}
|
||||
(tr "branding-illustrations-marketing-pieces")]
|
||||
[:& fm/radio-buttons {:options [{:label (tr "questions.none") :value "none"}
|
||||
{:label (tr "questions.some") :value "some"}
|
||||
{:label (tr "questions.a-lot") :value "a-lot"}]
|
||||
:name :experience-branding-illustrations-marketing-pieces
|
||||
:class (stl/css :radio-btns)}]]
|
||||
|
||||
[:div {:class (stl/css-case :modal-question true
|
||||
:question-centered true)}
|
||||
[:div {:class (stl/css-case :modal-subtitle true
|
||||
:centered true)}
|
||||
(tr "questions.interface-design-visual-assets-design-systems")]
|
||||
[:& fm/radio-buttons {:options [{:label (tr "questions.none") :value "none"}
|
||||
{:label (tr "questions.some") :value "some"}
|
||||
{:label (tr "questions.a-lot") :value "a-lot"}]
|
||||
:name :experience-interface-design-visual-assets-design-systems
|
||||
:class (stl/css :radio-btns)}]]
|
||||
|
||||
[:div {:class (stl/css-case :modal-question true
|
||||
:question-centered true)}
|
||||
[:div {:class (stl/css-case :modal-subtitle true
|
||||
:centered true)}
|
||||
(tr "questions.wireframes-user-journeys-flows-navigation-trees")]
|
||||
[:& fm/radio-buttons {:options [{:label (tr "questions.none") :value "none"}
|
||||
{:label (tr "questions.some") :value "some"}
|
||||
{:label (tr "questions.a-lot") :value "a-lot"}]
|
||||
:name :experience-interface-wireframes-user-journeys-flows-navigation-trees
|
||||
:class (stl/css :radio-btns)}]]])
|
||||
|
||||
(s/def ::questions-form-step-3
|
||||
(s/keys :req-un [::experience-design-tool]
|
||||
:opt-un [::experience-design-tool-other]))
|
||||
(s/keys :req-un [::planning
|
||||
::expected-use]
|
||||
:opt-un [::planning-other]))
|
||||
|
||||
(defn- step-1-form-validator
|
||||
[errors data]
|
||||
(let [planning (-> (:planning data) (str/trim))]
|
||||
(let [planning (:planning data)
|
||||
planning-other (:planning-other data)]
|
||||
(cond-> errors
|
||||
(= planning "")
|
||||
(and (= planning "other")
|
||||
(str/blank? planning-other))
|
||||
(assoc :planning-other {:code "missing"})
|
||||
|
||||
(not= planning "other")
|
||||
(assoc :planning-other nil)
|
||||
|
||||
(str/blank? planning)
|
||||
(assoc :planning {:code "missing"}))))
|
||||
|
||||
(mf/defc step-1
|
||||
{::mf/props :obj}
|
||||
[{:keys [on-next form]}]
|
||||
(let [use-options
|
||||
(mf/with-memo []
|
||||
(shuffle [{:label (tr "questions.use-work") :value "use-work"}
|
||||
{:label (tr "questions.use-education") :value "use-education"}
|
||||
{:label (tr "questions.use-personal") :value "use-personal"}]))
|
||||
|
||||
planning-options
|
||||
(mf/with-memo []
|
||||
(-> (shuffle [{:label (tr "questions.select-option")
|
||||
:value "" :key "questions:what-brings-you-here"
|
||||
:disabled true}
|
||||
{:label (tr "questions.reasons.exploring")
|
||||
:value "discover-more-about-penpot"
|
||||
:key "discover-more-about-penpot"}
|
||||
{:label (tr "questions.reasons.fit")
|
||||
:value "test-penpot-to-see-if-its-a-fit-for-team"
|
||||
:key "test-penpot-to-see-if-its-a-fit-for-team"}
|
||||
{:label (tr "questions.reasons.alternative")
|
||||
:value "alternative-to-figma"
|
||||
:key "alternative-to-figma"}
|
||||
{:label (tr "questions.reasons.testing")
|
||||
:value "try-out-before-using-penpot-on-premise"
|
||||
:key "try-out-before-using-penpot-on-premise"}])
|
||||
(conj {:label (tr "questions.other-short") :value "other"})))
|
||||
|
||||
current-planning
|
||||
(dm/get-in @form [:data :planning])]
|
||||
|
||||
[:& step-container {:form form
|
||||
:step 1
|
||||
:label "questions:about-you"
|
||||
:on-next on-next
|
||||
:class (stl/css :step-1)}
|
||||
|
||||
[:img {:class (stl/css :header-image)
|
||||
:src "images/form/use-for-1.png"
|
||||
:alt (tr "questions.lets-get-started")}]
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
(tr "questions.step1-title")]
|
||||
[:p {:class (stl/css :modal-text)}
|
||||
(tr "questions.step1-subtitle")]
|
||||
|
||||
[:div {:class (stl/css :modal-question)}
|
||||
[:h3 {:class (stl/css :modal-subtitle)}
|
||||
(tr "questions.step1-question1")]
|
||||
|
||||
[:& fm/radio-buttons {:options use-options
|
||||
:name :expected-use
|
||||
:class (stl/css :radio-btns)}]
|
||||
|
||||
[:h3 {:class (stl/css :modal-subtitle)}
|
||||
(tr "questions.step1-question2")]
|
||||
|
||||
[:& fm/select
|
||||
{:options planning-options
|
||||
:select-class (stl/css :select-class)
|
||||
:default ""
|
||||
:name :planning
|
||||
:dropdown-class (stl/css :question-dropdown)}]
|
||||
|
||||
(when (= current-planning "other")
|
||||
[:& fm/input {:name :planning-other
|
||||
:class (stl/css :input-spacing)
|
||||
:placeholder (tr "questions.other")
|
||||
:label ""}])]]))
|
||||
|
||||
(s/def ::questions-form-step-2
|
||||
(s/keys :req-un [::experience-design-tool]
|
||||
:opt-un [::experience-design-tool-other]))
|
||||
|
||||
(defn- step-2-form-validator
|
||||
[errors data]
|
||||
(let [experience (:experience-design-tool data)
|
||||
experience-other (:experience-design-tool-other data)]
|
||||
|
||||
(cond-> errors
|
||||
(and (= experience "other")
|
||||
(str/blank? experience-other))
|
||||
(assoc :experience-design-tool-other {:code "missing"})
|
||||
|
||||
(not= experience "other")
|
||||
(assoc :experience-design-tool-other nil))))
|
||||
|
||||
(mf/defc step-2
|
||||
{::mf/props :obj}
|
||||
[{:keys [on-next on-prev form]}]
|
||||
(let [design-tool-options
|
||||
(mf/with-memo []
|
||||
(-> (shuffle [{:label (tr "questions.figma") :img-width "48px" :img-height "60px"
|
||||
:value "figma" :image "images/form/figma.png"}
|
||||
{:label (tr "questions.sketch") :img-width "48px" :img-height "60px"
|
||||
:value "sketch" :image "images/form/sketch.png"}
|
||||
{:label (tr "questions.adobe-xd") :img-width "48px" :img-height "60px"
|
||||
:value "adobe-xd" :image "images/form/adobe-xd.png"}
|
||||
{:label (tr "questions.canva") :img-width "48px" :img-height "60px"
|
||||
:value "canva" :image "images/form/canva.png"}
|
||||
{:label (tr "questions.invision") :img-width "48px" :img-height "60px"
|
||||
:value "invision" :image "images/form/invision.png"}])
|
||||
(conj {:label (tr "questions.other-short") :value "other" :icon i/curve})))
|
||||
|
||||
current-experience
|
||||
(dm/get-in @form [:clean-data :experience-design-tool])
|
||||
|
||||
on-design-tool-change
|
||||
(mf/use-fn
|
||||
(mf/deps current-experience)
|
||||
(fn []
|
||||
(when (not= current-experience "other")
|
||||
(swap! form d/dissoc-in [:data :experience-design-tool-other])
|
||||
(swap! form d/dissoc-in [:errors :experience-design-tool-other]))))]
|
||||
|
||||
[:& step-container {:form form
|
||||
:step 2
|
||||
:label "questions:experience-design-tool"
|
||||
:on-next on-next
|
||||
:on-prev on-prev
|
||||
:class (stl/css :step-2)}
|
||||
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
(tr "question.design-tool-more-used")]
|
||||
[:div {:class (stl/css :radio-wrapper)}
|
||||
[:& fm/image-radio-buttons {:options design-tool-options
|
||||
:img-width "48px"
|
||||
:img-height "60px"
|
||||
:name :experience-design-tool
|
||||
:image true
|
||||
:class (stl/css :image-radio)
|
||||
:on-change on-design-tool-change}]
|
||||
|
||||
(when (= current-experience "other")
|
||||
[:& fm/input {:name :experience-design-tool-other
|
||||
:class (stl/css :input-spacing)
|
||||
:placeholder (tr "questions.other")
|
||||
:label ""}])]]))
|
||||
|
||||
(s/def ::questions-form-step-3
|
||||
(s/keys :req-un [::team-size ::role ::responsability]
|
||||
:opt-un [::role-other ::responsability-other]))
|
||||
|
||||
(defn- step-3-form-validator
|
||||
[errors data]
|
||||
(let [experience-design-tool (:experience-design-tool data)
|
||||
experience-design-tool-other (-> (:experience-design-tool-other data) str/trim)]
|
||||
(let [role (:role data)
|
||||
role-other (:role-other data)
|
||||
responsability (:responsability data)
|
||||
responsability-other (:responsability-other data)]
|
||||
|
||||
(cond-> errors
|
||||
(and (= experience-design-tool "other") (= 0 (count experience-design-tool-other)))
|
||||
(assoc :experience-design-tool-other {:code "missing"}))))
|
||||
(and (= role "other")
|
||||
(str/blank? role-other))
|
||||
(assoc :role-other {:code "missing"})
|
||||
|
||||
(not= role "other")
|
||||
(assoc :role-other nil)
|
||||
|
||||
(and (= responsability "other")
|
||||
(str/blank? responsability-other))
|
||||
(assoc :responsability-other {:code "missing"})
|
||||
|
||||
(not= responsability "other")
|
||||
(assoc :responsability-other nil))))
|
||||
|
||||
(mf/defc step-3
|
||||
[{:keys [on-next on-prev form] :as props}]
|
||||
(let [experience-design-tool (dm/get-in @form [:clean-data :experience-design-tool])
|
||||
on-design-tool-change
|
||||
(fn [_ _]
|
||||
(let [experience-design-tool (dm/get-in @form [:clean-data :experience-design-tool])]
|
||||
(when (not= experience-design-tool "other")
|
||||
(do
|
||||
(swap! form d/dissoc-in [:data :experience-design-tool-other])
|
||||
(swap! form d/dissoc-in [:errors :experience-design-tool-other])))))]
|
||||
{::mf/props :obj}
|
||||
[{:keys [on-next on-prev form]}]
|
||||
(let [role-options
|
||||
(mf/with-memo []
|
||||
(-> (shuffle [{:label (tr "questions.select-option") :value "" :key "role" :disabled true}
|
||||
{:label (tr "questions.work-type.ux") :value "designer" :key "designer"}
|
||||
{:label (tr "questions.work-type.dev") :value "developer" :key "developer"}
|
||||
{:label (tr "questions.work-type.student") :value "student-teacher" :key "student"}
|
||||
{:label (tr "questions.work-type.graphic") :value "graphic-design" :key "design"}
|
||||
{:label (tr "questions.work-type.marketing") :value "marketing" :key "marketing"}
|
||||
{:label (tr "questions.work-type.product") :value "manager" :key "manager"}])
|
||||
(conj {:label (tr "questions.other-short") :value "other"})))
|
||||
|
||||
[:& step-container {:form form :step 3 :on-next on-next :on-prev on-prev :class (stl/css :step-3)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
(tr "question.design-tool-more-experienced-with")]
|
||||
[:div {:class (stl/css :radio-wrapper)}
|
||||
[:& fm/radio-buttons {:options [{:label (tr "questions.figma") :value "figma" :image "images/form/figma.png" :area "image1"}
|
||||
{:label (tr "questions.sketch") :value "sketch" :image "images/form/sketch.png" :area "image2"}
|
||||
{:label (tr "questions.adobe-xd") :value "adobe-xd" :image "images/form/adobe-xd.png" :area "image3"}
|
||||
{:label (tr "questions.canva") :value "canva" :image "images/form/canva.png" :area "image4"}
|
||||
{:label (tr "questions.invision") :value "invision" :image "images/form/invision.png" :area "image5"}
|
||||
{:label (tr "questions.never-used-one") :area "image6" :value "never-used-a-tool" :icon i/curve}
|
||||
{:label (tr "questions.other") :value "other" :area "other"}]
|
||||
:name :experience-design-tool
|
||||
:image true
|
||||
:class (stl/css :image-radio)
|
||||
:on-change on-design-tool-change}]
|
||||
responsability-options
|
||||
(mf/with-memo []
|
||||
(-> (shuffle [{:label (tr "questions.select-option") :value "" :key "responsability" :disabled true}
|
||||
{:label (tr "questions.role.team-leader") :value "team-leader"}
|
||||
{:label (tr "questions.role.team-member") :value "team-member"}
|
||||
{:label (tr "questions.role.freelancer") :value "freelancer"}
|
||||
{:label (tr "questions.role.founder") :value "ceo-founder"}
|
||||
{:label (tr "questions.role.director") :value "director"}
|
||||
{:label (tr "questions.student-teacher") :value "student-teacher"}])
|
||||
(conj {:label (tr "questions.other-short") :value "other"})))
|
||||
|
||||
[:& fm/input {:name :experience-design-tool-other
|
||||
:class (stl/css :input-spacing)
|
||||
:placeholder (tr "questions.other")
|
||||
:label ""
|
||||
:disabled (not= experience-design-tool "other")}]]]))
|
||||
|
||||
team-size-options
|
||||
(mf/with-memo []
|
||||
[{:label (tr "questions.select-option") :value "" :key "team-size" :disabled true}
|
||||
{:label (tr "questions.more-than-50") :value "more-than-50" :key "more-than-50"}
|
||||
{:label (tr "questions.31-50") :value "31-50" :key "31-50"}
|
||||
{:label (tr "questions.11-30") :value "11-30" :key "11-30"}
|
||||
{:label (tr "questions.2-10") :value "2-10" :key "2-10"}
|
||||
{:label (tr "questions.freelancer") :value "freelancer" :key "freelancer"}
|
||||
{:label (tr "questions.personal-project") :value "personal-project" :key "personal-project"}])
|
||||
|
||||
current-role
|
||||
(dm/get-in @form [:data :role])
|
||||
|
||||
current-responsability
|
||||
(dm/get-in @form [:data :responsability])]
|
||||
|
||||
[:& step-container {:form form
|
||||
:step 3
|
||||
:label "questions:about-your-job"
|
||||
:on-next on-next
|
||||
:on-prev on-prev
|
||||
:class (stl/css :step-3)}
|
||||
|
||||
[:h1 {:class (stl/css :modal-title)} (tr "questions.step3-title")]
|
||||
[:div {:class (stl/css :modal-question)}
|
||||
[:h3 {:class (stl/css :modal-subtitle)} (tr "questions.step3.question1")]
|
||||
[:& fm/select {:options role-options
|
||||
:select-class (stl/css :select-class)
|
||||
:default ""
|
||||
:name :role}]
|
||||
|
||||
(when (= current-role "other")
|
||||
[:& fm/input {:name :role-other
|
||||
:class (stl/css :input-spacing)
|
||||
:placeholder (tr "questions.other")
|
||||
:label ""}])]
|
||||
|
||||
[:div {:class (stl/css :modal-question)}
|
||||
[:h3 {:class (stl/css :modal-subtitle)} (tr "questions.step3.question2")]
|
||||
[:& fm/select {:options responsability-options
|
||||
:select-class (stl/css :select-class)
|
||||
:default ""
|
||||
:name :responsability}]
|
||||
|
||||
(when (= current-responsability "other")
|
||||
[:& fm/input {:name :responsability-other
|
||||
:class (stl/css :input-spacing)
|
||||
:placeholder (tr "questions.other")
|
||||
:label ""}])]
|
||||
|
||||
[:div {:class (stl/css :modal-question)}
|
||||
[:h3 {:class (stl/css :modal-subtitle)} (tr "questions.company-size")]
|
||||
[:& fm/select {:options team-size-options
|
||||
:default ""
|
||||
:select-class (stl/css :select-class)
|
||||
:name :team-size}]]]))
|
||||
|
||||
(s/def ::questions-form-step-4
|
||||
(s/keys :req-un [::team-size ::role]
|
||||
:opt-un [::role-other]))
|
||||
(s/keys :req-un [::start-with]
|
||||
:opt-un [::start-with-other]))
|
||||
|
||||
(defn- step-4-form-validator
|
||||
[errors data]
|
||||
(let [role (:role data)
|
||||
role-other (-> (:role-other data) str/trim)]
|
||||
(let [start (:start-with data)
|
||||
start-other (:start-with-other data)]
|
||||
(cond-> errors
|
||||
(and (= role "other") (= 0 (count role-other)))
|
||||
(assoc :role-other {:code "missing"}))))
|
||||
(and (= start "other")
|
||||
(str/blank? start-other))
|
||||
(assoc :start-with-other {:code "missing"})
|
||||
|
||||
(not= start "other")
|
||||
(assoc :start-with-other nil))))
|
||||
|
||||
(mf/defc step-4
|
||||
[{:keys [on-next on-prev form] :as props}]
|
||||
(let [role (dm/get-in @form [:data :role])
|
||||
on-role-change
|
||||
(fn [_ _]
|
||||
(let [experience-design-tool (dm/get-in @form [:clean-data :experience-design-tool])]
|
||||
(when (not= experience-design-tool "other")
|
||||
(do
|
||||
(swap! form d/dissoc-in [:data :role-other])
|
||||
(swap! form d/dissoc-in [:errors :role-other])))))]
|
||||
{::mf/props :obj}
|
||||
[{:keys [on-next on-prev form]}]
|
||||
(let [start-options
|
||||
(mf/with-memo []
|
||||
(-> (shuffle [{:label (tr "questions.starting-ui") :value "ui" :image "images/form/Design.png"}
|
||||
{:label (tr "questions.starting-wireframing") :value "wireframing" :image "images/form/templates.png"}
|
||||
{:label (tr "questions.starting-prototyping") :value "prototyping" :image "images/form/Prototype.png"}
|
||||
{:label (tr "questions.starting-ds") :value "ds" :image "images/form/components.png"}
|
||||
{:label (tr "questions.starting-code") :value "code" :image "images/form/design-and-dev.png"}])
|
||||
(conj {:label (tr "questions.other-short") :value "other" :icon i/curve})))
|
||||
|
||||
[:& step-container {:form form :step 4 :on-next on-next :on-prev on-prev :class (stl/css :step-4)}
|
||||
[:h1 {:class (stl/css :modal-title)} (tr "questions.role")]
|
||||
current-start (dm/get-in @form [:data :start-with])
|
||||
|
||||
on-start-change
|
||||
(mf/use-fn
|
||||
(mf/deps current-start)
|
||||
(fn [_ _]
|
||||
(when (not= current-start "other")
|
||||
(swap! form d/dissoc-in [:data :start-with-other])
|
||||
(swap! form d/dissoc-in [:errors :start-with-other]))))]
|
||||
|
||||
[:& step-container {:form form
|
||||
:step 4
|
||||
:label "questions:how-start"
|
||||
:on-next on-next
|
||||
:on-prev on-prev
|
||||
:class (stl/css :step-4)}
|
||||
|
||||
[:h1 {:class (stl/css :modal-title)} (tr "questions.step4-title")]
|
||||
[:div {:class (stl/css :radio-wrapper)}
|
||||
[:& fm/radio-buttons {:options [{:label (tr "questions.designer") :value "designer"}
|
||||
{:label (tr "questions.developer") :value "developer"}
|
||||
{:label (tr "questions.manager") :value "manager"}
|
||||
{:label (tr "questions.founder") :value "founder"}
|
||||
{:label (tr "questions.marketing") :value "marketing"}
|
||||
{:label (tr "questions.student-teacher") :value "student-teacher"}
|
||||
{:label (tr "questions.other") :value "other"}]
|
||||
:name :role
|
||||
:on-change on-role-change}]
|
||||
[:& fm/input {:name :role-other
|
||||
:class (stl/css :input-spacing)
|
||||
:label ""
|
||||
:placeholder (tr "questions.other")
|
||||
:disabled (not= role "other")}]]
|
||||
[:& fm/image-radio-buttons {:options start-options
|
||||
:img-width "159px"
|
||||
:img-height "120px"
|
||||
:class (stl/css :image-radio)
|
||||
:name :start-with
|
||||
:on-change on-start-change}]
|
||||
|
||||
[:div {:class (stl/css :modal-question)}
|
||||
[:h3 {:class (stl/css :modal-subtitle)} (tr "questions.team-size")]
|
||||
[:& fm/select {:options [{:label (tr "questions.select-option") :value "" :key "team-size" :disabled true}
|
||||
{:label (tr "questions.more-than-50") :value "more-than-50" :key "more-than-50"}
|
||||
{:label (tr "questions.31-50") :value "31-50" :key "31-50"}
|
||||
{:label (tr "questions.11-30") :value "11-30" :key "11-30"}
|
||||
{:label (tr "questions.2-10") :value "2-10" :key "2-10"}
|
||||
{:label (tr "questions.freelancer") :value "freelancer" :key "freelancer"}
|
||||
{:label (tr "questions.personal-project") :value "personal-project" :key "personal-project"}]
|
||||
:default ""
|
||||
:name :team-size}]]]))
|
||||
(when (= current-start "other")
|
||||
[:& fm/input {:name :start-with-other
|
||||
:class (stl/css :input-spacing)
|
||||
:label ""
|
||||
:placeholder (tr "questions.other")}])]]))
|
||||
|
||||
;; NOTE: we don't register it on registry modal because we reference
|
||||
;; this modal directly on the ui namespace.
|
||||
(s/def ::questions-form-step-5
|
||||
(s/keys :req-un [::referer]
|
||||
:opt-un [::referer-other]))
|
||||
|
||||
(defn- step-5-form-validator
|
||||
[errors data]
|
||||
(let [referer (:referer data)
|
||||
referer-other (:referer-other data)]
|
||||
(cond-> errors
|
||||
(and (= referer "other")
|
||||
(str/blank? referer-other))
|
||||
(assoc :referer-other {:code "missing"})
|
||||
|
||||
(not= referer "other")
|
||||
(assoc :referer-other nil))))
|
||||
|
||||
(mf/defc step-5
|
||||
{::mf/props :obj}
|
||||
[{:keys [on-next on-prev form]}]
|
||||
(let [referer-options
|
||||
(mf/with-memo []
|
||||
(-> (shuffle [{:label (tr "questions.referer.youtube") :value "youtube"}
|
||||
{:label (tr "questions.referer.event") :value "event"}
|
||||
{:label (tr "questions.referer.search") :value "search"}
|
||||
{:label (tr "questions.referer.social") :value "social"}
|
||||
{:label (tr "questions.referer.article") :value "article"}])
|
||||
(conj {:label (tr "questions.other-short") :value "other"})))
|
||||
|
||||
current-referer
|
||||
(dm/get-in @form [:data :referer])
|
||||
|
||||
on-referer-change
|
||||
(mf/use-fn
|
||||
(mf/deps current-referer)
|
||||
(fn []
|
||||
(when (not= current-referer "other")
|
||||
(swap! form d/dissoc-in [:data :referer-other])
|
||||
(swap! form d/dissoc-in [:errors :referer-other]))))]
|
||||
|
||||
[:& step-container {:form form
|
||||
:step 5
|
||||
:label "questions:referer"
|
||||
:on-next on-next
|
||||
:on-prev on-prev
|
||||
:class (stl/css :step-5)}
|
||||
|
||||
[:h1 {:class (stl/css :modal-title)} (tr "questions.step5-title")]
|
||||
[:div {:class (stl/css :radio-wrapper)}
|
||||
[:& fm/radio-buttons {:options referer-options
|
||||
:class (stl/css :radio-btns)
|
||||
:name :referer
|
||||
:on-change on-referer-change}]
|
||||
(when (= current-referer "other")
|
||||
[:& fm/input {:name :referer-other
|
||||
:class (stl/css :input-spacing)
|
||||
:label ""
|
||||
:placeholder (tr "questions.other")}])]]))
|
||||
|
||||
(mf/defc questions-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :onboarding-questions}
|
||||
[]
|
||||
(let [container (mf/use-ref)
|
||||
step (mf/use-state 1)
|
||||
|
@ -247,9 +454,12 @@
|
|||
:initial {}
|
||||
:validators [step-1-form-validator]
|
||||
:spec ::questions-form-step-1)
|
||||
|
||||
step-2-form (fm/use-form
|
||||
:initial {}
|
||||
:validators [step-2-form-validator]
|
||||
:spec ::questions-form-step-2)
|
||||
|
||||
step-3-form (fm/use-form
|
||||
:initial {}
|
||||
:validators [step-3-form-validator]
|
||||
|
@ -260,6 +470,11 @@
|
|||
:validators [step-4-form-validator]
|
||||
:spec ::questions-form-step-4)
|
||||
|
||||
step-5-form (fm/use-form
|
||||
:initial {}
|
||||
:validators [step-5-form-validator]
|
||||
:spec ::questions-form-step-5)
|
||||
|
||||
on-next
|
||||
(mf/use-fn
|
||||
(fn [form]
|
||||
|
@ -275,27 +490,22 @@
|
|||
(mf/use-fn
|
||||
(mf/deps @clean-data)
|
||||
(fn [form]
|
||||
(let [questionnaire (merge @clean-data (:clean-data @form))]
|
||||
(reset! clean-data questionnaire)
|
||||
(st/emit! (du/mark-questions-as-answered questionnaire))
|
||||
(let [data (merge @clean-data (:clean-data @form))]
|
||||
(reset! clean-data data)
|
||||
(st/emit! (du/mark-questions-as-answered data)))))
|
||||
|
||||
(cond
|
||||
(contains? cf/flags :onboarding-newsletter)
|
||||
(modal/show! {:type :onboarding-newsletter})
|
||||
onboarding-a-b-test?
|
||||
(cf/external-feature-flag "signup-background" "test")]
|
||||
|
||||
(contains? cf/flags :onboarding-team)
|
||||
(modal/show! {:type :onboarding-team})
|
||||
|
||||
:else
|
||||
(modal/hide!)))))
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div {:class (stl/css-case
|
||||
:modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div {:class (stl/css :modal-container)
|
||||
:ref container}
|
||||
|
||||
(case @step
|
||||
1 [:& step-1 {:on-next on-next :on-prev on-prev :form step-1-form}]
|
||||
2 [:& step-2 {:on-next on-next :on-prev on-prev :form step-2-form}]
|
||||
3 [:& step-3 {:on-next on-next :on-prev on-prev :form step-3-form}]
|
||||
4 [:& step-4 {:on-next on-submit :on-prev on-prev :form step-4-form}])]]))
|
||||
4 [:& step-4 {:on-next on-next :on-prev on-prev :form step-4-form}]
|
||||
5 [:& step-5 {:on-next on-submit :on-prev on-prev :form step-5-form}])]]))
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
}
|
||||
|
||||
.modal-container {
|
||||
max-width: $s-744;
|
||||
max-width: $s-960;
|
||||
max-height: fit-content;
|
||||
width: $s-744;
|
||||
width: fit-content;
|
||||
padding-inline: $s-100;
|
||||
padding-block-start: $s-40;
|
||||
padding-block-end: $s-72;
|
||||
|
@ -47,15 +47,15 @@
|
|||
@extend .modal-cancel-btn;
|
||||
}
|
||||
|
||||
.radio-btns label,
|
||||
.select-class span {
|
||||
@include bodyMediumTypography;
|
||||
}
|
||||
|
||||
// STEP 1
|
||||
|
||||
// .step-1 {
|
||||
// max-height: $s-468;
|
||||
// height: $s-468;
|
||||
// }
|
||||
|
||||
.header-image {
|
||||
height: $s-112;
|
||||
height: $s-60;
|
||||
width: auto;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
@ -81,9 +81,15 @@
|
|||
}
|
||||
|
||||
// STEP-2
|
||||
|
||||
.step-1,
|
||||
.step-2,
|
||||
.step-3,
|
||||
.step-5 {
|
||||
max-width: $s-540;
|
||||
width: $s-540;
|
||||
}
|
||||
.step-2 {
|
||||
grid-template-rows: $s-20 auto auto auto auto $s-32;
|
||||
grid-template-rows: $s-20 auto auto $s-32;
|
||||
}
|
||||
|
||||
.modal-question {
|
||||
|
@ -103,36 +109,36 @@
|
|||
.radio-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: $s-8;
|
||||
gap: $s-16;
|
||||
}
|
||||
|
||||
// STEP-3
|
||||
.step-3 {
|
||||
grid-template-rows: $s-20 auto auto $s-32;
|
||||
grid-template-rows: $s-20 auto auto auto auto $s-32;
|
||||
}
|
||||
|
||||
.image-radio {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr $s-32;
|
||||
grid-template-columns: $s-88 $s-92 $s-92 $s-92 $s-88;
|
||||
grid-template-areas:
|
||||
". image1 image2 image3 ."
|
||||
". image4 image5 image6 ."
|
||||
"other other other other other";
|
||||
grid-template-rows: 1fr 1fr;
|
||||
grid-template-columns: $s-92 $s-92 $s-92;
|
||||
row-gap: $s-16;
|
||||
column-gap: $s-24;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.input-spacing {
|
||||
height: $s-32;
|
||||
width: calc(100% - $s-24);
|
||||
margin-inline-start: $s-24;
|
||||
width: 100%;
|
||||
margin-block-end: $s-8;
|
||||
}
|
||||
|
||||
.input-spacing input {
|
||||
@include bodyMediumTypography;
|
||||
}
|
||||
|
||||
// STEP-4
|
||||
|
||||
.step-4 {
|
||||
grid-template-rows: $s-20 auto auto auto $s-32;
|
||||
grid-template-rows: $s-20 auto auto $s-32;
|
||||
row-gap: $s-16;
|
||||
}
|
||||
|
|
|
@ -7,34 +7,29 @@
|
|||
(ns app.main.ui.onboarding.team-choice
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dmc]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.timers :as tm]
|
||||
[cljs.spec.alpha :as s]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::team-form
|
||||
(s/keys :req-un [::name]))
|
||||
|
||||
(mf/defc team-modal-left
|
||||
(mf/defc left-sidebar
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[]
|
||||
[:div {:class (stl/css :modal-left)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
(tr "onboarding-v2.welcome.title")]
|
||||
|
||||
[:h2 {:class (stl/css :modal-subtitle)}
|
||||
(tr "onboarding.team-modal.team-definition")]
|
||||
[:p {:class (stl/css :modal-text)}
|
||||
|
@ -61,99 +56,27 @@
|
|||
[:p {:class (stl/css :modal-desc)}
|
||||
(tr "onboarding.team-modal.create-team-feature-5")]]]])
|
||||
|
||||
(mf/defc onboarding-team-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :onboarding-team}
|
||||
[]
|
||||
(let [form (fm/use-form :spec ::team-form
|
||||
:initial {}
|
||||
:validators [(fm/validate-not-empty :name (tr "auth.name.not-all-space"))
|
||||
(fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))])
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(fn [form _]
|
||||
(let [tname (get-in @form [:clean-data :name])]
|
||||
(st/emit! (modal/show {:type :onboarding-team-invitations :name tname})
|
||||
(ptk/event ::ev/event {::ev/name "choose-team-name"
|
||||
::ev/origin "onboarding"
|
||||
:name tname
|
||||
:step 1})))))
|
||||
on-skip
|
||||
(fn []
|
||||
(tm/schedule 400 #(st/emit! (modal/hide)
|
||||
(ptk/event ::ev/event {::ev/name "create-team-later"
|
||||
::ev/origin "onboarding"
|
||||
:step 1}))))
|
||||
|
||||
teams (mf/deref refs/teams)
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
|
||||
(mf/with-effect [teams]
|
||||
(when (> (count teams) 1)
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
(when (< (count teams) 2)
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated.fadeIn {:class (stl/css :modal-container)}
|
||||
[:& team-modal-left]
|
||||
[:div {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :modal-right)}
|
||||
[:div {:class (stl/css :first-block)}
|
||||
[:h2 {:class (stl/css :modal-subtitle)}
|
||||
(tr "onboarding.team-modal.create-team")]
|
||||
[:p {:class (stl/css :modal-text)}
|
||||
(tr "onboarding.choice.team-up.create-team-desc")]
|
||||
[:& fm/form {:form form
|
||||
:class (stl/css :modal-form)
|
||||
:on-submit on-submit}
|
||||
|
||||
[:& fm/input {:type "text"
|
||||
:class (stl/css :team-name-input)
|
||||
:name :name
|
||||
:placeholder "Team name"
|
||||
:label (tr "onboarding.choice.team-up.create-team-placeholder")}]
|
||||
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:> fm/submit-button*
|
||||
{:class (stl/css :accept-button)
|
||||
:label (tr "onboarding.choice.team-up.continue-creating-team")}]]]]
|
||||
[:div {:class (stl/css :second-block)}
|
||||
[:h2 {:class (stl/css :modal-subtitle)}
|
||||
(tr "onboarding.choice.team-up.start-without-a-team")]
|
||||
[:p {:class (stl/css :modal-text)}
|
||||
(tr "onboarding.choice.team-up.start-without-a-team-description")]
|
||||
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:button {:class (stl/css :accept-button)
|
||||
:on-click on-skip}
|
||||
(tr "onboarding.choice.team-up.continue-without-a-team")]]]]
|
||||
|
||||
[:div {:class (stl/css :paginator)} "1/2"]]])))
|
||||
|
||||
(defn get-available-roles
|
||||
[]
|
||||
[{:value "editor" :label (tr "labels.editor")}
|
||||
{:value "admin" :label (tr "labels.admin")}])
|
||||
|
||||
(s/def ::emails (s/and ::us/set-of-valid-emails))
|
||||
(s/def ::role ::us/keyword)
|
||||
(s/def ::invite-form
|
||||
(s/keys :req-un [::role ::emails]))
|
||||
|
||||
;; This is the final step of team creation, consists in provide a
|
||||
;; shortcut for invite users.
|
||||
(defn- get-available-roles
|
||||
[]
|
||||
[{:value "editor" :label (tr "labels.editor")}
|
||||
{:value "admin" :label (tr "labels.admin")}])
|
||||
|
||||
(mf/defc team-form-step-2
|
||||
{::mf/props :obj}
|
||||
[{:keys [name on-back]}]
|
||||
(let [initial (mf/use-memo
|
||||
#(do {:role "editor"
|
||||
:name name}))
|
||||
|
||||
(mf/defc onboarding-team-invitations-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :onboarding-team-invitations
|
||||
::mf/props :obj}
|
||||
[{:keys [name]}]
|
||||
(let [initial (mf/use-memo (constantly
|
||||
{:role "editor"
|
||||
:name name}))
|
||||
form (fm/use-form :spec ::invite-form
|
||||
:initial initial)
|
||||
|
||||
params (:clean-data @form)
|
||||
emails (:emails params)
|
||||
|
||||
|
@ -161,51 +84,48 @@
|
|||
|
||||
on-success
|
||||
(mf/use-fn
|
||||
(fn [_form response]
|
||||
(let [team-id (:id response)]
|
||||
(st/emit!
|
||||
(modal/hide)
|
||||
(rt/nav :dashboard-projects {:team-id team-id}))
|
||||
(tm/schedule 400 #(st/emit!
|
||||
(modal/hide))))))
|
||||
(fn [response]
|
||||
(let [team-id (:id response)]
|
||||
(st/emit! (du/update-profile-props {:onboarding-team-id team-id
|
||||
:onboarding-viewed true})
|
||||
(rt/nav :dashboard-projects {:team-id team-id})))))
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
(fn [_form _cause]
|
||||
(st/emit! (msg/error "Error on creating team."))))
|
||||
(fn [_]
|
||||
(st/emit! (msg/error (tr "errors.generic")))))
|
||||
|
||||
;; The SKIP branch only creates the team, without invitations
|
||||
on-invite-later
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
(let [mdata {:on-success (partial on-success form)
|
||||
:on-error (partial on-error form)}
|
||||
(fn [{:keys [name]}]
|
||||
(let [mdata {:on-success on-success
|
||||
:on-error on-error}
|
||||
params {:name name}]
|
||||
(st/emit! (dd/create-team (with-meta params mdata))
|
||||
(ptk/event ::ev/event {::ev/name "create-team-and-invite-later"
|
||||
::ev/origin "onboarding"
|
||||
:name name
|
||||
:step 2})))))
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "onboarding-step"
|
||||
:label "team:create-team-and-invite-later"
|
||||
:team-name name
|
||||
:step 7})
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "onboarding-finish"})))))
|
||||
|
||||
;; The SUBMIT branch creates the team with the invitations
|
||||
on-invite-now
|
||||
(mf/use-fn
|
||||
(fn [form]
|
||||
(let [mdata {:on-success (partial on-success form)
|
||||
:on-error (partial on-error form)}
|
||||
params (:clean-data @form)
|
||||
emails (:emails params)]
|
||||
(fn [{:keys [name] :as params}]
|
||||
(let [mdata {:on-success on-success
|
||||
:on-error on-error}]
|
||||
|
||||
(st/emit! (if (> (count emails) 0)
|
||||
;; If the user is only inviting to itself we don't call to create-team-with-invitations
|
||||
(dd/create-team-with-invitations (with-meta params mdata))
|
||||
(dd/create-team (with-meta {:name name} mdata)))
|
||||
(ptk/event ::ev/event {::ev/name "create-team-and-send-invitations"
|
||||
::ev/origin "onboarding"
|
||||
:invites (count emails)
|
||||
:role (:role params)
|
||||
:name name
|
||||
:step 2})))))
|
||||
(st/emit! (dd/create-team-with-invitations (with-meta params mdata))
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "onboarding-step"
|
||||
:label "team:create-team-and-invite"
|
||||
:invites (count emails)
|
||||
:team-name name
|
||||
:role (:role params)
|
||||
:step 7})
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "onboarding-finish"})))))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
|
@ -213,55 +133,146 @@
|
|||
(let [params (:clean-data @form)
|
||||
emails (:emails params)]
|
||||
(if (> (count emails) 0)
|
||||
(on-invite-now form)
|
||||
(on-invite-later form))
|
||||
(modal/hide!))))
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
(on-invite-now params)
|
||||
(on-invite-later params)))))]
|
||||
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated.fadeIn {:class (stl/css :modal-container)}
|
||||
[:& team-modal-left]
|
||||
[:*
|
||||
[:div {:class (stl/css :modal-right-invitations)}
|
||||
[:h2 {:class (stl/css :modal-subtitle)} (tr "onboarding.choice.team-up.invite-members")]
|
||||
[:p {:class (stl/css :modal-text)} (tr "onboarding.choice.team-up.invite-members-info")]
|
||||
[:& fm/form {:form form
|
||||
:class (stl/css :modal-form-invitations)
|
||||
:on-submit on-submit}
|
||||
[:div {:class (stl/css :role-select)}
|
||||
[:p {:class (stl/css :role-title)} (tr "onboarding.choice.team-up.roles")]
|
||||
[:& fm/select {:name :role :options roles}]]
|
||||
|
||||
[:div {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :modal-right-invitations)}
|
||||
[:h2 {:class (stl/css :modal-subtitle)} (tr "onboarding.choice.team-up.invite-members")]
|
||||
[:p {:class (stl/css :modal-text)} (tr "onboarding.choice.team-up.invite-members-info")]
|
||||
[:div {:class (stl/css :invitation-row)}
|
||||
[:& fm/multi-input {:type "email"
|
||||
:name :emails
|
||||
:auto-focus? true
|
||||
:trim true
|
||||
:valid-item-fn us/parse-email
|
||||
:caution-item-fn #{}
|
||||
:label (tr "modals.invite-member.emails")
|
||||
:on-submit on-submit}]]
|
||||
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:button {:class (stl/css :back-button)
|
||||
:on-click on-back}
|
||||
(tr "labels.back")]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:class (stl/css :accept-button)
|
||||
:label (if (> (count emails) 0)
|
||||
(tr "onboarding.choice.team-up.create-team-and-invite")
|
||||
(tr "onboarding.choice.team-up.create-team-without-invite"))}]]
|
||||
[:div {:class (stl/css :modal-hint)}
|
||||
"(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")"]]]
|
||||
|
||||
|
||||
[:div {:class (stl/css :paginator)} "2/2"]]))
|
||||
|
||||
(mf/defc team-form-step-1
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [on-submit]}]
|
||||
(let [validators (mf/with-memo []
|
||||
[(fm/validate-not-empty :name (tr "auth.name.not-all-space"))
|
||||
(fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))])
|
||||
|
||||
form (fm/use-form
|
||||
:spec ::team-form
|
||||
:initial {}
|
||||
:validators validators)
|
||||
|
||||
on-submit*
|
||||
(mf/use-fn
|
||||
(fn [form]
|
||||
(let [name (dm/get-in @form [:clean-data :name])]
|
||||
|
||||
(st/emit! (ptk/data-event ::ev/event
|
||||
{::ev/name "onboarding-step"
|
||||
:label "team:choice-team-name"
|
||||
:step 7}))
|
||||
(on-submit name))))
|
||||
|
||||
on-skip
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (du/update-profile-props {:onboarding-viewed true})
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "onboarding-step"
|
||||
:label "team:skip-team-creation"
|
||||
:step 7})
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "onboarding-finish"}))))]
|
||||
[:*
|
||||
[:div {:class (stl/css :modal-right)}
|
||||
[:div {:class (stl/css :first-block)}
|
||||
[:h2 {:class (stl/css :modal-subtitle)}
|
||||
(tr "onboarding.team-modal.create-team")]
|
||||
[:p {:class (stl/css :modal-text)}
|
||||
(tr "onboarding.choice.team-up.create-team-desc")]
|
||||
[:& fm/form {:form form
|
||||
:class (stl/css :modal-form-invitations)
|
||||
:on-submit on-submit}
|
||||
[:div {:class (stl/css :role-select)}
|
||||
[:p {:class (stl/css :role-title)} (tr "onboarding.choice.team-up.roles")]
|
||||
[:& fm/select {:name :role :options roles}]]
|
||||
:class (stl/css :modal-form)
|
||||
:on-submit on-submit*}
|
||||
|
||||
[:div {:class (stl/css :invitation-row)}
|
||||
[:& fm/multi-input {:type "email"
|
||||
:name :emails
|
||||
:auto-focus? true
|
||||
:trim true
|
||||
:valid-item-fn us/parse-email
|
||||
:caution-item-fn #{}
|
||||
:label (tr "modals.invite-member.emails")
|
||||
:on-submit on-submit}]]
|
||||
[:& fm/input {:type "text"
|
||||
:class (stl/css :team-name-input)
|
||||
:name :name
|
||||
:placeholder "Team name"
|
||||
:label (tr "onboarding.choice.team-up.create-team-placeholder")}]
|
||||
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:button {:class (stl/css :back-button)
|
||||
:on-click #(st/emit! (modal/show {:type :onboarding-team})
|
||||
(ptk/event ::ev/event {::ev/name "invite-members-back"
|
||||
::ev/origin "onboarding"
|
||||
:name name
|
||||
:step 2}))}
|
||||
(tr "labels.back")]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:class (stl/css :accept-button)
|
||||
:label (if (> (count emails) 0)
|
||||
(tr "onboarding.choice.team-up.create-team-and-invite")
|
||||
(tr "onboarding.choice.team-up.create-team-without-invite"))}]]
|
||||
[:div {:class (stl/css :modal-hint)}
|
||||
(dmc/str "(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")")]]]
|
||||
:label (tr "onboarding.choice.team-up.continue-creating-team")}]]]]
|
||||
[:div {:class (stl/css :second-block)}
|
||||
[:h2 {:class (stl/css :modal-subtitle)}
|
||||
(tr "onboarding.choice.team-up.start-without-a-team")]
|
||||
[:p {:class (stl/css :modal-text)}
|
||||
(tr "onboarding.choice.team-up.start-without-a-team-description")]
|
||||
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:button {:class (stl/css :accept-button)
|
||||
:on-click on-skip}
|
||||
(tr "onboarding.choice.team-up.continue-without-a-team")]]]]
|
||||
|
||||
[:div {:class (stl/css :paginator)} "1/2"]]))
|
||||
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::team-form
|
||||
(s/keys :req-un [::name]))
|
||||
|
||||
(mf/defc onboarding-team-modal
|
||||
{::mf/props :obj}
|
||||
[]
|
||||
(let [name* (mf/use-state nil)
|
||||
name (deref name*)
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(fn [tname]
|
||||
(swap! name* (constantly tname))))
|
||||
|
||||
|
||||
[:div {:class (stl/css :paginator)} "2/2"]]]))
|
||||
on-back
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(swap! name* (constantly nil))))
|
||||
|
||||
onboarding-a-b-test?
|
||||
(cf/external-feature-flag "signup-background" "test")]
|
||||
|
||||
[:div {:class (stl/css-case
|
||||
:modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
|
||||
[:div.animated.fadeIn {:class (stl/css :modal-container)}
|
||||
[:& left-sidebar]
|
||||
[:div {:class (stl/css :separator)}]
|
||||
(if name
|
||||
[:& team-form-step-2 {:name name :on-back on-back}]
|
||||
[:& team-form-step-1 {:on-submit on-submit}])]]))
|
||||
|
||||
|
|
|
@ -160,6 +160,7 @@
|
|||
|
||||
.custom-input-token {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
&:focus {
|
||||
|
|
|
@ -134,6 +134,8 @@
|
|||
page-id (:id page)
|
||||
file-id (:id file)
|
||||
frame-id (:id frame)
|
||||
vsize (-> (mf/deref refs/viewer-local)
|
||||
:viewport-size)
|
||||
|
||||
tpos-ref (mf/with-memo [page-id]
|
||||
(-> (l/in [:pages page-id :options :comment-threads-position])
|
||||
|
@ -216,6 +218,7 @@
|
|||
[:& cmt/thread-comments
|
||||
{:thread thread
|
||||
:position-modifier modifier1
|
||||
:viewport {:offset-x 0 :offset-y 0 :width (:width vsize) :height (:height vsize)}
|
||||
:users users
|
||||
:zoom zoom}])
|
||||
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
|
||||
.suffix-input {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
|
||||
.input-text {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
color: var(--input-foreground-color-active);
|
||||
padding-left: $s-8;
|
||||
margin: 0;
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
@include flexRow;
|
||||
.input-wrapper {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-84;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
@ -26,6 +27,7 @@
|
|||
@include flexRow;
|
||||
.input-wrapper {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-84;
|
||||
&.hex {
|
||||
width: $s-172;
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
|
||||
.input-wrapper {
|
||||
@extend .input-with-label;
|
||||
@include bodySmallTypography;
|
||||
label {
|
||||
text-transform: none;
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
}
|
||||
.input-wrapper {
|
||||
@extend .input-with-label;
|
||||
@include bodySmallTypography;
|
||||
margin-bottom: $s-8;
|
||||
}
|
||||
.action-buttons {
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
:ref dref
|
||||
:on-click on-select-shape
|
||||
:on-context-menu on-context-menu
|
||||
:data-testid "layer-row"
|
||||
:class (stl/css-case
|
||||
:layer-row true
|
||||
:highlight highlighted?
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
|
||||
.second-row {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-92;
|
||||
.label {
|
||||
padding-left: $s-8;
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
.suffix-input {
|
||||
grid-column: span 3;
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
|
|
|
@ -77,6 +77,7 @@
|
|||
border-radius: 0 $br-8 $br-8 0;
|
||||
.numeric-input {
|
||||
@extend .input-base;
|
||||
@include bodySmallTypography;
|
||||
}
|
||||
}
|
||||
.editable-select-wrapper {
|
||||
|
@ -93,6 +94,7 @@
|
|||
border: $s-1 solid var(--input-border-color);
|
||||
.numeric-input {
|
||||
@extend .input-base;
|
||||
@include bodySmallTypography;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -196,6 +198,7 @@
|
|||
}
|
||||
.height {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
.icon-text {
|
||||
padding-top: $s-1;
|
||||
|
@ -204,6 +207,7 @@
|
|||
.gutter,
|
||||
.margin {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
.icon {
|
||||
&.rotated svg {
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
|
||||
.area-input {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: 100%;
|
||||
padding: $s-8;
|
||||
}
|
||||
|
@ -51,6 +52,7 @@
|
|||
|
||||
.coord-input {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
border-radius: 0 $br-8 $br-8 0;
|
||||
border-left: $s-1 solid var(--panel-background-color);
|
||||
}
|
||||
|
|
|
@ -143,6 +143,7 @@
|
|||
}
|
||||
.input-element-wrapper {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
grid-area: content;
|
||||
}
|
||||
.buttons-wrapper {
|
||||
|
@ -319,6 +320,7 @@
|
|||
|
||||
.flow-input {
|
||||
@extend .input-base;
|
||||
@include bodySmallTypography;
|
||||
background-color: transparent;
|
||||
height: $s-28;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
}
|
||||
.input {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-60;
|
||||
}
|
||||
.actions {
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
gap: $s-4;
|
||||
.column-gap {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
&.disabled {
|
||||
@extend .disabled-input;
|
||||
|
@ -91,6 +92,7 @@
|
|||
}
|
||||
.row-gap {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
&.disabled {
|
||||
@extend .disabled-input;
|
||||
|
@ -113,6 +115,7 @@
|
|||
|
||||
.padding-simple {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
max-width: $s-108;
|
||||
}
|
||||
}
|
||||
|
@ -124,6 +127,7 @@
|
|||
|
||||
.padding-multiple {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
max-width: $s-108;
|
||||
}
|
||||
}
|
||||
|
@ -239,6 +243,7 @@
|
|||
|
||||
.track-info-value {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
border-radius: 0;
|
||||
border-right: $s-1 solid var(--panel-background-color);
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
|
||||
.z-index-wrapper {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-60;
|
||||
}
|
||||
|
||||
|
@ -94,6 +95,7 @@
|
|||
.vertical-margin,
|
||||
.horizontal-margin {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
}
|
||||
}
|
||||
|
@ -109,6 +111,7 @@
|
|||
.left-margin,
|
||||
.right-margin {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
}
|
||||
|
||||
|
@ -127,6 +130,7 @@
|
|||
.layout-item-max-w,
|
||||
.layout-item-max-h {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
.icon-text {
|
||||
justify-content: flex-start;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.shape.radius :as ctsr]
|
||||
[app.main.constants :refer [size-presets]]
|
||||
|
@ -325,16 +326,15 @@
|
|||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/checked?)
|
||||
undo-id (js/Symbol)]
|
||||
(do
|
||||
(st/emit! (dwu/start-undo-transaction undo-id)
|
||||
(dwsh/update-shapes ids (fn [shape] (assoc shape :hide-in-viewer (not value)))))
|
||||
(st/emit! (dwu/start-undo-transaction undo-id)
|
||||
(dwsh/update-shapes ids (fn [shape] (cls/change-show-in-viewer shape (not value)))))
|
||||
|
||||
(when-not value
|
||||
(when-not value
|
||||
;; when a frame is no longer shown in view mode, cannot have
|
||||
;; interactions that navigate to it.
|
||||
(apply st/emit! (map #(dwi/remove-all-interactions-nav-to %) ids)))
|
||||
(apply st/emit! (map #(dwi/remove-all-interactions-nav-to %) ids)))
|
||||
|
||||
(st/emit! (dwu/commit-undo-transaction undo-id))))))]
|
||||
(st/emit! (dwu/commit-undo-transaction undo-id)))))]
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps radius-mode @radius-multi?)
|
||||
|
|
|
@ -114,6 +114,7 @@
|
|||
.height,
|
||||
.width {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
.icon-text {
|
||||
padding-top: $s-1;
|
||||
|
@ -145,6 +146,7 @@
|
|||
.x-position,
|
||||
.y-position {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
.icon-text {
|
||||
padding-top: $s-1;
|
||||
|
@ -163,6 +165,7 @@
|
|||
|
||||
.rotation {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
.icon-text {
|
||||
padding-top: $s-1;
|
||||
|
@ -181,6 +184,7 @@
|
|||
|
||||
.radius-1 {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-108;
|
||||
}
|
||||
|
||||
|
@ -190,6 +194,7 @@
|
|||
gap: $s-4;
|
||||
.small-input {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-52;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,6 +114,7 @@
|
|||
.spread-input,
|
||||
.offset-y-input {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-60;
|
||||
min-width: $s-60;
|
||||
align-items: baseline;
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
|
||||
.attr-input {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-124;
|
||||
}
|
||||
|
||||
|
|
|
@ -308,6 +308,7 @@
|
|||
.line-height,
|
||||
.letter-spacing {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
.icon {
|
||||
@include flexCenter;
|
||||
width: $s-28;
|
||||
|
@ -339,6 +340,7 @@
|
|||
padding: $s-8;
|
||||
.numeric-input {
|
||||
@extend .input-base;
|
||||
@include bodySmallTypography;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
|
||||
.color-name-wrapper {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
@ -166,6 +167,7 @@
|
|||
|
||||
.opacity-element-wrapper {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-60;
|
||||
border-radius: 0 $br-8 $br-8 0;
|
||||
.opacity-input {
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
@include flexRow;
|
||||
.stroke-width-input-element {
|
||||
@extend .input-element;
|
||||
@include bodySmallTypography;
|
||||
width: $s-60;
|
||||
}
|
||||
.select-wrapper {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.util.forms
|
||||
(:refer-clojure :exclude [uuid])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.spec :as us]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cljs.spec.alpha :as s]
|
||||
|
@ -69,11 +70,15 @@
|
|||
(::s/problems (s/explain-data spec (:data state))))
|
||||
|
||||
errors (reduce interpret-problem {} problems)
|
||||
|
||||
|
||||
errors (reduce (fn [errors vf]
|
||||
(merge errors (vf errors (:data state))))
|
||||
errors
|
||||
validators)
|
||||
errors (merge errors (:errors state))]
|
||||
errors (merge (:errors state) errors)
|
||||
errors (d/without-nils errors)]
|
||||
|
||||
|
||||
(assoc state
|
||||
:errors errors
|
||||
|
|
|
@ -278,7 +278,6 @@
|
|||
old-id (parser/get-id node)
|
||||
interactions (->> (parser/parse-interactions node)
|
||||
(mapv #(update % :destination resolve)))
|
||||
|
||||
data (-> (parser/parse-data type node)
|
||||
(resolve-data-ids type context)
|
||||
(cond-> (some? old-id)
|
||||
|
|
|
@ -244,19 +244,22 @@
|
|||
(first))
|
||||
|
||||
;; The nodes with the "frame-background" class can have some anidation depending on the strokes they have
|
||||
g-nodes (find-all-nodes node :g)
|
||||
defs-nodes (flatten (map #(find-all-nodes % :defs) g-nodes))
|
||||
gg-nodes (flatten (map #(find-all-nodes % :g) g-nodes))
|
||||
g-nodes (find-all-nodes node :g)
|
||||
defs-nodes (flatten (map #(find-all-nodes % :defs) g-nodes))
|
||||
gg-nodes (flatten (map #(find-all-nodes % :g) g-nodes))
|
||||
|
||||
;; The first g node contains the opacity for frames
|
||||
main-g-node (first g-nodes)
|
||||
|
||||
rect-nodes (flatten [[(find-all-nodes node :rect)]
|
||||
(map #(find-all-nodes % #{:rect :path}) defs-nodes)
|
||||
(map #(find-all-nodes % #{:rect :path}) g-nodes)
|
||||
(map #(find-all-nodes % #{:rect :path}) gg-nodes)])
|
||||
svg-node (d/seek #(= "frame-background" (get-in % [:attrs :class])) rect-nodes)]
|
||||
rect-nodes (flatten [[(find-all-nodes node :rect)]
|
||||
(map #(find-all-nodes % #{:rect :path}) defs-nodes)
|
||||
(map #(find-all-nodes % #{:rect :path}) g-nodes)
|
||||
(map #(find-all-nodes % #{:rect :path}) gg-nodes)])
|
||||
svg-node (d/seek #(= "frame-background" (get-in % [:attrs :class])) rect-nodes)]
|
||||
(merge
|
||||
(add-attrs {} (:attrs frame-clip-rect-node))
|
||||
(add-attrs {} (:attrs svg-node))
|
||||
(add-attrs {} (:attrs main-g-node))
|
||||
node-attrs))
|
||||
|
||||
(= type :svg-raw)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue