diff --git a/.changeset/bright-impalas-fetch.md b/.changeset/bright-impalas-fetch.md
new file mode 100644
index 00000000..d7af500b
--- /dev/null
+++ b/.changeset/bright-impalas-fetch.md
@@ -0,0 +1,6 @@
+---
+'@banana-ui/react': minor
+'@banana-ui/banana': minor
+---
+
+add switch component
diff --git a/.dumirc.ts b/.dumirc.ts
index b9f41882..cc5020fc 100644
--- a/.dumirc.ts
+++ b/.dumirc.ts
@@ -124,6 +124,10 @@ export default defineConfig({
link: '/example/stepper',
title: 'Stepper 数量选择器',
},
+ {
+ link: '/example/switch',
+ title: 'Switch 开关',
+ },
],
},
{
diff --git a/docs/example/Switch/demos/basicUsage.tsx b/docs/example/Switch/demos/basicUsage.tsx
new file mode 100644
index 00000000..44aa9511
--- /dev/null
+++ b/docs/example/Switch/demos/basicUsage.tsx
@@ -0,0 +1,9 @@
+/**
+ * title: 基本使用
+ */
+
+import { Switch } from '@banana-ui/react';
+
+export default function BasicUsage() {
+ return
+
+
+
+
+
+## 属性 - Attributes & Properties
+
+| 属性 | 说明 | 类型 | 默认值 |
+| -------------- | --------------------------------- | ------------------------ | ----------- |
+| checked | 是否选中 | `boolean` | false |
+| defaultChecked | 默认是否选中 | `boolean` | false |
+| disabled | 是否禁用 | `boolean` | false |
+| readonly | 是否只读 | `boolean` | false |
+| name | 用于表单提交的字段名 | `string` | - |
+| size | 尺寸 | `'small'` \| `'default'` | `'default'` |
+| required | 是否必填 | `boolean` | false |
+| controlled | 是否受控 | `boolean` | false |
+| form | 可以传入一个 id, 用于指定所属表单 | `string` | - |
+
+## 事件 - Events
+
+| 事件 | 说明 | Event Detail |
+| ------ | ------------------ | ---------------------- |
+| change | checked 变化时触发 | `{ checked: boolean }` |
+
+## 插槽 - Slots
+
+| 插槽 | 说明 |
+| --------- | --------------------------------- |
+| checked | 开关为 checked 状态时展示的内容 |
+| unchecked | 开关为 unchecked 状态时展示的内容 |
+
+## CSS Parts
+
+| Part | 说明 |
+| ------- | ------------------------------------------- |
+| base | 包裹组件的容器 |
+| control | 开关的 control 部分,也就是开关移动的小圆点 |
+| inner | 开关有内容的时候,存放内容的容器 |
+
+## 样式变量
+
+| 变量 | 说明 | 默认值 |
+| ------------------------------------------- | ---------------------------------------- | ------------------- |
+| --banana-switch-gap | 开关的 control 与容器的 padding | 2px |
+| --banana-color-text | 开关的文字的颜色 | #fff |
+| --banana-font-size | 开关的字体大小 | 14px |
+| --banana-font-family | 开关的字体 | inherit |
+| --banana-switch-width | 开关容器的最小宽度 | 44px |
+| --banana-switch-height | 开关容器的高度 | 22px |
+| --banana-switch-background-no-checked | 开关不是 checked 状态时的背景颜色 | rgba(0, 0, 0, 0.25) |
+| --banana-switch-background-no-checked-hover | 开关不是 checked 状态时 hover 的背景颜色 | rgba(0, 0, 0, 0.45) |
+| --banana-switch-background-checked | 开关是 checked 状态背景颜色 | #1677ff |
+| --banana-switch-control-size | 开关的 control 的大小 | 18px |
+| --banana-inner-gap | 开关的 control 与容器间的间距 | 2px |
diff --git a/packages/banana-react/src/index.ts b/packages/banana-react/src/index.ts
index b13aeef2..5b6c38ab 100644
--- a/packages/banana-react/src/index.ts
+++ b/packages/banana-react/src/index.ts
@@ -20,6 +20,7 @@ import { Rating } from './rating';
import { Select } from './select';
import { SelectOption } from './select-option';
import { Stepper } from './stepper';
+import { Switch } from './switch';
import { Tooltip } from './tooltip';
export {
@@ -45,5 +46,6 @@ export {
Select,
SelectOption,
Stepper,
+ Switch,
Tooltip,
};
diff --git a/packages/banana-react/src/switch/index.ts b/packages/banana-react/src/switch/index.ts
new file mode 100644
index 00000000..a509f348
--- /dev/null
+++ b/packages/banana-react/src/switch/index.ts
@@ -0,0 +1,18 @@
+import { BSwitch } from '@banana-ui/banana';
+import { EventName, createComponent } from '@lit-labs/react';
+import * as React from 'react';
+
+const events = {
+ onChange: 'change' as EventName<
+ CustomEvent<{
+ value: boolean;
+ }>
+ >,
+};
+
+export const Switch = createComponent({
+ tagName: 'b-switch',
+ react: React,
+ elementClass: BSwitch,
+ events,
+});
diff --git a/packages/banana/src/index.ts b/packages/banana/src/index.ts
index 9e28961b..ad835551 100644
--- a/packages/banana/src/index.ts
+++ b/packages/banana/src/index.ts
@@ -20,6 +20,7 @@ import BRating from './rating';
import BSelect from './select';
import BSelectOption from './select-option';
import BStepper from './stepper';
+import BSwitch from './switch';
import BTooltip from './tooltip';
export {
@@ -45,5 +46,6 @@ export {
BSelect,
BSelectOption,
BStepper,
+ BSwitch,
BTooltip,
};
diff --git a/packages/banana/src/switch/index.styles.ts b/packages/banana/src/switch/index.styles.ts
new file mode 100644
index 00000000..feb524be
--- /dev/null
+++ b/packages/banana/src/switch/index.styles.ts
@@ -0,0 +1,138 @@
+import { css, unsafeCSS } from 'lit';
+import componentStyles from '../../styles/components.styles';
+import { Colors, Variables as Var } from '../../styles/global-variables';
+
+export default [
+ componentStyles,
+ css`
+ :host {
+ color: rgba(${unsafeCSS(Colors.Red5)});
+ line-height: ${unsafeCSS(Var.LineHeightDense)};
+ --banana-switch-gap: 2px;
+ --banana-color-text: #fff;
+ --banana-font-size: 14px;
+ --banana-font-family: inherit;
+ --banana-switch-width: 44px;
+ --banana-switch-height: 22px;
+ --banana-switch-background-no-checked: rgba(0, 0, 0, 0.25);
+ --banana-switch-background-no-checked-hover: rgba(0, 0, 0, 0.45);
+ --banana-switch-background-checked: #1677ff;
+ --banana-switch-control-size: 18px;
+ --banana-inner-gap: 4px;
+ }
+ :host([checked]) > .banana-switch {
+ background-color: var(--banana-switch-background-checked);
+ }
+ :host([disabled]) > .banana-switch {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ :host(:not([disabled], [checked])) > .banana-switch:hover {
+ background-color: var(--banana-switch-background-no-checked-hover);
+ }
+
+ :host([checked]) > .banana-switch:hover {
+ background-color: #4096ff;
+ }
+
+ .banana-switch {
+ position: relative;
+ box-sizing: border-box;
+ margin: 0;
+ padding: var(--banana-switch-gap);
+ color: var(--banana-color-text);
+ font-size: var(--banana-font-size);
+ list-style: none;
+ font-family: var(--banana-font-family);
+ display: inline-flex;
+ align-items: center;
+ min-width: var(--banana-switch-width);
+ height: var(--banana-switch-height);
+ vertical-align: middle;
+ border: 0;
+ border-radius: 100px;
+ cursor: pointer;
+ transition: all ${unsafeCSS(Var.TransitionNormal)};
+ user-select: none;
+ background-color: var(--banana-switch-background-no-checked);
+ }
+
+ .banana-switch-sm {
+ min-width: var(--banana-switch-width-sm, 28px);
+ height: var(--banana-switch-height-sm, 16px);
+ }
+
+ .switch__input {
+ position: absolute;
+ opacity: 0;
+ padding: 0;
+ margin: 0;
+ pointer-events: none;
+ }
+
+ .switch__inner {
+ position: relative;
+ overflow: hidden;
+ height: 100%;
+ width: calc(var(--banana-inner-width) + var(--banana-switch-control-size) + var(--banana-inner-gap) * 2);
+ white-space: nowrap;
+ }
+
+ :host([checked]) .switch__inner {
+ padding-inline-start: var(--banana-inner-gap);
+ padding-inline-end: calc(var(--banana-switch-control-size) + var(--banana-switch-gap));
+ }
+
+ :host(:not([checked])) .switch__inner {
+ padding-inline-start: calc(var(--banana-switch-control-size) + var(--banana-switch-gap));
+ padding-inline-end: var(--banana-inner-gap);
+ }
+
+ .switch__control {
+ position: absolute;
+ display: inline-block;
+ width: var(--banana-switch-control-size);
+ height: var(--banana-switch-control-size);
+ background: #fff;
+ border-radius: calc(var(--banana-switch-control-size) / 2);
+ transition: all ${unsafeCSS(Var.TransitionNormal)};
+ inset-inline-start: var(--banana-switch-gap);
+ }
+
+ .switch__control-sm {
+ width: var(--banana-switch-control-size-sm, 12px);
+ height: var(--banana-switch-control-size-sm, 12px);
+ }
+
+ :host([checked][size='default']) .switch__control {
+ inset-inline-start: calc(100% - calc(var(--banana-switch-control-size) + var(--banana-switch-gap)));
+ }
+
+ :host([checked][size='small']) .switch__control {
+ inset-inline-start: calc(100% - calc(var(--banana-switch-control-size-sm, 12px) + var(--banana-switch-gap)));
+ }
+
+ .switch__inner-wrapper {
+ display: inline-block;
+ width: 100%;
+ text-align: center;
+ transition: all ${unsafeCSS(Var.TransitionNormal)};
+ }
+
+ :host(:not([checked])) .switch__checked-offset {
+ translate: calc(-100% - var(--banana-inner-gap) - var(--banana-switch-control-size));
+ }
+ :host([checked]) .switch__checked-offset {
+ translate: 0;
+ }
+
+ :host(:not([checked])) .switch__unchecked-offset {
+ translate: calc(-100% - var(--banana-inner-gap));
+ }
+
+ :host([checked]) .switch__unchecked-offset {
+ translate: var(--banana-switch-control-size);
+ }
+ `,
+];
diff --git a/packages/banana/src/switch/index.test.ts b/packages/banana/src/switch/index.test.ts
new file mode 100644
index 00000000..bcdf0a7f
--- /dev/null
+++ b/packages/banana/src/switch/index.test.ts
@@ -0,0 +1,231 @@
+import { expect, fixture, html } from '@open-wc/testing';
+import sinon from 'sinon';
+import BSwitch from '.';
+
+describe('b-switch', () => {
+ it('accessibility tests', async () => {
+ const element = await fixture