diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index 89a1bcd42d..89eaa3e700 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -10,7 +10,6 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.main.ui.components.select :as cs] - [app.main.ui.hooks :as hooks] [app.main.ui.icons :as deprecated-icon] [app.util.dom :as dom] [app.util.forms :as fm] @@ -450,189 +449,3 @@ (on-submit form event))))] [:> (mf/provider form-ctx) {:value form} [:form {:class class :on-submit on-submit'} children]])) - -(defn- conj-dedup - "A helper that adds item into a vector and removes possible - duplicates. This is not very efficient implementation but is ok for - handling form input that will have a small number of items." - [coll item] - (into [] (distinct) (conj coll item))) - -(mf/defc multi-input - [{:keys [form label class trim valid-item-fn caution-item-fn on-submit] :as props}] - (let [form (or form (mf/use-ctx form-ctx)) - input-name (get props :name) - touched? (get-in @form [:touched input-name]) - error (get-in @form [:errors input-name]) - focus? (mf/use-state false) - - auto-focus? (get props :auto-focus? false) - - items (mf/use-state - (fn [] - (let [initial (get-in @form [:data input-name])] - (if (or (vector? initial) - (set? initial)) - (mapv (fn [val] - {:text val - :valid (valid-item-fn val) - :caution (caution-item-fn val)}) - initial) - [])))) - - value (mf/use-state "") - result (hooks/use-equal-memo @items) - - empty? (and (str/empty? @value) - (zero? (count @items))) - - klass (str (get props :class) " " - (stl/css-case - :focus @focus? - :valid (and touched? (not error)) - :invalid (and touched? error) - :empty empty? - :custom-multi-input true)) - - in-klass (str class " " - (stl/css-case - :inside-input true - :no-padding (pos? (count @items)) - :invalid (and (some? valid-item-fn) - touched? - (not (str/empty? @value)) - (not (valid-item-fn @value))))) - - on-focus - (mf/use-fn #(reset! focus? true)) - - on-change - (mf/use-fn - (fn [event] - (let [content (-> event dom/get-target dom/get-input-value)] - (reset! value content)))) - - update-form! - (mf/use-fn - (mf/deps form) - (fn [items] - (let [value (str/join " " (map :text items))] - (fm/update-input-value! form input-name value)))) - - on-key-down - (mf/use-fn - (mf/deps @value) - (fn [event] - (let [val (cond-> @value trim str/trim)] - (cond - (or (kbd/enter? event) (kbd/comma? event) (kbd/space? event)) - (do - (dom/prevent-default event) - (dom/stop-propagation event) - - ;; Once enter/comma is pressed we mark it as touched - (swap! form assoc-in [:touched input-name] true) - - ;; Empty values means "submit" the form (whent some items have been added - (when (and (kbd/enter? event) (str/empty? @value) (not-empty @items)) - (when (fn? on-submit) - (on-submit form event))) - - ;; If we have a string in the input we add it only if valid - (when (and (valid-item-fn val) (not (str/empty? @value))) - (reset! value "") - - ;; Once added the form is back as "untouched" - (swap! form assoc-in [:touched input-name] false) - - ;; This split will allow users to copy comma/space separated values of emails - (doseq [val (str/split val #",|\s+")] - (swap! items conj-dedup {:text (str/trim val) - :valid (valid-item-fn val) - :caution (caution-item-fn val)})))) - - (and (kbd/backspace? event) (str/empty? @value)) - (do - (dom/prevent-default event) - (dom/stop-propagation event) - (swap! items (fn [items] (if (c/empty? items) items (pop items))))))))) - - on-paste - (mf/use-fn - (fn [event] - (let [paste-data (-> event .-clipboardData (.getData "text"))] - (when (and (string? paste-data) - (re-find #"[,\s]" paste-data)) - (dom/prevent-default event) - (dom/stop-propagation event) - - ;; Mark as touched - (swap! form assoc-in [:touched input-name] true) - - ;; Split pasted text by commas and/or whitespace, add each valid part - (let [parts (->> (str/split paste-data #",|\s+") - (map str/trim) - (remove str/empty?))] - (doseq [part parts] - (when (valid-item-fn part) - (swap! items conj-dedup {:text part - :valid true - :caution (caution-item-fn part)}))) - - ;; Reset input value and mark as untouched after successful paste - (reset! value "") - (swap! form assoc-in [:touched input-name] false)))))) - - on-blur - (mf/use-fn - (fn [_] - (reset! focus? false) - (when-not (get-in @form [:touched input-name]) - (swap! form assoc-in [:touched input-name] true)))) - - remove-item! - (mf/use-fn - (fn [item] - (swap! items #(into [] (remove (fn [x] (= x item))) %)))) - - manage-key-down - (mf/use-fn - (fn [item event] - (when (kbd/enter? event) - (remove-item! item))))] - - (mf/with-effect [result @value] - (let [val (cond-> @value trim str/trim) - values (conj-dedup result {:text val :valid (valid-item-fn val)}) - values (filterv #(:valid %) values)] - - (update-form! values))) - - [:div {:class klass} - [:input {:id (name input-name) - :name (name input-name) - :class in-klass - :type "text" - :auto-focus auto-focus? - :on-focus on-focus - :on-blur on-blur - :on-key-down on-key-down - :on-paste on-paste - :value @value - :on-change on-change - :placeholder (when empty? label)}] - [:label {:for (name input-name)} label] - - (when-let [items (seq @items)] - [:div {:class (stl/css :selected-items)} - (for [item items] - [:div {:class (stl/css :selected-item) - :key (:text item) - :tab-index "0" - :on-key-down (partial manage-key-down item)} - [:span {:class (stl/css-case :around true - :invalid (not (:valid item)) - :caution (:caution item))} - [:span {:class (stl/css :text)} (:text item)] - [:button {:class (stl/css :icon) - :on-click #(remove-item! item)} deprecated-icon/close]]])])])) diff --git a/frontend/src/app/main/ui/components/forms.scss b/frontend/src/app/main/ui/components/forms.scss index dc3f521ab1..30901f43ec 100644 --- a/frontend/src/app/main/ui/components/forms.scss +++ b/frontend/src/app/main/ui/components/forms.scss @@ -326,112 +326,6 @@ } } -// MULTI INPUT -.custom-multi-input { - display: flex; - flex-direction: column; - position: relative; - min-block-size: $sz-40; - max-block-size: px2rem(180); - inline-size: 100%; - overflow-y: hidden; - - .inside-input { - @include deprecated.remove-input-style; - @include t.use-typography("body-small"); - @include text-ellipsis; - - inline-size: 100%; - max-inline-size: calc(100% - deprecated.$s-1); - min-block-size: $sz-32; - padding-block-start: 0; - block-size: $sz-32; - padding: var(--sp-s); - margin: 0; - border-radius: var(--sp-s); - color: var(--input-foreground-color-active); - background-color: var(--input-background-color); - - &:focus { - outline: none; - border: $b-1 solid var(--input-border-color-focus); - } - - &.invalid { - border: $b-1 solid var(--input-border-color-error); - - &:hover, - &:focus { - border: $b-1 solid var(--input-border-color-error); - } - } - } - - label { - display: none; - } - - .selected-items { - display: flex; - flex-wrap: wrap; - gap: var(--sp-xs); - max-block-size: px2rem(136); - padding: var(--sp-xs) 0; - overflow-y: auto; - - .selected-item { - .around { - display: flex; - align-items: center; - gap: var(--sp-xs); - block-size: $sz-24; - inline-size: fit-content; - padding-inline-start: px2rem(6); - border-radius: $br-6; - background-color: var(--pill-background-color); - border: $b-1 solid var(--pill-background-color); - box-sizing: border-box; - - .text { - @include t.use-typography("body-small"); - - padding-inline-end: var(--sp-s); - color: var(--pill-foreground-color); - } - - .icon { - display: flex; - justify-content: center; - align-items: center; - border: none; - background: none; - cursor: pointer; - block-size: $sz-32; - inline-size: $sz-24; - - svg { - @extend %button-icon-small; - - stroke: var(--icon-foreground); - } - } - - &.invalid { - background-color: var(--status-widget-background-color-error); - - .text { - color: var(--alert-text-foreground-color-error); - } - - .icon svg { - stroke: var(--alert-text-foreground-color-error); - } - } - } - } - } -} - // RADIO BUTTONS .custom-radio { display: grid; diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index cdf7d0d648..ee9ca03325 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -23,6 +23,7 @@ [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.pin-button :refer [pin-button*]] [app.main.ui.dashboard.project-menu :refer [project-menu*]] + [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as deprecated-icon] @@ -88,11 +89,9 @@ [:div {:class (stl/css :info)} [:span (tr "dasboard.team-hero.text")] [:a {:on-click on-nav-members-click} (tr "dasboard.team-hero.management")]] - [:button - {:class (stl/css :btn-primary :invite) - :on-click on-invite} + [:> button* {:variant "primary" + :on-click on-invite} (tr "onboarding.choice.team-up.invite-members")]] - [:button {:class (stl/css :close) :on-click on-close' :aria-label (tr "labels.close")} diff --git a/frontend/src/app/main/ui/dashboard/projects.scss b/frontend/src/app/main/ui/dashboard/projects.scss index 4f47567d87..774d8ee3fa 100644 --- a/frontend/src/app/main/ui/dashboard/projects.scss +++ b/frontend/src/app/main/ui/dashboard/projects.scss @@ -244,11 +244,6 @@ stroke: var(--close-icon-foreground-color); } -.invite { - block-size: $sz-32; - inline-size: px2rem(180); -} - .img-wrapper { display: flex; align-items: center; diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index c7d6aed7a0..0746a49a6b 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -34,6 +34,11 @@ [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.controls.combobox :refer [combobox*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] + [app.main.ui.ds.foundations.typography :as t] + [app.main.ui.ds.foundations.typography.heading :refer [heading*]] + [app.main.ui.ds.foundations.typography.text :refer [text*]] + [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] + [app.main.ui.forms :as fc] [app.main.ui.icons :as deprecated-icon] [app.main.ui.notifications.badge :refer [badge-notification]] [app.main.ui.notifications.context-notification :refer [context-notification]] @@ -126,15 +131,12 @@ [:li {:class (when settings-section? (stl/css :active))} [:a {:on-click on-nav-settings} (tr "labels.settings")]]]] [:div {:class (stl/css :dashboard-buttons)} - (if (and (or invitations-section? members-section?) (not-empty invitations)) - [:button - {:class (stl/css :btn-secondary :btn-small) - :type "button" - :disabled (not can-invite?) - :on-click on-invite-member - :data-testid "invite-member"} - (tr "dashboard.invite-profile")] - [:div {:class (stl/css :blank-space)}])]])) + (when (and (or invitations-section? members-section?) (not-empty invitations)) + [:> button* {:variant "secondary" + :on-click on-invite-member + :disabled (not can-invite?) + :data-testid "invite-member"} + (tr "dashboard.invite-profile")])]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; INVITATIONS MODAL @@ -142,10 +144,10 @@ (defn get-available-roles [permissions] - (->> [{:value "viewer" :label (tr "labels.viewer")} - {:value "editor" :label (tr "labels.editor")} + (->> [{:id "viewer" :value "viewer" :label (tr "labels.viewer")} + {:id "editor" :value "editor" :label (tr "labels.editor")} (when (:is-admin permissions) - {:value "admin" :label (tr "labels.admin")})] + {:id "admin" :value "admin" :label (tr "labels.admin")})] (filterv identity))) (def ^:private schema:invite-member-form @@ -236,46 +238,55 @@ :on-error (partial on-error form)}] (st/emit! (dtm/check-and-submit-invite-members (with-meta params mdata) origin do-invite-members!))))] + (mf/with-effect [team-id] + (st/emit! (dtm/fetch-members team-id))) + [:div {:class (stl/css-case :modal-team-container true :modal-team-container-workspace (= origin :workspace) :hero (= origin :hero))} - [:& fm/form {:on-submit on-submit :form form} - [:div {:class (stl/css :modal-title)} + [:> fc/form* {:form form + :class (stl/css :form-wrapper) + :on-submit on-submit} + [:> heading* {:level 2 + :typography t/headline-medium + :class (stl/css :color-light)} (tr "modals.invite-team-member.title")] (when (= :workspace origin) - [:div {:class (stl/css :invite-team-member-text)} + [:> text* {:as "p" + :typography t/body-large + :class (stl/css :color-light)} (tr "modals.invite-team-member.text")]) (when-not (= "" @error-text) - [:& context-notification {:content @error-text - :level :error}]) + [:> context-notification* {:level :error} + @error-text]) (when (some current-data-emails current-members-emails) - [:& context-notification {:content (tr "modals.invite-member.repeated-invitation") - :level :warning}]) + [:> context-notification* {:level :warning} + (tr "modals.invite-member.repeated-invitation")]) - [:div {:class (stl/css :role-select)} - [:p {:class (stl/css :role-title)} + [:div {:class (stl/css :form-group)} + [:> text* {:as "label" + :typography t/body-medium + :class (stl/css :color-light)} (tr "onboarding.choice.team-up.roles")] - [:& fm/select {:name :role :options roles}]] + [:> fc/form-select* {:name :role + :default-selected "editor" + :options roles}] + [:> fc/form-multi-input* {:type "email" + :name :emails + :auto-focus? true + :trim true + :valid-item-fn sm/parse-email + :caution-item-fn current-members-emails + :placeholder (tr "modals.invite-member.emails")}]] - [:div {:class (stl/css :invitation-row)} - [:& fm/multi-input {:type "email" - :class (stl/css :email-input) - :name :emails - :auto-focus? true - :trim true - :valid-item-fn sm/parse-email - :caution-item-fn current-members-emails - :label (tr "modals.invite-member.emails")}]] - - [:div {:class (stl/css :action-buttons)} - [:> fm/submit-button* - {:label (tr "modals.invite-member-confirm.accept") - :class (stl/css :accept-btn) - :disabled (and (boolean (some current-data-emails current-members-emails)) - (empty? (remove current-members-emails current-data-emails)))}]]]])) + [:div {:class (stl/css :form-buttons)} + [:> fc/form-submit* + {:disabled (and (boolean (some current-data-emails current-members-emails)) + (empty? (remove current-members-emails current-data-emails)))} + (tr "modals.invite-member-confirm.accept")]]]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; INVITE RESTRICTED MEMBERS MODAL @@ -821,15 +832,13 @@ [:div {:class (stl/css :empty-invitations)} [:div (tr "labels.no-invitations")] (if ^boolean can-invite - [[:div (tr "labels.no-invitations-gather-people")] - [:div {:class (stl/css :empty-invitations-buttons)} - [:a - {:class (stl/css :btn-empty-invitations) - :role "button" - :on-click on-invite-member - :data-testid "invite-member"} - (tr "dashboard.invite-profile")]] - [:div {:class (stl/css :blank-space)}]] + [:* + [:div (tr "labels.no-invitations-gather-people")] + [:> button* {:variant "primary" + :class (stl/css :btn-empty-invitations) + :on-click on-invite-member + :data-testid "invite-member"} + (tr "dashboard.invite-profile")]] [:div {:class (stl/css :no-permission-text)} (tr "dashboard.invitations.no-permission")])])) (mf/defc invitation-modal diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss index 7509b52371..a1bf36a814 100644 --- a/frontend/src/app/main/ui/dashboard/team.scss +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -17,7 +17,7 @@ display: flex; flex-direction: column; align-items: center; - width: 100%; + inline-size: 100%; border-top: $b-1 solid var(--color-background-quaternary); overflow-y: auto; padding-inline-start: var(--sp-xl); @@ -35,8 +35,8 @@ display: grid; grid-auto-rows: min-content; gap: var(--sp-s); - max-width: $sz-1000; - width: 100%; + max-inline-size: $sz-1000; + inline-size: 100%; } .info-block { @@ -62,8 +62,8 @@ } .owner-icon { - width: $sz-32; - height: $sz-32; + inline-size: $sz-32; + block-size: $sz-32; border-radius: 50%; } @@ -73,8 +73,8 @@ display: flex; justify-content: center; align-items: center; - height: $sz-16; - width: $sz-16; + block-size: $sz-16; + inline-size: $sz-16; color: transparent; fill: none; stroke-width: $b-1; @@ -86,8 +86,8 @@ --update-button-opacity: 0; position: relative; - height: $sz-120; - width: $sz-120; + block-size: $sz-120; + inline-size: $sz-120; padding: var(--sp-l); margin-block-end: var(--sp-xxxl); @@ -101,8 +101,8 @@ top: 0; left: 0; border-radius: 50%; - width: $sz-120; - height: $sz-120; + inline-size: $sz-120; + block-size: $sz-120; } .update-overlay { @@ -116,8 +116,8 @@ position: absolute; top: 0; left: 0; - height: 100%; - width: 100%; + block-size: 100%; + inline-size: 100%; z-index: var(--z-index-set); border-radius: $br-circle; background-color: var(--color-accent-primary); @@ -127,13 +127,13 @@ display: flex; justify-content: center; align-items: center; - height: $sz-16; - width: $sz-16; + block-size: $sz-16; + inline-size: $sz-16; color: transparent; fill: none; stroke-width: $b-1; - min-width: $sz-24; - min-height: $sz-24; + min-inline-size: $sz-24; + min-block-size: $sz-24; stroke: var(--color-foreground-primary); } @@ -141,8 +141,8 @@ .dashboard-team-members { display: flex; justify-content: center; - width: 100%; - height: 100%; + inline-size: 100%; + block-size: 100%; padding-inline-start: var(--sp-xl); padding-block-start: var(--sp-xl); border-top: $b-1 solid var(--color-background-quaternary); @@ -158,9 +158,9 @@ .team-members { display: grid; grid-template-rows: auto 1fr; - height: fit-content; - max-width: $sz-1000; - width: 100%; + block-size: fit-content; + max-inline-size: $sz-1000; + inline-size: 100%; } .table-header { @@ -169,9 +169,9 @@ display: grid; align-items: center; grid-template-columns: 43% 1fr px2rem(108) var(--sp-m); - height: $sz-40; - width: 100%; - max-width: $sz-1000; + block-size: $sz-40; + inline-size: 100%; + max-inline-size: $sz-1000; padding: 0 var(--sp-l); user-select: none; color: var(--color-foreground-secondary); @@ -181,9 +181,9 @@ display: grid; grid-auto-rows: px2rem(64); gap: var(--sp-l); - width: 100%; - height: 100%; - max-width: $sz-1000; + inline-size: 100%; + block-size: 100%; + max-inline-size: $sz-1000; margin-top: var(--sp-l); color: var(--color-foreground-secondary); } @@ -192,8 +192,8 @@ display: grid; grid-template-columns: 43% 1fr auto; align-items: center; - height: px2rem(64); - width: 100%; + block-size: px2rem(64); + inline-size: 100%; padding: 0 var(--sp-l); border-radius: $br-8; background-color: var(--color-background-tertiary); @@ -201,8 +201,8 @@ } .title-field-name { - width: 43%; - min-width: px2rem(300); + inline-size: 43%; + min-inline-size: px2rem(300); } .title-field-role { @@ -221,8 +221,8 @@ display: grid; grid-template-columns: auto 1fr; gap: var(--sp-l); - width: 43%; - min-width: px2rem(300); + inline-size: 43%; + min-inline-size: px2rem(300); } .field-roles { @@ -236,15 +236,15 @@ // MEMBER INFO .member-image { - height: $sz-32; - width: $sz-32; + block-size: $sz-32; + inline-size: $sz-32; border-radius: $br-circle; } .member-info { display: grid; grid-template-rows: 1fr 1fr; - width: 100%; + inline-size: 100%; } .member-name, @@ -272,9 +272,9 @@ display: grid; grid-template-columns: 1fr auto; align-items: center; - height: $sz-32; - min-width: $sz-160; - width: fit-content; + block-size: $sz-32; + min-inline-size: $sz-160; + inline-size: fit-content; padding: var(--sp-xs) var(--sp-s); border-radius: $br-8; border-color: var(--color-background-quaternary); @@ -303,8 +303,8 @@ border: $b-2 solid var(--color-background-quaternary); margin: 0; bottom: calc(-1 * px2rem(76)); - width: fit-content; - min-width: $sz-160; + inline-size: fit-content; + min-inline-size: $sz-160; } .rol-dropdown-item { @@ -313,8 +313,8 @@ display: flex; align-items: center; justify-content: space-between; - height: $sz-28; - width: 100%; + block-size: $sz-28; + inline-size: 100%; padding: px2rem(6); border-radius: $br-8; cursor: pointer; @@ -329,8 +329,8 @@ display: flex; justify-content: center; align-items: center; - height: $sz-16; - width: $sz-16; + block-size: $sz-16; + inline-size: $sz-16; color: transparent; fill: none; stroke-width: $b-1; @@ -366,8 +366,8 @@ bottom: calc(-1 * var(--sp-xxxl)); right: 0; left: unset; - width: fit-content; - min-width: $sz-160; + inline-size: fit-content; + min-inline-size: $sz-160; } .action-dropdown-item { @@ -376,8 +376,8 @@ display: flex; align-items: center; justify-content: space-between; - height: $sz-28; - width: 100%; + block-size: $sz-28; + inline-size: 100%; padding: px2rem(6); border-radius: $br-8; cursor: pointer; @@ -391,8 +391,8 @@ .dashboard-team-invitations { display: flex; justify-content: center; - width: 100%; - height: 100%; + inline-size: 100%; + block-size: 100%; padding-inline-start: var(--sp-xl); padding-block-start: var(--sp-xl); border-top: $b-1 solid var(--color-background-quaternary); @@ -403,9 +403,9 @@ .invitations { display: grid; grid-template-rows: auto 1fr; - height: fit-content; - max-width: $sz-1000; - width: 100%; + block-size: fit-content; + max-inline-size: $sz-1000; + inline-size: 100%; } .invitations-actions { @@ -416,7 +416,7 @@ align-items: center; gap: var(--sp-l); color: var(--title-foreground-color); - height: $sz-40; + block-size: $sz-40; margin-block-end: px2rem(36); } @@ -430,7 +430,7 @@ } .empty-invitations { - width: 100%; + inline-size: 100%; margin-top: var(--sp-l); border: $b-1 solid var(--color-background-quaternary); border-radius: $br-8; @@ -440,21 +440,17 @@ } .empty-invitations-buttons { - width: fit-content; + inline-size: fit-content; margin: auto; } .no-permission-text { - max-width: $sz-512; + max-inline-size: $sz-512; margin: auto; } .btn-empty-invitations { - // TODO: Remove this extend add DS component - @extend %button-primary; - margin-block-start: var(--sp-l); - padding-inline: var(--sp-m); } .title-field-status { @@ -490,8 +486,8 @@ grid-template-rows: auto 1fr; justify-items: center; gap: var(--sp-xxl); - width: 100%; - height: 100%; + inline-size: 100%; + block-size: 100%; padding-inline-start: var(--sp-xl); padding-block-start: var(--sp-xl); border-top: $b-1 solid var(--color-background-quaternary); @@ -509,9 +505,9 @@ display: grid; place-items: center; align-content: center; - height: px2rem(156); - max-width: $sz-1000; - width: 100%; + block-size: px2rem(156); + max-inline-size: $sz-1000; + inline-size: 100%; padding: var(--sp-xxxl); border: $b-1 solid var(--color-background-quaternary); border-radius: $br-8; @@ -527,7 +523,7 @@ margin: 0; padding: var(--sp-xxxl); padding: 0; - width: px2rem(468); + inline-size: px2rem(468); } .hero-title { @@ -541,19 +537,19 @@ color: var(--color-foreground-secondary); margin-bottom: 0; - max-width: $sz-512; + max-inline-size: $sz-512; } .hero-btn { // TODO: Remove this extended class using a DS component @extend %button-primary; - height: $sz-32; - max-width: $sz-512; + block-size: $sz-32; + max-inline-size: $sz-512; } .webhook-table { - height: fit-content; + block-size: fit-content; } .webhook-row { @@ -569,7 +565,7 @@ .menu-disabled { color: var(--color-foreground-secondary); - width: $sz-28; + inline-size: $sz-28; display: flex; justify-content: center; align-items: center; @@ -590,8 +586,8 @@ margin: 0; right: calc(-1 * var(--sp-l)); bottom: calc(-1 * $sz-40); - width: fit-content; - min-width: $sz-160; + inline-size: fit-content; + min-inline-size: $sz-160; } .webhook-dropdown-item { @@ -600,8 +596,8 @@ display: flex; align-items: center; justify-content: space-between; - height: $sz-28; - width: 100%; + block-size: $sz-28; + inline-size: 100%; padding: px2rem(6); border-radius: $br-8; cursor: pointer; @@ -615,8 +611,8 @@ display: flex; justify-content: center; align-items: center; - height: $sz-16; - width: $sz-16; + block-size: $sz-16; + inline-size: $sz-16; color: transparent; fill: none; stroke-width: $b-1; @@ -627,8 +623,8 @@ display: flex; justify-content: center; align-items: center; - height: $sz-16; - width: $sz-16; + block-size: $sz-16; + inline-size: $sz-16; color: transparent; fill: none; stroke-width: $b-1; @@ -639,16 +635,16 @@ .modal-team-container { border-radius: $br-8; border: $b-2 solid var(--color-background-quaternary); - min-width: $sz-364; - min-height: $sz-192; - max-width: $sz-512; - max-height: $sz-512; + min-inline-size: $sz-364; + min-block-size: $sz-192; + max-inline-size: $sz-512; + max-block-size: $sz-512; box-shadow: var(--el-shadow-dark); position: fixed; top: px2rem(72); right: var(--sp-m); left: unset; - width: $sz-400; + inline-size: $sz-400; padding: var(--sp-xxxl); background-color: var(--color-background-primary); z-index: var(--z-index-set); @@ -664,26 +660,33 @@ z-index: var(--z-index-set); } -.modal-title { - @include t.use-typography("headline-medium"); - - height: $sz-32; +.color-light { color: var(--color-foreground-primary); } -.role-select { +.form-wrapper { display: flex; flex-direction: column; - gap: var(--sp-xs); - row-gap: var(--sp-s); + gap: var(--sp-xl); +} + +.form-group { + display: flex; + flex-direction: column; + gap: var(--sp-s); +} + +.form-buttons { + display: flex; + justify-content: flex-end; } .arrow-icon { display: flex; justify-content: center; align-items: center; - height: $sz-16; - width: $sz-16; + block-size: $sz-16; + inline-size: $sz-16; color: transparent; fill: none; stroke-width: $b-1; @@ -691,25 +694,6 @@ transform: rotate(90deg); } -.invite-team-member-text { - @include t.use-typography("body-large"); - - margin: 0 0 var(--sp-l) 0; - color: var(--color-foreground-primary); -} - -.role-title { - @include t.use-typography("body-large"); - - margin: 0; - color: var(--color-foreground-primary); -} - -.invitation-row { - margin-top: var(--sp-s); - margin-bottom: var(--sp-xxl); -} - .action-buttons { display: flex; justify-content: flex-end; @@ -729,8 +713,8 @@ position: fixed; left: 0; top: 0; - height: 100%; - width: 100%; + block-size: 100%; + inline-size: 100%; z-index: var(--z-index-set); background-color: var(--overlay-color); } @@ -741,10 +725,10 @@ border-radius: $br-8; background-color: var(--color-background-primary); border: $b-2 solid var(--color-background-quaternary); - min-width: $sz-364; - min-height: $sz-192; - max-width: $sz-512; - max-height: $sz-512; + min-inline-size: $sz-364; + min-block-size: $sz-192; + max-inline-size: $sz-512; + max-block-size: $sz-512; } .modal-header { @@ -795,23 +779,11 @@ .action-buttons { @extend %modal-action-btns; - button { - @extend %modal-accept-btn; - } - .cancel-button { @extend %modal-cancel-btn; } } -// TODO: Remove this extended class using input component -.email-input { - @include t.use-typography("body-small"); - @extend %input-base; - - height: auto; -} - // FIXME: This does not conform to our CSS Guidelines. Need to unnest and to use // custom properties to handle state changes. .input-wrapper { @@ -963,7 +935,7 @@ .modal-select-org-container { display: flex; flex-direction: column; - width: $sz-512; + inline-size: $sz-512; } .modal-select-org-header { @@ -1017,7 +989,7 @@ grid-template-columns: var(--sp-xxxl) 1fr var(--sp-xxxl); align-items: center; gap: var(--sp-m); - width: max-content; + inline-size: max-content; } .org-options-btn { @@ -1045,8 +1017,8 @@ display: flex; justify-content: center; align-items: center; - height: $sz-16; - width: $sz-16; + block-size: $sz-16; + inline-size: $sz-16; color: transparent; fill: none; stroke-width: $b-1; @@ -1067,8 +1039,8 @@ border: $b-2 solid var(--color-background-quaternary); margin: 0; top: var(--sp-xxxl); - width: fit-content; - min-width: $sz-160; + inline-size: fit-content; + min-inline-size: $sz-160; } .org-dropdown-item { @@ -1077,8 +1049,8 @@ display: flex; align-items: center; justify-content: space-between; - height: $sz-28; - width: 100%; + block-size: $sz-28; + inline-size: 100%; padding: px2rem(6); border-radius: $br-8; cursor: pointer; diff --git a/frontend/src/app/main/ui/forms.cljs b/frontend/src/app/main/ui/forms.cljs index 82b0335008..b542a06073 100644 --- a/frontend/src/app/main/ui/forms.cljs +++ b/frontend/src/app/main/ui/forms.cljs @@ -66,57 +66,53 @@ touched? (and (contains? (:data @form) name) (get-in @form [:touched name])) - value (mf/use-state "") - focus? (mf/use-state false) + value* (mf/use-state "") + value (deref value*) - items - (mf/use-state - (fn [] - (let [initial (get-in @form [:data name])] - (if (or (vector? initial) (set? initial)) - (mapv (fn [val] - {:text val - :valid (valid-item-fn val) - :caution (caution-item-fn val)}) - initial) - [])))) - - on-focus - (mf/use-fn - #(reset! focus? true)) + items* (mf/use-state + (fn [] + (let [initial (get-in @form [:data name])] + (if (or (vector? initial) (set? initial)) + (mapv (fn [val] + {:text val + :valid (valid-item-fn val) + :caution (caution-item-fn val)}) + initial) + [])))) + items (deref items*) on-change (mf/use-fn (fn [event] (let [content (-> event dom/get-target dom/get-input-value)] - (reset! value content)))) + (reset! value* content)))) on-key-down (mf/use-fn - (mf/deps @value form name valid-item-fn caution-item-fn trim) + (mf/deps value form name valid-item-fn caution-item-fn trim) (fn [event] - (let [val (cond-> @value trim str/trim)] + (let [val (cond-> value trim str/trim)] (cond (or (k/enter? event) (k/comma? event) (k/space? event)) (do (dom/prevent-default event) (dom/stop-propagation event) (swap! form assoc-in [:touched name] true) - (when (and (valid-item-fn val) (not (str/empty? @value))) - (reset! value "") + (when (and (valid-item-fn val) (not (str/empty? value))) + (reset! value* "") (swap! form assoc-in [:touched name] false) (doseq [v (str/split val #",|\s+")] (let [v (str/trim v)] - (swap! items conj-dedup {:text v - :valid (valid-item-fn v) - :caution (caution-item-fn v)}))))) + (swap! items* conj-dedup {:text v + :valid (valid-item-fn v) + :caution (caution-item-fn v)}))))) - (and (k/backspace? event) (str/empty? @value)) + (and (k/backspace? event) (str/empty? value)) (do (dom/prevent-default event) (dom/stop-propagation event) - (swap! items (fn [items] - (if (empty? items) items (pop items))))))))) + (swap! items* (fn [items] + (if (empty? items) items (pop items))))))))) on-paste (mf/use-fn @@ -136,43 +132,41 @@ (remove str/empty?))] (doseq [part parts] (when (valid-item-fn part) - (swap! items conj-dedup {:text part - :valid true - :caution (caution-item-fn part)}))) + (swap! items* conj-dedup {:text part + :valid true + :caution (caution-item-fn part)}))) ;; Reset input value and mark as untouched after successful paste - (reset! value "") + (reset! value* "") (swap! form assoc-in [:touched name] false)))))) on-blur (mf/use-fn (fn [] - (reset! focus? false) (when-not (get-in @form [:touched name]) (swap! form assoc-in [:touched name] true)))) on-remove-item (mf/use-fn (fn [item] - (swap! items #(filterv (fn [x] (not= x item)) %)))) + (swap! items* #(filterv (fn [x] (not= x item)) %)))) props - (mf/spread-props props {:value @value + (mf/spread-props props {:value value :on-change on-change - :on-focus on-focus :on-blur on-blur :on-key-down on-key-down :on-paste on-paste :hint-type (when (and touched? - (not (str/empty? @value)) - (not (valid-item-fn @value))) "error")})] + (not (str/empty? value)) + (not (valid-item-fn value))) "error")})] ;; Sync form data whenever items or input value changes. ;; This ensures the current (unconfirmed) input value is included in the ;; form data when the user submits without pressing Enter/Space/Comma. - (mf/with-effect [@items @value] - (let [items-text (mapv :text @items) - val (cond-> @value trim str/trim) + (mf/with-effect [items value] + (let [items-text (mapv :text items) + val (cond-> value trim str/trim) combined (if (and (valid-item-fn val) (not (str/empty? val))) (conj items-text val) items-text) @@ -182,7 +176,7 @@ [:div {:class (stl/css :multi-input)} [:> input* props] - (when-let [items-seq (seq @items)] + (when-let [items-seq (seq items)] [:div {:class (stl/css :multi-input-chips)} (for [item items-seq] [:div {:class (stl/css :multi-input-chip)