mirror of
https://github.com/penpot/penpot.git
synced 2025-12-24 06:58:34 -05:00
Compare commits
12 Commits
eva-replac
...
juan-compo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c565915007 | ||
|
|
2de6b6460e | ||
|
|
f905dfc699 | ||
|
|
c79f110177 | ||
|
|
f644b3744a | ||
|
|
0722af3a2f | ||
|
|
b4c6bbb191 | ||
|
|
cad9d03ca1 | ||
|
|
1d6389a3eb | ||
|
|
913a8d3148 | ||
|
|
34e3453f24 | ||
|
|
6f362f9211 |
@@ -13,6 +13,7 @@
|
||||
[app.main.ui.ds.controls.input :refer [input*]]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.ds.controls.select :refer [select*]]
|
||||
[app.main.ui.ds.controls.switcher.switcher :refer [switcher*]]
|
||||
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
|
||||
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
|
||||
[app.main.ui.ds.controls.utilities.label :refer [label*]]
|
||||
@@ -60,6 +61,7 @@
|
||||
:Loader loader*
|
||||
:RawSvg raw-svg*
|
||||
:Select select*
|
||||
:Switcher switcher*
|
||||
:Combobox combobox*
|
||||
:Text text*
|
||||
:TabSwitcher tab-switcher*
|
||||
|
||||
@@ -11,6 +11,7 @@ $br-8: px2rem(8);
|
||||
$br-4: px2rem(4);
|
||||
$br-6: px2rem(6);
|
||||
$br-circle: 50%;
|
||||
$br-full: px2rem(9999);
|
||||
|
||||
$b-1: px2rem(1);
|
||||
$b-2: px2rem(2);
|
||||
|
||||
@@ -79,6 +79,9 @@ $grayish-red: #bfbfbf;
|
||||
--color-accent-select: #{$purple-600-10};
|
||||
--color-accent-action: #{$purple-400};
|
||||
--color-accent-action-hover: #{$purple-500};
|
||||
--color-accent-ui-base: #{$purple-200};
|
||||
--color-accent-ui-hover: #{$purple-600};
|
||||
--color-accent-ui-active: #{$gray-200};
|
||||
|
||||
--color-accent-success: #{$green-500};
|
||||
--color-background-success: #{$green-200};
|
||||
@@ -127,6 +130,9 @@ $grayish-red: #bfbfbf;
|
||||
--color-accent-select: #{$mint-250-10};
|
||||
--color-accent-action: #{$purple-400};
|
||||
--color-accent-action-hover: #{$purple-500};
|
||||
--color-accent-ui-base: #{$green-500};
|
||||
--color-accent-ui-hover: #{$mint-250};
|
||||
--color-accent-ui-active: #{$gray-800};
|
||||
|
||||
--color-accent-success: #{$green-500};
|
||||
--color-background-success: #{$green-950};
|
||||
|
||||
14
frontend/src/app/main/ui/ds/controls/switcher.cljs
Normal file
14
frontend/src/app/main/ui/ds/controls/switcher.cljs
Normal file
@@ -0,0 +1,14 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.ds.controls.switcher
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.ds.controls.switcher.switcher :as impl]))
|
||||
|
||||
(dm/export impl/switcher*)
|
||||
|
||||
|
||||
87
frontend/src/app/main/ui/ds/controls/switcher/switcher.cljs
Normal file
87
frontend/src/app/main/ui/ds/controls/switcher/switcher.cljs
Normal file
@@ -0,0 +1,87 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy 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.switcher.switcher
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema:switcher
|
||||
[:and
|
||||
[:map
|
||||
[:id {:optional true} :string]
|
||||
[:label {:optional true} [:maybe :string]]
|
||||
[:aria-label {:optional true} [:maybe :string]]
|
||||
[:default-checked {:optional true} :boolean]
|
||||
[:on-change {:optional true} [:maybe fn?]]
|
||||
[:disabled {:optional true} :boolean]
|
||||
[:size {:optional true} [:enum "sm" "md" "lg"]]
|
||||
[:class {:optional true} :string]]
|
||||
[:fn {:error/message "Invalid Props"}
|
||||
(fn [props]
|
||||
(or (contains? props :label)
|
||||
(contains? props :aria-label)))]])
|
||||
|
||||
(mf/defc switcher*
|
||||
{::mf/schema schema:switcher}
|
||||
[{:keys [id label default-checked on-change disabled size aria-label class] :rest props}]
|
||||
(let [id (or id (mf/use-id))
|
||||
size (d/nilv size " md ")
|
||||
disabled (d/nilv disabled false)
|
||||
is-checked* (mf/use-state (d/nilv default-checked false))
|
||||
is-checked (deref is-checked*)
|
||||
|
||||
;; Toggle handler
|
||||
handle-toggle
|
||||
(mf/use-fn
|
||||
(mf/deps on-change is-checked* disabled)
|
||||
(fn []
|
||||
(when-not disabled
|
||||
(let [new-checked (not is-checked)]
|
||||
(reset! is-checked* new-checked)
|
||||
(when on-change
|
||||
(on-change new-checked))))))
|
||||
|
||||
;; Keyboard events
|
||||
handle-keydown
|
||||
(mf/use-fn
|
||||
(mf/deps handle-toggle)
|
||||
(fn [event]
|
||||
(when (or (kbd/space? event) (kbd/enter? event))
|
||||
(dom/prevent-default event)
|
||||
(handle-toggle event))))
|
||||
|
||||
has-label (not (str/blank? label))
|
||||
|
||||
props (mf/spread-props props {:id id
|
||||
:role "switch"
|
||||
:aria-label (when-not has-label
|
||||
aria-label)
|
||||
:class [class (stl/css :switcher)]
|
||||
:aria-checked is-checked
|
||||
:disabled disabled
|
||||
:on-click handle-toggle
|
||||
:on-key-down handle-keydown
|
||||
:tab-index (if disabled -1 0)})]
|
||||
|
||||
[:> :div props
|
||||
(when has-label
|
||||
[:label {:for id
|
||||
:class (stl/css-case :switcher-label true
|
||||
:switcher-label-disabled disabled)}
|
||||
label])
|
||||
[:div {:class (stl/css-case :switcher-track true
|
||||
:switcher-checked is-checked
|
||||
:switcher-sm (= size "sm")
|
||||
:switcher-md (= size "md")
|
||||
:switcher-lg (= size "lg")
|
||||
:switcher-track-disabled-checked (and is-checked disabled))}
|
||||
[:div {:class (stl/css-case :switcher-thumb true
|
||||
:switcher-thumb-disabled disabled)}]]]))
|
||||
61
frontend/src/app/main/ui/ds/controls/switcher/switcher.mdx
Normal file
61
frontend/src/app/main/ui/ds/controls/switcher/switcher.mdx
Normal file
@@ -0,0 +1,61 @@
|
||||
{ /* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy 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/blocks';
|
||||
import * as Switcher from "./switcher.stories";
|
||||
|
||||
<Meta title="Controls/Switcher" />
|
||||
|
||||
# Switcher
|
||||
|
||||
The `switcher*` component is a toggle control that allows users to switch between two states (on/off).
|
||||
|
||||
<Canvas of={Switcher.Default} />
|
||||
|
||||
## Anatomy
|
||||
|
||||
The switcher component consists of three main parts:
|
||||
|
||||
- **Label** (optional): Text that describes what the switcher controls
|
||||
- **Track**: The pill-shaped background that indicates the current state
|
||||
- **Thumb**: The circular knob that moves between positions
|
||||
|
||||
|
||||
## Variants
|
||||
|
||||
### Different Sizes
|
||||
|
||||
The switcher supports three size variants: `"sm"`, `"md"` (default), and `"lg"`.
|
||||
|
||||
```clj
|
||||
[:> switcher* {:size "sm" :label "Small"}]
|
||||
[:> switcher* {:size "md" :label "Medium"}]
|
||||
[:> switcher* {:size "lg" :label "Large"}]
|
||||
```
|
||||
|
||||
### Without Visible Label
|
||||
|
||||
When no visible label is provided, use `aria-label` for accessibility.
|
||||
|
||||
```clj
|
||||
[:> switcher* {:aria-label "Toggle dark mode"
|
||||
:default-checked false}]
|
||||
```
|
||||
|
||||
<Canvas of={Switcher.WithoutVisibleLabel} />
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
### When to Use
|
||||
|
||||
- For binary 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
|
||||
159
frontend/src/app/main/ui/ds/controls/switcher/switcher.scss
Normal file
159
frontend/src/app/main/ui/ds/controls/switcher/switcher.scss
Normal file
@@ -0,0 +1,159 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy 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/colors.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/spacing.scss" as *;
|
||||
|
||||
$switcher-sm-track-width: $sz-32;
|
||||
$switcher-sm-track-height: $sz-16;
|
||||
$switcher-sm-thumb-size: $sz-12;
|
||||
|
||||
$switcher-md-track-width: $sz-40;
|
||||
$switcher-md-track-height: $sz-24;
|
||||
$switcher-md-thumb-size: $sz-16;
|
||||
|
||||
$switcher-lg-track-width: $sz-48;
|
||||
$switcher-lg-track-height: $sz-28;
|
||||
$switcher-lg-thumb-size: $sz-24;
|
||||
|
||||
$switcher-transition-duration: 0.2s;
|
||||
|
||||
.switcher {
|
||||
--switcher-track-outline-color: transparent;
|
||||
--switcher-track-outline-offset: 0;
|
||||
--switcher-track-width: #{$switcher-md-track-width};
|
||||
--switcher-track-height: #{$switcher-md-track-height};
|
||||
--switcher-track-bg: var(--color-accent-ui-active);
|
||||
--switcher-track-opacity: 1;
|
||||
|
||||
--switcher-thumb-transform: translateX(0);
|
||||
--switcher-thumb-size: #{$switcher-md-thumb-size};
|
||||
--switcher-thumb-bg: var(--color-foreground-secondary);
|
||||
--switcher-thumb-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
--switcher-thumb-opacity: 1;
|
||||
|
||||
--switcher-label-foreground-color: var(--color-foreground-secondary);
|
||||
--switcher-cursor: pointer;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
padding: 0;
|
||||
inline-size: fit-content;
|
||||
outline: none;
|
||||
cursor: var(--switcher-cursor);
|
||||
|
||||
&:focus-visible {
|
||||
--switcher-track-outline: $b-2 solid var(--color-accent-primary);
|
||||
--switcher-track-outline-offset: #{$b-2};
|
||||
--switcher-track-outline-color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&[disabled] {
|
||||
--switcher-label-foreground-color: var(--color-foreground-secondary);
|
||||
--switcher-cursor: not-allowed;
|
||||
--switcher-track-opacity: 0.6;
|
||||
--switcher-thumb-opacity: 0.5;
|
||||
--switcher-track-background: var(--color-background-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.switcher-label {
|
||||
color: var(--switcher-label-foreground-color);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.switcher-track {
|
||||
position: relative;
|
||||
inline-size: var(--switcher-track-width);
|
||||
block-size: var(--switcher-track-height);
|
||||
border-radius: $br-full;
|
||||
background-color: var(--switcher-track-bg);
|
||||
transition: background-color $switcher-transition-duration ease-in-out;
|
||||
outline: $b-2 solid var(--switcher-track-outline-color);
|
||||
outline-offset: var(--switcher-track-outline-offset);
|
||||
opacity: var(--switcher-track-opacity);
|
||||
&:hover {
|
||||
--switcher-thumb-bg: var(--color-static-white);
|
||||
}
|
||||
}
|
||||
|
||||
.switcher-thumb {
|
||||
position: absolute;
|
||||
inline-size: var(--switcher-thumb-size);
|
||||
block-size: var(--switcher-thumb-size);
|
||||
inset-block-start: calc((var(--switcher-track-height) - var(--switcher-thumb-size)) / 2);
|
||||
inset-inline-start: calc((var(--switcher-track-height) - var(--switcher-thumb-size)) / 2);
|
||||
border-radius: 50%;
|
||||
background-color: var(--switcher-thumb-bg);
|
||||
opacity: var(--switcher-thumb-opacity);
|
||||
box-shadow: var(--switcher-thumb-shadow);
|
||||
transform: var(--switcher-thumb-transform);
|
||||
transition:
|
||||
transform $switcher-transition-duration ease-in-out,
|
||||
background-color $switcher-transition-duration ease-in-out;
|
||||
}
|
||||
|
||||
// Size variants - Small
|
||||
.switcher-sm {
|
||||
--switcher-track-width: #{$switcher-sm-track-width};
|
||||
--switcher-track-height: #{$switcher-sm-track-height};
|
||||
--switcher-thumb-size: #{$switcher-sm-thumb-size};
|
||||
}
|
||||
|
||||
// Size variants - Medium (default)
|
||||
.switcher-md {
|
||||
--switcher-track-width: #{$switcher-md-track-width};
|
||||
--switcher-track-height: #{$switcher-md-track-height};
|
||||
--switcher-thumb-size: #{$switcher-md-thumb-size};
|
||||
}
|
||||
|
||||
// Size variants - Large
|
||||
.switcher-lg {
|
||||
--switcher-track-width: #{$switcher-lg-track-width};
|
||||
--switcher-track-height: #{$switcher-lg-track-height};
|
||||
--switcher-thumb-size: #{$switcher-lg-thumb-size};
|
||||
}
|
||||
|
||||
// Checked state
|
||||
.switcher-checked {
|
||||
--switcher-track-bg: var(--color-accent-ui-base);
|
||||
--switcher-thumb-bg: var(--color-static-white);
|
||||
--switcher-thumb-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
&:hover {
|
||||
--switcher-track-bg: var(--color-accent-ui-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.switcher-track-disabled-checked {
|
||||
--switcher-track-bg: var(--color-background-tertiary);
|
||||
&:hover {
|
||||
--switcher-track-bg: var(--color-background-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.switcher-checked.switcher-sm {
|
||||
--switcher-thumb-transform: translateX(calc(#{$switcher-sm-track-width} - #{$switcher-sm-track-height}));
|
||||
}
|
||||
|
||||
.switcher-checked.switcher-md {
|
||||
--switcher-thumb-transform: translateX(calc(#{$switcher-md-track-width} - #{$switcher-md-track-height}));
|
||||
}
|
||||
|
||||
.switcher-checked.switcher-lg {
|
||||
--switcher-thumb-transform: translateX(calc(#{$switcher-lg-track-width} - #{$switcher-lg-track-height}));
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.switcher-track,
|
||||
.switcher-thumb {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
import * as React from "react";
|
||||
import Components from "@target/components";
|
||||
|
||||
const { Switcher } = Components;
|
||||
|
||||
export default {
|
||||
title: "Controls/Switcher",
|
||||
component: Switcher,
|
||||
argTypes: {
|
||||
defaultChecked: {
|
||||
control: { type: "boolean" },
|
||||
description: "Default checked state for uncontrolled mode",
|
||||
},
|
||||
label: {
|
||||
control: { type: "text" },
|
||||
description: "Label text displayed next to the switcher",
|
||||
},
|
||||
disabled: {
|
||||
control: { type: "boolean" },
|
||||
description: "Whether the switcher is disabled",
|
||||
},
|
||||
size: {
|
||||
options: ["sm", "md", "lg"],
|
||||
control: { type: "select" },
|
||||
description: "Size variant of the switcher",
|
||||
}
|
||||
},
|
||||
args: {
|
||||
disabled: false,
|
||||
size: "md",
|
||||
defaultChecked: false
|
||||
},
|
||||
parameters: {
|
||||
controls: { exclude: ["id", "class", "dataTestid", "on-change"] },
|
||||
},
|
||||
render: ({ onChange, ...args }) => (
|
||||
<Switcher {...args} onChange={onChange} label="Enable notifications"/>
|
||||
),
|
||||
};
|
||||
export const Default = {};
|
||||
|
||||
export const WithLongLabel = {
|
||||
args: {
|
||||
label: "This is a very long label that demonstrates how the switcher component handles text wrapping and layout when the label content is extensive",
|
||||
defaultChecked: true,
|
||||
},
|
||||
render: ({ ...args }) => (
|
||||
<div style={{ maxWidth: "300px" }}>
|
||||
<Switcher {...args}/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithoutVisibleLabel = {
|
||||
args: {
|
||||
defaultChecked: false,
|
||||
},
|
||||
render: ({ ...args }) => (
|
||||
<Switcher {...args} aria-label="Enable notification"/>
|
||||
),
|
||||
};
|
||||
@@ -1186,6 +1186,10 @@ msgstr "Detach token"
|
||||
msgid "ds.inputs.token-field.no-active-token-option"
|
||||
msgstr "This token is not available in any active set or theme."
|
||||
|
||||
#: src/app/main/ui/ds/controls/switcher/switcher.cljs:78
|
||||
msgid "ds.switcher.aria-label"
|
||||
msgstr "Switcher label"
|
||||
|
||||
#: src/app/main/data/auth.cljs:314
|
||||
msgid "errors.auth-provider-not-allowed"
|
||||
msgstr "Auth provider not allowed for this profile"
|
||||
|
||||
@@ -1198,6 +1198,10 @@ msgstr "Desvincular token"
|
||||
msgid "ds.inputs.token-field.no-active-token-option"
|
||||
msgstr "Este token no está disponible en ningún set ni tema activo."
|
||||
|
||||
#: src/app/main/ui/ds/controls/switcher/switcher.cljs:78
|
||||
msgid "ds.switcher.aria-label"
|
||||
msgstr "Etiqueta del switcher"
|
||||
|
||||
#: src/app/main/data/auth.cljs:314
|
||||
msgid "errors.auth-provider-not-allowed"
|
||||
msgstr "El proveedor de autenticación no permitido para este perfil de usuario"
|
||||
|
||||
Reference in New Issue
Block a user