@lobehub/chat 0.140.1 → 0.141.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.zh-CN.md +4 -1
  3. package/docs/self-hosting/advanced/analytics.mdx +1 -1
  4. package/docs/self-hosting/advanced/analytics.zh-CN.mdx +1 -1
  5. package/docs/self-hosting/environment-variables/analytics.mdx +91 -0
  6. package/docs/self-hosting/environment-variables/analytics.zh-CN.mdx +91 -0
  7. package/docs/self-hosting/environment-variables/basic.mdx +16 -89
  8. package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +17 -88
  9. package/locales/ar/common.json +34 -6
  10. package/locales/ar/setting.json +36 -0
  11. package/locales/de-DE/common.json +34 -6
  12. package/locales/de-DE/setting.json +36 -0
  13. package/locales/en-US/common.json +34 -6
  14. package/locales/en-US/setting.json +36 -0
  15. package/locales/es-ES/common.json +34 -6
  16. package/locales/es-ES/setting.json +36 -0
  17. package/locales/fr-FR/common.json +34 -6
  18. package/locales/fr-FR/setting.json +36 -0
  19. package/locales/it-IT/common.json +34 -6
  20. package/locales/it-IT/setting.json +38 -0
  21. package/locales/ja-JP/common.json +34 -6
  22. package/locales/ja-JP/setting.json +38 -0
  23. package/locales/ko-KR/common.json +34 -6
  24. package/locales/ko-KR/setting.json +36 -0
  25. package/locales/nl-NL/common.json +34 -6
  26. package/locales/nl-NL/setting.json +38 -0
  27. package/locales/pl-PL/common.json +34 -6
  28. package/locales/pl-PL/setting.json +36 -0
  29. package/locales/pt-BR/common.json +34 -6
  30. package/locales/pt-BR/setting.json +36 -0
  31. package/locales/ru-RU/common.json +34 -6
  32. package/locales/ru-RU/setting.json +36 -0
  33. package/locales/tr-TR/common.json +34 -6
  34. package/locales/tr-TR/setting.json +36 -0
  35. package/locales/vi-VN/common.json +34 -6
  36. package/locales/vi-VN/setting.json +36 -0
  37. package/locales/zh-CN/common.json +34 -6
  38. package/locales/zh-CN/setting.json +36 -0
  39. package/locales/zh-TW/common.json +34 -6
  40. package/locales/zh-TW/setting.json +36 -0
  41. package/package.json +12 -5
  42. package/src/app/chat/(desktop)/features/SessionHeader.tsx +5 -1
  43. package/src/app/chat/(mobile)/features/SessionHeader.tsx +9 -4
  44. package/src/app/chat/features/SessionListContent/List/SkeletonList.tsx +0 -1
  45. package/src/app/layout.tsx +2 -0
  46. package/src/app/settings/(desktop)/features/Header.tsx +11 -1
  47. package/src/app/settings/(mobile)/features/Header/index.tsx +12 -1
  48. package/src/app/settings/features/SettingList/index.tsx +2 -1
  49. package/src/app/settings/sync/Alert.tsx +39 -0
  50. package/src/app/settings/sync/DeviceInfo/Card.tsx +41 -0
  51. package/src/app/settings/sync/DeviceInfo/DeviceName.tsx +66 -0
  52. package/src/app/settings/sync/DeviceInfo/index.tsx +117 -0
  53. package/src/app/settings/sync/PageTitle.tsx +11 -0
  54. package/src/app/settings/sync/WebRTC/ChannelNameInput.tsx +46 -0
  55. package/src/app/settings/sync/WebRTC/index.tsx +97 -0
  56. package/src/app/settings/sync/components/SyncSwitch/index.css +237 -0
  57. package/src/app/settings/sync/components/SyncSwitch/index.tsx +79 -0
  58. package/src/app/settings/sync/components/SystemIcon.tsx +16 -0
  59. package/src/app/settings/sync/layout.tsx +9 -0
  60. package/src/app/settings/sync/page.tsx +23 -0
  61. package/src/app/settings/sync/util.ts +4 -0
  62. package/src/components/Analytics/Google.tsx +14 -0
  63. package/src/components/Analytics/Vercel.tsx +2 -4
  64. package/src/components/Analytics/index.tsx +9 -4
  65. package/src/components/BrowserIcon/components/Brave.tsx +56 -0
  66. package/src/components/BrowserIcon/components/Chrome.tsx +14 -0
  67. package/src/components/BrowserIcon/components/Chromium.tsx +14 -0
  68. package/src/components/BrowserIcon/components/Edge.tsx +36 -0
  69. package/src/components/BrowserIcon/components/Firefox.tsx +38 -0
  70. package/src/components/BrowserIcon/components/Opera.tsx +19 -0
  71. package/src/components/BrowserIcon/components/Safari.tsx +23 -0
  72. package/src/components/BrowserIcon/components/Samsung.tsx +21 -0
  73. package/src/components/BrowserIcon/index.tsx +50 -0
  74. package/src/components/BrowserIcon/types.ts +8 -0
  75. package/src/config/__tests__/client.test.ts +1 -8
  76. package/src/config/client.ts +0 -7
  77. package/src/config/server/analytics.ts +32 -0
  78. package/src/config/server/index.ts +3 -1
  79. package/src/const/settings.ts +6 -0
  80. package/src/database/core/__tests__/model.test.ts +2 -2
  81. package/src/database/core/db.ts +1 -1
  82. package/src/database/core/index.ts +1 -0
  83. package/src/database/core/model.ts +83 -5
  84. package/src/database/core/sync.ts +328 -0
  85. package/src/database/models/__tests__/message.test.ts +0 -1
  86. package/src/database/models/__tests__/plugin.test.ts +5 -2
  87. package/src/database/models/file.ts +1 -1
  88. package/src/database/models/message.ts +49 -30
  89. package/src/database/models/plugin.ts +6 -5
  90. package/src/database/models/session.ts +15 -16
  91. package/src/database/models/sessionGroup.ts +14 -8
  92. package/src/database/models/topic.ts +14 -21
  93. package/src/features/SyncStatusInspector/DisableSync.tsx +79 -0
  94. package/src/features/SyncStatusInspector/EnableSync.tsx +136 -0
  95. package/src/features/SyncStatusInspector/EnableTag.tsx +66 -0
  96. package/src/features/SyncStatusInspector/index.tsx +27 -0
  97. package/src/hooks/useSyncData.ts +48 -0
  98. package/src/layout/GlobalLayout/StoreHydration.tsx +5 -0
  99. package/src/locales/default/common.ts +27 -5
  100. package/src/locales/default/setting.ts +37 -1
  101. package/src/services/chat.ts +6 -2
  102. package/src/services/config.ts +1 -1
  103. package/src/services/global.ts +15 -0
  104. package/src/store/chat/slices/topic/action.test.ts +1 -1
  105. package/src/store/chat/slices/topic/action.ts +21 -10
  106. package/src/store/global/slices/common/action.ts +71 -1
  107. package/src/store/global/slices/common/initialState.ts +9 -0
  108. package/src/store/global/slices/common/selectors.ts +1 -0
  109. package/src/store/global/slices/preference/initialState.ts +2 -1
  110. package/src/store/global/slices/preference/selectors.ts +3 -0
  111. package/src/store/global/slices/settings/selectors/index.ts +1 -0
  112. package/src/store/global/slices/settings/selectors/sync.ts +14 -0
  113. package/src/types/settings/index.ts +3 -0
  114. package/src/types/settings/sync.ts +10 -0
  115. package/src/types/sync.ts +41 -0
  116. package/src/utils/platform.ts +9 -3
  117. package/src/utils/responsive.ts +21 -0
@@ -0,0 +1,41 @@
1
+ import { createStyles } from 'antd-style';
2
+ import { ReactNode, memo } from 'react';
3
+ import { Center, Flexbox } from 'react-layout-kit';
4
+
5
+ const useStyles = createStyles(({ css, token, responsive }) => ({
6
+ container: css`
7
+ background: ${token.colorFillTertiary};
8
+ border-radius: 12px;
9
+
10
+ .${responsive.mobile} {
11
+ width: 100%;
12
+ }
13
+ `,
14
+ icon: css`
15
+ width: 40px;
16
+ height: 40px;
17
+ `,
18
+ title: css`
19
+ font-size: 20px;
20
+ `,
21
+ }));
22
+
23
+ const Card = memo<{ icon: ReactNode; title: string }>(({ title, icon }) => {
24
+ const { styles } = useStyles();
25
+
26
+ return (
27
+ <Flexbox
28
+ align={'center'}
29
+ className={styles.container}
30
+ gap={12}
31
+ horizontal
32
+ paddingBlock={12}
33
+ paddingInline={20}
34
+ >
35
+ <Center className={styles.icon}>{icon}</Center>
36
+ <Flexbox className={styles.title}>{title}</Flexbox>
37
+ </Flexbox>
38
+ );
39
+ });
40
+
41
+ export default Card;
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ import { EditableText } from '@lobehub/ui';
4
+ import { Typography } from 'antd';
5
+ import { memo, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { useGlobalStore } from '@/store/global';
10
+ import { syncSettingsSelectors } from '@/store/global/selectors';
11
+
12
+ const DeviceName = memo(() => {
13
+ const { t } = useTranslation('setting');
14
+
15
+ const [deviceName, setSettings] = useGlobalStore((s) => [
16
+ syncSettingsSelectors.deviceName(s),
17
+ s.setSettings,
18
+ ]);
19
+
20
+ const [editing, setEditing] = useState(false);
21
+
22
+ const updateDeviceName = (deviceName: string) => {
23
+ setSettings({ sync: { deviceName } });
24
+ setEditing(false);
25
+ };
26
+
27
+ return (
28
+ <Flexbox gap={4}>
29
+ <Flexbox
30
+ align={'center'}
31
+ height={40}
32
+ horizontal
33
+ style={{ fontSize: 20, fontWeight: 'bold' }}
34
+ width={'100%'}
35
+ >
36
+ {!deviceName && !editing && (
37
+ <Flexbox
38
+ onClick={() => {
39
+ setEditing(true);
40
+ }}
41
+ style={{ cursor: 'pointer' }}
42
+ >
43
+ <Typography.Text type={'secondary'}>{t('sync.device.deviceName.hint')}</Typography.Text>
44
+ </Flexbox>
45
+ )}
46
+ <EditableText
47
+ editing={editing}
48
+ onBlur={(e) => {
49
+ updateDeviceName(e.target.value);
50
+ }}
51
+ onChange={(e) => {
52
+ updateDeviceName(e);
53
+ }}
54
+ onEditingChange={setEditing}
55
+ placeholder={t('sync.device.deviceName.placeholder')}
56
+ size={'large'}
57
+ style={{ maxWidth: 200 }}
58
+ type={'block'}
59
+ value={deviceName}
60
+ />
61
+ </Flexbox>
62
+ </Flexbox>
63
+ );
64
+ });
65
+
66
+ export default DeviceName;
@@ -0,0 +1,117 @@
1
+ 'use client';
2
+
3
+ import { Typography } from 'antd';
4
+ import { createStyles } from 'antd-style';
5
+ import { rgba } from 'polished';
6
+ import { memo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Flexbox } from 'react-layout-kit';
9
+
10
+ import { BrowserIcon } from '@/components/BrowserIcon';
11
+ import { MAX_WIDTH } from '@/const/layoutTokens';
12
+
13
+ import SystemIcon from '../components/SystemIcon';
14
+ import Card from './Card';
15
+ import DeviceName from './DeviceName';
16
+
17
+ const useStyles = createStyles(({ css, cx, responsive, isDarkMode, token, stylish }) => ({
18
+ cards: css`
19
+ flex-direction: row;
20
+ ${responsive.mobile} {
21
+ flex-direction: column;
22
+ width: 100%;
23
+ }
24
+ `,
25
+ container: css`
26
+ position: relative;
27
+ width: 100%;
28
+ border-radius: ${token.borderRadiusLG}px;
29
+ `,
30
+ content: cx(
31
+ stylish.blurStrong,
32
+ css`
33
+ z-index: 2;
34
+
35
+ flex-direction: row;
36
+ justify-content: space-between;
37
+
38
+ height: 88px;
39
+ padding: 12px;
40
+
41
+ background: ${rgba(token.colorBgContainer, isDarkMode ? 0.7 : 1)};
42
+ border-radius: ${token.borderRadiusLG - 1}px;
43
+
44
+ ${responsive.mobile} {
45
+ flex-direction: column;
46
+ gap: 16px;
47
+ align-items: flex-start;
48
+
49
+ width: 100%;
50
+ padding: 8px;
51
+ }
52
+ `,
53
+ ),
54
+ glow: cx(
55
+ stylish.gradientAnimation,
56
+ css`
57
+ pointer-events: none;
58
+ opacity: 0.5;
59
+ background-image: linear-gradient(
60
+ -45deg,
61
+ ${isDarkMode ? token.geekblue4 : token.geekblue},
62
+ ${isDarkMode ? token.cyan4 : token.cyan}
63
+ );
64
+ animation-duration: 10s;
65
+ `,
66
+ ),
67
+ wrapper: css`
68
+ ${responsive.mobile} {
69
+ padding-block: 8px;
70
+ padding-inline: 4px;
71
+ }
72
+ `,
73
+ }));
74
+
75
+ interface DeviceCardProps {
76
+ browser?: string;
77
+ os?: string;
78
+ }
79
+
80
+ const DeviceCard = memo<DeviceCardProps>(({ browser, os }) => {
81
+ const { styles } = useStyles();
82
+ const { t } = useTranslation('setting');
83
+
84
+ return (
85
+ <Flexbox
86
+ className={styles.wrapper}
87
+ style={{ maxWidth: MAX_WIDTH, position: 'relative' }}
88
+ width={'100%'}
89
+ >
90
+ <Flexbox className={styles.container} padding={4}>
91
+ <Flexbox horizontal paddingBlock={8} paddingInline={12}>
92
+ <div>
93
+ <Typography style={{ fontWeight: 'bold' }}>{t('sync.device.title')}</Typography>
94
+ </div>
95
+ </Flexbox>
96
+ <Flexbox align={'center'} className={styles.content} flex={1} padding={12}>
97
+ <DeviceName />
98
+ <Flexbox className={styles.cards} gap={12}>
99
+ <Card icon={<SystemIcon title={os} />} title={os || t('sync.device.unknownOS')} />
100
+ <Card
101
+ icon={browser && <BrowserIcon browser={browser} size={32} />}
102
+ title={browser || t('sync.device.unknownBrowser')}
103
+ />
104
+ </Flexbox>
105
+ </Flexbox>
106
+ <Flexbox
107
+ className={styles.glow}
108
+ height={'100%'}
109
+ style={{ left: 0, position: 'absolute', top: 0 }}
110
+ width={'100%'}
111
+ />
112
+ </Flexbox>
113
+ </Flexbox>
114
+ );
115
+ });
116
+
117
+ export default DeviceCard;
@@ -0,0 +1,11 @@
1
+ 'use client';
2
+
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import PageTitle from '@/components/PageTitle';
7
+
8
+ export default memo(() => {
9
+ const { t } = useTranslation('setting');
10
+ return <PageTitle title={t('tab.sync')} />;
11
+ });
@@ -0,0 +1,46 @@
1
+ import { ActionIcon } from '@lobehub/ui';
2
+ import { Input, InputProps } from 'antd';
3
+ import { FormInstance } from 'antd/es/form/hooks/useForm';
4
+ import { LucideDices } from 'lucide-react';
5
+ import { memo, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+
8
+ import { generateRandomRoomName } from '@/app/settings/sync/util';
9
+
10
+ interface ChannelNameInputProps extends Omit<InputProps, 'form'> {
11
+ form: FormInstance;
12
+ }
13
+
14
+ const ChannelNameInput = memo<ChannelNameInputProps>(({ form, ...res }) => {
15
+ const { t } = useTranslation('setting');
16
+ const [loading, setLoading] = useState(false);
17
+
18
+ return (
19
+ <Input
20
+ placeholder={t('sync.webrtc.channelName.placeholder')}
21
+ suffix={
22
+ <ActionIcon
23
+ active
24
+ icon={LucideDices}
25
+ loading={loading}
26
+ onClick={async () => {
27
+ setLoading(true);
28
+ const name = await generateRandomRoomName();
29
+ setLoading(false);
30
+ form.setFieldValue(['sync', 'webrtc', 'channelName'], name);
31
+ form.setFieldValue(['sync', 'webrtc', 'enabled'], false);
32
+ form.submit();
33
+ }}
34
+ size={'small'}
35
+ style={{
36
+ marginRight: -4,
37
+ }}
38
+ title={t('sync.webrtc.channelName.shuffle')}
39
+ />
40
+ }
41
+ {...res}
42
+ />
43
+ );
44
+ });
45
+
46
+ export default ChannelNameInput;
@@ -0,0 +1,97 @@
1
+ 'use client';
2
+
3
+ import { SiWebrtc } from '@icons-pack/react-simple-icons';
4
+ import { Form, type ItemGroup, Tooltip } from '@lobehub/ui';
5
+ import { Form as AntForm, Input, Switch, Typography } from 'antd';
6
+ import { memo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Flexbox } from 'react-layout-kit';
9
+
10
+ import { FORM_STYLE } from '@/const/layoutTokens';
11
+ import SyncStatusInspector from '@/features/SyncStatusInspector';
12
+ import { useGlobalStore } from '@/store/global';
13
+
14
+ import { useSyncSettings } from '../../hooks/useSyncSettings';
15
+ import ChannelNameInput from './ChannelNameInput';
16
+
17
+ type SettingItemGroup = ItemGroup;
18
+
19
+ const WebRTC = memo(() => {
20
+ const { t } = useTranslation('setting');
21
+ const [form] = AntForm.useForm();
22
+
23
+ const [setSettings] = useGlobalStore((s) => [s.setSettings]);
24
+
25
+ useSyncSettings(form);
26
+
27
+ const channelName = AntForm.useWatch(['sync', 'webrtc', 'channelName'], form);
28
+
29
+ const config: SettingItemGroup = {
30
+ children: [
31
+ {
32
+ children: <ChannelNameInput form={form} />,
33
+ desc: t('sync.webrtc.channelName.desc'),
34
+ label: t('sync.webrtc.channelName.title'),
35
+ name: ['sync', 'webrtc', 'channelName'],
36
+ },
37
+ {
38
+ children: (
39
+ <Input.Password
40
+ autoComplete={'nw-password'}
41
+ placeholder={t('sync.webrtc.channelPassword.placeholder')}
42
+ />
43
+ ),
44
+ desc: t('sync.webrtc.channelPassword.desc'),
45
+ label: t('sync.webrtc.channelPassword.title'),
46
+ name: ['sync', 'webrtc', 'channelPassword'],
47
+ },
48
+ {
49
+ children: !channelName ? (
50
+ <Tooltip title={t('sync.webrtc.enabled.invalid')}>
51
+ <Switch disabled />
52
+ </Tooltip>
53
+ ) : (
54
+ <Switch />
55
+ // <SyncSwitch />
56
+ ),
57
+
58
+ label: t('sync.webrtc.enabled.title'),
59
+ minWidth: undefined,
60
+ name: ['sync', 'webrtc', 'enabled'],
61
+ },
62
+ ],
63
+ extra: (
64
+ <div
65
+ onClick={(e) => {
66
+ e.stopPropagation();
67
+ }}
68
+ >
69
+ <SyncStatusInspector hiddenActions hiddenEnableGuide />
70
+ </div>
71
+ ),
72
+ title: (
73
+ <Flexbox gap={8} horizontal>
74
+ {/* @ts-ignore */}
75
+ <SiWebrtc />
76
+ <Flexbox align={'baseline'} gap={8} horizontal>
77
+ {t('sync.webrtc.title')}
78
+ <Typography.Text style={{ fontWeight: 'normal' }} type={'secondary'}>
79
+ {t('sync.webrtc.desc')}
80
+ </Typography.Text>
81
+ </Flexbox>
82
+ </Flexbox>
83
+ ),
84
+ };
85
+
86
+ return (
87
+ <Form
88
+ form={form}
89
+ items={[config]}
90
+ onFinish={setSettings}
91
+ onValuesChange={setSettings}
92
+ {...FORM_STYLE}
93
+ />
94
+ );
95
+ });
96
+
97
+ export default WebRTC;
@@ -0,0 +1,237 @@
1
+ /* stylelint-disable */
2
+ .wrapper {
3
+ --hue: 223;
4
+ --off-hue: 3;
5
+ --on-hue1: 123;
6
+ --on-hue2: 168;
7
+ --fg: hsl(var(--hue), 10%, 90%);
8
+ --primary: hsl(var(--hue), 90%, 50%);
9
+ --trans-dur: 0.6s;
10
+ --trans-timing: cubic-bezier(0.65, 0, 0.35, 1);
11
+
12
+ font-size: 14px;
13
+ }
14
+
15
+ .switch,
16
+ .switch__input {
17
+ -webkit-tap-highlight-color: #0000;
18
+ }
19
+
20
+ .switch {
21
+ position: relative;
22
+
23
+ display: block;
24
+
25
+ width: 5em;
26
+ height: 3em;
27
+ margin: auto;
28
+ }
29
+
30
+ .switch__base-outer,
31
+ .switch__base-inner {
32
+ position: absolute;
33
+ display: block;
34
+ }
35
+
36
+ .switch__base-outer {
37
+ top: 0.125em;
38
+ left: 0.125em;
39
+
40
+ width: 4.75em;
41
+ height: 2.75em;
42
+
43
+ border-radius: 1.25em;
44
+ box-shadow:
45
+ -0.125em -0.125em 0.25em hsl(var(--hue), 10%, 30%),
46
+ 0.125em 0.125em 0.125em hsl(var(--hue), 10%, 30%) inset,
47
+ 0.125em 0.125em 0.25em hsl(0deg, 0%, 0%),
48
+ -0.125em -0.125em 0.125em hsl(var(--hue), 10%, 5%) inset;
49
+ }
50
+
51
+ .switch__base-inner {
52
+ top: 0.375em;
53
+ left: 0.375em;
54
+
55
+ width: 4.25em;
56
+ height: 2.25em;
57
+
58
+ border-radius: 1.125em;
59
+ box-shadow:
60
+ -0.25em -0.25em 0.25em hsl(var(--hue), 10%, 30%) inset,
61
+ 0.0625em 0.0625em 0.125em hsla(var(--hue), 10%, 30%),
62
+ 0.125em 0.25em 0.25em hsl(var(--hue), 10%, 5%) inset,
63
+ -0.0625em -0.0625em 0.125em hsla(var(--hue), 10%, 5%);
64
+ }
65
+
66
+ .switch__base-neon {
67
+ position: absolute;
68
+ top: 0;
69
+ left: 0;
70
+
71
+ overflow: visible;
72
+ display: block;
73
+
74
+ width: 100%;
75
+ height: auto;
76
+ }
77
+
78
+ .switch__base-neon path {
79
+ stroke-dasharray: 0 104.26 0;
80
+ transition: stroke-dasharray var(--trans-dur) var(--trans-timing);
81
+ }
82
+
83
+ .switch__input {
84
+ position: relative;
85
+
86
+ width: 100%;
87
+ height: 100%;
88
+ appearance: none;
89
+ outline: transparent;
90
+ }
91
+
92
+ .switch__input::before {
93
+ content: '';
94
+
95
+ position: absolute;
96
+ inset: -0.125em;
97
+
98
+ display: block;
99
+
100
+ border-radius: 0.125em;
101
+ box-shadow: 0 0 0 0.125em hsla(var(--hue), 90%, 50%, 0%);
102
+
103
+ transition: box-shadow 0.15s linear;
104
+ }
105
+
106
+ .switch__input:focus-visible::before {
107
+ box-shadow: 0 0 0 0.125em var(--primary);
108
+ }
109
+
110
+ .switch__knob,
111
+ .switch__knob-container {
112
+ position: absolute;
113
+ display: block;
114
+ border-radius: 1em;
115
+ }
116
+
117
+ .switch__knob {
118
+ width: 2em;
119
+ height: 2em;
120
+
121
+ background-color: hsl(var(--hue), 10%, 15%);
122
+ background-image: radial-gradient(
123
+ 88% 88% at 50% 50%,
124
+ hsl(var(--hue), 10%, 20%) 47%,
125
+ hsla(var(--hue), 10%, 20%, 0%) 50%
126
+ ),
127
+ radial-gradient(
128
+ 88% 88% at 47% 47%,
129
+ hsl(var(--hue), 10%, 85%) 45%,
130
+ hsla(var(--hue), 10%, 85%, 0%) 50%
131
+ ),
132
+ radial-gradient(
133
+ 65% 70% at 40% 60%,
134
+ hsl(var(--hue), 10%, 20%) 46%,
135
+ hsla(var(--hue), 10%, 20%, 0%) 50%
136
+ );
137
+ box-shadow:
138
+ -0.0625em -0.0625em 0.0625em hsl(var(--hue), 10%, 15%) inset,
139
+ -0.125em -0.125em 0.0625em hsl(var(--hue), 10%, 5%) inset,
140
+ 0.75em 0.25em 0.125em hsla(0deg, 0%, 0%, 80%);
141
+
142
+ transition: transform var(--trans-dur) var(--trans-timing);
143
+ }
144
+
145
+ .switch__knob-container {
146
+ top: 0.5em;
147
+ left: 0.5em;
148
+
149
+ overflow: hidden;
150
+
151
+ width: 4em;
152
+ height: 2em;
153
+ }
154
+
155
+ .switch__knob-neon {
156
+ display: block;
157
+ width: 2em;
158
+ height: auto;
159
+ }
160
+
161
+ .switch__knob-neon circle {
162
+ opacity: 0;
163
+ stroke-dasharray: 0 90.32 0 54.19;
164
+ transition:
165
+ opacity var(--trans-dur) steps(1, end),
166
+ stroke-dasharray var(--trans-dur) var(--trans-timing);
167
+ }
168
+
169
+ .switch__knob-shadow {
170
+ position: absolute;
171
+ top: 0.5em;
172
+ left: 0.5em;
173
+
174
+ display: block;
175
+
176
+ width: 2em;
177
+ height: 2em;
178
+
179
+ border-radius: 50%;
180
+ box-shadow: 0.125em 0.125em 0.125em hsla(0deg, 0%, 0%, 90%);
181
+
182
+ transition: transform var(--trans-dur) var(--trans-timing);
183
+ }
184
+
185
+ .switch__led {
186
+ position: absolute;
187
+ top: 0;
188
+ left: 0;
189
+
190
+ display: block;
191
+
192
+ width: 0.25em;
193
+ height: 0.25em;
194
+
195
+ background-color: hsl(var(--off-hue), 90%, 70%);
196
+ border-radius: 50%;
197
+ box-shadow:
198
+ 0 -0.0625em 0.0625em hsl(var(--off-hue), 90%, 40%) inset,
199
+ 0 0 0.125em hsla(var(--off-hue), 90%, 70%, 30%),
200
+ 0 0 0.125em hsla(var(--off-hue), 90%, 70%, 30%),
201
+ 0.125em 0.125em 0.125em hsla(0deg, 0%, 0%, 50%);
202
+
203
+ transition:
204
+ background-color var(--trans-dur) var(--trans-timing),
205
+ box-shadow var(--trans-dur) var(--trans-timing);
206
+ }
207
+
208
+ .switch__text {
209
+ position: absolute;
210
+ overflow: hidden;
211
+ width: 1px;
212
+ height: 1px;
213
+ }
214
+
215
+ .switch__input:checked ~ .switch__led {
216
+ background-color: hsl(var(--on-hue1), 90%, 70%);
217
+ box-shadow:
218
+ 0 -0.0625em 0.0625em hsl(var(--on-hue1), 90%, 40%) inset,
219
+ 0 -0.125em 0.125em hsla(var(--on-hue1), 90%, 70%, 30%),
220
+ 0 0.125em 0.125em hsla(var(--on-hue1), 90%, 70%, 30%),
221
+ 0.125em 0.125em 0.125em hsla(0deg, 0%, 0%, 50%);
222
+ }
223
+
224
+ .switch__input:checked ~ .switch__base-neon path {
225
+ stroke-dasharray: 52.13 0 52.13;
226
+ }
227
+
228
+ .switch__input:checked ~ .switch__knob-shadow,
229
+ .switch__input:checked ~ .switch__knob-container .switch__knob {
230
+ transform: translateX(100%);
231
+ }
232
+
233
+ .switch__input:checked ~ .switch__knob-container .switch__knob-neon circle {
234
+ opacity: 1;
235
+ stroke-dasharray: 45.16 0 45.16 54.19;
236
+ transition-timing-function: steps(1, start), var(--trans-timing);
237
+ }
@@ -0,0 +1,79 @@
1
+ import { memo } from 'react';
2
+
3
+ import './index.css';
4
+
5
+ interface SyncSwitchProps {
6
+ onChange?: (checked: boolean) => void;
7
+ value?: boolean;
8
+ }
9
+ const SyncSwitch = memo<SyncSwitchProps>(({ value, onChange }) => {
10
+ return (
11
+ <div className={'wrapper'}>
12
+ <label className="switch">
13
+ <input
14
+ checked={value}
15
+ className="switch__input"
16
+ onChange={(e) => {
17
+ onChange?.(e.target.checked);
18
+ }}
19
+ role="switch"
20
+ type="checkbox"
21
+ />
22
+ <span className="switch__base-outer"></span>
23
+ <span className="switch__base-inner"></span>
24
+ <svg className="switch__base-neon" height="24px" viewBox="0 0 40 24" width="40px">
25
+ <defs>
26
+ <filter id="switch-glow">
27
+ <feGaussianBlur result="coloredBlur" stdDeviation="1"></feGaussianBlur>
28
+ <feMerge>
29
+ <feMergeNode in="coloredBlur"></feMergeNode>
30
+ <feMergeNode in="SourceGraphic"></feMergeNode>
31
+ </feMerge>
32
+ </filter>
33
+ <linearGradient id="switch-gradient1" x1="0" x2="1" y1="0" y2="0">
34
+ <stop offset="0%" stopColor="hsl(var(--on-hue1),90%,70%)" />
35
+ <stop offset="100%" stopColor="hsl(var(--on-hue2),90%,70%)" />
36
+ </linearGradient>
37
+ <linearGradient id="switch-gradient2" x1="0.7" x2="0.3" y1="0" y2="1">
38
+ <stop offset="25%" stopColor="hsla(var(--on-hue1),90%,70%,0)" />
39
+ <stop offset="50%" stopColor="hsla(var(--on-hue1),90%,70%,0.3)" />
40
+ <stop offset="100%" stopColor="hsla(var(--on-hue2),90%,70%,0.3)" />
41
+ </linearGradient>
42
+ </defs>
43
+ <path
44
+ d="m.5,12C.5,5.649,5.649.5,12,.5h16c6.351,0,11.5,5.149,11.5,11.5s-5.149,11.5-11.5,11.5H12C5.649,23.5.5,18.351.5,12Z"
45
+ fill="none"
46
+ filter="url(#switch-glow)"
47
+ stroke="url(#switch-gradient1)"
48
+ strokeDasharray="0 104.26 0"
49
+ strokeDashoffset="0.01"
50
+ strokeLinecap="round"
51
+ strokeWidth="1"
52
+ />
53
+ </svg>
54
+ <span className="switch__knob-shadow"></span>
55
+ <span className="switch__knob-container">
56
+ <span className="switch__knob">
57
+ <svg className="switch__knob-neon" height="48px" viewBox="0 0 48 48" width="48px">
58
+ <circle
59
+ cx="24"
60
+ cy="24"
61
+ fill="none"
62
+ r="23"
63
+ stroke="url(#switch-gradient2)"
64
+ strokeDasharray="0 90.32 0 54.19"
65
+ strokeLinecap="round"
66
+ strokeWidth="1"
67
+ transform="rotate(-112.5,24,24)"
68
+ />
69
+ </svg>
70
+ </span>
71
+ </span>
72
+ <span className="switch__led"></span>
73
+ <span className="switch__text">Power</span>
74
+ </label>
75
+ </div>
76
+ );
77
+ });
78
+
79
+ export default SyncSwitch;