Compare commits
11 Commits
d32d88b56c
...
4378d85fe8
| Author | SHA1 | Date | |
|---|---|---|---|
| 4378d85fe8 | |||
| 4943b7b611 | |||
| 9d4bb5d7d9 | |||
| 30e6b98cff | |||
| d705058e1d | |||
| 6b4df26886 | |||
| 614130e303 | |||
| 03a56ec480 | |||
| b841b2641c | |||
| 00bd397986 | |||
| be210a3e36 |
@ -52,6 +52,7 @@
|
|||||||
"vite": "^7.2.6"
|
"vite": "^7.2.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bits-ui": "^2.14.4"
|
"bits-ui": "^2.14.4",
|
||||||
|
"svelte-sonner": "^1.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@ -11,6 +11,9 @@ importers:
|
|||||||
bits-ui:
|
bits-ui:
|
||||||
specifier: ^2.14.4
|
specifier: ^2.14.4
|
||||||
version: 2.14.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0)
|
version: 2.14.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0)
|
||||||
|
svelte-sonner:
|
||||||
|
specifier: ^1.0.7
|
||||||
|
version: 1.0.7(svelte@5.46.0)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@commitlint/cli':
|
'@commitlint/cli':
|
||||||
specifier: ^20.2.0
|
specifier: ^20.2.0
|
||||||
@ -1743,6 +1746,11 @@ packages:
|
|||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
runed@0.28.0:
|
||||||
|
resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5.7.0
|
||||||
|
|
||||||
runed@0.35.1:
|
runed@0.35.1:
|
||||||
resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==}
|
resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1849,6 +1857,11 @@ packages:
|
|||||||
svelte:
|
svelte:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
svelte-sonner@1.0.7:
|
||||||
|
resolution: {integrity: sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5.0.0
|
||||||
|
|
||||||
svelte-toolbelt@0.10.6:
|
svelte-toolbelt@0.10.6:
|
||||||
resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==}
|
resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==}
|
||||||
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
||||||
@ -3466,6 +3479,11 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc': 4.53.5
|
'@rollup/rollup-win32-x64-msvc': 4.53.5
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
runed@0.28.0(svelte@5.46.0):
|
||||||
|
dependencies:
|
||||||
|
esm-env: 1.2.2
|
||||||
|
svelte: 5.46.0
|
||||||
|
|
||||||
runed@0.35.1(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0):
|
runed@0.35.1(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
@ -3574,6 +3592,11 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
svelte: 5.46.0
|
svelte: 5.46.0
|
||||||
|
|
||||||
|
svelte-sonner@1.0.7(svelte@5.46.0):
|
||||||
|
dependencies:
|
||||||
|
runed: 0.28.0(svelte@5.46.0)
|
||||||
|
svelte: 5.46.0
|
||||||
|
|
||||||
svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0):
|
svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|||||||
44808
src/lib/assets/usb-device.json
Normal file
44808
src/lib/assets/usb-device.json
Normal file
File diff suppressed because it is too large
Load Diff
43
src/lib/components/SettingPanel/DeviceSetting.svelte
Normal file
43
src/lib/components/SettingPanel/DeviceSetting.svelte
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from '$lib/components/ui/tabs';
|
||||||
|
import SerialSetting from './components/SerialSetting.svelte';
|
||||||
|
let activeTab = 'serial';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Tabs bind:value={activeTab} className="p-2">
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="serial">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M7 3h10v2h2v3h-3v6H8V8H5V5h2zm10 6h2v5h-2zm-6 6h2v7h-2zM5 9h2v5H5z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
串口</TabsTrigger
|
||||||
|
>
|
||||||
|
<TabsTrigger value="ble">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M14.88 16.29L13 18.17v-3.76m0-8.58l1.88 1.88L13 9.58m4.71-1.87L12 2h-1v7.58L6.41 5L5 6.41L10.59 12L5 17.58L6.41 19L11 14.41V22h1l5.71-5.71l-4.3-4.29z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
蓝牙</TabsTrigger
|
||||||
|
>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="serial"><SerialSetting /></TabsContent>
|
||||||
|
<TabsContent value="ble">蓝牙设置</TabsContent>
|
||||||
|
</Tabs>
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectTrigger,
|
||||||
|
} from '$lib/components/ui/select';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
items: { label: string; value: string }[];
|
||||||
|
value: string;
|
||||||
|
placeholder: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { id, label, items, value = $bindable(), placeholder }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label
|
||||||
|
for={id}
|
||||||
|
class="text-md leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Select type="single" bind:value>
|
||||||
|
<SelectTrigger
|
||||||
|
{id}
|
||||||
|
class="w-full shadow outline-none focus:border-base-content/20 focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
{items.find((i) => i.value === value)?.label ?? placeholder}
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent {items} />
|
||||||
|
</Select>
|
||||||
133
src/lib/components/SettingPanel/components/SerialSetting.svelte
Normal file
133
src/lib/components/SettingPanel/components/SerialSetting.svelte
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
baudRateValue,
|
||||||
|
baudRateItems,
|
||||||
|
dataBitsValue,
|
||||||
|
parityValue,
|
||||||
|
stopBitsValue,
|
||||||
|
} from '$lib/stores/serial/serial.ui';
|
||||||
|
import { serialOptions } from '$lib/stores/serial/serial.options';
|
||||||
|
import SerialParamSelect from './SerialParamSelect.svelte';
|
||||||
|
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
|
||||||
|
import { getSerialContext } from '$lib/serial/serial.store';
|
||||||
|
|
||||||
|
const serial = getSerialContext();
|
||||||
|
const serialState = serial.state;
|
||||||
|
|
||||||
|
$inspect($serialState);
|
||||||
|
|
||||||
|
const dataBitsItems = [
|
||||||
|
{ value: '7', label: '7' },
|
||||||
|
{ value: '8', label: '8' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const parityItems = [
|
||||||
|
{ value: 'none', label: 'None' },
|
||||||
|
{ value: 'even', label: 'Even' },
|
||||||
|
{ value: 'odd', label: 'Odd' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const stopBitsItems = [
|
||||||
|
{ value: '1', label: '1' },
|
||||||
|
{ value: '2', label: '2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
await serial.requestPort();
|
||||||
|
await serial.openPort($serialOptions);
|
||||||
|
if ($serialState.error) {
|
||||||
|
toast.error(`连接串口失败: ${$serialState.error}`);
|
||||||
|
} else {
|
||||||
|
toast.success('串口连接成功');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col p-4">
|
||||||
|
<div class="flex flex-col gap-y-1.5 pb-4">
|
||||||
|
<h3 class="leading-none font-semibold tracking-tight">
|
||||||
|
{$serialState.portName ?? '串口设置'}
|
||||||
|
</h3>
|
||||||
|
<p class="text-neutral/50">请选择串口连接相关参数</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-3 pb-4">
|
||||||
|
<SerialParamSelect
|
||||||
|
id="baud-rate"
|
||||||
|
label="波特率"
|
||||||
|
items={$baudRateItems}
|
||||||
|
bind:value={$baudRateValue}
|
||||||
|
placeholder="选择波特率"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SerialParamSelect
|
||||||
|
id="data-bits"
|
||||||
|
label="数据位"
|
||||||
|
items={dataBitsItems}
|
||||||
|
bind:value={$dataBitsValue}
|
||||||
|
placeholder="选择数据位"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SerialParamSelect
|
||||||
|
id="parity"
|
||||||
|
label="校验位"
|
||||||
|
items={parityItems}
|
||||||
|
bind:value={$parityValue}
|
||||||
|
placeholder="选择校验位"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SerialParamSelect
|
||||||
|
id="stop-bits"
|
||||||
|
label="停止位"
|
||||||
|
items={stopBitsItems}
|
||||||
|
bind:value={$stopBitsValue}
|
||||||
|
placeholder="选择停止位"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex grid-cols-1 flex-col gap-4">
|
||||||
|
{#if $serialState.status === 'idle'}
|
||||||
|
{#if $serialState.port !== null}
|
||||||
|
<Button class="w-full" color="primary" onclick={connect}
|
||||||
|
>重新选择设备</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="w-full"
|
||||||
|
color="primary"
|
||||||
|
onclick={() => {
|
||||||
|
serial.reopenPort($serialOptions);
|
||||||
|
}}>重新连接</Button
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<Button class="w-full" color="primary" onclick={connect}
|
||||||
|
>选择串口设备</Button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{:else if $serialState.status === 'requesting' || $serialState.status === 'connecting'}
|
||||||
|
<Button class="w-full" disabled
|
||||||
|
><svg class="h-5 w-5 animate-spin" viewBox="0 0 50 50">
|
||||||
|
<circle
|
||||||
|
class="animate-dash stroke-base-content/10 stroke-8"
|
||||||
|
cx="25"
|
||||||
|
cy="25"
|
||||||
|
r="20"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
连接中...</Button
|
||||||
|
>
|
||||||
|
{:else if $serialState.status === 'connected'}
|
||||||
|
<Button class="w-full" color="error" onclick={serial.closePort}
|
||||||
|
>断开连接</Button
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<Button class="w-full" color="error" onclick={connect}
|
||||||
|
>连接失败,点击重试</Button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button as BitsButton, type WithoutChildren } from 'bits-ui';
|
import { Button as BitsButton, type WithoutChildren } from 'bits-ui';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { COLOR_MAP, VARIANT_MAP, SIZE_MAP } from './button.variants';
|
import { COLOR_MAP, VARIANT_MAP, SIZE_MAP } from './Button.variants';
|
||||||
import type { Color, Variant, Size } from './button.variants';
|
import type { Color, Variant, Size } from './Button.variants';
|
||||||
|
|
||||||
type Props = WithoutChildren<BitsButton.RootProps> & {
|
type Props = WithoutChildren<BitsButton.RootProps> & {
|
||||||
color?: Color;
|
color?: Color;
|
||||||
|
|||||||
21
src/lib/components/ui/select/Select.variants.ts
Normal file
21
src/lib/components/ui/select/Select.variants.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export const COLOR_MAP = {
|
||||||
|
neutral: 'select-neutral',
|
||||||
|
primary: 'select-primary',
|
||||||
|
secondary: 'select-secondary',
|
||||||
|
accent: 'select-accent',
|
||||||
|
info: 'select-info',
|
||||||
|
success: 'select-success',
|
||||||
|
warning: 'select-warning',
|
||||||
|
error: 'select-error',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const SIZE_MAP = {
|
||||||
|
xs: 'select-xs',
|
||||||
|
sm: 'select-sm',
|
||||||
|
md: 'select-md',
|
||||||
|
lg: 'select-lg',
|
||||||
|
xl: 'select-xl',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type Color = keyof typeof COLOR_MAP;
|
||||||
|
export type Size = keyof typeof SIZE_MAP;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Select as BitsSelect, type WithoutChildren } from 'bits-ui';
|
import { Select as BitsSelect, type WithoutChildren } from 'bits-ui';
|
||||||
import { elasticOut, cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
|
|
||||||
type Props = WithoutChildren<BitsSelect.ContentProps> & {
|
type Props = WithoutChildren<BitsSelect.ContentProps> & {
|
||||||
items: { value: string; label: string; disabled?: boolean }[];
|
items: { value: string; label: string; disabled?: boolean }[];
|
||||||
@ -21,7 +21,7 @@
|
|||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
delay = 0,
|
delay = 0,
|
||||||
duration = 1000,
|
duration = 200,
|
||||||
easing = cubicOut,
|
easing = cubicOut,
|
||||||
y = -6,
|
y = -6,
|
||||||
start = 0.95,
|
start = 0.95,
|
||||||
@ -59,11 +59,7 @@ transform: ${existingTransform} ${translate} ${scale};
|
|||||||
{#snippet child({ wrapperProps, props, open })}
|
{#snippet child({ wrapperProps, props, open })}
|
||||||
{#if open}
|
{#if open}
|
||||||
<div {...wrapperProps}>
|
<div {...wrapperProps}>
|
||||||
<div
|
<div {...props} transition:selectTransition>
|
||||||
{...props}
|
|
||||||
in:selectTransition={{ easing: elasticOut }}
|
|
||||||
out:selectTransition={{ duration: 200 }}
|
|
||||||
>
|
|
||||||
<BitsSelect.ScrollUpButton>up</BitsSelect.ScrollUpButton>
|
<BitsSelect.ScrollUpButton>up</BitsSelect.ScrollUpButton>
|
||||||
<BitsSelect.Viewport class="p-1">
|
<BitsSelect.Viewport class="p-1">
|
||||||
{#each items as { value, label, disabled } (value)}
|
{#each items as { value, label, disabled } (value)}
|
||||||
|
|||||||
@ -2,16 +2,40 @@
|
|||||||
import { Select as BitsSelect, type WithoutChildren } from 'bits-ui';
|
import { Select as BitsSelect, type WithoutChildren } from 'bits-ui';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
import { COLOR_MAP, SIZE_MAP } from './Select.variants';
|
||||||
|
import type { Color, Size } from './Select.variants';
|
||||||
|
|
||||||
type Props = WithoutChildren<BitsSelect.TriggerProps> & {
|
type Props = WithoutChildren<BitsSelect.TriggerProps> & {
|
||||||
|
color?: Color;
|
||||||
|
size?: Size;
|
||||||
|
ghost?: boolean;
|
||||||
|
|
||||||
class?: string;
|
class?: string;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { children, class: className = '' }: Props = $props();
|
let {
|
||||||
|
color,
|
||||||
|
size,
|
||||||
|
ghost,
|
||||||
|
children,
|
||||||
|
class: className = '',
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
const classes = $derived(
|
||||||
|
[
|
||||||
|
'select',
|
||||||
|
color && COLOR_MAP[color],
|
||||||
|
size && SIZE_MAP[size],
|
||||||
|
ghost && 'select-ghost',
|
||||||
|
className,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BitsSelect.Trigger
|
<BitsSelect.Trigger class={classes.trim()} {...restProps}>
|
||||||
class={`select outline-none focus:border-base-content/20 focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 ${className}`}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</BitsSelect.Trigger>
|
</BitsSelect.Trigger>
|
||||||
|
|||||||
2
src/lib/serial/index.ts
Normal file
2
src/lib/serial/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './serial.service';
|
||||||
|
export * from './serial.types';
|
||||||
105
src/lib/serial/serial.providers.svelte
Normal file
105
src/lib/serial/serial.providers.svelte
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy, type Snippet } from 'svelte';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { serialState, setSerialContext } from './serial.store';
|
||||||
|
import * as service from './serial.service';
|
||||||
|
import { getDeviceName } from '$lib/utils/device';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children?: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { children }: Props = $props();
|
||||||
|
|
||||||
|
async function requestPort() {
|
||||||
|
serialState.update((state) => ({
|
||||||
|
...state,
|
||||||
|
status: 'requesting',
|
||||||
|
error: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const port = await service.requestPort();
|
||||||
|
if (!port) return null;
|
||||||
|
|
||||||
|
const info = port.getInfo?.();
|
||||||
|
const name = info
|
||||||
|
? (getDeviceName(info) ??
|
||||||
|
`USB ${info.usbVendorId ?? ''}:${info.usbProductId ?? ''}`)
|
||||||
|
: 'Serial Device';
|
||||||
|
serialState.update((s) => ({
|
||||||
|
...s,
|
||||||
|
port,
|
||||||
|
portName: name,
|
||||||
|
status: 'idle',
|
||||||
|
}));
|
||||||
|
|
||||||
|
return port;
|
||||||
|
} catch (err) {
|
||||||
|
serialState.update((s) => ({
|
||||||
|
...s,
|
||||||
|
status: 'error',
|
||||||
|
error: String(err),
|
||||||
|
}));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openPort(options: SerialOptions) {
|
||||||
|
let port = get(serialState).port;
|
||||||
|
if (!port) return;
|
||||||
|
|
||||||
|
serialState.update((s) => ({
|
||||||
|
...s,
|
||||||
|
status: 'connecting',
|
||||||
|
error: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await service.openPort(port, options);
|
||||||
|
serialState.update((s) => ({ ...s, status: 'connected' }));
|
||||||
|
} catch (err) {
|
||||||
|
serialState.update((s) => ({
|
||||||
|
...s,
|
||||||
|
status: 'error',
|
||||||
|
error: String(err),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closePort() {
|
||||||
|
let port = get(serialState).port;
|
||||||
|
|
||||||
|
if (!port) return;
|
||||||
|
|
||||||
|
serialState.update((s) => ({ ...s, status: 'disconnecting' }));
|
||||||
|
await service.closePort(port);
|
||||||
|
|
||||||
|
serialState.update((s) => ({
|
||||||
|
...s,
|
||||||
|
status: 'idle',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reopenPort(options: SerialOptions) {
|
||||||
|
await closePort();
|
||||||
|
await openPort(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSerialContext({
|
||||||
|
state: {
|
||||||
|
subscribe: serialState.subscribe,
|
||||||
|
},
|
||||||
|
requestPort,
|
||||||
|
openPort,
|
||||||
|
reopenPort,
|
||||||
|
closePort,
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
let port = get(serialState).port;
|
||||||
|
if (port) service.closePort(port);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@render children?.()}
|
||||||
108
src/lib/serial/serial.service.ts
Normal file
108
src/lib/serial/serial.service.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// src/lib/serial/serial.service.ts
|
||||||
|
// Web Serial API implementation
|
||||||
|
// Requires: @types/w3c-web-serial
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部IO资源
|
||||||
|
*/
|
||||||
|
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
||||||
|
let writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
|
||||||
|
|
||||||
|
let isOpen = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前环境是否支持Web Serial API
|
||||||
|
*/
|
||||||
|
export function isWebSerialSupported(): boolean {
|
||||||
|
return typeof navigator !== 'undefined' && 'serial' in navigator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求用户选择串口设备
|
||||||
|
*/
|
||||||
|
export async function requestPort(
|
||||||
|
filters?: SerialPortRequestOptions['filters']
|
||||||
|
): Promise<SerialPort | null> {
|
||||||
|
if (!isWebSerialSupported()) {
|
||||||
|
throw new Error('Web Serial API is not supported in this environment.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = await navigator.serial.requestPort(
|
||||||
|
filters ? { filters } : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return port ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开串口
|
||||||
|
*/
|
||||||
|
export async function openPort(
|
||||||
|
port: SerialPort,
|
||||||
|
options: SerialOptions
|
||||||
|
): Promise<void> {
|
||||||
|
if (isOpen) {
|
||||||
|
// 串口已经打开,直接返回
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await port.open(options);
|
||||||
|
isOpen = true;
|
||||||
|
|
||||||
|
// 初始化reader和writer
|
||||||
|
reader = port.readable?.getReader() ?? null;
|
||||||
|
writer = port.writable?.getWriter() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭串口
|
||||||
|
*/
|
||||||
|
export async function closePort(port: SerialPort) {
|
||||||
|
if (!isOpen) {
|
||||||
|
// 串口未打开,直接返回
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await reader?.cancel();
|
||||||
|
reader?.releaseLock();
|
||||||
|
await writer?.close();
|
||||||
|
writer?.releaseLock();
|
||||||
|
|
||||||
|
reader = null;
|
||||||
|
writer = null;
|
||||||
|
isOpen = false;
|
||||||
|
|
||||||
|
await port.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向串口写入数据
|
||||||
|
*/
|
||||||
|
export async function write(data: Uint8Array): Promise<void> {
|
||||||
|
if (!writer) {
|
||||||
|
throw new Error('Serial port is not writable.');
|
||||||
|
}
|
||||||
|
await writer.write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始读取串口数据循环
|
||||||
|
*/
|
||||||
|
export async function startReadLoop(
|
||||||
|
onData: (chunk: Uint8Array) => void,
|
||||||
|
onError?: (err: unknown) => void
|
||||||
|
): Promise<void> {
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error('Serial port is not readable.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
if (value) onData(value);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
onError?.(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/lib/serial/serial.store.ts
Normal file
13
src/lib/serial/serial.store.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { createContext } from 'svelte';
|
||||||
|
import type { SerialState, SerialContext } from './serial.types';
|
||||||
|
|
||||||
|
export const serialState = writable<SerialState>({
|
||||||
|
port: null,
|
||||||
|
portName: null,
|
||||||
|
status: 'idle',
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const [getSerialContext, setSerialContext] =
|
||||||
|
createContext<SerialContext>();
|
||||||
24
src/lib/serial/serial.types.ts
Normal file
24
src/lib/serial/serial.types.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { Readable } from 'svelte/store';
|
||||||
|
|
||||||
|
export type SerialStatus =
|
||||||
|
| 'idle'
|
||||||
|
| 'requesting'
|
||||||
|
| 'connecting'
|
||||||
|
| 'connected'
|
||||||
|
| 'disconnecting'
|
||||||
|
| 'error';
|
||||||
|
|
||||||
|
export type SerialState = {
|
||||||
|
port: SerialPort | null;
|
||||||
|
portName: string | null;
|
||||||
|
status: SerialStatus;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SerialContext = {
|
||||||
|
state: Readable<SerialState>;
|
||||||
|
requestPort: () => Promise<SerialPort | null>;
|
||||||
|
openPort: (options: SerialOptions) => Promise<void>;
|
||||||
|
reopenPort: (options: SerialOptions) => Promise<void>;
|
||||||
|
closePort: () => Promise<void>;
|
||||||
|
};
|
||||||
29
src/lib/stores/serial/serial.options.ts
Normal file
29
src/lib/stores/serial/serial.options.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { derived, get } from 'svelte/store';
|
||||||
|
import { baudRate, dataBits, stopBits, parity, flowControl } from './serial';
|
||||||
|
|
||||||
|
export function getSerialOptions(): SerialOptions {
|
||||||
|
return {
|
||||||
|
baudRate: get(baudRate),
|
||||||
|
dataBits: get(dataBits) as 7 | 8,
|
||||||
|
stopBits: get(stopBits) as 1 | 2,
|
||||||
|
parity: get(parity),
|
||||||
|
flowControl: get(flowControl),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const serialOptions = derived(
|
||||||
|
[baudRate, dataBits, stopBits, parity, flowControl],
|
||||||
|
([
|
||||||
|
$baudRate,
|
||||||
|
$dataBits,
|
||||||
|
$stopBits,
|
||||||
|
$parity,
|
||||||
|
$flowControl,
|
||||||
|
]): SerialOptions => ({
|
||||||
|
baudRate: $baudRate,
|
||||||
|
dataBits: $dataBits as 7 | 8,
|
||||||
|
stopBits: $stopBits as 1 | 2,
|
||||||
|
parity: $parity,
|
||||||
|
flowControl: $flowControl,
|
||||||
|
})
|
||||||
|
);
|
||||||
47
src/lib/stores/serial/serial.ts
Normal file
47
src/lib/stores/serial/serial.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { derived, writable, get } from 'svelte/store';
|
||||||
|
import { useLocalStorage } from '../utils/useLocalStorage';
|
||||||
|
|
||||||
|
export type ReadType = 'hex' | 'ascii' | 'dec';
|
||||||
|
|
||||||
|
const defaultBaudRatesList = [9600, 19200, 38400, 57600, 115200];
|
||||||
|
|
||||||
|
/* ---------------- 派生状态 ---------------- */
|
||||||
|
export const baudRate = useLocalStorage<number>('baudRate', 9600);
|
||||||
|
export const baudRateList = useLocalStorage<number[]>(
|
||||||
|
'baudRateList',
|
||||||
|
defaultBaudRatesList
|
||||||
|
);
|
||||||
|
|
||||||
|
export const dataBits = writable<number>(8);
|
||||||
|
export const stopBits = writable<number>(1);
|
||||||
|
export const parity = writable<ParityType>('none');
|
||||||
|
export const flowControl = writable<FlowControlType>('none');
|
||||||
|
|
||||||
|
export const readType = useLocalStorage<ReadType>('readType', 'hex');
|
||||||
|
export const sendType = useLocalStorage<ReadType>('sendType', 'hex');
|
||||||
|
|
||||||
|
export const hasDecTypes = useLocalStorage<boolean>('hasDecTypes', false);
|
||||||
|
|
||||||
|
/* ---------------- 派生状态 ---------------- */
|
||||||
|
export const recordTypes = derived(hasDecTypes, ($hasDecTypes) =>
|
||||||
|
$hasDecTypes
|
||||||
|
? (['hex', 'ascii', 'dec'] satisfies ReadType[])
|
||||||
|
: (['hex', 'ascii'] satisfies ReadType[])
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---------------- Actions ---------------- */
|
||||||
|
export function nextReadType() {
|
||||||
|
const types = get(recordTypes);
|
||||||
|
const current = get(readType);
|
||||||
|
|
||||||
|
const index = types.indexOf(current);
|
||||||
|
readType.set(types[(index + 1) % types.length]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nextSendType() {
|
||||||
|
const types = get(recordTypes);
|
||||||
|
const current = get(sendType);
|
||||||
|
|
||||||
|
const index = types.indexOf(current);
|
||||||
|
sendType.set(types[(index + 1) % types.length]);
|
||||||
|
}
|
||||||
60
src/lib/stores/serial/serial.ui.ts
Normal file
60
src/lib/stores/serial/serial.ui.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { derived } from 'svelte/store';
|
||||||
|
import { baudRate, baudRateList, dataBits, parity, stopBits } from './serial';
|
||||||
|
import { createUIBridge } from '../utils/uiBridge';
|
||||||
|
import { isOneOf } from '../utils/typeGuard';
|
||||||
|
|
||||||
|
export const baudRateItems = derived(baudRateList, ($list) =>
|
||||||
|
$list.map((rate) => ({
|
||||||
|
value: String(rate),
|
||||||
|
label: `${rate}`,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const baudRateBridge = createUIBridge(
|
||||||
|
baudRate,
|
||||||
|
(v) => String(v),
|
||||||
|
(v) => {
|
||||||
|
const n = Number(v);
|
||||||
|
return Number.isFinite(n) ? n : undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const baudRateValue = baudRateBridge.store;
|
||||||
|
|
||||||
|
const dataBitsBridge = createUIBridge(
|
||||||
|
dataBits,
|
||||||
|
(v) => String(v),
|
||||||
|
(v) => {
|
||||||
|
const n = Number(v);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const dataBitsValue = dataBitsBridge.store;
|
||||||
|
|
||||||
|
const parityBridge = createUIBridge(
|
||||||
|
parity,
|
||||||
|
(v) => String(v),
|
||||||
|
(v) => {
|
||||||
|
const PARITY_VALUES = ['none', 'odd', 'even'] as const;
|
||||||
|
if (!isOneOf(v, PARITY_VALUES)) {
|
||||||
|
console.warn(`Invalid parity value: ${v}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const n = v;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const parityValue = parityBridge.store;
|
||||||
|
|
||||||
|
const stopBitsBridge = createUIBridge(
|
||||||
|
stopBits,
|
||||||
|
(v) => String(v),
|
||||||
|
(v) => {
|
||||||
|
const n = Number(v);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const stopBitsValue = stopBitsBridge.store;
|
||||||
6
src/lib/stores/utils/typeGuard.ts
Normal file
6
src/lib/stores/utils/typeGuard.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export function isOneOf<T extends readonly unknown[]>(
|
||||||
|
value: unknown,
|
||||||
|
allowed: T
|
||||||
|
): value is T[number] {
|
||||||
|
return allowed.includes(value as T[number]);
|
||||||
|
}
|
||||||
36
src/lib/stores/utils/uiBridge.ts
Normal file
36
src/lib/stores/utils/uiBridge.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export function createUIBridge<T, U>(
|
||||||
|
source: Writable<T>,
|
||||||
|
toUI: (value: T) => U,
|
||||||
|
fromUI: (value: U) => T | undefined
|
||||||
|
) {
|
||||||
|
const ui = writable<U>(toUI(get(source)));
|
||||||
|
|
||||||
|
let syncing = false;
|
||||||
|
|
||||||
|
const unsubSource = source.subscribe((v) => {
|
||||||
|
if (syncing) return;
|
||||||
|
syncing = true;
|
||||||
|
ui.set(toUI(v));
|
||||||
|
syncing = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsubUI = ui.subscribe((v) => {
|
||||||
|
if (syncing) return;
|
||||||
|
const next = fromUI(v);
|
||||||
|
if (next === undefined) return;
|
||||||
|
|
||||||
|
syncing = true;
|
||||||
|
source.set(next);
|
||||||
|
syncing = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
store: ui,
|
||||||
|
destroy() {
|
||||||
|
unsubSource();
|
||||||
|
unsubUI();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
22
src/lib/stores/utils/useLocalStorage.ts
Normal file
22
src/lib/stores/utils/useLocalStorage.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { writable, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export function useLocalStorage<T>(key: string, initialValue: T): Writable<T> {
|
||||||
|
let startValue = initialValue;
|
||||||
|
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
const stored = localStorage.getItem(key);
|
||||||
|
if (stored !== null) {
|
||||||
|
startValue = JSON.parse(stored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = writable<T>(startValue);
|
||||||
|
|
||||||
|
store.subscribe((value) => {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
32
src/lib/utils/device.ts
Normal file
32
src/lib/utils/device.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import USBJson from '$lib/assets/usb-device.json';
|
||||||
|
|
||||||
|
type UsbIds = {
|
||||||
|
[vendorId: string]: {
|
||||||
|
name: string;
|
||||||
|
devices: {
|
||||||
|
[productId: string]: {
|
||||||
|
devname: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
void (USBJson satisfies UsbIds);
|
||||||
|
|
||||||
|
const USB_IDS: UsbIds = USBJson;
|
||||||
|
|
||||||
|
export function getDeviceName(info: SerialPortInfo): string | undefined {
|
||||||
|
if (!info) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const { usbProductId, usbVendorId } = info;
|
||||||
|
if (!usbVendorId || !usbProductId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const vendor = USB_IDS[usbVendorId.toString(16).padStart(4, '0')];
|
||||||
|
if (!vendor) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const product = vendor.devices[usbProductId.toString(16).padStart(4, '0')];
|
||||||
|
return product ? product.devname : undefined;
|
||||||
|
}
|
||||||
@ -1,9 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import './layout.css';
|
import './layout.css';
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
|
import SerialProvider from '$lib/serial/serial.providers.svelte';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||||
{@render children()}
|
|
||||||
|
<SerialProvider>
|
||||||
|
{@render children()}
|
||||||
|
</SerialProvider>
|
||||||
|
|||||||
@ -1,6 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DeviceSetting from '$lib/components/SettingPanel/DeviceSetting.svelte';
|
||||||
|
import { Toaster } from 'svelte-sonner';
|
||||||
|
</script>
|
||||||
|
|
||||||
<h1>Welcome to SvelteKit</h1>
|
<h1>Welcome to SvelteKit</h1>
|
||||||
<p>
|
<p>
|
||||||
Visit <a class="text-orange-400" href="https://svelte.dev/docs/kit"
|
Visit <a class="text-orange-400" href="https://svelte.dev/docs/kit"
|
||||||
>svelte.dev/docs/kit</a
|
>svelte.dev/docs/kit</a
|
||||||
> to read the documentation
|
> to read the documentation
|
||||||
</p>
|
</p>
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex w-60">
|
||||||
|
<DeviceSetting />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex">123</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toaster />
|
||||||
|
|||||||
@ -1,5 +1,25 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@plugin "@tailwindcss/forms";
|
@plugin "@tailwindcss/forms";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--animate-dash: dash 1.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||||
|
|
||||||
|
@keyframes dash {
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 1, 150;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -40;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -120;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@plugin "daisyui" {
|
@plugin "daisyui" {
|
||||||
logs: false;
|
logs: false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user