diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs index 3919c5564d..51ad8abac9 100644 --- a/frontend/src/app/main/data/workspace/interactions.cljs +++ b/frontend/src/app/main/data/workspace/interactions.cljs @@ -119,21 +119,6 @@ (let [page (dsh/lookup-page state)] (rx/of (update-flow (:id page) flow-id #(assoc % :name name))))))) -(defn start-rename-flow - [id] - (dm/assert! (uuid? id)) - (ptk/reify ::start-rename-flow - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-local :flow-for-rename] id)))) - -(defn end-rename-flow - [] - (ptk/reify ::end-rename-flow - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local dissoc :flow-for-rename)))) - ;; --- Interactions (defn- connected-frame? diff --git a/frontend/src/app/main/ui/ds.cljs b/frontend/src/app/main/ui/ds.cljs index 4144bc9d6c..147a34bfa4 100644 --- a/frontend/src/app/main/ui/ds.cljs +++ b/frontend/src/app/main/ui/ds.cljs @@ -9,6 +9,7 @@ [app.config :as cf] [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.controls.checkbox :refer [checkbox*]] [app.main.ui.ds.controls.combobox :refer [combobox*]] [app.main.ui.ds.controls.input :refer [input*]] [app.main.ui.ds.controls.numeric-input :refer [numeric-input*]] @@ -62,6 +63,7 @@ :RawSvg raw-svg* :Select select* :Switch switch* + :Checkbox checkbox* :Combobox combobox* :Text text* :TabSwitcher tab-switcher* diff --git a/frontend/src/app/main/ui/ds/buttons/_buttons.scss b/frontend/src/app/main/ui/ds/buttons/_buttons.scss index 64f34c5b1a..433495c300 100644 --- a/frontend/src/app/main/ui/ds/buttons/_buttons.scss +++ b/frontend/src/app/main/ui/ds/buttons/_buttons.scss @@ -11,12 +11,18 @@ %base-button { --button-bg-color: initial; --button-fg-color: initial; + --button-hover-bg-color: initial; --button-hover-fg-color: initial; + --button-active-bg-color: initial; + --button-active-fg-color: initial; + --button-disabled-bg-color: initial; --button-disabled-fg-color: initial; + --button-border-color: var(--button-bg-color); + --button-focus-inner-ring-color: initial; --button-focus-outer-ring-color: initial; @@ -38,8 +44,10 @@ --button-fg-color: var(--button-hover-fg-color); } - &:active { + &:active, + &[aria-pressed="true"] { --button-bg-color: var(--button-active-bg-color); + --button-fg-color: var(--button-active-fg-color); } &:focus-visible { @@ -63,6 +71,7 @@ --button-hover-fg-color: var(--color-background-secondary); --button-active-bg-color: var(--color-accent-tertiary); + --button-active-fg-color: var(--color-background-secondary); --button-disabled-bg-color: var(--color-accent-primary-muted); --button-disabled-fg-color: var(--color-background-secondary); @@ -72,7 +81,8 @@ --button-focus-inner-ring-color: var(--color-background-secondary); --button-focus-outer-ring-color: var(--color-accent-primary); - &:active { + &:active, + &[aria-pressed="true"] { box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2); } } @@ -85,6 +95,7 @@ --button-hover-fg-color: var(--color-accent-primary); --button-active-bg-color: var(--color-background-quaternary); + --button-active-fg-color: var(--color-accent-primary); --button-disabled-bg-color: transparent; --button-disabled-fg-color: var(--color-foreground-secondary); @@ -103,6 +114,7 @@ --button-hover-fg-color: var(--color-accent-primary); --button-active-bg-color: var(--color-background-quaternary); + --button-active-fg-color: var(--color-accent-primary); --button-disabled-bg-color: transparent; --button-disabled-fg-color: var(--color-accent-primary-muted); @@ -121,6 +133,7 @@ --button-hover-fg-color: var(--color-foreground-primary); --button-active-bg-color: var(--color-accent-error); + --button-active-fg-color: var(--color-foreground-primary); --button-disabled-bg-color: var(--color-background-error); --button-disabled-fg-color: var(--color-accent-error); @@ -130,7 +143,8 @@ --button-focus-inner-ring-color: var(--color-background-primary); --button-focus-outer-ring-color: var(--color-accent-primary); - &:active { + &:active, + &[aria-pressed="true"] { box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2); } } diff --git a/frontend/src/app/main/ui/ds/controls/checkbox.cljs b/frontend/src/app/main/ui/ds/controls/checkbox.cljs new file mode 100644 index 0000000000..110d02bff1 --- /dev/null +++ b/frontend/src/app/main/ui/ds/controls/checkbox.cljs @@ -0,0 +1,45 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.ds.controls.checkbox + (:require-macros + [app.main.style :as stl]) + (:require + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] + [rumext.v2 :as mf])) + +(def ^:private schema:checkbox + [:map + [:id {:optional true} :string] + [:label {:optional true} :string] + [:checked {:optional true} :boolean] + [:on-change {:optional true} fn?] + [:disabled {:optional true} :boolean]]) + +(mf/defc checkbox* + {::mf/schema schema:checkbox} + [{:keys [id class label checked on-change disabled] :rest props}] + (let [props + (mf/spread-props props {:type "checkbox" + :class (stl/css :checkbox-input) + :id id + :checked checked + :on-change on-change + :disabled disabled})] + + [:div {:class [class (stl/css :checkbox)]} + [:label {:for id + :class (stl/css :checkbox-label)} + [:div {:class (stl/css-case :checkbox-box true + :checked checked + :disabled disabled)} + (when checked + [:> icon* {:icon-id i/tick + :size "s"}])] + + [:div {:class (stl/css :checkbox-text)} label] + + [:> :input props]]])) diff --git a/frontend/src/app/main/ui/ds/controls/checkbox.mdx b/frontend/src/app/main/ui/ds/controls/checkbox.mdx new file mode 100644 index 0000000000..fe3b525a9e --- /dev/null +++ b/frontend/src/app/main/ui/ds/controls/checkbox.mdx @@ -0,0 +1,40 @@ +{ /* This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + Copyright (c) KALEIDOS INC */ } + +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import * as Checkbox from "./checkbox.stories"; + + + +# Checkbox + +The `checkbox*` component is a toggle control. It allows users to switch between boolean states (`false` or `true`). + + + + + +## Anatomy + +The checkbox component consists of three main parts: + +- **Label** (optional): the text that describes what the checkbox controls. Clicking on this text also works for toggling. +- **Box**: the box which shows the current state. Contains a check if the state is `true`. +- **Native element**: the native HTML element which holds the state. It remains hidden to the user. + +## Usage Guidelines + +### When to Use + +- For boolean settings that take effect immediately. +- In preference panels and configuration screens. + +### When Not to Use + +- For actions that require confirmation (use buttons instead). +- For multiple choice selections (use radio buttons or select). +- For temporary states that need explicit "Apply" action. +- For ternary states. diff --git a/frontend/src/app/main/ui/ds/controls/checkbox.scss b/frontend/src/app/main/ui/ds/controls/checkbox.scss new file mode 100644 index 0000000000..81eda1fd47 --- /dev/null +++ b/frontend/src/app/main/ui/ds/controls/checkbox.scss @@ -0,0 +1,86 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/typography.scss" as *; + +.checkbox { + --input-checkbox-border-color: var(--color-foreground-secondary); + --input-checkbox-border-color-focus: var(--color-accent-primary); + --input-checkbox-border-color-hover: var(--color-accent-primary-muted); + --input-checkbox-foreground-color: var(--color-foreground-primary); + --input-checkbox-background-color: var(--color-background-quaternary); + + --input-checkbox-border-color-checked: var(--color-background-quaternary); + --input-checkbox-foreground-color-checked: var(--color-background-primary); + --input-checkbox-background-color-checked: var(--color-accent-primary); + + --input-checkbox-foreground-color-disabled: var(--color-background-primary); + --input-checkbox-background-color-disabled: var(--color-foreground-secondary); + + --input-checkbox-text-color: var(--color-foreground-secondary); +} + +.checkbox-label { + display: grid; + grid-template-columns: var(--sp-l) 1fr 0; + align-items: center; + + &:hover { + .checkbox-box { + border-color: var(--input-checkbox-border-color-hover); + } + } + + &:focus, + &:focus-within { + .checkbox-box { + border-color: var(--input-checkbox-border-color-focus); + } + } +} + +.checkbox-box { + display: flex; + align-items: center; + justify-content: center; + inline-size: $sz-16; + block-size: $sz-16; + border-radius: $br-4; + border: $b-1 solid var(--input-checkbox-border-color); + color: var(--input-checkbox-foreground-color); + background-color: var(--input-checkbox-background-color); + + &.disabled { + border: 0; + } + + &.checked { + --input-checkbox-border-color: var(--input-checkbox-border-color-checked); + --input-checkbox-foreground-color: var(--input-checkbox-foreground-color-checked); + --input-checkbox-background-color: var(--input-checkbox-background-color-checked); + + &.disabled { + --input-checkbox-foreground-color: var(--input-checkbox-foreground-color-disabled); + --input-checkbox-background-color: var(--input-checkbox-background-color-disabled); + } + } +} + +.checkbox-text { + @include use-typography("body-small"); + padding-inline-start: var(--sp-s); + color: var(--input-checkbox-text-color); +} + +.checkbox-input { + &:focus { + outline: 0; + inline-size: 0; + block-size: 0; + } +} diff --git a/frontend/src/app/main/ui/ds/controls/checkbox.stories.jsx b/frontend/src/app/main/ui/ds/controls/checkbox.stories.jsx new file mode 100644 index 0000000000..133e24c22b --- /dev/null +++ b/frontend/src/app/main/ui/ds/controls/checkbox.stories.jsx @@ -0,0 +1,76 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import * as React from "react"; +import Components from "@target/components"; + +const { Checkbox } = Components; + +export default { + title: "Controls/Checkbox", + component: Checkbox, + argTypes: { + label: { + control: { type: "text" }, + description: "Label text displayed next to the checkbox", + }, + checked: { + control: { type: "boolean" }, + description: "Whether the checkbox is checked", + }, + disabled: { + control: { type: "boolean" }, + description: "Whether the checkbox is disabled", + }, + }, + args: { + checked: false, + disabled: false, + }, + parameters: { + controls: { + exclude: ["id", "on-change"], + }, + }, + render: ({ ...args }) => , +}; + +export const Default = { + args: { + label: "Toggle something", + disabled: false, + }, + render: ({ ...args }) => , +}; + +export const Checked = { + args: { + label: "Toggle something", + checked: true, + disabled: false, + }, + render: ({ ...args }) => , +}; + +export const WithoutLabel = { + args: { + disabled: false, + }, + render: ({ ...args }) => , +}; + +export const WithLongLabel = { + args: { + label: + "This is a very long label that demonstrates how the checkbox component handles text wrapping and layout when the label content is extensive", + disabled: false, + }, + render: ({ ...args }) => ( +
+ +
+ ), +}; diff --git a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs index de4acf3c82..4f44ce4429 100644 --- a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs +++ b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs @@ -300,6 +300,7 @@ "A collection of all icons" (collect-icons)) +(def ^:private ^:const icon-size-l 32) (def ^:private ^:const icon-size-m 16) (def ^:private ^:const icon-size-s 12) @@ -308,7 +309,7 @@ [:class {:optional true} [:maybe :string]] [:icon-id [:and :string [:fn #(contains? icon-list %)]]] [:size {:optional true} - [:maybe [:enum "s" "m"]]]]) + [:maybe [:enum "s" "m" "l"]]]]) (mf/defc icon* {::mf/schema schema:icon} @@ -317,10 +318,14 @@ {:class [class (stl/css :icon)] :width icon-size-m :height icon-size-m}) - size-px (if (= size "s") - icon-size-s - icon-size-m) - offset (/ (- icon-size-m size-px) 2)] + + size-px (cond (= size "l") icon-size-l + (= size "s") icon-size-s + :else icon-size-m) + + offset (if (or (= size "s") (= size "m")) + (/ (- icon-size-m size-px) 2) + 0)] [:> :svg props [:use {:href (dm/str "#icon-" icon-id) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index ddd314ca40..8cc8ceb903 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -25,7 +25,7 @@ [app.main.ui.workspace.sidebar.options.menus.bool :refer [bool-options*]] [app.main.ui.workspace.sidebar.options.menus.component :refer [component-menu*]] [app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell] - [app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu]] + [app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu*]] [app.main.ui.workspace.sidebar.options.menus.layout-container :as layout-container] [app.main.ui.workspace.sidebar.options.page :as page] [app.main.ui.workspace.sidebar.options.shapes.bool :as bool] @@ -215,7 +215,7 @@ (case options-mode :prototype [:div {:class (stl/css :element-options :interaction-options)} - [:& interactions-menu {:shape (first shapes)}]] + [:> interactions-menu* {:shape (first shapes)}]] :inspect [:div {:class (stl/css :element-options :inspect-options)} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index 2f8271e4d8..9a1a529897 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -8,33 +8,32 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] - [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] [app.common.types.page :as ctp] [app.common.types.shape-tree :as ctt] [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] - [app.main.data.workspace :as dw] + [app.main.data.common :as dcm] [app.main.data.workspace.interactions :as dwi] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] [app.main.ui.components.select :refer [select]] [app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] - [app.main.ui.ds.foundations.assets.icon :as i] - [app.main.ui.icons :as deprecated-icon] + [app.main.ui.ds.controls.checkbox :refer [checkbox*]] + [app.main.ui.ds.controls.input :refer [input*]] + [app.main.ui.ds.controls.numeric-input :refer [numeric-input*]] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [cuerdas.core :as str] - [okulary.core :as l] [rumext.v2 :as mf])) (defn- event-type-names [] - {:click (tr "workspace.options.interaction-on-click") + {:click (tr "workspace.options.interaction-on-click") ;; TODO: need more UX research ;; :mouse-over (tr "workspace.options.interaction-while-hovering") ;; :mouse-press (tr "workspace.options.interaction-while-pressing") @@ -49,16 +48,16 @@ (defn- action-summary [interaction destination] (case (:action-type interaction) - :navigate (tr "workspace.options.interaction-navigate-to-dest" - (get destination :name (tr "workspace.options.interaction-none"))) - :open-overlay (tr "workspace.options.interaction-open-overlay-dest" - (get destination :name (tr "workspace.options.interaction-none"))) + :navigate (tr "workspace.options.interaction-navigate-to-dest" + (get destination :name (tr "workspace.options.interaction-none"))) + :open-overlay (tr "workspace.options.interaction-open-overlay-dest" + (get destination :name (tr "workspace.options.interaction-none"))) :toggle-overlay (tr "workspace.options.interaction-toggle-overlay-dest" (get destination :name (tr "workspace.options.interaction-none"))) - :close-overlay (tr "workspace.options.interaction-close-overlay-dest" - (get destination :name (tr "workspace.options.interaction-self"))) - :prev-screen (tr "workspace.options.interaction-prev-screen") - :open-url (tr "workspace.options.interaction-open-url") + :close-overlay (tr "workspace.options.interaction-close-overlay-dest" + (get destination :name (tr "workspace.options.interaction-self"))) + :prev-screen (tr "workspace.options.interaction-prev-screen") + :open-url (tr "workspace.options.interaction-open-url") "--")) (defn- get-frames-options @@ -67,149 +66,117 @@ (filter #(and (not= (:id %) (:id shape)) ; A frame cannot navigate to itself (not= (:id %) (:frame-id shape)))) ; nor a shape to its container frame (map (fn [frame] - {:value (str (:id frame)) :label (:name frame)})))) + {:value (str (:id frame)) + :label (:name frame)})))) (defn- get-shared-frames-options [shared-frames] (map (fn [frame] - {:value (str (:id frame)) :label (:name frame)}) shared-frames)) + {:value (str (:id frame)) + :label (:name frame)}) shared-frames)) -(def flow-for-rename-ref - (l/derived (l/in [:workspace-local :flow-for-rename]) st/state)) +(mf/defc prototype-pill* + [{:keys [title description on-change is-editable + left-button-icon-id left-button-tooltip on-left-button-click is-left-button-active + right-button-icon-id right-button-tooltip on-right-button-click is-right-button-active]} external-ref] + (let [local-ref (mf/use-ref) + ref (or external-ref local-ref) -(mf/defc flow-item + handle-focus + (mf/use-fn + (fn [] + (let [input-node (mf/ref-val ref)] + (dom/select-text! input-node)))) + + handle-key-down + (mf/use-fn + (fn [event] + (let [input-node (mf/ref-val ref) + target (dom/get-target event) + value (-> target dom/get-value str/trim)] + (when (or (kbd/esc? event) (kbd/enter? event)) + (dom/blur! input-node) + (on-change value))))) + + handle-blur + (mf/use-fn + (fn [event] + (let [target (dom/get-target event) + value (-> target dom/get-value str/trim)] + (on-change value))))] + + [:div {:class (stl/css-case :prototype-pill true + :double (some? description))} + [:> icon-button* {:variant "secondary" + :class (stl/css :prototype-pill-button :left) + :aria-pressed is-left-button-active + :icon left-button-icon-id + :aria-label left-button-tooltip + :on-click on-left-button-click}] + + [:div {:class (stl/css :prototype-pill-main)} + (if is-editable + [:input {:type "text" + :class (stl/css :prototype-pill-input) + :ref ref + :default-value title + :on-focus handle-focus + :on-key-down handle-key-down + :on-blur handle-blur}] + + [:div {:class (stl/css :prototype-pill-center)} + [:div {:class (stl/css :prototype-pill-info)} + [:div {:class (stl/css :prototype-pill-name)} title] + [:div {:class (stl/css :prototype-pill-description)} description]]]) + + [:> icon-button* {:variant "secondary" + :class (stl/css :prototype-pill-button :right) + :aria-pressed is-right-button-active + :icon right-button-icon-id + :aria-label right-button-tooltip + :on-click on-right-button-click}]]])) + +(mf/defc flow-item* [{:keys [flow]}] - (let [editing? (mf/use-state false) - flow-for-rename (mf/deref flow-for-rename-ref) - name-ref (mf/use-ref) - - start-edit (fn [] - (reset! editing? true)) - - accept-edit (fn [] - (let [name-input (mf/ref-val name-ref) - name (str/trim (dom/get-value name-input))] - (reset! editing? false) - (st/emit! (dwi/end-rename-flow) - (when-not (str/empty? name) - (dwi/rename-flow (:id flow) name))))) - - cancel-edit (fn [] - (reset! editing? false) - (st/emit! (dwi/end-rename-flow))) - - on-key-down (fn [event] - (when (kbd/enter? event) (accept-edit)) - (when (kbd/esc? event) (cancel-edit))) - - start-flow + (let [start-flow (mf/use-fn (mf/deps flow) - #(st/emit! (dw/select-shape (:starting-frame flow)))) + #(st/emit! (dcm/go-to-viewer {:section "interactions" + :frame-id (:starting-frame flow)}))) - ;; rename-flow - ;; (mf/use-fn - ;; (mf/deps flow) - ;; #(st/emit! (dwi/start-rename-flow (:id flow)))) + rename-flow + (mf/use-fn + (mf/deps flow) + (fn [value] + (when-not (str/empty? value) + (st/emit! (dwi/rename-flow (:id flow) value))))) remove-flow (mf/use-fn (mf/deps flow) #(st/emit! (dwi/remove-flow (:id flow))))] - (mf/use-effect - (fn [] - #(when editing? - (cancel-edit)))) + [:> prototype-pill* {:title (:name flow "") + :is-editable true + :on-change rename-flow + :left-button-icon-id i/play + :left-button-tooltip (tr "workspace.options.flows.flow-start") + :on-left-button-click start-flow + :right-button-icon-id i/remove + :right-button-tooltip (tr "labels.remove") + :on-right-button-click remove-flow}])) - (mf/use-effect - (mf/deps flow-for-rename) - #(when (and (= flow-for-rename (:id flow)) - (not @editing?)) - (start-edit))) - - (mf/use-effect - (mf/deps @editing?) - #(when @editing? - (let [name-input (mf/ref-val name-ref)] - (dom/select-text! name-input)) - nil)) - - [:div {:class (stl/css :flow-element)} - [:span {:class (stl/css :flow-info)} - [:span {:class (stl/css :flow-name-wrapper)} - [:button {:class (stl/css :start-flow-btn) - :on-click start-flow} - [:span {:class (stl/css :button-icon)} - deprecated-icon/play]] - [:span {:class (stl/css :flow-input-wrapper)} - [:input - {:class (stl/css :flow-input) - :type "text" - :ref name-ref - :on-blur accept-edit - :on-key-down on-key-down - :default-value (:name flow "")}]]]] - - [:> icon-button* {:variant "ghost" - :aria-label (tr "workspace.options.flows.remove-flow") - :on-click remove-flow - :icon i/remove}]])) - -(mf/defc page-flows - {::mf/props :obj} - [{:keys [flows]}] - (when flows - [:div {:class (stl/css :interaction-options)} - [:> title-bar* {:collapsable false - :title (tr "workspace.options.flows.flow-starts") - :class (stl/css :title-spacing-layout-flow)}] - (for [[id flow] flows] - [:& flow-item {:flow flow :key (dm/str id)}])])) - -(mf/defc shape-flows - {::mf/props :obj} - [{:keys [flows shape]}] - (when (cfh/frame-shape? shape) - (let [flow (ctp/get-frame-flow flows (:id shape)) - add-flow (mf/use-fn #(st/emit! (dwi/add-flow-selected-frame)))] - - [:div {:class (stl/css :element-set)} - [:> title-bar* {:collapsable false - :title (tr "workspace.options.flows.flow") - :class (stl/css :title-spacing-layout-flow)} - (when (nil? flow) - [:> icon-button* {:variant "ghost" - :aria-label (tr "workspace.options.flows.add-flow-start") - :on-click add-flow - :icon i/add}])] - - (when (some? flow) - [:& flow-item {:flow flow :key (dm/str (:id flow))}])]))) - -(def ^:private corner-center-icon - (deprecated-icon/icon-xref :corner-center (stl/css :corner-icon))) -(def ^:private corner-bottom-icon - (deprecated-icon/icon-xref :corner-bottom (stl/css :corner-icon))) -(def ^:private corner-bottomleft-icon - (deprecated-icon/icon-xref :corner-bottom-left (stl/css :corner-icon))) -(def ^:private corner-bottomright-icon - (deprecated-icon/icon-xref :corner-bottom-right (stl/css :corner-icon))) -(def ^:private corner-top-icon - (deprecated-icon/icon-xref :corner-top (stl/css :corner-icon))) -(def ^:private corner-topleft-icon - (deprecated-icon/icon-xref :corner-top-left (stl/css :corner-icon))) -(def ^:private corner-topright-icon - (deprecated-icon/icon-xref :corner-top-right (stl/css :corner-icon))) - -(mf/defc interaction-entry +(mf/defc interaction-item* [{:keys [index shape interaction update-interaction remove-interaction]}] (let [objects (deref refs/workspace-page-objects) destination (get objects (:destination interaction)) - frames (mf/with-memo [objects] (ctt/get-viewer-frames objects {:all-frames? true})) - shape-parent-ids (mf/with-memo [objects] (cfh/get-parent-ids objects (:id shape))) - shape-parents (mf/with-memo [frames shape] (filter (comp (set shape-parent-ids) :id) frames)) + frames (mf/with-memo [objects] + (ctt/get-viewer-frames objects {:all-frames? true})) + shape-parent-ids (mf/with-memo [objects] + (cfh/get-parent-ids objects (:id shape))) + shape-parents (mf/with-memo [frames shape] + (filter (comp (set shape-parent-ids) :id) frames)) overlay-pos-type (:overlay-pos-type interaction) close-click-outside? (:close-click-outside interaction false) @@ -219,14 +186,16 @@ way (-> interaction :animation :way) direction (-> interaction :animation :direction) - state* (mf/use-state false) - extended-open? (deref state*) - - toggle-extended (mf/use-fn #(swap! state* not)) + open-extended* (mf/use-state false) + open-extended? (deref open-extended*) ext-delay-ref (mf/use-ref nil) ext-duration-ref (mf/use-ref nil) + toggle-extended + (mf/use-fn + #(swap! open-extended* not)) + change-event-type (mf/use-fn (mf/deps index update-interaction) @@ -364,7 +333,7 @@ (update-interaction index #(ctsi/set-offset-effect % value))))) - event-type-options (-> [{:value :click :label (tr "workspace.options.interaction-on-click")} + event-type-options (-> [{:value :click :label (tr "workspace.options.interaction-on-click")} ;; TODO: need more UX research ;; :mouse-over (tr "workspace.options.interaction-while-hovering") ;; :mouse-press (tr "workspace.options.interaction-while-pressing") @@ -373,12 +342,12 @@ (cond-> (cfh/frame-shape? shape) (conj {:value :after-delay :label (tr "workspace.options.interaction-after-delay")}))) - action-type-options [{:value :navigate :label (tr "workspace.options.interaction-navigate-to")} - {:value :open-overlay :label (tr "workspace.options.interaction-open-overlay")} + action-type-options [{:value :navigate :label (tr "workspace.options.interaction-navigate-to")} + {:value :open-overlay :label (tr "workspace.options.interaction-open-overlay")} {:value :toggle-overlay :label (tr "workspace.options.interaction-toggle-overlay")} - {:value :close-overlay :label (tr "workspace.options.interaction-close-overlay")} - {:value :prev-screen :label (tr "workspace.options.interaction-prev-screen")} - {:value :open-url :label (tr "workspace.options.interaction-open-url")}] + {:value :close-overlay :label (tr "workspace.options.interaction-close-overlay")} + {:value :prev-screen :label (tr "workspace.options.interaction-prev-screen")} + {:value :open-url :label (tr "workspace.options.interaction-open-url")}] frames-opts (get-frames-options frames shape) @@ -400,18 +369,18 @@ [{:value (str (:id shape)) :label (str (:name shape) " (" (tr "workspace.options.interaction-self") ")")}]) [{:value (str (:id shape)) :label (str (:name shape) " (" (tr "workspace.options.interaction-self") ")")}])) - overlay-position-opts [{:value :manual :label (tr "workspace.options.interaction-pos-manual")} - {:value :center :label (tr "workspace.options.interaction-pos-center")} - {:value :top-left :label (tr "workspace.options.interaction-pos-top-left")} - {:value :top-right :label (tr "workspace.options.interaction-pos-top-right")} - {:value :top-center :label (tr "workspace.options.interaction-pos-top-center")} - {:value :bottom-left :label (tr "workspace.options.interaction-pos-bottom-left")} - {:value :bottom-right :label (tr "workspace.options.interaction-pos-bottom-right")} - {:value :bottom-center :label (tr "workspace.options.interaction-pos-bottom-center")}] + overlay-position-opts [{:value :manual :label (tr "workspace.options.interaction-pos-manual")} + {:value :center :label (tr "workspace.options.interaction-pos-center")} + {:value :top-left :label (tr "workspace.options.interaction-pos-top-left")} + {:value :top-right :label (tr "workspace.options.interaction-pos-top-right")} + {:value :top-center :label (tr "workspace.options.interaction-pos-top-center")} + {:value :bottom-left :label (tr "workspace.options.interaction-pos-bottom-left")} + {:value :bottom-right :label (tr "workspace.options.interaction-pos-bottom-right")} + {:value :bottom-center :label (tr "workspace.options.interaction-pos-bottom-center")}] - basic-animation-opts [{:value "" :label (tr "workspace.options.interaction-animation-none")} - {:value :dissolve :label (tr "workspace.options.interaction-animation-dissolve")} - {:value :slide :label (tr "workspace.options.interaction-animation-slide")}] + basic-animation-opts [{:value "" :label (tr "workspace.options.interaction-animation-none")} + {:value :dissolve :label (tr "workspace.options.interaction-animation-dissolve")} + {:value :slide :label (tr "workspace.options.interaction-animation-slide")}] animation-opts (mf/with-memo [basic-animation-opts] @@ -419,207 +388,194 @@ (d/concat-vec basic-animation-opts [{:value :push :label (tr "workspace.options.interaction-animation-push")}]) basic-animation-opts)) - easing-options [{:icon :easing-linear :value :linear :label (tr "workspace.options.interaction-easing-linear")} - {:icon :easing-ease :value :ease :label (tr "workspace.options.interaction-easing-ease")} - {:icon :easing-ease-in :value :ease-in :label (tr "workspace.options.interaction-easing-ease-in")} - {:icon :easing-ease-out :value :ease-out :label (tr "workspace.options.interaction-easing-ease-out")} - {:icon :easing-ease-in-out :value :ease-in-out :label (tr "workspace.options.interaction-easing-ease-in-out")}]] + easing-options [{:icon :easing-linear :value :linear :label (tr "workspace.options.interaction-easing-linear")} + {:icon :easing-ease :value :ease :label (tr "workspace.options.interaction-easing-ease")} + {:icon :easing-ease-in :value :ease-in :label (tr "workspace.options.interaction-easing-ease-in")} + {:icon :easing-ease-out :value :ease-out :label (tr "workspace.options.interaction-easing-ease-out")} + {:icon :easing-ease-in-out :value :ease-in-out :label (tr "workspace.options.interaction-easing-ease-in-out")}]] + [:div {:class (stl/css :interaction-item)} + [:> prototype-pill* {:title (event-type-name interaction) + :description (action-summary interaction destination) + :left-button-icon-id i/hsva + :left-button-tooltip (tr "labels.options") + :is-left-button-active open-extended? + :on-left-button-click toggle-extended + :right-button-icon-id i/remove + :right-button-tooltip (tr "labels.remove") + :on-right-button-click #(remove-interaction index)}] - [:div {:class (stl/css-case :element-set-options-group true - :element-set-options-group-open extended-open?)} - ; Summary - [:div {:class (stl/css :interactions-summary)} - [:button {:class (stl/css-case :extend-btn true - :extended extended-open?) - :on-click toggle-extended} - deprecated-icon/menu] - - [:div {:class (stl/css :interactions-info) - :on-click toggle-extended} - [:div {:class (stl/css :trigger-name)} (event-type-name interaction)] - [:div {:class (stl/css :action-summary)} (action-summary interaction destination)]] - [:button {:class (stl/css :remove-btn) - :data-value index - :on-click #(remove-interaction index)} - deprecated-icon/remove-icon]] - - (when extended-open? - [:div {:class (stl/css :extended-options)} - ;; Trigger select - [:div {:class (stl/css :property-row)} - [:span {:class (stl/css :interaction-name)} - (tr "workspace.options.interaction-trigger")] - [:div {:class (stl/css :select-wrapper)} - [:& select {:class (stl/css :interaction-type-select) - :default-value (:event-type interaction) + (when open-extended? + [:* + ;; Trigger select + [:div {:class (stl/css :interaction-row)} + [:label {:class (stl/css :interaction-row-label)} + [:div {:class (stl/css :interaction-row-name)} + (tr "workspace.options.interaction-trigger")]] + [:div {:class (stl/css :interaction-row-select)} + [:& select {:default-value (:event-type interaction) :options event-type-options :on-change change-event-type}]]] - ;; Delay + ;; Delay (when (ctsi/has-delay interaction) - [:div {:class (stl/css :property-row)} - [:span {:class (stl/css :interaction-name)} - (tr "workspace.options.interaction-delay")] - [:div {:class (stl/css :input-element-wrapper) - :title (tr "workspace.options.interaction-ms")} - [:span.after (tr "workspace.options.interaction-ms")] + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-label)} + [:div {:class (stl/css :interaction-row-name)} + (tr "workspace.options.interaction-delay")]] + [:div {:class (stl/css :interaction-row-input)} [:> numeric-input* {:ref ext-delay-ref - :className (stl/css :numeric-input) + :icon i/character-m + :property (tr "workspace.options.interaction-ms") :on-change change-delay - :value (:delay interaction) - :title (tr "workspace.options.interaction-ms")}]]]) + :value (:delay interaction)}]]]) - ;; Action select - [:div {:class (stl/css :property-row)} - [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-action")] - [:div {:class (stl/css :select-wrapper)} - [:& select {:class (stl/css :interaction-type-select) - :default-value (:action-type interaction) + ;; Action select + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-label)} + [:div {:class (stl/css :interaction-row-name)} + (tr "workspace.options.interaction-action")]] + [:div {:class (stl/css :interaction-row-select)} + [:& select {:default-value (:action-type interaction) :options action-type-options :on-change change-action-type}]]] - ;; Destination + ;; Destination (when (ctsi/has-destination interaction) - [:div {:class (stl/css :property-row)} - [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-destination")] - [:div {:class (stl/css :select-wrapper)} - [:& select {:class (stl/css :interaction-type-select) - :default-value (str (:destination interaction)) + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-label)} + [:div {:class (stl/css :interaction-row-name)} + (tr "workspace.options.interaction-destination")]] + [:div {:class (stl/css :interaction-row-select)} + [:& select {:default-value (str (:destination interaction)) :options destination-options :on-change change-destination}]]]) - ;; Preserve scroll + ;; Preserve scroll (when (ctsi/has-preserve-scroll interaction) - [:div {:class (stl/css :property-row)} - [:div {:class (stl/css :checkbox-option)} - [:label {:for (str "preserve-" index) - :class (stl/css-case :global/checked preserve-scroll?)} - [:span {:class (stl/css-case :global/checked preserve-scroll?)} - (when preserve-scroll? - deprecated-icon/status-tick)] - (tr "workspace.options.interaction-preserve-scroll") - [:input {:type "checkbox" - :id (str "preserve-" index) - :checked preserve-scroll? - :on-change change-preserve-scroll}]]]]) + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-checkbox)} + [:> checkbox* {:id (str "preserve-" index) + :label (tr "workspace.options.interaction-preserve-scroll") + :checked preserve-scroll? + :on-change change-preserve-scroll}]]]) - ;; URL + ;; URL (when (ctsi/has-url interaction) - [:div {:class (stl/css :property-row)} - [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-url")] - [:div {:class (stl/css :input-element-wrapper)} - [:input {:class (stl/css :input-text) - :type "url" - :placeholder "http://example.com" - :default-value (str (:url interaction)) - :on-blur change-url}]]]) + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-label)} + [:div {:class (stl/css :interaction-row-name)} + (tr "workspace.options.interaction-url")]] + [:div {:class (stl/css :interaction-row-input)} + [:> input* {:type "url" + :placeholder "http://example.com" + :default-value (:url interaction) + :on-blur change-url}]]]) (when (ctsi/has-overlay-opts interaction) [:* - ;; Overlay position relative-to (select) - [:div {:class (stl/css :property-row)} - [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-relative-to")] - [:div {:class (stl/css :select-wrapper)} - [:& select {:class (stl/css :interaction-type-select) - :default-value (str (:position-relative-to interaction)) + ;; Overlay position relative-to (select) + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-label)} + [:div {:class (stl/css :interaction-row-name)} + (tr "workspace.options.interaction-relative-to")]] + [:div {:class (stl/css :interaction-row-select)} + [:& select {:default-value (str (:position-relative-to interaction)) :options relative-to-opts :on-change change-position-relative-to}]]] - ;; Overlay position (select) - [:div {:class (stl/css :property-row)} - [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-position")] - [:div {:class (stl/css :select-wrapper)} - [:& select {:class (stl/css :interaction-type-select) - :default-value (:overlay-pos-type interaction) + ;; Overlay position (select) + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-label)} + [:div {:class (stl/css :interaction-row-name)} + (tr "workspace.options.interaction-position")]] + [:div {:class (stl/css :interaction-row-select)} + [:& select {:default-value (:overlay-pos-type interaction) :options overlay-position-opts :on-change change-overlay-pos-type}]]] - ;; Overlay position (buttons) - [:div {:class (stl/css-case :property-row true - :big-row true)} - [:div {:class (stl/css :position-btns-wrapper)} - [:button {:class (stl/css-case :direction-btn true - :center-btn true - :active (= overlay-pos-type :center)) - :data-value "center" - :on-click toggle-overlay-pos-type} - corner-center-icon] - [:button {:class (stl/css-case :direction-btn true - :top-left-btn true - :active (= overlay-pos-type :top-left)) - :data-value "top-left" - :on-click toggle-overlay-pos-type} - corner-topleft-icon] - [:button {:class (stl/css-case :direction-btn true - :top-right-btn true - :active (= overlay-pos-type :top-right)) - :data-value "top-right" - :on-click toggle-overlay-pos-type} - corner-topright-icon] + ;; Overlay position (buttons) + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-position)} + [:div {:class (stl/css :center)} + [:> icon-button* {:variant "secondary" + :aria-pressed (= overlay-pos-type :center) + :data-value "center" + :icon i/corner-center + :aria-label (tr "workspace.options.interaction-pos-center") + :on-click toggle-overlay-pos-type}]] - [:button {:class (stl/css-case :direction-btn true - :top-center-btn true - :active (= overlay-pos-type :top-center)) - :data-value "top-center" - :on-click toggle-overlay-pos-type} - corner-top-icon] + [:div {:class (stl/css :top-left)} + [:> icon-button* {:variant "secondary" + :aria-pressed (= overlay-pos-type :top-left) + :data-value "top-left" + :icon i/corner-top-left + :aria-label (tr "workspace.options.interaction-pos-top-left") + :on-click toggle-overlay-pos-type}]] - [:button {:class (stl/css-case :direction-btn true - :bottom-left-btn true - :active (= overlay-pos-type :bottom-left)) - :data-value "bottom-left" - :on-click toggle-overlay-pos-type} - corner-bottomleft-icon] - [:button {:class (stl/css-case :direction-btn true - :bottom-right-btn true - :active (= overlay-pos-type :bottom-right)) - :data-value "bottom-right" - :on-click toggle-overlay-pos-type} - corner-bottomright-icon] - [:button {:class (stl/css-case :direction-btn true - :bottom-center-btn true - :active (= overlay-pos-type :bottom-center)) - :data-value "bottom-center" - :on-click toggle-overlay-pos-type} - corner-bottom-icon]]] + [:div {:class (stl/css :top-right)} + [:> icon-button* {:variant "secondary" + :aria-pressed (= overlay-pos-type :top-right) + :data-value "top-right" + :icon i/corner-top-right + :aria-label (tr "workspace.options.interaction-pos-top-right") + :on-click toggle-overlay-pos-type}]] - ;; Overlay click outside + [:div {:class (stl/css :top-center)} + [:> icon-button* {:variant "secondary" + :aria-pressed (= overlay-pos-type :top-center) + :data-value "top-center" + :icon i/corner-top + :aria-label (tr "workspace.options.interaction-pos-top-center") + :on-click toggle-overlay-pos-type}]] - [:ul {:class (stl/css :property-list)} - [:li {:class (stl/css :property-row)} - [:div {:class (stl/css :checkbox-option)} - [:label {:for (str "close-" index) - :class (stl/css-case :global/checked close-click-outside?)} - [:span {:class (stl/css-case :global/checked close-click-outside?)} - (when close-click-outside? - deprecated-icon/status-tick)] - (tr "workspace.options.interaction-close-outside") - [:input {:type "checkbox" - :id (str "close-" index) - :checked close-click-outside? - :on-change change-close-click-outside}]]]] + [:div {:class (stl/css :bottom-left)} + [:> icon-button* {:variant "secondary" + :aria-pressed (= overlay-pos-type :bottom-left) + :data-value "bottom-left" + :icon i/corner-bottom-left + :aria-label (tr "workspace.options.interaction-pos-bottom-left") + :on-click toggle-overlay-pos-type}]] - ;; Overlay background - [:li {:class (stl/css :property-row)} - [:div {:class (stl/css :checkbox-option)} - [:label {:for (str "background-" index) - :class (stl/css-case :global/checked background-overlay?)} - [:span {:class (stl/css-case :global/checked background-overlay?)} - (when background-overlay? - deprecated-icon/status-tick)] - (tr "workspace.options.interaction-background") - [:input {:type "checkbox" - :id (str "background-" index) - :checked background-overlay? - :on-change change-background-overlay}]]]]]]) + [:div {:class (stl/css :bottom-right)} + [:> icon-button* {:variant "secondary" + :aria-pressed (= overlay-pos-type :bottom-right) + :data-value "bottom-right" + :icon i/corner-bottom-right + :aria-label (tr "workspace.options.interaction-pos-bottom-right") + :on-click toggle-overlay-pos-type}]] + + [:div {:class (stl/css :bottom-center)} + [:> icon-button* {:variant "secondary" + :aria-pressed (= overlay-pos-type :bottom-center) + :data-value "bottom-center" + :icon i/corner-bottom + :aria-label (tr "workspace.options.interaction-pos-bottom-center") + :on-click toggle-overlay-pos-type}]]]] + + ;; Overlay click outside + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-checkbox)} + [:> checkbox* {:id (str "close-" index) + :label (tr "workspace.options.interaction-close-outside") + :checked close-click-outside? + :on-change change-close-click-outside}]]] + + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-checkbox)} + [:> checkbox* {:id (str "background-" index) + :label (tr "workspace.options.interaction-background") + :checked background-overlay? + :on-change change-background-overlay}]]]]) (when (ctsi/has-animation? interaction) [:* - ;; Animation select - [:div {:class (stl/css :property-row)} - [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-animation")] - [:div {:class (stl/css :select-wrapper)} + ;; Animation select + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-label)} + [:div {:class (stl/css :interaction-row-name)} + (tr "workspace.options.interaction-animation")]] + [:div {:class (stl/css :interaction-row-select)} [:& select {:class (stl/css :animation-select) :default-value (or (-> interaction :animation :animation-type) "") :options animation-opts @@ -627,9 +583,8 @@ ;; Direction (when (ctsi/has-way? interaction) - [:div {:class (stl/css :property-row)} - [:div {:class (stl/css :inputs-wrapper)} - + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-radio)} [:& radio-buttons {:selected (d/name way) :on-change change-way :name "animation-way"} @@ -640,16 +595,16 @@ ;; Direction (when (ctsi/has-direction? interaction) - [:div {:class (stl/css :property-row)} - [:div {:class (stl/css :buttons-wrapper)} + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-radio)} [:& radio-buttons {:selected (d/name direction) :on-change change-direction :name "animation-direction"} - [:& radio-button {:icon i/column + [:& radio-button {:icon i/row :icon-class (stl/css :right) :value "right" :id "animation-right"}] - [:& radio-button {:icon i/column + [:& radio-button {:icon i/row-reverse :icon-class (stl/css :left) :id "animation-left" :value "left"}] @@ -657,29 +612,31 @@ :icon-class (stl/css :down) :id "animation-down" :value "down"}] - [:& radio-button {:icon i/column + [:& radio-button {:icon i/column-reverse :icon-class (stl/css :up) :id "animation-up" :value "up"}]]]]) ;; Duration (when (ctsi/has-duration? interaction) - [:div {:class (stl/css :property-row)} - [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-duration")] - [:div {:class (stl/css :input-element-wrapper) - :title (tr "workspace.options.interaction-ms")} - [:span {:class (stl/css :after)} - (tr "workspace.options.interaction-ms")] + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-label)} + [:div {:class (stl/css :interaction-row-name)} + (tr "workspace.options.interaction-duration")]] + [:div {:class (stl/css :interaction-row-input)} [:> numeric-input* {:ref ext-duration-ref + :icon i/character-m + :property (tr "workspace.options.interaction-ms") :on-change change-duration - :value (-> interaction :animation :duration) - :title (tr "workspace.options.interaction-ms")}]]]) + :value (-> interaction :animation :duration)}]]]) ;; Easing (when (ctsi/has-easing? interaction) - [:div {:class (stl/css :property-row)} - [:span {:class (stl/css :interaction-name)} (tr "workspace.options.interaction-easing")] - [:div {:class (stl/css :select-wrapper)} + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-label)} + [:div {:class (stl/css :interaction-row-name)} + (tr "workspace.options.interaction-easing")]] + [:div {:class (stl/css :interaction-row-select)} [:& select {:class (stl/css :easing-select) :dropdown-class (stl/css :dropdown-upwards) :default-value (-> interaction :animation :easing) @@ -688,76 +645,159 @@ ;; Offset effect (when (ctsi/has-offset-effect? interaction) - [:div {:class (stl/css :property-row)} - [:div {:class (stl/css :checkbox-option)} - [:label {:for (str "offset-effect-" index) - :class (stl/css-case :global/checked (-> interaction :animation :offset-effect))} - [:span {:class (stl/css-case :global/checked (-> interaction :animation :offset-effect))} - (when (-> interaction :animation :offset-effect) - deprecated-icon/status-tick)] - (tr "workspace.options.interaction-offset-effect") - [:input {:type "checkbox" - :id (str "offset-effect-" index) - :checked (-> interaction :animation :offset-effect) - :on-change change-offset-effect}]]]])])])])) + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-checkbox)} + [:> checkbox* {:id (str "offset-effect-" index) + :label (tr "workspace.options.interaction-offset-effect") + :checked (-> interaction :animation :offset-effect) + :on-change change-offset-effect}]]])])])])) -(mf/defc interactions-menu - {::mf/props :obj} - [{:keys [shape]}] - (let [interactions - (get shape :interactions []) +(mf/defc page-flows* + [{:keys [flows]}] + (let [show-content* (mf/use-state true) + show-content? (deref show-content*) - flows (mf/deref refs/workspace-page-flows) + toggle-content + (mf/use-fn + #(swap! show-content* not))] + + [:div {:class (stl/css :section)} + [:div {:class (stl/css :title)} + [:> title-bar* {:collapsable (> (count flows) 0) + :collapsed (not show-content?) + :on-collapsed toggle-content + :title (tr "workspace.options.flows.flow-starts") + :class (stl/css :title-bar)}]] + (when show-content? + [:div {:class (stl/css :content)} + (for [[id flow] flows] + [:> flow-item* {:key id + :flow flow}])])])) + +(mf/defc shape-flows* + [{:keys [flows shape]}] + (let [show-content* (mf/use-state true) + show-content? (deref show-content*) + + flow (ctp/get-frame-flow flows (:id shape)) + + toggle-content + (mf/use-fn + #(swap! show-content* not)) + + add-flow + (mf/use-fn + #(st/emit! (dwi/add-flow-selected-frame)))] + + [:div {:class (stl/css :section)} + [:div {:class (stl/css :title)} + [:> title-bar* {:collapsable (some? flow) + :collapsed (not show-content?) + :on-collapsed toggle-content + :title (tr "workspace.options.flows.flow") + :class (stl/css :title-bar)} + (when (nil? flow) + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.options.flows.add-flow-start") + :on-click add-flow + :icon i/add}])]] + (when (and show-content? (some? flow)) + [:div {:class (stl/css :content)} + [:> flow-item* {:key (:id flow) + :flow flow}]])])) + +(mf/defc interactions* + [{:keys [interactions shape]}] + (let [show-content* (mf/use-state true) + show-content? (deref show-content*) + + toggle-content + (mf/use-fn + #(swap! show-content* not)) add-interaction - (fn [] - (st/emit! (dwi/add-new-interaction shape))) + (mf/use-fn + (mf/deps shape) + #(st/emit! (dwi/add-new-interaction shape))) remove-interaction - (fn [index] - (st/emit! (dwi/remove-interaction shape index))) + (mf/use-fn + (mf/deps shape) + (fn [index] + (st/emit! (dwi/remove-interaction shape index)))) update-interaction - (fn [index update-fn] - (st/emit! (dwi/update-interaction shape index update-fn)))] - [:div {:class (stl/css :interactions-content)} + (mf/use-fn + (mf/deps shape) + (fn [index update-fn] + (st/emit! (dwi/update-interaction shape index update-fn))))] + + [:div {:class (stl/css :section)} + [:div {:class (stl/css :title)} + [:> title-bar* {:collapsable (> (count interactions) 0) + :collapsed (not show-content?) + :on-collapsed toggle-content + :title (tr "workspace.options.interactions") + :class (stl/css :title-bar)} + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.options.interactions.add-interaction") + :on-click add-interaction + :icon i/add}]]] + + (when show-content? + [:div {:class (stl/css :content :content-interactions)} + (for [[index interaction] (d/enumerate interactions)] + [:> interaction-item* {:key (str (:id shape) "-" index) + :index index + :shape shape + :interaction interaction + :update-interaction update-interaction + :remove-interaction remove-interaction}])])])) + +(mf/defc interactions-menu* + [{:keys [shape]}] + (let [interactions (get shape :interactions []) + flows (mf/deref refs/workspace-page-flows) + framed-shape? (and shape (not (cfh/unframed-shape? shape)))] + + [:div {:class (stl/css :wrapper)} (if shape - [:& shape-flows {:flows flows - :shape shape}] - [:& page-flows {:flows flows}]) - [:div {:class (stl/css :interaction-options)} - (when (and shape (not (cfh/unframed-shape? shape))) - [:div {:class (stl/css :element-title)} - [:> title-bar* {:collapsable false - :title (tr "workspace.options.interactions") - :class (stl/css :title-spacing-layout-interactions)} + (when (cfh/frame-shape? shape) + [:> shape-flows* {:flows flows + :shape shape}]) + (when flows + [:> page-flows* {:flows flows}])) - [:> icon-button* {:variant "ghost" - :aria-label (tr "workspace.options.interactions.add-interaction") - :on-click add-interaction - :icon i/add}]]]) + (when framed-shape? + [:> interactions* {:interactions interactions + :shape shape}]) - (when (= (count interactions) 0) - [:div {:class (stl/css :help-content)} - (when (and shape (not (cfh/unframed-shape? shape))) - [:div {:class (stl/css :help-group)} - [:div {:class (stl/css :interactions-help-icon)} deprecated-icon/add] - [:div {:class (stl/css :interactions-help)} - (tr "workspace.options.add-interaction")]]) - [:div {:class (stl/css :help-group)} - [:div {:class (stl/css :interactions-help-icon)} deprecated-icon/interaction] - [:div {:class (stl/css :interactions-help)} - (tr "workspace.options.select-a-shape")]] - [:div {:class (stl/css :help-group)} - [:div {:class (stl/css :interactions-help-icon)} deprecated-icon/play] - [:div {:class (stl/css :interactions-help)} - (tr "workspace.options.use-play-button")]]]) - [:div {:class (stl/css :groups)} - (for [[index interaction] (d/enumerate interactions)] - [:& interaction-entry {:key (dm/str (:id shape) "-" index) - :index index - :shape shape - :interaction interaction - :update-interaction update-interaction - :remove-interaction remove-interaction}])]]])) + (when (= (count interactions) 0) + [:div {:class (stl/css :section)} + [:div {:class (stl/css :content)} + [:div {:class (stl/css :help)} + (when framed-shape? + [:div {:class (stl/css :help-group)} + [:div {:class (stl/css :help-icon)} + [:> icon* {:icon-id i/add + :size "l" + :class (stl/css :help-icon-inner)}]] + [:div {:class (stl/css :help-text)} + (tr "workspace.options.add-interaction")]]) + + [:div {:class (stl/css :help-group)} + [:div {:class (stl/css :help-icon)} + [:> icon* {:icon-id i/interaction + :size "l" + :class (stl/css :help-icon-inner)}]] + [:div {:class (stl/css :help-text)} + (tr "workspace.options.select-a-shape")]] + + [:div {:class (stl/css :help-group)} + [:div {:class (stl/css :help-icon)} + [:> icon* {:icon-id i/play + :size "l" + :class (stl/css :help-icon-inner)}]] + [:div {:class (stl/css :help-text)} + (tr "workspace.options.use-play-button")]]]]])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss index 313f5a4a2f..62b8930b81 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss @@ -4,22 +4,51 @@ // // Copyright (c) KALEIDOS INC -@use "refactor/common-refactor.scss" as deprecated; +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/mixins.scss" as *; +@use "ds/typography.scss" as t; +@use "../../../sidebar/common/sidebar.scss" as sidebar; -.interactions-content { +.wrapper { display: flex; flex-direction: column; - gap: deprecated.$s-8; - padding-left: var(--sp-m); + gap: var(--sp-s); + padding-inline-start: var(--sp-m); } -.interaction-options { - @include deprecated.flexColumn; +.section { + @include sidebar.option-grid-structure; } -.help-content { - padding: deprecated.$s-32 0; - width: deprecated.$s-200; +.title { + grid-column: span 8; +} + +.title-bar { + padding-inline-start: var(--sp-xxs); +} + +.content { + grid-column: span 8; + + display: flex; + flex-direction: column; + gap: var(--sp-xs); + margin-block-start: var(--sp-xs); +} + +.content-interactions { + gap: var(--sp-l); +} + +.help { + display: flex; + flex-direction: column; + gap: var(--sp-xxxl); + inline-size: $sz-200; + padding: var(--sp-xxxl) 0; margin: 0 auto; } @@ -27,130 +56,168 @@ display: flex; flex-direction: column; align-items: center; - margin-bottom: deprecated.$s-40; - gap: deprecated.$s-12; + gap: var(--sp-m); } -.interactions-help-icon { - @include deprecated.flexCenter; - width: deprecated.$s-48; - height: deprecated.$s-48; - border-radius: deprecated.$br-circle; - background-color: var(--pill-background-color); - svg { - @extend .button-icon; - stroke: var(--icon-foreground); - height: deprecated.$s-32; - width: deprecated.$s-32; +.help-text { + @include t.use-typography("body-small"); + text-align: center; + color: var(--color-foreground-secondary); +} + +.help-icon { + display: flex; + justify-content: center; + align-items: center; + inline-size: $sz-48; + block-size: $sz-48; +} + +.help-icon-inner { + color: var(--color-foreground-secondary); + inline-size: $sz-32; + block-size: $sz-32; +} + +.interaction-item { + display: flex; + flex-direction: column; + gap: var(--sp-xs); +} + +.prototype-pill { + display: flex; + align-items: center; + gap: px2rem(1); + border-radius: $br-8; + padding: var(--sp-s) var(--sp-m); + block-size: $sz-32; + padding: 0; + + &.double { + block-size: $sz-48; + .prototype-pill-button { + block-size: $sz-48; + } + } + + &:has(.prototype-pill-input:focus) { + outline: $b-1 solid var(--color-accent-primary); } } -.after { - @include deprecated.bodySmallTypography; - margin-top: deprecated.$s-1; +.prototype-pill-button { + &.left { + border-end-end-radius: 0; + border-start-end-radius: 0; + } + + &.right { + border-start-start-radius: 0; + border-end-start-radius: 0; + } } -.interactions-help { - @include deprecated.bodySmallTypography; - text-align: center; - color: var(--title-foreground-color); -} - -.element-set { - @include deprecated.flexColumn; -} - -.interactions-info { +.prototype-pill-main { + display: flex; flex-grow: 1; - display: grid; + block-size: 100%; + inline-size: 100%; } -.trigger-name { +.prototype-pill-center { + flex-grow: 1; + display: flex; + align-items: center; + block-size: 100%; + padding: 0 var(--sp-s); + background-color: var(--color-background-tertiary); color: var(--color-foreground-primary); } -.action-summary { - color: var(--color-foreground-secondary); - @include deprecated.textEllipsis; -} - -.groups { - @include deprecated.flexColumn(deprecated.$s-12); -} - -.element-set-options-group-open { - @include deprecated.flexColumn; -} - -.extended-options { - @include deprecated.flexColumn; -} - -.property-list { - list-style: none; - margin: 0; +.prototype-pill-info { display: grid; - row-gap: deprecated.$s-16; - margin-block: calc(#{deprecated.$s-16} - #{deprecated.$s-4}); + inline-size: 100%; } -.property-row { - @extend .attr-row; - height: auto; - &.big-row { - height: 100%; - } - .interaction-name { - @include deprecated.twoLineTextEllipsis; - @include deprecated.bodySmallTypography; - padding-left: deprecated.$s-4; - width: deprecated.$s-92; - margin: auto 0; - grid-area: name; - color: var(--title-foreground-color); - } - .select-wrapper { - display: flex; - align-items: center; - grid-area: content; - .easing-select { - width: deprecated.$s-156; - padding: 0 deprecated.$s-8; - .dropdown-upwards { - bottom: deprecated.$s-36; - width: deprecated.$s-156; - top: unset; - } +.prototype-pill-input { + @include t.use-typography("body-small"); + border: none; + background: none; + outline: none; + block-size: 100%; + inline-size: 100%; + flex-grow: 1; + margin: var(--sp-xxs) 0; + padding: 0 0 0 var(--sp-s); + margin: 0; + background-color: var(--color-background-tertiary); + color: var(--color-foreground-primary); + display: grid; + inline-size: 100%; + + &:hover { + background-color: var(--color-background-quaternary); + &:active { + background-color: var(--color-background-quaternary); } } - .input-element-wrapper { - @extend .input-element; - @include deprecated.bodySmallTypography; - grid-area: content; - } - .buttons-wrapper { - grid-area: content; - .right svg { - transform: rotate(-90deg); - } - .left svg { - transform: rotate(90deg); - } - .up svg { - transform: rotate(180deg); - } - } - .inputs-wrapper { - grid-area: content; - @include deprecated.flexRow; - .radio-btn { - @extend .input-checkbox; - } + + &:focus { + background-color: var(--color-background-tertiary); } } -.position-btns-wrapper { - grid-area: content; +.prototype-pill-name { + @include t.use-typography("body-small"); + @include textEllipsis; + color: var(--color-foreground-primary); +} + +.prototype-pill-description { + @include t.use-typography("body-small"); + @include textEllipsis; + color: var(--color-foreground-secondary); +} + +.interaction-row { + @include sidebar.option-grid-structure; +} + +.interaction-row-label { + grid-column: span 3; + display: flex; + align-items: center; + color: var(--color-foreground-secondary); +} + +.interaction-row-name { + @include twoLineTextEllipsis; + @include t.use-typography("body-small"); + color: var(--color-foreground-secondary); +} + +.interaction-row-select { + grid-column: span 5; +} + +.interaction-row-checkbox { + grid-column: 4 / span 5; + min-block-size: $sz-32; + display: flex; + align-items: center; +} + +.interaction-row-input { + grid-column: span 5; +} + +.interaction-row-radio { + grid-column: 4 / span 5; +} + +.interaction-row-position { + grid-column: 4 / span 5; display: grid; grid-template-areas: "topleft top topright" @@ -158,191 +225,30 @@ "bottomleft bottom bottomright"; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); - width: deprecated.$s-84; - height: deprecated.$s-84; - border-radius: deprecated.$br-8; + inline-size: calc($sz-32 * 3); + block-size: calc($sz-32 * 3); + border-radius: $br-8; background-color: var(--color-background-tertiary); - .center-btn { + + .center { grid-area: center; } - .top-left-btn { + .top-left { grid-area: topleft; } - .top-right-btn { - grid-area: topright; - } - .top-center-btn { + .top-center { grid-area: top; } - .bottom-left-btn { + .top-right { + grid-area: topright; + } + .bottom-left { grid-area: bottomleft; } - .bottom-right-btn { - grid-area: bottomright; - } - .bottom-center-btn { + .bottom-center { grid-area: bottom; } -} - -.direction-btn { - @extend .button-tertiary; - height: deprecated.$s-28; - width: deprecated.$s-28; - - &.active { - @extend .button-icon-selected; - } -} - -.checkbox-option { - @extend .input-checkbox; - grid-area: content; - line-height: 1.2; - label { - align-items: start; - } -} - -.interactions-summary { - @extend .asset-element; - height: deprecated.$s-44; - padding: 0; - gap: deprecated.$s-8; - - .remove-btn { - @extend .button-tertiary; - height: deprecated.$s-32; - width: deprecated.$s-28; - svg { - @extend .button-icon-small; - } - } -} - -.extend-btn { - @extend .button-tertiary; - --button-tertiary-border-width: var(--expand-button-icon-border-width); - height: 100%; - width: deprecated.$s-28; - border-end-end-radius: 0; - border-start-end-radius: 0; - padding: 0; - svg { - @extend .button-icon; - } - position: relative; - &:after { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-inline-end: deprecated.$s-1 solid var(--panel-background-color); - } - &.extended { - @extend .button-icon-selected; - --button-tertiary-border-width: var(--expand-button-icon-border-width-selected); - } -} - -.corner-icon { - fill: none; - stroke: currentColor; - width: deprecated.$s-12; - height: deprecated.$s-12; -} - -.flow-element { - @include deprecated.flexRow; -} - -.flow-info { - display: flex; - align-items: center; - gap: deprecated.$s-2; - border-radius: deprecated.$s-8; - background-color: var(--input-details-color); - height: deprecated.$s-32; - width: 100%; - flex-grow: 1; -} - -.flow-name-wrapper { - @include deprecated.bodySmallTypography; - @include deprecated.focusInput; - display: flex; - align-items: center; - gap: deprecated.$s-4; - flex-grow: 1; - height: deprecated.$s-32; - width: 100%; - border-radius: deprecated.$br-8; - padding: 0; - margin-right: 0; - background-color: var(--input-background-color); - border: deprecated.$s-1 solid var(--input-border-color); - color: var(--input-foreground-color); - .start-flow-btn { - @include deprecated.buttonStyle; - height: deprecated.$s-32; - width: deprecated.$s-28; - padding: 0 deprecated.$s-2 0 deprecated.$s-8; - border-radius: deprecated.$br-8 0 0 deprecated.$br-8; - background-color: transparent; - svg { - @extend .button-icon; - stroke: var(--icon-foreground); - &:hover { - stroke: var(--input-foreground-color-active); - } - } - } - - .flow-input { - @extend .input-base; - @include deprecated.bodySmallTypography; - background-color: transparent; - height: deprecated.$s-28; - } - - .flow-input-wrapper { - @include deprecated.bodySmallTypography; - display: flex; - align-items: center; - height: deprecated.$s-28; - padding: 0; - width: 100%; - margin: 0; - flex-grow: 1; - background-color: transparent; - color: var(--input-foreground-color); - border-radius: deprecated.$br-8; - } - - &:hover { - background-color: var(--input-background-color-hover); - border: deprecated.$s-1 solid var(--input-border-color-hover); - &:active { - background-color: var(--input-background-color-hover); - .flow-input-wrapper { - background-color: var(--input-background-color-hover); - } - } - } - - &:focus, - &:focus-within { - background-color: var(--input-background-color-focus); - border: deprecated.$s-1 solid var(--input-border-color-focus); - &:hover { - border: deprecated.$s-1 solid var(--input-border-color-focus); - } - } - - &.editing { - background-color: var(--input-background-color-active); - border: deprecated.$s-1 solid var(--input-border-color-active); + .bottom-right { + grid-area: bottomright; } } diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index 4f46f4133b..daac5372e8 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -100,8 +100,8 @@ [{:keys [x y stroke action-type arrow-dir zoom] :as props}] (let [icon-pdata (case action-type :navigate (case arrow-dir - :right "M -6.5 0 l 12 0 l -6 -6 m 6 6 l -6 6" - :left "M 6.5 0 l -12 0 l 6 -6 m -6 6 l 6 6" + :right "M -6.5 0 L 5.5 0 M 6.715 0.715 L -0.5 -6.5 M 6.715 -0.715 L -0.365 6.635" + :left "M 6.5 0 l -12 0 m -0.715 0.715 l 6.5 -6.9 m -6 6 l 6 6.35" nil) :open-overlay "M-5 -5 h7 v7 h-7 z M2 -2 h3.5 v7 h-7 v-2.5" diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 6e2107e0aa..c329b483a8 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -17,7 +17,6 @@ [app.common.uuid :as uuid] [app.main.data.common :as dcm] [app.main.data.workspace :as dw] - [app.main.data.workspace.interactions :as dwi] [app.main.refs :as refs] [app.main.store :as st] [app.main.streams :as ms] @@ -277,7 +276,6 @@ pos (gpt/point x (- y (/ 35 zoom))) frame-id (:id frame) - flow-id (:id flow) flow-name (:name flow) on-pointer-down @@ -291,11 +289,6 @@ (dom/stop-propagation event) (st/emit! (dcm/go-to-viewer params)))))) - on-double-click - (mf/use-fn - (mf/deps flow-id) - #(st/emit! (dwi/start-rename-flow flow-id))) - on-pointer-enter (mf/use-fn (mf/deps frame-id on-frame-enter) @@ -319,7 +312,6 @@ [:div {:class (stl/css-case :frame-flow-badge-content true :selected is-selected) :on-pointer-down on-pointer-down - :on-double-click on-double-click :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave} [:> icon* {:icon-id i/play